diff --git a/.github/workflows/commit-validation.yml b/.github/workflows/commit-validation.yml index a6699de2..2f78b131 100644 --- a/.github/workflows/commit-validation.yml +++ b/.github/workflows/commit-validation.yml @@ -11,6 +11,11 @@ concurrency: permissions: contents: read +env: + # Keep the engine's shared File/FileSystem ABI locked to the companion + # revision that implements GAME_API 45 and generated-animation cache v3. + OPENQ4_GAMELIBS_SHA: 79c9d7d1e26952ffd8e5352f7d51b64ee08b9d82 + jobs: script-smoke: name: Validation Script Smoke @@ -26,7 +31,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -62,7 +71,11 @@ jobs: tools/build/write_pak_manifest.py \ src/sys/linux/pk4/id_utils.py \ tools/analysis/clang_tidy_input_safety.py \ + tools/assets/pack_pbr_orm.py \ + tools/validation/audit_source_provenance.py \ + tools/validation/generate_pbr_fixture.py \ tools/validation/openq4_validate.py \ + tools/validation/stock_asset_baseline.py \ tools/tests/renderer_validation_matrix.py \ tools/tests/arena_campaign.py \ tools/tests/async_drop_client_contract.py \ @@ -85,6 +98,7 @@ jobs: tools/tests/game_type_module_selection.py \ tools/tests/gamelibs_staging.py \ tools/tests/generated_animation_cache.py \ + tools/tests/level_load_cache.py \ tools/tests/hdr_postprocess_math.py \ tools/tests/idstr_input_safety.py \ tools/tests/lexer_peek_contract.py \ @@ -157,16 +171,38 @@ jobs: tools/tests/native_glx_shutdown.py \ tools/tests/network_ipv4_support.py \ tools/tests/network_ipv6_support.py \ + tools/tests/network_security.py \ + tools/tests/openurl_security.py \ tools/tests/openq4_pure_pack.py \ + tools/tests/p0_governance_evidence.py \ tools/tests/packaging_safety.py \ + tools/tests/parallel_job_system.py \ + tools/tests/pbr_orm_packer.py \ + tools/tests/pbr_procedural_fixture.py \ tools/tests/preprocessor_macro_safety.py \ tools/tests/posix_memory_management.py \ tools/tests/posix_monotonic_time.py \ tools/tests/posix_network_resolution.py \ tools/tests/posix_thread_shutdown.py \ tools/tests/release_tooling_safety.py \ + tools/tests/renderer_advanced_lighting.py \ + tools/tests/renderer_budget_contract.py \ tools/tests/renderer_cel_shading.py \ + tools/tests/renderer_gpu_frame_timing.py \ + tools/tests/renderer_gpu_skinning.py \ + tools/tests/renderer_temporal_presentation.py \ tools/tests/renderer_mp_flat_items.py \ + tools/tests/renderer_pbr_materials.py \ + tools/tests/renderer_classic_gui_domain.py \ + tools/tests/renderer_classic_cinematic_post_domain.py \ + tools/tests/renderer_milestone_d_acceptance.py \ + tools/tests/renderer_milestone_d_fixture.py \ + tools/tests/renderer_classic_special_frame_domain.py \ + tools/tests/renderer_classic_world_ambient_domain.py \ + tools/tests/renderer_classic_interaction_domain.py \ + tools/tests/renderer_classic_fog_blend_domain.py \ + tools/tests/renderer_classic_subview_domain.py \ + tools/tests/renderer_classic_deform_domain.py \ tools/tests/renderer_msaa_cvar_safety.py \ tools/tests/renderer_picmip_policy.py \ tools/tests/renderer_player_visibility.py \ @@ -177,6 +213,7 @@ jobs: tools/tests/renderer_vulkan_md5r_compatibility.py \ tools/tests/renderer_vulkan_shadow_compatibility.py \ tools/tests/renderer_vulkan_world_interaction_compatibility.py \ + tools/tests/rigid_body_recovery_diagnostics.py \ tools/tests/savegame_corruption_contract.py \ tools/tests/savegame_pointer_width_safety.py \ tools/tests/savegame_v3_contract.py \ @@ -186,15 +223,19 @@ jobs: tools/tests/steam_deck_support.py \ tools/tests/system_console_presentation.py \ tools/tests/startup_language_override.py \ + tools/tests/stock_asset_baseline.py \ + tools/tests/stock_media_fallbacks.py \ tools/tests/ui_embedded_icons.py \ tools/tests/validation_hardening.py \ tools/tests/vk_shader_header_pin.py \ + tools/tests/vk_temporal_resolve_shader_pin.py \ tools/tests/vscode_fast_build.py bash -n tools/build/meson_setup.sh bash -n tools/macos/collect_macos_support_info.sh bash -n tools/validation/validate_macos_static.sh bash -n tools/validation/validate_push.sh bash -n tools/validation/validate_pr.sh + python tools/validation/audit_source_provenance.py --check python tools/tests/arena_campaign.py python tools/tests/async_drop_client_contract.py python tools/tests/campaign_split_state_transition.py @@ -216,6 +257,7 @@ jobs: python tools/tests/game_type_module_selection.py python tools/tests/gamelibs_staging.py python tools/tests/generated_animation_cache.py + python tools/tests/level_load_cache.py python tools/tests/hdr_postprocess_math.py python tools/tests/idstr_input_safety.py python tools/tests/lexer_peek_contract.py @@ -284,16 +326,36 @@ jobs: python tools/tests/native_glx_shutdown.py python tools/tests/network_ipv4_support.py python tools/tests/network_ipv6_support.py + python tools/tests/network_security.py + python tools/tests/openurl_security.py python tools/tests/openq4_pure_pack.py + python tools/tests/p0_governance_evidence.py python tools/tests/packaging_safety.py + python tools/tests/parallel_job_system.py + python tools/tests/pbr_orm_packer.py + python tools/tests/pbr_procedural_fixture.py python tools/tests/preprocessor_macro_safety.py python tools/tests/release_tooling_safety.py python tools/tests/posix_memory_management.py python tools/tests/posix_monotonic_time.py python tools/tests/posix_network_resolution.py python tools/tests/posix_thread_shutdown.py + python tools/tests/renderer_advanced_lighting.py + python tools/tests/renderer_budget_contract.py python tools/tests/renderer_cel_shading.py + python tools/tests/renderer_gpu_frame_timing.py + python tools/tests/renderer_gpu_skinning.py + python tools/tests/renderer_temporal_presentation.py python tools/tests/renderer_mp_flat_items.py + python tools/tests/renderer_pbr_materials.py + python tools/tests/renderer_classic_gui_domain.py + python tools/tests/renderer_classic_cinematic_post_domain.py + python tools/tests/renderer_classic_special_frame_domain.py + python tools/tests/renderer_classic_world_ambient_domain.py + python tools/tests/renderer_classic_interaction_domain.py + python tools/tests/renderer_classic_fog_blend_domain.py + python tools/tests/renderer_classic_subview_domain.py + python tools/tests/renderer_classic_deform_domain.py python tools/tests/renderer_msaa_cvar_safety.py python tools/tests/renderer_picmip_policy.py python tools/tests/renderer_player_visibility.py @@ -304,6 +366,7 @@ jobs: python tools/tests/renderer_vulkan_md5r_compatibility.py python tools/tests/renderer_vulkan_shadow_compatibility.py python tools/tests/renderer_vulkan_world_interaction_compatibility.py + python tools/tests/rigid_body_recovery_diagnostics.py python tools/tests/savegame_corruption_contract.py python tools/tests/savegame_pointer_width_safety.py python tools/tests/savegame_v3_contract.py @@ -313,9 +376,12 @@ jobs: python tools/tests/steam_deck_support.py python tools/tests/system_console_presentation.py python tools/tests/startup_language_override.py + python tools/tests/stock_asset_baseline.py + python tools/tests/stock_media_fallbacks.py python tools/tests/ui_embedded_icons.py python tools/tests/validation_hardening.py python tools/tests/vk_shader_header_pin.py + python tools/tests/vk_temporal_resolve_shader_pin.py python tools/tests/vscode_fast_build.py - name: Dry-run validation profiles @@ -343,7 +409,13 @@ jobs: shell: pwsh run: | $ErrorActionPreference = "Stop" - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "$env:OPENQ4_GAMELIBS_REPO" + git init "$env:OPENQ4_GAMELIBS_REPO" + git -C "$env:OPENQ4_GAMELIBS_REPO" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "$env:OPENQ4_GAMELIBS_REPO" fetch --depth 1 origin "$env:OPENQ4_GAMELIBS_SHA" + git -C "$env:OPENQ4_GAMELIBS_REPO" checkout --detach "$env:OPENQ4_GAMELIBS_SHA" + if ( ( git -C "$env:OPENQ4_GAMELIBS_REPO" rev-parse HEAD ).Trim() -ne $env:OPENQ4_GAMELIBS_SHA ) { + throw "openQ4-game checkout did not resolve to $env:OPENQ4_GAMELIBS_SHA" + } - name: Setup Python uses: actions/setup-python@v6 @@ -400,7 +472,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -471,7 +547,7 @@ jobs: env LIBGL_ALWAYS_SOFTWARE=1 SDL_VIDEO_DRIVER=x11 SDL_VIDEODRIVER=x11 xvfb-run -a bash tools/validation/validate_pr.sh \ --fail-on-dirty \ --runtime \ - --runtime-cases renderer-default-safety-selftest,renderer-shadow-planner-selftest,renderer-shadow-projected-diagnostic,sdl3-x11-display-diagnostics \ + --runtime-cases renderer-foundation-selftests,renderer-pbr-visible-selftest,renderer-cluster-grid-selftest,renderer-default-safety-selftest,renderer-shadow-planner-selftest,renderer-shadow-projected-diagnostic,sdl3-x11-display-diagnostics \ --runtime-tiers auto \ --runtime-basepath "" \ --runtime-skip-official-pak-validation @@ -529,7 +605,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -779,7 +859,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -1014,7 +1098,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -1143,7 +1231,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/push-verification.yml b/.github/workflows/push-verification.yml index 5d60d2a7..bdc59bcb 100644 --- a/.github/workflows/push-verification.yml +++ b/.github/workflows/push-verification.yml @@ -7,6 +7,11 @@ on: permissions: contents: read +env: + # Keep the engine's shared File/FileSystem ABI locked to the companion + # revision that implements GAME_API 45 and generated-animation cache v3. + OPENQ4_GAMELIBS_SHA: 79c9d7d1e26952ffd8e5352f7d51b64ee08b9d82 + concurrency: group: openq4-push-verification-${{ github.ref }} cancel-in-progress: true @@ -26,7 +31,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 @@ -62,7 +71,11 @@ jobs: tools/build/write_pak_manifest.py \ src/sys/linux/pk4/id_utils.py \ tools/analysis/clang_tidy_input_safety.py \ + tools/assets/pack_pbr_orm.py \ + tools/validation/audit_source_provenance.py \ + tools/validation/generate_pbr_fixture.py \ tools/validation/openq4_validate.py \ + tools/validation/stock_asset_baseline.py \ tools/tests/renderer_validation_matrix.py \ tools/tests/arena_campaign.py \ tools/tests/async_drop_client_contract.py \ @@ -85,6 +98,7 @@ jobs: tools/tests/game_type_module_selection.py \ tools/tests/gamelibs_staging.py \ tools/tests/generated_animation_cache.py \ + tools/tests/level_load_cache.py \ tools/tests/hdr_postprocess_math.py \ tools/tests/idstr_input_safety.py \ tools/tests/lexer_peek_contract.py \ @@ -157,16 +171,38 @@ jobs: tools/tests/native_glx_shutdown.py \ tools/tests/network_ipv4_support.py \ tools/tests/network_ipv6_support.py \ + tools/tests/network_security.py \ + tools/tests/openurl_security.py \ tools/tests/openq4_pure_pack.py \ + tools/tests/p0_governance_evidence.py \ tools/tests/packaging_safety.py \ + tools/tests/parallel_job_system.py \ + tools/tests/pbr_orm_packer.py \ + tools/tests/pbr_procedural_fixture.py \ tools/tests/preprocessor_macro_safety.py \ tools/tests/posix_memory_management.py \ tools/tests/posix_monotonic_time.py \ tools/tests/posix_network_resolution.py \ tools/tests/posix_thread_shutdown.py \ tools/tests/release_tooling_safety.py \ + tools/tests/renderer_advanced_lighting.py \ + tools/tests/renderer_budget_contract.py \ tools/tests/renderer_cel_shading.py \ + tools/tests/renderer_gpu_frame_timing.py \ + tools/tests/renderer_gpu_skinning.py \ + tools/tests/renderer_temporal_presentation.py \ tools/tests/renderer_mp_flat_items.py \ + tools/tests/renderer_pbr_materials.py \ + tools/tests/renderer_classic_gui_domain.py \ + tools/tests/renderer_classic_cinematic_post_domain.py \ + tools/tests/renderer_milestone_d_acceptance.py \ + tools/tests/renderer_milestone_d_fixture.py \ + tools/tests/renderer_classic_special_frame_domain.py \ + tools/tests/renderer_classic_world_ambient_domain.py \ + tools/tests/renderer_classic_interaction_domain.py \ + tools/tests/renderer_classic_fog_blend_domain.py \ + tools/tests/renderer_classic_subview_domain.py \ + tools/tests/renderer_classic_deform_domain.py \ tools/tests/renderer_msaa_cvar_safety.py \ tools/tests/renderer_picmip_policy.py \ tools/tests/renderer_player_visibility.py \ @@ -177,6 +213,7 @@ jobs: tools/tests/renderer_vulkan_md5r_compatibility.py \ tools/tests/renderer_vulkan_shadow_compatibility.py \ tools/tests/renderer_vulkan_world_interaction_compatibility.py \ + tools/tests/rigid_body_recovery_diagnostics.py \ tools/tests/savegame_corruption_contract.py \ tools/tests/savegame_pointer_width_safety.py \ tools/tests/savegame_v3_contract.py \ @@ -186,15 +223,19 @@ jobs: tools/tests/steam_deck_support.py \ tools/tests/system_console_presentation.py \ tools/tests/startup_language_override.py \ + tools/tests/stock_asset_baseline.py \ + tools/tests/stock_media_fallbacks.py \ tools/tests/ui_embedded_icons.py \ tools/tests/validation_hardening.py \ tools/tests/vk_shader_header_pin.py \ + tools/tests/vk_temporal_resolve_shader_pin.py \ tools/tests/vscode_fast_build.py bash -n tools/build/meson_setup.sh bash -n tools/macos/collect_macos_support_info.sh bash -n tools/validation/validate_macos_static.sh bash -n tools/validation/validate_push.sh bash -n tools/validation/validate_pr.sh + python tools/validation/audit_source_provenance.py --check python tools/tests/arena_campaign.py python tools/tests/async_drop_client_contract.py python tools/tests/campaign_split_state_transition.py @@ -216,6 +257,7 @@ jobs: python tools/tests/game_type_module_selection.py python tools/tests/gamelibs_staging.py python tools/tests/generated_animation_cache.py + python tools/tests/level_load_cache.py python tools/tests/hdr_postprocess_math.py python tools/tests/idstr_input_safety.py python tools/tests/lexer_peek_contract.py @@ -284,16 +326,36 @@ jobs: python tools/tests/native_glx_shutdown.py python tools/tests/network_ipv4_support.py python tools/tests/network_ipv6_support.py + python tools/tests/network_security.py + python tools/tests/openurl_security.py python tools/tests/openq4_pure_pack.py + python tools/tests/p0_governance_evidence.py python tools/tests/packaging_safety.py + python tools/tests/parallel_job_system.py + python tools/tests/pbr_orm_packer.py + python tools/tests/pbr_procedural_fixture.py python tools/tests/preprocessor_macro_safety.py python tools/tests/release_tooling_safety.py python tools/tests/posix_memory_management.py python tools/tests/posix_monotonic_time.py python tools/tests/posix_network_resolution.py python tools/tests/posix_thread_shutdown.py + python tools/tests/renderer_advanced_lighting.py + python tools/tests/renderer_budget_contract.py python tools/tests/renderer_cel_shading.py + python tools/tests/renderer_gpu_frame_timing.py + python tools/tests/renderer_gpu_skinning.py + python tools/tests/renderer_temporal_presentation.py python tools/tests/renderer_mp_flat_items.py + python tools/tests/renderer_pbr_materials.py + python tools/tests/renderer_classic_gui_domain.py + python tools/tests/renderer_classic_cinematic_post_domain.py + python tools/tests/renderer_classic_special_frame_domain.py + python tools/tests/renderer_classic_world_ambient_domain.py + python tools/tests/renderer_classic_interaction_domain.py + python tools/tests/renderer_classic_fog_blend_domain.py + python tools/tests/renderer_classic_subview_domain.py + python tools/tests/renderer_classic_deform_domain.py python tools/tests/renderer_msaa_cvar_safety.py python tools/tests/renderer_picmip_policy.py python tools/tests/renderer_player_visibility.py @@ -304,6 +366,7 @@ jobs: python tools/tests/renderer_vulkan_md5r_compatibility.py python tools/tests/renderer_vulkan_shadow_compatibility.py python tools/tests/renderer_vulkan_world_interaction_compatibility.py + python tools/tests/rigid_body_recovery_diagnostics.py python tools/tests/savegame_corruption_contract.py python tools/tests/savegame_pointer_width_safety.py python tools/tests/savegame_v3_contract.py @@ -313,9 +376,12 @@ jobs: python tools/tests/steam_deck_support.py python tools/tests/system_console_presentation.py python tools/tests/startup_language_override.py + python tools/tests/stock_asset_baseline.py + python tools/tests/stock_media_fallbacks.py python tools/tests/ui_embedded_icons.py python tools/tests/validation_hardening.py python tools/tests/vk_shader_header_pin.py + python tools/tests/vk_temporal_resolve_shader_pin.py python tools/tests/vscode_fast_build.py - name: Dry-run push profile @@ -379,7 +445,11 @@ jobs: shell: bash run: | set -euo pipefail - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "${OPENQ4_GAMELIBS_REPO}" + git init "${OPENQ4_GAMELIBS_REPO}" + git -C "${OPENQ4_GAMELIBS_REPO}" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "${OPENQ4_GAMELIBS_REPO}" fetch --depth 1 origin "${OPENQ4_GAMELIBS_SHA}" + git -C "${OPENQ4_GAMELIBS_REPO}" checkout --detach "${OPENQ4_GAMELIBS_SHA}" + test "$(git -C "${OPENQ4_GAMELIBS_REPO}" rev-parse HEAD)" = "${OPENQ4_GAMELIBS_SHA}" - name: Setup Python uses: actions/setup-python@v6 with: @@ -456,7 +526,7 @@ jobs: ) fi if [[ "${{ matrix.runtime_smoke }}" == "true" ]]; then - runtime_cases="renderer-default-safety-selftest" + runtime_cases="renderer-foundation-selftests,renderer-pbr-visible-selftest,renderer-cluster-grid-selftest,renderer-default-safety-selftest" if [[ "${{ matrix.os }}" == ubuntu-* ]]; then runtime_cases="${runtime_cases},renderer-shadow-planner-selftest,renderer-shadow-projected-diagnostic,sdl3-x11-display-diagnostics" fi @@ -553,7 +623,13 @@ jobs: shell: pwsh run: | $ErrorActionPreference = "Stop" - git clone --depth 1 https://github.com/themuffinator/openQ4-game.git "$env:OPENQ4_GAMELIBS_REPO" + git init "$env:OPENQ4_GAMELIBS_REPO" + git -C "$env:OPENQ4_GAMELIBS_REPO" remote add origin https://github.com/themuffinator/openQ4-game.git + git -C "$env:OPENQ4_GAMELIBS_REPO" fetch --depth 1 origin "$env:OPENQ4_GAMELIBS_SHA" + git -C "$env:OPENQ4_GAMELIBS_REPO" checkout --detach "$env:OPENQ4_GAMELIBS_SHA" + if ( ( git -C "$env:OPENQ4_GAMELIBS_REPO" rev-parse HEAD ).Trim() -ne $env:OPENQ4_GAMELIBS_SHA ) { + throw "openQ4-game checkout did not resolve to $env:OPENQ4_GAMELIBS_SHA" + } - name: Setup Python uses: actions/setup-python@v6 diff --git a/.vscode/launch.json b/.vscode/launch.json index 751a77c1..45f3135d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -58,6 +58,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm1" ], @@ -121,6 +124,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm2" ], @@ -184,6 +190,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm3" ], @@ -247,6 +256,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm4" ], @@ -310,6 +322,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm5" ], @@ -373,6 +388,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm6" ], @@ -436,6 +454,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm7" ], @@ -499,6 +520,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm8" ], @@ -562,6 +586,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm9" ], @@ -625,6 +652,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm10" ], @@ -688,6 +718,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm11" ], @@ -751,6 +784,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp1" ], @@ -814,6 +850,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp2" ], @@ -877,6 +916,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp3" ], @@ -940,6 +982,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp4" ], @@ -1003,6 +1048,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp5" ], @@ -1066,6 +1114,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp6" ], @@ -1129,6 +1180,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp7" ], @@ -1192,6 +1246,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp8" ], @@ -1255,6 +1312,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp9" ], @@ -1318,6 +1378,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp10" ], @@ -1381,6 +1444,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp11" ], @@ -1444,6 +1510,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp12" ], @@ -1507,6 +1576,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp13" ], @@ -1570,6 +1642,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp14" ], @@ -1633,6 +1708,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4cmp15" ], @@ -1696,6 +1774,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xdm10" ], @@ -1759,6 +1840,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xdm11" ], @@ -1822,6 +1906,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xdm13" ], @@ -1885,6 +1972,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xdm14" ], @@ -1948,6 +2038,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xdm15" ], @@ -2011,6 +2104,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dm11v1" ], @@ -2074,6 +2170,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4tourney1" ], @@ -2137,6 +2236,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xtourney1" ], @@ -2200,6 +2302,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xtourney2" ], @@ -2263,6 +2368,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf1" ], @@ -2326,6 +2434,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf2" ], @@ -2389,6 +2500,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf3" ], @@ -2452,6 +2566,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf4" ], @@ -2515,6 +2632,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf5" ], @@ -2578,6 +2698,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf6" ], @@ -2641,6 +2764,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf7" ], @@ -2704,6 +2830,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4ctf8" ], @@ -2767,6 +2896,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4xctf6" ], @@ -2830,6 +2962,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dz1" ], @@ -2893,6 +3028,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dz2" ], @@ -2956,6 +3094,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dz3" ], @@ -3019,6 +3160,9 @@ "+set", "si_pure", "0", + "+set", + "ui_autoJoin", + "1", "+spawnServer", "mp/q4dz4" ], @@ -4966,6 +5110,9 @@ "si_gameType", "DM", "+set", + "ui_autoJoin", + "1", + "+set", "si_pure", "0" ], diff --git a/AGENTS.md b/AGENTS.md index 5d6bedbe..e2bb7e63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,7 @@ This file describes project goals, rules, and upstream credits for anyone workin - Any existing custom `q4base/` content is treated as an expedient bootstrap, not a long-term solution. The goal is to remove this reliance by fixing engine compatibility issues rather than shipping replacement assets. - For investigations, reference the log file written by `logFileName` (VS Code launch uses `logs/openq4.log`), located under `fs_savepath\\` (e.g. `${workspaceFolder}\\.home\\baseoq4\\logs\\openq4.log`). - For runtime validation, use mode-specific launch tasks: use the SP launch task for single-player testing and the MP launch task for multiplayer testing. +- Keep `ui_autoJoin 1` enabled for multiplayer testing so clients enter gameplay automatically. Use an explicit `+set ui_autoJoin 0` only when the test specifically targets the join menu or initial spectator/join flow; do not rely on omission because the setting is archived. - Do not treat main-menu startup as sufficient validation; enter in-game/map gameplay relevant to the change before concluding tests. - For macOS testing/debugging, use the compliant Apple-hardware VM/host workflow in `docs/dev/macos-vm-testing-workflow.md` and `tools/macos/Invoke-openQ4MacOSWorkflow.ps1`. Keep macOS installer/restore-image inventory under `E:\ISO\macos\`. Do not use Windows VMware macOS unlocker/Hackintosh-style setup guides for openQ4 automation. - Use `.tmp/` directory in repository for any temporary files required for tasks. diff --git a/LICENSES/DOOM-3-ADDITIONAL-TERMS.txt b/LICENSES/DOOM-3-ADDITIONAL-TERMS.txt new file mode 100644 index 00000000..e0cb3835 --- /dev/null +++ b/LICENSES/DOOM-3-ADDITIONAL-TERMS.txt @@ -0,0 +1,19 @@ +ADDITIONAL TERMS APPLICABLE TO THE DOOM 3 GPL SOURCE CODE. + + The following additional terms (“Additional Terms”) supplement and modify the GNU General Public License, Version 3 (“GPL”) applicable to the Doom 3 GPL Source Code (“Doom 3 Source Code”). In addition to the terms and conditions of the GPL, the Doom 3 Source Code is subject to the further restrictions below. + +1. Replacement of Section 15. Section 15 of the GPL shall be deleted in its entirety and replaced with the following: + +“15. Disclaimer of Warranty. + +THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY. THE PROGRAM IS BEING DELIVERED OR MADE AVAILABLE “AS IS”, “WITH ALL FAULTS” AND WITHOUT WARRANTY OR REPRESENTATION. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.” + +2. Replacement of Section 16. Section 16 of the GPL shall be deleted in its entirety and replaced with the following: + +“16. LIMITATION OF LIABILITY. + +UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH THE PROGRAM(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT HOLDER OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER OR NOT SUCH DAMAGES COULD HAVE BEEN FORESEEN.” + +3. LEGAL NOTICES; NO TRADEMARK LICENSE; ORIGIN. You must reproduce faithfully all trademark, copyright and other proprietary and legal notices on any copies of the Program or any other required author attributions. This license does not grant you rights to use any copyright holder or any other party’s name, logo, or trademarks. Neither the name of the copyright holder or its affiliates, or any other party who modifies and/or conveys the Program may be used to endorse or promote products derived from this software without specific prior written permission. The origin of the Program must not be misrepresented; you must not claim that you wrote the original Program. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original Program. + +4. INDEMNIFICATION. IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAEGS, DEMANDS, CLAIMS, LOSSES, CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. diff --git a/LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt b/LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt new file mode 100644 index 00000000..4dd74945 --- /dev/null +++ b/LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt @@ -0,0 +1,58 @@ +ADDITIONAL TERMS APPLICABLE TO THE Doom 3 BFG Edition GPL Source Code. + + The following additional terms ("Additional Terms") supplement and modify + the GNU General Public License, Version 3 ("GPL") applicable to the Doom 3 + BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code"). In addition + to the terms and conditions of the GPL, the Doom 3 BFG Edition Source Code is + subject to the further restrictions below. + +1. Replacement of Section 15. Section 15 of the GPL shall be deleted in its +entirety and replaced with the following: + +"15. Disclaimer of Warranty. + +THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED, +INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY. THE PROGRAM IS BEING +DELIVERED OR MADE AVAILABLE "AS IS", "WITH ALL FAULTS" AND WITHOUT WARRANTY OR +REPRESENTATION. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST +OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION." + +2. Replacement of Section 16. Section 16 of the GPL shall be deleted in its +entirety and replaced with the following: + +"16. LIMITATION OF LIABILITY. + +UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY +OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE +LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY +DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL, +INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN +CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH +THE PROGRAM(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT +HOLDER OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER +OR NOT SUCH DAMAGES COULD HAVE BEEN FORESEEN." + +3. LEGAL NOTICES; NO TRADEMARK LICENSE; ORIGIN. You must reproduce faithfully +all trademark, copyright and other proprietary and legal notices on any copies +of the Program or any other required author attributions. This license does +not grant you rights to use any copyright holder or any other party’s name, +logo, or trademarks. Neither the name of the copyright holder or its +affiliates, or any other party who modifies and/or conveys the Program may be +used to endorse or promote products derived from this software without +specific prior written permission. The origin of the Program must not be +misrepresented; you must not claim that you wrote the original Program. + +Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original Program. + +4. INDEMNIFICATION. IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT +OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, +YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS +AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAEGS, DEMANDS, CLAIMS, LOSSES, +CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION +REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, +RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. diff --git a/README.md b/README.md index 9830d45b..def62b4c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ It is designed for players who want the original Quake 4 experience with a clean - **OpenAL audio** restored to the pre-plan compatibility path by default, with newer voice handling revalidated behind an opt-in gate. - **Improved input and quality-of-life features** including controller support, better console UX, and modern settings behavior. - **Single-player and multiplayer in one install** with active compatibility work aimed at the stock game. +- **Smarter repeat level loads** with exact-match learned source preparation and validated local model, world, collision, and animation caches that fall back safely to the installed assets. - **A stock-map Arena Campaign** (experimental) with five escalating bot tiers, varied combat game types, boss matches, and persistent ladder progress beside the original story. - **A unified demo library and player** with pause, speed, stepping, rewind/fast-forward controls, honest legacy-format status, and full-world free-fly/player-follow playback for server-side multi-view recordings. - **Cross-platform support** with Windows packages, directly executable Linux AppImages and archives for x86_64 plus preview aarch64, Steam Deck support on Linux, and experimental Apple Silicon/arm64 macOS OpenGL/Metal bridge packages through the signed/notarized DMG lane for credentialed release runs. @@ -135,9 +136,10 @@ Packaged support currently focuses on Windows, Linux x64, Steam Deck/SteamOS, pr - [Demo Library and Multi-View Demos](docs/user/multiview-demos.md) - browse formats, use playback controls, and record or replay complete multiplayer matches - [Shadow Mapping](docs/user/shadow-mapping.md) - optional shadow-map settings and troubleshooting - [Light Grids](docs/user/light-grids.md) - advanced lighting guide for players and testers +- [Advanced Screen-Space Lighting](docs/user/advanced-screen-space-lighting.md) (experimental) - bounded froxel volumetrics, SSR, SSGI, performance controls, and one-setting rollback - [Cel Shading](docs/user/cel-shading.md) - banded lighting and outline settings for the cel-shaded look - [DDS Texture Replacements](docs/user/texture-replacements.md) - install and diagnose DXT/BC7 texture packs -- [Level-Load Cache](docs/user/level-load-cache.md) - generated animation cache behavior, controls, and cleanup +- [Level-Load Cache](docs/user/level-load-cache.md) - learned preload and generated model, world, collision, and animation cache behavior, controls, rollback, and cleanup ### Build and technical docs @@ -154,6 +156,8 @@ Packaged support currently focuses on Windows, Linux x64, Steam Deck/SteamOS, pr - It is **not** a drop-in runtime for the original proprietary Quake 4 DLL mods. - The project is still in **beta development**, so compatibility work is ongoing. +Developers and testers should use the [engine capability matrix](docs/dev/engine-capability-matrix.md) for authoritative implemented/experimental/missing status, the [idTech 5-level modernization roadmap](docs/dev/idtech5-modernization-roadmap.md) for the compatibility-safe implementation order, the [shared interaction-lighting contract](docs/dev/classic-interaction-domain-modernization.md), [shared fog/blend contract](docs/dev/classic-fog-blend-domain-modernization.md), and [shared material-deform contract](docs/dev/classic-deform-domain-modernization.md) for the default-off ownership and rollback boundaries, the [loading/cache contract](docs/dev/loading-cache-modernization.md) for the source-authoritative preload and generated-cache boundary plus pending evidence, and the [stock-asset baseline](docs/dev/stock-asset-baseline.md) for reproducible PK4, SP/MP, save/load, demo, log, and engine-screenshot evidence. + If you run into problems, please use the [issue tracker](https://github.com/themuffinator/openQ4/issues) and include crash logs or setup details when possible. For experimental macOS crashes, use the [macOS support-data guide](docs/user/macos-support-data.md) before filing or updating an issue. --- @@ -170,6 +174,7 @@ Bug reports, compatibility reports, testing feedback, and code contributions are - **DarkMatter Productions** - project stewardship and website - **Justin Marshall** - Quake4Doom and early BSE reverse engineering reference work - **Robert Beckebans** - renderer modernization reference work, including RBDOOM-3-BFG inspiration +- **id Software's official Doom 3 and Doom 3 BFG source releases** - retained idTech 4 source lineage; see the [audited provenance inventory](docs/dev/source-provenance.md) - **id Software** and **Raven Software** - Quake 4 and the underlying technology - **akacross** (Discord user) - Thorough playtesting on Linux and Windows, a huge help moving the project forward! @@ -177,7 +182,7 @@ Bug reports, compatibility reports, testing feedback, and code contributions are ## License and disclaimer -openQ4 engine code is licensed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0). See [LICENSE](LICENSE) for details. +openQ4 engine code is licensed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0). See [LICENSE](LICENSE) for details. Files retaining Doom 3 or Doom 3 BFG Edition headers also retain their upstream notices and are accompanied by the corresponding published Additional Terms; the [source-provenance inventory](docs/dev/source-provenance.md) records their scope, pinned audit references, and intermediate lineage without offering a legal conclusion. The game-library code in [openQ4-game](https://github.com/themuffinator/openQ4-game) is derived from the Quake 4 SDK and remains subject to id Software's SDK EULA. Quake 4 assets remain the property of id Software and ZeniMax Media. diff --git a/content/baseoq4/pak0/default.cfg b/content/baseoq4/pak0/default.cfg index 315cdef2..dbc3ca2a 100644 --- a/content/baseoq4/pak0/default.cfg +++ b/content/baseoq4/pak0/default.cfg @@ -119,12 +119,12 @@ bind b "buymenu" // // FUNCTION KEYS // -bind F1 voteyes // Vote yes -bind F2 voteno // Vote no -bind F3 ready // Ready to play +bind F1 _impulse28 // Vote yes +bind F2 _impulse29 // Vote no +bind F3 _impulse17 // Toggle ready to play bind F5 "savegame quick" // Quick save -bind F6 toggleteam // Toggle team -bind F7 spectate // Spectate +bind F6 _impulse20 // Toggle team +bind F7 _impulse22 // Spectate bind F9 "loadgame quick" // Quick load bind F12 screenshot // Take screenshot diff --git a/content/baseoq4/pak0/glprogs/shadow_interaction.fs b/content/baseoq4/pak0/glprogs/shadow_interaction.fs index 0a5ba97e..93de710d 100644 --- a/content/baseoq4/pak0/glprogs/shadow_interaction.fs +++ b/content/baseoq4/pak0/glprogs/shadow_interaction.fs @@ -244,14 +244,14 @@ float StableShadowHash( vec3 value ) { return fract( sin( dot( value, vec3( 12.9898, 78.233, 37.719 ) ) ) * 43758.5453 ); } -vec2 RotateShadowOffset( vec2 offset, vec2 uv, float depth ) { +mat2 ShadowOffsetRotation( vec2 uv, float depth ) { if ( uShadowFilterMode < 0.5 ) { - return offset; + return mat2( 1.0, 0.0, 0.0, 1.0 ); } float angle = StableShadowHash( vec3( floor( uv / max( uShadowTexelSize.x, 1.0e-6 ) ), floor( depth * 1024.0 ) ) ) * 6.2831853; float s = sin( angle ); float c = cos( angle ); - return vec2( c * offset.x - s * offset.y, s * offset.x + c * offset.y ); + return mat2( c, s, -s, c ); } float TranslucentFilterRadius() { @@ -388,42 +388,59 @@ float RawShadowDepth( vec2 uv ) { } #ifndef OPENQ4_SHADOW_COMPARE -float ProjectedPCSSRadius( vec2 uv, float depth, int cascadeIndex, vec2 clampMin, vec2 clampMax ) { +float ProjectedPCSSRadius( vec2 uv, float depth, int cascadeIndex, vec2 clampMin, vec2 clampMax, mat2 rotation ) { + // The CPU already applies the distant-source scale to every radius. Keep + // the contact floor at the configured PCF radius; the larger PCSS search and + // maximum radii are guard/search bounds, not minimum blur. + float baseRadius = max( uShadowFilterRadius, 0.0 ); if ( uShadowFilterMode < 1.5 || uShadowPCSSLightRadius <= 0.0 || uShadowPCSSMaxRadius <= 0.0 ) { - return uShadowFilterRadius; + return baseRadius; } - float bias = ShadowReceiverBias( cascadeIndex, depth ); + float compareDepth = depth - ShadowReceiverBias( cascadeIndex, depth ); vec2 searchTap = uShadowTexelSize * max( uShadowPCSSLightRadius, 0.5 ); float blockerDepth = 0.0; float blockerCount = 0.0; float d0 = RawShadowDepth( uv ); - if ( d0 < depth - bias ) { + if ( d0 < compareDepth ) { blockerDepth += d0; blockerCount += 1.0; } - vec2 o1 = RotateShadowOffset( vec2( -0.5, -0.5 ), uv, depth ); - vec2 o2 = RotateShadowOffset( vec2( 0.5, -0.5 ), uv, depth ); - vec2 o3 = RotateShadowOffset( vec2( -0.5, 0.5 ), uv, depth ); - vec2 o4 = RotateShadowOffset( vec2( 0.5, 0.5 ), uv, depth ); + vec2 o1 = rotation * vec2( -0.326212, -0.405805 ); + vec2 o2 = rotation * vec2( -0.840144, -0.073580 ); + vec2 o3 = rotation * vec2( -0.695914, 0.457137 ); + vec2 o4 = rotation * vec2( -0.203345, 0.620716 ); + vec2 o5 = rotation * vec2( 0.962340, -0.194983 ); + vec2 o6 = rotation * vec2( 0.473434, -0.480026 ); + vec2 o7 = rotation * vec2( 0.519456, 0.767022 ); + vec2 o8 = rotation * vec2( 0.185461, -0.893124 ); float d1 = RawShadowDepth( clamp( uv + o1 * searchTap, clampMin, clampMax ) ); float d2 = RawShadowDepth( clamp( uv + o2 * searchTap, clampMin, clampMax ) ); float d3 = RawShadowDepth( clamp( uv + o3 * searchTap, clampMin, clampMax ) ); float d4 = RawShadowDepth( clamp( uv + o4 * searchTap, clampMin, clampMax ) ); - if ( d1 < depth - bias ) { blockerDepth += d1; blockerCount += 1.0; } - if ( d2 < depth - bias ) { blockerDepth += d2; blockerCount += 1.0; } - if ( d3 < depth - bias ) { blockerDepth += d3; blockerCount += 1.0; } - if ( d4 < depth - bias ) { blockerDepth += d4; blockerCount += 1.0; } + float d5 = RawShadowDepth( clamp( uv + o5 * searchTap, clampMin, clampMax ) ); + float d6 = RawShadowDepth( clamp( uv + o6 * searchTap, clampMin, clampMax ) ); + float d7 = RawShadowDepth( clamp( uv + o7 * searchTap, clampMin, clampMax ) ); + float d8 = RawShadowDepth( clamp( uv + o8 * searchTap, clampMin, clampMax ) ); + if ( d1 < compareDepth ) { blockerDepth += d1; blockerCount += 1.0; } + if ( d2 < compareDepth ) { blockerDepth += d2; blockerCount += 1.0; } + if ( d3 < compareDepth ) { blockerDepth += d3; blockerCount += 1.0; } + if ( d4 < compareDepth ) { blockerDepth += d4; blockerCount += 1.0; } + if ( d5 < compareDepth ) { blockerDepth += d5; blockerCount += 1.0; } + if ( d6 < compareDepth ) { blockerDepth += d6; blockerCount += 1.0; } + if ( d7 < compareDepth ) { blockerDepth += d7; blockerCount += 1.0; } + if ( d8 < compareDepth ) { blockerDepth += d8; blockerCount += 1.0; } if ( blockerCount <= 0.0 ) { - return uShadowFilterRadius; + return 0.0; } float averageBlocker = blockerDepth / blockerCount; - float penumbra = ( depth - averageBlocker ) / max( averageBlocker, 1.0e-4 ); + float separation = max( compareDepth - averageBlocker, 0.0 ); + float penumbra = separation / max( averageBlocker, 1.0e-4 ); // Keep the clamp ordered when a user deliberately sets a PCSS maximum // below the base PCF radius. A zero maximum still disables PCSS above. - float maxRadius = max( uShadowFilterRadius, uShadowPCSSMaxRadius ); - return clamp( max( uShadowFilterRadius, penumbra * uShadowPCSSLightRadius ), uShadowFilterRadius, maxRadius ); + float maxRadius = max( baseRadius, uShadowPCSSMaxRadius ); + return clamp( max( baseRadius, penumbra * uShadowPCSSLightRadius ), baseRadius, maxRadius ); } #endif @@ -459,11 +476,14 @@ vec4 SampleShadowCascade( vec4 shadowCoord, vec4 atlasRect, int cascadeIndex ) { vec2 clampMax = atlasMax - guardBand; clampMin = min( clampMin, clampMax ); uv = clamp( uv, clampMin, clampMax ); + // Blocker search and PCF use the same stable kernel orientation. Computing + // the hash/trigonometry once also keeps PCSS cost bounded per receiver. + mat2 rotation = ShadowOffsetRotation( uv, depth ); float filterRadius = EffectiveShadowFilterRadius(); #ifndef OPENQ4_SHADOW_COMPARE if ( !ShadowDebugModeIs( kShadowDebugPCFOff ) ) { - filterRadius = ProjectedPCSSRadius( uv, depth, cascadeIndex, clampMin, clampMax ); + filterRadius = ProjectedPCSSRadius( uv, depth, cascadeIndex, clampMin, clampMax, rotation ); } #endif if ( filterRadius <= 0.0 ) { @@ -476,10 +496,10 @@ vec4 SampleShadowCascade( vec4 shadowCoord, vec4 atlasRect, int cascadeIndex ) { if ( uShadowFilterTaps <= 1.0 ) { return vec4( shadow, localUv.x, localUv.y, depth ); } - vec2 o1 = RotateShadowOffset( vec2( -0.326212, -0.405805 ), uv, depth ); - vec2 o2 = RotateShadowOffset( vec2( -0.840144, -0.073580 ), uv, depth ); - vec2 o3 = RotateShadowOffset( vec2( -0.695914, 0.457137 ), uv, depth ); - vec2 o4 = RotateShadowOffset( vec2( -0.203345, 0.620716 ), uv, depth ); + vec2 o1 = rotation * vec2( -0.326212, -0.405805 ); + vec2 o2 = rotation * vec2( -0.840144, -0.073580 ); + vec2 o3 = rotation * vec2( -0.695914, 0.457137 ); + vec2 o4 = rotation * vec2( -0.203345, 0.620716 ); shadow += SampleShadowCompare( clamp( uv + o1 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o2 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o3 * tap, clampMin, clampMax ), depth, cascadeIndex ); @@ -487,10 +507,10 @@ vec4 SampleShadowCascade( vec4 shadowCoord, vec4 atlasRect, int cascadeIndex ) { if ( uShadowFilterTaps <= 5.0 ) { return vec4( shadow * ( 1.0 / 5.0 ), localUv.x, localUv.y, depth ); } - vec2 o5 = RotateShadowOffset( vec2( 0.962340, -0.194983 ), uv, depth ); - vec2 o6 = RotateShadowOffset( vec2( 0.473434, -0.480026 ), uv, depth ); - vec2 o7 = RotateShadowOffset( vec2( 0.519456, 0.767022 ), uv, depth ); - vec2 o8 = RotateShadowOffset( vec2( 0.185461, -0.893124 ), uv, depth ); + vec2 o5 = rotation * vec2( 0.962340, -0.194983 ); + vec2 o6 = rotation * vec2( 0.473434, -0.480026 ); + vec2 o7 = rotation * vec2( 0.519456, 0.767022 ); + vec2 o8 = rotation * vec2( 0.185461, -0.893124 ); shadow += SampleShadowCompare( clamp( uv + o5 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o6 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o7 * tap, clampMin, clampMax ), depth, cascadeIndex ); @@ -498,10 +518,10 @@ vec4 SampleShadowCascade( vec4 shadowCoord, vec4 atlasRect, int cascadeIndex ) { if ( uShadowFilterTaps <= 9.0 ) { return vec4( shadow * ( 1.0 / 9.0 ), localUv.x, localUv.y, depth ); } - vec2 o9 = RotateShadowOffset( vec2( 0.507431, 0.064425 ), uv, depth ); - vec2 o10 = RotateShadowOffset( vec2( 0.896420, 0.412458 ), uv, depth ); - vec2 o11 = RotateShadowOffset( vec2( -0.321940, -0.932615 ), uv, depth ); - vec2 o12 = RotateShadowOffset( vec2( -0.791559, -0.597705 ), uv, depth ); + vec2 o9 = rotation * vec2( 0.507431, 0.064425 ); + vec2 o10 = rotation * vec2( 0.896420, 0.412458 ); + vec2 o11 = rotation * vec2( -0.321940, -0.932615 ); + vec2 o12 = rotation * vec2( -0.791559, -0.597705 ); shadow += SampleShadowCompare( clamp( uv + o9 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o10 * tap, clampMin, clampMax ), depth, cascadeIndex ); shadow += SampleShadowCompare( clamp( uv + o11 * tap, clampMin, clampMax ), depth, cascadeIndex ); diff --git a/content/baseoq4/pak0/glprogs/shadow_point_interaction.fs b/content/baseoq4/pak0/glprogs/shadow_point_interaction.fs index 421543d5..fb8e64ad 100644 --- a/content/baseoq4/pak0/glprogs/shadow_point_interaction.fs +++ b/content/baseoq4/pak0/glprogs/shadow_point_interaction.fs @@ -211,14 +211,14 @@ float StableShadowHash( vec3 value ) { return fract( sin( dot( value, vec3( 12.9898, 78.233, 37.719 ) ) ) * 43758.5453 ); } -vec2 RotateShadowOffset( vec2 offset, vec3 direction ) { +mat2 ShadowOffsetRotation( vec3 direction ) { if ( uShadowFilterMode < 0.5 ) { - return offset; + return mat2( 1.0, 0.0, 0.0, 1.0 ); } float angle = StableShadowHash( floor( direction * 37.0 ) ) * 6.2831853; float s = sin( angle ); float c = cos( angle ); - return vec2( c * offset.x - s * offset.y, s * offset.x + c * offset.y ); + return mat2( c, s, -s, c ); } float TranslucentFilterRadius() { @@ -331,10 +331,11 @@ float SamplePointShadow() { if ( uShadowFilterTaps <= 1.0 ) { return shadow; } - vec2 o1 = RotateShadowOffset( vec2( -0.326212, -0.405805 ), direction ); - vec2 o2 = RotateShadowOffset( vec2( -0.840144, -0.073580 ), direction ); - vec2 o3 = RotateShadowOffset( vec2( -0.695914, 0.457137 ), direction ); - vec2 o4 = RotateShadowOffset( vec2( -0.203345, 0.620716 ), direction ); + mat2 rotation = ShadowOffsetRotation( direction ); + vec2 o1 = rotation * vec2( -0.326212, -0.405805 ); + vec2 o2 = rotation * vec2( -0.840144, -0.073580 ); + vec2 o3 = rotation * vec2( -0.695914, 0.457137 ); + vec2 o4 = rotation * vec2( -0.203345, 0.620716 ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o1.x + bitangent * o1.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o2.x + bitangent * o2.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o3.x + bitangent * o3.y ) * tap ), depth ); @@ -342,10 +343,10 @@ float SamplePointShadow() { if ( uShadowFilterTaps <= 5.0 ) { return shadow * ( 1.0 / 5.0 ); } - vec2 o5 = RotateShadowOffset( vec2( 0.962340, -0.194983 ), direction ); - vec2 o6 = RotateShadowOffset( vec2( 0.473434, -0.480026 ), direction ); - vec2 o7 = RotateShadowOffset( vec2( 0.519456, 0.767022 ), direction ); - vec2 o8 = RotateShadowOffset( vec2( 0.185461, -0.893124 ), direction ); + vec2 o5 = rotation * vec2( 0.962340, -0.194983 ); + vec2 o6 = rotation * vec2( 0.473434, -0.480026 ); + vec2 o7 = rotation * vec2( 0.519456, 0.767022 ); + vec2 o8 = rotation * vec2( 0.185461, -0.893124 ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o5.x + bitangent * o5.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o6.x + bitangent * o6.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o7.x + bitangent * o7.y ) * tap ), depth ); @@ -353,10 +354,10 @@ float SamplePointShadow() { if ( uShadowFilterTaps <= 9.0 ) { return shadow * ( 1.0 / 9.0 ); } - vec2 o9 = RotateShadowOffset( vec2( 0.507431, 0.064425 ), direction ); - vec2 o10 = RotateShadowOffset( vec2( 0.896420, 0.412458 ), direction ); - vec2 o11 = RotateShadowOffset( vec2( -0.321940, -0.932615 ), direction ); - vec2 o12 = RotateShadowOffset( vec2( -0.791559, -0.597705 ), direction ); + vec2 o9 = rotation * vec2( 0.507431, 0.064425 ); + vec2 o10 = rotation * vec2( 0.896420, 0.412458 ); + vec2 o11 = rotation * vec2( -0.321940, -0.932615 ); + vec2 o12 = rotation * vec2( -0.791559, -0.597705 ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o9.x + bitangent * o9.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o10.x + bitangent * o10.y ) * tap ), depth ); shadow += SamplePointShadowCompare( SafeNormalize( direction + ( tangent * o11.x + bitangent * o11.y ) * tap ), depth ); diff --git a/content/baseoq4/pak0/guis/menu/settings/controls.gui b/content/baseoq4/pak0/guis/menu/settings/controls.gui index f67a6345..16f8d7c9 100644 --- a/content/baseoq4/pak0/guis/menu/settings/controls.gui +++ b/content/baseoq4/pak0/guis/menu/settings/controls.gui @@ -1052,7 +1052,7 @@ rect 452,215,149,18 visible 1 forecolor 1,0.745,0.137,0.8 - bind voteyes + bind _impulse28 textscale 0.24 font "fonts/lowpixel" @@ -1066,7 +1066,7 @@ rect 452,239,149,18 visible 1 forecolor 1,0.745,0.137,0.8 - bind voteno + bind _impulse29 textscale 0.24 font "fonts/lowpixel" @@ -1094,7 +1094,7 @@ rect 452,287,149,17 visible 1 forecolor 1,0.745,0.137,0.8 - bind ready + bind _impulse17 textscale 0.24 font "fonts/lowpixel" diff --git a/docs/dev/airdefense1-optimization-evidence.md b/docs/dev/airdefense1-optimization-evidence.md new file mode 100644 index 00000000..6a787239 --- /dev/null +++ b/docs/dev/airdefense1-optimization-evidence.md @@ -0,0 +1,110 @@ +# `game/airdefense1` Optimization Evidence + +This note records the 2026-08-24 Windows x64 optimization pass performed after +the idTech 5 modernization implementation. The complex opening scene in +`game/airdefense1` was used throughout. All game runs were bordered windowed at +1280x720, used the engine's registered screenshot command, and required actual +SP gameplay rather than main-menu startup. No mouse/keyboard input was +controlled or captured. + +## Findings and changes + +Two modernization regressions were isolated: + +1. Presentation interpolation walked every spawned entity on every authoritative + 60 Hz tick. The opening cinematic fast-forward executes more than 11,600 + simulation ticks in one host frame, turning this into an + `spawned entities x skipped ticks` workload even though none of the + intermediate poses can be presented. Sampling now exits during cinematic + fast-forward and, on visible frames, visits active movers, the bounded + physics-team members those movers own, plus the previous presentation + members that need one final cleanup sample. +2. Maps without baked light-grid assets built a 22,024-point runtime probe + layout during ordinary loading. That layout is useful only for baking and + debug preparation. Ordinary runtime setup now keeps an empty grid; bake + entry points create the layout on demand. + +The classic source-loading path remains the default. This pass did not mask the +regression by enabling `com_levelLoadModernization`; that experiment remains +default off until its own cold/warm qualification is favourable. + +## Attributable measurements + +The direct `g_airdefense1SkipProbe` holds the simulation rate and endpoint +constant, so it is the most useful causal comparison: + +| Measurement | Before | After | Change | +|---|---:|---:|---:| +| Opening fast-forward wall time | 7,113 ms | 5,770 ms | -1,343 ms (-18.9%) | +| Final scripted game time | 187.008 s | 187.008 s | exact | +| Missing-light-grid setup | 357 ms | 1 ms | -356 ms (-99.7%) | + +The final implementation preserves the stock 60 Hz simulation step. A bounded +30 Hz experiment reduced the fast-forward to 2,968 ms and reached a +near-identical end state, but it was rejected because broader script, physics, +and frame-command parity was not established. + +## End-to-end and frame evidence + +The isolated gameplay benchmark reported the following before/after snapshots: + +| Measurement | Before | After | +|---|---:|---:| +| Settled gameplay total | 33,121 ms | 25,091 ms | +| Render-world phase | 3,210 ms | 2,323 ms | +| Opening settle phase | 8,324 ms | 5,874 ms | +| Steady presentation rate | 110.8 Hz | 120.4 Hz | +| Frame pacing P95 / P99 | 13 / 13 ms | 11 / 12 ms | + +The end-to-end total is indicative rather than wholly attributable: each run +used an isolated save path, generated its own binary-image cache, and could not +reset the operating-system file cache. The controlled measurements above are +the causal acceptance values. + +A final diagnostic capture showed CPU P50/P95/P99 of +7,151/9,163/10,533 microseconds and GPU P50/P95/P99 of +2,714/3,531/3,679 microseconds. `airdefense1` is therefore CPU/front-end limited +on this system. The optimization targets simulation/presentation and setup +work instead of lowering GPU image quality. + +## Advanced screen-space load + +With froxel volumetrics, SSR, and SSGI enabled together, real gameplay passed +with zero shader, framebuffer, OpenGL, Vulkan-validation, VUID, Vulkan-call, +fatal, or engine-error counters: + +| Backend | Final hardened rate | Final P95 / P99 | Observed post-audit range | +|---|---:|---:|---| +| OpenGL | 117.3 Hz | 11 / 11 ms | 102.9--117.3 Hz; engine TGA, all leaves together; each leaf also passed independently | +| Vulkan | 125.8 Hz | 10 / 11 ms | 114.1--125.8 Hz; engine TGA, all leaves together | + +The live scene contains time-dependent BSE smoke and actors, so independently +launched screenshots are not exact-hash fixtures. The shared native contract +therefore enforces exact master rollback by publishing a zeroed eight-float +packet, while runtime master-off gameplay verifies the complete integration +path. The range retains both final-build samples rather than selecting only the +faster run; scene timing and system cache state vary between process launches. + +## Final staged-package regression + +The final x64 rebuild compiled all 1,210 engine/client/dedicated and GL/Vulkan +targets plus both companion game modules, staged `.install/`, and passed all +11 Meson native tests. The post-rebuild windowed gameplay audit retained an +engine TGA for every role and reported no shader, framebuffer, GL, +Vulkan-validation, VUID, Vulkan-call, fatal, or engine-error counters: + +| Route | Rate | P95 / P99 | +|---|---:|---:| +| OpenGL `game/airdefense2` defaults | 189.6 Hz | 7 / 8 ms | +| Vulkan `game/airdefense2` defaults | 240.2 Hz | 6 / 6 ms | +| OpenGL `mp/q4dm1` listen server / auto-joined client | 174.0 / 180.7 Hz | 7 / 7 and 7 / 8 ms | +| Vulkan `mp/q4dm1` listen server / auto-joined client | 224.8 / 189.1 Hz | 6 / 6 and 7 / 7 ms | + +The final clean-asset compatibility run used all 40 verified retail PK4s and +zero loose `q4base`/`q4mp` files. SP capture, SP demo playback, the pure MP +server, and its auto-joined client all passed from the staged package. The +packaged openQ4 overlay PK4s were intentionally present, so this proves retail +asset compatibility rather than an overlay-free executable. Representative +OpenGL/Vulkan SP, screen-effect, and MP engine screenshots were reviewed for +black output, invalid geometry, missing HUD, and obvious presentation defects; +none were found. diff --git a/docs/dev/classic-cinematic-post-domain-modernization.md b/docs/dev/classic-cinematic-post-domain-modernization.md new file mode 100644 index 00000000..f9202f2a --- /dev/null +++ b/docs/dev/classic-cinematic-post-domain-modernization.md @@ -0,0 +1,227 @@ +# Shared Classic Cinematic and Authored-Post Transaction + +## Status + +This Milestone D corridor is **Implemented (default-off; native/static and +controlled Windows OpenGL/Vulkan runtime acceptance recorded; clean +final-package, retained-review, broader-content, platform, and driver release +qualification pending)**. +`r_rendererSharedCinematicPost 1` lets OpenGL and Vulkan use the same sealed +transaction boundary for two dynamic classic-frame islands: + +- an eligible root 2D view containing `videoMap` or `soundMap` playback; and +- the complete ordered `SS_POST_PROCESS` tail of an eligible ordinary root 3D + view or of an eligible child in a sealed special-view tree. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Transaction boundary + +The shared domain does not attempt to turn a cinematic decoder, current-frame +capture, or custom authored shader into a static material record. Those inputs +are dynamic by contract. Instead, front-end scene packets seal the complete +source range, exact admitted draw-packet identity/order, view identity, and the +cinematic render-view clock before either backend can claim it. + +For root cinematics, all root sources remain in one transaction; a matching GUI +packet sequence proves the packet-admitted surface order. The stored time is +derived from the same `floatTime` and render-view parameter consumed by the +classic video path. For authored post, the transaction starts exactly at the +first `SS_POST_PROCESS` surface and must consume the entire remaining tail with +matching `RENDER_PASS_AUTHORED_POST` packet identities. It records cinematic, +`_currentRender`, and `_currentDepth` stage use for diagnostics. + +Special-view topology is prepared first. A nested authored-post record must +resolve the already sealed subview member and root, then retain that root view, +root scene-packet index, and exact nesting depth. Missing, rejected, or changed +special-view topology is not treated as an ordinary root post range. + +After preflight, each backend dispatches the established dynamic-stage adapter: +OpenGL retains `RB_STD_DrawShaderPasses`; Vulkan retains +`VK_Exec_DrawAmbientStages`. Therefore video decode, scratch-image upload, +sound/video timing, feedback capture, custom program stages, and ordered +post-material execution stay on their known classic implementations. This is a +sealed ownership and fallback boundary, not a second decoder or post shader +implementation. + +Nested completion is deliberately two-phase. The dynamic-stage executor first +records exact source-surface completion without publishing cinematic/post +ownership. The outermost special-view transaction then verifies every admitted +dynamic range and every direct/capture member before publishing all nested +cinematic/post and subview records. A cinematic/post rejection marks the whole +special-view tree fallback; a subview rejection marks every nested +cinematic/post record for that root fallback. No descendant can remain reported +shared-owned after its root transaction fails. + +## Eligibility and fallback + +The root cinematic path accepts only a non-subview, non-editor, non-world 2D +view with no post-process sources and at least one cinematic stage. The authored +post path accepts either an ordinary root 3D view or a child view with an exact +`superView` and source surface while `r_rendererSharedSubview 1` has already +prepared a ready member/root record. In either form, the post tail must be +complete and packet-matching. Both paths reject invalid scene/pass ranges, +changed source identity, overflow, non-finite cinematic time, render targets, +debug/skip modes, feedback-capture failure, and backend rejection. + +A rejection uses the complete classic fallback path. Once an adapter starts, it +retains the whole sealed range and reports +ownership only if its exact source-surface count is reconciled; it never mixes a +partially shared cinematic or post chain with the classic walker. + +Nested admission and rollback have explicit diagnostics. `missingSpecialView` +identifies a post tail that cannot bind to a ready special member/root; +`specialViewTransactionRejected` propagates special-tree rejection into its +dynamic records; and `backendSpecialViewIncomplete` names incomplete backend +coupling in the cinematic/post vocabulary. In the subview vocabulary, +`nestedCinematicPostFallback` propagates a dynamic-stage rejection to the whole +tree and `backendNestedCinematicPostIncomplete` rejects root finalization when +an admitted dynamic range did not finish with exact coverage. + +## Control and diagnostics + +`r_rendererSharedCinematicPost` remains independently selectable from GUI, +in-world GUI, world ambient, interaction, fog/blend, subview, and deform +ownership. When it and `r_rendererSharedSubview` are both enabled for an +eligible nested tail, their records deliberately share one atomic root +transaction: + +- `0` (default): cinematics and authored post surfaces use the normal backend + path; +- `1`: an eligible complete root cinematic view, root authored-post tail, or + authored-post tail inside a sealed special view may use the transaction; all + other work remains classic-owned. Nested admission remains unavailable when + `r_rendererSharedSubview` is off. + +`gfxInfo` reports request/preparation state, source scenes, root/post counts, +ready/fallback views, cinematic and current-render/depth stage counts, semantic +hash, and OpenGL/Vulkan ownership/fallback/mismatch/duplicate totals. Its +`nested=/` +and `nestedCinematic=` fields expose admitted special-view members, +distinct root transactions, and nested cinematic stages. Focused +dependency-light validation is: + +```text +rendererScenePacketSelfTest +rendererClassicCinematicPostDomainSelfTest +gfxInfo +``` + +`tools/tests/renderer_classic_cinematic_post_domain.py` guards the bounded +domain interface, packet/timing admission, subview-before-cinematic preparation, +exact OpenGL/Vulkan dynamic-stage handoff, deferred nested completion, +bidirectional transaction fallback, conservative defaults, release-harness +isolation, diagnostics, and CI registration. + +## Controlled nested acceptance evidence + +The final controlled Windows acceptance on 2026-08-23 passed all six cases on +both OpenGL and Vulkan. It used an isolated temporary runtime and the generated +`maps/tools/milestone_d_nested_dynamic` fixture: one stock-material, +capture-backed color-2D mirror whose child-only authored-post material has one +frozen stock ROQ cinematic stage and one `_currentRender` stage. The fixture is +validation-only `.tmp` content and is neither committed nor shipped. + +- The foundation self-test report is + `.tmp/renderer-validation/milestone-d-foundation-20260823-r6/renderer_validation_report.json` + (SHA-256 + `0659f1f2ee5108a422cd6c41bdc78809731f25ad7eda4b5ee84328a9163ce7ac`), + with accompanying Markdown SHA-256 + `a619169ee97f2d6e8c7dfab991adbab5b5b282fd2e1197e2d59eb3609c87cddf`. + It records a passing foundation case with no warning signature. +- The fixture manifest is + `.tmp/renderer-milestone-d/qualification-20260823-r6/fixture_manifest.json` + (SHA-256 + `24a2a4926f4b1263e311ad762bf40d391b33de225732d12154ebd7625705282d`). + It pins the generated map/material/`.proc`/`.cm`, the copied retail ROQ, the + required runtime components, and a 3,884-file, 7,281,560,043-byte stock + dependency inventory sealed by SHA-256 + `13abad18f70eb8b4bf6ea0e9697b317718bb065ee76f86070787005b045dda7a`. + Its eight-file, 25,330,583-byte fixture evidence inventory is sealed by + SHA-256 + `0572fb8d9c132f7e97060345464f97750e59e69819871fce90eed13d1348d8ae`. + The fixture and both inventories remained unchanged across acceptance. +- The acceptance report is + `.tmp/renderer-milestone-d/acceptance-20260823-r6-all/renderer_milestone_d_acceptance_report.json` + (SHA-256 + `b49031c48267fbfa86c295082707525a390d6090c24f600d00644237a67b252a`). + Its runtime at + `.tmp/stock-runtime/milestone-d-qualification-20260823-r6` remained + unchanged with manifest SHA-256 + `dbece44597ec5e0686eed215f6ae99c5d6ba4cdd959c1efa7c4acaf1bc2ca673`. + Runtime, fixture, and stock verification failures were all empty. + +Each backend ran classic normal, subview-only normal, cinematic-only normal, +both-on normal, classic post-skip, and both-on post-skip sequentially. The fixed +capture script disabled input and produced actual windowed mode `-1` runs at +1280x720 with pacing-only sampling, GPU timers off, and the engine `screenshot` +command writing 1280x720 TGA images from the render target. All 12 cases passed. +Each backend's four exact comparisons reported RMS `0`, maximum delta `0`, and +zero differing channels: the three enabled normal combinations matched classic +normal, and both-on skip matched classic skip. Normal versus skip remained +visibly non-vacuous: OpenGL changed 46,359 RGB channels at RMS `2.0073` / +maximum `72`; Vulkan changed 3,329 channels at RMS `0.2506` / maximum `18`. + +In both-on normal, the active backend reported one prepared and valid nested +post view, one nested special transaction, one nested cinematic stage, one +cinematic stage, and one `_currentRender` stage. Its cinematic +owned/fallback/mismatch/duplicate tuple was `1/0/0/0`, while the matching +subview owned/fallback tuple was `1/0`. With `r_skipPostProcess 1`, those tuples +were `0/1/0/0` and `0/1`, and the subview named +`nestedCinematicPostFallback`; classic/shared output remained exact. The +inactive backend's counters stayed zero in every run. The independent-CVar +cases stayed classic-equivalent: subview-only owned the mirror with cinematic +request/view counts at zero, while cinematic-only was prepared but correctly +vacuous because this fixture authors the post tail only inside the subview. + +A supplemental windowed Vulkan run exercised the terminal `r_skipAmbient 1` +rollback path. Its report at +`.tmp/renderer-gameplay/milestone-d-vk-skipambient-20260823-r6/renderer_gameplay_benchmark_report.json` +(SHA-256 +`f5083b492b832557bf30e6bc268b1dec94079e3828fb615ce10261c4e26c5c99`) +passed and recorded cinematic `VK=0/1/0/0` plus subview fallback `1`, with the +named `nestedCinematicPostFallback` reason and no mismatch or duplicate. This +is targeted early-exit coverage, not an expansion of the promotion scope. + +The retained invocation is: + +```powershell +python tools/tests/renderer_milestone_d_acceptance.py ` + --runtime-dir .tmp/stock-runtime/milestone-d-qualification-20260823-r6 ` + --fixture-manifest .tmp/renderer-milestone-d/qualification-20260823-r6/fixture_manifest.json ` + --render-api all ` + --output-dir .tmp/renderer-milestone-d/acceptance-20260823-r6-all +``` + +## Release-qualification boundary + +The final R6 acceptance closes the controlled runtime gate only for the +single nested cinematic-plus-`_currentRender` mirror fixture above. It does not +qualify root cinematic views, ordinary-root authored post, `_currentDepth`, +multi-level or other broad special-view forms, in-world GUI, render-demo/Raven +special frames, a final release package, non-Windows platforms, or broader +driver coverage. + +Before promotion, retain engine-written `screenshot` captures—not OS +captures—for stock root cinematic and ordinary-root authored-post scenes with +the setting off and on, separately on OpenGL and Vulkan. Add a controlled +`_currentDepth` nested case and the applicable broader special-view forms. +Require nonzero reconciled ownership where content is eligible, exact +same-settings output, a deterministic cinematic clock, and named malformed +packet/capture or dynamic-stage fallback with zero committed ownership. Also +retain clean final-package diagnostics, human review, and target-platform/driver +coverage. + +Render-demo and Raven special-frame ownership now have their own sealed +[transaction](classic-special-frame-domain-modernization.md). Direct +special-subview ownership is now documented in the [Shared Special-Subview +Transaction](classic-subview-domain-modernization.md), including atomic +authored-post completion and rollback for eligible nested tails. + +Together with the other independently guarded shared corridors, this completes +Milestone D's scoped implementation. It does not make every classic-frame +category or the aggregate modern-visible renderer shared-owned. The narrow +controlled nested runtime gate above is recorded; clean-package, broader +content, human-review, and target-platform/driver evidence remain +release-promotion gates. Temporal presentation is the next roadmap milestone. diff --git a/docs/dev/classic-deform-domain-modernization.md b/docs/dev/classic-deform-domain-modernization.md new file mode 100644 index 00000000..7a498c9e --- /dev/null +++ b/docs/dev/classic-deform-domain-modernization.md @@ -0,0 +1,245 @@ +# Shared Classic Material-Deform Contract + +## Status + +The Milestone D material-deform dependency is **Implemented (default-off; +controlled local OpenGL/Vulkan qualification recorded below; freshly staged +final package and platform release promotion pending)**. +`r_rendererSharedDeform 1` allows an existing shared classic domain to consume +the final geometry produced by the established CPU material-deform path when +that result has a complete sealed provenance record. The classic front-end +deform algorithms remain authoritative, and every unsupported, skipped, stale, +or incomplete record keeps the owning GUI view, world-ambient view, +interaction view, or fog/blend phase on its complete established path. + +This is a cross-domain geometry dependency, not a fifth framebuffer pass and +not a GPU rewrite of `R_DeformDrawSurf`. It does not independently enable root +GUI, world ambient/material, interaction, or fog/blend ownership; the applicable +`r_rendererShared*` domain setting must also be enabled. All settings remain +off by default. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Why a separate contract is required + +Two unrelated concepts previously shared the word "deformed": + +- a material declaration can request `sprite`, `rectsprite`, `tube`, `flare`, + `expand`, `move`, `turbulent`, `eyeball`, `particle`, or `particle2`; and +- `srfTriangles_t::deformedSurface` describes generated/dynamic model geometry, + including ordinary CPU-skinned character surfaces. + +The second flag does not prove that a material deform ran. Treating it as that +proof either rejects valid generated models or admits an unexecuted material +deform. `ClassicDeformDomain` instead brackets the established +`R_FinalizeDrawSurf` call and records the exact source and published geometry. +Scene-packet geometry records own one immutable copy per draw role and draw +packets retain a stable reference to that copy, so geometry records which share +a triangle pointer cannot alias different deform provenance without inflating +the fixed draw-packet arena. + +## Sealed record + +Each record contains: + +- the consumer role: finalized draw, interaction receiver, fog receiver, + shadow volume, or another explicitly classified use; +- the material-deform kind and one of `none`, `notApplicable`, `skipped`, + `completed`, `empty`, `failed`, or `unsupported`; +- source and result material identity and stable name hashes; +- source and result geometry identity, vertex/index counts, cache pointers, + buffer identity, byte ranges, tags, client-data availability, primitive-batch + state, and upload lifetime; +- the evaluated deform-register indices and finite values, deform-declaration + identity, view/model input hash, and flare scale where applicable; +- a nonzero front-end frame token and explicit CPU-finalization proof; and +- a stable semantic hash which excludes pointers, backend buffer names, and the + frame token while retaining all behaviorally relevant values. + +Validation is fail-closed. A drawable completed result must be a distinct, +frame-temporary geometry object with positive triangle work and enough sealed +vertex/index backing. An intentional empty result must be a distinct valid +result with exactly zero indexes and no drawable cache claim. Cache, pointer, +material, count, role, outcome, frame, or hash drift invalidates the record. + +## Classic role semantics + +The original renderer does not run material deforms for every consumer of a +surface, so the contract preserves that behavior rather than inventing new +lighting or fog geometry: + +- root GUI and world ambient/material draw surfaces are finalized before scene + packet construction and require `completed` or intentional `empty` output; +- mapped shadow casters also use their finalized result; an empty result becomes + a sealed no-op caster and contributes no backend draw; +- classic interaction and fog receiver chains intentionally retain their source + geometry and therefore require the explicit `notApplicable` receiver role; +- stencil shadow volumes retain their established geometry ownership and are + not reclassified as material-deform output. + +This distinction preserves the classic material-deform limitation noted in the +original code: the ambient result does not create a new interaction surface. +It also lets CPU-skinned model geometry remain governed by its own skinning and +dynamic-geometry checks instead of the material-deform switch. + +## Supported kinds and rollback + +The shared contract admits successful CPU results for the material kinds whose +classic algorithms are implemented: `sprite`, `rectsprite`, `tube`, `flare`, +`expand`, `move`, `turbulent`, and `eyeball`. Their algorithms, register +evaluation, topology generation, tangent generation, and frame-cache allocation +are unchanged. + +`particle` and `particle2` remain explicit `unsupported` outcomes because the +current `R_ParticleDeform` implementation emits no replacement geometry. +`r_skipDeforms 1`, malformed topology, a failed frame-cache allocation, +non-finite or out-of-range inputs, and any stale or mismatched record are named +failures. The owning shared view or phase rejects before its first shared +framebuffer write and executes the complete classic path exactly once. The skip +setting also blocks interaction/fog receiver ownership even though those roles +are normally sealed as `notApplicable`; this keeps the explicit diagnostic +comparison path completely outside every shared deform-dependent phase. + +## Domain and backend flow + +1. The front end evaluates material registers and brackets the authoritative CPU + deform call with a source/result snapshot. +2. Scene packets seal the record with the exact pass role and frame token. + Geometry lookup includes deform provenance, preventing pointer-only aliasing. +3. `ClassicGuiDomain`, `ClassicWorldAmbientDomain`, + `ClassicInteractionDomain`, and `ClassicFogBlendDomain` validate every + material-deform record before publishing their bounded transaction. Deform + outcome and semantic hash participate in each owning record and view hash. +4. OpenGL and Vulkan consume the ordinary final `idDrawVert`/index stream. They + do not implement, rerun, or reinterpret a material deform. +5. Existing complete-domain backend preflight and reconciliation prove that a + completed result was drawn once or an empty result was counted once without a + draw. Any failure leaves zero mixed shared/classic ownership. + +Vulkan GUI/world preflight separately snapshots its transient geometry rings, +direct-mapped upload memos, GPU-skin memo, and bound offset. A mid-list failure +restores that state before classic fallback; world sampler mutations are also +restored. Successful preflight commits only after the complete geometry batch +fits. + +## Control and diagnostics + +`r_rendererSharedDeform` is an archived Boolean control: + +- `0` (default): a material deform remains a named blocker for shared classic + domains, preserving the earlier behavior; +- `1`: completed/empty finalized results and explicit not-applicable receiver + roles may participate; all other outcomes retain complete-domain rollback. + +Default-safety, stock-baseline, ordinary gameplay-benchmark, and aggregate +modern-visible profiles force the option off unless a deform qualification run +enables it deliberately. + +Use these commands for focused validation: + +```text +rendererClassicDeformDomainSelfTest +rendererClassicGuiDomainSelfTest +rendererClassicWorldAmbientDomainSelfTest +rendererClassicInteractionDomainSelfTest +rendererClassicFogBlendDomainSelfTest +gfxInfo +``` + +`gfxInfo` reports the request state, scene-packet deform roles/outcomes, +semantic hashes, per-domain deform coverage, named failures, and OpenGL/Vulkan +owned/fallback reconciliation. The top-level outcome counters describe emitted +draw packets only; a finalized `empty` result has no draw packet by definition, +so GUI/world/mapped-caster empty results are reported by their owning domain's +empty/no-op counters without being double-counted across passes. +`tools/tests/renderer_classic_deform_domain.py` guards the outcome and freshness +contract, source/final cache proof, pointer/provenance isolation, all four +domain admission rules, backend preflight ordering, default-off isolation, +self-test registration, and validation-workflow coverage. + +## Local qualification + +The registered `deform` gameplay profile uses the stock `maps/tools/mv2` scene, +a fixed bordered/windowed 1280x720 camera, and the shipped +`shaderDemos/move` material through `r_materialOverride`. It leaves normal +material deformation enabled and combines `r_rendererSharedWorldAmbient 1` +with `r_rendererSharedDeform 1`. References must be captured separately for +OpenGL and Vulkan with the shared settings disabled, using only the engine's +registered `screenshot` command. Because the shipped material evaluates a +time-based move expression, the profile starts frozen, advances a fixed number +of one-tic frames, and freezes again before sampling. It also disables mouse +input before startup so physical input cannot move the retained camera. + +The implementation gate requires: + +- exact same-backend classic/shared TGA parity with nonzero completed deform + ownership and zero coverage mismatches; +- a material effect delta against otherwise identical `r_skipDeforms 1` + output; +- exact classic parity and zero committed shared draws for an intentional + skipped, unsupported, failed, or stale-record blocker; +- clean OpenGL diagnostics and Vulkan validation output; and +- clean toggle, map-restart, frame-reset, native, static, and build checks. + +The 2026-08-22 development-worktree run used the staged Windows x64 runtime. +Both backends passed the complete controlled gate: + +- Normal classic/shared pairs match exactly at RMS `0` and maximum channel + delta `0`. OpenGL's pair has screenshot SHA-256 + `f7cb398ac9a2b51003f6b33db25fde3b8167808cc3996a57fd6224e64d737500`; + Vulkan's pair has + `6dccd89c48eceddc830f9a6e70aa08e73024258f448927374bb6cd5cc87a9295`. +- Each shared run emits two valid `completed` records, no failed, unsupported, + or invalid record, semantic hash `8ea371dfc9b550ff`, and one reconciled owned + world-ambient draw. Both backends report domain hash `d498cca50db350d0`, + `ready=1`, `fallback=0`, `surfaces=1/1`, `deform=1/1/0`, `passes=1`, and + `draw=1`; only the active backend's owned counter is nonzero. +- With `r_skipDeforms 1`, each shared image again matches its otherwise + identical classic image at RMS `0` and maximum delta `0`. OpenGL's skipped + pair has SHA-256 + `4cecc8b621ae618ddbcbacb96efea15bd79b84e8613b3c41346dd289bce8e291`; + Vulkan's has + `e3b092f7b787d40af5e915febee14f3bd631a191b3be6eb9b47d883666c9eb0b`. +- The skipped runs emit two valid `skipped` records with semantic hash + `4f7f7e6329163123`. World ownership fails closed with + `failure=deformContract detail=9`, `ready=0`, `fallback=1`, and zero shared + surfaces, passes, or draws; only the active backend records the fallback. +- The skipped image differs materially from the normal deform on both + backends, proving the controlled effect is visible: OpenGL reports RMS + `57.54`, maximum delta `194`, and `2,727,697` changed RGB channels; Vulkan + reports RMS `55.03`, maximum delta `190`, and `2,728,103` changed channels. +- The gameplay reports contain no OpenGL error, framebuffer failure, Vulkan + validation warning, VUID, Vulkan call failure, fatal error, or engine error + line. The native foundation suite and Vulkan clear/startup safety case pass at + `.tmp/renderer-validation/deform-foundation-final3` and + `.tmp/renderer-validation/deform-vulkan-final2`. + +The retained gameplay reports are under +`.tmp/renderer-gameplay/deform-{gl,vk}-final3-{classic,shared,skip-classic,skip-shared}`. +They bind the staged runtime and dirty source revision used for this local +qualification. Clean committed-source recapture, a freshly staged final +package, human image review, and target-platform/driver coverage remain +separate release-promotion gates. + +## Milestone D completion and release-promotion boundary + +Root GUI, world ambient/material, fixed-classic interaction, and fog/blend can +now consume sealed classic material-deform results through one independent +default-off dependency. This does not make the complete frame modern-owned. + +The completed cinematic/authored-post corridor seals eligible root video/audio +views and complete post tails while retaining the existing dynamic stage +executors. Eligible authored-post tails inside a sealed special-view tree now +complete and publish with that tree's root transaction; see [Shared Classic +Cinematic and Authored-Post +Transaction](classic-cinematic-post-domain-modernization.md) and [Shared +Special-Subview Transaction](classic-subview-domain-modernization.md). +Render-demo and Raven special-frame ownership have a dedicated +[transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation. They do not make every classic-frame category or the aggregate +modern-visible renderer shared-owned. Clean-package, human-review, and +target-platform/driver evidence remain release-promotion gates; temporal +presentation is the next roadmap milestone. diff --git a/docs/dev/classic-fog-blend-domain-modernization.md b/docs/dev/classic-fog-blend-domain-modernization.md new file mode 100644 index 00000000..31ece9c7 --- /dev/null +++ b/docs/dev/classic-fog-blend-domain-modernization.md @@ -0,0 +1,277 @@ +# Shared Classic Fog/Blend Domain + +## Status + +The fourth Milestone D classic-frame domain is **Implemented (default-off; +native/static and controlled OpenGL/Vulkan runtime qualification recorded; +authored-stock, final-package, and platform release promotion pending)**. +`r_rendererSharedWorldFogBlend 1` allows the complete fog/blend phase of one +eligible ordinary root 3D view to be evaluated once into backend-neutral sealed +records and consumed by OpenGL or Vulkan. The established backend fog/blend +walker remains the supported default and the complete-phase rollback. + +This is a complete fog/blend ownership boundary, not a claim that the complete +frame, the older aggregate `r_rendererModernVisible` experiment, or all of +Milestone D is modern-owned. Shared root GUI, world ambient/material, and +fixed-classic interaction ownership remain independent domains with independent +settings and rollback decisions. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Why the boundary is a complete phase + +Fog and blend lights are compositing operations, not ordinary additive +interaction primitives. The established frame order is interaction lighting, +pre-fog ambient/material work, the complete fog/blend pass, and then post-fog +material work. A fog light also draws its frustum cap even when it has no +receiver packet, while a blend light can contain multiple conditionally active +stages with distinct projection images, matrices, colors, and framebuffer blend +state. + +The shared domain therefore accepts or rejects the complete fog/blend phase of +one view. It never hands off one light or receiver after another light has +already changed the main target. The transaction covers: + +- every fog or blend light in original `viewLights` order; +- exact GLOBAL then LOCAL receiver-chain identity and order; +- every blend-light material stage, including inactive and recognized no-op + dispositions; +- every fog receiver plus the fog light's frustum-cap work; +- evaluated light registers, texture matrices, projection/falloff planes, + colors, density, scissors, culling, depth, and blend state; +- generation-checked projection, falloff, `_fog`, and `_fogEnter` resources; + and +- exact light, receiver, stage, cap, draw/no-op, resource, semantic-hash, and + backend coverage accounting. + +If any required record cannot be represented, the transaction publishes no +shared phase. If backend preflight cannot retain the complete plan, that backend +records one named fallback and performs zero shared main-target fog/blend draws. + +## Exact classic fog contract + +The fog corridor preserves the established fixed-classic behavior: + +- only fog lights admitted by the original view-light walk participate; +- GLOBAL receivers execute before LOCAL receivers, while the classic path's + non-fogged translucent chain remains outside this pass; +- the first light-material stage supplies the evaluated fog color and density; +- distance and entry texgen planes are derived per receiver space from the view + origin, model matrix, fog plane, and classic fog-distance rule; +- `_fog` and `_fogEnter` retain their ordinary RGBA lookup behavior; +- receiver work uses the classic depth-equal, depth-mask, source-alpha blend + state and original per-surface scissor/cull semantics; and +- the fog frustum cap uses its sealed geometry, fog plane, back-sided culling, + depth-less comparison, view scissor, and the same evaluated fog color. + +A fog light with no eligible receiver still owns its required frustum-cap work. +Missing that cap from packet or backend accounting is a complete-phase failure, +not an empty successful view. + +## Exact classic blend-light contract + +The blend corridor walks every authored light-material stage in order. Each +record retains its checked condition, active/no-op disposition, evaluated RGBA, +authored blend factors and write/depth state, projection image, falloff image, +optional texture matrix, localized projection/falloff planes, and GLOBAL or +LOCAL receiver identity. + +The backend result remains the projected stage image multiplied by the falloff +sample and evaluated stage color, then composited with the stage's authored +framebuffer blend state. The shared path does not reinterpret a blend light as +a clustered additive light or collapse repeated stages into one draw. + +## Eligibility and whole-phase fallback + +The first shared corridor admits ordinary root 3D views whose complete active +fog/blend phase can be sealed and preflighted. It preserves the established +`r_skipFogLights`, `r_skipBlendLights`, overdraw, x-ray-view, material `noFog`, +surface-coverage, and suppress-in-subview decisions rather than weakening them +to obtain ownership. + +Preparation or backend preflight rejects the complete phase for conditions such +as: + +- subview, mirror, x-ray, editor, offscreen, render-demo, or other non-root view + ownership; +- packet/light/receiver/stage/geometry identity or ordering mismatch; +- unsupported or unsealed material-deform provenance, generated/deformed, + packed, GPU-palette, missing-cache, depth-hack, or other unsealed receiver or + cap geometry; +- custom, cinematic, dynamic, defaulted, unloaded, stale, or invalid image + resources; +- invalid or non-finite registers, colors, matrices, planes, density, or state; +- unsupported blend, depth, cull, sampler, descriptor, target, or pipeline + state; +- bounded light, receiver, stage, texture, geometry, uniform, descriptor, or + pipeline-capacity overflow; or +- any backend transaction-preflight failure. + +Fallback is decided before the first shared main-framebuffer write. An +unexpected post-commit coverage mismatch is a diagnostic fault; the classic +fog/blend stream is never layered over a partially submitted shared result. + +## Shared record and backend flow + +1. Scene packets identify fog/blend receiver work and preserve its source light, + receiver class, chain ordinal, surface, material, geometry, and instance + identities. +2. `ClassicFogBlendDomain` walks the original view lights transactionally, + evaluates all required light stages and fog state once, resolves every opaque + resource id, seals receiver/cap records, and computes a backend-independent + semantic hash. +3. The domain publishes the view only after exact packet/source reconciliation + and complete bounded preparation. Failure rewinds every frame-local arena to + its checkpoint and retains only the named fallback. +4. OpenGL and Vulkan independently preflight the complete published phase before + the first shared draw. The consumers use only sealed records; they do not + reread light material stages or shader registers during submission. +5. A backend reports ownership only when its fog lights, blend lights, + receivers, ordered stages, caps, drawable/no-op records, and total submitted + work reconcile exactly with the shared view. + +OpenGL retains all required geometry, images, sampler/state transitions, and +program/fixed-function requirements before commit, then executes the sealed +phase at the established fog point. Vulkan retains geometry-ring, uniform, +descriptor, exact pipeline-key, render-scope, target, and capacity requirements +before command recording can affect the main target. Both backends leave the +surrounding pre-fog/post-fog material ordering unchanged. + +## Control and diagnostics + +`r_rendererSharedWorldFogBlend` is the single cross-backend control: + +- `0` (default): every fog/blend phase uses the established backend walker; +- `1`: an eligible complete phase may use the shared domain; any rejection uses + the complete established phase. + +The setting is independent of `r_rendererSharedWorldInteraction` and +`r_rendererSharedWorldAmbient`. Stock-baseline, ordinary gameplay-benchmark, +and renderer default-safety profiles force it off unless a run intentionally +collects shared fog/blend evidence. + +`gfxInfo` reports requested/prepared/valid state; ready and fallback view +counts; fog/blend light, receiver, stage, cap, drawable, inactive, and no-op +counts; the stable semantic hash and first failure; and exact OpenGL/Vulkan +owned/fallback coverage. + +Use these commands for focused validation: + +```text +rendererContractsSelfTest +rendererClassicFogBlendDomainSelfTest +gfxInfo +``` + +The dependency-light self-test covers bounded publication and rewind, stable +hashing, fog density and plane state, receiver/cap accounting, repeated and +inactive blend stages, source-order reconciliation, backend coverage, and named +atomic fallback. `tools/tests/renderer_classic_fog_blend_domain.py` guards +packet/source identity, complete OpenGL/Vulkan preflight before commit, sealed +consumer bodies, the pre-fog/fog/post-fog insertion order, conservative default +and bootstrap state, benchmark/baseline isolation, and validation-workflow +registration. + +Native and static validation passes. The controlled runtime evidence below +establishes exact same-backend parity, nonempty ownership, visible fog/blend +contributions, and atomic rollback for the bounded fixture; it is not a +substitute for the remaining authored-stock and release-package gate. + +## Controlled runtime qualification + +The registered `fog-blend` gameplay profile uses a controlled +`sp-mv2-fog-blend` scene on stock `maps/tools/mv2`. It may spawn only shipped +light declarations, including `lights/fog_generic` for fog and the deterministic +`lights/fog_ambient` declaration for blend-light coverage. The animated +`lights/stream_fog` declaration is intentionally excluded from reference-image +comparisons because its time-driven flicker tables make captures from separate +runs non-comparable. No authored stock-map use of the shipped blend-light +declarations has been identified, so this controlled stock-declaration scene is +the explicit blend qualification route. + +The 2026-08-21 development-worktree run used the staged Windows x64 runtime, +bordered/windowed 1280x720 output, and only the engine's registered `screenshot` +command. OpenGL and Vulkan both passed the following acceptance set: + +- The mixed fog-plus-blend shared/classic engine TGAs match exactly at RMS `0`, + maximum delta `0`, and zero differing RGB channels. The OpenGL shared image is + SHA-256 `2364ae2cb2d3a779662d7a3bff1787e1cf00b160eaf0e778691cceeb8faef6ec`; + the Vulkan image is + `1d1e7eea9562bf8b79926d7f6ba9cf612543e48c14cd977825a34ba3ab6c45d5`. +- Each mixed run owns one ready view containing two lights (one fog and one + blend), six reconciled GLOBAL receivers, two active stages, seven drawable + primitives, three fog receivers, one fog cap, and three blend draws. OpenGL + reports domain/view hashes `6a8bd7fc8e02ab52` / `9d4a3224d67600ea`; + Vulkan reports `2bd11c573a8ab38f` / `ed0bb8aac24c2771`. Backend mismatch, + duplicate, and untracked counters remain zero. +- With `r_skipBlendLights 1`, the shared fog-only result still matches its + same-settings classic TGA exactly on both backends. It owns three fog + receivers and one fog cap, records the blend light as one sealed no-op, and + commits zero blend draws. The OpenGL and Vulkan image SHA-256 values are + `e0be5e2cdf19a72eae8895c8036bf80b734e6a980917eb27bf1b6e06dd7c0557` + and `81c84e165de0bafd33afaa98e24f8f73d64f9d574bf1d08cacaf7bd61669de45`. +- The mixed result differs materially from that fog-only reference on both + backends at RMS `36.7624`, maximum delta `71`, and `1,921,110` changed RGB + channels, proving the three owned blend draws affect the final image. +- The fog-only result differs materially from the matching established + `r_skipFogLights 1` outer-phase skip at RMS `61.3216` and maximum delta `162`: + `2,764,380` RGB channels change on OpenGL and `2,764,369` on Vulkan. This + comparison intentionally uses the classic outer skip because the established + `r_skipFogLights` behavior suppresses the complete fog/blend phase. +- The intentional `r_singleTriangle 1` blocker reports + `failure=unsupported-state detail=200`, one complete-phase fallback, zero + committed shared content counters, and exact same-settings classic TGA parity + on both backends. The active backend alone records the fallback; the inactive + backend remains zeroed. +- Logs report no OpenGL error, framebuffer failure, Vulkan validation warning, + VUID, Vulkan call failure, fatal error, or engine error line. + +Generate and consume separate classic references for OpenGL and Vulkan. A +same-looking or cross-backend screenshot without exact ownership diagnostics is +not sufficient. Earlier Vulkan Phase G fog evidence proves the established +Vulkan fog implementation, not this shared transaction, and cannot close this +gate. + +The retained local reports are under +`.tmp/renderer-gameplay/fog-blend-{gl,vk}-{classic-stable-pass,owned-stable-qualified,classic-skip-stable,owned-fog-delta-qualified,classic-skipfog-pass,classic-rollback,shared-rollback-qualified}`. +They bind the staged runtime and dirty source revision used for this development +qualification. + +## Remaining release evidence + +A separate authored-stock fog case must retain a fixed camera in a map such as +`game/storage2` or `mp/q4dm10`, exact same-backend shared/classic parity, +nonempty fog receiver/cap ownership, a material fog-on/off delta, and clean +Vulkan validation. Clean committed-source recapture, a freshly staged final +package, retained human review, and target-platform/driver coverage also remain +required before the default-off option can be promoted. + +## Milestone D completion and release-promotion boundary + +Root GUI, world ambient/material, fixed-classic interaction, and fog/blend are +now four independently guarded complete shared domains with retained local +runtime qualification. They do not make the complete classic frame modern, and +their clean-package/platform release-promotion gates remain separate. + +The independent default-off +[material-deform contract](classic-deform-domain-modernization.md) now records +the classic source-preserving fog-receiver role explicitly. A material-deform +receiver is admissible only with a fresh `notApplicable` record; unsupported, +skipped, failed, stale, or mismatched records still reject the complete phase. + +The completed cinematic/authored-post corridor seals eligible root video/audio +views and complete post tails while preserving the established dynamic-stage +executor. Eligible authored-post tails inside a sealed special-view tree now +complete and publish with that tree's root transaction; see [Shared Classic +Cinematic and Authored-Post +Transaction](classic-cinematic-post-domain-modernization.md) and [Shared +Special-Subview Transaction](classic-subview-domain-modernization.md). +Render-demo and Raven special-frame ownership have a dedicated +[transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation. They do not make every classic-frame category or the aggregate +modern-visible renderer shared-owned. Authored-stock, clean-package, +human-review, and target-platform/driver evidence remain release-promotion +gates; temporal presentation is the next roadmap milestone. diff --git a/docs/dev/classic-gui-domain-modernization.md b/docs/dev/classic-gui-domain-modernization.md new file mode 100644 index 00000000..9bcc90bd --- /dev/null +++ b/docs/dev/classic-gui-domain-modernization.md @@ -0,0 +1,190 @@ +# Shared Classic 2D GUI Domain + +## Status + +The first Milestone D classic-frame domain is **Implemented (default-off; local +Windows OpenGL/Vulkan implementation evidence recorded; final-package and +platform release qualification pending)** for complete fixed-function 2D GUI +views. It is now joined by +the separate [shared in-world GUI domain](classic-inworld-gui-domain-modernization.md), +[shared world ambient/material domain](classic-world-ambient-domain-modernization.md) +and [shared interaction-lighting domain](classic-interaction-domain-modernization.md), +plus the [shared fog/blend domain](classic-fog-blend-domain-modernization.md). +OpenGL and Vulkan +consume the same ordered, per-draw evaluated material records. The established +classic view remains the supported default and the whole-view rollback. + +This is deliberately a complete domain, not a single-texture GUI shortcut. A +view is shared-owned only when every source surface and every ambient material +stage can be represented, resolved, preflighted, and submitted in source order. +One unsupported surface, stage, image, register, state, cache, descriptor, or +pipeline keeps the complete view on the classic backend walker. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Domain boundary + +The initial domain covers root generated 2D `RC_DRAW_VIEW` work on the ordinary +backbuffer. This includes eligible static menu, console, HUD, loading, and other +fullscreen GUI views whose material stages use the classic fixed-function +ambient contract. + +The shared path preserves: + +- original surface and repeated-stage order, including repeated image semantics; +- exact stage conditions and evaluated RGBA values; +- 2x3 texture matrices, including classic large-scroll translation wrapping; +- blend factors, color/depth write masks, depth comparison, culling, and legacy + fixed-function alpha comparison state that the classic color walker executes; +- explicit texgen, vertex-color modes, material/private polygon offset, image + sampler state, geometry, model-view/projection matrices, and scissors; +- inactive and recognized framebuffer no-op stages as ordered diagnostic + records rather than silently deleting them. + +The first domain intentionally rejects world/in-world GUI views, subviews, +mirrors, editor or render-demo mutations, offscreen render targets, weapon/model +depth hacks, suppress-in-subview materials, decal-color streams, decal-sorted +materials, current-render/depth images, cinematics, +dynamic images, custom programs, post-process sorting, combined material/private +polygon offsets, authored depth/coverage `alphaTest`, +non-explicit texgen, missing/default/unloaded +images, unsupported backend state, and every bounded-pool or resource failure. +Those cases execute the untouched classic view. + +## Shared record flow + +1. Front-end scene packets identify materials actually referenced by a GUI pass. + Intrinsic material class does not decide eligibility, so a material first seen + in the world can still be compiled correctly when a 2D view uses it. +2. `MaterialResourceTable` compiles each referenced material into a bounded + ordered authored-pass span. Repeated semantic bindings remain distinct, and + each stage receives an opaque generation-checked texture-resource id. +3. `ClassicGuiDomain` walks the original view surfaces transactionally, matches + drawable surfaces to GUI draw packets in source order, evaluates the authored + span once against that draw's bounded shader-register block, resolves every + opaque image binding, and hashes the stable evaluated contract. +4. OpenGL and Vulkan preflight the complete prepared view before the first + framebuffer-affecting command. Each backend then submits only `DRAW` + dispositions, accounts for every inactive/no-op record, and records one + owned or fallback outcome for the view. + +Preparation retains no partial view. A failed transaction rolls back its draw +and evaluated-pass arenas and publishes only a precise fallback reason. + +## Backend execution + +The OpenGL adapter uses the classic fixed-function state surface, but it does +not read `shaderStage_t` or raw shader registers. Cache availability, texture +identity and sampler state, matrix/scissor state, alpha comparison, blend/depth/ +cull/write state, vertex colors, polygon offset, and capacity are all checked +before `RB_BeginDrawingView` commits the view. + +The Vulkan adapter similarly consumes only the sealed records. It validates an +exact pipeline-cache key, prepares descriptors and geometry offsets, and checks +the active swapchain render scope before recording the first indexed draw. It +never accepts the older pipeline-cache behavior that could substitute a +differently keyed pipeline after cache exhaustion. Alpha comparisons not +expressible by the current GUI shader, and authored `alphaTest` stages whose +classic meaning belongs to the separate depth/coverage pass, cause complete +classic rollback. + +Neither backend can return to the classic walker after its first visible draw; +an unexpected post-commit coverage-report failure is diagnostic only, preventing +double rendering. + +## Control and diagnostics + +`r_rendererSharedGui` is the single cross-backend control: + +- `0` (default): every 2D view uses the established classic backend path; +- `1`: an eligible complete view may use the shared domain; any rejection uses + the complete classic view. + +The setting is independent of `r_rendererModernVisible`, allowing Vulkan and +OpenGL to exercise the same domain without enabling the larger hybrid-lighting +experiment. Baseline and benchmark tools explicitly force it off unless a run +is intentionally collecting shared-domain evidence. + +`gfxInfo` reports prepared/valid state, ready and fallback view counts, source +and no-op surfaces, evaluated/drawable/inactive/active-no-op pass counts, a +deterministic contract hash, and per-backend owned/fallback counts. Material +resource diagnostics report referenced/eligible/fallback records, pass-pool +use, the first material/stage failure, and stale opaque resource rejection. + +Use these renderer commands for focused validation: + +```text +rendererContractsSelfTest +rendererMaterialResourceTableSelfTest +rendererClassicGuiDomainSelfTest +gfxInfo +``` + +The dependency-light contract test covers per-draw bounded register evaluation, +non-finite data, malformed enums, overflow, atomic destination reset, repeated +order, alpha comparison, and every inactive/no-op disposition. The material +self-test covers repeated same-semantic stages, packet-derived admission, +generation-safe texture ids, state translation, explicit alpha/decal/post-process +rejection, and atomic pass-pool exhaustion. The domain self-test covers its +bounded ranges, stable hashing, and diagnostic vocabulary. The static +`renderer_classic_gui_domain.py` contract locks down the transactional +preparation and preflight-before-commit structure and prohibits raw material or +register reads in either shared backend consumer. Runtime screenshot and +ownership capture remains the end-to-end proof of whole-view rollback. + +Runtime promotion evidence must use the engine `screenshot` command in a +bordered window. Capture the same static menu/loading/HUD domain with the option +off and on for both OpenGL and Vulkan, retain the domain hash and ownership +diagnostics, and exercise at least one unsupported material/view that reports a +whole-view fallback with no mixed or dropped-stage count. + +Local implementation evidence collected on 20 August 2026 used the stock +`game/storage1` scene at 1280x720 in a bordered window. The benchmark harness +passed with the option both off and on for OpenGL and Vulkan and retained an +engine TGA for each run. With the option on, the final sampled frame reported +one complete view owned and six complete views on explicit classic fallback on +each backend; earlier sampled frames reported two owned views. The off runs +reported no prepared shared views. Visual inspection of all four engine captures +showed the complete stock scene and crosshair. These are development-worktree +results, not clean-package or cross-platform release-promotion evidence. + +## Milestone D completion and release-promotion boundary + +This domain meets Milestone D's first complete-domain implementation gate; it +does not make the whole classic frame modern. The second complete-domain +implementation now covers eligible ambient-only 3D world views behind its own +default-off setting, established depth prerequisite, pre-fog/post-fog split, +and whole-view rollback. Stock `maps/tools/mv2` option-off/on engine captures +now match exactly on GL and Vulkan, and a stock deform override proves named +zero-draw whole-view fallback on both backends; see +[Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md). + +Eligible unshadowed and shadow-coupled fixed-classic interaction lighting now +form the expanded third complete default-off shared domain with whole-view +rollback. Its shadow corridor covers stencil volumes, projected/CSM/parallel +maps, point cubes, mixed and same-light hybrid ownership, dynamic/perforated +mapped casters, and named atomic translucent-moment fallback. The fourth +complete domain now transactionally owns eligible fog/blend phases on both +backends behind its own default-off setting; native/static and controlled +GL/Vulkan runtime qualification pass, while authored-stock and release +promotion remain open. The independent default-off +[material-deform contract](classic-deform-domain-modernization.md) now lets this +domain consume a sealed completed or intentional-empty CPU deform result; an +unsupported, skipped, failed, stale, or mismatched record still rolls the +complete GUI view back before drawing. + +Subview, cinematic/authored-post, and render-demo/Raven special-frame ownership +now have dedicated default-off transactions. Eligible authored-post tails +inside a sealed special-view tree keep their own semantic records but publish +or roll back atomically with that tree; see [Shared Classic Cinematic and +Authored-Post Transaction](classic-cinematic-post-domain-modernization.md), +[Shared Special-Subview Transaction](classic-subview-domain-modernization.md), +and [Shared Render-Demo and Raven Special-Frame +Transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation. They do not make the aggregate `r_rendererModernVisible` path +or every classic-frame category shared-owned. Final-package, human-review, and +target-platform/driver promotion evidence remains separate; temporal +presentation is the next roadmap milestone. diff --git a/docs/dev/classic-interaction-domain-modernization.md b/docs/dev/classic-interaction-domain-modernization.md new file mode 100644 index 00000000..518d0820 --- /dev/null +++ b/docs/dev/classic-interaction-domain-modernization.md @@ -0,0 +1,341 @@ +# Shared Classic Interaction-Lighting Domain + +## Status + +The fixed-classic interaction transaction is **Implemented (default-off; +native/static and controlled local OpenGL/Vulkan qualification recorded; stock, +final-package, and platform release promotion pending)**. It contains two +complete Milestone D ownership corridors behind the +`r_rendererSharedWorldInteraction 1` setting: + +- the previously completed unshadowed interaction corridor; and +- shadow-coupled interaction ownership for classic stencil volumes, projected + single-map and CSM/parallel shadows, point-light cube maps, mixed mapped and + stencil lights, and same-light mapped-plus-stencil supplements. + +Together with shared root 2D GUI and world ambient/material ownership, this is +the expanded third complete shared classic-frame domain. It is now joined by +the separate [shared fog/blend domain](classic-fog-blend-domain-modernization.md). +OpenGL and Vulkan +consume the same backend-neutral sealed light, receiver, +interaction-primitive, shadow-caster, and mapped-pass contract. The established interaction renderer remains the +supported default and the whole-view rollback. + +This is a complete interaction-phase ownership boundary for an accepted view, +not a claim that the complete frame or every Quake 4 lighting feature is shared. +Fog/blend lights, custom lighting programs, unsupported receiver geometry, +translucent moment-map casters, special views, and any incomplete resource or +backend transaction leave every interaction in the view on the untouched +classic path. Ambient/material, fog/blend, GUI, subview, and post work retain +their existing owners. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Why the boundary is whole-view + +An idTech 4 interaction is not one material draw. Each contributing light +multiplies its active light stages by an ordered bump/diffuse/specular surface +decomposition. One surface can occur under multiple lights and in local, +global, or translucent receiver chains. Stencil receivers additionally depend +on ordered global/local volume families, while mapped receivers depend on a +complete per-light map and may require same-light stencil supplements. A partial +handoff can therefore draw a receiver twice, omit a contribution, sample an +incomplete map, or apply a different stencil state. + +The shared domain accepts or rejects the entire interaction contribution of one +ordinary root 3D view: + +- every non-fog/non-blend light with material interactions; +- every local, global, and translucent receiver in classic order; +- every active light stage and decomposed surface interaction; +- every draw and no-op disposition; +- every stencil-volume, mapped-caster, alpha-stage, and supplement record; +- every mapped receiver pass and its sealed projected or point state; and +- exact light, surface, primitive, shadow, map, resource, and stable-hash + accounting. + +If any part cannot be represented, no shared record from that view is published. +If backend preflight cannot retain the complete plan, that backend records one +named whole-view fallback and performs zero shared main-target draws. + +## Packet identity and sealed records + +Interaction packets carry the source `viewLight_t`, light ordinal, receiver +class, receiver ordinal, and global interaction source ordinal. Shadow packets +carry the source light, light ordinal, caster class, chain ordinal, and source +ordinal. Preparation verifies these identities while walking the original +light, receiver, stencil, and shadow-map chains. This distinguishes the same +surface under two lights and proves that no receiver or caster was reordered or +silently dropped. + +`ClassicInteractionDomain` prepares bounded frame-local arenas for: + +- views, including packet/pass identity, shadow mode, expected ranges, + readiness, fallback detail, semantic hash, and per-backend outcomes; +- lights, including source order, receiver spans, interaction spans, exact + shadow-chain ranges, mapped-pass indices for LOCAL and GLOBAL receivers, and + the physical stencil work implied by LOCAL -> GLOBAL -> TRANSLUCENT order; +- receiver surfaces and interaction primitives, including packet/material/ + geometry/instance identity, scissor/state, images, projections, matrices, + evaluated colors, vertex-color mode, and explicit no-op disposition; +- stencil and supplement casters, including selected classic volume geometry, + cap/preload choice, depth bounds, scissor, cull, transforms, and draw/no-op + disposition; +- mapped casters and perforated alpha stages, including static/dynamic and + GLOBAL/LOCAL chain identity, ambient geometry, texture transform, alpha + comparison, hashed-alpha policy, and generation-checked image identity; and +- mapped passes, including receiver/resource-owner identity, map and supplement + counts, resource alias, plan/generation diagnostics, cache/update/scratch + policy, projected single-map or CSM state, point-cube state, filter/bias + policy, completeness masks, and a backend-independent semantic hash. + +The semantic map hash deliberately ignores backend allocation identity such as +the resource plan id and generation, while remaining sensitive to mapped and +supplement caster coverage, dynamic/alpha state, completeness, class, cascade, +filter, and bias semantics. Backend consumers do not reinterpret authored +material stages or reread shader registers after publication. Retained legacy +pointers are sealed geometry/resource bridges only. + +## Exact classic interaction and shadow order + +Preparation mirrors the fixed-classic interaction rules once for both +backends: + +- light-stage order and LOCAL, GLOBAL, then TRANSLUCENT receiver order; +- condition-register handling with checked finite register access; +- bump-stage flushes, repeated diffuse/specular flushes, and the final flush; +- surface-color clamping and light-color multiplication using the view's + classic light scale; +- texture-translation wrapping outside the classic `+/-40` range; +- black and flat-normal substitution, ambient-light specular suppression, + vertex-color semantics, and explicit no-op records; and +- exact shadow mode per receiver: `NONE`, `STENCIL`, `PROJECTED`, `POINT`, or + `HYBRID`. + +Stencil work is planned as physical receiver-order submission rather than as a +unique-caster count. A mode transition clears and rebuilds the required family. +For example, with one caster in each full/supplement GLOBAL/LOCAL chain, +`STENCIL -> HYBRID -> STENCIL` plans five logical volume submissions and three +preloads; `HYBRID -> HYBRID -> HYBRID` reuses the prepared family and plans two +logical submissions and one preload. Both backends reconcile these exact totals +after drawing. + +Mapped passes reconcile the exact `MAP_GLOBAL_STATIC`, `MAP_GLOBAL_DYNAMIC`, +`MAP_LOCAL_STATIC`, and `MAP_LOCAL_DYNAMIC` ranges. A `HYBRID` receiver also +requires complete `SUPPLEMENT_GLOBAL` and, where GLOBAL ownership requires it, +`SUPPLEMENT_LOCAL` ranges. LOCAL and GLOBAL receivers carry explicit +`shadowMapPassIndex` values; a compatible GLOBAL resource alias must name and +match its LOCAL owner exactly. Point maps always expose six faces. Projected +passes distinguish one-map projection from multi-cascade parallel/CSM state. + +## Eligibility and whole-view fallback + +The shared corridor admits ordinary root fixed-classic interaction views with: + +- no shadows, classic stencil volumes, mapped projected/parallel/CSM lights, + mapped point lights, mixed per-light modes, or complete hybrid supplements; +- static and dynamic opaque mapped casters, including authoritative CPU-skinned + `idDrawVert` streams; +- perforated/alpha-tested mapped casters with sealed alpha stages; +- exact cache reuse, cache update, or bounded non-cacheable scratch resources; + and +- fixed-classic receiver primitives that both backends can preflight in full. + +Preparation or backend preflight rejects the complete view for any of these +conditions: + +- translucent moment-map casters, incomplete mapped or supplement chains, a + stale/signature-mismatched resource, an invalid alias, or a map admission/ + allocation/update failure; +- fog/blend lights inside the interaction pass; +- GPU-palette skinning, custom/new-style GLSL lighting, parallax, + enhanced-material, cel, flat- + diffuse, simple/test, or other alternate interaction modes; +- unsupported or unsealed material-deform output, generated/deformed geometry, + GPU-palette-skinned, packed, primitive-batch, missing-cache, depth-hack, + negative-scale, or otherwise unsealed receiver geometry; +- subview, mirror, x-ray, editor, offscreen, render-demo, global-material, or + other non-root view ownership; +- cinematic, defaulted, unloaded, unbound, or invalid image resources; +- out-of-range/non-finite registers or derived values; +- packet/light/receiver/material/geometry/instance identity mismatch; +- bounded arena, geometry, descriptor, uniform, cache, atlas, or scratch + overflow; or +- any backend program, target, resource, or transaction-preflight failure. + +Fallback is decided before the first shared main-framebuffer write. Shadow-map +preparation may write only retained backend-private cache/scratch resources; a +failure restores or abandons those reservations and leaves the classic visible +path untouched. After ownership commits, an unexpected coverage mismatch is a +diagnostic fault and the classic interaction stream is never layered over the +partially submitted shared result. + +## Backend execution + +OpenGL preflights the complete interaction view inside the ARB2 interaction +slot. It retains all interaction, volume, mapped-caster, and perforated-alpha +geometry; reserves cache or scratch resources for every non-aliased mapped +pass; renders projected/CSM and point-cube maps from sealed caster chains; then +revalidates texture handle, storage generation, render target, dimensions, +atlas region, signatures, and physical aliasing for every pass. Failure restores +the main target and GL state before the classic walker is selected. Commit binds +the retained projected or point resource, draws mapped receivers, and rebuilds +stencil/supplement families only when receiver order requires it. +Cache allocation during one sealed schedule never evicts a map prepared earlier +in that same transaction; excess projected or point work uses the bounded +scratch policy instead. + +Vulkan snapshots its shared-geometry and uniform cursors before mapped +preflight. The shadow transaction reserves scheduler/cache/atlas/point-cube +resources, retains every mapped caster and alpha descriptor, builds projected +or point receiver blocks, and records the complete command plan without an +attachment write. Failure aborts shadow reservations and restores both cursors. +Commit occurs before any shared main-target interaction write, renders the +retained maps, binds the mapped receiver pipeline/descriptor set, and executes +the same receiver-order stencil/supplement plan. Projected single-map, +parallel/CSM, point cube, cache hit/update, scratch/non-cacheable, static, +dynamic, and perforated paths are included. + +Both backends report ownership only when drawable/no-op primitives, +drawable/no-op shadow casters, physical logical/preload volume submissions, +mapped passes, and hybrid passes exactly match the shared view. + +## Controls and diagnostics + +- `r_rendererSharedWorldInteraction 0|1` controls both interaction corridors + and defaults to `0`. +- `rendererClassicInteractionDomainSelfTest` exercises bounded publication, + semantic hashing, mapped coverage, duplicate reporting, coverage mismatch, + atomic rewind, and receiver-order stencil planning. +- `gfxInfo` aggregate and view lines report + `shadow=L/C/D+N volumes=V+P maps=total+hybrid + modes=projected+point csm=passes`, plus exact GL/Vulkan + `draw+noop/shadow+shadowNoop+logical+preload/maps+hybrid` outcomes. +- Per-map diagnostics report view, light, LOCAL/GLOBAL receiver, shadow mode, + light class, cascade count, alias, plan id, generation, mapped+supplement + caster counts, `features=static+dynamic+alpha+translucent`, and semantic hash. + +The setting participates in renderer default-safety reporting. Unrelated stock +baseline, gameplay benchmark, and startup profiles force it off. + +## Validation contract + +Automated native and static coverage proves: + +- multi-light packet identity and all receiver/caster chain identities; +- exact primitive, map, supplement, no-op, and physical stencil reconciliation; +- projected single-map, parallel/CSM, point-cube, mixed, and hybrid sealed state; +- semantic-hash stability across plan/generation changes and sensitivity to + caster, supplement, dynamic, alpha, completeness, filter, and bias changes; +- static/dynamic and opaque/perforated caster preparation; +- cache reuse/update, projected/point scratch resources, exact aliases, and + resource revalidation; +- GL and Vulkan preflight/commit/abort ordering before visible ownership; +- atomic named fallback with zero committed backend counters and no shared + main-target draw; and +- default-off isolation plus direct CI registration. + +Runtime qualification uses only the engine's registered `screenshot` command +in a bordered/windowed 1280x720 run. The `interaction` profile creates a +controlled stock `maps/tools/mv2` scene with a fixed camera, stock crate, one +projected test light, and one point test light. Its five cases are: + +- unshadowed ownership; +- stencil ownership; +- projected plus point mapped ownership; +- projected-map plus point-stencil mixed ownership; and +- a constrained map-update budget that must produce one named atomic backend + fallback with zero committed ownership counters and no shared main-target + draw. + +The `interaction-shadow-stock` profile is the stock-map release-qualification +set for projected, point, CSM/parallel, dynamic mapped-caster, +perforated/cutout, same-light hybrid-supplement, and translucent-moment fallback +coverage. Its scene entries are candidates, not proof by label: each final run +must retain the sampled six-component `viewpos`, and every owned frame must have +nonzero, exactly reconciled backend counters plus one valid diagnostic record +per mapped pass. The projected target requires an actual single-map +`class=projected` record; the ordinary `game/airdefense2` point candidate must +report a six-face point record; dynamic and perforated targets require their +explicit feature bit; the hybrid target requires a nonzero supplement and +physical volume submission; and the translucent-moment target must fall back +atomically. + +The controlled test lights do not form a real CSM case: enabling the CSM preset +there still produces only a single projected map. CSM acceptance therefore +belongs to a retained stock multi-cascade projected/parallel view; the current +`shadow-csm-airdefense1` entry remains fail-closed until its final camera reports +a nonzero multi-cascade record. + +For each supported owned shadow setting, compare shared off/on under identical +settings and require the engine TGA to match the classic reference. Separately +compare the shadowed result with its shadows-off engine TGA and require a +material image difference; an identical shadows-on/off image is not acceptable +shadow evidence. Every intentional fallback must match the same-settings +classic TGA exactly. No operating-system screen capture is permitted. + +## Retained local evidence and promotion boundary + +The earlier two-light unshadowed `maps/tools/mv2` capture and its old +shadows-enabled rollback remain historical development evidence only. The +current controlled profile adds a second crate plus projected and point test +lights, so its five-case capture below supersedes those older counts and hashes. + +The completed controlled shadow corridor also passed locally on 2026-08-21: +all five OpenGL cases and all five Vulkan cases passed from the staged +development worktree. Shared-off/on engine TGAs matched exactly (`RMS 0`, +maximum channel delta `0`) for unshadowed, stencil, mapped, mixed, and +map-budget fallback. Stencil, mapped, and mixed captures were materially +different from their shadows-off references: OpenGL recorded RMS deltas +`5.5301`, `5.4817`, and `5.5313`; Vulkan recorded `5.5613`, `5.5625`, and +`5.5625`. Enabled diagnostics reconciled nonzero casters, physical stencil +work, projected and six-face point maps, and mixed ownership; the constrained +budget cases recorded one named backend fallback with zero committed ownership +counters and no shared main-target draw. +Private map preflight/cache/scratch work may occur before that decision; the +evidence proves zero committed ownership counters and no shared main-target +draw. + +The controlled and stock profiles above remain the authoritative release +runtime acceptance set. Every stock-profile case still requires fixed-camera +and reference qualification, including projected, point, CSM/parallel, dynamic, +perforated, hybrid, and translucent-moment fallback. Clean committed-source +recapture, a freshly staged final package, target-platform coverage, and driver coverage are still required +before the default-off option can be promoted. Retain reports, +logs, engine screenshots, image hashes/deltas, exact per-map records, final +poses, and Vulkan validation results with that release evidence. + +## Milestone D completion and release-promotion boundary + +The shared root GUI, world ambient/material, fixed-classic interaction, and +fog/blend corridors are four independently guarded complete domains. The +interaction domain admits both unshadowed and shadow-coupled eligible views; +the fog/blend domain owns a complete eligible phase independently because its +compositing order and rollback boundary differ from interaction lighting. +Native/static and controlled GL/Vulkan fog/blend qualification pass, while +authored-stock and release promotion remain open. These domains do not make the +whole classic frame modern. + +The independent default-off +[material-deform contract](classic-deform-domain-modernization.md) now seals +both sides of the classic interaction behavior: mapped casters require their +finalized completed or intentional-empty CPU result, while receivers explicitly +retain source geometry through the `notApplicable` role. Unsupported, skipped, +failed, stale, or mismatched records still reject the complete interaction view. + +The completed cinematic/authored-post corridor seals eligible root video/audio +views and complete post tails while retaining the established dynamic-stage +executor. Eligible authored-post tails inside a sealed special-view tree now +complete and publish with that tree's root transaction; see [Shared Classic +Cinematic and Authored-Post +Transaction](classic-cinematic-post-domain-modernization.md) and [Shared +Special-Subview Transaction](classic-subview-domain-modernization.md). +Render-demo and Raven special-frame ownership have a dedicated +[transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation. They do not make every classic-frame category or the aggregate +modern-visible renderer shared-owned. Stock-scene, clean-package, human-review, +and target-platform/driver evidence remain release-promotion gates; temporal +presentation is the next roadmap milestone. diff --git a/docs/dev/classic-inworld-gui-domain-modernization.md b/docs/dev/classic-inworld-gui-domain-modernization.md new file mode 100644 index 00000000..1775be38 --- /dev/null +++ b/docs/dev/classic-inworld-gui-domain-modernization.md @@ -0,0 +1,98 @@ +# Shared Classic In-World GUI Domain + +## Status + +In-world GUI ownership is **Implemented (default-off; native/static +implementation evidence recorded; runtime, final-package, and platform release +qualification pending)** as a Milestone D corridor. +`r_rendererSharedInWorldGui 1` can promote the complete +set of eligible GUI quads generated by `R_RenderGuiSurf` for one ordinary 3D +view. OpenGL and Vulkan consume the same sealed evaluated material records; +one source, packet, material, resource, target, capacity, or backend rejection +leaves every generated GUI quad for that view on the established classic path. + +This is a bounded ownership domain, not a sort-based attempt to take arbitrary +world materials. Root 2D GUI remains independently controlled by +`r_rendererSharedGui`, while ambient, interaction, fog/blend, subview, +cinematic, and post domains keep their existing owners. + +The implementation is original openQ4 work. It incorporates no external code +and changes no stock Quake 4 asset. + +## Ownership boundary + +`R_RenderGuiSurf` marks only the generated output emitted while it expands a +GUI onto a 3D surface. The marker is front-end provenance, not a material-sort +guess, so root GUI and ordinary `SS_GUI`/`SS_PREGUI` world materials cannot be +claimed accidentally. + +An admitted view must be an ordinary root 3D view with a render world and no +subview, mirror, x-ray, editor, render-demo, clip-plane, target-feedback, or +debug mutation. Each tagged source must retain its matching world-category GUI +packet, sealed final geometry, instance, material, register, and texture +records. The domain uses the material table's world pass stream, preserving the +classic 3D depth contract rather than the root-2D depth-disabled convention. + +The first corridor deliberately retains complete classic fallback for weapon or +model depth hacks, decal-color streams, negative scale, suppressed/subview or +post-process material behavior, current-render/depth images, cinematics, +dynamic images, custom programs, non-explicit texgen, unsupported state, +missing/default/unloaded resources, render targets, and bounded-pool failures. + +## Submission and rollback + +1. Scene packet generation adds a world-category `RENDER_PASS_GUI` stream only + when the option is enabled and tagged geometry exists. Empty ordinary world + scenes receive no synthetic GUI pass. +2. `ClassicGuiDomain` transactionally matches the tagged source subset to that + stream in original order, evaluates every ordered pass once, resolves each + opaque texture resource, and publishes no partial draw/pass range on failure. +3. Each backend preflights the full subset before its first visible submission. + It commits at the pre-ambient boundary, after classic lighting and before + the normal ambient walks, using the matching 3D depth state. +4. Only a backend-recorded owned subset is skipped by that backend's classic + ambient walker. A rejection records fallback and the untouched walker draws + the complete tagged subset normally. + +The result prevents both partial ownership and double submission. `gfxInfo` +reports separate root and in-world GUI request/view counts together with the +existing domain hash, readiness, fallback, and backend coverage diagnostics. + +## Controls and validation + +```text +r_rendererSharedInWorldGui 0 // default, established renderer path +r_rendererSharedInWorldGui 1 // eligible tagged in-world GUI subset +rendererClassicGuiDomainSelfTest +rendererMaterialResourceTableSelfTest +gfxInfo +``` + +Baseline and benchmark profiles force the option to `0`. The static +`renderer_classic_gui_domain.py` contract verifies provenance tagging, +world-category packet admission, depth-aware material compilation, both +backend preflight-before-commit handoffs, duplicate suppression, and default +rollback wiring. + +Runtime promotion still requires engine-render-target `screenshot` captures in +a bordered window: compare the same stock in-world GUI view with the option off +and on on OpenGL and Vulkan, retain ownership/hash output, and force one named +tagged-subset fallback. Those captures, clean-package recapture, and +platform/driver qualification remain separate from this implementation gate. + +## Milestone D completion and release-promotion boundary + +The cinematic/authored-post corridor owns eligible complete root video/audio +views and authored-post tails behind its own default-off setting. Eligible +authored-post tails inside a sealed special-view tree now complete and publish +with that tree's root transaction; see [Shared Classic Cinematic and +Authored-Post Transaction](classic-cinematic-post-domain-modernization.md) and +[Shared Special-Subview Transaction](classic-subview-domain-modernization.md). +Render-demo and Raven special-frame ownership have a dedicated sealed +[transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation. They do not make every classic-frame category or the aggregate +modern-visible renderer shared-owned. Runtime, clean-package, human-review, and +target-platform/driver evidence remain release-promotion gates; temporal +presentation is the next roadmap milestone. diff --git a/docs/dev/classic-special-frame-domain-modernization.md b/docs/dev/classic-special-frame-domain-modernization.md new file mode 100644 index 00000000..035c4579 --- /dev/null +++ b/docs/dev/classic-special-frame-domain-modernization.md @@ -0,0 +1,103 @@ +# Shared Render-Demo and Raven Special-Frame Transaction + +## Status + +This Milestone D corridor is **Implemented (default-off; native/static +implementation evidence recorded; runtime, final-package, and platform release +qualification pending)**. +`r_rendererSharedSpecialFrame 1` gives OpenGL and Vulkan the same sealed +ownership boundary for two otherwise exceptional classic-frame paths: + +- a complete 3D frame being played from the active render-demo session stream; + and +- an exact `RC_DRAW_SPECIAL_EFFECTS` Raven controller command, including its + later blur composite and projected AL-light work. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Transaction boundary + +A negative `viewID` is not sufficient evidence of render-demo playback: the +portal-sky camera uses that convention too. Front-end scene packets therefore +mark a render-demo view only when the live `session->readDemo` stream owns the +same render world. The record then seals the ordinary root-view identity, +demo-version state, full source surface count, exact packet associations, and +depth, interaction, and ambient pass presence. + +Raven special effects are command-only work. Their packet preserves the exact +controller view and the admitted `SPECIAL_EFFECT_BLUR` / `SPECIAL_EFFECT_AL` +mask from `RC_DRAW_SPECIAL_EFFECTS`. The visible work can finish later: OpenGL +reports the blur composite and actual AL lights after their established paths +complete; Vulkan reports its normal or resolve-backed completion after its +pending controller is consumed. Both bits must complete before the record is +owned. + +Neither route replaces any mature rendering behavior. A render-demo record +dispatches the complete normal 3D view executor. Special effects retain their +normal renderer-resource preparation, depth/color/resolve handling, blur, and +projected-light paths. The shared transaction seals provenance and completion; +it is not a second demo decoder or a replacement BSE/effect renderer. + +## Eligibility and fallback + +Render-demo ownership accepts only a complete ordinary root 3D view from the +active session stream. Portal-sky, subview, mirror, x-ray, editor, clip-plane, +and malformed/partial packet cases are rejected. Special-frame ownership accepts +only the exact normal-root Raven controller command with a nonempty admitted +mask. Packet overflow, source mismatch, stale/missing session state, +unsupported view shape, incomplete effect output, resolve/capture state, or a +backend rejection leaves the whole item on the classic fallback path. + +Ownership is reported only after complete coverage: a demo must reconcile its +entire source-surface count, while Raven blur and AL coverage are accumulated +until they match the sealed mask. An unreported record is finalized as a named +classic fallback, so a partial shared report can never silently hide an +unfinished special effect. + +## Control and diagnostics + +`r_rendererSharedSpecialFrame` is independent of GUI, cinematic/post, world +ambient, interaction, fog/blend, subview, and deform ownership: + +- `0` (default): render-demo and Raven special-frame work use their normal + backend paths; +- `1`: an eligible sealed transaction may report OpenGL/Vulkan ownership after + the same complete executor finishes; all other work remains classic-owned. + +`gfxInfo` reports source scenes, demo/effect records, admitted effect mask, +ready/fallback views, semantic hash, and per-backend ownership, fallback, +coverage-mismatch, and duplicate-report totals. Focused dependency-light +validation is: + +```text +rendererScenePacketSelfTest +rendererClassicSpecialFrameDomainSelfTest +gfxInfo +``` + +`tools/tests/renderer_classic_special_frame_domain.py` guards exact session +provenance, portal-sky exclusion, controller-mask capture, source validation, +OpenGL/Vulkan completion handoff, conservative defaults, release-harness +isolation, diagnostics, and CI registration. + +## Release-qualification boundary + +Before promotion, retain engine-written `screenshot` captures—not OS +captures—for a stock render-demo and a stock Raven blur/AL scene with the +setting off and on, separately on OpenGL and Vulkan. Require nonzero reconciled +ownership where the content is eligible, exact same-settings output, an +incomplete-mask zero-commit fallback, clean backend diagnostics, and fresh +staged-package and target-platform/driver evidence. + +The special-subview transaction now covers direct mirror/clip-plane and +dynamic mirror/reflection/x-ray handoffs; see [Shared Special-Subview +Transaction](classic-subview-domain-modernization.md). Its eligible nested trees +also couple separately sealed authored-post tails to the same atomic root +completion and rollback decision. + +Together with the other independently guarded shared corridors, this completes +Milestone D's scoped implementation. It does not make every classic-frame +category or the aggregate modern-visible renderer shared-owned. Runtime, +clean-package, human-review, and target-platform/driver evidence remain +release-promotion gates; temporal presentation is the next roadmap milestone. diff --git a/docs/dev/classic-subview-domain-modernization.md b/docs/dev/classic-subview-domain-modernization.md new file mode 100644 index 00000000..4e2af90b --- /dev/null +++ b/docs/dev/classic-subview-domain-modernization.md @@ -0,0 +1,218 @@ +# Shared Special-Subview Transaction + +## Status + +The special-subview Milestone D corridor is **Implemented (default-off; +native/static and controlled Windows OpenGL/Vulkan nested-runtime acceptance +recorded; clean final-package, retained-review, broader-content, platform, and +driver release qualification pending)**. `r_rendererSharedSubview 1` lets an +eligible direct `SS_SUBVIEW` mirror publish its complete camera, clip-plane, +viewport, and scissor record and lets eligible remote-camera, mirror, +reflection, refraction, and x-ray +surfaces publish their exact child-scene-to-image capture transaction. The +capture edge preserves 2D or cubemap destination type, the exact cubemap face, +and color or depth aspect. Eligible nested special views seal their complete +parent/child tree and publish shared ownership only when its outermost special +view has completed. Eligible authored-post tails inside those views retain their +separate dynamic-stage records but join the same atomic root completion and +rollback decision. OpenGL and Vulkan consume the same sealed records. + +This seals a subview handoff, not its material/light implementation. Each child +scene still uses its established classic 3D walker, so it is not a claim that +child ambient, interaction, or fog work is shared-owned. The direct record +selects the complete established child-view executor; a capture record selects +only the exact transfer after that executor returns. When +`r_rendererSharedCinematicPost 1` is also enabled, an eligible child +authored-post tail may be sealed by its dedicated domain without transferring +the child's other material or lighting work. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Transaction boundary + +The front end emits a child `RC_DRAW_VIEW`, restores the parent view, and then +emits `RC_COPY_RENDER` for a capture-backed surface. A direct `SS_SUBVIEW` +mirror has no copy command: its child output is already composited into the +parent target. Both forms must therefore be recovered from immutable records, +not mutable global view state: + +1. the child scene packet retains its exact `viewDef`, parent source surface, + camera origin/axis, final culling parity, clip plane, viewport, scissor, + time, and packet index; +2. a capture record retains the child identity, destination image, source + rectangle, destination type/format, cube face, and color/depth mode when a + transfer exists; +3. admission reconciles the direct source with no capture, or a capture source + with exactly one full-viewport 2D/cubemap color or depth capture; +4. the backend reports direct ownership only after the complete classic child + view returns, or capture ownership only after the sealed transfer completes; +5. a nested child identifies its exact `superView` domain parent, the + depth-first child-before-parent scene-packet order, a bounded root/depth, + and every descendant in that root transaction; and +6. every admitted nested cinematic/post range identifies the same root and + records exact dynamic-stage coverage before any subview or post member + publishes shared ownership. + +OpenGL performs a direct child through the established `RB_DrawView` executor +or performs a capture transfer with `CopyFramebuffer`/`CopyDepthbuffer` sourced +from the sealed record. Vulkan similarly retains its established 3D executor +for a direct child and its image-copy implementation for a capture, creating +an exact-format six-layer depth target for a depth cubemap when required. A +changed camera, clip plane, viewport, scissor, image, destination type/aspect, +cube face, or copy rectangle never borrows the shared record: the normal +command executes on the classic path and the subview records a named fallback +instead. + +Nested work remains executed by the mature classic child-view executor and +copy implementation. The shared corridor does not substitute a partial child +material or lighting pass. Its additional guarantee is ownership atomicity: +if a nested source, camera, resource, transfer, or completion edge is rejected, +the whole sealed special-view tree remains on the established command path. +The same rule covers admitted authored-post tails. Their backend work may finish +first, but ownership stays unpublished until the outermost special view verifies +all descendants and dynamic ranges. A cinematic/post rejection returns the +whole special tree to classic ownership; a special-tree rejection marks every +nested cinematic/post range for that root fallback. + +## Eligibility and fallback + +The current corridor accepts an eligible 3D child view that has all of the +following: + +- a parent scene and source draw surface still present in that scene; +- either an `SS_SUBVIEW` direct mirror with no capture and exactly one sealed + clip plane, or exactly one `RC_COPY_RENDER` record matching the full child + viewport and a 2D/cubemap color/depth target with its legal exact face; +- for a capture, one matching parent dynamic stage: `DI_REMOTE_RENDER`, + `DI_MIRROR_RENDER`, `DI_REFLECTION_RENDER`, `DI_REFRACTION_RENDER`, or + `DI_XRAY_RENDER`; and +- the exact kind-specific camera/clip semantics: one portal plane for direct + mirrors, dynamic mirrors, and reflections; x-ray state with no portal plane; + and ordinary no-clip state for remote-camera/refraction, plus a loaded + destination image for a capture backend; and +- for nesting, an exact parent subview record for every `superView` link, a + strict descendant-before-parent packet order, and a fully admissible + parent/child subtree. Child completion is deferred until the root view has + completed every direct or capture-backed member; and +- when `r_rendererSharedCinematicPost 1` admits a child authored-post tail, a + ready cinematic/post record bound to the same special root, followed by exact + backend completion for every such dynamic range before root publication. + +Editor/render-demo/global-material views and every missing or +duplicate/mismatched parent, capture, material, semantic, resource, command +order, or capacity condition retain the established complete classic fallback. +A malformed or rejected nested member returns its complete root transaction to +that path; no admitted descendant remains reported shared-owned on its own. A +capture target with an unsupported texture type, illegal cube face, or +color/depth-aspect mismatch receives the named `unsupportedCaptureTarget` +fallback. Named nesting fallbacks distinguish a missing parent, +child-before-parent order failure, rejected parent/child tree, and incomplete +backend completion. `nestedCinematicPostFallback` identifies a dynamic-stage +rejection propagated to the complete special tree, while +`backendNestedCinematicPostIncomplete` identifies root finalization attempted +before all admitted dynamic ranges completed with exact coverage. The setting +cannot split a capture, a direct child, a nested post range, or a nested tree: +the root transaction consumes every sealed member or owns none of them. + +## Control and diagnostics + +`r_rendererSharedSubview` remains independently selectable from the GUI, +world-ambient, interaction, fog/blend, cinematic/post, and deform controls. +When both subview and cinematic/post ownership admit a nested tail, the two +domains deliberately share one completion and rollback boundary: + +- `0` (default): every direct and capture-backed subview uses the established + backend path; +- `1`: an eligible direct mirror or remote-camera/mirror/reflection/refraction/ + x-ray 2D/cubemap color or depth capture—including a fully admissible nested + tree and its separately admitted authored-post tails—may consume the sealed + transaction; all other subviews use the classic fallback. + +The stock-baseline and ordinary gameplay-benchmark harnesses explicitly set the +control to `0`. `gfxInfo` reports packet/capture counts, ready and fallback +views, direct mirror/remote/mirror/reflection/refraction/x-ray totals, +color-cubemap/depth-2D/depth-cubemap target counts, semantic hash, individual +capture rectangles/types/faces/aspects, parent/root/depth/subtree records, and +OpenGL/Vulkan ownership/fallback counters including nested-tree completion and +rollback totals. The cinematic/post `gfxInfo` line complements this with +`nested=/` and `nestedCinematic=` counts; ownership +must not appear there before the matching outer special-view transaction +finishes. + +Focused dependency-light validation: + +```text +rendererScenePacketSelfTest +rendererClassicSubviewDomainSelfTest +gfxInfo +``` + +`tools/tests/renderer_classic_subview_domain.py` guards the front-end and +legacy command capture association, bounded parent/source/capture admission, +direct-mirror and special capture scope, sealed camera/clip/scissor semantics, +sealed GL/Vulkan copy arguments, exact post-view/post-copy ownership reporting, +subview-before-cinematic preparation, deferred nested dynamic completion, +bidirectional whole-root fallback, conservative defaults, diagnostics, and CI +registration. + +## Controlled nested acceptance evidence + +The final 2026-08-23 R6 acceptance passed all six cases on both OpenGL and +Vulkan for one deliberately narrow topology: a stock-material, capture-backed +color-2D mirror whose child view alone contains one frozen cinematic stage and +one `_currentRender` authored-post stage. Both-on normal owned exactly one +mirror and its one nested cinematic/post transaction on the active backend, +with zero mismatch, duplicate, or untracked counters. Both-on +`r_skipPostProcess 1` reported zero ownership, one fallback in each domain, and +the named `nestedCinematicPostFallback` outcome. Subview-only owned the mirror +while cinematic request/view counts stayed zero; all normal enabled cases and +the shared/classic skip pair had exact same-backend engine-TGA parity. All four +exact comparisons per backend reported RMS `0`, maximum delta `0`, and zero +differing channels; the inactive backend's counters stayed zero. + +The authoritative acceptance report is +`.tmp/renderer-milestone-d/acceptance-20260823-r6-all/renderer_milestone_d_acceptance_report.json` +(SHA-256 +`b49031c48267fbfa86c295082707525a390d6090c24f600d00644237a67b252a`), +bound to fixture manifest SHA-256 +`24a2a4926f4b1263e311ad762bf40d391b33de225732d12154ebd7625705282d` +and immutable runtime manifest SHA-256 +`dbece44597ec5e0686eed215f6ae99c5d6ba4cdd959c1efa7c4acaf1bc2ca673`. +The exact command, capture contract, deltas, inventory seals, and passing +foundation-report hashes are +recorded in the +[cinematic/post transaction evidence](classic-cinematic-post-domain-modernization.md#controlled-nested-acceptance-evidence) +and the [renderer validation matrix](renderer-validation-matrix.md). + +## Release-qualification boundary + +R6 proves one capture-backed color-2D mirror coupled to a nested dynamic tail; +it is not evidence for a direct mirror, a multi-level nested-subview chain, +remote-camera/reflection/refraction/x-ray forms, color cubemaps, depth captures, +illegal cube-face/aspect rollback, `_currentDepth`, in-world GUI, or +render-demo/Raven special-frame ownership. + +Before promotion, retain engine-render-target screenshots—not OS captures—for +fixed direct mirror, dynamic mirror/reflection, x-ray, remote-camera, and +refraction views with the setting off and on, separately on OpenGL and Vulkan. +Require exact same-settings output, nonzero reconciled ownership, named +zero-commit fallback on a deliberately malformed semantic and capture edge, +clean backend diagnostics, retained final package evidence, and target-platform/ +driver coverage. Retain a multi-level nested direct/capture chain with nonzero root-tree +ownership plus a malformed descendant that reports a whole-tree rollback on +both backends. R6 already covers one color-2D capture; extend retained target +coverage to color cubemaps and depth-2D/depth-cubemap ownership plus an +illegal-face/aspect zero-commit fallback on both backends. Repeat the R6 nested +authored-post and forced `nestedCinematicPostFallback` gate from clean committed +source and a final package, and add an otherwise equivalent `_currentDepth` +case. In-world GUI retains its own provenance-tagged transaction documented +in [Shared Classic In-World GUI +Domain](classic-inworld-gui-domain-modernization.md). + +Together with the other independently guarded shared corridors, this completes +Milestone D's scoped implementation. It does not make every child material, +lighting category, or the aggregate modern-visible renderer shared-owned. The +narrow controlled nested runtime gate above is recorded; clean-package, +broader-form, human-review, and target-platform/driver evidence remain +release-promotion gates. Temporal presentation is the next roadmap milestone. diff --git a/docs/dev/classic-world-ambient-domain-modernization.md b/docs/dev/classic-world-ambient-domain-modernization.md new file mode 100644 index 00000000..b43baa10 --- /dev/null +++ b/docs/dev/classic-world-ambient-domain-modernization.md @@ -0,0 +1,290 @@ +# Shared Classic World Ambient/Material Domain + +## Status + +The second Milestone D classic-frame domain is **Implemented (default-off; +local Windows stock-runtime OpenGL/Vulkan qualification recorded; freshly +staged final package and platform release promotion pending)** for complete +eligible 3D +world ambient/material views. OpenGL and Vulkan consume the same ordered, +per-draw evaluated material records. The established classic view remains the +supported default and the whole-view rollback. + +Implementation, dependency-light validation, and local Windows stock-runtime +image/ownership/rollback evidence are complete for both backends. The retained +OpenGL and Vulkan captures prove exact option-off/on output for one eligible +view and exact complete-view fallback for one deform blocker. These are +development-worktree results, not clean committed-package or cross-platform +release-promotion evidence. + +This is an ambient/material ownership boundary, not modern interaction-lighting +ownership. The established depth prepass remains authoritative, and any view +containing non-owned interaction, shadow, light-grid, fog/blend, special-effect, +GUI, or post work remains completely on its established backend walker. + +The implementation is original openQ4 work. It incorporates no external source +code and changes no stock Quake 4 asset. + +## Domain boundary + +The domain considers complete ordinary 3D world `RC_DRAW_VIEW` work. A view is +eligible only when it is a normal world scene with an enabled ambient packet, +contains no non-owned packet work, and every original source surface can be +classified, matched, evaluated, resolved, and preflighted in source order. +The only benign render-flag mask is `RF_NO_GUI | RF_PENUMBRA_MAP | +RF_PRIMARY_VIEW`; any other flag rejects the view. + +For an eligible view, the shared path preserves: + +- original surface and repeated ambient-stage order; +- exact stage conditions and evaluated RGBA values; +- 2x3 texture matrices, blend factors, color/depth write masks, depth + comparison, culling, fixed-function alpha comparison, explicit texgen, + vertex-color mode, and polygon offset; +- generation-checked texture-resource identity and sampler state; +- geometry, instance/model-view and projection matrices, viewport, and + scissors; +- inactive and recognized no-op passes as ordered coverage records rather than + silently deleting them; and +- a deterministic complete-view hash and per-backend coverage report. + +The first version deliberately rejects portal skies, suppress-in-subview and +in-world GUI materials, subviews and mirrors, editor/render-demo mutations, +clip planes, global material overrides, BSE/outline/dynamic-texcoord special +surfaces, unsealed or non-admitted material deforms and skinned geometry, +weapon/model depth hacks, negative +scale, decals and decal-color streams, post-process sorting, synthetic or +missing spaces, current-render/depth images, cinematics, dynamic/defaulted/ +unloaded images, custom programs, non-explicit texgen, player-visibility +surface work, unsupported fixed-function state, and every bounded-pool or stale +resource failure. One rejection keeps the complete view classic. + +Unpacketized or backend-local view mutations are also explicit blockers: +visible light lists, portal-distance fades, a positive forced-ambient floor, +cel/world-outline work, overdraw or single-triangle diagnostics, ambient/render +skip modes, same-view blur/AL special-effect commands, prepared +offscreen render targets, and a non-swapchain Vulkan render scope. A benign +zero-mask special-effect command is a no-op rather than a false blocker. + +Packet categories with active stencil-shadow, shadow-map, ARB2-interaction, +light-grid, deferred, forward+, fog/blend, SSAO, motion-blur, bloom, authored +post, special-effect, GUI, or present work are explicit whole-view blockers. +Only the depth and ambient packet categories may participate in this first +domain. + +## Depth and phase contract + +The shared domain owns ambient/material color work only. It does not replace or +redraw the established depth prepass. Before a view can be published, every +opaque or perforated ambient draw must have a matching `RENDER_PASS_DEPTH` +packet for the same source surface, material, geometry, and instance. A +translucent ambient draw has no depth-packet prerequisite. Missing or mismatched +coverage rejects the complete view. + +An authored stage `hasAlphaTest` remains coverage metadata for the established +depth pass; it is not reapplied as an ambient color test. The world compiler can +therefore retain such a stage only because the complete-view transaction proves +the matching depth prerequisite. Fixed-function alpha-comparison bits that the +classic color walker actually executes remain sealed in the evaluated color +pass. + +World materials are split at the established fog boundary: + +- material sort values below `SS_MEDIUM` execute in the pre-fog phase; +- material sort values from `SS_MEDIUM` up to the rejected post-process range + execute in the post-fog phase. + +Both backends submit the pre-fog records, leave the established fog position in +the frame sequence intact, and then submit the post-fog records. This phase +split preserves ordering without claiming ownership of fog or blend lights. + +## Shared record flow + +1. Front-end scene packets identify materials actually referenced by + `RENDER_PASS_AMBIENT` world draws. Intrinsic material class does not decide + admission. +2. `MaterialResourceTable` compiles those references into a distinct bounded + world-pass pool. Every record uses `RENDERER_MATERIAL_PASS_SURFACE`, + `RENDERER_PROGRAM_FIXED`, enabled depth testing, the authored depth-write + bit, and a translated `LESS`, `EQUAL`, or `ALWAYS` comparison. +3. `ClassicWorldAmbientDomain` walks every original surface transactionally, + matches the ambient packet stream and depth prerequisites, evaluates each + authored pass once against the bounded instance-register block, resolves + every opaque texture id, classifies the pre/post-fog phase, and hashes the + stable evaluated contract. +4. The view publishes its draw and evaluated-pass spans only after the complete + transaction succeeds. A failure rolls the arenas back to their checkpoints + and publishes only a precise fallback reason. +5. OpenGL and Vulkan preflight the complete prepared view before the first + shared ambient draw. Each backend then submits only sealed `DRAW` records, + accounts for all inactive/no-op records, and records one owned or fallback + outcome for the view. + +## Backend execution + +The OpenGL adapter completes geometry/cache, texture, sampler, state, scissor, +capacity, and coverage preflight before committing the first pre-fog draw. The +Vulkan adapter completes geometry offsets, descriptors, exact pipeline keys, +render-scope, state, scissor, capacity, and coverage preflight before the view's +depth clear or first framebuffer-affecting command. + +The actual shared consumers do not reread `shaderStage_t`, `GetStage`, or raw +`shaderRegisters`; those inputs are sealed by the front-end transaction. The +legacy draw-surface pointer remains only as a bounded geometry submission +bridge. + +Neither backend can return to the classic ambient walker after its first shared +draw. Any pre-commit failure leaves the established complete ambient walk +untouched. An unexpected post-commit coverage mismatch is diagnostic only, so +the engine cannot double-render the view while attempting a late fallback. + +## Control and diagnostics + +`r_rendererSharedWorldAmbient` is the single cross-backend control: + +- `0` (default): every 3D world view uses the established classic ambient path; +- `1`: a complete eligible view may use the shared domain; any rejection uses + the complete classic view. + +The option is independent of `r_rendererSharedGui`. Requesting it also prevents +the older aggregate modern-visible path from suppressing or recreating ambient +work before the complete-view transaction is decided. Stock-baseline and +gameplay-benchmark tooling explicitly force the option off unless a run is +intentionally collecting shared-domain evidence. + +`gfxInfo` reports whether ownership was requested, prepared, and frame-valid; +world/ready/fallback view and surface counts; evaluated, drawable, inactive, +active-no-op, pre-fog, and post-fog pass counts; the deterministic hash; status; +and OpenGL/Vulkan owned/fallback coverage. Material-table diagnostics separately +report world-referenced, eligible, fallback, pass-pool, overflow, and first +failure information. + +Use these renderer commands for focused validation: + +```text +rendererContractsSelfTest +rendererMaterialResourceTableSelfTest +rendererClassicWorldAmbientDomainSelfTest +gfxInfo +``` + +The dependency-light tests cover the distinct packet-derived world-pass pool, +fixed surface/program/depth semantics, depth-prerequisite and phase vocabulary, +bounded transaction reset and stable hashing, and backend coverage accounting. +The static `renderer_classic_world_ambient_domain.py` regression additionally +locks down complete GL/Vulkan preflight-before-commit ordering, the pre-fog / fog +/ post-fog split, sealed consumer bodies, conservative default/bootstrap state, +benchmark/baseline isolation, and validation-workflow registration. + +## Local runtime evidence + +Runtime qualification used stock assets, a bordered 1280x720 window, and the +engine `screenshot` command. The exact stock map/view/camera and isolation +settings are retained by each harness report; no custom map, material, or shader +was added. + +The gameplay harness registers the controlled stock `sp-mv2-ambient` case on +`maps/tools/mv2`. Use its exact validation-matrix command: + +```powershell +python tools\tests\renderer_gameplay_benchmark.py --profile world-ambient --pacing-only --no-gpu-timers +``` + +The final profile launches with `ui_showGun 0`, `g_showHud 0`, and +`r_multiSamples 0`. After the normal spawn it runs `noclip`, then +`setviewpos 0 0 256 80 0 0`. Its post-map controls select the fast direct +no-post path, set `r_postAA 0`, select an unmatched `r_singleLight`, and disable +subview, light-grid, player-visibility overlay, portal-distance-fade, cel, and +debug islands while leaving ambient, deform, and normal rendering enabled. The +exact post-map set is: + +```text +g_renderFastNoPost 1 +g_renderFastNoPostDirect 1 +r_postAA 0 +r_singleLight 2147483647 +r_skipSubviews 1 +r_useLightGrid 0 +r_skipPlayerVisibilityEffects 1 +r_portalsDistanceCull 0 +r_forceAmbient 0 +r_celShading 0 +r_celShadingWorld 0 +r_showOverDraw 0 +r_singleTriangle 0 +r_skipAmbient 0 +r_skipNewAmbient 0 +r_skipDeforms 0 +r_skipRender 0 +``` + +It does not set `r_skipPostProcess` or `r_skipGuiShaders` and uses no +replacement asset. The base command retains the default-off comparison; append +`--set-cvar r_rendererSharedWorldAmbient=1` for the enabled run and select +Vulkan with `--render-api vk`. Because these are controlled visual and ownership +captures, `--pacing-only --no-gpu-timers` deliberately prevents them from being +mistaken for promotable per-map CPU/GPU budget evidence. + +| Capture | Result | Retained evidence | +|---|---|---| +| OpenGL eligible view, option off/on | **Pass** | `.tmp/renderer-gameplay/world-ambient-gl-off-final3` and `.tmp/renderer-gameplay/world-ambient-gl-on-final3`; engine-TGA comparison RMS `0`, maximum delta `0`; enabled diagnostics `views=1 ready=1 fallback=0 surfaces=1/1 passes=1 draw=1 pre=1 post=0 hash=dc18ed8c0539bbfc GL=1/0`; view hash `bad7344c6394edf8`; both screenshots SHA-256 `3050cb852ebf8a7ba50b847926ab52255edbb19ded8545ccfe4f3f5683708334` | +| Vulkan eligible view, option off/on | **Pass** | `.tmp/renderer-gameplay/world-ambient-vk-off-final` and `.tmp/renderer-gameplay/world-ambient-vk-on-final`; engine-TGA comparison RMS `0`, maximum delta `0`; the same domain/view hashes and counts with `VK=1/0`; both screenshots SHA-256 `5549ee3dde0fa959f8185335dd9cc2b3306c8b6dd442d12285031054ef6d6ddf`; Vulkan validation/VUID/call-failure gates clean | +| OpenGL deform blocker, option off/on | **Pass** | `.tmp/renderer-gameplay/world-ambient-gl-fallback-off-final` and `.tmp/renderer-gameplay/world-ambient-gl-fallback-on-final`; stock override `r_materialOverride shaderDemos/move`; engine-TGA comparison RMS `0`, maximum delta `0`; enabled diagnostics `ready=0 fallback=1 surfaces=0/1 draw=0`, `failure=sourceSurfaceFallback detail=13 sourceClass=fallbackDeform`, GL fallback; both screenshots SHA-256 `7044775af6be6bc075ffababbda3d3bbd62632b44b4836651bec25a6ea0ef3e4` | +| Vulkan deform blocker, option off/on | **Pass** | `.tmp/renderer-gameplay/world-ambient-vk-fallback-off-final` and `.tmp/renderer-gameplay/world-ambient-vk-fallback-on-final`; the same stock override and named zero-draw failure with Vulkan fallback; engine-TGA comparison RMS `0`, maximum delta `0`; both screenshots SHA-256 `380115efeb2354b8d1d43c2c5aed146538e267e87dca759ffd4ac7ff42f7db10` | + +The safe matrix retained at `.tmp/renderer-validation/world-ambient-final-2` +also passed all three selected cases: `renderer-foundation-selftests`, +`renderer-default-safety-selftest`, and `renderer-vk-clear-startup`. + +Two additional OpenGL lifecycle captures passed. The live-toggle run retained +at `.tmp/renderer-gameplay/world-ambient-gl-toggle-final` first reported owned +coverage with `requested=1` and `GL=1/0`, then reported `prepared=0` and +`status=empty` after the setting changed to `0`; its final engine TGA matched the +option-off reference exactly. The partial-video-restart run at +`.tmp/renderer-gameplay/world-ambient-gl-restart-final` reported owned coverage +before and after `vid_restart partial`, with no stale/fallback outcome, the final +stable hash `dc18ed8c0539bbfc`, and an exact final engine-TGA match. Both final +lifecycle screenshots have the eligible OpenGL reference SHA-256 +`3050cb852ebf8a7ba50b847926ab52255edbb19ded8545ccfe4f3f5683708334`. + +This completes the local implementation/runtime gate. The evidence comes from a +development worktree and does not replace a clean committed-package recapture +or target-platform/driver qualification before promotion. + +## Milestone D completion and release-promotion boundary + +The GUI and world ambient/material corridors are joined by the +[fixed-classic interaction domain](classic-interaction-domain-modernization.md) +and [fog/blend domain](classic-fog-blend-domain-modernization.md) as the first +four complete shared classic-frame domains. The interaction domain accepts +eligible unshadowed and shadow-coupled views; its shadow extension covers +stencil, projected/CSM/parallel, point, mixed, and hybrid ownership with +dynamic/perforated mapped casters and atomic translucent-moment fallback. The +fog/blend implementation seals the complete eligible phase for both backends; +native/static and controlled GL/Vulkan runtime qualification pass, while +authored-stock and release promotion remain open. +These domains still do not make the whole classic frame modern. + +The independent default-off +[material-deform contract](classic-deform-domain-modernization.md) now admits +sealed completed or intentional-empty CPU deform results. The earlier +`shaderDemos/move` evidence remains the conservative +`r_rendererSharedDeform 0` rollback baseline; deform-enabled qualification uses +the dedicated profile and requires nonempty ownership plus exact same-backend +parity. + +The completed cinematic/authored-post corridor seals eligible root video/audio +views and complete authored-post tails, including eligible tails nested inside +a sealed special-view tree. Nested dynamic completion and special-view +completion publish or roll back as one root transaction; see [Shared Classic +Cinematic and Authored-Post +Transaction](classic-cinematic-post-domain-modernization.md) and [Shared +Special-Subview Transaction](classic-subview-domain-modernization.md). +Render-demo and Raven special-frame ownership have their own dedicated sealed +[transaction](classic-special-frame-domain-modernization.md). + +These independently guarded corridors complete Milestone D's scoped +implementation without making the aggregate modern-visible renderer or every +classic-frame category shared-owned. Clean-package, human-review, and +target-platform/driver evidence remain release-promotion gates; temporal +presentation is the next roadmap milestone. diff --git a/docs/dev/engine-capability-matrix.md b/docs/dev/engine-capability-matrix.md new file mode 100644 index 00000000..bd17eeb8 --- /dev/null +++ b/docs/dev/engine-capability-matrix.md @@ -0,0 +1,92 @@ +# Engine Capability Matrix + +This is the authoritative current-state index for openQ4 engine capability claims. It answers whether a capability exists in the tree as audited on 2026-08-23; proposals describe intent, and release notes describe historical changes, but neither overrides this matrix. Domain documents linked below own their detailed acceptance evidence. + +Status means: + +- **Implemented** — a functional scoped path is integrated with the named implementation evidence. It may remain optional and default-off pending release promotion; the scope/qualification column limits the claim and never implies every platform or asset is certified. +- **Experimental** — the scoped implementation is incomplete, trial-grade, or lacks the minimum evidence claimed by its row. +- **Missing** — there is no production implementation. A plan, cvar stub, shader prototype, or unused data structure does not count. + +Any change that moves a row between these states must update this file and its evidence in the same change. When documentation disagrees, treat the narrower claim here as current. + +## P0 security and provenance + +| Capability | Status | Scope and qualification | Evidence | +|---|---|---|---| +| Network-driven executable updater | **Implemented** (retired safely) | The legacy version-check wire exchange remains compatible, but the client sends no GUID identity, ignores all server-provided text/URL/MIME/action fields, contains no updater download or execute path, and can only open the compile-time project releases URL without forcing exit. Pure-server package negotiation is a separate, bounded compatibility path described below. | [`AsyncClient.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncClient.cpp), [`licensee.h`](https://github.com/themuffinator/openQ4/blob/master/src/framework/licensee.h), [`openq4_pure_pack.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/openq4_pure_pack.py) | +| Server-supplied package transport | **Implemented** (validated redirect path; direct transfer build-conditional) | Pure-server redirects and PK4 download entries accept only bounded `http://` or `https://` URLs whose authority contains a syntactically valid DNS name, IPv4 literal, or bracketed IPv6 literal. Standard Meson packages do not enable libcurl, so syntax-validated redirect prompts remain available but in-process direct PK4 transfer reports unavailable. In a separately integrated curl-enabled build, the generic downloader revalidates the URL, restricts libcurl to HTTP(S), disables redirects, and applies connect, stall, and whole-transfer limits; package size, checksum, and destination-path validation remain mandatory. This validates syntax and protocol, not DNS ownership or server trust. | [`AsyncClient.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncClient.cpp), [`FileSystem.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/FileSystem.cpp), [`BuildDefines.h`](https://github.com/themuffinator/openQ4/blob/master/src/framework/BuildDefines.h), [`URLPolicy.h`](https://github.com/themuffinator/openQ4/blob/master/src/sys/URLPolicy.h), [`openurl_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/openurl_security.py) | +| Pure multiplayer game-module boundary | **Implemented** (contained protocol 2.41 compatibility path) | Pure mode enforces the ordered asset-PK4 list while the legacy game-code field carries the stock 1.4.2 `game300.pk4` checksum only as a platform-independent token for a module already resolved from trusted local openQ4 package/module roots. Pure negotiation never selects, downloads, extracts, or restarts into executable code. The token is not a hash of the loaded module, cryptographic module-equality proof, or anti-cheat guarantee. | [Official PK4 checksums](official-pk4-checksums.md), [server security](../user/server-security.md), [`FileSystem.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/FileSystem.cpp), [`openq4_pure_pack.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/openq4_pure_pack.py) | +| Malformed network and snapshot input handling | **Implemented** (hardened legacy boundary) | Bit-message reads record underflow instead of silently continuing, bounded queue and user-command decoders reject incomplete payloads, and SP/MP snapshot readers validate covered entity, player, spectator, weapon, instance, projectile-owner, PVS, hit-scan, and game-state fields before use. Audited leaf readers stage decoded fields until their payload is valid. A late malformed top-level snapshot tears down the affected session before another game or presentation frame; the legacy entity lifecycle is not claimed to provide whole-snapshot rollback. This is targeted hardening of the audited paths, not a claim that every legacy parser is formally verified. | [`BitMsg.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/idlib/BitMsg.cpp), [`AsyncNetwork.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncNetwork.cpp), [`network_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_security.py), [`openQ4-game`](https://github.com/themuffinator/openQ4-game) snapshot readers | +| Connection challenge entropy | **Implemented** | Client IDs, server-instance identity, and connection challenges use the OS CSPRNG and fail closed when it is unavailable. A connection challenge is endpoint/client-bound, expires after 30 seconds, and is consumed on successful admission; the legacy pure handshake may validate the same transaction across connect/pure/connect messages before admission. | [`AsyncClient.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncClient.cpp), [`AsyncServer.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncServer.cpp), [`network_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_security.py), [`CoreSafetyTest.cpp`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/native/CoreSafetyTest.cpp) | +| Authenticated remote console (`rcon2`) | **Implemented** | Challenge/proof exchange uses CSPRNG nonces, endpoint binding, PBKDF2-HMAC-SHA-256 password verification, request binding, constant-time proof comparison, one-use expiry, and secret zeroing. It does not transmit the password. | [`Rcon2Protocol.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/Rcon2Protocol.cpp), [`CryptoHash.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/idlib/CryptoHash.cpp), [`AsyncServer.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncServer.cpp), [`network_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_security.py) | +| Rcon abuse limits and secret redaction | **Implemented** | Per-source and global OOB/rcon limits plus cooldowns bound unauthenticated work. Private assignments are suppressed before dispatch from in-game, Win32, SDL, and TTY echo/history; old persistent history is purged; `$ cvar` expansion, startup dumps, config persistence, generic cvar output, and console completion previews redact; private journal payloads are omitted; and consumed command buffers are cleared. The password is case-sensitive/private. | [`AsyncServer.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncServer.cpp), [`CVarSystem.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/CVarSystem.cpp), [`Console.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/Console.cpp), [`PrivateCommand.h`](https://github.com/themuffinator/openQ4/blob/master/src/idlib/PrivateCommand.h), [`network_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_security.py) | +| Legacy plaintext rcon | **Implemented** (contained compatibility path) | Insecure plaintext rcon is disabled on both client and server by default. It is available only through explicit `net_clientUseLegacyRcon 1` / `net_serverAllowLegacyRcon 1` opt-in and should be limited to trusted legacy environments. | [`AsyncNetwork.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncNetwork.cpp), [`AsyncClient.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncClient.cpp), [`AsyncServer.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncServer.cpp), [`network_security.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_security.py) | +| Doom 3 / Doom 3 BFG provenance inventory | **Implemented** | Both retained header families, their distinct Additional Terms, official pinned snapshots, and the six BFG-headered OpenAL files found through an intermediate RBDOOM lineage are inventoried without asserting a legal conclusion. | [Source provenance](source-provenance.md), [`audit_source_provenance.py`](https://github.com/themuffinator/openQ4/blob/master/tools/validation/audit_source_provenance.py) | +| Reproducible retail-PK4 SP/MP compatibility evidence | **Implemented** | Offline retail/overlay/runtime identity, windowed SP save/restore, demo record/playback, pure MP listen/client, logs, and engine screenshots have one non-interactive harness. It binds the approved-manifest file and current Git provenance, rejects dry-run/failure-bearing reports, reconstructs each exact role launch, and inventories every packaged-overlay path that supersedes a retail virtual path instead of calling the run stock-only. MP roles explicitly use archived `ui_autoJoin 1` and prove an active, non-spectating player with a visible HUD; only a join-menu/initial-spectator test may explicitly set `ui_autoJoin 0`. A human visual/gameplay review and final-package/platform evidence remain required for promotion. | [Retail-PK4 compatibility baseline](stock-asset-baseline.md), [`stock_asset_baseline.py`](https://github.com/themuffinator/openQ4/blob/master/tools/validation/stock_asset_baseline.py) | + +## Quake 4 compatibility and engine foundation + +| Capability | Status | Scope and qualification | Evidence | +|---|---|---|---| +| Retail PK4 filesystem and idTech 4 asset/decl loaders | **Implemented** | Stock Quake 4 PK4 search, decls, materials, maps, MD5 models/animations, images, sounds, GUIs, cinematics, and scripts are the compatibility foundation. Full content behavior is still qualified scene-by-scene rather than claimed universally. | [`FileSystem.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/FileSystem.cpp), [`DeclManager.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/DeclManager.cpp), [renderer validation](renderer-validation-matrix.md) | +| Unified open-source SP and MP game modules | **Implemented** | `game_sp` and `game_mp` are built from the companion `openQ4-game` source and consumed under the unified `baseoq4/` runtime. Proprietary retail DLL compatibility is intentionally out of scope. | [`meson.build`](https://github.com/themuffinator/openQ4/blob/master/meson.build), [`meson_sources.py`](https://github.com/themuffinator/openQ4/blob/master/tools/build/meson_sources.py) | +| BSE effects runtime | **Implemented** | BSE is first-party in-tree client code and supports stock Quake 4 effect declarations; dedicated builds retain the disabled manager path. | [`BSE_Manager.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/bse/BSE_Manager.cpp), [BSE research/validation](quake4-bse-research-and-implementation-plan.md) | +| Save-game write/read compatibility | **Implemented** | Versioned openQ4 saves, integrity checking, recovery, legacy read support, entity-filter state, and save/load tests are present. Cross-version compatibility is governed explicitly rather than assumed. | [Save compatibility policy](savegame-compatibility-policy.md), [`savegame_v3_contract.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/savegame_v3_contract.py) | +| Render demos and multiview demos | **Implemented** | Render-demo record/playback and server multiview recording/playback exist with versioned compatibility checks. | [`Session.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/Session.cpp), [`MultiViewDemo.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/MultiViewDemo.cpp), [`demo_playback.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/demo_playback.py), [`multiview_demo.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/multiview_demo.py) | +| Fixed 60 Hz simulation with high-refresh presentation | **Implemented** | Authoritative simulation remains 60 Hz while presentation/interpolation and frame-pacing diagnostics support higher display rates. This does not change save/demo/network cadence. | [`Session.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/Session.cpp), [`renderer_gameplay_benchmark.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_gameplay_benchmark.py) | +| Dedicated server | **Implemented** | Headless dedicated builds avoid client renderer/BSE presentation and have stock-map smoke coverage. | [`dedicated.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/sys/linux/dedicated.cpp), [`linux_dedicated_stock_map_smoke.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/linux_dedicated_stock_map_smoke.py) | +| Background job system for general engine work | **Implemented** | Client, tool, and dedicated builds own one portable service with bounded list/job/dependency admission, sleepable waits, low/normal/high priority aging, dependency ordering, cooperative cancellation, deterministic inline fallback, shutdown joining, and observable counters. Dedicated builds default inline, and `jobs_enable 0` executes rather than drops work. The learned level-load read/PK4-inflate and framing/integrity pipeline is its first production consumer; it uses independently opened VFS handles and publishes only sealed immutable source DTOs. Existing Milestone A current-build lifecycle evidence remains valid, while clean final-package recapture and the new consumer's retained Milestone B parity/cancellation evidence remain pending. | [`ParallelJobSystem.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/ParallelJobSystem.cpp), [`LevelLoadPipeline.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/LevelLoadPipeline.cpp), [`ParallelJobSystemTest.cpp`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/native/ParallelJobSystemTest.cpp), [portable job-system contract](parallel-job-system.md) | +| Learned level-load manifests and bounded read/decode staging | **Experimental** (implemented, default-off; performance requalification required) | A successful opt-in map load learns an exact map plus full-SHA-256 mode/entity-filter and ordered search/PK4/settings manifest. Matching reads, bounded worker preparation, transactional model/world/collision and version-3 animation caches, cancellation, corruption handling, and source fallback are integrated. A regression audit found materially excessive stock loading time and cache rewrite churn under the former default-on policy, so `com_levelLoadModernization 0` now gates every cache/preload/animation read and write and preserves classic loading even when older individual settings remain archived on. This is not portal-aware live reprioritization, general asset streaming, or a speed-up claim; cold and warm performance must be requalified against the classic baseline before promotion. | [loading/cache contract](loading-cache-modernization.md), [`LevelLoadCacheManager.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/LevelLoadCacheManager.cpp), [`level_load_cache.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/level_load_cache.py), [`LevelLoadCacheFormatTest.cpp`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/native/LevelLoadCacheFormatTest.cpp) | +| General asynchronous asset decode and GPU/audio upload streaming | **Missing** | The production level-load worker stage now performs source framing and integrity decode, but deliberately stops before asset-specific image, sound, declaration, model, world, collision, animation, or other owner parsing/transformation. Those owners still parse/decode, adopt, finalize, and upload through their established main-owner paths; there is no general runtime streaming scheduler, portal-aware dynamic reprioritization, or arbitrary worker-side renderer/audio API use. | [loading/cache contract](loading-cache-modernization.md), [`ImageManager.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ImageManager.cpp), [idTech 5-level roadmap](idtech5-modernization-roadmap.md) | + +## Rendering + +| Capability | Status | Scope and qualification | Evidence | +|---|---|---|---| +| Classic ARB2 interaction renderer | **Implemented** | This is the supported/default visible-lighting path and compatibility rollback for stock assets. | [`draw_arb2.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/draw_arb2.cpp), [renderer validation](renderer-validation-matrix.md) | +| Modern OpenGL scene packets/resources/render graph | **Experimental** | Scene packets, material/geometry tables, render-graph resources, upload rings, and a modern executor exist. They are opt-in infrastructure, not proof that a complete stock frame is modern-owned. | [`ScenePackets.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ScenePackets.cpp), [`MaterialResourceTable.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/MaterialResourceTable.cpp), [`GeometryResources.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/GeometryResources.cpp), [`RenderGraph.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/RenderGraph.cpp), [`ModernGLExecutor.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernGLExecutor.cpp) | +| Modern visible lighting ownership | **Experimental** | Separate shared classic interaction and fog/blend ownership now exists for complete eligible root-view domains, a provenance-tagged in-world GUI subset has its own depth-aware transaction, and a special-subview transaction seals direct `SS_SUBVIEW` mirrors plus eligible remote-camera, mirror, reflection, refraction, and x-ray child-view/capture edges, including exact 2D/cubemap color/depth target aspect and face. Eligible nested special-view trees seal parent/root/depth records, retain depth-first command order, and publish only after their outermost special view completes; a rejected member rolls back the complete tree. Cinematic/authored-post and render-demo/Raven special-frame transactions seal other eligible exceptional ranges. The material-deform dependency distinguishes finalized CPU output from generated/skinned geometry and lets those domains seal completed, intentional-empty, and classic source-preserving receiver roles. Unsupported or stale results still roll back atomically. This does not mark the older aggregate `r_rendererModernVisible` lighting contract proven. | [Shared Classic Interaction-Lighting Domain](classic-interaction-domain-modernization.md), [Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md), [Shared Classic In-World GUI Domain](classic-inworld-gui-domain-modernization.md), [Shared Special-Subview Transaction](classic-subview-domain-modernization.md), [Shared Classic Cinematic and Authored-Post Transaction](classic-cinematic-post-domain-modernization.md), [Shared Render-Demo and Raven Special-Frame Transaction](classic-special-frame-domain-modernization.md), [Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md), [Modern visible-lighting ownership](plans/2026-08-16-modern-visible-lighting-ownership.md), [renderer validation](renderer-validation-matrix.md) | +| Shared fixed-function 2D GUI ownership | **Implemented** (default-off; release promotion pending) | `r_rendererSharedGui` promotes only a complete eligible root 2D view. The material table preserves repeated stages and opaque image identities, the shared domain evaluates register-driven condition/color/matrix/state once per draw, and GL/Vulkan preflight the full view before submitting it. Any unsupported surface, material, state, resource, target, or backend condition executes the untouched classic view. This is scoped GUI ownership, not modern world-lighting ownership. | [Shared Classic 2D GUI Domain](classic-gui-domain-modernization.md), [`ClassicGuiDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicGuiDomain.cpp), [`renderer_classic_gui_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_gui_domain.py) | +| Shared in-world GUI ownership | **Implemented** (default-off; release promotion pending) | `r_rendererSharedInWorldGui` promotes only the complete provenance-tagged GUI subset emitted by `R_RenderGuiSurf` for an eligible ordinary 3D view. Its world-category packet stream compiles ordered fixed-function passes with the established 3D depth semantics; GL/Vulkan preflight the complete tagged subset before the ambient walks, and only a recorded owned subset is removed from the matching classic walker. Any unsupported source, packet, material, resource, target, capacity, or backend condition keeps every tagged GUI surface on the untouched classic path. | [Shared Classic In-World GUI Domain](classic-inworld-gui-domain-modernization.md), [`ClassicGuiDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicGuiDomain.cpp), [`renderer_classic_gui_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_gui_domain.py) | +| Shared classic world ambient/material ownership | **Implemented** (default-off; controlled local validation passed) | `r_rendererSharedWorldAmbient` promotes only a complete eligible ambient-only 3D world view. Packet-derived surface/fixed passes preserve ordered evaluated state, opaque/perforated draws require matching established depth packets, and GL/Vulkan preflight the full pre-fog/post-fog plan before committing. Stock `maps/tools/mv2` GL/Vulkan captures prove exact option-off/on output and named zero-draw deform fallback. Any non-owned pass, unsupported source/material/resource, missing depth prerequisite, or backend rejection executes the untouched complete classic ambient view; clean-package/platform promotion remains open. | [Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md), [`ClassicWorldAmbientDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicWorldAmbientDomain.cpp), [`renderer_classic_world_ambient_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_world_ambient_domain.py) | +| Shared classic interaction-lighting ownership | **Implemented** (default-off; controlled local validation passed) | `r_rendererSharedWorldInteraction` promotes only a complete eligible fixed-classic interaction view. Explicit packet identities and bounded records seal all interaction primitives plus classic stencil volumes, projected single-map and CSM/parallel shadows, point cubes, mixed mapped/stencil lights, complete hybrid supplements, dynamic mapped casters, and perforated alpha stages. GL and Vulkan retain complete plans before visible ownership and reconcile exact coverage. Unsupported work executes the untouched complete classic interaction walker. The controlled five-case GL/Vulkan profile has same-backend classic image parity, visible shadow deltas, and named atomic fallback evidence; stock fixed-camera, clean-package, and platform promotion remain open. | [Shared Classic Interaction-Lighting Domain](classic-interaction-domain-modernization.md), [`ClassicInteractionDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicInteractionDomain.cpp), [`renderer_classic_interaction_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_interaction_domain.py), [`renderer_vulkan_shadow_compatibility.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_vulkan_shadow_compatibility.py) | +| Shared classic fog/blend ownership | **Implemented** (default-off; controlled local validation passed) | `r_rendererSharedWorldFogBlend` promotes only the complete fog/blend phase of an eligible ordinary root 3D view. One bounded transaction preserves original light and receiver order, every active/inactive blend stage, fog receiver/cap work, evaluated state, images, and exact coverage. GL and Vulkan preflight the whole phase; unsupported work executes the untouched classic walker. The controlled suite passes exact same-backend classic parity, nonempty ownership, visible fog/blend deltas, and atomic rollback on both backends; authored-stock fog and clean-package/platform promotion remain open. | [Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md), [`ClassicFogBlendDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicFogBlendDomain.cpp), [`renderer_classic_fog_blend_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_fog_blend_domain.py) | +| Shared special-subview transaction | **Implemented** (default-off; controlled nested GL/Vulkan fixture passed; broader release qualification pending) | `r_rendererSharedSubview` seals direct mirror camera/clip/scissor semantics and exact parent/child 2D or cubemap color/depth capture records for eligible remote-camera, mirror, reflection, refraction, and x-ray views. Eligible trees seal parent/root/depth and depth-first order; ownership remains unpublished until all child work and capture/direct edges finish. The final R6 acceptance proves one capture-backed color-2D mirror coupled to a nested cinematic plus `_currentRender` tail, including exact parity, ownership, and atomic named fallback on Windows GL/Vulkan. Direct mirrors, multi-level nested-subview chains, the other capture kinds/aspects/faces, `_currentDepth`, final-package, and platform/driver breadth remain unqualified. | [Shared Special-Subview Transaction](classic-subview-domain-modernization.md), [`ClassicSubviewDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicSubviewDomain.cpp), [`renderer_classic_subview_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_subview_domain.py) | +| Shared classic cinematic and authored-post ownership | **Implemented** (default-off; controlled nested GL/Vulkan fixture passed; broader release qualification pending) | `r_rendererSharedCinematicPost` admits only a complete eligible root 2D video/audio view or complete ordered post tail. Scene packets reconcile source identity/order and retain the cinematic clock plus `_currentRender`/`_currentDepth` diagnostics. The final R6 acceptance proves one cinematic plus `_currentRender` post tail inside one capture-backed mirror on Windows GL/Vulkan, with exact parity and bidirectional atomic fallback. Root cinematic, ordinary-root post, `_currentDepth`, broader special-view, final-package, and platform/driver qualification remain open. OpenGL and Vulkan retain their mature decoder, feedback, and custom-program executors. | [Shared Classic Cinematic and Authored-Post Transaction](classic-cinematic-post-domain-modernization.md), [`ClassicCinematicPostDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicCinematicPostDomain.cpp), [`renderer_classic_cinematic_post_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_cinematic_post_domain.py) | +| Shared render-demo and Raven special-frame ownership | **Implemented** (default-off; implementation evidence recorded; release promotion pending) | `r_rendererSharedSpecialFrame` admits only a complete ordinary root 3D view identified by the active render-demo session stream, or an exact normal-root Raven special-effects controller with a nonempty blur/AL mask. OpenGL and Vulkan dispatch the complete established executors and report ownership only after exact coverage; incomplete session, source, packet, resolve, effect, or backend state preserves the classic path. Runtime breadth, clean-package, and platform qualification remain open. | [Shared Render-Demo and Raven Special-Frame Transaction](classic-special-frame-domain-modernization.md), [`ClassicSpecialFrameDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicSpecialFrameDomain.cpp), [`renderer_classic_special_frame_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_special_frame_domain.py) | +| Shared classic material-deform dependency | **Implemented** (default-off; controlled local validation passed) | `r_rendererSharedDeform` authorizes the shared classic domains to consume ordinary geometry published by the authoritative CPU material-deform path. Per-draw records seal source/result geometry, cache lifetime, evaluated inputs, freshness, consumer role, outcome, and semantic hash. Particle/particle2, skip/failure, stale cache, or provenance mismatch rejects the complete owning transaction. The controlled GL/Vulkan suite passes exact classic parity, nonzero completed ownership, visible deform delta, and named zero-commit rollback; clean-package and platform promotion remain open. | [Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md), [`ClassicDeformDomain.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ClassicDeformDomain.cpp), [`renderer_classic_deform_domain.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_classic_deform_domain.py) | +| GPU-driven GL submission / clustered Forward+ | **Experimental** | SSBO/compute/MDI, clustered-lighting, Hi-Z, and persistent/DSA paths exist at capable tiers, but remain opt-in and depend on the incomplete modern-visible path. | [`ModernClusteredLighting.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernClusteredLighting.cpp), [`ModernGLSubmitPlan.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernGLSubmitPlan.cpp), [`ModernGLExecutor.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernGLExecutor.cpp) | +| Vulkan renderer | **Experimental** | Broad stock material, interaction, decal, GUI, MD5R, and shadow support exists and can reach gameplay. It remains non-default while cubemap render targets, soft particles, debug tooling, SMP, custom programs, and other long-tail parity gaps remain. | [`vk_Backend.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/vk_Backend.cpp), [`renderer_vulkan_world_interaction_compatibility.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_vulkan_world_interaction_compatibility.py), [`renderer_vulkan_shadow_compatibility.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_vulkan_shadow_compatibility.py) | +| Backend-neutral whole-frame GPU timing | **Implemented** | OpenGL uses a delayed four-slot timestamp ring and Vulkan resolves per-slot timestamps only after its existing frame fence retires. Renderer ABI v11 exposes one common microsecond sample with backend/frame/generation identity and cumulative availability, drop, and reset diagnostics; map/device/context discontinuities invalidate old generations. Benchmark capture pairs unique valid GPU frames with high-resolution whole-renderer CPU samples without current-frame query waits. | [`GpuFrameTimingCore.h`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/GpuFrameTimingCore.h), [`RendererMetrics.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/RendererMetrics.cpp), [`VulkanGpuFrameTiming.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/VulkanGpuFrameTiming.cpp), [`renderer_gpu_frame_timing.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_gpu_frame_timing.py) | +| Replay-verifiable per-map CPU/GPU budgets | **Implemented** (locally validated; release promotion pending) | A versioned contract selects exact map/backend/profile rows, requires independent CPU/GPU samples and percentiles, and fails closed on missing GPU timing, identity drift, threshold failure, changed contracts/runtimes/artifacts, or replay mismatch. Promotion captures also bind an exact bordered-window 1280x720 display contract so archived settings cannot change the measured workload. Gameplay reports can bind either GL or Vulkan; the fixed retail baseline binds OpenGL SP plus pure, auto-joined MP roles. A schema-10 four-role stock capture and replay pass, current-build storage/campaign evidence exercises both timing backends, and the final immutable development runtime passes and replay-verifies all eight OpenGL and all eight Vulkan required-profile cases. Clean committed-source and final-package capture plus platform/driver qualification remain open. The v1 20/28 ms values are initial target ceilings, not universal performance claims. | [`renderer_per_map_budgets.json`](https://github.com/themuffinator/openQ4/blob/master/tools/validation/renderer_per_map_budgets.json), [`renderer_budget_contract.py`](https://github.com/themuffinator/openQ4/blob/master/tools/validation/renderer_budget_contract.py), [renderer validation](renderer-validation-matrix.md), [retail baseline](stock-asset-baseline.md) | +| Shadow maps | **Experimental** | Projected/point maps, CSM, cutout handling, caching, debug views, and stencil fallback exist, but `r_useShadowMap` remains opt-in/default-off pending complete promotion evidence. Vulkan now degrades a receiver to unshadowed direct lighting for a frame if both mapped and stencil ownership fail, avoiding whole-light pop-out while retaining a diagnostic and sticky stencil recovery. | [Shadow mapping](../user/shadow-mapping.md), [`Interaction.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Interaction.cpp), [`renderer_gameplay_benchmark.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_gameplay_benchmark.py) | +| Baked light grids | **Experimental** | Bake, packed atlas, visibility/distance moments, portal-aware sampling, streaming controls, and worker-assisted baking exist. They require generated per-map data and are not a stock-asset default. | [Light grids](../user/light-grids.md), [`RenderWorld_lightgrid.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/RenderWorld_lightgrid.cpp), [`draw_common.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/draw_common.cpp) | +| SMAA post-process anti-aliasing | **Implemented** | Supported post-AA path for current renderers; it remains the compatibility/low-cost choice for future temporal work. | [`draw_common.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/draw_common.cpp), [`material_smaa_edge.frag`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/shaders/material_smaa_edge.frag) | +| Internal HDR scene/post chain and bloom | **Experimental** | Floating-point scene/post, exposure, tone mapping, bloom, and color controls exist, but modern-visible handoff and complete parity qualification constrain the path. This is not true HDR display output. | [`draw_common.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/draw_common.cpp), [display settings](../user/display-settings.md) | +| True HDR display output (scRGB/HDR10) | **Missing** | Swapchain/window colorspace negotiation, paper-white UI composition, HDR screenshots, and platform qualification are not implemented. | [`VulkanDevice.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/VulkanDevice.cpp), [display settings](../user/display-settings.md) | +| GPU skeletal skinning | **Experimental** (implemented, default-off; promotion pending) | `r_gpuSkinning` admits only exactly representable four-weight MD5/MD5R surfaces. Dedicated full-precision skin attributes and canonical 12-float joint palettes feed bounded OpenGL/Vulkan compute paths which emit the ordinary `idDrawVert` ABI for depth, ambient, interactions, subviews, view models, and shadow maps. CPU positions remain current for collision, hits, decals, overlays, and software consumers; stencil volumes and every invalid/capability/allocation case use complete CPU fallback. Clean-package SP/MP images, collision/hit digests, repeated animation-heavy performance, and platform/driver evidence remain required for promotion. | [GPU animation contract](gpu-skinning-modernization.md), [`GpuSkinning.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/GpuSkinning.cpp), [`GpuSkinningGL.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/OpenGL/GpuSkinningGL.cpp), [`vk_GuiExecutor.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/vk_GuiExecutor.cpp) | +| Temporal AA / temporal upscaling | **Experimental** (implemented, default-off; release promotion pending) | Native-resolution ping-pong history, camera/depth reprojection, neighbourhood clamp, disocclusion/reactive rejection, capture/cut invalidation, and native UI composition are implemented for OpenGL and Vulkan. OpenGL supplies exact velocity for eligible rigid surfaces; skinned, particle, deform, subview, GUI, view-model, and currently unsupported Vulkan object-velocity domains have explicit conservative reactive ownership. Missing depth, policy, resources, or exact generation identity fails closed to a current-frame spatial present, while `r_temporalAA 0` retains SMAA. Dependency-light and static contracts pass; local windowed Windows GL/Vulkan gameplay evidence closes the implementation gate, while clean-package and platform/driver promotion remain open. | [Temporal presentation](temporal-presentation.md), [`TemporalPresentation.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/TemporalPresentation.cpp), [`renderer_temporal_presentation.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_temporal_presentation.py) | +| Automatic dynamic resolution | **Experimental** (implemented, default-off; release promotion pending) | `r_rendererDynamicResolution 1` consumes delayed generation-bound whole-frame GPU timestamps without a current-frame wait, applies bounded aligned drops and hysteretic recovery, and feeds the shared native-history TAAU path. Unsupported/stale timing stays at the configured safe ceiling; captures freeze or explicitly force native scale without contaminating timing/history; fixed `r_screenFraction` remains available. Local Windows OpenGL/Vulkan gameplay exercised forced scale drops; broader hardware and release-package qualification remain open. | [Temporal presentation](temporal-presentation.md), [`TemporalPresentation.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/TemporalPresentation.cpp), [`renderer_gpu_frame_timing.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/renderer_gpu_frame_timing.py) | +| Namespaced PBR materials and IBL/specular probes | **Experimental** (implemented, default-off; local Windows implementation exit passed; release promotion pending) | Opt-in namespaced material parsing, typed PBR image lifecycle handling, classic ARB2 fallback generation, packet/resource metadata, guarded OpenGL G-buffer/deferred/clustered-forward shader branches, GGX/Smith/Schlick direct lighting, and a PBR-only analytic environment baseline are implemented. Visible OpenGL PBR and probe evaluation requires `r_rendererModernVisible 1` in addition to the applicable default-off leaves; leaf controls alone do not promote a visible PBR frame. OpenGL authored light-material specular probes use a fixed eight-cubemap atlas, at most 32 frame records, and deterministic top-two selection per cluster. Probe sampling is a bounded base-mip specular approximation: it does not claim box parallax, diffuse irradiance, or probe capture. Missing, stale, unsupported, or exhausted probe data returns PBR shading to the analytic environment. Vulkan remains limited to a fail-closed opaque packed-ORM `tangentXYZ` direct-light subset with exactly one declared and active classic bump -> diffuse -> specular sequence. Duplicate, inactive, invalid, reordered, or custom-lighting interaction ownership remains entirely classic, and an admitted packed-PBR draw replaces only the final interaction submit. Vulkan has no authored-probe consumer. Stock assets contain no PBR declarations, `r_pbrMaterials` and `r_rendererReflectionProbes` default off, and `r_rendererModernQuality 0` is the one-setting Milestone F rollback. Current-source debug x64 self-tests and four-role retail-PK4 gameplay pass; the procedural fixture executes visibly on GL 3.3/4.1/4.3/4.5 and the narrow Vulkan route, while exact master-disabled captures match the leaf-disabled references. Final committed-package, platform/driver, and release review remain open. | [PBR material plan](plans/2026-05-19-pbr-material-support.md), [`Material.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Material.cpp), [`MaterialResourceTable.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/MaterialResourceTable.cpp), [`ModernSpecularProbeAtlas.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernSpecularProbeAtlas.cpp), [`ModernGLShaderLibrary.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernGLShaderLibrary.cpp), [`vk_Interactions.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/vk_Interactions.cpp), [renderer validation](renderer-validation-matrix.md) | +| Clustered decals and reflection probes | **Experimental** (implemented for OpenGL, default-off; local contract gate passed; broader visual/release qualification pending) | Authored specular probes share the bounded clustered-light grid through at most 32 records and deterministic top-two-per-cluster indices backed by an eight-cubemap atlas. Eligible clustered decals use an atomic prepare/seal ownership transaction capped at 1,024 records and 65,536 cluster references; malformed, stale, incomplete, or overflowing input publishes no ownership, so the complete affected subset remains classic. Dependency-light and engine contracts pass locally, but no broad authored probe/decal scene or RenderDoc coverage is claimed. Both domains are OpenGL-only, require their separate default-off leaf cvars plus `r_rendererModernQuality`, and do not promote GPU-driven visible lighting: `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`. Final committed-package, platform/driver, retained visual review, and release promotion remain pending. | [`ModernClusteredLighting.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernClusteredLighting.cpp), [`ModernSpecularProbeAtlas.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/ModernSpecularProbeAtlas.cpp), [renderer validation](renderer-validation-matrix.md) | +| Froxel volumetrics, SSR, and SSGI | **Experimental** (implemented as independent default-off GL/Vulkan leaves; release promotion pending) | The shared native scene-presentation tail consumes resolved scene colour/depth and bounds work to 16 view-depth froxel slices, 16 depth-normal reflection steps, and eight depth-derived diffuse-GI taps. Each leaf can run alone or with TAA; effect-only presentation does not allocate temporal histories, captures retain current-frame effects, and missing resources keep the established current/classic presentation path. `r_rendererModernQuality 0` publishes an exact zero-feature packet even when archived leaves remain on. These are explicitly screen-space approximations: there is no shadowed per-light volumetric injection, material-roughness/G-buffer SSR, off-screen reflection recovery, or world-space/multi-bounce GI claim. Current-source native/static checks, individual/combined windowed OpenGL gameplay, and combined validation-clean Vulkan gameplay pass locally; clean-package, platform/driver, and retained visual promotion remain open. | [Advanced screen-space lighting](../user/advanced-screen-space-lighting.md), [`AdvancedScreenSpaceCore.h`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/AdvancedScreenSpaceCore.h), [`draw_common.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/draw_common.cpp), [`temporal_resolve.frag`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Vulkan/shaders/temporal_resolve.frag), [renderer validation](renderer-validation-matrix.md) | +| Backend-neutral renderer contracts | **Implemented** (scoped Milestone D consumers; broader modern-renderer use pending) | Pure authored and evaluated material/pass records retain repeated stages, bounded register evaluation, inactive/no-op disposition, state semantics, and fail-closed atomic evaluation. The implemented consumers cover eligible root GUI, world ambient, interaction, fog/blend, provenance-tagged in-world GUI, special-subview, cinematic/authored-post, render-demo/Raven special-frame, and material-deform work. Special-view and nested cinematic/post records share exact root/depth identity, defer publication together, and roll the complete tree back on either-domain rejection. Shared clip/viewport conversion, semantic layouts, and typed generational buffer slices retain dependency-light coverage. The domains remain default-off with clean-package/platform promotion gates. | [Shared Classic 2D GUI Domain](classic-gui-domain-modernization.md), [Shared Classic In-World GUI Domain](classic-inworld-gui-domain-modernization.md), [Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md), [Shared Classic Interaction-Lighting Domain](classic-interaction-domain-modernization.md), [Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md), [Shared Special-Subview Transaction](classic-subview-domain-modernization.md), [Shared Classic Cinematic and Authored-Post Transaction](classic-cinematic-post-domain-modernization.md), [Shared Render-Demo and Raven Special-Frame Transaction](classic-special-frame-domain-modernization.md), [Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md), [GPU animation contract](gpu-skinning-modernization.md), [`RendererContracts.h`](https://github.com/themuffinator/openQ4/blob/master/src/renderer/RendererContracts.h), [`RendererContractsTest.cpp`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/native/RendererContractsTest.cpp) | + +## Platform, audio, and networking + +| Capability | Status | Scope and qualification | Evidence | +|---|---|---|---| +| SDL3 platform foundation | **Implemented** | Shared SDL3 window/display/input infrastructure is integrated with platform-specific bridges where required. Platform qualification is tracked separately below. | [`sdl3_backend.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/sys/sdl3/sdl3_backend.cpp), [SDL3 migration](sdl3-linux-macos-migration.md) | +| Windows x64 client/server | **Implemented** | Primary build/package target with staged client, dedicated server, renderer modules, and game modules. | [`meson_setup.ps1`](https://github.com/themuffinator/openQ4/blob/master/tools/build/meson_setup.ps1), [`platform-support.md`](platform-support.md) | +| Linux x64 client/server | **Implemented** | Native builds, Wayland/X11 paths, packaging, and physical-host stock SP/dedicated evidence exist. | [`platform-support.md`](platform-support.md), [`linux_wayland_stock_sp_smoke.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/linux_wayland_stock_sp_smoke.py) | +| macOS client/server | **Experimental** | Build/package/VM workflows and renderer corridors exist, but hardware qualification and backend/audio consistency remain narrower than Windows/Linux. | [macOS workflow](macos-vm-testing-workflow.md), [`macos_matrix_policy.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/macos_matrix_policy.py) | +| ARM64 | **Experimental** | Linux ARM64 cross/native release evidence exists; it is not yet a universal platform support claim. | [`linux_arm64_release_evidence.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/linux_arm64_release_evidence.py), [`platform-support.md`](platform-support.md) | +| OpenAL spatial audio, streaming, HRTF, and EFX | **Implemented** | The OpenAL backend includes streaming voices, device recovery, HRTF controls, EFX routing, and diagnostics. Provider/feature availability remains platform/device-dependent. | [`AL_SoundHardware.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/sound/OpenAL/AL_SoundHardware.cpp), [`AL_SoundVoice.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/sound/OpenAL/AL_SoundVoice.cpp), [`macos_openal_provider_policy.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/macos_openal_provider_policy.py) | +| IPv4/IPv6 transport and LAN discovery | **Implemented** | Dual-stack UDP, IPv6 literals/zones, DNS, fragmentation policy, and IPv6 multicast LAN discovery exist with self-tests. | [Multiplayer networking](../user/multiplayer-networking.md), [`network_ipv4_support.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_ipv4_support.py), [`network_ipv6_support.py`](https://github.com/themuffinator/openQ4/blob/master/tools/tests/network_ipv6_support.py) | +| Prediction and lag compensation | **Implemented** | Client prediction and opt-in server rewind/lag compensation exist; gameplay defaults and tuning remain game-mode policy. | [Multiplayer networking](../user/multiplayer-networking.md), [`AsyncNetwork.cpp`](https://github.com/themuffinator/openQ4/blob/master/src/framework/async/AsyncNetwork.cpp) | +| Voice chat | **Missing** | A disabled game-side capture/playback skeleton exists, but reliable transport, codec, user controls, moderation, and production validation do not. | [Multiplayer networking](../user/multiplayer-networking.md) | + +## Promotion rule + +“Implemented” is not shorthand for “release-qualified everywhere.” Renderer default promotion still requires the full evidence token defined by the [renderer validation matrix](renderer-validation-matrix.md), including clean warnings, visual/gameplay/RenderDoc/performance/presentation/rollback passes with debug features off. Retail-asset compatibility changes additionally require a passing [retail-PK4 compatibility baseline](stock-asset-baseline.md) plus the manual review gates recorded with that bundle. diff --git a/docs/dev/gl-renderer-modernization.md b/docs/dev/gl-renderer-modernization.md index 6d65c964..8ecc8914 100644 --- a/docs/dev/gl-renderer-modernization.md +++ b/docs/dev/gl-renderer-modernization.md @@ -257,7 +257,9 @@ Use `rendererUploadSelfTest` to run the ring, allocator, and static-buffer tests The metrics layer records front-end time, visibility time, scene-packet build time, render-graph build time, submit time, back-end time, present/swap time, view/entity/light counts, draw/surface/vertex/index counts, upload bytes, buffer stalls, upload-stream high-water/overflow data, scene-packet counts, packet material/resource/geometry/instance coverage, packet category and sort-key validation counters, packet-driven render-graph counts, modern-executor preparation coverage, modern shader-library readiness, modern draw-plan coverage, modern submit-plan readiness, modern clustered-light readiness, modern deferred-resolve readiness, modern forward+ readiness, modern visible-frame readiness, and selected renderer tier. -`r_rendererGpuTimers 1` samples GL timer queries when `r_rendererMetrics` is enabled and the driver exposes timer-query support. Samples are resolved on a delayed, nonblocking path; unavailable results are reported as `not-sampled` or dropped instead of stalling the CPU. Detail mode reports resolved GPU timing for the current compatibility backend command categories: +`r_rendererGpuTimers 1` enables a backend-neutral whole-frame timestamp sample on supported OpenGL and Vulkan renderers. OpenGL resolves a four-slot timestamp ring only after both query objects report availability; Vulkan reads a reused slot only after its ordinary frame fence has already retired and never requests `VK_QUERY_RESULT_WAIT_BIT`. Renderer ABI v11 reports validity, backend, source frame, reset generation, microseconds, frame latency, and cumulative resolved/unavailable/dropped/reset counters. Map loads and renderer/context/device discontinuities invalidate the old generation. The benchmark capture window records high-resolution `BeginFrame` to `EndFrame` CPU microseconds and counts each delayed GPU `(generation, frame)` only once in `OPENQ4_FRAME_TIMING_V1`. + +OpenGL detail mode continues to report the compatibility backend command categories when `r_rendererMetrics` is enabled: - 3D views - 2D/GUI views @@ -267,7 +269,7 @@ The metrics layer records front-end time, visibility time, scene-packet build ti - buffer switches - swap/present -Use `rendererGpuTimerSelfTest` to verify live timer-query support. `gfxInfo` reports whether renderer GPU timers are available and whether the cvar is enabled. +Use `rendererGpuTimerSelfTest` to verify live timestamp support without forcing completion. `gfxInfo` reports backend support, latest validity/frame/generation/microseconds/latency, cumulative availability/drop/reset counters, and the no-extra-wait contract. Metrics now also include the front-end scene-packet stream, resource-backed render graph, graph resource owner, modern GL executor path, modern clustered-light data model, deferred-lite resolve bridge, clustered forward+ bridge, visible-frame composition bridge, modern GL state-cache counters, and benchmark capture summaries. Completed `RenderWorld` views emit `ScenePacket`, `PassPacket`, and `DrawPacket` records after portal/area/scissor culling, surface extraction, special-effect surface submission, subview generation, and draw-surface sorting. Full-screen GUI views emit the same packet contract from the GUI model builder. Draw packets carry legacy sort keys, material records, first bump/diffuse/specular stage images where available, geometry counts, scissor data, shader-register availability, and cache availability. The old ARB2 command-stream translator remains as a backend fallback for direct legacy command flushes, but normal frames report `packets=frontend` in `r_rendererMetrics`. The render graph now preserves ordered packet-pass nodes and attaches explicit virtual resources such as `sceneColor`, `sceneDepth`, G-buffer attachments, `deferredLight`, `hybridSceneColor`, `postA`, `backBuffer`, and imported light-grid/cluster data. It records per-pass read/write/clear/resolve/invalidate/present edges, transient/imported resource counts, first/last resource lifetimes, and aliasable transient groups while ARB2 still owns visible pass execution by default. When `r_rendererModernExecutor 1` is enabled on a GL 3.3+ capable tier, the modern executor consumes that packet/graph data, keeps a starter VAO plus frame-constants UBO alive, validates the internal shader-library variants, builds a draw plan with program selections and state-batch counts, derives a submit plan from vertex/index cache state, builds CPU clustered-light records and UBOs for the hybrid-lighting milestones, can resolve the G-buffer subset into graph-owned deferred lighting, can submit graph-backed clustered forward+ opaque/alpha/transparent side-path draws, can compose a controlled modern visible frame from `deferredLight` and `sceneColor` through graph-owned `hybridSceneColor`, updates the frame UBO once per backend frame through the shared `GLStateCache`, and reports prepared pass/draw/plan/submit/cluster/deferred/forward+/visible-frame coverage. When `r_rendererModernSubmit 1` is also enabled, the executor issues diagnostic GL 3.3 draw calls before the legacy backend runs while masking color/depth writes so ARB2 remains the visible renderer. @@ -277,8 +279,18 @@ Metrics now also include the front-end scene-packet stream, resource-backed rend `MaterialResourceTable` turns packet material records into stable backend-facing material ids. Each entry records the source material id/name/class, blend and sort group, alpha-test state, shader-register ranges, first bump/diffuse/specular/emissive/GUI/post images, GL 3.3 classic texture slots, GL 4.3+ array/view descriptor candidates, disabled bindless fields, and an explicit fallback reason. Detailed metrics report `materialTable(...)` record, texture, class, missing-image, unsupported-feature, and fallback-reason counters, and `rendererMaterialResourceTableSelfTest` covers opaque, perforated, translucent, GUI, post, and missing-texture records. +The table also compiles materials actually referenced by a GUI packet into bounded, ordered authored-pass spans. Unlike the older one-binding-per-semantic convenience view, these spans retain repeated stage bindings, register references, exact fixed-function state, and opaque generation-checked texture-resource ids. `ClassicGuiDomain` evaluates those spans once per draw, matches them transactionally to every drawable source surface in a root 2D view, and exposes the same immutable records to OpenGL and Vulkan. With `r_rendererSharedGui 1`, each backend preflights the complete view before drawing; any unsupported view, material, state, resource, cache, descriptor, or pipeline executes the untouched classic view. The independent `r_rendererSharedInWorldGui 1` corridor instead admits only provenance-tagged `R_RenderGuiSurf` output in an ordinary 3D view and uses the world depth contract, with complete tagged-subset fallback. Both paths are default-off and independent of the aggregate GUI command used by `r_rendererModernVisible`, which remains a separate hybrid-renderer experiment and is suppressed while shared GUI ownership is requested. See [Shared Classic 2D GUI Domain](classic-gui-domain-modernization.md) and [Shared Classic In-World GUI Domain](classic-inworld-gui-domain-modernization.md). + +Materials actually referenced by world ambient packets now compile into a separate bounded world-pass pool rather than reusing the GUI spans or the one-binding convenience record. Each pass is a fixed-program surface pass with enabled depth testing, translated `LESS`/`EQUAL`/`ALWAYS` comparison, authored depth-write state, repeated order, register references, and generation-checked texture identity. `ClassicWorldAmbientDomain` transactionally matches every original surface in an eligible ambient-only 3D view, requires matching established depth packets for opaque/perforated draws, evaluates all passes once, and publishes only a complete pre-fog/post-fog plan. With default-off `r_rendererSharedWorldAmbient 1`, OpenGL and Vulkan complete all resource/state/pipeline preflight before the first shared ambient draw; a non-owned packet, unsupported surface/material/resource, missing depth prerequisite, or backend rejection leaves the complete classic ambient walker untouched. The actual consumers use sealed records and do not reread material stages or shader registers. Local development-worktree stock `maps/tools/mv2` captures now match option-off output exactly on GL and Vulkan with one owned pre-fog draw and domain hash `dc18ed8c0539bbfc`; a stock deform override reports the same named zero-draw whole-view fallback on both backends. Clean-package/platform promotion evidence remains open. See [Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md). + +`ClassicFogBlendDomain` is the separate fourth default-off shared domain. It transactionally walks every fog/blend light in one eligible ordinary root 3D view, preserves GLOBAL-to-LOCAL receiver order, evaluates every ordered active/inactive blend stage, seals fog receiver and frustum-cap work, and retains complete texgen, color/density, blend/depth/cull, geometry, and generation-checked image state. With `r_rendererSharedWorldFogBlend 1`, OpenGL and Vulkan preflight the whole phase before the first shared main-target draw; any source, geometry, material, resource, target, capacity, or backend rejection executes the untouched complete classic fog/blend walker. This corridor is independent of interaction ownership and of the older aggregate `r_rendererModernVisible` fog classification described above. Native/static validation and the controlled stock-declaration GL/Vulkan runtime suite pass with exact same-backend classic parity, nonempty ownership, material fog/blend deltas, and atomic rollback. Authored-stock fog, clean-package, and platform promotion remain open. See [Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md). + +`ClassicSubviewDomain` is the separate default-off special-subview transaction. It seals direct `SS_SUBVIEW` mirror camera/clip/culling/viewport/scissor semantics, then retains the mature complete child-view executor; it also seals the immediate child scene/copy relationship for eligible remote-camera, mirror, reflection, refraction, and x-ray 2D/cubemap color/depth captures. With `r_rendererSharedSubview 1`, OpenGL copies to the sealed 2D/cube face through `CopyFramebuffer`/`CopyDepthbuffer`, and Vulkan uses an exact type/aspect/face image-copy target, reporting capture ownership only after that transfer completes; direct ownership is reported only after the sealed child executor returns. Eligible nested trees additionally seal exact parent/root/depth records and defer ownership publication until their outermost special view completes; a rejected member returns the complete tree to the classic command path. This is a subview handoff, not shared child-scene material/light rendering. Cinematic, in-world GUI, and post forms retain their separate ownership boundaries. Native/static validation passes; fixed-camera engine-render-target parity, clean-package, and platform qualification remain open. See [Shared Special-Subview Transaction](classic-subview-domain-modernization.md). + Scene Packet V2 adds copied geometry and instance records to the packet stream. `GeometryResourceRecord` captures ambient/index buffer ids, cache offsets and byte ranges, vertex/index formats and counts, bounds, skinning/deform/upload classifications, and CPU index fallback provenance. `InstanceRecord` captures model and model-view matrices, previous matrix, entity color, shader-register ranges, palette offsets, and visibility flags for world, GUI, viewmodel, subview, remote-camera, render-demo, and bridge cases. Detailed metrics report geometry/instance record counts, record references, packet category counts, sort-key validation failures, and overflow causes, and `rendererGeometryResourceSelfTest` validates record copy, category classification, and overflow behavior. +Scene packets now also carry a per-draw classic material-deform record rather than deriving material-deform completion from `srfTriangles_t::deformedSurface`. `ClassicDeformDomain` brackets the authoritative CPU finalization and seals source/result material and geometry, cache backing and lifetime, evaluated inputs, frame freshness, consumer role, outcome, and a pointer-independent semantic hash. With default-off `r_rendererSharedDeform 1`, finalized completed or intentional-empty results can participate in the shared GUI, world-ambient, interaction mapped-caster, and fog/blend transactions; interaction/fog receivers explicitly preserve their classic source geometry. Particle/particle2, skipped, failed, stale, or mismatched records retain complete-domain rollback. Both backends consume only the resulting ordinary vertex/index stream and never rerun the deform. See [Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md). + `GLStateCache` is the central modern-state ownership point for program, VAO, buffer, texture, sampler, framebuffer, blend, depth, stencil, raster, viewport, scissor, and color-mask changes. It is invalidated explicitly when the backend hands control back to the legacy ARB2 renderer, so the modern diagnostic path never trusts state that ARB2 may have changed. `r_rendererMetrics 2` reports state-cache hits, misses, per-category misses, forced invalidations, and legacy handoff resets; `gfxInfo` reports debug-group/object-label availability and the latest cache state. Use `rendererGLStateCacheSelfTest` to exercise redundant state changes and legacy invalidation. ## Modern GL Executor Bring-Up @@ -311,7 +323,7 @@ Scene Packet V2 adds copied geometry and instance records to the packet stream. - `r_rendererModernDeferredDebug` overlays deferred light contribution, cluster id, light-count heat, or fallback/overflow pressure before swap. - `r_rendererClusterDebug` overlays clustered-light occupancy, light-count heatmap, or overflow pressure before swap. - `r_shadowMapReport` and `gfxInfo` report the modern shadow plan: mapped, stencil-fallback, and skipped lights; projected/point/CSM counts; caster/receiver totals; atlas tile use, budgeted shadow size, pixels, update cadence, descriptor readiness, and unsupported-resource fallback reasons. Deferred resolve and forward+ consume the clustered shadow descriptor policy from that plan, but mapped lights whose modern receiver sampler is not production-ready are converted to per-light stencil fallbacks so visible-frame handoff rejects them instead of double-shadowing or silently dropping shadows. -- Keeps stencil shadows, deform surfaces, unsupported material classes, GPU-palette skinning, unsupported geometry, unavailable graph resources, unavailable MRT state, unsupported forward+ blend modes, GUI draws without ready packet/cache records, subview/post/special-effect packets, and alpha-test surfaces without a diffuse texture on the legacy path with explicit fallback counters. +- Keeps stencil shadows, unsupported or unsealed material deforms, unsupported material classes, GPU-palette skinning, unsupported geometry, unavailable graph resources, unavailable MRT state, unsupported forward+ blend modes, GUI draws without ready packet/cache records, subview/post/special-effect packets, and alpha-test surfaces without a diffuse texture on the legacy path with explicit fallback counters. - Reports prepared pass/draw coverage, shader-library readiness, draw-plan coverage, submit-plan readiness, pipeline demand/skipped-pass/FBO-cache/batch stability, cache-backed versus CPU-uploaded index counts, submitted draw/fallback counts, visible depth/shadow draw and fallback counts, G-buffer draw/fallback/bandwidth/MRT/overlay state, deferred resolve pixel/light/cluster-read/fallback/GPU-timer/overlay state, forward+ opaque/alpha/transparent/sort/fallback/overdraw/cluster-read/GPU-timer state, modern visible-frame composition/owner/fallback/resource/GPU-timer state, clustered-light grid/light/reference/shadow-descriptor/overflow/build/UBO/debug-overlay state, shadow-plan mapped/fallback/skipped/budget state, resource readiness, debug-overlay readiness, GL 4.3 GPU-driven resource/compute/indirect/readback/mismatch metrics, GL 4.5 DSA texture/FBO/sampler creation, named buffer/FBO updates, buffer/texture/sampler multi-bind usage, persistent-upload fence diagnostics, bindless experiment state, and GL state-cache hit/miss behavior through `gfxInfo` and `r_rendererMetrics 2`. This gives the GL 3.3/4.1 executor a live object lifetime, frame-constant upload, packet-consumption, shader-library reflection, draw-plan generation, diagnostic GL submission, graph-backed depth and G-buffer resource execution, clustered-light preparation, shared shadow planning, deferred-lite resolve execution, clustered forward+ side-path execution, guarded hybrid visible-frame composition, conservative visibility packets, explicit max-depth Hi-Z construction, fit-for-purpose pass demand, fallback accounting, state-cache handoff safety, and metrics contract without replacing the stock Quake 4 color path by default. The GL 4.3 and GL 4.5 tiers are now active executor paths rather than capability labels: forced `r_glTier gl43` exercises SSBO-backed compute culling, clustered-bin validation, compacted indirect command generation, CPU-reference readback, and masked MDI execution, while forced `r_glTier gl45` additionally exercises DSA graph texture/FBO allocation, DSA sampler creation, named buffer/FBO updates, texture/sampler multi-bind groups, submit-batch compaction metrics, FBO attachment-cache diagnostics, and persistent-upload fence diagnostics. Driver quirk and compatibility gates now run before tier selection, so known broken UBO, MRT, timer-query, buffer-storage, and debug-context paths report deterministic fallback behavior in `gfxInfo`. Use `rendererModernGLExecutorSelfTest` to verify the analysis path, live GL object state, GPU-driven buffers, compute validation dispatch, Hi-Z reduction resources, and low-overhead binding path. Use `rendererModernVisibilitySelfTest` to verify conservative scissor/frustum/screen rejection, projected screen bounds, shadow-caster safety, the occlusion kill switch, Hi-Z candidate accounting, and no-query-stall policy. Use `rendererCompatibilityGatesSelfTest` to verify missing-feature downgrades, timer-query fallback, debug-context fallback, and synthetic driver-quirk table entries. Use `rendererGpuDrivenSelfTest` to verify GL 4.3 compute culling, generated indirect commands, CPU/GPU mismatch accounting, and masked multi-draw indirect execution. Use `rendererLowOverheadSelfTest` to verify GL 4.5 DSA resource allocation, DSA sampler setup, buffer/texture/sampler multi-bind, compacted batches, FBO attachment-cache hits, bindless experiment reporting, and upload fence diagnostics. Use `rendererVisiblePathSelfTest` to verify the visible-depth bridge, graph-backed scene/shadow resources, alpha-aware depth bindings, depth-overlay program, and conservative fallback contract. Use `rendererGBufferSelfTest` to verify graph-backed G-buffer resources, MRT setup, diffuse texture binding, packing assumptions, attachment bandwidth metrics, and G-buffer overlay readiness. Use `rendererClusterGridSelfTest` to verify light classification, cluster slicing, overflow accounting, UBO upload readiness, and debug-overlay texture generation. Use `rendererShadowPlannerSelfTest` to verify shadow-light policy, fallback accounting, budgeted map selection, render-graph shadow resource ownership, and clustered descriptor handoff. Use `rendererDeferredResolveSelfTest` to verify graph-backed deferred output, G-buffer/depth/cluster inputs, point/projected light accumulation, clustered shadow-policy consumption, light-grid contribution, fallback counters, and deferred overlay readiness. Use `rendererForwardPlusSelfTest` to verify graph-backed scene-color/depth resources, clustered forward opaque/alpha/transparent programs, transparent sort preservation, clustered-light and shadow-policy reads, and forward+ fallback counters. Use `rendererModernVisibleSelfTest` to verify the full guarded modern-visible sequence, pass-owner accounting, deferred/forward source readiness including single-source composition, hybrid scene target readiness, shadow-ready handoff, back-buffer copy, and fallback blocking. Use `rendererGLStateCacheSelfTest` to verify redundant-state suppression and legacy invalidation. Use `rendererShaderLibrarySelfTest` to verify the internal GLSL families, compact permutation keys, and reflected frame/resource bindings. Use `rendererModernGLDrawPlanSelfTest` to verify packet-to-plan classification against the current resource graph. Use `rendererModernGLSubmitPlanSelfTest` to verify submit-readiness classification, persistent/temp index-cache classification, CPU-index upload classification, and cache-fallback accounting. @@ -363,7 +375,12 @@ The active visible renderer remains ARB2 by default. The modern executor is stil - `RendererBootstrap` owns selected tier/features and marks the ARB2 bridge. - `RenderGraph` builds ordered packet-pass nodes from the front-end scene-packet frame and attaches virtual resource edges for current legacy depth, color, post-process, GUI, and present work. - `RenderGraphResources` turns graph resource records into stable imported or transient GL handles, owns transient texture/FBO allocation and validation, tracks lifetimes and conservative aliasing, and reports graph resource state without changing visible output. -- `MaterialResourceTable` turns packet material records into stable table records with texture/sampler descriptors and explicit fallback reasons, so modern draw plans can carry a material table id forward instead of treating the legacy material pointer as submit-time state. +- `MaterialResourceTable` turns packet material records into stable table records with texture/sampler descriptors and explicit fallback reasons, so modern draw plans can carry a material table id forward instead of treating the legacy material pointer as submit-time state. Its packet-referenced GUI and world compilers additionally preserve distinct complete repeated fixed-function stage spans and opaque generation-checked texture ids for the shared 2D and ambient/material domains. +- `ClassicGuiDomain` owns the default-off, whole-view fixed-function 2D GUI transaction and the separate complete provenance-tagged in-world GUI subset transaction. It evaluates register-driven pass records once per draw, hashes complete source/pass coverage, and exposes only sealed state to both backend consumers; any unsupported or incomplete root view or tagged subset remains entirely classic. +- `ClassicWorldAmbientDomain` owns the default-off, whole-view fixed-function world ambient/material transaction. It matches ambient packets and opaque/perforated depth prerequisites, evaluates register-driven surface passes once, splits them around established fog ordering, hashes complete source/pass coverage, and exposes only sealed state to both backend consumers; any non-owned or incomplete view remains entirely classic. +- `ClassicDeformDomain` owns the default-off material-deform provenance dependency used by all four shared classic domains. It preserves the established CPU algorithms, distinguishes final output from generated/skinned geometry, and makes completed, empty, source-preserving, skipped, failed, and unsupported outcomes explicit before backend ownership. +- `ClassicInteractionDomain` owns the separate default-off, whole-view unshadowed and shadow-coupled fixed-classic interaction transactions. Interaction and shadow packets carry explicit light, LOCAL/GLOBAL/TRANSLUCENT receiver, caster-chain, and source identity; one bounded preparation walk seals every accepted light stage, decomposed bump/diffuse/specular primitive, no-op, stencil volume, mapped caster/alpha stage, projected/CSM/point pass, hybrid supplement, resource policy, state, and semantic hash. GL and Vulkan preflight complete cache/update/scratch plus receiver plans before visible ownership and reconcile physical stencil replay exactly. Translucent moment-map casters and every custom, unsupported geometry/resource, special-view, overflow, or backend condition retain the untouched complete classic interaction walker. +- `ClassicFogBlendDomain` owns the separate default-off, whole-phase fixed-classic fog/blend transaction. Fog/blend packets and the source view-light walk carry exact light and GLOBAL/LOCAL receiver identity; one bounded preparation walk seals every ordered blend stage, fog receiver/cap, no-op, texgen plane, color/density, resource, compositing state, and semantic hash. GL and Vulkan preflight the complete phase before visible ownership. Every unsupported source/view/geometry/resource/target/overflow/backend condition retains the untouched complete classic fog/blend walker. Native/static and controlled GL/Vulkan image qualification pass; authored-stock fog and release promotion remain open. - `ScenePacket`, `DrawPacket`, `PassPacket`, `MaterialResourceRecord`, `GeometryResourceRecord`, and `InstanceRecord` define the backend-neutral packet contract. `RenderWorld` and GUI producers emit draw-view packets before the legacy command queue reaches the backend; command-only passes such as render-target, copy, special-effect, and present operations are tracked alongside them, and the backend command-stream translator is kept as a survival fallback. ARB2 still executes the original commands for visible output. - `ModernGLExecutor` owns the opt-in GL 3.3+ executor shell, validates packet/graph coverage, keeps Shader Library V2 alive, updates a frame-constants UBO, optionally performs masked diagnostic GL submission, optionally writes graph-backed scene/shadow depth resources through `r_rendererModernVisibleDepth`, optionally writes graph-backed opaque G-buffer resources through `r_rendererModernOpaque`, optionally resolves that G-buffer into graph-owned `deferredLight` through `r_rendererModernDeferred`, optionally writes clustered forward+ opaque/alpha/transparent side-path output through `r_rendererForwardPlus`, optionally composes a modern visible frame through `r_rendererModernVisible`, replays ready fullscreen GUI draw packets through a modern GUI overlay after composition, inventories post/copy/subview/render-demo/BSE categories with explicit modern-owner or compatibility-fallback accounting, promotes GL 4.3+ tiers into live SSBO/compute/indirect culling and generated-command work with CPU-reference validation, promotes GL 4.5+ tiers into DSA buffer/FBO/sampler updates and buffer/texture/sampler multi-bind, and leaves mixed/unsupported visible color execution on the ARB2 bridge. - `ModernClusteredLighting` builds conservative CPU light-cluster grids from packet-scene `viewLight_t` lists, uploads grid/light/index records through GL 3.3 UBOs or GL 4.3+ SSBOs for hybrid-lighting shaders, tracks uploaded/spilled/hard-overflow references separately, and exposes `r_rendererClusterDebug` occupancy, count, and overflow overlays without changing final lighting. @@ -373,7 +390,7 @@ The active visible renderer remains ARB2 by default. The modern executor is stil - `ModernGLSubmitPlan` translates draw-plan entries into submit commands from copied geometry and instance records when the legacy vertex cache exposes VBO-backed ambient buffers and either VBO-backed, frame-temp, generated, or CPU-uploadable index data, carries shader-kind/reflection metadata forward, and records missing-buffer fallback reasons for the GL3 executor. - `RendererUpload` owns static vertex/index buffer uploads, the feature-gated dynamic frame-temp stream, GL 4.5 persistent mapped defaulting with fence-retirement diagnostics, and the old vertex-cache path as a fallback. -Use `gfxInfo` to inspect the selected tier, context profile, feature flags, capability summary, driver quirk report, compatibility gates, default-promotion state, benchmark preset/threshold state, GPU-timer availability, scene-packet limits/source reporting, geometry/instance record limits, resource-graph limits, render-graph resource ownership, material resource-table readiness, modern-executor availability, visible-depth resource/debug-overlay state, G-buffer resource/MRT/debug-overlay state, deferred resolve resource/debug-overlay state, forward+ resource/program/cluster/draw/fallback state, modern visible-frame owner/composition/fallback state, modern compatibility inventory state, clustered-light UBO/debug-overlay state, modern shadow-plan policy/budget/fallback state, GL state-cache/debug-label status, Shader Library V2 status, draw-plan coverage, submit-plan readiness, GL 4.3 GPU-driven resource/validation/indirect status, GL 4.5 DSA/multi-bind/bindless-experiment status, and upload stream/fence diagnostics. Use `rendererContextLadderSelfTest`, `rendererTierSelfTest`, `rendererCompatibilityGatesSelfTest`, `rendererDefaultPromotionSelfTest`, `rendererBenchmarkSelfTest`, `rendererBenchmarkCapture`, `rendererUploadSelfTest`, `rendererGpuTimerSelfTest`, `rendererScenePacketSelfTest`, `rendererRenderGraphSelfTest`, `rendererRenderGraphResourceSelfTest`, `rendererMaterialResourceTableSelfTest`, `rendererGeometryResourceSelfTest`, `rendererGLStateCacheSelfTest`, `rendererModernGLExecutorSelfTest`, `rendererGpuDrivenSelfTest`, `rendererLowOverheadSelfTest`, `rendererVisiblePathSelfTest`, `rendererGBufferSelfTest`, `rendererClusterGridSelfTest`, `rendererShadowPlannerSelfTest`, `rendererDeferredResolveSelfTest`, `rendererForwardPlusSelfTest`, `rendererModernVisibleSelfTest`, `rendererModernCompatibilitySelfTest`, `rendererShaderLibrarySelfTest`, `rendererModernGLDrawPlanSelfTest`, and `rendererModernGLSubmitPlanSelfTest` to run the live renderer foundation tests. +Use `gfxInfo` to inspect the selected tier, context profile, feature flags, capability summary, driver quirk report, compatibility gates, default-promotion state, benchmark preset/threshold state, GPU-timer availability, scene-packet limits/source reporting, geometry/instance record limits, resource-graph limits, render-graph resource ownership, material resource-table readiness, shared classic-GUI readiness/hash/backend coverage, shared cinematic/authored-post readiness/timing/hash/backend coverage, shared render-demo/Raven special-frame readiness/hash/backend coverage, shared world-ambient readiness/phases/hash/backend coverage, shared interaction light/receiver/primitive/shadow/map readiness, per-map static/dynamic/alpha/translucent features, shared fog/blend light/receiver/stage/cap readiness/hash/backend coverage, modern-executor availability, visible-depth resource/debug-overlay state, G-buffer resource/MRT/debug-overlay state, deferred resolve resource/debug-overlay state, forward+ resource/program/cluster/draw/fallback state, modern visible-frame owner/composition/fallback state, modern compatibility inventory state, clustered-light UBO/debug-overlay state, modern shadow-plan policy/budget/fallback state, GL state-cache/debug-label status, Shader Library V2 status, draw-plan coverage, submit-plan readiness, GL 4.3 GPU-driven resource/validation/indirect status, GL 4.5 DSA/multi-bind/bindless-experiment status, and upload stream/fence diagnostics. Use `rendererContextLadderSelfTest`, `rendererTierSelfTest`, `rendererCompatibilityGatesSelfTest`, `rendererDefaultPromotionSelfTest`, `rendererBenchmarkSelfTest`, `rendererBenchmarkCapture`, `rendererUploadSelfTest`, `rendererGpuTimerSelfTest`, `rendererScenePacketSelfTest`, `rendererRenderGraphSelfTest`, `rendererRenderGraphResourceSelfTest`, `rendererMaterialResourceTableSelfTest`, `rendererClassicGuiDomainSelfTest`, `rendererClassicCinematicPostDomainSelfTest`, `rendererClassicSpecialFrameDomainSelfTest`, `rendererClassicWorldAmbientDomainSelfTest`, `rendererClassicInteractionDomainSelfTest`, `rendererClassicFogBlendDomainSelfTest`, `rendererGeometryResourceSelfTest`, `rendererGLStateCacheSelfTest`, `rendererModernGLExecutorSelfTest`, `rendererGpuDrivenSelfTest`, `rendererLowOverheadSelfTest`, `rendererVisiblePathSelfTest`, `rendererGBufferSelfTest`, `rendererClusterGridSelfTest`, `rendererShadowPlannerSelfTest`, `rendererDeferredResolveSelfTest`, `rendererForwardPlusSelfTest`, `rendererModernVisibleSelfTest`, `rendererModernCompatibilitySelfTest`, `rendererShaderLibrarySelfTest`, `rendererModernGLDrawPlanSelfTest`, and `rendererModernGLSubmitPlanSelfTest` to run the live renderer foundation tests. The automated safe validation matrix lives in [renderer-validation-matrix.md](renderer-validation-matrix.md) and can be run with: diff --git a/docs/dev/gpu-skinning-modernization.md b/docs/dev/gpu-skinning-modernization.md new file mode 100644 index 00000000..e9ccb23d --- /dev/null +++ b/docs/dev/gpu-skinning-modernization.md @@ -0,0 +1,194 @@ +# Shared Renderer Contracts and GPU Animation + +This document is the authoritative implementation and qualification boundary +for Milestone C of the [idTech 5-level modernization roadmap](idtech5-modernization-roadmap.md). +The capability is deliberately reversible: `r_gpuSkinning 0` retains the +established CPU renderer path, and every unsupported or invalid surface falls +back as a whole rather than mixing partially transformed vertex data. + +## Delivered contract + +The renderer now has a small backend-neutral contract layer for the semantic +data that OpenGL and Vulkan must agree on: + +- ordered material/pass records retain stage order, repeated texture + semantics, condition and color registers, texture matrices, texgen and + vertex-color policy, blend/depth/cull/color-mask state, alpha test, polygon + offset, and program-family identity; +- a canonical OpenGL clip convention converts explicitly to Vulkan's zero-to-one + depth interval and negative-height viewport convention; +- semantic vertex layouts describe the 64-byte `idDrawVert` ABI and a separate + full-precision skin binding without embedding GL or Vulkan formats; +- typed, generational buffer handles and checked slices reject stale, expired, + wrong-kind, empty, or overflowing ranges before a backend sees a native + object. + +These records are intentionally narrower than complete modern-frame ownership. +They establish the shared vocabulary required by both current backends; +Milestone D remains responsible for making complete classic material domains +consume the pass representation. + +## Exact four-weight corridor + +GPU animation uses a dedicated 32-byte record per vertex: + +| Field | Format | Purpose | +|---|---:|---| +| joint indices | `uint32[4]` | Full joint indices; no 8-bit joint ceiling | +| joint weights | `float32[4]` | Exact authored weights, including signed MD5R values | + +Joint palettes use three row-major `vec4` rows (12 floats) per joint, matching +`idJointMat`: translation is stored in elements 3, 7, and 11. The format does +not reuse `idDrawVert::color` or `color2`, so authored diffuse vertex colors +survive unchanged. + +Classic MD5 meshes are admitted only when every final output vertex, including +mirrored vertices, is exactly representable with at most four meaningful +influences. The implementation does not truncate or renormalize a residual +tail. Packed MD5R data preserves its signed implicit fourth-weight rule and is +normalized into the same sidecar contract. Rigid and one- through four-weight +vertices share the same validation path. + +Invalid counts, non-finite data, out-of-range joints, unsupported skin-scale or +topology variants, allocation exhaustion, stale generations, unavailable +compute support, and backend submission failures all select the complete CPU +path. The fallback reason remains visible in diagnostics and is never treated +as successful GPU admission. + +## Ownership and CPU invariants + +Immutable bind-pose and four-weight sidecars are owned by the source render +mesh. A dynamic triangle surface only references those arrays. Its current +joint palette is copied into triangle-owned storage with separate logical count +and allocation capacity, then retired through the established deferred +triangle lifetime. + +Reference surfaces may borrow the immutable contract but never its allocation +marker. Topology-changing copies, reversed back sides, decals, overlays, +deforms, cache serialization, and frame-arena copies clear or explicitly +reference the metadata so they cannot retain a stale pointer or double-free a +palette. + +GPU admission does not change gameplay geometry ownership: + +- CPU positions and bounds remain current for collision, traces, hit + detection, decals, overlays, and software/debug consumers; +- compatible MD5 surfaces can avoid CPU normal/tangent skinning while the GPU + produces the complete visible `idDrawVert` stream; +- stencil shadow volumes remain explicitly CPU generated and bind through the + existing shadow-cache path; +- disabling the feature restores the previous full CPU deformation behavior. + +No network, save, demo, material, model, or stock-asset format changes are +introduced. The mirrored `srfTriangles_t` ABI in the companion game-library +repository is updated in lockstep because game code allocates that structure +directly. + +## Backend execution + +Both backends consume the same bind-pose, skin, and palette contract and emit +an ordinary 64-byte `idDrawVert` stream. Existing visible passes therefore do +not need skin-specific material permutations. + +### OpenGL + +On compute/SSBO-capable contexts, bounded frame-ring slices hold source +vertices, skin records, palettes, and output. A 64-thread compute dispatch +copies non-deformed fields verbatim and transforms position, normal, and both +tangents. A shader-storage-to-vertex barrier publishes the result before the +existing ambient cache is bound. Any missing capability or allocation returns +to CPU cache creation. + +### Vulkan + +The per-frame vertex ring also carries storage-buffer usage. Eligible surfaces +are deformed in a bounded compute prepass before the view's graphics state is +established, and the ordinary geometry memo points later depth, ambient, +interaction, subview, view-model, and shadow-map draws at the computed slice. +Host-to-compute and compute-to-vertex barriers make the ownership transition +explicit. The separate stencil-shadow binding never consumes the computed +stream. + +## Controls and diagnostics + +`r_gpuSkinning` is archived, experimental, and defaults to `0`. It is safe to +toggle for an A/B capture; a renderer restart is not required. `gfxInfo` +reports backend availability and the shared counters distinguish eligible, +admitted, dispatched, and CPU-fallback work, including vertices, joints, +palette bytes, and fallback reasons. + +The dependency-light `RendererContractsTest` validates ordered/repeated passes, +clip conversion, viewport orientation, semantic layouts, and typed buffer +slices. `rendererContractsSelfTest` and `rendererGpuSkinningSelfTest` exercise +the same contracts through either renderer module. The validation matrix must +observe the same versioned semantic markers for OpenGL and Vulkan; a skipped +GPU-animation contract test is not a pass. + +## Acceptance procedure + +Use a fresh bordered-window 1280x720 runtime and engine-written screenshots. +Never compare OpenGL directly with Vulkan pixel-for-pixel: capture a CPU +reference for each backend, then compare its GPU run against that reference +from a pose frozen at launch with `g_stopTime 1`. + +Required gameplay coverage is: + +- SP `game/airdefense2` and `game/mcc_landing`, including animated characters, + view models, interactions, decals, subviews, and mapped shadows; +- pure, auto-joined MP `mp/q4dm1` with the stock package contract; +- one explicit stencil-shadow run proving the CPU fallback counter and image; +- an animation-heavy `evaluateMPPerformance` workload, repeated at least three + times per backend with GPU skinning off and on. + +The paired performance report must prove that GPU work was actually admitted, +retain the per-run source/package/GPU/driver identity, and report whole-frame +CPU percentiles together with the skinning-specific CPU time and fallback +inventory. A change in collision or hit results, an unclassified fallback, a +missing screenshot, or a performance result without admitted vertices fails +the gate. + +Capture each backend as its own CPU/GPU pair, changing only the launch-time +feature switch. For example: + +```powershell +python tools/tests/renderer_gameplay_benchmark.py --cases sp-airdefense2,shadow-character-airdefense2,sp-mcc-landing --render-api gl --shadow-presets stencil --set-launch-cvar g_stopTime=1 --set-launch-cvar r_gpuSkinning=0 --output-dir .tmp/renderer-gameplay/gpu-skinning-cpu-gl +python tools/tests/renderer_gameplay_benchmark.py --cases sp-airdefense2,shadow-character-airdefense2,sp-mcc-landing --render-api gl --shadow-presets stencil --set-launch-cvar g_stopTime=1 --set-launch-cvar r_gpuSkinning=1 --output-dir .tmp/renderer-gameplay/gpu-skinning-gpu-gl +python tools/validation/gpu_skinning_evidence.py .tmp/renderer-gameplay/gpu-skinning-cpu-gl/renderer_gameplay_benchmark_report.json .tmp/renderer-gameplay/gpu-skinning-gpu-gl/renderer_gameplay_benchmark_report.json --minimum-cpu-p95-improvement 1.0 --output .tmp/renderer-gameplay/gpu-skinning-gl-evidence.json +``` + +Repeat the same commands with `--render-api vk` and distinct output paths. The +strict pair pins `--shadow-presets stencil` so its required CPU stencil-volume +fallback is deterministic; run a separate backend-local pair with +`--shadow-presets mapped` for the shadow-map caster corridor rather than +claiming that the stencil pair covers it. +Capture `mp-q4dm1-listen` as its own pure, auto-joined CPU/GPU campaign without +`g_stopTime`; retain its hashes, logs, and human engine-screenshot review as +separate promotion evidence rather than feeding live multiplayer frames to the +strict frozen-image pair verifier. The verifier reads and hashes the retained +engine logs and engine-written TGA +screenshots, requires a clean identical runtime/source identity, verifies the +launch-time `r_gpuSkinning=0/1` pairing, rejects missing or unnamed fallbacks, +requires admitted GPU work and an observed CPU stencil-volume fallback, and +defaults to at least a one-percent reduction in CPU P95 for every paired role. + +## Evidence status + +The 2026-08-20 Windows x64 implementation gate passed the full MSVC build for +the client, dedicated server, OpenGL/Vulkan renderer modules, and SP/MP game +modules; all six Meson native tests; the GPU-skinning, MD5R/Vulkan compatibility, +shader-header-pin, evidence-verifier, and validation-wiring checks; the windowed +OpenGL foundation self-test case; and the staged windowed Vulkan startup case +with the identical renderer-contract and GPU-skinning success markers. A +windowed `game/airdefense2` SP gameplay smoke run also passed the renderer +budgets and wrote engine-target screenshots on both backends. Its final +diagnostics recorded 942 admitted/prepared surfaces on OpenGL and 1,662 on +Vulkan, with compute available and no validation, allocation, stale-palette, +or backend-unavailable fallback. + +Those runs were integration evidence from a dirty source pair, not a release +qualification. Stock-package screenshot, pure-MP, +animation-heavy performance, and cross-platform/driver evidence remain +release-promotion evidence until they have been captured from clean committed +source and a freshly staged final package. That separation does not weaken the +runtime rollback: the feature remains opt-in and fail-closed while promotion +evidence is accumulated. diff --git a/docs/dev/high-framerate-rendering-plan.md b/docs/dev/high-framerate-rendering-plan.md index 1968d7a5..d8f0ebad 100644 --- a/docs/dev/high-framerate-rendering-plan.md +++ b/docs/dev/high-framerate-rendering-plan.md @@ -248,43 +248,48 @@ Interaction notes from current validation: Repeated-state rendering is not enough for "true" high-framerate support. Motion smoothness requires interpolation. -Status: `Narrow first-person slice implemented on the current companion game-library branch; broader Phase 3 work and release qualification remain in progress` as of `2026-08-13`. +Status: `First-person, eligible moving-world, and skeletal presentation implemented; multiplayer actors and bespoke visual owners remain deliberately scoped out, and release qualification remains in progress` as of `2026-08-20`. Implemented scope: - SP and MP keep transient previous/current samples for first-person camera origin, camera axis, and FOV, plus the complete first-person viewmodel origin and axis. The local player, or the currently spectated player in MP, consumes those samples while preparing each `Draw()`. - The presentation fraction is clamped from `0` to `1` across one authoritative `60 Hz` usercmd interval. This is previous-to-current interpolation, not extrapolation: it deliberately accepts at most one simulation tic of visual latency (about `16.7 ms`) rather than predicting a camera pose through collision or correction boundaries. +- Skeletal animation uses that same frozen fraction, rounded onto the actual `previousTime` to `time` interval so alternating `16/17 ms` authoritative tics cannot put the joints ahead of the interpolated root or camera. An eligible animated entity remains in the presentation list while its root is stationary, and an animated client model bound to an otherwise stationary owner can admit that owner solely so the attachment is updated. +- `idAnimator::CreatePresentationFrame()` evaluates the arbitrary-millisecond pose into an animator-owned aligned scratch buffer. It restores the authoritative joint pointer, transform time, stopped-animation flag, and stateful joint-modifier data before returning; gameplay joint queries, save data, networking, collision, and the next simulation tic therefore never observe the draw-only pose. Active AF snapshots and angular-velocity joint modifiers cannot be rewound exactly and deliberately copy the authoritative pose instead. +- Each skeletal presentation copy suppresses the normal renderer callback, supplies the scratch joints, and recomputes joint bounds. While that copy and its attachments are being submitted, joint-bound models and effects read the already-prepared scratch pose without evaluating animation again. The same held-pose rule covers first-person weapons, their client entities, and the lightning beam. Joint-query validity ends after submission, while the animator-owned buffer remains alive for the renderer's shallow copy and the scene clock is cleared after `RenderPlayerView()`. - Repeated-state draw preparation recalculates the render view and resubmits a copied viewmodel render entity only. It does not rerun player or weapon `Think()`, scripts, physics, networking, sound, or other gameplay work. - Client entities bound to view model joints — muzzle-flash and beam BSE effects, attached weapon models — are re-anchored in the same pass through `rvViewWeapon::UpdatePresentationClientEntities()`. Their bind transform resolves through the view model's physics pose, so leaving them out made them sit on the last authoritative pose while the gun itself was drawn interpolated, and they visibly unstuck from the muzzle during view rotation. Only the transform is re-pushed: BSE spawns and ages only when a def's game time advances past its service time, so repeating the authoritative time moves an effect without respawning or double-ageing its particles, and effect lifetime stays in `rvClientEffect::Think()`. - Presentation samples and clock anchors are transient. The SP and MP `Save()` paths do not write them, and restore/map initialization reseeds them instead of changing the save-game or network contract. - Live first-person `renderView.time` uses the presentation clock, while demo playback and timedemo remain on authoritative simulation time. - Cadence/discontinuity checks snap instead of blending across missing tics, teleports, or other large changes. MP also disables interpolation while active prediction-error smoothing is correcting the viewed player. -- World entities that moved during the last authoritative tic are re-anchored onto the same presentation time as the camera (`g_presentationInterpolation`, default on). `idGameLocal::SamplePresentationEntityPoses()` takes one transform sample per tic across `spawnedEntities` and lists the ones that moved; `UpdatePresentationEntityPoses()` re-pushes those render entities from the interpolated transform on each draw frame. Only the transform is re-pushed — physics, clip models, scripts, animation and sound are untouched, and the authoritative render entity is restored before anything gameplay-facing can read it through `GetPosition()`. `idLight` also carries its `renderLight`, and client entities bound to an interpolated entity follow through the same `UpdatePresentationTransform()` walk the view model uses. +- World entities that moved during the last authoritative tic are re-anchored onto the same presentation time as the camera (`g_presentationInterpolation`, default on). `idGameLocal::SamplePresentationEntityPoses()` takes one transform sample per tic across `spawnedEntities` and lists the ones that moved; `UpdatePresentationEntityPoses()` re-pushes copied render entities from the interpolated transform on each draw frame. Physics, clip models, scripts, authoritative animation state, and sound are untouched. Eligible skeletons are evaluated only into the draw scratch described above, and the authoritative render entity is restored before anything gameplay-facing can read it through `GetPosition()`. `idLight` also carries its `renderLight`, and client entities bound to an interpolated entity follow through the same `UpdatePresentationTransform()` walk the view model uses. This closes the mover-relative case. The camera reaches the current authoritative pose one tic late, so a lift or tram drawn at its authoritative pose separated from the rider's eye by a whole tic of its own travel and snapped back every tic. The result was a 60 Hz sawtooth of everything the player was riding — invisible against static geometry at any speed, and filling the entire view inside a tram car (`game/storage2`, `game/tram1b`). - Two rules keep the scene on one clock rather than trading one mismatch for another. `idPlayer::CanInterpolatePresentationView()` refuses to interpolate the eye when its carrier — the vehicle being driven, the bind master, or the entity under its feet — moved but cannot be interpolated this frame. `idPlayer::IsPresentationViewInterpolated()` gates the entity pass on the drawn camera actually being the interpolated first-person pose, so cinematic, private-camera and third-person frames put the whole scene back on the authoritative clock together. The view model opts out because `UpdatePresentationWeapon()` already owns it, and actors opt out in multiplayer so remote players keep being drawn where the server rewinds them to. + Two rules keep the scene on one clock rather than trading one mismatch for another. `idPlayer::CanInterpolatePresentationView()` refuses to interpolate the eye when its carrier — the vehicle being driven, the bind master, or the entity under its feet — moved but cannot be interpolated this frame. `idPlayer::IsPresentationViewInterpolated()` gates the entity pass on the drawn camera actually being the interpolated first-person pose, so cinematic, private-camera and third-person frames put the whole scene back on the authoritative clock together. The view model opts out because `UpdatePresentationWeapon()` already owns it. Every multiplayer actor also opts out: remote players must remain where server rewind expects them, while the local hidden body and its world weapon share the same actor/attachment presentation boundary. Their first-person stencil shadows therefore still advance on authoritative tics; moving those shadows independently requires a dedicated shadow-only presentation owner rather than admitting the live actor to the generic transform pass. The samples are taken from `renderEntity.origin/axis` — the composed *visual* transform — not from the physics pose. That distinction is load-bearing rather than cosmetic: `rvVehicleAnimated` (the walker) keeps its entire heading in `idActor::viewAxis` and exposes it through `GetPhysicsToVisualTransform()`, while its physics axis barely turns, so interpolating physics left the vehicle body snapping once a tic underneath a smoothly turning camera. Every actor carries a `modelOffset` the same way, and AF-driven entities compose their visual transform there too. Sampling the visual transform covers all of them, and means `UpdatePresentationPose()` must not apply the physics-to-visual step a second time. Validation recorded for this slice: -- The SP/MP static parity and draw-boundary contract in `openQ4-game/tools/tests/presentation_interpolation_contract.py` passes. -- Both SP and MP GameLib builds pass. +- The expanded SP/MP static parity and draw-boundary contract in `openQ4-game/tools/tests/presentation_interpolation_contract.py` passes. It pins the shared skeletal clock, scratch-state restoration and lifetime, stationary animation eligibility, joint-bound effects, viewmodel and client-model submission, authoritative fallbacks, and the multiplayer actor boundary. +- Both standalone and integrated SP/MP GameLib builds pass. The full `tools/validation/validate_push.ps1 --build-gamelibs` profile also passes all Python checks, both renderer/engine builds, and all three native tests. - A windowed active-gameplay SP pass measured `86.2 Hz` presentation while simulation remained at `60 Hz`. -- An MP listen-server pass loaded the game module and map and produced an engine-side screenshot. The auxiliary loopback-client harness did not complete its requested client capture, although no fatal engine error was observed; this is not evidence of a successful two-client or remote-client validation pass. +- A staged bordered-window OpenGL campaign-transition pass crossed `game/mcc_2`, both `game/storage1` states, and `game/storage2`, asserted the final `game/tram1` state, and wrote an engine-side screenshot. It measured `189.4 Hz` presentation with `60.0 Hz` simulation and reported no renderer, fatal, or engine-error diagnostics. +- Post-change staged OpenGL stencil-shadow smokes passed on `game/airdefense2` at `202.0 Hz` and on an `mp/q4dm1` host plus loopback client at `148.0 Hz` and `116.1 Hz`; every role retained `60.0 Hz` simulation, wrote an engine-side screenshot, exited cleanly, and left the hashed runtime immutable. These automated captures establish clean high-refresh execution, not subjective motion quality. +- A bordered-window `1280x720` OpenGL stencil-shadow host plus loopback-client pass targeted `240 Hz` presentation on `mp/q4dm1`. Both roles completed, wrote engine-side screenshots, reported no OpenGL, snapshot, or fatal diagnostics, and measured `241.1 Hz` on the host and `240.3 Hz` on the client. The run exercises the corrected client projectile/viewweapon replay paths; projectile visibility during live combat and the deliberately deferred local body/world-weapon stencil-shadow cadence still require human-motion review of the final package. +- A second scripted no-input pass added a skill-3 bot and readied the match. The server recorded a rocket kill, the client captured an active-combat frame with a coherent first-person weapon, both roles exited cleanly, and neither log reported OpenGL, snapshot, or fatal diagnostics. This is a bounded projectile/reconciliation smoke test, not a substitute for judging motion continuity by eye. Explicitly deferred: -- remote-player and AI interpolation in multiplayer, deliberately excluded so drawn positions keep matching what lag compensation rewinds to -- entities whose visuals are not driven by the render entity transform: multi-body AF presenters that own their own `modelDefHandles[]`, and the weapon's own muzzle-flash `renderLight_t`, which still moves on the simulation clock +- multiplayer actor interpolation, deliberately excluded so remote-player positions keep matching what lag compensation rewinds to. This currently also leaves the local player's body and world-weapon stencil shadows on the authoritative tic, so their motion can step at high presentation rates; that is an expected limitation of this phase, not a renderer frame-pacing fault +- entities whose visuals are not driven by the render entity transform: multi-body AF presenters that own their own `modelDefHandles[]`, unbound animated client models, and the weapon's own muzzle-flash `renderLight_t`, which still moves on the simulation clock. Active AF poses and angular-velocity joint modifiers also retain their authoritative joint pose because their state cannot be evaluated historically without changing gameplay state - world-space BSE effects that are not client entities bound to an interpolated entity, plus trail, fracture and other bespoke visual owners that rebuild geometry per tic rather than moving a transform - alternate camera, security-camera, portal-sky and cinematic-camera interpolation. These are not vibration sources today because `IsPresentationViewInterpolated()` puts the whole scene back on the authoritative clock whenever one of them is the drawn view, but it does mean high-refresh smoothing is unavailable in those modes -- animated interpolated entities still drop their cached dynamic model on every draw frame, because the renderer's `r_useRepeatedStateReuse` transform-only path requires `renderEntity.callback == NULL`. This is a CPU cost on a small set of entities, not a correctness gap; extending the reuse path to callback entities would need a way to prove the callback is time-only -- **skeletal joints are still evaluated on the simulation clock.** `idEntity::UpdateRenderEntity()` calls `animator->CreateFrame( gameLocal.time )`, so an interpolated entity moves its root smoothly while its joints step at 60 Hz. Usually invisible, because root motion dominates. It is visible where a *camera* is anchored to a joint of the thing it is looking at: `rvVehiclePosition::GetPosition()` builds the driver's eye from `GetJointTransform( eyeJoint, gameLocal.GetTime() )`, so the eye is interpolated across the vehicle's animation while the cockpit around it is drawn at the authoritative joint pose. Residual amplitude is one tic of eye-joint travel — the walk-cycle bob, not the vehicle's heading. `idAnimator::CreateFrame()` is a pure function of the requested time and keyframe-interpolates at arbitrary milliseconds, so passing the presentation clock there would close this and smooth all skeletal animation at high refresh; the open question is the cached `lastTransformTime` / `joints[]` state it shares with gameplay joint queries, which is why it has not been done as part of this pass +- a skeletal pose whose joint-content hash changes must regenerate its cached skinned model on that draw frame. Transform-only entities still take the renderer's repeated-state reuse path; the extra CPU work is bounded to actively presented skeletons and is an intentional cost of smoothing their joints - same-fire-frame weapon FX/tracer compensation and other presentation-time gameplay-adjacent cosmetic retraces - raw-input compensation or presentation bias/extrapolation; the engine's optional presentation-input sampler is not consumed by the current game-library slice -- a successful full loopback-client capture, human SP/MP feel testing, cut-heavy cinematic coverage, Apple-platform testing, and dedicated high-refresh display/hardware qualification +- human SP/MP motion and vehicle-cockpit review, cut-heavy cinematic coverage, Apple-platform testing, and dedicated high-refresh display/hardware qualification. Automated host/client captures now pass, but they cannot establish subjective continuity or the small nonlinear residual possible when a joint-anchored camera and a rotating hierarchy are sampled through different composition paths Phase 3 exit criteria remain unmet until the first-person path passes those manual and platform checks and the broader visual scope is either implemented or deliberately scoped out for release. diff --git a/docs/dev/idtech5-modernization-roadmap.md b/docs/dev/idtech5-modernization-roadmap.md new file mode 100644 index 00000000..56eacf79 --- /dev/null +++ b/docs/dev/idtech5-modernization-roadmap.md @@ -0,0 +1,732 @@ +# idTech 5-Level Modernization Roadmap + +This document turns the project's modernization goal into an implementation +order that preserves the shipped Quake 4 asset and gameplay contracts. It is +based on the official Doom 3 BFG Edition source snapshot (`1caba197`) pinned in +the [source-provenance inventory](source-provenance.md), the current openQ4 +[capability matrix](engine-capability-matrix.md), and the stock-asset acceptance +harness. + +Doom 3 BFG is not the full idTech 5 or idTech 6 source tree. It is a late +idTech 4 branch containing several idTech 5-era architectural ideas: a parallel +job manager, jobbed renderer front end, GPU skinning, explicit GPU buffers, +binary/generated resources, preload manifests, GPU timing, automatic resolution +scaling, a refined front-end/back-end render command stream, and a newer +session/network stack. Those subsystems are useful references, but importing the +whole engine would replace Quake 4 contracts rather than modernize them. + +## Non-negotiable compatibility boundary + +Modernization is acceptable only when all of these remain true: + +- Retail Quake 4 PK4s remain the source of truth. Players are not required to + convert, unpack, or patch them. +- Classic material, decl, GUI, map, MD5/MD5R, BSE, sound, save, demo, and + protocol behavior remains available as the fallback and comparison path. +- Generated data is a disposable, versioned cache under `fs_savepath`, keyed to + its source identity (including the containing PK4 checksum where relevant). + Missing or invalid cache data falls back to the retail source. A cache never + becomes downloadable content or part of the pure-package authority set. +- New material features use namespaced, opt-in syntax. A stock material is not + silently reinterpreted as PBR content. +- Wire, save, and demo format changes are explicitly versioned. Protocol 2.41 + compatibility is not changed by an internal refactor. +- Dedicated builds do not acquire renderer, presentation, or client-only job + dependencies. +- Every promoted default passes the four-role stock baseline and a human review + of engine-written screenshots. MP validation explicitly uses + `+set ui_autoJoin 1`, `+set si_pure 1`, and + `+set net_serverAllowServerMod 0` unless the test is specifically for the join + menu or non-pure behavior. + +## What openQ4 already uses from the BFG lineage + +The most directly reusable BFG work is not hypothetical. openQ4 already carries +audited BFG-lineage code in these areas: + +- binary image loading/writing, image options, image programs, color-space + conversion, and DXT encode/decode; +- renderer image management and intrinsic-image support; +- the newer sound world/emitter/voice/sample architecture, with the OpenAL + implementation arriving through the documented RBDOOM lineage; +- small idlib utilities such as static strings, swapping, and sorting. + +The authoritative inventory currently records 37 BFG-headered files. Any +additional incorporation must retain the upstream notices, update that inventory +and the applicable Additional Terms coverage, and record any intermediate +lineage. This roadmap is technical guidance, not a legal conclusion. + +"Readily available" below means that the named implementation is present in the +audited official snapshot and can be studied or adapted without reverse +engineering. It does not mean drop-in: several paths assume Win32, PS3/SPU, +trusted pre-generated data, 32-bit offsets, or assert-only validation. New code +must use openQ4's portable interfaces and fail-closed input rules. + +## Current implementation state (2026-08-24) + +This table is the dated delivery snapshot for this roadmap. **Implemented** +means the compatibility-safe foundation is present and covered by the cited +project evidence and may still be default-off while release promotion is +pending; **Experimental** means the scoped implementation remains incomplete, +trial-grade, or below its stated evidence floor; **Partial** means only part of the required +contract exists; and **Planned** means the roadmap is still design guidance. +The [engine capability matrix](engine-capability-matrix.md) remains authoritative +when a status differs or a narrower qualification is needed. + +| Workstream | State | Current boundary and next work | +|---|---|---| +| Stock-compatibility and security foundation | **Implemented** | Protocol 2.41 preservation, pure-MP game-module containment, bounded malformed-input handling with immediate session teardown, challenge entropy, rcon2, private-CVar redaction/remote authority, HTTP(S)-only transfer policy, source-provenance auditing, archived MP auto-join test policy, and the four-role retail-PK4 evidence harness are present. Release promotion still requires a clean source pair, final-package capture, and retained human review. | +| Audited BFG-lineage image, sound, and idlib work | **Implemented** | The existing 37-file BFG inventory is tracked with source lineage and Additional Terms. Further imports must update the same manifest and notices. | +| PBR material authoring and advanced-lighting path | **Experimental; implemented default-off; local Windows implementation exit passed; release promotion pending** | Namespaced parsing, typed color/data image usage, classic ARB2 fallbacks, packet/resource diagnostics, guarded OpenGL G-buffer/deferred/clustered-forward shader branches, GGX direct lighting, and a PBR-only analytic environment baseline cover the current PBR plan. OpenGL also has an authored, bounded specular-probe consumer: eight atlas cubemaps, no more than 32 records, deterministic top-two selection per cluster, and analytic fallback for every incomplete case. It is base-mip specular approximation only, with no parallax or diffuse-irradiance claim. Vulkan remains deliberately limited to opaque packed-ORM `tangentXYZ` direct lighting and has no authored-probe consumer. PBR, probe, and decal leaf controls default off, while `r_rendererModernQuality 0` rolls every Milestone F domain back at once. Current-source debug x64 self-tests, four-role retail-PK4 gameplay, controlled GL 3.3--4.5 and Vulkan fixture runs, visible PBR ownership, and exact master rollback pass locally; final committed-package, platform/driver, and release review remain open. | +| GPU measurement and dynamic resolution | **Implemented measurement; experimental default-off controller** | OpenGL and Vulkan publish the same delayed, non-blocking whole-frame microsecond result through renderer ABI v11, including frame/generation identity and availability/drop/reset counters. The Milestone E controller binds each retired sample to its source scale, rejects stale identity, drops quickly above budget, raises only after an under-budget streak, and keeps bounded aligned scene dimensions. Captures freeze or deliberately force native scale without feeding their timing into control. Current-build required-profile captures exercise both timing backends and forced controller drops; clean committed-package and platform/driver qualification remain open. | +| General job system | **Implemented foundation** | The engine-owned [portable bounded job service](parallel-job-system.md) provides sleepable workers and waits, bounded list/job/dependency admission, low/normal/high priority aging, dependency ordering, cooperative cancellation, deterministic inline execution, metrics, and dedicated-safe lifecycle ownership. Threaded and synchronous native coverage passes. Its first production consumer is the learned level-load read/PK4-inflate and framing/integrity pipeline; live asset parsing, renderer/audio upload, and renderer-front-end work remain with their established owners. Current-build stock validation also produced identical jobs-on/off storage1 screenshots and game state, completed jobs-on/off OpenGL plus jobs-on Vulkan repeated-map campaigns with clean shutdown markers, and recorded five deterministic synchronous dedicated-server exits. Clean final-package recapture remains a separate release-promotion gate. | +| Generated caches, streaming, and learned preload manifests | **Implemented and locally validated; release promotion pending** | Successful loads produce exact map/mode/entity-filter/search/PK4/settings manifests. Matching loads use bounded cancellable read/PK4 inflation followed by worker-safe typed framing and integrity validation, publish an immutable generation/source-identity DTO, and let the established main owner parse, adopt, and upload. Transactional static/MD5/MD5R model, classic-proc world, collision, and animation-v3 caches are private to `fs_savepath` and fall back to authoritative VFS sources. This is learned level-load preparation, not general asynchronous asset decode/upload streaming or portal-aware live reprioritization. Local Windows runtime evidence is recorded; clean committed-package performance, broader cancellation/failure campaigns, and release-platform evidence remain open. | +| Shared renderer contracts and GPU skinning | **Implemented; opt-in and release promotion pending** | Ordered material/pass, clip-space, semantic vertex-layout, typed buffer-slice, exact four-weight, and joint-palette contracts are shared by OpenGL and Vulkan. `r_gpuSkinning` remains default-off; admitted MD5/MD5R surfaces produce the ordinary `idDrawVert` stream through backend compute while CPU positions and complete fallback remain authoritative for gameplay consumers and stencil volumes. Clean-package visual/performance and platform/driver promotion evidence remains open. | +| Modern classic-frame ownership | **Implemented; default-off; controlled nested GL/Vulkan fixture passed; release promotion pending** | Fixed-function root 2D GUI, eligible ambient-only 3D world, fixed-classic interaction, complete eligible fog/blend phases, provenance-tagged in-world GUI output, special subviews, cinematic/authored-post ranges, render-demo/Raven special frames, and their material-deform dependency have separate sealed GL/Vulkan corridors with complete classic rollback. Eligible authored-post tails inside a sealed special view bind to its exact root and depth; their executed work remains unpublished until the complete special-view transaction succeeds, and either domain can roll back the whole tree. Final R6 runtime acceptance proves one capture-backed color-2D mirror with a nested cinematic plus `_currentRender` tail on Windows GL/Vulkan. This is scoped ownership, not a claim that the aggregate modern-visible renderer owns every stock frame or that root cinematic/post, `_currentDepth`, broad subview, in-world-GUI, special-frame, final-package, platform, or driver breadth is qualified. | +| Temporal presentation | **Implemented; experimental and default-off; release promotion pending** | OpenGL and Vulkan now share native-resolution history ownership, TAA/TAAU, camera/depth reprojection, conservative motion-domain rejection, camera/capture resets, and native-resolution UI composition. OpenGL has exact eligible rigid velocity; unsupported streams, Vulkan object motion, particles, skinned/deformed work, subviews, in-world GUI, and the view model are explicitly reactive rather than silently zero-motion. Missing resources or exact frame/generation depth ownership fails closed, and SMAA remains the immediate rollback. Local Windows GL/Vulkan gameplay closes the implementation gate; clean package, platform, driver, and retained human-review promotion remain open. | +| Modern PBR quality and idTech 6-like follow-ons | **Implemented for the scoped roadmap; default-off; local Windows gate passed; release promotion pending** | Guarded direct metallic/roughness lighting, analytic fallback IBL, authored bounded OpenGL specular probes, atomic bounded OpenGL clustered decals, and three independent GL/Vulkan screen-space leaves now cover the scoped quality plan. The shared native presentation tail provides bounded 16-slice view-aligned froxel integration, bounded 16-step depth-normal SSR, and fixed eight-tap depth-derived SSGI. The effects deliberately do not claim shadowed light-injected volumetrics, material-roughness reflections, or world-space GI. This does not establish broad authored scene coverage or whole-frame lighting promotion: `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`, while complete GPU-driven visible ownership and optional sparse residency remain later follow-ons. | + +Milestones A, B, and C have completed their implementation and local integration +gates. **Milestone D scoped implementation is complete.** Its default-off +corridors cover four complete fixed-classic domains, a bounded in-world GUI +subset transaction, special-subview trees, cinematic/authored-post ranges, +render-demo/Raven special frames, and their shared material-deform dependency: +eligible +fixed-function root 2D GUI views, eligible ambient-only 3D world views, +fixed-classic interaction, and complete eligible fog/blend phases use +backend-neutral sealed records on both backends, with independent settings and +complete classic rollback. Interaction accepts eligible unshadowed and +shadow-coupled views with explicit light, receiver, shadow, map, material, +resource, and fallback accounting. Fog/blend preserves complete light, +GLOBAL-to-LOCAL receiver, ordered stage, fog-cap, texgen/state/resource, and +backend accounting without mixing shared and classic phase draws. All four +domains have retained local runtime qualification; fog/blend's controlled +stock-declaration profile passes exact parity, effect-delta, ownership, and +rollback gates on both backends. The controlled shipped-material deform profile +also passes exact parity, visible effect-delta, completed ownership, and +zero-commit skipped rollback gates on both backends. Completed/empty finalized +material deforms and the classic source-geometry receiver roles now have an +explicit sealed contract, while particle/particle2 remain named fallback. The +special-subview corridor seals direct `SS_SUBVIEW` mirror camera/clip semantics +and the child scene/copy edge for remote-camera, mirror, reflection, refraction, +and x-ray views, including exact 2D/cubemap color/depth target type, aspect, +and face. Nested special views now seal exact parent records, depth-first +child-before-parent packet order, bounded root/depth metadata, and all-member +backend completion before publishing ownership; any member rejection restores +the complete tree to the classic owner. +In-world GUI output from `R_RenderGuiSurf` now has independently +sealed, depth-aware ownership with complete tagged-subset fallback. The new +cinematic/authored-post transaction seals complete eligible root video/audio +views and post tails while retaining the mature decoder and feedback-stage +executors. An eligible post tail inside a sealed special view records the exact +special root/depth, defers ownership publication, and co-publishes or rolls back +atomically with the complete tree. The shared special-frame transaction seals active-session +render-demo views and exact Raven special-effect controller masks while keeping +their complete established backend executors. **Milestone E temporal +presentation is now implemented and locally validated as a default-off +capability.** Native histories, explicit motion/reactive ownership, TAAU, +capture/cut isolation, native UI, and delayed GPU-time resolution control share +one GL/Vulkan contract with SMAA rollback. Milestone F now has a guarded +implementation for direct PBR, authored bounded OpenGL specular probes, +atomic bounded OpenGL clustered decals, and independent bounded GL/Vulkan +froxel-volumetric, SSR, and SSGI leaves. Its leaf controls remain default-off, +`r_rendererModernQuality 0` is the one-setting rollback, and the local +current-source Windows implementation exit gate has passed. Final +committed-package, platform/driver, and release-review promotion remains open. +This work is separate from frame-ownership promotion: +`MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains +`0`. +The final controlled R6 nested acceptance ran six sequential cases per backend +with the fixed input-disabled capture script in actual windowed mode `-1` at +1280x720, using 1280x720 engine TGAs. All 12 cases passed; all four exact +comparisons per API reported RMS `0`, maximum delta `0`, and zero differing +channels, with expected owned/fallback accounting and named atomic rollback. +Normal versus skip remained non-vacuous at RMS `2.0073`, maximum delta `72`, +and 46,359 changed channels on OpenGL and RMS `0.2506`, maximum delta `18`, and +3,329 channels on Vulkan. The authoritative report SHA-256 is +`b49031c48267fbfa86c295082707525a390d6090c24f600d00644237a67b252a`, +bound to fixture manifest SHA-256 +`24a2a4926f4b1263e311ad762bf40d391b33de225732d12154ebd7625705282d` +and runtime manifest SHA-256 +`dbece44597ec5e0686eed215f6ae99c5d6ba4cdd959c1efa7c4acaf1bc2ca673`. +The unchanged 3,884-file / 7,281,560,043-byte stock dependency seal is +`13abad18f70eb8b4bf6ea0e9697b317718bb065ee76f86070787005b045dda7a`; +the unchanged eight-file / 25,330,583-byte fixture evidence inventory seal is +`0572fb8d9c132f7e97060345464f97750e59e69819871fce90eed13d1348d8ae`. +This evidence is deliberately limited to one nested cinematic-plus- +`_currentRender` mirror transaction; its `currentDepth` count is zero. +Release qualification remains a separate track: repeat and retain the +Milestone A-D acceptance sets from clean committed source and a freshly staged +final package, with the required platform and driver coverage. The PBR path +does not bypass those complete-frame ownership requirements. + +## Best official Doom 3 BFG candidates + +The paths below are relative to official `DOOM-3-BFG/neo/` at the pinned +snapshot. + +| Candidate | Readily available BFG code | openQ4 use | Reuse level | Priority | +|---|---|---|---|---| +| Parallel job substrate | `idlib/ParallelJobList.*`, `idlib/Thread.*`, renderer consumers in `tr_frontend_addmodels.cpp` and `tr_frontend_addlights.cpp` | A bounded, dependency-aware worker pool for renderer front-end work, archive/decode jobs, animation work, and cache generation | Adapt architecture; replace platform primitives and spin waits with SDL3/portable C++, and retain deterministic single-thread fallback | **P1** | +| GPU skeletal skinning | `renderer/BufferObject.*`, `VertexCache.*`, `Model_md5.cpp`, `tr_frontend_addmodels.cpp`, `tr_backend_draw.cpp`, `RenderProgs*` | Joint-buffer uploads and optional four-weight GPU deformation for rendered MD5/MD5R draw surfaces and shadow-map casters while preserving CPU consumers | Port the algorithm into dedicated backend-neutral skin attributes and buffers; do not import BFG's GL backend or blindly reuse its vertex-color packing | **P1** | +| GPU timing and automatic resolution scaling | `renderer/ResolutionScale.*`, the timer query in `RenderSystem.cpp`, CPU profiling blocks in `RenderLog.*` | Feed openQ4's implemented backend-neutral, non-blocking GL/Vulkan whole-frame timing result into a bounded controller | Reuse the controller logic, not BFG's single-query blocking readback | **P1** | +| Generated model, render-world, and collision caches | `renderer/Model.cpp`, `Model_md5.cpp`, `ModelManager.cpp`, `RenderWorld_load.cpp`, `cm/CollisionModel_files.cpp` | Cache parsed static/MD5 geometry, `.proc` world data, and collision data after first trusted-source load | Design a hardened openQ4 format; follow the generated-animation cache contract and include Quake 4 MD5R/source-PK4 identity | **P1** | +| Preload manifests | `framework/File_Manifest.*` and resource-type discovery in `FileSystem.cpp` | Record actual per-map image/model/animation/sample/collision use and replay it through a cancellable preload queue | Reuse the manifest concept, not BFG's retail manifest contents | **P1** | +| Resource containers | `framework/File_Resource.*` | Optional developer-generated, sequential cache containers for derived data | Reuse the access-order concept only; BFG uses 32-bit offsets and trusted tables, so prefer individual cache files or a new bounded 64-bit format and never require a BFG `.resources` package | **P2** | +| Parallel renderer front end | `renderer/tr_frontend_*`, `renderer/jobs/ShadowShared.*`, atomic frame allocation in `tr_frontend_main.cpp` | Parallel entity/light visibility, interaction preparation, and shadow-caster work after ownership is made immutable for a frame | Architectural port with substantial Quake 4/BSE and modern-renderer adaptation | **P1/P2** | +| Explicit transient/static GPU buffers | `renderer/BufferObject.*`, `VertexCache.*` | Complete the current upload manager with backend-neutral vertex/index/joint handles, frame rings, fences, budgets, and overflow diagnostics | Mine lifecycle and handle ideas; current openQ4 GL/Vulkan ownership must remain authoritative | **P1** | +| Render-matrix and culling utilities | `idlib/geometry/RenderMatrix.*` | Shared, tested MVP/frustum/depth-bounds math for scene packets, shadow planning, Hi-Z, and GL/Vulkan clip-space variants | Selectively adapt algorithms and tests; do not force BFG's matrix or vertex ABI onto Quake 4 data | **P2** | +| Render-program parameter model | `renderer/RenderProgs.*`, `RenderProgs_GLSL.cpp` | Common parameter names/layouts for optional skinning and shared passes | Selective reference only; build a backend-neutral material/pass IR rather than another GL-specific shader manager | **P2** | +| Stereo presentation | `renderer/RenderContext.h`, stereo portions of `GuiModel.cpp`, `RenderSystem.*`, and `OpenGL/gl_backend.cpp` | Optional side-by-side/top-bottom rendering and stereo-aware full-screen GUI depth | Adapt only after ordinary presentation is stable; use SDL/OpenXR-era platform interfaces instead of old WGL assumptions | **P3** | +| Lightweight compression | `sys/LightweightCompression.*` | Potential LZW/zero-run compression for new cache payloads where profiling proves a benefit | Reuse only behind a new bounded, fuzzed decoder and versioned container; do not insert it into protocol 2.41 | **P3** | + +### 1. Parallel jobs: the highest-leverage import + +BFG's `idParallelJobList` provides job lists, priorities, synchronization +points, list dependencies, bounded parallelism, timing, and a deterministic wait +boundary. Its real BFG renderer users are deliberately coarse: add visible +models, add lights, and build shadow work. That is a better starting point than +spawning ad-hoc threads throughout openQ4. + +The landed openQ4 service adapts that architecture rather than copying it: + +- portable C++ threads and condition variables provide bounded workers and + blocking waits instead of BFG's spinning `Wait()` behavior and fixed + processing-unit assumptions; +- cancellation, shutdown, payload ownership, and lifetime are explicit in the + service contract; +- deterministic synchronous mode is available for dedicated builds, tests, + and debugging; +- list, job, and dependency admission is bounded, and saturation is reported + instead of growing or dropping work silently; +- queue, execution, wait, high-water, rejection, and starvation-aging metrics + are observable; dependency critical-path aggregation remains for real + consumer graphs; +- arbitrary worker-side renderer calls remain prohibited unless a future + backend-owned queue defines that boundary. + +The production contract, controls, saturation behavior, native coverage, and +remaining consumer/promotion boundary are documented in the +[portable job-system guide](parallel-job-system.md). + +The first production consumer is now the learned level-load read/PK4-inflate and +source-framing pipeline. Further consumers should still require a clean join +point and detached output: asset-specific image/audio/model decode or transcode, +generated-cache preparation, and only then renderer model/light preparation. +PK4 archive mutation and game-state mutation remain outside the worker contract. + +### 2. GPU skinning: a concrete idTech 5-class capability + +BFG converts MD5 vertices to four normalized byte weights and four joint +indices, uploads joint matrices through an aligned joint buffer, and keeps a CPU +path for unsupported or special surfaces. openQ4 now adapts that architecture +through its own full-precision, backend-neutral and fail-closed contract rather +than adopting BFG's packed vertex ABI. + +The packed layout is not itself a safe compatibility contract. BFG asserts that +a model has fewer than 256 joints, stores joint indices in `color`, stores +weights in `color2`, and sorts, truncates, then renormalizes vertices with more +than four influences. Its own source notes residual weights above 25 percent in +some assets. Quake 4's packed MD5R path can also carry diffuse vertex colors, so +openQ4 must not silently repurpose those channels. + +The landed corridor applies the required compatibility rules: + +- validate joint and influence counts and retain the CPU path rather than + truncating any MD5 vertex with more than four meaningful influences; +- use dedicated `uint32[4]` joint indices and `float32[4]` weights so packed + MD5R diffuse colors and signed implicit residual-weight behavior remain intact; +- preserve CPU deformation for collision, traces, deforms, software-only debug + tools, decals/overlays that need current positions, stencil shadow-volume + construction, and any shader/material path lacking the skinning contract; +- carry joint data through ambient surfaces, light interactions, shadow-map + casters, subviews, and view models, while explicitly routing incompatible + surfaces to CPU deformation; +- use one typed, generational joint-buffer/slice contract represented + consistently in GL and Vulkan; +- keep the capability default-off while clean-package bounds, vertex, + silhouette, screenshot, and performance promotion evidence is accumulated. + +This should land as capability and parity infrastructure first. It should not be +coupled to PBR, TAA, or a renderer-default switch. + +### 3. Generated assets and learned preloading + +BFG assumes generated BFG resources exist. Stock Quake 4 installations do not, +so its packaged manifests and resource containers cannot be required. The landed +Milestone B adaptation extends openQ4's generated-animation and binary-image +pattern: + +1. Load the original PK4 asset normally. +2. Record the resolved source path, containing PK4 checksum, parser/build + version, platform-independent format version, and relevant quality settings. +3. Write derived data under `fs_savepath/baseoq4/generated/` using an atomic + temporary-file replacement. +4. On the next run, validate every bound before allocation and every source key + before use. +5. Delete or ignore an invalid cache and fall back to the original asset. + +Parsed static/MD5/MD5R render models, classic `.proc` render-world data, +collision models, animation v3, and an exact learned per-map preload manifest +now follow that contract. BFG's serializers remain useful field inventories, +but their timestamp checks and trusted-data assumptions are not sufficient for +a PK4-backed, fail-closed runtime. The openQ4 manifest schedules a bounded +deterministic subset and never overrides VFS resolution or becomes a second +source of asset truth. Asset-specific asynchronous owner decode/upload and +portal-aware live reprioritization remain future work. The exact format, +ownership, limits, rollback, and promotion-evidence boundary are documented in +[Level-Load Cache Modernization](loading-cache-modernization.md). + +### 4. Dynamic resolution from real GPU time + +BFG's resolution controller is compact and readily adaptable. It lowers +resolution quickly when GPU time exceeds a threshold and raises it more slowly +after several under-budget frames, avoiding constant oscillation. openQ4 already +has render scaling, renderer metrics, high-refresh presentation, and a +four-slot, non-blocking GL timestamp ring. Milestone A exposes that ring +as a backend-neutral whole-frame result and provides the equivalent Vulkan +timestamp-query path. Both backends resolve only retired/available slots, reset +their generation at workload discontinuities, and feed high-resolution CPU plus +de-duplicated GPU samples into `OPENQ4_FRAME_TIMING_V1`. What remains is the +feedback controller and complete promotion evidence. + +The future controller should build on this implemented timing foundation: + +- preserve the non-blocking GL/Vulkan timestamp contract and never wait on a + current-frame result; +- target a user/display frame budget and account for VRR; +- quantize dimensions to backend-friendly alignments; +- expose minimum scale, response rate, and a conservative default-off rollout; +- reset history on map load, teleport, video restart, backend switch, and other + discontinuities; +- keep GUI/HUD composition at native output resolution; +- integrate with future TAAU, while remaining useful with SMAA/bilinear scaling. + +## BFG systems to study but not transplant + +| BFG subsystem | Why it is not a direct openQ4 import | Safer direction | +|---|---|---| +| `sys/PacketProcessor.*`, `Snapshot*`, lobby/session code | Different wire model, object snapshots, lobby assumptions, platform services, and game semantics; replacing it would break protocol 2.41 and existing Quake 4 networking | Keep the hardened Quake 4 path. If a new transport is justified, negotiate an explicit openQ4 protocol while retaining 2.41 as a separate path | +| Depth-fail stencil-shadow back end | The official BFG release expressly omits the code that enables Carmack's Reverse; the included shadow jobs and shared geometry helpers are not a complete replacement renderer | Keep openQ4's existing Quake 4-compatible stencil path authoritative. Study the BFG job boundaries independently of the omitted back-end operation | +| SWF UI runtime | Quake 4 ships idTech 4 GUI scripts, not BFG SWFs; replacing the UI runtime would strand stock menus and in-world GUIs | Modernize the existing GUI renderer/parser and add optional new UI surfaces without removing the stock path | +| XAudio2 backend | Windows-specific and redundant with openQ4's cross-platform OpenAL voice/HRTF/EFX work | Continue improving the current backend and SDL/platform device lifecycle | +| Doom 3 `d3xp` gameplay, aim, inventory, achievements, and save/session code | Different game rules, class layouts, scripts, maps, and save data | Port isolated engine-agnostic ideas only; implement Quake 4 gameplay changes canonically in `openQ4-game` | +| BFG render backend as a whole | Assumes BFG vertex formats, shaders, material behavior, generated resources, and GL/platform interfaces | Extract contracts and algorithms into the current backend-neutral GL/Vulkan architecture | +| BFG resource packages as shipped data | No corresponding generated packages exist in a retail Quake 4 installation | Generate disposable caches locally and retain source-PK4 fallback | +| Doom Classic integration and platform storefront code | Unrelated content and unavailable proprietary service pieces | Keep out of the runtime | + +## Capabilities beyond the official BFG drop + +Reaching an idTech 6-like standard requires work that the 2012 BFG source does +not provide. These should build on the BFG-derived foundations rather than be +treated as code-import tasks. + +The highest-leverage renderer step is to finish coherent ownership, not add one +more isolated experimental pass. openQ4 already has experimental scene packets, +a render graph, clustered/MDI submission, shadow maps, and Vulkan coverage, but +the capability matrix records no proven modern visible-lighting domain yet. +Classic ambient/material, interaction, and fog/blend semantics now have +independent shared contracts alongside root 2D GUI, and their material-deform +dependency is explicit. The direct-mirror and eligible color-capture subview +edges and the provenance-tagged in-world GUI subset are also explicit; cinematic +and remaining post/fallback semantics must still become explicit before temporal +or PBR work multiplies the parity surface. + +### Backend and submission + +- A backend-neutral material/pass intermediate representation shared by GL and + Vulkan. +- Complete modern visible-lighting ownership before promoting GPU-driven + submission. +- Persistent resource descriptors, pipeline/shader caches, indirect draws, + Hi-Z culling, and a safe CPU rollback. +- Explicit resource state/lifetime tracking and asynchronous upload budgets. + +### Image quality + +- Complete motion vectors for rigid, skinned, particle, deform, subview, GUI, + and view-model surfaces. +- TAA/TAAU with reactive masks, disocclusion handling, camera-cut resets, and + SMAA fallback. +- Dynamic resolution driven by backend timestamps. +- Namespaced PBR material extensions, GGX lighting, IBL/specular probes, and + stock-material defaults that preserve the classic look. +- **Implemented as independent default-off leaves:** bounded view-aligned + froxel fog/volumetrics, depth-normal SSR, and fixed-tap screen-space GI now + consume the reliable scene-colour/depth presentation tail on GL and Vulkan. + Their documented screen-space limitations remain explicit. +- True HDR output (scRGB/HDR10 negotiation, paper-white GUI composition, and HDR + screenshot policy), distinct from the existing internal HDR scene chain. + +### Streaming and CPU scalability + +- Extend the implemented bounded read/PK4-inflate -> framing/integrity DTO -> + main-owner adoption corridor into asset-specific decode/transcode/upload only + where detached results and per-stage budgets make ownership safe. +- Add portal-aware live priority changes to the current exact-match learned + manifest. The implemented replay is a bounded, deterministically ordered + subset rather than an unconditional whole-level preload, but it does not + reprioritize dynamically from portal visibility. +- Optional virtual-texture/sparse-residency support for high-resolution community + content, after ordinary streaming is reliable. The official BFG drop does not + provide idTech 5's virtual-texturing implementation, and stock Quake 4 assets + must never depend on this path. +- GPU skinning plus jobbed animation/model preparation. +- Background shader/pipeline compilation with deterministic cache keys and an + always-available synchronous fallback. + +### Networking and operations + +- Keep protocol 2.41 for compatibility, but consider a separately negotiated + openQ4 transport for larger sequence spaces, stronger session authentication, + modern congestion/fragmentation behavior, and optional traffic protection. +- Continue bounded parser work, fuzzable decode APIs, rate limits, structured + diagnostics, and headless dedicated-server soak tests independently of any + future protocol. + +## Recommended implementation order + +| Milestone | Current state | Dependency that prevents promotion | +|---|---|---| +| A. Foundation and measurement | **Implemented and locally validated; release promotion pending** | The portable bounded job substrate, backend-neutral delayed GL/Vulkan whole-frame timing, and versioned, replay-verifiable per-map CPU/GPU budget tooling are implemented. Current-build jobs-on/off parity, repeated map-change shutdown, deterministic dedicated exits, schema-10 stock capture/replay, and complete replay-verified 8/8 OpenGL plus 8/8 Vulkan required profiles have passed. Promotion still requires the same evidence retained from clean committed source and a freshly staged final package, plus release platform/driver qualification. | +| B. Loading and cache modernization | **Implemented but default-off; performance requalification required** | Exact learned manifests, bounded cancellable read/PK4-inflate and framing/integrity stages, immutable source DTOs, and transactional model/world/collision plus animation-v3 caches are integrated with source fallback. A 2026-08-20 regression audit found that the prior default-on experiment could materially lengthen stock map loads, so `com_levelLoadModernization 0` now restores the classic baseline and gates every framework/animation cache read and write. Promotion requires a clean committed-package campaign that beats or matches classic cold and warm loads without rewrite churn, plus release-platform qualification. | +| C. Shared renderer contracts and GPU animation | **Implemented; promotion pending** | Ordered pass semantics, clip/viewport conversion, semantic layouts, typed buffer slices, exact four-weight MD5/MD5R sidecars, bounded joint palettes, and GL/Vulkan deformation paths are present with full-surface CPU rollback. Dependency-light and module self-tests cover the common contract; clean-package SP/MP image, collision/hit, animation-heavy performance, and platform/driver evidence remains the promotion gate. | +| D. Modern classic-frame ownership | **Implemented; default-off; controlled nested GL/Vulkan fixture passed; release promotion pending** | The scoped GL/Vulkan ownership corridors are complete for eligible fixed-classic GUI, world ambient, interaction, fog/blend, in-world GUI, special-subview, cinematic/authored-post, special-frame, and material-deform work. Authored-post/video/current-render/depth tails nested in a sealed special-view tree share its root transaction: no member publishes until every child draw, dynamic tail, and capture/direct edge completes, and any failure restores the whole tree to classic execution. Final R6 evidence proves one nested cinematic-plus-`_currentRender` color-2D mirror transaction; root cinematic/post, `_currentDepth`, broad subview forms, in-world GUI, special frame, authored-stock breadth, clean-package, retained-review, and target-platform/driver qualification remain open as applicable. | +| E. Temporal presentation | **Implemented; default-off; local Windows gate passed; release promotion pending** | Native game-owned and backend-owned histories, exact frame/depth/generation admission, camera reprojection, OpenGL rigid velocity, conservative reactive ownership for every unsupported motion stream, capture/cut isolation, native UI, TAAU, delayed dynamic resolution, and SMAA rollback are integrated across GL/Vulkan. Dependency-light/static coverage and windowed stock SP/MP gameplay close the local implementation gate; clean-package, target-platform/driver, and retained-review qualification remain open. | +| F. Modern materials and advanced lighting | **Implemented; default-off; local Windows exit gate passed; release promotion pending** | Guarded direct PBR and analytic fallback IBL are joined by authored OpenGL specular probes bounded to eight cubemaps, 32 records, and the top two probes per cluster; an atomic OpenGL clustered-decal transaction bounded to 1,024 records and 65,536 cluster references; and independent GL/Vulkan froxel-volumetric, SSR, and SSGI leaves. The screen-space tail is bounded to 16 depth slices, 16 reflection steps, and eight GI taps, shares scene colour/depth without expanding Vulkan's 256-byte resolve block, and keeps captures plus effect-only/TAA presentation explicit. All leaves default off and `r_rendererModernQuality 0` publishes an exact zero-feature packet. Current-source static/native checks, retail gameplay, individual and combined windowed OpenGL gameplay, combined validation-clean Vulkan gameplay, visible image deltas, and master rollback close the scoped local implementation gate. Final committed-package, platform/driver, retained-review, and release qualification remain open; no RenderDoc, broad authored probe/decal scene, light-injected volumetric, material-roughness SSR, or world-space GI coverage is claimed. `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS = 0`. | + +### Milestone A: foundation and measurement + +1. **Implemented:** the engine-owned portable bounded job manager provides + synchronous mode, starvation-safe priorities, dependency tests, + shutdown/cancellation tests, and timing counters. +2. **Implemented:** the delayed GL timestamp result is exposed through a + backend-neutral whole-frame timing contract, and the equivalent Vulkan + timestamp path is integrated; both are generation-aware and never wait for a + current-frame result. +3. **Implemented and locally validated; release promotion pending:** enforce versioned, + configurable map/backend/profile CPU and GPU percentile budgets in the + gameplay benchmark and stock baseline; bind contract/runtime/artifact + provenance and replay measurements fail-closed. The initial repeated 20/28 + ms rows are target ceilings until complete GL/Vulkan captures calibrate and, + where justified, tighten each explicit identity. + +The 2026-08-19 current-build evidence snapshot closes the local job lifecycle +portion of this gate: + +- jobs-on and jobs-off `game/storage1` runs produced identical engine TGA bytes + and matching game-state evidence; +- jobs-on and jobs-off OpenGL campaigns, plus a jobs-on Vulkan campaign, crossed + `game/mcc_2` -> `game/storage1` -> `game/storage2` -> `game/storage1` -> + `game/tram1` and ended with + `jobsShutdown PASS v1 initialized=0 queued=0 running=0`; +- five dedicated-server runs exited normally with one synchronous self-test and + one clean shutdown marker each; +- the schema-10 four-role retail-PK4 baseline passed capture and immediate + replay under the canonical display/budget contract, and its engine screenshots + and save preview passed local human review; +- storage and repeated-map runs exercised nonblocking OpenGL and Vulkan timing; + the final immutable development runtime then passed and replay-verified all + eight OpenGL and all eight Vulkan required-profile cases. The earlier + `game/medlabs` failure was fixed by ordering and clamping depth bounds before + the OpenGL call; its debug-context rerun records zero GL errors. + +This evidence set culminated in the immutable development runtime +`milestone-a-20260819-final3`; its complete required-profile reports are +`ma-a-gl3` and `ma-a-vk3`. It came from an uncommitted current source tree and is +not a retained release artifact. It does not replace clean-source provenance, a +freshly staged final package, platform/driver qualification, or retained release +review. + +Exit gate: replay-valid exact bordered-window 1280x720 GL/Vulkan captures for +the required budget identities; identical stock screenshots and game state with +jobs on/off; repeated map changes ending in +`jobsShutdown PASS v1 initialized=0 queued=0 running=0`; deterministic +dedicated-server exit; and the general four-role retail-PK4 and human-review +promotion evidence. The current-build job, lifecycle, timing-path, required-map, +and stock-baseline checks above satisfy the local implementation gate. Only the +general clean-source, final-package, retained-review, and platform/driver gates +keep release promotion open. + +### Milestone B: loading and cache modernization + +1. **Implemented:** a learned manifest is keyed to the exact normalized map, + full SHA-256 runtime-role and entity-filter identities, ordered VFS/search and + pure-PK4 state, individual source identities, and load-affecting settings. +2. **Implemented:** bounded cancellable workers read independently opened + sources, perform the PK4 inflation reached by those reads, validate supported + source framing and whole-buffer integrity, and publish a sealed immutable DTO. + The ordinary VFS lookup runs again before substitution; format-specific asset + parsing, adoption, and renderer/audio upload remain with the main owner. +3. **Implemented:** versioned binary render-model, classic render-world, and + collision caches under `fs_savepath`, plus the companion SP/MP animation-v3 + cache, validate detached state and publish atomically or fall back to source. +4. **Corrected after regression audit:** all Milestone B cache, preload, and + animation-cache paths now require the default-off + `com_levelLoadModernization` master gate. Archived individual controls from + earlier builds cannot silently retain the slower path. + +Exit gate: cold and warm load measurements, bounded memory, cancellation during +map/restart/disconnect, corrupt-cache fallback, and zero required loose assets. +Focused native/static tests, Windows integration builds, and current-build +stock compatibility checks satisfy the implementation/safety gate, but not the +performance gate. Local development cold/warm and bounded-memory measurements, focused +corruption/rollback, dedicated teardown, and engine-screenshot review are now +recorded; the staged stock report remains failed on its unchanged MP CPU budget. +Exact committed source/package identity, a clean-package budget pass, broader +cancellation/failure campaigns, and release-platform coverage remain required +for promotion. Requalification must also demonstrate that both cold and warm +opt-in loads are no slower than the classic default before any default change. +Their authoritative status is recorded in +[Level-Load Cache Modernization](loading-cache-modernization.md). + +### Milestone C: shared renderer contracts and GPU animation + +1. **Implemented:** define the minimum backend-neutral material/pass, + clip-space, vertex-layout, and typed buffer-slice contracts needed by both GL + and Vulkan. +2. **Implemented:** add bounded backend joint-buffer rings and dedicated + full-precision four-weight vertex data. +3. **Implemented:** establish the rigid/MD5/MD5R CPU-vs-GPU parity corridor, + including preserved diffuse vertex colors, signed MD5R residual weights, + exact-only MD5 admission, and joint/data/capability fallbacks. +4. **Implemented:** feed an ordinary deformed `idDrawVert` stream to depth, + ambient, interaction, subview, shadow-map, and view-model consumers while + decals/overlays keep CPU positions and stencil volumes remain explicitly CPU. + +Exit gate: stock SP/MP visual equivalence, CPU fallback parity, no collision or +hit-detection changes, and measured CPU-frame reduction in animation-heavy maps. +The implementation and deterministic contract gate is complete. Clean committed +package screenshots, pure-MP collision/hit digests, repeated animation-heavy +measurements, and target-platform/driver coverage remain required before the +default-off capability can be promoted; the exact procedure is recorded in +[Shared Renderer Contracts and GPU Animation](gpu-skinning-modernization.md). + +### Milestone D: modern classic-frame ownership + +1. **Implemented:** fixed-function root 2D GUI, world ambient/material, + unshadowed plus shadow-coupled fixed-classic interaction, and fog/blend + conditions, colors, repeated order, matrices/texgen, images, samplers, + light/receiver/shadow/cap identity, and render state are expressed through + shared contracts. Their CPU material-deform dependency is also sealed. The + capture-backed and direct special-subview edges are sealed. +2. **Implemented across the scoped domains:** GL and Vulkan consume the + same per-draw evaluated semantic records, with backend-specific execution and + an untouched complete-domain classic rollback. +3. **Implemented across the scoped domains:** scene packets and the + material resource table promote a GUI, eligible ambient-only world view, or + eligible unshadowed/shadow-coupled fixed-classic interaction view, while the + fog/blend transaction promotes only a complete eligible phase, after + transactional preparation and complete backend preflight; all other domains + retain their established owner. +4. **Implemented for world ambient/material:** opaque and perforated draws must + match an established depth packet, translucent draws retain their classic + depth behavior, and ordered material work is split into pre-fog and post-fog + phases without taking ownership of depth or fog. +5. **Implemented for fixed-classic interaction:** every accepted + light, local/global/translucent receiver, light stage, decomposed primitive, + no-op, and resource is sealed and reconciled together. +6. **Implemented for shadow-coupled fixed-classic interaction:** classic stencil + volumes, projected single-map and CSM/parallel shadows, point cubes, mixed + mapped/stencil lights, complete hybrid supplements, dynamic casters, and + perforated casters are sealed and reconciled by both backends. Translucent + moment casters and any incomplete/custom/unsupported/backend condition reject + the whole interaction view before visible ownership. +7. **Implemented for fog/blend:** every fog/blend light, GLOBAL-to-LOCAL receiver, + ordered active/inactive blend stage, fog receiver/cap, evaluated texgen/state, + and resource is sealed and reconciled together. Any source, geometry, + resource, capacity, target, or backend blocker rejects the complete phase + before the first shared main-target draw. +8. **Implemented for classic material deforms:** finalized root GUI, world + ambient, and mapped-shadow draws seal source/result material and geometry, + evaluated inputs, frame/cache lifetime, outcome, and semantic hash around the + authoritative CPU deform. Interaction and fog receivers seal the classic + not-applicable/source-geometry role. Completed and intentional-empty results + are admissible only behind `r_rendererSharedDeform`; particle/particle2, + skipped, failed, stale, or mismatched records reject the complete owning + transaction before backend work. +9. **Implemented for special subviews:** a direct `SS_SUBVIEW` mirror record + seals its exact child-view camera, clip-plane, culling, viewport, and scissor + semantics before OpenGL or Vulkan invokes the established whole-view executor. + Remote-camera, mirror, reflection, refraction, and x-ray forms reconcile a + child scene packet, parent source surface, exact full-viewport 2D/cubemap + color/depth `RC_COPY_RENDER`, destination image type/aspect, and cube face. + Both backends report ownership only after the sealed child executor or + transfer returns. Nested children additionally seal exact parent/root/depth + records and depth-first packet order; their entire tree publishes only after + its outermost special view completes. Eligible cinematic/post work inside + that tree is admitted by its dedicated domain and joins the same atomic + publication/rollback boundary; unsupported special forms retain their + untouched classic owners. +10. **Implemented for in-world GUI:** only GUI quads emitted under + `R_RenderGuiSurf` receive front-end provenance. Their complete tagged subset + uses a world-category GUI packet stream and depth-aware world pass records; + GL and Vulkan preflight and commit before the ambient walks, then suppress + only a successfully owned subset from their matching classic walker. Any + source, packet, material, resource, target, capacity, or backend blocker + retains every tagged source on the untouched classic path. +11. **Implemented for cinematics and authored post:** a root 2D video/audio + view or a complete ordered `SS_POST_PROCESS` tail is admitted only after + scene-packet/source identity and ordering reconcile. The sealed record + carries the original render-view cinematic clock plus current-render/depth + stage diagnostics. OpenGL and Vulkan keep their mature dynamic stage + executors for decode, scratch upload, feedback capture, and custom program + behavior. An eligible authored-post range inside a sealed special view binds + to the exact special root and depth, completes without early publication, + and co-publishes only when the complete special-view tree succeeds. An + invalid source, packet, time, capture, target, or backend condition rolls the + complete tree back to the classic path. +12. **Implemented for render demos and Raven special frames:** scene packets + distinguish an active session render-demo stream from portal-sky negative + view ids, seal complete root-view source coverage and the demo version, and + preserve exact `RC_DRAW_SPECIAL_EFFECTS` blur/AL controller masks. OpenGL + and Vulkan deliberately execute their mature full-view, resource, resolve, + blur, and projected-light paths, reporting ownership only after the sealed + coverage completes; any incomplete record retains the classic path. + +Exit gate: every scoped Milestone D corridor has a sealed GL/Vulkan owner, +no visible light or surface is silently dropped, and rejection restores the +complete applicable domain or cross-domain tree to the classic result. The +implementation gate is complete while all corridors remain default-off; their exact contract, validation +procedure, and remaining boundaries are recorded in +[Shared Classic 2D GUI Domain](classic-gui-domain-modernization.md). This is not +a claim that the aggregate modern-visible renderer owns every stock frame. Local +Windows stock `game/storage1` engine captures passed with the corridor both off +and on for GL and Vulkan; enabled diagnostics recorded complete owned views and +explicit whole-view fallbacks on both backends. The second complete-domain +implementation is documented in +[Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md); +its bordered 1280x720 stock `maps/tools/mv2` option-off/on engine captures match +exactly on GL and Vulkan, enabled diagnostics report one owned pre-fog draw with +domain hash `dc18ed8c0539bbfc`, and the stock `shaderDemos/move` deform override +reports a named zero-draw whole-view fallback on both backends. These are local +development-worktree results, not clean-package or platform promotion. The +third complete-domain implementation and its shadow-coupled expansion are documented in +[Shared Classic Interaction-Lighting Domain](classic-interaction-domain-modernization.md); +its unshadowed controlled GL/Vulkan captures retain exact classic image parity, +and its expanded native/static gates cover the completed stencil, mapped, +mixed, dynamic/perforated, hybrid, and atomic-fallback contract. The documented +controlled and stock shadow profile is the runtime release-acceptance set. +Clean committed-package and target-platform/driver recapture remain promotion +requirements. The fourth complete-domain implementation is documented in +[Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md). +Its native/static gate and controlled GL/Vulkan profile now pass exact +shared/classic engine-image parity, nonempty reconciled ownership, material +fog/blend deltas, and named +zero-commit atomic rollback. Authored-stock fog plus clean-package and +target-platform/driver promotion remain open. +The material-deform dependency is documented in +[Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md). +The direct and capture-backed special-subview implementation is documented in +[Shared Special-Subview Transaction](classic-subview-domain-modernization.md). +The in-world GUI implementation is documented in +[Shared Classic In-World GUI Domain](classic-inworld-gui-domain-modernization.md). +The cinematic/authored-post implementation is documented in +[Shared Classic Cinematic and Authored-Post Transaction](classic-cinematic-post-domain-modernization.md). +The render-demo/Raven special-frame implementation is documented in +[Shared Render-Demo and Raven Special-Frame Transaction](classic-special-frame-domain-modernization.md). +The scoped Milestone D implementation is complete. The final controlled R6 +cinematic-plus-`_currentRender` mirror transaction closes its narrow Windows +GL/Vulkan runtime gate with exact engine-TGA parity and named rollback; it does +not close the root, `_currentDepth`, broad subview, in-world-GUI, special-frame, +final-package, human-review, or platform/driver gates. Those remaining +qualifications stay on the separate Milestone D release-promotion track; +Milestone E no longer depends on expanding Milestone D's promotion scope. + +### Milestone E: temporal presentation + +1. **Implemented:** promote delayed backend GPU timing into a bounded, + generation-aware, default-off dynamic-resolution controller. +2. **Implemented:** assign every visible motion domain to exact history, + camera/depth reprojection, or explicit conservative reactive rejection. +3. **Implemented:** provide native-history TAA/TAAU with camera/capture resets, + native-resolution UI, current-frame spatial fallback, and SMAA rollback. + +**Local implementation exit gate passed on Windows:** dependency-light and +cross-repository contracts cover history, controller, motion-domain, depth, +capture, and ABI invariants. Windowed engine-TGA gameplay covers SP and MP, +stable motion, particles, the weapon view, portals/subviews, screenshots, and +both GL/Vulkan paths; targeted camera-cut, menu/native-UI, fixed-scale, +save-preview, and SMAA rollback cases complete the gate. Clean staged-package, +additional platform/driver, and retained human-review evidence remain release +promotion work rather than implementation blockers. + +### Milestone F: modern materials and advanced lighting + +1. **Implemented and locally validated; release promotion pending:** keep direct PBR explicitly + authored and default-off. Unsupported materials retain their complete classic + owner, retail content is never inferred as PBR, and the original procedural + `.tmp` fixture provides deterministic albedo/normal/ORM inputs without + incorporating external assets. +2. **Implemented and locally contract-validated; broader authored-scene evidence pending:** authored OpenGL specular probes + use a fixed eight-cubemap atlas, at most 32 frame records, and deterministic + top-two selection per cluster. Sampling is base-mip specular approximation + with analytic fallback; it does not claim box parallax, diffuse irradiance, + runtime capture, or Vulkan support. +3. **Implemented and locally contract-validated; broader authored-scene evidence pending:** eligible OpenGL clustered decals + transfer ownership only through one prepare/seal transaction bounded to + 1,024 records and 65,536 cluster references. Any malformed, stale, + incomplete, or overflowing input publishes no ownership for the affected + transaction. +4. **Safeguard retained:** clustered/GPU-driven visible-lighting submission is + not promoted. `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0` until + visible-lighting parity is proven independently. +5. **Implemented and locally validated; release promotion pending:** froxel + volumetrics, SSR, and SSGI are independent default-off GL/Vulkan leaves in + the native scene-presentation tail. Shared admission clamps work to 16 + depth slices, 16 reflection steps, and eight GI taps; missing resources keep + the current/classic presentation owner, and `r_rendererModernQuality 0` + publishes an exact zero-feature packet. The implementation is deliberately + screen-space: it does not claim shadowed per-light volumetric injection, + roughness-aware G-buffer reflections, or world-space/multi-bounce GI. + +**Local implementation exit gate passed on Windows:** the 2026-08-23 evidence +under `.tmp/milestone-f-evidence/20260823-final/` uses a current-source debug +x64 build. Static PBR/advanced-lighting checks passed, the final native suite +passed 10/10, the final focused engine rerun passed 2/2 after the earlier 8/8 +safe set, and the four-role retail baseline passed against 40 PK4s with zero +loose retail files. Controlled OpenGL GL 3.3/4.1/4.3/4.5 and Vulkan fixture +runs prove visible PBR execution, a non-vacuous enabled/disabled image delta, +and exact `r_rendererModernQuality 0` rollback. This gate does not claim +RenderDoc evidence or broad authored probe/decal scene coverage. The leaf +controls remain default-off, `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains +`0`, and final committed-package, platform/driver, retained-review, and release +promotion remain pending. + +**2026-08-24 advanced screen-space gate:** the dependency-light +`AdvancedScreenSpaceCoreTest` and the static advanced-lighting contract pass, +and the pinned Vulkan temporal-resolve shader matches the checked-in GLSL. +Windowed `game/airdefense1` gameplay passes with all three leaves together on +OpenGL and Vulkan, with zero shader/FBO/GL/Vulkan-validation/VUID/call/fatal or +engine-error counters. OpenGL also passes each leaf independently. The combined +OpenGL engine-TGA capture differs non-vacuously from the same-build default +control; the live BSE/actor scene is not an exact-hash fixture, so exact master +rollback is enforced by the zeroed shared packet and supplemented by a clean +master-off gameplay run. Two post-audit captures observed OpenGL at +102.9--117.3 Hz (final hardened P95/P99 11/11 ms) and Vulkan at +114.1--125.8 Hz (final hardened P95/P99 10/11 ms) on this development system. +The same staged package passed default +`game/airdefense2` gameplay and pure auto-joined `mp/q4dm1` on both backends, +the complete 11/11 native suite, and a fresh four-role compatibility run +against 40 verified retail PK4s with zero loose retail files. +See the [user guide](../user/advanced-screen-space-lighting.md) and retained +[`airdefense1` optimization evidence](airdefense1-optimization-evidence.md). + +## Promotion evidence for every milestone + +Each milestone should record: + +- source provenance and retained notices for incorporated code; +- focused unit/static tests and malformed-cache/input tests; +- Windows x64 builds plus Linux/macOS compile-policy coverage appropriate to the + changed subsystem; +- stock-only SP load/save/reload/demo evidence; +- pure MP listen-server and auto-joined client gameplay evidence; +- engine-render-target screenshots and human visual review; +- before/after CPU frame, GPU frame, load time, memory, and cache-size metrics; +- rollback results with the new feature disabled; +- confirmation that no new loose content is required. + +The [engine capability matrix](engine-capability-matrix.md) remains the current +truth. This roadmap describes sequence and acceptance gates; it does not mark a +capability implemented merely because BFG source exists or a prototype compiles. diff --git a/docs/dev/loading-cache-modernization.md b/docs/dev/loading-cache-modernization.md new file mode 100644 index 00000000..ab5a6b14 --- /dev/null +++ b/docs/dev/loading-cache-modernization.md @@ -0,0 +1,480 @@ +# Level-Load Cache Modernization + +This document owns the implementation contract and promotion evidence for the +Milestone B loading/cache slice in the +[idTech 5-level modernization roadmap](idtech5-modernization-roadmap.md). +The implementation and local integration gate are complete, but final +committed-package runtime, platform, and performance evidence is **not +complete**. Nothing in this document should be read as a universal measured +speed-up or release-platform qualification: the measurements below are local +development evidence, and the explicitly open promotion gates still apply. + +## Compatibility boundary + +The installed Quake 4 and mod sources remain authoritative: + +- Every replay source is resolved first through the normal VFS search order, + addon and pure-PK4 rules. A learned manifest never authorizes a file. +- A ready preload may replace only the bytes behind an already authorized file + handle, and only when generation, semantic type, normalized path, + uncompressed size, loose-file timestamp, and containing-PK4 checksum still + match exactly. +- The production worker stage reads complete source bytes and performs the + `idFile_InZip` PK4 inflation reached by that read. It then validates supported + RIFF/WAVE, Ogg, PNG, and JPEG framing and exact termination, walks the whole + buffer for transport and SHA-256 integrity, and seals an immutable typed DTO. + DDS and formats without a worker framing contract remain deliberately opaque. +- This worker decode is framing and integrity validation, not asset-specific + model, world, collision, animation, declaration, image, or sound parsing. It + does not resolve declarations, mutate live managers, or call renderer/audio + APIs. Existing main-thread owners still perform those operations. If a + result is absent, late, rejected, malformed, or cancelled, the owner + continues with the ordinary VFS handle. +- Generated model, world, collision, and animation files are private derived + data under `fs_savepath`. They do not participate in VFS search or pure-PK4 + negotiation and are always disposable. + +This keeps source parsing as the correctness path and makes the entire feature +reversible without converting or modifying retail assets. + +## Learned manifest identity + +The first successful load learns the source files that were actually opened, +their semantic type and priority, first-use order, use count, options, and +source identity. Successful post-publication source opens continue to be +learned until the generation is closed, so demand-loaded media can be included +on the next visit. The canonical manifest merges duplicate observations and +orders replay deterministically by priority, first use, and stable identity. + +A manifest is accepted only when every campaign key matches: + +The current `.oqpm` manifest is format/producer version 2; earlier records are +not widened or guessed and therefore miss cleanly. + +| Key | Exact input | +|---|---| +| Producer and format | Manifest producer version, format magic/version, end marker, canonical ordering, bounds, and SHA-256 integrity | +| Map | Normalized lowercase slash-separated map key, such as `maps/game/airdefense1` | +| Runtime role | Full 32-byte SHA-256 of exactly `singleplayer`, `multiplayer`, or `dedicated` | +| Entity selection | Full 32-byte SHA-256 of the active `si_entityFilter` value | +| Content/search state | `fs_game`, `fs_game_base`, the exact ordered active search list, and the active pure-PK4 checksum list | +| Load-affecting settings | SHA-256 of the versioned settings string listed below | + +For each PK4 search entry, the content key includes its lowercase filename, +checksum, archive length, file count, addon-search flag, and search-order +position. Directory entries include the lowercase game directory and position; +absolute installation roots are intentionally excluded. Loose files are then +validated individually by normalized path, size, and timestamp. The manifest +intentionally does not hash every loose source byte; same-size, +timestamp-preserving edits can therefore evade this preload identity check. +Generated-cache owners that need stronger loose-source identity add a content +CRC/signature to their own key. PK4 members are validated by normalized path, +uncompressed size, and containing-PK4 checksum. + +The learned-manifest settings contract is `level-load-settings-v2` followed, in +this exact order, by: + +```text +com_binaryRead +r_renderer +r_actualRenderer +r_mergeModelSurfaces +r_slopVertex +r_slopTexCoord +r_slopNormal +r_useNewSkinning +r_useFastSkinning +r_forceConvertMD5R +r_convertMD5toMD5R +r_convertStaticToMD5R +r_convertProcToMD5R +r_pbrMaterials +image_downSize +image_downSizeLimit +image_downSizeSpecular +image_downSizeSpecularLimit +image_downSizeBump +image_downSizeBumpLimit +image_ignoreHighQuality +image_picmip +image_picmipFilter +image_picmipMinSize +image_usePrecompressedTextures +s_useCompression +sys_lang +``` + +Display-only settings are not part of this key. Generated-cache owners add +their own parser/settings signatures for representation-specific choices such +as surface merging, silhouette remapping, slop values, MD5R source/conversion +mode, deferred tangents, and collision source/geometry identity. + +## Bounded replay pipeline + +Replay is generation-scoped and follows this ownership sequence: + +1. The main thread validates a matching manifest, applies entry/file/aggregate + budgets, resolves each candidate through the ordinary VFS, and checks its + current source identity before admitting it. +2. High-priority job-list items read independently opened handles in bounded + chunks. For PK4 members, the handle inflates the member during this worker + read. Jobs poll cooperative cancellation between chunks. +3. A complete read enters the worker framing/integrity stage. Supported + container formats must consume their exact framed extent; every result must + report the whole decoded byte count, match the independent read transport + checksum, and produce nonzero SHA-256 integrity. Only then is a sealed, + immutable DTO published with generation, type, normalized path, + authoritative source identity, frame kind/count, payload extent, and shared + backing bytes. Partial reads, malformed/trailing frames, allocation failures, + cancellation, and identity mismatches never publish. +4. A later ordinary source open performs VFS and pure authorization again. If + the exact immutable result is already ready, the filesystem closes the + physical/PK4 handle and returns a read-only view carrying the authoritative + name, full path, timestamp, and container checksum. It never waits for an + unfinished item at this point. +5. The normal owner performs its format-specific parse/decode, validation, + adoption, and renderer/audio upload work on its established main-owner path. + Replay results remain alive through renderer, sound, declaration, BSE, and UI + `EndLevelLoad` processing, then the session releases the generation. +6. Map failure, unload, filesystem restart, module reload, and shutdown cancel + and join the generation before dependent state is released. + +If the job service is disabled, deterministic, saturated, or unavailable, the +admitted bounded batch executes synchronously. Sources outside the budget and +all unsuccessful results continue through ordinary VFS reads. + +Default replay budgets are: + +| Control | Default | Allowed range | Meaning | +|---|---:|---:|---| +| `com_levelLoadPreloadMaxEntries` | `64` | 1-64 | Independently opened source files admitted per map generation; the hard cap prevents PK4 stream-handle exhaustion | +| `com_levelLoadPreloadMaxFileMB` | `128` | 1-512 MiB | Maximum uncompressed size of one source | +| `com_levelLoadPreloadMaxStagingMB` | `384` | 8-2048 MiB | Aggregate admitted staging memory | +| `com_levelLoadPreloadMaxDecodeMB` | `384` | 8-2048 MiB | Aggregate admitted immutable decoded-result residency | +| `com_levelLoadPreloadReadChunkKB` | `256` | 16-4096 KiB | Cancellation and read-accounting chunk; implementation additionally caps it at 4 MiB | +| `com_levelLoadPreloadDecodeChunkKB` | `256` | 16-4096 KiB | Cancellation and integrity-walk chunk; implementation additionally caps it at 4 MiB | + +The framing stage does not transcode or duplicate the complete source buffer: +on success the DTO takes shared ownership of the staging allocation and that +allocation moves from staging accounting to decoded-result accounting. Normal +owner parsing can allocate its established live representation in addition to +the retained source buffer; those owner allocations are outside these two +pipeline budgets. The per-file and aggregate caps bound replay work, but they +are not a claim that total level-load memory is limited to 384 MiB. + +The portable scheduler's independent list/job/dependency and queue caps remain +in force; see the [portable job-system contract](parallel-job-system.md). + +Replay priority is learned and deterministically bounded at generation start. +There is no portal-visibility-driven live reprioritization, arbitrary background +asset streaming, or worker-side owner parsing/upload in this milestone. + +## Generated-cache contract + +Framework-owned `.oqgc` files use a versioned, bounded, little-endian envelope +with kind, parser version, authoritative source identity, content and settings +signatures, decoded length, payload SHA-256, whole-record integrity, end marker, +and trailing-data rejection. The runtime currently writes and accepts the +uncompressed envelope codec; the enum reservation for deflate is not a claim +that generated payload compression is enabled. + +The framework opens and identifies the authoritative source before deriving or +opening a cache filename. An envelope or payload failure removes that exact +private file, records a miss/corruption when reporting is enabled, and lets the +owner parse the source. Writes use a unique staging file, flush it, then +atomically promote it over the final path. + +### Render models + +The model cache supports the exact static, MD5, and MD5R owner payloads selected +by their loaders. Each decoder validates bounded counts, indices, strings, +finite numeric data, surfaces, geometry, joints/weights, and type-specific +state into a detached model, rejects trailing bytes, and swaps state only after +the complete payload is valid. Unsupported model states remain on their source +loader and are not silently flattened. + +### Render worlds + +The world cache covers the finalized classic `.proc` CPU representation: +inline static and shadow model payloads, per-area model identity and `procSky` +state, inter-area portal windings and fade/cull data, area BSP nodes, map name, +source timestamp, and map CRC. The reader applies strict per-field and aggregate +caps, finite-coordinate and graph/index/area validation, and builds all models, +portals, and nodes off to the side before publishing and running the normal +world finalization path. + +Packed MD5RProc worlds can share buffers across models. The world writer refuses +that representation rather than losing the sharing contract. A selected +MD5RProc companion, or classic proc converted through `r_convertProcToMD5R`, +therefore uses the authoritative source path with no world-cache write. + +### Collision models + +The collision payload stores pointer-free model vertices, edges, polygons, +brushes, BSP nodes, references, material names, source slots, and geometry CRC. +It bounds all counts and aggregate allocations, validates contents, finite +planes/bounds, indices, graph topology, reference coverage, proc-clip ordering, +and current material declarations, then constructs a detached model set. The +live collision table changes only after every staged model is adoptable. + +Authored `.cmc`/`.cm` sources are preferred by the normal binary/text selection. +Map-derived collision caches retain the `.map` source identity and include the +exact `.proc` path, length, timestamp/container checksum, and loose-file CRC in +their owner settings. Any failure returns to the existing authored or +map/`.proc` build path. + +### MD5 animations + +SP and MP share the separate version-3 `.banim` contract owned by +`openQ4-game`. Its source identity follows the parser's exact selection: with +`com_binaryRead 1`, the compiled `c` source wins when present; +otherwise the requested `.md5anim` source is used. The record stores that +selected normalized path, length, timestamp, containing-PK4 checksum, and a +content CRC for loose sources. A PK4 member relies on its containing archive +checksum rather than rereading the member for an additional source CRC. + +Before parsing any record field, the reader bounds the whole file and checks a +stored record length plus CRC-32 trailer. It then validates the selected source +identity, bounded frame/joint/component counts, frame-rate and duration +arithmetic, hierarchy and component ranges, finite floats, ordered bounds, +near-unit base-frame quaternions, end marker, and exact stream consumption into +private lists. Only then does it replace the live animation. Logical cache paths +are slash/lowercase normalized and reject rooted/drive, dot, empty, control, +unsafe, trailing-dot/space, and Windows device-name segments. Invalid files are +removed and the selected parser source is used. Writes use a `.tmp` file, +`Sync()`, and atomic promotion. + +The v3 reader first holds the bounded on-disk record so it can verify the whole +record CRC before trusting lengths, then copies the verified record into a +read-only memory file for field parsing. Its 528 MiB record cap therefore makes +the temporary copy finite but potentially material; it is separate from the +framework preload staging/decode budgets. + +## Private paths + +All paths are below the active game directory under `fs_savepath` (normally +`/baseoq4/`): + +| Data | Private path | +|---|---| +| Learned manifests | `generated/manifests/.oqpm` | +| Render-model payloads | `generated/models/.oqgc` | +| Render-world payloads | `generated/worlds/.oqgc` | +| Collision-model payloads | `generated/collision/.oqgc` | +| Animation payloads | `generated/animations/.banim` | + +The opaque names are derived from validated keys, not asset names. Do not ship +these files in PK4s or treat them as authored content. + +## Controls and rollback + +The experiment is now protected by a default-off archived master control. The +individual controls retain their prior values so an explicit opt-in needs only +one additional switch: + +| Control | Default | Effect | +|---|---:|---| +| `com_levelLoadModernization` | `0` | Master admission gate for every framework and SP/MP animation cache/preload path | +| `com_levelLoadCache` | `1` | Enables learned manifests and generated model/world/collision cache reads | +| `com_levelLoadCacheWrite` | `1` | Enables atomic manifest and generated-cache writes | +| `com_levelLoadPreload` | `1` | Replays a matching manifest through the bounded read/framing pipeline | +| `com_levelLoadCacheReport` | `0` | Prints per-generation replay, read/decode, cancellation, memory, and generated-cache counters | + +The six replay-budget controls are listed above. Animation cache reads and +writes additionally require `g_useGeneratedAnimCache 1` and +`g_writeGeneratedAnimCache 1` in both game modules. Scheduler behavior is +controlled by the startup settings documented in +[parallel-job-system.md](parallel-job-system.md). + +This default was corrected on 2026-08-20 after stock MP evidence showed the +experimental path dominating a 32.3-second map load (16.5 seconds in game init +and 15.2 seconds in media finish), with repeated generated-cache publication. +The retained Milestone B cold/warm campaign proved boundedness and reuse, but +did not justify changing the player baseline: 69.3 seconds cold and 53.3 seconds +warm remained materially slower than the expected classic experience. + +A controlled 2026-08-20 Vulkan `mp/q4dm9` pair used the same staged runtime, +retail base path, bordered 1280x720 mode, and isolated save roots. The new +default-off run completed the client map load in 9,449 ms (`gameInit=3,107`, +`mediaFinish=6,000`) with no generated-cache writes. Explicitly setting +`com_levelLoadModernization 1` took 21,935 ms (`gameInit=5,093`, +`mediaFinish=16,224`) and published 35 framework cache records on the client; +the paired server published 103. The opt-in cold path was therefore 132% slower +than the classic default in this case. Artifacts are retained under +`.tmp/regression-q4dm9-fixed/` and `.tmp/regression-q4dm9-cache-on/`. + +The master rollback is immediate and takes precedence over older archived +individual settings: + +- Keep `com_levelLoadModernization 0` for ordinary play and baseline or + benchmark runs. No framework or animation cache is read or written. +- Set `com_levelLoadModernization 1` only for focused cache evaluation; the + individual rollback choices below then apply. + +- Set `com_levelLoadPreload 0` before loading a map to disable learned source + preparation while retaining validated generated model/world/collision + payloads. +- Set `com_levelLoadCacheWrite 0` to keep valid existing framework caches + read-only and stop new framework derived data from being published. +- Set `com_levelLoadCache 0` before loading a map to use ordinary source paths + for manifests and framework model/world/collision caches. +- Set both animation controls to `0` to force `.md5anim` source parsing without + reading or writing `.banim` files. +- With openQ4 closed, delete any or all of the `generated/` subdirectories above + for a clean rebuild. Never delete the original retail/mod source files. + +## Evidence status + +The implementation contract above is source-backed. Focused native format and +pipeline tests, Windows SP/MP and engine integration builds, workflow-contract +checks, static macOS/ARM64 policy checks, and the local Windows runtime campaigns +below pass within their stated scope. Those checks close the local +implementation gate, not the release gate. The openQ4 tree was not committed +when the evidence was captured; the companion content matches local commit +`c32a3858b30139ddb850c3cec4f1274cd93bb98c`, which was not yet published. The +staged development package is not a clean release candidate, and there has +been no Linux/macOS runtime qualification. Only the measurements explicitly +recorded below may be inferred. + +### Required run identity + +Record these fields for every retained campaign: + +| Field | Value | +|---|---| +| openQ4 commit | _Pending_: evidence was captured from the uncommitted final-development tree | +| `openQ4-game` commit | Local `c32a3858b30139ddb850c3cec4f1274cd93bb98c`; remote publication pending | +| Build/package identity and hashes | Windows x64 staged development package; client, dedicated, OpenGL/Vulkan modules, and SP/MP GameLibs built and staged successfully. Release-candidate hashes remain pending. | +| OS, architecture, compiler, and storage | Windows x64 production build with MSVC; native format/pipeline tests also pass strict Clang C++20. Storage-device identity was not retained. | +| Renderer/backend, SP/MP/dedicated role, and map sequence | Direct windowed OpenGL 1280x720 SP `game/storage1` campaigns; two dedicated `q4dm1` loads separated by disconnect; staged stock SP load/save/reload/demo and pure auto-joined MP server/client campaign. Vulkan was built, not runtime-qualified here. | +| Retail/mod PK4 and overlay identity | Stock campaign recorded 40 retail PK4s and zero loose retail files beneath the staged openQ4 overlays. Exact package/hash binding remains part of clean-candidate promotion. | +| Cache, preload, job, binary-read, conversion, and renderer settings | Cache/preload defaults for cold/warm/corruption runs; `jobs_enable 0` for synchronous fallback; framework cache/preload and animation reads/writes disabled for rollback. Remaining exact settings are retained in the logs/reports. | +| Log/report/artifact paths | `.tmp/milestone-b-final-evidence-v2/savepaths/sp/baseoq4/logs/`; `.tmp/milestone-b-dedicated-final/baseoq4/logs/milestone_b_dedicated.log`; `.tmp/milestone-b-stock-20260820-final4/` | + +### Functional and failure evidence + +| Gate | Required retained evidence | Status | +|---|---|---| +| Cold learn then exact warm replay | First load writes a canonical manifest; next identical load reports a manifest match and successful source acquisitions | **Local development pass:** cold learned 5,425 entries; exact warm matched, replayed/decoded 64 entries, and recorded 68 acquisitions with no decode failure/cancellation | +| Search/PK4/source invalidation | Ordered search, pure list, PK4 checksum, loose timestamp/size, map, mode, entity filter, and settings changes each prevent stale reuse | **Partial:** exact-match replay passed, and the second dedicated load correctly missed after the exact state was deliberately changed. A retained runtime matrix for every key remains pending. | +| Corruption fallback | Truncated, bit-flipped, oversized, wrong-kind/version, wrong-owner, and trailing-byte manifest/model/world/collision/animation cases are removed or ignored and load from source | **Local focused pass:** a corrupt world cache fell back and rewrote cleanly; a corrupt animation-v3 record was rejected and atomically rebuilt; focused malformed-input tests pass. Broader owner/runtime corruption coverage remains pending. | +| Transactional publication | Failed model/world/collision/animation payloads leave no partial live owner state | **Local focused pass:** the world and animation corruption runs reached clean map completion; the rebuilt animation matched its pre-corruption SHA-256 and left no `.tmp`. Model/collision runtime fault injection remains pending. | +| Job parity | Jobs threaded, deterministic, and disabled produce matching gameplay state and engine-rendered evidence | **Partial:** the jobs-disabled warm run completed the same map through `syncFallback=1` with 64 replayed/decoded entries, 68 acquisitions, 188 generated hits, and no failure. Final jobs-on/off engine-image parity from a clean candidate remains pending. | +| Cancellation and teardown | Repeated map changes, load failure, module/filesystem restart, and shutdown cancel/join without stale publication or leaked handles | **Partial:** native read/decode cancellation tests pass, and both dedicated generations closed and joined across disconnect. Broader runtime load-failure, module/filesystem-restart, and in-flight cancellation campaigns remain pending. | +| Role/backend coverage | Required SP, pure auto-joined MP, dedicated, OpenGL, and Vulkan campaigns complete from the final package | **Partial:** Windows OpenGL SP, pure auto-joined MP, and dedicated lifecycles passed functionally; Vulkan and all roles built. No Vulkan runtime or Linux/macOS runtime qualification is claimed. | +| Retail asset coverage | Representative loose and PK4 sources plus classic proc, MD5RProc fallback, model, collision, image, sound, GUI, decl, effect, and animation opens are exercised | **Local PK4 pass; broader matrix pending:** the staged stock campaign used 40 retail PK4s and zero loose retail files and completed its SP/MP functional artifacts. Explicit loose-source and full representation coverage remains pending. | + +### Performance and resource evidence + +The direct comparison used windowed OpenGL at 1280x720 on `game/storage1` with +the same stock retail-PK4 set. Milliseconds are engine-reported map-phase times; +process peaks are observed working-set peaks, not pipeline allocation totals. + +| Measurement | Cold learn/write | Exact warm cache/replay | Artifact | +|---|---:|---:|---| +| Total map-load time | 69,285 ms | 53,299 ms (23.1% lower in this local comparison) | Local SP log root below | +| Render-world phase | 7,882 ms | 3,014 ms | Local SP log root below | +| Game initialization phase | 25,736 ms | 15,538 ms | Local SP log root below | +| Player-spawn phase | 1,395 ms | 636 ms | Local SP log root below | +| Cache join | 111 ms | 115 ms | Local SP log root below | +| Media finish | 34,034 ms | 33,857 ms | Local SP log root below | +| Settle | 126 ms | 130 ms | Local SP log root below | +| Process peak | 2,075,062,272 B | 2,147,344,384 B | Observed Windows process peak | +| Manifest/replay/acquisition | learned 5,425; match 0; replay 0 | match 1; replay 64; acquired 68 | `com_levelLoadCacheReport 1` | +| Read/inflate and framing/integrity | no replay | 77,318,587 B; decoded 64; failed 0; cancelled 0 | `com_levelLoadCacheReport 1` | +| Peak pipeline residency | no replay | staging 76,932,512 B; decoded 77,318,587 B (both below 384 MiB defaults) | `com_levelLoadCacheReport 1` | +| Generated-cache counters | hits 0; misses 188; writes 188; corrupt 0 | hits 188; misses 0; writes 0; corrupt 0 | `com_levelLoadCacheReport 1` | + +The local SP artifacts are under +`.tmp/milestone-b-final-evidence-v2/savepaths/sp/baseoq4/logs/`. +These timings describe this one development machine and map. In particular, the +warm process peak was higher than the cold peak even though both pipeline peaks +were inside their configured bounds; the 23.1% total-time difference is not a +cross-platform or release-candidate guarantee. + +### Focused fallback and rollback runs + +| Run | Result | Observed process peak | +|---|---|---:| +| Jobs disabled | 55,183 ms total; manifest match 1, replay/decoded 64, acquired 68, generated hits 188, no read/decode failures, `syncFallback=1`, clean completion | 2,149,097,472 B | +| Corrupt world cache | 55,144 ms total; generated hits 187, miss 1, write 1, corrupt 1; source fallback/rewrite and clean map marker | 2,148,675,584 B | +| Corrupt animation v3 | The marine-idle `.banim` was opened, rejected, reparsed from source, and atomically rewritten. Its recorded SHA-256 (prefix `CE5F99E0`) exactly matched the backup, and no `.tmp` remained. | Not separately recorded | +| Full rollback | Framework cache/preload plus animation cache read/write disabled; join 0; no generated model/world/collision/animation reference; clean completion in 29,969 ms | 2,062,823,424 B | + +The animation corruption record is +`.tmp/milestone-b-final-evidence-v2/savepaths/sp/baseoq4/logs/milestone_b_final_v2_anim_corrupt.log`. +The rollback time is recorded as a correctness result, not compared as a speed +claim, because the run intentionally changed cache/write behavior. + +The resulting private `generated/` tree contained 987 files and 180,618,336 B: + +| Kind | Files | Bytes | +|---|---:|---:| +| Animations | 707 | 12,859,371 | +| Collision | 1 | 25,914,726 | +| Images | 91 | 76,535,744 | +| Manifests | 1 | 923,296 | +| Models | 186 | 22,846,157 | +| Worlds | 1 | 41,539,042 | + +### Dedicated-server development run + +`.tmp/milestone-b-dedicated-final/baseoq4/logs/milestone_b_dedicated.log` +records exit code 0 after 20.6 seconds with a 720,044,032 B observed process +peak. The actual `game_mp` module was selected, and two complete `q4dm1` loads +separated by `disconnect` both reached ready state. Generations 1 and 2 closed +and joined; the first load recorded 93 generated misses/writes and the second a +generated hit. No renderer or OpenGL markers appeared. The second in-process +manifest deliberately missed under changed exact state, so this run is not +dedicated manifest-replay evidence. + +### Staged stock compatibility campaign + +The final-development staged campaign at +`.tmp/milestone-b-stock-20260820-final4/` completed all functional SP +load/save/reload/demo and pure MP server/client lifecycle checks with +`si_pure 1`, `net_serverAllowServerMod 0`, and `ui_autoJoin 1`. It retained the +expected artifacts and engine screenshots against 40 retail PK4s and zero loose +retail files. Local visual review found coherent SP world/save views, active +`q4dm1` MP HUD/player views, and no cache or rendering corruption. + +The report's overall status is nevertheless **FAIL**, solely because sustained +MP CPU timing exceeded the unchanged fixed budget: server p95/p99 were +22.548/29.739 ms and client p95/p99 were 24.406/28.778 ms. GPU budgets passed, +as did SP CPU/GPU budgets. The harness now waits up to 120 seconds for the cold +server and samples the settled client for 10 seconds, without changing the +budget. The earlier `.tmp/milestone-b-stock-20260820-2/` campaign passed every +role and budget, but its hashes predate final fixes and it is historical only, +not final-package promotion evidence. + +### Build and platform qualification + +- Windows x64 client, dedicated, OpenGL, Vulkan, SP, and MP build/link/stage + checks pass. +- Native manifest-format and pipeline suites pass with MSVC and strict Clang + C++20; the broad no-build/no-runtime validation set passes 132/132. +- The provenance audit remains 581 Doom 3 and 37 Doom 3 BFG header-family files. +- Linux ARM64 and macOS source/static/policy checks pass only. WSL runtime was + unavailable with `E_ACCESSDENIED`, Docker was unavailable, and no Apple + hardware runtime was available; none of those platforms is runtime-qualified + by this evidence. + +Do not mark Milestone B release promotion complete until the source pair is +committed, the package and hashes are bound to those commits, the fixed MP CPU +budget passes (or is changed through a separately reviewed contract update), +and the required release-platform and broader cancellation/failure campaigns +are retained and reviewed. The roadmap's implemented/local-integration status +does not waive this evidence gate. + +## Source locations + +- [Cache/manifest format](https://github.com/themuffinator/openQ4/blob/master/src/framework/LevelLoadCacheFormat.h) +- [Cache manager](https://github.com/themuffinator/openQ4/blob/master/src/framework/LevelLoadCacheManager.cpp) +- [Bounded read/framing pipeline](https://github.com/themuffinator/openQ4/blob/master/src/framework/LevelLoadPipeline.cpp) +- [Filesystem authorization/substitution](https://github.com/themuffinator/openQ4/blob/master/src/framework/FileSystem.cpp) +- [Session campaign key](https://github.com/themuffinator/openQ4/blob/master/src/framework/Session.cpp) +- [Render-model payloads](https://github.com/themuffinator/openQ4/blob/master/src/renderer/Model.cpp) +- [Render-world payload](https://github.com/themuffinator/openQ4/blob/master/src/renderer/RenderWorld_load.cpp) +- [Collision-model payload](https://github.com/themuffinator/openQ4/blob/master/src/cm/CollisionModel_files.cpp) +- [Player controls and cleanup](../user/level-load-cache.md) +- [Source provenance](source-provenance.md) diff --git a/docs/dev/multiview-demos.md b/docs/dev/multiview-demos.md index fcd7b935..7f92810f 100644 --- a/docs/dev/multiview-demos.md +++ b/docs/dev/multiview-demos.md @@ -111,8 +111,8 @@ The 32-bit game compatibility slot deliberately stays in the same header position: - format 1.0 and 1.1 interpret it as the raw `GAME_API_VERSION`; playback uses - an explicit allowlist of stream-compatible writers (currently API 39 and the - current API), rather than treating every older ABI as safe; + an explicit allowlist of stream-compatible writers (currently APIs 39, 42, + 44, and the current API), rather than treating every older ABI as safe; - format 1.2 and newer interpret it as a packed MVD game-schema `major:minor` (`major << 16 | minor`) and ask the game module whether that schema is compatible. @@ -121,9 +121,10 @@ This avoids rewriting the established major-1 header while decoupling future recording compatibility from unrelated game API changes. API 39 is explicitly retained because API 40 only appended the versioned MVD -callbacks: the version-1 snapshot and reliable record grammar remains -available in the current game module. Future legacy API exceptions must be -reviewed and added individually. +callbacks; APIs 42 and 44 are retained because their later lifecycle and +presentation-interface revisions did not change the version-1 snapshot or +reliable-record grammar. Future legacy API exceptions must be reviewed and +added individually. ### Records diff --git a/docs/dev/official-pk4-checksums.md b/docs/dev/official-pk4-checksums.md index fc1c1aef..a2fdef90 100644 --- a/docs/dev/official-pk4-checksums.md +++ b/docs/dev/official-pk4-checksums.md @@ -7,7 +7,15 @@ This table captures the PK4 checksums loaded from official installed game direct - Log source: `logs/openq4.log` startup lines (`Loaded pk4 ... with checksum ...`) under `fs_savepath\\` - Checksum format: engine PK4 checksum (`MD4` of zip-entry CRC list, as computed in `src/framework/FileSystem.cpp`) -openQ4 ignores the retail game-binary PK4 archives (`game000.pk4` through `game300.pk4`, plus `gamex*.pk4` variants) because it ships its own game modules. They are not required and are not verified. +openQ4 ignores the retail game-binary PK4 archives (`game000.pk4` through `game300.pk4`, plus `gamex*.pk4` variants) as loadable content because it ships its own game modules. They are not required, mounted, or content-verified. The one retained checksum value described below is a network compatibility token, not a requirement to install or trust the archive. + +## Pure multiplayer game-module token + +Quake 4 protocol 2.41 carries a game-code PK4 checksum after the ordered pure asset list. openQ4 preserves that field by sending the official Quake 4 1.4.2 `q4base/game300.pk4` engine checksum, `0x68fb90b1`, for the legacy Windows, Linux, and macOS OS IDs. The same value is used on every platform, so an openQ4 client and server do not need identical executable formats to complete the asset handshake. + +The value is only a protocol compatibility token. openQ4 does not open, extract, download, restart into, or execute `game300.pk4`. It accepts the token only when a game module has already been resolved from the trusted local openQ4 package/module roots; a missing local module or any other token fails the pure handshake as unavailable game code. The ordered retail/openQ4 asset PK4 list is still checked separately. + +`0x68fb90b1` is not a hash of the loaded openQ4 module, cryptographic attestation, or an anti-cheat guarantee. It preserves the stock 1.4.2 wire meaning while executable trust remains a local packaging decision. ## Required official baseline diff --git a/docs/dev/parallel-job-system.md b/docs/dev/parallel-job-system.md new file mode 100644 index 00000000..9de6b981 --- /dev/null +++ b/docs/dev/parallel-job-system.md @@ -0,0 +1,185 @@ +# Portable Job System + +openQ4 now owns a portable bounded job service for coarse engine work. It is +available in client, tool, and dedicated builds without changing Quake 4 asset, +save, demo, or network formats. Its first production consumer is the learned +level-load read/decode pipeline: workers read independently opened, +identity-checked source handles, perform the PK4 inflation reached by those +reads, validate supported source framing plus whole-buffer integrity, and +publish a sealed immutable DTO. Resource owners still perform asset-specific +parse/decode, validation, adoption, and upload on their established main path; +no live renderer, audio, archive, or game state is mutated by those jobs. + +The architecture was informed by id Software's GPLv3 +[Doom 3 BFG `idParallelJobList`](https://github.com/id-Software/DOOM-3-BFG/blob/1caba1979589971b5ed44e315d9ead30b278d8b4/neo/idlib/ParallelJobList.h). +The implementation is original openQ4 code. It deliberately replaces BFG's +platform-specific worker assumptions and spinning list wait with portable C++ +threads and condition variables, explicit saturation, and cooperative +cancellation. Both projects use GPLv3-compatible licensing; no BFG source file +was copied into this subsystem, so the audited BFG-lineage source-file count is +unchanged. + +## Runtime policy and lifecycle + +`idCommon` initializes the engine-owned service before commands and higher-level +runtime systems can submit work. Shutdown stops admission, requests cancellation, +waits for running callers and workers, and joins every worker before network, +session, renderer, or other subsystem teardown begins. + +The startup-only controls are: + +- `jobs_enable 1` enables threaded scheduling. Setting it to `0` never discards + work; accepted lists execute inline in deterministic insertion order. +- `jobs_deterministic 1` selects the same synchronous implementation for tests + and debugging. Dedicated builds default this setting to `1`, so they create no + background job workers unless explicitly configured otherwise; threaded + client execution requires `jobs_enable 1` with `jobs_deterministic 0`. +- `jobs_numThreads 0` reserves one hardware thread when possible and clamps the + worker count to 1-32. An explicit value selects that bounded count. +- `jobs_queueCapacity 64` bounds admitted lists and accepts values from 1-1024. + +Synchronous work participates in the same active-list and quiescence contract as +threaded work. A concurrent `CancelAll`, `WaitAll`, or shutdown therefore sees +and joins an inline list running on another submitting thread. Concurrent inline +submissions sleep until their submit sequence reaches the front rather than +spinning. + +### Level-load read/decode consumer + +The level-load manager validates an exact learned manifest and applies its own +entry, per-file, staging/decode aggregate-byte, and read/decode chunk budgets +before creating one high-priority list. Every candidate file is opened through +ordinary VFS and pure-PK4 policy on the main thread, and its current path, size, +loose timestamp, and PK4 checksum must match before admission. Workers perform +bounded reads, PK4 inflation, typed framing validation for supported RIFF/WAVE, +Ogg, PNG, and JPEG sources, and whole-buffer transport/SHA-256 integrity walks with +cooperative cancellation. DDS and other owner-specific formats remain opaque. +Complete results become sealed immutable DTOs backed by shared byte arrays; +partial, malformed, late, failed, or cancelled results are never published. + +When an owner later opens a resource, the normal VFS lookup runs first. Only an +already-ready exact-identity result may replace the bytes behind that authorized +handle, without waiting. Image, sound, model, world, collision, declaration, +GUI, effect, skin, animation, and raw-file owners therefore keep their ordinary +asset-specific parse/decode/adopt/upload behavior and source fallback. Map +failure, unload, filesystem restart, module reload, and shutdown cancel and join +the generation before dependent state is released. + +`com_levelLoadModernization 0` disables the complete experimental consumer; +when explicitly enabled, `com_levelLoadPreload 0` disables preload without +disabling validated generated model/world/collision payloads. If the scheduler is deterministic, +disabled, saturated, or unavailable, the admitted bounded batch runs inline; +unadmitted sources continue through ordinary VFS reads. The exact campaign key, +budgets, controls, rollback, and still-pending promotion evidence are documented +in [Level-Load Cache Modernization](loading-cache-modernization.md). + +## Submission contract + +Each `idJobList` declares fixed job and dependency capacities when it is created. +The implementation also fails closed above 1,048,576 jobs or 256 dependencies +per list. Combined with the configured active-list bound, this places a finite +upper limit on admitted work. Scheduler list and worker storage is reserved +before workers start; admission, dependency refresh, cancellation, and teardown +do not grow scheduler-owned containers. Job functions receive their payload, +worker index, and an `idJobCancellationToken`; payload ownership and lifetime +remain the caller's responsibility through the list's terminal state. + +Admission returns an explicit `idJobSubmitResult`. In particular, +`QUEUE_FULL` leaves the list in its building state so the owner can wait for its +own safe join point and retry. Work is never silently dropped or placed in an +unbounded overflow queue. Callers must handle every non-success result; the +service does not guess whether blocking, inline fallback, or cancellation is +safe for a consumer. + +Dependencies are deliberately chronological: a list may depend only on a list +already submitted to the same service. Self-dependencies, duplicate edges, +foreign-service lists, unsubmitted dependencies, and reverse edges into an +already-submitted list fail closed. This makes a dependency cycle +unrepresentable. A dependent list sleeps until all prerequisites complete and +is cancelled if a prerequisite fails or is cancelled. + +Lists have low, normal, or high priority. Higher priority wins initially, while +every skipped runnable list gains a scheduling age. At the starvation threshold, +aged lists enter a primary oldest-age selection class which ignores base +priority; equal ages use the monotonic submit sequence. A continuously runnable +low-priority list therefore runs after a bounded number of dispatches even when +nine or more earlier high-priority lists remain runnable. Selection is +deterministic even though completion timing with multiple workers is necessarily +concurrent. + +Cancellation is cooperative. Jobs that have not started are accounted for and +never invoked; running jobs must poll `context.IsCancellationRequested()` and +leave their own data in a safe state. List waits, worker waits, dependency waits, +and shutdown waits all use condition variables. There is no spin or sleep-poll +loop in the scheduler. + +## Diagnostics and validation + +`jobsStats` prints current admission, queue high-water, running/sleeping worker, +completion, rejection, cancellation, failure, wake, priority-promotion, +execution-time, and wait-time counters. Per-list status and timing counters are +also available through the C++ API. + +`jobsSelfTest` exercises engine-service execution, dependency ordering, batch +work, and counters in either threaded or synchronous mode. Automation should +require the exact versioned success prefix: + +```text +jobsSelfTest PASS v1 +``` + +Engine shutdown emits this exact clean-quiescence marker only after all accepted +work has reached a terminal state and every worker has joined: + +```text +jobsShutdown PASS v1 initialized=0 queued=0 running=0 +``` + +The focused native test covers deterministic inline execution, worker +parallelism, fixed capacities, retryable saturation, dependency and failure +propagation, self/cycle rejection, cooperative cancellation, low-priority aging, +threaded shutdown, and cancellation/join of an inline job submitted from another +thread. `tools/tests/parallel_job_system.py` keeps the production lifecycle, +public contract, build registration, and documentation wired into the default +validation suite. + +## Consumer boundary and promotion evidence + +Production consumers must have immutable inputs, caller-owned result storage, +and an explicit main-thread join point. The learned preload consumer meets that +contract with a worker-produced framing/integrity DTO whose generation, semantic +type, authoritative source identity, frame metadata, transport checksum, +SHA-256 integrity, and backing bytes are sealed before publication. This is a +real decode stage, but it deliberately stops before asset-specific owner parsing +or transformation. Future image/audio/model decode, transcode, upload, or +generated-cache preparation jobs require their own detached result and +main-owner adoption contract. Archive mutation, live game-state mutation, and +renderer API calls from arbitrary workers remain out of bounds. + +The current light-grid CPU integration lives inside a dynamically loaded renderer +module. That module does not import the engine's `jobSystem` through +`RenderModuleAPI`, so wiring it directly would create an unresolved/duplicate +service boundary and unclear shutdown ownership. Light-grid work should adopt +the service only after a narrow renderer job import or engine-owned work packet +contract is designed; it remains outside this consumer migration. + +The learned level-load read/framing pipeline is the first production migration. +`jobs_enable 1` and `jobs_enable 0` must still produce identical owner-visible +results even though one may stage reads concurrently and the other runs the +same bounded work inline. The 2026-08-19 current-build validation closed the +earlier local Milestone A lifecycle gate: +jobs-on and jobs-off `game/storage1` produced identical engine TGA bytes and +matching game state; both OpenGL modes and jobs-on Vulkan completed the repeated +`game/mcc_2` -> `game/storage1` -> `game/storage2` -> `game/storage1` -> +`game/tram1` campaign with clean shutdown markers; and five dedicated-server +runs each exited normally with exactly one synchronous self-test and one clean +shutdown marker. + +Those runs used an immutable development runtime built from an uncommitted +source tree. Release promotion still requires the same evidence to be retained +from clean source and a freshly staged final package. Multiplayer evidence must +retain `ui_autoJoin 1` unless the join flow itself is under test. Milestone B's +new consumer parity, cancellation, corruption, memory, and timing evidence is +also still pending in +[loading-cache-modernization.md](loading-cache-modernization.md); the integrated +consumer is not itself a final performance or platform claim. diff --git a/docs/dev/plans/2026-05-13-clustered-hybrid-gl-renderer.md b/docs/dev/plans/2026-05-13-clustered-hybrid-gl-renderer.md index 5bf00073..8ec9d8ec 100644 --- a/docs/dev/plans/2026-05-13-clustered-hybrid-gl-renderer.md +++ b/docs/dev/plans/2026-05-13-clustered-hybrid-gl-renderer.md @@ -483,6 +483,7 @@ Goal: close the compatibility gaps that usually break old game content. - Completed: The modern visible bridge now assigns every known render-pass category to a visible owner. Depth, shadow-map, G-buffer/deferred, forward+, light-grid sampling, present, and fullscreen GUI are modern-owned when their guarded resources and programs are available. Stencil shadows, authored post/copy-render work, SSAO, motion blur, bloom, subview/remote-camera/render-demo views, and BSE special effects remain explicit compatibility fallbacks instead of silent misses. - GUI path: `RENDER_PASS_GUI` now has a dedicated modern draw-plan pipeline backed by the internal GUI shader. When `r_rendererModernVisible 1` composes a frame, ready GUI submit commands are replayed as a modern fullscreen overlay after the hybrid scene composite so legacy GUI command packets no longer automatically block the modern visible path. +- 2026-08-20 qualification: that aggregate overlay is retained only as part of the guarded GL hybrid experiment; it does not preserve or prove complete classic material-stage ownership. The separate default-off `r_rendererSharedGui` corridor now owns the first complete fixed-function root 2D domain from ordered per-draw evaluated records on both GL and Vulkan, and disables the aggregate replay while requested. See [Shared Classic 2D GUI Domain](../classic-gui-domain-modernization.md). - Resource and fallback accounting: Post, copy-render, subview, render-demo, cinematic/AVI-sensitive, and BSE-heavy categories now appear in the modern compatibility inventory with graph/pass counts, fallback counts, deterministic render-demo status, and BSE category buckets for particles, trails, beams, decals, and material-driven effects. - Metrics added/changed: `gfxInfo` and `r_rendererMetrics 2` now print `modernCompatibility`, including inventory coverage, modern/legacy owner totals, light-grid promotion, GUI program/pass/draw/readiness/execution state, post/copy fallback counts, subview/remote/render-demo fallback counts, render-demo determinism, BSE fallback buckets, and cinematic compatibility coverage. - Self-tests added/changed: Added `rendererModernCompatibilitySelfTest` and a safe validation-matrix case that enables `r_rendererModernVisible`, proves GUI readiness, verifies explicit post/subview/render-demo/BSE fallback ownership, and checks that unsupported categories block composition rather than disappearing. diff --git a/docs/dev/plans/2026-05-19-pbr-material-support.md b/docs/dev/plans/2026-05-19-pbr-material-support.md index e0db3135..98328a76 100644 --- a/docs/dev/plans/2026-05-19-pbr-material-support.md +++ b/docs/dev/plans/2026-05-19-pbr-material-support.md @@ -6,6 +6,20 @@ Add physically based material support to openQ4 without changing how shipped Qua The short version: stock materials stay stock; PBR materials get a modern material model; every unsupported case has a visible fallback reason and a legacy rendering path. +## Implementation Status + +As of 2026-08-23, openQ4 has an **experimental, default-off Milestone F implementation whose local Windows exit gate has passed**: the compatibility-safe PBR authoring foundation, guarded OpenGL PBR lighting, a deliberately narrow Vulkan direct-light subset, authored bounded OpenGL specular probes, and atomic bounded OpenGL clustered decals. Namespaced parser metadata, typed image lifecycle handling, classic fallback generation, scene-packet propagation, and material-resource records remain intact. Eligible metallic/roughness materials can enter the modern G-buffer and clustered-forward paths, use GGX/Smith/Schlick direct lighting plus a content-free analytic environment contribution, and carry albedo, normal, packed ORM, separate metallic/roughness/AO, or scalar material data, emissive, and `r_pbrDebug` data through the existing graph resources. + +This is deliberately a guarded renderer capability, not a promoted whole-frame renderer: `r_pbrMaterials`, `r_rendererReflectionProbes`, and `r_rendererClusteredDecals` default to `0`; PBR never reinterprets stock materials; unsupported workflows, dynamic images, custom programs, unsafe geometry, unusual blend expressions, or unavailable modern resources retain classic ownership. The current implementation supports a packed glTF-style ORM map, separate metallic/roughness/AO maps, or scalar-only material values. Separate maps use dedicated direct samplers after the four-entry classic material table and are admitted only when the complete resource/shader contract is available. Visible OpenGL PBR and probe evaluation additionally requires `r_rendererModernVisible 1`; enabling a PBR or probe leaf alone does not promote a visible PBR frame. `r_pbrIBL 1` supplies a neutral analytic environment for explicitly PBR-authored materials without requiring a new content asset; `r_pbrIBLIntensity` controls only that contribution. A conventional one-stage `blend blend` source-alpha declaration is admitted to the ordered PBR forward path, while complex transparency stays classic. `r_rendererModernQuality 0` is the one-setting rollback for PBR, authored probes, and clustered decals regardless of their leaf cvars. The legacy frame owner remains authoritative whenever its complete transaction cannot be replaced, and `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`, so this is not a player-facing stock-map visual or GPU-driven-lighting promotion. + +The OpenGL probe path accepts explicitly authored light-material metadata into a fixed eight-cubemap atlas and no more than 32 frame records, then selects at most two probes for each cluster deterministically. It samples base-mip specular colour only and blends back to the analytic environment when images, generations, capacity, resources, or cluster identity are incomplete. It does not provide box parallax, diffuse irradiance, runtime probe capture, or a Vulkan probe consumer. The clustered-decal path is also OpenGL-only: an atomic prepare/seal transaction publishes at most 1,024 records and 65,536 cluster references, while malformed, stale, incomplete, or overflowing input publishes no ownership and leaves the complete affected subset classic. + +The Vulkan backend has a deliberately narrow direct-light PBR corridor: an opaque metallic/roughness material with a packed ORM map and `normalFormat tangentXYZ` must also have exactly one declared classic bump stage, one diffuse stage, and one specular stage, all active and ordered bump -> diffuse -> specular. Duplicate declarations, an inactive or invalid owner condition, noncanonical order, a custom-lighting co-owner, or any other admission failure keeps the material entirely classic. An admitted packed-PBR draw replaces only the canonical final interaction submit so the complete BRDF is evaluated exactly once per surface/light. Separate metallic/roughness/AO inputs, `tangentRG`/Quake 4 normal encodings, source-alpha and other transparent PBR, analytic IBL, and authored specular probes remain classic on Vulkan. + +The retained local evidence is under `.tmp/milestone-f-evidence/20260823-final/`. A current-source debug x64 build passed the static PBR/advanced-lighting contracts, the final 10/10 dependency-light native run, the final 2/2 focused engine rerun after an earlier 8/8 safe set, and all four retail-PK4 baseline roles. That baseline used 40 retail PK4s and zero loose retail files. The procedural fixture passed on forced GL 3.3, 4.1, 4.3, and 4.5 with zero GL/FBO warning signatures: each real plan reduced 24 source packets to five depth plus five ambient owners with `planFallback=0`, forward+ executed five draws, and the visible bridge executed with resources, source, and composition present. The GL 4.5 debug-7 image contained the exact green ownership marker in 639,507 of 921,600 pixels (69.39%). `r_rendererModernQuality 0` matched the leaf-disabled OpenGL images exactly on all four tiers. + +The validation-enabled Vulkan fixture also passed and differed non-vacuously from its leaf-disabled control (per-channel RGB RMS 26.374685/20.829991/21.549890, maximum delta 252); the Vulkan master-disabled image then matched that control at exact RMS 0 and maximum delta 0. These results prove implementation, execution, and rollback, not broad visual-quality, authored-probe/decal scene, or RenderDoc coverage. All feature leaves remain default-off, `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`, and final committed-package, platform/driver, retained-review, and release promotion remain pending. Checked items below distinguish this local implementation exit from those broader release gates. + ## Suitability Review This plan is suitable for openQ4 only if legacy fallback is treated as a first-class authoring contract, not as a nice-to-have conversion step. openQ4's shipped-content compatibility still comes from the classic material parser and the `SL_BUMP`/`SL_DIFFUSE`/`SL_SPECULAR` interaction model, so PBR metadata must never be the only runtime description for a material that can ship in `baseoq4/`. @@ -86,6 +100,26 @@ materials/example/pbr_panel_preview } ``` +For conventional source-alpha PBR, keep the declaration deliberately simple: use one ordinary `blend blend` stage whose map is the PBR `albedoMap`, then provide the normal material PBR block. The direct path preserves ordered source-alpha blending and samples the PBR albedo alpha. Multiple blend/add/filter stages, alpha-tested blends, animated stage state, and other complex transparency are intentionally not promoted and remain on their authored classic path. + +```text +materials/example/pbr_window +{ + { + blend blend + map textures/example/window_albedo + } + + pbr { + workflow metallicRoughness + albedoMap textures/example/window_albedo + normalMap textures/example/window_normal + normalFormat tangentRG + ormMap textures/example/window_orm + } +} +``` + Recommended initial tokens: | Token | Meaning | Notes | @@ -114,9 +148,64 @@ Recommended initial tokens: Do not add `metalmap` or `roughmap` shorthand in the first pass. PBR materials will be rare at first, and clear names are worth the extra typing. +An image-program path is parsed as an expression, not as a raw filename. Use parser-safe asset names such as `procedural_panel_albedo`: a hyphen is an image-program operator and must not be used unescaped in a map path. + +### Authored Specular-Probe Metadata + +Author a probe as a dedicated point-light material. The light entity supplies +the origin, axis, and radius used as the probe volume; `openQ4SpecularProbe` +supplies only immutable cubemap and blend metadata: + +```text +lights/openq4/probe_room +{ + openQ4SpecularProbe { + cubeMap env/openq4/room_probe + tint 1.0 0.95 0.9 + intensity 1.25 + blendFraction 0.25 + priority 16 + } +} +``` + +Use exactly one `cubeMap` (native face convention) or `cameraCubeMap` (camera +face convention) with a static cubemap image. Render targets, image programs, +mutable images, and 2D images fail the complete declaration. `tint` defaults to +`1 1 1` and accepts finite components in `[0,64]`; `intensity` defaults to `1` +and accepts `(0,64]`; `blendFraction` defaults to `0.25` and accepts `(0,1]` as +the fraction of the point-light radius used at the volume edge; `priority` +defaults to `0` and accepts an integer from `0` through `255`. + +The probe light is metadata-only and never contributes an additive classic +light, even if ordinary light stages are present in the same material. Keep it +separate from visible classic lights. If the OpenGL probe transaction is +disabled, unsupported, incomplete, or over budget, PBR surfaces use the +analytic environment fallback; the metadata light does not become a classic +light. At most eight cubemaps can be resident, at most 32 probe records can be +published per frame, and each cluster selects at most two probes. + +### Authoring Fast Path + +Use an 8-bit tangent-space normal map and either one glTF-compatible ORM image or separate linear metallic, roughness, and AO maps. The packed route has the most portable binding contract: ORM is always **R = AO, G = roughness, B = metallic**. `tools/assets/pack_pbr_orm.py` packs separate input maps without colour-space conversion and deliberately refuses to overwrite an existing output unless `--force` is supplied: + +```powershell +python tools\assets\pack_pbr_orm.py ` + --ao path\to\asset_ao.tga ` + --roughness path\to\asset_roughness.tga ` + --metallic path\to\asset_metallic.tga ` + --output path\to\asset_orm.tga +``` + +`--ao` is optional; omitting it writes white AO. The tool accepts source images Pillow can decode and writes `.png` or `.tga`. It is covered by `python tools\tests\pbr_orm_packer.py`; use a normal file destination rather than a link. Do not gamma-correct, premultiply, or use a colour LUT on normal, ORM, metallic, roughness, or AO inputs. Albedo and emissive are colour inputs; the remaining maps are data. + +For a distributable material, dual-author ordinary Quake 4 `bumpmap`/`diffusemap`/`specularmap` stages or provide the explicit `legacy*Map` fields. `autoLegacyFallback 1` is a development preview and ARB2 safety net, not release-quality art. Keep generated PBR validation assets outside `content/` unless the project has intentionally approved them as shippable content. The repository's procedural fixture is original openQ4 test data generated only below `.tmp/`; it is validation scaffolding, not release art. + +The legacy Windows Material Editor is not a supported openQ4 authoring workflow: it is not a Meson-built tool and this workspace does not ship its required `editors/material.def`. Use a text editor with the declaration form above, then run `rendererPBRMaterialSelfTest` and a windowed test-map probe. This avoids an obsolete editor silently dropping a `pbr { ... }` block during source round-tripping. + ## Data Model -Add a compact PBR metadata record to `idMaterial` rather than encoding PBR as extra classic stages: +The live implementation stores this compact PBR metadata record on `idMaterial` rather than encoding PBR as extra classic stages: ```cpp enum pbrWorkflow_t { @@ -126,43 +215,49 @@ enum pbrWorkflow_t { }; enum pbrNormalFormat_t { + PBR_NORMAL_UNSPECIFIED = -1, PBR_NORMAL_QUAKE4_AGB = 0, PBR_NORMAL_TANGENT_RG, PBR_NORMAL_TANGENT_XYZ }; -struct pbrMaterialStage_t { - textureStage_t texture; - int colorRegisters[4]; - int scalarRegister; +struct pbrMaterialTexture_t { + idImage *image; bool present; + int filter; + int repeat; + bool allowPicmip; + bool noMips; + bool highQuality; + bool forceHighQuality; }; struct pbrMaterialInfo_t { bool enabled; pbrWorkflow_t workflow; pbrNormalFormat_t normalFormat; - pbrMaterialStage_t albedo; - pbrMaterialStage_t normal; - pbrMaterialStage_t orm; - pbrMaterialStage_t metallic; - pbrMaterialStage_t roughness; - pbrMaterialStage_t ao; - pbrMaterialStage_t emissive; - pbrMaterialStage_t legacyBump; - pbrMaterialStage_t legacyDiffuse; - pbrMaterialStage_t legacySpecular; - pbrMaterialStage_t legacyEmissive; + pbrMaterialTexture_t albedo; + pbrMaterialTexture_t normal; + pbrMaterialTexture_t orm; + pbrMaterialTexture_t metallic; + pbrMaterialTexture_t roughness; + pbrMaterialTexture_t ao; + pbrMaterialTexture_t emissive; + pbrMaterialTexture_t legacyBump; + pbrMaterialTexture_t legacyDiffuse; + pbrMaterialTexture_t legacySpecular; + pbrMaterialTexture_t legacyEmissive; int metallicRegister; int roughnessRegister; int aoRegister; int normalScaleRegister; int emissiveColorRegisters[3]; bool autoLegacyFallback; - bool hasExplicitLegacyFallback; bool hasAuthoredClassicFallback; + bool hasExplicitLegacyFallback; bool usesGeneratedLegacyFallback; bool usesApproximateLegacyFallback; + bool legacyFallbackMissing; }; ``` @@ -170,10 +265,10 @@ Implementation details: - Store PBR image references outside `stages[]` so `SortInteractionStages`, `AddImplicitStages`, coverage, classic lighting, and GUI/post behavior remain unchanged. - Initialize and clear the PBR record in `CommonInit`/`FreeData`, then copy it out of parser-temporary state before `pd` is cleared. PBR data must not point into stack-owned parsing storage. -- Add const getters such as `HasPBR()`, `GetPBRInfo()`, and targeted image/scalar helpers. Avoid exposing mutable parser internals. -- Update `ReloadImages`, `AddReference`, `SetImageClassifications`, `FreeData`, `Size`, `Print`, and material validation for the PBR images. +- Expose const getters such as `HasPBR()`, `GetPBRInfo()`, and targeted image/scalar helpers. Avoid exposing mutable parser internals. +- Include the PBR images in `ReloadImages`, `AddReference`, `SetImageClassifications`, `FreeData`, `Size`, `Print`, and material validation. - Keep material expressions as the single scalar system. PBR scalar tokens should call existing expression parsing and use `shaderRegisters` at draw time. -- Add a new texture usage such as `TD_MATERIAL_DATA` for ORM, metallic, roughness, and AO. Do not load these as `TD_SPECULAR`, because that path may modify alpha and compression behavior for classic specular maps. +- Use `TD_MATERIAL_DATA` for ORM, metallic, roughness, and AO. Do not load these as `TD_SPECULAR`, because that path may modify alpha and compression behavior for classic specular maps. - Add a color-input usage or explicit PBR image flag for albedo/emissive if the final sRGB policy needs them to bypass the legacy `TD_DIFFUSE` cache identity. Do not let PBR albedo change how stock diffuse maps are cached. ## Parser Strategy @@ -236,7 +331,7 @@ Record additions: Implementation notes: -- Increase `MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS` only if self-tests prove the GL 3.3 path can still bind required classic slots without exceeding limits. Prefer packed `ormMap` over separate maps on GL 3.3. +- Keep the legacy `uTextureIndices` table ABI at four entries. Separate metallic/roughness/AO maps use direct sampler units 4, 5, and 6, so they cannot silently displace classic or shadow sampler bindings. Prefer packed `ormMap` when authoring portability-sensitive content. - Preserve the existing classic `BUMP`, `DIFFUSE`, `SPECULAR`, and `EMISSIVE` semantics for non-PBR materials. - Add metrics: PBR record count, PBR-ready record count, missing albedo/normal/ORM counts, separate-map count, packed-map count, authored-legacy-fallback count, explicit-generated-fallback count, approximate-fallback count, modern-PBR fallback count, and old-style classic record count. - Add `rendererMaterialResourceTableSelfTest` cases for PBR packed ORM, PBR separate maps, scalar-only fallback, explicit legacy fallback, and unsupported workflow fallback. @@ -287,118 +382,154 @@ Add separate PBR controls instead of overloading enhanced materials: | Cvar | Default | Purpose | |---|---:|---| +| `r_rendererModernQuality` | `1` | Master permit for Milestone F quality domains. Set to `0` for one-setting rollback of PBR, authored specular probes, and clustered decals even if their leaf controls are enabled. | | `r_pbrMaterials` | `0` initially | Allows modern PBR shader ownership for PBR-authored materials when the modern renderer gates also pass. | +| `r_rendererReflectionProbes` | `0` | Enables the authored bounded OpenGL specular-probe consumer for eligible PBR views. Incomplete probe ownership falls back to analytic PBR environment lighting. | +| `r_rendererClusteredDecals` | `0` | Enables atomic bounded OpenGL clustered-decal ownership for complete eligible subsets. Rejected transactions remain entirely classic. | | `r_pbrGeneratedLegacyFallback` | `1` | Allows approximate generated classic fallback stages for development/test PBR materials. Authored classic stages and explicit legacy fallback maps remain valid regardless of this cvar. | -| `r_pbrDebug` | `0` | Debug overlay: albedo, normal, metallic, roughness, AO, emissive, fallback reason. | +| `r_pbrDebug` | `0` | Debug overlay: albedo, normal, metallic, roughness, AO, emissive, fallback state; mode `7` is a state marker (green = PBR shader, magenta = contract mismatch). | +| `r_pbrIBL` | `1` | Enables the content-free analytic environment contribution for explicitly PBR-authored materials. It never changes stock material interpretation. | +| `r_pbrIBLIntensity` | `1` | Scales that analytic PBR environment contribution from `0` to `4`. | | `r_pbrInferFromLegacyMaterials` | `0` | Experimental legacy material reinterpretation for research only. Never required for stock support. | -`r_pbrMaterials 0` must not change existing rendering. `r_pbrMaterials 1` should affect only materials whose parsed metadata says PBR is enabled, and it is necessary but not sufficient: `r_rendererModernVisible`, G-buffer/deferred/forward+ readiness, material-table readiness, geometry readiness, shadow policy, and pass-owner gates still decide visible ownership. +`r_pbrMaterials 0` must not change existing rendering. `r_pbrMaterials 1` should affect only materials whose parsed metadata says PBR is enabled, and it is necessary but not sufficient: `r_rendererModernQuality`, `r_rendererModernVisible`, G-buffer/deferred/forward+ readiness, material-table readiness, geometry readiness, shadow policy, and pass-owner gates still decide visible ownership. The probe and decal leaf cvars follow the same master gate. Setting `r_rendererModernQuality 0` must restore classic ownership for all three Milestone F domains without requiring a list of settings. + +## Stock And Procedural Test Candidates + +Retail Quake 4 declarations do not currently contain `pbr { ... }` metadata, so there is no native stock PBR material to enable. The best real-asset candidate for authoring and map validation is `textures/medlabs/tubearmsplatform_fastener`: `materials/medlab.mtr` supplies its diffuse, local-normal-plus-height, and specular maps, and `game/medlabs` references the material in its compiled world. It is a useful high-frequency metal fastener surface, but `game/medlabs` is BSE-heavy and should be used only after a controlled map probe is clean. + +For repeatable implementation checks, generate the original openQ4 fixture below the repository `.tmp/` tree: + +```powershell +python tools\validation\generate_pbr_fixture.py --runtime-root .tmp\milestone-f-fixture +``` + +The standard-library-only generator creates deterministic 24-bit TGA albedo, `tangentXYZ` normal, and packed ORM images, a dual-authored material declaration, and a SHA-256 manifest. It reads or copies no external texture, shader, or material asset, and its output is temporary validation scaffolding rather than shippable `content/`. `tools/tests/pbr_procedural_fixture.py` checks deterministic bytes and hashes, material wiring, manifest provenance, TGA layout, and the `.tmp` containment rule. `tools/assets/pack_pbr_orm.py` remains the authoring helper for separate data maps; it reads an explicitly selected stored channel instead of deriving luminance and writes `R=AO`, `G=roughness`, `B=metallic`. + +Run the generated material windowed on stock `maps/tools/mv2`, retain only engine-render-target screenshots, and compare the enabled case with `r_rendererModernQuality 0` plus the appropriate leaf-disabled controls. `gfxInfo` must expose exact PBR/probe/decal readiness and fallback reasons; `r_pbrDebug 7` must show PBR ownership without a magenta contract mismatch. The 2026-08-23 local campaign now records real PBR ownership on GL 3.3/4.1/4.3/4.5, an exact 639,507-pixel green debug-7 marker on GL 4.5, a non-vacuous validation-enabled Vulkan result, and exact master rollback on both APIs. Probe validation must separately exercise the eight-cubemap atlas limit, 32-record limit, top-two cluster selection, analytic fallback, and malformed/stale/capacity rejection. Clustered-decal validation must separately exercise the 1,024-record and 65,536-reference limits plus atomic zero-ownership rollback. The bounded dependency-light and engine contracts pass, but broad authored probe/decal scene review and RenderDoc evidence remain unclaimed release work. ## Implementation Phases ### Phase 0: Baseline And Gates -- [ ] Record stock-material baseline with PBR code absent or fully disabled. -- [ ] Add `r_pbrMaterials`, `r_pbrGeneratedLegacyFallback`, `r_pbrDebug`, and `r_pbrInferFromLegacyMaterials`. -- [ ] Add `gfxInfo` lines that show PBR parser support, PBR modern support, and fallback status. -- [ ] Add empty metrics counters with zero values on stock startup. -- [ ] Acceptance: safe validation matrix passes, stock startup logs do not gain material warnings, and `r_pbrMaterials 0/1` changes nothing when no PBR materials are loaded. +- [x] Record the current-source windowed stock SP load/save/reload/demo and pure MP server/auto-joined-client baseline, plus controlled leaf-disabled and `r_rendererModernQuality 0` fixture references. All four stock roles passed against 40 retail PK4s and zero loose retail files. +- [ ] Repeat every Milestone F leaf independently across stock SP and pure MP only if that broader matrix is selected for final release promotion; the local implementation gate does not claim this breadth. +- [x] Add `r_rendererModernQuality`, `r_pbrMaterials`, `r_rendererReflectionProbes`, `r_rendererClusteredDecals`, `r_pbrGeneratedLegacyFallback`, `r_pbrDebug`, `r_pbrIBL`, `r_pbrIBLIntensity`, and `r_pbrInferFromLegacyMaterials`. +- [x] Add `gfxInfo` lines that show PBR parser support, PBR modern support, and fallback status. +- [x] Add PBR metrics counters to the material-resource table. +- [x] Confirm from current-source SP/MP logs that PBR/probe/decal records remain zero on stock startup with legacy inference disabled. +- [ ] Acceptance: the final safe validation matrix passes, stock startup logs do not gain material warnings, and leaf cvar changes do nothing when no matching authored metadata is loaded. ### Phase 1: Parser Metadata Only -- [ ] Add `pbrMaterialInfo_t` to `idMaterial`. -- [ ] Parse `pbr { ... }` tokens into metadata without changing classic stages. -- [ ] Add image loading for PBR maps with correct texture usage classes. -- [ ] Update material lifecycle methods for PBR images. -- [ ] Add parser validation tests through material decl validation. -- [ ] Acceptance: PBR sample declarations parse, stock declarations compile identically, and no renderer path consumes PBR metadata yet. +- [x] Add `pbrMaterialInfo_t` to `idMaterial`. +- [x] Parse `pbr { ... }` tokens into metadata without changing classic stages. +- [x] Add image loading for PBR maps with correct texture usage classes. +- [x] Update material lifecycle methods for PBR images. +- [x] Add parser validation tests through material decl validation. +- [x] Acceptance: PBR sample declarations parse, stock declarations remain metadata-free, and metadata enters only the guarded, default-off PBR visible path. ### Phase 2: Legacy Fallback For PBR Authored Materials -- [ ] Detect whether a PBR material already has classic interaction stages. -- [ ] Add explicit `legacyBumpMap`/`legacyDiffuseMap`/`legacySpecularMap`/`legacyEmissiveMap` support. -- [ ] Add generated fallback stages for PBR-only materials as development fallback, not as release-quality fallback. -- [ ] Report approximate fallback warnings once per material. -- [ ] Track authored, explicit-generated, approximate, and missing fallback counts. -- [ ] Add a self-test that creates a PBR-only material and verifies ARB2 sees bump/diffuse/specular interaction stages. +- [x] Detect whether a PBR material already has classic interaction stages. +- [x] Add explicit `legacyBumpMap`/`legacyDiffuseMap`/`legacySpecularMap`/`legacyEmissiveMap` support. +- [x] Add generated fallback stages for PBR-only materials as development fallback, not as release-quality fallback. +- [x] Report one aggregate approximate-fallback warning per material parse. +- [x] Track authored, explicit-generated, approximate, and missing fallback counts. +- [x] Add a self-test that creates a PBR-only material and verifies ARB2 sees bump/diffuse/specular interaction stages. - [ ] Acceptance: PBR materials render something sane under the default ARB2 path, explicit fallback maps generate the expected classic stages, approximate fallback is visible in metrics, and stock materials remain unchanged. ### Phase 3: Material Resource Table Integration -- [ ] Extend packet material records with PBR flags and first PBR texture handles. -- [ ] Extend `MaterialResourceTable` semantics, records, fallback reasons, and metrics. -- [ ] Dump PBR metadata in `rendererMaterialResourceTableDump`. -- [ ] Update draw/submit plan fallback checks to understand PBR-ready versus legacy-ready materials. -- [ ] Acceptance: modern side paths can identify PBR materials and explain why they are or are not renderable. +- [x] Extend packet material records with PBR flags and first PBR texture handles. +- [x] Extend `MaterialResourceTable` semantics, records, fallback reasons, and metrics. +- [x] Dump PBR metadata in `rendererMaterialResourceTableDump`. +- [x] Update draw/submit plan fallback checks to understand PBR-ready versus legacy-ready materials. +- [x] Acceptance: modern side paths can identify PBR materials and explain why they are or are not renderable. ### Phase 4: PBR G-buffer Side Path -- [ ] Add PBR G-buffer shader variants. -- [ ] Pack albedo, normal, metallic, roughness, AO, and emissive into graph-owned attachments. -- [ ] Add `r_pbrDebug` attachment overlays. -- [ ] Keep the pass sidecar/default-off until stock parity and debug overlays are verified. -- [ ] Acceptance: a synthetic PBR material writes expected G-buffer values; stock materials still use legacy variants. +- [x] Add guarded PBR branches to the existing opaque and alpha-test G-buffer shader families. +- [x] Pack albedo, normal, metallic, roughness, AO, and emissive into graph-owned attachments. +- [x] Add `r_pbrDebug` material outputs (albedo, normal, metallic, roughness, AO, emissive, and an unmistakable PBR marker). +- [x] Keep the pass sidecar/default-off and preserve classic ownership on every unsupported PBR record. +- [x] Acceptance: the synthetic PBR command contract binds the expected G-buffer inputs and values; non-PBR records keep their classic shader route. ### Phase 5: PBR Direct Lighting -- [ ] Add PBR deferred resolve for opaque PBR G-buffer records. -- [ ] Add PBR forward+ path for alpha-tested PBR records. -- [ ] Use existing clustered light records, projected light images, falloff images, shadow descriptors, and light-grid data. -- [ ] Add CPU reference math tests for BRDF helper functions where practical. -- [ ] Acceptance: PBR test materials respond correctly to point/projected lights, roughness changes highlight width, metallic changes F0/base-color behavior, and shadow/light-grid fallbacks remain fail-closed. +- [x] Add PBR deferred resolve for opaque PBR G-buffer records. +- [x] Add PBR clustered forward paths for opaque and alpha-tested PBR records. +- [x] Reuse clustered point/projected light records, light images, falloff images, shadow descriptors, and light-grid data; AO affects indirect light only. +- [x] Keep the GGX distribution, Smith visibility, Schlick Fresnel, scalar multipliers, and roughness floor directly covered by shader-source and synthetic-command tests. +- [ ] Retain a human-reviewed engine-TGA shaded capture of the original procedural PBR fixture with analytic IBL enabled. +- [x] Add a fail-closed Vulkan direct-PBR branch for opaque `tangentXYZ` metallic/roughness materials with packed ORM, reusing the established interaction normal/albedo/specular descriptor slots as normal/albedo/ORM. +- [x] Validate the Vulkan subset windowed and unshadowed on `maps/tools/mv2` with the original procedural packed-ORM fixture, validation enabled, a non-vacuous leaf-enabled/disabled image delta, and exact `r_rendererModernQuality 0` rollback. +- [ ] Retain the point-shadow-mapped Vulkan fixture and broader human visual review before making a wider Vulkan PBR qualification claim. ### Phase 6: Guarded Visible Ownership -- [ ] Allow `r_rendererModernVisible 1` plus `r_pbrMaterials 1` to modern-own eligible PBR materials. -- [ ] Keep classic materials on existing legacy or modern-classic paths. -- [ ] Block visible ownership if a PBR material has unsupported workflow, texture layout, dynamic image, custom shader, unsafe geometry, or missing graph resources. -- [ ] Add pass-owner metrics for PBR-modern, PBR-legacy-fallback, and PBR-blocked. -- [ ] Acceptance: PBR materials can be visible in a test map without forcing stock materials into PBR interpretation. +- [x] Admit eligible PBR records when `r_rendererModernVisible 1` and the existing modern ownership gates are all satisfied. +- [x] Keep classic materials on their existing legacy or modern-classic routes. +- [x] Block modern PBR ownership for unsupported workflow/layout, dynamic image, custom shader, unsafe geometry, or missing graph resources. +- [x] Report PBR modern readiness and named resource fallback reasons through `gfxInfo` and material-resource diagnostics. +- [x] Retain real OpenGL plans on GL 3.3/4.1/4.3/4.5 with 24 source packets becoming five depth plus five ambient owners, `planFallback=0`, five forward+ draws, visible execution/resources/source/composition, and an exact green debug-7 ownership marker. +- [ ] Retain current-source human-reviewed PBR-owned opaque and source-alpha whole-frame engine-TGA images without forcing stock materials into PBR interpretation. ### Phase 7: Authoring And Tooling -- [ ] Document material syntax and texture packing in `docs/dev` and user-facing docs when ready. -- [ ] Add Material Editor awareness if the tool is still maintained for openQ4 workflows. -- [ ] Add optional import-helper guidance for glTF-style ORM maps. -- [ ] Add sample PBR materials only if the project intentionally wants shipped sample content. Otherwise keep test assets under `.tmp/`. -- [ ] Acceptance: artists can author a PBR material without reading renderer code. +- [x] Document material syntax, supported packing, test candidates, compatibility behavior, and rollback in `docs/dev`. +- [x] Decide Material Editor ownership: the legacy MFC editor is not Meson-built and lacks its required definition file, so it is explicitly unsupported rather than allowed to lose PBR source blocks. +- [x] Add an optional, tested import helper and guidance for glTF-style ORM maps (`tools/assets/pack_pbr_orm.py`; `R=AO`, `G=roughness`, `B=metallic`). +- [x] Generate original deterministic openQ4 validation assets only below `.tmp/`; do not incorporate external assets or ship the generated fixture as `content/`. +- [x] Acceptance: the declaration template, texture rules, ORM helper, fallback rules, and test commands allow artists to author a PBR material without reading renderer code. ### Phase 8: Optional IBL And Quality Layer -- [ ] Decide whether light-grid data can provide diffuse irradiance for PBR materials. -- [ ] Add optional specular environment probe support only after direct PBR lighting is stable. +- [x] Add a PBR-only analytic diffuse/specular environment baseline behind `r_pbrIBL` and `r_pbrIBLIntensity`; it needs no content asset and does not alter legacy materials. +- [x] Add guarded authored OpenGL specular-probe support with a fixed eight-cubemap atlas, at most 32 records, deterministic top-two-per-cluster selection, and analytic fallback. +- [x] Keep the probe approximation explicitly specular-only and base-mip: no box parallax, diffuse irradiance, runtime capture, or Vulkan support is claimed. +- [x] Add guarded atomic OpenGL clustered-decal ownership bounded to 1,024 records and 65,536 cluster references, with zero published ownership on a rejected transaction. +- [x] Pass current-source dependency-light and engine contracts for probe atlas packing, bounded top-two selection, analytic fallback, and atomic malformed/stale/capacity decal rejection. - [ ] Consider clearcoat, sheen, anisotropy, height/parallax, and detail normals as later material extensions. -- [ ] Acceptance: optional quality features never become required for base PBR correctness. +- [ ] Acceptance for broad release promotion: retain authored probe/decal scene evidence beyond the passing bounded contracts; analytic IBL remains the complete PBR fallback when probe ownership is unavailable, and advanced quality features never become required for base PBR correctness. ### Phase 9: Promotion And Release -- [ ] Extend renderer validation matrix with PBR parser, material table, G-buffer, deferred, forward+, visible, and fallback self-tests. -- [ ] Run SP/MP gameplay with stock assets and PBR disabled/enabled. -- [ ] Run a PBR test scene on `auto`, `gl33`, `gl41`, `gl43`, and `gl45` where available. +- [x] Extend renderer validation coverage for the PBR parser, material table, G-buffer, deferred, forward+, visible, source-alpha, analytic-IBL, authored-probe atlas/selection, atomic clustered decals, master rollback, and fallback contracts. +- [x] Run current-source windowed stock SP load/save/reload/demo and pure MP server/auto-joined-client gameplay with the Milestone F leaves at their default-off values; retain controlled enabled/disabled fixture runs separately. +- [x] Run the original procedural PBR fixture on `gl33`, `gl41`, `gl43`, and `gl45`, retaining engine-TGA ownership images and exact master-disabled controls. These prove PBR execution and rollback, not broad human visual-quality acceptance; `auto` remains part of general promotion coverage. +- [x] Run the procedural packed-ORM fixture through validation-enabled Vulkan unshadowed direct interactions with `r_pbrIBL 0`, including a non-vacuous enabled/disabled delta and exact master-disabled control. Authored probes and clustered decals remain OpenGL-only. +- [ ] Add the point-shadow-mapped Vulkan case and any wider stock/leaf matrix selected for final release promotion. - [ ] Capture RenderDoc on forced modern tiers. -- [ ] Update `docs/dev/release-completion.md` only when user-visible PBR support lands. -- [ ] Add curated release notes in `docs/dev/releases/vX.Y.Z.md` before shipping. +- [x] Record the local current-source implementation-exit evidence while leaving final committed-package, platform/driver, retained-review, and release promotion in carry-forward. +- [x] Update curated `docs/dev/releases/v0.12.0.md` notes before shipping. - [ ] Acceptance: no stock asset regression, PBR feature documented, rollback documented, shipped PBR assets have authored/explicit legacy fallback with zero approximate fallback unless explicitly waived, and release notes are player-readable. ## Validation Matrix Additions Add safe tests: -- `renderer-pbr-parser-selftest`: parser metadata, scalar registers, image usage classes, normal format enum, and error cases. -- `renderer-pbr-legacy-fallback-selftest`: generated classic stage fallback and explicit fallback maps. -- `renderer-pbr-material-table-selftest`: PBR semantics, texture binding counts, packed/separate maps, fallback reasons. -- `renderer-pbr-gbuffer-selftest`: G-buffer packing and debug overlays. -- `renderer-pbr-lighting-selftest`: deferred/forward PBR shader readiness and BRDF sanity. -- `renderer-pbr-visible-selftest`: guarded visible ownership on a synthetic packet frame. +- [x] Parser metadata, scalar registers, image usage classes, normal format enum, and error cases through `rendererPBRMaterialSelfTest`. +- [x] Generated classic-stage fallback and explicit fallback maps through `rendererPBRMaterialSelfTest`. +- [x] Packet propagation plus PBR semantics, texture binding counts, packed/separate maps, and fallback reasons through `rendererScenePacketSelfTest` and `rendererMaterialResourceTableSelfTest`. +- [x] Guarded opaque PBR resource admission, G-buffer command/input packing, scalar propagation, `r_rendererModernQuality 0` rollback, and clustered-forward surface-owner deduplication through `rendererPBRVisibleSelfTest`; attachment-overlay readiness remains covered by `rendererGBufferSelfTest`, while PBR debug shader routes remain covered by `renderer_pbr_materials.py`. +- [x] G-buffer/deferred/forward PBR program-link readiness through `rendererPBRVisibleSelfTest`; direct texture-unit bindings, normal-format markers, scalar/BRDF source contracts, source-alpha admission, and analytic-IBL routes through `renderer_pbr_materials.py`. +- [x] Guarded visible PBR ownership admission on a synthetic packet frame through `rendererPBRVisibleSelfTest`. +- [x] Source-alpha PBR blend admission, ordered forward selection, and analytic-IBL uniform/shader contracts through the static `renderer_pbr_materials.py` contract. +- [x] Original fixture determinism, hashes, TGA layout, manifest provenance, material wiring, and `.tmp` containment through `pbr_procedural_fixture.py`. +- [x] Explicit stored-channel ORM packing and dependency-injected core behavior through `pbr_orm_packer.py`. +- [x] Run and retain the current-source PBR, authored-probe, clustered-decal, and master-rollback reports: final focused engine 2/2 after the earlier 8/8 set, static PBR/advanced-lighting passes, and the final native 10/10 pass. Add gameplay/manual coverage: - Stock SP map with `r_pbrMaterials 0`. - Stock SP map with `r_pbrMaterials 1` and `r_pbrInferFromLegacyMaterials 0`. - Stock MP listen-server case with the same two settings. -- PBR test material scene under `r_rendererModernVisible 1`. +- Original procedural PBR fixture under `r_rendererModernVisible 1` with retained human-reviewed PBR-owned whole-frame engine-TGA images on each applicable OpenGL tier. +- Authored-probe fixture with atlas-ready and analytic-fallback controls, including record/cubemap limits and deterministic top-two cluster selection. +- Clustered-decal fixture with a sealed complete subset plus malformed, stale, incomplete, record-overflow, and reference-overflow zero-ownership controls. - PBR test material scene under `r_renderer arb2`, `r_glTier legacy`, `r_pbrMaterials 0`, and `r_pbrGeneratedLegacyFallback 0` to prove authored/explicit fallback does not depend on modern PBR or approximate generation. -- Legacy rollback run with `r_renderer arb2` and `r_glTier legacy`. +- One-setting Milestone F rollback with `r_rendererModernQuality 0`, plus legacy renderer/tier fallback with `r_renderer arb2` and `r_glTier legacy`. Failure conditions: @@ -416,7 +547,7 @@ Failure conditions: | Stock `specularmap` content is mistaken for roughness or metallic data | Never infer PBR from classic stages by default. Keep `r_pbrInferFromLegacyMaterials 0`. | | PBR normal maps use different channel conventions than Quake 4 bump maps | Require `normalFormat` for new PBR maps or warn loudly; support both Quake 4 A/G and common RG encodings. | | Color-space mismatch makes PBR look wrong | Define PBR-only albedo/emissive decode policy before visible promotion; keep legacy path unchanged. | -| Texture unit pressure on GL 3.3 | Prefer packed `ormMap`; fail closed to legacy fallback when separate maps exceed limits. | +| Texture unit pressure on GL 3.3 | Separate metallic/roughness/AO direct samplers are fixed at units 4–6 and are admitted only with a complete sampler contract; prefer packed `ormMap` for portability-sensitive content. | | Legacy fallback looks poor | Prefer authored classic stages; support explicit `legacyBumpMap`, `legacyDiffuseMap`, and `legacySpecularMap`; warn and fail release validation when fallback is approximate. | | PBR shader variants explode | Add dedicated PBR families with compact workflow flags; do not cross-product every legacy feature. | | Modern visible path drops special effects or shadows | Reuse existing pass ownership and fallback gates; PBR cannot override those gates. | @@ -440,6 +571,11 @@ Primary: - `src/renderer/ModernGLShaderLibrary.cpp` - `src/renderer/ModernGLExecutor.h` - `src/renderer/ModernGLExecutor.cpp` +- `src/renderer/ModernClusteredLighting.h` +- `src/renderer/ModernClusteredLighting.cpp` +- `src/renderer/ModernSpecularProbeAtlas.h` +- `src/renderer/ModernSpecularProbeAtlas.cpp` +- `src/renderer/AdvancedLightingCore.h` - `src/renderer/RenderSystem_init.cpp` - `src/renderer/tr_local.h` @@ -447,6 +583,10 @@ Secondary: - `tools/tests/renderer_validation_matrix.py` - `tools/tests/renderer_gameplay_benchmark.py` +- `tools/tests/pbr_procedural_fixture.py` +- `tools/tests/pbr_orm_packer.py` +- `tools/validation/generate_pbr_fixture.py` +- `tools/assets/pack_pbr_orm.py` - `docs/dev/gl-renderer-modernization.md` - `docs/dev/renderer-validation-matrix.md` - `docs/dev/release-completion.md` @@ -454,12 +594,21 @@ Secondary: ## Definition Of Done +The 2026-08-23 local Windows implementation exit satisfies the scoped +implementation, stock-compatibility, execution, and master-rollback portion of +this list. It does not by itself satisfy final committed-package, +platform/driver, RenderDoc, broad authored probe/decal visual, retained-review, +or release-promotion requirements. + PBR support is complete enough to ship when: - Existing shipped Quake 4 materials render through the same default compatibility path unless explicitly opted into modern visible rendering. - New PBR-authored materials parse, load, reload, reference-count, validate, and dump correctly. - PBR-authored materials have a working ARB2 fallback, and shipped PBR materials use authored classic stages or explicit legacy fallback maps rather than approximate generation. - Modern PBR G-buffer, deferred, and forward+ paths are cvar-gated, fail closed, and covered by self-tests. +- Authored OpenGL probes stay within eight cubemaps and 32 records, publish no more than two deterministic indices per cluster, and fall back to analytic PBR environment lighting without claiming parallax or diffuse irradiance. +- OpenGL clustered decals publish only a completely prepared and sealed transaction within the 1,024-record and 65,536-reference limits; every rejected transaction retains classic ownership. +- `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0` until its separate visible-lighting parity gate is proven, and `r_rendererModernQuality 0` restores classic ownership for every Milestone F domain. - PBR debug overlays show albedo, normal, metallic, roughness, AO, emissive, and fallback state. - SP and MP validation passes with PBR disabled and enabled on stock assets. - A PBR test scene validates modern visible output and legacy rollback. diff --git a/docs/dev/plans/phase-f-recon/shadow-infra.md b/docs/dev/plans/phase-f-recon/shadow-infra.md index ee39a0a7..21a29a1f 100644 --- a/docs/dev/plans/phase-f-recon/shadow-infra.md +++ b/docs/dev/plans/phase-f-recon/shadow-infra.md @@ -34,7 +34,7 @@ - One GLSL caster program for all opaque+perforated casters; **slope-scale/constant depth offset applied in the shader** (`r_shadowMapPolygonFactor` 0.75 / `r_shadowMapPolygonOffset` 0.5) because shader-written depth ignores glPolygonOffset (:7866-7883). (On Vulkan, dynamic depth-bias state is an alternative if the receiver convention stays consistent — speculation/design choice.) - State: colorMask off, depth LEQUAL+write, blend/stencil off, scissor on (:7885-7891). - Per cascade: viewport+scissor to the tile (slot origin + `cascadeIndex%atlasDiv × tileSize`), `glClear(DEPTH)` per tile, projection = `R_ShadowMapClipPlanesToGLMatrix(clipPlanes[cascade])` (:7895-7923). Caster chains: primary/secondary static, tertiary/quaternary dynamic; compose mode blits cached static tiles then draws dynamics only (:7852-7864). -- Caster culling: `r_shadowMapCasterCulling` default 2 = store back faces (:305, apply :2907). +- At the pinned reconstruction revision, caster culling used `r_shadowMapCasterCulling` default 2 = store back faces (:305, apply :2907). Current semantics are documented in the user shadow-mapping guide. - Point pass (:7986-8160): 6 faces, per-face FBO bind, colorMask ON, clear color 1.0, near=clamp(0.5, depthClamp?16:4, far×0.01), depth clamp when available, per-face view matrices (:7303-7349). ### Receiver (shadow interaction) uniform surface (:9822-9971, :8549-8615) @@ -74,4 +74,4 @@ Required changes: - **Recommendation**: keep the 128B push range for per-draw volatile data (MVP 64B + a few params/UBO offsets); stream everything else through a new host-visible uniform ring (same `vkRing_t` pattern :72-78, usage UNIFORM_BUFFER_BIT, 256B alignment — `minUniformBufferOffsetAlignment` ≤ 256 guaranteed) bound as VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC with dynamic offsets: binding A = per-draw interaction block (origins/projections/stage matrices/colors, ~256B slice), binding B = per-light shadow block (~768B slice, rewritten per space for shadowRows). Optional simplification (speculation): upload world-space shadow rows once per light plus the 64B model matrix per draw and do the world→local plane transform in-shader, eliminating the per-space CPU recompute at :8567-8576. ## Cross-check summary of what vk must build for full F -(a) interaction decomposition walk (from tr_render.cpp), (b) interaction SPIR-V (unshadowed + shadowed projected + shadowed point), (c) atlas VkImage + tile allocator (simple row scan suffices initially; GL's is :3572), (d) caster depth-only pipeline + shader with in-shader depth offset (or dynamic depth bias), (e) point cube pipeline + packed-depth/hardware-compare shader pair, (f) comparison sampler support in the image layer, (g) frame-scope suspend/resume with LOAD, (h) uniform ring + new pipeline layout/descriptor sets, (i) later: real `RB_ShadowMapResourcesKnownGood` so the front-end can shed stencil volume generation (tr_light.cpp:1447). \ No newline at end of file +(a) interaction decomposition walk (from tr_render.cpp), (b) interaction SPIR-V (unshadowed + shadowed projected + shadowed point), (c) atlas VkImage + tile allocator (simple row scan suffices initially; GL's is :3572), (d) caster depth-only pipeline + shader with in-shader depth offset (or dynamic depth bias), (e) point cube pipeline + packed-depth/hardware-compare shader pair, (f) comparison sampler support in the image layer, (g) frame-scope suspend/resume with LOAD, (h) uniform ring + new pipeline layout/descriptor sets, (i) later: real `RB_ShadowMapResourcesKnownGood` so the front-end can shed stencil volume generation (tr_light.cpp:1447). diff --git a/docs/dev/proposals/rbdoom3-bfg-parity-modernization-plan.md b/docs/dev/proposals/rbdoom3-bfg-parity-modernization-plan.md index a19d4ec5..900eb58f 100644 --- a/docs/dev/proposals/rbdoom3-bfg-parity-modernization-plan.md +++ b/docs/dev/proposals/rbdoom3-bfg-parity-modernization-plan.md @@ -3,6 +3,9 @@ Date: 2026-02-09 Author: Codex (analysis + implementation plan) +> [!IMPORTANT] +> This is a historical proposal, not a current-state inventory. Several baseline statements below predate the modern GL, Vulkan, shadow-map, and light-grid work now in the tree. Use the [engine capability matrix](../engine-capability-matrix.md) for authoritative implemented/experimental/missing status, the [idTech 5-level modernization roadmap](../idtech5-modernization-roadmap.md) for the current compatibility-safe delivery order, and [source provenance](../source-provenance.md) before considering any upstream import. + ## 1. Goal Bring openQ4 to the same technical standard as modern RBDOOM-3-BFG, with special focus on lighting and shadowing, while preserving openQ4 rules: diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index 6cc2111d..f263106c 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -36,6 +36,22 @@ is `docs/dev/macos-moltenvk-decision.md`. ## Ready For Changelog +- [x] Milestone F now has a locally validated, default-off implementation for explicitly authored PBR materials, bounded OpenGL specular probes, and atomic OpenGL clustered decals. The 2026-08-23 current-source debug x64 evidence passed the static PBR/advanced-lighting checks, the final native 10/10 run, the final focused engine 2/2 rerun after an earlier 8/8 safe set, all four retail-PK4 compatibility roles, forced GL 3.3/4.1/4.3/4.5 PBR execution, the narrow validation-enabled Vulkan route, and exact `r_rendererModernQuality 0` rollback against leaf-disabled images. The stock baseline used all 40 retail PK4s with zero loose retail files; the generated PBR fixture remained temporary under `.tmp/`. PBR, probe, decal, and modern-visible controls stay default-off, `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` stays `0`, and this local implementation exit is not final committed-package/platform/driver promotion or broad authored probe/decal visual qualification. +- [x] Milestone F's remaining quality leaves are implemented independently and default off on OpenGL and Vulkan. Bounded view-aligned froxel integration, depth-normal SSR, and fixed eight-tap depth-derived SSGI share the native scene-colour/depth presentation tail without allocating temporal history for effect-only use. The stable eight-float contract, final 11/11 native suite, static backend checks, Vulkan shader pin, combined GL/Vulkan `game/airdefense1` gameplay, individual OpenGL leaves, visible enabled delta, and exact master-off zero packet pass locally with engine screenshots and clean API/error counters. `r_rendererModernQuality 0` remains the one-setting rollback. This is a scoped screen-space implementation, not shadowed light-injected volumetrics, roughness-aware G-buffer reflections, world-space GI, or whole-frame modern-lighting promotion. +- [x] The post-roadmap performance pass removes two default-path regressions without enabling the guarded level-load cache experiment. Cinematic fast-forward no longer samples non-presented poses for every spawned entity on every 60 Hz tick, visible-frame interpolation samples active movers plus their bounded physics-team members and cleanup members, and maps without baked light-grid assets no longer construct a 22,024-point bake/debug layout during ordinary loading. On `game/airdefense1`, the controlled 186-second scripted skip fell from 7,113 to 5,770 ms at the exact same game-time endpoint and missing-grid setup fell from 357 to 1 ms; the isolated end-to-end run improved from 33,121 to 25,091 ms and steady pacing from 110.8 to 120.4 Hz, with the documented cache/scene-variance qualification. The final staged build also passed default `game/airdefense2`, pure auto-joined `mp/q4dm1`, and a four-role compatibility run against 40 retail PK4s with zero loose retail files on the tested Windows system. +- [x] Players and testers can opt into native-history temporal anti-aliasing/upscaling with `r_temporalAA 1`, and can add delayed GPU-time dynamic resolution with `r_rendererDynamicResolution 1`. The OpenGL and Vulkan paths keep HUD, menus, console, screenshots, and save previews out of accumulated low-resolution history; reject stale depth, camera cuts, capture frames, particles, unsupported deformation, subviews, in-world GUI, and view-model motion conservatively; and fall back to the current frame or established SMAA path whenever the exact history contract is unavailable. Both features remain default-off while clean-package and broader platform/driver qualification continues. +- [x] openQ4 now has a portable job service for bounded engine work without changing stock assets or compatibility formats. It bounds admitted lists and per-list work, supports dependency ordering and starvation-safe low/normal/high priorities, exposes cooperative cancellation and diagnostics, and uses sleepable waits instead of spinning. Dedicated servers default to deterministic inline execution, and `jobs_enable 0` still executes every accepted job instead of dropping it. The learned level-load read/PK4-inflate and framing/integrity pipeline is its first production consumer: workers read independently opened, identity-checked sources and publish sealed immutable DTOs while established owners retain asset-specific parse/decode, adoption, and upload authority. The foundation and consumer are integrated; clean final-package recapture and retained new-consumer parity/cancellation evidence remain promotion work. +- [x] Repeat map loads can now reuse only work learned from the exact installed content and settings. Successful loads record a per-map manifest with full SHA-256 mode/entity-filter keys, bound to the ordered VFS search, PK4/pure state, individual source identities, and load-affecting settings; a later exact match performs bounded cancellable reads, PK4 inflation, supported source framing, and whole-buffer integrity validation, then substitutes an immutable generation/type/source-identity DTO only after the ordinary authoritative VFS lookup succeeds again. Versioned private model, classic-world, and collision caches decode transactionally. Animation cache v3 follows the exact compiled-or-text parser source, adds a loose-source CRC and whole-record length/CRC trailer, validates private state all-or-nothing, and publishes atomically in both SP and MP. Corrupt or mismatched data is removed or ignored before source fallback, while packed MD5RProc worlds deliberately remain on their source path. `com_levelLoadModernization` defaults off and gates cache reads, writes, animation-cache work, and preload replay together; diagnostics also default off. The subordinate controls remain available for focused experiments after the master is enabled, and `com_levelLoadModernization 0` is the full classic source-path rollback. No retail assets or pure-PK4 behavior are changed. The local implementation gate is complete; final committed-package runtime/platform timings and Milestone B release-promotion evidence remain pending in `docs/dev/loading-cache-modernization.md`. +- [x] Renderer performance capture now uses one backend-neutral whole-frame GPU timing contract on OpenGL and Vulkan. Both paths resolve delayed timestamps without adding a current-frame wait, invalidate old generations across map/session and renderer/device discontinuities, and expose validity plus availability/drop/reset diagnostics through renderer ABI v11. `rendererBenchmarkCapture` pairs unique resolved GPU frames with high-resolution whole-renderer CPU samples in the versioned budget marker; target-hardware GL/Vulkan evidence is still required before performance promotion claims. +- [x] The renderer now shares ordered material/pass semantics, explicit GL/Vulkan clip and viewport conversion, semantic legacy/skinned vertex layouts, and typed generational buffer slices. The default-off `r_gpuSkinning` corridor admits only exact four-weight MD5/MD5R data, preserves signed residual weights and diffuse vertex colors, and emits the ordinary draw-vertex stream through bounded OpenGL/Vulkan compute paths. CPU positions remain authoritative for collision, hits, decals, overlays, and software consumers; stencil volumes and every invalid, unsupported, stale, exhausted, or unavailable case retain a named whole-surface CPU fallback. Dependency-light and renderer-module self-tests cover the shared contract. Clean-package SP/MP images, collision/hit digests, repeated animation-heavy CPU reduction, and platform/driver evidence remain required for promotion under `docs/dev/gpu-skinning-modernization.md`. +- [x] Internet-server administration and package negotiation now fail closed at their remaining legacy edges. `rcon2` keeps remote-console passwords out of packets, applies bounded challenge and reply budgets, and redacts private settings from console output, history, journals, configuration serialization, command expansion, and completion previews; plaintext rcon is disabled unless both relevant sides explicitly opt into it. Server-originated userinfo and synchronized-CVar dictionaries decode transactionally and can update only the CVar class owned by that wire message, so they cannot borrow authority over private or unrelated settings. Server-provided redirects and PK4 entries accept only bounded HTTP or HTTPS URLs with a syntactically valid DNS, IPv4, or bracketed IPv6 host. Standard Meson packages keep in-process direct PK4 transfer disabled while preserving validated web-redirect prompts; a separately integrated curl-enabled build additionally rechecks the URL, refuses redirects, applies connect/stall/total-duration limits, and retains package path, size, and checksum validation. The obsolete network-driven executable updater no longer downloads or runs code and ignores server-controlled release text and links. +- [x] Pure multiplayer can remain enabled without giving a server control over executable code. `si_pure 1` is no longer silently disabled; the ordered asset-PK4 list is still enforced, while the protocol 2.41 game-code field uses the stock 1.4.2 `game300.pk4` checksum only as a platform-independent compatibility token for an already loaded module from trusted local openQ4 package/module roots. Missing or unexpected tokens, modules, asset lists, and platform IDs fail closed, code-bearing server mods require the explicit `net_serverAllowServerMod 1` opt-in, and neither pure negotiation nor package download can install or restart into game code. The token is not a cryptographic measurement of the loaded module, a module-equality proof, or an anti-cheat guarantee. +- [x] Truncated and malformed multiplayer state now fails at an explicit safety boundary. Bit-message underflow is tracked across normal and delta reads, queued messages and delta user commands are length-checked, and audited SP/MP leaf readers stage decoded fields until their payload is valid while validating referenced slots, types, ranges, and remaining data. Invalid covered traffic is dropped; a late malformed top-level snapshot tears down the affected session before another game or presentation frame. The legacy entity lifecycle is not a whole-snapshot transaction, and this targeted hardening does not claim formal verification of every parser. +- [x] Multiplayer validation now reaches gameplay without menu-input automation. Supported MP test profiles explicitly launch with the archived `ui_autoJoin 1` setting and the retail-PK4 compatibility baseline proves that the local client is active, not spectating, outside the session menu, and drawing its HUD on both sides of the screenshot. Only tests specifically covering the join menu or initial spectator/join flow may explicitly set `ui_autoJoin 0`. +- [x] Multiplayer function keys once again use Quake 4's canonical impulse actions: F1/F2 vote, F3 ready, F6 team, and F7 spectate bindings execute real key actions, while the controls menu edits the same vote/ready actions. Ready presses retain their throttle but reach the server through the reliable ready message, so the warmup prompt names a real key; its stock two-line non-tourney layout uses the inline keycap size so the instruction is not clipped out of the authored 40-pixel HUD window. Existing configs migrate only the exact historical openQ4 defaults (`voteyes`, `voteno`, `ready`, `toggleteam`, and `spectate`) on those five keys; custom and compound bindings are preserved, and the idempotent `ready` console command remains available. Casual no-time-limit matches also normalize a retained positive `si_overtime` value to sudden death with a zero timed period, avoiding a rejected competitive-rules import at stock deathmatch defaults. +- [x] Retail-asset compatibility reports now describe what actually ran: unchanged approved retail PK4 bytes beneath packaged openQ4 overlays. Verification rejects dry runs, stale or missing expected-asset bindings, mismatched source provenance, recorded failure arrays, and altered role launches; reports also count every packaged-overlay virtual path that supersedes retail content instead of presenting an overlay-colliding run as stock-only evidence. +- [x] Successful Windows full installs and VS Code fast stages now apply one shared, explicit cleanup manifest so extensionless POSIX client/dedicated binaries and obsolete renderer `.dll.mainbak` files cannot survive a platform switch. The old `baseoq4/skins` directory is removed only when empty and populated local content is preserved. Stage-root or directory links, junctions, and wrong entry types fail closed; a known stale leaf symlink is unlinked without following its target. +- [x] Manual save-game previews now render one coherent full-size frame before a bounded CPU center crop and resize produces the intended 320x240 thumbnail, eliminating repeated or nested screen-effect copies at widescreen resolutions. Renderer readback also keeps row-alignment padding out of captures whose widths are not multiples of four. - [x] Liquids now behave like complete combat volumes: projectiles and hitscan shots pass through while producing reliable crossing splashes and sounds, submerged projectile smoke becomes bubbles, and hitscan wakes cover only the underwater ray segment. Quake 4-height probes and a gravity-aware launch arc make water jumps reliable from clear pool ledges; the wading basin is also normally jumpable. Clear water remains readable, liquid acoustics suppress hall-like wet tails and duplicate jump splashes, and drowning, slime, and lava report distinct localized obituaries with graphical icons. The well-lit `mp/liquid_lab` developer map provides recessed deep, shallow, and wading water plus vivid cellular slime and bright lava; its sky ceiling, shadow-casting beams, illuminated deep-pool floor, lava heat haze, steam, bubbling surfaces, submerged ambience, and full-arsenal respawns make every behavior immediately testable. - [x] The marine hovertank once again plays its engine and hover-pad loops throughout the vehicle sequence. Single-player now creates and updates the moving sound emitters just like the retail game and openQ4's multiplayer module; the later null-safe sound-world teardown keeps those emitters safe during map and engine shutdown. This addresses the missing vehicle audio reported in GitHub issue #114 without requiring different OpenAL, EAX, or speaker settings. - [x] The built-in `dmap` compiler and other engine-side geometry tools now initialize their own triangle-surface allocator copy before use. This prevents light-volume generation from dereferencing an uninitialized allocator after the renderer was split into a runtime module, fixing the Windows crash reported while compiling `game/airdefense1` in GitHub issue #108. @@ -50,7 +66,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] macOS OpenAL Soft migration builds now fail clearly during configuration when the requested system package is not visible, instead of silently falling back to Apple's OpenAL framework and then failing to compile against `AL/...` headers. The migration lookup is pkg-config-only, and the provider guide documents the `PKG_CONFIG_PATH` needed for Homebrew's keg-only OpenAL Soft package. Release packages remain on Apple's framework. This resolves GitHub issue #94. - [x] Managed competitive multiplayer now has one cohesive, server-authoritative match system instead of scattered ready, vote, pause, team, demo and admin paths. An explicit competitive profile enables validated/frozen rules, readiness blockers, connection-scoped player/captain/coach/broadcaster/referee roles, durable rosters and substitutes, self-only coach/substitute withdrawal, Duel queues, team locks, tactical and technical pauses, typed proposals, recipient-filtered spectator follow/vitals, and BO1/BO3/BO5 map-pool/veto workflows; casual servers retain their existing behaviour. The localized Match Control surface exposes Status, Teams, Proposals, Rules, Series and Evidence from bounded recipient views with truthful denial reasons and confirmations. Series progress and its mutable report draft use one digest-protected schema-3 atomic checkpoint across maps without persisting names, addresses or client-slot authority; team-side mappings recover directly, while a trusted operator explicitly rebinds current Duel connections with `matchSeriesBind`. Managed matches also link series identity and an automatically owned server MVD into bounded schema-v2 per-map JSON evidence and an immutable schema-1 final series report promoted beneath `fs_savepath`, with recording/write failures isolated from the result. Managed activation cancels any inherited vote lifecycle, and legacy settings/kick/team/shuffle/restart/next-map adapters fail closed instead of bypassing typed authority. Local listen-server operators can grant a strictly observational broadcaster role through Match Control, and dedicated operators have the equivalent typed `matchBroadcaster` adapter. Supported placed, respawning major items are tracked authoritatively on the pause-safe match clock and projected only to explicit broadcaster/referee recipients; players, coaches and ordinary spectators receive no timers, and raw item tokens are never rendered. Captain invitations for neutral spectators remain fail-closed. The broadcaster role and recorded server MVD are supported, but no reachable engine Q4TV/repeater transport, delayed broadcast or live multi-POV path is shipped; the pure public-only repeater policy remains a fail-closed future integration boundary. - [x] Bordered window placement now accounts for the complete native frame on Windows, X11, and macOS instead of treating the drawable client rectangle as the whole window. Saved and restored positions track the outer-frame origin, usable-area constraints reserve the actual title-bar and resize-border extents, oversized client requests shrink enough for all chrome to remain reachable, display/DPI moves persist the updated frame position, and startup repeats placement after the window server finishes decorating the window. Native Wayland continues to delegate absolute positioning to the compositor by design. -- [x] Malformed legacy console, configuration, and numeric input now fails safely instead of risking an invalid memory access or overflowing an impulse command. Command argument collection is bounded by both slot and storage capacity, escaped argument reconstruction grows safely, lexer lookahead preserves the next token, and extreme string growth is checked before allocator rounding or legacy 32-bit narrowing. Relative file mutations reject traversal, rooted paths, platform-specific filename aliases, and other non-portable targets before touching the save directory. Server-offered package downloads are confined to new PK4 destinations beneath that directory, including Windows device-name protection, reject invalid or overflowing advertised sizes and response types, enforce accepted sizes against the received HTTP body, and release temporary files on every completion path; multiplayer negotiation now reports a deterministic empty game-package slot when no game-code package is requested. +- [x] Malformed legacy console, configuration, and numeric input now fails safely instead of risking an invalid memory access or overflowing an impulse command. Command argument collection is bounded by both slot and storage capacity, escaped argument reconstruction grows safely, lexer lookahead preserves the next token, and extreme string growth is checked before allocator rounding or legacy 32-bit narrowing. Relative file mutations reject traversal, rooted paths, platform-specific filename aliases, and other non-portable targets before touching the save directory. In separately integrated curl-enabled builds, server-offered package transfers are confined to new PK4 destinations beneath that directory, including Windows device-name protection, reject invalid or overflowing advertised sizes and response types, enforce accepted sizes against the received HTTP body, and release temporary files on every completion path; standard Meson packages leave that direct-transfer path disabled. Multiplayer negotiation reports a deterministic empty game-package slot when no game-code package is requested. - [x] Standalone game-module builds now refresh whenever either gameplay code or their shared engine support sources change, verify the complete staged snapshot by content, and compile the legacy inline-assembly SIMD implementations only for the 32-bit x86 target that supports them. This prevents stale modules and restores reliable x64 and modern-platform builds without dropping the existing x86 path. - [x] Windows x64 engine and standalone game-module builds are warning-clean again. Legacy native-width conversions now use checked boundaries or native-width accounting, intentional allocator alignment is explicit and layout-guarded, dead SDK remnants are removed, and the cleaned warning classes fail the build if they return. SDL configuration no longer reports a false missing-libdecor-version warning on platforms that do not use libdecor, and the Windows validation wrapper now forwards an explicitly selected companion repository correctly instead of treating its parameter name as a path. - [x] Bound controls are now easier to recognize at a glance: Settings key-bind rows show a bounded summary across keyboard, mouse, and controller inputs, while supported in-game prompts follow the device family the player used most recently. Keyboard bindings use clean labeled keycaps with square arrow symbols, upright mouse silhouettes fill the bound physical button without ambiguous numbers, and controller bindings use neutral positional graphics plus a localized Back/View label rather than assuming an Xbox, PlayStation, or Nintendo legend scheme. Inline icons now sit at nearly the full text-line height, important centered multiplayer prompts use a larger 150% treatment, and higher-sample rounded edges keep the procedural button art smooth at modern resolutions; the spectator HUD uses the same presentation for its follow-cycle and exit controls. Bind capture is safer too: Escape, Start, or Back cancels without erasing the action, while Backspace or Delete explicitly clears it. @@ -117,7 +133,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] OpenGL renderer robustness is stronger across driver tiers and repeated video restarts: render-target names are tied to their owning context generation, framebuffer validation is removed from the bind hot path and cached per cubemap face, texture-unit binding has core DSA, EXT DSA, and compatibility fallbacks, VAO element-buffer state is tracked correctly, multi-bind fallback is target-aware, upload rings try idle slots before bounded fence waits, debug contexts queue structured driver messages safely, GPU-driven shaders reject invalid buffer indices, and renderer validation fails on hidden GL errors or failed benchmark thresholds. - [x] The single-player weapon wheel now uses a smoother circular depth-of-field gather with a per-pixel blur radius and depth-edge rejection, reducing star-shaped smears and foreground/background color bleed while keeping the nearby view weapon legible. - [x] Performance presets now change only real, registered runtime controls: their 1x-to-16x anisotropy ladder drives the renderer instead of being ignored in favor of the hardware maximum, texture downsizing uses typed/range-checked cvars without a duplicate case-only alias, obsolete no-op image settings no longer pollute configs, every applied value is verified, and an unexpected normalization failure rolls the entire preset back instead of leaving a partial profile active. -- [x] Shadow-map correctness and cost rework (phase 0 of the 2026-07-09 shadow plan): the projected caster path now applies a real slope-scale depth offset in the caster shader (the previous `glPolygonOffset` state was silently inert against shader-written depth, which is why no bias cvar combination could resolve acne vs. detachment), point-light casters no longer leave fragment depth undefined on the default path and store slope-biased radial depth in both variants, hardware depth-compare sampling with linearly filtered (hardware PCF) taps is now the default for projected and point paths with PCSS-lite automatically selecting the raw-depth path, caster passes bind their program once per pass instead of per surface, receiver-plane bias derivatives are computed in uniform control flow, the PCSS filter-radius guard uses the correct bound, and `r_shadowMapSkipStencilShadows` (default on) stops generating and linking stencil shadow volumes for lights that will render shadow maps, with a per-light automatic fallback that restores volumes the frame after any shadow-map pass failure or whenever fallback receivers still need stencil shadows. +- [x] Shadow-map correctness and cost rework (phase 0 of the 2026-07-09 shadow plan): the projected caster path now applies a real slope-scale depth offset in the caster shader (the previous `glPolygonOffset` state was silently inert against shader-written depth, which is why no bias cvar combination could resolve acne vs. detachment), point-light casters no longer leave fragment depth undefined on the default path and store slope-biased radial depth in both variants, hardware depth-compare sampling with linearly filtered (hardware PCF) taps is now the default for projected and point paths with PCSS-lite automatically selecting the raw-depth path, caster passes bind their program once per pass instead of per surface, receiver-plane bias derivatives are computed in uniform control flow, and the PCSS filter-radius guard uses the correct bound. `r_shadowMapSkipStencilShadows` stops generating/linking proven-replaceable stencil volumes; a failed map restores sticky stencil ownership, and Vulkan preserves direct illumination unshadowed in any failure frame where neither map nor stencil ownership can be submitted instead of visibly dropping the light. - [x] SDL3 Windows startup now keeps the splash screen stable through renderer startup: openQ4 sets DPI awareness before creating the Win32 splash window and removes the splash at the render-window handoff, avoiding the brief scale/position jump when the SDL3 window initializes. - [x] Objective popups now preserve retail simple-window clipping behavior, so long stock objective titles such as "Regroup with Rhino Squad" render fully instead of losing their final characters. - [x] Failed or incompatible save restores no longer crash a second time while the game tears down a partially restored actor attachment. Cleanup now safely releases an attachment whose physics state was never installed, while preserving the normal visual and collision teardown path for fully restored entities. This fixes the save-load crash reported in GitHub issue #107. @@ -328,7 +344,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Experimental macOS SDL3 renderer shutdown and error paths are safer: the default SDL3 backend now checks for a live, current OpenGL window/context before screen changes, swap, teardown, or `glFinish`, rejects empty OpenGL extension lookups before calling SDL, uses overflow-safe selected-display viewport math, clamps malformed mouse movement, wheel, controller, and rumble values before integer conversion or event queuing, normalizes unusual app-entry argument state, guards POSIX clipboard/console allocation copies plus null fatal-error formats, falls back cleanly when Cocoa display enumeration is partial, and has validation coverage to keep those detached-context and null-pointer crash guards in place. - [x] macOS fallback support is harder to crash during bring-up: the native Cocoa/OpenGL path now records a valid CGL context, validates pixel-format/context/window setup before use, releases a created context if the final make-current step fails, checks Cocoa screen and display-mode array bounds before `objectAtIndex:` calls, fails cleanly on fullscreen setup errors, unwinds partial display captures, treats missing or invalid-display VRAM telemetry as non-fatal, guards nil event helper entry points plus mouse capture, scroll-wheel overflow, and display/gamma-table lifetime paths, verifies the OpenGL context is current before swap/pause/resume/activation work, avoids stack allocation during extension lookup, returns clipboard text through the engine allocator, owns localized string fallbacks, displays fatal Cocoa alerts through a safe format wrapper, keeps obsolete Carbon/Xcode-era macOS sources out of the Meson build manifest, rejects raw C string builders from live macOS backend sources, and has validation coverage for the old undefined-return, transient-pointer, allocator-mismatch, allocation, overflow, stale-context, and null-pointer startup/shutdown hazards. - [x] Experimental macOS POSIX support paths now reject more bad runtime state before it can crash: thread and trigger-event helpers guard invalid indexes in release builds, pthread setup avoids uninitialized attribute use, requires error-checking mutex attributes before enabling critical sections, and tracks mutex/condition initialization before later lock/wait/signal calls, thread creation validates null function/tracking arguments and negative tracking counts before registering handles, terminal and desktop-console input cursors are clamped before pointer arithmetic, console scroll-wheel deltas clamp and saturate before mutating scroll state, console command and input events check allocation and oversized input before copying, null debug/print format strings are ignored or normalized, signal-number reporting avoids signed-overflow edge cases, and process handoff survives an unexpected null environment pointer from the macOS runtime. -- [x] macOS local handoff paths are tighter: URL opening now allows only normal web links plus existing local files under openQ4 runtime roots, process handoff requires an absolute executable file path instead of searching `PATH`, strips dynamic-loader injection variables such as `DYLD_*`/`LD_PRELOAD` before launching the child process, and native fatal-error dialogs use the modern `NSAlert` API. +- [x] macOS local handoff paths are tighter: URL opening allows only bounded HTTP or HTTPS web links with a syntactically valid host and rejects local-file URLs, process handoff requires an absolute executable file path instead of searching `PATH`, strips dynamic-loader injection variables such as `DYLD_*`/`LD_PRELOAD` before launching the child process, and native fatal-error dialogs use the modern `NSAlert` API. - [x] macOS static policy validation now guards the remaining platform-risk boundaries: validation rejects PATH-based process spawning, shell process helpers, broad AppKit URL opening, deprecated `NSRunAlertPanel`, Carbon leaking into the SDL3 path, unconditional Apple OpenAL linkage, and release workflows missing Developer ID/notarization/stapling while documenting the native fallback's legacy `NSOpenGL`/Carbon exceptions. - [x] Experimental macOS hardware signoff now has a structured Apple-host workflow action: `Invoke-openQ4MacOSWorkflow.ps1 -Action Signoff -MacOSGraphicsBridge both` builds the OpenGL and Metal bridge variants in separate Meson build directories, runs the real-asset smoke profile and macOS renderer matrix for each, installs the Desktop launcher, records the selected OpenAL provider, captures host/display/audio/USB/Bluetooth inventory, writes bridge-specific `macos-runtime-signoff.md` reports with the remaining manual input, audio, display-mode, and in-game renderer checks, and copies the matching result directories back to `.tmp/macos-vm/results/openq4-macos-results-.tar.gz` for review on the host. - [x] Collected experimental macOS hardware signoff archives now validate on the host immediately after copy-back: `tools/macos/validate_signoff_archive.py` rejects unsafe tar entries, requires both OpenGL and Metal bridge reports, checks workflow logs plus renderer smoke/matrix output, `-Action CollectResults -MacOSRunId ` can re-collect completed reports without rebuilding, and `-RequireCompletedSignoffChecklist` can fail final evidence if any manual hardware checklist item remains unchecked. @@ -358,7 +374,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] macOS guest signoff runs fail earlier on malformed workflow inputs: action, graphics bridge, OpenAL provider, build type, and platform backend tokens are validated before result directories or Meson setup arguments are created, keeping bad environment values from producing misleading signoff folders or logs. - [x] macOS host workflow cleanup is more reliable: the SSH runner now removes its per-run `/tmp/openq4-macos-` transfer directory from the Apple host in a `finally` block, while refusing to remove any path that does not match the expected temp-root pattern. - [x] macOS host/guest workflow paths and limits are more robust: documented `~/openq4-work` defaults now expand to the Apple user's home directory inside remote transfer, asset, build, and result-collection scripts; host parameters reject invalid ports, build types, empty build directories, and non-positive smoke limits; guest Meson setup refuses source/runtime/tool directories as build outputs; and signoff archives reject oversized non-text payloads or malformed action tokens. -- [x] macOS runtime handoff paths are more defensive: `Sys_OpenURL` now rejects overlong or control-character URLs before logging them, requires HTTP(S) URLs to include a host, keeps file URLs constrained to clean local runtime files, and `Sys_StartProcess` fails clearly if an executable bit cannot be applied before `execve`/`posix_spawn`; the macOS bootstrap also checks for `xcrun`, `plutil`, `lipo`, `otool`, and `codesign` before build/signoff work starts. +- [x] macOS runtime handoff paths are more defensive: `Sys_OpenURL` rejects overlong, control-character, non-HTTP(S), local-file, and malformed-host URLs before handoff, and `Sys_StartProcess` fails clearly if an executable bit cannot be applied before `execve`/`posix_spawn`; the macOS bootstrap also checks for `xcrun`, `plutil`, `lipo`, `otool`, and `codesign` before build/signoff work starts. - [x] Experimental macOS package metadata validation is stricter before artifacts are published: app bundle validation rejects empty icon/version/plist payloads and non-dictionary `Info.plist` roots, bundle creation fails immediately when no staged icon is available, localized `InfoPlist.strings` files are parsed for malformed or duplicate keys instead of substring-matched, and archive metadata members are capped so malformed packages cannot hide oversized app metadata. - [x] Sound-reactive world lights now match stock Quake 4 behavior more closely: hum-driven map lights such as Air Defense 1's corridor lighting use the authored sound-shader flicker envelope instead of dropping to black. - [x] Single-player enemy-hit feedback now matches retail Quake 4 again: the stock cursor GUI's per-component color aliases resolve correctly, so damaging enemies flashes the crosshair red without requiring custom HUD content or a new setting. @@ -400,7 +416,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] macOS app base-path discovery no longer depends only on Cocoa bundle metadata: when the executable path is inside `*.app/Contents/MacOS`, the engine derives the bundle's embedded resource/module roots directly, while complete older packages can still use their extracted adjacent root. - [x] macOS first-launch save/config handling is more robust: the default `~/Library/Application Support/openQ4` save root is normalized, created recursively with private permissions when missing, verified as writable/searchable before use, and falls back through the current working directory with clear diagnostics instead of silently selecting an unusable support-log path. - [x] Native macOS OpenGL context deactivation is safer during renderer skip/restart diagnostics: deactivation now tolerates missing contexts and only clears openQ4's own Cocoa context, avoiding accidental state disruption when Cocoa has no active game context. -- [x] macOS URL and process handoff diagnostics are safer for support logs: rejected URLs are no longer echoed before UTF-8 parsing, Foundation parsing, and the runtime-root/scheme allowlist pass, failed AppKit URL handoffs use a generic diagnostic, and malformed process command lines are rejected without copying the rejected command into logs. +- [x] macOS URL and process handoff diagnostics are safer for support logs: rejected URLs are no longer echoed before the shared HTTP(S)-only policy, UTF-8 parsing, and Foundation validation pass, failed AppKit URL handoffs use a generic diagnostic, and malformed process command lines are rejected without copying the rejected command into logs. - [x] Experimental macOS support is covered earlier in CI: pull-request and push validation now build/install both macOS ARM64 SDL3 OpenGL and Metal bridge variants, staged macOS payloads verify their icon and splash assets, and release packaging validates the generated `.app` executable and `Info.plist` before archiving. - [x] Experimental macOS packages now advertise the same macOS 11.0 minimum version that Meson builds target, avoiding an accidental macOS 12 metadata floor for users on supported Big Sur systems. - [x] macOS app launch diagnostics are clearer: the generated `.app` now embeds the packaged client binary directly, release CI checks that embedded executable against the adjacent package client plus key plist values, and executable-path discovery handles long `_NSGetExecutablePath` buffers without silently deriving paths from truncated strings. @@ -463,7 +479,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Shadow-map contact now stays tighter on level-start lighting and grazing surfaces: receiver slope bias is bounded, projected and point defaults use lower bias, caster polygon offset is less aggressive, and the modern clustered path follows the same receiver-plane-bias policy as ARB2. - [x] Level loads now start real map work sooner and are easier to diagnose: the loading screen no longer waits a full extra second before blocking initialization, repeated missing collision-cache probes and duplicate map media precache requests are skipped, cold-cache image generation avoids redundant source timestamp probes when no generated file exists yet, runtime image loads write generated binary-image cache files only for sources that actually need CPU decode/compression (anything served straight from a precompressed DDS bypasses the generated file), so the cache stays small on retail data while still sparing every later load the recompression cost — `image_writeGeneratedImages` defaults on and can be turned off to trade load time for disk, loading-screen presents temporarily bypass VSync so `r_swapInterval` cannot throttle blocking map loads, missing image fallbacks now correctly mark themselves defaulted so optional generated light-grid atlases stop retrying every frame, generated binary-image cache writes and optional light-grid misses no longer spam normal logs, and load logs now break startup time into render-world, game, collision, entity-spawn, renderer media, sound, decl, BSE, UI, and settle phases. - [x] Level transitions now release collision geometry with balanced cache ownership: gameplay clip models acquire and release render-model collision references, renderer extraction and trace export stay reference-neutral, SP and MP use the same `.cm`-only precache path, and precaching avoids duplicate canonicalization/cache lookup passes. A clean Air Defense 2 load and an Air Defense 1-to-2 transition both settled at 7,714 KB of live collision geometry, confirming that the earlier map was not retained. -- [x] Animation-heavy level loads now build validated, endian-stable generated MD5 animation caches under `fs_savepath`: cache entries are tied to the source path, size, timestamp, and containing PK4 checksum, corrupt or stale entries fail safely back to text parsing, SP and MP share the same cache format, and animation memory accounting now includes base-frame storage. On the Windows x64 Air Defense 1 profile, 820 entries occupied 26.9 MiB and reduced the model-definition phase from 2.7–2.8 seconds to 0.98–0.99 seconds after the one-time cache build. +- [x] Animation-heavy level loads now build validated, endian-stable generated MD5 animation caches under `fs_savepath`: SP and MP share portable version 3, cache identity follows the exact compiled-or-text source selected by the parser, loose sources add a content CRC, and a bounded whole-record length/CRC check runs before private all-or-nothing field parsing. Corrupt or stale entries fail safely back to the selected source, atomic writes cannot publish a partial record, and animation memory accounting includes base-frame storage. An earlier Windows x64 Air Defense 1 predecessor-format profile showed the feature's potential (820 entries, 26.9 MiB, model-definition phase reduced from 2.7–2.8 seconds to 0.98–0.99 seconds after the one-time build); current v3 release measurements remain part of the pending Milestone B promotion record. - [x] Starting a campaign map partway in no-save/dev-start flows now treats the authored loadout as already owned equipment: startup armor, ammo, inventory items, and weapons no longer flash pickup notifications, and the selected weapon is settled into its ready state instead of playing a pickup-style raise after the map appears. - [x] Steam Deck setup is easier to diagnose and friendlier on battery: Steam asset discovery now accepts explicit Quake 4, Steam root, Steam library, Steam compatibility, and XDG override paths with startup logging, while the Deck profile can apply a display-refresh-aware default frame cap and low-battery rumble cap without rewriting user-tuned settings. - [x] Steam Deck support now has a dedicated `listControllers` diagnostics command plus a shipping QA checklist, making it easier to capture SDL controller, gyro, touchpad, touchscreen, battery, Steam Input pass-through, suspend/resume, display, asset-discovery, and performance evidence before release. @@ -701,11 +717,23 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Modern clustered lighting now uses compact CSR-style cluster lists instead of fixed per-cluster slots: each per-view grid uploads `(offset,count)` headers plus a flat packed light-index stream, dense clusters stay lossless within the shared budget on GL 3.3, and GL 4.3+ can rebuild the same headers/index stream with compute count/prefix/fill passes while `gfxInfo`, renderer metrics, and `rendererClusterGridSelfTest` report CSR and compute-binning readiness. - [x] GL 4.3+ modern material draws can now collapse repeated classic texture binds through a conservative per-frame sampler table: the material resource table assigns table indices for loaded textures and fallback images, shaders keep the GL 3.3 classic sampler path but can select table entries with per-draw texture indices, and `gfxInfo`, renderer metrics, and low-overhead self-tests report table size, draw usage, uniform updates, and fallback pressure while bindless remains opt-in only. - [x] Modern renderer pass handoff now avoids full GL teardown between modern passes: depth, Hi-Z, G-buffer, deferred, forward+, GUI, and diagnostic submits use a soft intra-modern restore that keeps the state cache warm, then perform one full restore before the ARB2 legacy handoff, with `gfxInfo`, renderer metrics, and visible-path self-tests reporting soft/full restore coverage. +- [x] The idTech 5 modernization path now has its first complete shared classic-frame domain: eligible fixed-function root 2D GUI views preserve repeated ordered material stages, evaluate conditions/colors/matrices/state once per draw, resolve generation-checked image resources, and run from the same sealed records on OpenGL and Vulkan. `r_rendererSharedGui` remains default-off, and any unsupported view, material, resource, or backend condition executes the untouched whole-view classic path with explicit coverage and fallback diagnostics. +- [x] The idTech 5 modernization path now has a second complete shared classic-frame implementation for eligible ambient-only 3D world views: packet-derived fixed-function surface passes preserve repeated evaluated state, opaque/perforated surfaces require matching established depth work, and both backends preflight the whole pre-fog/post-fog plan before committing. `r_rendererSharedWorldAmbient` remains default-off; any interaction, shadow, fog/blend, special-surface, material, resource, or backend blocker leaves the complete ambient view on its untouched classic walker. Local stock `maps/tools/mv2` GL/Vulkan option-off/on engine captures match exactly with one owned pre-fog draw, and a stock deform override proves named zero-draw whole-view fallback on both backends; clean-package/platform promotion evidence remains open. +- [x] The idTech 5 modernization path established its third complete shared classic-frame implementation with eligible unshadowed fixed-classic interaction views: scene packets identify the exact source light and local/global/translucent receiver, one bounded transaction seals the complete light-stage x bump/diffuse/specular stream, and both backends preflight every program/resource/geometry/transient requirement before ownership. `r_rendererSharedWorldInteraction` remains default-off; custom, deform/skinned, special-view, resource, overflow, or backend blockers perform zero shared draws and leave the complete interaction view on its untouched classic walker. Native/static coverage and controlled stock GL/Vulkan unshadowed qualification pass with nonempty ownership and exact option-off/on engine images; clean-package/platform promotion evidence remains open. +- [x] The third complete shared classic-frame domain now includes shadow-coupled interaction views. The same default-off interaction transaction seals classic stencil volumes, projected single-map and CSM/parallel shadows, point cubes, mixed mapped/stencil lights, same-light hybrid supplements, dynamic mapped casters, and perforated alpha stages for both OpenGL and Vulkan. Each backend retains the complete cache/update/scratch and receiver plan before visible ownership, reconciles physical volume/map/hybrid work exactly, and commits no shared main-target draw on an admission/resource/backend failure. Translucent moment-map casters remain an intentional shadow-specific whole-view fallback. Native/static contracts pass, and the controlled five-case engine-screenshot profile passed 5/5 on both GL and Vulkan with same-backend classic parity, material shadow deltas, and atomic fallback. Stock fixed-camera/reference, clean final-package, and platform/driver qualification remain open. +- [x] The idTech 5 modernization path now has a fourth complete shared classic-frame implementation for eligible fog/blend phases. `r_rendererSharedWorldFogBlend` remains default-off; one bounded transaction seals every fog/blend light, GLOBAL-to-LOCAL receiver, ordered active/inactive blend stage, fog receiver and frustum cap, evaluated texgen/compositing state, image resource, no-op, and coverage identity for both OpenGL and Vulkan. Each backend preflights the whole phase before the first shared main-target draw, and any unsupported source, view, geometry, material, resource, target, capacity, or backend condition keeps the complete phase on its untouched classic walker. Native/static validation passes, and the controlled stock-declaration GL/Vulkan profile now has exact shared/classic engine-image parity, nonempty reconciled ownership, material fog/blend deltas, and named zero-commit atomic fallback. Authored-stock fog and clean-package/platform promotion remain open. +- [x] The Milestone D material-deform dependency is implemented behind default-off `r_rendererSharedDeform`: the authoritative CPU material-deform result now carries sealed source/result geometry, evaluated input, frame/cache provenance, consumer role, explicit completed/intentional-empty/not-applicable/skipped/failed/unsupported outcome, and a stable semantic hash. Root GUI, world ambient, mapped interaction casters, and fog/interaction receivers validate the correct classic role before either backend owns their complete transaction. Particle/particle2 and any skipped, failed, stale, or mismatched record remain named atomic fallback; OpenGL and Vulkan consume only the ordinary final vertex/index stream. +- [x] The special-subview Milestone D transaction is implemented behind default-off `r_rendererSharedSubview`: direct mirrors seal camera/clip/scissor semantics, while eligible remote-camera, mirror, reflection, refraction, and x-ray child scenes seal their parent surface and exact 2D/cubemap color/depth capture. Nested chains retain exact parent/root/depth and depth-first order. Eligible authored-post/video/current-render/depth work inside a child must complete before root publication; either domain's rejection rolls the whole tree back, and later members bypass duplicate fallback reporting. Native/static validation and the narrow nested cinematic-plus-`_currentRender` mirror acceptance pass on Windows GL/Vulkan; direct, multi-level, other capture kind/aspect/face, `_currentDepth`, clean-package, retained-review, and platform/driver qualification remain open. +- [x] In-world GUI ownership now has its own default-off `r_rendererSharedInWorldGui` corridor: only quads emitted by `R_RenderGuiSurf` are provenance-tagged, packetized as a world-category GUI subset, and evaluated through the existing ordered material records with the classic 3D depth contract. OpenGL and Vulkan preflight every tagged surface before submitting at the pre-ambient boundary; any source, packet, material, resource, target, capacity, or backend rejection keeps the entire tagged subset on the untouched classic walker. Root 2D GUI remains independently controlled by `r_rendererSharedGui`; runtime screenshot, clean-package, and platform qualification remain open. +- [x] A default-off shared cinematic/authored-post transaction now seals an eligible complete root `videoMap`/`soundMap` view or ordered post-process tail behind `r_rendererSharedCinematicPost`. It reconciles source/packet order, cinematic clock, and `_currentRender`/`_currentDepth` use before OpenGL and Vulkan dispatch their mature dynamic-stage executors. Nested tails bind to the exact special-view root/depth, remain unpublished until that tree completes, and propagate fallback atomically in either direction. Native/static validation and the narrow nested cinematic-plus-`_currentRender` acceptance pass on Windows GL/Vulkan; root views, ordinary-root post, `_currentDepth`, clean-package, retained-review, and platform/driver qualification remain open. +- [x] Final controlled Milestone D nested acceptance is retained at `.tmp/renderer-milestone-d/acceptance-20260823-r6-all/renderer_milestone_d_acceptance_report.json` (SHA-256 `b49031c48267fbfa86c295082707525a390d6090c24f600d00644237a67b252a`), bound to `.tmp/renderer-milestone-d/qualification-20260823-r6/fixture_manifest.json` (SHA-256 `24a2a4926f4b1263e311ad762bf40d391b33de225732d12154ebd7625705282d`) and `.tmp/stock-runtime/milestone-d-qualification-20260823-r6` (manifest SHA-256 `dbece44597ec5e0686eed215f6ae99c5d6ba4cdd959c1efa7c4acaf1bc2ca673`). The unchanged 3,884-file / 7,281,560,043-byte stock dependency seal is `13abad18f70eb8b4bf6ea0e9697b317718bb065ee76f86070787005b045dda7a`; the unchanged eight-file / 25,330,583-byte fixture evidence inventory seal is `0572fb8d9c132f7e97060345464f97750e59e69819871fce90eed13d1348d8ae`. The separate passing R6 foundation JSON has SHA-256 `0659f1f2ee5108a422cd6c41bdc78809731f25ad7eda4b5ee84328a9163ce7ac`, with Markdown SHA-256 `a619169ee97f2d6e8c7dfab991adbab5b5b282fd2e1197e2d59eb3609c87cddf`. All 12 sequential GL/Vulkan cases passed using the fixed input-disabled capture script, actual windowed mode `-1` at 1280x720, and 1280x720 engine TGAs. All four exact comparisons per API had RMS `0`, maximum delta `0`, and zero differing channels; active normal cinematic owned/fallback/mismatch/duplicate was `1/0/0/0` and subview owned/fallback was `1/0`, skip was `0/1/0/0` and `0/1`, inactive-backend counters were zero, and no runtime, fixture, or stock verification failed. Normal versus skip changed 46,359 channels at RMS `2.0073` / maximum `72` on GL and 3,329 channels at RMS `0.2506` / maximum `18` on Vulkan. This closes only the controlled nested cinematic-plus-`_currentRender` mirror gate. +- [x] A default-off shared render-demo/Raven special-frame transaction now seals an eligible complete active-session render-demo view or exact `RC_DRAW_SPECIAL_EFFECTS` blur/AL mask behind `r_rendererSharedSpecialFrame`. Session/render-world provenance avoids confusing portal-sky negative view IDs with demo playback; OpenGL and Vulkan retain their mature full-view, resource, resolve, blur, and projected-light executors, reporting ownership only after all sealed coverage completes. Incomplete source, session, effect, resolve, or backend state retains the complete classic item; native/static validation passes while engine-screenshot, clean-package, and platform qualification remain open. +- [x] Milestone D scoped implementation is complete: every named classic-frame corridor has a default-off sealed GL/Vulkan owner, explicit classic rollback, diagnostics, static/native evidence, and a documented release-promotion boundary. This does not promote the aggregate modern-visible renderer or close clean-package, retained-review, authored-stock, target-platform, or driver qualification. - [x] Modern renderer transient uploads now use the shared upload-manager stream for frame constants and GL 4.3 GPU-driven scene, indirect, draw-record, bucket, and validation buffers when available; fixed-buffer fallbacks remain intact, upload ring rotation is configurable through `r_rendererUploadFrameBuffers`, opt-in GPU validation readbacks are deferred behind nonblocking sync polling, and launch-time validation now waits for real scene draw packets instead of doing GPU-driven work on loading/GUI frames. - [x] Forced modern GL tiers now keep stock-world lighting on the visible ARB2 bridge by preferring versioned compatibility contexts before core-profile diagnostics; ARB2 initialization also rejects core-profile contexts explicitly instead of advertising a bridge that can only produce a black frame. - [x] Modern renderer auto-promotion now requires explicit Phase 8 validation evidence, not just a boolean sign-off: `r_rendererPromotionEvidence` must carry the complete zero-warning visual/gameplay/RenderDoc/performance/presentation/rollback/debug-off token before `r_rendererModernAutoPromote 1` can activate, and `gfxInfo`, `rendererDefaultPromotionSelfTest`, `rendererDefaultSafetySelfTest`, and the validation matrix all report the same evidence gate. - [x] Filter-style decal mips now fold coverage alpha into the neutral multiply color per generated level, so stock terrain stains such as `game/airdefense1` no longer reveal their decal bounds when the flashlight crosses them. -- [x] Decal rendering now applies after all direct and baked light-grid lighting, so bullet marks, scorch marks, and terrain stains modulate the fully lit scene consistently; `r_skipDecals` also suppresses ordinary decal-sort surfaces and the modern forward+ decal overlay, matching projected decal behavior. +- [x] Decal surfaces retain their established sorted pre-fog material position after direct interactions and before baked light-grid and fog work. When a sealed clustered-decal transaction owns a surface, its modern forward+ draw substitutes one-for-one for that exact classic draw occurrence; transaction rejection leaves the classic draw unchanged, and `r_skipDecals` suppresses both ordinary and clustered-decal submissions. - [x] Runtime logs now open under the selected game directory as soon as filesystem startup can write files, so `+set fs_game baseoq4 +set logFileName logs/openq4.log` writes to `fs_savepath/baseoq4/logs/` instead of being captured early under `q4base/logs/`. - [x] Retail BSE trail-material parity is restored for shared bullet-impact effects: particle templates now retain the authored/default trail-material name, motion trails resolve that material at finish time instead of relying on an early static pointer, and the stock machinegun/shotgun impact spark trails once again use Quake 4's `gfx/effects/particles_shapes/motionblur` trail instead of falling back to the engine default texture. - [x] Ragdoll startup now follows Quake 4's retail articulated-figure path again: stock corpses no longer pick up openQ4-only AF handoff velocity/depenetration heuristics or extra per-body angular-velocity injection during pose transfer, and the AF contact solver is back on retail's tighter contact budget and per-pair planar-contact cap for SP/MP ragdolls. @@ -726,7 +754,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Light-grid area transitions now blend across visible, unblocked portal neighbors near doorway/window boundaries, using `r_lightGridPortalBlend` to smooth indirect-light seams without changing baked asset formats. - [x] Light-grid atlas residency now keeps visible areas and unblocked portal neighbors hot while loose fallback atlases can still use an age-based window controlled by `r_lightGridResidencyFrames`, reducing reload churn and traversal hitches when moving through connected areas. - [x] Light-grid indirect rendering now uses a material-compiled representative diffuse stage instead of redrawing every diffuse stage, reducing extra geometry submissions for multi-diffuse materials while retaining a fallback when the preferred stage is conditionally disabled. -- [x] Startup asset validation now ignores retail Quake 4 game-binary PK4 archives (`game000.pk4` through `game300.pk4`, plus `gamex*.pk4` variants), so openQ4 only requires and verifies the stock media PK4s it actually uses. +- [x] Startup asset validation ignores retail Quake 4 game-binary PK4 archives (`game000.pk4` through `game300.pk4`, plus `gamex*.pk4` variants), so openQ4 only requires and verifies the stock media PK4s it actually uses. Pure multiplayer retains the official `game300.pk4` checksum solely as the protocol 2.41 compatibility token for trusted local openQ4 modules; the archive itself remains optional and is never loaded as game code. - [x] Retail decl loading parity improved: Quake 4 decl folders now honor retail recursion/unique-file behavior, no-cache decl lookups reach entityDef inheritance/media caching, legacy Doom 3 PDA/email/video/audio console commands are no longer exposed as supported decls, and playback/table/lip-sync/material-type decl parsing plus playback runtime sampling/record hooks now handle more shipped Raven asset data safely. - [x] Packed decl-manager parity advanced: the retail single-decl-file API is enabled again across engine/game headers, packed `.decls` sections can be read and written through controlled cvars/commands, stored decl indices are preserved during load, validation/allocation/tool hooks are available, packed stub decls expand lazily from their original source text, PDA/email/video/audio decl families are included in the framework packed section, and the startup/session/network asset-log wiring now selects and refreshes per-map packed decl files when the retail cvars are enabled. - [x] Packed decl asset-log wiring now follows the retail `assetlogs/` naming contract, including entity-filter suffixes, while the decl manager preserves the real `maps/...` path when selecting per-map `.decls` files. @@ -852,8 +880,10 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Multiplayer server-side hitscan lag compensation added with configurable rewind controls (`net_mpLagCompensation`, `net_mpLagCompMaxMS`, `net_mpLagCompBiasMS`) and server diagnostics (`net_mpLagCompDebug`). - [x] Multiplayer non-local prediction mode is now runtime-selectable through `net_mpPredictMode` (`0` legacy limited behavior, `1` enhanced per-frame prediction). - [x] Scope/zoom handling documentation updated to reflect multiplayer zoom stability and scope yaw alignment behavior. -- [x] High-refresh first-person presentation now has a deliberately narrow SP/MP interpolation slice: transient previous/current camera origin, axis, and FOV plus the full first-person viewmodel pose are blended only during `Draw()`, without rerunning gameplay or adding fields to save data. The MP path snaps during active prediction-error smoothing, and the non-extrapolating blend accepts at most one `60 Hz` tic (about `16.7 ms`) of visual latency. -- [x] Current issue #99 validation records a passing SP/MP static parity contract and passing SP/MP GameLib builds. Windowed active SP gameplay measured `86.2 Hz` presentation with `60 Hz` simulation; an MP listen server loaded the module and map and produced an engine screenshot, but the auxiliary loopback-client harness did not complete its requested client capture. Full remote-client, human-feel, Apple-platform, and dedicated high-refresh hardware qualification are not claimed. +- [x] High-refresh presentation blends transient previous/current camera origin, axis, and FOV plus the full first-person viewmodel pose only during `Draw()`, without rerunning gameplay or adding fields to save data. Eligible moving-world entities, projectiles, lights, bound client effects, and actively animating skeletons share that presentation clock. The MP path snaps during active prediction-error smoothing, and the non-extrapolating blend accepts at most one `60 Hz` tic (about `16.7 ms`) of visual latency. +- [x] Skeletal presentation evaluates the same rounded previous-to-current time as the root and camera into isolated animator-owned scratch joints. Renderer callbacks are suppressed only on the copied draw entity, bounds and joint-bound attachments follow the scratch pose, and authoritative animator caches, AF/angular joint state, demos, saves, networking, and gameplay queries remain unchanged. +- [x] Remote-client presentation no longer advances a projectile twice during prediction replay, overwrites viewweapon interpolation endpoints on a replayed frame, or silently drops the impact/fizzle visual when the first received state is already terminal. Multiplayer actors remain outside the generic pass: remote bodies stay aligned with server rewind, and the local body/world-weapon stencil shadows remain an explicitly deferred simulation-clock visual. +- [x] Current issue #99 validation includes the expanded SP/MP presentation contract, standalone and integrated SP/MP builds, the full push profile, a scripted campaign transition ending on `game/tram1` at `189.4 Hz`, an animated-character/stencil SP pass at `202.0 Hz`, and a clean MP host/client pass at `148.0/116.1 Hz`; simulation remained `60.0 Hz` and every staged role wrote an engine-side screenshot. Human-motion, Apple, and dedicated high-refresh hardware qualification remain open. - [x] Ragdoll activation quality improved without changing the fixed 60 Hz simulation cadence: startup now keeps owner/world motion, handles initial penetrations more cleanly, and preserves slightly richer contact support for grounded corpses. - [x] Rigid-body physics timing-safe quality pass applied without changing the fixed simulation cadence: angular velocity handoff now respects world inertia, water is handled as drag instead of a one-time collision-state hack, rigid-body contacts keep richer deduplicated support points, and impacts now preserve time-of-impact momentum while consuming a small bounded amount of leftover fixed-step time. - [x] Script compiler x64 pointer-temp parity fix ported from OpenD3: right-associative indirect-expression retagging now guards 4-byte object-ref temp vs 8-byte pointer temp storage mismatch by allocating pointer-sized result defs when needed, preventing trigger/door script chain corruption. @@ -898,6 +928,13 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Accessibility: text can be given a solid black backing so it stays readable over the world and over busy panel artwork. `gui_textBackground` sets how opaque the backing is (0 is off, 1 is fully opaque; around 0.75 keeps the artwork visible while making text easy to read) and `gui_textBackgroundPadding` controls how far it extends past the text. It covers menu, HUD and in-game GUI text, applies immediately with no restart, and works with either the scalable or the original bitmap fonts. The backing is drawn once per line, so it never shows through the gaps between characters, and every line of a given font gets the same height whatever characters it contains. - [x] A texture quality control in the style of Quake 3's `r_picmip` is available for players who need to trade sharpness for memory and bandwidth. `image_picmip` drops whole mip levels, so every step halves a texture no matter how large it started, but unlike the original it only reduces the **diffuse layer** of a material — normal maps, specular maps, lighting, skies, decals, fonts, and the whole HUD and menu keep their authored resolution, so surfaces stay correctly lit and the interface stays sharp. `image_picmipFilter` restricts it to world surfaces (the default), models, or both, `image_picmipMinSize` stops small textures from turning to mush, and a material that declares `nopicmip` opts out. Textures reload as soon as one of these changes, with no `vid_restart` needed, and the generated texture cache is keyed by the active reduction so switching settings never leaves a stale size behind. +- [x] Renderer performance evidence now binds exact stock-map, OpenGL/Vulkan backend, benchmark profile, and bordered-window 1280x720 launch state to a versioned CPU/GPU percentile contract. Gameplay and retail-baseline reports retain the selected thresholds, measured samples, runtime/artifact hashes, and source provenance, then replay them fail-closed; unavailable GPU timing, archived display drift, fullscreen/noncanonical budget runs, or changed evidence cannot pass. The initial 20/28 ms rows are target ceilings rather than universal measured claims. +- [x] Milestone A's current-build integration gate has exercised identical jobs-on/off storage output, clean OpenGL and Vulkan repeated-map job shutdown, five deterministic dedicated exits, delayed whole-frame timing on both backends, a replay-valid schema-10 four-role retail baseline with local human image review, and replay-verified 8/8 OpenGL plus 8/8 Vulkan required profiles from the final immutable development runtime. This validates the development build, not a release package or universal performance level. +- [x] OpenGL depth-bounds submission now orders and clamps both values to the legal range before calling the driver, eliminating the two `GL_INVALID_VALUE` records that previously made `game/medlabs` the lone required-profile failure; its debug-context rerun is error-free. +- [x] Shadow-map caster coverage and contact quality are corrected across the classic OpenGL, direct Vulkan, and shared interaction paths. Automatic culling now stores the near shell for sealed geometry and renders open/uncertain stock geometry two-sided; large point-light bias is world-bounded, balanced filter footprints are tighter and cheaper, PCSS blocker search is bias-consistent, map/alpha/dynamic cache ownership fails closed, projected and point resources carry exact world/pass/storage provenance through bind time, partial mapped ownerships receive exact stencil supplements or return to full stencil, Modern OpenGL refuses resources it cannot consume completely, atlas placement follows physical limits, and nested OpenGL elapsed queries no longer generate validation errors. Final staged, windowed OpenGL `game/airdefense1` stencil/mapped qualification passed 2/2 with retained engine captures under `.tmp/shadowmap-review/final-ab-runtime-6/`; stencil measured 96.8 Hz at p95 14 ms and mapped measured 74.8 Hz at p95 16 ms, while the mapped capture keeps the start-area buggy, crates, arches, and rails grounded without the former light-corner gap. A matching direct-Vulkan mapped smoke passed at 83.7 Hz and p95 15 ms with a clean capture and no Vulkan validation/call failure under `.tmp/shadowmap-review/final-vk-runtime-5/`. The low-frequency telemetry rerun under `.tmp/shadowmap-review/final-telemetry-6/` recorded 22 lights with six supported point lights, four local and four global mapped ownerships, 212 admitted casters, two successful translucent receiver chains, three complete fail-closed stencil fallbacks, 48 rendered point faces, zero unshadowed/render/mask/receiver failures, and 43 completed plus 21 still-pending asynchronous elapsed-time queries with zero synchronous waits, slot exhaustion, drops, or query failures. +- [x] Known stock startup warnings are resolved without replacement content: the three brown-fluid images and large water splash omitted from the retail PK4s use fail-after-primary shipped-media fallbacks, generated TrueType atlas materials bind their already-uploaded intrinsic images on first parse and survive renderer restart, wide loading backgrounds retain atomic publication under long Windows save paths through a root-relative 128-bit CSPRNG staging identity, generated image caches fall back from the legacy qpath to a versioned SHA-256/128 compact identity without bypassing VFS or pure-server policy, and rigid-body states that are immediately clamped or forced to rest remain developer diagnostics while unrecovered physics faults keep warning severity. Every binary-image header, mip record, and payload write now rejects short output, and the renderer evidence tools fail closed on either cache-write or expanded-loadscreen-publication warnings. The final staged, windowed OpenGL `game/airdefense1` cold run and same-savepath warm reuse run passed with engine captures and no warning, error, fatal, or asset-load-failure records under `.tmp/unrelated-warning-fixes/airdefense1-compact-cache-cold-longpath-final-2/`: the fixture held the valid expanded TGA at 240 characters, reduced its formerly failing 263-character staging path to 222, forced nine overlong legacy cache identities onto compact paths, left no staging files behind, and preserved byte hashes plus modification times for all eight persistent stock/image-program compact caches on warm load. The generated loadscreen derivative alone refreshed as expected when its source TGA was atomically regenerated. +- [ ] Finish Milestone A release qualification by repeating and retaining the full job, renderer-budget, retail-baseline, and human-review evidence from clean committed source and a freshly staged final package, then recording the required release platform/driver coverage. + ## macOS Evidence Gate Complete this section before release notes claim macOS support beyond the current experimental Apple Silicon/arm64 status. @@ -930,7 +967,17 @@ Complete this section before release notes claim macOS support beyond the curren ## Carry Forward +- [ ] Complete Milestone F release promotion from clean committed source and a freshly staged final package, then retain target platform/driver and release-review evidence. The local Windows current-source implementation exit already has passing PBR/probe/decal contracts and execution plus the 2026-08-24 independent froxel-volumetric, SSR, and SSGI native/static/shader-pin gate, individual/combined windowed OpenGL gameplay, combined validation-clean Vulkan gameplay, visible effect delta, and exact master-off packet rollback. Promotion still needs final-package recapture and any retained RenderDoc, broader authored probe/decal scene, advanced screen-space visual review, and target platform/driver evidence selected for release. All leaves remain default-off, and `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`. +- [ ] Repeat and retain the `r_rendererSharedGui 0/1` root-menu/loading/HUD release-acceptance set from clean committed source and a freshly staged final package on OpenGL and Vulkan, with exact engine-TGA parity, complete ownership/fallback diagnostics, human review, and target-platform/driver coverage. +- [ ] Run and retain the `r_rendererSharedInWorldGui 0/1` fixed-camera tagged-surface release-acceptance set from clean committed source and a freshly staged final package on OpenGL and Vulkan. Require exact same-settings parity, nonzero tagged-subset ownership, named zero-commit rollback, and no duplicate classic/shared draws. +- [ ] Repeat the locally passing `r_rendererSharedWorldAmbient 0/1` eligible-view and deform-fallback evidence from clean committed source and a freshly staged final package, then retain target-platform/driver coverage before promotion. The development-worktree GL/Vulkan engine captures, exact hashes, named rollback, and safe-matrix results are recorded in `docs/dev/classic-world-ambient-domain-modernization.md`. +- [ ] Run and retain the complete `r_rendererSharedWorldInteraction 0/1` release-acceptance set from clean committed source and a freshly staged final package on OpenGL and Vulkan: controlled unshadowed, stencil, projected+point mapped, mixed map/stencil, and constrained map-admission fallback; stock projected, point, CSM/parallel, dynamic, perforated, same-light hybrid, and translucent-moment fallback. Require exact per-map/backend reconciliation, explicit dynamic/alpha feature evidence, a retained final six-component camera pose, same-settings classic/shared engine-TGA parity, a material shadows-on/off engine-TGA delta, named fallbacks with zero committed ownership counters and no shared main-target draw, and retained target-platform/driver coverage before promotion. The controlled development-worktree suite already passed 5/5 on GL and 5/5 on Vulkan; every stock-profile case still needs fixed-camera and reference qualification. The controlled test light is not a multi-cascade case. The contract and local evidence are recorded in `docs/dev/classic-interaction-domain-modernization.md`. +- [ ] Repeat and retain the complete `r_rendererSharedWorldFogBlend 0/1` release-acceptance set on OpenGL and Vulkan from clean committed source and a freshly staged final package, then add fixed-camera authored-stock fog, retained human review, and target-platform/driver coverage. The controlled development-worktree stock-declaration suite already closes exact same-backend classic/shared engine-TGA parity, nonzero light/receiver/stage/fog-cap/draw/no-op/hash/backend reconciliation, material fog/blend deltas, named zero-commit complete-phase fallback, and clean Vulkan validation on both backends. The contract, hashes, deltas, and local report directories are recorded in `docs/dev/classic-fog-blend-domain-modernization.md`. +- [ ] Repeat and retain the `r_rendererSharedDeform 0/1` release-acceptance set from clean committed source and a freshly staged final package on OpenGL and Vulkan. Require exact same-backend classic/shared engine-TGA parity with nonzero completed output, an otherwise identical `r_skipDeforms 1` effect delta, named unsupported/skipped/failure rollback with zero committed shared work, clean restart/frame-reset diagnostics, human review, and target-platform/driver coverage. The controlled development-worktree suite already passes exact parity, nonzero completed ownership, material effect delta, skipped zero-commit rollback, native/static checks, and clean backend diagnostics on both backends; its hashes, deltas, and report directories are recorded in `docs/dev/classic-deform-domain-modernization.md`. +- [ ] Repeat and retain the broader `r_rendererSharedSubview 0/1` release-acceptance set from clean committed source and a freshly staged final package on OpenGL and Vulkan. R6 already closes one capture-backed color-2D mirror coupled to a cinematic-plus-`_currentRender` tail. Still require fixed direct-mirror, dynamic mirror/reflection, x-ray, remote-camera, refraction, multi-level nested-special-view parity, qualifying 2D/cubemap color/depth ownership, named malformed semantic/target/face/descendant zero-commit fallback, a coupled `_currentDepth` case, retained coordinates, human review, and platform/driver coverage. +- [ ] Repeat and retain the `r_rendererSharedCinematicPost 0/1` root cinematic and ordinary-root authored-post release set from clean committed source and a freshly staged final package on both backends, including frozen decoder time, exact engine-TGA parity, visible enabled/skip delta, complete ownership/fallback accounting, and `_currentDepth`. The R6 independent-CVar and nested cinematic-plus-`_currentRender` cases are controlled development-runtime evidence, not substitutes for these root/final-package cases. +- [ ] Run and retain the `r_rendererSharedSpecialFrame 0/1` stock render-demo plus Raven blur/AL controller release set from a fresh final package on OpenGL and Vulkan, proving exact session/effect-mask ownership, parity, named fallback, no partial effect execution, and retained human/platform review. - [ ] Complete competitive-match release qualification: run the staged warning-clean MP build and full automated contracts, then exercise the compact HUD/scoreboard and Match Control through windowed 16:9/4:3 keyboard/controller workflows for every supported live role, encoded recipient-redaction and camera-follow checks, supported placed-major-item pickup/respawn timing across repeated world pauses, supported-mode lifecycle edges, BO1/BO3/BO5 map/recovery paths, JSON interruption handling and playback of the linked generated MVD. Captain spectator invitations remain fail-closed. Verify the pure future-repeater policy separately for a public-only, no-item-timing result without treating it as a shipped live Q4TV/repeater transport; delayed broadcast and live multi-POV remain deferred. -- [ ] Complete issue #99 high-refresh qualification: obtain a successful auxiliary loopback-client capture, run human SP/MP feel and moving-platform checks, validate on Apple and dedicated high-refresh hardware, and decide release scope for generic entities, remote players, projectiles, FX/BSE, alternate cameras, and raw-input compensation. None of those broader systems are covered by the current first-person-only slice. +- [ ] Complete issue #99 high-refresh qualification: repeat human SP/MP feel, projectile/viewweapon, moving-platform, and cut-heavy camera checks on the final package; validate on Apple and dedicated high-refresh hardware; and decide the release scope for multiplayer actor/shadow presentation, bespoke FX/BSE owners, alternate cameras, and raw-input compensation. Remote multiplayer actors and the local body/world-weapon stencil shadows remain explicitly outside the current generic presentation pass. - [ ] Run macOS `Signoff -MacOSGraphicsBridge both`, complete the manual hardware checklist, then run `CollectResults -MacOSRunId -MacOSGraphicsBridge both -RequireCompletedSignoffChecklist` on real Apple hardware and attach the auto-collected, auto-validated `.tmp/macos-vm/results/openq4-macos-results-.tar.gz` evidence for input devices, audio devices, display modes, SP gameplay, MP `renderer-mp-smoke`, and in-game OpenGL/Metal renderer coverage beyond hosted CI compile/link/package checks; record the accepted archive in `docs/dev/macos-signoff-evidence.md`. - [ ] Replace temporary MSVC compatibility flag (`/Zc:strictStrings-`) with full strict-strings codebase compliance. diff --git a/docs/dev/releases/v0.12.0.md b/docs/dev/releases/v0.12.0.md index c80c71e4..3a0666c5 100644 --- a/docs/dev/releases/v0.12.0.md +++ b/docs/dev/releases/v0.12.0.md @@ -2,26 +2,64 @@ ## Highlights -- **High-refresh gameplay looks smoother without changing game logic.** Players, weapons, projectiles, lights, client effects, and moving-world attachments now sample a presentation transform between authoritative game ticks. View-model effects stay anchored to the pose actually drawn, including the lightning gun beam, while simulation, networking, collision, demos, and savegames retain their original timing. +- **High-refresh gameplay looks smoother without changing game logic.** The first-person camera and weapon, skeletal animation, projectiles, eligible moving-world entities, lights, client effects, and moving-world attachments now sample a presentation pose between authoritative game ticks. Animated cockpits, held weapons, and joint-bound effects stay aligned with the pose actually drawn, including the lightning gun beam, while simulation, networking, collision, demos, and savegames retain their original timing. Multiplayer actor bodies remain on the network/simulation clock; this includes the local body and world-weapon stencil shadows visible in first person. +- **Experimental temporal AA can keep the 3D scene steadier while resolution follows GPU load.** `r_temporalAA 1` enables native-history TAA/TAAU on OpenGL or Vulkan, and `r_rendererDynamicResolution 1` adds a delayed, non-blocking GPU-time controller. HUD and menus stay native-sized, camera cuts and captures start from clean history, unsupported moving effects are rejected conservatively, and the established SMAA path remains one setting away. +- **Repeat level-load caching is available as a guarded experiment without slowing ordinary play.** Classic source loading is again the default after testing found that cache preparation could make some stock loads much longer. Developers can opt in with `com_levelLoadModernization 1`; every reuse remains tied to exact source and runtime identity, while stale or corrupt data falls back to the installed source automatically. +- **Experimental GPU animation is available without changing gameplay geometry.** Capable OpenGL and Vulkan systems can opt into an exact four-weight MD5/MD5R deformation path with `r_gpuSkinning 1`. Collision, hits, decals, overlays, and stencil shadows retain their CPU-owned data, authored vertex colors remain intact, and any unsupported model or resource condition falls back to the complete CPU path. +- **Material authors can evaluate guarded PBR lighting and bounded authored reflections without changing retail materials.** Dual-authored `pbr { ... }` materials retain their normal Quake 4 fallback while eligible metallic/roughness data can use the experimental OpenGL G-buffer, deferred, and clustered-forward route when `r_rendererModernVisible 1` is also enabled. OpenGL authors can opt into specular probes bounded to eight cubemaps, 32 records, and the best two probes per cluster; incomplete probe data returns to the analytic environment. A separate default-off clustered-decal corridor transfers only complete sealed subsets. Vulkan remains limited to a narrow opaque packed-ORM direct-light route whose classic fallback has exactly one active bump -> diffuse -> specular interaction sequence. The local Windows current-source implementation gate passed across stock SP/MP, forced GL 3.3--4.5, Vulkan, and exact master rollback. These Milestone F paths remain experimental and default-off; final committed-package, platform/driver, and release promotion remain pending. +- **Players can independently preview bounded volumetric atmosphere, reflections, and indirect light on either renderer.** Default-off froxel volumetrics, SSR, and SSGI now share the native scene presentation path on OpenGL and Vulkan. Each has a fixed work ceiling and its own quality controls; `r_rendererModernQuality 0` rolls all experimental Milestone F lighting back at once. These are deliberately screen-space approximations, so off-screen reflections, per-light volumetric shadows, and world-space multi-bounce GI are outside this release. +- **The complex `airdefense1` opening reaches gameplay faster and its CPU-limited scene runs more smoothly.** Cinematic fast-forward no longer spends time interpolating thousands of poses that cannot be displayed, and ordinary maps without baked light grids no longer build a bake-only probe layout while loading. The guarded learned-cache experiment remains off by default; these gains apply to the classic source path. +- **Shadow-mapped props now cast complete, grounded shadows.** Open and thin stock models such as the buggy, crates, railings, and computer props around the `airdefense1` start no longer disappear from point-light shadow maps, while sealed meshes store their near surface instead of a detached far shell. Tighter balanced filtering and a world-space cap for huge point-light bias preserve contact corners; any caster that cannot enter the map is supplied by the matching stencil ownership or safely returns that ownership to full stencil. +- **Stock maps no longer report or expose known startup gaps.** The engine first honours any loose or mod-provided versions of the three omitted brown-fluid effect images and the omitted large water-splash sample, then uses shape- and family-compatible media that did ship with Quake 4. Generated TrueType atlases also bind their uploaded images on first parse, wide loading backgrounds and generated image caches remain reliable from deeply nested save locations, and rigid bodies that the stock game deliberately clamps or forces to rest remain visible as developer diagnostics without being mislabeled as unresolved warnings. - **Polish and Russian are now built in.** Text rendering works by Unicode code point, the Polish tables use the correct Central European encoding path, and generated font data is checksum-validated so translated menus and HUD text remain reproducible across packages. - **The marine hovertank has its vehicle audio back.** Single-player once again creates and updates the engine and hover-pad loops used throughout the vehicle sequence, with safe cleanup during map or engine shutdown. - **Arena Campaign scoring starts reliably.** Disabling ready-up now also clears the ready threshold, preventing deathmatch-backed Arena cards from remaining forever in a non-scoring warmup. - **Multiplayer setup is easier to control.** The server menu exposes guarded bot controls, and competitive free-for-all disclosure now accepts the active recipients intended by the match policy. +- **Multiplayer function keys work as shown.** F1/F2 voting, F3 ready-up, F6 team switching, and F7 spectating once again use Quake 4's real impulse actions. Ready presses travel over the reliable path, the two-line warmup instruction remains fully visible, and only the exact older openQ4 defaults are upgraded—custom bindings stay untouched. No-time-limit deathmatch also starts without a spurious competitive-rules rejection. - **Developer map compilation is more reliable.** `dmap` and other engine-side geometry tools initialize their triangle-surface allocator before generating light volumes, fixing the Windows crash seen while compiling `game/airdefense1`. - **Liquids now look, sound, and behave like real volumes in combat.** Projectiles and hitscan shots cross the surface with a splash and sound, underwater travel produces bubble trails, clear water stays readable, and swimmers can reliably water-jump out over a clear ledge. Drowning, slime, and lava have distinct localized death-feed icons and messages. The multiplayer Liquid Volume Lab makes everything easy to inspect across deep, shallow, and wading water, vivid cellular slime, and bright heat-hazed, steaming lava, with restrained underwater ambience and boiling hazardous surfaces. +- **Internet multiplayer has safer administration, package negotiation, packet handling, and pure play.** The default remote console no longer sends its password over the network, private password settings stay out of console completion and persistence paths, and unauthenticated replies are bounded. A server can now update only the userinfo or synchronized-setting class owned by each message, and a truncated settings dictionary changes nothing. Server-provided package links are limited to bounded HTTP or HTTPS URLs with syntax-checked hosts; validated web redirects remain available, while standard packages keep in-process direct PK4 transfer disabled. Truncated covered messages and snapshots now fail safely and a malformed snapshot ends the affected session before another frame, the obsolete executable updater can no longer download or launch code, and pure servers keep asset checks enabled while game modules remain trusted local package components rather than server-supplied downloads. ## Upgrade Notes - Replace the complete openQ4 package for your platform so the engine, renderer modules, `game_sp`, `game_mp`, and bundled openQ4 data all come from 0.12.0. Do not mix modules from older releases. - Existing settings and compatible saves do not need to be reset. As a precaution, retain a copy of important saves before replacing an older installation. +- The exact older broad shadow-filter defaults are upgraded once to the new balanced contact-shadow values; customized shadow profiles are left untouched. `r_shadowMapCasterCulling 2` remains the recommended value but now means topology-aware automatic culling rather than far-shell storage. Use `0` for an always-two-sided comparison or `1` to force near-shell culling. +- Level-load data under the active `fs_savepath` `generated/` directory is private and disposable. `com_levelLoadModernization` defaults to `0` and overrides older archived cache settings, so ordinary play uses the classic source path. Set it to `1` only for focused cache evaluation. Never delete the original retail or mod assets. - Retail Quake 4 assets are still required. Point openQ4 at a complete retail installation, including the unsuffixed base dialogue archive such as `zpak_english.pk4`. - OpenGL remains the default and recommended renderer. Vulkan is experimental and opt-in with `r_renderApi vulkan`, applied after an engine restart. The modern OpenGL visible path also remains guarded and experimental. +- Temporal presentation is experimental and defaults off. Set `r_temporalAA 1` to try native-history TAA/TAAU; optionally add `r_rendererDynamicResolution 1` and tune `r_dynamicResolutionMinScale`/`r_dynamicResolutionMaxScale`. For a fixed lower 3D resolution, leave automatic scaling off and set `r_screenFraction` below `100`. HUD, menus, console, screenshots, and save previews remain isolated from temporal accumulation. Set both temporal controls to `0` to return immediately to the established SMAA/spatial path. +- PBR material support is experimental and defaults to `r_pbrMaterials 0`. Retail Quake 4 materials do not contain PBR declarations and remain on their classic path. For authored materials, provide classic `bumpmap`/`diffusemap`/`specularmap` fallback stages, then use a packed ORM map, individual metallic/roughness/AO maps, or scalar values. `tools/assets/pack_pbr_orm.py` creates packed `R=AO`, `G=roughness`, `B=metallic` data from an explicitly selected stored input channel. `tools/validation/generate_pbr_fixture.py` creates an original deterministic temporary fixture only below `.tmp/`; it incorporates no external assets and is not shipped as game content. Visible OpenGL PBR and probe evaluation also requires `r_rendererModernVisible 1`; enabling a material or probe leaf alone does not promote a visible PBR frame. `r_pbrIBL 1` and `r_pbrIBLIntensity` control the built-in PBR-only analytic environment contribution on that guarded OpenGL path. A conventional one-stage `blend blend` source-alpha material is supported when it uses the PBR albedo map; complex transparency remains classic. Vulkan's native route requires an opaque metallic/roughness material with a packed ORM map, `normalFormat tangentXYZ`, and exactly one declared, active classic bump stage, diffuse stage, and specular stage in that order. Duplicate, inactive, invalid, or reordered interaction owners and any custom-lighting co-owner remain entirely classic; an admitted packed-PBR draw replaces only the final interaction submit. Vulkan does not support separate data maps, source alpha, analytic IBL, authored probes, or clustered decals. Keep image-program paths parser-safe (use underscores rather than hyphens). +- Authored specular probes and clustered decals are experimental OpenGL-only options, defaulting to `r_rendererReflectionProbes 0` and `r_rendererClusteredDecals 0`. Author `openQ4SpecularProbe { ... }` only on a dedicated point-light material: that light supplies the probe volume and is metadata-only, so it never becomes an additive classic light. Probes sample base-mip specular colour from at most eight atlas cubemaps and 32 frame records, choose no more than two per cluster, and fall back to analytic PBR lighting when ownership is incomplete. They do not provide box parallax, diffuse irradiance, or runtime capture. Clustered decals publish only an atomic sealed transaction within 1,024 records and 65,536 references; rejected work stays completely classic. Set `r_rendererModernQuality 0` for one-setting rollback of PBR, probes, clustered decals, froxel volumetrics, SSR, and SSGI. The screen-space leaves default off as `r_rendererFroxelVolumetrics 0`, `r_rendererSSR 0`, and `r_rendererSSGI 0`; see the [advanced screen-space lighting guide](../../user/advanced-screen-space-lighting.md) for their bounded tuning and limitations. The local current-source Windows gate proved real OpenGL/Vulkan execution and master rollback, but it did not establish broad authored visual or RenderDoc coverage. GPU-driven visible-lighting promotion remains disabled. Final committed-package, platform/driver, and release promotion remain pending. +- Milestone D's shared renderer corridors are implemented, optional, and default-off while release qualification continues. `r_rendererSharedGui 1` lets eligible complete menu/loading/HUD views use the same ordered material-stage records on OpenGL and Vulkan; one unsupported surface or stage rolls the complete view back to the established backend path. Use `r_rendererSharedGui 0` for the release comparison path. +- `r_rendererSharedInWorldGui 1` lets eligible GUI quads rendered onto 3D surfaces use the same sealed material records and depth behavior on OpenGL and Vulkan; one unsupported tagged surface or stage keeps that complete in-world GUI subset on the established renderer path. It remains default-off pending release promotion. +- `r_rendererSharedCinematicPost 1` can route an eligible complete root video/audio view or complete authored post tail through a sealed OpenGL/Vulkan transaction while retaining the established decoder and feedback-stage behavior. An eligible video/`_currentRender`/`_currentDepth` tail inside a shared special view now binds to that exact root and publishes or rolls back with the whole tree. Final controlled R6 Windows GL/Vulkan captures prove the nested cinematic-plus-`_currentRender` mirror case and its rollback; root cinematic/post, `_currentDepth`, broader special-view forms, final-package, and platform/driver qualification remain pending. Timing, source, packet, target, capture, or backend mismatches keep the complete range on the normal renderer path. +- `r_rendererSharedSpecialFrame 1` can seal an eligible active-session render-demo view or exact Raven blur/AL controller while retaining the complete established view and effect executors. Portal-sky views are excluded, and any session, packet, effect, resolve, or backend mismatch keeps the whole item on the normal renderer path. It remains default-off pending runtime breadth and release qualification. +- `r_rendererSharedWorldAmbient 1` lets an eligible complete ambient-only 3D world view use the same ordered fixed-function surface records on OpenGL and Vulkan. The established depth pass remains in place; interaction lights, shadows, fog/blend, special surfaces, and unsupported work roll the complete ambient view back. Local stock option-off/on and deform-fallback captures match exactly on both backends. +- `r_rendererSharedWorldInteraction 1` can keep an eligible complete shadowed view on the same shared OpenGL/Vulkan path across classic stencil shadows, projected or CSM/parallel maps, point cubes, mixed map/stencil lights, dynamic/cutout casters, and complete hybrid supplements. An incomplete or unsupported case rolls the entire view back, so shared and classic lighting are never layered together. +- `r_rendererSharedWorldFogBlend 1` lets an eligible complete fog/blend phase use one ordered, rollback-safe record stream on OpenGL and Vulkan; one unsupported light, receiver, stage, fog cap, resource, or backend condition keeps the complete phase on the established renderer path. Controlled captures match classic exactly and prove visible work plus atomic rollback on both backends. +- `r_rendererSharedSubview 1` seals direct mirror camera/clip/scissor state and verified remote-camera, mirror, reflection, refraction, or x-ray child-scene captures, including exact 2D/cubemap color/depth target aspect and face. Nested trees and eligible authored-post tails now form one atomic transaction: no member publishes early, and either-domain rejection returns the whole tree to the established renderer path. The final controlled R6 acceptance covers one capture-backed color-2D mirror with a nested cinematic-plus-`_currentRender` tail; direct, multi-level, other capture-kind/aspect/face, `_currentDepth`, and platform breadth remain promotion work. +- `r_rendererSharedDeform 1` lets an enabled shared GUI, world-ambient, interaction, or fog/blend domain consume a fully validated result from the existing CPU material-deform path. Unsupported particle deforms, skipped/failed work, or stale geometry roll the complete owning domain back automatically. It remains default-off pending release promotion. +- GPU animation is experimental and defaults off. `r_gpuSkinning 1` enables it on a capable renderer without requiring converted assets; use `r_gpuSkinning 0` for the established full-CPU comparison path. +- Remote administration uses authenticated `rcon2` by default. Legacy plaintext rcon remains available only through explicit client/server compatibility settings and should be limited to a trusted network; use a strong unique password and do not put it on the process command line. +- Servers that use `si_pure 1` now keep it enabled. The default `net_serverAllowServerMod 0` permits content-only mods to inherit the base module but rejects a mod-supplied game module; set it to `1` only when intentionally operating and separately distributing a trusted code-bearing mod. The protocol 2.41 game-code checksum is a compatibility token for an already installed local module, not a cryptographic check that client and server module bytes match and not an anti-cheat system. - Linux ARM64 downloads remain preview packages. The experimental macOS packages target Apple Silicon/arm64 and may be unsigned when Apple signing and notarization credentials are unavailable. Intel Mac packages are not published or supported, and Rosetta is not a supported substitute. OpenGL remains the recommended renderer; the Vulkan option runs through MoltenVK, a Vulkan-on-Metal translation layer. The `macos_graphics_bridge=metal` package is a Metal bridge around the OpenGL renderer, not a native Metal renderer, while `platform_backend=native` is comparison-only diagnostic infrastructure, not a release backend. Visual parity remains tracked by issue #98, and support promotion remains gated by `docs/dev/macos-signoff-evidence.md`. ## Change Log -- Added presentation-only interpolation for high-refresh rendering of actors, movers, first-person weapons, projectiles, lights, and client effects while preserving authoritative tick behavior. -- Kept view-model client entities and the lightning gun beam attached to the exact rendered weapon pose. +- Added default-off OpenGL/Vulkan TAA/TAAU with native histories, camera/depth reprojection, OpenGL rigid velocity, explicit reactive ownership for unsupported motion streams, disocclusion/neighbourhood rejection, camera/capture resets, native-resolution UI, fixed-scale support, SMAA rollback, and a delayed generation-bound GPU-time dynamic-resolution controller. +- Added guarded, default-off PBR lighting for dual-authored metallic/roughness materials. With `r_rendererModernVisible 1`, the OpenGL G-buffer, deferred, and clustered-forward branches use GGX/Smith/Schlick direct lighting plus a content-free analytic environment contribution, accept packed ORM, separate metallic/roughness/AO, or scalar data, and retain the classic Quake 4 owner whenever the full resource contract is unavailable. Vulkan adds a fail-closed direct-light branch only for opaque packed-ORM `tangentXYZ` materials whose classic interaction topology is exactly one active bump -> diffuse -> specular sequence; only its final interaction submit is replaced. A standard one-stage source-alpha blend can use the ordered OpenGL PBR forward path; complex transparent material graphs remain on their authored classic path. +- Added guarded OpenGL authored specular probes with a fixed eight-cubemap atlas, 32-record frame budget, deterministic top-two-per-cluster selection, and analytic specular fallback; no parallax, diffuse-irradiance, capture, or Vulkan support is implied. +- Added atomic OpenGL clustered-decal ownership bounded to 1,024 records and 65,536 cluster references. Malformed, stale, incomplete, or overflowing transactions publish no ownership and retain the classic path. +- Added `r_rendererModernQuality` as the one-setting Milestone F rollback while leaving the PBR, probe, and decal leaf features default-off. Visible-lighting parity promotion remains disabled. +- Added independent, default-off OpenGL/Vulkan froxel-volumetric, SSR, and SSGI leaves to the native scene presentation pass, bounded respectively to 16 view-depth slices, 16 depth-normal ray steps, and eight diffuse-GI taps. Effect-only use does not allocate temporal history, engine captures retain the result, and missing resources preserve the established presentation owner. +- Added presentation-only interpolation for high-refresh rendering of eligible SP actors, skeletal animation, movers, first-person weapons, projectiles, lights, and client effects while preserving authoritative tick behavior; multiplayer actor bodies and their stencil shadows remain authoritative. +- Removed presentation interpolation work from non-presented cinematic fast-forward ticks and limited visible-frame pose sampling to active movers, their bounded physics-team members, and cleanup members, improving `airdefense1` loading/settle time and CPU frame pacing without changing its authoritative 60 Hz endpoint. +- Avoided constructing bake/debug-only fallback light-grid layouts during ordinary play on maps without baked grids; bake commands still generate the required layout on demand. +- Corrected OpenGL and Vulkan shadow-map caster coverage with topology-aware face culling, near-shell depth for sealed geometry, two-sided depth for open geometry, bounded huge-light point bias, tighter 9-tap balanced filters, consistent PCSS blocker sampling, exact world/pass/storage cache identity, bind-time atlas and point-cube provenance, live point-format and projected-atlas generation validation, newest-compatible stale reuse that rejects moved or rescaled point lights, dynamic and partial-map stencil supplements, translucent-only GLOBAL ownership, hardware atlas bounds, and safely nested OpenGL GPU timing. +- Added fail-after-primary compatibility fallbacks for the three stock brown-fluid images and large water-splash sample omitted from the retail PK4s, while preserving loose-file and mod overrides; generated TrueType atlas materials now use their uploaded intrinsic image identity from first parse and reinstall the full stage after renderer restart; generated wide loading backgrounds stage through a short CSPRNG-named root path before atomic publication; generated image caches retain their legacy identity first and use a deterministic compact identity only when a long save path cannot be opened, with every header, mip record, and payload write checked; expected rigid-body rest and inertia recovery remains developer-visible but no longer enters the warning stream. +- Kept view-model client entities, attached animated models, and the lightning gun beam on the exact rendered skeletal pose. +- Prevented remote-client prediction replays from advancing projectile visuals twice or replacing the viewweapon's authoritative interpolation endpoint, and restored impact/fizzle visuals when a short-lived projectile is first received in a terminal state. - Fixed weapon swaps that could inherit firing state or leave stale effects behind. - Added complete Polish and Russian UI tables, Unicode code-point drawing, CP1250 conversion support, and stricter generated-font checksum validation. - Restored the marine hovertank engine and hover-pad loops with null-safe sound-emitter teardown. @@ -29,11 +67,34 @@ - Restored Arena Campaign countdown and scoring progression when ready-up is disabled. - Added multiplayer bot controls and validation to the server setup menu. - Allowed the intended active free-for-all recipients through competitive match disclosure policy. +- Restored the stock F1/F2/F3/F6/F7 impulse bindings, aligned the controls menu's vote/ready rows, routed the throttled ready toggle through the reliable operation, kept the two-line readiness prompt inside its authored HUD bounds, narrowly migrated only the exact historical openQ4 defaults, and normalized no-time-limit casual matches to sudden-death overtime with a zero period. - Corrected `s_muteUnfocused` transitions so returning to the game cannot leave audio muted for the rest of the level. - Initialized engine-side triangle-surface allocators for `dmap` and related geometry tools. +- Restored clean, fixed-size manual save-game previews at widescreen resolutions and prevented renderer row padding from leaking into narrow captures. +- Replaced plaintext-by-default remote console authentication with bounded `rcon2`, redacted private password CVars throughout console and persistence paths, restricted server-originated CVar dictionaries to their declared userinfo/network-sync authority with all-or-nothing decoding, constrained server package URLs to bounded HTTP/HTTPS with syntax-checked hosts, and retired executable download/launch behavior from the legacy updater. Standard packages keep direct PK4 transfer disabled; separately integrated curl-enabled builds add time-limited, no-redirect transfer containment. +- Restored `si_pure` multiplayer enforcement with ordered asset-PK4 checks and a platform-independent Quake 4 1.4.2 protocol 2.41 compatibility token for already installed openQ4 game modules; pure negotiation cannot download, extract, or restart into executable code, and the token is not cryptographic module attestation. +- Added fail-closed read-underflow tracking and range/type checks to audited queued-message, user-command, SP/MP snapshot, server-demo, player, projectile, and hit-scan decode paths. Audited leaf readers stage their fields until valid, and a malformed top-level snapshot tears down the session before another game or presentation frame. +- Made Windows staging self-cleaning for explicitly known cross-platform leftovers: extensionless POSIX engine binaries and obsolete renderer backup files are removed from successful full and fast stages, while the legacy empty `baseoq4/skins` directory is pruned only when it contains nothing. - Added the well-lit `mp/liquid_lab` multiplayer showcase with recessed pools, a sky ceiling, shadow-casting beams, deep-pool floor lights, vivid animated slime, bright lava heat haze and steam, bubbling hazardous surfaces, submerged local ambience, and full-arsenal respawns. -- Let projectiles and hitscan weapons pass through liquid surfaces while retaining reliable entry splashes and sounds; submerged projectile trails and hitscan segments now render as bubbles. +- Let projectiles and hitscan weapons pass through liquid surfaces while retaining reliable entry splashes and sounds; submerged projectile trails and hitscan segments now render as liquid-specific bubbles. - Restored practical water jumps with Quake 4-height ledge probes, gravity-aware launch arcs, and an escapable wading basin. - Brightened clear-water visibility, removed hall-like underwater reverb tails, and debounced rapid surface sounds without suppressing their splash effects. - Added localized drowning, slime, and lava obituaries with distinct graphical death-feed icons and normal burn feedback for slime. - Strengthened the experimental modern OpenGL lighting path with per-light image ownership and missing-binding fallbacks. +- Added a bounded, portable background-job foundation with deterministic fallback, cooperative cancellation, dependency ordering, starvation protection, and clean-shutdown diagnostics. Foundation validation produced identical jobs-on/off stock output and clean repeated-map and dedicated exits; its first production workload is the level-load read/framing consumer described below. +- Added the first production job consumer: an exact-match learned level-load manifest and bounded read/PK4-inflate plus source-framing/integrity pipeline. VFS and pure rules remain authoritative, unfinished jobs never delay a normal open, and existing owners retain asset-specific parsing, validation, adoption, and renderer/audio upload. Added SHA-256-validated, atomically published private envelopes and transactional payloads for supported static/MD5/MD5R models, classic `.proc` worlds, and collision models; upgraded SP/MP animation caches to version 3 with exact compiled-or-text source identity, a loose-source CRC, a whole-record length/CRC check before parsing, private all-or-nothing decode, and atomic writes. Unsupported packed MD5RProc world state, any mismatch, and corrupt data fall back to source. +- Restored classic loading as the default after a regression audit found excessive stock-map cache/preload work; the new default-off `com_levelLoadModernization` gate also prevents older archived subcontrols from continuing animation or framework cache rewrites. +- Vulkan shadow-map allocation or stencil-submission failures now preserve the affected direct light unshadowed for that frame instead of dropping its receivers, preventing visible light pop-out while the sticky stencil fallback recovers. +- Added non-blocking whole-frame GPU timing for OpenGL and Vulkan, together with backend-neutral high-resolution renderer CPU timing. Delayed results avoid forcing the GPU to idle, reset safely across renderer, map, and capture transitions, and have been exercised on both backends in current-build gameplay. +- Added shared material/pass, clip-space, semantic vertex-layout, and typed buffer-slice contracts plus an opt-in exact four-weight GPU-animation corridor for MD5 and MD5R models on OpenGL and Vulkan. The GPU produces the ordinary draw-vertex stream, while CPU positions and complete fallbacks preserve collision, hit, decal, overlay, and stencil-shadow behavior. +- Added an optional, default-off whole-view fixed-function 2D GUI corridor behind `r_rendererSharedGui`. Repeated material stages, conditions, colors, texture transforms, render state, and images are evaluated once into the same ordered records for OpenGL and Vulkan; either backend owns the complete eligible view or runs the untouched classic view, preventing partial or silently dropped overlays. +- Added an optional, default-off in-world GUI corridor behind `r_rendererSharedInWorldGui`. It only considers GUI quads generated onto 3D surfaces, keeps their established depth behavior, and either submits the complete tagged subset from sealed OpenGL/Vulkan records or leaves every tagged quad on the classic renderer, preventing partial ownership and duplicate overlays. +- Added an optional, default-off cinematic/authored-post corridor behind `r_rendererSharedCinematicPost`. Eligible complete root video/audio views and ordered post-process tails seal packet/source order and cinematic time before OpenGL and Vulkan reuse the established dynamic executors. Tails nested in a sealed special view now share its exact root/depth, deferred publication, and whole-tree rollback boundary. +- Added an optional, default-off render-demo/Raven special-frame corridor behind `r_rendererSharedSpecialFrame`. Active session render-demo playback is distinguished from portal-sky negative view IDs, while exact Raven blur/AL controller masks are sealed from `RC_DRAW_SPECIAL_EFFECTS`. OpenGL and Vulkan report ownership only after all sealed coverage completes; incomplete state retains the normal classic path. +- Added an optional, default-off whole-view fixed-function world ambient/material corridor behind `r_rendererSharedWorldAmbient`. Eligible ambient-only 3D views evaluate repeated material stages once into packet-derived surface records shared by OpenGL and Vulkan and complete all backend preflight before drawing. Unsupported work keeps the complete ambient view on the untouched classic path. Local GL/Vulkan stock captures prove exact enabled output and named zero-draw deform fallback. +- Expanded the optional, default-off whole-view fixed-classic interaction corridor behind `r_rendererSharedWorldInteraction` so eligible shadowed views can remain coherently owned on either backend. The transaction keeps interaction draws and stencil/projected/CSM/point/mixed/hybrid shadow work together. Its controlled GL/Vulkan suite has exact same-backend classic image parity, visible shadow deltas, and atomic-fallback evidence. +- Added an optional, default-off complete-phase classic fog/blend corridor behind `r_rendererSharedWorldFogBlend`. One bounded transaction preserves ordered lights, receivers, repeated/inactive stages, fog caps, evaluated state, images, and exact coverage. The controlled GL/Vulkan suite passes exact classic parity, visible fog/blend deltas, and atomic rollback. +- Expanded the optional, default-off special-subview transaction behind `r_rendererSharedSubview`. Direct mirrors and dynamic remote-camera, mirror, reflection, refraction, and x-ray captures retain exact semantics and target aspect/face. Nested trees now share root/depth identity and deferred publication with eligible cinematic/post tails; any member rejection rolls the complete cross-domain tree back without duplicate ownership or fallback reports. +- Added a default-off shared classic material-deform contract behind `r_rendererSharedDeform`. The authoritative CPU algorithms are unchanged; per-draw records now distinguish them from generated/skinned model geometry and seal source/result geometry, cache lifetime, evaluated inputs, frame freshness, consumer role, outcome, and stable hash. Existing shared domains can consume completed or intentional-empty output without rerunning it in OpenGL or Vulkan, while unsupported, skipped, failed, stale, or mismatched work falls back atomically. Controlled OpenGL and Vulkan captures match the classic path exactly and prove both the visible deform and zero-draw skipped fallback. +- Added replayable per-map renderer budget contracts and exact bordered-window 1280x720 evidence checks for both gameplay benchmarks and the stock-asset baseline. Current-build required profiles pass on all eight OpenGL and all eight Vulkan roles. The checked-in limits are initial target ceilings rather than a performance guarantee; clean final-package and platform qualification remain separate release gates. +- Prevented invalid OpenGL depth-bounds values in effects-heavy scenes by ordering and clamping the submitted range, making the `game/medlabs` validation run error-free. diff --git a/docs/dev/renderer-validation-matrix.md b/docs/dev/renderer-validation-matrix.md index 26fcf65c..b59c1424 100644 --- a/docs/dev/renderer-validation-matrix.md +++ b/docs/dev/renderer-validation-matrix.md @@ -1,6 +1,8 @@ # Renderer Validation Matrix -This matrix is the validation source of truth for the staged GL renderer work. It separates safe automated startup/self-test coverage from gameplay smoke coverage that must be run manually with the mode-specific SP/MP launch tasks. +This matrix is the validation source of truth for staged renderer work. Most safe tier probes remain GL-focused, while shared renderer-contract, classic-domain, and GPU-animation self-tests plus replayable budget evidence cover both OpenGL and Vulkan. Supervised gameplay can use the mode-specific SP/MP launch tasks or the noninteractive gameplay harness described below. + +For cross-engine feature status, use the [engine capability matrix](engine-capability-matrix.md). This document owns renderer acceptance evidence and promotion gates; it does not turn an experimental renderer capability into a supported/default one by itself. ## Build And Stage @@ -27,6 +29,11 @@ The safe matrix starts the staged client, runs renderer self-tests or startup pr python tools\tests\renderer_validation_matrix.py ``` +Each invocation uses `/savepath` by default, so archived settings in +the developer `.home` tree cannot silently select Vulkan or enable experimental +renderer paths. Pass `--savepath` only when a case intentionally needs a named, +pre-seeded save tree. + For focused validation without relaunching the full matrix, use `--cases` with one or more case ids: ```powershell @@ -39,10 +46,12 @@ Automated coverage: | Case | Coverage | |---|---| -| `renderer-foundation-selftests` | context ladder, tier selector, tier workload contract, upload manager, GPU timer, scene packet, render graph, render graph resource owner, material resource table, geometry/instance resource records, GL state cache, Shader Library V2 pass-family/permutation/reflection coverage, draw plan, submit plan, modern executor, and shadow planner self-tests | +| `renderer-foundation-selftests` | context ladder, tier selector, tier workload contract, backend-neutral authored/evaluated pass/clip/layout/buffer contracts, exact four-weight GPU-animation contract, upload manager, GPU timer, scene packet, render graph, render graph resource owner, ordered material resource table, PBR/authored-probe material parsing, specular-probe atlas placement/slot/generation, transactional classic-GUI, classic cinematic/authored-post, render-demo/Raven special-frame, classic-world-ambient, classic-interaction, classic-fog/blend, and capture-backed classic-subview domains, geometry/instance resource records, GL state cache, Shader Library V2 pass-family/permutation/reflection coverage, draw plan, submit plan, modern executor, and shadow planner self-tests | +| `renderer-vk-clear-startup` | Vulkan module startup plus the same mandatory backend-neutral renderer-contract, classic cinematic/authored-post, render-demo/Raven special-frame, classic-world-ambient, classic-interaction, classic-fog/blend, capture-backed classic-subview, and exact GPU-animation self-test markers used by OpenGL; device, swapchain, and GUI executor initialization run with validation layers enabled | | `renderer-visible-depth-selftest` | opt-in `r_rendererModernVisibleDepth` coverage for graph-backed scene depth, compatible shadow-depth resources, fallback accounting, depth-overlay readiness, and `gfxInfo` reporting | | `renderer-gbuffer-selftest` | opt-in `r_rendererModernOpaque` coverage for graph-backed G-buffer resources, MRT setup, opaque/alpha-test draw classification, diffuse texture binding, packing assumptions, fallback accounting, bandwidth metrics, attachment debug-overlay readiness, and `gfxInfo` reporting | -| `renderer-cluster-grid-selftest` | opt-in modern clustered-light preparation coverage for point/projected/fog/ambient/special light classification, budgeted dynamic grid slicing, cluster reference packing, spill/overflow accounting, GL 3.3 UBO fallback readiness, GL 4.3+ SSBO upload readiness, cluster debug-overlay texture generation, and `gfxInfo` reporting | +| `renderer-cluster-grid-selftest` | opt-in modern clustered-light preparation coverage for point/projected/fog/ambient/special light classification, budgeted dynamic grid slicing, cluster reference packing, spill/overflow accounting, authored-probe top-two selection, and atomic clustered-decal prepare/seal ownership including malformed, stale, record-overflow, and reference-overflow zero-publication cases; GL 3.3 UBO fallback readiness, GL 4.3+ SSBO upload readiness, cluster debug-overlay texture generation, and `gfxInfo` reporting | +| `renderer-pbr-visible-selftest` | guarded opaque PBR resource admission, linked G-buffer/deferred/forward program readiness, G-buffer command/input packing, scalar propagation, `r_rendererModernQuality` rollback, and clustered-forward exactly-one surface-owner/interaction-consumption accounting without promoting stock materials or GPU-driven visible lighting | | `renderer-shadow-planner-selftest` | modern shadow planner coverage for projected/point/CSM policy, mapped/stencil-fallback/skipped accounting, benchmark-budgeted shadow resolution/light/pixel caps, render-graph shadow resource reporting, clustered shadow descriptor integration, and `gfxInfo` reporting | | `renderer-deferred-resolve-selftest` | opt-in `r_rendererModernDeferred` coverage for graph-backed deferred resolve output, G-buffer/depth/cluster buffer inputs, point/projected light accumulation, light-grid contribution, fallback accounting, deferred debug-overlay readiness, GPU timer coverage, and `gfxInfo` reporting | | `renderer-forward-plus-selftest` | opt-in `r_rendererForwardPlus` coverage for graph-backed scene-color/depth resources, clustered opaque/alpha-test/transparent programs, clustered-light UBO/SSBO reads, transparent sort preservation, fallback accounting, overdraw estimates, GPU timer coverage, and `gfxInfo` reporting | @@ -50,7 +59,7 @@ Automated coverage: | `renderer-modern-compatibility-selftest` | Phase 14 modern-visible compatibility coverage for command-category ownership inventory, modern fullscreen GUI readiness, light-grid ownership, explicit post/copy/subview/render-demo/BSE fallback buckets, deterministic render-demo accounting, and `gfxInfo` reporting | | `renderer-compatibility-gates-selftest` | Phase 15 fallback-gate coverage for missing UBO, broken MRT, missing timer query, missing buffer storage, rejected debug-context fallback, and synthetic driver-quirk downgrades | | `renderer-default-promotion-selftest` | Phase 8 evidence-gated default-promotion coverage for `r_glTier auto`, explicit `r_renderer arb2` escape behavior, compatibility gates, modern-executor readiness, ARB2 rollback availability, missing/incomplete/complete `r_rendererPromotionEvidence`, and `r_rendererModernAutoPromote` sign-off control | -| `renderer-default-safety-selftest` | Phase 13 conservative-default coverage for ARB2 default visibility, `r_renderer best` or explicit `r_renderer arb2`, `r_glTier auto`, rollback availability, and default-off modern executor, visible, diagnostic, GPU-validation, bindless, shader-reload, and auto-promotion cvars | +| `renderer-default-safety-selftest` | Phase 13 conservative-default coverage for ARB2 default visibility, `r_renderer best` or explicit `r_renderer arb2`, `r_glTier auto`, rollback availability, default-off PBR/probe/clustered-decal leaves, `r_rendererModernQuality` master rollback, and default-off shared root/in-world GUI, cinematic/post, special-frame, world-ambient, interaction, fog/blend, subview, and deform corridors plus modern executor, visible, diagnostic, GPU-validation, bindless, shader-reload, and auto-promotion cvars | | `renderer-benchmark-selftest` | Phase 16 benchmark coverage for rolling P50/P95/P99 frame-time capture, CPU front-end/visibility/packet/graph/submit/present timings, GPU pass timing fields, upload/draw/light/cluster/fallback counters, benchmark presets, and performance-threshold reporting | | `renderer-gpu-driven-selftest` | forced `r_glTier gl43` coverage for GL 4.3 SSBO submit records, compute scissor culling, clustered-bin validation, compacted indirect command generation, CPU/GPU readback comparison, masked multi-draw indirect execution, GPU timer coverage, and `gfxInfo` reporting | | `renderer-low-overhead-selftest` | forced `r_glTier gl45` coverage for GL 4.5 DSA graph texture/FBO allocation, DSA sampler creation, named buffer/FBO updates, UBO/SSBO/texture/sampler multi-bind batches, submit-batch compaction, bindless experiment reporting, persistent upload defaults, fence diagnostics, and `gfxInfo` reporting | @@ -75,10 +84,166 @@ The forced tier cases pass when startup succeeds and the selected tier is report Automated safe cases also fail if their logs contain renderer warning signatures such as `idStr::snPrintf` overflow, `WARNING: idStr`, shader compile/program link failures, or OpenGL error markers. The generated Markdown/JSON report records per-case warning-signature counts so the Phase 8 `warnings=0` promotion token cannot be inferred from expected-line checks alone. +The foundation case runs the dependency-light classic-GUI, +classic-cinematic/authored-post, render-demo/Raven special-frame, classic-world-ambient, classic-interaction, +classic-fog/blend, and capture-backed classic-subview contract self-tests. The +Vulkan startup case also requires the cinematic/authored-post, render-demo/Raven special-frame, world-ambient, +interaction, fog/blend, and subview self-test markers so that module +registration and the shared domain contracts are exercised through both +renderer APIs. + +The separate `tools/tests/renderer_classic_gui_domain.py` static regression +guards packet-derived GUI material admission, ordered evaluation, opaque +resource ids, GL/Vulkan whole-view handoff ordering, source no-op accounting, +and suppression of the older aggregate GL GUI replay. +`tools/tests/renderer_classic_world_ambient_domain.py` guards the distinct +packet-derived world-pass pool, fixed surface/program/depth semantics, +transactional source/depth matching, explicit non-owned blockers, pre-fog/fog/ +post-fog ordering, complete GL/Vulkan preflight before commit, and sealed +consumer bodies with no material-stage or raw-register reinterpretation. It also +checks conservative default/bootstrap state, benchmark/baseline isolation, and +validation workflow registration. Enabled-path image evidence for these first +two domains is the supervised GL/Vulkan capture pair below. + +`tools/tests/renderer_classic_interaction_domain.py`, +`renderer_vulkan_world_interaction_compatibility.py`, and +`renderer_vulkan_shadow_compatibility.py` guard the unshadowed and +shadow-coupled interaction contracts. They require exact packet receiver/caster +identity, stencil physical-replay counts, projected single-map and CSM/parallel +state, point cubes, mapped/static/dynamic/perforated casters, complete hybrid +supplements, semantic map hashes, GL cache/update/scratch retention, Vulkan +reservation/commit/abort ordering, exact backend reconciliation, and atomic +fallback with zero committed ownership counters and no shared main-target draw. +The gameplay harness parser also fails a dynamic or +perforated stock target unless a per-map line explicitly reports the matching +sealed feature. + +`tools/tests/renderer_classic_fog_blend_domain.py` guards the complete +fog/blend-phase transaction: exact view-light and GLOBAL/LOCAL receiver +identity, ordered active/inactive blend stages, fog receiver/cap accounting, +evaluated texgen/state/resource sealing, stable hashing, complete GL/Vulkan +preflight before the first main-target draw, backend reconciliation, atomic +fallback, conservative default/bootstrap state, benchmark/baseline isolation, +and validation registration. Its dependency-light self-test and static +contract pass, but enabled-path stock engine-screenshot qualification remains +pending. + +`tools/tests/renderer_classic_subview_domain.py` guards exact front-end and +legacy child-view/capture association, bounded parent/source/material admission, +remote-camera/refraction-only scope, sealed OpenGL/Vulkan copy arguments, +post-copy ownership reporting, default isolation, diagnostics, and workflow +registration. Its native/static contract passes; fixed-camera engine-screenshot +qualification remains required before enabling the setting beyond focused work. + +The focused safe-matrix run retained at +`.tmp/renderer-validation/world-ambient-final-2` passed all three selected +cases: `renderer-foundation-selftests`, `renderer-default-safety-selftest`, and +`renderer-vk-clear-startup`. + The visible-depth, G-buffer, clustered-light, deferred-resolve, forward+, modern-visible, modern-compatibility, compatibility-gates, default-promotion, default-safety, benchmark, GPU-driven, low-overhead, and shader-library tier self-tests intentionally run as their own safe cases instead of being appended to the foundation self-test startup command, because the engine command parser has a fixed startup command list budget. The shader-library tier cases force `r_glTier gl33`, `gl41`, `gl43`, `gl45`, and `gl46`, run `rendererShaderLibrarySelfTest`, and require `gfxInfo` to report `Modern GL shader library: available` with program, kind, permutation, and sampler-reflection coverage. The runner marks these cases as assetless startup probes, because they only need renderer initialization and should not load game scripts just to validate internal shader variants. +The foundation self-test case also runs `rendererPBRMaterialSelfTest`, while `rendererScenePacketSelfTest` and `rendererMaterialResourceTableSelfTest` include the matching PBR packet/resource cases. Together these assetless contracts are expected to verify that opt-in `pbr {}` metadata leaves classic Quake 4 stages untouched, image usage and scalar registers survive parsing, explicit and approximate classic fallbacks are classified deterministically, packet records preserve PBR metadata, and packed, separate, scalar-only, unsupported-workflow, and missing-map resource records expose deterministic reasons. Packed ORM, separate metallic/roughness/AO, and scalar-only records may become `pbrModernReady`; the separate maps are bound through direct sampler units rather than the four-entry texture-table ABI. The material parser self-test also owns the namespaced authored light-material probe metadata contract without changing the public light/game/save/demo/network ABI. + +`renderer-pbr-visible-selftest` runs `rendererPBRVisibleSelfTest` for guarded opaque resource admission, linked G-buffer/deferred/forward programs, G-buffer command/input packing, scalar propagation, `r_rendererModernQuality 0` rollback, and clustered-forward surface-owner deduplication. It does not itself exercise source-alpha admission, analytic IBL, PBR debug routes, or authored-probe atlas/binding readiness. `renderer_pbr_materials.py` statically pins the GGX/Smith/Schlick source, explicit PBR eligibility marker, direct texture-unit contracts, analytic-IBL path, source-alpha admission, debug routes, probe shader ABI, and the narrow Vulkan packed-PBR contract. The foundation executor self-test owns probe atlas placement/slot/generation checks; the deferred and forward+ self-tests validate reflected probe sampler/UBO bindings; `rendererClusterGridSelfTest` owns producer-side probe selection and clustered-decal transactions. Vulkan admission additionally requires exactly one declared and active classic bump -> diffuse -> specular sequence, rejects duplicate/invalid/reordered or custom-lighting co-ownership, and replaces only the final interaction submit. It intentionally falls back for separate maps, non-RGB tangent normals, transparency, analytic IBL, authored probes, and clustered decals. + +Milestone F adds three dependency-light contracts. `openq4-advanced-lighting-core-test` covers generation-bound bounded transactions, malformed/NaN input, capacity and reference overflow, deterministic priority/weight/stable-id top-two selection, probe blend weights, and complete master-disabled rejection. `openq4-specular-probe-atlas-packing-test` covers fixed six-face placement for eight cubemaps without needing a GL context. `rendererClusterGridSelfTest` covers the engine-side probe/decal integration, including the 32-probe record limit, top-two cluster indices, and atomic decal prepare/seal ownership capped at 1,024 records and 65,536 references. A rejected probe uses analytic PBR fallback; a rejected decal transaction publishes no modern ownership. These paths are OpenGL-only, and `MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`. + +The durable visual fixture is generated by `tools/validation/generate_pbr_fixture.py`. It uses only the Python standard library to create original deterministic 24-bit TGA albedo, `tangentXYZ` normal, and packed ORM data, a dual-authored material, and a SHA-256 provenance manifest below `.tmp/`; it reads or copies no external asset. `pbr_procedural_fixture.py` checks determinism, hashes, TGA layout, material wiring, provenance, and output containment. `pbr_orm_packer.py` separately checks that the ORM helper reads an explicitly selected stored channel and writes `R=AO`, `G=roughness`, `B=metallic` without luminance conversion. + +### Milestone F current-source Windows evidence + +The local implementation exit gate passed on 2026-08-23. The retained root is +`.tmp/milestone-f-evidence/20260823-final/`; it was produced from a +current-source debug x64 build and a retail-PK4 asset root rather than a clean +committed release package. + +| Gate | Local result | +|---|---| +| Static/native/engine contracts | PASS: the PBR and advanced-lighting static contracts passed; the dependency-light native run passed 10/10. `engine-selftests-final4` passed its 2/2 focused PBR/modern-visible rerun against the final staged package after the earlier `engine-selftests` set passed 8/8. | +| Retail compatibility and budgets | PASS: the final staged-package rerun in `stock-baseline-final4` passed all four roles (SP capture, SP demo playback, pure MP server, and auto-joined MP client) against 40 retail PK4s and zero loose `q4base`/`q4mp` files, and its recorded asset/runtime/artifact hashes passed replay verification. CPU P95/P99 and GPU P95/P99 were respectively 8,259/8,703 and 2,277/2,743 us for SP, 7,127/7,490 and 2,901/3,896 us for the MP server, and 9,757/10,556 and 3,618/3,898 us for the MP client. Packaged openQ4 overlays were still present, so this is not an overlay-free claim. | +| OpenGL procedural PBR | PASS on forced GL 3.3, 4.1, 4.3, and 4.5 with frame-pacing P95/P99 of 6/6, 5/5, 5/5, and 6/6 ms and zero GL/FBO warning signatures. Each real plan reduced 24 source packets to five depth plus five ambient owners with `planFallback=0`; forward+ executed five draws and the visible bridge reported execution, resources, source, and composition. The GL 4.5 debug-7 capture contained the exact green ownership marker in 639,507 of 921,600 pixels (69.39%). The final GL 4.5 functional capture in `pbr-gl-harness-gate-final5` also passed schema-4 replay with the exact fixture, generated config, loaded map, runtime, process outcome, screenshot, and API-specific telemetry bound. | +| OpenGL master rollback | PASS: `pbr-gl-quality0-rollback-final2` matched the leaf-disabled reference at RMS 0 and maximum delta 0 on all four tiers. | +| Vulkan procedural PBR | PASS with validation enabled: `pbr-vk-enabled-final2` differed non-vacuously from its leaf-disabled control with per-channel RGB RMS 26.374685/20.829991/21.549890 and maximum delta 252. `pbr-vk-quality0-rollback-final2` then matched the leaf-disabled reference at RMS 0 and maximum delta 0. The final API-specific functional capture in `pbr-vk-harness-gate-final5` required a positive native packed-PBR draw marker and passed schema-4 replay with the same provenance bindings as the GL capture. | + +This evidence proves the scoped PBR/probe/decal implementation, real PBR shader +ownership, and one-setting rollback. It does not claim RenderDoc coverage, +broad visual-quality acceptance, or broad authored probe/decal scene coverage. +PBR, probe, decal, and modern-visible feature controls remain default-off; +`MODERN_LIGHTING_PARITY_PROVEN_DOMAINS` remains `0`. Final committed-package, +platform/driver, retained-review, and release promotion remain pending. + +### Advanced screen-space current-source Windows evidence + +The independent froxel-volumetric, SSR, and SSGI leaves closed their scoped +implementation gate on 2026-08-24: + +| Gate | Local result | +|---|---| +| Shared contract | PASS: `AdvancedScreenSpaceCoreTest` covers independent/combined masks, finite bounded tuning, disabled-leaf canonicalization, the stable eight-float GL/Vulkan packet, and exact master-off zero initialization. `renderer_advanced_lighting.py` verifies the default-off leaf/master admission and both backend consumers. The final Meson native suite passed 11/11. | +| Vulkan shader provenance | PASS: the checked-in temporal-resolve SPIR-V header was generated from the checked-in GLSL with the local official Vulkan SDK compiler, and `vk_temporal_resolve_shader_pin.py` passes. | +| OpenGL gameplay | PASS: all three leaves together and each leaf independently entered windowed `game/airdefense1` gameplay with an engine TGA and zero shader/FBO/GL/fatal/error counters. Two post-audit combined captures observed 102.9--117.3 Hz; the final hardened run measured 11/11 ms P95/P99 and the earlier run measured 12/13 ms. The enabled image differed non-vacuously from the same-build default control. | +| Vulkan gameplay | PASS: all three leaves together entered the same windowed scene with an engine TGA and zero validation/VUID/Vulkan-call/fatal/error counters. Two post-audit captures observed 114.1--125.8 Hz; the final hardened run measured 10/11 ms P95/P99 and the earlier run measured 11/12 ms. Effect-only presentation retained one accepted configuration generation rather than churning temporal history state. | +| Master rollback | PASS at the exact shared-packet boundary and clean in runtime: `r_rendererModernQuality 0` zeroes the complete eight-float feature packet despite enabled leaves, and the master-off `airdefense1` run enters gameplay without diagnostics. Live BSE smoke and actors make separate process launches unsuitable for an exact screenshot hash. | +| Final regression breadth | PASS: default `game/airdefense2` measured 189.6 Hz with 7/8 ms P95/P99 on OpenGL and 240.2 Hz with 6/6 ms on Vulkan. Pure `mp/q4dm1` listen-server/auto-joined-client roles passed at 174.0/180.7 Hz on OpenGL and 224.8/189.1 Hz on Vulkan. A fresh staged-package baseline then passed SP capture, SP demo playback, MP server, and MP client against all 40 verified retail PK4s with zero loose retail files. Every role retained an engine TGA and clean API/error counters; representative images passed human review. | + +This is a bounded scene-colour/depth implementation, not evidence for shadowed +light-injected volumetrics, roughness-aware G-buffer reflections, world-space +GI, or whole-frame modern lighting ownership. All leaves remain default off; +clean committed-package, target-platform/driver, and retained visual-review +promotion remain open. See the [user controls and limitations](../user/advanced-screen-space-lighting.md) +and [`airdefense1` optimization evidence](airdefense1-optimization-evidence.md). + +The same foundation case requires `rendererContractsSelfTest` and +`rendererGpuSkinningSelfTest` to pass without a skip. The Vulkan startup case +runs those commands through the Vulkan module and requires the identical +semantic markers plus `GPU skinning:` diagnostics. The dependency-light +`openq4-renderer-contracts` native test covers ordered/repeated material passes, +GL/Vulkan depth and viewport conversion, legacy and exact-skinned layouts, and +typed stale/expired/overflowing buffer slices on every native-test platform. + +Milestone E has two dependency-light native tests. `openq4-temporal-presentation-core` +covers delayed sample identity, bounded/aligned scale decisions, hysteretic +recovery, unsupported timing, and capture policy. `openq4-temporal-history-core` +covers view/generation continuity, motion-domain ownership, bounded reactive +regions, camera cuts, depth validity, and capture invalidation. +`renderer_temporal_presentation.py` pins the cross-repository ABI, SP/MP camera +cut signaling, game-owned and backend-owned history swap rules, exact +frame/generation depth stamps, GL rigid velocity, Vulkan conservative reactive +fallback, manual mode-0 TAAU, native UI ownership, and SMAA rollback. +`vk_temporal_resolve_shader_pin.py` rebuilds and compares the embedded temporal +SPIR-V when `glslangValidator` is available. Both scripts are syntax-checked and +run by commit/push CI. `renderer_screenshot_readback.py` additionally requires a +UNORM + `SRGB_NONLINEAR` Vulkan swapchain and rejects arbitrary sRGB/HDR-only +fallback so the legacy SDR code values are not encoded twice. + +The 2026-08-23 Windows x64 local exit gate used the staged `.install` runtime, +the retail PK4s, a bordered 1280x720 window, engine-written TGAs, and no injected +mouse or keyboard input. OpenGL passed all six required SP scenes plus both MP +listen/connect cases; the MP server roles were rerun with a two-second wall-clock +sample after the first frame-wait schedule outlived the harness timeout. Vulkan +passed the complete eight-case matrix in one run. A forced `0.1` ms controller +target produced valid delayed samples and reduced OpenGL to 50--75% and Vulkan +to 50%, while every passing role retained zero renderer, GL, Vulkan-validation, +fatal, and map-state diagnostic counters. + +Targeted runs then covered the boundaries outside the main matrix. A same-path +60% spatial/TAAU comparison and a stable `game/medlabs` comparison retained the +same scene tonality; the initially suspected gray lift was the authored +five-second white fade in `game/storage1`, while mode 0 was an intentionally +cropped legacy path and not a full-output reference. GL and Vulkan save commands +issued at 50% scale each wrote a valid 320x240 preview, froze capture feedback, +advanced history generation from 4 to 6, and reported +`historyReset=synchronous readback`. Disconnecting from 50% gameplay produced a +native 1280x720 main menu on both backends with `historyReset=session unload`. +Finally, separate GL and Vulkan gameplay runs with both temporal controls off +reported 100% scale and the requested SMAA-high path. Engine screenshots enter +the current-frame capture branch by design, so these TGAs prove presentation, +native UI, and capture isolation; accumulated-history admission and blending are +covered by the native/static contracts above rather than claimed from a capture +that deliberately bypasses history. + Gameplay benchmark acceptance should use wall-clock sampling for FPS claims. The `--sample-msec` option emits `waitMsec` into the generated cfg so the measurement window is a real duration rather than a frame count: ```powershell @@ -110,7 +275,7 @@ python tools\tests\renderer_gameplay_benchmark.py --profile smoke --maxfps 0 --s | renderer escape | `r_renderer best` leaves promotion available; explicit `r_renderer arb2` keeps the ARB2 bridge | | compatibility gates | modern baseline features, UBOs, MRT, scene packets, render graph, and Shader Library V2 readiness are available | | fallback escape | the ARB2 compatibility bridge remains selectable through `r_renderer arb2` and `r_glTier legacy` | -| conservative defaults | `r_renderer best` or explicit `r_renderer arb2` keeps ARB2 visible; `r_rendererModernAutoPromote`, modern executor/submit/visible/pass/debug paths, GPU validation, bindless, and shader reload all remain off in a clean startup | +| conservative defaults | `r_renderer best` or explicit `r_renderer arb2` keeps ARB2 visible; `r_rendererSharedGui`, `r_rendererSharedInWorldGui`, `r_rendererSharedCinematicPost`, `r_rendererSharedSpecialFrame`, `r_rendererSharedWorldAmbient`, `r_rendererSharedWorldInteraction`, `r_rendererSharedWorldFogBlend`, `r_rendererSharedSubview`, `r_rendererSharedDeform`, `r_vkShadowFallbackTest`, `r_rendererModernAutoPromote`, modern executor/submit/visible/pass/debug paths, GPU validation, bindless, and shader reload all remain off in a clean startup | | validation evidence | `r_rendererPromotionEvidence` carries the complete Phase 8 token after zero-warning visual, gameplay, RenderDoc, performance, presentation, rollback, and debug-off checks pass | | manual sign-off | `r_rendererModernAutoPromote 1` is used only together with a complete `r_rendererPromotionEvidence` token | @@ -127,10 +292,84 @@ These image captures are the comparison set for scenes where deterministic outpu | Case | Mode | Scene | Purpose | |---|---|---|---| | `capture-startup-mainmenu` | SP | main menu after logo skip | deterministic GUI composition, font/material atlas, and widescreen expansion | +| `capture-shared-gui-mainmenu` | SP | same static main-menu view with `r_rendererSharedGui 0` then `1`, separately on GL and Vulkan | engine-TGA equivalence, identical stable domain coverage/hash for the same authored view, backend-owned count, and zero mixed/dropped pass accounting | +| `capture-shared-gui-rollback` | SP | loading/logo/camera-preview view containing a cinematic, dynamic, current-render, or otherwise unsupported stage | one explicit whole-view fallback, zero shared draws for that view, and classic screenshot equivalence | +| `capture-shared-inworld-gui-owned` | SP | a stable stock 3D GUI surface with `r_rendererSharedInWorldGui 0` then `1`, separately on GL and Vulkan | engine-TGA equivalence, stable tagged-subset hash, nonzero backend-owned tagged subset, correct pre-ambient depth ordering, and zero duplicate GUI draws | +| `capture-shared-inworld-gui-rollback` | SP | the same stable stock 3D GUI surface forced through an unsupported tagged stage/resource/view condition | one named tagged-subset fallback, zero shared tagged draws, and classic screenshot equivalence | +| `capture-shared-world-ambient-owned` | SP | `sp-mv2-ambient` on stock `maps/tools/mv2`, using the `world-ambient` profile with `r_rendererSharedWorldAmbient 0` then `1`, separately on GL and Vulkan | **Passed locally:** exact engine-TGA equivalence, stable domain hash `dc18ed8c0539bbfc`, view hash `bad7344c6394edf8`, one backend-owned pre-fog draw, and zero mixed/dropped pass accounting | +| `capture-shared-world-ambient-rollback` | SP | same `sp-mv2-ambient` view with stock `r_materialOverride shaderDemos/move` to force a deform blocker | **Passed locally:** exact engine-TGA equivalence, `failure=sourceSurfaceFallback detail=13 sourceClass=fallbackDeform`, zero shared draws, and complete classic fallback on GL and Vulkan | +| `capture-shared-interaction-owned` | SP | current `sp-mv2-interaction` controlled stock-asset fixture with fixed camera, two crates, projected test light, and point test light, using `r_rendererSharedWorldInteraction 0` then `1` and `r_shadows 0`, separately on GL and Vulkan | **Passed locally:** same-backend shared/classic TGAs match exactly; both runs own four lights and 14 global receiver surfaces with 28 primitives (14 draws plus 14 no-ops) and exact backend accounting | +| `capture-shared-interaction-stencil` | SP | controlled `sp-mv2-interaction` fixed camera, stock crate, projected test light, and point test light with the `stencil` preset, separately on GL and Vulkan | **Passed locally:** shared/classic TGAs match exactly on both backends; diagnostics reconcile nonzero casters and physical volumes; shadows-off deltas are RMS `5.5301` on GL and `5.5613` on Vulkan | +| `capture-shared-interaction-mapped` | SP | the same controlled scene with the `mapped` preset | **Passed locally:** both backends own projected and six-face point passes with exact record/backend reconciliation and exact classic image parity; shadows-off deltas are RMS `5.4817` on GL and `5.5625` on Vulkan. The synthetic projected light remains single-map, so multi-cascade qualification stays in the stock row below. | +| `capture-shared-interaction-mixed` | SP | the same controlled scene with point shadow maps disabled by the `mixed` preset | **Passed locally:** both backends combine projected mapped ownership with point-light stencil work, reconcile physical replay, match the classic TGAs exactly, and differ from shadows-off at RMS `5.5313` on GL and `5.5625` on Vulkan | +| `capture-shared-interaction-map-fallback` | SP | the same controlled scene with `map-budget-fallback`, which disables the static cache and permits only one update | **Passed locally:** GL and Vulkan each report one named backend fallback, zero committed primitive/shadow/volume/map/hybrid counters, no shared main-target draw, complete fallback coverage, and exact same-settings classic TGA parity | +| `capture-shared-interaction-stock-shadow` | SP | `interaction-shadow-stock`: stock-map qualification candidates for projected, point-cube at ordinary `game/airdefense2` spawn, CSM/parallel, dynamic mapped caster, perforated/cutout, same-light hybrid, and translucent-moment fallback | **Required stock acceptance:** retain the final six-component pose; exact projected/point/multi-cascade classes must be present; dynamic and cutout targets must explicitly report `features` dynamic/alpha; hybrid must report supplements plus physical volumes; translucent moments must fall back atomically; owned shared/classic TGAs must match and their shadowed/shadows-off pairs must materially differ | +| `capture-shared-fog-blend-owned` | SP | controlled `sp-mv2-fog-blend` stock-asset fixture on `maps/tools/mv2`, using shipped `lights/fog_generic` and deterministic `lights/fog_ambient` declarations with `r_rendererSharedWorldFogBlend 0` then `1`, separately on GL and Vulkan | **Passed locally:** exact same-backend engine-TGA parity on GL and Vulkan; each shared run owns one ready view, one fog and one blend light, six GLOBAL receivers, two active stages, seven primitives, three fog receivers, one cap, and three blend draws with exact backend reconciliation and zero mismatch/duplicate/untracked counters | +| `capture-shared-fog-blend-effect-deltas` | SP | the same fixed controlled scene with matching established `r_skipFogLights 1` and owned `r_skipBlendLights 1` references | **Passed locally:** mixed versus fog-only changes `1,921,110` RGB channels at RMS `36.7624` / maximum `71` on both backends; fog-only versus the outer phase skip changes `2,764,380` channels on GL and `2,764,369` on Vulkan at RMS `61.3216` / maximum `162` | +| `capture-shared-fog-blend-rollback` | SP | the same controlled stock-asset view with intentional `r_singleTriangle 1` admission blocker | **Passed locally:** `failure=unsupported-state detail=200`, one complete-phase fallback on the active backend, zero committed shared content counters, no shared main-target fog/blend draw, and exact same-settings classic engine-TGA parity on GL and Vulkan | +| `capture-shared-fog-authored-stock` | SP/MP | a fixed retained camera in an authored stock fog scene such as `game/storage2` or `mp/q4dm10` | **Required; pending:** exact same-backend classic/shared TGA parity, nonempty reconciled fog receiver/cap ownership, material fog-on/off delta, final six-component pose, and clean Vulkan validation results; this does not substitute for controlled blend-light qualification | +| `capture-shared-subview` | SP | fixed direct-mirror, dynamic mirror/reflection, x-ray, remote-camera, refraction, multi-level nested special-view chains, and qualifying 2D/cubemap color/depth captures with `r_rendererSharedSubview 0` then `1`, separately on GL and Vulkan | **Required; pending:** exact same-settings engine-TGA parity, nonzero direct/capture and root-tree ownership, plus named zero-commit malformed-semantic, target-aspect, cube-face, and descendant rollback. R6 qualifies only the single color-2D capture-backed mirror coupled to the dynamic tail in the next row; it does not qualify these broader forms. In-world GUI retains its separate tagged-subset boundary. | +| `capture-shared-cinematic-root` | SP | a frozen eligible root `videoMap`/`soundMap` view and an ordinary-root authored-post view with `r_rendererSharedCinematicPost 0` then `1`, separately on GL and Vulkan | **Required; pending:** exact classic/shared engine-TGA parity, fixed cinematic clock, nonzero root/stage/backend ownership, zero mismatch/duplicate counters, and named zero-commit timing/source/backend fallback. The R6 nested fixture is not root-view evidence. | +| `capture-shared-nested-cinematic-post` | SP | generated `maps/tools/milestone_d_nested_dynamic`: one stock-material capture-backed color-2D mirror plus a frozen stock ROQ and one `_currentRender` authored-post stage visible only in the child view | **Passed final controlled Windows GL/Vulkan acceptance:** all 12 cases pass; all four exact comparisons per API have RMS `0`, maximum delta `0`, and zero differing channels. Both-on normal owns one mirror and one nested cinematic/post transaction on the active backend, with cinematic owned/fallback/mismatch/duplicate `1/0/0/0` and subview owned/fallback `1/0`; both-on skip reports `nestedCinematicPostFallback` with `0/1/0/0` and `0/1`; the inactive backend remains all-zero and independent-CVar cases remain classic-equivalent. Normal versus skip changes 46,359 channels at RMS `2.0073` / maximum `72` on GL and 3,329 channels at RMS `0.2506` / maximum `18` on Vulkan. `currentDepth=0`, so `_currentDepth` remains pending. | +| `capture-shared-render-demo` | SP | a retained stock render-demo with `r_rendererSharedSpecialFrame 0` then `1` | **Required; pending:** exact classic/shared engine-TGA parity, active-session/demo-version provenance, complete view ownership, and named zero-commit incomplete-session/source/backend fallback | +| `capture-shared-raven-special-frame` | SP | fixed Raven blur-only, AL-only, and combined blur/AL controller cases | **Required; pending:** exact classic/shared engine-TGA parity, exact nonempty controller mask, all-effects completion before ownership, and named zero-commit resolve/effect/backend fallback | | `capture-renderer-visible-selftest` | safe startup | `rendererModernVisibleSelfTest` | synthetic modern-visible depth/G-buffer/deferred/forward+/hybrid-scene/present composition with shadow-policy handoff | | `capture-renderer-compatibility-selftest` | safe startup | `rendererModernCompatibilitySelfTest` | known fallback inventory for GUI/post/subview/render-demo/BSE categories | | `capture-sp-airdefense1-static` | SP | `game/airdefense1` fixed spawn, no input for 3 seconds | outdoor lighting, terrain decals, BSE smoke, and stock material parity | +The nested Milestone D case is generated and retained only under `.tmp`; it is +not distributable game content. The fixture tool hash-verifies and retrieves +the user's extracted retail `maps/tools/mv2.map` and `video/idlogo.roq`, appends +original validation-only geometry/material text, compiles `.proc`/`.cm` with +openQ4's `dmap`, and stages an isolated runtime below `.tmp/stock-runtime`. +No retail map, ROQ, or generated fixture is committed or shipped. The map is a +visual renderer fixture with no AI actors; missing renamed AAS files are an +explicit non-navigation warning allowance for this case, not warning-clean +release evidence. + +The final controlled evidence is the R6 acceptance report at +`.tmp/renderer-milestone-d/acceptance-20260823-r6-all/renderer_milestone_d_acceptance_report.json` +(SHA-256 +`b49031c48267fbfa86c295082707525a390d6090c24f600d00644237a67b252a`), +bound to fixture manifest +`.tmp/renderer-milestone-d/qualification-20260823-r6/fixture_manifest.json` +(SHA-256 +`24a2a4926f4b1263e311ad762bf40d391b33de225732d12154ebd7625705282d`) +and runtime +`.tmp/stock-runtime/milestone-d-qualification-20260823-r6`, whose manifest +SHA-256 is +`dbece44597ec5e0686eed215f6ae99c5d6ba4cdd959c1efa7c4acaf1bc2ca673`. +The 3,884-file, 7,281,560,043-byte stock dependency inventory remained +immutable under seal +`13abad18f70eb8b4bf6ea0e9697b317718bb065ee76f86070787005b045dda7a`; +the eight-file, 25,330,583-byte fixture evidence inventory remained immutable +under seal +`0572fb8d9c132f7e97060345464f97750e59e69819871fce90eed13d1348d8ae`. +Runtime, fixture, and stock verification failures were empty. The matching +foundation report at +`.tmp/renderer-validation/milestone-d-foundation-20260823-r6/renderer_validation_report.json` +(SHA-256 +`0659f1f2ee5108a422cd6c41bdc78809731f25ad7eda4b5ee84328a9163ce7ac`) +passed; its accompanying Markdown has SHA-256 +`a619169ee97f2d6e8c7dfab991adbab5b5b282fd2e1197e2d59eb3609c87cddf`. +The fixed capture script disabled input and used actual windowed mode `-1` at +1280x720, pacing-only sampling, GPU timers off, and 1280x720 +engine-render-target TGA screenshots. +This is controlled development-runtime evidence, not final-package, human-review, +non-Windows, broad subview/in-world-GUI/special-frame, root cinematic/post, or +`_currentDepth` qualification. + +```powershell +python tools/tests/renderer_milestone_d_fixture.py ` + --output-dir .tmp/renderer-milestone-d/ ` + --runtime-dir .tmp/stock-runtime/ +python tools/tests/renderer_milestone_d_acceptance.py ` + --runtime-dir .tmp/stock-runtime/ ` + --fixture-manifest .tmp/renderer-milestone-d//fixture_manifest.json ` + --render-api all ` + --output-dir .tmp/renderer-milestone-d/ +``` + ## RenderDoc Tier Checklist Capture with `r_rendererMetrics 2`, `r_rendererGpuTimers 1` when available, and the matching forced tier. Every capture should show named debug scopes and object labels for graph resources, modern executor buffers/programs, and pass-owned FBOs. @@ -176,6 +415,56 @@ These are manual long-run sign-off loops. They are intentionally outside the saf | `modern` | 16 ms | 24 ms | 100% | 8x6x16 | 96 | 64 | 1024 px / every frame | 2 | | `high-end` | 12 ms | 18 ms | 100% | 8x6x16 | 128 | 96 | 2048 px / every frame | 3 | +These preset values are renderer workload/scalability defaults, not per-map +measurements. Promotion evidence uses the separate, versioned +`tools/validation/renderer_per_map_budgets.json` contract. Each row is selected +by the exact active map, launch-derived backend (`opengl` or `vulkan`), and +benchmark profile, and carries minimum sample counts plus independent CPU and +whole-frame GPU P95/P99 ceilings in integer microseconds. The initial v1 rows +deliberately apply the baseline 20/28 ms target to every listed stock scene on +both backends. They are cross-map target ceilings, not a claim that every row +was measured at those values; a captured report retains each row's actual +samples and percentiles so confirmed target-machine results can tighten a row +without changing the schema. + +`rendererBenchmarkCapture` supplies the budget tools with one backend-neutral +line of this exact shape: + +```text +OPENQ4_FRAME_TIMING_V1 map=game/storage1 backend=opengl profile=baseline cpuSamples=256 cpuP50Us=7000 cpuP95Us=11000 cpuP99Us=14000 gpuAvailable=1 gpuSamples=252 gpuP50Us=5000 gpuP95Us=8000 gpuP99Us=10000 +``` + +CPU and GPU percentiles must be monotonic. GPU values come from nonblocking +whole-frame backend timestamps rather than a sum of overlapping pass timers. +An unsupported/unresolved backend reports `gpuAvailable=0`, zero samples, and +`-1` for all three GPU percentiles; because promotion rows require GPU timing, +that representation fails closed. The verifier rejects malformed lines, +multiple markers in one source, conflicting mirrored sources, identity or +sample-count mismatches, missing exact budget rows, exceeded ceilings, changed +contract hashes, altered runtime/artifact bytes, and recorded measurements that +do not recompute from the retained log streams. + +### Milestone A current-build evidence snapshot + +The 2026-08-19 development snapshot verifies that the implementation works as +an integrated system, but it is not final-package or universal performance +evidence: + +| Gate | Current development-build result | +|---|---| +| Job parity | PASS: jobs-on and jobs-off `game/storage1` produced identical engine TGA bytes and matching game-state evidence. | +| Repeated lifecycle | PASS: jobs-on/off OpenGL and jobs-on Vulkan completed the five-map campaign through `game/tram1`; each run ended with zero initialized, queued, or running jobs. | +| Dedicated lifecycle | PASS: five independent dedicated-server runs exited normally, each with one synchronous job self-test and one clean shutdown marker. | +| Backend timing | PASS for the exercised storage and campaign captures: OpenGL and Vulkan returned delayed whole-frame GPU samples without a current-frame wait. | +| Retail baseline | PASS locally: the schema-10 four-role OpenGL capture and its immediate replay retained pure MP, `ui_autoJoin 1`, canonical display/budget evidence, artifacts, and source/runtime identities; engine screenshots and the save preview also passed local human review. | +| Required map budgets | PASS locally: the immutable `milestone-a-20260819-final3` runtime passes and replay-verifies all eight OpenGL cases in `ma-a-gl3` and all eight Vulkan cases in `ma-a-vk3`. The corrected `game/medlabs` debug-context run records zero GL errors after depth bounds are ordered and clamped before submission. | + +The renderer sweep uses the immutable current-build runtime named above, staged +from an uncommitted development tree. Promotion still requires clean committed +source provenance, a freshly staged final package, retained reports/artifacts +and release review, and separate platform/driver qualification. The local 8/8 +results do not establish a universal performance level. + ## Manual Gameplay Matrix Gameplay validation remains mandatory before renderer release sign-off, but it is not run by the safe matrix by default because map loads need target-hardware supervision. Use the SP launch task for single-player maps, the MP launch task or `tools\debug\start_listen_server_client.ps1` for multiplayer, or the opt-in gameplay benchmark harness below when you want a repeatable logged capture set. @@ -188,7 +477,11 @@ Gameplay validation remains mandatory before renderer release sign-off, but it i | `sp-storage2` | SP | `game/storage2` | indoor materials and post-process coverage | | `sp-bse-heavy` | SP | `game/medlabs` | stress BSE effects without replacement content | | `sp-cinematic-subview` | SP | `game/mcc_landing` | subviews, remote cameras, cinematic and GUI interaction | +| `sp-mv2-ambient` | SP | `maps/tools/mv2` | controlled stock fixed-function world-ambient ownership and whole-view rollback evidence | +| `sp-mv2-interaction` | SP | `maps/tools/mv2` | controlled stock unshadowed, stencil, projected/point mapped, mixed map/stencil, and map-budget whole-view fallback evidence | +| `sp-mv2-fog-blend` | SP | `maps/tools/mv2` | locally passed controlled stock-declaration fog, blend, mixed-phase ownership, visible effect-delta, and complete-phase rollback evidence; authored-stock and release promotion remain open | | `mp-q4dm1-listen` | MP | `mp/q4dm1` | listen-server and local-client MP parity | +| `mp-q4dm9-listen` | MP | `mp/q4dm9` | default-off load-cache timing and forced Vulkan shadow-ownership fallback regression | For each gameplay case, validate the matrix variants that the hardware supports: @@ -198,41 +491,94 @@ For each gameplay case, validate the matrix variants that the hardware supports: | renderer escape | `r_renderer best`, `r_renderer arb2`, `r_glTier legacy` | | `r_swapInterval` | `0`, `1` | | `com_maxfps` | `120`, `240`, `0` | +| GPU animation | paired `r_gpuSkinning 0` CPU reference and `r_gpuSkinning 1` exact-contract run on each backend; require admitted vertices and a classified CPU stencil fallback | | display mode | windowed, fullscreen | | renderer diagnostics | `r_rendererMetrics 1`, `r_rendererMetrics 2`, `r_rendererModernAutoPromote 0`, and one signed `r_rendererModernAutoPromote 1` candidate run with the complete `r_rendererPromotionEvidence` token after the other rows are clean | After each gameplay smoke, inspect the configured log file under `fs_savepath\\logs\openq4.log` or the case-specific log emitted by the launch tool. Fix errors and warnings, then repeat the loop until the case is clean. +Milestone C GPU-animation captures follow the stricter backend-local A/B +procedure in [Shared Renderer Contracts and GPU Animation](gpu-skinning-modernization.md): +freeze the pose from launch with `g_stopTime 1`, compare each GPU run only with +its own backend's CPU reference, use engine-written screenshots, retain pure +auto-joined MP evidence, and repeat the animation-heavy +`evaluateMPPerformance` workload at least three times. CPU-frame improvement is +not accepted unless the diagnostics prove that eligible vertices were actually +admitted and every fallback has a named reason. +Validate each retained backend-local CPU/GPU JSON pair with the +`tools/validation/gpu_skinning_evidence.py` verifier at the default one-percent +CPU-P95 gate; the complete command, launch identity, and artifact contract is +documented in the GPU-animation guide. + ## Gameplay Benchmark Harness -`tools\tests\renderer_gameplay_benchmark.py` is the Phase 12 map-loading runner. It launches the staged client from `.install`, uses isolated save paths under `.tmp\renderer-gameplay\`, enters SP maps or an MP listen server plus loopback client, waits for streaming, runs a fixed static spawn camera path unless a case is later extended with authored poses, captures screenshots, emits `rendererBenchmarkCapture`, `framePacingSnapshot`, and `gfxInfo`, and writes Markdown/JSON reports. +`tools\tests\renderer_gameplay_benchmark.py` is the Phase 12 map-loading runner. It launches the staged client from `.install` or a named ordinary package below `.tmp\stock-runtime\`, uses isolated save paths under `.tmp\renderer-gameplay\`, enters SP maps or a pure MP listen server plus auto-joining loopback client, waits for streaming, runs a fixed static spawn camera path unless a case is later extended with authored poses, captures screenshots, emits `rendererBenchmarkCapture`, `framePacingSnapshot`, and `gfxInfo`, and writes Markdown/JSON reports. The alternate root deliberately matches `stage_fast_install.py --temporary-runtime` and the stock-baseline harness, so one immutable staged package can be hashed and exercised by both evidence tools. The report binds the selected runtime path, executable, every runtime-file hash, current Git state, budget-contract id/hash, launch-derived backend, thresholds, and measured CPU/GPU percentiles. + +Capture output directories must be new or empty. The runner hashes the complete +runtime before launch and again after every case; any package mutation fails the +run, while later `--verify-report` replay requires the same runtime and retained +log/stdout/stderr/screenshot bytes. Generated configs, caches, and screenshots +stay below each role's isolated save path rather than writing into the staged +package. + +Per-map CPU/GPU budget evidence has one comparable display contract: +`r_fullscreen 0`, `r_borderless 0`, `r_borderlessDefaultMigrated 1`, +`r_fullscreenDesktop 0`, and a 1280x720 drawable selected through both +`r_windowWidth`/`r_windowHeight` and `r_mode -1` plus `r_customWidth`/ +`r_customHeight`. The harness applies those startup values after optional +launch CVars, records the exact contract in every result, and replay rejects a +different size, border policy, or fullscreen result. A passing role must also +contain `gfxInfo` runtime evidence equivalent to `MODE: -1, 1280 x 720 +windowed`, and its engine-written TGA must decode at exactly 1280x720; replay +recomputes both from the hashed artifacts instead of trusting report fields. +`--width` and `--height` +are available for pacing-only diagnostics; non-pacing budget runs reject any +size other than 1280x720. Fullscreen coverage remains a presentation/pacing +test and is not promotable CPU/GPU budget evidence. The runner uses the SP/MP `g_autoExecAfterMapLoad` hook to execute its generated cfg after the map is active, not during loading UI. Renderer metrics are enabled only inside the gameplay capture window, which keeps load-screen logs quiet while still producing benchmark samples, GPU timing where available, frame-pacing output, and a screenshot artifact. -Use `--pacing-only` for high-FPS acceptance after a diagnostic metrics pass is already clean. This keeps `r_rendererMetrics`, GL timer queries, and the FPS overlay out of the timed window, still emits `framePacingSnapshot`, and can fail the run with parsed thresholds such as `--min-pacing-hz 120 --max-p95-ms 12`. The `game/storage1` acceptance run should start sampling two seconds after the active map draw with `r_swapInterval 0` and `com_maxfps 0` so the result measures renderer throughput rather than the old low-FPS plan cap. +Use `--pacing-only` for high-FPS presentation acceptance after a diagnostic metrics pass is already clean. This keeps renderer metrics, GPU timestamps, and the FPS overlay out of the timed window, still emits `framePacingSnapshot`, and can fail the run with presentation-only thresholds such as `--min-pacing-hz 120 --max-p95-ms 12`. A pacing-only report explicitly records that per-map CPU/GPU budgets were not enforced and cannot pass budget-evidence replay. The `game/storage1` acceptance run should start sampling two seconds after the active map draw with `r_swapInterval 0` and `com_maxfps 0` so the result measures renderer throughput rather than the old low-FPS plan cap. Common runs: ```powershell python tools\tests\renderer_gameplay_benchmark.py --list python tools\tests\renderer_gameplay_benchmark.py --profile smoke +python tools\tests\renderer_gameplay_benchmark.py --profile smoke --render-api gl --runtime-dir .tmp\stock-runtime\current-build +python tools\tests\renderer_gameplay_benchmark.py --profile smoke --render-api vk --runtime-dir .tmp\stock-runtime\current-build python tools\tests\renderer_gameplay_benchmark.py --profile smoke --pacing-only --autoexec-delay-ms 2000 --min-pacing-hz 120 --max-p95-ms 12 python tools\tests\renderer_gameplay_benchmark.py --profile required python tools\tests\renderer_gameplay_benchmark.py --profile campaign-split-state-transition --timeout 360 +python tools\tests\renderer_gameplay_benchmark.py --profile world-ambient --pacing-only --no-gpu-timers +python tools\tests\renderer_gameplay_benchmark.py --profile fog-blend --pacing-only --no-gpu-timers +python tools\tests\renderer_gameplay_benchmark.py --profile interaction --render-api gl --pacing-only --no-gpu-timers --reference-dir .tmp\renderer-references\interaction\gl\classic\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\renderer-references\interaction\gl\shadows-off\savepaths +python tools\tests\renderer_gameplay_benchmark.py --profile interaction --render-api vk --pacing-only --no-gpu-timers --reference-dir .tmp\renderer-references\interaction\vk\classic\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\renderer-references\interaction\vk\shadows-off\savepaths +python tools\tests\renderer_gameplay_benchmark.py --profile interaction-shadow-stock --render-api gl --pacing-only --no-gpu-timers --reference-dir .tmp\renderer-references\interaction-shadow-stock\gl\classic\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\renderer-references\interaction-shadow-stock\gl\shadows-off\savepaths +python tools\tests\renderer_gameplay_benchmark.py --profile interaction-shadow-stock --render-api vk --pacing-only --no-gpu-timers --reference-dir .tmp\renderer-references\interaction-shadow-stock\vk\classic\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\renderer-references\interaction-shadow-stock\vk\shadows-off\savepaths +python tools\tests\renderer_gameplay_benchmark.py --profile deform --render-api gl --pacing-only --no-gpu-timers +python tools\tests\renderer_gameplay_benchmark.py --profile deform --render-api vk --pacing-only --no-gpu-timers +python tools\tests\renderer_gameplay_benchmark.py --profile smoke --cases mp-q4dm9-listen --render-api vk --shadow-presets mapped --pacing-only --no-gpu-timers python tools\tests\renderer_gameplay_benchmark.py --profile tiers -python tools\tests\renderer_gameplay_benchmark.py --profile presentation +python tools\tests\renderer_gameplay_benchmark.py --profile presentation --pacing-only python tools\tests\renderer_gameplay_benchmark.py --profile shadows +python tools\tests\renderer_gameplay_benchmark.py --runtime-dir .tmp\stock-runtime\current-build --verify-report .tmp\renderer-gameplay\candidate\renderer_gameplay_benchmark_report.json ``` -The runner fails a case when the process times out, no gameplay screenshot is produced, the benchmark/gfxInfo lines are missing, image comparison fails when references are required, or renderer warning markers such as `idStr::snPrintf: overflow`, `WARNING: idStr`, shader compile failures, program link failures, or fatal OpenGL startup failures appear in the log. +The runner fails a case when the process times out, no gameplay screenshot is produced, the benchmark/gfxInfo/timing lines are missing, the exact map/backend/profile budget is absent, required CPU/GPU samples are unavailable or over budget, image comparison fails when references are required, or renderer warning markers such as `idStr::snPrintf: overflow`, `WARNING: idStr`, shader compile failures, program link failures, or fatal graphics startup failures appear in the log. MP benchmark roles always use `ui_autoJoin 1`, `si_pure 1`, and `net_serverAllowServerMod 0`. | Profile | Coverage | |---|---| | `smoke` | bounded `game/storage1` SP gameplay smoke with screenshot, metrics, frame-pacing snapshot, and zero-warning log gates | | `required` | `game/storage1`, `game/airdefense1`, `game/airdefense2`, `game/storage2`, `game/medlabs`, `game/mcc_landing`, and `mp/q4dm1` listen server plus local client | | `campaign-split-state-transition` | triggers the real SP end-level targets from `game/mcc_2` through `game/storage1 first`, `game/storage2`, `game/storage1 second`, and into `game/tram1`, asserting the active `si_entityFilter` after each load | +| `world-ambient` | controlled bordered 1280x720 `maps/tools/mv2` stock capture; launch sets `ui_showGun 0`, `g_showHud 0`, and `r_multiSamples 0`, then normal spawn runs `noclip` and `setviewpos 0 0 256 80 0 0`; the exact post-map isolation set is recorded in the world-ambient guide and keeps ambient/deform/render enabled while disabling direct-light, subview, light-grid, player-overlay, portal-fade, cel, and debug islands through their structural controls. It does not set `r_skipPostProcess` or `r_skipGuiShaders`. Run with `--pacing-only --no-gpu-timers`, so it is not CPU/GPU budget evidence. | +| `deform` | controlled bordered 1280x720 `maps/tools/mv2` capture with the same fixed camera, the shipped `shaderDemos/move` material override, `r_rendererSharedWorldAmbient 1`, and `r_rendererSharedDeform 1`. The profile starts game time frozen, advances the common settle interval at exactly one game tic per rendered frame, freezes before sampling, and disables mouse input so separate runs retain the same deform value and camera. Separate same-backend classic references must match exactly with nonzero completed deform ownership; an otherwise identical `r_skipDeforms 1` reference must differ, and an unsupported/skipped/failure case must report named zero-commit rollback. Run with `--pacing-only --no-gpu-timers`; it is visual/ownership evidence, not budget evidence. | +| `fog-blend` | controlled bordered 1280x720 `maps/tools/mv2` qualification using only shipped fog and deterministic blend-light declarations, fixed camera/settings, separate GL/Vulkan classic references, positive established `r_skipFogLights` outer-skip and owned `r_skipBlendLights` effect references, exact ownership reconciliation, and a named complete-phase fallback. The development-worktree set passes on both backends. Run with `--pacing-only --no-gpu-timers`; it is visual/ownership evidence, not CPU/GPU budget evidence. Exact results and remaining release gates are recorded in the fog/blend domain guide. | +| `interaction` | controlled bordered 1280x720 `maps/tools/mv2` capture with fixed camera `0 -192 96 20 90 0`, a stock `crate1_small` caster, a stock `crate1_medium` receiver, a projected `testLight`, and a side `testPointLight`; the separated receiver makes stencil and mapped silhouettes materially visible. Five presets cover unshadowed, stencil, projected+point mapped, projected-map+point-stencil mixed, and constrained map-update fallback. Run with `--pacing-only --no-gpu-timers`; require exact same-settings classic references plus the matching unshadowed difference reference. The synthetic projected light remains single-map under the CSM preset, so multi-cascade acceptance stays in the stock profile. | +| `interaction-shadow-stock` | stock-map qualification candidates for projected, point, CSM/parallel, dynamic mapped-caster, perforated/cutout, same-light hybrid-supplement, and translucent-moment fallback coverage. Each run starts frozen at tic zero, hides the unsupported first-person viewmodel, advances the real SP scene through the settle interval, freezes before sampling, and records the final six-component pose. Labels never establish coverage: the parser requires an exact projected class, per-map class/cascade/alias/plan/generation/caster/hash data, and `features=static+dynamic+alpha+translucent`; dynamic and perforated cases fail without their explicit feature bit. | | `tiers` | forced `r_glTier auto`, `legacy`, `gl33`, `gl41`, `gl43`, `gl45`, and `gl46` gameplay probes | -| `presentation` | `r_swapInterval 0/1`, `com_maxfps 0/120/240`, windowed, and fullscreen coverage for uncapped/high-refresh validation | +| `presentation` | pacing-only `r_swapInterval 0/1`, `com_maxfps 0/120/240`, windowed, and fullscreen coverage for uncapped/high-refresh validation; never budget-promotion evidence | | `shadows` | stencil fallback, mapped shadows, CSM, translucent moments, and debug-overlay modes `1..6` over the shadow correctness scenes | Optional deterministic image comparison uses TGA references: @@ -240,6 +586,26 @@ Optional deterministic image comparison uses TGA references: python tools\tests\renderer_gameplay_benchmark.py --profile smoke --reference-dir .tmp\renderer-references --require-references ``` +The two interaction acceptance commands above are intentionally stricter than +the generic optional comparison: `--require-references` binds every capture to +its same-settings shared-off/classic TGA at exact RMS/max delta zero, while +`--difference-reference-dir` binds each owned shadow case to the matching +unshadowed case id and requires a material shadow delta. Generate those two +reference trees in separate shared-off and shadows-off runs before executing an +acceptance command. Generate and consume separate references for each renderer; +replace `` with `interaction` or `interaction-shadow-stock` and +`` with `gl` or `vk` in this three-run workflow: + +```powershell +python tools\tests\renderer_gameplay_benchmark.py --profile --render-api --pacing-only --no-gpu-timers --set-cvar r_rendererSharedWorldInteraction=0 --output-dir .tmp\renderer-references\\\classic +python tools\tests\renderer_gameplay_benchmark.py --profile --render-api --shadow-presets unshadowed --pacing-only --no-gpu-timers --set-cvar r_rendererSharedWorldInteraction=0 --output-dir .tmp\renderer-references\\\shadows-off +python tools\tests\renderer_gameplay_benchmark.py --profile --render-api --pacing-only --no-gpu-timers --reference-dir .tmp\renderer-references\\\classic\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\renderer-references\\\shadows-off\savepaths +``` + +The reference roots include `savepaths` because each generated capture lives at +`\savepaths\\baseoq4\screenshots\...`. Omitting either +tree is not a passing interaction result. + Nondeterministic BSE, cinematic, and MP scenes need human review in addition to the automated log/screenshot gates: | Case | Focus | Checks | @@ -247,6 +613,7 @@ Nondeterministic BSE, cinematic, and MP scenes need human review in addition to | `sp-bse-heavy` | BSE-heavy effects in `game/medlabs` | effect sprites/trails animate at the expected cadence, no black quads, no missing additive passes, no warning spam | | `sp-cinematic-subview` | cinematic/subview flow in `game/mcc_landing` | remote-camera/subview content is visible, GUI overlays composite in the right order, cinematic handoff keeps frame pacing stable | | `mp-q4dm1-listen` | local MP listen server plus loopback client | client reaches the map, player/world lighting matches host expectations, frame pacing remains uncapped when requested | +| `mp-q4dm9-listen` | local MP q4dm9 load and Vulkan mapped-shadow fallback | ordinary runs force `com_levelLoadModernization 0` and must show no generated-cache writes; explicit cache-on A/B evidence records map phases. A focused run with `r_vkShadowFallbackTest 1` must retain a lit engine TGA, log `affected light receivers fall back unshadowed`, omit the former `receivers are skipped fail-closed` diagnostic, and remain free of Vulkan validation/VUID/call failures. | ## Shadow Correctness Matrix @@ -269,3 +636,7 @@ Nondeterministic BSE, cinematic, and MP scenes need human review in addition to - Benchmark captures report P50/P95/P99 frame pacing, active preset budgets, and threshold pass/fail status before any claim that the modern visible path matches or beats ARB2 on target scenes. - `rendererDefaultSafetySelfTest` and `rendererDefaultPromotionSelfTest` pass before any default-promotion discussion. - `r_rendererModernAutoPromote 1` is used only with the complete `r_rendererPromotionEvidence` token after the default-promotion criteria pass; `r_renderer arb2`, `r_glTier legacy`, and the modern-disable cvar set remain documented rollback paths. +- Shared world-ambient local runtime qualification passed: retained GL and Vulkan option-off/on engine screenshots for the same eligible stock `maps/tools/mv2` view match at RMS `0` / maximum delta `0`, enabled diagnostics report one owned pre-fog draw with stable domain/view hashes, and the stock `shaderDemos/move` deform override reports zero shared draws plus the same named complete-view fallback on both backends. Vulkan validation gates are clean. Exact directories and SHA-256 values are recorded in [Shared Classic World Ambient/Material Domain](classic-world-ambient-domain-modernization.md). The domain is implemented and remains default-off pending clean committed-package and target-platform/driver promotion evidence. +- Shared interaction's local qualification passed for the current controlled corridor. Its five-case profile passed 5/5 on GL and 5/5 on Vulkan with exact same-backend classic/shared TGAs, four lights, 14 global receiver surfaces, material shadows-on/off deltas for stencil/mapped/mixed ownership, exact backend accounting, and named map-admission fallbacks with zero committed shared counters and no shared main-target draw. The earlier two-light/four-surface capture is superseded. The stock-map row remains a release-qualification gate: its projected/CSM/dynamic/perforated/hybrid candidates need retained final cameras and fresh classic/shadows-off references before they can pass. Exact scope and deltas are recorded in [Shared Classic Interaction-Lighting Domain](classic-interaction-domain-modernization.md); the option is implemented and remains default-off pending stock, clean committed-package, and target-platform/driver evidence. +- Shared fog/blend's controlled development-worktree qualification passes on GL and Vulkan. Mixed and fog-only shared TGAs match their same-settings classic references exactly; the mixed run owns one fog plus one blend light, six GLOBAL receivers, two stages, seven primitives, three fog receivers, one cap, and three blend draws with exact backend accounting. Mixed/fog-only and fog-only/outer-skip comparisons materially prove the blend and fog contributions, and the intentional blocker produces named zero-commit atomic fallback with exact classic parity. Vulkan warning/VUID/call-failure counters remain clean. Exact hashes, deltas, and local report directories are recorded in [Shared Classic Fog/Blend Domain](classic-fog-blend-domain-modernization.md). Authored-stock fog, clean committed-package recapture, human review, and target-platform/driver retention remain promotion gates. +- Shared material-deform's controlled development-worktree qualification passes on GL and Vulkan. Normal and skipped shared TGAs match their same-settings classic references exactly; normal runs emit two completed records and one owned ambient draw, skipped runs emit two named skipped records and commit zero shared draws, and the normal/skipped comparison materially proves the deform contribution. Native/static and backend diagnostic gates are clean. Exact hashes, deltas, and local report directories are recorded in [Shared Classic Material-Deform Contract](classic-deform-domain-modernization.md). Clean committed-package recapture, human review, and target-platform/driver retention remain promotion gates. diff --git a/docs/dev/settings-menu-registry.json b/docs/dev/settings-menu-registry.json index 46a56b88..2cc95d42 100644 --- a/docs/dev/settings-menu-registry.json +++ b/docs/dev/settings-menu-registry.json @@ -914,7 +914,7 @@ "widget_type": "bind", "label_key": "#str_200129", "target_type": "bind", - "target": "voteyes", + "target": "_impulse28", "default": "F1", "default_keys": [ "F1" @@ -936,7 +936,7 @@ "widget_type": "bind", "label_key": "#str_200130", "target_type": "bind", - "target": "voteno", + "target": "_impulse29", "default": "F2", "default_keys": [ "F2" @@ -980,7 +980,7 @@ "widget_type": "bind", "label_key": "#str_201009", "target_type": "bind", - "target": "ready", + "target": "_impulse17", "default": "F3", "default_keys": [ "F3" diff --git a/docs/dev/source-provenance-manifest.json b/docs/dev/source-provenance-manifest.json new file mode 100644 index 00000000..e3d8a8fc --- /dev/null +++ b/docs/dev/source-provenance-manifest.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "auditedOn": "2026-08-19", + "scope": "Tracked source files under src/ whose retained id Software header identifies a covered source family", + "textHashNormalization": "UTF-8; CRLF and CR normalized to LF", + "families": { + "doom3": { + "displayName": "Doom 3 GPL Source Code", + "headerMarker": "Doom 3 GPL Source Code", + "expectedFileCount": 581, + "officialRepository": "https://github.com/id-Software/DOOM-3", + "auditedCommit": "a9c49da5afb18201d31e3f0a429a037e56ce2b9a", + "officialCopying": "https://github.com/id-Software/DOOM-3/blob/a9c49da5afb18201d31e3f0a429a037e56ce2b9a/COPYING.txt", + "officialCopyingSha256": "f83520e077c35722a56889cd54da8e46105e31278f8f97fa39318957e865c5c7", + "localAdditionalTerms": "LICENSES/DOOM-3-ADDITIONAL-TERMS.txt", + "localAdditionalTermsSha256": "0745111e721bad901aa67e16af761f35598e2d92f971a134ef732d3519b09a3e" + }, + "doom3_bfg": { + "displayName": "Doom 3 BFG Edition GPL Source Code", + "headerMarker": "Doom 3 BFG Edition GPL Source Code", + "expectedFileCount": 37, + "officialRepository": "https://github.com/id-Software/DOOM-3-BFG", + "auditedCommit": "1caba1979589971b5ed44e315d9ead30b278d8b4", + "officialCopying": "https://github.com/id-Software/DOOM-3-BFG/blob/1caba1979589971b5ed44e315d9ead30b278d8b4/COPYING.txt", + "officialCopyingSha256": "1689ee84a23d3985c7478b10080f45d10fef6f2945ddd2b67777d1990defe5c8", + "localAdditionalTerms": "LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt", + "localAdditionalTermsSha256": "de40806c9c1ab3cd2c180cc18cdc37c9b896f3e4ea4818739baad78a0abc45b2", + "officialPathRule": "src/ maps to neo/, except src/imagetools/ maps to neo/renderer/", + "intermediateSources": { + "rbdoom3_bfg": { + "repository": "https://github.com/RobertBeckebans/RBDOOM-3-BFG", + "auditedCommit": "ea29c006e84fedcc0e7c173c383a087ce0a8c0d5", + "pathOverrides": { + "src/sound/OpenAL/AL_SoundHardware.cpp": "neo/sound/OpenAL/AL_SoundHardware.cpp", + "src/sound/OpenAL/AL_SoundHardware.h": "neo/sound/OpenAL/AL_SoundHardware.h", + "src/sound/OpenAL/AL_SoundSample.cpp": "neo/sound/OpenAL/AL_SoundSample.cpp", + "src/sound/OpenAL/AL_SoundSample.h": "neo/sound/OpenAL/AL_SoundSample.h", + "src/sound/OpenAL/AL_SoundVoice.cpp": "neo/sound/OpenAL/AL_SoundVoice.cpp", + "src/sound/OpenAL/AL_SoundVoice.h": "neo/sound/OpenAL/AL_SoundVoice.h" + } + } + } + } + } +} diff --git a/docs/dev/source-provenance.md b/docs/dev/source-provenance.md new file mode 100644 index 00000000..f4a0d565 --- /dev/null +++ b/docs/dev/source-provenance.md @@ -0,0 +1,67 @@ +# Source Provenance and Accompanying Terms + +This is the authoritative engineering inventory for retained id Software source headers under openQ4's `src/` tree. It records what the tree says about its origin and makes the accompanying notices available; it is not a legal opinion or a conclusion about licence compatibility. + +The repository-wide [GPLv3 text](../../LICENSE) remains the primary licence file. Two sets of tracked files also retain headers that expressly refer to a distinct set of Additional Terms: + +| Header family | Tracked files at the 2026-08-19 audit | Audited official source snapshot | Accompanying terms in this tree | +|---|---:|---|---| +| Doom 3 GPL Source Code | 581 | [id-Software/DOOM-3 `a9c49da`](https://github.com/id-Software/DOOM-3/tree/a9c49da5afb18201d31e3f0a429a037e56ce2b9a) | [`LICENSES/DOOM-3-ADDITIONAL-TERMS.txt`](https://github.com/themuffinator/openQ4/blob/master/LICENSES/DOOM-3-ADDITIONAL-TERMS.txt) | +| Doom 3 BFG Edition GPL Source Code | 37 | [id-Software/DOOM-3-BFG `1caba19`](https://github.com/id-Software/DOOM-3-BFG/tree/1caba1979589971b5ed44e315d9ead30b278d8b4) | [`LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt`](https://github.com/themuffinator/openQ4/blob/master/LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt) | + +The two Additional-Terms files preserve the wording published in the corresponding official `COPYING.txt`, including upstream spelling errors; only character encoding, line endings, and insignificant trailing whitespace are normalized for the repository. They are separate because their scope and published text identify different source releases. The machine-readable audit pins the official repositories, full commit object IDs, complete upstream `COPYING.txt` hashes, and canonical UTF-8 local notice hashes in [`source-provenance-manifest.json`](https://github.com/themuffinator/openQ4/blob/master/docs/dev/source-provenance-manifest.json). The local hash normalizes CRLF/CR to LF so Git's checkout policy cannot create a false mismatch; all other text changes fail. + +The official Doom 3 BFG repository describes its release as GPL source with omissions for Steam integration, Bink playback, and the depth-fail stencil-shadow implementation. Those omissions are a boundary on what can be copied from that release; they are not an invitation to reconstruct excluded code from non-source binaries. Quake 4 retail assets are not included in either source release and are not relicensed by openQ4. + +## Milestone B loading/cache reference boundary + +The level-load manifest/envelope format, bounded read/framing pipeline and +immutable DTO, cache manager and filesystem substitution, render-model payloads, +and collision-model payload are original openQ4 designs and implementations. No +cache format, serializer, validation algorithm, pipeline code, or +collision/model codec was copied or adapted from Doom 3 BFG. + +During the render-world payload work, the official GPLv3 +[`neo/renderer/RenderWorld_load.cpp` at `1caba1979589971b5ed44e315d9ead30b278d8b4`](https://github.com/id-Software/DOOM-3-BFG/blob/1caba1979589971b5ed44e315d9ead30b278d8b4/neo/renderer/RenderWorld_load.cpp) +was consulted only as a field inventory for the finalized classic proc-world CPU +representation: inline world models, inter-area portals, and area BSP nodes. +The openQ4 wire format, bounds, graph and finite-value checks, transaction, +source-authoritative envelope, failure behavior, and finalization integration +were written independently; no BFG code or algorithm was copied or adapted. +Because no BFG-headered source file was added or changed for this work, the +audited Doom 3 BFG header-family inventory remains 37 files. + +## Reproducible inventory + +Run the offline audit from the repository root: + +```text +python tools/validation/audit_source_provenance.py --check +python tools/validation/audit_source_provenance.py --family doom3_bfg +python tools/validation/audit_source_provenance.py --format json --output .tmp/source-provenance.json +python tools/validation/audit_source_provenance.py --check --doom3-source E:\_SOURCE\_CODE\DOOM-3-master --doom3-bfg-source E:\_SOURCE\_CODE\DOOM-3-BFG-master --rbdoom3-bfg-source E:\_SOURCE\_CODE\RBDOOM-3-BFG-master +``` + +The JSON report is the exact current file inventory. The check fails when a tracked header referring to Additional Terms is unknown, either accompanying notice is missing or differs from its canonical local SHA-256, or a family count changes without an explicit manifest review. When official source roots are supplied, the audit also checks the raw published bytes of each complete `COPYING.txt` against its separate official hash; the UTF-8/newline normalization applies only to the extracted local notice files. A header-family match establishes only that the local file retains that notice. It does not claim byte identity with the pinned official snapshot or reconstruct the complete chain of intermediate forks. + +The 37 BFG-marked files currently occupy these groups: + +| Classification | Files | Audited path source | +|---|---:|---| +| Path exists in official Doom 3 BFG snapshot `1caba19` | 31 | `neo/`; local `src/imagetools/` corresponds to official `neo/renderer/` | +| Path absent from the official snapshot; BFG-header lineage is present in RBDOOM-3-BFG snapshot `ea29c00` | 6 | `src/sound/OpenAL/AL_Sound{Hardware,Sample,Voice}.{cpp,h}` at `neo/sound/OpenAL/` | + +The audit prints every file with its classification. For the six OpenAL files, the pinned RBDOOM tree is an intermediate lineage reference, not proof of the exact commit from which openQ4 first received the file. The optional source-root arguments verify that every configured path actually exists in the identified reference tree. Most Doom 3-marked files came through the historical idTech 4/Quake 4 lineage and do not necessarily have a one-to-one path in the official Doom 3 snapshot, so the inventory intentionally does not invent such a mapping. + +## Import policy + +Before incorporating more external code: + +1. Use an official or otherwise auditable source revision and record its repository, immutable commit, path, and licence family. +2. Verify that every required licence and notice is present. Do not remove or rewrite an upstream header merely to make the tree look uniform. +3. Mark the openQ4 version as altered in the change and its documentation; never represent it as the original upstream program. +4. Add the new local path to the reproducible inventory by updating the expected count after reviewing the audit diff. +5. Add an elegant upstream credit in the relevant documentation or README. +6. Do not import excluded third-party code, game data, proprietary SDK binaries, or source recovered from a retail executable. + +The companion `openQ4-game` repository is a separate provenance boundary: its Quake4SDK-derived game-library source remains subject to the Quake 4 SDK EULA. This document inventories this engine repository only. diff --git a/docs/dev/stock-asset-baseline.md b/docs/dev/stock-asset-baseline.md new file mode 100644 index 00000000..f58fe71e --- /dev/null +++ b/docs/dev/stock-asset-baseline.md @@ -0,0 +1,195 @@ +# Retail-PK4 Compatibility Baseline With Packaged openQ4 Overlays + +[`stock_asset_baseline.py`](https://github.com/themuffinator/openQ4/blob/master/tools/validation/stock_asset_baseline.py) is the authoritative P0 capture harness for Quake 4 retail-asset compatibility. It binds an evidence run to the exact retail PK4 bytes and approved-manifest file, selected client, packaged renderer modules, SP/MP game modules, other packaged shared libraries, openQ4 overlay packages, current openQ4 Git revision and dirty-state policy, the versioned per-map renderer budget contract, logs, saves, demos, engine-rendered screenshots, and measured CPU/GPU frame percentiles. + +This is not an overlay-free or “stock-only” execution claim. `baseoq4` is the active game directory and its packaged openQ4 PK4s take normal virtual-filesystem precedence over the verified retail `q4base`/`q4mp` fallback. The report preserves both facts: retail archive bytes must match the separately approved manifest, while every packaged-overlay member that supersedes a retail virtual path is inventoried and counted. + +The harness is deliberately non-interactive: + +- every real evidence client is forced to the versioned bordered-window + 1280x720 display contract: `r_fullscreen 0`, `r_borderless 0`, the legacy + migration guard, non-desktop fullscreen policy, and matching window/custom + dimensions; runtime `MODE` output and engine-written TGA dimensions must + independently confirm that exact workload; +- map actions are registered console commands in generated cfg files; +- images are produced by the engine `screenshot` command from the render target; +- it does not call an operating-system capture API; +- it does not control, inject, or capture mouse or keyboard input; +- every role uses an isolated save path below the chosen evidence directory. +- the staged package is mounted through the engine's working-directory + `fs_cdpath`, while writable `fs_devpath` output is confined to that role's + isolated evidence tree. + +## Evidence sequence + +| Role | Automated evidence | +|---|---| +| SP capture | Loads stock `game/storage1`, records a render demo, takes a full-size engine screenshot immediately before writing `StockBaselineSP`, loads that save, waits for an active restored draw, emits renderer/frame-pacing information plus the versioned OpenGL CPU/GPU timing marker, enforces the exact per-map budget row, and takes a separate post-load engine screenshot. | +| SP demo playback | Reopens the recorded demo with `timeDemoQuit`; a completed timedemo line and clean exit prove that the recorded stream can be read, not merely that a file exists. | +| MP server | Starts a windowed pure `mp/q4dm1` listen server, accepts the loopback run, records a demo, emits diagnostics and the versioned OpenGL CPU/GPU timing marker, enforces the server's exact per-map budget row, and takes an engine screenshot. | +| MP client | Connects through IPv4 loopback with the existing archived `ui_autoJoin 1` userinfo setting, proves the local player is in-game and neither spectating nor requesting spectate, proves the session GUI is closed and the HUD is enabled, records a demo, emits the versioned OpenGL CPU/GPU timing marker, enforces the client's exact per-map budget row, and takes its own gameplay screenshot. | + +Each role must exit normally, reach its explicit completion marker, avoid the harness's blocking diagnostic denylist, and produce non-empty expected artifacts. That denylist is limited to fatal errors, line-start engine `ERROR` records, shader compile/program-link failures, Vulkan validation messages or VUIDs, and OpenGL errors. Other warnings are retained in the evidence but do not by themselves fail the baseline, so an automated pass is not a warning-free-log claim. The MP client also requires exact active-player proofs on both sides of the screenshot; those proofs include closed game/session menu state and an enabled HUD, so a joined player hidden behind the JOIN GAME screen cannot pass. TGA screenshots must fully decode, contain an exact uncompressed 24/32-bit payload, exactly match the renderer dimensions recorded by `gfxInfo`, and avoid blank or recursive scaled-strip corruption. The SP save preview is held to the same pixel-content checks and the retail 320x240 dimensions. It must also remain visually coherent with the full-size engine screenshot issued immediately before `saveGame`, while simulation state is unchanged. The later post-load screenshot remains separately required and proves that the restored game reaches a renderable gameplay frame; it is deliberately not the preview-comparison reference because the validation waits and renderer measurements allow the live scene, vehicle pose, and effects to advance. The harness centre-aspect-crops and bilinearly downsamples the same-state reference and preview to 80x60, then requires a luma correlation of at least 0.75. For the secondary colour check, it fits one shared, bounded affine exposure transform from preview luma to reference luma (gain 0.25–4.0 and bias -192–192), applies that same transform to all three colour channels, and requires the remaining mean absolute RGB error to be no greater than 48. A single shared transform tolerates capture-path exposure and contrast changes without concealing an arbitrary per-channel colour defect. Raw RGB error, fitted exposure parameters, and compensated error are all recorded for review. The combined correlation, colour, and recursive-strip gates deliberately permit modest post-process differences while rejecting structurally valid feedback/recursion captures. The report hashes every artifact with SHA-256. + +The 2026-08-19 Milestone A development capture exercised report schema 10 and +passed all four roles plus immediate `--verify-report` replay. It retained the +canonical 1280x720 display contract, versioned CPU/GPU budget rows, pure MP and +`ui_autoJoin 1` proofs, save/demo lifecycle, and artifact hashes. Local human +review found the SP before/after images and save preview coherent, and both +`mp/q4dm1` roles showed active gameplay with the HUD rather than the join menu. +This is current-build evidence only: it came from an immutable development +runtime staged from an uncommitted source tree, so it is not the clean-source, +freshly staged final-package bundle required for release promotion. + +Both MP roles explicitly launch with `ui_autoJoin 1`. This follows the normal +game userinfo/server-policy path and avoids automating menu input. All openQ4 +MP validation should keep auto-join enabled unless the subject of the test is +the join menu or the initial spectator/join flow itself. Such a test must set +`ui_autoJoin 0` explicitly rather than relying on omission because the CVar is +archived. + +The MP server also runs with the normal `si_pure 1` policy and with +`net_serverAllowServerMod 0`. A baseline therefore has to complete the real +pure-PK4 negotiation; disabling pure mode or using the legacy server-mod escape +hatch cannot produce promotable compatibility evidence. + +Before reporting success, the harness re-hashes the retail assets, staged +runtime and overlays, and all captured artifacts. It also recomputes the SP +save-preview comparison against the same-state pre-save screenshot and requires +the recorded reference artifact, algorithm, thresholds, and metrics to match, +so updating an image's recorded hash cannot hide an incoherent preview. A run that mutates the staged +package or any other recorded input therefore fails in the same invocation. + +The default `stock-baseline-targets-v1` contract is stored at +`tools/validation/renderer_per_map_budgets.json`. Its repeated 20 ms P95 / 28 +ms P99 CPU and GPU values are initial cross-map baseline target ceilings, not +claimed measurements. Every evidence result records the selected contract row, +minimum samples, ceilings, exact timing marker, and measured CPU/GPU P50/P95/P99 +values so results from this machine remain distinguishable from universal +release qualification and confirmed rows can be tightened without changing +the schema. Required GPU timing is fail-closed: unavailable, insufficient, +malformed, duplicate, identity-mismatched, or over-budget samples cannot +produce a passing stock report. + +Passing budget evidence is comparable only at the canonical bordered-window +1280x720 workload. Non-dry captures reject other `--width`/`--height` values, +and report replay rejects a changed display contract, non-windowed or +non-1280x720 runtime `MODE` evidence, and screenshots whose decoded TGA size is +not exactly 1280x720. Custom sizes remain available only for dry planning and +do not produce promotable budget evidence. + +`--verify-report` fails closed unless the report represents a real capture +(`dryRun` is exactly `false`), every top-level and per-role failure array is +empty, the bound expected-assets file still exists at its recorded absolute +path and has its recorded SHA-256, and that manifest's retail inventory matches +the report. It resolves the explicitly selected/default budget contract, +requires its stable id/schema/SHA-256 to match the portable report binding, and +recomputes each role's measurements and thresholds from the hashed diagnostic +streams. A changed marker, map/backend/profile, sample count, threshold, +contract byte, runtime byte, or recorded measurement fails replay. The verifier +also requires the recorded openQ4 HEAD and dirty flag +to match the current checkout under the named provenance policy. A dirty flag +records only that uncommitted changes exist; it is not a fingerprint of those +changes, so promotion evidence should come from a clean checkout. + +Capture and dry-run output directories must be new or empty. The harness will not reuse earlier logs, screenshots, saves, or demos. Both process streams are captured and hashed even when empty; report verification requires the exact four roles, their lifecycle fields, completion markers, artifact kinds and paths, and unchanged engine log/stdout/stderr bytes. It reconstructs and compares the complete canonical argument list for every role, including `fs_game`, SP map/game type, MP listen map/game type/dedicated mode, loopback connection, `ui_autoJoin 1`, and pure/server-mod policy. The per-role engine logfile is authoritative for ordered lifecycle markers and MP screenshot-bracketing proofs. This avoids false duplicate-proof failures on POSIX builds that mirror identical engine diagnostics to stdout; stdout and stderr remain independently hashed and all three channels are scanned for the same blocking denylist described above, while general warnings remain review evidence rather than automatic failures. + +The timeout is applied independently to each SP role. Multiplayer uses one absolute timeout deadline beginning when the listen server is launched; the server and loopback client are monitored together and any roles still running at that deadline are terminated together. The client-start delay consumes part of that shared MP budget rather than granting either process a second sequential timeout. + +## Run it + +List the fixed cases and safety invariants without touching assets or launching: + +```text +python tools/validation/stock_asset_baseline.py --list +``` + +First create a PK4-only view. A normal Steam tree is not suitable because saves, +generated collision caches, extracted maps, or mod files can override the retail +archives even when every loose byte is hashed: + +```powershell +$source = "C:\Program Files (x86)\Steam\steamapps\common\Quake 4" +$assetRoot = ".tmp\stock-assets-pk4-only" +New-Item -ItemType Directory -Force "$assetRoot\q4base", "$assetRoot\q4mp" +Copy-Item "$source\q4base\*.pk4" "$assetRoot\q4base\" +Copy-Item "$source\q4mp\*.pk4" "$assetRoot\q4mp\" +``` + +Write the complete plan and the separately reviewed expected-assets manifest +without launching the game: + +```text +python tools/validation/stock_asset_baseline.py --dry-run --asset-root .tmp\stock-assets-pk4-only --output-dir .tmp\stock-baseline\approved +``` + +Capture the baseline. Every non-dry capture must bind to an approved manifest +from the same retail edition/language set; a missing binding, archive mismatch, +or loose q4base/q4mp file fails before openQ4 launches: + +```text +python tools/validation/stock_asset_baseline.py --asset-root .tmp\stock-assets-pk4-only --expected-assets .tmp\stock-baseline\approved\stock_pk4_manifest.json --budget-contract tools\validation\renderer_per_map_budgets.json --output-dir .tmp\stock-baseline\candidate +``` + +By default the harness hashes and launches the canonical repository `.install` +package. If that directory is occupied by an unrelated running test, stage one +fresh, ordinary alternate package below `.tmp/stock-runtime/` with the canonical +fast-staging tool, then name that exact directory explicitly for capture and +verification: + +```text +python tools/build/stage_fast_install.py --build-dir builddir --install-dir .tmp/stock-runtime/current-build --temporary-runtime +python tools/validation/stock_asset_baseline.py --runtime-dir .tmp/stock-runtime/current-build --asset-root .tmp/stock-assets-pk4-only --expected-assets .tmp/stock-baseline/approved/stock_pk4_manifest.json --output-dir .tmp/stock-baseline/candidate +python tools/validation/stock_asset_baseline.py --runtime-dir .tmp/stock-runtime/current-build --asset-root .tmp/stock-assets-pk4-only --verify-report .tmp/stock-baseline/candidate/stock_asset_baseline_report.json +``` + +Temporary staging refuses an existing destination, links/junctions, or a path +outside `.tmp/stock-runtime/`. The report and runtime manifest record its +resolved root, and every collector, launch working directory, post-capture +rehash, and later verifier uses that same root. This proves the recorded current +build against verified retail assets plus its packaged openQ4 overlays; it does not replace the separate release-packaging +gate that restages and validates canonical `.install`. + +Re-hash an existing report's retail assets, bound expected-assets manifest, openQ4 runtime/overlays, current source provenance, exact launch contract, and recorded artifacts without launching. Keep the evidence directory and expected-manifest file at their recorded absolute locations and verify from the same openQ4 HEAD/dirty state: + +```text +python tools/validation/stock_asset_baseline.py --asset-root .tmp\stock-assets-pk4-only --verify-report .tmp\stock-baseline\candidate\stock_asset_baseline_report.json +``` + +The output directory contains: + +- `stock_asset_baseline_report.json`: full machine-readable plan, identities, results, artifact hashes, compatibility model, retail/overlay path-collision inventory, portable budget-contract binding, per-role thresholds, and measured CPU/GPU percentiles; +- `stock_asset_baseline_report.md`: concise reviewer report, collision counts by packaged overlay, and manual gates; +- `stock_pk4_manifest.json`: portable retail PK4 identity manifest; +- `openq4_runtime_manifest.json`: resolved runtime root plus the selected client, dedicated server, diagnostic symbols, all packaged renderer/game modules and shared libraries, overlay PK4s, and loose overlay files; +- `savepaths/`: isolated cfg files, engine logs, screenshots, demos, and saves; +- per-role stdout/stderr text. + +The retail manifest hashes every top-level `.pk4` under `q4base/` and `q4mp/`; any loose non-PK4 file is a hard failure. It establishes byte identity with the separately supplied approved manifest; by itself it does not establish ownership or authenticity. The runtime manifest separately hashes the selected client, dedicated server, diagnostic symbols, every packaged dynamic library under the recorded runtime root (including renderer modules, both game modules, and packaged dependencies such as OpenAL), every top-level `baseoq4/*.pk4`, and every loose file recursively visible under its `baseoq4/`. The verifier opens the retail and packaged-overlay PK4s as ZIP archives, compares their case-insensitive idTech virtual paths, and requires the recorded path-by-path collision inventory and count to match the current packages. + +The baseline fixes `r_renderApi gl`, `r_rendererBenchmarkPreset baseline`, and backend-neutral whole-frame GPU timestamps; requires the OpenGL renderer module on non-macOS packages; and records any additional packaged renderer module. Vulkan per-map evidence is captured separately with the renderer gameplay benchmark because this compatibility baseline intentionally has one fixed backend launch contract. Only the SP/MP modules, their top-level Windows symbol sidecars, and `mod.json` may be loose below the recorded runtime's `baseoq4/`; any other loose overlay file fails preflight before launch. Symbol sidecars are hashed but are not VFS game content. This prevents an unreported map, material, GUI, or script from weakening the declared retail-fallback-plus-packaged-overlay model. + +The harness fails closed when the supplied asset root has a non-empty `baseoq4/` directory. With `fs_game=baseoq4`, that second asset-root overlay would silently alter the explicitly inventoried precedence model. Use a clean retail fallback view containing only top-level retail PK4s in `q4base/` and `q4mp/`. The tool rejects every loose retail file, and by default rejects links at or inside those trees so recursive inventory cannot be bypassed. + +A boundary junction is accepted only with explicit opt-in and only when its +resolved directory has already been made PK4-only. Never junction the normal +Steam q4base/q4mp directories: loose generated or extracted files in those +trees take precedence over shipped archive members. + +```powershell +python tools/validation/stock_asset_baseline.py --allow-asset-dir-links --asset-root .tmp\stock-assets-linked-pk4-only --expected-assets .tmp\stock-baseline\approved\stock_pk4_manifest.json --output-dir .tmp\stock-baseline\candidate +``` + +The explicit flag permits links only at the `q4base`/`q4mp` boundary. Their resolved targets are recorded in the JSON, Markdown, and retail manifests; links nested inside either content tree and every loose file still fail closed. + +## Promotion gate + +An automated `pass` establishes the artifact and lifecycle contract only. Before using a bundle as the release baseline, a reviewer must still: + +1. inspect the engine screenshots for black frames, broken materials, missing effects, bad UI/subviews, and obvious shadow or post-process failures; +2. play representative SP and MP sequences to assess input, audio, scripting, collision, prediction, and subjective presentation; +3. capture from a clean source checkout, then retain the approved JSON report and PK4 manifest with the tested binary or immutable build identifier; +4. verify the final, freshly staged release package rather than treating a debug/development staging tree as release proof; +5. record platform/driver coverage separately rather than treating one host as universal qualification. + +The broader renderer visual/performance promotion rules remain in the [renderer validation matrix](renderer-validation-matrix.md). This P0 harness is the compatibility identity and lifecycle baseline, not a replacement for renderer-specific image references or target-hardware performance evidence. diff --git a/docs/dev/temporal-presentation.md b/docs/dev/temporal-presentation.md new file mode 100644 index 00000000..93468562 --- /dev/null +++ b/docs/dev/temporal-presentation.md @@ -0,0 +1,126 @@ +# Temporal Presentation + +Milestone E adds a default-off temporal presentation path shared by OpenGL and +Vulkan. It combines delayed GPU-time dynamic resolution with native-resolution +temporal accumulation, while preserving the existing SMAA/spatial path as the +immediate rollback. + +## Ownership Contract + +The renderer selects one immutable presentation state at `BeginFrame`: + +- `sceneWidth` and `sceneHeight` size the game-owned 3D and authored-post chain; +- `outputWidth` and `outputHeight` size the temporal histories and final output; +- 3D rendering and authored scene effects complete before temporal resolve; +- HUD, menus, console, and other root 2D views are submitted after that resolve + directly at native output resolution. + +Renderer ABI v11 exposes this frame state and one append-only temporal-resolve +entry point to both game modules. The SP authored-post path owns two native-sized +history render targets; direct and MP paths that do not use that post chain use +an equivalent lazy backend-owned pair. Either owner swaps only after the exact +frame/generation resolve is accepted. A generation change, native extent change, +video/backend restart, map/session discontinuity, camera cut, capture, missing +current-frame depth stamp, or rejected write invalidates continuity. A dynamic +scene-size change does not: history is native-sized, so retaining it is required +for TAAU. + +No loose material or shader asset is required. Backend resolve programs are +engine-owned resources, and retail Quake 4 materials remain unchanged. + +## Dynamic Resolution + +`r_rendererDynamicResolution 1` enables the controller and automatically enables +the backend-neutral whole-frame timing stream needed by it. OpenGL uses the +existing four-slot timestamp-query ring; Vulkan reads timestamps only after the +ordinary frame fence for that slot retires. Neither backend waits for a current +frame query. + +The controller associates each delayed sample with the exact scale of its source +frame, rejects stale/out-of-order/generation-mismatched samples, drops resolution +quickly above budget, and raises it only after an under-budget streak. Dimensions +are bounded and aligned before target allocation. Unsupported timing keeps the +configured safe ceiling and reports `dynamicActive=0`. + +Manual `r_screenFraction` scaling remains available when the automatic +controller is off. With temporal AA active, even mode `0` uses the scene-target +route so TAAU receives the requested low-resolution scene instead of the legacy +back-buffer crop path. Root UI-only frames never enter either scaling route. + +Relevant controls: + +| CVar | Default | Purpose | +|---|---:|---| +| `r_rendererDynamicResolution` | `0` | Enable automatic 3D scene scaling | +| `r_dynamicResolutionMinScale` | `50` | Minimum automatic scale, percent | +| `r_dynamicResolutionMaxScale` | `100` | Maximum automatic scale, percent | +| `r_dynamicResolutionTargetMsec` | `0` | Explicit GPU budget; `0` derives it from presentation timing | +| `r_dynamicResolutionTargetUtilization` | `90` | Share of the presentation interval available to GPU work | +| `r_dynamicResolutionDropStep` | `5` | Maximum percentage-point drop per over-budget sample | +| `r_dynamicResolutionRaiseStep` | `2` | Percentage-point increase after a recovery streak | +| `r_dynamicResolutionRaiseFrames` | `30` | Retired under-budget samples required before a raise | +| `r_dynamicResolutionAlignment` | `8` | Scene-dimension alignment | +| `r_dynamicResolutionCaptureNative` | `0` | Force known captures to native scene resolution instead of freezing scale | + +`rendererTemporalPresentationStatus` prints the latched extents, delayed sample +identity and age, target budget, controller decision, image-history generation, +and capture state. + +## Temporal History And Motion + +`r_temporalAA 1` replaces the game SMAA tail only when the renderer accepts a +validated temporal resolve command. Rejection leaves the established SMAA or +spatial final pass in place. The resolve uses an eight-sample Halton sequence, +previous camera projection, depth reprojection, object velocity where available, +neighbourhood clamping, colour/depth disocclusion rejection, and conservative +reactive rejection. + +Every visible motion class has explicit ownership: + +| Domain | History treatment | +|---|---| +| Static world | Camera/depth reprojection and disocclusion testing | +| Rigid entities | Previous model transform plus camera reprojection; OpenGL writes exact eligible object velocity, while Vulkan conservatively rejects the packet region | +| Skinned geometry | Reactive rejection unless a backend supplies an explicitly validated previous palette and velocity stream | +| Particles/BSE | Reactive rejection | +| Material/generated deforms | Reactive rejection unless a backend supplies explicitly validated previous vertices | +| Portal, mirror, and remote subviews | Stable unjittered child render; sampled parent surface is reactive and marked as separate-history ownership | +| In-world GUI | Rigid/camera motion plus reactive rejection | +| First-person view model | Depth-hack-aware rigid/camera motion plus reactive rejection | + +The conservative reactive routes are deliberate ownership, not zero-vector +claims: history is suppressed when a precise prior vertex stream is unavailable. +The shared packet policy carries at most two conservative normalized regions; +when a backend cannot establish that policy, it rejects history over the full +view. Root 2D UI is never part of the temporal history or scene scaling. + +`r_temporalAAFeedback`, `r_temporalAAReactiveScale`, and `r_temporalAADebug` +control maximum history weight, rejection strength, and the velocity/reactive/ +history-weight diagnostic views. + +## Cuts And Captures + +Per-view identity includes the render world/map, root or subview chain, view id, +flags, and capture surface provenance. History is rejected for identity or +generation changes, native output resizes, time discontinuities, teleports, +large rotation cuts, and projection cuts. Equal simulation times remain valid +so high-refresh presentation can accumulate between game tics. + +Known screenshot and save-preview frames render without temporal jitter. A +capture that begins after the game queued its scene marks the source timing +sample ineligible, advances only the image-history generation, and makes both +backends bypass history reads and writes. The current scene is spatially +presented for readback, then ordinary history starts cleanly on a later frame. + +The stock renderer remains a numeric SDR pipeline: temporal targets preserve the +same UNORM/code-value contract as their source, and neither resolve shader adds +an sRGB or gamma transfer. Native UI is composed after the scene resolve. + +## Validation + +Dependency-light policy coverage lives in +`openq4-temporal-presentation-core-test` and +`openq4-temporal-history-core-test`. The static cross-repository/backend contract +is checked by `tools/tests/renderer_temporal_presentation.py`. Runtime acceptance +uses windowed engine-TGA captures and mode-specific SP/MP gameplay; operating- +system capture and injected user input are not part of the workflow. diff --git a/docs/dev/ttf-font-system.md b/docs/dev/ttf-font-system.md index 21c6a139..87dfc0c6 100644 --- a/docs/dev/ttf-font-system.md +++ b/docs/dev/ttf-font-system.md @@ -246,6 +246,15 @@ Each atlas is bound through a material this module generates at registration time. Those declarations have no `.mtr` behind them, which makes them fragile in a way a shipped material is not. +The material and its intrinsic scratch image intentionally use the same +`_ttfatlas...` identity. The declaration manager parses a newly requested +implicit material before the font module can replace its source, and that +implicit stage maps an image named after the material. Sharing the already +uploaded atlas identity makes this first parse valid and prevents spurious +missing-image warnings and redundant defaulted image records. The font module +then installs its full generated stage unconditionally so the required blend, +linear filtering, clamp, and no-picmip state remains explicit. + `idDeclManagerLocal::BeginLevelLoad` purges every declaration that is not marked as parsed outside a level load, and `FindType` clears that mark on anything it resolves *during* a load. A face a map's own GUIs are the first to name — the @@ -272,9 +281,11 @@ Two things close that off: parsing first, so a healthy declaration costs nothing and is not demoted by the lookup itself. -Registration is also self-healing now: `R_TTFCreateAtlasMaterial` reinstalls the -stage whenever the declaration it finds is not already sampling the atlas, -instead of handing back whatever the manager last left there. +Registration is also self-healing: `R_TTFCreateAtlasMaterial` always reinstalls +the complete generated stage. This is intentionally stronger than comparing +the old stage's image name, because a full renderer restart can recreate the +image manager while leaving a generated declaration carrying a stale image +pointer. A related hazard sits on the GUI side. `idDeviceContext` holds `activeFont` and `useFont` as pointers into its `fonts` list, and that list has granularity 1, so diff --git a/docs/user/advanced-screen-space-lighting.md b/docs/user/advanced-screen-space-lighting.md new file mode 100644 index 00000000..1f4a0b5a --- /dev/null +++ b/docs/user/advanced-screen-space-lighting.md @@ -0,0 +1,80 @@ +# Advanced Screen-Space Lighting + +openQ4 provides three experimental, independently controlled screen-space +lighting effects on OpenGL and Vulkan: + +- view-aligned froxel volumetrics; +- screen-space reflections (SSR); and +- screen-space diffuse global illumination (SSGI). + +They are optional, default off, and do not alter the installed Quake 4 assets. +The effects share the native scene-presentation pass, run before temporal AA +when TAA is enabled, and leave the HUD, menus, and console at native resolution. +Engine screenshots include the effect result. + +## Enable and roll back + +The Milestone F master permit must be on. It defaults to `1`, while each effect +leaf defaults to `0`: + +```text +set r_rendererModernQuality 1 +set r_rendererFroxelVolumetrics 1 +set r_rendererSSR 1 +set r_rendererSSGI 1 +``` + +Use any subset of the three leaf settings. Set +`r_rendererModernQuality 0` for immediate one-setting rollback of these effects +and the other experimental Milestone F lighting domains. The renderer publishes +an exact zero-feature packet when the master is off, even if archived leaf +settings remain enabled. + +## Controls and bounds + +| Setting | Default | Effect | +|---|---:|---| +| `r_rendererFroxelVolumetrics` | `0` | Enables bounded view-depth volumetric integration. | +| `r_froxelVolumetricDensity` | `0.00012` | Base extinction density, clamped to `0.00001`--`0.01`. | +| `r_froxelVolumetricMaxDistance` | `2048` | Maximum integrated world-space distance, clamped to `64`--`8192`. | +| `r_froxelVolumetricSlices` | `12` | View-depth slices, clamped to `4`--`16`. | +| `r_rendererSSR` | `0` | Enables bounded depth/normal screen-space reflections. | +| `r_screenReflectionIntensity` | `0.35` | Maximum reflection contribution, clamped to `0`--`1`. | +| `r_screenReflectionMaxDistance` | `512` | Maximum view-space ray distance, clamped to `32`--`2048`. | +| `r_screenReflectionSteps` | `10` | Ray-march steps, clamped to `4`--`16`. | +| `r_rendererSSGI` | `0` | Enables fixed eight-tap screen-space diffuse GI. | +| `r_screenGIIntensity` | `0.25` | Maximum GI contribution, clamped to `0`--`1`. | + +Changing a leaf or tuning value invalidates temporal history so the new result +does not blend with stale lighting. + +## Deliberate limitations + +These are bounded scene-colour/depth effects, not replacements for authored +lighting: + +- Froxel volumetrics integrate analytic ambient/directional scattering through + view-depth slices. They do not inject every scene light, cast volumetric + shadows, or supply a persistent world voxel volume. +- SSR reconstructs normals from depth. It has no material roughness/G-buffer + input, cannot see off-screen or occluded colour, and keeps the current scene + colour when a ray has no usable hit. +- SSGI uses eight depth-derived neighbour samples. It is a local diffuse + approximation, not a multi-bounce world-space light solver. + +Missing scene colour/depth or shader resources fail safely to the established +presentation path. These effects do not promote GPU-driven visible-lighting +ownership; the classic renderer remains authoritative. + +## Performance and troubleshooting + +Each enabled leaf adds bounded work to one full-screen presentation pass. SSR +step count and froxel slice count are the main quality/performance controls. +Test the effects individually first, particularly at high resolutions. + +If an effect is absent: + +1. Check that `r_rendererModernQuality` and the intended leaf are both `1`. +2. Run `gfxInfo` and check the renderer log for shader or framebuffer errors. +3. Test with the default tuning values and native resolution. +4. Set `r_rendererModernQuality 0` to confirm the classic rollback path. diff --git a/docs/user/level-load-cache.md b/docs/user/level-load-cache.md index 26d0bf39..757ad18a 100644 --- a/docs/user/level-load-cache.md +++ b/docs/user/level-load-cache.md @@ -1,38 +1,142 @@ # Level-Load Cache -openQ4 automatically builds compact binary caches for MD5 animations as they are first encountered. Later launches can load the final animation data directly instead of decompressing and parsing the much larger text source again. +openQ4 learns which source files a map actually uses and can prepare those +bytes during a later matching load. Worker jobs read or inflate the selected +source, validate supported container framing and whole-buffer integrity, then +hand immutable bytes back to the normal owner. openQ4 also keeps validated local +binary caches for parsed models, classic map worlds, collision data, and MD5 +animations. -The first visit to animation-heavy maps can therefore take slightly longer while cache files are written. Subsequent visits should be faster. A representative Air Defense 1 run generated 820 cache entries using about 27 MiB and reduced the animation-heavy model-definition phase from roughly 2.7–2.8 seconds to about 1.0 second on the tested Windows x64 system. +The experimental cache is disabled by default because current stock-map +measurements do not yet prove a reliable improvement over classic loading. If +enabled, the first visit still uses the installed Quake 4 or mod sources and may +take longer while private cache files are written. A later visit can reuse work +only when the map, game mode, entity filter, active search/PK4 set, source +identity, and load-affecting settings still match. + +## Safety and compatibility + +The retail or mod file selected by the normal virtual filesystem always remains +authoritative. openQ4 resolves that file through the usual search and pure-PK4 +rules before it may substitute already prepared, immutable bytes. Parsing, +validation, and renderer/audio upload stay with the normal resource owner. +The worker framing step recognizes RIFF/WAVE, Ogg, PNG, and JPEG sources; DDS +and other formats remain opaque until their ordinary owner parses them. + +Generated model, world, collision, and animation readers build private state +first and publish it only after the whole payload is valid. A stale, truncated, +corrupt, oversized, wrong-version, or otherwise mismatched cache is ignored; +the affected private file is removed where appropriate and openQ4 loads the +original source instead. Packed MD5RProc map worlds remain on their source path +rather than being flattened into an incompatible world cache. + +These files are disposable runtime data. They are not replacement assets, do +not change pure-server package negotiation, and must not be copied into retail +Quake 4 PK4s. ## Location -Caches are written below the active save path: +Caches are written below the active game directory under `fs_savepath`, normally +`/baseoq4/`: ```text -/baseoq4/generated/animations/ +generated/manifests/ learned per-map source lists +generated/models/ parsed render-model data +generated/worlds/ finalized classic .proc world data +generated/collision/ parsed/generated collision data +generated/animations/ parsed MD5 animation data ``` -They are generated runtime data, not replacement game assets, and should not be copied into retail Quake 4 PK4s. +With the standard unified game directory, the full animation root is +`/baseoq4/generated/animations/`. -## Validation and portability +Manifest and model/world/collision filenames are opaque hashes of their +validated keys. Animation files retain the normalized source path and add a +`.banim` suffix. Moving cache files between installations is neither necessary +nor recommended; a different source package or setting can intentionally cause +a clean miss and rebuild. -Each cache records the source path, source size, timestamp, and containing PK4 checksum where applicable. A stale, truncated, corrupt, or mismatched cache is ignored and rebuilt from the installed source animation. +Animation v3 stores fixed-width values and floating-point arrays in a portable +byte order; x64 and ARM64 builds use the same format. It identifies the exact +source that the parser would select (`c` when binary reads are enabled and +that compiled source exists, otherwise the requested `.md5anim`), uses a content +CRC for loose sources, and verifies a whole-record length/CRC trailer before +parsing cache fields. A stale, truncated, corrupt, or mismatched cache is ignored +and the selected source animation is parsed. -The cache stores fixed-width values and floating-point arrays in a portable byte order. x64 and ARM64 builds use the same format, although moving the game installation or changing the source package can intentionally trigger a rebuild. - -Loaded cache data becomes the normal runtime animation data; openQ4 does not retain a second permanent copy in memory. Animation memory reporting also includes base-frame storage. +Learned preload identity for a loose file uses its normalized path, size, and +timestamp. A tool that deliberately preserves both size and timestamp while +rewriting a loose file can evade that preparation check; close openQ4 and remove +the generated data, or disable `com_levelLoadCache`, when testing such edits. +PK4 members remain tied to the containing archive checksum. ## Controls -Both controls default to enabled: +The master switch defaults off. The individual controls retain their enabled +defaults so an intentional experiment needs only the master switch: + +```text +com_levelLoadModernization 0 +com_levelLoadCache 1 +com_levelLoadCacheWrite 1 +com_levelLoadPreload 1 +com_levelLoadCacheReport 0 +``` + +- Set `com_levelLoadModernization 1` before loading a map to opt into all + individually enabled cache paths. This also permits the SP/MP animation-cache + controls below to take effect. +- Set `com_levelLoadModernization 0` for the classic loading path. This master + rollback overrides archived values from builds that previously enabled the + individual controls by default. +- Set `com_levelLoadPreload 0` before loading a map to disable learned source + preparation while retaining valid generated model/world/collision caches. +- Set `com_levelLoadCacheWrite 0` to stop new manifests and generated + model/world/collision files from being written while valid existing files can + still be read. +- Set `com_levelLoadCache 0` before loading a map to bypass learned manifests + and generated model/world/collision caches completely. +- Set `com_levelLoadCacheReport 1` for per-map hit, miss, replay, byte, + framing/integrity decode, cancellation, and staging/decoded-memory diagnostics + in the log. + +Advanced staging limits are available as +`com_levelLoadPreloadMaxEntries`, `com_levelLoadPreloadMaxFileMB`, +`com_levelLoadPreloadMaxStagingMB`, `com_levelLoadPreloadMaxDecodeMB`, +`com_levelLoadPreloadReadChunkKB`, and +`com_levelLoadPreloadDecodeChunkKB`. Their defaults are deliberately bounded; +increasing them can raise transient memory use and is not normally needed. The +normal owner can allocate its parsed representation in addition to the retained +source bytes, so these controls are pipeline limits rather than a cap on total +level-load memory. + +Animation caches are owned separately by the SP and MP game modules. Their +individual controls default to enabled, but reads and writes occur only while +`com_levelLoadModernization 1`: ```text g_useGeneratedAnimCache 1 g_writeGeneratedAnimCache 1 ``` -- Set `g_useGeneratedAnimCache 0` to bypass existing generated animation caches. -- Set `g_writeGeneratedAnimCache 0` to prevent new cache files from being written. -- Disable both for a source-parse benchmark. +- Set `g_useGeneratedAnimCache 0` to bypass existing `.banim` files. +- Set `g_writeGeneratedAnimCache 0` to prevent new `.banim` files. + +## Clean rebuild and rollback + +To force a clean rebuild, close openQ4 and delete one or more of the +`generated/` subdirectories listed above under the active `fs_savepath`. openQ4 +will recreate needed private data from the installed sources. + +For a complete source-path comparison, leave `com_levelLoadModernization 0`. +No framework or animation cache is read or written in that mode, regardless of +older archived values for the individual controls. + +Never delete the original `.proc`, `.cm`, model, image, sound, or +`models/**/*.md5anim` files from the Quake 4 installation. If a map still fails +with all generated caches disabled, the problem is not caused by this cache +layer; include the active `fs_savepath` and `logs/openq4.log` when reporting it. -To force a clean rebuild, close openQ4 and delete the `generated/animations/` directory under the active `fs_savepath`. Do not delete the original `models/**/*.md5anim` files from the Quake 4 installation. +Developers and testers can find the exact key, ownership, validation, and +pending evidence contract in +[Level-Load Cache Modernization](../dev/loading-cache-modernization.md). diff --git a/docs/user/light-grids.md b/docs/user/light-grids.md index 29c3aff4..04aff8bb 100644 --- a/docs/user/light-grids.md +++ b/docs/user/light-grids.md @@ -64,7 +64,7 @@ Current scope and limits: - intended for openQ4's native bake/load path, not drop-in BFG asset parity - translucent effects, decals, and other non-lighting surfaces remain outside the runtime light-grid pass -If no baked assets are found, openQ4 can still generate a runtime probe layout for debugging and baking, but there will be no indirect-light contribution until actual baked files exist. +If no baked assets are found, openQ4 leaves the runtime grid empty. Starting a light-grid bake generates the requested probe layout on demand; this avoids paying the collision and visibility setup cost on every stock-map load when there is no atlas to render. ## Runtime Controls diff --git a/docs/user/multiview-demos.md b/docs/user/multiview-demos.md index 2ffb83a8..01f1d20e 100644 --- a/docs/user/multiview-demos.md +++ b/docs/user/multiview-demos.md @@ -97,7 +97,7 @@ stop MVD recording or playback. | Format | Playback | Seeking and speed | Camera | |---|---|---|---| -| openQ4 MVD `.mvd` 1.0-1.2 | Supported, including API-39 1.0/1.1 recordings | Pause, speed, step, and replay-based seek | Full-world free roam and player follow | +| openQ4 MVD `.mvd` 1.0-1.2 | Supported, including compatible API-39, API-42, and API-44 1.0/1.1 recordings | Pause, speed, step, and replay-based seek | Full-world free roam and player follow | | openQ4 render `.demo` | Supported | Pause, speed, step, and replay-based seek | Fixed recorded camera | | Retail Quake 4 render `.demo` | Not supported yet | None | Not available | | Retail Quake 4 `.netdemo` | Not supported yet | None | Not available | diff --git a/docs/user/server-security.md b/docs/user/server-security.md new file mode 100644 index 00000000..b2b448dd --- /dev/null +++ b/docs/user/server-security.md @@ -0,0 +1,172 @@ +# Server and Remote-Console Security + +openQ4 uses an authenticated remote-console protocol, `rcon2`, by default. +It replaces Quake 4's legacy packet, which sent the administration password +verbatim over UDP. Gameplay, server discovery, the connection handshake's wire +layout, and the retail Quake 4 asset format are unchanged. + +## Configure a strong password + +Set the same password on the server and on the administrator's client: + +```text +net_serverRemoteConsolePassword "a-long-random-password" +net_clientRemoteConsolePassword "a-long-random-password" +``` + +Then issue a command from the client in the usual form: + +```text +rcon status +``` + +`rcon2` requires at least 12 password bytes. Use a unique, randomly generated +password of 24 or more characters for an Internet-facing server. Do not reuse a +login, referee, database, or service password. + +Enter the password at a trusted local console, or put the server-side setting +in a configuration file readable only by the account that runs the server. +Do not commit that file, share it with a package, or use the password on the +process command line. In particular, never launch with +`+set net_serverRemoteConsolePassword ...` or +`+set net_clientRemoteConsolePassword ...`: launch arguments can be visible to +other local users, process-monitoring tools, crash reports, and service logs. + +Private CVars are redacted from broad console listings and direct console +queries, omitted from generic CVar serialization and dictionaries, blocked +from `$` CVar expansion, and suppressed from console echo, command history, +completion previews, startup-command reporting, and event journals. These +protections reduce accidental disclosure; they do not make an insecure +configuration file safe. + +Server-originated userinfo and synchronized-CVar messages have separate, +explicit authority. Each can update only registered CVars carrying the matching +network flag; unrelated and private settings are ignored. Dictionary decoding +is all-or-nothing, so a truncated update does not leave a partially changed +settings set behind. + +## Server-provided package links + +Pure-server redirects and PK4 download entries are accepted only as bounded +`http://` or `https://` URLs. Their authority must contain a syntactically valid +DNS name, IPv4 literal, or bracketed IPv6 literal; malformed labels, ports, +credentials, ambiguous authorities, control characters, and every other URL +scheme (including `file:` and SMB-style paths) are rejected before prompting or +queueing. The generic background downloader applies the same check again. + +Standard Meson packages do not enable libcurl. They can show a validated web +redirect offered by a server, but in-process direct PK4 transfer reports +unavailable; obtain any required package separately from a source you trust. +This is intentional and should not be diagnosed as a broken downloader. + +If a separately integrated build deliberately enables libcurl, package +transfers are limited to HTTP(S), do not follow redirects, time out if a +connection cannot be established within 15 seconds, stop after 30 seconds below +1 KiB/s, and have a one-hour absolute cap. Advertised and received sizes, +destination paths, and package checksums retain their separate validation. +These restrictions bound that optional legacy path and prevent +local-file/protocol substitution; they do not prove DNS ownership, make an +untrusted server trustworthy, or certify arbitrary package content. + +The platform URL opener follows the same HTTP(S)-only syntax policy on Windows, +Linux, and macOS. In particular, macOS no longer accepts local `file:` URLs. + +## What rcon2 protects + +The server issues short-lived, one-use random challenges. The proof binds the +client and server nonces, the exact UDP endpoint, a server binding value, and a +SHA-256 digest of the requested command. The server derives its proof verifier +with PBKDF2-HMAC-SHA-256 at 200,000 iterations, compares proofs without an +early-exit timing leak, consumes a challenge before accepting or rejecting the +proof, and rate-limits both individual sources and global unauthenticated reply +traffic. Connection challenges, client IDs, server IDs, download-request IDs, +and rcon2 nonces use the operating system's cryptographic random source; an +operation fails closed if that source is unavailable. + +`rcon2` authenticates the command sender to the server, but it is **not an encrypted transport**. +The command and server output still travel in cleartext +UDP packets. A captured rcon2 exchange also permits PBKDF2-throttled offline password guessing. +PBKDF2 makes each guess more expensive; it does not rescue a +short, reused, or predictable password. Anyone able to observe or alter the +network path can read commands and output and can disrupt the exchange. Use a +trusted network or an encrypted tunnel/VPN when command or output confidentiality +matters. + +## Pure multiplayer and local game modules + +`si_pure 1` is supported and remains enabled when selected; openQ4 no longer +silently turns it off at server startup. Pure mode compares the ordered PK4 +asset list used by the server and client. The protocol can identify missing +asset PK4s, but standard packages do not enable in-process direct transfer. Any +separately integrated curl-enabled package path remains asset-only and never +supplies executable game code. + +The legacy protocol also carries a game-code checksum. openQ4 sends the official +Quake 4 1.4.2 `game300.pk4` checksum, `0x68fb90b1`, only as a compatibility +token. It does not require, mount, download, extract, or execute that archive. +The engine accepts the token only when the required `game_sp` or `game_mp` +module was already loaded from trusted local openQ4 package/module roots. A missing +module, an unsupported platform ID, an empty pure asset list, or a different +token fails closed instead of weakening pure mode or starting a code download. + +By default, `net_serverAllowServerMod 0` also prevents a selected mod from +supplying its own game module. Content-only mods can continue to use the base +openQ4 module. An administrator who intentionally operates a code-bearing mod +must set `net_serverAllowServerMod 1` and distribute a complete, trusted package +to clients separately; the setting does not make module code downloadable. + +The compatibility token preserves the stock 1.4.2 packet field across Windows, +Linux, and macOS. It is not a cryptographic measurement of the loaded module and +does not turn pure mode into an anti-cheat system. It also does not guarantee +that an arbitrary gameplay mod remains compatible with stock clients. + +## Malformed network traffic + +The shared bit-message reader records attempts to read beyond the received +payload, including through delta messages. Covered queue and user-command +decoders check their encoded sizes, while audited SP and MP leaf readers stage +decoded fields until their payload is valid and check referenced entity/player +types and bounds, spectator and weapon values, tournament instances, projectile +owners, PVS and game state, and hit-scan fields. A late malformed top-level +snapshot tears down the affected session before another game or presentation +frame. The legacy entity lifecycle is not a whole-snapshot rollback +transaction. + +This is targeted hardening of the audited legacy paths. It is not a claim that +every parser is formally verified, and malformed-packet regression tests do not +replace normal Internet-server isolation, operating-system updates, or careful +review of future protocol changes. + +## Legacy compatibility mode + +Legacy plaintext rcon is disabled on both sides by default. It remains only as +an explicit compatibility escape hatch for an old client or administration +tool: + +```text +// Server: accept legacy packets. +net_serverAllowLegacyRcon 1 + +// openQ4 client: send a legacy packet. +net_clientUseLegacyRcon 1 +``` + +An openQ4 client talking to an openQ4 server needs both settings to use the +legacy path. A third-party legacy tool needs the server setting; an openQ4 +client talking to an old server needs the client setting. Legacy mode sends the +password and command in plaintext and should be enabled only temporarily on a +trusted network. Return both variables to `0` when compatibility testing ends. + +Stock Quake 4 clients can still join and play on an openQ4 server because the +gameplay and connection packet layouts remain compatible. The intentional +administration compatibility change is that old plaintext-rcon tools receive no +response until the server operator explicitly enables the legacy path. + +## Standards and provenance + +The engine implementation is original GPL code based on the public algorithm +specifications, not on the Quake 4 SDK game-library implementation: + +- [FIPS 180-4: Secure Hash Standard (SHA-256)](https://csrc.nist.gov/pubs/fips/180-4/upd1/final) +- [RFC 2104: HMAC](https://www.rfc-editor.org/rfc/rfc2104) +- [RFC 8018: PBKDF2](https://www.rfc-editor.org/rfc/rfc8018) diff --git a/docs/user/server-setup.md b/docs/user/server-setup.md index a5a76edf..9c65e7a5 100644 --- a/docs/user/server-setup.md +++ b/docs/user/server-setup.md @@ -33,6 +33,9 @@ Example startup flow: openQ4-ded_x64 +set net_ip 0.0.0.0 +set net_port 28004 +set si_name "My openQ4 Server" +set si_map mp/q4dm1 +set si_gameType DM +spawnServer ``` +For remote administration, follow the [server and remote-console security +guide](server-security.md). Never put an rcon password in launch arguments. + ## IPv4 Binding and Ports For predictable hosting, set the interface and UDP port before starting the server: diff --git a/docs/user/shadow-mapping.md b/docs/user/shadow-mapping.md index be2df7d4..481391a5 100644 --- a/docs/user/shadow-mapping.md +++ b/docs/user/shadow-mapping.md @@ -26,6 +26,7 @@ vid_restart Notes: - `r_shadows` must stay enabled for any shadow path to render. - If the shadow-map path is unavailable or fails for a light, openQ4 falls back to the legacy shadow path instead of leaving the light unshadowed. +- If only part of a receiver ownership can be represented in a shadow map, the classic renderer combines that exact map with the missing casters' stencil supplements. If those supplements are incomplete, the complete ownership falls back to stencil instead of publishing a partial shadow. - Point lights shadow-map by default (`r_shadowMapPointLights 1`); they are the dominant light class in Quake 4 content. Set `r_shadowMapPointLights 0` to fall back to stencil shadows for point lights only. - Lights touching animated, deformed, or packed character receivers can also fall back to the legacy stencil path so stock character lighting, mirrored seams, and eye materials retain retail-style interaction behavior. - Modern renderer diagnostics keep lighting visible when shadow-map receiver sampling is not ready, but full modern visible-frame replacement stays fail-closed so the legacy path continues to provide the actual shadowed frame. @@ -55,10 +56,11 @@ seta r_shadows 1 seta r_useShadowMap 1 seta r_shadowMapCSM 1 seta r_shadowMapSize 1024 -seta r_shadowMapFilterRadius 2.0 -seta r_shadowMapFilterTaps 13 -seta r_shadowMapPointFilterRadius 2.5 -seta r_shadowMapPointFilterTaps 13 +seta r_shadowMapFilterRadius 0.75 +seta r_shadowMapFilterTaps 9 +seta r_shadowMapPointFilterRadius 1.0 +seta r_shadowMapPointFilterTaps 9 +seta r_shadowMapCasterCulling 2 seta r_shadowMapHashedAlpha 1 seta r_shadowMapCascadeStabilize 1 vid_restart @@ -71,11 +73,13 @@ seta r_shadows 1 seta r_useShadowMap 1 seta r_shadowMapCSM 1 seta r_shadowMapSize 2048 +seta r_shadowMapPointSize 1024 seta r_shadowMapCascadeCount 4 -seta r_shadowMapFilterRadius 2.5 +seta r_shadowMapFilterRadius 1.0 seta r_shadowMapFilterTaps 13 -seta r_shadowMapPointFilterRadius 3.0 +seta r_shadowMapPointFilterRadius 1.0 seta r_shadowMapPointFilterTaps 13 +seta r_shadowMapCasterCulling 2 seta r_shadowMapHashedAlpha 1 seta r_shadowMapCascadeStabilize 1 vid_restart @@ -99,9 +103,10 @@ seta r_shadows 1 seta r_useShadowMap 1 seta r_shadowMapCSM 0 seta r_shadowMapSize 512 -seta r_shadowMapFilterRadius 1.5 +seta r_shadowMapPointSize 256 +seta r_shadowMapFilterRadius 0.75 seta r_shadowMapFilterTaps 5 -seta r_shadowMapPointFilterRadius 2.0 +seta r_shadowMapPointFilterRadius 1.0 seta r_shadowMapPointFilterTaps 5 seta r_shadowMapHashedAlpha 1 vid_restart @@ -130,17 +135,17 @@ vid_restart | `r_shadowMapProjectedCSM` | `1` | `0..1` | Allows ordinary projected lights to use CSM when `r_shadowMapCSM` is enabled; parallel/global lights keep their dedicated large-coverage policy. | | `r_shadowMapConservativeCasters` | `1` | `0..1` | Keeps shadow-map caster submission separate from visible receiver scissors, so off-screen blockers can still shadow visible receivers. | | `r_shadowMapSkipStencilShadows` | `1` | `0..1` | Skips stencil shadow volume generation and linking for lights that will render shadow maps, removing the duplicate CPU shadow work. A light that fails a shadow-map pass automatically restores its stencil volumes on the next frame. | -| `r_shadowMapCasterCulling` | `2` | `0..2` | Caster face culling: `0` renders casters two-sided, `1` stores light-facing faces, `2` stores back faces (fewer acne artifacts, slight detachment on thin geometry). Materials declared `twoSided`/`backSided` are always honored, so grates, foliage, and curtains cast correctly in every mode. | +| `r_shadowMapCasterCulling` | `2` | `0..2` | Caster face culling: `0` always renders two-sided, `1` forces the material-oriented light-facing near shell, and `2` automatically uses the near shell for sealed hulls outside the light while rendering open, uncertain, or light-enclosing geometry two-sided. Automatic mode prevents front-only/open props and enclosing shells from disappearing without storing a detached far shell. Authored `twoSided`/`backSided` orientation is honored whenever one-sided culling is active; the deliberately two-sided modes override it. | | `r_shadowMapSize` | `1024` | `128..4096` | Base shadow-map resolution. Higher values cost more VRAM and GPU time. | | `r_shadowMapAtlasSize` | `4096` | `2048..8192` | Edge size of the shared projected-light shadow atlas. Cached projected lights occupy `r_shadowMapSize`-sized cells inside it (CSM lights use a 2x2 cell block), so all cached lights stay resident in one texture. | -| `r_shadowMapFilterRadius` | `2.0` | `0..8` | Projected-light PCF filter radius in texels. | -| `r_shadowMapFilterTaps` | `13` | `1..13` | Projected-light PCF tap budget. Values up to `1`, `5`, `9`, and `13` select progressively wider sample sets. | +| `r_shadowMapFilterRadius` | `0.75` | `0..8` | Projected-light PCF radius in texels. Increase resolution for more detail; increasing this value deliberately softens and widens the edge. | +| `r_shadowMapFilterTaps` | `9` | `1..13` | Projected-light PCF tap budget. Values up to `1`, `5`, `9`, and `13` select progressively denser sample sets without changing the requested radius. | | `r_shadowMapFilterMode` | `0` | `0..2` | Projected-light filter mode: fixed PCF, stable rotated Poisson, or experimental PCSS-lite with raw depth sampling. | | `r_shadowMapDistantFilterScale` | `0.35` | `0..1` | Scales projected PCF and PCSS radii for parallel/global distant sources such as sky or sun lights. Their texels cover much more world space than local projectors, so a tighter kernel preserves caster silhouettes instead of over-blurring them. Set `1` to use the ordinary projected-light radii unchanged. | | `r_shadowMapPCSSLightRadius` | `4.0` | `0..16` | Projected PCSS-lite blocker search radius in shadow texels. | | `r_shadowMapPCSSMaxRadius` | `8.0` | `0..16` | Maximum projected PCSS-lite filter radius in shadow texels. | -| `r_shadowMapPointFilterRadius` | `2.5` | `0..8` | Point-light PCF filter radius in texels. | -| `r_shadowMapPointFilterTaps` | `13` | `1..13` | Point-light PCF tap budget. Values up to `1`, `5`, `9`, and `13` select progressively wider sample sets. | +| `r_shadowMapPointFilterRadius` | `1.0` | `0..8` | Point-light PCF radius in cubemap texels. Large-radius lights can cover substantial world distance per texel, so wide values can visibly erase contact detail. | +| `r_shadowMapPointFilterTaps` | `9` | `1..13` | Point-light PCF tap budget. Values up to `1`, `5`, `9`, and `13` select progressively denser sample sets without changing the requested radius. | | `r_shadowMapPointFilterMode` | `0` | `0..1` | Point-light filter mode: fixed PCF or stable rotated Poisson. | | `r_shadowMapDepthCompare` | `1` | `0..1` | Uses hardware comparison sampling (with hardware-filtered PCF taps) for projected depth maps. Selecting PCSS-lite (`r_shadowMapFilterMode 2`) automatically uses the manual raw-depth path instead. Set `0` if a driver has trouble with GLSL shadow samplers. | | `r_shadowMapPointDepthCompare` | `1` | `0..1` | Uses hardware comparison sampling for point-light depth cubemaps when GLSL 1.30 support is available. | @@ -172,7 +177,9 @@ openQ4 can keep static-only shadow maps resident and reuse them across backend v | `r_shadowMapSubviewPolicy` | `1` | `0..2` | Shadow maps in mirror/remote-camera subviews: `0` renders them like main views, `1` reuses cached maps or falls back to stencil, `2` prefers stencil in subviews. When the target has no usable stencil attachment or an ownership contains map-only casters, Vulkan still renders the required fresh map rather than dropping its shadow. | | `r_shadowMapTranslucentReceivers` | `1` | `0..1` | Lets translucent surfaces sample the shadow map like opaque receivers, matching the stencil path's `r_stencilTranslucentShadows` behavior. | -`r_shadowMapMaxUpdatesPerView` still limits discretionary dynamic shadow-map work. Resident cache hits do not consume that update budget, so static light reuse can reduce update pressure without starving moving lights. When the budget constrains a frame, updates go to the most important stale lights first (screen coverage, staleness, and view proximity ordered), and starved lights age up the priority list so every light is eventually refreshed; denied lights reuse their last cached map when one exists. On Vulkan, a receiver ownership containing a map-only caster can exceed the nominal budget because no equivalent complete stencil result exists, and those correctness-required maps are admitted before optional maps can consume bounded resources. Cache signatures include caster materials, alpha/hash settings, caster offset settings, point-light range/depth mode, and view-fitted CSM state, so changing those inputs forces a fresh shadow map instead of reusing stale resident data. +`r_shadowMapMaxUpdatesPerView` still limits discretionary dynamic shadow-map work. Resident cache hits do not consume that update budget, so static light reuse can reduce update pressure without starving moving lights. When the budget constrains a frame, updates go to the most important stale lights first (screen coverage, staleness, and view proximity ordered), and starved lights age up the priority list so every light is eventually refreshed. A denied light may reuse its newest projection-compatible resident map. Point-light reuse additionally requires the same light origin, padded far range, cube resolution, depth-storage mode, and live physical storage generations; a moved or rescaled light therefore renders a fresh map or returns to stencil instead of decoding an old cube with a new projection. On Vulkan, a receiver ownership containing a map-only caster can exceed the nominal budget because no equivalent complete stencil result exists, and those correctness-required maps are admitted before optional maps can consume bounded resources. Cache signatures include caster materials, alpha/hash settings, caster offset settings, point-light range/depth mode, and view-fitted CSM state, so changing those inputs forces a fresh shadow map instead of reusing stale resident data. + +Resident entries are scoped to the loaded render world and are invalidated on map changes. Recreated atlas or cube storage invalidates its old metadata before cache occupancy and eviction are counted. Modern OpenGL also verifies the exact world, light, GLOBAL-pass signature, resolution, storage generation, and selected atlas slot or cube immediately before use; a resource belonging to another light, pass, storage generation, or previous map is treated as unavailable. Because that path exposes one shadow resource per light and cannot compose stencil supplements, it accepts only a complete GLOBAL ownership with no distinct local/no-self caster ownership and otherwise leaves the complete shadowed frame to the classic path. GLOBAL ownership is the union of opaque and enabled translucent receivers, so a translucent-only receiver can still request a complete map; if that map cannot be prepared, the affected translucent subset returns to filtered stencil shadowing at its normal depth phase instead of being drawn unshadowed. Projected lights with moving casters no longer pay full re-renders: the light's static geometry stays cached in the shared atlas, and each frame the cached tiles are copied and only the moving casters are drawn on top. A walking character in a cached light costs one small copy plus its own triangles. @@ -255,9 +262,9 @@ Projected shadow maps store Quake 4's authored light falloff depth directly, so | `r_shadowMapBias` | `0.00016` | `0..0.05` | Constant receiver depth bias for projected lights. | | `r_shadowMapNormalBias` | `0.00075` | `0..0.05` | Extra projected-light bias on sloped receivers. | | `r_shadowMapTexelBiasScale` | `0.45` | `0..8` | Uses texel-aware receiver bias based on fitted cascade/light footprint. Constant bias acts as a compatibility floor. | -| `r_shadowMapNormalOffsetScale` | `1.0` | `0..8` | Normal-offset bias in shadow texels: pushes the receiver sample point along the surface normal on sloped surfaces, fixing acne without detaching contact shadows. The preferred first knob for slope acne. | -| `r_shadowMapReceiverPlaneBias` | `0` | `0..1` | Allows derivative receiver-plane bias for wider projected-light filters. | -| `r_shadowMapPolygonFactor` | `0.75` | `0..16` | Slope-scale caster depth offset applied by the caster shaders while rendering shadow maps (shadow casters write shader depth, which `glPolygonOffset` cannot bias). | +| `r_shadowMapNormalOffsetScale` | `1.0` | `0..8` | Normal-offset bias in shadow texels. It helps slope acne but can move contact edges when set too high, especially on lights whose texels cover a large world-space footprint. | +| `r_shadowMapReceiverPlaneBias` | `0` | `0..1` | Enables the experimental derivative receiver-plane approximation. Keep it disabled for ordinary play. | +| `r_shadowMapPolygonFactor` | `0.25` | `0..16` | Slope-scale caster depth offset applied by the caster shaders while rendering shadow maps (shadow casters write shader depth, which `glPolygonOffset` cannot bias). | | `r_shadowMapPolygonOffset` | `0.5` | `0..64` | Constant caster depth offset in resolvable depth-buffer steps, applied by the caster shaders alongside the slope-scale term. | Point-light tuning: @@ -266,8 +273,9 @@ Point-light tuning: |---|---:|---:|---| | `r_shadowMapPointBias` | `0.00010` | `0..0.05` | Constant receiver depth bias for point lights. | | `r_shadowMapPointNormalBias` | `0.0010` | `0..0.05` | Extra point-light bias on sloped receivers. | +| `r_shadowMapPointMaxWorldBias` | `4.0` | `0..64` | Conservative world-space ceiling for the combined point-light receiver depth and normal-offset bias. It prevents huge-radius lights from turning small normalized values into large contact gaps. `0` disables the ceiling for comparison. | -Point-light constant and texel-aware receiver bias also use the larger value rather than stacking both terms, so tuning one path does not automatically over-bias the other. The slope-amplified texel-aware term is capped internally so grazing receivers do not create detached shadows. +Point-light constant and texel-aware receiver bias use the larger value rather than stacking both terms. The slope-amplified texel-aware term is bounded internally, and the complete receiver-depth plus normal-offset budget is scaled together when it would exceed `r_shadowMapPointMaxWorldBias`. Practical advice: - Raise bias values slowly in very small steps. @@ -289,6 +297,11 @@ Practical advice: | `r_shadowMapGpuSyncTimings` | `0` | Diagnostic-only GPU-synchronized pass timing using `glFinish`; leave off during normal play. | | `reportShaderPrograms` | n/a | Prints current ARB/GLSL shader validity, including shadow programs. | +If Vulkan cannot submit either a required shadow map or the matching stencil +ownership for one frame, it keeps that receiver's direct light unshadowed and +logs the degradation. This avoids a complete light pop-out; the sticky fallback +requests stencil ownership for later frames. + `r_shadowMapDebugMode` values: - `0`: Off - `1`: Projected shadow atlas/depth @@ -328,9 +341,9 @@ Useful workflow: - If cutout materials cast solid-looking shadows, make sure `r_shadowMapHashedAlpha 1` is enabled and the material is actually alpha-tested with explicit texture coordinates. Unusual animated texgen cutouts may cast conservative solid depth until that stage type is supported. - If translucent shadows are too strong or too noisy, lower `r_shadowMapTranslucentDensity` or disable `r_shadowMapTranslucentMoments`. - If blended materials still do not cast translucent shadows, that material may be outside the currently supported stage set. Common additive pickup orbs are supported, but many particle/effect materials still are not. -- If point-light shadows look too detached, reduce `r_shadowMapPointBias` or `r_shadowMapPointNormalBias`. +- If point-light shadows look too detached, first confirm `r_shadowMapCasterCulling 2` and `r_shadowMapPointMaxWorldBias 4`; then reduce `r_shadowMapPointFilterRadius`, `r_shadowMapPointBias`, or `r_shadowMapPointNormalBias` in small steps. - If projected-light shadows look detached, reduce `r_shadowMapBias`, `r_shadowMapNormalBias`, `r_shadowMapTexelBiasScale`, `r_shadowMapPolygonFactor`, or `r_shadowMapPolygonOffset` in small steps. -- If projected-light acne appears only with large filter radii, try `r_shadowMapReceiverPlaneBias 1` before greatly increasing constant bias. +- If projected-light acne appears only with a large filter radius, prefer a higher shadow-map resolution or a narrower radius before increasing bias. The experimental receiver-plane approximation remains off by default. - If PCSS-lite seems unchanged, confirm `r_shadowMapFilterMode 2` is active; the renderer selects the manual raw-depth path for that mode automatically. - If point-light depth compare causes shader trouble on a driver, leave `r_shadowMapPointDepthCompare 0`; the renderer falls back to the high-precision color-depth cubemap path. - If static-cache reuse hides expected updates while testing unusual content, set `r_shadowMapStaticCache 0` or reduce `r_shadowMapResidentFrames`. diff --git a/docs/user/temporal-presentation.md b/docs/user/temporal-presentation.md new file mode 100644 index 00000000..9b667737 --- /dev/null +++ b/docs/user/temporal-presentation.md @@ -0,0 +1,36 @@ +# Temporal AA And Dynamic Resolution + +openQ4 includes experimental, default-off temporal anti-aliasing/upscaling and +GPU-time dynamic resolution for OpenGL and Vulkan. + +To try temporal AA: + +```cfg +r_temporalAA 1 +``` + +To let the 3D scene adjust its resolution to GPU load as well: + +```cfg +r_rendererDynamicResolution 1 +r_dynamicResolutionMinScale 50 +r_dynamicResolutionMaxScale 100 +``` + +The HUD, menus, and console remain at your native output resolution. Screenshots +and save previews bypass temporal history so they cannot feed captured pixels +back into later gameplay. `r_dynamicResolutionCaptureNative 1` asks known +capture frames to render the 3D scene at full resolution; its default of `0` +keeps the current scale and avoids disturbing the controller. + +For a fixed scale instead of automatic adjustment, leave +`r_rendererDynamicResolution 0` and set `r_screenFraction` below `100`. Temporal +AA uses that lower-resolution 3D scene as TAAU input even with +`r_resolutionScaleMode 0`; UI remains native-sized. + +Use `rendererTemporalPresentationStatus` in the console to inspect the current +scene size, GPU target, delayed timing sample, and history reset reason. Set both +`r_temporalAA 0` and `r_rendererDynamicResolution 0` to restore the established +SMAA/spatial presentation path immediately. + +These features remain experimental and are not enabled automatically. diff --git a/meson.build b/meson.build index 00c0a519..a518cebc 100644 --- a/meson.build +++ b/meson.build @@ -207,7 +207,11 @@ endif if get_option('build_native_tests') openq4_core_safety_test = executable( 'openq4-core-safety-test', - files('tools/tests/native/CoreSafetyTest.cpp'), + files( + 'tools/tests/native/CoreSafetyTest.cpp', + 'src/idlib/CryptoHash.cpp', + 'src/framework/async/Rcon2Protocol.cpp', + ), include_directories: root_include_dir, install: false, ) @@ -216,6 +220,140 @@ if get_option('build_native_tests') openq4_core_safety_test, timeout: 30, ) + + openq4_level_load_cache_format_test = executable( + 'openq4-level-load-cache-format-test', + files( + 'tools/tests/native/LevelLoadCacheFormatTest.cpp', + 'src/framework/LevelLoadCacheFormat.cpp', + ), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-level-load-cache-format', + openq4_level_load_cache_format_test, + timeout: 30, + ) + + openq4_level_load_pipeline_test = executable( + 'openq4-level-load-pipeline-test', + files( + 'tools/tests/native/LevelLoadPipelineTest.cpp', + 'src/framework/LevelLoadPipeline.cpp', + 'src/framework/ParallelJobSystem.cpp', + ), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-level-load-pipeline', + openq4_level_load_pipeline_test, + timeout: 30, + ) + + openq4_job_system_test = executable( + 'openq4-job-system-test', + files( + 'tools/tests/native/ParallelJobSystemTest.cpp', + 'src/framework/ParallelJobSystem.cpp', + ), + include_directories: root_include_dir, + dependencies: dependency('threads', required: true), + install: false, + ) + test( + 'openq4-job-system', + openq4_job_system_test, + timeout: 30, + ) + + openq4_gpu_frame_timing_test = executable( + 'openq4-gpu-frame-timing-test', + files('tools/tests/native/GpuFrameTimingTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-gpu-frame-timing', + openq4_gpu_frame_timing_test, + timeout: 30, + ) + + openq4_temporal_presentation_core_test = executable( + 'openq4-temporal-presentation-core-test', + files('tools/tests/native/TemporalPresentationCoreTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-temporal-presentation-core', + openq4_temporal_presentation_core_test, + timeout: 30, + ) + + openq4_temporal_history_core_test = executable( + 'openq4-temporal-history-core-test', + files('tools/tests/native/TemporalHistoryCoreTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-temporal-history-core', + openq4_temporal_history_core_test, + timeout: 30, + ) + + openq4_renderer_contracts_test = executable( + 'openq4-renderer-contracts-test', + files( + 'tools/tests/native/RendererContractsTest.cpp', + 'src/renderer/RendererContracts.cpp', + ), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-renderer-contracts', + openq4_renderer_contracts_test, + timeout: 30, + ) + + openq4_advanced_lighting_core_test = executable( + 'openq4-advanced-lighting-core-test', + files('tools/tests/native/AdvancedLightingCoreTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-advanced-lighting-core', + openq4_advanced_lighting_core_test, + timeout: 30, + ) + + openq4_advanced_screen_space_core_test = executable( + 'openq4-advanced-screen-space-core-test', + files('tools/tests/native/AdvancedScreenSpaceCoreTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-advanced-screen-space-core', + openq4_advanced_screen_space_core_test, + timeout: 30, + ) + + openq4_specular_probe_atlas_packing_test = executable( + 'openq4-specular-probe-atlas-packing-test', + files('tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp'), + include_directories: root_include_dir, + install: false, + ) + test( + 'openq4-specular-probe-atlas-packing', + openq4_specular_probe_atlas_packing_test, + timeout: 30, + ) endif openq4_pak_files = [] diff --git a/meson_options.txt b/meson_options.txt index b5595801..4b2c42ed 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -124,5 +124,5 @@ option( 'build_native_tests', type: 'boolean', value: false, - description: 'Build the small dependency-free native safety test executable used by validation profiles.', + description: 'Build focused dependency-light native engine tests used by validation profiles.', ) diff --git a/src/cm/CollisionModel_files.cpp b/src/cm/CollisionModel_files.cpp index 66f80f86..be01143b 100644 --- a/src/cm/CollisionModel_files.cpp +++ b/src/cm/CollisionModel_files.cpp @@ -40,6 +40,15 @@ If you have questions concerning this license or the applicable additional terms #include "CollisionModel_local.h" #include "../idlib/LexerFactory.h" +#include +#include +#include +#include +#include +#include +#include +#include + #define CM_FILE_EXT "cm" #define CM_FILEID "CM" #define CM_FILEVERSION "3" @@ -740,3 +749,1202 @@ bool idCollisionModelManagerLocal::LoadCollisionModelFile( const char *name, uns delete src; return true; } + + +/* +=============================================================================== + + Private generated collision cache + + This codec is intentionally unrelated to the retail-compatible text .cm and + binary .cmc paths above. The framework owns the authenticated envelope and + the writable generated/ namespace; this file owns only the pointer-free + collision payload and its transactional adoption. + +=============================================================================== +*/ + +namespace { + static const unsigned int CM_GENERATED_CACHE_MAGIC = 0x434d514fU; // "OQMC" + static const unsigned int CM_GENERATED_CACHE_PAYLOAD_VERSION = 1; + static const unsigned int CM_GENERATED_CACHE_FLAGS = 0; + static const int CM_CACHE_MAX_STRING = 4096; + static const int CM_CACHE_MAX_VERTICES = 1 << 22; + static const int CM_CACHE_MAX_EDGES = 1 << 23; + static const int CM_CACHE_MAX_POLYGONS = 1 << 20; + static const int CM_CACHE_MAX_BRUSHES = 1 << 20; + static const int CM_CACHE_MAX_NODES = 1 << 20; + static const int CM_CACHE_MAX_BRUSH_PLANES = 4096; + static const int CM_CACHE_MAX_REFS = 1 << 24; + static const unsigned int CM_CACHE_ALLOWED_CONTENTS = 0x0fcfffffU; + static const float CM_CACHE_MAX_WORLD_COORDINATE = 1000000000.0f; + static const float CM_CACHE_MAX_TEX_COORDINATE = 1000000000000.0f; + static const uint64_t CM_CACHE_MAX_TRANSFER_BYTES = 512ULL * 1024ULL * 1024ULL; + static const uint64_t CM_CACHE_MAX_ALLOCATION_BYTES = 384ULL * 1024ULL * 1024ULL; + + static bool CM_CacheFloatIsFinite( const float value ) { + return std::isfinite( value ); + } + + static bool CM_CacheVec2IsValid( const idVec2 &value ) { + return CM_CacheFloatIsFinite( value.x ) && CM_CacheFloatIsFinite( value.y ) + && std::fabs( value.x ) <= CM_CACHE_MAX_TEX_COORDINATE + && std::fabs( value.y ) <= CM_CACHE_MAX_TEX_COORDINATE; + } + + static bool CM_CacheVec3IsValid( const idVec3 &value ) { + return CM_CacheFloatIsFinite( value.x ) && CM_CacheFloatIsFinite( value.y ) + && CM_CacheFloatIsFinite( value.z ) + && std::fabs( value.x ) <= CM_CACHE_MAX_WORLD_COORDINATE + && std::fabs( value.y ) <= CM_CACHE_MAX_WORLD_COORDINATE + && std::fabs( value.z ) <= CM_CACHE_MAX_WORLD_COORDINATE; + } + + static bool CM_CachePlaneIsValid( const idPlane &value ) { + return CM_CacheVec3IsValid( value.Normal() ) && CM_CacheFloatIsFinite( value.Dist() ) + && std::fabs( value.Dist() ) <= CM_CACHE_MAX_WORLD_COORDINATE; + } + + static bool CM_CacheBoundsIsValid( const idBounds &value ) { + return CM_CacheVec3IsValid( value[0] ) && CM_CacheVec3IsValid( value[1] ) + && value[0].x <= value[1].x && value[0].y <= value[1].y + && value[0].z <= value[1].z; + } + + static bool CM_CacheStringIsSafe( const char *value, const bool allowEmpty ) { + if ( value == NULL ) { + return allowEmpty; + } + size_t length = 0; + while ( length <= static_cast( CM_CACHE_MAX_STRING ) && value[length] != '\0' ) { + ++length; + } + if ( length > static_cast( CM_CACHE_MAX_STRING ) || ( !allowEmpty && length == 0 ) ) { + return false; + } + for ( size_t i = 0; i < length; ++i ) { + const unsigned char c = static_cast( value[i] ); + if ( c < 0x20 || c == 0x7f ) { + return false; + } + } + return true; + } + + class cmCacheWriter_t { + public: + explicit cmCacheWriter_t( idFile &destination ) : file( destination ), transferred( 0 ), valid( true ) {} + + bool WriteBytes( const void *data, const int length ) { + if ( !valid || length < 0 || ( length > 0 && data == NULL ) + || static_cast( length ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || ( length > 0 && file.Write( data, length ) != length ) ) { + valid = false; + return false; + } + transferred += static_cast( length ); + return true; + } + + bool WriteInt( const int value ) { + if ( !valid || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.WriteInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool WriteUnsignedInt( const unsigned int value ) { + if ( !valid || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.WriteUnsignedInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool WriteBool( const bool value ) { + const unsigned char encoded = value ? 1 : 0; + if ( !valid || transferred >= CM_CACHE_MAX_TRANSFER_BYTES + || file.WriteUnsignedChar( encoded ) != sizeof( encoded ) ) { + valid = false; + return false; + } + ++transferred; + return true; + } + + bool WriteFloat( const float value ) { + if ( !CM_CacheFloatIsFinite( value ) || !valid + || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.WriteFloat( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool WriteVec2( const idVec2 &value ) { + return WriteFloat( value.x ) && WriteFloat( value.y ); + } + + bool WriteVec3( const idVec3 &value ) { + return WriteFloat( value.x ) && WriteFloat( value.y ) && WriteFloat( value.z ); + } + + bool WritePlane( const idPlane &value ) { + return WriteVec3( value.Normal() ) && WriteFloat( value.Dist() ); + } + + bool WriteBounds( const idBounds &value ) { + return WriteVec3( value[0] ) && WriteVec3( value[1] ); + } + + bool WriteString( const char *value, const bool allowEmpty = false ) { + if ( !CM_CacheStringIsSafe( value, allowEmpty ) ) { + valid = false; + return false; + } + const size_t length = strlen( value ); + return WriteInt( static_cast( length ) ) + && WriteBytes( value, static_cast( length ) ); + } + + bool IsValid() const { return valid; } + + private: + idFile &file; + uint64_t transferred; + bool valid; + }; + + class cmCacheReader_t { + public: + explicit cmCacheReader_t( idFile &source ) : + file( source ), end( source.Length() ), transferred( 0 ), allocated( 0 ), valid( true ) { + if ( end < 0 || source.Tell() < 0 || source.Tell() > end + || static_cast( end - source.Tell() ) > CM_CACHE_MAX_TRANSFER_BYTES ) { + valid = false; + } + } + + bool ReadBytes( void *data, const int length ) { + const int position = file.Tell(); + if ( !valid || length < 0 || ( length > 0 && data == NULL ) || position < 0 + || position > end || length > end - position + || static_cast( length ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || ( length > 0 && file.Read( data, length ) != length ) ) { + valid = false; + return false; + } + transferred += static_cast( length ); + return true; + } + + bool ReadInt( int &value ) { + const int position = file.Tell(); + if ( !valid || position < 0 || position > end || sizeof( value ) > static_cast( end - position ) + || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.ReadInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool ReadUnsignedInt( unsigned int &value ) { + const int position = file.Tell(); + if ( !valid || position < 0 || position > end || sizeof( value ) > static_cast( end - position ) + || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.ReadUnsignedInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool ReadBool( bool &value ) { + unsigned char encoded = 0; + const int position = file.Tell(); + if ( !valid || position < 0 || position >= end || transferred >= CM_CACHE_MAX_TRANSFER_BYTES + || file.ReadUnsignedChar( encoded ) != sizeof( encoded ) || encoded > 1 ) { + valid = false; + return false; + } + ++transferred; + value = encoded != 0; + return true; + } + + bool ReadFloat( float &value ) { + const int position = file.Tell(); + if ( !valid || position < 0 || position > end || sizeof( value ) > static_cast( end - position ) + || sizeof( value ) > CM_CACHE_MAX_TRANSFER_BYTES - transferred + || file.ReadFloat( value ) != sizeof( value ) || !CM_CacheFloatIsFinite( value ) ) { + valid = false; + return false; + } + transferred += sizeof( value ); + return true; + } + + bool ReadVec2( idVec2 &value ) { + return ReadFloat( value.x ) && ReadFloat( value.y ); + } + + bool ReadVec3( idVec3 &value ) { + return ReadFloat( value.x ) && ReadFloat( value.y ) && ReadFloat( value.z ); + } + + bool ReadPlane( idPlane &value ) { + idVec3 normal; + float distance = 0.0f; + if ( !ReadVec3( normal ) || !ReadFloat( distance ) ) { + return false; + } + value.SetNormal( normal ); + value.SetDist( distance ); + return true; + } + + bool ReadBounds( idBounds &value ) { + return ReadVec3( value[0] ) && ReadVec3( value[1] ) && CM_CacheBoundsIsValid( value ); + } + + bool Reserve( const int count, const size_t elementSize ) { + if ( !valid || count < 0 || ( count > 0 && elementSize > CM_CACHE_MAX_ALLOCATION_BYTES / static_cast( count ) ) ) { + valid = false; + return false; + } + const uint64_t bytes = static_cast( count ) * static_cast( elementSize ); + if ( bytes > CM_CACHE_MAX_ALLOCATION_BYTES - allocated ) { + valid = false; + return false; + } + allocated += bytes; + return true; + } + + bool ReadCount( int &value, const int maximum, const size_t elementSize ) { + return maximum >= 0 && ReadInt( value ) && value >= 0 && value <= maximum + && Reserve( value, elementSize ); + } + + bool ReadString( idStr &value, const bool allowEmpty = false ) { + int length = 0; + if ( !ReadCount( length, CM_CACHE_MAX_STRING, sizeof( char ) ) || ( !allowEmpty && length == 0 ) ) { + valid = false; + value.Clear(); + return false; + } + value.Fill( ' ', length ); + if ( length > 0 && !ReadBytes( &value[0], length ) ) { + value.Clear(); + return false; + } + if ( !CM_CacheStringIsSafe( value.c_str(), allowEmpty ) ) { + valid = false; + value.Clear(); + return false; + } + return true; + } + + bool AtEnd() const { return valid && file.Tell() == end; } + bool IsValid() const { return valid; } + + private: + idFile &file; + int end; + uint64_t transferred; + uint64_t allocated; + bool valid; + }; + + struct cmCacheEdgeStage_t { + int vertexNum[2]; + int internal; + int numUsers; + idVec3 normal; + }; + + struct cmCachePolygonStage_t { + std::vector edges; + idPlane plane; + idBounds bounds; + idStr materialName; + const idMaterial *material; + int contents; + int primitiveNum; + int featureIndex; + idVec2 texBounds[3]; + + cmCachePolygonStage_t() : material( NULL ), contents( 0 ), primitiveNum( 0 ), featureIndex( 0 ) {} + }; + + struct cmCacheBrushStage_t { + std::vector planes; + idBounds bounds; + idStr materialName; + const idMaterial *material; + int contents; + int primitiveNum; + + cmCacheBrushStage_t() : material( NULL ), contents( 0 ), primitiveNum( 0 ) {} + }; + + struct cmCacheNodeStage_t { + int planeType; + float planeDist; + int children[2]; + std::vector polygons; + std::vector brushes; + + cmCacheNodeStage_t() : planeType( -1 ), planeDist( 0.0f ) { + children[0] = children[1] = -1; + } + }; + + struct cmCacheModelStage_t { + int sourceSlot; + idStr name; + int numPrimitives; + bool isConvex; + std::vector vertices; + std::vector edges; + std::vector polygons; + std::vector brushes; + std::vector nodes; + + cmCacheModelStage_t() : sourceSlot( -1 ), numPrimitives( 0 ), isConvex( false ) {} + }; + + static idStr CM_CacheSourceBase( const char *sourcePath ) { + idStr base = sourcePath != NULL ? sourcePath : ""; + base.BackSlashesToSlashes(); + base.StripFileExtension(); + return base; + } + + static bool CM_CacheModelBelongsToSource( const char *modelName, const char *sourcePath ) { + if ( !CM_CacheStringIsSafe( modelName, false ) ) { + return false; + } + if ( idStr::Cmpn( modelName, PROC_CLIPMODEL_STRING_PRFX, + static_cast( sizeof( PROC_CLIPMODEL_STRING_PRFX ) - 1 ) ) == 0 ) { + return true; + } + idStr prefix = CM_CacheSourceBase( sourcePath ); + prefix += "/"; + return idStr::IcmpnPath( modelName, prefix.c_str(), prefix.Length() ) == 0; + } + + static idStr CM_CacheSettings( const unsigned int mapFileCRC, const char *ownerSettings ) { + return va( "cm-payload=%u;cm-text=%s;geometry=%08x;owner=%s", + CM_GENERATED_CACHE_PAYLOAD_VERSION, CM_FILEVERSION, mapFileCRC, + ownerSettings != NULL ? ownerSettings : "" ); + } + + static bool CM_CacheValidateNodeGraph( const std::vector &nodes ) { + if ( nodes.empty() || nodes.size() > static_cast( CM_CACHE_MAX_NODES ) ) { + return false; + } + std::vector parents( nodes.size(), 0 ); + for ( size_t i = 0; i < nodes.size(); ++i ) { + const cmCacheNodeStage_t &node = nodes[i]; + if ( !CM_CacheFloatIsFinite( node.planeDist ) + || std::fabs( node.planeDist ) > CM_CACHE_MAX_WORLD_COORDINATE ) { + return false; + } + if ( node.planeType == -1 ) { + if ( node.children[0] != -1 || node.children[1] != -1 ) { + return false; + } + continue; + } + if ( node.planeType < 0 || node.planeType > 2 ) { + return false; + } + for ( int side = 0; side < 2; ++side ) { + const int child = node.children[side]; + if ( child <= 0 || child >= static_cast( nodes.size() ) || child == static_cast( i ) + || ++parents[child] != 1 ) { + return false; + } + } + } + if ( parents[0] != 0 ) { + return false; + } + std::vector visited( nodes.size(), 0 ); + std::vector pending( 1, 0 ); + for ( size_t cursor = 0; cursor < pending.size(); ++cursor ) { + const int index = pending[cursor]; + if ( visited[index] != 0 ) { + return false; + } + visited[index] = 1; + if ( nodes[index].planeType != -1 ) { + pending.push_back( nodes[index].children[0] ); + pending.push_back( nodes[index].children[1] ); + } + } + if ( pending.size() != nodes.size() ) { + return false; + } + for ( size_t i = 1; i < parents.size(); ++i ) { + if ( parents[i] != 1 ) { + return false; + } + } + return true; + } + + struct cmCacheLiveModel_t { + std::vector nodes; + std::vector polygons; + std::vector brushes; + std::unordered_map nodeIndices; + std::unordered_map polygonIndices; + std::unordered_map brushIndices; + int polygonRefs; + int brushRefs; + + cmCacheLiveModel_t() : polygonRefs( 0 ), brushRefs( 0 ) {} + }; + + static bool CM_CacheCollectLiveModel( const idCollisionModelLocal *model, cmCacheLiveModel_t &live ) { + if ( model == NULL || model->node == NULL || model->node->parent != NULL + || model->numVertices < 0 || model->numVertices > CM_CACHE_MAX_VERTICES + || model->maxVertices < model->numVertices + || ( model->numVertices > 0 && model->vertices == NULL ) + || model->numEdges < 0 || model->numEdges > CM_CACHE_MAX_EDGES + || model->maxEdges < model->numEdges + || ( model->numEdges > 0 && model->edges == NULL ) + || model->numPrimitives < 0 || !CM_CacheStringIsSafe( model->name.c_str(), false ) ) { + return false; + } + + live.nodeIndices.emplace( model->node, 0 ); + live.nodes.push_back( model->node ); + for ( size_t cursor = 0; cursor < live.nodes.size(); ++cursor ) { + if ( live.nodes.size() > static_cast( CM_CACHE_MAX_NODES ) ) { + return false; + } + const cm_node_t *node = live.nodes[cursor]; + if ( node == NULL || !CM_CacheFloatIsFinite( node->planeDist ) + || std::fabs( node->planeDist ) > CM_CACHE_MAX_WORLD_COORDINATE ) { + return false; + } + if ( node->planeType == -1 ) { + if ( node->children[0] != NULL || node->children[1] != NULL ) { + return false; + } + } else { + if ( node->planeType < 0 || node->planeType > 2 || node->children[0] == NULL + || node->children[1] == NULL || node->children[0] == node->children[1] ) { + return false; + } + for ( int side = 0; side < 2; ++side ) { + const cm_node_t *child = node->children[side]; + if ( child->parent != node || live.nodeIndices.find( child ) != live.nodeIndices.end() ) { + return false; + } + const int index = static_cast( live.nodes.size() ); + live.nodeIndices.emplace( child, index ); + live.nodes.push_back( child ); + } + } + + std::unordered_set polygonRefSet; + for ( const cm_polygonRef_t *ref = node->polygons; ref != NULL; ref = ref->next ) { + if ( ref->p == NULL || !polygonRefSet.insert( ref ).second + || live.polygonRefs >= CM_CACHE_MAX_REFS ) { + return false; + } + ++live.polygonRefs; + if ( live.polygonIndices.find( ref->p ) == live.polygonIndices.end() ) { + if ( live.polygons.size() >= static_cast( CM_CACHE_MAX_POLYGONS ) ) { + return false; + } + const int index = static_cast( live.polygons.size() ); + live.polygonIndices.emplace( ref->p, index ); + live.polygons.push_back( ref->p ); + } + } + + std::unordered_set brushRefSet; + for ( const cm_brushRef_t *ref = node->brushes; ref != NULL; ref = ref->next ) { + if ( ref->b == NULL || !brushRefSet.insert( ref ).second + || live.brushRefs >= CM_CACHE_MAX_REFS ) { + return false; + } + ++live.brushRefs; + if ( live.brushIndices.find( ref->b ) == live.brushIndices.end() ) { + if ( live.brushes.size() >= static_cast( CM_CACHE_MAX_BRUSHES ) ) { + return false; + } + const int index = static_cast( live.brushes.size() ); + live.brushIndices.emplace( ref->b, index ); + live.brushes.push_back( ref->b ); + } + } + } + + if ( model->numNodes != static_cast( live.nodes.size() ) + || model->numPolygons != static_cast( live.polygons.size() ) + || model->numBrushes != static_cast( live.brushes.size() ) + || model->numPolygonRefs != live.polygonRefs || model->numBrushRefs != live.brushRefs ) { + return false; + } + return true; + } +} + +/* +================ +idCollisionModelManagerLocal::WriteGeneratedCollisionCache +================ +*/ +void idCollisionModelManagerLocal::WriteGeneratedCollisionCache( const char *sourcePath, + unsigned int mapFileCRC, const char *ownerSettings ) { + if ( sourcePath == NULL || sourcePath[0] == '\0' || models == NULL ) { + return; + } + + struct record_t { + int slot; + const idCollisionModelLocal *model; + }; + std::vector records; + for ( int i = 0; i < numModels; ++i ) { + const idCollisionModelLocal *model = models[i]; + if ( model == NULL || model->fileTime == static_cast( -1 ) + || !CM_CacheModelBelongsToSource( model->name.c_str(), sourcePath ) ) { + continue; + } + record_t record; + record.slot = i; + record.model = model; + records.push_back( record ); + } + if ( records.empty() || records.size() > static_cast( MAX_SUBMODELS ) ) { + return; + } + + idFile_Memory payload( "generated-collision-cache-payload" ); + cmCacheWriter_t writer( payload ); + if ( !writer.WriteUnsignedInt( CM_GENERATED_CACHE_MAGIC ) + || !writer.WriteUnsignedInt( CM_GENERATED_CACHE_PAYLOAD_VERSION ) + || !writer.WriteUnsignedInt( CM_GENERATED_CACHE_FLAGS ) + || !writer.WriteUnsignedInt( mapFileCRC ) + || !writer.WriteInt( static_cast( records.size() ) ) ) { + return; + } + + for ( size_t recordIndex = 0; recordIndex < records.size(); ++recordIndex ) { + const idCollisionModelLocal *model = records[recordIndex].model; + cmCacheLiveModel_t live; + if ( !CM_CacheCollectLiveModel( model, live ) + || !writer.WriteInt( records[recordIndex].slot ) + || !writer.WriteString( model->name.c_str() ) + || !writer.WriteInt( model->numPrimitives ) + || !writer.WriteBool( model->isConvex ) + || !writer.WriteInt( model->numVertices ) ) { + return; + } + + for ( int i = 0; i < model->numVertices; ++i ) { + if ( !CM_CacheVec3IsValid( model->vertices[i].p ) || !writer.WriteVec3( model->vertices[i].p ) ) { + return; + } + } + + if ( !writer.WriteInt( model->numEdges ) ) { + return; + } + for ( int i = 0; i < model->numEdges; ++i ) { + const cm_edge_t &edge = model->edges[i]; + if ( edge.vertexNum[0] < 0 || edge.vertexNum[0] >= model->numVertices + || edge.vertexNum[1] < 0 || edge.vertexNum[1] >= model->numVertices + || !CM_CacheVec3IsValid( edge.normal ) + || !writer.WriteInt( edge.vertexNum[0] ) || !writer.WriteInt( edge.vertexNum[1] ) + || !writer.WriteInt( edge.internal ) || !writer.WriteInt( edge.numUsers ) + || !writer.WriteVec3( edge.normal ) ) { + return; + } + } + + if ( !writer.WriteInt( static_cast( live.polygons.size() ) ) ) { + return; + } + std::unordered_set featureIndices; + for ( size_t i = 0; i < live.polygons.size(); ++i ) { + const cm_polygon_t *polygon = live.polygons[i]; + if ( polygon == NULL || polygon->material == NULL || polygon->numEdges < 3 + || polygon->numEdges > CM_MAX_POLYGON_EDGES + || !CM_CachePlaneIsValid( polygon->plane ) || !CM_CacheBoundsIsValid( polygon->bounds ) + || !CM_CacheStringIsSafe( polygon->material->GetName(), false ) + || polygon->featureIndex <= 0 || !featureIndices.insert( polygon->featureIndex ).second + || !writer.WriteInt( polygon->numEdges ) ) { + return; + } + for ( int edgeIndex = 0; edgeIndex < polygon->numEdges; ++edgeIndex ) { + const int encodedEdge = polygon->edges[edgeIndex]; + if ( encodedEdge == ( std::numeric_limits::min )() + || std::abs( encodedEdge ) >= model->numEdges || !writer.WriteInt( encodedEdge ) ) { + return; + } + } + if ( !writer.WritePlane( polygon->plane ) || !writer.WriteBounds( polygon->bounds ) + || !writer.WriteString( polygon->material->GetName() ) + || !writer.WriteInt( polygon->contents ) || !writer.WriteInt( polygon->primitiveNum ) + || !writer.WriteInt( polygon->featureIndex ) ) { + return; + } + for ( int texBound = 0; texBound < 3; ++texBound ) { + if ( !CM_CacheVec2IsValid( polygon->texBounds[texBound] ) + || !writer.WriteVec2( polygon->texBounds[texBound] ) ) { + return; + } + } + } + + if ( !writer.WriteInt( static_cast( live.brushes.size() ) ) ) { + return; + } + for ( size_t i = 0; i < live.brushes.size(); ++i ) { + const cm_brush_t *brush = live.brushes[i]; + const char *materialName = brush != NULL && brush->material != NULL ? brush->material->GetName() : ""; + if ( brush == NULL || brush->numPlanes <= 0 || brush->numPlanes > CM_CACHE_MAX_BRUSH_PLANES + || brush->contents == 0 + || ( static_cast( brush->contents ) & ~CM_CACHE_ALLOWED_CONTENTS ) != 0 + || !CM_CacheBoundsIsValid( brush->bounds ) + || !CM_CacheStringIsSafe( materialName, true ) + || !writer.WriteInt( brush->numPlanes ) ) { + return; + } + for ( int planeIndex = 0; planeIndex < brush->numPlanes; ++planeIndex ) { + if ( !CM_CachePlaneIsValid( brush->planes[planeIndex] ) + || !writer.WritePlane( brush->planes[planeIndex] ) ) { + return; + } + } + if ( !writer.WriteBounds( brush->bounds ) || !writer.WriteInt( brush->contents ) + || !writer.WriteString( materialName, true ) || !writer.WriteInt( brush->primitiveNum ) ) { + return; + } + } + + if ( !writer.WriteInt( static_cast( live.nodes.size() ) ) ) { + return; + } + for ( size_t i = 0; i < live.nodes.size(); ++i ) { + const cm_node_t *node = live.nodes[i]; + const int child0 = node->planeType == -1 ? -1 : live.nodeIndices[node->children[0]]; + const int child1 = node->planeType == -1 ? -1 : live.nodeIndices[node->children[1]]; + if ( !writer.WriteInt( node->planeType ) || !writer.WriteFloat( node->planeDist ) + || !writer.WriteInt( child0 ) || !writer.WriteInt( child1 ) ) { + return; + } + + int polygonRefCount = 0; + for ( const cm_polygonRef_t *ref = node->polygons; ref != NULL; ref = ref->next ) { + ++polygonRefCount; + } + if ( !writer.WriteInt( polygonRefCount ) ) { + return; + } + for ( const cm_polygonRef_t *ref = node->polygons; ref != NULL; ref = ref->next ) { + const std::unordered_map::const_iterator found = live.polygonIndices.find( ref->p ); + if ( found == live.polygonIndices.end() || !writer.WriteInt( found->second ) ) { + return; + } + } + + int brushRefCount = 0; + for ( const cm_brushRef_t *ref = node->brushes; ref != NULL; ref = ref->next ) { + ++brushRefCount; + } + if ( !writer.WriteInt( brushRefCount ) ) { + return; + } + for ( const cm_brushRef_t *ref = node->brushes; ref != NULL; ref = ref->next ) { + const std::unordered_map::const_iterator found = live.brushIndices.find( ref->b ); + if ( found == live.brushIndices.end() || !writer.WriteInt( found->second ) ) { + return; + } + } + } + } + + if ( !writer.IsValid() || payload.Length() <= 0 + || static_cast( payload.Length() ) > CM_CACHE_MAX_TRANSFER_BYTES ) { + return; + } + const idStr settings = CM_CacheSettings( mapFileCRC, ownerSettings ); + fileSystem->WriteGeneratedCache( GENERATED_CACHE_COLLISION_MODEL, sourcePath, + CM_GENERATED_CACHE_PAYLOAD_VERSION, settings.c_str(), payload.GetDataPtr(), + static_cast( payload.Length() ) ); +} + +namespace { + static bool CM_CacheProcClipOrdinal( const char *name, int &ordinal ) { + ordinal = -1; + if ( name == NULL ) { + return false; + } + const int prefixLength = static_cast( sizeof( PROC_CLIPMODEL_STRING_PRFX ) - 1 ); + if ( idStr::Cmpn( name, PROC_CLIPMODEL_STRING_PRFX, prefixLength ) != 0 ) { + return false; + } + const char *digits = name + prefixLength; + if ( digits[0] == '\0' ) { + return false; + } + uint64_t value = 0; + for ( const char *cursor = digits; cursor[0] != '\0'; ++cursor ) { + if ( cursor[0] < '0' || cursor[0] > '9' ) { + return false; + } + value = value * 10 + static_cast( cursor[0] - '0' ); + if ( value >= static_cast( MAX_SUBMODELS ) ) { + return false; + } + } + ordinal = static_cast( value ); + return idStr::Cmp( name, va( "%s%d", PROC_CLIPMODEL_STRING_PRFX, ordinal ) ) == 0; + } + + static bool CM_CacheModelNameMatches( const char *lhs, const char *rhs ) { + return lhs != NULL && rhs != NULL && idStr::IcmpPath( lhs, rhs ) == 0; + } +} + +/* +================ +idCollisionModelManagerLocal::LoadGeneratedCollisionCache +================ +*/ +bool idCollisionModelManagerLocal::LoadGeneratedCollisionCache( const char *sourcePath, + unsigned int mapFileCRC, const char *ownerSettings ) { + if ( sourcePath == NULL || sourcePath[0] == '\0' ) { + return false; + } + const idStr settings = CM_CacheSettings( mapFileCRC, ownerSettings ); + idFile *cacheFile = fileSystem->OpenGeneratedCacheRead( GENERATED_CACHE_COLLISION_MODEL, + sourcePath, CM_GENERATED_CACHE_PAYLOAD_VERSION, settings.c_str() ); + if ( cacheFile == NULL ) { + return false; + } + + cmCacheReader_t reader( *cacheFile ); + unsigned int magic = 0; + unsigned int version = 0; + unsigned int flags = 0; + unsigned int cachedCRC = 0; + int modelCount = 0; + bool valid = reader.ReadUnsignedInt( magic ) && magic == CM_GENERATED_CACHE_MAGIC + && reader.ReadUnsignedInt( version ) && version == CM_GENERATED_CACHE_PAYLOAD_VERSION + && reader.ReadUnsignedInt( flags ) && flags == CM_GENERATED_CACHE_FLAGS + && reader.ReadUnsignedInt( cachedCRC ) && cachedCRC == mapFileCRC + && reader.ReadCount( modelCount, MAX_SUBMODELS, sizeof( cmCacheModelStage_t ) ) + && modelCount > 0; + + std::vector stagedModels; + if ( valid ) { + stagedModels.resize( modelCount ); + } + std::unordered_set sourceSlots; + bool foundWorldModel = false; + const idStr worldModelName = CM_CacheSourceBase( sourcePath ) + "/" WORLD_MODEL_NAME; + + for ( int modelIndex = 0; valid && modelIndex < modelCount; ++modelIndex ) { + cmCacheModelStage_t &model = stagedModels[modelIndex]; + int vertexCount = 0; + int edgeCount = 0; + int polygonCount = 0; + int brushCount = 0; + int nodeCount = 0; + valid = reader.ReadInt( model.sourceSlot ) && model.sourceSlot >= 0 + && model.sourceSlot < MAX_SUBMODELS && sourceSlots.insert( model.sourceSlot ).second + && reader.ReadString( model.name ) + && CM_CacheModelBelongsToSource( model.name.c_str(), sourcePath ) + && reader.ReadInt( model.numPrimitives ) && model.numPrimitives >= 0 + && model.numPrimitives <= CM_CACHE_MAX_REFS + && reader.ReadBool( model.isConvex ) + && reader.ReadCount( vertexCount, CM_CACHE_MAX_VERTICES, sizeof( idVec3 ) ); + if ( !valid ) { + break; + } + if ( CM_CacheModelNameMatches( model.name.c_str(), worldModelName.c_str() ) ) { + if ( foundWorldModel ) { + valid = false; + break; + } + foundWorldModel = true; + } + + model.vertices.resize( vertexCount ); + for ( int i = 0; valid && i < vertexCount; ++i ) { + valid = reader.ReadVec3( model.vertices[i] ) && CM_CacheVec3IsValid( model.vertices[i] ); + } + + valid = valid && reader.ReadCount( edgeCount, CM_CACHE_MAX_EDGES, sizeof( cmCacheEdgeStage_t ) ); + if ( !valid ) { + break; + } + model.edges.resize( edgeCount ); + for ( int i = 0; valid && i < edgeCount; ++i ) { + cmCacheEdgeStage_t &edge = model.edges[i]; + valid = reader.ReadInt( edge.vertexNum[0] ) && reader.ReadInt( edge.vertexNum[1] ) + && edge.vertexNum[0] >= 0 && edge.vertexNum[0] < vertexCount + && edge.vertexNum[1] >= 0 && edge.vertexNum[1] < vertexCount + && reader.ReadInt( edge.internal ) && edge.internal >= 0 + && edge.internal <= ( std::numeric_limits::max )() + && reader.ReadInt( edge.numUsers ) && edge.numUsers >= 0 + && edge.numUsers <= ( std::numeric_limits::max )() + && reader.ReadVec3( edge.normal ) && CM_CacheVec3IsValid( edge.normal ); + } + + valid = valid && reader.ReadCount( polygonCount, CM_CACHE_MAX_POLYGONS, sizeof( cmCachePolygonStage_t ) ); + if ( !valid ) { + break; + } + model.polygons.resize( polygonCount ); + std::unordered_set featureIndices; + for ( int i = 0; valid && i < polygonCount; ++i ) { + cmCachePolygonStage_t &polygon = model.polygons[i]; + int polygonEdgeCount = 0; + valid = reader.ReadCount( polygonEdgeCount, CM_MAX_POLYGON_EDGES, sizeof( int ) ) + && polygonEdgeCount >= 3; + if ( !valid ) { + break; + } + polygon.edges.resize( polygonEdgeCount ); + for ( int edgeIndex = 0; valid && edgeIndex < polygonEdgeCount; ++edgeIndex ) { + valid = reader.ReadInt( polygon.edges[edgeIndex] ) + && polygon.edges[edgeIndex] != ( std::numeric_limits::min )() + && std::abs( polygon.edges[edgeIndex] ) < edgeCount; + } + valid = valid && reader.ReadPlane( polygon.plane ) && CM_CachePlaneIsValid( polygon.plane ) + && reader.ReadBounds( polygon.bounds ) + && reader.ReadString( polygon.materialName ) + && reader.ReadInt( polygon.contents ) + && reader.ReadInt( polygon.primitiveNum ) && polygon.primitiveNum >= 0 + && ( model.numPrimitives == 0 || polygon.primitiveNum < model.numPrimitives ) + && reader.ReadInt( polygon.featureIndex ) && polygon.featureIndex > 0 + && featureIndices.insert( polygon.featureIndex ).second; + for ( int texBound = 0; valid && texBound < 3; ++texBound ) { + valid = reader.ReadVec2( polygon.texBounds[texBound] ) + && CM_CacheVec2IsValid( polygon.texBounds[texBound] ); + } + } + + valid = valid && reader.ReadCount( brushCount, CM_CACHE_MAX_BRUSHES, sizeof( cmCacheBrushStage_t ) ); + if ( !valid ) { + break; + } + model.brushes.resize( brushCount ); + for ( int i = 0; valid && i < brushCount; ++i ) { + cmCacheBrushStage_t &brush = model.brushes[i]; + int planeCount = 0; + valid = reader.ReadCount( planeCount, CM_CACHE_MAX_BRUSH_PLANES, sizeof( idPlane ) ) + && planeCount > 0; + if ( !valid ) { + break; + } + brush.planes.resize( planeCount ); + for ( int planeIndex = 0; valid && planeIndex < planeCount; ++planeIndex ) { + valid = reader.ReadPlane( brush.planes[planeIndex] ) + && CM_CachePlaneIsValid( brush.planes[planeIndex] ); + } + valid = valid && reader.ReadBounds( brush.bounds ) + && reader.ReadInt( brush.contents ) && brush.contents != 0 + && ( static_cast( brush.contents ) & ~CM_CACHE_ALLOWED_CONTENTS ) == 0 + && reader.ReadString( brush.materialName, true ) + && reader.ReadInt( brush.primitiveNum ) && brush.primitiveNum >= 0 + && ( model.numPrimitives == 0 || brush.primitiveNum < model.numPrimitives ); + } + if ( valid && model.numPrimitives == 0 && ( polygonCount > 0 || brushCount > 0 ) ) { + valid = false; + } + + valid = valid && reader.ReadCount( nodeCount, CM_CACHE_MAX_NODES, sizeof( cmCacheNodeStage_t ) ) + && nodeCount > 0; + if ( !valid ) { + break; + } + model.nodes.resize( nodeCount ); + std::vector polygonUses( polygonCount, 0 ); + std::vector brushUses( brushCount, 0 ); + int totalPolygonRefs = 0; + int totalBrushRefs = 0; + for ( int i = 0; valid && i < nodeCount; ++i ) { + cmCacheNodeStage_t &node = model.nodes[i]; + int polygonRefCount = 0; + int brushRefCount = 0; + valid = reader.ReadInt( node.planeType ) && reader.ReadFloat( node.planeDist ) + && reader.ReadInt( node.children[0] ) && reader.ReadInt( node.children[1] ) + && reader.ReadCount( polygonRefCount, CM_CACHE_MAX_REFS - totalPolygonRefs, sizeof( int ) ); + if ( !valid ) { + break; + } + totalPolygonRefs += polygonRefCount; + node.polygons.resize( polygonRefCount ); + std::unordered_set localPolygons; + for ( int refIndex = 0; valid && refIndex < polygonRefCount; ++refIndex ) { + valid = reader.ReadInt( node.polygons[refIndex] ) + && node.polygons[refIndex] >= 0 && node.polygons[refIndex] < polygonCount + && localPolygons.insert( node.polygons[refIndex] ).second; + if ( valid ) { + ++polygonUses[node.polygons[refIndex]]; + } + } + valid = valid && reader.ReadCount( brushRefCount, CM_CACHE_MAX_REFS - totalBrushRefs, sizeof( int ) ); + if ( !valid ) { + break; + } + totalBrushRefs += brushRefCount; + node.brushes.resize( brushRefCount ); + std::unordered_set localBrushes; + for ( int refIndex = 0; valid && refIndex < brushRefCount; ++refIndex ) { + valid = reader.ReadInt( node.brushes[refIndex] ) + && node.brushes[refIndex] >= 0 && node.brushes[refIndex] < brushCount + && localBrushes.insert( node.brushes[refIndex] ).second; + if ( valid ) { + ++brushUses[node.brushes[refIndex]]; + } + } + } + valid = valid && CM_CacheValidateNodeGraph( model.nodes ); + for ( int i = 0; valid && i < polygonCount; ++i ) { + valid = polygonUses[i] > 0; + } + for ( int i = 0; valid && i < brushCount; ++i ) { + valid = brushUses[i] > 0; + } + } + + valid = valid && foundWorldModel && reader.AtEnd(); + fileSystem->CloseFile( cacheFile ); + if ( !valid ) { + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_COLLISION_MODEL, sourcePath, + CM_GENERATED_CACHE_PAYLOAD_VERSION, settings.c_str() ); + common->Warning( "Discarded malformed generated collision cache for %s", sourcePath ); + return false; + } + + // Resolve declarations only after the complete pointer-free structure has + // passed its bounds and graph checks. Missing or changed declarations make + // the payload non-adoptable and fall back to the authoritative source. + int procClipCount = 0; + std::vector procClipModelForOrdinal( MAX_SUBMODELS, -1 ); + for ( int modelIndex = 0; valid && modelIndex < modelCount; ++modelIndex ) { + cmCacheModelStage_t &model = stagedModels[modelIndex]; + int ordinal = -1; + if ( CM_CacheProcClipOrdinal( model.name.c_str(), ordinal ) ) { + if ( ordinal < 0 || ordinal >= MAX_SUBMODELS + || model.sourceSlot != PROC_CLIPMODEL_INDEX_START + ordinal + || procClipModelForOrdinal[ordinal] != -1 ) { + valid = false; + break; + } + procClipModelForOrdinal[ordinal] = modelIndex; + ++procClipCount; + } + for ( size_t i = 0; valid && i < model.polygons.size(); ++i ) { + cmCachePolygonStage_t &polygon = model.polygons[i]; + polygon.material = declManager->FindMaterial( polygon.materialName.c_str() ); + valid = polygon.material != NULL + && idStr::IcmpPath( polygon.material->GetName(), polygon.materialName.c_str() ) == 0 + && polygon.material->GetContentFlags() == polygon.contents; + } + for ( size_t i = 0; valid && i < model.brushes.size(); ++i ) { + cmCacheBrushStage_t &brush = model.brushes[i]; + if ( brush.materialName.IsEmpty() ) { + brush.material = NULL; + } else { + brush.material = declManager->FindMaterial( brush.materialName.c_str() ); + valid = brush.material != NULL + && idStr::IcmpPath( brush.material->GetName(), brush.materialName.c_str() ) == 0; + } + } + } + for ( int ordinal = 0; valid && ordinal < procClipCount; ++ordinal ) { + valid = procClipModelForOrdinal[ordinal] >= 0; + } + if ( !valid ) { + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_COLLISION_MODEL, sourcePath, + CM_GENERATED_CACHE_PAYLOAD_VERSION, settings.c_str() ); + common->Warning( "Discarded generated collision cache with invalid declarations for %s", sourcePath ); + return false; + } + + // Construct a completely detached model set. All allocations and pointer + // wiring finish before the live table is touched. + std::vector builtModels( modelCount, NULL ); + for ( int modelIndex = 0; modelIndex < modelCount; ++modelIndex ) { + const cmCacheModelStage_t &stage = stagedModels[modelIndex]; + idCollisionModelLocal *model = AllocModel(); + builtModels[modelIndex] = model; + model->name = stage.name; + model->fileTime = mapFileCRC; + model->numPrimitives = stage.numPrimitives; + model->isConvex = stage.isConvex; + model->maxVertices = model->numVertices = static_cast( stage.vertices.size() ); + if ( model->numVertices > 0 ) { + model->vertices = static_cast( Mem_ClearedAlloc( + static_cast( model->numVertices ) * sizeof( cm_vertex_t ) ) ); + for ( int i = 0; i < model->numVertices; ++i ) { + model->vertices[i].p = stage.vertices[i]; + } + } + + model->maxEdges = model->numEdges = static_cast( stage.edges.size() ); + if ( model->numEdges > 0 ) { + model->edges = static_cast( Mem_ClearedAlloc( + static_cast( model->numEdges ) * sizeof( cm_edge_t ) ) ); + for ( int i = 0; i < model->numEdges; ++i ) { + model->edges[i].vertexNum[0] = stage.edges[i].vertexNum[0]; + model->edges[i].vertexNum[1] = stage.edges[i].vertexNum[1]; + model->edges[i].internal = static_cast( stage.edges[i].internal ); + model->edges[i].numUsers = static_cast( stage.edges[i].numUsers ); + model->edges[i].normal = stage.edges[i].normal; + model->numInternalEdges += stage.edges[i].internal; + } + } + + std::vector polygons( stage.polygons.size(), NULL ); + for ( size_t i = 0; i < stage.polygons.size(); ++i ) { + const cmCachePolygonStage_t &source = stage.polygons[i]; + cm_polygon_t *polygon = AllocPolygon( model, static_cast( source.edges.size() ) ); + polygons[i] = polygon; + polygon->numEdges = static_cast( source.edges.size() ); + for ( int edgeIndex = 0; edgeIndex < polygon->numEdges; ++edgeIndex ) { + polygon->edges[edgeIndex] = source.edges[edgeIndex]; + } + polygon->plane = source.plane; + polygon->bounds = source.bounds; + polygon->material = source.material; + polygon->contents = source.contents; + polygon->primitiveNum = source.primitiveNum; + polygon->featureIndex = source.featureIndex; + for ( int texBound = 0; texBound < 3; ++texBound ) { + polygon->texBounds[texBound] = source.texBounds[texBound]; + } + } + + std::vector brushes( stage.brushes.size(), NULL ); + for ( size_t i = 0; i < stage.brushes.size(); ++i ) { + const cmCacheBrushStage_t &source = stage.brushes[i]; + cm_brush_t *brush = AllocBrush( model, static_cast( source.planes.size() ) ); + brushes[i] = brush; + brush->numPlanes = static_cast( source.planes.size() ); + for ( int planeIndex = 0; planeIndex < brush->numPlanes; ++planeIndex ) { + brush->planes[planeIndex] = source.planes[planeIndex]; + } + brush->bounds = source.bounds; + brush->contents = source.contents; + brush->material = source.material; + brush->primitiveNum = source.primitiveNum; + brush->checkcount = 0; + } + + std::vector nodes( stage.nodes.size(), NULL ); + for ( size_t i = 0; i < stage.nodes.size(); ++i ) { + nodes[i] = AllocNode( model, model->numNodes < NODE_BLOCK_SIZE_SMALL + ? NODE_BLOCK_SIZE_SMALL : NODE_BLOCK_SIZE_LARGE ); + ++model->numNodes; + nodes[i]->planeType = stage.nodes[i].planeType; + nodes[i]->planeDist = stage.nodes[i].planeDist; + } + for ( size_t i = 0; i < stage.nodes.size(); ++i ) { + if ( stage.nodes[i].planeType == -1 ) { + continue; + } + for ( int side = 0; side < 2; ++side ) { + nodes[i]->children[side] = nodes[stage.nodes[i].children[side]]; + nodes[stage.nodes[i].children[side]]->parent = nodes[i]; + } + } + model->node = nodes[0]; + for ( size_t i = 0; i < stage.nodes.size(); ++i ) { + for ( int refIndex = static_cast( stage.nodes[i].polygons.size() ) - 1; refIndex >= 0; --refIndex ) { + AddPolygonToNode( model, nodes[i], polygons[stage.nodes[i].polygons[refIndex]] ); + } + for ( int refIndex = static_cast( stage.nodes[i].brushes.size() ) - 1; refIndex >= 0; --refIndex ) { + AddBrushToNode( model, nodes[i], brushes[stage.nodes[i].brushes[refIndex]] ); + } + } + + CM_GetNodeBounds( &model->bounds, model->node ); + model->contents = CM_GetNodeContents( model->node ); + model->usedMemory = model->numVertices * sizeof( cm_vertex_t ) + + model->numEdges * sizeof( cm_edge_t ) + model->polygonMemory + model->brushMemory + + model->numNodes * sizeof( cm_node_t ) + + model->numPolygonRefs * sizeof( cm_polygonRef_t ) + + model->numBrushRefs * sizeof( cm_brushRef_t ); + } + + // Cache slots are treated as hints, but adoption cannot evict an in-use + // collision model. Reject before any mutation if the learned layout is no + // longer compatible with the live table. + EnsureModelTable(); + for ( int i = 0; valid && i < numModels; ++i ) { + const idCollisionModelLocal *existing = models[i]; + if ( existing == NULL ) { + continue; + } + bool replace = existing->name.Cmpn( PROC_CLIPMODEL_STRING_PRFX, + static_cast( sizeof( PROC_CLIPMODEL_STRING_PRFX ) - 1 ) ) == 0; + for ( int modelIndex = 0; !replace && modelIndex < modelCount; ++modelIndex ) { + replace = CM_CacheModelNameMatches( existing->name.c_str(), stagedModels[modelIndex].name.c_str() ) + || stagedModels[modelIndex].sourceSlot == i; + } + if ( replace ) { + valid = existing->refCount <= 0 && !existing->isTrmModel; + } + } + if ( !valid ) { + for ( size_t i = 0; i < builtModels.size(); ++i ) { + DestroyModel( builtModels[i] ); + } + return false; + } + + // Remove stale proc-clip entries and duplicate names before installing the + // detached set. No validation or allocation remains after this point. + for ( int i = 0; i < numModels; ++i ) { + idCollisionModelLocal *existing = models[i]; + if ( existing == NULL ) { + continue; + } + bool replace = existing->name.Cmpn( PROC_CLIPMODEL_STRING_PRFX, + static_cast( sizeof( PROC_CLIPMODEL_STRING_PRFX ) - 1 ) ) == 0; + for ( int modelIndex = 0; !replace && modelIndex < modelCount; ++modelIndex ) { + replace = CM_CacheModelNameMatches( existing->name.c_str(), stagedModels[modelIndex].name.c_str() ) + || stagedModels[modelIndex].sourceSlot == i; + } + if ( replace ) { + DestroyModel( existing ); + models[i] = NULL; + } + } + int highestSlot = numModels - 1; + for ( int modelIndex = 0; modelIndex < modelCount; ++modelIndex ) { + const int slot = stagedModels[modelIndex].sourceSlot; + models[slot] = builtModels[modelIndex]; + builtModels[modelIndex] = NULL; + highestSlot = Max( highestSlot, slot ); + } + numModels = highestSlot + 1; + numInlinedProcClipModels = procClipCount; + return true; +} diff --git a/src/cm/CollisionModel_load.cpp b/src/cm/CollisionModel_load.cpp index cb3bc442..fe463aaf 100644 --- a/src/cm/CollisionModel_load.cpp +++ b/src/cm/CollisionModel_load.cpp @@ -4090,15 +4090,128 @@ void idCollisionModelManagerLocal::PrintMemInfo( MemInfo *mi ) { idCollisionModelManagerLocal::BuildModels ================ */ +static bool CM_GeneratedCacheSourceExists( const char *path ) { + idFile *file = fileSystem->OpenFileRead( path, false ); + if ( file == NULL ) { + return false; + } + fileSystem->CloseFile( file ); + return true; +} + +static bool CM_GeneratedCacheProcSettings( const char *mapName, idStr &settings ) { + idStr sourceProcPath = mapName; + sourceProcPath.SetFileExtension( PROC_FILE_EXT ); + idStr resolvedProcPath = sourceProcPath; + idFile *procFile = NULL; + + // LexerFactory enables LEXFL_READBINARY when com_binaryRead is set. The + // lexer then opens the compiled suffix first and falls back to the text + // source only when that open fails. Select the fingerprint source in the + // same order so a newly-created or changed .procc cannot reuse a cache that + // was built from the .proc file. + if ( cvarSystem->GetCVarBool( "com_binaryRead" ) ) { + idStr compiledProcPath = sourceProcPath; + compiledProcPath += Lexer::sCompiledFileSuffix; + procFile = fileSystem->OpenFileRead( compiledProcPath.c_str(), false ); + if ( procFile != NULL ) { + resolvedProcPath = compiledProcPath; + } + } + if ( procFile == NULL ) { + procFile = fileSystem->OpenFileRead( sourceProcPath.c_str(), false ); + } + if ( procFile == NULL ) { + settings = va( "mode=map-proc;proc=%s;state=missing", sourceProcPath.c_str() ); + return false; + } + + const int length = procFile->Length(); + const ID_TIME_T timestamp = procFile->Timestamp(); + const unsigned int containerChecksum = static_cast( procFile->GetContainerChecksum() ); + uint32_t looseChecksum = 0; + bool complete = length >= 0; + if ( complete && containerChecksum == 0 ) { + CRC32_InitChecksum( looseChecksum ); + idList buffer; + buffer.SetNum( 64 * 1024 ); + procFile->Rewind(); + int remaining = length; + while ( remaining > 0 ) { + const int requested = Min( remaining, buffer.Num() ); + const int bytesRead = procFile->Read( buffer.Ptr(), requested ); + if ( bytesRead != requested ) { + complete = false; + break; + } + CRC32_UpdateChecksum( looseChecksum, buffer.Ptr(), bytesRead ); + remaining -= bytesRead; + } + if ( complete ) { + CRC32_FinishChecksum( looseChecksum ); + } + } + fileSystem->CloseFile( procFile ); + settings = va( "mode=map-proc;proc=%s;length=%d;timestamp=%lld;container=%08x;loose-crc=%08x;complete=%d", + resolvedProcPath.c_str(), length, static_cast( timestamp ), containerChecksum, + looseChecksum, complete ? 1 : 0 ); + return complete; +} + void idCollisionModelManagerLocal::BuildModels( const idMapFile *mapFile, bool forceCreateMap) { int i; const idMapEntity *mapEnt; + idStr mapSource = mapFile->GetName(); + mapSource.SetFileExtension( "map" ); + idStr authoredTextSource = mapFile->GetName(); + authoredTextSource.SetFileExtension( "cm" ); + idStr authoredBinarySource = authoredTextSource; + authoredBinarySource += "c"; + + bool hasAuthoredSource = false; + idStr generatedCacheSource = mapSource; + if ( !forceCreateMap && cvarSystem->GetCVarBool( "com_binaryRead" ) + && CM_GeneratedCacheSourceExists( authoredBinarySource.c_str() ) ) { + generatedCacheSource = authoredBinarySource; + hasAuthoredSource = true; + } else if ( !forceCreateMap && CM_GeneratedCacheSourceExists( authoredTextSource.c_str() ) ) { + generatedCacheSource = authoredTextSource; + hasAuthoredSource = true; + } + const idStr authoredSettings = va( "mode=authored;source=%s", generatedCacheSource.c_str() ); + const idStr generatedCacheOptions = va( "geometry=%08x;source=%s", + mapFile->GetGeometryCRC(), generatedCacheSource.c_str() ); idTimer timer; timer.Start(); session->PacifierUpdate(); + fileSystem->RecordLevelLoadResource( LEVEL_LOAD_RESOURCE_COLLISION, + generatedCacheSource.c_str(), generatedCacheOptions.c_str(), 0, 3 ); + + bool loadedCollisionData = false; + if ( hasAuthoredSource ) { + loadedCollisionData = LoadGeneratedCollisionCache( generatedCacheSource.c_str(), + mapFile->GetGeometryCRC(), authoredSettings.c_str() ); + if ( !loadedCollisionData ) { + loadedCollisionData = LoadCollisionModelFile( mapFile->GetName(), mapFile->GetGeometryCRC() ); + if ( loadedCollisionData ) { + WriteGeneratedCollisionCache( generatedCacheSource.c_str(), mapFile->GetGeometryCRC(), + authoredSettings.c_str() ); + } + } + } + + idStr procSettings; + bool procCacheKeyValid = false; + if ( !loadedCollisionData ) { + procCacheKeyValid = CM_GeneratedCacheProcSettings( mapFile->GetName(), procSettings ); + if ( !forceCreateMap && procCacheKeyValid ) { + loadedCollisionData = LoadGeneratedCollisionCache( generatedCacheSource.c_str(), + mapFile->GetGeometryCRC(), procSettings.c_str() ); + } + } - if (forceCreateMap || !LoadCollisionModelFile( mapFile->GetName(), mapFile->GetGeometryCRC() ) ) { + if ( !loadedCollisionData ) { if ( !mapFile->GetNumEntities() ) { return; @@ -4129,6 +4242,10 @@ void idCollisionModelManagerLocal::BuildModels( const idMapFile *mapFile, bool f // write the collision models to a file WriteCollisionModelsToFile( mapFile->GetName(), 0, numModels, mapFile->GetGeometryCRC() ); + if ( procCacheKeyValid ) { + WriteGeneratedCollisionCache( generatedCacheSource.c_str(), mapFile->GetGeometryCRC(), + procSettings.c_str() ); + } } session->PacifierUpdate(); diff --git a/src/cm/CollisionModel_local.h b/src/cm/CollisionModel_local.h index b8d11182..37b9c330 100644 --- a/src/cm/CollisionModel_local.h +++ b/src/cm/CollisionModel_local.h @@ -585,6 +585,12 @@ class idCollisionModelManagerLocal : public idCollisionModelManager { void ParseBrushes( Lexer *src, idCollisionModelLocal *model ); bool ParseCollisionModel( Lexer *src, const char *fileName, unsigned int mapFileCRC ); bool LoadCollisionModelFile( const char *name, unsigned int mapFileCRC ); + // Private, source-authoritative generated caches. These are deliberately + // separate from the retail-compatible .cm/.cmc authoring path above. + bool LoadGeneratedCollisionCache( const char *sourcePath, unsigned int mapFileCRC, + const char *ownerSettings ); + void WriteGeneratedCollisionCache( const char *sourcePath, unsigned int mapFileCRC, + const char *ownerSettings ); private: // CollisionMap_debug int ContentsFromString( const char *string ) const; diff --git a/src/framework/CVarSystem.cpp b/src/framework/CVarSystem.cpp index c2483b10..8da3f488 100644 --- a/src/framework/CVarSystem.cpp +++ b/src/framework/CVarSystem.cpp @@ -29,6 +29,9 @@ If you have questions concerning this license or the applicable additional terms +#include "../idlib/PrivateCommand.h" +#include "RemoteCVarPolicy.h" + idCVar * idCVar::staticVars = NULL; /* @@ -90,12 +93,31 @@ static void ArgCompletion_CvarName( const idCmdArgs &args, void(*callback)( cons s_cvarNameArgCompletionCommand = NULL; } +static void CVar_AssignString( idStr &target, const char *newValue, bool privateValue ) { + if ( newValue == NULL ) { + newValue = ""; + } + if ( !privateValue ) { + target = newValue; + return; + } + + // newValue can point into target (or another long-lived CVar string), so + // preserve it before clearing the complete current allocation. + idStr replacement( newValue ); + target.SecureClear(); + target = replacement; + replacement.SecureClear(); +} + /* ============ idInternalCVar::idInternalCVar ============ */ idInternalCVar::idInternalCVar( void ) { + flags = 0; + valueStrings = NULL; } /* @@ -106,9 +128,9 @@ idInternalCVar::idInternalCVar idInternalCVar::idInternalCVar( const char *newName, const char *newValue, int newFlags ) { nameString = newName; name = nameString.c_str(); - valueString = newValue; + CVar_AssignString( valueString, newValue, ( newFlags & CVAR_PRIVATE ) != 0 ); value = valueString.c_str(); - resetString = newValue; + CVar_AssignString( resetString, newValue, ( newFlags & CVAR_PRIVATE ) != 0 ); descriptionString = ""; description = descriptionString.c_str(); flags = ( newFlags & ~CVAR_STATIC ) | CVAR_MODIFIED; @@ -129,9 +151,9 @@ idInternalCVar::idInternalCVar idInternalCVar::idInternalCVar( const idCVar *cvar ) { nameString = cvar->GetName(); name = nameString.c_str(); - valueString = cvar->GetString(); + CVar_AssignString( valueString, cvar->GetString(), ( cvar->GetFlags() & CVAR_PRIVATE ) != 0 ); value = valueString.c_str(); - resetString = cvar->GetString(); + CVar_AssignString( resetString, cvar->GetString(), ( cvar->GetFlags() & CVAR_PRIVATE ) != 0 ); descriptionString = cvar->GetDescription(); description = descriptionString.c_str(); flags = cvar->GetFlags() | CVAR_MODIFIED; @@ -150,6 +172,10 @@ idInternalCVar::~idInternalCVar ============ */ idInternalCVar::~idInternalCVar( void ) { + if ( flags & CVAR_PRIVATE ) { + valueString.SecureClear(); + resetString.SecureClear(); + } Mem_Free( valueStrings ); valueStrings = NULL; } @@ -208,6 +234,7 @@ idInternalCVar::Update ============ */ void idInternalCVar::Update( const idCVar *cvar ) { + const bool privateValue = ( ( flags | cvar->GetFlags() ) & CVAR_PRIVATE ) != 0; // if this is a statically declared variable if ( cvar->GetFlags() & CVAR_STATIC ) { @@ -228,7 +255,7 @@ void idInternalCVar::Update( const idCVar *cvar ) { } // the code is now specifying a variable that the user already set a value for, take the new value as the reset value - resetString = cvar->GetString(); + CVar_AssignString( resetString, cvar->GetString(), privateValue ); descriptionString = cvar->GetDescription(); description = descriptionString.c_str(); valueMin = cvar->GetMinValue(); @@ -246,9 +273,13 @@ void idInternalCVar::Update( const idCVar *cvar ) { // only allow one non-empty reset string without a warning if ( resetString.Length() == 0 ) { - resetString = cvar->GetString(); + CVar_AssignString( resetString, cvar->GetString(), privateValue ); } else if ( cvar->GetString()[0] && resetString.Cmp( cvar->GetString() ) != 0 ) { - common->Warning( "cvar \"%s\" given initial values: \"%s\" and \"%s\"\n", nameString.c_str(), resetString.c_str(), cvar->GetString() ); + if ( privateValue ) { + common->Warning( "private cvar \"%s\" was declared with conflicting initial values\n", nameString.c_str() ); + } else { + common->Warning( "cvar \"%s\" given initial values: \"%s\" and \"%s\"\n", nameString.c_str(), resetString.c_str(), cvar->GetString() ); + } } } @@ -382,11 +413,13 @@ void idInternalCVar::Set( const char *newValue, bool force, bool fromServer ) { } } - if ( valueString.Icmp( newValue ) == 0 ) { + const bool unchanged = ( flags & CVAR_CASE_SENSITIVE ) ? + valueString.Cmp( newValue ) == 0 : valueString.Icmp( newValue ) == 0; + if ( unchanged ) { return; } - valueString = newValue; + CVar_AssignString( valueString, newValue, ( flags & CVAR_PRIVATE ) != 0 ); value = valueString.c_str(); UpdateValue(); @@ -400,7 +433,7 @@ idInternalCVar::Reset ============ */ void idInternalCVar::Reset( void ) { - valueString = resetString; + CVar_AssignString( valueString, resetString.c_str(), ( flags & CVAR_PRIVATE ) != 0 ); value = valueString.c_str(); UpdateValue(); } @@ -498,6 +531,8 @@ class idCVarSystemLocal : public idCVarSystem { virtual const idDict * MoveCVarsToDict( int flags ) const; virtual void SetCVarsFromDict( const idDict &dict ); + virtual bool SetCVarsFromDictByFlags( const idDict &dict, int requiredFlag ); + virtual bool CommandContainsPrivateCVar( const char *commandText ) const; void RegisterInternal( idCVar *cvar ); idInternalCVar * FindInternal( const char *name ) const; @@ -768,6 +803,50 @@ float idCVarSystemLocal::GetCVarFloat( const char *name ) const { return 0.0f; } +/* +============ +idCVarSystemLocal::CommandContainsPrivateCVar + +Scan the full, possibly semicolon-separated input instead of trusting argv(0). +The case-insensitive private name must be bounded by non-cvar characters. Also +inspect the expanded tokens: otherwise `set $target secret`, where target names +a private CVar, reaches the private assignment after raw echo/history/journaling. +============ +*/ +bool idCVarSystemLocal::CommandContainsPrivateCVar( const char *commandText ) const { + if ( commandText == NULL || commandText[0] == '\0' ) { + return false; + } + for ( int index = 0; index < cvars.Num(); ++index ) { + const idInternalCVar *cvar = cvars[ index ]; + if ( !( cvar->GetFlags() & CVAR_PRIVATE ) ) { + continue; + } + if ( idPrivateCommand::ContainsBoundedCaseInsensitiveToken( + commandText, cvar->GetName() ) ) { + return true; + } + } + + idCmdArgs expandedArgs; + expandedArgs.TokenizeString( commandText, false ); + bool containsPrivateCVar = false; + for ( int argIndex = 0; argIndex < expandedArgs.Argc() && !containsPrivateCVar; ++argIndex ) { + for ( int cvarIndex = 0; cvarIndex < cvars.Num(); ++cvarIndex ) { + const idInternalCVar *cvar = cvars[ cvarIndex ]; + if ( ( cvar->GetFlags() & CVAR_PRIVATE ) && + idPrivateCommand::ContainsBoundedCaseInsensitiveToken( + expandedArgs.Argv( argIndex ), cvar->GetName() ) ) { + containsPrivateCVar = true; + break; + } + } + } + // Token expansion may have copied the assignment value into this temporary. + expandedArgs.ClearSensitive(); + return containsPrivateCVar; +} + /* ============ idCVarSystemLocal::Command @@ -790,14 +869,19 @@ bool idCVarSystemLocal::Command( const idCmdArgs &args ) { if ( args.Argc() == 1 ) { // print the variable + const char *value = ( internal->GetFlags() & CVAR_PRIVATE ) ? "" : internal->valueString.c_str(); + const char *defaultValue = ( internal->GetFlags() & CVAR_PRIVATE ) ? "" : internal->resetString.c_str(); common->Printf( "\"%s\" is:\"%s\"" S_COLOR_WHITE " default:\"%s\"\n", - internal->nameString.c_str(), internal->valueString.c_str(), internal->resetString.c_str() ); + internal->nameString.c_str(), value, defaultValue ); if ( idStr::Length( internal->GetDescription() ) > 0 ) { common->Printf( S_COLOR_WHITE "%s\n", internal->GetDescription() ); } } else { // set the value internal->Set( args.Args(), false, false ); + if ( internal->GetFlags() & CVAR_PRIVATE ) { + idCmdArgs::ClearArgsScratch(); + } } return true; } @@ -905,7 +989,7 @@ with the "flags" flag set to true. void idCVarSystemLocal::WriteFlaggedVariables( int flags, const char *setCmd, idFile *f ) const { for( int i = 0; i < cvars.Num(); i++ ) { idInternalCVar *cvar = cvars[i]; - if ( cvar->GetFlags() & flags ) { + if ( ( cvar->GetFlags() & flags ) && !( cvar->GetFlags() & CVAR_PRIVATE ) ) { f->Printf( "%s %s \"%s\"\n", setCmd, cvar->GetName(), cvar->GetString() ); } } @@ -920,7 +1004,7 @@ const idDict* idCVarSystemLocal::MoveCVarsToDict( int flags ) const { moveCVarsToDict.Clear(); for( int i = 0; i < cvars.Num(); i++ ) { idCVar *cvar = cvars[i]; - if ( cvar->GetFlags() & flags ) { + if ( ( cvar->GetFlags() & flags ) && !( cvar->GetFlags() & CVAR_PRIVATE ) ) { moveCVarsToDict.Set( cvar->GetName(), cvar->GetString() ); } } @@ -933,17 +1017,53 @@ idCVarSystemLocal::SetCVarsFromDict ============ */ void idCVarSystemLocal::SetCVarsFromDict( const idDict &dict ) { + // Retain the historical API for local/legacy callers, but never let it + // force-set PRIVATE variables or anything outside the three legacy network + // dictionary classes. New protocol consumers must use + // SetCVarsFromDictByFlags so one wire opcode cannot borrow another class's + // authority. idInternalCVar *internal; for( int i = 0; i < dict.GetNumKeyVals(); i++ ) { const idKeyValue *kv = dict.GetKeyVal( i ); internal = FindInternal( kv->GetKey() ); - if ( internal ) { + if ( internal && + ( internal->GetFlags() & ( CVAR_USERINFO | CVAR_SERVERINFO | CVAR_NETWORKSYNC ) ) != 0 && + !( internal->GetFlags() & CVAR_PRIVATE ) ) { internal->InternalServerSetString( kv->GetValue() ); } } } +/* +============ +idCVarSystemLocal::SetCVarsFromDictByFlags + +Apply a dictionary under one explicit network authority. Requiring a single +known flag prevents a userinfo packet from setting network-sync/server-info +state (or vice versa), while the private check is defense in depth for any +future CVar that is accidentally declared with both flag classes. +============ +*/ +bool idCVarSystemLocal::SetCVarsFromDictByFlags( const idDict &dict, int requiredFlag ) { + const int allowedRemoteFlags = CVAR_USERINFO | CVAR_SERVERINFO | CVAR_NETWORKSYNC; + if ( !idRemoteCVarPolicy::IsSingleAllowedAuthority( requiredFlag, allowedRemoteFlags ) ) { + common->Warning( "SetCVarsFromDictByFlags: invalid remote CVar authority 0x%x", requiredFlag ); + return false; + } + + for ( int i = 0; i < dict.GetNumKeyVals(); ++i ) { + const idKeyValue *kv = dict.GetKeyVal( i ); + idInternalCVar *internal = FindInternal( kv->GetKey() ); + if ( internal == NULL || !idRemoteCVarPolicy::CanApply( internal->GetFlags(), + requiredFlag, allowedRemoteFlags, CVAR_PRIVATE ) ) { + continue; + } + internal->InternalServerSetString( kv->GetValue() ); + } + return true; +} + /* ============ idCVarSystemLocal::Toggle_f @@ -969,6 +1089,10 @@ void idCVarSystemLocal::Toggle_f( const idCmdArgs &args ) { common->Warning( "Toggle_f: cvar \"%s\" not found", args.Argv( 1 ) ); return; } + if ( cvar->GetFlags() & CVAR_PRIVATE ) { + common->Printf( "toggle is unavailable for private CVar %s\n", cvar->GetName() ); + return; + } if ( argc > 3 ) { // cycle through multiple values @@ -1014,6 +1138,10 @@ void idCVarSystemLocal::Set_f( const idCmdArgs &args ) { str = args.Args( 2, args.Argc() - 1 ); localCVarSystem.SetCVarString( args.Argv(1), str ); + idInternalCVar *cvar = localCVarSystem.FindInternal( args.Argv( 1 ) ); + if ( cvar != NULL && ( cvar->GetFlags() & CVAR_PRIVATE ) ) { + idCmdArgs::ClearArgsScratch(); + } } /* @@ -1171,7 +1299,8 @@ void idCVarSystemLocal::ListByFlags( const idCmdArgs &args, cvarFlags_t flags ) case SHOW_VALUE: { for ( i = 0; i < cvarList.Num(); i++ ) { cvar = cvarList[i]; - common->Printf( FORMAT_STRING S_COLOR_WHITE "\"%s\"\n", cvar->nameString.c_str(), cvar->valueString.c_str() ); + const char *value = ( cvar->GetFlags() & CVAR_PRIVATE ) ? "" : cvar->valueString.c_str(); + common->Printf( FORMAT_STRING S_COLOR_WHITE "\"%s\"\n", cvar->nameString.c_str(), value ); } break; } diff --git a/src/framework/CVarSystem.h b/src/framework/CVarSystem.h index 3abb4314..317e99cd 100644 --- a/src/framework/CVarSystem.h +++ b/src/framework/CVarSystem.h @@ -81,6 +81,7 @@ typedef enum { CVAR_SPECIAL_CONCAT = BIT(22), // special concatination of the incoming string to the cvar system, will remove space between ^ and the code that is produced by tokenzier CVAR_STRIPTRAILING = BIT(23), // always strip trailing / on that cvar CVAR_REPEATERINFO = BIT(24), // sent from repeaters, available to menu + CVAR_PRIVATE = BIT(25), // secret value: redact from console output and omit from generic serialization } cvarFlags_t; @@ -253,6 +254,17 @@ class idCVarSystem { // Moves CVars to and from dictionaries. virtual const idDict * MoveCVarsToDict( int flags ) const = 0; virtual void SetCVarsFromDict( const idDict &dict ) = 0; + // Applies only CVars carrying exactly the authority represented by one of + // CVAR_USERINFO, CVAR_SERVERINFO, or CVAR_NETWORKSYNC. Network decoders + // must use this entry point instead of granting a received dictionary the + // authority to force-set arbitrary registered CVars. + virtual bool SetCVarsFromDictByFlags( const idDict &dict, int requiredFlag ) = 0; + + // ABI rule: append new virtual methods here. Inserting one above the + // legacy SetCVarsFromDict tail silently changes every later vtable slot for + // an otherwise version-compatible game module. + // Console front ends use this before echoing or retaining typed input. + virtual bool CommandContainsPrivateCVar( const char *commandText ) const = 0; }; extern idCVarSystem * cvarSystem; diff --git a/src/framework/CmdSystem.cpp b/src/framework/CmdSystem.cpp index 64d42d73..480e6d90 100644 --- a/src/framework/CmdSystem.cpp +++ b/src/framework/CmdSystem.cpp @@ -264,6 +264,11 @@ void idCmdSystemLocal::Vstr_f( const idCmdArgs &args ) { return; } + idCVar *cvar = cvarSystem->Find( args.Argv( 1 ) ); + if ( cvar != NULL && ( cvar->GetFlags() & CVAR_PRIVATE ) ) { + common->Printf( "vstr is unavailable for private CVar %s\n", cvar->GetName() ); + return; + } v = cvarSystem->GetCVarString( args.Argv( 1 ) ); cmdSystemLocal.BufferCommandText( CMD_EXEC_APPEND, va( "%s\n", v ) ); @@ -744,9 +749,25 @@ void idCmdSystemLocal::ExecuteCommandBuffer( void ) { text[i] = 0; + bool privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( text ); if ( !idStr::Cmp( text, "_execTokenized" ) ) { - args = tokenizedCmds[ 0 ]; - tokenizedCmds.RemoveIndex( 0 ); + if ( tokenizedCmds.Num() == 0 ) { + common->Warning( "ignored unmatched internal tokenized-command marker" ); + // args is reused across loop iterations. An unmatched marker must + // not execute the preceding command a second time. + args.ClearSensitive(); + } else { + args = tokenizedCmds[ 0 ]; + for ( int argIndex = 0; argIndex < args.Argc() && !privateCommand; ++argIndex ) { + privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( args.Argv( argIndex ) ); + } + if ( privateCommand ) { + tokenizedCmds[ 0 ].ClearSensitive(); + } + tokenizedCmds.RemoveIndex( 0 ); + } } else { args.TokenizeString( text, false ); } @@ -755,6 +776,7 @@ void idCmdSystemLocal::ExecuteCommandBuffer( void ) { // this is necessary because commands (exec) can insert data at the // beginning of the text buffer + const int previousTextLength = textLength; if ( i == textLength ) { textLength = 0; } else { @@ -762,9 +784,16 @@ void idCmdSystemLocal::ExecuteCommandBuffer( void ) { textLength -= i; memmove( text, text+i, textLength ); } + // Do not leave consumed command arguments (which can include private + // CVar assignments) in the unused tail of this long-lived buffer. + memset( textBuf + textLength, 0, previousTextLength - textLength ); // execute the command line that we have already tokenized ExecuteTokenizedString( args ); + if ( privateCommand ) { + args.ClearSensitive(); + idCmdArgs::ClearArgsScratch(); + } } } diff --git a/src/framework/Common.cpp b/src/framework/Common.cpp index 29206d28..6e0bef15 100644 --- a/src/framework/Common.cpp +++ b/src/framework/Common.cpp @@ -37,6 +37,7 @@ If you have questions concerning this license or the applicable additional terms #include "ArenaCampaign.h" #include "GameModuleDiagnostics.h" #include "RenderDoc.h" +#include "ParallelJobSystem.h" #include "../sys/NetworkEndpoint.h" #if defined( USE_SDL3 ) @@ -99,6 +100,15 @@ idCVar com_WriteSingleDeclFile( "com_WriteSingleDeclFile", "0", CVAR_SYSTEM | CV idCVar com_memoryMarker( "com_memoryMarker", "-1", CVAR_INTEGER | CVAR_SYSTEM | CVAR_INIT, "used as a marker for memory stats" ); idCVar com_preciseTic( "com_preciseTic", "1", CVAR_BOOL|CVAR_SYSTEM, "run one game tick every async thread update" ); idCVar com_asyncInput( "com_asyncInput", "0", CVAR_BOOL|CVAR_SYSTEM, "sample input from the async thread" ); +#if defined( ID_DEDICATED ) +static const char *OPENQ4_JOBS_DETERMINISTIC_DEFAULT = "1"; +#else +static const char *OPENQ4_JOBS_DETERMINISTIC_DEFAULT = "0"; +#endif +idCVar jobs_enable( "jobs_enable", "1", CVAR_BOOL | CVAR_ARCHIVE | CVAR_INIT | CVAR_SYSTEM, "enable the portable job service; 0 preserves all work by executing job lists synchronously" ); +idCVar jobs_deterministic( "jobs_deterministic", OPENQ4_JOBS_DETERMINISTIC_DEFAULT, CVAR_BOOL | CVAR_ARCHIVE | CVAR_INIT | CVAR_SYSTEM, "execute every job inline in deterministic insertion order (default on dedicated servers)" ); +idCVar jobs_numThreads( "jobs_numThreads", "0", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_INIT | CVAR_SYSTEM, "portable job worker threads (0 = automatic, 1..32 = explicit)", 0, 32, idCmdSystem::ArgCompletion_Integer<0,32> ); +idCVar jobs_queueCapacity( "jobs_queueCapacity", "64", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_INIT | CVAR_SYSTEM, "maximum admitted job lists; saturated submissions fail explicitly and remain retryable", 1, 1024, idCmdSystem::ArgCompletion_Integer<1,1024> ); #define ASYNCSOUND_INFO "0: mix sound inline, 1: memory mapped async mix, 2: callback mixing, 3: write async mix" #if defined( MACOS_X ) idCVar com_asyncSound( "com_asyncSound", "2", CVAR_INTEGER|CVAR_SYSTEM|CVAR_ROM, ASYNCSOUND_INFO ); @@ -1576,6 +1586,10 @@ idCommonLocal::ClearCommandLine ================== */ void idCommonLocal::ClearCommandLine( void ) { + for ( int i = 0; i < com_numConsoleLines; ++i ) { + com_consoleLines[ i ].ClearSensitive(); + } + idCmdArgs::ClearArgsScratch(); com_numConsoleLines = 0; } @@ -3648,6 +3662,120 @@ static void Com_PerformancePresetSelfTest_f( const idCmdArgs &args ) { } } +typedef struct openQ4JobSelfTestStage_s { + std::atomic *phase; + std::atomic *failed; + int expectedPhase; + int replacementPhase; +} openQ4JobSelfTestStage_t; + +static void Common_JobSelfTestStage( const idJobContext &context ) { + openQ4JobSelfTestStage_t *stage = static_cast( context.data ); + if ( stage == NULL || stage->phase == NULL || stage->failed == NULL ) { + return; + } + if ( stage->phase->load( std::memory_order_acquire ) != stage->expectedPhase ) { + stage->failed->store( true, std::memory_order_release ); + } + stage->phase->store( stage->replacementPhase, std::memory_order_release ); +} + +static void Common_JobSelfTestIncrement( const idJobContext &context ) { + std::atomic *counter = static_cast *>( context.data ); + if ( counter != NULL ) { + counter->fetch_add( 1, std::memory_order_relaxed ); + } +} + +static bool Common_JobSubmitSucceeded( const idJobSubmitResult result ) { + return result == idJobSubmitResult::ACCEPTED || + result == idJobSubmitResult::EXECUTED_SYNCHRONOUSLY; +} + +static void Com_JobsStats_f( const idCmdArgs &args ) { + ( void )args; + const idJobSystemMetrics metrics = jobSystem.GetMetrics(); + common->Printf( + "Job system: initialized=%s mode=%s workers=%u accepting=%s queued=%llu/%d highWater=%llu running=%llu sleeping=%llu\n", + jobSystem.IsInitialized() ? "yes" : "no", + metrics.synchronous ? "synchronous" : "threaded", + metrics.workerThreads, + metrics.accepting ? "yes" : "no", + static_cast( metrics.queuedLists ), + jobs_queueCapacity.GetInteger(), + static_cast( metrics.queueHighWatermark ), + static_cast( metrics.runningJobs ), + static_cast( metrics.sleepingWorkers ) ); + common->Printf( + "Job totals: lists submitted=%llu completed=%llu cancelled=%llu failed=%llu rejected=%llu; jobs submitted=%llu executed=%llu cancelled=%llu failed=%llu; wakes=%llu priorityPromotions=%llu execution=%lluus wait=%lluus\n", + static_cast( metrics.submittedLists ), + static_cast( metrics.completedLists ), + static_cast( metrics.cancelledLists ), + static_cast( metrics.failedLists ), + static_cast( metrics.rejectedSubmissions ), + static_cast( metrics.submittedJobs ), + static_cast( metrics.executedJobs ), + static_cast( metrics.cancelledJobs ), + static_cast( metrics.failedJobs ), + static_cast( metrics.workerWakeups ), + static_cast( metrics.priorityPromotions ), + static_cast( metrics.totalExecutionMicroseconds ), + static_cast( metrics.totalWaitMicroseconds ) ); +} + +static void Com_JobsSelfTest_f( const idCmdArgs &args ) { + ( void )args; + bool passed = jobSystem.IsInitialized(); + std::atomic phase( 0 ); + std::atomic dependencyFailed( false ); + std::atomic parallelCount( 0 ); + openQ4JobSelfTestStage_t rootData = { &phase, &dependencyFailed, 0, 1 }; + openQ4JobSelfTestStage_t dependentData = { &phase, &dependencyFailed, 1, 2 }; + const idJobSystemMetrics before = jobSystem.GetMetrics(); + + std::unique_ptr root = jobSystem.CreateJobList( "engine-self-test-root", 1, 0, idJobPriority::HIGH ); + std::unique_ptr dependent = jobSystem.CreateJobList( "engine-self-test-dependent", 1, 1, idJobPriority::NORMAL ); + std::unique_ptr batch = jobSystem.CreateJobList( "engine-self-test-batch", 8, 0, idJobPriority::LOW ); + passed &= root != NULL && dependent != NULL && batch != NULL; + if ( passed ) { + passed &= root->AddJob( Common_JobSelfTestStage, &rootData ); + passed &= Common_JobSubmitSucceeded( root->Submit() ); + passed &= root->Wait(); + passed &= dependent->AddDependency( *root ); + passed &= dependent->AddJob( Common_JobSelfTestStage, &dependentData ); + passed &= Common_JobSubmitSucceeded( dependent->Submit() ); + passed &= dependent->Wait(); + for ( int jobIndex = 0; jobIndex < 8; ++jobIndex ) { + passed &= batch->AddJob( Common_JobSelfTestIncrement, ¶llelCount ); + } + passed &= Common_JobSubmitSucceeded( batch->Submit() ); + passed &= batch->Wait(); + } + + const idJobSystemMetrics after = jobSystem.GetMetrics(); + passed &= !dependencyFailed.load( std::memory_order_acquire ); + passed &= phase.load( std::memory_order_acquire ) == 2; + passed &= parallelCount.load( std::memory_order_acquire ) == 8; + passed &= after.submittedLists >= before.submittedLists + 3; + passed &= after.executedJobs >= before.executedJobs + 10; + passed &= after.failedLists == before.failedLists; + + if ( passed ) { + common->Printf( "jobsSelfTest PASS v1 mode=%s workers=%u phase=%d batch=%d\n", + after.synchronous ? "synchronous" : "threaded", + after.workerThreads, + phase.load(), + parallelCount.load() ); + } else { + common->Warning( "jobsSelfTest FAILED v1 root=%s dependent=%s batch=%s phase=%d batchCount=%d", + root != NULL ? idJobListStatusName( root->GetStatus() ) : "unavailable", + dependent != NULL ? idJobListStatusName( dependent->GetStatus() ) : "unavailable", + batch != NULL ? idJobListStatusName( batch->GetStatus() ) : "unavailable", + phase.load(), + parallelCount.load() ); + } +} + static void Common_NetIPv4SelfTestFailure( bool &passed, const char *reason ) { common->Printf( "IPv4 network self-test failure: %s\n", reason ); passed = false; @@ -5304,6 +5432,8 @@ void idCommonLocal::InitCommands( void ) { cmdSystem->AddCommand( "applyPerformancePreset", Com_ApplyPerformancePreset_f, CMD_FL_SYSTEM, "applies the selected openQ4 performance preset", idCmdSystem::ArgCompletion_String ); cmdSystem->AddCommand( "autoDetectPerformancePreset", Com_AutoDetectPerformancePreset_f, CMD_FL_SYSTEM, "detects and applies a conservative openQ4 performance preset" ); cmdSystem->AddCommand( "performancePresetSelfTest", Com_PerformancePresetSelfTest_f, CMD_FL_SYSTEM, "validates openQ4 performance preset commands and cvar mappings" ); + cmdSystem->AddCommand( "jobsStats", Com_JobsStats_f, CMD_FL_SYSTEM, "prints bounded job-service scheduling and timing counters" ); + cmdSystem->AddCommand( "jobsSelfTest", Com_JobsSelfTest_f, CMD_FL_SYSTEM, "validates job execution, dependencies, deterministic fallback, and metrics" ); cmdSystem->AddCommand( "netIPv4SelfTest", Com_NetIPv4SelfTest_f, CMD_FL_SYSTEM, "validates IPv4 parsing and UDP loopback transport" ); cmdSystem->AddCommand( "netIPv6SelfTest", Com_NetIPv6SelfTest_f, CMD_FL_SYSTEM, "validates IPv6 parsing and UDP loopback transport" ); @@ -6438,6 +6568,24 @@ void idCommonLocal::Init( int argc, const char **argv, const char *cmdline ) { // initialize processor specific SIMD implementation InitSIMD(); + // The job service is engine-owned and renderer-independent, so the same + // lifecycle is available to clients, tools, and headless dedicated builds. + // Disabling jobs never drops work: it selects the deterministic inline path. + idJobSystemConfig jobConfig; + jobConfig.synchronous = !jobs_enable.GetBool() || jobs_deterministic.GetBool(); + jobConfig.workerThreads = jobConfig.synchronous ? 0 : + idJobSystem::ResolveWorkerThreadCount( jobs_numThreads.GetInteger() ); + jobConfig.maxQueuedLists = static_cast( jobs_queueCapacity.GetInteger() ); + if ( !jobSystem.Initialize( jobConfig ) ) { + FatalError( "Failed to initialize the portable job system" ); + } + Printf( "Job system initialized: mode=%s workers=%u queueCapacity=%d (jobs_enable=%d deterministic=%d)\n", + jobSystem.IsSynchronous() ? "synchronous" : "threaded", + jobSystem.GetWorkerThreadCount(), + jobs_queueCapacity.GetInteger(), + jobs_enable.GetBool() ? 1 : 0, + jobs_deterministic.GetBool() ? 1 : 0 ); + // init commands InitCommands(); @@ -6455,6 +6603,12 @@ void idCommonLocal::Init( int argc, const char **argv, const char *cmdline ) { if ( !com_consoleLines[ i ].Argc() ) { continue; } + const char *lineText = com_consoleLines[ i ].Args( 0, com_consoleLines[ i ].Argc() - 1 ); + if ( cvarSystem->CommandContainsPrivateCVar( lineText ) ) { + idCmdArgs::ClearArgsScratch(); + Printf( " %d: \n", i ); + continue; + } Printf( " %d:", i ); for ( int j = 0; j < com_consoleLines[ i ].Argc(); ++j ) { Printf( " %s", com_consoleLines[ i ].Argv( j ) ); @@ -6507,6 +6661,19 @@ void idCommonLocal::Shutdown( void ) { com_shuttingDown = true; + // Stop every engine job before any subsystem it may reference is torn down. + // Running jobs receive cancellation and are joined; queued jobs never start. + jobSystem.Shutdown( idJobShutdownMode::CANCEL_PENDING ); + const idJobSystemMetrics shutdownJobMetrics = jobSystem.GetMetrics(); + if ( jobSystem.IsInitialized() || shutdownJobMetrics.queuedLists != 0 || shutdownJobMetrics.runningJobs != 0 ) { + Warning( "jobsShutdown FAILED v1 initialized=%d queued=%llu running=%llu", + jobSystem.IsInitialized() ? 1 : 0, + static_cast( shutdownJobMetrics.queuedLists ), + static_cast( shutdownJobMetrics.runningJobs ) ); + } else { + Printf( "jobsShutdown PASS v1 initialized=0 queued=0 running=0\n" ); + } + idAsyncNetwork::server.Kill(); idAsyncNetwork::client.Shutdown(); diff --git a/src/framework/Console.cpp b/src/framework/Console.cpp index 2d63be75..c7afa59a 100644 --- a/src/framework/Console.cpp +++ b/src/framework/Console.cpp @@ -1569,6 +1569,7 @@ void idConsoleLocal::LoadCommandHistory( void ) { return; } + bool removedPrivateCommand = false; int lineStart = 0; for ( int i = 0; i <= fileLength; ++i ) { const bool atEnd = ( i == fileLength ); @@ -1588,6 +1589,14 @@ void idConsoleLocal::LoadCommandHistory( void ) { if ( lineLength > 0 ) { line.Append( fileBuffer + lineStart, lineLength ); } + if ( cvarSystem != NULL && cvarSystem->CommandContainsPrivateCVar( line.c_str() ) ) { + // Older builds persisted the complete command line, including values + // assigned to secret CVars. Purge those entries instead of bringing a + // credential back into live history (or writing it out again). + removedPrivateCommand = true; + lineStart = i + 1; + continue; + } historyEditLines[nextHistoryLine % COMMAND_HISTORY].SetBuffer( line.c_str() ); nextHistoryLine++; @@ -1595,7 +1604,15 @@ void idConsoleLocal::LoadCommandHistory( void ) { } historyLine = nextHistoryLine; + if ( removedPrivateCommand && fileLength > 0 ) { + // The old history buffer can contain the credential that was just + // purged from disk. Scrub it before returning the allocation. + memset( const_cast( fileBuffer ), 0, fileLength ); + } fileSystem->FreeFile( ( void * )fileBuffer ); + if ( removedPrivateCommand ) { + SaveCommandHistory(); + } } /* @@ -3150,7 +3167,12 @@ bool idConsoleLocal::GetCompletionCvarInfo( const char *match, char *value, int *modified = cvar->IsModified(); } if ( value != NULL && valueSize > 0 ) { - const char *cvarValue = cvar->GetString(); + // Completion candidates are populated from the full CVar-name list, so a + // partial prefix can reach this path before the edit-field private-command + // guard sees the complete name. Never let the popup turn completion into a + // private-value query. + const char *cvarValue = ( cvar->GetFlags() & CVAR_PRIVATE ) ? + "" : cvar->GetString(); idStr::Copynz( value, ( cvarValue != NULL && cvarValue[0] != '\0' ) ? cvarValue : "\"\"", valueSize ); } return true; @@ -4113,14 +4135,22 @@ void idConsoleLocal::KeyDownEvent( int key ) { return; } - common->Printf( "]%s\n", consoleField.GetBuffer() ); + const bool privateCommand = cvarSystem != NULL && + cvarSystem->CommandContainsPrivateCVar( consoleField.GetBuffer() ); + if ( privateCommand ) { + common->Printf( "]\n" ); + } else { + common->Printf( "]%s\n", consoleField.GetBuffer() ); + } cmdSystem->BufferCommandText( CMD_EXEC_APPEND, consoleField.GetBuffer() ); cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "\n" ); - historyEditLines[nextHistoryLine % COMMAND_HISTORY] = consoleField; - nextHistoryLine++; - historyLine = nextHistoryLine; - SaveCommandHistory(); + if ( !privateCommand ) { + historyEditLines[nextHistoryLine % COMMAND_HISTORY] = consoleField; + nextHistoryLine++; + historyLine = nextHistoryLine; + SaveCommandHistory(); + } consoleField.Clear(); consoleField.SetWidthInChars( lineWidth ); diff --git a/src/framework/EditField.cpp b/src/framework/EditField.cpp index 952106f3..bc20e752 100644 --- a/src/framework/EditField.cpp +++ b/src/framework/EditField.cpp @@ -195,7 +195,10 @@ PrintCvarMatches static void PrintCvarMatches( const char *s ) { if ( idStr::Icmpn( s, globalAutoComplete.currentMatch, idLib::SizeToInt( strlen( globalAutoComplete.currentMatch ), "PrintCvarMatches" ) ) == 0 ) { - common->Printf( " %s" S_COLOR_WHITE " = \"%s\"\n", s, cvarSystem->GetCVarString( s ) ); + idCVar *cvar = cvarSystem->Find( s ); + const char *value = ( cvar != NULL && ( cvar->GetFlags() & CVAR_PRIVATE ) ) ? + "" : cvarSystem->GetCVarString( s ); + common->Printf( " %s" S_COLOR_WHITE " = \"%s\"\n", s, value ); } } @@ -223,11 +226,13 @@ idEditField::Clear =============== */ void idEditField::Clear( void ) { - buffer[0] = 0; + // Edit fields can hold console-entered credentials. Clear the backing + // storage, not only its first byte, so discarded private commands do not + // remain recoverable in stale field memory. + memset( buffer, 0, sizeof( buffer ) ); cursor = 0; scroll = 0; - autoComplete.length = 0; - autoComplete.valid = false; + memset( &autoComplete, 0, sizeof( autoComplete ) ); } /* @@ -365,6 +370,12 @@ idEditField::AutoComplete void idEditField::AutoComplete( void ) { char completionArgString[MAX_EDIT_LINE]; idCmdArgs args; + if ( cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( buffer ) ) { + idCmdArgs::ClearArgsScratch(); + common->Printf( "autocomplete is unavailable for private CVar commands\n" ); + return; + } if ( !autoComplete.valid ) { const bool explicitCommandPrefix = ( buffer[0] == '/' || buffer[0] == '\\' ); @@ -501,6 +512,10 @@ static int QueryCompletionInternal( const char *cmd, bool *appendSpace, editFiel if ( cmd == NULL || cmd[0] == '\0' ) { return 0; } + if ( cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( cmd ) ) { + return 0; + } NormalizeCompletionCommandString( cmd, normalizedCmd, sizeof( normalizedCmd ) ); if ( normalizedCmd[0] == '\0' ) { @@ -787,6 +802,7 @@ void idEditField::Paste( void ) { CharEvent( cbd[i] ); } + memset( cbd, 0, pasteLen ); Mem_Free( cbd ); } diff --git a/src/framework/EventLoop.cpp b/src/framework/EventLoop.cpp index 90190748..13fb20f1 100644 --- a/src/framework/EventLoop.cpp +++ b/src/framework/EventLoop.cpp @@ -34,6 +34,18 @@ idCVar idEventLoop::com_journal( "com_journal", "0", CVAR_INIT|CVAR_SYSTEM, "1 = idEventLoop eventLoopLocal; idEventLoop *eventLoop = &eventLoopLocal; +static bool EventLoop_IsPrivateConsoleEvent( const sysEvent_t &event ) { + if ( event.evType != SE_CONSOLE || event.evPtr == NULL || event.evPtrLength <= 0 || + cvarSystem == NULL || !cvarSystem->IsInitialized() ) { + return false; + } + if ( memchr( event.evPtr, '\0', static_cast( event.evPtrLength ) ) == NULL ) { + // A malformed console event is not safe to persist as text. + return true; + } + return cvarSystem->CommandContainsPrivateCVar( static_cast( event.evPtr ) ); +} + /* ================= @@ -81,13 +93,20 @@ sysEvent_t idEventLoop::GetRealEvent( void ) { // write the journal value out if needed if ( com_journal.GetInteger() == 1 ) { - r = com_journalFile->Write( &ev, sizeof(ev) ); + static const char PRIVATE_EVENT_TEXT[] = ""; + sysEvent_t journalEvent = ev; + const void *journalData = ev.evPtr; + if ( EventLoop_IsPrivateConsoleEvent( ev ) ) { + journalEvent.evPtrLength = sizeof( PRIVATE_EVENT_TEXT ); + journalData = PRIVATE_EVENT_TEXT; + } + r = com_journalFile->Write( &journalEvent, sizeof(journalEvent) ); if ( r != sizeof(ev) ) { common->FatalError( "Error writing to journal file" ); } - if ( ev.evPtrLength ) { - r = com_journalFile->Write( ev.evPtr, ev.evPtrLength ); - if ( r != ev.evPtrLength ) { + if ( journalEvent.evPtrLength ) { + r = com_journalFile->Write( journalData, journalEvent.evPtrLength ); + if ( r != journalEvent.evPtrLength ) { common->FatalError( "Error writing to journal file" ); } } @@ -117,6 +136,9 @@ void idEventLoop::PushEvent( sysEvent_t *event ) { } if ( ev->evPtr ) { + if ( EventLoop_IsPrivateConsoleEvent( *ev ) ) { + memset( ev->evPtr, 0, ev->evPtrLength ); + } Mem_Free( ev->evPtr ); } com_pushedEventsTail++; @@ -166,6 +188,9 @@ void idEventLoop::ProcessEvent( sysEvent_t ev ) { // free any block data if ( ev.evPtr ) { + if ( EventLoop_IsPrivateConsoleEvent( ev ) ) { + memset( ev.evPtr, 0, ev.evPtrLength ); + } Mem_Free( ev.evPtr ); } } diff --git a/src/framework/File.h b/src/framework/File.h index ade72264..c3a79d45 100644 --- a/src/framework/File.h +++ b/src/framework/File.h @@ -125,6 +125,11 @@ class idFile { virtual void ReadSyncId(const char* detail = "unspecified", const char* classname = NULL) { } // jmarshall end + // Append-only memory-file staging hooks. Keeping these after the complete + // historical idFile vtable preserves old game-module dispatch positions. + virtual void MakeReadOnly( void ) {} + virtual const char * GetDataPtr( void ) const { return NULL; } + template ID_INLINE size_t ReadBig(type& c) { size_t r = Read(&c, sizeof(c)); BigRevBytes(&c, sizeof(c), 1); @@ -181,7 +186,7 @@ class idFile_Memory : public idFile { // set data for reading void SetData( const char *data, int length ); // returns const pointer to the memory buffer - const char * GetDataPtr( void ) const { return filePtr; } + virtual const char * GetDataPtr( void ) const { return filePtr; } // set the file granularity void SetGranularity( int g ) { assert( g > 0 ); granularity = g; } diff --git a/src/framework/FileSystem.cpp b/src/framework/FileSystem.cpp index 20426e40..fdfe6f43 100644 --- a/src/framework/FileSystem.cpp +++ b/src/framework/FileSystem.cpp @@ -30,10 +30,17 @@ If you have questions concerning this license or the applicable additional terms #include "Unzip.h" +#include "GameDirPolicy.h" +#include "LevelLoadCacheManager.h" #include "openq4_paks_generated.h" +#include "../sys/URLPolicy.h" #include #include +#include +#include +#include +#include #if defined( USE_SDL3 ) #include @@ -57,6 +64,11 @@ If you have questions concerning this license or the applicable additional terms #if ID_ENABLE_CURL #include "../curl/include/curl/curl.h" + #if defined( LIBCURL_VERSION_NUM ) && LIBCURL_VERSION_NUM >= 0x071304 && defined( CURL_VERSION_ASYNCHDNS ) + #define OPENQ4_CURL_CAPABLE_BUILD 1 + #else + #define OPENQ4_CURL_CAPABLE_BUILD 0 + #endif #endif int Com_GetNumStartupCommandLines( void ); @@ -1344,11 +1356,22 @@ typedef struct searchpath_s { // + .jpg and .tga #define MAX_CACHED_DIRS 6 -// how many OSes to handle game paks for ( we don't have to know them precisely ) -#define MAX_GAME_OS 6 #define BINARY_CONFIG "binary.conf" #define ADDON_CONFIG "addon.conf" +// Protocol 2.41 identifies compatible game code with the official 1.4.2 +// q4base/game300.pk4 checksum. openQ4 never loads executable code from that +// archive; the unchanged value is a platform-independent wire token for a +// module that FindDLL resolved from the trusted package/module roots. +static const int OPENQ4_Q4_142_GAME300_PAK_CHECKSUM = 0x68fb90b1; + +typedef enum { + GAME_MODULE_ORIGIN_NONE = 0, + GAME_MODULE_ORIGIN_ACTIVE_MOD, + GAME_MODULE_ORIGIN_BASE_GAME, + GAME_MODULE_ORIGIN_PACKAGE_ROOT +} gameModuleOrigin_t; + class idDEntry : public idStrList { public: idDEntry() {} @@ -1369,6 +1392,8 @@ typedef enum modManifestStatus_s { MOD_MANIFEST_INVALID } modManifestStatus_t; +class idLevelLoadCacheManager; + class idFileSystemLocal : public idFileSystem { public: idFileSystemLocal( void ); @@ -1416,6 +1441,24 @@ class idFileSystemLocal : public idFileSystem { virtual void WriteAssetLog(); virtual void ClearAssetLog(); virtual const char * GetAssetLogName(); + virtual void BeginLevelLoadCache( const char *mapKey, const char *gameMode, + const char *entityFilter, const char *settingsKey ); + virtual void FinishLevelLoadCache( bool successful ); + void ReleaseLevelLoadCache( void ); + virtual void CancelLevelLoadCache( void ); + virtual void RecordLevelLoadResource( levelLoadResourceType_t type, + const char *name, const char *options, + unsigned int flags, unsigned int priority ); + virtual idFile * OpenGeneratedCacheRead( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey ); + virtual bool WriteGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey, const void *payload, + unsigned int payloadBytes ); + virtual void DiscardGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey ); virtual idFile * GetNewFileMemory( void ); virtual idFile * GetNewFilePermanent( void ); virtual idFile * OpenFileReadFlags( const char *relativePath, int searchFlags, pack_t **foundInPak = NULL, bool allowCopyFiles = true, const char* gamedir = NULL ); @@ -1428,31 +1471,9 @@ class idFileSystemLocal : public idFileSystem { virtual idFile * OpenExplicitFileWrite( const char *OSPath ); virtual void CloseFile( idFile *f ); virtual void BackgroundDownload( backgroundDownload_t *bgl ); - virtual void ResetReadCount( void ) { readCount = 0; readCountPacifierBytes = 0; } - virtual void AddToReadCount( int c ) { - if ( c <= 0 ) { - return; - } - - readCount += c; - - // Keep loading progress responsive on PC as data is read, but offer the redraw - // per megabyte rather than per 64 KiB read chunk. Both callers read in 64 KiB - // chunks, so offering every call put thousands of redraw opportunities per map - // load in the middle of asset decode. The pacifier is clock-throttled anyway; - // this just keeps the offer at asset-sized granularity. Progress accuracy is - // unaffected - readCount above still advances on every chunk, and that is what - // the loading bar's byte target reads. - readCountPacifierBytes += c; - if ( readCountPacifierBytes < READCOUNT_PACIFIER_INTERVAL_BYTES ) { - return; - } - readCountPacifierBytes = 0; - - // idSessionLocal::PacifierUpdate is internally throttled and no-ops outside map load. - session->PacifierUpdate(); - } - virtual int GetReadCount( void ) { return readCount; } + virtual void ResetReadCount( void ); + virtual void AddToReadCount( int c ); + virtual int GetReadCount( void ); virtual void FindDLL( const char *basename, char dllPath[ MAX_OSPATH ], bool updateChecksum ); virtual void ClearDirCache( void ); virtual bool HasD3XP( void ); @@ -1481,6 +1502,8 @@ class idFileSystemLocal : public idFileSystem { // bytes read since the last loading-pacifier offer, see AddToReadCount static const int READCOUNT_PACIFIER_INTERVAL_BYTES = 1024 * 1024; int readCountPacifierBytes; + std::mutex readCountMutex; + std::thread::id fileSystemMainThread; int loadCount; // total files read int loadStack; // total files in memory idStr gameFolder; // this will be a single name without separators @@ -1513,10 +1536,12 @@ class idFileSystemLocal : public idFileSystem { int restartGamePakChecksum; int gameDLLChecksum; // the checksum of the last loaded game DLL int gamePakChecksum; // the checksum of the pak holding the loaded game DLL + gameModuleOrigin_t gameModuleOrigin; // trusted root and active-mod provenance bool isFileLoadingAllowed; idStr currentAssetLog; idStr currentAssetLogUnfiltered; idStrList assetLog; + idLevelLoadCacheManager *levelLoadCache; int gamePakForOS[ MAX_GAME_OS ]; @@ -1537,6 +1562,9 @@ class idFileSystemLocal : public idFileSystem { int DirectFileLength( FILE *o ); void CopyFile( idFile *src, const char *toOSPath ); void AddAssetLogEntry( const char *relativePath ); + void BuildLevelLoadContentKey( idStr &key ) const; + void RecordOpenedLevelLoadSource( const char *relativePath, idFile *file ); + idFile * UsePreloadedLevelLoadSource( const char *relativePath, idFile *file ); int AddUnique( const char *name, idStrList &list, idHashIndex &hashIndex ) const; void GetExtensionList( const char *extension, idStrList &extensionList ) const; int GetFileList( const char *relativePath, const idStrList &extensions, idStrList &list, idHashIndex &hashIndex, bool fullRelativePath, const char* gamedir = NULL ); @@ -1607,6 +1635,7 @@ idFileSystemLocal::idFileSystemLocal */ idFileSystemLocal::idFileSystemLocal( void ) { searchPaths = NULL; + fileSystemMainThread = std::this_thread::get_id(); readCount = 0; readCountPacifierBytes = 0; loadCount = 0; @@ -1616,16 +1645,205 @@ idFileSystemLocal::idFileSystemLocal( void ) { d3xp = 0; loadedFileFromDir = false; restartGamePakChecksum = 0; + gameDLLChecksum = 0; + gamePakChecksum = 0; + gameModuleOrigin = GAME_MODULE_ORIGIN_NONE; + memset( gamePakForOS, 0, sizeof( gamePakForOS ) ); isFileLoadingAllowed = false; currentAssetLog.Clear(); currentAssetLogUnfiltered.Clear(); assetLog.Clear(); + levelLoadCache = NULL; backgroundDownloads = NULL; - memset( &defaultBackgroundDownload, 0, sizeof( defaultBackgroundDownload ) ); + defaultBackgroundDownload.next = NULL; + defaultBackgroundDownload.opcode = DLTYPE_FILE; + defaultBackgroundDownload.f = NULL; + defaultBackgroundDownload.file.position = 0; + defaultBackgroundDownload.file.length = 0; + defaultBackgroundDownload.file.buffer = NULL; + defaultBackgroundDownload.url.url.Clear(); + defaultBackgroundDownload.url.dlerror[ 0 ] = '\0'; + defaultBackgroundDownload.url.expectedSize = 0; + defaultBackgroundDownload.url.dltotal = 0; + defaultBackgroundDownload.url.dlnow = 0; + defaultBackgroundDownload.url.dlstatus = 0; + defaultBackgroundDownload.url.status = DL_WAIT; + defaultBackgroundDownload.completed = true; memset( &backgroundThread, 0, sizeof( backgroundThread ) ); addonPaks = NULL; } +/* +================ +idFileSystemLocal::BuildLevelLoadContentKey + +Produces an installation-root-independent fingerprint input for the exact +ordered active PK4/search set. Per-file loose-source timestamps are still +validated separately by each manifest/cache source identity. +================ +*/ +void idFileSystemLocal::BuildLevelLoadContentKey( idStr &key ) const { + key = va( "game=%s|base=%s|", fs_game.GetString(), fs_game_base.GetString() ); + int order = 0; + for ( const searchpath_t *search = searchPaths; search != NULL; search = search->next, order++ ) { + if ( search->pack != NULL ) { + idStr fileName; + search->pack->pakFilename.ExtractFileName( fileName ); + fileName.ToLower(); + key += va( "p%d=%s:%08x:%d:%d:%d|", order, fileName.c_str(), + static_cast( search->pack->checksum ), + search->pack->length, search->pack->numfiles, + search->pack->addon_search ? 1 : 0 ); + } else if ( search->dir != NULL ) { + idStr gameDir = search->dir->gamedir; + gameDir.ToLower(); + key += va( "d%d=%s|", order, gameDir.c_str() ); + } + } + key += "pure="; + for ( int i = 0; i < serverPaks.Num(); i++ ) { + key += va( "%08x,", static_cast( serverPaks[ i ]->checksum ) ); + } +} + +void idFileSystemLocal::RecordOpenedLevelLoadSource( const char *relativePath, idFile *file ) { + if ( levelLoadCache != NULL ) { + levelLoadCache->RecordOpenedSource( relativePath, file ); + } +} + +idFile *idFileSystemLocal::UsePreloadedLevelLoadSource( const char *relativePath, idFile *file ) { + if ( levelLoadCache == NULL || file == NULL ) { + return file; + } + idFile *preloaded = levelLoadCache->OpenPreloadedSource( relativePath, file ); + if ( preloaded == NULL ) { + return file; + } + CloseFile( file ); + return preloaded; +} + +void idFileSystemLocal::BeginLevelLoadCache( const char *mapKey, const char *gameMode, + const char *entityFilter, const char *settingsKey ) { + if ( levelLoadCache == NULL ) { + return; + } + idStr contentKey; + BuildLevelLoadContentKey( contentKey ); + levelLoadCache->Begin( mapKey, gameMode, entityFilter, contentKey.c_str(), settingsKey ); +} + +void idFileSystemLocal::FinishLevelLoadCache( const bool successful ) { + if ( levelLoadCache != NULL ) { + levelLoadCache->Finish( successful ); + } +} + +void idFileSystemLocal::ReleaseLevelLoadCache( void ) { + if ( levelLoadCache != NULL ) { + levelLoadCache->Release(); + } +} + +// Engine-only lifecycle hook. Keeping this outside the public filesystem ABI +// lets Session retain replay bytes through all EndLevelLoad consumers without +// exposing coordinator internals to renderer or game modules. +void FS_ReleaseLevelLoadCache( void ) { + fileSystemLocal.ReleaseLevelLoadCache(); +} + +void idFileSystemLocal::CancelLevelLoadCache( void ) { + if ( levelLoadCache != NULL ) { + levelLoadCache->Cancel(); + } +} + +void idFileSystemLocal::RecordLevelLoadResource( const levelLoadResourceType_t type, + const char *name, const char *options, const unsigned int flags, + const unsigned int priority ) { + if ( levelLoadCache != NULL ) { + levelLoadCache->RecordSemantic( type, name, options, flags, priority ); + } +} + +idFile *idFileSystemLocal::OpenGeneratedCacheRead( const generatedCacheKind_t kind, + const char *sourcePath, const unsigned int parserVersion, const char *settingsKey ) { + if ( levelLoadCache == NULL ) { + return NULL; + } + idStr contentKey; + BuildLevelLoadContentKey( contentKey ); + return levelLoadCache->OpenGeneratedCacheRead( kind, sourcePath, parserVersion, + settingsKey, contentKey.c_str() ); +} + +bool idFileSystemLocal::WriteGeneratedCache( const generatedCacheKind_t kind, + const char *sourcePath, const unsigned int parserVersion, const char *settingsKey, + const void *payload, const unsigned int payloadBytes ) { + if ( levelLoadCache == NULL ) { + return false; + } + idStr contentKey; + BuildLevelLoadContentKey( contentKey ); + return levelLoadCache->WriteGeneratedCache( kind, sourcePath, parserVersion, + settingsKey, payload, payloadBytes, contentKey.c_str() ); +} + +void idFileSystemLocal::DiscardGeneratedCache( const generatedCacheKind_t kind, + const char *sourcePath, const unsigned int parserVersion, const char *settingsKey ) { + if ( levelLoadCache == NULL ) { + return; + } + idStr contentKey; + BuildLevelLoadContentKey( contentKey ); + levelLoadCache->DiscardGeneratedCache( kind, sourcePath, parserVersion, + settingsKey, contentKey.c_str() ); +} + +/* +================ +idFileSystemLocal::ResetReadCount + +Level-load workers may inflate independently opened PK4 streams. Protect the +shared progress counter, but keep presentation calls on the main thread. +================ +*/ +void idFileSystemLocal::ResetReadCount( void ) { + std::lock_guard lock( readCountMutex ); + readCount = 0; + readCountPacifierBytes = 0; +} + +int idFileSystemLocal::GetReadCount( void ) { + std::lock_guard lock( readCountMutex ); + return readCount; +} + +void idFileSystemLocal::AddToReadCount( const int count ) { + if ( count <= 0 ) { + return; + } + + bool offerPacifier = false; + { + std::lock_guard lock( readCountMutex ); + const int maxCount = ( std::numeric_limits::max )(); + readCount = count > maxCount - readCount ? maxCount : readCount + count; + readCountPacifierBytes = count > maxCount - readCountPacifierBytes + ? maxCount + : readCountPacifierBytes + count; + if ( readCountPacifierBytes >= READCOUNT_PACIFIER_INTERVAL_BYTES ) { + readCountPacifierBytes = 0; + offerPacifier = true; + } + } + + if ( offerPacifier && std::this_thread::get_id() == fileSystemMainThread && session != NULL ) { + session->PacifierUpdate(); + } +} + /* ================ idFileSystemLocal::HashFileName @@ -4254,6 +4472,12 @@ bool idFileSystemLocal::GetModInfo( const char *modDir, idModInfo &modInfo, idSt } return false; } + if ( !idGameDirPolicy::IsPortableSegment( modDir ) ) { + if ( reason != NULL ) { + *reason = "mod directory must be one portable directory segment"; + } + return false; + } const char *search[ 3 ]; search[ 0 ] = fs_cdpath.GetString(); @@ -5560,64 +5784,23 @@ idFileSystemLocal::UpdateGamePakChecksums ===================== */ bool idFileSystemLocal::UpdateGamePakChecksums( void ) { - searchpath_t *search; - fileInPack_t *pakFile; - int confHash; - idFile *confFile; - char *buf; - idLexer *lexConf; - idToken token; - int id; - - confHash = HashFileName( BINARY_CONFIG ); - memset( gamePakForOS, 0, sizeof( gamePakForOS ) ); - for ( search = searchPaths; search; search = search->next ) { - if ( !search->pack ) { - continue; - } - search->pack->binary = BINARY_NO; - for ( pakFile = search->pack->hashTable[confHash]; pakFile; pakFile = pakFile->next ) { - if ( !FilenameCompare( pakFile->name, BINARY_CONFIG ) ) { - search->pack->binary = BINARY_YES; - confFile = ReadFileFromZip( search->pack, pakFile, BINARY_CONFIG ); - if ( confFile == NULL ) { - search->pack->binary = BINARY_NO; - break; - } - buf = new char[ confFile->Length() + 1 ]; - confFile->Read( (void *)buf, confFile->Length() ); - buf[ confFile->Length() ] = '\0'; - lexConf = new idLexer( buf, confFile->Length(), confFile->GetFullPath() ); - while ( lexConf->ReadToken( &token ) ) { - if ( token.IsNumeric() ) { - id = atoi( token ); - if ( id < MAX_GAME_OS && !gamePakForOS[ id ] ) { - if ( fs_debug.GetBool() ) { - common->Printf( "Adding game pak checksum for OS %d: %s 0x%x\n", id, confFile->GetFullPath(), search->pack->checksum ); - } - gamePakForOS[ id ] = search->pack->checksum; - } - } - } - CloseFile( confFile ); - delete lexConf; - delete[] buf; - } - } + if ( gameDLLChecksum == 0 || gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM || + gameModuleOrigin == GAME_MODULE_ORIGIN_NONE ) { + common->Warning( "No trusted openQ4 game module is loaded for pure-server startup" ); + return false; } - - // some sanity checks on the game code references - // make sure that at least the local OS got a pure reference - if ( !gamePakForOS[ BUILD_OS_ID ] ) { - common->Warning( "No game code pak reference found for the local OS" ); + if ( gameModuleOrigin == GAME_MODULE_ORIGIN_ACTIVE_MOD && + !cvarSystem->GetCVarBool( "net_serverAllowServerMod" ) ) { + common->Warning( "The active mod supplies game code (net_serverAllowServerMod is off)" ); return false; } - if ( !cvarSystem->GetCVarBool( "net_serverAllowServerMod" ) && - gamePakChecksum != gamePakForOS[ BUILD_OS_ID ] ) { - common->Warning( "The current game code doesn't match pak files (net_serverAllowServerMod is off)" ); - return false; + // Protocol 2.41 defines Windows, Linux and macOS as OS IDs 0..2. The + // compatibility token is deliberately platform-independent; executable + // bytes remain outside the downloadable/pure-PK4 path. + for ( int os = 0; os <= 2; ++os ) { + gamePakForOS[ os ] = OPENQ4_Q4_142_GAME300_PAK_CHECKSUM; } return true; @@ -5676,6 +5859,16 @@ int idFileSystemLocal::ValidateDownloadPakForChecksum( int checksum, char path[ } // check the binary // a pure server sets the binary flag when starting the game + if ( pak->binary == BINARY_UNKNOWN ) { + const int confHash = HashFileName( BINARY_CONFIG ); + pak->binary = BINARY_NO; + for ( fileInPack_t *pakFile = pak->hashTable[ confHash ]; pakFile; pakFile = pakFile->next ) { + if ( !FilenameCompare( pakFile->name, BINARY_CONFIG ) ) { + pak->binary = BINARY_YES; + break; + } + } + } assert( pak->binary != BINARY_UNKNOWN ); pakBinary = ( pak->binary == BINARY_YES ) ? true : false; if ( isBinary != pakBinary ) { @@ -5709,6 +5902,7 @@ idFileSystemLocal::ClearPureChecksums void idFileSystemLocal::ClearPureChecksums( void ) { common->DPrintf( "Cleared pure server lock\n" ); serverPaks.Clear(); + memset( gamePakForOS, 0, sizeof( gamePakForOS ) ); } /* @@ -5722,8 +5916,10 @@ can be: some pak files currently referenced are not referenced by the server wrong order - if the pak order doesn't match, means some stuff could have been loaded from somewhere else server referenced files are prepended to the list if possible ( that doesn't break pureness ) -DLL: - the checksum of the pak containing the DLL is maintained seperately, the server can send different replies by OS +Game code: + protocol 2.41 carries the official q4base/game300.pk4 checksum as a + compatibility token, but openQ4 resolves executable modules only from its + trusted package/module roots and never from a server-selected PK4 ===================== */ fsPureReply_t idFileSystemLocal::SetPureServerChecksums( const int pureChecksums[ MAX_PURE_PAKS ], int _gamePakChecksum, int missingChecksums[ MAX_PURE_PAKS ], int *missingGamePakChecksum ) { @@ -5731,18 +5927,24 @@ fsPureReply_t idFileSystemLocal::SetPureServerChecksums( const int pureChecksums int i, j, imissing; bool success = true; bool canPrepend = true; - char dllName[MAX_OSPATH]; - int dllHash; - fileInPack_t * pakFile; - - sys->DLL_GetFileName( "game", dllName, MAX_OSPATH ); - dllHash = HashFileName( dllName ); imissing = 0; missingChecksums[ 0 ] = 0; assert( missingGamePakChecksum ); *missingGamePakChecksum = 0; + // Validate the legacy game-code field before touching the server-selected + // asset list. It is only a wire-compatibility token in openQ4; it must never + // select, download, extract, or restart into executable code. + if ( _gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM || + gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM || + gameDLLChecksum == 0 || gameModuleOrigin == GAME_MODULE_ORIGIN_NONE ) { + if ( fs_debug.GetBool() ) { + common->Printf( "pure server supplied unsupported game-code token 0x%x\n", _gamePakChecksum ); + } + return PURE_NODLL; + } + if ( pureChecksums[ 0 ] == 0 ) { ClearPureChecksums(); return PURE_OK; @@ -5815,48 +6017,6 @@ fsPureReply_t idFileSystemLocal::SetPureServerChecksums( const int pureChecksums j++; } - // DLL checksuming - if ( !_gamePakChecksum ) { - // server doesn't have knowledge of code we can use ( OS issue ) - return PURE_NODLL; - } - assert( gameDLLChecksum ); -#if ID_FAKE_PURE - gamePakChecksum = _gamePakChecksum; -#endif - if ( _gamePakChecksum != gamePakChecksum ) { - // current DLL is wrong, search for a pak with the approriate checksum - // ( search all paks, the pure list is not relevant here ) - pack = GetPackForChecksum( _gamePakChecksum ); - if ( !pack ) { - if ( fs_debug.GetBool() ) { - common->Printf( "missing the game code pak ( 0x%x )\n", _gamePakChecksum ); - } - // if there are other paks missing they have also been marked above - *missingGamePakChecksum = _gamePakChecksum; - return PURE_MISSING; - } - // if assets paks are missing, don't try any of the DLL restart / NODLL - if ( imissing ) { - return PURE_MISSING; - } - // we have a matching pak - if ( fs_debug.GetBool() ) { - common->Printf( "server's game code pak candidate is '%s' ( 0x%x )\n", pack->pakFilename.c_str(), pack->checksum ); - } - // make sure there is a valid DLL for us - if ( pack->hashTable[ dllHash ] ) { - for ( pakFile = pack->hashTable[ dllHash ]; pakFile; pakFile = pakFile->next ) { - if ( !FilenameCompare( pakFile->name, dllName ) ) { - gamePakChecksum = _gamePakChecksum; // this will be used to extract the DLL in pure mode FindDLL - return PURE_RESTART; - } - } - } - common->Warning( "media is misconfigured. server claims pak '%s' ( 0x%x ) has media for us, but '%s' is not found\n", pack->pakFilename.c_str(), pack->checksum, dllName ); - return PURE_NODLL; - } - // we reply to missing after DLL check so it can be part of the list if ( imissing ) { return PURE_MISSING; @@ -5885,10 +6045,12 @@ void idFileSystemLocal::GetPureServerChecksums( int checksums[ MAX_PURE_PAKS ], } checksums[ i ] = 0; if ( _gamePakChecksum ) { - if ( OS >= 0 ) { + if ( OS >= 0 && OS < MAX_GAME_OS ) { *_gamePakChecksum = gamePakForOS[ OS ]; - } else { + } else if ( OS == -1 ) { *_gamePakChecksum = gamePakChecksum; + } else { + *_gamePakChecksum = 0; } } } @@ -5928,6 +6090,7 @@ is resetting due to a game change ================ */ void idFileSystemLocal::Init( void ) { + fileSystemMainThread = std::this_thread::get_id(); // allow command line parms to override our defaults // we have to specially handle this, because normal command // line variable sets don't happen until after the filesystem @@ -5999,6 +6162,9 @@ void idFileSystemLocal::Init( void ) { // see if we are going to allow add-ons SetRestrictions(); + if ( levelLoadCache == NULL ) { + levelLoadCache = new idLevelLoadCacheManager( this ); + } // spawn a thread to handle background file reads StartBackgroundDownloadThread(); @@ -6031,6 +6197,13 @@ void idFileSystemLocal::Restart( void ) { // see if we are going to allow add-ons SetRestrictions(); + if ( levelLoadCache == NULL ) { + levelLoadCache = new idLevelLoadCacheManager( this ); + } + + // Shutdown( true ) stops the worker, so filesystem restarts must restore it + // before another background file read or pure-pack download is queued. + StartBackgroundDownloadThread(); // if we can't find default.cfg, assume that the paths are // busted and error out now, rather than getting an unreadable @@ -6052,6 +6225,13 @@ Frees all resources and closes all files */ void idFileSystemLocal::Shutdown( bool reloading ) { searchpath_t *sp, *next, *loop; + if ( levelLoadCache != NULL ) { + levelLoadCache->Cancel(); + if ( !reloading ) { + delete levelLoadCache; + levelLoadCache = NULL; + } + } StopBackgroundDownloadThread(); @@ -6075,6 +6255,8 @@ void idFileSystemLocal::Shutdown( bool reloading ) { loadedFileFromDir = false; gameDLLChecksum = 0; gamePakChecksum = 0; + gameModuleOrigin = GAME_MODULE_ORIGIN_NONE; + memset( gamePakForOS, 0, sizeof( gamePakForOS ) ); ClearDirCache(); @@ -6445,7 +6627,8 @@ idFile *idFileSystemLocal::OpenFileReadFlags( const char *relativePath, int sear } AddAssetLogEntry( relativePath ); - return file; + RecordOpenedLevelLoadSource( relativePath, file ); + return UsePreloadedLevelLoadSource( relativePath, file ); } else if ( search->pack && ( searchFlags & FSFLAG_SEARCH_PAKS ) ) { if ( !search->pack->hashTable[hash] ) { @@ -6506,7 +6689,8 @@ idFile *idFileSystemLocal::OpenFileReadFlags( const char *relativePath, int sear common->Printf( "idFileSystem::OpenFileRead: %s (found in '%s')\n", relativePath, pak->pakFilename.c_str() ); } AddAssetLogEntry( relativePath ); - return file; + RecordOpenedLevelLoadSource( relativePath, file ); + return UsePreloadedLevelLoadSource( relativePath, file ); } } } @@ -6531,7 +6715,8 @@ idFile *idFileSystemLocal::OpenFileReadFlags( const char *relativePath, int sear common->Printf( "idFileSystem::OpenFileRead: %s (found in addon pk4 '%s')\n", relativePath, search->pack->pakFilename.c_str() ); } AddAssetLogEntry( relativePath ); - return file; + RecordOpenedLevelLoadSource( relativePath, file ); + return UsePreloadedLevelLoadSource( relativePath, file ); } } } @@ -6831,6 +7016,55 @@ int idFileSystemLocal::CurlProgressFunction( void *clientp, double dltotal, doub } #if ID_ENABLE_CURL +static bool fsCurlGlobalInitialized = false; +static bool fsCurlHTTPReady = false; + +static void FS_ShutdownCurl() { + fsCurlHTTPReady = false; + if ( fsCurlGlobalInitialized ) { + curl_global_cleanup(); + fsCurlGlobalInitialized = false; + } +} + +static bool FS_InitializeCurl() { +#if !OPENQ4_CURL_CAPABLE_BUILD + common->Warning( "HTTP downloads disabled: libcurl 7.19.4 or newer with asynchronous DNS is required" ); + return false; +#else + if ( fsCurlHTTPReady ) { + return true; + } + + const CURLcode initResult = curl_global_init( CURL_GLOBAL_DEFAULT ); + if ( initResult != CURLE_OK ) { + common->Warning( "HTTP downloads disabled: curl_global_init failed (%s)", curl_easy_strerror( initResult ) ); + return false; + } + fsCurlGlobalInitialized = true; + + const curl_version_info_data *versionInfo = curl_version_info( CURLVERSION_NOW ); + bool hasHTTP = false; + if ( versionInfo != NULL && versionInfo->protocols != NULL ) { + for ( const char *const *protocol = versionInfo->protocols; *protocol != NULL; ++protocol ) { + if ( idStr::Icmp( *protocol, "http" ) == 0 ) { + hasHTTP = true; + break; + } + } + } + if ( versionInfo == NULL || versionInfo->version_num < 0x071304 || + ( versionInfo->features & CURL_VERSION_ASYNCHDNS ) == 0 || !hasHTTP ) { + common->Warning( "HTTP downloads disabled: libcurl lacks the required version, HTTP, or asynchronous-DNS capability" ); + FS_ShutdownCurl(); + return false; + } + + fsCurlHTTPReady = true; + return true; +#endif +} + class idScopedCurlEasySession { public: explicit idScopedCurlEasySession( CURL *session ) : session( session ) {} @@ -6843,6 +7077,52 @@ class idScopedCurlEasySession { idScopedCurlEasySession &operator=( const idScopedCurlEasySession & ) = delete; CURL *session; }; + +static CURLcode FS_ConfigureBoundedHTTPTransfer( CURL *session ) { +#if !OPENQ4_CURL_CAPABLE_BUILD + return CURLE_FAILED_INIT; +#else + CURLcode result; + + // Keep package transfer and shutdown behavior bounded even when a remote + // endpoint accepts a connection and then stops making progress. Protocol + // masks are repeated here so future callers cannot bypass URLPolicy by + // reaching libcurl directly. + result = curl_easy_setopt( session, CURLOPT_PROTOCOLS, static_cast( CURLPROTO_HTTP | CURLPROTO_HTTPS ) ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_REDIR_PROTOCOLS, static_cast( CURLPROTO_HTTP | CURLPROTO_HTTPS ) ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_FOLLOWLOCATION, 0L ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_MAXREDIRS, 0L ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_CONNECTTIMEOUT, 15L ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_LOW_SPEED_LIMIT, 1024L ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_LOW_SPEED_TIME, 30L ); + if ( result != CURLE_OK ) { + return result; + } + result = curl_easy_setopt( session, CURLOPT_TIMEOUT, 3600L ); + if ( result != CURLE_OK ) { + return result; + } + return curl_easy_setopt( session, CURLOPT_NOSIGNAL, 1L ); +#endif +} #endif /* @@ -6875,8 +7155,27 @@ dword BackgroundDownloadThread( void *parms ) { // use the low level read function, because fread may allocate memory fread(bgl->file.buffer, bgl->file.length, 1, static_cast(bgl->f)->GetFilePtr()); bgl->completed = true; - } else { + } else if ( bgl->opcode == DLTYPE_URL ) { #if ID_ENABLE_CURL + dlStatus_t expectedStatus = DL_WAIT; + if ( !bgl->url.status.compare_exchange_strong( expectedStatus, DL_INPROGRESS ) ) { + bgl->url.dlstatus = -1; + bgl->url.status = DL_FAILED; + idStr::Copynz( bgl->url.dlerror, + expectedStatus == DL_ABORTING ? "download cancelled" : "invalid download state", + MAX_STRING_CHARS ); + bgl->completed = true; + continue; + } + if ( !fsCurlHTTPReady ) { + bgl->url.dlstatus = CURLE_FAILED_INIT; + bgl->url.status = DL_FAILED; + idStr::Copynz( bgl->url.dlerror, + "HTTP download support is unavailable with this libcurl runtime", + MAX_STRING_CHARS ); + bgl->completed = true; + continue; + } // DLTYPE_URL // use a local buffer for curl error since the size define is local char error_buf[ CURL_ERROR_SIZE ]; @@ -6907,7 +7206,14 @@ dword BackgroundDownloadThread( void *parms ) { bgl->completed = true; continue; } - ret = curl_easy_setopt( session, CURLOPT_FAILONERROR, 1 ); + ret = FS_ConfigureBoundedHTTPTransfer( session ); + if ( ret ) { + bgl->url.dlstatus = ret; + bgl->url.status = DL_FAILED; + bgl->completed = true; + continue; + } + ret = curl_easy_setopt( session, CURLOPT_FAILONERROR, 1L ); if ( ret ) { bgl->url.dlstatus = ret; bgl->url.status = DL_FAILED; @@ -6928,7 +7234,7 @@ dword BackgroundDownloadThread( void *parms ) { bgl->completed = true; continue; } - ret = curl_easy_setopt( session, CURLOPT_NOPROGRESS, 0 ); + ret = curl_easy_setopt( session, CURLOPT_NOPROGRESS, 0L ); if ( ret ) { bgl->url.dlstatus = ret; bgl->url.status = DL_FAILED; @@ -6951,7 +7257,6 @@ dword BackgroundDownloadThread( void *parms ) { } bgl->url.dlnow = 0; bgl->url.dltotal = 0; - bgl->url.status = DL_INPROGRESS; ret = curl_easy_perform( session ); if ( ret ) { const char *errorMessage = bgl->url.dlerror[ 0 ] != '\0' ? bgl->url.dlerror : @@ -6975,12 +7280,26 @@ dword BackgroundDownloadThread( void *parms ) { bgl->completed = true; continue; } - bgl->url.status = DL_DONE; + expectedStatus = DL_INPROGRESS; + if ( !bgl->url.status.compare_exchange_strong( expectedStatus, DL_DONE ) ) { + bgl->url.dlstatus = -1; + bgl->url.status = DL_FAILED; + idStr::Copynz( bgl->url.dlerror, + expectedStatus == DL_ABORTING ? "download cancelled" : "invalid download completion state", + MAX_STRING_CHARS ); + } bgl->completed = true; #else bgl->url.status = DL_FAILED; + bgl->url.dlstatus = -1; + idStr::Copynz( bgl->url.dlerror, "HTTP download support is unavailable in this build", MAX_STRING_CHARS ); bgl->completed = true; #endif + } else { + bgl->url.status = DL_FAILED; + bgl->url.dlstatus = -1; + idStr::Copynz( bgl->url.dlerror, "unsupported background download operation", MAX_STRING_CHARS ); + bgl->completed = true; } } return 0; @@ -6993,9 +7312,15 @@ idFileSystemLocal::StartBackgroundReadThread */ void idFileSystemLocal::StartBackgroundDownloadThread() { if ( !backgroundThread.threadHandle ) { +#if ID_ENABLE_CURL + FS_InitializeCurl(); +#endif Sys_CreateThread( (xthread_t)BackgroundDownloadThread, NULL, THREAD_NORMAL, backgroundThread, "backgroundDownload", g_threads, &g_thread_count ); if ( !backgroundThread.threadHandle ) { common->Warning( "idFileSystemLocal::StartBackgroundDownloadThread: failed" ); +#if ID_ENABLE_CURL + FS_ShutdownCurl(); +#endif } } else { common->Printf( "background thread already running\n" ); @@ -7027,6 +7352,10 @@ void idFileSystemLocal::StopBackgroundDownloadThread() { bgl->completed = true; bgl = next; } + +#if ID_ENABLE_CURL + FS_ShutdownCurl(); +#endif } /* @@ -7035,6 +7364,28 @@ idFileSystemLocal::BackgroundDownload ================= */ void idFileSystemLocal::BackgroundDownload( backgroundDownload_t *bgl ) { + if ( bgl == NULL ) { + return; + } + if ( bgl->opcode != DLTYPE_FILE && bgl->opcode != DLTYPE_URL ) { + bgl->url.status = DL_FAILED; + bgl->url.dlstatus = -1; + idStr::Copynz( bgl->url.dlerror, "unsupported background download operation", sizeof( bgl->url.dlerror ) ); + bgl->completed = true; + return; + } + if ( bgl->opcode == DLTYPE_URL && !idURLPolicy::IsAllowedHTTPURL( bgl->url.url.c_str() ) ) { + // Keep the generic downloader fail-closed too. Today the async pure-pack + // client is the sole URL producer, but a future caller must not silently + // re-enable file:, SMB, or another libcurl-supported protocol. + bgl->url.status = DL_FAILED; + bgl->url.dlstatus = -1; + idStr::Copynz( bgl->url.dlerror, + "download URL must be bounded HTTP or HTTPS with a host", + sizeof( bgl->url.dlerror ) ); + bgl->completed = true; + return; + } if ( bgl->opcode == DLTYPE_FILE ) { if ( dynamic_cast(bgl->f) ) { // add the bgl to the background download list @@ -7169,6 +7520,7 @@ void idFileSystemLocal::FindDLL( const char *name, char _dllPath[ MAX_OSPATH ], idFile *dllFile = NULL; char dllName[MAX_OSPATH]; idStr dllPath; + gameModuleOrigin_t resolvedOrigin = GAME_MODULE_ORIGIN_NONE; sys->DLL_GetFileName( name, dllName, MAX_OSPATH ); @@ -7207,6 +7559,16 @@ void idFileSystemLocal::FindDLL( const char *name, char _dllPath[ MAX_OSPATH ], if ( !moduleGameDir[0] ) { moduleGameDir = OPENQ4_GAMEDIR; } + if ( !idGameDirPolicy::IsPortableSegment( moduleGameDir ) ) { + common->Warning( "Refusing game module lookup for unsafe fs_game '%s'", moduleGameDir ); + if ( updateChecksum ) { + gameDLLChecksum = 0; + gamePakChecksum = 0; + gameModuleOrigin = GAME_MODULE_ORIGIN_NONE; + } + _dllPath[ 0 ] = '\0'; + return; + } idStr trustedModuleRoots; idStr attemptedModulePaths; for ( int i = 0; i < numModuleSearchRoots; ++i ) { @@ -7218,12 +7580,19 @@ void idFileSystemLocal::FindDLL( const char *name, char _dllPath[ MAX_OSPATH ], for ( int i = 0; !dllFile && i < numModuleSearchRoots; i++ ) { FS_AppendGameModuleSearchPath( attemptedModulePaths, moduleSearchRoots[i], moduleGameDir, dllName ); dllFile = FS_OpenGameModuleFromExeDir( this, moduleSearchRoots[i], moduleGameDir, dllName, dllPath ); + if ( dllFile ) { + resolvedOrigin = idStr::Icmp( moduleGameDir, OPENQ4_GAMEDIR ) == 0 ? + GAME_MODULE_ORIGIN_BASE_GAME : GAME_MODULE_ORIGIN_ACTIVE_MOD; + } } if ( !dllFile && idStr::Icmp( moduleGameDir, OPENQ4_GAMEDIR ) != 0 ) { for ( int i = 0; !dllFile && i < numModuleSearchRoots; i++ ) { FS_AppendGameModuleSearchPath( attemptedModulePaths, moduleSearchRoots[i], OPENQ4_GAMEDIR, dllName ); dllFile = FS_OpenGameModuleFromExeDir( this, moduleSearchRoots[i], OPENQ4_GAMEDIR, dllName, dllPath ); + if ( dllFile ) { + resolvedOrigin = GAME_MODULE_ORIGIN_BASE_GAME; + } } if ( dllFile ) { common->DPrintf( "Game DLL '%s' not found in mod directory '%s'; falling back to '%s'.\n", dllName, moduleGameDir, OPENQ4_GAMEDIR ); @@ -7235,6 +7604,9 @@ void idFileSystemLocal::FindDLL( const char *name, char _dllPath[ MAX_OSPATH ], dllPath = moduleSearchRoots[i]; dllPath.AppendPath( dllName ); dllFile = OpenExplicitFileRead( dllPath ); + if ( dllFile ) { + resolvedOrigin = GAME_MODULE_ORIGIN_PACKAGE_ROOT; + } } if ( updateChecksum ) { @@ -7243,7 +7615,8 @@ void idFileSystemLocal::FindDLL( const char *name, char _dllPath[ MAX_OSPATH ], } else { gameDLLChecksum = 0; } - gamePakChecksum = 0; + gameModuleOrigin = resolvedOrigin; + gamePakChecksum = dllFile ? OPENQ4_Q4_142_GAME300_PAK_CHECKSUM : 0; } if ( dllFile ) { dllPath = dllFile->GetFullPath( ); diff --git a/src/framework/FileSystem.h b/src/framework/FileSystem.h index d86222c7..fba88836 100644 --- a/src/framework/FileSystem.h +++ b/src/framework/FileSystem.h @@ -29,6 +29,8 @@ If you have questions concerning this license or the applicable additional terms #ifndef __FILESYSTEM_H__ #define __FILESYSTEM_H__ +#include + /* =============================================================================== @@ -55,6 +57,7 @@ If you have questions concerning this license or the applicable additional terms static const ID_TIME_T FILE_NOT_FOUND_TIMESTAMP = static_cast( -1 ); static const int MAX_PURE_PAKS = 128; static const int MAX_OSPATH = 256; +static const int MAX_GAME_OS = 6; // modes for OpenFileByMode. used as bit mask internally typedef enum { @@ -83,25 +86,44 @@ typedef enum { DL_FAILED } dlStatus_t; -typedef enum { - FILE_EXEC, - FILE_OPEN -} dlMime_t; - typedef enum { FIND_NO, FIND_YES, FIND_ADDON } findFile_t; +// Typed scheduling records used by the learned, per-map preload manifest. +// These values are part of the engine/game and renderer-module ABI; append new +// values instead of reordering existing ones. +typedef enum { + LEVEL_LOAD_RESOURCE_RENDER_MODEL = 1, + LEVEL_LOAD_RESOURCE_IMAGE, + LEVEL_LOAD_RESOURCE_ANIMATION, + LEVEL_LOAD_RESOURCE_SOUND, + LEVEL_LOAD_RESOURCE_COLLISION, + LEVEL_LOAD_RESOURCE_WORLD, + LEVEL_LOAD_RESOURCE_MATERIAL, + LEVEL_LOAD_RESOURCE_GUI, + LEVEL_LOAD_RESOURCE_EFFECT, + LEVEL_LOAD_RESOURCE_SKIN, + LEVEL_LOAD_RESOURCE_DECL, + LEVEL_LOAD_RESOURCE_RAW +} levelLoadResourceType_t; + +typedef enum { + GENERATED_CACHE_RENDER_MODEL = 1, + GENERATED_CACHE_RENDER_WORLD, + GENERATED_CACHE_COLLISION_MODEL +} generatedCacheKind_t; + typedef struct urlDownload_s { idStr url; char dlerror[ MAX_STRING_CHARS ]; int expectedSize; // immutable transfer limit; zero permits an unrestricted download - int dltotal; - int dlnow; - int dlstatus; - dlStatus_t status; + std::atomic dltotal; + std::atomic dlnow; + std::atomic dlstatus; + std::atomic status; } urlDownload_t; typedef struct fileDownload_s { @@ -116,7 +138,7 @@ typedef struct backgroundDownload_s { idFile * f; fileDownload_t file; urlDownload_t url; - volatile bool completed; + std::atomic completed; } backgroundDownload_t; // file list for directory listings @@ -332,6 +354,27 @@ class idFileSystem { virtual bool FilenameCompare( const char *s1, const char *s2 ) const = 0; virtual bool InProductionMode() = 0; + + // Milestone B extensions are append-only at the end of the shared + // engine/game vtable. Older game modules retain the complete historical + // prefix, while API v44 modules can opt into these coordinator services. + virtual void BeginLevelLoadCache( const char *mapKey, const char *gameMode, + const char *entityFilter, const char *settingsKey ) = 0; + virtual void FinishLevelLoadCache( bool successful ) = 0; + virtual void CancelLevelLoadCache( void ) = 0; + virtual void RecordLevelLoadResource( levelLoadResourceType_t type, + const char *name, const char *options = "", + unsigned int flags = 0, unsigned int priority = 1 ) = 0; + virtual idFile * OpenGeneratedCacheRead( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey = "" ) = 0; + virtual bool WriteGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey, const void *payload, + unsigned int payloadBytes ) = 0; + virtual void DiscardGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey = "" ) = 0; }; extern idFileSystem * fileSystem; diff --git a/src/framework/GameDirPolicy.h b/src/framework/GameDirPolicy.h new file mode 100644 index 00000000..10f3cc90 --- /dev/null +++ b/src/framework/GameDirPolicy.h @@ -0,0 +1,108 @@ +/* +=========================================================================== + +openQ4 GPL Source Code +Copyright (C) 2026 the openQ4 contributors. + +This file is part of the openQ4 Source Code. See docs/legal for details. + +=========================================================================== +*/ + +#ifndef __GAMEDIRPOLICY_H__ +#define __GAMEDIRPOLICY_H__ + +// fs_game values cross network, package and host-filesystem boundaries. Keep +// them to one filename segment that has the same meaning on every supported +// host; in particular, never let a separator or Windows alias reach a path +// join performed under a trusted package root. +namespace idGameDirPolicy { + +static const int MAX_SEGMENT_BYTES = 255; + +inline unsigned char LowerASCII( unsigned char value ) { + return value >= 'A' && value <= 'Z' ? static_cast( value + ( 'a' - 'A' ) ) : value; +} + +inline bool HasASCIIStem( const char *segment, int stemLength, const char *expected, int expectedLength ) { + if ( stemLength != expectedLength ) { + return false; + } + for ( int index = 0; index < expectedLength; ++index ) { + if ( LowerASCII( static_cast( segment[ index ] ) ) != + static_cast( expected[ index ] ) ) { + return false; + } + } + return true; +} + +inline bool HasASCIIPrefix( const char *segment, const char *expected, int expectedLength ) { + for ( int index = 0; index < expectedLength; ++index ) { + if ( LowerASCII( static_cast( segment[ index ] ) ) != + static_cast( expected[ index ] ) ) { + return false; + } + } + return true; +} + +inline bool IsWindowsDeviceName( const char *segment, int segmentLength ) { + int stemLength = 0; + while ( stemLength < segmentLength && segment[ stemLength ] != '.' ) { + stemLength++; + } + + if ( HasASCIIStem( segment, stemLength, "con", 3 ) || + HasASCIIStem( segment, stemLength, "prn", 3 ) || + HasASCIIStem( segment, stemLength, "aux", 3 ) || + HasASCIIStem( segment, stemLength, "nul", 3 ) ) { + return true; + } + + if ( stemLength < 4 ) { + return false; + } + const bool portPrefix = HasASCIIPrefix( segment, "com", 3 ) || HasASCIIPrefix( segment, "lpt", 3 ); + if ( !portPrefix ) { + return false; + } + + const unsigned char digit = static_cast( segment[ 3 ] ); + if ( stemLength == 4 ) { + return ( digit >= '1' && digit <= '9' ) || digit == 0xB9 || digit == 0xB2 || digit == 0xB3; + } + + return stemLength == 5 && digit == 0xC2 && + ( static_cast( segment[ 4 ] ) == 0xB9 || + static_cast( segment[ 4 ] ) == 0xB2 || + static_cast( segment[ 4 ] ) == 0xB3 ); +} + +inline bool IsPortableSegment( const char *segment ) { + if ( segment == nullptr || segment[ 0 ] == '\0' ) { + return false; + } + + int segmentLength = 0; + for ( const unsigned char *scan = reinterpret_cast( segment ); *scan != '\0'; ++scan ) { + if ( ++segmentLength > MAX_SEGMENT_BYTES || *scan < 32 || *scan == '/' || *scan == '\\' || + *scan == ':' || *scan == '<' || *scan == '>' || *scan == '"' || *scan == '|' || + *scan == '?' || *scan == '*' ) { + return false; + } + } + + if ( ( segmentLength == 1 && segment[ 0 ] == '.' ) || + ( segmentLength == 2 && segment[ 0 ] == '.' && segment[ 1 ] == '.' ) || + segment[ 0 ] == ' ' || segment[ segmentLength - 1 ] == '.' || + segment[ segmentLength - 1 ] == ' ' ) { + return false; + } + + return !IsWindowsDeviceName( segment, segmentLength ); +} + +} // namespace idGameDirPolicy + +#endif /* !__GAMEDIRPOLICY_H__ */ diff --git a/src/framework/LevelLoadCacheFormat.cpp b/src/framework/LevelLoadCacheFormat.cpp new file mode 100644 index 00000000..8d1f33ed --- /dev/null +++ b/src/framework/LevelLoadCacheFormat.cpp @@ -0,0 +1,1291 @@ +/* +=========================================================================== + +openQ4 hardened level-load cache and preload-manifest format + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +This is an original openQ4 implementation. No engine structures are written +to disk: the wire representation is explicit, little endian, bounded, and +protected by SHA-256. + +=========================================================================== +*/ + +#include "LevelLoadCacheFormat.h" + +#include +#include +#include +#include +#include +#include + +// The engine's Windows PCH includes windows.h. Its legacy min/max macros are +// incompatible with numeric_limits::max() and std::min/std::max. +#if defined( min ) + #undef min +#endif +#if defined( max ) + #undef max +#endif + +namespace idLevelLoadCache { +namespace { + +constexpr std::uint32_t MakeMagic( const char a, const char b, const char c, const char d ) { + return static_cast( static_cast( a ) ) | + ( static_cast( static_cast( b ) ) << 8 ) | + ( static_cast( static_cast( c ) ) << 16 ) | + ( static_cast( static_cast( d ) ) << 24 ); +} + +static constexpr std::uint32_t CACHE_MAGIC = MakeMagic( 'O', 'Q', '4', 'C' ); +static constexpr std::uint32_t CACHE_END_MAGIC = MakeMagic( 'E', 'C', '4', 'O' ); +static constexpr std::uint32_t MANIFEST_MAGIC = MakeMagic( 'O', 'Q', '4', 'M' ); +static constexpr std::uint32_t MANIFEST_END_MAGIC = MakeMagic( 'E', 'M', '4', 'O' ); +static constexpr std::size_t TRAILER_BYTES = HASH_BYTES + sizeof( std::uint32_t ); +static constexpr std::size_t MIN_MANIFEST_ENTRY_BYTES = 62; + +Result Success() { + return Result{}; +} + +Result Failure( const Status status, const std::size_t offset, const char *diagnostic ) { + Result result; + result.status = status; + result.offset = offset; + result.diagnostic = diagnostic; + return result; +} + +bool HashIsZero( const Hash &hash ) { + std::uint8_t combined = 0; + for ( const std::uint8_t value : hash ) { + combined |= value; + } + return combined == 0; +} + +bool HashesEqual( const Hash &left, const Hash &right ) { + std::uint8_t difference = 0; + for ( std::size_t index = 0; index < HASH_BYTES; ++index ) { + difference |= static_cast( left[index] ^ right[index] ); + } + return difference == 0; +} + +bool AddWithoutOverflow( const std::size_t left, const std::size_t right, + std::size_t &sum ) { + if ( right > std::numeric_limits::max() - left ) { + return false; + } + sum = left + right; + return true; +} + +std::uint32_t RotateRight( const std::uint32_t value, const unsigned int bits ) { + return ( value >> bits ) | ( value << ( 32u - bits ) ); +} + +std::uint32_t LoadBigEndian32( const std::uint8_t *bytes ) { + return ( static_cast( bytes[0] ) << 24 ) | + ( static_cast( bytes[1] ) << 16 ) | + ( static_cast( bytes[2] ) << 8 ) | + static_cast( bytes[3] ); +} + +void StoreBigEndian32( std::uint8_t *bytes, const std::uint32_t value ) { + bytes[0] = static_cast( value >> 24 ); + bytes[1] = static_cast( value >> 16 ); + bytes[2] = static_cast( value >> 8 ); + bytes[3] = static_cast( value ); +} + +void StoreBigEndian64( std::uint8_t *bytes, const std::uint64_t value ) { + for ( unsigned int index = 0; index < 8; ++index ) { + bytes[index] = static_cast( value >> ( 56u - index * 8u ) ); + } +} + +class Sha256 { +public: + Sha256() { + state[0] = 0x6a09e667u; + state[1] = 0xbb67ae85u; + state[2] = 0x3c6ef372u; + state[3] = 0xa54ff53au; + state[4] = 0x510e527fu; + state[5] = 0x9b05688cu; + state[6] = 0x1f83d9abu; + state[7] = 0x5be0cd19u; + } + + void Update( const void *input, const std::size_t bytes ) { + if ( bytes == 0 ) { + return; + } + const auto *source = static_cast( input ); + totalBytes += static_cast( bytes ); + std::size_t remaining = bytes; + while ( remaining != 0 ) { + const std::size_t available = sizeof( buffer ) - bufferBytes; + const std::size_t amount = std::min( available, remaining ); + std::memcpy( buffer + bufferBytes, source, amount ); + bufferBytes += amount; + source += amount; + remaining -= amount; + if ( bufferBytes == sizeof( buffer ) ) { + Transform( buffer ); + bufferBytes = 0; + } + } + } + + Hash Finish() { + const std::uint64_t messageBits = totalBytes * 8u; + const std::uint8_t highBit = 0x80u; + Update( &highBit, 1 ); + const std::uint8_t zero = 0; + while ( bufferBytes != 56 ) { + Update( &zero, 1 ); + } + std::uint8_t lengthBytes[8]; + StoreBigEndian64( lengthBytes, messageBits ); + Update( lengthBytes, sizeof( lengthBytes ) ); + + Hash digest{}; + for ( std::size_t index = 0; index < 8; ++index ) { + StoreBigEndian32( digest.data() + index * 4, state[index] ); + } + return digest; + } + +private: + void Transform( const std::uint8_t block[64] ) { + static constexpr std::uint32_t constants[64] = { + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, + 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, + 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, + 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, + 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, + 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, + 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, + 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, + 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, + 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, + 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, + 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, + 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, + 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, + 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, + 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u + }; + + std::uint32_t schedule[64]; + for ( std::size_t index = 0; index < 16; ++index ) { + schedule[index] = LoadBigEndian32( block + index * 4 ); + } + for ( std::size_t index = 16; index < 64; ++index ) { + const std::uint32_t s0 = RotateRight( schedule[index - 15], 7 ) ^ + RotateRight( schedule[index - 15], 18 ) ^ ( schedule[index - 15] >> 3 ); + const std::uint32_t s1 = RotateRight( schedule[index - 2], 17 ) ^ + RotateRight( schedule[index - 2], 19 ) ^ ( schedule[index - 2] >> 10 ); + schedule[index] = schedule[index - 16] + s0 + schedule[index - 7] + s1; + } + + std::uint32_t a = state[0]; + std::uint32_t b = state[1]; + std::uint32_t c = state[2]; + std::uint32_t d = state[3]; + std::uint32_t e = state[4]; + std::uint32_t f = state[5]; + std::uint32_t g = state[6]; + std::uint32_t h = state[7]; + + for ( std::size_t index = 0; index < 64; ++index ) { + const std::uint32_t upperE = RotateRight( e, 6 ) ^ RotateRight( e, 11 ) ^ RotateRight( e, 25 ); + const std::uint32_t choose = ( e & f ) ^ ( ( ~e ) & g ); + const std::uint32_t temporary1 = h + upperE + choose + constants[index] + schedule[index]; + const std::uint32_t upperA = RotateRight( a, 2 ) ^ RotateRight( a, 13 ) ^ RotateRight( a, 22 ); + const std::uint32_t majority = ( a & b ) ^ ( a & c ) ^ ( b & c ); + const std::uint32_t temporary2 = upperA + majority; + h = g; + g = f; + f = e; + e = d + temporary1; + d = c; + c = b; + b = a; + a = temporary1 + temporary2; + } + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + state[5] += f; + state[6] += g; + state[7] += h; + } + + std::uint32_t state[8]{}; + std::uint8_t buffer[64]{}; + std::size_t bufferBytes = 0; + std::uint64_t totalBytes = 0; +}; + +class Writer { +public: + explicit Writer( const std::size_t maximumBytes ) : maximum( maximumBytes ) { + } + + bool WriteU32( const std::uint32_t value ) { + const std::uint8_t bytes[4] = { + static_cast( value ), + static_cast( value >> 8 ), + static_cast( value >> 16 ), + static_cast( value >> 24 ) + }; + return WriteBytes( bytes, sizeof( bytes ) ); + } + + bool WriteU64( const std::uint64_t value ) { + std::uint8_t bytes[8]; + for ( unsigned int index = 0; index < 8; ++index ) { + bytes[index] = static_cast( value >> ( index * 8u ) ); + } + return WriteBytes( bytes, sizeof( bytes ) ); + } + + bool WriteHash( const Hash &hash ) { + return WriteBytes( hash.data(), hash.size() ); + } + + bool WriteString( const std::string &value ) { + if ( value.size() > std::numeric_limits::max() ) { + return false; + } + return WriteU32( static_cast( value.size() ) ) && + WriteBytes( value.data(), value.size() ); + } + + bool WriteBytes( const void *source, const std::size_t bytes ) { + if ( bytes > maximum - std::min( maximum, data.size() ) ) { + return false; + } + if ( bytes == 0 ) { + return true; + } + const auto *begin = static_cast( source ); + data.insert( data.end(), begin, begin + bytes ); + return true; + } + + std::size_t Size() const { + return data.size(); + } + + const std::vector &Data() const { + return data; + } + + std::vector Take() { + return std::move( data ); + } + +private: + std::size_t maximum; + std::vector data; +}; + +class Reader { +public: + Reader( const void *source, const std::size_t sourceBytes ) : + data( static_cast( source ) ), size( sourceBytes ) { + } + + bool ReadU32( std::uint32_t &value, const char *field ) { + if ( !Require( 4, field ) ) { + return false; + } + value = static_cast( data[position] ) | + ( static_cast( data[position + 1] ) << 8 ) | + ( static_cast( data[position + 2] ) << 16 ) | + ( static_cast( data[position + 3] ) << 24 ); + position += 4; + return true; + } + + bool ReadU64( std::uint64_t &value, const char *field ) { + if ( !Require( 8, field ) ) { + return false; + } + value = 0; + for ( unsigned int index = 0; index < 8; ++index ) { + value |= static_cast( data[position + index] ) << ( index * 8u ); + } + position += 8; + return true; + } + + bool ReadHash( Hash &hash, const char *field ) { + if ( !Require( hash.size(), field ) ) { + return false; + } + std::memcpy( hash.data(), data + position, hash.size() ); + position += hash.size(); + return true; + } + + bool ReadString( std::string &value, const std::size_t maximumBytes, + std::size_t &aggregateBytes, const std::size_t maximumAggregateBytes, + const char *field ) { + std::uint32_t length = 0; + if ( !ReadU32( length, field ) ) { + return false; + } + if ( length == 0 ) { + Fail( Status::INVALID_STRING, field ); + return false; + } + if ( static_cast( length ) > maximumBytes ) { + Fail( Status::SIZE_LIMIT_EXCEEDED, field ); + return false; + } + if ( static_cast( length ) > maximumAggregateBytes - + std::min( maximumAggregateBytes, aggregateBytes ) ) { + Fail( Status::SIZE_LIMIT_EXCEEDED, "aggregate variable-length data" ); + return false; + } + if ( !Require( length, field ) ) { + return false; + } + value.assign( reinterpret_cast( data + position ), length ); + position += length; + aggregateBytes += length; + return true; + } + + bool ReadByteVector( std::vector &value, + const std::size_t maximumBytes, std::size_t &aggregateBytes, + const std::size_t maximumAggregateBytes, const char *field ) { + std::uint32_t length = 0; + if ( !ReadU32( length, field ) ) { + return false; + } + if ( static_cast( length ) > maximumBytes ) { + Fail( Status::SIZE_LIMIT_EXCEEDED, field ); + return false; + } + if ( static_cast( length ) > maximumAggregateBytes - + std::min( maximumAggregateBytes, aggregateBytes ) ) { + Fail( Status::SIZE_LIMIT_EXCEEDED, "aggregate variable-length data" ); + return false; + } + if ( !Require( length, field ) ) { + return false; + } + value.assign( data + position, data + position + length ); + position += length; + aggregateBytes += length; + return true; + } + + bool Skip( const std::size_t bytes, const char *field ) { + if ( !Require( bytes, field ) ) { + return false; + } + position += bytes; + return true; + } + + const std::uint8_t *At( const std::size_t offset ) const { + return data + offset; + } + + std::size_t Position() const { + return position; + } + + std::size_t Remaining() const { + return size - position; + } + + const Result &Error() const { + return error; + } + + void Fail( const Status status, const char *field ) { + if ( error.Ok() ) { + error.status = status; + error.offset = position; + error.diagnostic = field; + } + } + +private: + bool Require( const std::size_t bytes, const char *field ) { + if ( bytes > Remaining() ) { + Fail( Status::TRUNCATED, field ); + return false; + } + return true; + } + + const std::uint8_t *data; + std::size_t size; + std::size_t position = 0; + Result error; +}; + +bool IsValidCacheKind( const CacheKind kind ) { + return kind == CacheKind::RENDER_MODEL || kind == CacheKind::RENDER_WORLD || + kind == CacheKind::COLLISION_MODEL; +} + +bool IsValidCodec( const CompressionCodec codec ) { + return codec == CompressionCodec::NONE || codec == CompressionCodec::DEFLATE; +} + +bool IsValidEntryType( const ManifestEntryType type ) { + const auto value = static_cast( type ); + return value >= static_cast( ManifestEntryType::RENDER_MODEL ) && + value <= static_cast( ManifestEntryType::RAW_FILE ); +} + +bool IsValidPriority( const ManifestPriority priority ) { + return static_cast( priority ) <= + static_cast( ManifestPriority::CRITICAL ); +} + +Result ValidateSource( const SourceIdentity &source, const DecodeLimits &limits ) { + if ( source.normalizedPath.size() > limits.maxAggregateStringBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "source aggregate variable-length data" ); + } + std::string normalized; + Result result = NormalizeVirtualPath( source.normalizedPath, normalized, limits.maxPathBytes ); + if ( !result ) { + return result; + } + if ( normalized != source.normalizedPath ) { + return Failure( Status::PATH_NOT_NORMALIZED, 0, "source path is not canonical" ); + } + if ( source.containerKind == SourceContainerKind::LOOSE_FILE ) { + if ( source.containerPk4Checksum != 0 ) { + return Failure( Status::INVALID_FIELD, 0, "loose source has a PK4 checksum" ); + } + } else if ( source.containerKind == SourceContainerKind::PK4_ARCHIVE ) { + if ( source.containerPk4Checksum == 0 ) { + return Failure( Status::INVALID_FIELD, 0, "PK4 source has an unknown checksum" ); + } + } else { + return Failure( Status::INVALID_ENUM_VALUE, 0, "unknown source-container kind" ); + } + return Success(); +} + +bool WriteSource( Writer &writer, const SourceIdentity &source ) { + return writer.WriteString( source.normalizedPath ) && + writer.WriteU64( source.size ) && writer.WriteU64( source.timestamp ) && + writer.WriteU32( static_cast( source.containerKind ) ) && + writer.WriteU32( source.containerPk4Checksum ); +} + +Result ReadSource( Reader &reader, SourceIdentity &source, const DecodeLimits &limits, + std::size_t &aggregateBytes ) { + std::uint32_t containerKind = 0; + if ( !reader.ReadString( source.normalizedPath, limits.maxPathBytes, + aggregateBytes, limits.maxAggregateStringBytes, "source path" ) || + !reader.ReadU64( source.size, "source size" ) || + !reader.ReadU64( source.timestamp, "source timestamp" ) || + !reader.ReadU32( containerKind, "source-container kind" ) || + !reader.ReadU32( source.containerPk4Checksum, "source PK4 checksum" ) ) { + return reader.Error(); + } + source.containerKind = static_cast( containerKind ); + Result result = ValidateSource( source, limits ); + if ( !result ) { + result.offset = reader.Position(); + } + return result; +} + +bool SourceLess( const SourceIdentity &left, const SourceIdentity &right ) { + if ( left.normalizedPath != right.normalizedPath ) { + return left.normalizedPath < right.normalizedPath; + } + if ( left.containerKind != right.containerKind ) { + return static_cast( left.containerKind ) < + static_cast( right.containerKind ); + } + if ( left.containerPk4Checksum != right.containerPk4Checksum ) { + return left.containerPk4Checksum < right.containerPk4Checksum; + } + if ( left.size != right.size ) { + return left.size < right.size; + } + return left.timestamp < right.timestamp; +} + +bool EntryIdentityLess( const ManifestEntry &left, const ManifestEntry &right ) { + if ( left.type != right.type ) { + return static_cast( left.type ) < static_cast( right.type ); + } + if ( left.normalizedName != right.normalizedName ) { + return left.normalizedName < right.normalizedName; + } + if ( SourceLess( left.source, right.source ) ) { + return true; + } + if ( SourceLess( right.source, left.source ) ) { + return false; + } + if ( left.flags != right.flags ) { + return left.flags < right.flags; + } + return left.options < right.options; +} + +bool EntryIdentityEqual( const ManifestEntry &left, const ManifestEntry &right ) { + return !EntryIdentityLess( left, right ) && !EntryIdentityLess( right, left ); +} + +bool EntryReplayLess( const ManifestEntry &left, const ManifestEntry &right ) { + if ( left.priority != right.priority ) { + return static_cast( left.priority ) > + static_cast( right.priority ); + } + if ( left.firstUseOrder != right.firstUseOrder ) { + return left.firstUseOrder < right.firstUseOrder; + } + if ( EntryIdentityLess( left, right ) ) { + return true; + } + if ( EntryIdentityLess( right, left ) ) { + return false; + } + return left.useCount < right.useCount; +} + +Result ValidateManifestFields( const Manifest &manifest, const DecodeLimits &limits ) { + if ( manifest.producerVersion == 0 ) { + return Failure( Status::INVALID_FIELD, 0, "manifest producer version is zero" ); + } + if ( HashIsZero( manifest.gameMode ) || HashIsZero( manifest.entityFilter ) || + HashIsZero( manifest.contentSignature ) || HashIsZero( manifest.settingsSignature ) ) { + return Failure( Status::INVALID_FIELD, 0, "manifest signature is unset" ); + } + std::string normalized; + Result result = NormalizeVirtualPath( manifest.mapKey, normalized, limits.maxMapKeyBytes ); + if ( !result ) { + return result; + } + if ( normalized != manifest.mapKey ) { + return Failure( Status::PATH_NOT_NORMALIZED, 0, "manifest map key is not canonical" ); + } + if ( manifest.entries.size() > limits.maxManifestEntries || + manifest.entries.size() > std::numeric_limits::max() ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "manifest entry count" ); + } + + std::size_t aggregateBytes = manifest.mapKey.size(); + if ( aggregateBytes > limits.maxAggregateStringBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "manifest aggregate variable-length data" ); + } + for ( const ManifestEntry &entry : manifest.entries ) { + if ( !IsValidEntryType( entry.type ) || !IsValidPriority( entry.priority ) ) { + return Failure( Status::INVALID_ENUM_VALUE, 0, "manifest entry type or priority" ); + } + if ( entry.useCount == 0 ) { + return Failure( Status::INVALID_FIELD, 0, "manifest entry use count is zero" ); + } + result = NormalizeVirtualPath( entry.normalizedName, normalized, limits.maxEntryNameBytes ); + if ( !result ) { + return result; + } + if ( normalized != entry.normalizedName ) { + return Failure( Status::PATH_NOT_NORMALIZED, 0, "manifest entry name is not canonical" ); + } + result = ValidateSource( entry.source, limits ); + if ( !result ) { + return result; + } + if ( entry.options.size() > limits.maxEntryOptionsBytes || + entry.options.size() > std::numeric_limits::max() ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "manifest entry options" ); + } + std::size_t added = 0; + if ( !AddWithoutOverflow( entry.normalizedName.size(), entry.source.normalizedPath.size(), added ) || + !AddWithoutOverflow( added, entry.options.size(), added ) ) { + return Failure( Status::INTEGER_OVERFLOW, 0, "manifest aggregate variable-length data" ); + } + if ( added > limits.maxAggregateStringBytes - + std::min( limits.maxAggregateStringBytes, aggregateBytes ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "manifest aggregate variable-length data" ); + } + aggregateBytes += added; + } + return Success(); +} + +bool WriteManifestEntry( Writer &writer, const ManifestEntry &entry ) { + return writer.WriteU32( static_cast( entry.type ) ) && + writer.WriteU32( static_cast( entry.priority ) ) && + writer.WriteU64( entry.firstUseOrder ) && writer.WriteU32( entry.useCount ) && + writer.WriteU32( entry.flags ) && writer.WriteString( entry.normalizedName ) && + WriteSource( writer, entry.source ) && + writer.WriteU32( static_cast( entry.options.size() ) ) && + writer.WriteBytes( entry.options.data(), entry.options.size() ); +} + +Result ReadManifestEntry( Reader &reader, ManifestEntry &entry, const DecodeLimits &limits, + std::size_t &aggregateBytes ) { + std::uint32_t type = 0; + std::uint32_t priority = 0; + if ( !reader.ReadU32( type, "manifest entry type" ) || + !reader.ReadU32( priority, "manifest entry priority" ) || + !reader.ReadU64( entry.firstUseOrder, "manifest entry first-use order" ) || + !reader.ReadU32( entry.useCount, "manifest entry use count" ) || + !reader.ReadU32( entry.flags, "manifest entry flags" ) ) { + return reader.Error(); + } + entry.type = static_cast( type ); + entry.priority = static_cast( priority ); + if ( !IsValidEntryType( entry.type ) || !IsValidPriority( entry.priority ) ) { + return Failure( Status::INVALID_ENUM_VALUE, reader.Position(), + "unknown manifest entry type or priority" ); + } + if ( entry.useCount == 0 ) { + return Failure( Status::INVALID_FIELD, reader.Position(), "manifest entry use count is zero" ); + } + if ( !reader.ReadString( entry.normalizedName, limits.maxEntryNameBytes, + aggregateBytes, limits.maxAggregateStringBytes, "manifest entry name" ) ) { + return reader.Error(); + } + std::string normalized; + Result result = NormalizeVirtualPath( entry.normalizedName, normalized, limits.maxEntryNameBytes ); + if ( !result ) { + result.offset = reader.Position(); + return result; + } + if ( normalized != entry.normalizedName ) { + return Failure( Status::PATH_NOT_NORMALIZED, reader.Position(), + "manifest entry name is not canonical" ); + } + result = ReadSource( reader, entry.source, limits, aggregateBytes ); + if ( !result ) { + return result; + } + if ( !reader.ReadByteVector( entry.options, limits.maxEntryOptionsBytes, + aggregateBytes, limits.maxAggregateStringBytes, "manifest entry options" ) ) { + return reader.Error(); + } + return Success(); +} + +Result AllocationFailure() { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "allocation failed within configured format limits" ); +} + +} // namespace + +Result NormalizeVirtualPath( const std::string_view input, std::string &output, + const std::size_t maximumBytes ) { + if ( input.empty() ) { + return Failure( Status::INVALID_STRING, 0, "virtual path is empty" ); + } + if ( maximumBytes == 0 ) { + return Failure( Status::INVALID_ARGUMENT, 0, "invalid virtual-path limit" ); + } + const std::size_t effectiveMaximum = std::min( maximumBytes, + std::numeric_limits::max() ); + if ( input.front() == '/' || input.front() == '\\' ) { + return Failure( Status::INVALID_STRING, 0, "virtual path is absolute" ); + } + + try { + std::string normalized; + normalized.reserve( std::min( input.size(), effectiveMaximum ) ); + std::size_t segmentStart = 0; + for ( std::size_t position = 0; position <= input.size(); ++position ) { + const bool atEnd = position == input.size(); + const bool separator = !atEnd && ( input[position] == '/' || input[position] == '\\' ); + if ( !atEnd && !separator ) { + continue; + } + const std::size_t segmentBytes = position - segmentStart; + if ( segmentBytes != 0 ) { + const std::string_view segment = input.substr( segmentStart, segmentBytes ); + if ( segment == ".." ) { + return Failure( Status::INVALID_STRING, segmentStart, "virtual path contains a parent segment" ); + } + if ( segment != "." ) { + const std::size_t separatorBytes = normalized.empty() ? 0 : 1; + const std::size_t available = effectiveMaximum - normalized.size(); + if ( separatorBytes > available || segment.size() > available - separatorBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, segmentStart, "virtual path length" ); + } + if ( separatorBytes != 0 ) { + normalized.push_back( '/' ); + } + for ( std::size_t index = 0; index < segment.size(); ++index ) { + unsigned char value = static_cast( segment[index] ); + if ( value == 0 || value < 0x20u || value == 0x7fu || value == ':' ) { + return Failure( Status::INVALID_STRING, segmentStart + index, + "virtual path contains a forbidden byte" ); + } + if ( value >= 'A' && value <= 'Z' ) { + value = static_cast( value - 'A' + 'a' ); + } + normalized.push_back( static_cast( value ) ); + } + } + } + segmentStart = position + 1; + } + if ( normalized.empty() ) { + return Failure( Status::INVALID_STRING, 0, "virtual path has no name segments" ); + } + output = std::move( normalized ); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Hash ComputeHash( const void *data, const std::size_t bytes ) { + if ( data == nullptr && bytes != 0 ) { + return Hash{}; + } + Sha256 sha256; + sha256.Update( data, bytes ); + return sha256.Finish(); +} + +const char *StatusName( const Status status ) { + switch ( status ) { + case Status::OK: return "ok"; + case Status::INVALID_ARGUMENT: return "invalid-argument"; + case Status::SIZE_LIMIT_EXCEEDED: return "size-limit-exceeded"; + case Status::INTEGER_OVERFLOW: return "integer-overflow"; + case Status::TRUNCATED: return "truncated"; + case Status::INVALID_MAGIC: return "invalid-magic"; + case Status::UNSUPPORTED_VERSION: return "unsupported-version"; + case Status::INVALID_END_MARKER: return "invalid-end-marker"; + case Status::INVALID_ENUM_VALUE: return "invalid-enum-value"; + case Status::INVALID_STRING: return "invalid-string"; + case Status::PATH_NOT_NORMALIZED: return "path-not-normalized"; + case Status::INVALID_FIELD: return "invalid-field"; + case Status::PAYLOAD_HASH_MISMATCH: return "payload-hash-mismatch"; + case Status::INTEGRITY_MISMATCH: return "integrity-mismatch"; + case Status::TRAILING_DATA: return "trailing-data"; + case Status::DUPLICATE_ENTRY: return "duplicate-entry"; + case Status::NON_CANONICAL_ORDER: return "non-canonical-order"; + case Status::KIND_MISMATCH: return "kind-mismatch"; + case Status::PARSER_VERSION_MISMATCH: return "parser-version-mismatch"; + case Status::SOURCE_MISMATCH: return "source-mismatch"; + case Status::CONTENT_SIGNATURE_MISMATCH: return "content-signature-mismatch"; + case Status::SETTINGS_SIGNATURE_MISMATCH: return "settings-signature-mismatch"; + case Status::MAP_KEY_MISMATCH: return "map-key-mismatch"; + case Status::GAME_MODE_MISMATCH: return "game-mode-mismatch"; + case Status::ENTITY_FILTER_MISMATCH: return "entity-filter-mismatch"; + case Status::PRODUCER_VERSION_MISMATCH: return "producer-version-mismatch"; + } + return "unknown-status"; +} + +Result EncodeEnvelope( const CacheEnvelope &envelope, std::vector &encoded, + const DecodeLimits &limits ) { + try { + if ( !IsValidCacheKind( envelope.kind ) || !IsValidCodec( envelope.codec ) ) { + return Failure( Status::INVALID_ENUM_VALUE, 0, "cache kind or compression codec" ); + } + if ( envelope.parserVersion == 0 ) { + return Failure( Status::INVALID_FIELD, 0, "cache parser version is zero" ); + } + Result result = ValidateSource( envelope.source, limits ); + if ( !result ) { + return result; + } + if ( HashIsZero( envelope.contentSignature ) || HashIsZero( envelope.settingsSignature ) ) { + return Failure( Status::INVALID_FIELD, 0, "cache key signature is unset" ); + } + if ( envelope.payload.size() > limits.maxPayloadBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "stored cache payload" ); + } + + std::uint64_t decodedPayloadBytes = envelope.decodedPayloadBytes; + Hash decodedPayloadHash = envelope.decodedPayloadHash; + if ( envelope.codec == CompressionCodec::NONE ) { + decodedPayloadBytes = static_cast( envelope.payload.size() ); + decodedPayloadHash = ComputeHash( envelope.payload.data(), envelope.payload.size() ); + } else { + if ( decodedPayloadBytes > limits.maxDecodedPayloadBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "decoded cache payload" ); + } + if ( HashIsZero( decodedPayloadHash ) ) { + return Failure( Status::INVALID_FIELD, 0, "decoded payload hash is unset" ); + } + } + if ( decodedPayloadBytes > limits.maxDecodedPayloadBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "decoded cache payload" ); + } + + Writer writer( limits.maxEnvelopeBytes ); + if ( !writer.WriteU32( CACHE_MAGIC ) || !writer.WriteU32( CACHE_FORMAT_VERSION ) || + !writer.WriteU32( static_cast( envelope.kind ) ) || + !writer.WriteU32( envelope.parserVersion ) || + !writer.WriteU32( static_cast( envelope.codec ) ) || + !WriteSource( writer, envelope.source ) || + !writer.WriteHash( envelope.contentSignature ) || + !writer.WriteHash( envelope.settingsSignature ) || + !writer.WriteU64( static_cast( envelope.payload.size() ) ) || + !writer.WriteU64( decodedPayloadBytes ) || !writer.WriteHash( decodedPayloadHash ) || + !writer.WriteBytes( envelope.payload.data(), envelope.payload.size() ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, writer.Size(), "encoded cache envelope" ); + } + const Hash integrityHash = ComputeHash( writer.Data().data(), writer.Data().size() ); + if ( !writer.WriteHash( integrityHash ) || !writer.WriteU32( CACHE_END_MAGIC ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, writer.Size(), "cache integrity trailer" ); + } + encoded = writer.Take(); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Result DecodeEnvelope( const void *data, const std::size_t bytes, CacheEnvelope &envelope, + const DecodeLimits &limits ) { + if ( data == nullptr && bytes != 0 ) { + return Failure( Status::INVALID_ARGUMENT, 0, "null cache data" ); + } + if ( bytes > limits.maxEnvelopeBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "encoded cache envelope" ); + } + try { + Reader reader( data, bytes ); + std::uint32_t magic = 0; + std::uint32_t version = 0; + std::uint32_t kind = 0; + std::uint32_t parserVersion = 0; + std::uint32_t codec = 0; + if ( !reader.ReadU32( magic, "cache magic" ) ) { + return reader.Error(); + } + if ( magic != CACHE_MAGIC ) { + return Failure( Status::INVALID_MAGIC, 0, "cache magic" ); + } + if ( !reader.ReadU32( version, "cache format version" ) ) { + return reader.Error(); + } + if ( version != CACHE_FORMAT_VERSION ) { + return Failure( Status::UNSUPPORTED_VERSION, 4, "cache format version" ); + } + if ( !reader.ReadU32( kind, "cache kind" ) || + !reader.ReadU32( parserVersion, "cache parser version" ) || + !reader.ReadU32( codec, "cache compression codec" ) ) { + return reader.Error(); + } + + CacheEnvelope decoded; + decoded.kind = static_cast( kind ); + decoded.parserVersion = parserVersion; + decoded.codec = static_cast( codec ); + if ( !IsValidCacheKind( decoded.kind ) || !IsValidCodec( decoded.codec ) ) { + return Failure( Status::INVALID_ENUM_VALUE, 8, "cache kind or compression codec" ); + } + if ( decoded.parserVersion == 0 ) { + return Failure( Status::INVALID_FIELD, 12, "cache parser version is zero" ); + } + std::size_t aggregateBytes = 0; + Result result = ReadSource( reader, decoded.source, limits, aggregateBytes ); + if ( !result ) { + return result; + } + if ( !reader.ReadHash( decoded.contentSignature, "cache content signature" ) || + !reader.ReadHash( decoded.settingsSignature, "cache settings signature" ) ) { + return reader.Error(); + } + if ( HashIsZero( decoded.contentSignature ) || HashIsZero( decoded.settingsSignature ) ) { + return Failure( Status::INVALID_FIELD, reader.Position(), "cache key signature is unset" ); + } + std::uint64_t storedPayloadBytes = 0; + if ( !reader.ReadU64( storedPayloadBytes, "stored payload length" ) || + !reader.ReadU64( decoded.decodedPayloadBytes, "decoded payload length" ) || + !reader.ReadHash( decoded.decodedPayloadHash, "decoded payload hash" ) ) { + return reader.Error(); + } + if ( storedPayloadBytes > std::numeric_limits::max() ) { + return Failure( Status::INTEGER_OVERFLOW, reader.Position(), "stored payload length" ); + } + if ( storedPayloadBytes > limits.maxPayloadBytes || + decoded.decodedPayloadBytes > limits.maxDecodedPayloadBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, reader.Position(), "cache payload length" ); + } + if ( decoded.codec == CompressionCodec::NONE && + decoded.decodedPayloadBytes != storedPayloadBytes ) { + return Failure( Status::INVALID_FIELD, reader.Position(), + "uncompressed payload lengths disagree" ); + } + if ( HashIsZero( decoded.decodedPayloadHash ) ) { + return Failure( Status::INVALID_FIELD, reader.Position(), "decoded payload hash is unset" ); + } + + const std::size_t payloadBytes = static_cast( storedPayloadBytes ); + const std::size_t payloadOffset = reader.Position(); + if ( reader.Remaining() < TRAILER_BYTES || + payloadBytes > reader.Remaining() - TRAILER_BYTES ) { + return Failure( Status::TRUNCATED, reader.Position(), "cache payload or integrity trailer" ); + } + if ( !reader.Skip( payloadBytes, "cache payload" ) ) { + return reader.Error(); + } + const std::size_t protectedBytes = reader.Position(); + Hash storedIntegrityHash{}; + std::uint32_t endMagic = 0; + if ( !reader.ReadHash( storedIntegrityHash, "cache integrity hash" ) || + !reader.ReadU32( endMagic, "cache end marker" ) ) { + return reader.Error(); + } + if ( endMagic != CACHE_END_MAGIC ) { + return Failure( Status::INVALID_END_MARKER, reader.Position() - 4, "cache end marker" ); + } + if ( reader.Remaining() != 0 ) { + return Failure( Status::TRAILING_DATA, reader.Position(), "bytes after cache end marker" ); + } + const Hash actualIntegrityHash = ComputeHash( data, protectedBytes ); + if ( !HashesEqual( storedIntegrityHash, actualIntegrityHash ) ) { + return Failure( Status::INTEGRITY_MISMATCH, protectedBytes, "cache integrity hash" ); + } + if ( decoded.codec == CompressionCodec::NONE ) { + const Hash actualPayloadHash = ComputeHash( reader.At( payloadOffset ), payloadBytes ); + if ( !HashesEqual( decoded.decodedPayloadHash, actualPayloadHash ) ) { + return Failure( Status::PAYLOAD_HASH_MISMATCH, payloadOffset, "cache payload hash" ); + } + } + decoded.payload.assign( reader.At( payloadOffset ), reader.At( payloadOffset ) + payloadBytes ); + envelope = std::move( decoded ); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Result DecodeEnvelope( const std::vector &encoded, CacheEnvelope &envelope, + const DecodeLimits &limits ) { + return DecodeEnvelope( encoded.data(), encoded.size(), envelope, limits ); +} + +Result ValidateEnvelopeKey( const CacheEnvelope &envelope, const EnvelopeExpectation &expected ) { + if ( envelope.kind != expected.kind ) { + return Failure( Status::KIND_MISMATCH, 0, "cache resource kind" ); + } + if ( envelope.parserVersion != expected.parserVersion ) { + return Failure( Status::PARSER_VERSION_MISMATCH, 0, "cache parser version" ); + } + if ( !( envelope.source == expected.source ) ) { + return Failure( Status::SOURCE_MISMATCH, 0, "cache source identity" ); + } + if ( !HashesEqual( envelope.contentSignature, expected.contentSignature ) ) { + return Failure( Status::CONTENT_SIGNATURE_MISMATCH, 0, "cache content signature" ); + } + if ( !HashesEqual( envelope.settingsSignature, expected.settingsSignature ) ) { + return Failure( Status::SETTINGS_SIGNATURE_MISMATCH, 0, "cache settings signature" ); + } + return Success(); +} + +Result DecodeEnvelopeForKey( const void *data, const std::size_t bytes, + const EnvelopeExpectation &expected, CacheEnvelope &envelope, + const DecodeLimits &limits ) { + CacheEnvelope decoded; + Result result = DecodeEnvelope( data, bytes, decoded, limits ); + if ( !result ) { + return result; + } + result = ValidateEnvelopeKey( decoded, expected ); + if ( !result ) { + return result; + } + envelope = std::move( decoded ); + return Success(); +} + +Result DecodeEnvelopeForKey( const std::vector &encoded, + const EnvelopeExpectation &expected, CacheEnvelope &envelope, + const DecodeLimits &limits ) { + return DecodeEnvelopeForKey( encoded.data(), encoded.size(), expected, envelope, limits ); +} + +Result ValidateDecodedPayload( const CacheEnvelope &envelope, + const void *decodedPayload, const std::size_t decodedBytes ) { + if ( decodedPayload == nullptr && decodedBytes != 0 ) { + return Failure( Status::INVALID_ARGUMENT, 0, "null decoded payload" ); + } + if ( envelope.decodedPayloadBytes != decodedBytes ) { + return Failure( Status::INVALID_FIELD, 0, "decoded payload length" ); + } + const Hash actualHash = ComputeHash( decodedPayload, decodedBytes ); + if ( !HashesEqual( envelope.decodedPayloadHash, actualHash ) ) { + return Failure( Status::PAYLOAD_HASH_MISMATCH, 0, "decoded payload hash" ); + } + return Success(); +} + +Result CanonicalizeManifest( Manifest &manifest, const DecodeLimits &limits ) { + try { + Result result = ValidateManifestFields( manifest, limits ); + if ( !result ) { + return result; + } + Manifest canonical = manifest; + std::sort( canonical.entries.begin(), canonical.entries.end(), EntryIdentityLess ); + std::vector merged; + merged.reserve( canonical.entries.size() ); + for ( ManifestEntry &entry : canonical.entries ) { + if ( !merged.empty() && EntryIdentityEqual( merged.back(), entry ) ) { + ManifestEntry &previous = merged.back(); + if ( entry.useCount > std::numeric_limits::max() - previous.useCount ) { + return Failure( Status::INTEGER_OVERFLOW, 0, "merged manifest use count" ); + } + previous.useCount += entry.useCount; + if ( static_cast( entry.priority ) > + static_cast( previous.priority ) ) { + previous.priority = entry.priority; + } + previous.firstUseOrder = std::min( previous.firstUseOrder, entry.firstUseOrder ); + } else { + merged.push_back( std::move( entry ) ); + } + } + std::sort( merged.begin(), merged.end(), EntryReplayLess ); + canonical.entries = std::move( merged ); + manifest = std::move( canonical ); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Result EncodeManifest( const Manifest &manifest, std::vector &encoded, + const DecodeLimits &limits ) { + try { + Manifest canonical = manifest; + Result result = CanonicalizeManifest( canonical, limits ); + if ( !result ) { + return result; + } + Writer writer( limits.maxManifestBytes ); + if ( !writer.WriteU32( MANIFEST_MAGIC ) || !writer.WriteU32( MANIFEST_FORMAT_VERSION ) || + !writer.WriteU32( canonical.producerVersion ) || + !writer.WriteHash( canonical.gameMode ) || + !writer.WriteHash( canonical.entityFilter ) || + !writer.WriteHash( canonical.contentSignature ) || + !writer.WriteHash( canonical.settingsSignature ) || + !writer.WriteString( canonical.mapKey ) || + !writer.WriteU32( static_cast( canonical.entries.size() ) ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, writer.Size(), "encoded manifest header" ); + } + for ( const ManifestEntry &entry : canonical.entries ) { + if ( !WriteManifestEntry( writer, entry ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, writer.Size(), "encoded manifest entry" ); + } + } + const Hash integrityHash = ComputeHash( writer.Data().data(), writer.Data().size() ); + if ( !writer.WriteHash( integrityHash ) || !writer.WriteU32( MANIFEST_END_MAGIC ) ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, writer.Size(), "manifest integrity trailer" ); + } + encoded = writer.Take(); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Result DecodeManifest( const void *data, const std::size_t bytes, Manifest &manifest, + const DecodeLimits &limits ) { + if ( data == nullptr && bytes != 0 ) { + return Failure( Status::INVALID_ARGUMENT, 0, "null manifest data" ); + } + if ( bytes > limits.maxManifestBytes ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, 0, "encoded manifest" ); + } + try { + Reader reader( data, bytes ); + std::uint32_t magic = 0; + std::uint32_t version = 0; + if ( !reader.ReadU32( magic, "manifest magic" ) ) { + return reader.Error(); + } + if ( magic != MANIFEST_MAGIC ) { + return Failure( Status::INVALID_MAGIC, 0, "manifest magic" ); + } + if ( !reader.ReadU32( version, "manifest format version" ) ) { + return reader.Error(); + } + if ( version != MANIFEST_FORMAT_VERSION ) { + return Failure( Status::UNSUPPORTED_VERSION, 4, "manifest format version" ); + } + + Manifest decoded; + if ( !reader.ReadU32( decoded.producerVersion, "manifest producer version" ) || + !reader.ReadHash( decoded.gameMode, "manifest game mode" ) || + !reader.ReadHash( decoded.entityFilter, "manifest entity filter" ) || + !reader.ReadHash( decoded.contentSignature, "manifest content signature" ) || + !reader.ReadHash( decoded.settingsSignature, "manifest settings signature" ) ) { + return reader.Error(); + } + if ( decoded.producerVersion == 0 ) { + return Failure( Status::INVALID_FIELD, 8, "manifest producer version is zero" ); + } + if ( HashIsZero( decoded.gameMode ) || HashIsZero( decoded.entityFilter ) || + HashIsZero( decoded.contentSignature ) || HashIsZero( decoded.settingsSignature ) ) { + return Failure( Status::INVALID_FIELD, reader.Position(), "manifest signature is unset" ); + } + std::size_t aggregateBytes = 0; + if ( !reader.ReadString( decoded.mapKey, limits.maxMapKeyBytes, + aggregateBytes, limits.maxAggregateStringBytes, "manifest map key" ) ) { + return reader.Error(); + } + std::string normalized; + Result result = NormalizeVirtualPath( decoded.mapKey, normalized, limits.maxMapKeyBytes ); + if ( !result ) { + result.offset = reader.Position(); + return result; + } + if ( normalized != decoded.mapKey ) { + return Failure( Status::PATH_NOT_NORMALIZED, reader.Position(), + "manifest map key is not canonical" ); + } + + std::uint32_t entryCount = 0; + if ( !reader.ReadU32( entryCount, "manifest entry count" ) ) { + return reader.Error(); + } + if ( static_cast( entryCount ) > limits.maxManifestEntries ) { + return Failure( Status::SIZE_LIMIT_EXCEEDED, reader.Position(), "manifest entry count" ); + } + if ( reader.Remaining() < TRAILER_BYTES ) { + return Failure( Status::TRUNCATED, reader.Position(), "manifest entries or integrity trailer" ); + } + const std::size_t availableForEntries = reader.Remaining() - TRAILER_BYTES; + if ( static_cast( entryCount ) > availableForEntries / MIN_MANIFEST_ENTRY_BYTES ) { + return Failure( Status::TRUNCATED, reader.Position(), "manifest entry count exceeds remaining data" ); + } + decoded.entries.reserve( entryCount ); + for ( std::uint32_t index = 0; index < entryCount; ++index ) { + ManifestEntry entry; + result = ReadManifestEntry( reader, entry, limits, aggregateBytes ); + if ( !result ) { + return result; + } + decoded.entries.push_back( std::move( entry ) ); + } + + const std::size_t protectedBytes = reader.Position(); + Hash storedIntegrityHash{}; + std::uint32_t endMagic = 0; + if ( !reader.ReadHash( storedIntegrityHash, "manifest integrity hash" ) || + !reader.ReadU32( endMagic, "manifest end marker" ) ) { + return reader.Error(); + } + if ( endMagic != MANIFEST_END_MAGIC ) { + return Failure( Status::INVALID_END_MARKER, reader.Position() - 4, "manifest end marker" ); + } + if ( reader.Remaining() != 0 ) { + return Failure( Status::TRAILING_DATA, reader.Position(), "bytes after manifest end marker" ); + } + const Hash actualIntegrityHash = ComputeHash( data, protectedBytes ); + if ( !HashesEqual( storedIntegrityHash, actualIntegrityHash ) ) { + return Failure( Status::INTEGRITY_MISMATCH, protectedBytes, "manifest integrity hash" ); + } + + Manifest canonical = decoded; + result = CanonicalizeManifest( canonical, limits ); + if ( !result ) { + return result; + } + if ( canonical.entries.size() != decoded.entries.size() ) { + return Failure( Status::DUPLICATE_ENTRY, protectedBytes, "duplicate manifest entry" ); + } + if ( canonical.entries != decoded.entries ) { + return Failure( Status::NON_CANONICAL_ORDER, protectedBytes, + "manifest entries are not in canonical order" ); + } + manifest = std::move( decoded ); + return Success(); + } catch ( const std::bad_alloc & ) { + return AllocationFailure(); + } catch ( const std::length_error & ) { + return AllocationFailure(); + } +} + +Result DecodeManifest( const std::vector &encoded, Manifest &manifest, + const DecodeLimits &limits ) { + return DecodeManifest( encoded.data(), encoded.size(), manifest, limits ); +} + +Result ValidateManifestKey( const Manifest &manifest, const ManifestExpectation &expected ) { + if ( manifest.producerVersion != expected.producerVersion ) { + return Failure( Status::PRODUCER_VERSION_MISMATCH, 0, "manifest producer version" ); + } + if ( manifest.mapKey != expected.mapKey ) { + return Failure( Status::MAP_KEY_MISMATCH, 0, "manifest map key" ); + } + if ( manifest.gameMode != expected.gameMode ) { + return Failure( Status::GAME_MODE_MISMATCH, 0, "manifest game mode" ); + } + if ( manifest.entityFilter != expected.entityFilter ) { + return Failure( Status::ENTITY_FILTER_MISMATCH, 0, "manifest entity filter" ); + } + if ( !HashesEqual( manifest.contentSignature, expected.contentSignature ) ) { + return Failure( Status::CONTENT_SIGNATURE_MISMATCH, 0, "manifest content signature" ); + } + if ( !HashesEqual( manifest.settingsSignature, expected.settingsSignature ) ) { + return Failure( Status::SETTINGS_SIGNATURE_MISMATCH, 0, "manifest settings signature" ); + } + return Success(); +} + +Result DecodeManifestForKey( const void *data, const std::size_t bytes, + const ManifestExpectation &expected, Manifest &manifest, + const DecodeLimits &limits ) { + Manifest decoded; + Result result = DecodeManifest( data, bytes, decoded, limits ); + if ( !result ) { + return result; + } + result = ValidateManifestKey( decoded, expected ); + if ( !result ) { + return result; + } + manifest = std::move( decoded ); + return Success(); +} + +Result DecodeManifestForKey( const std::vector &encoded, + const ManifestExpectation &expected, Manifest &manifest, + const DecodeLimits &limits ) { + return DecodeManifestForKey( encoded.data(), encoded.size(), expected, manifest, limits ); +} + +} // namespace idLevelLoadCache diff --git a/src/framework/LevelLoadCacheFormat.h b/src/framework/LevelLoadCacheFormat.h new file mode 100644 index 00000000..a83cc78d --- /dev/null +++ b/src/framework/LevelLoadCacheFormat.h @@ -0,0 +1,257 @@ +/* +=========================================================================== + +openQ4 hardened level-load cache and preload-manifest format + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +This is an original openQ4 format and implementation. It deliberately uses +only fixed-width values and C++ standard-library types so cache validation can +be tested independently of engine and platform ABIs. + +=========================================================================== +*/ + +#ifndef __LEVEL_LOAD_CACHE_FORMAT_H__ +#define __LEVEL_LOAD_CACHE_FORMAT_H__ + +#include +#include +#include +#include +#include +#include + +namespace idLevelLoadCache { + +static constexpr std::uint32_t CACHE_FORMAT_VERSION = 1; +static constexpr std::uint32_t MANIFEST_FORMAT_VERSION = 2; +static constexpr std::size_t HASH_BYTES = 32; + +static constexpr std::size_t DEFAULT_MAX_PATH_BYTES = 1024; +static constexpr std::size_t DEFAULT_MAX_MAP_KEY_BYTES = 512; +static constexpr std::size_t DEFAULT_MAX_ENTRY_NAME_BYTES = 1024; +static constexpr std::size_t DEFAULT_MAX_ENTRY_OPTIONS_BYTES = 4096; +static constexpr std::size_t DEFAULT_MAX_PAYLOAD_BYTES = 512u * 1024u * 1024u; +static constexpr std::size_t DEFAULT_MAX_DECODED_PAYLOAD_BYTES = 1024u * 1024u * 1024u; +static constexpr std::size_t DEFAULT_MAX_MANIFEST_ENTRIES = 65536; +static constexpr std::size_t DEFAULT_MAX_AGGREGATE_STRING_BYTES = 16u * 1024u * 1024u; +static constexpr std::size_t DEFAULT_MAX_ENVELOPE_BYTES = DEFAULT_MAX_PAYLOAD_BYTES + 16384u; +static constexpr std::size_t DEFAULT_MAX_MANIFEST_BYTES = 64u * 1024u * 1024u; + +using Hash = std::array; + +enum class Status { + OK = 0, + INVALID_ARGUMENT, + SIZE_LIMIT_EXCEEDED, + INTEGER_OVERFLOW, + TRUNCATED, + INVALID_MAGIC, + UNSUPPORTED_VERSION, + INVALID_END_MARKER, + INVALID_ENUM_VALUE, + INVALID_STRING, + PATH_NOT_NORMALIZED, + INVALID_FIELD, + PAYLOAD_HASH_MISMATCH, + INTEGRITY_MISMATCH, + TRAILING_DATA, + DUPLICATE_ENTRY, + NON_CANONICAL_ORDER, + KIND_MISMATCH, + PARSER_VERSION_MISMATCH, + SOURCE_MISMATCH, + CONTENT_SIGNATURE_MISMATCH, + SETTINGS_SIGNATURE_MISMATCH, + MAP_KEY_MISMATCH, + GAME_MODE_MISMATCH, + ENTITY_FILTER_MISMATCH, + PRODUCER_VERSION_MISMATCH +}; + +struct Result { + Status status = Status::OK; + std::size_t offset = 0; + std::string diagnostic; + + bool Ok() const { + return status == Status::OK; + } + + explicit operator bool() const { + return Ok(); + } +}; + +struct DecodeLimits { + std::size_t maxPathBytes = DEFAULT_MAX_PATH_BYTES; + std::size_t maxMapKeyBytes = DEFAULT_MAX_MAP_KEY_BYTES; + std::size_t maxEntryNameBytes = DEFAULT_MAX_ENTRY_NAME_BYTES; + std::size_t maxEntryOptionsBytes = DEFAULT_MAX_ENTRY_OPTIONS_BYTES; + std::size_t maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES; + std::size_t maxDecodedPayloadBytes = DEFAULT_MAX_DECODED_PAYLOAD_BYTES; + std::size_t maxManifestEntries = DEFAULT_MAX_MANIFEST_ENTRIES; + std::size_t maxAggregateStringBytes = DEFAULT_MAX_AGGREGATE_STRING_BYTES; + std::size_t maxEnvelopeBytes = DEFAULT_MAX_ENVELOPE_BYTES; + std::size_t maxManifestBytes = DEFAULT_MAX_MANIFEST_BYTES; +}; + +enum class SourceContainerKind : std::uint32_t { + LOOSE_FILE = 0, + PK4_ARCHIVE = 1 +}; + +struct SourceIdentity { + std::string normalizedPath; + std::uint64_t size = 0; + std::uint64_t timestamp = 0; + SourceContainerKind containerKind = SourceContainerKind::LOOSE_FILE; + std::uint32_t containerPk4Checksum = 0; + + bool operator==( const SourceIdentity &other ) const = default; +}; + +enum class CacheKind : std::uint32_t { + RENDER_MODEL = 1, + RENDER_WORLD = 2, + COLLISION_MODEL = 3 +}; + +enum class CompressionCodec : std::uint32_t { + NONE = 0, + DEFLATE = 1 +}; + +struct CacheEnvelope { + CacheKind kind = CacheKind::RENDER_MODEL; + std::uint32_t parserVersion = 0; + CompressionCodec codec = CompressionCodec::NONE; + SourceIdentity source; + Hash contentSignature{}; + Hash settingsSignature{}; + std::uint64_t decodedPayloadBytes = 0; + Hash decodedPayloadHash{}; + std::vector payload; + + bool operator==( const CacheEnvelope &other ) const = default; +}; + +struct EnvelopeExpectation { + CacheKind kind = CacheKind::RENDER_MODEL; + std::uint32_t parserVersion = 0; + SourceIdentity source; + Hash contentSignature{}; + Hash settingsSignature{}; +}; + +enum class ManifestEntryType : std::uint32_t { + RENDER_MODEL = 1, + IMAGE = 2, + ANIMATION = 3, + SOUND_SAMPLE = 4, + COLLISION_MODEL = 5, + RENDER_WORLD = 6, + MATERIAL = 7, + GUI = 8, + EFFECT = 9, + SKIN = 10, + DECL = 11, + RAW_FILE = 12 +}; + +enum class ManifestPriority : std::uint32_t { + LOW = 0, + NORMAL = 1, + HIGH = 2, + CRITICAL = 3 +}; + +struct ManifestEntry { + ManifestEntryType type = ManifestEntryType::RAW_FILE; + ManifestPriority priority = ManifestPriority::NORMAL; + std::uint64_t firstUseOrder = 0; + std::uint32_t useCount = 1; + std::uint32_t flags = 0; + std::string normalizedName; + SourceIdentity source; + std::vector options; + + bool operator==( const ManifestEntry &other ) const = default; +}; + +struct Manifest { + std::uint32_t producerVersion = 0; + std::string mapKey; + // Exact SHA-256 tokens are stored on disk. Truncated digests would allow + // distinct mod-controlled filters or runtime roles to share a manifest. + Hash gameMode{}; + Hash entityFilter{}; + Hash contentSignature{}; + Hash settingsSignature{}; + std::vector entries; + + bool operator==( const Manifest &other ) const = default; +}; + +struct ManifestExpectation { + std::uint32_t producerVersion = 0; + std::string mapKey; + Hash gameMode{}; + Hash entityFilter{}; + Hash contentSignature{}; + Hash settingsSignature{}; +}; + +// Virtual paths are lowercase, slash-separated, relative paths without empty, +// dot, or parent segments. The result is unchanged when the input is already +// canonical. Normalization never consults the host filesystem. +Result NormalizeVirtualPath( std::string_view input, std::string &output, + std::size_t maximumBytes = DEFAULT_MAX_PATH_BYTES ); + +Hash ComputeHash( const void *data, std::size_t bytes ); +const char *StatusName( Status status ); + +Result EncodeEnvelope( const CacheEnvelope &envelope, std::vector &encoded, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeEnvelope( const void *data, std::size_t bytes, CacheEnvelope &envelope, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeEnvelope( const std::vector &encoded, CacheEnvelope &envelope, + const DecodeLimits &limits = DecodeLimits() ); +Result ValidateEnvelopeKey( const CacheEnvelope &envelope, const EnvelopeExpectation &expected ); +Result DecodeEnvelopeForKey( const void *data, std::size_t bytes, + const EnvelopeExpectation &expected, CacheEnvelope &envelope, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeEnvelopeForKey( const std::vector &encoded, + const EnvelopeExpectation &expected, CacheEnvelope &envelope, + const DecodeLimits &limits = DecodeLimits() ); +Result ValidateDecodedPayload( const CacheEnvelope &envelope, + const void *decodedPayload, std::size_t decodedBytes ); + +// Canonicalization sorts entries by replay priority and first-use order, then +// by stable entry identity. Exact duplicate observations are merged by taking +// their earliest order, highest priority, and checked sum of use counts. +Result CanonicalizeManifest( Manifest &manifest, + const DecodeLimits &limits = DecodeLimits() ); +Result EncodeManifest( const Manifest &manifest, std::vector &encoded, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeManifest( const void *data, std::size_t bytes, Manifest &manifest, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeManifest( const std::vector &encoded, Manifest &manifest, + const DecodeLimits &limits = DecodeLimits() ); +Result ValidateManifestKey( const Manifest &manifest, const ManifestExpectation &expected ); +Result DecodeManifestForKey( const void *data, std::size_t bytes, + const ManifestExpectation &expected, Manifest &manifest, + const DecodeLimits &limits = DecodeLimits() ); +Result DecodeManifestForKey( const std::vector &encoded, + const ManifestExpectation &expected, Manifest &manifest, + const DecodeLimits &limits = DecodeLimits() ); + +} // namespace idLevelLoadCache + +#endif /* !__LEVEL_LOAD_CACHE_FORMAT_H__ */ diff --git a/src/framework/LevelLoadCacheManager.cpp b/src/framework/LevelLoadCacheManager.cpp new file mode 100644 index 00000000..c86e5e46 --- /dev/null +++ b/src/framework/LevelLoadCacheManager.cpp @@ -0,0 +1,1077 @@ +/* +=========================================================================== + +openQ4 learned level-load manifest and generated-cache coordinator + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +This is an original openQ4 implementation. Cache and manifest files are +private derived data: the selected VFS source is opened and identified before +any cached payload can be accepted. + +=========================================================================== +*/ + +#include "../idlib/precompiled.h" +#include "LevelLoadCacheManager.h" +#include "LevelLoadCacheFormat.h" +#include "LevelLoadPipeline.h" +#include "File.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined( min ) + #undef min +#endif +#if defined( max ) + #undef max +#endif + +namespace { + +static idCVar com_levelLoadModernization( + "com_levelLoadModernization", "0", CVAR_BOOL | CVAR_ARCHIVE | CVAR_SYSTEM, + "enable the experimental level-load cache, preload, and generated-animation paths" ); +static idCVar com_levelLoadCache( + "com_levelLoadCache", "1", CVAR_BOOL | CVAR_ARCHIVE | CVAR_SYSTEM, + "enable versioned source-authoritative level-load caches and learned manifests" ); +static idCVar com_levelLoadCacheWrite( + "com_levelLoadCacheWrite", "1", CVAR_BOOL | CVAR_ARCHIVE | CVAR_SYSTEM, + "atomically write learned manifests and generated model/world/collision caches" ); +static idCVar com_levelLoadPreload( + "com_levelLoadPreload", "1", CVAR_BOOL | CVAR_ARCHIVE | CVAR_SYSTEM, + "replay a matching learned manifest through the bounded level-load job pipeline" ); +static idCVar com_levelLoadCacheReport( + "com_levelLoadCacheReport", "0", CVAR_BOOL | CVAR_ARCHIVE | CVAR_SYSTEM, + "print per-map manifest, cancellation, byte, and generated-cache diagnostics" ); +static idCVar com_levelLoadPreloadMaxEntries( + "com_levelLoadPreloadMaxEntries", "64", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "maximum independently opened learned sources admitted per map generation", 1, 64, + idCmdSystem::ArgCompletion_Integer<1,64> ); +static idCVar com_levelLoadPreloadMaxFileMB( + "com_levelLoadPreloadMaxFileMB", "128", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "maximum size of one learned preload source in MiB", 1, 512, + idCmdSystem::ArgCompletion_Integer<1,512> ); +static idCVar com_levelLoadPreloadMaxStagingMB( + "com_levelLoadPreloadMaxStagingMB", "384", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "maximum aggregate learned preload staging memory in MiB", 8, 2048, + idCmdSystem::ArgCompletion_Integer<8,2048> ); +static idCVar com_levelLoadPreloadMaxDecodeMB( + "com_levelLoadPreloadMaxDecodeMB", "384", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "maximum aggregate decoded preload result bytes in MiB", 8, 2048, + idCmdSystem::ArgCompletion_Integer<8,2048> ); +static idCVar com_levelLoadPreloadReadChunkKB( + "com_levelLoadPreloadReadChunkKB", "256", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "cooperative cancellation/read accounting chunk size in KiB", 16, 4096, + idCmdSystem::ArgCompletion_Integer<16,4096> ); +static idCVar com_levelLoadPreloadDecodeChunkKB( + "com_levelLoadPreloadDecodeChunkKB", "256", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, + "cooperative cancellation/decode integrity chunk size in KiB", 16, 4096, + idCmdSystem::ArgCompletion_Integer<16,4096> ); + +static constexpr std::uint32_t MANIFEST_PRODUCER_VERSION = 2; +static constexpr std::size_t MAX_GENERATED_FILE_BYTES = + idLevelLoadCache::DEFAULT_MAX_ENVELOPE_BYTES; + +std::atomic stagedFileSerial( 1 ); + +std::string HashHex( const idLevelLoadCache::Hash &hash ) { + std::ostringstream stream; + stream << std::hex << std::setfill( '0' ); + for ( const std::uint8_t value : hash ) { + stream << std::setw( 2 ) << static_cast( value ); + } + return stream.str(); +} + +idLevelLoadCache::Hash HashString( const char *text ) { + const char *safe = text != nullptr ? text : ""; + return idLevelLoadCache::ComputeHash( safe, std::strlen( safe ) ); +} + +bool NormalizePath( const char *path, std::string &normalized ) { + if ( path == nullptr || path[ 0 ] == '\0' ) { + return false; + } + return idLevelLoadCache::NormalizeVirtualPath( path, normalized ).Ok(); +} + +idLevelLoadCache::CacheKind ToFormatKind( const generatedCacheKind_t kind ) { + switch ( kind ) { + case GENERATED_CACHE_RENDER_WORLD: + return idLevelLoadCache::CacheKind::RENDER_WORLD; + case GENERATED_CACHE_COLLISION_MODEL: + return idLevelLoadCache::CacheKind::COLLISION_MODEL; + case GENERATED_CACHE_RENDER_MODEL: + default: + return idLevelLoadCache::CacheKind::RENDER_MODEL; + } +} + +bool IsValidCacheKind( const generatedCacheKind_t kind ) { + return kind == GENERATED_CACHE_RENDER_MODEL || + kind == GENERATED_CACHE_RENDER_WORLD || + kind == GENERATED_CACHE_COLLISION_MODEL; +} + +idLevelLoadCache::ManifestEntryType ToFormatType( const levelLoadResourceType_t type ) { + const unsigned int value = static_cast( type ); + if ( value < static_cast( LEVEL_LOAD_RESOURCE_RENDER_MODEL ) || + value > static_cast( LEVEL_LOAD_RESOURCE_RAW ) ) { + return idLevelLoadCache::ManifestEntryType::RAW_FILE; + } + return static_cast( value ); +} + +levelLoadResourceType_t ClassifyPath( const std::string &path ) { + const std::size_t dot = path.find_last_of( '.' ); + const std::string extension = dot == std::string::npos ? std::string() : path.substr( dot ); + if ( extension == ".proc" || extension == ".procc" || + extension == ".md5rproc" || extension == ".md5rprocc" ) { + return LEVEL_LOAD_RESOURCE_WORLD; + } + if ( extension == ".cm" || extension == ".cmc" ) { + return LEVEL_LOAD_RESOURCE_COLLISION; + } + if ( extension == ".md5anim" || extension == ".md5animc" ) { + return LEVEL_LOAD_RESOURCE_ANIMATION; + } + if ( extension == ".ase" || extension == ".lwo" || extension == ".ma" || + extension == ".flt" || extension == ".md5mesh" || extension == ".md5r" || + extension == ".md5rc" || extension == ".md5rmesh" || extension == ".md5rmeshc" ) { + return LEVEL_LOAD_RESOURCE_RENDER_MODEL; + } + if ( extension == ".tga" || extension == ".dds" || extension == ".jpg" || + extension == ".jpeg" || extension == ".png" || extension == ".bmp" ) { + return LEVEL_LOAD_RESOURCE_IMAGE; + } + if ( extension == ".wav" || extension == ".ogg" ) { + return LEVEL_LOAD_RESOURCE_SOUND; + } + if ( extension == ".gui" ) { + return LEVEL_LOAD_RESOURCE_GUI; + } + if ( extension == ".skin" ) { + return LEVEL_LOAD_RESOURCE_SKIN; + } + if ( extension == ".mtr" || extension == ".def" || extension == ".snd" || + extension == ".fx" || extension == ".af" || extension == ".pda" ) { + return LEVEL_LOAD_RESOURCE_DECL; + } + return LEVEL_LOAD_RESOURCE_RAW; +} + +unsigned int DefaultPriority( const levelLoadResourceType_t type ) { + switch ( type ) { + case LEVEL_LOAD_RESOURCE_WORLD: + case LEVEL_LOAD_RESOURCE_COLLISION: + return static_cast( idLevelLoadCache::ManifestPriority::CRITICAL ); + case LEVEL_LOAD_RESOURCE_RENDER_MODEL: + case LEVEL_LOAD_RESOURCE_ANIMATION: + return static_cast( idLevelLoadCache::ManifestPriority::HIGH ); + default: + return static_cast( idLevelLoadCache::ManifestPriority::NORMAL ); + } +} + +bool SemanticPathMatchesSource( const levelLoadResourceType_t type, + const std::string &semanticName, const std::string &sourcePath ) { + if ( semanticName == sourcePath ) { + return true; + } + if ( type == LEVEL_LOAD_RESOURCE_ANIMATION || + type == LEVEL_LOAD_RESOURCE_RENDER_MODEL || type == LEVEL_LOAD_RESOURCE_WORLD ) { + std::string compiledName = semanticName; + compiledName += Lexer::sCompiledFileSuffix.c_str(); + if ( compiledName == sourcePath ) { + return true; + } + } + if ( type != LEVEL_LOAD_RESOURCE_IMAGE && type != LEVEL_LOAD_RESOURCE_SOUND ) { + return false; + } + const std::size_t semanticDot = semanticName.find_last_of( '.' ); + const std::size_t sourceDot = sourcePath.find_last_of( '.' ); + const std::string semanticStem = semanticDot == std::string::npos + ? semanticName : semanticName.substr( 0, semanticDot ); + const std::string sourceStem = sourceDot == std::string::npos + ? sourcePath : sourcePath.substr( 0, sourceDot ); + return semanticStem == sourceStem; +} + +bool BuildSourceIdentity( const std::string &normalizedPath, idFile *file, + idLevelLoadCache::SourceIdentity &identity ) { + if ( file == nullptr || file->Length() < 0 ) { + return false; + } + identity.normalizedPath = normalizedPath; + identity.size = static_cast( file->Length() ); + const int checksum = file->GetContainerChecksum(); + if ( checksum != 0 ) { + identity.containerKind = idLevelLoadCache::SourceContainerKind::PK4_ARCHIVE; + identity.containerPk4Checksum = static_cast( checksum ); + identity.timestamp = 0; + } else { + identity.containerKind = idLevelLoadCache::SourceContainerKind::LOOSE_FILE; + identity.containerPk4Checksum = 0; + identity.timestamp = static_cast( file->Timestamp() ); + } + return true; +} + +int ReadPipelineSource( idFile *file, void *buffer, const int byteCount, void * ) { + // For PK4 sources the VFS idFile::Read implementation performs archive + // inflate before returning these member bytes. This is therefore the fused + // read/decompress stage; DecodePipelineSource below performs worker-safe + // framing and integrity decode without invoking renderer/game parsers. + return file != nullptr ? file->Read( buffer, byteCount ) : 0; +} + +std::uint64_t UpdatePipelineTransportChecksum( std::uint64_t checksum, + const unsigned char *bytes, const std::size_t byteCount ) { + static constexpr std::uint64_t FNV1A64_PRIME = 1099511628211ull; + for ( std::size_t index = 0; index < byteCount; ++index ) { + checksum ^= bytes[index]; + checksum *= FNV1A64_PRIME; + } + return checksum; +} + +idLevelLoadDecodeStatus DecodePipelineSource( + const idLevelLoadPipelineSource &source, const unsigned char *bytes, + const std::size_t byteCount, const idLevelLoadDecodeContext &context, + idLevelLoadDecodeOutput &output, void * ) { + if ( bytes == nullptr || byteCount == 0 || source.type == 0 || + context.generation == 0 || context.chunkBytes == 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + const idLevelLoadDecodeStatus frameStatus = idLevelLoadDecodeSourceFrame( + source, bytes, byteCount, output, &context ); + if ( frameStatus != idLevelLoadDecodeStatus::COMPLETE ) { + return frameStatus; + } + + static constexpr unsigned char HASH_DOMAIN[] = { + 'o', 'p', 'e', 'n', 'Q', '4', '-', 'l', 'e', 'v', 'e', 'l', '-', + 'l', 'o', 'a', 'd', '-', 'd', 'e', 'c', 'o', 'd', 'e', '-', 'v', '1' + }; + idLevelLoadCache::Hash chain = idLevelLoadCache::ComputeHash( + HASH_DOMAIN, sizeof( HASH_DOMAIN ) ); + std::uint64_t transportChecksum = 14695981039346656037ull; + std::size_t offset = 0; + while ( offset < byteCount ) { + if ( context.IsCancellationRequested() ) { + return idLevelLoadDecodeStatus::CANCELLED; + } + const std::size_t chunkBytes = std::min( context.chunkBytes, byteCount - offset ); + const idLevelLoadCache::Hash chunkHash = idLevelLoadCache::ComputeHash( + bytes + offset, chunkBytes ); + std::array chained; + std::copy( chain.begin(), chain.end(), chained.begin() ); + std::copy( chunkHash.begin(), chunkHash.end(), + chained.begin() + idLevelLoadCache::HASH_BYTES ); + chain = idLevelLoadCache::ComputeHash( chained.data(), chained.size() ); + transportChecksum = UpdatePipelineTransportChecksum( + transportChecksum, bytes + offset, chunkBytes ); + if ( !context.ReportDecodedBytes( chunkBytes ) ) { + return context.IsCancellationRequested() + ? idLevelLoadDecodeStatus::CANCELLED + : idLevelLoadDecodeStatus::MALFORMED; + } + offset += chunkBytes; + } + + std::array finalRecord; + std::copy( chain.begin(), chain.end(), finalRecord.begin() ); + std::size_t cursor = idLevelLoadCache::HASH_BYTES; + const std::uint64_t decodedByteCount = static_cast( byteCount ); + for ( std::size_t index = 0; index < 8; ++index ) { + finalRecord[cursor++] = static_cast( + decodedByteCount >> ( index * 8 ) ); + } + for ( std::size_t index = 0; index < 8; ++index ) { + finalRecord[cursor++] = static_cast( context.generation >> ( index * 8 ) ); + } + const std::uint32_t type = source.type; + for ( std::size_t index = 0; index < 4; ++index ) { + finalRecord[cursor++] = static_cast( type >> ( index * 8 ) ); + } + output.transportChecksum = transportChecksum; + output.contentIntegrity = idLevelLoadCache::ComputeHash( + finalRecord.data(), finalRecord.size() ); + return idLevelLoadDecodeStatus::COMPLETE; +} + +// Immutable view of bytes owned by a completed pipeline item. The ordinary +// VFS lookup has already run before this object is created, so it retains the +// authoritative file's diagnostics and identity without becoming a second +// source-resolution path. +class idFile_LevelLoadPreloaded final : public idFile { +public: + idFile_LevelLoadPreloaded( const std::shared_ptr > &sourceBytes, + idFile *authoritativeSource ) + : bytes( sourceBytes ) + , position( 0 ) + , timestamp( authoritativeSource != nullptr ? authoritativeSource->Timestamp() : 0 ) + , containerChecksum( authoritativeSource != nullptr + ? authoritativeSource->GetContainerChecksum() : 0 ) { + if ( authoritativeSource != nullptr ) { + const char *sourceName = authoritativeSource->GetName(); + const char *sourceFullPath = authoritativeSource->GetFullPath(); + name = sourceName != nullptr ? sourceName : ""; + fullPath = sourceFullPath != nullptr ? sourceFullPath : name.c_str(); + } + } + + const char *GetName() override { return name.c_str(); } + const char *GetFullPath() override { return fullPath.c_str(); } + int Read( void *buffer, int length ) override { + if ( buffer == nullptr || length <= 0 || bytes == nullptr || position >= bytes->size() ) { + return 0; + } + const std::size_t requested = static_cast( length ); + const std::size_t available = bytes->size() - position; + const std::size_t copied = std::min( requested, available ); + memcpy( buffer, bytes->data() + position, copied ); + position += copied; + return static_cast( copied ); + } + int Write( const void *, int ) override { return 0; } + int Length() override { + return bytes != nullptr ? static_cast( bytes->size() ) : 0; + } + ID_TIME_T Timestamp() override { return timestamp; } + int GetContainerChecksum() const override { return containerChecksum; } + int Tell() override { return static_cast( position ); } + int Seek( long offset, fsOrigin_t origin ) override { + const std::int64_t length = bytes != nullptr + ? static_cast( bytes->size() ) : 0; + std::int64_t requested = 0; + switch ( origin ) { + case FS_SEEK_CUR: + requested = static_cast( position ) + offset; + break; + case FS_SEEK_END: + requested = length - offset; + break; + case FS_SEEK_SET: + requested = offset; + break; + default: + return -1; + } + if ( requested < 0 ) { + position = 0; + return -1; + } + if ( requested > length ) { + position = static_cast( length ); + return -1; + } + position = static_cast( requested ); + return 0; + } + const char *GetDataPtr() const override { + return bytes != nullptr && !bytes->empty() + ? reinterpret_cast( bytes->data() ) : nullptr; + } + +private: + std::shared_ptr > bytes; + std::size_t position; + idStr name; + idStr fullPath; + ID_TIME_T timestamp; + int containerChecksum; +}; + +bool ReadWholeFile( idFileSystem *fileSystem, const char *qpath, + std::vector &bytes, const std::size_t maximumBytes, + bool *invalidFile = nullptr ) { + bytes.clear(); + if ( invalidFile != nullptr ) { + *invalidFile = false; + } + if ( fileSystem == nullptr || qpath == nullptr ) { + return false; + } + idStr osPath = fileSystem->RelativePathToOSPath( qpath, "fs_savepath" ); + idFile *file = fileSystem->OpenExplicitFileRead( osPath.c_str() ); + if ( file == nullptr ) { + return false; + } + const int length = file->Length(); + if ( length <= 0 || static_cast( length ) > maximumBytes ) { + fileSystem->CloseFile( file ); + if ( invalidFile != nullptr ) { + *invalidFile = true; + } + return false; + } + try { + bytes.resize( static_cast( length ) ); + } catch ( ... ) { + fileSystem->CloseFile( file ); + return false; + } + const int read = file->Read( bytes.data(), length ); + fileSystem->CloseFile( file ); + if ( read != length ) { + bytes.clear(); + if ( invalidFile != nullptr ) { + *invalidFile = true; + } + return false; + } + return true; +} + +bool WriteAtomic( idFileSystem *fileSystem, const std::string &finalPath, + const std::vector &bytes ) { + if ( fileSystem == nullptr || bytes.empty() || + bytes.size() > static_cast( std::numeric_limits::max() ) ) { + return false; + } + const std::uint64_t serial = stagedFileSerial.fetch_add( 1, std::memory_order_relaxed ); + std::ostringstream stage; + stage << finalPath << '.' << serial << ".tmp"; + const std::string stagedPath = stage.str(); + idFile *file = fileSystem->OpenFileWrite( stagedPath.c_str(), "fs_savepath" ); + if ( file == nullptr ) { + return false; + } + const int requested = static_cast( bytes.size() ); + const bool complete = file->Write( bytes.data(), requested ) == requested; + const bool synced = complete && file->Sync(); + fileSystem->CloseFile( file ); + if ( !synced || !fileSystem->PromoteFile( stagedPath.c_str(), finalPath.c_str(), "fs_savepath" ) ) { + fileSystem->RemoveFileChecked( stagedPath.c_str(), "fs_savepath" ); + return false; + } + return true; +} + +std::string ManifestPath( const idLevelLoadCache::ManifestExpectation &expected ) { + std::ostringstream key; + key << expected.producerVersion << '|' << expected.mapKey << '|' + << HashHex( expected.gameMode ) << '|' << HashHex( expected.entityFilter ) << '|' + << HashHex( expected.contentSignature ) << '|' << HashHex( expected.settingsSignature ); + return std::string( "generated/manifests/" ) + HashHex( HashString( key.str().c_str() ) ) + ".oqpm"; +} + +std::string CachePath( const idLevelLoadCache::EnvelopeExpectation &expected ) { + const char *subdirectory = "models"; + if ( expected.kind == idLevelLoadCache::CacheKind::RENDER_WORLD ) { + subdirectory = "worlds"; + } else if ( expected.kind == idLevelLoadCache::CacheKind::COLLISION_MODEL ) { + subdirectory = "collision"; + } + std::ostringstream key; + key << static_cast( expected.kind ) << '|' << expected.parserVersion << '|' + << expected.source.normalizedPath << '|' << expected.source.size << '|' + << expected.source.timestamp << '|' << static_cast( expected.source.containerKind ) << '|' + << expected.source.containerPk4Checksum << '|' << HashHex( expected.contentSignature ) << '|' + << HashHex( expected.settingsSignature ); + return std::string( "generated/" ) + subdirectory + '/' + + HashHex( HashString( key.str().c_str() ) ) + ".oqgc"; +} + +} // namespace + +struct idLevelLoadCacheManager::Impl { + struct SemanticHint { + levelLoadResourceType_t type; + std::string normalizedName; + std::vector options; + unsigned int flags; + unsigned int priority; + }; + + explicit Impl( idFileSystem *owner ) + : fileSystem( owner ) + , generation( 0 ) + , firstUseOrder( 0 ) + , recording( false ) + , completedGeneration( false ) + , manifestMatched( false ) + , manifestRemoved( false ) + , generatedHits( 0 ) + , generatedMisses( 0 ) + , generatedWrites( 0 ) + , generatedCorruptions( 0 ) { + } + + idFileSystem *fileSystem; + idLevelLoadPipeline pipeline; + std::uint64_t generation; + std::uint64_t firstUseOrder; + bool recording; + bool completedGeneration; + bool manifestMatched; + bool manifestRemoved; + std::atomic generatedHits; + std::atomic generatedMisses; + std::atomic generatedWrites; + std::atomic generatedCorruptions; + idLevelLoadCache::Manifest learned; + idLevelLoadCache::ManifestExpectation expectation; + std::vector hints; + std::mutex mutex; + + void ClosePipelineFiles() { + std::vector files; + pipeline.DrainOpenFiles( files ); + for ( idFile *file : files ) { + if ( file != nullptr ) { + fileSystem->CloseFile( file ); + } + } + } + + void StopPipeline( const bool cancel ) { + if ( cancel ) { + pipeline.CancelAndWait(); + } else { + pipeline.Wait(); + } + ClosePipelineFiles(); + } + + bool WriteLearnedManifest() { + if ( !com_levelLoadModernization.GetBool() || !com_levelLoadCache.GetBool() || + !com_levelLoadCacheWrite.GetBool() || + learned.mapKey.empty() ) { + return false; + } + idLevelLoadCache::Manifest snapshot; + { + std::lock_guard lock( mutex ); + snapshot = learned; + } + std::vector encoded; + const idLevelLoadCache::Result result = idLevelLoadCache::EncodeManifest( snapshot, encoded ); + if ( !result ) { + common->Warning( "Could not encode level-load manifest: %s (%s)", + idLevelLoadCache::StatusName( result.status ), result.diagnostic.c_str() ); + return false; + } + return WriteAtomic( fileSystem, ManifestPath( expectation ), encoded ); + } + + idLevelLoadCache::EnvelopeExpectation BuildExpectation( + const generatedCacheKind_t kind, const std::string &normalizedPath, + const unsigned int parserVersion, const char *settingsKey, + const char *contentKey, idFile *source ) const { + idLevelLoadCache::EnvelopeExpectation result; + result.kind = ToFormatKind( kind ); + result.parserVersion = parserVersion; + BuildSourceIdentity( normalizedPath, source, result.source ); + result.contentSignature = HashString( contentKey ); + result.settingsSignature = HashString( settingsKey ); + return result; + } +}; + +idLevelLoadCacheManager::idLevelLoadCacheManager( idFileSystem *fileSystem ) + : impl( new Impl( fileSystem ) ) { +} + +idLevelLoadCacheManager::~idLevelLoadCacheManager() { + Cancel(); +} + +void idLevelLoadCacheManager::Begin( const char *mapKey, const char *gameMode, + const char *entityFilter, const char *contentKey, const char *settingsKey ) { + Cancel(); + if ( impl->fileSystem == nullptr || !com_levelLoadModernization.GetBool() || + !com_levelLoadCache.GetBool() ) { + return; + } + + std::string normalizedMap; + if ( !NormalizePath( mapKey, normalizedMap ) ) { + common->Warning( "Level-load cache rejected invalid map key '%s'", mapKey != nullptr ? mapKey : "" ); + return; + } + + impl->generation++; + impl->firstUseOrder = 0; + impl->completedGeneration = false; + impl->learned = idLevelLoadCache::Manifest(); + impl->learned.producerVersion = MANIFEST_PRODUCER_VERSION; + impl->learned.mapKey = normalizedMap; + impl->learned.gameMode = HashString( gameMode ); + impl->learned.entityFilter = HashString( entityFilter ); + impl->learned.contentSignature = HashString( contentKey ); + impl->learned.settingsSignature = HashString( settingsKey ); + impl->expectation.producerVersion = impl->learned.producerVersion; + impl->expectation.mapKey = impl->learned.mapKey; + impl->expectation.gameMode = impl->learned.gameMode; + impl->expectation.entityFilter = impl->learned.entityFilter; + impl->expectation.contentSignature = impl->learned.contentSignature; + impl->expectation.settingsSignature = impl->learned.settingsSignature; + impl->hints.clear(); + impl->manifestMatched = false; + impl->manifestRemoved = false; + impl->generatedHits.store( 0, std::memory_order_relaxed ); + impl->generatedMisses.store( 0, std::memory_order_relaxed ); + impl->generatedWrites.store( 0, std::memory_order_relaxed ); + impl->generatedCorruptions.store( 0, std::memory_order_relaxed ); + + const std::size_t replayMaxEntries = + static_cast( com_levelLoadPreloadMaxEntries.GetInteger() ); + const std::uint64_t replayMaxSourceBytes = + static_cast( com_levelLoadPreloadMaxFileMB.GetInteger() ) * 1024ull * 1024ull; + const std::uint64_t replayMaxTotalBytes = + static_cast( com_levelLoadPreloadMaxStagingMB.GetInteger() ) * 1024ull * 1024ull; + const std::uint64_t replayMaxDecodedBytes = + static_cast( com_levelLoadPreloadMaxDecodeMB.GetInteger() ) * 1024ull * 1024ull; + std::uint64_t replayAdmittedBytes = 0; + + std::vector sources; + const std::string manifestPath = ManifestPath( impl->expectation ); + if ( com_levelLoadPreload.GetBool() ) { + std::vector encoded; + bool invalidManifestFile = false; + if ( ReadWholeFile( impl->fileSystem, manifestPath.c_str(), encoded, + idLevelLoadCache::DEFAULT_MAX_MANIFEST_BYTES, &invalidManifestFile ) ) { + idLevelLoadCache::Manifest replay; + const idLevelLoadCache::Result decoded = idLevelLoadCache::DecodeManifestForKey( + encoded, impl->expectation, replay ); + if ( !decoded ) { + impl->fileSystem->RemoveFileChecked( manifestPath.c_str(), "fs_savepath" ); + impl->manifestRemoved = true; + if ( com_levelLoadCacheReport.GetBool() ) { + common->Printf( "Level-load manifest ignored and removed (%s at %u): %s\n", + idLevelLoadCache::StatusName( decoded.status ), + static_cast( decoded.offset ), decoded.diagnostic.c_str() ); + } + } else { + impl->manifestMatched = true; + std::set admittedIdentities; + for ( const idLevelLoadCache::ManifestEntry &entry : replay.entries ) { + // Decode canonicalizes entries into replay priority/first-use order. + // Apply all admission budgets before opening a handle so an otherwise + // valid but very large manifest cannot exhaust process resources. + if ( sources.size() >= replayMaxEntries ) { + break; + } + if ( entry.source.size == 0 || entry.source.size > replayMaxSourceBytes || + entry.source.size > replayMaxTotalBytes - replayAdmittedBytes ) { + continue; + } + std::ostringstream identityKey; + identityKey << entry.source.normalizedPath << '|' << entry.source.size << '|' + << entry.source.timestamp << '|' << entry.source.containerPk4Checksum; + if ( !admittedIdentities.insert( identityKey.str() ).second ) { + continue; + } + idFile *file = impl->fileSystem->OpenFileRead( + entry.source.normalizedPath.c_str(), false ); + if ( file == nullptr ) { + continue; + } + idLevelLoadCache::SourceIdentity current; + if ( !BuildSourceIdentity( entry.source.normalizedPath, file, current ) || + current != entry.source ) { + impl->fileSystem->CloseFile( file ); + continue; + } + idLevelLoadPipelineSource source; + source.normalizedPath = entry.source.normalizedPath; + source.type = static_cast( entry.type ); + source.priority = static_cast( entry.priority ); + source.firstUseOrder = entry.firstUseOrder > std::numeric_limits::max() + ? std::numeric_limits::max() + : static_cast( entry.firstUseOrder ); + source.file = file; + source.sourceBytes = entry.source.size; + source.sourceTimestamp = entry.source.timestamp; + source.containerChecksum = entry.source.containerPk4Checksum; + sources.push_back( std::move( source ) ); + replayAdmittedBytes += entry.source.size; + } + } + } else if ( invalidManifestFile ) { + impl->fileSystem->RemoveFileChecked( manifestPath.c_str(), "fs_savepath" ); + impl->manifestRemoved = true; + } + } + + idLevelLoadPipelineConfig config; + config.maxEntries = replayMaxEntries; + config.maxSourceBytes = replayMaxSourceBytes; + config.maxTotalBytes = replayMaxTotalBytes; + config.maxDecodedBytes = replayMaxDecodedBytes; + config.readChunkBytes = static_cast( com_levelLoadPreloadReadChunkKB.GetInteger() ) * 1024u; + config.decodeChunkBytes = static_cast( com_levelLoadPreloadDecodeChunkKB.GetInteger() ) * 1024u; + if ( !impl->pipeline.Begin( impl->generation, config, std::move( sources ), + &ReadPipelineSource, nullptr, &DecodePipelineSource ) ) { + impl->ClosePipelineFiles(); + common->Warning( "Could not start level-load preload generation %llu", + static_cast( impl->generation ) ); + } + impl->recording = true; +} + +void idLevelLoadCacheManager::Finish( const bool successful ) { + if ( impl == nullptr ) { + return; + } + { + std::lock_guard lock( impl->mutex ); + impl->recording = false; + } + impl->StopPipeline( !successful ); + const idLevelLoadPipelineMetrics metrics = impl->pipeline.GetMetrics(); + + const bool wroteManifest = successful && impl->WriteLearnedManifest(); + + if ( com_levelLoadCacheReport.GetBool() && !impl->learned.mapKey.empty() ) { + common->Printf( + "Level-load cache: map=%s learned=%u manifestMatch=%d manifestRemoved=%d replayed=%llu rejected=%llu failed=%llu cancelled=%llu readInflatedBytes=%llu peakStaging=%llu decodeStarted=%llu decoded=%llu decodeFailed=%llu decodeCancelled=%llu decodeBudgetRejected=%llu decodeBytes=%llu decodeAdmitted=%llu decodeBudget=%llu peakDecoded=%llu acquired=%llu generatedHits=%llu generatedMisses=%llu generatedWrites=%llu generatedCorrupt=%llu syncFallback=%d wrote=%d\n", + impl->learned.mapKey.c_str(), + static_cast( impl->learned.entries.size() ), + impl->manifestMatched ? 1 : 0, + impl->manifestRemoved ? 1 : 0, + static_cast( metrics.completedEntries ), + static_cast( metrics.rejectedEntries ), + static_cast( metrics.failedEntries ), + static_cast( metrics.cancelledEntries ), + static_cast( metrics.bytesRead ), + static_cast( metrics.peakStagingBytes ), + static_cast( metrics.decodeStartedEntries ), + static_cast( metrics.decodeCompletedEntries ), + static_cast( metrics.decodeFailedEntries ), + static_cast( metrics.decodeCancelledEntries ), + static_cast( metrics.decodeBudgetRejectedEntries ), + static_cast( metrics.bytesDecoded ), + static_cast( metrics.admittedDecodedBytes ), + static_cast( metrics.decodeBudgetBytes ), + static_cast( metrics.peakDecodedBytes ), + static_cast( metrics.cacheHits ), + static_cast( impl->generatedHits.load( std::memory_order_relaxed ) ), + static_cast( impl->generatedMisses.load( std::memory_order_relaxed ) ), + static_cast( impl->generatedWrites.load( std::memory_order_relaxed ) ), + static_cast( impl->generatedCorruptions.load( std::memory_order_relaxed ) ), + metrics.synchronousFallback ? 1 : 0, wroteManifest ? 1 : 0 ); + } + if ( successful ) { + // Continue learning source-backed resources that first become visible or + // are demanded after map publication. Unload writes that completed + // generation once more before teardown. + std::lock_guard lock( impl->mutex ); + impl->completedGeneration = true; + impl->recording = true; + } else { + impl->hints.clear(); + impl->learned = idLevelLoadCache::Manifest(); + impl->completedGeneration = false; + } +} + +void idLevelLoadCacheManager::Release() { + if ( impl != nullptr ) { + impl->pipeline.Reset(); + } +} + +void idLevelLoadCacheManager::Cancel() { + if ( impl == nullptr ) { + return; + } + bool hadGeneration = false; + { + std::lock_guard lock( impl->mutex ); + hadGeneration = impl->recording || !impl->learned.mapKey.empty(); + impl->recording = false; + } + impl->StopPipeline( true ); + const bool wroteFinalManifest = impl->completedGeneration && impl->WriteLearnedManifest(); + if ( hadGeneration && com_levelLoadCacheReport.GetBool() ) { + const idLevelLoadPipelineMetrics metrics = impl->pipeline.GetMetrics(); + common->Printf( "Level-load cache generation %llu closed and joined (%llu completed, %llu cancelled, finalManifest=%d)\n", + static_cast( impl->generation ), + static_cast( metrics.completedEntries ), + static_cast( metrics.cancelledEntries ), + wroteFinalManifest ? 1 : 0 ); + } + impl->pipeline.Reset(); + impl->hints.clear(); + impl->learned = idLevelLoadCache::Manifest(); + impl->completedGeneration = false; +} + +void idLevelLoadCacheManager::RecordSemantic( const levelLoadResourceType_t type, + const char *name, const char *options, const unsigned int flags, + const unsigned int priority ) { + std::string normalized; + if ( impl == nullptr || !NormalizePath( name, normalized ) ) { + return; + } + { + std::lock_guard lock( impl->mutex ); + if ( !impl->recording || impl->hints.size() >= 65536 ) { + return; + } + Impl::SemanticHint hint; + hint.type = type; + hint.normalizedName = normalized; + hint.flags = flags; + hint.priority = std::min( priority, + static_cast( idLevelLoadCache::ManifestPriority::CRITICAL ) ); + if ( options != nullptr ) { + const std::size_t length = std::min( std::strlen( options ), + idLevelLoadCache::DEFAULT_MAX_ENTRY_OPTIONS_BYTES ); + hint.options.assign( options, options + length ); + } + impl->hints.push_back( std::move( hint ) ); + } + + // A semantic lookup may be satisfied by an already resident manager object + // and therefore perform no source I/O this generation. Resolve only the + // identity (without reading bytes) so those real uses are still learned. + idFile *source = impl->fileSystem->OpenFileRead( normalized.c_str(), false ); + if ( source != nullptr ) { + impl->fileSystem->CloseFile( source ); + } +} + +void idLevelLoadCacheManager::RecordOpenedSource( const char *path, idFile *source ) { + std::string normalized; + if ( impl == nullptr || source == nullptr || !NormalizePath( path, normalized ) || + normalized.rfind( "generated/", 0 ) == 0 ) { + return; + } + idLevelLoadCache::SourceIdentity identity; + if ( !BuildSourceIdentity( normalized, source, identity ) ) { + return; + } + + std::lock_guard lock( impl->mutex ); + if ( !impl->recording || impl->learned.entries.size() >= idLevelLoadCache::DEFAULT_MAX_MANIFEST_ENTRIES ) { + return; + } + levelLoadResourceType_t type = ClassifyPath( normalized ); + unsigned int priority = DefaultPriority( type ); + unsigned int flags = 0; + std::vector options; + for ( auto hint = impl->hints.rbegin(); hint != impl->hints.rend(); ++hint ) { + if ( SemanticPathMatchesSource( hint->type, hint->normalizedName, normalized ) ) { + type = hint->type; + priority = hint->priority; + flags = hint->flags; + options = hint->options; + break; + } + } + idLevelLoadCache::ManifestEntry entry; + entry.type = ToFormatType( type ); + entry.priority = static_cast( + std::min( priority, + static_cast( idLevelLoadCache::ManifestPriority::CRITICAL ) ) ); + entry.firstUseOrder = impl->firstUseOrder++; + entry.useCount = 1; + entry.flags = flags; + entry.normalizedName = normalized; + entry.source = identity; + entry.options = std::move( options ); + impl->learned.entries.push_back( std::move( entry ) ); +} + +idFile *idLevelLoadCacheManager::OpenPreloadedSource( const char *path, + idFile *authoritativeSource ) { + std::string normalized; + idLevelLoadCache::SourceIdentity identity; + if ( impl == nullptr || authoritativeSource == nullptr || + !NormalizePath( path, normalized ) || + !BuildSourceIdentity( normalized, authoritativeSource, identity ) || + identity.size > static_cast( ( std::numeric_limits::max )() ) ) { + return nullptr; + } + + const std::uint32_t expectedType = static_cast( ClassifyPath( normalized ) ); + std::shared_ptr decoded = impl->pipeline.Acquire( + impl->generation, normalized.c_str(), expectedType, identity.size, identity.timestamp, + identity.containerPk4Checksum ); + if ( decoded == nullptr || decoded->bytes == nullptr || + decoded->bytes->size() != identity.size || decoded->generation != impl->generation || + decoded->type != expectedType ) { + return nullptr; + } + try { + return new idFile_LevelLoadPreloaded( decoded->bytes, authoritativeSource ); + } catch ( ... ) { + return nullptr; + } +} + +idFile *idLevelLoadCacheManager::OpenGeneratedCacheRead( + const generatedCacheKind_t kind, const char *sourcePath, + const unsigned int parserVersion, const char *settingsKey, + const char *contentKey ) { + if ( impl == nullptr || !com_levelLoadModernization.GetBool() || + !com_levelLoadCache.GetBool() || !IsValidCacheKind( kind ) || + parserVersion == 0 ) { + return nullptr; + } + std::string normalized; + if ( !NormalizePath( sourcePath, normalized ) ) { + return nullptr; + } + + // Resolve the authoritative retail/mod source first. The generated file is + // never searched through VFS and therefore cannot affect pure negotiation. + idFile *source = impl->fileSystem->OpenFileRead( normalized.c_str(), false ); + if ( source == nullptr ) { + impl->generatedMisses.fetch_add( 1, std::memory_order_relaxed ); + return nullptr; + } + const idLevelLoadCache::EnvelopeExpectation expected = impl->BuildExpectation( + kind, normalized, parserVersion, settingsKey, contentKey, source ); + impl->fileSystem->CloseFile( source ); + const std::string cachePath = CachePath( expected ); + std::vector encoded; + bool invalidCacheFile = false; + if ( !ReadWholeFile( impl->fileSystem, cachePath.c_str(), encoded, + MAX_GENERATED_FILE_BYTES, &invalidCacheFile ) ) { + impl->generatedMisses.fetch_add( 1, std::memory_order_relaxed ); + if ( invalidCacheFile ) { + impl->fileSystem->RemoveFileChecked( cachePath.c_str(), "fs_savepath" ); + impl->generatedCorruptions.fetch_add( 1, std::memory_order_relaxed ); + } + return nullptr; + } + + idLevelLoadCache::CacheEnvelope envelope; + idLevelLoadCache::Result result = idLevelLoadCache::DecodeEnvelopeForKey( encoded, expected, envelope ); + if ( result && envelope.codec == idLevelLoadCache::CompressionCodec::NONE ) { + result = idLevelLoadCache::ValidateDecodedPayload( envelope, + envelope.payload.data(), envelope.payload.size() ); + } else if ( result ) { + result.status = idLevelLoadCache::Status::INVALID_ENUM_VALUE; + result.diagnostic = "runtime does not support this generated-cache codec"; + } + if ( !result || envelope.payload.size() > static_cast( std::numeric_limits::max() ) ) { + impl->fileSystem->RemoveFileChecked( cachePath.c_str(), "fs_savepath" ); + impl->generatedMisses.fetch_add( 1, std::memory_order_relaxed ); + impl->generatedCorruptions.fetch_add( 1, std::memory_order_relaxed ); + if ( com_levelLoadCacheReport.GetBool() ) { + common->Printf( "Generated cache ignored and removed for %s (%s): %s\n", + normalized.c_str(), idLevelLoadCache::StatusName( result.status ), + result.diagnostic.c_str() ); + } + return nullptr; + } + + idFile_Memory *memory = new idFile_Memory( cachePath.c_str() ); + if ( !envelope.payload.empty() && memory->Write( envelope.payload.data(), + static_cast( envelope.payload.size() ) ) != static_cast( envelope.payload.size() ) ) { + delete memory; + return nullptr; + } + memory->MakeReadOnly(); + impl->generatedHits.fetch_add( 1, std::memory_order_relaxed ); + return memory; +} + +bool idLevelLoadCacheManager::WriteGeneratedCache( const generatedCacheKind_t kind, + const char *sourcePath, const unsigned int parserVersion, + const char *settingsKey, const void *payload, const unsigned int payloadBytes, + const char *contentKey ) { + if ( impl == nullptr || !com_levelLoadModernization.GetBool() || + !com_levelLoadCache.GetBool() || !com_levelLoadCacheWrite.GetBool() || + !IsValidCacheKind( kind ) || parserVersion == 0 || + ( payload == nullptr && payloadBytes != 0 ) || payloadBytes > idLevelLoadCache::DEFAULT_MAX_PAYLOAD_BYTES ) { + return false; + } + std::string normalized; + if ( !NormalizePath( sourcePath, normalized ) ) { + return false; + } + idFile *source = impl->fileSystem->OpenFileRead( normalized.c_str(), false ); + if ( source == nullptr ) { + return false; + } + const idLevelLoadCache::EnvelopeExpectation expected = impl->BuildExpectation( + kind, normalized, parserVersion, settingsKey, contentKey, source ); + impl->fileSystem->CloseFile( source ); + + idLevelLoadCache::CacheEnvelope envelope; + envelope.kind = expected.kind; + envelope.parserVersion = expected.parserVersion; + envelope.codec = idLevelLoadCache::CompressionCodec::NONE; + envelope.source = expected.source; + envelope.contentSignature = expected.contentSignature; + envelope.settingsSignature = expected.settingsSignature; + envelope.decodedPayloadBytes = payloadBytes; + envelope.decodedPayloadHash = idLevelLoadCache::ComputeHash( payload, payloadBytes ); + try { + const std::uint8_t *begin = static_cast( payload ); + if ( payloadBytes != 0 ) { + envelope.payload.assign( begin, begin + payloadBytes ); + } + } catch ( ... ) { + return false; + } + std::vector encoded; + const idLevelLoadCache::Result result = idLevelLoadCache::EncodeEnvelope( envelope, encoded ); + if ( !result ) { + common->Warning( "Could not encode generated cache for %s: %s (%s)", + normalized.c_str(), idLevelLoadCache::StatusName( result.status ), + result.diagnostic.c_str() ); + return false; + } + const bool wrote = WriteAtomic( impl->fileSystem, CachePath( expected ), encoded ); + if ( wrote ) { + impl->generatedWrites.fetch_add( 1, std::memory_order_relaxed ); + } + if ( com_levelLoadCacheReport.GetBool() ) { + common->Printf( "Generated cache %s for %s (%u payload bytes)\n", + wrote ? "stored" : "write failed", normalized.c_str(), payloadBytes ); + } + return wrote; +} + +void idLevelLoadCacheManager::DiscardGeneratedCache( const generatedCacheKind_t kind, + const char *sourcePath, const unsigned int parserVersion, + const char *settingsKey, const char *contentKey ) { + if ( impl == nullptr || !IsValidCacheKind( kind ) || parserVersion == 0 ) { + return; + } + std::string normalized; + if ( !NormalizePath( sourcePath, normalized ) ) { + return; + } + idFile *source = impl->fileSystem->OpenFileRead( normalized.c_str(), false ); + if ( source == nullptr ) { + return; + } + const idLevelLoadCache::EnvelopeExpectation expected = impl->BuildExpectation( + kind, normalized, parserVersion, settingsKey, contentKey, source ); + impl->fileSystem->CloseFile( source ); + const std::string cachePath = CachePath( expected ); + impl->fileSystem->RemoveFileChecked( cachePath.c_str(), "fs_savepath" ); + impl->generatedCorruptions.fetch_add( 1, std::memory_order_relaxed ); + if ( com_levelLoadCacheReport.GetBool() ) { + common->Printf( "Generated cache owner payload rejected and removed for %s\n", + normalized.c_str() ); + } +} diff --git a/src/framework/LevelLoadCacheManager.h b/src/framework/LevelLoadCacheManager.h new file mode 100644 index 00000000..1bb5f7bf --- /dev/null +++ b/src/framework/LevelLoadCacheManager.h @@ -0,0 +1,67 @@ +/* +=========================================================================== + +openQ4 learned level-load manifest and generated-cache coordinator + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __LEVEL_LOAD_CACHE_MANAGER_H__ +#define __LEVEL_LOAD_CACHE_MANAGER_H__ + +#include "FileSystem.h" + +#include + +class idFile; + +class idLevelLoadCacheManager { +public: + explicit idLevelLoadCacheManager( idFileSystem *fileSystem ); + ~idLevelLoadCacheManager(); + + idLevelLoadCacheManager( const idLevelLoadCacheManager & ) = delete; + idLevelLoadCacheManager &operator=( const idLevelLoadCacheManager & ) = delete; + + void Begin( const char *mapKey, const char *gameMode, + const char *entityFilter, const char *contentKey, + const char *settingsKey ); + void Finish( bool successful ); + // Finish joins the generation but deliberately retains immutable replay + // bytes while the resource owners complete EndLevelLoad(). Release drops + // that bounded staging storage once those main-thread consumers are done. + void Release(); + void Cancel(); + + void RecordSemantic( levelLoadResourceType_t type, const char *name, + const char *options, unsigned int flags, unsigned int priority ); + void RecordOpenedSource( const char *normalizedPath, idFile *source ); + // The caller has already resolved and authorized authoritativeSource through + // normal VFS/pure rules. Returns an immutable scheduled view only when its + // complete source identity matches; otherwise returns NULL. + idFile *OpenPreloadedSource( const char *normalizedPath, idFile *authoritativeSource ); + + idFile *OpenGeneratedCacheRead( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey, const char *contentKey ); + bool WriteGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey, const void *payload, + unsigned int payloadBytes, const char *contentKey ); + void DiscardGeneratedCache( generatedCacheKind_t kind, + const char *sourcePath, unsigned int parserVersion, + const char *settingsKey, const char *contentKey ); + +private: + struct Impl; + std::unique_ptr impl; +}; + +#endif /* !__LEVEL_LOAD_CACHE_MANAGER_H__ */ diff --git a/src/framework/LevelLoadPipeline.cpp b/src/framework/LevelLoadPipeline.cpp new file mode 100644 index 00000000..a7187751 --- /dev/null +++ b/src/framework/LevelLoadPipeline.cpp @@ -0,0 +1,882 @@ +/* +=========================================================================== + +openQ4 generation-scoped level-load byte pipeline + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#include "LevelLoadPipeline.h" + +#include +#include +#include +#include +#include + +#if defined( min ) + #undef min +#endif +#if defined( max ) + #undef max +#endif + +namespace { + +enum class PipelineItemState : unsigned int { + QUEUED = 0, + READING, + DECODING, + READY, + CANCELLED, + FAILED +}; + +static constexpr std::uint32_t LEVEL_LOAD_DECODED_FRAMING_VERSION = 1; +static constexpr std::uint64_t FNV1A64_OFFSET = 14695981039346656037ull; +static constexpr std::uint64_t FNV1A64_PRIME = 1099511628211ull; +static constexpr std::uint32_t MAX_FRAME_UNITS = 1024u * 1024u; + +void UpdatePeak( std::atomic &peak, const std::uint64_t value ) { + std::uint64_t observed = peak.load( std::memory_order_relaxed ); + while ( observed < value && + !peak.compare_exchange_weak( observed, value, + std::memory_order_relaxed, std::memory_order_relaxed ) ) { + } +} + +std::uint64_t UpdateTransportChecksum( std::uint64_t checksum, + const unsigned char *bytes, const std::size_t byteCount ) { + for ( std::size_t index = 0; index < byteCount; ++index ) { + checksum ^= bytes[ index ]; + checksum *= FNV1A64_PRIME; + } + return checksum; +} + +void SealBytes( std::uint64_t &seal, const void *data, const std::size_t byteCount ) { + seal = UpdateTransportChecksum( seal, + static_cast( data ), byteCount ); +} + +void SealU32( std::uint64_t &seal, const std::uint32_t value ) { + unsigned char encoded[4]; + for ( std::size_t index = 0; index < 4; ++index ) { + encoded[index] = static_cast( value >> ( index * 8 ) ); + } + SealBytes( seal, encoded, sizeof( encoded ) ); +} + +void SealU64( std::uint64_t &seal, const std::uint64_t value ) { + unsigned char encoded[8]; + for ( std::size_t index = 0; index < 8; ++index ) { + encoded[index] = static_cast( value >> ( index * 8 ) ); + } + SealBytes( seal, encoded, sizeof( encoded ) ); +} + +std::uint64_t ComputeFramingSeal( const idLevelLoadDecodedSource &decoded ) { + std::uint64_t seal = FNV1A64_OFFSET; + static constexpr unsigned char domain[] = { + 'o', 'p', 'e', 'n', 'Q', '4', '-', 'p', 'r', 'e', 'l', 'o', 'a', 'd' + }; + SealBytes( seal, domain, sizeof( domain ) ); + SealU32( seal, decoded.framingVersion ); + SealU64( seal, decoded.generation ); + SealU64( seal, decoded.normalizedPath.size() ); + SealBytes( seal, decoded.normalizedPath.data(), decoded.normalizedPath.size() ); + SealU32( seal, decoded.type ); + SealU32( seal, static_cast( decoded.frameKind ) ); + SealU64( seal, decoded.sourceBytes ); + SealU64( seal, decoded.payloadOffset ); + SealU64( seal, decoded.payloadBytes ); + SealU32( seal, decoded.frameUnitCount ); + SealU64( seal, decoded.sourceTimestamp ); + SealU32( seal, decoded.containerChecksum ); + SealU64( seal, decoded.transportChecksum ); + SealBytes( seal, decoded.contentIntegrity.data(), decoded.contentIntegrity.size() ); + return seal; +} + +bool HasContentIntegrity( const std::array &integrity ) { + return std::any_of( integrity.begin(), integrity.end(), + []( const unsigned char value ) { return value != 0; } ); +} + +bool HasExtension( const std::string &path, const char *extension ) { + const std::size_t extensionLength = std::strlen( extension ); + return path.size() >= extensionLength && + path.compare( path.size() - extensionLength, extensionLength, extension ) == 0; +} + +std::uint32_t ReadLittleU32( const unsigned char *bytes ) { + return static_cast( bytes[0] ) | + ( static_cast( bytes[1] ) << 8 ) | + ( static_cast( bytes[2] ) << 16 ) | + ( static_cast( bytes[3] ) << 24 ); +} + +std::uint32_t ReadBigU32( const unsigned char *bytes ) { + return ( static_cast( bytes[0] ) << 24 ) | + ( static_cast( bytes[1] ) << 16 ) | + ( static_cast( bytes[2] ) << 8 ) | + static_cast( bytes[3] ); +} + +struct DecodeControl { + const idJobCancellationToken *cancellation; + std::atomic *bytesDecoded; + std::uint64_t maximumBytes; + std::uint64_t reportedBytes; + bool invalidProgress; +}; + +bool DecodeCancellationRequested( void *opaque ) { + const DecodeControl *control = static_cast( opaque ); + return control == nullptr || control->cancellation == nullptr || + control->cancellation->IsCancellationRequested(); +} + +bool ReportDecodeProgress( void *opaque, const std::size_t byteCount ) { + DecodeControl *control = static_cast( opaque ); + if ( control == nullptr || control->bytesDecoded == nullptr || + DecodeCancellationRequested( opaque ) ) { + return false; + } + if ( byteCount == 0 || byteCount > control->maximumBytes - control->reportedBytes ) { + control->invalidProgress = true; + return false; + } + control->reportedBytes += byteCount; + control->bytesDecoded->fetch_add( byteCount, std::memory_order_relaxed ); + return true; +} + +} // namespace + +idLevelLoadDecodeStatus idLevelLoadDecodeSourceFrame( + const idLevelLoadPipelineSource &source, const unsigned char *bytes, + const std::size_t byteCount, idLevelLoadDecodeOutput &output, + const idLevelLoadDecodeContext *context ) { + if ( bytes == nullptr || byteCount == 0 || source.type == 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + output.frameKind = idLevelLoadDecodedFrameKind::OPAQUE_VFS_BYTES; + output.decodedBytes = static_cast( byteCount ); + output.payloadOffset = 0; + output.payloadBytes = static_cast( byteCount ); + output.frameUnitCount = 1; + + // DDS layout depends on legacy/DX10 headers, texture kind, mip dimensions, + // block formats, arrays, and cubemap faces. The renderer remains the only + // owner of that full validation, so a DDS preload is deliberately opaque. + if ( HasExtension( source.normalizedPath, ".dds" ) ) { + return idLevelLoadDecodeStatus::COMPLETE; + } + if ( HasExtension( source.normalizedPath, ".wav" ) ) { + output.frameKind = idLevelLoadDecodedFrameKind::RIFF_WAVE; + if ( byteCount < 12 || std::memcmp( bytes, "RIFF", 4 ) != 0 || + std::memcmp( bytes + 8, "WAVE", 4 ) != 0 || + static_cast( ReadLittleU32( bytes + 4 ) ) + 8ull != byteCount ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + std::size_t cursor = 12; + std::uint32_t unitCount = 0; + bool sawFormat = false; + bool sawData = false; + while ( cursor < byteCount ) { + if ( context != nullptr && context->IsCancellationRequested() ) { + return idLevelLoadDecodeStatus::CANCELLED; + } + if ( byteCount - cursor < 8 || unitCount == MAX_FRAME_UNITS ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + const std::uint32_t chunkBytes = ReadLittleU32( bytes + cursor + 4 ); + const std::uint64_t storedBytes = 8ull + chunkBytes + ( chunkBytes & 1u ); + if ( storedBytes > byteCount - cursor ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + if ( std::memcmp( bytes + cursor, "fmt ", 4 ) == 0 ) { + if ( sawFormat || chunkBytes < 16 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + sawFormat = true; + } else if ( std::memcmp( bytes + cursor, "data", 4 ) == 0 ) { + if ( !sawFormat ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + sawData = true; + } + cursor += static_cast( storedBytes ); + ++unitCount; + } + if ( cursor != byteCount || !sawFormat || !sawData || unitCount == 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + output.frameUnitCount = unitCount; + return idLevelLoadDecodeStatus::COMPLETE; + } + if ( HasExtension( source.normalizedPath, ".ogg" ) ) { + output.frameKind = idLevelLoadDecodedFrameKind::OGG; + std::size_t cursor = 0; + std::uint32_t pageCount = 0; + while ( cursor < byteCount ) { + if ( context != nullptr && context->IsCancellationRequested() ) { + return idLevelLoadDecodeStatus::CANCELLED; + } + if ( byteCount - cursor < 27 || pageCount == MAX_FRAME_UNITS || + std::memcmp( bytes + cursor, "OggS", 4 ) != 0 || bytes[cursor + 4] != 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + const std::size_t segmentCount = bytes[cursor + 26]; + if ( segmentCount > byteCount - cursor - 27 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + std::uint64_t pageBytes = 27ull + segmentCount; + for ( std::size_t index = 0; index < segmentCount; ++index ) { + pageBytes += bytes[cursor + 27 + index]; + } + if ( pageBytes > byteCount - cursor ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + cursor += static_cast( pageBytes ); + ++pageCount; + } + if ( cursor != byteCount || pageCount == 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + output.frameUnitCount = pageCount; + return idLevelLoadDecodeStatus::COMPLETE; + } + if ( HasExtension( source.normalizedPath, ".png" ) ) { + static constexpr unsigned char PNG_SIGNATURE[8] = { + 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a + }; + output.frameKind = idLevelLoadDecodedFrameKind::PNG; + if ( byteCount < sizeof( PNG_SIGNATURE ) + 12 || + std::memcmp( bytes, PNG_SIGNATURE, sizeof( PNG_SIGNATURE ) ) != 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + std::size_t cursor = sizeof( PNG_SIGNATURE ); + std::uint32_t chunkCount = 0; + bool sawHeader = false; + bool sawData = false; + bool sawEnd = false; + while ( cursor < byteCount ) { + if ( context != nullptr && context->IsCancellationRequested() ) { + return idLevelLoadDecodeStatus::CANCELLED; + } + if ( sawEnd || byteCount - cursor < 12 || chunkCount == MAX_FRAME_UNITS ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + const std::uint32_t chunkBytes = ReadBigU32( bytes + cursor ); + const std::uint64_t storedBytes = 12ull + chunkBytes; + if ( storedBytes > byteCount - cursor ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + const unsigned char *type = bytes + cursor + 4; + const bool isHeader = std::memcmp( type, "IHDR", 4 ) == 0; + const bool isData = std::memcmp( type, "IDAT", 4 ) == 0; + const bool isEnd = std::memcmp( type, "IEND", 4 ) == 0; + if ( chunkCount == 0 && ( !isHeader || chunkBytes != 13 ) ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + if ( isHeader ) { + if ( sawHeader || chunkBytes != 13 || ReadBigU32( bytes + cursor + 8 ) == 0 || + ReadBigU32( bytes + cursor + 12 ) == 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + sawHeader = true; + } + if ( isEnd && chunkBytes != 0 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + sawData = sawData || isData; + cursor += static_cast( storedBytes ); + ++chunkCount; + sawEnd = isEnd; + } + if ( cursor != byteCount || !sawHeader || !sawData || !sawEnd || chunkCount < 3 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + output.frameUnitCount = chunkCount; + return idLevelLoadDecodeStatus::COMPLETE; + } + if ( HasExtension( source.normalizedPath, ".jpg" ) || + HasExtension( source.normalizedPath, ".jpeg" ) ) { + output.frameKind = idLevelLoadDecodedFrameKind::JPEG; + if ( byteCount < 4 || bytes[0] != 0xff || bytes[1] != 0xd8 || + bytes[byteCount - 2] != 0xff || bytes[byteCount - 1] != 0xd9 ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + return idLevelLoadDecodeStatus::COMPLETE; + } + return idLevelLoadDecodeStatus::COMPLETE; +} + +struct idLevelLoadPipeline::Item { + idLevelLoadPipeline *owner; + idLevelLoadPipelineSource source; + std::shared_ptr > stagingBytes; + std::shared_ptr decoded; + std::uint64_t readTransportChecksum; + std::atomic state; + + Item() + : owner( nullptr ) + , stagingBytes( std::make_shared >() ) + , readTransportChecksum( FNV1A64_OFFSET ) + , state( PipelineItemState::QUEUED ) { + } +}; + +idLevelLoadPipelineConfig::idLevelLoadPipelineConfig() + : maxEntries( 512 ) + , maxSourceBytes( 128ull * 1024ull * 1024ull ) + , maxTotalBytes( 384ull * 1024ull * 1024ull ) + , maxDecodedBytes( 384ull * 1024ull * 1024ull ) + , readChunkBytes( 256u * 1024u ) + , decodeChunkBytes( 256u * 1024u ) { +} + +idLevelLoadPipelineSource::idLevelLoadPipelineSource() + : type( 0 ) + , priority( 0 ) + , firstUseOrder( 0 ) + , file( nullptr ) + , sourceBytes( 0 ) + , sourceTimestamp( 0 ) + , containerChecksum( 0 ) { +} + +idLevelLoadPipelineMetrics::idLevelLoadPipelineMetrics() + : generation( 0 ) + , admittedEntries( 0 ) + , rejectedEntries( 0 ) + , completedEntries( 0 ) + , cancelledEntries( 0 ) + , failedEntries( 0 ) + , bytesRead( 0 ) + , peakStagingBytes( 0 ) + , decodeStartedEntries( 0 ) + , decodeCompletedEntries( 0 ) + , decodeCancelledEntries( 0 ) + , decodeFailedEntries( 0 ) + , decodeBudgetRejectedEntries( 0 ) + , bytesDecoded( 0 ) + , admittedDecodedBytes( 0 ) + , decodeBudgetBytes( 0 ) + , peakDecodedBytes( 0 ) + , cacheHits( 0 ) + , synchronousFallback( false ) + , active( false ) + , cancelled( false ) { +} + +idLevelLoadDecodeOutput::idLevelLoadDecodeOutput() + : frameKind( idLevelLoadDecodedFrameKind::INVALID ) + , decodedBytes( 0 ) + , payloadOffset( 0 ) + , payloadBytes( 0 ) + , frameUnitCount( 0 ) + , transportChecksum( 0 ) { + contentIntegrity.fill( 0 ); +} + +idLevelLoadDecodeContext::idLevelLoadDecodeContext() + : generation( 0 ) + , chunkBytes( 0 ) + , cancellationFunction( nullptr ) + , progressFunction( nullptr ) + , state( nullptr ) { +} + +bool idLevelLoadDecodeContext::IsCancellationRequested() const { + return cancellationFunction == nullptr || cancellationFunction( state ); +} + +bool idLevelLoadDecodeContext::ReportDecodedBytes( const std::size_t byteCount ) const { + return progressFunction != nullptr && progressFunction( state, byteCount ); +} + +idLevelLoadDecodedSource::idLevelLoadDecodedSource() + : framingVersion( LEVEL_LOAD_DECODED_FRAMING_VERSION ) + , generation( 0 ) + , type( 0 ) + , frameKind( idLevelLoadDecodedFrameKind::INVALID ) + , sourceBytes( 0 ) + , payloadOffset( 0 ) + , payloadBytes( 0 ) + , frameUnitCount( 0 ) + , sourceTimestamp( 0 ) + , containerChecksum( 0 ) + , transportChecksum( 0 ) + , framingSeal( 0 ) { + contentIntegrity.fill( 0 ); +} + +idLevelLoadPipeline::idLevelLoadPipeline() + : generation( 0 ) + , readFunction( nullptr ) + , readUserData( nullptr ) + , decodeFunction( nullptr ) + , decodeUserData( nullptr ) + , admittedEntries( 0 ) + , rejectedEntries( 0 ) + , completedEntries( 0 ) + , cancelledEntries( 0 ) + , failedEntries( 0 ) + , bytesRead( 0 ) + , currentStagingBytes( 0 ) + , peakStagingBytes( 0 ) + , decodeStartedEntries( 0 ) + , decodeCompletedEntries( 0 ) + , decodeCancelledEntries( 0 ) + , decodeFailedEntries( 0 ) + , decodeBudgetRejectedEntries( 0 ) + , bytesDecoded( 0 ) + , admittedDecodedBytes( 0 ) + , currentDecodedBytes( 0 ) + , peakDecodedBytes( 0 ) + , cacheHits( 0 ) + , synchronousFallback( false ) + , active( false ) + , cancelled( false ) { +} + +idLevelLoadPipeline::~idLevelLoadPipeline() { + CancelAndWait(); +} + +bool idLevelLoadPipeline::Begin( const std::uint64_t newGeneration, + const idLevelLoadPipelineConfig &config, + std::vector sources, + const idLevelLoadReadFunction newReadFunction, + void *newReadUserData, + const idLevelLoadDecodeFunction newDecodeFunction, + void *newDecodeUserData ) { + if ( active.load( std::memory_order_acquire ) || jobList != nullptr ) { + return false; + } + + Reset(); + currentConfig = config; + generation = newGeneration; + readFunction = newReadFunction; + readUserData = newReadUserData; + decodeFunction = newDecodeFunction; + decodeUserData = newDecodeUserData; + + if ( readFunction == nullptr || decodeFunction == nullptr || currentConfig.maxEntries == 0 || + currentConfig.maxSourceBytes == 0 || currentConfig.maxTotalBytes == 0 || + currentConfig.maxDecodedBytes == 0 || currentConfig.readChunkBytes == 0 || + currentConfig.decodeChunkBytes == 0 ) { + // Preserve ownership of every already-open main-thread handle so the + // caller can close it through DrainOpenFiles even on setup failure. + for ( idLevelLoadPipelineSource &source : sources ) { + if ( source.file != nullptr ) { + rejectedOpenFiles.push_back( source.file ); + source.file = nullptr; + } + } + return false; + } + currentConfig.readChunkBytes = std::min( + currentConfig.readChunkBytes, + static_cast( 4u * 1024u * 1024u ) ); + currentConfig.decodeChunkBytes = std::min( + currentConfig.decodeChunkBytes, + static_cast( 4u * 1024u * 1024u ) ); + + // Stable priority/order sorting makes budget admission deterministic. + std::stable_sort( sources.begin(), sources.end(), + []( const idLevelLoadPipelineSource &a, const idLevelLoadPipelineSource &b ) { + if ( a.priority != b.priority ) { + return a.priority > b.priority; + } + if ( a.firstUseOrder != b.firstUseOrder ) { + return a.firstUseOrder < b.firstUseOrder; + } + return a.normalizedPath < b.normalizedPath; + } ); + + std::uint64_t admittedBytes = 0; + std::uint64_t decodedAdmissionBytes = 0; + items.reserve( std::min( sources.size(), currentConfig.maxEntries ) ); + for ( idLevelLoadPipelineSource &source : sources ) { + const bool invalid = source.file == nullptr || source.normalizedPath.empty() || + source.sourceBytes == 0 || source.sourceBytes > currentConfig.maxSourceBytes || + source.sourceBytes > static_cast( std::numeric_limits::max() ); + const bool stagingSaturated = items.size() >= currentConfig.maxEntries || + source.sourceBytes > currentConfig.maxTotalBytes - admittedBytes; + const bool decodeSaturated = source.sourceBytes > + currentConfig.maxDecodedBytes - decodedAdmissionBytes; + const bool saturated = stagingSaturated || decodeSaturated; + if ( invalid || saturated ) { + rejectedEntries.fetch_add( 1, std::memory_order_relaxed ); + if ( !invalid && decodeSaturated ) { + decodeBudgetRejectedEntries.fetch_add( 1, std::memory_order_relaxed ); + } + if ( source.file != nullptr ) { + rejectedOpenFiles.push_back( source.file ); + source.file = nullptr; + } + continue; + } + + std::unique_ptr item( new Item() ); + item->owner = this; + item->source = std::move( source ); + items.push_back( std::move( item ) ); + admittedBytes += items.back()->source.sourceBytes; + decodedAdmissionBytes += items.back()->source.sourceBytes; + admittedEntries.fetch_add( 1, std::memory_order_relaxed ); + admittedDecodedBytes.fetch_add( items.back()->source.sourceBytes, + std::memory_order_relaxed ); + } + + if ( items.empty() ) { + return true; + } + + jobList = jobSystem.CreateJobList( + "level-load-read-decode", + items.size(), + 0, + idJobPriority::HIGH ); + if ( jobList != nullptr ) { + for ( const std::unique_ptr &item : items ) { + if ( !jobList->AddJob( &idLevelLoadPipeline::RunJob, item.get() ) ) { + jobList.reset(); + break; + } + } + } + + active.store( true, std::memory_order_release ); + if ( jobList != nullptr ) { + const idJobSubmitResult submitResult = jobList->Submit(); + if ( submitResult == idJobSubmitResult::ACCEPTED ) { + return true; + } + if ( submitResult == idJobSubmitResult::EXECUTED_SYNCHRONOUSLY ) { + // Report jobs-disabled/deterministic execution through the same + // explicit synchronous-path metric used for admission fallback. + synchronousFallback.store( true, std::memory_order_relaxed ); + active.store( false, std::memory_order_release ); + return true; + } + jobList.reset(); + } + + // Admission saturation or an unavailable service must not affect correctness: + // execute the bounded prefetch batch inline and leave ordinary VFS fallback + // authoritative for anything that was not admitted. + synchronousFallback.store( true, std::memory_order_relaxed ); + idJobCancellationToken noCancellation; + for ( const std::unique_ptr &item : items ) { + RunItem( *item, noCancellation ); + } + active.store( false, std::memory_order_release ); + return true; +} + +void idLevelLoadPipeline::RunJob( const idJobContext &context ) { + Item *item = static_cast( context.data ); + if ( item == nullptr || item->owner == nullptr ) { + return; + } + item->owner->RunItem( *item, context.cancellation ); +} + +void idLevelLoadPipeline::RunItem( Item &item, + const idJobCancellationToken &cancellation ) { + if ( cancellation.IsCancellationRequested() ) { + item.state.store( PipelineItemState::CANCELLED, std::memory_order_release ); + cancelledEntries.fetch_add( 1, std::memory_order_relaxed ); + return; + } + + item.state.store( PipelineItemState::READING, std::memory_order_release ); + try { + item.stagingBytes->resize( static_cast( item.source.sourceBytes ) ); + } catch ( ... ) { + item.stagingBytes->clear(); + item.state.store( PipelineItemState::FAILED, std::memory_order_release ); + failedEntries.fetch_add( 1, std::memory_order_relaxed ); + return; + } + + const std::uint64_t staging = currentStagingBytes.fetch_add( + item.source.sourceBytes, std::memory_order_relaxed ) + item.source.sourceBytes; + UpdatePeak( peakStagingBytes, staging ); + + std::size_t offset = 0; + while ( offset < item.stagingBytes->size() ) { + if ( cancellation.IsCancellationRequested() ) { + item.stagingBytes->clear(); + currentStagingBytes.fetch_sub( item.source.sourceBytes, std::memory_order_relaxed ); + item.state.store( PipelineItemState::CANCELLED, std::memory_order_release ); + cancelledEntries.fetch_add( 1, std::memory_order_relaxed ); + return; + } + const std::size_t remaining = item.stagingBytes->size() - offset; + const std::size_t request = std::min( remaining, currentConfig.readChunkBytes ); + const int read = readFunction( item.source.file, item.stagingBytes->data() + offset, + static_cast( request ), readUserData ); + if ( read <= 0 || static_cast( read ) > request ) { + item.stagingBytes->clear(); + currentStagingBytes.fetch_sub( item.source.sourceBytes, std::memory_order_relaxed ); + item.state.store( PipelineItemState::FAILED, std::memory_order_release ); + failedEntries.fetch_add( 1, std::memory_order_relaxed ); + return; + } + item.readTransportChecksum = UpdateTransportChecksum( + item.readTransportChecksum, item.stagingBytes->data() + offset, + static_cast( read ) ); + offset += static_cast( read ); + bytesRead.fetch_add( static_cast( read ), std::memory_order_relaxed ); + } + + item.state.store( PipelineItemState::DECODING, std::memory_order_release ); + decodeStartedEntries.fetch_add( 1, std::memory_order_relaxed ); + DecodeControl control; + control.cancellation = &cancellation; + control.bytesDecoded = &bytesDecoded; + control.maximumBytes = item.source.sourceBytes; + control.reportedBytes = 0; + control.invalidProgress = false; + idLevelLoadDecodeContext context; + context.generation = generation; + context.chunkBytes = currentConfig.decodeChunkBytes; + context.cancellationFunction = &DecodeCancellationRequested; + context.progressFunction = &ReportDecodeProgress; + context.state = &control; + idLevelLoadDecodeOutput output; + idLevelLoadDecodeStatus decodeStatus = idLevelLoadDecodeStatus::CANCELLED; + if ( !cancellation.IsCancellationRequested() ) { + decodeStatus = decodeFunction( item.source, item.stagingBytes->data(), + item.stagingBytes->size(), context, output, decodeUserData ); + } + const bool wasCancelled = cancellation.IsCancellationRequested() || + decodeStatus == idLevelLoadDecodeStatus::CANCELLED; + const bool invalidDecode = decodeStatus != idLevelLoadDecodeStatus::COMPLETE || + control.invalidProgress || control.reportedBytes != item.source.sourceBytes || + output.decodedBytes != item.source.sourceBytes || + output.payloadOffset > output.decodedBytes || + output.payloadBytes > output.decodedBytes - output.payloadOffset || + output.payloadBytes == 0 || output.frameUnitCount == 0 || + output.frameUnitCount > MAX_FRAME_UNITS || + output.transportChecksum != item.readTransportChecksum || + !HasContentIntegrity( output.contentIntegrity ) || + output.frameKind == idLevelLoadDecodedFrameKind::INVALID || + static_cast( output.frameKind ) > + static_cast( idLevelLoadDecodedFrameKind::JPEG ); + if ( wasCancelled || invalidDecode ) { + item.stagingBytes->clear(); + currentStagingBytes.fetch_sub( item.source.sourceBytes, std::memory_order_relaxed ); + item.state.store( wasCancelled ? PipelineItemState::CANCELLED : PipelineItemState::FAILED, + std::memory_order_release ); + if ( wasCancelled ) { + cancelledEntries.fetch_add( 1, std::memory_order_relaxed ); + decodeCancelledEntries.fetch_add( 1, std::memory_order_relaxed ); + } else { + failedEntries.fetch_add( 1, std::memory_order_relaxed ); + decodeFailedEntries.fetch_add( 1, std::memory_order_relaxed ); + } + return; + } + + std::shared_ptr decoded; + try { + decoded = std::make_shared(); + decoded->generation = generation; + decoded->normalizedPath = item.source.normalizedPath; + decoded->type = item.source.type; + decoded->frameKind = output.frameKind; + decoded->sourceBytes = item.source.sourceBytes; + decoded->payloadOffset = output.payloadOffset; + decoded->payloadBytes = output.payloadBytes; + decoded->frameUnitCount = output.frameUnitCount; + decoded->sourceTimestamp = item.source.sourceTimestamp; + decoded->containerChecksum = item.source.containerChecksum; + decoded->transportChecksum = output.transportChecksum; + decoded->contentIntegrity = output.contentIntegrity; + decoded->bytes = item.stagingBytes; + decoded->framingSeal = ComputeFramingSeal( *decoded ); + } catch ( ... ) { + item.stagingBytes->clear(); + currentStagingBytes.fetch_sub( item.source.sourceBytes, std::memory_order_relaxed ); + item.state.store( PipelineItemState::FAILED, std::memory_order_release ); + failedEntries.fetch_add( 1, std::memory_order_relaxed ); + decodeFailedEntries.fetch_add( 1, std::memory_order_relaxed ); + return; + } + item.decoded = decoded; + item.stagingBytes.reset(); + currentStagingBytes.fetch_sub( item.source.sourceBytes, std::memory_order_relaxed ); + const std::uint64_t decodedResident = currentDecodedBytes.fetch_add( + item.source.sourceBytes, std::memory_order_relaxed ) + item.source.sourceBytes; + UpdatePeak( peakDecodedBytes, decodedResident ); + item.state.store( PipelineItemState::READY, std::memory_order_release ); + completedEntries.fetch_add( 1, std::memory_order_relaxed ); + decodeCompletedEntries.fetch_add( 1, std::memory_order_relaxed ); +} + +void idLevelLoadPipeline::CancelAndWait() { + WaitInternal( true ); +} + +void idLevelLoadPipeline::Wait() { + WaitInternal( false ); +} + +void idLevelLoadPipeline::WaitInternal( const bool cancel ) { + if ( cancel ) { + cancelled.store( true, std::memory_order_relaxed ); + if ( jobList != nullptr ) { + jobList->Cancel(); + } + } + if ( jobList != nullptr ) { + jobList->Wait(); + jobList.reset(); + } + active.store( false, std::memory_order_release ); +} + +bool idLevelLoadPipeline::IsActive() const { + return active.load( std::memory_order_acquire ); +} + +std::uint64_t idLevelLoadPipeline::GetGeneration() const { + return generation; +} + +std::shared_ptr idLevelLoadPipeline::Acquire( + const std::uint64_t expectedGeneration, + const char *path, + const std::uint32_t expectedType, + const std::uint64_t expectedBytes, + const std::uint64_t expectedTimestamp, + const std::uint32_t expectedContainerChecksum ) { + if ( path == nullptr || path[ 0 ] == '\0' ) { + return std::shared_ptr(); + } + for ( const std::unique_ptr &item : items ) { + if ( expectedGeneration != generation || item->source.normalizedPath != path || + item->source.type != expectedType || + item->source.sourceBytes != expectedBytes || + item->source.sourceTimestamp != expectedTimestamp || + item->source.containerChecksum != expectedContainerChecksum ) { + continue; + } + if ( item->state.load( std::memory_order_acquire ) != PipelineItemState::READY ) { + return std::shared_ptr(); + } + if ( item->decoded == nullptr || !ValidateDecodedSource( *item->decoded ) || + item->decoded->generation != expectedGeneration || + item->decoded->normalizedPath != path || + item->decoded->type != expectedType || + item->decoded->sourceBytes != expectedBytes || + item->decoded->sourceTimestamp != expectedTimestamp || + item->decoded->containerChecksum != expectedContainerChecksum ) { + return std::shared_ptr(); + } + cacheHits.fetch_add( 1, std::memory_order_relaxed ); + return item->decoded; + } + return std::shared_ptr(); +} + +bool idLevelLoadPipeline::ValidateDecodedSource( + const idLevelLoadDecodedSource &decoded ) const { + return decoded.framingVersion == LEVEL_LOAD_DECODED_FRAMING_VERSION && + decoded.generation == generation && !decoded.normalizedPath.empty() && + decoded.type != 0 && decoded.frameKind != idLevelLoadDecodedFrameKind::INVALID && + static_cast( decoded.frameKind ) <= + static_cast( idLevelLoadDecodedFrameKind::JPEG ) && + decoded.sourceBytes != 0 && decoded.bytes != nullptr && + decoded.bytes->size() == decoded.sourceBytes && + decoded.payloadOffset <= decoded.sourceBytes && decoded.payloadBytes != 0 && + decoded.payloadBytes <= decoded.sourceBytes - decoded.payloadOffset && + decoded.frameUnitCount != 0 && decoded.frameUnitCount <= MAX_FRAME_UNITS && + HasContentIntegrity( decoded.contentIntegrity ) && + decoded.framingSeal == ComputeFramingSeal( decoded ); +} + +void idLevelLoadPipeline::DrainOpenFiles( std::vector &files ) { + for ( const std::unique_ptr &item : items ) { + if ( item->source.file != nullptr ) { + files.push_back( item->source.file ); + item->source.file = nullptr; + } + } + files.insert( files.end(), rejectedOpenFiles.begin(), rejectedOpenFiles.end() ); + rejectedOpenFiles.clear(); +} + +void idLevelLoadPipeline::Reset() { + CancelAndWait(); + items.clear(); + rejectedOpenFiles.clear(); + generation = 0; + readFunction = nullptr; + readUserData = nullptr; + decodeFunction = nullptr; + decodeUserData = nullptr; + admittedEntries.store( 0, std::memory_order_relaxed ); + rejectedEntries.store( 0, std::memory_order_relaxed ); + completedEntries.store( 0, std::memory_order_relaxed ); + cancelledEntries.store( 0, std::memory_order_relaxed ); + failedEntries.store( 0, std::memory_order_relaxed ); + bytesRead.store( 0, std::memory_order_relaxed ); + currentStagingBytes.store( 0, std::memory_order_relaxed ); + peakStagingBytes.store( 0, std::memory_order_relaxed ); + decodeStartedEntries.store( 0, std::memory_order_relaxed ); + decodeCompletedEntries.store( 0, std::memory_order_relaxed ); + decodeCancelledEntries.store( 0, std::memory_order_relaxed ); + decodeFailedEntries.store( 0, std::memory_order_relaxed ); + decodeBudgetRejectedEntries.store( 0, std::memory_order_relaxed ); + bytesDecoded.store( 0, std::memory_order_relaxed ); + admittedDecodedBytes.store( 0, std::memory_order_relaxed ); + currentDecodedBytes.store( 0, std::memory_order_relaxed ); + peakDecodedBytes.store( 0, std::memory_order_relaxed ); + cacheHits.store( 0, std::memory_order_relaxed ); + synchronousFallback.store( false, std::memory_order_relaxed ); + active.store( false, std::memory_order_relaxed ); + cancelled.store( false, std::memory_order_relaxed ); +} + +idLevelLoadPipelineMetrics idLevelLoadPipeline::GetMetrics() const { + idLevelLoadPipelineMetrics metrics; + metrics.generation = generation; + metrics.admittedEntries = admittedEntries.load( std::memory_order_relaxed ); + metrics.rejectedEntries = rejectedEntries.load( std::memory_order_relaxed ); + metrics.completedEntries = completedEntries.load( std::memory_order_relaxed ); + metrics.cancelledEntries = cancelledEntries.load( std::memory_order_relaxed ); + metrics.failedEntries = failedEntries.load( std::memory_order_relaxed ); + metrics.bytesRead = bytesRead.load( std::memory_order_relaxed ); + metrics.peakStagingBytes = peakStagingBytes.load( std::memory_order_relaxed ); + metrics.decodeStartedEntries = decodeStartedEntries.load( std::memory_order_relaxed ); + metrics.decodeCompletedEntries = decodeCompletedEntries.load( std::memory_order_relaxed ); + metrics.decodeCancelledEntries = decodeCancelledEntries.load( std::memory_order_relaxed ); + metrics.decodeFailedEntries = decodeFailedEntries.load( std::memory_order_relaxed ); + metrics.decodeBudgetRejectedEntries = decodeBudgetRejectedEntries.load( std::memory_order_relaxed ); + metrics.bytesDecoded = bytesDecoded.load( std::memory_order_relaxed ); + metrics.admittedDecodedBytes = admittedDecodedBytes.load( std::memory_order_relaxed ); + metrics.decodeBudgetBytes = currentConfig.maxDecodedBytes; + metrics.peakDecodedBytes = peakDecodedBytes.load( std::memory_order_relaxed ); + metrics.cacheHits = cacheHits.load( std::memory_order_relaxed ); + metrics.synchronousFallback = synchronousFallback.load( std::memory_order_relaxed ); + metrics.active = active.load( std::memory_order_relaxed ); + metrics.cancelled = cancelled.load( std::memory_order_relaxed ); + return metrics; +} diff --git a/src/framework/LevelLoadPipeline.h b/src/framework/LevelLoadPipeline.h new file mode 100644 index 00000000..0c9f69f9 --- /dev/null +++ b/src/framework/LevelLoadPipeline.h @@ -0,0 +1,258 @@ +/* +=========================================================================== + +openQ4 generation-scoped level-load byte pipeline + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __LEVEL_LOAD_PIPELINE_H__ +#define __LEVEL_LOAD_PIPELINE_H__ + +#include "ParallelJobSystem.h" + +#include +#include +#include +#include +#include +#include +#include + +class idFile; + +struct idLevelLoadPipelineConfig { + std::size_t maxEntries; + std::uint64_t maxSourceBytes; + std::uint64_t maxTotalBytes; + std::uint64_t maxDecodedBytes; + std::size_t readChunkBytes; + std::size_t decodeChunkBytes; + + idLevelLoadPipelineConfig(); +}; + +struct idLevelLoadPipelineSource { + std::string normalizedPath; + std::uint32_t type; + std::uint32_t priority; + std::uint32_t firstUseOrder; + idFile *file; + std::uint64_t sourceBytes; + std::uint64_t sourceTimestamp; + std::uint32_t containerChecksum; + + idLevelLoadPipelineSource(); +}; + +struct idLevelLoadPipelineMetrics { + std::uint64_t generation; + std::uint64_t admittedEntries; + std::uint64_t rejectedEntries; + std::uint64_t completedEntries; + std::uint64_t cancelledEntries; + std::uint64_t failedEntries; + std::uint64_t bytesRead; + std::uint64_t peakStagingBytes; + std::uint64_t decodeStartedEntries; + std::uint64_t decodeCompletedEntries; + std::uint64_t decodeCancelledEntries; + std::uint64_t decodeFailedEntries; + std::uint64_t decodeBudgetRejectedEntries; + std::uint64_t bytesDecoded; + std::uint64_t admittedDecodedBytes; + std::uint64_t decodeBudgetBytes; + std::uint64_t peakDecodedBytes; + std::uint64_t cacheHits; + bool synchronousFallback; + bool active; + bool cancelled; + + idLevelLoadPipelineMetrics(); +}; + +enum class idLevelLoadDecodedFrameKind : std::uint32_t { + INVALID = 0, + OPAQUE_VFS_BYTES = 1, + RIFF_WAVE = 2, + OGG = 3, + PNG = 4, + JPEG = 5 +}; + +enum class idLevelLoadDecodeStatus : std::uint32_t { + COMPLETE = 0, + MALFORMED, + CANCELLED +}; + +struct idLevelLoadDecodeOutput { + idLevelLoadDecodedFrameKind frameKind; + std::uint64_t decodedBytes; + std::uint64_t payloadOffset; + std::uint64_t payloadBytes; + std::uint32_t frameUnitCount; + std::uint64_t transportChecksum; + std::array contentIntegrity; + + idLevelLoadDecodeOutput(); +}; + +struct idLevelLoadDecodeContext; + +// Portable, allocation-free framing validation used by the production +// decoder and its native malformed/truncation/trailing-data tests. DDS remains +// opaque here because its complete mip/array layout is owned by the renderer. +idLevelLoadDecodeStatus idLevelLoadDecodeSourceFrame( + const idLevelLoadPipelineSource &source, + const unsigned char *bytes, + std::size_t byteCount, + idLevelLoadDecodeOutput &output, + const idLevelLoadDecodeContext *context = nullptr ); + +// A decoder may only publish a result after reporting every inspected byte. +// Both operations are safe to call from a worker and make long decode loops +// cooperatively cancellable without exposing engine globals to that worker. +struct idLevelLoadDecodeContext { + std::uint64_t generation; + std::size_t chunkBytes; + + bool IsCancellationRequested() const; + bool ReportDecodedBytes( std::size_t byteCount ) const; + +private: + typedef bool ( *CancellationFunction )( void *state ); + typedef bool ( *ProgressFunction )( void *state, std::size_t byteCount ); + + CancellationFunction cancellationFunction; + ProgressFunction progressFunction; + void *state; + + idLevelLoadDecodeContext(); + friend class idLevelLoadPipeline; +}; + +typedef idLevelLoadDecodeStatus ( *idLevelLoadDecodeFunction )( + const idLevelLoadPipelineSource &source, + const unsigned char *bytes, + std::size_t byteCount, + const idLevelLoadDecodeContext &context, + idLevelLoadDecodeOutput &output, + void *userData ); + +typedef int ( *idLevelLoadReadFunction )( + idFile *file, + void *buffer, + int byteCount, + void *userData ); + +// Immutable worker-produced framing/integrity DTO. Asset-specific parsers +// remain main-thread owners; this only authorizes a byte-view substitution. +struct idLevelLoadDecodedSource { + std::uint32_t framingVersion; + std::uint64_t generation; + std::string normalizedPath; + std::uint32_t type; + idLevelLoadDecodedFrameKind frameKind; + std::uint64_t sourceBytes; + std::uint64_t payloadOffset; + std::uint64_t payloadBytes; + std::uint32_t frameUnitCount; + std::uint64_t sourceTimestamp; + std::uint32_t containerChecksum; + std::uint64_t transportChecksum; + std::array contentIntegrity; + std::shared_ptr > bytes; + std::uint64_t framingSeal; + + idLevelLoadDecodedSource(); +}; + +class idLevelLoadPipeline { +public: + idLevelLoadPipeline(); + ~idLevelLoadPipeline(); + + idLevelLoadPipeline( const idLevelLoadPipeline & ) = delete; + idLevelLoadPipeline &operator=( const idLevelLoadPipeline & ) = delete; + + // Every source file must be independently opened on the main thread. The + // pipeline owns no VFS resolution state and returns all handles to the owner + // through DrainOpenFiles() after a join. + bool Begin( std::uint64_t generation, + const idLevelLoadPipelineConfig &config, + std::vector sources, + idLevelLoadReadFunction readFunction, + void *readUserData = nullptr, + idLevelLoadDecodeFunction decodeFunction = nullptr, + void *decodeUserData = nullptr ); + + void CancelAndWait(); + void Wait(); + bool IsActive() const; + std::uint64_t GetGeneration() const; + + // Returns an immutable decoded DTO only after a main-thread caller supplies + // the active generation plus the exact authoritative VFS source identity. + // Returned shared ownership remains valid across Reset() and map teardown. + std::shared_ptr Acquire( + std::uint64_t expectedGeneration, + const char *normalizedPath, + std::uint32_t expectedType, + std::uint64_t expectedBytes, + std::uint64_t expectedTimestamp, + std::uint32_t expectedContainerChecksum ); + + void DrainOpenFiles( std::vector &files ); + void Reset(); + idLevelLoadPipelineMetrics GetMetrics() const; + +private: + struct Item; + + static void RunJob( const idJobContext &context ); + void RunItem( Item &item, const idJobCancellationToken &cancellation ); + void WaitInternal( bool cancel ); + bool ValidateDecodedSource( const idLevelLoadDecodedSource &decoded ) const; + + idLevelLoadPipelineConfig currentConfig; + std::uint64_t generation; + std::vector > items; + std::vector rejectedOpenFiles; + std::unique_ptr jobList; + idLevelLoadReadFunction readFunction; + void *readUserData; + idLevelLoadDecodeFunction decodeFunction; + void *decodeUserData; + + std::atomic admittedEntries; + std::atomic rejectedEntries; + std::atomic completedEntries; + std::atomic cancelledEntries; + std::atomic failedEntries; + std::atomic bytesRead; + std::atomic currentStagingBytes; + std::atomic peakStagingBytes; + std::atomic decodeStartedEntries; + std::atomic decodeCompletedEntries; + std::atomic decodeCancelledEntries; + std::atomic decodeFailedEntries; + std::atomic decodeBudgetRejectedEntries; + std::atomic bytesDecoded; + std::atomic admittedDecodedBytes; + std::atomic currentDecodedBytes; + std::atomic peakDecodedBytes; + std::atomic cacheHits; + std::atomic synchronousFallback; + std::atomic active; + std::atomic cancelled; +}; + +#endif diff --git a/src/framework/ParallelJobSystem.cpp b/src/framework/ParallelJobSystem.cpp new file mode 100644 index 00000000..9e66d178 --- /dev/null +++ b/src/framework/ParallelJobSystem.cpp @@ -0,0 +1,1045 @@ +/* +=========================================================================== + +openQ4 portable parallel job system + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +The scheduling model is informed by id Software's Doom 3 BFG +idParallelJobList, released under GPLv3. This is an original implementation +using portable C++ condition variables and bounded openQ4-owned state. + +=========================================================================== +*/ + +#include "ParallelJobSystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const unsigned int MAX_JOB_WORKERS = 32; +const std::size_t MAX_JOB_LIST_CAPACITY = 1024; +const std::size_t MAX_JOBS_PER_LIST = 1024 * 1024; +const std::size_t MAX_DEPENDENCIES_PER_LIST = 256; +const std::size_t MAX_JOB_LIST_NAME = 63; +const std::uint64_t JOB_LIST_STARVATION_THRESHOLD = 8; + +std::uint64_t JobClockMicroseconds() { + return static_cast( std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() ).count() ); +} + +bool JobStatusIsTerminal( const idJobListStatus status ) { + return status == idJobListStatus::COMPLETED || + status == idJobListStatus::CANCELLED || + status == idJobListStatus::FAILED; +} + +bool JobStatusIsDependencyFailure( const idJobListStatus status ) { + return status == idJobListStatus::CANCELLED || status == idJobListStatus::FAILED; +} + +int JobPriorityRank( const idJobPriority priority ) { + return static_cast( priority ); +} + +struct idJobRecord { + idJobFunction function; + void *data; +}; + +struct idJobWork { + std::shared_ptr list; + idJobRecord job; + unsigned int workerIndex; +}; + +} // namespace + +struct idJobListState { + std::weak_ptr owner; + std::string name; + std::size_t maxJobs; + std::size_t maxDependencies; + idJobPriority priority; + std::vector jobs; + std::vector > dependencies; + std::shared_ptr > cancellationFlag; + std::condition_variable completionCondition; + idJobListStatus status; + std::size_t nextJob; + std::size_t runningJobs; + std::uint64_t submitSequence; + std::uint64_t submitTimeMicroseconds; + std::uint64_t startTimeMicroseconds; + std::uint64_t finishTimeMicroseconds; + std::uint64_t waitTimeMicroseconds; + std::uint64_t executionTimeMicroseconds; + std::uint64_t executedJobs; + std::uint64_t cancelledJobs; + std::uint64_t failedJobs; + std::uint64_t schedulingAge; + bool failureObserved; + + idJobListState( const std::shared_ptr &owner_, const char *name_, + const std::size_t maxJobs_, const std::size_t maxDependencies_, const idJobPriority priority_ ) + : owner( owner_ ) + , name( name_ != nullptr ? name_ : "unnamed" ) + , maxJobs( maxJobs_ ) + , maxDependencies( maxDependencies_ ) + , priority( priority_ ) + , cancellationFlag( std::make_shared >( false ) ) + , status( idJobListStatus::BUILDING ) + , nextJob( 0 ) + , runningJobs( 0 ) + , submitSequence( 0 ) + , submitTimeMicroseconds( 0 ) + , startTimeMicroseconds( 0 ) + , finishTimeMicroseconds( 0 ) + , waitTimeMicroseconds( 0 ) + , executionTimeMicroseconds( 0 ) + , executedJobs( 0 ) + , cancelledJobs( 0 ) + , failedJobs( 0 ) + , schedulingAge( 0 ) + , failureObserved( false ) { + if ( name.size() > MAX_JOB_LIST_NAME ) { + name.resize( MAX_JOB_LIST_NAME ); + } + jobs.reserve( maxJobs ); + dependencies.reserve( maxDependencies ); + } +}; + +struct idJobSystemImpl : public std::enable_shared_from_this { + mutable std::mutex mutex; + std::condition_variable workCondition; + std::condition_variable allDoneCondition; + std::vector workers; + std::vector > activeLists; + idJobSystemConfig config; + idJobSystemMetrics metrics; + bool initialized; + bool accepting; + bool stopping; + std::uint64_t nextSubmitSequence; + + idJobSystemImpl() + : initialized( false ) + , accepting( false ) + , stopping( false ) + , nextSubmitSequence( 1 ) { + } + + bool DependenciesReadyLocked( const idJobListState &list ) const { + for ( const std::shared_ptr &dependency : list.dependencies ) { + if ( dependency == nullptr || dependency->status != idJobListStatus::COMPLETED ) { + return false; + } + } + return true; + } + + bool DependencyFailedLocked( const idJobListState &list ) const { + for ( const std::shared_ptr &dependency : list.dependencies ) { + if ( dependency == nullptr || JobStatusIsDependencyFailure( dependency->status ) ) { + return true; + } + } + return false; + } + + void RemoveActiveLocked( const std::shared_ptr &list ) { + const auto found = std::find( activeLists.begin(), activeLists.end(), list ); + if ( found != activeLists.end() ) { + activeLists.erase( found ); + } + metrics.queuedLists = activeLists.size(); + if ( activeLists.empty() ) { + allDoneCondition.notify_all(); + } + } + + void FinishLocked( const std::shared_ptr &list, const idJobListStatus status ) { + if ( list == nullptr || JobStatusIsTerminal( list->status ) ) { + return; + } + list->status = status; + list->finishTimeMicroseconds = JobClockMicroseconds(); + if ( status == idJobListStatus::COMPLETED ) { + metrics.completedLists++; + } else if ( status == idJobListStatus::CANCELLED ) { + metrics.cancelledLists++; + } else if ( status == idJobListStatus::FAILED ) { + metrics.failedLists++; + } + RemoveActiveLocked( list ); + list->completionCondition.notify_all(); + } + + void RequestCancellationLocked( const std::shared_ptr &list ) { + if ( list == nullptr || JobStatusIsTerminal( list->status ) ) { + return; + } + list->cancellationFlag->store( true, std::memory_order_release ); + if ( list->status == idJobListStatus::BUILDING ) { + list->status = idJobListStatus::CANCELLED; + list->finishTimeMicroseconds = JobClockMicroseconds(); + list->cancelledJobs = list->jobs.size(); + list->completionCondition.notify_all(); + return; + } + if ( list->nextJob < list->jobs.size() ) { + const std::uint64_t cancelled = static_cast( list->jobs.size() - list->nextJob ); + list->cancelledJobs += cancelled; + metrics.cancelledJobs += cancelled; + list->nextJob = list->jobs.size(); + } + list->status = idJobListStatus::CANCELLING; + if ( list->runningJobs == 0 ) { + FinishLocked( list, idJobListStatus::CANCELLED ); + } + } + + void RefreshWaitingListsLocked() { + std::size_t index = 0; + while ( index < activeLists.size() ) { + const std::shared_ptr list = activeLists[ index ]; + if ( list == nullptr || list->status != idJobListStatus::WAITING ) { + index++; + continue; + } + if ( DependencyFailedLocked( *list ) ) { + RequestCancellationLocked( list ); + } else if ( DependenciesReadyLocked( *list ) ) { + list->status = idJobListStatus::READY; + if ( list->jobs.empty() ) { + FinishLocked( list, idJobListStatus::COMPLETED ); + } + } + // FinishLocked erases the current element. In that case, inspect the + // element shifted into this slot without allocating a snapshot vector. + if ( index < activeLists.size() && activeLists[ index ] == list ) { + index++; + } + } + } + + bool HasRunnableWorkLocked() { + RefreshWaitingListsLocked(); + for ( const std::shared_ptr &list : activeLists ) { + if ( list != nullptr && + ( list->status == idJobListStatus::READY || list->status == idJobListStatus::RUNNING ) && + list->nextJob < list->jobs.size() ) { + return true; + } + } + return false; + } + + bool AcquireWorkLocked( const unsigned int workerIndex, idJobWork &work ) { + RefreshWaitingListsLocked(); + std::shared_ptr selected; + bool selectedIsStarved = false; + for ( const std::shared_ptr &candidate : activeLists ) { + if ( candidate == nullptr || + ( candidate->status != idJobListStatus::READY && candidate->status != idJobListStatus::RUNNING ) || + candidate->nextJob >= candidate->jobs.size() ) { + continue; + } + // Once any runnable list reaches the starvation threshold, scheduling + // switches to a primary oldest-waiter class. Base priority is ignored + // inside that class so a ring of earlier HIGH lists cannot permanently + // pin a later LOW list at a tied, capped score. + const bool candidateIsStarved = candidate->schedulingAge >= JOB_LIST_STARVATION_THRESHOLD; + bool preferCandidate = selected == nullptr; + if ( selected != nullptr ) { + if ( candidateIsStarved != selectedIsStarved ) { + preferCandidate = candidateIsStarved; + } else if ( candidateIsStarved ) { + preferCandidate = candidate->schedulingAge > selected->schedulingAge || + ( candidate->schedulingAge == selected->schedulingAge && + candidate->submitSequence < selected->submitSequence ); + } else { + const int candidatePriority = JobPriorityRank( candidate->priority ); + const int selectedPriority = JobPriorityRank( selected->priority ); + preferCandidate = candidatePriority > selectedPriority || + ( candidatePriority == selectedPriority && + ( candidate->schedulingAge > selected->schedulingAge || + ( candidate->schedulingAge == selected->schedulingAge && + candidate->submitSequence < selected->submitSequence ) ) ); + } + } + if ( preferCandidate ) { + selected = candidate; + selectedIsStarved = candidateIsStarved; + } + } + if ( selected == nullptr ) { + return false; + } + for ( const std::shared_ptr &candidate : activeLists ) { + if ( candidate == nullptr || candidate == selected || + ( candidate->status != idJobListStatus::READY && candidate->status != idJobListStatus::RUNNING ) || + candidate->nextJob >= candidate->jobs.size() ) { + continue; + } + if ( candidate->schedulingAge != ( std::numeric_limits::max )() ) { + candidate->schedulingAge++; + } + } + if ( selectedIsStarved ) { + metrics.priorityPromotions++; + } + selected->schedulingAge = 0; + + work.list = selected; + work.job = selected->jobs[ selected->nextJob++ ]; + work.workerIndex = workerIndex; + selected->runningJobs++; + metrics.runningJobs++; + if ( selected->startTimeMicroseconds == 0 ) { + selected->startTimeMicroseconds = JobClockMicroseconds(); + } + selected->status = idJobListStatus::RUNNING; + return true; + } + + void CompleteWorkLocked( const idJobWork &work, const std::uint64_t elapsedMicroseconds, const bool failed ) { + const std::shared_ptr &list = work.list; + if ( list == nullptr ) { + return; + } + if ( list->runningJobs > 0 ) { + list->runningJobs--; + } + if ( metrics.runningJobs > 0 ) { + metrics.runningJobs--; + } + list->executionTimeMicroseconds += elapsedMicroseconds; + metrics.totalExecutionMicroseconds += elapsedMicroseconds; + list->executedJobs++; + metrics.executedJobs++; + if ( failed ) { + list->failureObserved = true; + list->failedJobs++; + metrics.failedJobs++; + list->cancellationFlag->store( true, std::memory_order_release ); + if ( list->nextJob < list->jobs.size() ) { + const std::uint64_t cancelled = static_cast( list->jobs.size() - list->nextJob ); + list->cancelledJobs += cancelled; + metrics.cancelledJobs += cancelled; + list->nextJob = list->jobs.size(); + } + list->status = idJobListStatus::CANCELLING; + } + + if ( list->runningJobs == 0 && list->nextJob >= list->jobs.size() ) { + if ( list->failureObserved ) { + FinishLocked( list, idJobListStatus::FAILED ); + } else if ( list->cancellationFlag->load( std::memory_order_acquire ) ) { + FinishLocked( list, idJobListStatus::CANCELLED ); + } else { + FinishLocked( list, idJobListStatus::COMPLETED ); + } + } + RefreshWaitingListsLocked(); + workCondition.notify_all(); + } + + void WorkerMain( const unsigned int workerIndex ) { + for ( ;; ) { + idJobWork work = {}; + { + std::unique_lock lock( mutex ); + metrics.sleepingWorkers++; + workCondition.wait( lock, [this]() { + return stopping || HasRunnableWorkLocked(); + } ); + metrics.sleepingWorkers--; + metrics.workerWakeups++; + if ( stopping && !HasRunnableWorkLocked() ) { + return; + } + if ( !AcquireWorkLocked( workerIndex, work ) ) { + continue; + } + } + + const std::uint64_t start = JobClockMicroseconds(); + bool failed = false; + try { + idJobContext context = {}; + context.data = work.job.data; + context.workerIndex = workerIndex; + context.cancellation = idJobCancellationToken( work.list->cancellationFlag ); + work.job.function( context ); + } catch ( ... ) { + failed = true; + } + const std::uint64_t elapsed = JobClockMicroseconds() - start; + + std::lock_guard lock( mutex ); + CompleteWorkLocked( work, elapsed, failed ); + } + } + + idJobSubmitResult SubmitSynchronous( const std::shared_ptr &list ) { + std::unique_lock admissionLock( mutex ); + if ( !initialized || !accepting ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::SYSTEM_UNAVAILABLE; + } + if ( list == nullptr || list->status != idJobListStatus::BUILDING ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::INVALID_STATE; + } + for ( const std::shared_ptr &dependency : list->dependencies ) { + if ( dependency == nullptr || dependency->owner.lock().get() != this || + dependency->status == idJobListStatus::BUILDING ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::INVALID_DEPENDENCY; + } + if ( JobStatusIsDependencyFailure( dependency->status ) ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::DEPENDENCY_FAILED; + } + } + if ( activeLists.size() >= config.maxQueuedLists ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::QUEUE_FULL; + } + + list->submitSequence = nextSubmitSequence++; + list->submitTimeMicroseconds = JobClockMicroseconds(); + list->status = idJobListStatus::WAITING; + metrics.submittedLists++; + metrics.submittedJobs += list->jobs.size(); + activeLists.push_back( list ); + metrics.queuedLists = activeLists.size(); + metrics.queueHighWatermark = ( std::max )( metrics.queueHighWatermark, metrics.queuedLists ); + workCondition.notify_all(); + + // Synchronous lists still participate in the global active/quiescence + // contract. Concurrent callers sleep until their deterministic submit + // sequence reaches the front; shutdown and CancelAll can therefore observe, + // cancel, and join every inline job just like worker-owned work. + workCondition.wait( admissionLock, [this, &list]() { + return JobStatusIsTerminal( list->status ) || + ( !activeLists.empty() && activeLists.front() == list ); + } ); + if ( JobStatusIsTerminal( list->status ) ) { + return idJobSubmitResult::EXECUTED_SYNCHRONOUSLY; + } + if ( DependencyFailedLocked( *list ) ) { + RequestCancellationLocked( list ); + workCondition.notify_all(); + return idJobSubmitResult::DEPENDENCY_FAILED; + } + if ( !DependenciesReadyLocked( *list ) ) { + // Dependencies are admitted before dependents. Reaching the head while + // one is still non-terminal would violate that ordering contract. + list->failureObserved = true; + FinishLocked( list, idJobListStatus::FAILED ); + workCondition.notify_all(); + return idJobSubmitResult::INVALID_DEPENDENCY; + } + list->status = idJobListStatus::RUNNING; + list->startTimeMicroseconds = JobClockMicroseconds(); + list->runningJobs = 1; + metrics.runningJobs++; + admissionLock.unlock(); + + for ( std::size_t jobIndex = 0; jobIndex < list->jobs.size(); ++jobIndex ) { + if ( list->cancellationFlag->load( std::memory_order_acquire ) ) { + break; + } + const idJobRecord &job = list->jobs[ jobIndex ]; + { + std::lock_guard lock( mutex ); + // Mark the job acquired before invoking user work so a concurrent + // cancellation accounts only jobs that have not started. + list->nextJob = jobIndex + 1; + } + const std::uint64_t start = JobClockMicroseconds(); + bool failed = false; + try { + idJobContext context = {}; + context.data = job.data; + context.workerIndex = 0; + context.cancellation = idJobCancellationToken( list->cancellationFlag ); + job.function( context ); + } catch ( ... ) { + failed = true; + } + const std::uint64_t elapsed = JobClockMicroseconds() - start; + std::lock_guard lock( mutex ); + list->executionTimeMicroseconds += elapsed; + metrics.totalExecutionMicroseconds += elapsed; + list->executedJobs++; + metrics.executedJobs++; + if ( failed ) { + list->failureObserved = true; + list->cancellationFlag->store( true, std::memory_order_release ); + list->failedJobs++; + metrics.failedJobs++; + const std::uint64_t cancelled = static_cast( list->jobs.size() - list->nextJob ); + list->cancelledJobs += cancelled; + metrics.cancelledJobs += cancelled; + list->nextJob = list->jobs.size(); + break; + } + } + + { + std::lock_guard lock( mutex ); + list->runningJobs = 0; + if ( metrics.runningJobs > 0 ) { + metrics.runningJobs--; + } + if ( list->failureObserved ) { + FinishLocked( list, idJobListStatus::FAILED ); + } else if ( list->cancellationFlag->load( std::memory_order_acquire ) ) { + FinishLocked( list, idJobListStatus::CANCELLED ); + } else { + FinishLocked( list, idJobListStatus::COMPLETED ); + } + RefreshWaitingListsLocked(); + workCondition.notify_all(); + } + return idJobSubmitResult::EXECUTED_SYNCHRONOUSLY; + } + + idJobSubmitResult SubmitThreaded( const std::shared_ptr &list ) { + std::lock_guard lock( mutex ); + if ( !initialized || !accepting ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::SYSTEM_UNAVAILABLE; + } + if ( list == nullptr || list->status != idJobListStatus::BUILDING ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::INVALID_STATE; + } + for ( const std::shared_ptr &dependency : list->dependencies ) { + if ( dependency == nullptr || dependency->owner.lock().get() != this || + dependency->status == idJobListStatus::BUILDING ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::INVALID_DEPENDENCY; + } + if ( JobStatusIsDependencyFailure( dependency->status ) ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::DEPENDENCY_FAILED; + } + } + if ( activeLists.size() >= config.maxQueuedLists ) { + metrics.rejectedSubmissions++; + return idJobSubmitResult::QUEUE_FULL; + } + + list->submitSequence = nextSubmitSequence++; + list->submitTimeMicroseconds = JobClockMicroseconds(); + list->status = DependenciesReadyLocked( *list ) ? idJobListStatus::READY : idJobListStatus::WAITING; + metrics.submittedLists++; + metrics.submittedJobs += list->jobs.size(); + activeLists.push_back( list ); + metrics.queuedLists = activeLists.size(); + metrics.queueHighWatermark = ( std::max )( metrics.queueHighWatermark, metrics.queuedLists ); + + if ( list->jobs.empty() && list->status == idJobListStatus::READY ) { + FinishLocked( list, idJobListStatus::COMPLETED ); + RefreshWaitingListsLocked(); + } + workCondition.notify_all(); + return idJobSubmitResult::ACCEPTED; + } +}; + +idJobCancellationToken::idJobCancellationToken() { +} + +idJobCancellationToken::idJobCancellationToken( const std::shared_ptr > &flag_ ) + : flag( flag_ ) { +} + +bool idJobCancellationToken::IsCancellationRequested() const { + return flag != nullptr && flag->load( std::memory_order_acquire ); +} + +idJobCancellationToken::operator bool() const { + return flag != nullptr; +} + +idJobSystemConfig::idJobSystemConfig() + : workerThreads( 0 ) + , maxQueuedLists( 64 ) + , synchronous( false ) { +} + +idJobListMetrics::idJobListMetrics() + : status( idJobListStatus::BUILDING ) + , jobCapacity( 0 ) + , dependencyCapacity( 0 ) + , submittedJobs( 0 ) + , executedJobs( 0 ) + , cancelledJobs( 0 ) + , failedJobs( 0 ) + , submitTimeMicroseconds( 0 ) + , startTimeMicroseconds( 0 ) + , finishTimeMicroseconds( 0 ) + , waitTimeMicroseconds( 0 ) + , executionTimeMicroseconds( 0 ) { +} + +idJobSystemMetrics::idJobSystemMetrics() + : submittedLists( 0 ) + , rejectedSubmissions( 0 ) + , completedLists( 0 ) + , cancelledLists( 0 ) + , failedLists( 0 ) + , submittedJobs( 0 ) + , executedJobs( 0 ) + , cancelledJobs( 0 ) + , failedJobs( 0 ) + , workerWakeups( 0 ) + , priorityPromotions( 0 ) + , totalExecutionMicroseconds( 0 ) + , totalWaitMicroseconds( 0 ) + , queuedLists( 0 ) + , queueHighWatermark( 0 ) + , runningJobs( 0 ) + , sleepingWorkers( 0 ) + , workerThreads( 0 ) + , synchronous( false ) + , accepting( false ) { +} + +idJobSystem::idJobSystem() + : impl( std::make_shared() ) { +} + +idJobSystem::~idJobSystem() { + Shutdown( idJobShutdownMode::CANCEL_PENDING ); + impl.reset(); +} + +bool idJobSystem::Initialize( const idJobSystemConfig &requestedConfig ) { + if ( impl == nullptr ) { + impl = std::make_shared(); + } + if ( requestedConfig.maxQueuedLists == 0 || requestedConfig.maxQueuedLists > MAX_JOB_LIST_CAPACITY || + requestedConfig.workerThreads > MAX_JOB_WORKERS ) { + return false; + } + + std::unique_lock lock( impl->mutex ); + if ( impl->initialized ) { + return false; + } + if ( !impl->activeLists.empty() || !impl->workers.empty() ) { + return false; + } + impl->config = requestedConfig; + if ( impl->config.workerThreads == 0 ) { + impl->config.workerThreads = ResolveWorkerThreadCount( 0 ); + } + impl->config.synchronous = requestedConfig.synchronous; + try { + // All scheduler-owned vectors are reserved before admission or worker + // creation begins. Accepted submissions and teardown therefore require + // no container growth and allocation failure remains an Initialize error. + impl->activeLists.reserve( impl->config.maxQueuedLists ); + if ( !impl->config.synchronous ) { + impl->workers.reserve( impl->config.workerThreads ); + } + } catch ( ... ) { + return false; + } + impl->metrics = idJobSystemMetrics(); + impl->metrics.synchronous = impl->config.synchronous; + impl->metrics.workerThreads = impl->config.synchronous ? 0 : impl->config.workerThreads; + impl->metrics.accepting = true; + impl->accepting = true; + impl->stopping = false; + impl->initialized = true; + impl->nextSubmitSequence = 1; + + if ( !impl->config.synchronous ) { + try { + for ( unsigned int workerIndex = 0; workerIndex < impl->config.workerThreads; ++workerIndex ) { + const std::shared_ptr workerImpl = impl; + impl->workers.emplace_back( [workerImpl, workerIndex]() { + workerImpl->WorkerMain( workerIndex ); + } ); + } + } catch ( ... ) { + impl->accepting = false; + impl->metrics.accepting = false; + impl->stopping = true; + impl->workCondition.notify_all(); + std::vector startedWorkers = std::move( impl->workers ); + impl->initialized = false; + lock.unlock(); + for ( std::thread &worker : startedWorkers ) { + if ( worker.joinable() ) { + worker.join(); + } + } + return false; + } + } + return true; +} + +void idJobSystem::Shutdown( const idJobShutdownMode mode ) { + if ( impl == nullptr ) { + return; + } + std::vector workers; + { + std::unique_lock lock( impl->mutex ); + if ( !impl->initialized ) { + return; + } + impl->accepting = false; + impl->metrics.accepting = false; + if ( mode == idJobShutdownMode::CANCEL_PENDING ) { + std::size_t index = 0; + while ( index < impl->activeLists.size() ) { + const std::shared_ptr list = impl->activeLists[ index ]; + impl->RequestCancellationLocked( list ); + if ( index < impl->activeLists.size() && impl->activeLists[ index ] == list ) { + index++; + } + } + } + impl->workCondition.notify_all(); + impl->allDoneCondition.wait( lock, [this]() { + return impl->activeLists.empty(); + } ); + impl->stopping = true; + impl->workCondition.notify_all(); + workers = std::move( impl->workers ); + impl->initialized = false; + } + for ( std::thread &worker : workers ) { + if ( worker.joinable() ) { + worker.join(); + } + } +} + +void idJobSystem::CancelAll() { + if ( impl == nullptr ) { + return; + } + std::lock_guard lock( impl->mutex ); + std::size_t index = 0; + while ( index < impl->activeLists.size() ) { + const std::shared_ptr list = impl->activeLists[ index ]; + impl->RequestCancellationLocked( list ); + if ( index < impl->activeLists.size() && impl->activeLists[ index ] == list ) { + index++; + } + } + impl->RefreshWaitingListsLocked(); + impl->workCondition.notify_all(); +} + +void idJobSystem::WaitAll() { + if ( impl == nullptr ) { + return; + } + const std::uint64_t start = JobClockMicroseconds(); + std::unique_lock lock( impl->mutex ); + impl->allDoneCondition.wait( lock, [this]() { + return impl->activeLists.empty(); + } ); + impl->metrics.totalWaitMicroseconds += JobClockMicroseconds() - start; +} + +bool idJobSystem::IsInitialized() const { + if ( impl == nullptr ) { + return false; + } + std::lock_guard lock( impl->mutex ); + return impl->initialized; +} + +bool idJobSystem::IsSynchronous() const { + if ( impl == nullptr ) { + return true; + } + std::lock_guard lock( impl->mutex ); + return !impl->initialized || impl->config.synchronous; +} + +unsigned int idJobSystem::GetWorkerThreadCount() const { + if ( impl == nullptr ) { + return 0; + } + std::lock_guard lock( impl->mutex ); + return impl->initialized && !impl->config.synchronous ? impl->config.workerThreads : 0; +} + +idJobSystemMetrics idJobSystem::GetMetrics() const { + if ( impl == nullptr ) { + return idJobSystemMetrics(); + } + std::lock_guard lock( impl->mutex ); + idJobSystemMetrics snapshot = impl->metrics; + snapshot.queuedLists = impl->activeLists.size(); + snapshot.workerThreads = impl->initialized && !impl->config.synchronous ? impl->config.workerThreads : 0; + snapshot.synchronous = impl->config.synchronous; + snapshot.accepting = impl->accepting; + return snapshot; +} + +std::unique_ptr idJobSystem::CreateJobList( const char *name, + const std::size_t maxJobs, const std::size_t maxDependencies, + const idJobPriority priority ) { + if ( impl == nullptr || maxJobs == 0 || maxJobs > MAX_JOBS_PER_LIST || + maxDependencies > MAX_DEPENDENCIES_PER_LIST ) { + return std::unique_ptr(); + } + { + std::lock_guard lock( impl->mutex ); + if ( !impl->initialized || !impl->accepting ) { + return std::unique_ptr(); + } + } + try { + return std::unique_ptr( new idJobList( std::make_shared( + impl, name, maxJobs, maxDependencies, priority ) ) ); + } catch ( ... ) { + return std::unique_ptr(); + } +} + +unsigned int idJobSystem::ResolveWorkerThreadCount( const int requestedThreads ) { + if ( requestedThreads > 0 ) { + return ( std::min )( static_cast( requestedThreads ), MAX_JOB_WORKERS ); + } + const unsigned int hardwareThreads = std::thread::hardware_concurrency(); + if ( hardwareThreads <= 1 ) { + return 1; + } + return ( std::min )( hardwareThreads - 1, MAX_JOB_WORKERS ); +} + +idJobList::idJobList( const std::shared_ptr &state_ ) + : state( state_ ) { +} + +idJobList::~idJobList() { + if ( state != nullptr ) { + Cancel(); + Wait(); + } +} + +idJobList::idJobList( idJobList &&other ) noexcept + : state( std::move( other.state ) ) { +} + +idJobList &idJobList::operator=( idJobList &&other ) noexcept { + if ( this != &other ) { + if ( state != nullptr ) { + Cancel(); + Wait(); + } + state = std::move( other.state ); + } + return *this; +} + +bool idJobList::AddJob( const idJobFunction function, void *data ) { + if ( state == nullptr || function == nullptr ) { + return false; + } + const std::shared_ptr owner = state->owner.lock(); + if ( owner == nullptr ) { + return false; + } + std::lock_guard lock( owner->mutex ); + if ( state->status != idJobListStatus::BUILDING || state->jobs.size() >= state->maxJobs ) { + return false; + } + state->jobs.push_back( { function, data } ); + return true; +} + +bool idJobList::AddDependency( const idJobList &dependency ) { + if ( state == nullptr || dependency.state == nullptr || state == dependency.state ) { + return false; + } + const std::shared_ptr owner = state->owner.lock(); + const std::shared_ptr dependencyOwner = dependency.state->owner.lock(); + if ( owner == nullptr || owner != dependencyOwner ) { + return false; + } + std::lock_guard lock( owner->mutex ); + if ( state->status != idJobListStatus::BUILDING || + state->dependencies.size() >= state->maxDependencies || + dependency.state->status == idJobListStatus::BUILDING ) { + return false; + } + if ( std::find( state->dependencies.begin(), state->dependencies.end(), dependency.state ) != state->dependencies.end() ) { + return false; + } + state->dependencies.push_back( dependency.state ); + return true; +} + +idJobSubmitResult idJobList::Submit() { + if ( state == nullptr ) { + return idJobSubmitResult::INVALID_STATE; + } + const std::shared_ptr owner = state->owner.lock(); + if ( owner == nullptr ) { + return idJobSubmitResult::SYSTEM_UNAVAILABLE; + } + bool synchronous = false; + { + std::lock_guard lock( owner->mutex ); + synchronous = owner->config.synchronous; + } + if ( synchronous ) { + return owner->SubmitSynchronous( state ); + } + return owner->SubmitThreaded( state ); +} + +bool idJobList::Cancel() { + if ( state == nullptr ) { + return false; + } + const std::shared_ptr owner = state->owner.lock(); + if ( owner == nullptr ) { + state->cancellationFlag->store( true, std::memory_order_release ); + return false; + } + std::lock_guard lock( owner->mutex ); + if ( JobStatusIsTerminal( state->status ) ) { + return false; + } + owner->RequestCancellationLocked( state ); + owner->RefreshWaitingListsLocked(); + owner->workCondition.notify_all(); + return true; +} + +bool idJobList::Wait() { + if ( state == nullptr ) { + return false; + } + const std::shared_ptr owner = state->owner.lock(); + if ( owner == nullptr ) { + return state->status == idJobListStatus::COMPLETED; + } + const std::uint64_t start = JobClockMicroseconds(); + std::unique_lock lock( owner->mutex ); + if ( state->status == idJobListStatus::BUILDING ) { + return false; + } + state->completionCondition.wait( lock, [this]() { + return JobStatusIsTerminal( state->status ); + } ); + const std::uint64_t waited = JobClockMicroseconds() - start; + state->waitTimeMicroseconds += waited; + owner->metrics.totalWaitMicroseconds += waited; + return state->status == idJobListStatus::COMPLETED; +} + +bool idJobList::TryWait() const { + return state != nullptr && JobStatusIsTerminal( GetStatus() ); +} + +idJobListStatus idJobList::GetStatus() const { + if ( state == nullptr ) { + return idJobListStatus::FAILED; + } + const std::shared_ptr owner = state->owner.lock(); + if ( owner == nullptr ) { + return state->status; + } + std::lock_guard lock( owner->mutex ); + return state->status; +} + +idJobListMetrics idJobList::GetMetrics() const { + idJobListMetrics snapshot; + if ( state == nullptr ) { + snapshot.status = idJobListStatus::FAILED; + return snapshot; + } + const std::shared_ptr owner = state->owner.lock(); + std::unique_lock lock; + if ( owner != nullptr ) { + lock = std::unique_lock( owner->mutex ); + } + snapshot.status = state->status; + snapshot.jobCapacity = state->maxJobs; + snapshot.dependencyCapacity = state->maxDependencies; + snapshot.submittedJobs = state->submitTimeMicroseconds != 0 ? state->jobs.size() : 0; + snapshot.executedJobs = state->executedJobs; + snapshot.cancelledJobs = state->cancelledJobs; + snapshot.failedJobs = state->failedJobs; + snapshot.submitTimeMicroseconds = state->submitTimeMicroseconds; + snapshot.startTimeMicroseconds = state->startTimeMicroseconds; + snapshot.finishTimeMicroseconds = state->finishTimeMicroseconds; + snapshot.waitTimeMicroseconds = state->waitTimeMicroseconds; + snapshot.executionTimeMicroseconds = state->executionTimeMicroseconds; + return snapshot; +} + +idJobCancellationToken idJobList::GetCancellationToken() const { + return state != nullptr ? idJobCancellationToken( state->cancellationFlag ) : idJobCancellationToken(); +} + +const char *idJobList::GetName() const { + return state != nullptr ? state->name.c_str() : ""; +} + +const char *idJobListStatusName( const idJobListStatus status ) { + switch ( status ) { + case idJobListStatus::BUILDING: return "building"; + case idJobListStatus::WAITING: return "waiting"; + case idJobListStatus::READY: return "ready"; + case idJobListStatus::RUNNING: return "running"; + case idJobListStatus::COMPLETED: return "completed"; + case idJobListStatus::CANCELLING: return "cancelling"; + case idJobListStatus::CANCELLED: return "cancelled"; + case idJobListStatus::FAILED: return "failed"; + } + return "unknown"; +} + +const char *idJobSubmitResultName( const idJobSubmitResult result ) { + switch ( result ) { + case idJobSubmitResult::ACCEPTED: return "accepted"; + case idJobSubmitResult::EXECUTED_SYNCHRONOUSLY: return "executed-synchronously"; + case idJobSubmitResult::QUEUE_FULL: return "queue-full"; + case idJobSubmitResult::INVALID_STATE: return "invalid-state"; + case idJobSubmitResult::INVALID_DEPENDENCY: return "invalid-dependency"; + case idJobSubmitResult::DEPENDENCY_FAILED: return "dependency-failed"; + case idJobSubmitResult::SYSTEM_UNAVAILABLE: return "system-unavailable"; + } + return "unknown"; +} + +idJobSystem jobSystem; diff --git a/src/framework/ParallelJobSystem.h b/src/framework/ParallelJobSystem.h new file mode 100644 index 00000000..c9a765be --- /dev/null +++ b/src/framework/ParallelJobSystem.h @@ -0,0 +1,203 @@ +/* +=========================================================================== + +openQ4 portable parallel job system + +The job-list architecture is informed by id Software's GPLv3 Doom 3 BFG +idParallelJobList design. This implementation is original openQ4 code: it +uses portable C++ sleepable synchronization, explicit bounds and cooperative +cancellation instead of carrying over BFG's platform-specific worker code or +spin-wait behavior. + +=========================================================================== +*/ + +#ifndef __PARALLEL_JOB_SYSTEM_H__ +#define __PARALLEL_JOB_SYSTEM_H__ + +#include +#include +#include +#include + +class idJobList; +struct idJobListState; +struct idJobSystemImpl; + +class idJobCancellationToken { +public: + idJobCancellationToken(); + + bool IsCancellationRequested() const; + explicit operator bool() const; + +private: + explicit idJobCancellationToken( const std::shared_ptr > &flag ); + + std::shared_ptr > flag; + + friend class idJobList; + friend class idJobSystem; + friend struct idJobListState; + friend struct idJobSystemImpl; +}; + +struct idJobContext { + void * data; + unsigned int workerIndex; + idJobCancellationToken cancellation; + + bool IsCancellationRequested() const { + return cancellation.IsCancellationRequested(); + } +}; + +typedef void ( *idJobFunction )( const idJobContext &context ); + +enum class idJobPriority { + LOW = 0, + NORMAL, + HIGH +}; + +enum class idJobListStatus { + BUILDING = 0, + WAITING, + READY, + RUNNING, + COMPLETED, + CANCELLING, + CANCELLED, + FAILED +}; + +enum class idJobSubmitResult { + ACCEPTED = 0, + EXECUTED_SYNCHRONOUSLY, + QUEUE_FULL, + INVALID_STATE, + INVALID_DEPENDENCY, + DEPENDENCY_FAILED, + SYSTEM_UNAVAILABLE +}; + +enum class idJobShutdownMode { + DRAIN = 0, + CANCEL_PENDING +}; + +struct idJobSystemConfig { + unsigned int workerThreads; + std::size_t maxQueuedLists; + bool synchronous; + + idJobSystemConfig(); +}; + +struct idJobListMetrics { + idJobListStatus status; + std::size_t jobCapacity; + std::size_t dependencyCapacity; + std::uint64_t submittedJobs; + std::uint64_t executedJobs; + std::uint64_t cancelledJobs; + std::uint64_t failedJobs; + std::uint64_t submitTimeMicroseconds; + std::uint64_t startTimeMicroseconds; + std::uint64_t finishTimeMicroseconds; + std::uint64_t waitTimeMicroseconds; + std::uint64_t executionTimeMicroseconds; + + idJobListMetrics(); +}; + +struct idJobSystemMetrics { + std::uint64_t submittedLists; + std::uint64_t rejectedSubmissions; + std::uint64_t completedLists; + std::uint64_t cancelledLists; + std::uint64_t failedLists; + std::uint64_t submittedJobs; + std::uint64_t executedJobs; + std::uint64_t cancelledJobs; + std::uint64_t failedJobs; + std::uint64_t workerWakeups; + std::uint64_t priorityPromotions; + std::uint64_t totalExecutionMicroseconds; + std::uint64_t totalWaitMicroseconds; + std::size_t queuedLists; + std::size_t queueHighWatermark; + std::size_t runningJobs; + std::size_t sleepingWorkers; + unsigned int workerThreads; + bool synchronous; + bool accepting; + + idJobSystemMetrics(); +}; + +class idJobSystem { +public: + idJobSystem(); + ~idJobSystem(); + + idJobSystem( const idJobSystem & ) = delete; + idJobSystem &operator=( const idJobSystem & ) = delete; + + bool Initialize( const idJobSystemConfig &config ); + void Shutdown( idJobShutdownMode mode = idJobShutdownMode::DRAIN ); + void CancelAll(); + void WaitAll(); + + bool IsInitialized() const; + bool IsSynchronous() const; + unsigned int GetWorkerThreadCount() const; + idJobSystemMetrics GetMetrics() const; + + std::unique_ptr CreateJobList( const char *name, + std::size_t maxJobs, std::size_t maxDependencies = 0, + idJobPriority priority = idJobPriority::NORMAL ); + + static unsigned int ResolveWorkerThreadCount( int requestedThreads ); + +private: + std::shared_ptr impl; + + friend class idJobList; +}; + +class idJobList { +public: + ~idJobList(); + + idJobList( const idJobList & ) = delete; + idJobList &operator=( const idJobList & ) = delete; + idJobList( idJobList &&other ) noexcept; + idJobList &operator=( idJobList &&other ) noexcept; + + bool AddJob( idJobFunction function, void *data ); + bool AddDependency( const idJobList &dependency ); + idJobSubmitResult Submit(); + bool Cancel(); + bool Wait(); + bool TryWait() const; + + idJobListStatus GetStatus() const; + idJobListMetrics GetMetrics() const; + idJobCancellationToken GetCancellationToken() const; + const char * GetName() const; + +private: + explicit idJobList( const std::shared_ptr &state ); + + std::shared_ptr state; + + friend class idJobSystem; +}; + +extern idJobSystem jobSystem; + +const char *idJobListStatusName( idJobListStatus status ); +const char *idJobSubmitResultName( idJobSubmitResult result ); + +#endif diff --git a/src/framework/RemoteCVarPolicy.h b/src/framework/RemoteCVarPolicy.h new file mode 100644 index 00000000..cbc133dd --- /dev/null +++ b/src/framework/RemoteCVarPolicy.h @@ -0,0 +1,35 @@ +/* +=========================================================================== + +openQ4 GPL Source Code +Copyright (C) 2026 the openQ4 contributors. + +This file is part of the openQ4 Source Code. See docs/legal for details. + +=========================================================================== +*/ + +#ifndef __REMOTECVARPOLICY_H__ +#define __REMOTECVARPOLICY_H__ + +// This small, dependency-free policy is shared with the native safety test. +// The caller supplies the engine's concrete CVar flag mask, keeping this +// header independent of the large CVarSystem interface and game-module ABI. +namespace idRemoteCVarPolicy { + +inline bool IsSingleAllowedAuthority( int requiredFlag, int allowedRemoteFlags ) { + return requiredFlag != 0 && + ( requiredFlag & allowedRemoteFlags ) == requiredFlag && + ( requiredFlag & ( requiredFlag - 1 ) ) == 0; +} + +inline bool CanApply( int variableFlags, int requiredFlag, int allowedRemoteFlags, + int forbiddenFlags ) { + return IsSingleAllowedAuthority( requiredFlag, allowedRemoteFlags ) && + ( variableFlags & requiredFlag ) != 0 && + ( variableFlags & forbiddenFlags ) == 0; +} + +} // namespace idRemoteCVarPolicy + +#endif /* !__REMOTECVARPOLICY_H__ */ diff --git a/src/framework/Session.cpp b/src/framework/Session.cpp index dcf69f29..993994bf 100644 --- a/src/framework/Session.cpp +++ b/src/framework/Session.cpp @@ -81,6 +81,10 @@ bool Sys_IsGameWindowFocused( void ); idSessionLocal sessLocal; idSession *session = &sessLocal; +// Implemented by FileSystem.cpp as an engine-only coordinator hook; this is +// intentionally not part of the public module-facing idFileSystem ABI. +void FS_ReleaseLevelLoadCache( void ); + static float Session_UpdateMetricAverage( float currentAverage, float sample, int sampleCount ) { const int averagingWindow = idMath::ClampInt( 1, 120, sampleCount ); if ( averagingWindow <= 1 ) { @@ -1423,6 +1427,25 @@ class sessionSaveOperationGuard_t { int previousLastCheckPoint; bool complete; }; + +class sessionRenderCropGuard_t { +public: + sessionRenderCropGuard_t( int width, int height ) : active( false ) { + if ( renderSystem != NULL && renderSystem->IsOpenGLRunning() && width > 0 && height > 0 ) { + renderSystem->CropRenderSize( width, height, false, true ); + active = true; + } + } + + ~sessionRenderCropGuard_t() { + if ( active && renderSystem != NULL ) { + renderSystem->UnCrop(); + } + } + +private: + bool active; +}; #endif void idSessionLocal::ResetFramePacingStats( void ) { @@ -2393,10 +2416,15 @@ static bool Session_PrepareExpandedLoadingBackground( const idStr &backgroundPat // Multiple isolated clients may generate the same resolution at once. Give // every publication a CSPRNG-backed staging qpath so one client cannot // truncate another client's in-progress TGA before either atomic rename. - static uint32 stagingSequence = 0; - const idStr stagingPath = va( "%s.%016llx%016llx.%u.partial", generatedPath.c_str(), + // Stage below a short root-level namespace rather than beside the final + // loadscreen. The source and destination still share fs_savepath (and thus a + // filesystem), preserving atomic cross-directory rename while leaving enough + // path budget for legacy Windows stdio under deeply nested save roots. The + // 128-bit nonce is the complete collision identity; no predictable shared + // staging name or process-local sequence is needed. + const idStr stagingPath = va( "_oq4/%016llx%016llx.tmp", static_cast( stagingNonce[0] ), - static_cast( stagingNonce[1] ), ++stagingSequence ); + static_cast( stagingNonce[1] ) ); bool published = R_WriteTGA( stagingPath.c_str(), composite.Ptr(), outputWidth, outputHeight, false, "fs_savepath" ); if ( published ) { published = fileSystem->PromoteFile( stagingPath.c_str(), generatedPath.c_str(), "fs_savepath" ); @@ -2409,6 +2437,22 @@ static bool Session_PrepareExpandedLoadingBackground( const idStr &backgroundPat fileSystem->RemoveFileChecked( stagingPath.c_str(), "fs_savepath" ); common->Warning( "Could not publish expanded loading background '%s'; using the source levelshot", generatedPath.c_str() ); + } else if ( !Session_FileExistsInSearchPaths( generatedPath.c_str() ) ) { + // A pure server deliberately excludes loose image files even when this + // client just authored them under fs_savepath. Publication alone is not + // permission to consume the file: require the active VFS policy to expose + // it before handing its name to the loading GUI/material system. Keeping + // published=false leaves the caller's stock levelshot selected. + common->DPrintf( "Expanded loading background '%s' is unavailable through the active VFS; using the source levelshot\n", + generatedPath.c_str() ); + published = false; + } else { + const idMaterial *generatedMaterial = declManager->FindMaterial( generatedPath.c_str() ); + if ( generatedMaterial == NULL || generatedMaterial->TestMaterialFlag( MF_DEFAULTED ) ) { + common->DPrintf( "Expanded loading background '%s' has no usable material; using the source levelshot\n", + generatedPath.c_str() ); + published = false; + } } R_StaticFree( centerPic ); @@ -3519,6 +3563,24 @@ static void Session_openQ4AssertMapState_f( const idCmdArgs &args ) { actualEntityFilter.c_str() ); } } + +/* +================== +Session_openQ4AssertMPGameplayView_f +================== +*/ +static void Session_openQ4AssertMPGameplayView_f( const idCmdArgs &args ) { + if ( !sessLocal.IsMapSpawned() || !sessLocal.IsMultiplayer() || + sessLocal.GetActiveGUI() != NULL ) { + common->Error( "openq4_assertMPGameplayView failed: map=%d multiplayer=%d gui=%d", + sessLocal.IsMapSpawned() ? 1 : 0, + sessLocal.IsMultiplayer() ? 1 : 0, + sessLocal.GetActiveGUI() != NULL ? 1 : 0 ); + return; + } + + common->Printf( "OPENQ4_STOCK_BASELINE_MP_CLIENT_VIEW gui=0\n" ); +} #endif /* @@ -5147,6 +5209,13 @@ Exits with mapSpawned = false =============== */ void idSessionLocal::UnloadMap() { + // A level-load generation owns worker-visible file handles and immutable + // staging buffers. Join it before any game, render-world, renderer-module, + // or filesystem state used by the outgoing map can be destroyed. + fileSystem->CancelLevelLoadCache(); + if ( renderSystem != NULL ) { + renderSystem->ResetGpuFrameTiming( "session unload" ); + } StopPlayingRenderDemo(); if ( com_showFramePacing.GetInteger() >= 2 && framePacingStats.valid ) { @@ -5189,6 +5258,54 @@ void idSessionLocal::UnloadMap() { mapSpawned = false; } +/* +=============== +Session_BuildLevelLoadCacheSettings + +Only settings that can alter source selection, decoded CPU data, image/sample +policy, or renderer-owned cache payloads belong here. Display-only settings +must not cause manifest churn. +=============== +*/ +static void Session_BuildLevelLoadCacheSettings( idStr &settings ) { + static const char *const settingNames[] = { + "com_binaryRead", + "r_renderer", + "r_actualRenderer", + "r_mergeModelSurfaces", + "r_slopVertex", + "r_slopTexCoord", + "r_slopNormal", + "r_useNewSkinning", + "r_useFastSkinning", + "r_forceConvertMD5R", + "r_convertMD5toMD5R", + "r_convertStaticToMD5R", + "r_convertProcToMD5R", + "r_pbrMaterials", + "image_downSize", + "image_downSizeLimit", + "image_downSizeSpecular", + "image_downSizeSpecularLimit", + "image_downSizeBump", + "image_downSizeBumpLimit", + "image_ignoreHighQuality", + "image_picmip", + "image_picmipFilter", + "image_picmipMinSize", + "image_usePrecompressedTextures", + "s_useCompression", + "sys_lang" + }; + settings = "level-load-settings-v2;"; + for ( unsigned int i = 0; i < sizeof( settingNames ) / sizeof( settingNames[ 0 ] ); i++ ) { + settings += settingNames[ i ]; + settings += '='; + settings += cvarSystem->GetCVarString( settingNames[ i ] ); + settings += ';'; + } +} + /* =============== idSessionLocal::LoadLoadingGui @@ -5481,6 +5598,19 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { lastCheckPoint = -1; } fileSystem->SetAssetLogName( fullMapName.c_str() ); + // Reset before BeginLevelLoadCache can publish work. Preload workers add + // their reads to this counter, so resetting after the generation starts can + // nondeterministically discard bytes already read by a worker. + fileSystem->ResetReadCount(); + idStr levelLoadCacheSettings; + Session_BuildLevelLoadCacheSettings( levelLoadCacheSettings ); +#ifdef ID_DEDICATED + const char *levelLoadGameMode = "dedicated"; +#else + const char *levelLoadGameMode = IsMultiplayer() ? "multiplayer" : "singleplayer"; +#endif + fileSystem->BeginLevelLoadCache( fullMapName.c_str(), levelLoadGameMode, + filterString.c_str(), levelLoadCacheSettings.c_str() ); if ( !reloadingSameMap && com_SingleDeclFile.GetBool() ) { declManager->FlushDecls(); @@ -5512,7 +5642,6 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { // if this works out we will probably want all the sizes in a def file although this solution will // work for new maps etc. after the first load. we can also drop the sizes into the default.cfg - fileSystem->ResetReadCount(); if ( !reloadingSameMap ) { bytesNeededForMapLoad = GetBytesNeededForMapLoad( mapString.c_str() ); } else { @@ -5541,6 +5670,7 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { int renderWorldMsec = 0; int gameInitMsec = 0; int playerSpawnMsec = 0; + int cacheJoinMsec = 0; int mediaFinishMsec = 0; int mediaRenderMsec = 0; int mediaSoundMsec = 0; @@ -5598,6 +5728,9 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { } playerSpawnMsec = Sys_Milliseconds() - phaseStart; phaseStart = Sys_Milliseconds(); + fileSystem->FinishLevelLoadCache( true ); + cacheJoinMsec = Sys_Milliseconds() - phaseStart; + phaseStart = Sys_Milliseconds(); // actually purge/load the media int mediaPhaseStart = phaseStart; @@ -5622,6 +5755,7 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { } uiManager->EndLevelLoad(); mediaUiMsec = Sys_Milliseconds() - mediaPhaseStart; + FS_ReleaseLevelLoadCache(); mediaFinishMsec = Sys_Milliseconds() - phaseStart; phaseStart = Sys_Milliseconds(); @@ -5641,10 +5775,11 @@ void idSessionLocal::ExecuteMapChange( bool noFadeWipe ) { common->Printf( "%6d msec to load %s\n", msec, mapString.c_str() ); if ( com_showLevelLoadTimes.GetBool() ) { common->Printf( - "Map load phases: renderWorld=%d gameInit=%d playerSpawn=%d mediaFinish=%d settle=%d total=%d msec\n", + "Map load phases: renderWorld=%d gameInit=%d playerSpawn=%d cacheJoin=%d mediaFinish=%d settle=%d total=%d msec\n", renderWorldMsec, gameInitMsec, playerSpawnMsec, + cacheJoinMsec, mediaFinishMsec, settleMsec, msec ); @@ -6133,14 +6268,17 @@ bool idSessionLocal::SaveGame( const char *saveName, saveType_t saveType ) { // Write screenshot if ( saveType != ST_AUTO ) { - renderSystem->CropRenderSize( 320, 240, false ); if ( rw ) { rw->PushMarkedDefs(); } + // The current crop can be smaller than the drawable in legacy + // r_screenFraction mode. Push a physical-size crop so all screen-space + // feedback targets see one coherent frame, then restore the prior crop. + sessionRenderCropGuard_t previewCrop( renderSystem->GetScreenWidth(), renderSystem->GetScreenHeight() ); common->SetRenderableGameFrame( true ); game->Draw( 0 ); - renderSystem->CaptureRenderToFile( tempPreviewFile, true ); - renderSystem->UnCrop(); + // The renderer reduces the coherent physical frame after readback. + renderSystem->CaptureRenderToFile( tempPreviewFile, true, 320, 240 ); } mapName = mapSpawnData.serverInfo.GetString( "si_map" ); @@ -7719,6 +7857,7 @@ void idSessionLocal::Init() { #ifndef ID_DEDICATED cmdSystem->AddCommand( "openq4_startSingleplayer", Session_openQ4StartSingleplayer_f, CMD_FL_SYSTEM, "internal helper to start singleplayer after game-module switches" ); cmdSystem->AddCommand( "openq4_assertMapState", Session_openQ4AssertMapState_f, CMD_FL_SYSTEM|CMD_FL_CHEAT, "asserts the active map and entity filter for validation harnesses" ); + cmdSystem->AddCommand( "openq4_assertMPGameplayView", Session_openQ4AssertMPGameplayView_f, CMD_FL_SYSTEM|CMD_FL_CHEAT, "asserts that multiplayer rendering is not covered by an active session GUI" ); cmdSystem->AddCommand( "openq4_resumeBakeLightGrids", Session_openQ4ResumeBakeLightGrids_f, CMD_FL_SYSTEM|CMD_FL_CHEAT, "internal helper to continue light-grid baking after game-module switches" ); cmdSystem->AddCommand( "iamtheduke", Session_IAmTheDuke_f, CMD_FL_SYSTEM|CMD_FL_CHEAT, "toggles the SP-only iamtheduke cheat text overlay" ); cmdSystem->AddCommand( "bakeLightGrids", Session_BakeLightGrids_f, CMD_FL_SYSTEM|CMD_FL_CHEAT, "bakes openQ4-compatible lightgrid metadata and irradiance atlases for the current map or a batch of maps" ); diff --git a/src/framework/async/AsyncClient.cpp b/src/framework/async/AsyncClient.cpp index 6007a096..1aad71ff 100644 --- a/src/framework/async/AsyncClient.cpp +++ b/src/framework/async/AsyncClient.cpp @@ -30,10 +30,54 @@ If you have questions concerning this license or the applicable additional terms #include "AsyncNetwork.h" +#include "Rcon2Protocol.h" #include "../ArenaCampaign.h" #include "../Session_local.h" #include "../../sys/NetworkEndpoint.h" +#include "../../sys/URLPolicy.h" + +#include + +static const int ASYNC_CLIENT_MAX_NETWORK_TIME = idMath::INT_MAX - 65536; +static const int ASYNC_CLIENT_MAX_PREDICTION_MSEC = 60000; + +static ID_INLINE int AsyncClient_MaxNetworkGameFrame() { + const std::int64_t msecNumerator = common->GetUserCmdMsecNumerator(); + const std::int64_t msecDenominator = common->GetUserCmdMsecDenominator(); + const std::int64_t legacyTickMsec = common->GetUserCmdMSec(); + if ( msecNumerator <= 0 || msecDenominator <= 0 || legacyTickMsec <= 0 ) { + return 0; + } + + // Both exact-tic and legacy integer-tic conversions are used below. Leave + // one frame of headroom for GetUserCmdTime( frame + 1 ) as well. + const std::int64_t exactLimit = + ( static_cast( ASYNC_CLIENT_MAX_NETWORK_TIME ) * msecDenominator ) / msecNumerator; + const std::int64_t legacyLimit = + static_cast( ASYNC_CLIENT_MAX_NETWORK_TIME ) / legacyTickMsec; + const std::int64_t maximumFrame = ( exactLimit < legacyLimit ? exactLimit : legacyLimit ) - 1; + return maximumFrame > 0 ? static_cast( maximumFrame ) : 0; +} + +static ID_INLINE bool AsyncClient_ValidNetworkTiming( int gameFrame, int gameTime ) { + return gameFrame >= 0 && gameFrame <= AsyncClient_MaxNetworkGameFrame() && + gameTime >= 0 && gameTime <= ASYNC_CLIENT_MAX_NETWORK_TIME; +} + +static void AsyncClient_StopAfterMalformedSnapshot() { + // ClientReadSnapshot may already have published timing, entities, or PVS state + // before a later field proves that the packet is malformed. Disconnecting the + // channel alone leaves that partially updated map available to the remainder of + // the outer frame, so tear down the loaded session immediately. + arenaCampaign.AbortMatch(); + session->Stop(); +} + +static ID_INLINE int AsyncClient_MaxPredictionMsec() { + return idMath::ClampInt( 0, ASYNC_CLIENT_MAX_PREDICTION_MSEC, + idAsyncNetwork::clientMaxPrediction.GetInteger() ); +} static ID_INLINE int AsyncClient_NextGameFrameMsec( int gameFrame ) { return common->GetUserCmdDeltaMsec( gameFrame + 1 ); @@ -42,11 +86,35 @@ static ID_INLINE int AsyncClient_NextGameFrameMsec( int gameFrame ) { static ID_INLINE int AsyncClient_ConfiguredPredictionMsec( int gameFrame ) { // Preserve the legacy default as "one exact tic" instead of a literal 16 ms. if ( idAsyncNetwork::clientPrediction.GetInteger() == 16 ) { - return AsyncClient_NextGameFrameMsec( gameFrame ); + return Min( AsyncClient_NextGameFrameMsec( gameFrame ), AsyncClient_MaxPredictionMsec() ); } - return Max( 0, idAsyncNetwork::clientPrediction.GetInteger() ); + return idMath::ClampInt( 0, AsyncClient_MaxPredictionMsec(), + idAsyncNetwork::clientPrediction.GetInteger() ); +} + +static ID_INLINE bool AsyncClient_SameEndpoint( const netadr_t &left, const netadr_t &right ) { + return left.port == right.port && Sys_CompareNetAdrBase( left, right ); } +static ID_INLINE std::uint32_t AsyncClient_Elapsed( int now, int then ) { + return static_cast( now ) - static_cast( then ); +} + +static bool AsyncClient_SecureConnectionId( int &identifier ) { + std::uint32_t randomValue = 0; + if ( !Sys_GetSecureRandomBytes( &randomValue, sizeof( randomValue ) ) ) { + return false; + } + identifier = static_cast( randomValue & CONNECTIONLESS_MESSAGE_ID_MASK ); + if ( identifier == CONNECTIONLESS_MESSAGE_ID_MASK ) { + identifier = 0; + } + return true; +} + +static const int RCON2_CLIENT_TIMEOUT_MSEC = 10000; +static const int RCON2_CLIENT_RESEND_MSEC = 1000; + static bool AsyncClient_IsWindowsDevicePathSegment( const char *segment, int segmentLength ) { int stemLength = 0; while ( stemLength < segmentLength && segment[ stemLength ] != '.' ) { @@ -179,6 +247,9 @@ void idAsyncClient::Clear( void ) { backgroundDownload.completed = true; backgroundDownload.url.expectedSize = 0; lastRconTime = 0; + memset( &lastRconAddress, 0, sizeof( lastRconAddress ) ); + idCrypto::SecureZero( &rcon2Request, sizeof( rcon2Request ) ); + rcon2Request.state = RCON_REPLY_NONE; showUpdateMessage = false; lastFrameDelta = 0; @@ -197,9 +268,6 @@ idAsyncClient::Shutdown void idAsyncClient::Shutdown( void ) { guiNetMenu = NULL; updateMSG.Clear(); - updateURL.Clear(); - updateFile.Clear(); - updateFallback.Clear(); backgroundDownload.url.url.Clear(); dlList.Clear(); } @@ -229,6 +297,7 @@ idAsyncClient::ClosePort ================== */ void idAsyncClient::ClosePort( void ) { + ClearRemoteConsoleRequest(); clientPort.Close(); } @@ -302,8 +371,11 @@ void idAsyncClient::ConnectToServer( const netadr_t adr ) { // clear the client state Clear(); - // get a pseudo random client id, but don't use the id which is reserved for connectionless packets - clientId = Sys_Milliseconds() & CONNECTIONLESS_MESSAGE_ID_MASK; + // Keep the legacy 15-bit wire field but remove the predictable clock seed. + if ( !AsyncClient_SecureConnectionId( clientId ) ) { + common->Warning( "OS secure random unavailable; connection attempt cancelled" ); + return; + } // calculate a checksum on some of the essential data used clientDataChecksum = declManager->GetChecksum(); @@ -578,12 +650,14 @@ idAsyncClient::RemoteConsole */ void idAsyncClient::RemoteConsole( const char *command ) { netadr_t adr; - idBitMsg msg; - byte msgBuf[MAX_MESSAGE_SIZE]; if ( !InitPort() ) { return; } + if ( command == NULL || command[0] == '\0' || strlen( command ) >= MAX_STRING_CHARS ) { + common->Printf( "usage: rcon (maximum %d bytes)\n", MAX_STRING_CHARS - 1 ); + return; + } if ( active ) { adr = serverAddress; @@ -599,16 +673,205 @@ void idAsyncClient::RemoteConsole( const char *command ) { adr.port = PORT_SERVER; } + ClearRemoteConsoleRequest(); lastRconAddress = adr; lastRconTime = realTime; + const char *password = idAsyncNetwork::clientRemoteConsolePassword.GetString(); + if ( password[0] == '\0' ) { + common->Printf( "Set net_clientRemoteConsolePassword before using rcon.\n" ); + ClearRemoteConsoleRequest(); + return; + } + + if ( idAsyncNetwork::clientUseLegacyRcon.GetBool() ) { + byte msgBuf[MAX_MESSAGE_SIZE]; + idBitMsg msg; + msg.Init( msgBuf, sizeof( msgBuf ) ); + msg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); + msg.WriteString( "rcon" ); + msg.WriteString( password ); + msg.WriteString( command ); + rcon2Request.state = RCON_REPLY_LEGACY; + rcon2Request.address = adr; + rcon2Request.startTime = realTime; + common->Warning( "sending legacy rcon password as plaintext because net_clientUseLegacyRcon is enabled" ); + clientPort.SendPacket( adr, msg.GetData(), msg.GetSize() ); + idCrypto::SecureZero( msgBuf, sizeof( msgBuf ) ); + return; + } + + if ( strlen( password ) < idRcon2::MIN_PASSWORD_BYTES ) { + common->Printf( "rcon2 requires a password of at least %u bytes.\n", + static_cast( idRcon2::MIN_PASSWORD_BYTES ) ); + ClearRemoteConsoleRequest(); + return; + } + + rcon2Request.state = RCON_REPLY_CHALLENGE; + rcon2Request.address = adr; + rcon2Request.startTime = realTime; + rcon2Request.lastSendTime = realTime; + idStr::Copynz( rcon2Request.command, command, sizeof( rcon2Request.command ) ); + if ( !Sys_GetSecureRandomBytes( rcon2Request.clientNonce, sizeof( rcon2Request.clientNonce ) ) ) { + common->Warning( "OS secure random unavailable; rcon2 request cancelled" ); + ClearRemoteConsoleRequest(); + return; + } + idRcon2::HashRequest( rcon2Request.command, rcon2Request.requestDigest ); + SendRemoteConsole2Challenge(); +} + +/* +================== +idAsyncClient::ClearRemoteConsoleRequest +================== +*/ +void idAsyncClient::ClearRemoteConsoleRequest( void ) { + idCrypto::SecureZero( &rcon2Request, sizeof( rcon2Request ) ); + rcon2Request.state = RCON_REPLY_NONE; + memset( &lastRconAddress, 0, sizeof( lastRconAddress ) ); + lastRconTime = 0; +} + +void idAsyncClient::SendRemoteConsole2Challenge( void ) { + if ( rcon2Request.state != RCON_REPLY_CHALLENGE ) { + return; + } + byte msgBuf[128]; + idBitMsg msg; + msg.Init( msgBuf, sizeof( msgBuf ) ); + msg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); + msg.WriteString( "rcon2Challenge" ); + msg.WriteByte( idRcon2::PROTOCOL_VERSION ); + msg.WriteData( rcon2Request.clientNonce, sizeof( rcon2Request.clientNonce ) ); + msg.WriteData( rcon2Request.requestDigest, sizeof( rcon2Request.requestDigest ) ); + clientPort.SendPacket( rcon2Request.address, msg.GetData(), msg.GetSize() ); + rcon2Request.lastSendTime = realTime; + lastRconTime = realTime; + idCrypto::SecureZero( msgBuf, sizeof( msgBuf ) ); +} + +void idAsyncClient::SendRemoteConsole2Proof( void ) { + if ( rcon2Request.state != RCON_REPLY_OUTPUT ) { + return; + } + byte msgBuf[MAX_MESSAGE_SIZE]; + idBitMsg msg; msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); - msg.WriteString( "rcon" ); - msg.WriteString( idAsyncNetwork::clientRemoteConsolePassword.GetString() ); - msg.WriteString( command ); + msg.WriteString( "rcon2" ); + msg.WriteByte( idRcon2::PROTOCOL_VERSION ); + msg.WriteData( rcon2Request.clientNonce, sizeof( rcon2Request.clientNonce ) ); + msg.WriteData( rcon2Request.serverNonce, sizeof( rcon2Request.serverNonce ) ); + msg.WriteString( rcon2Request.command ); + msg.WriteData( rcon2Request.proof, sizeof( rcon2Request.proof ) ); + clientPort.SendPacket( rcon2Request.address, msg.GetData(), msg.GetSize() ); + rcon2Request.lastSendTime = realTime; + lastRconTime = realTime; + idCrypto::SecureZero( msgBuf, sizeof( msgBuf ) ); +} - clientPort.SendPacket( adr, msg.GetData(), msg.GetSize() ); +void idAsyncClient::UpdateRemoteConsoleRequest( void ) { + if ( rcon2Request.state == RCON_REPLY_NONE ) { + return; + } + if ( AsyncClient_Elapsed( realTime, rcon2Request.startTime ) > RCON2_CLIENT_TIMEOUT_MSEC ) { + common->Printf( "remote console request timed out\n" ); + ClearRemoteConsoleRequest(); + return; + } + if ( rcon2Request.state == RCON_REPLY_LEGACY ) { + return; + } + if ( AsyncClient_Elapsed( realTime, rcon2Request.lastSendTime ) < RCON2_CLIENT_RESEND_MSEC ) { + return; + } + if ( rcon2Request.state == RCON_REPLY_CHALLENGE ) { + SendRemoteConsole2Challenge(); + } else if ( rcon2Request.state == RCON_REPLY_OUTPUT ) { + SendRemoteConsole2Proof(); + } +} + +/* +================== +idAsyncClient::ProcessRemoteConsole2ChallengeResponse +================== +*/ +void idAsyncClient::ProcessRemoteConsole2ChallengeResponse( const netadr_t from, const idBitMsg &msg ) { + const int responseBytes = 1 + idRcon2::NONCE_BYTES + idRcon2::NONCE_BYTES + + idRcon2::SALT_BYTES + 4 + idRcon2::ENDPOINT_BINDING_BYTES + idRcon2::REQUEST_DIGEST_BYTES; + if ( rcon2Request.state != RCON_REPLY_CHALLENGE || + !AsyncClient_SameEndpoint( from, rcon2Request.address ) || + msg.GetRemainingData() != responseBytes || + msg.ReadByte() != idRcon2::PROTOCOL_VERSION ) { + return; + } + byte clientNonce[idRcon2::NONCE_BYTES]; + byte serverNonce[idRcon2::NONCE_BYTES]; + byte salt[idRcon2::SALT_BYTES]; + byte endpointBinding[idRcon2::ENDPOINT_BINDING_BYTES]; + byte requestDigest[idRcon2::REQUEST_DIGEST_BYTES]; + byte verifier[idRcon2::VERIFIER_BYTES]; + msg.ReadData( clientNonce, sizeof( clientNonce ) ); + msg.ReadData( serverNonce, sizeof( serverNonce ) ); + msg.ReadData( salt, sizeof( salt ) ); + const int iterations = msg.ReadLong(); + msg.ReadData( endpointBinding, sizeof( endpointBinding ) ); + msg.ReadData( requestDigest, sizeof( requestDigest ) ); + + const bool responseMatches = iterations == static_cast( idRcon2::PBKDF2_ITERATIONS ) && + idCrypto::ConstantTimeEquals( clientNonce, rcon2Request.clientNonce, sizeof( clientNonce ) ) && + idCrypto::ConstantTimeEquals( requestDigest, rcon2Request.requestDigest, sizeof( requestDigest ) ); + if ( !responseMatches || !idRcon2::DeriveVerifier( + idAsyncNetwork::clientRemoteConsolePassword.GetString(), salt, verifier ) ) { + common->Warning( "invalid rcon2 challenge response" ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); + idCrypto::SecureZero( salt, sizeof( salt ) ); + idCrypto::SecureZero( endpointBinding, sizeof( endpointBinding ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); + idCrypto::SecureZero( verifier, sizeof( verifier ) ); + ClearRemoteConsoleRequest(); + return; + } + + memcpy( rcon2Request.serverNonce, serverNonce, sizeof( rcon2Request.serverNonce ) ); + idRcon2::ComputeProof( verifier, rcon2Request.clientNonce, serverNonce, + endpointBinding, rcon2Request.requestDigest, rcon2Request.proof ); + rcon2Request.state = RCON_REPLY_OUTPUT; + rcon2Request.lastSendTime = realTime; + SendRemoteConsole2Proof(); + + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); + idCrypto::SecureZero( salt, sizeof( salt ) ); + idCrypto::SecureZero( endpointBinding, sizeof( endpointBinding ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); + idCrypto::SecureZero( verifier, sizeof( verifier ) ); +} + +void idAsyncClient::ProcessRemoteConsole2Complete( const netadr_t from, const idBitMsg &msg ) { + const int completeBytes = 1 + idRcon2::NONCE_BYTES + idRcon2::NONCE_BYTES; + if ( rcon2Request.state != RCON_REPLY_OUTPUT || + !AsyncClient_SameEndpoint( from, rcon2Request.address ) || + msg.GetRemainingData() != completeBytes || + msg.ReadByte() != idRcon2::PROTOCOL_VERSION ) { + return; + } + byte clientNonce[idRcon2::NONCE_BYTES]; + byte serverNonce[idRcon2::NONCE_BYTES]; + msg.ReadData( clientNonce, sizeof( clientNonce ) ); + msg.ReadData( serverNonce, sizeof( serverNonce ) ); + const bool matches = idCrypto::ConstantTimeEquals( clientNonce, + rcon2Request.clientNonce, sizeof( clientNonce ) ) && + idCrypto::ConstantTimeEquals( serverNonce, rcon2Request.serverNonce, sizeof( serverNonce ) ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); + if ( matches ) { + ClearRemoteConsoleRequest(); + } } /* @@ -835,6 +1098,10 @@ void idAsyncClient::SendUsercmdsToServer( void ) { if ( idAsyncNetwork::verbose.GetInteger() == 2 ) { common->Printf( "sending usercmd to server: gameInitId = %d, gameFrame = %d, gameTime = %d\n", gameInitId, gameFrame, gameTime ); } + if ( gameFrame < 0 || gameFrame > AsyncClient_MaxNetworkGameFrame() ) { + common->Warning( "cannot send a user command for invalid game frame %d", gameFrame ); + return; + } // generate user command for this client index = gameFrame & ( MAX_USERCMD_BACKUP - 1 ); @@ -850,7 +1117,9 @@ void idAsyncClient::SendUsercmdsToServer( void ) { msg.WriteByte( CLIENT_UNRELIABLE_MESSAGE_USERCMD ); msg.WriteShort( clientPrediction ); - numUsercmds = idMath::ClampInt( 0, 10, idAsyncNetwork::clientUsercmdBackup.GetInteger() ) + 1; + const int requestedUsercmds = idMath::ClampInt( 0, MAX_USERCMD_PACKET_COMMANDS - 1, + idAsyncNetwork::clientUsercmdBackup.GetInteger() ) + 1; + numUsercmds = gameFrame >= requestedUsercmds - 1 ? requestedUsercmds : gameFrame + 1; // write the user commands msg.WriteLong( gameFrame ); @@ -898,6 +1167,11 @@ void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) { usercmd_t *last; bool pureWait; + if ( msg.GetRemainingReadBits() < 32 + 8 ) { + common->Warning( "server sent a truncated unreliable message; disconnecting safely" ); + DisconnectFromServer(); + return; + } serverGameInitId = msg.ReadLong(); id = msg.ReadByte(); @@ -912,13 +1186,28 @@ void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) { if ( idAsyncNetwork::verbose.GetInteger() == 2 ) { common->Printf( "received ping message from server\n" ); } + if ( msg.GetRemainingReadBits() < 32 ) { + common->Warning( "server sent a truncated ping; disconnecting safely" ); + DisconnectFromServer(); + return; + } SendPingResponseToServer( msg.ReadLong() ); break; } case SERVER_UNRELIABLE_MESSAGE_GAMEINIT: { + if ( msg.GetRemainingReadBits() < 32 + 32 ) { + common->Warning( "server sent a truncated game-init message; disconnecting safely" ); + DisconnectFromServer(); + return; + } serverGameFrame = msg.ReadLong(); serverGameTime = msg.ReadLong(); msg.ReadDeltaDict( serverSI, NULL ); + if ( msg.IsReadOverflowed() || !AsyncClient_ValidNetworkTiming( serverGameFrame, serverGameTime ) ) { + common->Warning( "server sent invalid game-init timing; disconnecting safely" ); + DisconnectFromServer(); + return; + } pureWait = serverSI.GetBool( "si_pure" ); InitGame( serverGameInitId, serverGameFrame, serverGameTime, serverSI ); @@ -956,11 +1245,25 @@ void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) { break; } - snapshotSequence = msg.ReadLong(); - snapshotGameFrame = msg.ReadLong(); - snapshotGameTime = msg.ReadLong(); + if ( msg.GetRemainingReadBits() < 32 + 32 + 32 + 8 + 16 ) { + common->Warning( "server sent a truncated snapshot header; disconnecting safely" ); + DisconnectFromServer(); + return; + } + const int receivedSnapshotSequence = msg.ReadLong(); + const int receivedSnapshotGameFrame = msg.ReadLong(); + const int receivedSnapshotGameTime = msg.ReadLong(); numDuplicatedUsercmds = msg.ReadByte(); aheadOfServer = msg.ReadShort(); + if ( msg.IsReadOverflowed() || + !AsyncClient_ValidNetworkTiming( receivedSnapshotGameFrame, receivedSnapshotGameTime ) ) { + common->Warning( "server sent invalid snapshot timing; disconnecting safely" ); + DisconnectFromServer(); + return; + } + snapshotSequence = receivedSnapshotSequence; + snapshotGameFrame = receivedSnapshotGameFrame; + snapshotGameTime = receivedSnapshotGameTime; // read the game snapshot if ( !game->ClientReadSnapshot( @@ -968,20 +1271,38 @@ void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) { numDuplicatedUsercmds, aheadOfServer, msg ) ) { common->Warning( "server sent malformed snapshot %d; disconnecting safely", snapshotSequence ); - DisconnectFromServer(); + AsyncClient_StopAfterMalformedSnapshot(); return; } // read user commands of other clients from the snapshot - for ( last = NULL, i = msg.ReadByte(); i < MAX_ASYNC_CLIENTS; i = msg.ReadByte() ) { - numUsercmds = msg.ReadByte(); - if ( numUsercmds > MAX_USERCMD_RELAY ) { - common->Error( "snapshot %d contains too many user commands for client %d", snapshotSequence, i ); + last = NULL; + while ( true ) { + i = msg.ReadByte(); + if ( msg.IsReadOverflowed() || i > MAX_ASYNC_CLIENTS ) { + common->Warning( "snapshot %d has an invalid user-command terminator; disconnecting safely", + snapshotSequence ); + AsyncClient_StopAfterMalformedSnapshot(); + return; + } + if ( i == MAX_ASYNC_CLIENTS ) { break; } + numUsercmds = msg.ReadByte(); + if ( msg.IsReadOverflowed() || numUsercmds < 1 || numUsercmds > MAX_USERCMD_RELAY ) { + common->Warning( "snapshot %d contains an invalid user-command count for client %d; disconnecting safely", + snapshotSequence, i ); + AsyncClient_StopAfterMalformedSnapshot(); + return; + } for ( j = 0; j < numUsercmds; j++ ) { index = ( snapshotGameFrame + j ) & ( MAX_USERCMD_BACKUP - 1 ); - idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][i], last ); + if ( !idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][i], last ) ) { + common->Warning( "snapshot %d contains a truncated user command for client %d; disconnecting safely", + snapshotSequence, i ); + AsyncClient_StopAfterMalformedSnapshot(); + return; + } userCmds[index][i].gameFrame = snapshotGameFrame + j; userCmds[index][i].duplicateCount = 0; last = &userCmds[index][i]; @@ -1005,19 +1326,24 @@ void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) { } // if the snapshot is newer than the clients current game time - if ( gameTime < snapshotGameTime || gameTime > snapshotGameTime + idAsyncNetwork::clientMaxPrediction.GetInteger() ) { + const int maximumPredictionMsec = AsyncClient_MaxPredictionMsec(); + if ( gameTime < snapshotGameTime || gameTime > snapshotGameTime + maximumPredictionMsec ) { gameFrame = snapshotGameFrame; gameTime = snapshotGameTime; - gameTimeResidual = idMath::ClampInt( -idAsyncNetwork::clientMaxPrediction.GetInteger(), idAsyncNetwork::clientMaxPrediction.GetInteger(), gameTimeResidual ); - clientPredictTime = idMath::ClampInt( -idAsyncNetwork::clientMaxPrediction.GetInteger(), idAsyncNetwork::clientMaxPrediction.GetInteger(), clientPredictTime ); + gameTimeResidual = idMath::ClampInt( -maximumPredictionMsec, maximumPredictionMsec, gameTimeResidual ); + clientPredictTime = idMath::ClampInt( -maximumPredictionMsec, maximumPredictionMsec, clientPredictTime ); } // adjust the client prediction time based on the snapshot time const int configuredPredictionMsec = AsyncClient_ConfiguredPredictionMsec( gameFrame ); clientPrediction -= ( 1 - ( INTSIGNBITSET( aheadOfServer - configuredPredictionMsec ) << 1 ) ); - clientPrediction = idMath::ClampInt( configuredPredictionMsec, idAsyncNetwork::clientMaxPrediction.GetInteger(), clientPrediction ); + clientPrediction = idMath::ClampInt( configuredPredictionMsec, maximumPredictionMsec, clientPrediction ); delta = gameTime - ( snapshotGameTime + clientPrediction ); - clientPredictTime -= ( delta / PREDICTION_FAST_ADJUST ) + ( 1 - ( INTSIGNBITSET( delta ) << 1 ) ); + const std::int64_t adjustedPredictTime = static_cast( clientPredictTime ) - + ( delta / PREDICTION_FAST_ADJUST ) - ( 1 - ( INTSIGNBITSET( delta ) << 1 ) ); + clientPredictTime = adjustedPredictTime < -maximumPredictionMsec ? -maximumPredictionMsec : + ( adjustedPredictTime > maximumPredictionMsec ? maximumPredictionMsec : + static_cast( adjustedPredictTime ) ); lastSnapshotTime = clientTime; @@ -1126,6 +1452,11 @@ void idAsyncClient::ProcessReliableServerMessages( void ) { while ( channel.GetReliableMessage( msg ) ) { id = msg.ReadByte(); + if ( msg.IsReadOverflowed() ) { + common->Warning( "server sent an empty reliable message; disconnecting safely" ); + DisconnectFromServer(); + return; + } switch( id ) { case SERVER_RELIABLE_MESSAGE_CLIENTINFO: { int clientNum; @@ -1133,13 +1464,19 @@ void idAsyncClient::ProcessReliableServerMessages( void ) { // openQ4: wire value, so it indexes nothing until it is known to be a // client slot. userInfo only has MAX_ASYNC_CLIENTS entries. - if ( clientNum < 0 || clientNum >= MAX_ASYNC_CLIENTS ) { + if ( msg.IsReadOverflowed() || clientNum < 0 || clientNum >= MAX_ASYNC_CLIENTS ) { common->Warning( "SERVER_RELIABLE_MESSAGE_CLIENTINFO: bad client number %d, ignored", clientNum ); - break; + DisconnectFromServer(); + return; } idDict &info = sessLocal.mapSpawnData.userInfo[ clientNum ]; bool haveBase = ( msg.ReadBits( 1 ) != 0 ); + if ( msg.IsReadOverflowed() ) { + common->Warning( "SERVER_RELIABLE_MESSAGE_CLIENTINFO: truncated base flag" ); + DisconnectFromServer(); + return; + } #if ID_CLIENTINFO_TAGS int checksum = info.Checksum(); @@ -1157,11 +1494,16 @@ void idAsyncClient::ProcessReliableServerMessages( void ) { } else { msg.ReadDeltaDict( info, NULL ); } + if ( msg.IsReadOverflowed() ) { + common->Warning( "SERVER_RELIABLE_MESSAGE_CLIENTINFO: malformed userinfo dictionary" ); + DisconnectFromServer(); + return; + } // server forces us to a different userinfo if ( clientNum == idAsyncClient::clientNum ) { common->DPrintf( "local user info modified by server\n" ); - cvarSystem->SetCVarsFromDict( info ); + cvarSystem->SetCVarsFromDictByFlags( info, CVAR_USERINFO ); cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); // don't emit back } game->SetUserInfo( clientNum, info, true ); @@ -1170,7 +1512,12 @@ void idAsyncClient::ProcessReliableServerMessages( void ) { case SERVER_RELIABLE_MESSAGE_SYNCEDCVARS: { idDict &info = sessLocal.mapSpawnData.syncedCVars; msg.ReadDeltaDict( info, &info ); - cvarSystem->SetCVarsFromDict( info ); + if ( msg.IsReadOverflowed() ) { + common->Warning( "SERVER_RELIABLE_MESSAGE_SYNCEDCVARS: malformed CVar dictionary" ); + DisconnectFromServer(); + return; + } + cvarSystem->SetCVarsFromDictByFlags( info, CVAR_NETWORKSYNC ); if ( !idAsyncNetwork::AreCheatsEnabled() ) { cvarSystem->ResetFlaggedVariables( CVAR_CHEAT ); } @@ -1299,7 +1646,7 @@ void idAsyncClient::ProcessChallengeResponseMessage( const netadr_t from, const return; } - common->Printf( "received challenge response 0x%x from %s\n", serverChallenge, Sys_NetAdrToString( from ) ); + common->DPrintf( "received connection challenge response from %s\n", Sys_NetAdrToString( from ) ); // start sending connect packets instead of challenge request packets clientState = CS_CONNECTING; @@ -1333,35 +1680,42 @@ void idAsyncClient::ProcessConnectResponseMessage( const netadr_t from, const id return; } - common->Printf( "received connect response from %s\n", Sys_NetAdrToString( from ) ); - + if ( msg.GetRemainingReadBits() < 32 + 32 + 32 + 32 ) { + common->Warning( "server sent a truncated connect response - aborting the connection" ); + cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" ); + return; + } const int serverClientNum = msg.ReadLong(); + serverGameInitId = msg.ReadLong(); + serverGameFrame = msg.ReadLong(); + serverGameTime = msg.ReadLong(); + msg.ReadDeltaDict( serverSI, NULL ); // openQ4: this wire value ends up indexing userInfo and userCmds for the whole // session, so refuse the connection outright rather than accept a bad slot. - if ( serverClientNum < 0 || serverClientNum >= MAX_ASYNC_CLIENTS ) { - common->Warning( "connect response assigned client number %d, out of range - aborting the connection", serverClientNum ); + if ( msg.IsReadOverflowed() || serverClientNum < 0 || serverClientNum >= MAX_ASYNC_CLIENTS || + !AsyncClient_ValidNetworkTiming( serverGameFrame, serverGameTime ) ) { + common->Warning( "server sent a malformed connect response - aborting the connection" ); cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" ); return; } + common->Printf( "received connect response from %s\n", Sys_NetAdrToString( from ) ); channel.Init( from, clientId ); clientNum = serverClientNum; clientState = CS_CONNECTED; lastPacketTime = -9999; - serverGameInitId = msg.ReadLong(); - serverGameFrame = msg.ReadLong(); - serverGameTime = msg.ReadLong(); - msg.ReadDeltaDict( serverSI, NULL ); - InitGame( serverGameInitId, serverGameFrame, serverGameTime, serverSI ); // load map session->SetGUI( NULL, NULL ); sessLocal.ExecuteMapChange(); - clientPredictTime = clientPrediction = idMath::ClampInt( 0, idAsyncNetwork::clientMaxPrediction.GetInteger(), clientTime - lastConnectTime ); + const std::uint32_t connectElapsed = AsyncClient_Elapsed( clientTime, lastConnectTime ); + const std::uint32_t maximumPrediction = static_cast( AsyncClient_MaxPredictionMsec() ); + clientPredictTime = clientPrediction = static_cast( + connectElapsed < maximumPrediction ? connectElapsed : maximumPrediction ); } /* @@ -1374,7 +1728,7 @@ void idAsyncClient::ProcessDisconnectMessage( const netadr_t from, const idBitMs common->Printf( "Disconnect packet while not connected.\n" ); return; } - if ( !Sys_CompareNetAdrBase( from, serverAddress ) ) { + if ( !AsyncClient_SameEndpoint( from, serverAddress ) ) { common->Printf( "Disconnect packet from unknown server.\n" ); return; } @@ -1585,22 +1939,27 @@ idAsyncClient::ProcessVersionMessage ================== */ void idAsyncClient::ProcessVersionMessage( const netadr_t from, const idBitMsg &msg ) { - char string[ MAX_STRING_CHARS ]; + char ignoredNetworkField[ MAX_STRING_CHARS ]; + (void)from; if ( updateState != UPDATE_SENT ) { common->Printf( "ProcessVersionMessage: version reply, != UPDATE_SENT\n" ); return; } + // Consume the complete legacy payload so old master servers remain wire + // compatible. The message, direct-download flag, URL, MIME action, and + // fallback URL are intentionally ignored: an unauthenticated datagram must + // never supply instructions or choose what the client downloads, opens, or + // executes. + msg.ReadString( ignoredNetworkField, sizeof( ignoredNetworkField ) ); + (void)msg.ReadByte(); + msg.ReadString( ignoredNetworkField, sizeof( ignoredNetworkField ) ); + (void)msg.ReadByte(); + msg.ReadString( ignoredNetworkField, sizeof( ignoredNetworkField ) ); + + updateMSG = common->GetLanguageDict()->GetString( "#str_104330" ); common->Printf( "A new version is available\n" ); - msg.ReadString( string, MAX_STRING_CHARS ); - updateMSG = string; - updateDirectDownload = ( msg.ReadByte() != 0 ); - msg.ReadString( string, MAX_STRING_CHARS ); - updateURL = string; - updateMime = (dlMime_t)msg.ReadByte(); - msg.ReadString( string, MAX_STRING_CHARS ); - updateFallback = string; updateState = UPDATE_READY; } @@ -1679,7 +2038,10 @@ bool idAsyncClient::ValidatePureServerChecksums( const netadr_t from, const idBi common->DPrintf( "game code pak: 0x%x\n", missingGamePakChecksum ); } // store the requested downloads - GetDownloadRequest( missingChecksums, numMissingChecksums, missingGamePakChecksum ); + if ( GetDownloadRequest( missingChecksums, numMissingChecksums, missingGamePakChecksum ) == -1 ) { + common->Warning( "OS secure random unavailable; download request cancelled" ); + return false; + } // build the download request message // NOTE: in a specific function? dlmsg.Init( msgBuf, sizeof( msgBuf ) ); @@ -1789,9 +2151,51 @@ void idAsyncClient::ConnectionlessMessage( const netadr_t from, const idBitMsg & } } - // ignore if not from the current/last server - if ( !Sys_CompareNetAdrBase( from, serverAddress ) && ( lastRconTime + 10000 < realTime || !Sys_CompareNetAdrBase( from, lastRconAddress ) ) ) { - common->DPrintf( "got message '%s' from bad source: %s\n", string, Sys_NetAdrToString( from ) ); + const bool fromCurrentServer = active && AsyncClient_SameEndpoint( from, serverAddress ); + const bool fromPendingRcon = rcon2Request.state != RCON_REPLY_NONE && + AsyncClient_Elapsed( realTime, rcon2Request.startTime ) <= RCON2_CLIENT_TIMEOUT_MSEC && + AsyncClient_SameEndpoint( from, rcon2Request.address ); + const bool fromPendingRconOutput = fromPendingRcon && + ( rcon2Request.state == RCON_REPLY_OUTPUT || rcon2Request.state == RCON_REPLY_LEGACY ); + + // Remote-console opcodes have their own exact-endpoint capability. A + // pending rcon request must not authorize this endpoint to inject game + // control messages, and the game server must not manufacture rcon replies + // when no matching request is pending. + if ( idStr::Icmp( string, "rcon2ChallengeResponse" ) == 0 ) { + if ( !fromPendingRcon ) { + common->DPrintf( "got rcon2 challenge response from bad source: %s\n", Sys_NetAdrToString( from ) ); + return; + } + ProcessRemoteConsole2ChallengeResponse( from, msg ); + return; + } + if ( idStr::Icmp( string, "rcon2Complete" ) == 0 ) { + if ( !fromPendingRcon ) { + common->DPrintf( "got rcon2 completion from bad source: %s\n", Sys_NetAdrToString( from ) ); + return; + } + ProcessRemoteConsole2Complete( from, msg ); + return; + } + if ( idStr::Icmp( string, "print" ) == 0 ) { + // A challenge request has not authorized an output window yet. Secure + // rcon2 opens it only after the proof is sent; legacy mode keeps its + // explicitly insecure bounded window for compatibility. Ordinary game + // prints remain tied to the exact current-server endpoint. + if ( !fromCurrentServer && !fromPendingRconOutput ) { + common->DPrintf( "got print from bad source: %s\n", Sys_NetAdrToString( from ) ); + return; + } + ProcessPrintMessage( from, msg ); + return; + } + + // Everything below is game-session control and therefore requires the + // exact current server endpoint. Merely owning an rcon reply window is not + // sufficient authority. + if ( !fromCurrentServer ) { + common->DPrintf( "got game control message '%s' from bad source: %s\n", string, Sys_NetAdrToString( from ) ); return; } @@ -1814,12 +2218,6 @@ void idAsyncClient::ConnectionlessMessage( const netadr_t from, const idBitMsg & return; } - // print request from server - if ( idStr::Icmp( string, "print" ) == 0 ) { - ProcessPrintMessage( from, msg ); - return; - } - // server pure list if ( idStr::Icmp( string, "pureServer" ) == 0 ) { ProcessPureMessage( from, msg ); @@ -1828,6 +2226,7 @@ void idAsyncClient::ConnectionlessMessage( const netadr_t from, const idBitMsg & if ( idStr::Icmp( string, "downloadInfo" ) == 0 ) { ProcessDownloadInfoMessage( from, msg ); + return; } common->DPrintf( "ignored message from %s: %s\n", Sys_NetAdrToString( from ), string ); @@ -1894,7 +2293,7 @@ void idAsyncClient::SetupConnection( void ) { msg.WriteLong( clientId ); clientPort.SendPacket( serverAddress, msg.GetData(), msg.GetSize() ); } else if ( clientState == CS_CONNECTING ) { - common->Printf( "sending connect to %s with challenge 0x%x\n", Sys_NetAdrToString( serverAddress ), serverChallenge ); + common->DPrintf( "sending authenticated connect request to %s\n", Sys_NetAdrToString( serverAddress ) ); msg.Init( msgBuf, sizeof( msgBuf ) ); msg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); msg.WriteString( "connect" ); @@ -2001,6 +2400,7 @@ void idAsyncClient::RunFrame( bool allowBlocking ) { if ( !clientPort.GetPort() ) { return; } + UpdateRemoteConsoleRequest(); // handle ongoing pk4 downloads and patch downloads HandleDownloads(); @@ -2160,7 +2560,9 @@ void idAsyncClient::SendVersionCheck( bool fromMenu ) { msg.WriteLong( ASYNC_PROTOCOL_VERSION ); msg.WriteShort( BUILD_OS_ID ); msg.WriteString( cvarSystem->GetCVarString( "si_version" ) ); - msg.WriteString( cvarSystem->GetCVarString( "com_guid" ) ); + // Retain the legacy field position without leaking the persistent client + // identifier to the unauthenticated UDP update service. + msg.WriteString( "" ); clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() ); common->DPrintf( "sent a version check request\n" ); @@ -2170,27 +2572,6 @@ void idAsyncClient::SendVersionCheck( bool fromMenu ) { showUpdateMessage = fromMenu; } -/* -================== -idAsyncClient::SendVersionDLUpdate - -sending those packets is not strictly necessary. just a way to tell the update server -about what is going on. allows the update server to have a more precise view of the overall -network load for the updates -================== -*/ -void idAsyncClient::SendVersionDLUpdate( int state ) { - idBitMsg msg; - byte msgBuf[MAX_MESSAGE_SIZE]; - - msg.Init( msgBuf, sizeof( msgBuf ) ); - msg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); - msg.WriteString( "versionDL" ); - msg.WriteLong( ASYNC_PROTOCOL_VERSION ); - msg.WriteShort( state ); - clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() ); -} - /* ================== idAsyncClient::HandleDownloads @@ -2209,76 +2590,14 @@ void idAsyncClient::HandleDownloads( void ) { } else if ( backgroundDownload.completed ) { // only enter these if the download slot is free if ( updateState == UPDATE_READY ) { - // - if ( session->MessageBox( MSG_YESNO, updateMSG, common->GetLanguageDict()->GetString ( "#str_04330" ), true, "yes" )[0] ) { - if ( !updateDirectDownload ) { - sys->OpenURL( updateURL, true ); - updateState = UPDATE_DONE; - } else { - - // we're just creating the file at toplevel inside fs_savepath - updateURL.ExtractFileName( updateFile ); - idFile_Permanent *f = static_cast< idFile_Permanent *>( fileSystem->OpenFileWrite( updateFile ) ); - if ( f == NULL ) { - common->Warning( "could not create update download destination '%s'", updateFile.c_str() ); - updateState = UPDATE_DONE; - SendVersionDLUpdate( 2 ); - session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04335" ), common->GetLanguageDict()->GetString ( "#str_04336" ), true ); - if ( updateFallback.Length() ) { - sys->OpenURL( updateFallback.c_str(), true ); - } else { - common->Printf( "no fallback URL\n" ); - } - return; - } - dltotal = 0; - dlnow = 0; - - backgroundDownload.completed = false; - backgroundDownload.opcode = DLTYPE_URL; - backgroundDownload.f = f; - backgroundDownload.url.status = DL_WAIT; - backgroundDownload.url.expectedSize = 0; - backgroundDownload.url.dlnow = 0; - backgroundDownload.url.dltotal = 0; - backgroundDownload.url.url = updateURL; - fileSystem->BackgroundDownload( &backgroundDownload ); - - updateState = UPDATE_DLING; - SendVersionDLUpdate( 0 ); - session->DownloadProgressBox( &backgroundDownload, va( "Downloading %s\n", updateFile.c_str() ) ); - updateState = UPDATE_DONE; - if ( backgroundDownload.url.status == DL_DONE ) { - SendVersionDLUpdate( 1 ); - idStr fullPath = f->GetFullPath(); - AsyncClient_CloseBackgroundDownloadFile( backgroundDownload ); - if ( session->MessageBox( MSG_YESNO, common->GetLanguageDict()->GetString ( "#str_04331" ), common->GetLanguageDict()->GetString ( "#str_04332" ), true, "yes" )[0] ) { - if ( updateMime == FILE_EXEC ) { - sys->StartProcess( fullPath, true ); - } else { - sys->OpenURL( va( "file://%s", fullPath.c_str() ), true ); - } - } else { - session->MessageBox( MSG_OK, va( common->GetLanguageDict()->GetString ( "#str_04333" ), fullPath.c_str() ), common->GetLanguageDict()->GetString ( "#str_04334" ), true ); - } - } else { - if ( backgroundDownload.url.dlerror[ 0 ] ) { - common->Warning( "update download failed. curl error: %s", backgroundDownload.url.dlerror ); - } - SendVersionDLUpdate( 2 ); - idStr name = f->GetName(); - AsyncClient_CloseBackgroundDownloadFile( backgroundDownload ); - fileSystem->RemoveFile( name ); - session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04335" ), common->GetLanguageDict()->GetString ( "#str_04336" ), true ); - if ( updateFallback.Length() ) { - sys->OpenURL( updateFallback.c_str(), true ); - } else { - common->Printf( "no fallback URL\n" ); - } - } - } - } else { - updateState = UPDATE_DONE; + const bool openReleasePage = session->MessageBox( MSG_YESNO, updateMSG, + common->GetLanguageDict()->GetString( "#str_04330" ), true, "yes" )[ 0 ] != '\0'; + updateState = UPDATE_DONE; + showUpdateMessage = false; + if ( openReleasePage ) { + // This compile-time HTTPS destination is the only action a legacy + // version reply may trigger. The network-provided URLs are inert. + sys->OpenURL( PROJECT_RELEASES_URL, false ); } } else if ( dlList.Num() ) { @@ -2478,6 +2797,10 @@ void idAsyncClient::ProcessDownloadInfoMessage( const netadr_t from, const idBit if ( infoType == SERVER_DL_REDIRECT ) { msg.ReadString( buf, MAX_STRING_CHARS ); cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" ); + if ( !idURLPolicy::IsAllowedHTTPURL( buf ) ) { + common->Warning( "server supplied an invalid download information URL; request ignored" ); + return; + } // "You are missing required pak files to connect to this server.\nThe server gave a web page though:\n%s\nDo you want to go there now?" // "Missing required files" if ( session->MessageBox( MSG_YESNO, va( common->GetLanguageDict()->GetString( "#str_07217" ), buf ), @@ -2517,6 +2840,11 @@ void idAsyncClient::ProcessDownloadInfoMessage( const netadr_t from, const idBit } entry.filename = buf; msg.ReadString( buf, MAX_STRING_CHARS ); + if ( !idURLPolicy::IsAllowedHTTPURL( buf ) ) { + common->Warning( "server supplied a package URL outside the bounded HTTP/HTTPS policy; download list ignored" ); + dlList.Clear(); + return; + } entry.url = buf; entry.size = msg.ReadLong(); if ( entry.size <= 0 || totalDlSize > idMath::INT_MAX - entry.size ) { @@ -2611,14 +2939,21 @@ int idAsyncClient::GetDownloadRequest( const int checksums[ MAX_PURE_PAKS ], int common->Warning( "download request checksum count %d exceeds storage capacity %d", count, storedChecksumCount ); } if ( count != storedChecksumCount || memcmp( dlChecksums + 1, checksums, sizeof( int ) * storedChecksumCount ) || gamePakChecksum != dlChecksums[ 0 ] ) { - idRandom newreq; - memset( dlChecksums, 0, sizeof( dlChecksums ) ); dlChecksums[ 0 ] = gamePakChecksum; memcpy( dlChecksums + 1, checksums, sizeof( int ) * storedChecksumCount ); - newreq.SetSeed( Sys_Milliseconds() ); - dlRequest = newreq.RandomInt(); + std::uint32_t secureRequest = 0; + if ( !Sys_GetSecureRandomBytes( &secureRequest, sizeof( secureRequest ) ) ) { + memset( dlChecksums, 0, sizeof( dlChecksums ) ); + dlRequest = -1; + dlCount = -1; + return -1; + } + if ( secureRequest == static_cast( -1 ) ) { + secureRequest = 0; + } + dlRequest = static_cast( secureRequest ); dlCount = storedChecksumCount + 1; return dlRequest; } diff --git a/src/framework/async/AsyncClient.h b/src/framework/async/AsyncClient.h index 51df14c6..2f603e64 100644 --- a/src/framework/async/AsyncClient.h +++ b/src/framework/async/AsyncClient.h @@ -51,6 +51,25 @@ typedef enum { AUTHKEY_GUID } authKeyMsg_t; +typedef enum { + RCON_REPLY_NONE, + RCON_REPLY_LEGACY, + RCON_REPLY_CHALLENGE, + RCON_REPLY_OUTPUT +} rconReplyState_t; + +typedef struct rcon2ClientRequest_s { + rconReplyState_t state; + netadr_t address; + int startTime; + int lastSendTime; + byte clientNonce[16]; + byte serverNonce[16]; + byte requestDigest[32]; + byte proof[32]; + char command[MAX_STRING_CHARS]; +} rcon2ClientRequest_t; + typedef enum { AUTHKEY_BAD_INVALID, AUTHKEY_BAD_BANNED, @@ -62,7 +81,6 @@ typedef enum { UPDATE_NONE, UPDATE_SENT, UPDATE_READY, - UPDATE_DLING, UPDATE_DONE } clientUpdateState_t; @@ -136,6 +154,7 @@ class idAsyncClient { netadr_t lastRconAddress; // last rcon address we emitted to int lastRconTime; // when last rcon emitted + rcon2ClientRequest_t rcon2Request; idMsgChannel channel; // message channel to server int lastConnectTime; // last time a connect message was sent @@ -159,16 +178,9 @@ class idAsyncClient { clientUpdateState_t updateState; int updateSentTime; idStr updateMSG; - idStr updateURL; - bool updateDirectDownload; - idStr updateFile; - dlMime_t updateMime; - idStr updateFallback; bool showUpdateMessage; backgroundDownload_t backgroundDownload; - int dltotal; - int dlnow; int lastFrameDelta; @@ -194,6 +206,8 @@ class idAsyncClient { void ProcessDisconnectMessage( const netadr_t from, const idBitMsg &msg ); void ProcessInfoResponseMessage( const netadr_t from, const idBitMsg &msg ); void ProcessPrintMessage( const netadr_t from, const idBitMsg &msg ); + void ProcessRemoteConsole2ChallengeResponse( const netadr_t from, const idBitMsg &msg ); + void ProcessRemoteConsole2Complete( const netadr_t from, const idBitMsg &msg ); void ProcessServersListMessage( const netadr_t from, const idBitMsg &msg ); void ProcessServersListExtMessage( const netadr_t from, const idBitMsg &msg ); void ProcessAuthKeyMessage( const netadr_t from, const idBitMsg &msg ); @@ -206,7 +220,6 @@ class idAsyncClient { void ProcessReliableMessagePure( const idBitMsg &msg ); static const char* HandleGuiCommand( const char *cmd ); const char* HandleGuiCommandInternal( const char *cmd ); - void SendVersionDLUpdate( int state ); void HandleDownloads( void ); void Idle( void ); int UpdateTime( int clamp ); @@ -214,6 +227,10 @@ class idAsyncClient { bool CheckTimeout( void ); void ProcessDownloadInfoMessage( const netadr_t from, const idBitMsg &msg ); int GetDownloadRequest( const int checksums[ MAX_PURE_PAKS ], int count, int gamePakChecksum ); + void ClearRemoteConsoleRequest( void ); + void SendRemoteConsole2Challenge( void ); + void SendRemoteConsole2Proof( void ); + void UpdateRemoteConsoleRequest( void ); }; #endif /* !__ASYNCCLIENT_H__ */ diff --git a/src/framework/async/AsyncNetwork.cpp b/src/framework/async/AsyncNetwork.cpp index a96c7f0f..d5d33237 100644 --- a/src/framework/async/AsyncNetwork.cpp +++ b/src/framework/async/AsyncNetwork.cpp @@ -53,12 +53,14 @@ idCVar idAsyncNetwork::serverZombieTimeout( "net_serverZombieTimeout", "5", C idCVar idAsyncNetwork::serverClientTimeout( "net_serverClientTimeout", "40", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "client time out in seconds" ); idCVar idAsyncNetwork::clientServerTimeout( "net_clientServerTimeout", "40", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "server time out in seconds" ); idCVar idAsyncNetwork::serverDrawClient( "net_serverDrawClient", "-1", CVAR_SYSTEM | CVAR_INTEGER, "number of client for which to draw view on server" ); -idCVar idAsyncNetwork::serverRemoteConsolePassword( "net_serverRemoteConsolePassword", "", CVAR_SYSTEM | CVAR_NOCHEAT, "remote console password" ); +idCVar idAsyncNetwork::serverRemoteConsolePassword( "net_serverRemoteConsolePassword", "", CVAR_SYSTEM | CVAR_NOCHEAT | CVAR_PRIVATE | CVAR_CASE_SENSITIVE, "private remote console password (rcon2 requires at least 12 bytes)" ); +idCVar idAsyncNetwork::serverAllowLegacyRcon( "net_serverAllowLegacyRcon", "0", CVAR_SYSTEM | CVAR_BOOL | CVAR_NOCHEAT, "allow the insecure legacy plaintext rcon protocol" ); idCVar idAsyncNetwork::clientPrediction( "net_clientPrediction", "16", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "additional client side prediction in milliseconds (legacy value 16 follows one exact base tic)" ); idCVar idAsyncNetwork::clientMaxPrediction( "net_clientMaxPrediction", "1000", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "maximum number of milliseconds a client can predict ahead of server." ); idCVar idAsyncNetwork::clientUsercmdBackup( "net_clientUsercmdBackup", "5", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "number of usercmds to resend" ); idCVar idAsyncNetwork::clientRemoteConsoleAddress( "net_clientRemoteConsoleAddress", "localhost", CVAR_SYSTEM | CVAR_NOCHEAT, "remote console address" ); -idCVar idAsyncNetwork::clientRemoteConsolePassword( "net_clientRemoteConsolePassword", "", CVAR_SYSTEM | CVAR_NOCHEAT, "remote console password" ); +idCVar idAsyncNetwork::clientRemoteConsolePassword( "net_clientRemoteConsolePassword", "", CVAR_SYSTEM | CVAR_NOCHEAT | CVAR_PRIVATE | CVAR_CASE_SENSITIVE, "private remote console password" ); +idCVar idAsyncNetwork::clientUseLegacyRcon( "net_clientUseLegacyRcon", "0", CVAR_SYSTEM | CVAR_BOOL | CVAR_NOCHEAT, "send the insecure legacy plaintext rcon protocol" ); idCVar idAsyncNetwork::master0( "net_master0", IDNET_HOST ":" IDNET_MASTER_PORT, CVAR_SYSTEM | CVAR_ROM, "idnet master server address" ); idCVar idAsyncNetwork::master1( "net_master1", "", CVAR_SYSTEM | CVAR_ARCHIVE, "1st master server address" ); idCVar idAsyncNetwork::master2( "net_master2", "", CVAR_SYSTEM | CVAR_ARCHIVE, "2nd master server address" ); @@ -164,7 +166,7 @@ void idAsyncNetwork::Init( void ) { cmdSystem->AddCommand( "serverInfo", GetServerInfo_f, CMD_FL_SYSTEM, "shows server info" ); cmdSystem->AddCommand( "LANScan", GetLANServers_f, CMD_FL_SYSTEM, "scans LAN for servers" ); cmdSystem->AddCommand( "listServers", ListServers_f, CMD_FL_SYSTEM, "lists scanned servers" ); - cmdSystem->AddCommand( "rcon", RemoteConsole_f, CMD_FL_SYSTEM, "sends remote console command to server" ); + cmdSystem->AddCommand( "rcon", RemoteConsole_f, CMD_FL_SYSTEM, "sends an authenticated remote console command to the server" ); cmdSystem->AddCommand( "heartbeat", Heartbeat_f, CMD_FL_SYSTEM, "send a heartbeat to the the master servers" ); cmdSystem->AddCommand( "kick", Kick_f, CMD_FL_SYSTEM, "kick a client by connection number" ); cmdSystem->AddCommand( "checkNewVersion", CheckNewVersion_f, CMD_FL_SYSTEM, "check if a new version of the game is available" ); @@ -293,33 +295,93 @@ void idAsyncNetwork::WriteUserCmdDelta( idBitMsg &msg, const usercmd_t &cmd, con idAsyncNetwork::ReadUserCmdDelta ================== */ -void idAsyncNetwork::ReadUserCmdDelta( const idBitMsg &msg, usercmd_t &cmd, const usercmd_t *base ) { - memset( &cmd, 0, sizeof( cmd ) ); +static bool AsyncNetwork_ProbeDeltaField( idBitMsg &probe, const int valueBits ) { + if ( probe.GetRemainingReadBits() < 1 ) { + return false; + } + const bool changed = probe.ReadBits( 1 ) != 0; + if ( changed ) { + if ( probe.GetRemainingReadBits() < valueBits ) { + return false; + } + probe.ReadBits( valueBits ); + } + return true; +} - if ( base ) { - cmd.gameTime = msg.ReadDeltaLongCounter( base->gameTime ); - cmd.buttons = msg.ReadDeltaShort( base->buttons ); - cmd.mx = msg.ReadDeltaShort( base->mx ); - cmd.my = msg.ReadDeltaShort( base->my ); - cmd.forwardmove = msg.ReadDeltaChar( base->forwardmove ); - cmd.rightmove = msg.ReadDeltaChar( base->rightmove ); - cmd.upmove = msg.ReadDeltaChar( base->upmove ); - cmd.angles[0] = msg.ReadDeltaShort( base->angles[0] ); - cmd.angles[1] = msg.ReadDeltaShort( base->angles[1] ); - cmd.angles[2] = msg.ReadDeltaShort( base->angles[2] ); - return; +static bool AsyncNetwork_ProbeDeltaLongCounter( idBitMsg &probe ) { + if ( probe.GetRemainingReadBits() < 5 ) { + return false; + } + const int valueBits = probe.ReadBits( 5 ); + if ( valueBits < 0 || valueBits > 31 ) { + return false; + } + if ( probe.GetRemainingReadBits() < valueBits ) { + return false; } + if ( valueBits > 0 ) { + probe.ReadBits( valueBits ); + } + return true; +} + +static bool AsyncNetwork_CanReadUserCmdDelta( const idBitMsg &msg, const bool hasBase ) { + idBitMsg probe = msg; + if ( !hasBase ) { + // long + three shorts + three chars + three angle shorts + return probe.GetRemainingReadBits() >= 152; + } + + return AsyncNetwork_ProbeDeltaLongCounter( probe ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ) && + AsyncNetwork_ProbeDeltaField( probe, 8 ) && + AsyncNetwork_ProbeDeltaField( probe, 8 ) && + AsyncNetwork_ProbeDeltaField( probe, 8 ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ) && + AsyncNetwork_ProbeDeltaField( probe, 16 ); +} + +bool idAsyncNetwork::ReadUserCmdDelta( const idBitMsg &msg, usercmd_t &cmd, const usercmd_t *base ) { + if ( !AsyncNetwork_CanReadUserCmdDelta( msg, base != NULL ) ) { + return false; + } + + usercmd_t decoded; + memset( &decoded, 0, sizeof( decoded ) ); - cmd.gameTime = msg.ReadLong(); - cmd.buttons = msg.ReadShort(); - cmd.mx = msg.ReadShort(); - cmd.my = msg.ReadShort(); - cmd.forwardmove = msg.ReadChar(); - cmd.rightmove = msg.ReadChar(); - cmd.upmove = msg.ReadChar(); - cmd.angles[0] = msg.ReadShort(); - cmd.angles[1] = msg.ReadShort(); - cmd.angles[2] = msg.ReadShort(); + if ( base ) { + decoded.gameTime = msg.ReadDeltaLongCounter( base->gameTime ); + decoded.buttons = msg.ReadDeltaShort( base->buttons ); + decoded.mx = msg.ReadDeltaShort( base->mx ); + decoded.my = msg.ReadDeltaShort( base->my ); + decoded.forwardmove = msg.ReadDeltaChar( base->forwardmove ); + decoded.rightmove = msg.ReadDeltaChar( base->rightmove ); + decoded.upmove = msg.ReadDeltaChar( base->upmove ); + decoded.angles[0] = msg.ReadDeltaShort( base->angles[0] ); + decoded.angles[1] = msg.ReadDeltaShort( base->angles[1] ); + decoded.angles[2] = msg.ReadDeltaShort( base->angles[2] ); + } else { + decoded.gameTime = msg.ReadLong(); + decoded.buttons = msg.ReadShort(); + decoded.mx = msg.ReadShort(); + decoded.my = msg.ReadShort(); + decoded.forwardmove = msg.ReadChar(); + decoded.rightmove = msg.ReadChar(); + decoded.upmove = msg.ReadChar(); + decoded.angles[0] = msg.ReadShort(); + decoded.angles[1] = msg.ReadShort(); + decoded.angles[2] = msg.ReadShort(); + } + + if ( msg.IsReadOverflowed() ) { + return false; + } + cmd = decoded; + return true; } /* diff --git a/src/framework/async/AsyncNetwork.h b/src/framework/async/AsyncNetwork.h index 6e2d95f4..f2309f62 100644 --- a/src/framework/async/AsyncNetwork.h +++ b/src/framework/async/AsyncNetwork.h @@ -44,6 +44,9 @@ const int ASYNC_PROTOCOL_VERSION = ( ASYNC_PROTOCOL_MAJOR << 16 ) + ASYNC_PROTOC const int MAX_ASYNC_CLIENTS = 32; const int MAX_USERCMD_BACKUP = 256; +// The wire protocol sends the current command plus at most ten backups. +// Keep packet work bounded independently of the larger circular history. +const int MAX_USERCMD_PACKET_COMMANDS = 11; const int MAX_USERCMD_DUPLICATION = 25; const int MAX_USERCMD_RELAY = 10; @@ -151,7 +154,7 @@ class idAsyncNetwork { static void RunFrame( void ); static void WriteUserCmdDelta( idBitMsg &msg, const usercmd_t &cmd, const usercmd_t *base ); - static void ReadUserCmdDelta( const idBitMsg &msg, usercmd_t &cmd, const usercmd_t *base ); + static bool ReadUserCmdDelta( const idBitMsg &msg, usercmd_t &cmd, const usercmd_t *base ); static bool DuplicateUsercmd( const usercmd_t &previousUserCmd, usercmd_t ¤tUserCmd, int frame, int time ); static bool UsercmdInputChanged( const usercmd_t &previousUserCmd, const usercmd_t ¤tUserCmd ); @@ -185,11 +188,13 @@ class idAsyncNetwork { static idCVar clientServerTimeout; // time out in seconds for server static idCVar serverDrawClient; // the server draws the view of this client static idCVar serverRemoteConsolePassword; // remote console password + static idCVar serverAllowLegacyRcon; // explicit plaintext compatibility opt-in static idCVar clientPrediction; // how many additional milliseconds the clients runs ahead static idCVar clientMaxPrediction; // max milliseconds into the future a client can run prediction static idCVar clientUsercmdBackup; // how many usercmds the client sends from previous frames static idCVar clientRemoteConsoleAddress; // remote console address static idCVar clientRemoteConsolePassword; // remote console password + static idCVar clientUseLegacyRcon; // explicit plaintext compatibility opt-in static idCVar master0; // idnet master server static idCVar master1; // 1st master server static idCVar master2; // 2nd master server diff --git a/src/framework/async/AsyncServer.cpp b/src/framework/async/AsyncServer.cpp index 9b582138..cec08d28 100644 --- a/src/framework/async/AsyncServer.cpp +++ b/src/framework/async/AsyncServer.cpp @@ -30,9 +30,12 @@ If you have questions concerning this license or the applicable additional terms #include "AsyncNetwork.h" +#include "Rcon2Protocol.h" #include "../Session_local.h" +#include + static ID_INLINE int AsyncServer_NextGameFrameMsec( int gameFrame ) { return common->GetUserCmdDeltaMsec( gameFrame + 1 ); } @@ -44,6 +47,67 @@ const int NOINPUT_IDLE_TIME = 30000; const int HEARTBEAT_MSEC = 5*60*1000; +const int CONNECTION_CHALLENGE_TIMEOUT_MSEC = 30000; +const int RCON2_CHALLENGE_TIMEOUT_MSEC = 10000; +const int RCON2_RESEND_MIN_MSEC = 500; +const int RCON2_RATE_WINDOW_MSEC = 10000; +const int RCON2_RATE_MAX_CHALLENGES = 5; +const int RCON2_FAILURE_WINDOW_MSEC = 60000; +const int RCON2_FAILURE_MAX_ATTEMPTS = 5; +const int RCON2_FAILURE_BLOCK_MSEC = 30000; +const int OOB_RATE_WINDOW_MSEC = 1000; +const int OOB_INFO_MAX_PER_SOURCE = 16; +const int OOB_CHALLENGE_MAX_PER_SOURCE = 4; +const int OOB_INFO_MAX_GLOBAL = 512; +const int OOB_CHALLENGE_MAX_GLOBAL = 256; + +static ID_INLINE bool AsyncServer_SameEndpoint( const netadr_t &left, const netadr_t &right ) { + return left.port == right.port && Sys_CompareNetAdrBase( left, right ); +} + +static ID_INLINE std::uint32_t AsyncServer_Elapsed( int now, int then ) { + return static_cast( now ) - static_cast( then ); +} + +static ID_INLINE bool AsyncServer_TimeBefore( int now, int future ) { + return static_cast( static_cast( now ) - + static_cast( future ) ) < 0; +} + +static bool AsyncServer_SecureConnectionId( int &identifier ) { + std::uint32_t randomValue = 0; + if ( !Sys_GetSecureRandomBytes( &randomValue, sizeof( randomValue ) ) ) { + return false; + } + identifier = static_cast( randomValue & CONNECTIONLESS_MESSAGE_ID_MASK ); + if ( identifier == CONNECTIONLESS_MESSAGE_ID_MASK ) { + identifier = 0; + } + return true; +} + +static void AsyncServer_ClearChallenge( challenge_t &challenge ) { + challenge.valid = false; + memset( &challenge.address, 0, sizeof( challenge.address ) ); + challenge.clientId = 0; + challenge.challenge = 0; + challenge.time = 0; + challenge.pingTime = 0; + challenge.connected = false; + challenge.authState = CDK_WAIT; + challenge.authReply = AUTH_NONE; + challenge.authReplyMsg = AUTH_REPLY_WAITING; + challenge.authReplyPrint.Clear(); + challenge.guid[ 0 ] = '\0'; + challenge.OS = 0; +} + +static void AsyncServer_ClearChallenges( challenge_t challenges[ MAX_CHALLENGES ] ) { + for ( int index = 0; index < MAX_CHALLENGES; ++index ) { + AsyncServer_ClearChallenge( challenges[ index ] ); + } +} + // openQ4: below this much room in the message channel there is no point building // a snapshot - the channel would refuse it and the game would have already // consumed the client's unreliable message queue writing it. @@ -88,7 +152,10 @@ idAsyncServer::idAsyncServer( void ) { gameFrame = 0; gameTime = 0; gameTimeResidual = 0; - memset( challenges, 0, sizeof( challenges ) ); + AsyncServer_ClearChallenges( challenges ); + memset( rcon2Challenges, 0, sizeof( rcon2Challenges ) ); + memset( rconRateLimits, 0, sizeof( rconRateLimits ) ); + memset( oobRateLimits, 0, sizeof( oobRateLimits ) ); memset( userCmds, 0, sizeof( userCmds ) ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { ClearClient( i ); @@ -97,6 +164,13 @@ idAsyncServer::idAsyncServer( void ) { nextHeartbeatTime = 0; nextAsyncStatsTime = 0; noRconOutput = true; + rcon2VerifierInitialized = false; + rcon2VerifierValid = false; + memset( rcon2Salt, 0, sizeof( rcon2Salt ) ); + memset( rcon2Verifier, 0, sizeof( rcon2Verifier ) ); + oobWindowStart = 0; + oobInfoResponses = 0; + oobChallengeResponses = 0; lastAuthTime = 0; memset( stats_outrate, 0, sizeof( stats_outrate ) ); @@ -155,6 +229,7 @@ void idAsyncServer::ClosePort( void ) { for ( i = 0; i < MAX_CHALLENGES; i++ ) { challenges[ i ].authReplyPrint.Clear(); } + ClearRconSecurityState( true ); } /* @@ -187,7 +262,13 @@ void idAsyncServer::Spawn( void ) { cvarSystem->ResetFlaggedVariables( CVAR_CHEAT ); } - memset( challenges, 0, sizeof( challenges ) ); + AsyncServer_ClearChallenges( challenges ); + memset( rcon2Challenges, 0, sizeof( rcon2Challenges ) ); + memset( rconRateLimits, 0, sizeof( rconRateLimits ) ); + memset( oobRateLimits, 0, sizeof( oobRateLimits ) ); + oobWindowStart = serverTime; + oobInfoResponses = 0; + oobChallengeResponses = 0; memset( userCmds, 0, sizeof( userCmds ) ); for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { ClearClient( i ); @@ -199,10 +280,16 @@ void idAsyncServer::Spawn( void ) { serverDataChecksum = declManager->GetChecksum(); common->DPrintf( "Server decl checksum: 0x%08x\n", static_cast( serverDataChecksum ) ); - // get a pseudo random server id, but don't use the id which is reserved for connectionless packets - serverId = Sys_Milliseconds() & CONNECTIONLESS_MESSAGE_ID_MASK; + // A server id is only a short wire correlation value, not an authentication + // secret, but making it unpredictable closes an unnecessary spoofing aid. + if ( !AsyncServer_SecureConnectionId( serverId ) ) { + common->Warning( "OS secure random unavailable; refusing to spawn network server" ); + serverPort.Close(); + return; + } active = true; + RefreshRcon2Verifier(); nextHeartbeatTime = 0; nextAsyncStatsTime = 0; @@ -245,6 +332,7 @@ void idAsyncServer::Kill( void ) { fileSystem->ClearPureChecksums(); active = false; + ClearRconSecurityState( true ); // shutdown any current game session->Stop(); @@ -327,14 +415,6 @@ void idAsyncServer::ExecuteMapChange( void ) { serverTime = 0; - // openQ4 dev/staging runs from directory overrides (fs_cdpath). Keep - // multiplayer server startup non-pure to avoid pure-lockdown failures. - if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) ) { - sessLocal.mapSpawnData.serverInfo.SetInt( "si_pure", 0 ); - cvarSystem->SetCVarBool( "si_pure", false ); - common->Printf( "openQ4: forcing si_pure 0 for local server startup\n" ); - } - // initialize game id and time gameInitId ^= Sys_Milliseconds(); // NOTE: make sure the gameInitId is always a positive number because negative numbers have special meaning gameFrame = 0; @@ -418,7 +498,9 @@ void idAsyncServer::ExecuteMapChange( void ) { for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { if ( clients[ i ].clientState == SCS_PUREWAIT ) { if ( !SendReliablePureToClient( i ) ) { - clients[ i ].clientState = SCS_CONNECTED; + // Never promote a client when the server cannot prove the + // prerequisites for its own pure policy. + DropClient( i, "#str_04337" ); } } } @@ -1016,7 +1098,7 @@ void idAsyncServer::SendUserInfoBroadcast( int userInfoNum, const idDict &info, if ( userInfoNum == localClientNum ) { common->DPrintf( "local user info modified by server\n" ); - cvarSystem->SetCVarsFromDict( *gameInfo ); + cvarSystem->SetCVarsFromDictByFlags( *gameInfo, CVAR_USERINFO ); cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); // don't emit back } @@ -1292,8 +1374,12 @@ bool idAsyncServer::SendSnapshotToClient( int clientNum ) { return false; } - // how far is the client ahead of the server minus the packet delay - client.clientAheadTime = client.gameTime - ( gameTime + gameTimeResidual ); + // How far the client is ahead of the server minus the packet delay. The + // client-reported time is untrusted, so keep every intermediate widened. + const std::int64_t clientAheadTime = static_cast( client.gameTime ) - + static_cast( gameTime ) - static_cast( gameTimeResidual ); + client.clientAheadTime = clientAheadTime < idMath::INT_MIN ? idMath::INT_MIN : + ( clientAheadTime > idMath::INT_MAX ? idMath::INT_MAX : static_cast( clientAheadTime ) ); // write the snapshot msg.Init( msgBuf, sizeof( msgBuf ) ); @@ -1371,6 +1457,10 @@ void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMs if ( client.clientState == SCS_ZOMBIE ) { return; } + if ( msg.GetRemainingReadBits() < 64 ) { + DropClient( clientNum, "#str_07138" ); + return; + } acknowledgeSequence = msg.ReadLong(); clientGameInitId = msg.ReadLong(); @@ -1395,7 +1485,8 @@ void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMs if ( sessLocal.mapSpawnData.serverInfo.GetBool( "si_pure" ) ) { client.clientState = SCS_PUREWAIT; if ( !SendReliablePureToClient( clientNum ) ) { - client.clientState = SCS_CONNECTED; + DropClient( clientNum, "#str_04337" ); + return; } } } else if ( idAsyncNetwork::verbose.GetInteger() ) { @@ -1403,6 +1494,10 @@ void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMs } return; } + if ( msg.GetRemainingReadBits() < 32 + 8 ) { + DropClient( clientNum, "#str_07138" ); + return; + } client.acknowledgeSnapshotSequence = msg.ReadLong(); @@ -1446,19 +1541,44 @@ void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMs break; } case CLIENT_UNRELIABLE_MESSAGE_PINGRESPONSE: { - client.clientPing = realTime - msg.ReadLong(); + if ( msg.GetRemainingReadBits() < 32 ) { + DropClient( clientNum, "#str_07138" ); + return; + } + const int echoedPingTime = msg.ReadLong(); + if ( echoedPingTime != client.lastPingTime ) { + break; + } + client.clientPing = static_cast( Min( AsyncServer_Elapsed( realTime, echoedPingTime ), 32767u ) ); break; } case CLIENT_UNRELIABLE_MESSAGE_USERCMD: { + if ( msg.GetRemainingReadBits() < 16 + 32 + 8 ) { + DropClient( clientNum, "#str_07138" ); + return; + } client.clientPrediction = msg.ReadShort(); // read user commands clientGameFrame = msg.ReadLong(); numUsercmds = msg.ReadByte(); - for ( last = NULL, i = clientGameFrame - numUsercmds + 1; i <= clientGameFrame; i++ ) { + if ( numUsercmds < 1 || numUsercmds > MAX_USERCMD_PACKET_COMMANDS || + clientGameFrame < numUsercmds - 1 || + static_cast( clientGameFrame ) > static_cast( gameFrame ) + MAX_USERCMD_BACKUP ) { + DropClient( clientNum, "#str_07138" ); + return; + } + + const int firstClientGameFrame = clientGameFrame - numUsercmds + 1; + last = NULL; + for ( int commandIndex = 0; commandIndex < numUsercmds; commandIndex++ ) { + i = firstClientGameFrame + commandIndex; index = i & ( MAX_USERCMD_BACKUP - 1 ); - idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][clientNum], last ); + if ( !idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][clientNum], last ) ) { + DropClient( clientNum, "#str_07138" ); + return; + } userCmds[index][clientNum].gameFrame = i; userCmds[index][clientNum].duplicateCount = 0; if ( idAsyncNetwork::UsercmdInputChanged( userCmds[( i - 1 ) & ( MAX_USERCMD_BACKUP - 1 )][clientNum], userCmds[index][clientNum] ) ) { @@ -1500,10 +1620,18 @@ void idAsyncServer::ProcessReliableClientMessages( int clientNum ) { while ( client.channel.GetReliableMessage( msg ) ) { id = msg.ReadByte(); + if ( msg.IsReadOverflowed() ) { + DropClient( clientNum, "#str_07138" ); + return; + } switch( id ) { case CLIENT_RELIABLE_MESSAGE_CLIENTINFO: { idDict info; msg.ReadDeltaDict( info, &sessLocal.mapSpawnData.userInfo[clientNum] ); + if ( msg.IsReadOverflowed() ) { + DropClient( clientNum, "#str_07138" ); + return; + } SendUserInfoBroadcast( clientNum, info ); break; } @@ -1569,7 +1697,8 @@ void idAsyncServer::ProcessAuthMessage( const idBitMsg &msg ) { // no message parsing below for ( i = 0; i < MAX_CHALLENGES; i++ ) { - if ( !challenges[i].connected && challenges[ i ].clientId == clientId ) { + if ( challenges[ i ].valid && !challenges[i].connected && + challenges[ i ].clientId == clientId ) { // return if something is wrong // break if we have found a valid auth if ( !strlen( challenges[ i ].guid ) ) { @@ -1593,7 +1722,7 @@ void idAsyncServer::ProcessAuthMessage( const idBitMsg &msg ) { } if ( challenges[ i ].authState != CDK_WAIT ) { - common->DWarning( "auth: challenge 0x%x %s authState %d != CDK_WAIT", challenges[ i ].challenge, Sys_NetAdrToString( challenges[ i ].address ), challenges[ i ].authState ); + common->DWarning( "auth: challenge for %s has authState %d instead of CDK_WAIT", Sys_NetAdrToString( challenges[ i ].address ), challenges[ i ].authState ); return; } @@ -1617,39 +1746,307 @@ void idAsyncServer::ProcessAuthMessage( const idBitMsg &msg ) { } } +/* +================== +idAsyncServer::AllowConnectionlessResponse + +Bound the two unauthenticated replies that can amplify a spoofed UDP request. +LAN browser traffic gets enough per-source headroom to probe every legacy +server port in one pass, while the global budget also limits distributed +reflection traffic. +================== +*/ +bool idAsyncServer::AllowConnectionlessResponse( const netadr_t from, bool infoResponse ) { + if ( AsyncServer_Elapsed( serverTime, oobWindowStart ) >= OOB_RATE_WINDOW_MSEC ) { + oobWindowStart = serverTime; + oobInfoResponses = 0; + oobChallengeResponses = 0; + } + + int slot = -1; + int oldest = 0; + std::uint32_t oldestAge = 0; + for ( int index = 0; index < MAX_OOB_RATE_LIMITS; ++index ) { + if ( oobRateLimits[ index ].active && + Sys_CompareNetAdrBase( from, oobRateLimits[ index ].address ) ) { + slot = index; + break; + } + if ( !oobRateLimits[ index ].active ) { + oldest = index; + oldestAge = static_cast( -1 ); + } else { + const std::uint32_t age = AsyncServer_Elapsed( serverTime, + oobRateLimits[ index ].windowStart ); + if ( oldestAge != static_cast( -1 ) && age > oldestAge ) { + oldest = index; + oldestAge = age; + } + } + } + if ( slot < 0 ) { + slot = oldest; + memset( &oobRateLimits[ slot ], 0, sizeof( oobRateLimits[ slot ] ) ); + oobRateLimits[ slot ].active = true; + oobRateLimits[ slot ].address = from; + oobRateLimits[ slot ].windowStart = serverTime; + } + + oobRateLimit_t &source = oobRateLimits[ slot ]; + if ( AsyncServer_Elapsed( serverTime, source.windowStart ) >= OOB_RATE_WINDOW_MSEC ) { + source.windowStart = serverTime; + source.infoResponses = 0; + source.challengeResponses = 0; + } + + if ( infoResponse ) { + const int sourceLimit = Sys_IsLANAddress( from ) ? OOB_INFO_MAX_PER_SOURCE * 2 : OOB_INFO_MAX_PER_SOURCE; + if ( source.infoResponses >= sourceLimit || oobInfoResponses >= OOB_INFO_MAX_GLOBAL ) { + return false; + } + source.infoResponses++; + oobInfoResponses++; + } else { + if ( source.challengeResponses >= OOB_CHALLENGE_MAX_PER_SOURCE || + oobChallengeResponses >= OOB_CHALLENGE_MAX_GLOBAL ) { + return false; + } + source.challengeResponses++; + oobChallengeResponses++; + } + return true; +} + +/* +================== +idAsyncServer::ClearRconSecurityState +================== +*/ +void idAsyncServer::ClearRconSecurityState( bool clearRateLimits ) { + idCrypto::SecureZero( rcon2Challenges, sizeof( rcon2Challenges ) ); + idCrypto::SecureZero( rcon2Salt, sizeof( rcon2Salt ) ); + idCrypto::SecureZero( rcon2Verifier, sizeof( rcon2Verifier ) ); + rcon2VerifierInitialized = false; + rcon2VerifierValid = false; + if ( clearRateLimits ) { + idCrypto::SecureZero( rconRateLimits, sizeof( rconRateLimits ) ); + } +} + +/* +================== +idAsyncServer::RefreshRcon2Verifier + +The expensive password KDF runs once per configured password, never once per +untrusted request. Changing the password invalidates every outstanding proof. +================== +*/ +bool idAsyncServer::RefreshRcon2Verifier( void ) { + if ( rcon2VerifierInitialized && !idAsyncNetwork::serverRemoteConsolePassword.IsModified() ) { + return rcon2VerifierValid; + } + + idCrypto::SecureZero( rcon2Challenges, sizeof( rcon2Challenges ) ); + idCrypto::SecureZero( rcon2Salt, sizeof( rcon2Salt ) ); + idCrypto::SecureZero( rcon2Verifier, sizeof( rcon2Verifier ) ); + rcon2VerifierInitialized = true; + rcon2VerifierValid = false; + idAsyncNetwork::serverRemoteConsolePassword.ClearModified(); + + const char *password = idAsyncNetwork::serverRemoteConsolePassword.GetString(); + const size_t passwordBytes = strlen( password ); + if ( passwordBytes == 0 ) { + return false; + } + if ( passwordBytes < idRcon2::MIN_PASSWORD_BYTES ) { + common->Warning( "rcon2 is disabled: net_serverRemoteConsolePassword must contain at least %u bytes", + static_cast( idRcon2::MIN_PASSWORD_BYTES ) ); + return false; + } + if ( !Sys_GetSecureRandomBytes( rcon2Salt, sizeof( rcon2Salt ) ) ) { + common->Warning( "rcon2 is disabled: OS secure random is unavailable" ); + return false; + } + if ( !idRcon2::DeriveVerifier( password, rcon2Salt, rcon2Verifier ) ) { + idCrypto::SecureZero( rcon2Salt, sizeof( rcon2Salt ) ); + common->Warning( "rcon2 verifier derivation failed" ); + return false; + } + rcon2VerifierValid = true; + return true; +} + +/* +================== +idAsyncServer::AllowRconChallenge +================== +*/ +bool idAsyncServer::AllowRconChallenge( const netadr_t from ) { + int slot = -1; + int oldest = 0; + std::uint32_t oldestAge = 0; + for ( int index = 0; index < MAX_RCON_RATE_LIMITS; ++index ) { + if ( rconRateLimits[ index ].active && + Sys_CompareNetAdrBase( from, rconRateLimits[ index ].address ) ) { + slot = index; + break; + } + if ( !rconRateLimits[ index ].active ) { + oldest = index; + oldestAge = static_cast( -1 ); + } else { + const std::uint32_t age = AsyncServer_Elapsed( serverTime, + rconRateLimits[ index ].challengeWindowStart ); + if ( oldestAge != static_cast( -1 ) && age > oldestAge ) { + oldest = index; + oldestAge = age; + } + } + } + if ( slot < 0 ) { + slot = oldest; + memset( &rconRateLimits[ slot ], 0, sizeof( rconRateLimits[ slot ] ) ); + rconRateLimits[ slot ].active = true; + rconRateLimits[ slot ].address = from; + rconRateLimits[ slot ].challengeWindowStart = serverTime; + rconRateLimits[ slot ].failureWindowStart = serverTime; + } + + rconRateLimit_t &limit = rconRateLimits[ slot ]; + if ( limit.blockedUntil != 0 ) { + if ( AsyncServer_TimeBefore( serverTime, limit.blockedUntil ) ) { + return false; + } + limit.blockedUntil = 0; + } + if ( limit.challengeCount > 0 && + AsyncServer_Elapsed( serverTime, limit.lastChallengeTime ) < RCON2_RESEND_MIN_MSEC ) { + return false; + } + if ( AsyncServer_Elapsed( serverTime, limit.challengeWindowStart ) >= RCON2_RATE_WINDOW_MSEC ) { + limit.challengeWindowStart = serverTime; + limit.challengeCount = 0; + } + if ( limit.challengeCount >= RCON2_RATE_MAX_CHALLENGES ) { + return false; + } + limit.lastChallengeTime = serverTime; + limit.challengeCount++; + return true; +} + +bool idAsyncServer::AllowRconProofAttempt( const netadr_t from ) { + for ( int index = 0; index < MAX_RCON_RATE_LIMITS; ++index ) { + if ( rconRateLimits[ index ].active && + Sys_CompareNetAdrBase( from, rconRateLimits[ index ].address ) ) { + rconRateLimit_t &limit = rconRateLimits[ index ]; + if ( limit.blockedUntil == 0 ) { + return true; + } + if ( AsyncServer_TimeBefore( serverTime, limit.blockedUntil ) ) { + return false; + } + limit.blockedUntil = 0; + return true; + } + } + return true; +} + +void idAsyncServer::RecordRconFailure( const netadr_t from ) { + // Ensure a rate slot exists without allowing the failure to bypass a full + // table. A proof can only reach this point after a challenge used this path. + for ( int index = 0; index < MAX_RCON_RATE_LIMITS; ++index ) { + rconRateLimit_t &limit = rconRateLimits[ index ]; + if ( !limit.active || !Sys_CompareNetAdrBase( from, limit.address ) ) { + continue; + } + if ( AsyncServer_Elapsed( serverTime, limit.failureWindowStart ) >= RCON2_FAILURE_WINDOW_MSEC ) { + limit.failureWindowStart = serverTime; + limit.failureCount = 0; + } + limit.failureCount++; + if ( limit.failureCount >= RCON2_FAILURE_MAX_ATTEMPTS ) { + limit.blockedUntil = static_cast( static_cast( serverTime ) + + static_cast( RCON2_FAILURE_BLOCK_MSEC ) ); + for ( int challengeIndex = 0; challengeIndex < MAX_RCON2_CHALLENGES; ++challengeIndex ) { + if ( rcon2Challenges[ challengeIndex ].active && + Sys_CompareNetAdrBase( from, rcon2Challenges[ challengeIndex ].address ) ) { + idCrypto::SecureZero( &rcon2Challenges[ challengeIndex ], + sizeof( rcon2Challenges[ challengeIndex ] ) ); + } + } + } + return; + } +} + /* ================== idAsyncServer::ProcessChallengeMessage ================== */ void idAsyncServer::ProcessChallengeMessage( const netadr_t from, const idBitMsg &msg ) { - int i, clientId, oldest, oldestTime; + int i, clientId, oldest; idBitMsg outMsg; byte msgBuf[MAX_MESSAGE_SIZE]; + if ( msg.GetRemainingData() != 4 ) { + return; + } clientId = msg.ReadLong(); oldest = 0; - oldestTime = 0x7fffffff; + bool foundFree = false; + std::uint32_t oldestAge = 0; // see if we already have a challenge for this ip for ( i = 0; i < MAX_CHALLENGES; i++ ) { - if ( !challenges[i].connected && Sys_CompareNetAdrBase( from, challenges[i].address ) && clientId == challenges[i].clientId ) { + if ( challenges[ i ].valid && + AsyncServer_Elapsed( serverTime, challenges[ i ].time ) > CONNECTION_CHALLENGE_TIMEOUT_MSEC ) { + AsyncServer_ClearChallenge( challenges[ i ] ); + } + if ( challenges[i].valid && !challenges[i].connected && + AsyncServer_SameEndpoint( from, challenges[i].address ) && + clientId == challenges[i].clientId ) { break; } - if ( challenges[i].time < oldestTime ) { - oldestTime = challenges[i].time; - oldest = i; + if ( !challenges[ i ].valid ) { + if ( !foundFree ) { + oldest = i; + foundFree = true; + } + } else if ( !foundFree ) { + const std::uint32_t age = AsyncServer_Elapsed( serverTime, challenges[ i ].time ); + if ( age > oldestAge ) { + oldestAge = age; + oldest = i; + } } } + if ( !AllowConnectionlessResponse( from, false ) ) { + return; + } + if ( i >= MAX_CHALLENGES ) { // this is the first time this client has asked for a challenge i = oldest; + std::uint32_t secureChallenge = 0; + if ( !Sys_GetSecureRandomBytes( &secureChallenge, sizeof( secureChallenge ) ) ) { + common->Warning( "OS secure random unavailable; connection challenge not issued" ); + return; + } + if ( secureChallenge == 0 ) { + secureChallenge = 1; + } + AsyncServer_ClearChallenge( challenges[ i ] ); + challenges[i].valid = true; challenges[i].address = from; challenges[i].clientId = clientId; - challenges[i].challenge = ( (rand() << 16) ^ rand() ) ^ serverTime; + challenges[i].challenge = static_cast( secureChallenge ); challenges[i].time = serverTime; + challenges[i].pingTime = serverTime; challenges[i].connected = false; challenges[i].authState = CDK_WAIT; challenges[i].authReply = AUTH_NONE; @@ -1657,9 +2054,7 @@ void idAsyncServer::ProcessChallengeMessage( const netadr_t from, const idBitMsg challenges[i].authReplyPrint = ""; challenges[i].guid[0] = '\0'; } - challenges[i].pingTime = serverTime; - - common->Printf( "sending challenge 0x%x to %s\n", challenges[i].challenge, Sys_NetAdrToString( from ) ); + common->DPrintf( "sending connection challenge to %s\n", Sys_NetAdrToString( from ) ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); @@ -1696,9 +2091,9 @@ bool idAsyncServer::SendPureServerMessage( const netadr_t to, int OS ) { int i; fileSystem->GetPureServerChecksums( serverChecksums, OS, &gamePakChecksum ); - if ( !serverChecksums[ 0 ] ) { + if ( !serverChecksums[ 0 ] || !gamePakChecksum ) { // happens if you run fully expanded assets with si_pure 1 - common->Warning( "pure server has no pak files referenced" ); + common->Warning( "pure server has no referenced pak files or compatible game module" ); return false; } common->DPrintf( "client %s: sending pure pak list\n", Sys_NetAdrToString( to ) ); @@ -1734,9 +2129,9 @@ bool idAsyncServer::SendReliablePureToClient( int clientNum ) { int gamePakChecksum; fileSystem->GetPureServerChecksums( serverChecksums, clients[ clientNum ].OS, &gamePakChecksum ); - if ( !serverChecksums[ 0 ] ) { + if ( !serverChecksums[ 0 ] || !gamePakChecksum ) { // happens if you run fully expanded assets with si_pure 1 - common->Warning( "pure server has no pak files referenced" ); + common->Warning( "pure server has no referenced pak files or compatible game module" ); return false; } @@ -1774,7 +2169,7 @@ int idAsyncServer::ValidateChallenge( const netadr_t from, int challenge, int cl } if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) && ( clientId == client.clientId || from.port == client.channel.GetRemoteAddress().port ) ) { - if ( serverTime - client.lastConnectTime < MIN_RECONNECT_TIME ) { + if ( AsyncServer_Elapsed( serverTime, client.lastConnectTime ) < MIN_RECONNECT_TIME ) { common->Printf( "%s: reconnect rejected : too soon\n", Sys_NetAdrToString( from ) ); return -1; } @@ -1783,14 +2178,24 @@ int idAsyncServer::ValidateChallenge( const netadr_t from, int challenge, int cl } for ( i = 0; i < MAX_CHALLENGES; i++ ) { - if ( Sys_CompareNetAdrBase( from, challenges[i].address ) && from.port == challenges[i].address.port ) { + if ( challenges[ i ].valid && + AsyncServer_Elapsed( serverTime, challenges[ i ].time ) > CONNECTION_CHALLENGE_TIMEOUT_MSEC ) { + AsyncServer_ClearChallenge( challenges[ i ] ); + continue; + } + if ( challenges[ i ].valid && !challenges[ i ].connected && + AsyncServer_Elapsed( serverTime, challenges[ i ].time ) <= CONNECTION_CHALLENGE_TIMEOUT_MSEC && + AsyncServer_SameEndpoint( from, challenges[i].address ) && + clientId == challenges[ i ].clientId ) { if ( challenge == challenges[i].challenge ) { break; } } } if ( i == MAX_CHALLENGES ) { - PrintOOB( from, SERVER_PRINT_BADCHALLENGE, "#str_04840" ); + if ( AllowConnectionlessResponse( from, false ) ) { + PrintOOB( from, SERVER_PRINT_BADCHALLENGE, "#str_04840" ); + } return -1; } return i; @@ -1809,21 +2214,45 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & char password[ 17 ]; int i, ichallenge, islot, OS, numClients; + // Parse the complete fixed header before deciding whether to reply. All + // response paths below require the endpoint-bound challenge first, which + // prevents malformed or spoofed connect packets from becoming reflectors. + const int fixedHeaderBytes = 4 + 2 + 4 + 4 + 2 + 4; + if ( msg.GetRemainingData() < fixedHeaderBytes ) { + return; + } protocol = msg.ReadLong(); OS = msg.ReadShort(); + clientDataChecksum = msg.ReadLong(); + challenge = msg.ReadLong(); + clientId = msg.ReadShort(); + clientRate = msg.ReadLong(); + if ( OS < 0 || OS >= MAX_GAME_OS ) { + common->DPrintf( "connect from %s rejected: invalid OS %d\n", Sys_NetAdrToString( from ), OS ); + return; + } + if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) ) { + const int osMask = fileSystem->GetOSMask(); + if ( osMask < 0 || ( static_cast( osMask ) & ( 1u << OS ) ) == 0 ) { + common->DPrintf( "connect from %s rejected: unsupported pure OS %d\n", Sys_NetAdrToString( from ), OS ); + return; + } + } + + if ( ( ichallenge = ValidateChallenge( from, challenge, clientId ) ) == -1 ) { + return; + } + if ( !AllowConnectionlessResponse( from, false ) ) { + return; + } - // check the protocol version + // check the protocol version only after the request proves possession of + // the challenge issued to this exact endpoint. if ( protocol != ASYNC_PROTOCOL_VERSION ) { - // that's a msg back to a client, we don't know about it's localization, so send english PrintOOB( from, SERVER_PRINT_BADPROTOCOL, va( "server uses protocol %d.%d\n", ASYNC_PROTOCOL_MAJOR, ASYNC_PROTOCOL_MINOR ) ); return; } - clientDataChecksum = msg.ReadLong(); - challenge = msg.ReadLong(); - clientId = msg.ReadShort(); - clientRate = msg.ReadLong(); - // check the client data - only for non pure servers if ( !sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && clientDataChecksum != serverDataChecksum ) { common->DPrintf( "Decl checksum mismatch from %s: client=0x%08x server=0x%08x (non-pure)\n", @@ -1833,16 +2262,21 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & return; } - if ( ( ichallenge = ValidateChallenge( from, challenge, clientId ) ) == -1 ) { + msg.ReadString( guid, sizeof( guid ) ); + if ( msg.IsReadOverflowed() ) { + common->DPrintf( "connect from %s rejected: truncated GUID\n", Sys_NetAdrToString( from ) ); return; } challenges[ ichallenge ].OS = OS; - msg.ReadString( guid, sizeof( guid ) ); - switch ( challenges[ ichallenge ].authState ) { case CDK_PUREWAIT: - SendPureServerMessage( from, OS ); + if ( !SendPureServerMessage( from, OS ) ) { + common->DPrintf( "client %s: pure challenge resend failed; rejecting connection\n", + Sys_NetAdrToString( from ) ); + PrintOOB( from, SERVER_PRINT_MISC, "#str_04337" ); + AsyncServer_ClearChallenge( challenges[ ichallenge ] ); + } return; case CDK_ONLYLAN: common->DPrintf( "%s: not a lan client\n", Sys_NetAdrToString( from ) ); @@ -1864,8 +2298,14 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & // if authState == CDK_PUREOK, the check was already performed once before entering pure checks // but meanwhile, the max players may have been reached msg.ReadString( password, sizeof( password ) ); + if ( msg.IsReadOverflowed() ) { + idCrypto::SecureZero( password, sizeof( password ) ); + common->DPrintf( "connect from %s rejected: truncated password\n", Sys_NetAdrToString( from ) ); + return; + } char reason[MAX_STRING_CHARS]; allowReply_t reply = game->ServerAllowClient(clientId, numClients, Sys_NetAdrToString( from ), guid, password, password, reason ); + idCrypto::SecureZero( password, sizeof( password ) ); if ( reply != ALLOW_YES ) { common->DPrintf( "game denied connection for %s\n", Sys_NetAdrToString( from ) ); @@ -1883,10 +2323,15 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & // enter pure checks if necessary if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && challenges[ ichallenge ].authState != CDK_PUREOK ) { - if ( SendPureServerMessage( from, OS ) ) { - challenges[ ichallenge ].authState = CDK_PUREWAIT; + if ( !SendPureServerMessage( from, OS ) ) { + common->DPrintf( "client %s: pure challenge could not be issued; rejecting connection\n", + Sys_NetAdrToString( from ) ); + PrintOOB( from, SERVER_PRINT_MISC, "#str_04337" ); + AsyncServer_ClearChallenge( challenges[ ichallenge ] ); return; } + challenges[ ichallenge ].authState = CDK_PUREWAIT; + return; } // push back decl checksum here when running pure. just an additional safe check @@ -1898,7 +2343,9 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & return; } - ping = serverTime - challenges[ ichallenge ].pingTime; + const std::uint32_t pingElapsed = AsyncServer_Elapsed( serverTime, challenges[ ichallenge ].pingTime ); + ping = pingElapsed > static_cast( idMath::INT_MAX ) ? idMath::INT_MAX : + static_cast( pingElapsed ); common->Printf( "challenge from %s connecting with %d ping\n", Sys_NetAdrToString( from ), ping ); challenges[ ichallenge ].connected = true; @@ -1964,7 +2411,7 @@ void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg & clients[clientNum].snapshotSequence = 1; // clear the challenge struct so a reconnect from this client IP starts clean - memset( &challenges[ ichallenge ], 0, sizeof( challenge_t ) ); + AsyncServer_ClearChallenge( challenges[ ichallenge ] ); } /* @@ -2042,7 +2489,9 @@ void idAsyncServer::ProcessPureMessage( const netadr_t from, const idBitMsg &msg } if ( !VerifyChecksumMessage( iclient, &from, msg, reply, challenges[ iclient ].OS ) ) { - PrintOOB( from, SERVER_PRINT_MISC, reply ); + if ( AllowConnectionlessResponse( from, false ) ) { + PrintOOB( from, SERVER_PRINT_MISC, reply ); + } return; } @@ -2072,7 +2521,7 @@ void idAsyncServer::ProcessReliablePure( int clientNum, const idBitMsg &msg ) { common->DPrintf( "client %d: got reliable pure while != SCS_PUREWAIT, sending a reload\n", clientNum ); outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD ); - SendReliableMessage( clientNum, msg ); + SendReliableMessage( clientNum, outMsg ); // go back to SCS_CONNECTED to sleep on the client until it goes away for a reconnect clients[ clientNum ].clientState = SCS_CONNECTED; return; @@ -2107,41 +2556,264 @@ void RConRedirect( const char *string ) { /* ================== -idAsyncServer::ProcessRemoteConsoleMessage +idAsyncServer::ExecuteRemoteConsoleCommand ================== */ -void idAsyncServer::ProcessRemoteConsoleMessage( const netadr_t from, const idBitMsg &msg ) { - idBitMsg outMsg; +void idAsyncServer::ExecuteRemoteConsoleCommand( const netadr_t from, const char *command, bool authenticated ) { byte msgBuf[952]; - char string[MAX_STRING_CHARS]; + common->Printf( "%s remote console command accepted from %s\n", + authenticated ? "authenticated" : "legacy plaintext", Sys_NetAdrToString( from ) ); - if ( idAsyncNetwork::serverRemoteConsolePassword.GetString()[0] == '\0' ) { - PrintOOB( from, SERVER_PRINT_MISC, "#str_04846" ); - return; + rconAddress = from; + noRconOutput = true; + common->BeginRedirect( (char *)msgBuf, sizeof( msgBuf ), RConRedirect ); + cmdSystem->BufferCommandText( CMD_EXEC_NOW, command ); + common->EndRedirect(); + + if ( noRconOutput ) { + PrintOOB( rconAddress, SERVER_PRINT_RCON, "#str_04848" ); } +} - msg.ReadString( string, sizeof( string ) ); +/* +================== +idAsyncServer::SendRemoteConsole2Complete +================== +*/ +void idAsyncServer::SendRemoteConsole2Complete( const netadr_t to, + const byte clientNonce[16], const byte serverNonce[16] ) { + byte msgBuf[128]; + idBitMsg outMsg; + outMsg.Init( msgBuf, sizeof( msgBuf ) ); + outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); + outMsg.WriteString( "rcon2Complete" ); + outMsg.WriteByte( idRcon2::PROTOCOL_VERSION ); + outMsg.WriteData( clientNonce, idRcon2::NONCE_BYTES ); + outMsg.WriteData( serverNonce, idRcon2::NONCE_BYTES ); + serverPort.SendPacket( to, outMsg.GetData(), outMsg.GetSize() ); +} - if ( idStr::Icmp( string, idAsyncNetwork::serverRemoteConsolePassword.GetString() ) != 0 ) { - PrintOOB( from, SERVER_PRINT_MISC, "#str_04847" ); +/* +================== +idAsyncServer::ProcessRemoteConsoleMessage + +Quake 4's original packet sends the password verbatim. It remains available +only as an explicit two-sided compatibility escape hatch and is still bounded +and constant-time checked. +================== +*/ +void idAsyncServer::ProcessRemoteConsoleMessage( const netadr_t from, const idBitMsg &msg ) { + char suppliedPassword[MAX_STRING_CHARS] = {}; + char command[MAX_STRING_CHARS] = {}; + if ( !idAsyncNetwork::serverAllowLegacyRcon.GetBool() || + idAsyncNetwork::serverRemoteConsolePassword.GetString()[0] == '\0' ) { + return; + } + if ( !AllowRconChallenge( from ) || !AllowConnectionlessResponse( from, false ) ) { + return; + } + msg.ReadString( suppliedPassword, sizeof( suppliedPassword ) ); + msg.ReadString( command, sizeof( command ) ); + if ( msg.GetRemainingData() != 0 || command[0] == '\0' ) { + idCrypto::SecureZero( suppliedPassword, sizeof( suppliedPassword ) ); + idCrypto::SecureZero( command, sizeof( command ) ); return; } - msg.ReadString( string, sizeof( string ) ); - - common->Printf( "rcon from %s: %s\n", Sys_NetAdrToString( from ), string ); + const char *configuredPassword = idAsyncNetwork::serverRemoteConsolePassword.GetString(); + const size_t suppliedBytes = strlen( suppliedPassword ); + const size_t configuredBytes = strlen( configuredPassword ); + const bool passwordMatches = suppliedBytes == configuredBytes && + idCrypto::ConstantTimeEquals( suppliedPassword, configuredPassword, configuredBytes ); + if ( !passwordMatches ) { + RecordRconFailure( from ); + PrintOOB( from, SERVER_PRINT_MISC, "#str_04847" ); + idCrypto::SecureZero( suppliedPassword, sizeof( suppliedPassword ) ); + idCrypto::SecureZero( command, sizeof( command ) ); + return; + } - rconAddress = from; - noRconOutput = true; - common->BeginRedirect( (char *)msgBuf, sizeof( msgBuf ), RConRedirect ); + idCrypto::SecureZero( suppliedPassword, sizeof( suppliedPassword ) ); + ExecuteRemoteConsoleCommand( from, command, false ); + idCrypto::SecureZero( command, sizeof( command ) ); +} - cmdSystem->BufferCommandText( CMD_EXEC_NOW, string ); +/* +================== +idAsyncServer::ProcessRemoteConsole2ChallengeMessage +================== +*/ +void idAsyncServer::ProcessRemoteConsole2ChallengeMessage( const netadr_t from, const idBitMsg &msg ) { + const int requestBytes = 1 + idRcon2::NONCE_BYTES + idRcon2::REQUEST_DIGEST_BYTES; + if ( !active || msg.GetRemainingData() != requestBytes || !RefreshRcon2Verifier() ) { + return; + } + if ( msg.ReadByte() != idRcon2::PROTOCOL_VERSION ) { + return; + } + byte clientNonce[idRcon2::NONCE_BYTES]; + byte requestDigest[idRcon2::REQUEST_DIGEST_BYTES]; + msg.ReadData( clientNonce, sizeof( clientNonce ) ); + msg.ReadData( requestDigest, sizeof( requestDigest ) ); + + int slot = -1; + int oldest = 0; + std::uint32_t oldestAge = 0; + for ( int index = 0; index < MAX_RCON2_CHALLENGES; ++index ) { + rcon2Challenge_t &candidate = rcon2Challenges[ index ]; + if ( candidate.active && + AsyncServer_Elapsed( serverTime, candidate.createdTime ) > RCON2_CHALLENGE_TIMEOUT_MSEC ) { + idCrypto::SecureZero( &candidate, sizeof( candidate ) ); + } + if ( candidate.active && AsyncServer_SameEndpoint( from, candidate.address ) && + idCrypto::ConstantTimeEquals( clientNonce, candidate.clientNonce, sizeof( clientNonce ) ) && + idCrypto::ConstantTimeEquals( requestDigest, candidate.requestDigest, sizeof( requestDigest ) ) ) { + slot = index; + break; + } + if ( !candidate.active ) { + oldest = index; + oldestAge = static_cast( -1 ); + } else { + const std::uint32_t age = AsyncServer_Elapsed( serverTime, candidate.createdTime ); + if ( oldestAge != static_cast( -1 ) && age > oldestAge ) { + oldest = index; + oldestAge = age; + } + } + } + if ( !AllowRconChallenge( from ) || !AllowConnectionlessResponse( from, false ) ) { + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); + return; + } + if ( slot < 0 ) { + slot = oldest; + rcon2Challenge_t &issued = rcon2Challenges[ slot ]; + idCrypto::SecureZero( &issued, sizeof( issued ) ); + byte randomValues[idRcon2::NONCE_BYTES + idRcon2::ENDPOINT_BINDING_BYTES]; + if ( !Sys_GetSecureRandomBytes( randomValues, sizeof( randomValues ) ) ) { + common->Warning( "OS secure random unavailable; rcon2 challenge not issued" ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); + return; + } + issued.active = true; + issued.address = from; + issued.createdTime = serverTime; + memcpy( issued.clientNonce, clientNonce, sizeof( issued.clientNonce ) ); + memcpy( issued.serverNonce, randomValues, sizeof( issued.serverNonce ) ); + memcpy( issued.endpointBinding, randomValues + sizeof( issued.serverNonce ), + sizeof( issued.endpointBinding ) ); + memcpy( issued.requestDigest, requestDigest, sizeof( issued.requestDigest ) ); + idCrypto::SecureZero( randomValues, sizeof( randomValues ) ); + } + + rcon2Challenge_t &issued = rcon2Challenges[ slot ]; + issued.lastResponseTime = serverTime; + byte msgBuf[192]; + idBitMsg outMsg; + outMsg.Init( msgBuf, sizeof( msgBuf ) ); + outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); + outMsg.WriteString( "rcon2ChallengeResponse" ); + outMsg.WriteByte( idRcon2::PROTOCOL_VERSION ); + outMsg.WriteData( issued.clientNonce, sizeof( issued.clientNonce ) ); + outMsg.WriteData( issued.serverNonce, sizeof( issued.serverNonce ) ); + outMsg.WriteData( rcon2Salt, sizeof( rcon2Salt ) ); + outMsg.WriteLong( static_cast( idRcon2::PBKDF2_ITERATIONS ) ); + outMsg.WriteData( issued.endpointBinding, sizeof( issued.endpointBinding ) ); + outMsg.WriteData( issued.requestDigest, sizeof( issued.requestDigest ) ); + serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); +} - common->EndRedirect(); +/* +================== +idAsyncServer::ProcessRemoteConsole2Message +================== +*/ +void idAsyncServer::ProcessRemoteConsole2Message( const netadr_t from, const idBitMsg &msg ) { + const int fixedPrefixBytes = 1 + idRcon2::NONCE_BYTES + idRcon2::NONCE_BYTES; + if ( !active || msg.GetRemainingData() < fixedPrefixBytes + 1 + idRcon2::PROOF_BYTES || + !RefreshRcon2Verifier() || !AllowRconProofAttempt( from ) ) { + return; + } + if ( msg.ReadByte() != idRcon2::PROTOCOL_VERSION ) { + return; + } + byte clientNonce[idRcon2::NONCE_BYTES]; + byte serverNonce[idRcon2::NONCE_BYTES]; + msg.ReadData( clientNonce, sizeof( clientNonce ) ); + msg.ReadData( serverNonce, sizeof( serverNonce ) ); + + int slot = -1; + for ( int index = 0; index < MAX_RCON2_CHALLENGES; ++index ) { + const rcon2Challenge_t &candidate = rcon2Challenges[ index ]; + if ( candidate.active && + AsyncServer_Elapsed( serverTime, candidate.createdTime ) <= RCON2_CHALLENGE_TIMEOUT_MSEC && + AsyncServer_SameEndpoint( from, candidate.address ) && + idCrypto::ConstantTimeEquals( clientNonce, candidate.clientNonce, sizeof( clientNonce ) ) && + idCrypto::ConstantTimeEquals( serverNonce, candidate.serverNonce, sizeof( serverNonce ) ) ) { + slot = index; + break; + } + } + if ( slot < 0 ) { + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); + return; + } - if ( noRconOutput ) { - PrintOOB( rconAddress, SERVER_PRINT_RCON, "#str_04848" ); + rcon2Challenge_t issued = rcon2Challenges[ slot ]; + // Consume before parsing or checking the proof: malformed, failed, and + // successful attempts are all one-shot and cannot be replayed. + idCrypto::SecureZero( &rcon2Challenges[ slot ], sizeof( rcon2Challenges[ slot ] ) ); + char command[MAX_STRING_CHARS] = {}; + msg.ReadString( command, sizeof( command ) ); + if ( command[0] == '\0' || msg.GetRemainingData() != idRcon2::PROOF_BYTES ) { + RecordRconFailure( from ); + idCrypto::SecureZero( command, sizeof( command ) ); + idCrypto::SecureZero( &issued, sizeof( issued ) ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); + return; } + byte suppliedProof[idRcon2::PROOF_BYTES]; + byte expectedProof[idRcon2::PROOF_BYTES]; + byte requestDigest[idRcon2::REQUEST_DIGEST_BYTES]; + msg.ReadData( suppliedProof, sizeof( suppliedProof ) ); + idRcon2::HashRequest( command, requestDigest ); + idRcon2::ComputeProof( rcon2Verifier, issued.clientNonce, issued.serverNonce, + issued.endpointBinding, issued.requestDigest, expectedProof ); + const bool requestMatches = idCrypto::ConstantTimeEquals( requestDigest, + issued.requestDigest, sizeof( requestDigest ) ); + const bool proofMatches = idCrypto::ConstantTimeEquals( suppliedProof, + expectedProof, sizeof( suppliedProof ) ); + + if ( !requestMatches || !proofMatches ) { + RecordRconFailure( from ); + PrintOOB( from, SERVER_PRINT_MISC, "#str_04847" ); + } else { + for ( int index = 0; index < MAX_RCON_RATE_LIMITS; ++index ) { + if ( rconRateLimits[ index ].active && + Sys_CompareNetAdrBase( from, rconRateLimits[ index ].address ) ) { + rconRateLimits[ index ].failureCount = 0; + rconRateLimits[ index ].failureWindowStart = serverTime; + break; + } + } + ExecuteRemoteConsoleCommand( from, command, true ); + } + SendRemoteConsole2Complete( from, issued.clientNonce, issued.serverNonce ); + + idCrypto::SecureZero( command, sizeof( command ) ); + idCrypto::SecureZero( suppliedProof, sizeof( suppliedProof ) ); + idCrypto::SecureZero( expectedProof, sizeof( expectedProof ) ); + idCrypto::SecureZero( requestDigest, sizeof( requestDigest ) ); + idCrypto::SecureZero( &issued, sizeof( issued ) ); + idCrypto::SecureZero( clientNonce, sizeof( clientNonce ) ); + idCrypto::SecureZero( serverNonce, sizeof( serverNonce ) ); } /* @@ -2157,6 +2829,9 @@ void idAsyncServer::ProcessGetInfoMessage( const netadr_t from, const idBitMsg & if ( !IsActive() ) { return; } + if ( msg.GetRemainingData() != 4 || !AllowConnectionlessResponse( from, true ) ) { + return; + } common->DPrintf( "Sending info response to %s\n", Sys_NetAdrToString( from ) ); @@ -2235,9 +2910,19 @@ bool idAsyncServer::ConnectionlessMessage( const netadr_t from, const idBitMsg & ProcessRemoteConsoleMessage( from, msg ); return true; } + if ( idStr::Icmp( string, "rcon2Challenge" ) == 0 ) { + ProcessRemoteConsole2ChallengeMessage( from, msg ); + return false; + } + if ( idStr::Icmp( string, "rcon2" ) == 0 ) { + ProcessRemoteConsole2Message( from, msg ); + return true; + } if ( !active ) { - PrintOOB( from, SERVER_PRINT_MISC, "#str_04849" ); + if ( AllowConnectionlessResponse( from, false ) ) { + PrintOOB( from, SERVER_PRINT_MISC, "#str_04849" ); + } return false; } @@ -2336,7 +3021,12 @@ bool idAsyncServer::ProcessMessage( const netadr_t from, idBitMsg &msg ) { } // if we received a sequenced packet from an address we don't recognize, - // send an out of band disconnect packet to it + // send an out of band disconnect packet to it. This is still a pre-auth + // response, so share the global/per-source control budget used by the + // connectionless admission paths. + if ( !AllowConnectionlessResponse( from, false ) ) { + return false; + } outMsg.Init( msgBuf, sizeof( msgBuf ) ); outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID ); outMsg.WriteString( "disconnect" ); @@ -2826,6 +3516,9 @@ void idAsyncServer::ProcessDownloadRequestMessage( const netadr_t from, const id common->DPrintf( "client %s: got download request message, not in CDK_PUREWAIT\n", Sys_NetAdrToString( from ) ); return; } + if ( !AllowConnectionlessResponse( from, false ) ) { + return; + } // the first token of the pak names list passed to the game will be empty if no game pak is requested dlGamePak = msg.ReadLong(); diff --git a/src/framework/async/AsyncServer.h b/src/framework/async/AsyncServer.h index 79d75bad..64deeffb 100644 --- a/src/framework/async/AsyncServer.h +++ b/src/framework/async/AsyncServer.h @@ -77,6 +77,7 @@ typedef enum { } authReplyMsg_t; typedef struct challenge_s { + bool valid; // entry contains an issued, unexpired challenge netadr_t address; // client address int clientId; // client identification int challenge; // challenge code @@ -91,6 +92,40 @@ typedef struct challenge_s { int OS; } challenge_t; +const int MAX_RCON2_CHALLENGES = 64; +const int MAX_RCON_RATE_LIMITS = 64; +const int MAX_OOB_RATE_LIMITS = 128; + +typedef struct rcon2Challenge_s { + bool active; + netadr_t address; + int createdTime; + int lastResponseTime; + byte clientNonce[ 16 ]; + byte serverNonce[ 16 ]; + byte endpointBinding[ 16 ]; + byte requestDigest[ 32 ]; +} rcon2Challenge_t; + +typedef struct rconRateLimit_s { + bool active; + netadr_t address; + int challengeWindowStart; + int lastChallengeTime; + int challengeCount; + int failureWindowStart; + int failureCount; + int blockedUntil; +} rconRateLimit_t; + +typedef struct oobRateLimit_s { + bool active; + netadr_t address; + int windowStart; + int infoResponses; + int challengeResponses; +} oobRateLimit_t; + typedef enum { SCS_FREE, // can be reused for a new connection SCS_ZOMBIE, // client has been disconnected, but don't reuse connection for a couple seconds @@ -196,6 +231,9 @@ class idAsyncServer { int localClientNum; // local client on listen server challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting + rcon2Challenge_t rcon2Challenges[MAX_RCON2_CHALLENGES]; + rconRateLimit_t rconRateLimits[MAX_RCON_RATE_LIMITS]; + oobRateLimit_t oobRateLimits[MAX_OOB_RATE_LIMITS]; serverClient_t clients[MAX_ASYNC_CLIENTS]; // clients usercmd_t userCmds[MAX_USERCMD_BACKUP][MAX_ASYNC_CLIENTS]; @@ -212,6 +250,13 @@ class idAsyncServer { bool serverReloadingEngine; // flip-flop to not loop over when net_serverReloadEngine is on bool noRconOutput; // for default rcon response when command is silent + bool rcon2VerifierInitialized; + bool rcon2VerifierValid; + byte rcon2Salt[16]; + byte rcon2Verifier[32]; + int oobWindowStart; + int oobInfoResponses; + int oobChallengeResponses; int lastAuthTime; // global for auth server timeout @@ -250,6 +295,8 @@ class idAsyncServer { void ProcessChallengeMessage( const netadr_t from, const idBitMsg &msg ); void ProcessConnectMessage( const netadr_t from, const idBitMsg &msg ); void ProcessRemoteConsoleMessage( const netadr_t from, const idBitMsg &msg ); + void ProcessRemoteConsole2ChallengeMessage( const netadr_t from, const idBitMsg &msg ); + void ProcessRemoteConsole2Message( const netadr_t from, const idBitMsg &msg ); void ProcessGetInfoMessage( const netadr_t from, const idBitMsg &msg ); bool ConnectionlessMessage( const netadr_t from, const idBitMsg &msg ); bool ProcessMessage( const netadr_t from, idBitMsg &msg ); @@ -257,6 +304,14 @@ class idAsyncServer { bool SendPureServerMessage( const netadr_t to, int OS ); // returns false if no pure paks on the list void ProcessPureMessage( const netadr_t from, const idBitMsg &msg ); int ValidateChallenge( const netadr_t from, int challenge, int clientId ); // returns -1 if validate failed + bool AllowConnectionlessResponse( const netadr_t from, bool infoResponse ); + bool RefreshRcon2Verifier( void ); + bool AllowRconChallenge( const netadr_t from ); + bool AllowRconProofAttempt( const netadr_t from ); + void RecordRconFailure( const netadr_t from ); + void ClearRconSecurityState( bool clearRateLimits ); + void ExecuteRemoteConsoleCommand( const netadr_t from, const char *command, bool authenticated ); + void SendRemoteConsole2Complete( const netadr_t to, const byte clientNonce[16], const byte serverNonce[16] ); bool SendReliablePureToClient( int clientNum ); void ProcessReliablePure( int clientNum, const idBitMsg &msg ); bool VerifyChecksumMessage( int clientNum, const netadr_t *from, const idBitMsg &msg, idStr &reply, int OS ); // if from is NULL, clientNum is used for error messages diff --git a/src/framework/async/MultiViewDemo.cpp b/src/framework/async/MultiViewDemo.cpp index da227c1f..ad68bdf6 100644 --- a/src/framework/async/MultiViewDemo.cpp +++ b/src/framework/async/MultiViewDemo.cpp @@ -47,10 +47,13 @@ static const int MVD_MAX_INSTANCES = 8; // record grammar did not change. API 40 only appended the versioned MVD // callbacks and retained the API-39 snapshot/reliable readers. The prior // engine also explicitly accepted API 42, whose intervening API changes did -// not alter those legacy readers. Preserve both known-compatible versions +// not alter those legacy readers. API 44 differs from API 45 only at the +// engine/game presentation interface and leaves the recorded grammar intact. +// Preserve each known-compatible version // explicitly instead of accepting an unsafe range of historical API numbers. static const unsigned int MVD_LEGACY_GAME_API_1_0_1_1 = 39u; static const unsigned int MVD_LEGACY_GAME_API_PRE_SHUTDOWN_SPLIT = 42u; +static const unsigned int MVD_LEGACY_GAME_API_PRE_PRESENTATION_STATE = 44u; enum mvdFeature_t { MVD_FEATURE_RECORD_CRC = 1u << 0, @@ -212,6 +215,7 @@ static int MVD_SchemaMinor( unsigned int version ) { static bool MVD_IsLegacyGameAPICompatible( unsigned int version ) { return version == MVD_LEGACY_GAME_API_1_0_1_1 || version == MVD_LEGACY_GAME_API_PRE_SHUTDOWN_SPLIT || + version == MVD_LEGACY_GAME_API_PRE_PRESENTATION_STATE || version == static_cast( GAME_API_VERSION ); } @@ -2003,7 +2007,7 @@ bool idMultiViewDemo::StartPlayback( const idCmdArgs &args ) { return false; } - cvarSystem->SetCVarsFromDict( sessLocal.mapSpawnData.syncedCVars ); + cvarSystem->SetCVarsFromDictByFlags( sessLocal.mapSpawnData.syncedCVars, CVAR_NETWORKSYNC ); sessLocal.ExecuteMapChange(); game->SetDemoState( DEMO_PLAYING, true, false ); for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { @@ -2072,7 +2076,7 @@ bool idMultiViewDemo::ResetPlaybackStream() { return false; } - cvarSystem->SetCVarsFromDict( sessLocal.mapSpawnData.syncedCVars ); + cvarSystem->SetCVarsFromDictByFlags( sessLocal.mapSpawnData.syncedCVars, CVAR_NETWORKSYNC ); sessLocal.ExecuteMapChange(); game->SetDemoState( DEMO_PLAYING, true, false ); for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) { diff --git a/src/framework/async/Rcon2Protocol.cpp b/src/framework/async/Rcon2Protocol.cpp new file mode 100644 index 00000000..57c54f50 --- /dev/null +++ b/src/framework/async/Rcon2Protocol.cpp @@ -0,0 +1,69 @@ +/* +=========================================================================== + +openQ4 authenticated remote-console protocol +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#include "Rcon2Protocol.h" + +#include + +namespace idRcon2 { + +void HashRequest( const char *command, + std::uint8_t digest[ REQUEST_DIGEST_BYTES ] ) { + if ( command == nullptr ) { + idCrypto::SHA256( nullptr, 0, digest ); + return; + } + idCrypto::SHA256( command, std::strlen( command ), digest ); +} + +bool DeriveVerifier( const char *password, + const std::uint8_t salt[ SALT_BYTES ], + std::uint8_t verifier[ VERIFIER_BYTES ] ) { + if ( password == nullptr || salt == nullptr || verifier == nullptr ) { + return false; + } + const std::size_t passwordBytes = std::strlen( password ); + if ( passwordBytes < MIN_PASSWORD_BYTES ) { + return false; + } + return idCrypto::PBKDF2HMACSHA256( password, passwordBytes, + salt, SALT_BYTES, PBKDF2_ITERATIONS, verifier, VERIFIER_BYTES ); +} + +void ComputeProof( const std::uint8_t verifier[ VERIFIER_BYTES ], + const std::uint8_t clientNonce[ NONCE_BYTES ], + const std::uint8_t serverNonce[ NONCE_BYTES ], + const std::uint8_t endpointBinding[ ENDPOINT_BINDING_BYTES ], + const std::uint8_t requestDigest[ REQUEST_DIGEST_BYTES ], + std::uint8_t proof[ PROOF_BYTES ] ) { + static constexpr char PROOF_DOMAIN[] = "openQ4-rcon2-proof-v1"; + static constexpr std::size_t PROOF_DOMAIN_BYTES = sizeof( PROOF_DOMAIN ) - 1; + std::uint8_t message[ PROOF_DOMAIN_BYTES + 1 + NONCE_BYTES + NONCE_BYTES + + ENDPOINT_BINDING_BYTES + REQUEST_DIGEST_BYTES ]; + std::size_t offset = 0; + std::memcpy( message + offset, PROOF_DOMAIN, PROOF_DOMAIN_BYTES ); + offset += PROOF_DOMAIN_BYTES; + message[ offset++ ] = PROTOCOL_VERSION; + std::memcpy( message + offset, clientNonce, NONCE_BYTES ); + offset += NONCE_BYTES; + std::memcpy( message + offset, serverNonce, NONCE_BYTES ); + offset += NONCE_BYTES; + std::memcpy( message + offset, endpointBinding, ENDPOINT_BINDING_BYTES ); + offset += ENDPOINT_BINDING_BYTES; + std::memcpy( message + offset, requestDigest, REQUEST_DIGEST_BYTES ); + idCrypto::HMACSHA256( verifier, VERIFIER_BYTES, message, sizeof( message ), proof ); + idCrypto::SecureZero( message, sizeof( message ) ); +} + +} // namespace idRcon2 diff --git a/src/framework/async/Rcon2Protocol.h b/src/framework/async/Rcon2Protocol.h new file mode 100644 index 00000000..651986a0 --- /dev/null +++ b/src/framework/async/Rcon2Protocol.h @@ -0,0 +1,51 @@ +/* +=========================================================================== + +openQ4 authenticated remote-console protocol +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __RCON2PROTOCOL_H__ +#define __RCON2PROTOCOL_H__ + +#include "../../idlib/CryptoHash.h" + +#include +#include + +namespace idRcon2 { + +static constexpr std::uint8_t PROTOCOL_VERSION = 1; +static constexpr std::size_t NONCE_BYTES = 16; +static constexpr std::size_t SALT_BYTES = 16; +static constexpr std::size_t ENDPOINT_BINDING_BYTES = 16; +static constexpr std::size_t REQUEST_DIGEST_BYTES = idCrypto::SHA256_DIGEST_BYTES; +static constexpr std::size_t PROOF_BYTES = idCrypto::SHA256_DIGEST_BYTES; +static constexpr std::size_t VERIFIER_BYTES = idCrypto::SHA256_DIGEST_BYTES; +static constexpr std::uint32_t PBKDF2_ITERATIONS = 200000; +static constexpr std::size_t MIN_PASSWORD_BYTES = 12; + +void HashRequest( const char *command, + std::uint8_t digest[ REQUEST_DIGEST_BYTES ] ); + +bool DeriveVerifier( const char *password, + const std::uint8_t salt[ SALT_BYTES ], + std::uint8_t verifier[ VERIFIER_BYTES ] ); + +void ComputeProof( const std::uint8_t verifier[ VERIFIER_BYTES ], + const std::uint8_t clientNonce[ NONCE_BYTES ], + const std::uint8_t serverNonce[ NONCE_BYTES ], + const std::uint8_t endpointBinding[ ENDPOINT_BINDING_BYTES ], + const std::uint8_t requestDigest[ REQUEST_DIGEST_BYTES ], + std::uint8_t proof[ PROOF_BYTES ] ); + +} // namespace idRcon2 + +#endif /* !__RCON2PROTOCOL_H__ */ diff --git a/src/framework/licensee.h b/src/framework/licensee.h index 1acd0d0e..3a751a65 100644 --- a/src/framework/licensee.h +++ b/src/framework/licensee.h @@ -19,6 +19,7 @@ #define PROJECT_VERSION_DATE OPENQ4_VERSION_DATE #define PROJECT_WEBSITE "www.darkmatter-quake.com" #define PROJECT_REPO "https://github.com/themuffinator/openQ4" +#define PROJECT_RELEASES_URL PROJECT_REPO "/releases" #define GAME_NAME PROJECT_NAME // appears on errors and secondary windows #define GAME_ICON "q4icon.bmp" diff --git a/src/idlib/BitMsg.cpp b/src/idlib/BitMsg.cpp index a308303a..de3558df 100644 --- a/src/idlib/BitMsg.cpp +++ b/src/idlib/BitMsg.cpp @@ -81,7 +81,7 @@ idBitMsg::WriteBits */ void idBitMsg::WriteBits( int value, int numBits ) { int put; - int fraction; + uint32_t fraction; if ( !writeData ) { idLib::common->Error( "idBitMsg::WriteBits: cannot write to message" ); @@ -95,13 +95,12 @@ void idBitMsg::WriteBits( int value, int numBits ) { // check for value overflows if ( numBits != 32 ) { if ( numBits > 0 ) { - if ( value > ( 1 << numBits ) - 1 ) { - idLib::common->Warning( "idBitMsg::WriteBits: value overflow %d %d", value, numBits ); - } else if ( value < 0 ) { + const uint32_t maximumValue = ( 1u << numBits ) - 1u; + if ( value < 0 || static_cast( value ) > maximumValue ) { idLib::common->Warning( "idBitMsg::WriteBits: value overflow %d %d", value, numBits ); } } else { - int r = 1 << ( - 1 - numBits ); + const int64_t r = static_cast( 1 ) << ( -1 - numBits ); if ( value > r - 1 ) { idLib::common->Warning( "idBitMsg::WriteBits: value overflow %d %d", value, numBits ); } else if ( value < -r ) { @@ -113,6 +112,7 @@ void idBitMsg::WriteBits( int value, int numBits ) { if ( numBits < 0 ) { numBits = -numBits; } + uint32_t unsignedValue = static_cast( value ); // check for msg overflow if ( CheckOverflow( numBits ) ) { @@ -129,10 +129,10 @@ void idBitMsg::WriteBits( int value, int numBits ) { if ( put > numBits ) { put = numBits; } - fraction = value & ( ( 1 << put ) - 1 ); - writeData[curSize - 1] |= fraction << writeBit; + fraction = unsignedValue & ( ( 1u << put ) - 1u ); + writeData[curSize - 1] |= static_cast( fraction << writeBit ); numBits -= put; - value >>= put; + unsignedValue >>= put; writeBit = ( writeBit + put ) & 7; } } @@ -204,18 +204,26 @@ idBitMsg::WriteDeltaByteCounter ================ */ void idBitMsg::WriteDeltaByteCounter( int oldValue, int newValue ) { - int i, x; + int i; + const uint32_t x = static_cast( oldValue ^ newValue ); - x = oldValue ^ newValue; - for ( i = 7; i > 0; i-- ) { - if ( x & ( 1 << i ) ) { + if ( x & ( 1u << 7 ) ) { + idLib::common->Warning( "idBitMsg::WriteDeltaByteCounter: unencodable high-bit change" ); + WriteBits( 0, 3 ); + return; + } + for ( i = 6; i >= 0; i-- ) { + if ( x & ( 1u << i ) ) { i++; break; } } + if ( i < 0 ) { + i = 0; + } WriteBits( i, 3 ); if ( i ) { - WriteBits( ( ( 1 << i ) - 1 ) & newValue, i ); + WriteBits( static_cast( ( ( 1u << i ) - 1u ) & static_cast( newValue ) ), i ); } } @@ -225,18 +233,26 @@ idBitMsg::WriteDeltaShortCounter ================ */ void idBitMsg::WriteDeltaShortCounter( int oldValue, int newValue ) { - int i, x; + int i; + const uint32_t x = static_cast( oldValue ^ newValue ); - x = oldValue ^ newValue; - for ( i = 15; i > 0; i-- ) { - if ( x & ( 1 << i ) ) { + if ( x & ( 1u << 15 ) ) { + idLib::common->Warning( "idBitMsg::WriteDeltaShortCounter: unencodable high-bit change" ); + WriteBits( 0, 4 ); + return; + } + for ( i = 14; i >= 0; i-- ) { + if ( x & ( 1u << i ) ) { i++; break; } } + if ( i < 0 ) { + i = 0; + } WriteBits( i, 4 ); if ( i ) { - WriteBits( ( ( 1 << i ) - 1 ) & newValue, i ); + WriteBits( static_cast( ( ( 1u << i ) - 1u ) & static_cast( newValue ) ), i ); } } @@ -246,18 +262,26 @@ idBitMsg::WriteDeltaLongCounter ================ */ void idBitMsg::WriteDeltaLongCounter( int oldValue, int newValue ) { - int i, x; + int i; + const uint32_t x = static_cast( oldValue ^ newValue ); - x = oldValue ^ newValue; - for ( i = 31; i > 0; i-- ) { - if ( x & ( 1 << i ) ) { + if ( x & ( 1u << 31 ) ) { + idLib::common->Warning( "idBitMsg::WriteDeltaLongCounter: unencodable high-bit change" ); + WriteBits( 0, 5 ); + return; + } + for ( i = 30; i >= 0; i-- ) { + if ( x & ( 1u << i ) ) { i++; break; } } + if ( i < 0 ) { + i = 0; + } WriteBits( i, 5 ); if ( i ) { - WriteBits( ( ( 1 << i ) - 1 ) & newValue, i ); + WriteBits( static_cast( ( ( 1u << i ) - 1u ) & static_cast( newValue ) ), i ); } } @@ -321,10 +345,10 @@ idBitMsg::ReadBits ================ */ int idBitMsg::ReadBits( int numBits ) const { - int value; + uint32_t value; int valueBits; int get; - int fraction; + uint32_t fraction; bool sgn; if ( !readData ) { @@ -347,7 +371,8 @@ int idBitMsg::ReadBits( int numBits ) const { } // check for overflow - if ( numBits > GetRemainingReadBits() ) { + if ( IsReadOverflowed() || numBits > GetRemainingReadBits() ) { + MarkReadOverflowed(); return -1; } @@ -361,7 +386,7 @@ int idBitMsg::ReadBits( int numBits ) const { } fraction = readData[readCount - 1]; fraction >>= readBit; - fraction &= ( 1 << get ) - 1; + fraction &= ( 1u << get ) - 1u; value |= fraction << valueBits; valueBits += get; @@ -369,12 +394,14 @@ int idBitMsg::ReadBits( int numBits ) const { } if ( sgn ) { - if ( value & ( 1 << ( numBits - 1 ) ) ) { - value |= -1 ^ ( ( 1 << numBits ) - 1 ); + if ( value & ( 1u << ( numBits - 1 ) ) ) { + value |= ~( ( 1u << numBits ) - 1u ); } } - return value; + int result; + memcpy( &result, &value, sizeof( result ) ); + return result; } /* @@ -419,13 +446,20 @@ int idBitMsg::ReadData( void *data, int length ) const { int cnt; ReadByteAlign(); + if ( IsReadOverflowed() || length < 0 ) { + MarkReadOverflowed(); + return 0; + } cnt = readCount; - if ( readCount + length > curSize ) { + if ( length > curSize - readCount ) { + const int remaining = curSize - readCount; if ( data ) { - memcpy( data, readData + readCount, GetRemainingData() ); + memcpy( data, readData + readCount, remaining ); + memset( static_cast( data ) + remaining, 0, length - remaining ); } - readCount = curSize; + MarkReadOverflowed(); + return remaining; } else { if ( data ) { memcpy( data, readData + readCount, length ); @@ -473,11 +507,16 @@ int idBitMsg::ReadDeltaByteCounter( int oldValue ) const { int i, newValue; i = ReadBits( 3 ); - if ( !i ) { + if ( IsReadOverflowed() || i <= 0 ) { return oldValue; } newValue = ReadBits( i ); - return ( oldValue & ~( ( 1 << i ) - 1 ) | newValue ); + if ( IsReadOverflowed() ) { + return oldValue; + } + const unsigned int mask = ( 1u << i ) - 1u; + return static_cast( ( static_cast( oldValue ) & ~mask ) | + ( static_cast( newValue ) & mask ) ); } /* @@ -489,11 +528,16 @@ int idBitMsg::ReadDeltaShortCounter( int oldValue ) const { int i, newValue; i = ReadBits( 4 ); - if ( !i ) { + if ( IsReadOverflowed() || i <= 0 ) { return oldValue; } newValue = ReadBits( i ); - return ( oldValue & ~( ( 1 << i ) - 1 ) | newValue ); + if ( IsReadOverflowed() ) { + return oldValue; + } + const unsigned int mask = ( 1u << i ) - 1u; + return static_cast( ( static_cast( oldValue ) & ~mask ) | + ( static_cast( newValue ) & mask ) ); } /* @@ -505,11 +549,16 @@ int idBitMsg::ReadDeltaLongCounter( int oldValue ) const { int i, newValue; i = ReadBits( 5 ); - if ( !i ) { + if ( IsReadOverflowed() || i <= 0 || i > 31 ) { return oldValue; } newValue = ReadBits( i ); - return ( oldValue & ~( ( 1 << i ) - 1 ) | newValue ); + if ( IsReadOverflowed() ) { + return oldValue; + } + const unsigned int mask = ( 1u << i ) - 1u; + return static_cast( ( static_cast( oldValue ) & ~mask ) | + ( static_cast( newValue ) & mask ) ); } /* @@ -521,24 +570,43 @@ bool idBitMsg::ReadDeltaDict( idDict &dict, const idDict *base ) const { char key[MAX_STRING_CHARS]; char value[MAX_STRING_CHARS]; bool changed = false; + idDict decoded; if ( base != NULL ) { - dict = *base; + decoded = *base; } else { - dict.Clear(); + decoded.Clear(); } - while( ReadString( key, sizeof( key ) ) != 0 ) { + while ( true ) { + ReadString( key, sizeof( key ) ); + if ( IsReadOverflowed() ) { + return false; + } + if ( key[ 0 ] == '\0' ) { + break; + } ReadString( value, sizeof( value ) ); - dict.Set( key, value ); + if ( IsReadOverflowed() ) { + return false; + } + decoded.Set( key, value ); changed = true; } - while( ReadString( key, sizeof( key ) ) != 0 ) { - dict.Delete( key ); + while ( true ) { + ReadString( key, sizeof( key ) ); + if ( IsReadOverflowed() ) { + return false; + } + if ( key[ 0 ] == '\0' ) { + break; + } + decoded.Delete( key ); changed = true; } + dict = decoded; return changed; } @@ -617,7 +685,7 @@ void idBitMsgDelta::WriteBits( int value, int numBits ) { changed = true; } else { int baseValue = base->ReadBits( numBits ); - if ( baseValue == value ) { + if ( !base->IsReadOverflowed() && baseValue == value ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -647,7 +715,7 @@ void idBitMsgDelta::WriteDelta( int oldValue, int newValue, int numBits ) { changed = true; } else { int baseValue = base->ReadBits( numBits ); - if ( baseValue == newValue ) { + if ( !base->IsReadOverflowed() && baseValue == newValue ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -676,8 +744,14 @@ int idBitMsgDelta::ReadBits( int numBits ) const { changed = true; } else { int baseValue = base->ReadBits( numBits ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + value = baseValue; + } else if ( readDelta->ReadBits( 1 ) == 0 ) { value = baseValue; + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { value = readDelta->ReadBits( numBits ); changed = true; @@ -707,8 +781,14 @@ int idBitMsgDelta::ReadDelta( int oldValue, int numBits ) const { changed = true; } else { int baseValue = base->ReadBits( numBits ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { value = baseValue; + } else if ( readDelta->ReadBits( 1 ) == 0 ) { + value = baseValue; + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else if ( readDelta->ReadBits( 1 ) == 0 ) { value = oldValue; changed = true; @@ -740,7 +820,7 @@ void idBitMsgDelta::WriteString( const char *s, int maxLength ) { } else { char baseString[MAX_DATA_BUFFER]; base->ReadString( baseString, sizeof( baseString ) ); - if ( idStr::Cmp( s, baseString ) == 0 ) { + if ( !base->IsReadOverflowed() && idStr::Cmp( s, baseString ) == 0 ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -767,7 +847,7 @@ void idBitMsgDelta::WriteData( const void *data, int length ) { byte baseData[MAX_DATA_BUFFER]; assert( length < sizeof( baseData ) ); base->ReadData( baseData, length ); - if ( memcmp( data, baseData, length ) == 0 ) { + if ( !base->IsReadOverflowed() && memcmp( data, baseData, length ) == 0 ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -812,7 +892,7 @@ void idBitMsgDelta::WriteDeltaByteCounter( int oldValue, int newValue ) { changed = true; } else { int baseValue = base->ReadBits( 8 ); - if ( baseValue == newValue ) { + if ( !base->IsReadOverflowed() && baseValue == newValue ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -837,7 +917,7 @@ void idBitMsgDelta::WriteDeltaShortCounter( int oldValue, int newValue ) { changed = true; } else { int baseValue = base->ReadBits( 16 ); - if ( baseValue == newValue ) { + if ( !base->IsReadOverflowed() && baseValue == newValue ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -862,7 +942,7 @@ void idBitMsgDelta::WriteDeltaLongCounter( int oldValue, int newValue ) { changed = true; } else { int baseValue = base->ReadBits( 32 ); - if ( baseValue == newValue ) { + if ( !base->IsReadOverflowed() && baseValue == newValue ) { writeDelta->WriteBits( 0, 1 ); } else { writeDelta->WriteBits( 1, 1 ); @@ -884,8 +964,14 @@ void idBitMsgDelta::ReadString( char *buffer, int bufferSize ) const { } else { char baseString[MAX_DATA_BUFFER]; base->ReadString( baseString, sizeof( baseString ) ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + idStr::Copynz( buffer, baseString, bufferSize ); + } else if ( readDelta->ReadBits( 1 ) == 0 ) { idStr::Copynz( buffer, baseString, bufferSize ); + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { readDelta->ReadString( buffer, bufferSize ); changed = true; @@ -910,8 +996,14 @@ void idBitMsgDelta::ReadData( void *data, int length ) const { char baseData[MAX_DATA_BUFFER]; assert( length < sizeof( baseData ) ); base->ReadData( baseData, length ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + memcpy( data, baseData, length ); + } else if ( readDelta->ReadBits( 1 ) == 0 ) { memcpy( data, baseData, length ); + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { readDelta->ReadData( data, length ); changed = true; @@ -960,8 +1052,14 @@ int idBitMsgDelta::ReadDeltaByteCounter( int oldValue ) const { changed = true; } else { int baseValue = base->ReadBits( 8 ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + value = baseValue; + } else if ( readDelta->ReadBits( 1 ) == 0 ) { value = baseValue; + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { value = readDelta->ReadDeltaByteCounter( oldValue ); changed = true; @@ -987,8 +1085,14 @@ int idBitMsgDelta::ReadDeltaShortCounter( int oldValue ) const { changed = true; } else { int baseValue = base->ReadBits( 16 ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + value = baseValue; + } else if ( readDelta->ReadBits( 1 ) == 0 ) { value = baseValue; + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { value = readDelta->ReadDeltaShortCounter( oldValue ); changed = true; @@ -1014,8 +1118,14 @@ int idBitMsgDelta::ReadDeltaLongCounter( int oldValue ) const { changed = true; } else { int baseValue = base->ReadBits( 32 ); - if ( !readDelta || readDelta->ReadBits( 1 ) == 0 ) { + const bool baseOverflowed = base->IsReadOverflowed(); + if ( !readDelta ) { + value = baseValue; + } else if ( readDelta->ReadBits( 1 ) == 0 ) { value = baseValue; + if ( baseOverflowed ) { + readDelta->MarkReadOverflowed(); + } } else { value = readDelta->ReadDeltaLongCounter( oldValue ); changed = true; @@ -1106,15 +1216,34 @@ bool idMsgQueue::Get( byte *data, int dataSize, int &size, bool sequencing ) { size = 0; return false; } - int sequence; + const int headerSize = sequencing ? 6 : 2; + const int totalSize = GetTotalSize(); + if ( totalSize < headerSize ) { + common->Warning( "idMsgQueue::Get: truncated queue record header" ); + size = 0; + first = last; + startIndex = endIndex; + return false; + } size = ReadUShort(); - if ( data && size > dataSize ) { - common->Error( "idMsgQueue::Get buffer size of %d < get size of %d", dataSize, size ); + if ( size <= 0 || size > totalSize - headerSize || ( data && size > dataSize ) ) { + common->Warning( "idMsgQueue::Get: invalid record size %d (available %d, destination %d)", + size, totalSize - headerSize, data ? dataSize : 0 ); + size = 0; + first = last; + startIndex = endIndex; + return false; } if ( sequencing ) { - sequence = ReadLong(); - assert( sequence == first ); + const int sequence = ReadLong(); + if ( sequence != first ) { + common->Warning( "idMsgQueue::Get: invalid record sequence %d (expected %d)", sequence, first ); + size = 0; + first = last; + startIndex = endIndex; + return false; + } } ReadData( data, size ); first++; @@ -1321,13 +1450,16 @@ idMsgQueue::ReadFrom void idMsgQueue::ReadFrom( const idBitMsg &msg ) { Init( 0 ); const int encodedSize = msg.ReadUShort(); + const int available = msg.GetRemainingData(); + const int remaining = available > 0 ? available : 0; if ( encodedSize < 0 || encodedSize >= MAX_MSG_QUEUE_SIZE || - encodedSize > msg.GetRemainingData() ) { + encodedSize > remaining ) { common->Warning( "idMsgQueue::ReadFrom: invalid encoded queue size %d", encodedSize ); - const int discard = idMath::ClampInt( 0, msg.GetRemainingData(), encodedSize ); + const int discard = idMath::ClampInt( 0, remaining, encodedSize ); if ( discard > 0 ) { msg.ReadData( NULL, discard ); } + msg.MarkReadOverflowed(); endIndex = 0; return; } @@ -1335,6 +1467,29 @@ void idMsgQueue::ReadFrom( const idBitMsg &msg ) { if ( msg.ReadData( buffer, endIndex ) != endIndex ) { common->Warning( "idMsgQueue::ReadFrom: truncated encoded queue" ); endIndex = 0; + return; + } + + // ReadFrom is used for serialized unreliable-message queues, whose nested + // records are a little-endian ushort length followed by that payload. + // Validate every nested boundary now so Get cannot later walk uninitialized + // ring storage when a snapshot supplied a forged record size. + for ( int offset = 0; offset < endIndex; ) { + if ( endIndex - offset < 2 ) { + common->Warning( "idMsgQueue::ReadFrom: truncated nested record header" ); + msg.MarkReadOverflowed(); + Init( 0 ); + return; + } + const int recordSize = buffer[ offset ] | ( buffer[ offset + 1 ] << 8 ); + offset += 2; + if ( recordSize <= 0 || recordSize > endIndex - offset ) { + common->Warning( "idMsgQueue::ReadFrom: invalid nested record size %d", recordSize ); + msg.MarkReadOverflowed(); + Init( 0 ); + return; + } + offset += recordSize; } } diff --git a/src/idlib/BitMsg.h b/src/idlib/BitMsg.h index 05167570..5cb69dc0 100644 --- a/src/idlib/BitMsg.h +++ b/src/idlib/BitMsg.h @@ -27,6 +27,8 @@ class idBitMsg { int GetMaxSize( void ) const; // get the maximum message size void SetAllowOverflow( bool set ); // generate error if not set and message is overflowed bool IsOverflowed( void ) const; // returns true if the message was overflowed + bool IsReadOverflowed( void ) const; // returns true after a read requested data beyond the message + void MarkReadOverflowed( void ) const; // fail a higher-level semantic decode without changing object layout int GetSize( void ) const; // size of the message in bytes void SetSize( int size ); // set the message size @@ -166,6 +168,15 @@ ID_INLINE bool idBitMsg::IsOverflowed( void ) const { return overflowed; } +ID_INLINE bool idBitMsg::IsReadOverflowed( void ) const { + return readBit == 8; +} + +ID_INLINE void idBitMsg::MarkReadOverflowed( void ) const { + readCount = curSize; + readBit = 8; +} + ID_INLINE int idBitMsg::GetSize( void ) const { return curSize; } @@ -339,7 +350,9 @@ ID_INLINE void idBitMsg::BeginReading( void ) const { } ID_INLINE void idBitMsg::ReadByteAlign( void ) const { - readBit = 0; + if ( !IsReadOverflowed() ) { + readBit = 0; + } } ID_INLINE int idBitMsg::ReadChar( void ) const { @@ -430,6 +443,8 @@ class idBitMsgDelta { void InitWriting( const idBitMsg *base, idBitMsg *newBase, idBitMsg *delta ); void InitReading( const idBitMsg *base, idBitMsg *newBase, const idBitMsg *delta ); bool HasChanged( void ) const; + bool IsReadOverflowed( void ) const; + void MarkReadOverflowed( void ) const; void WriteBits( int value, int numBits ); void WriteChar( int c ); @@ -523,6 +538,22 @@ ID_INLINE idBitMsgDelta::idBitMsgDelta() { changed = false; } +ID_INLINE bool idBitMsgDelta::IsReadOverflowed( void ) const { + // A conditional delta tail may legitimately extend beyond its trusted old + // base. The readers below promote that condition to wire overflow only if + // the delta asks to reuse a base value that is not available. + return readDelta != NULL ? readDelta->IsReadOverflowed() : + ( base != NULL && base->IsReadOverflowed() ); +} + +ID_INLINE void idBitMsgDelta::MarkReadOverflowed( void ) const { + if ( readDelta != NULL ) { + readDelta->MarkReadOverflowed(); + } else if ( base != NULL ) { + base->MarkReadOverflowed(); + } +} + ID_INLINE void idBitMsgDelta::InitWriting( const idBitMsg *base, idBitMsg *newBase, idBitMsg *delta ) { this->base = base; this->newBase = newBase; diff --git a/src/idlib/CmdArgs.cpp b/src/idlib/CmdArgs.cpp index aeb1b2f6..cd56c627 100644 --- a/src/idlib/CmdArgs.cpp +++ b/src/idlib/CmdArgs.cpp @@ -22,8 +22,9 @@ void idCmdArgs::operator=( const idCmdArgs &args ) { idCmdArgs::Args ============ */ +static idStr cmd_args; + const char *idCmdArgs::Args( int start, int end, bool escapeArgs ) const { - static idStr cmd_args; int i; if ( start < 0 ) { @@ -34,7 +35,9 @@ const char *idCmdArgs::Args( int start, int end, bool escapeArgs ) const { } else if ( end >= argc ) { end = argc - 1; } - cmd_args.Clear(); + // The previous result may have contained a private CVar value. Scrub the + // complete allocation before reusing this process-wide scratch string. + cmd_args.SecureClear(); if ( escapeArgs ) { cmd_args += "\""; } @@ -121,7 +124,12 @@ void idCmdArgs::TokenizeString( const char *text, bool keepAsStrings ) { return; } if ( idLib::cvarSystem ) { - token = idLib::cvarSystem->GetCVarString( token.c_str() ); + idCVar *expandedCVar = idLib::cvarSystem->Find( token.c_str() ); + if ( expandedCVar != NULL && ( expandedCVar->GetFlags() & CVAR_PRIVATE ) ) { + token = ""; + } else { + token = idLib::cvarSystem->GetCVarString( token.c_str() ); + } } else { token = ""; } @@ -181,6 +189,33 @@ void idCmdArgs::AppendArg( const char *text ) { argc++; } +/* +============ +idCmdArgs::ClearSensitive + +Clear command tokens that may have held a private CVar value. This is kept +separate from the hot-path Clear() used for ordinary argument objects. +============ +*/ +void idCmdArgs::ClearSensitive( void ) { + volatile byte *cursor = reinterpret_cast( this ); + for ( size_t remaining = sizeof( *this ); remaining > 0; --remaining ) { + *cursor++ = 0; + } +} + +/* +============ +idCmdArgs::ClearArgsScratch + +Args() assembles its result in a shared idStr. Private command values must not +survive there after the command object itself has been wiped. +============ +*/ +void idCmdArgs::ClearArgsScratch( void ) { + cmd_args.SecureClear(); +} + /* ============ idCmdArgs::GetArgs diff --git a/src/idlib/CmdArgs.h b/src/idlib/CmdArgs.h index 017af078..1b16868f 100644 --- a/src/idlib/CmdArgs.h +++ b/src/idlib/CmdArgs.h @@ -32,6 +32,8 @@ class idCmdArgs { void AppendArg( const char *text ); void Clear( void ) { argc = 0; } + void ClearSensitive( void ); + static void ClearArgsScratch( void ); const char ** GetArgs( int *argc ); private: diff --git a/src/idlib/CryptoHash.cpp b/src/idlib/CryptoHash.cpp new file mode 100644 index 00000000..e65ec8f3 --- /dev/null +++ b/src/idlib/CryptoHash.cpp @@ -0,0 +1,320 @@ +/* +=========================================================================== + +openQ4 cryptographic hash primitives +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +This is an original implementation of the published FIPS 180-4 SHA-256, +RFC 2104 HMAC, and RFC 8018 PBKDF2 specifications. No source code from the +Quake 4 SDK game module or another cryptographic implementation is used here. + +=========================================================================== +*/ + +#include "CryptoHash.h" + +#include + +namespace idCrypto { +namespace { + +struct sha256Context_t { + std::uint32_t state[ 8 ]; + std::uint64_t totalBytes; + std::uint8_t buffer[ SHA256_BLOCK_BYTES ]; + std::size_t bufferedBytes; +}; + +static constexpr std::uint32_t SHA256_ROUND_CONSTANTS[ 64 ] = { + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, + 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, + 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, + 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, + 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, + 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, + 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, + 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, + 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, + 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, + 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, + 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, + 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, + 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, + 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, + 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u +}; + +static inline std::uint32_t RotateRight( std::uint32_t value, unsigned int count ) { + return ( value >> count ) | ( value << ( 32u - count ) ); +} + +static inline std::uint32_t ReadBigEndian32( const std::uint8_t *bytes ) { + return ( static_cast( bytes[ 0 ] ) << 24 ) | + ( static_cast( bytes[ 1 ] ) << 16 ) | + ( static_cast( bytes[ 2 ] ) << 8 ) | + static_cast( bytes[ 3 ] ); +} + +static inline void WriteBigEndian32( std::uint8_t *bytes, std::uint32_t value ) { + bytes[ 0 ] = static_cast( value >> 24 ); + bytes[ 1 ] = static_cast( value >> 16 ); + bytes[ 2 ] = static_cast( value >> 8 ); + bytes[ 3 ] = static_cast( value ); +} + +static void SHA256Transform( sha256Context_t &context, + const std::uint8_t block[ SHA256_BLOCK_BYTES ] ) { + std::uint32_t schedule[ 64 ]; + for ( int index = 0; index < 16; ++index ) { + schedule[ index ] = ReadBigEndian32( block + index * 4 ); + } + for ( int index = 16; index < 64; ++index ) { + const std::uint32_t sigma0 = RotateRight( schedule[ index - 15 ], 7 ) ^ + RotateRight( schedule[ index - 15 ], 18 ) ^ ( schedule[ index - 15 ] >> 3 ); + const std::uint32_t sigma1 = RotateRight( schedule[ index - 2 ], 17 ) ^ + RotateRight( schedule[ index - 2 ], 19 ) ^ ( schedule[ index - 2 ] >> 10 ); + schedule[ index ] = schedule[ index - 16 ] + sigma0 + + schedule[ index - 7 ] + sigma1; + } + + std::uint32_t a = context.state[ 0 ]; + std::uint32_t b = context.state[ 1 ]; + std::uint32_t c = context.state[ 2 ]; + std::uint32_t d = context.state[ 3 ]; + std::uint32_t e = context.state[ 4 ]; + std::uint32_t f = context.state[ 5 ]; + std::uint32_t g = context.state[ 6 ]; + std::uint32_t h = context.state[ 7 ]; + + for ( int index = 0; index < 64; ++index ) { + const std::uint32_t sum1 = RotateRight( e, 6 ) ^ RotateRight( e, 11 ) ^ RotateRight( e, 25 ); + const std::uint32_t choose = ( e & f ) ^ ( ( ~e ) & g ); + const std::uint32_t temp1 = h + sum1 + choose + + SHA256_ROUND_CONSTANTS[ index ] + schedule[ index ]; + const std::uint32_t sum0 = RotateRight( a, 2 ) ^ RotateRight( a, 13 ) ^ RotateRight( a, 22 ); + const std::uint32_t majority = ( a & b ) ^ ( a & c ) ^ ( b & c ); + const std::uint32_t temp2 = sum0 + majority; + + h = g; + g = f; + f = e; + e = d + temp1; + d = c; + c = b; + b = a; + a = temp1 + temp2; + } + + context.state[ 0 ] += a; + context.state[ 1 ] += b; + context.state[ 2 ] += c; + context.state[ 3 ] += d; + context.state[ 4 ] += e; + context.state[ 5 ] += f; + context.state[ 6 ] += g; + context.state[ 7 ] += h; + idCrypto::SecureZero( schedule, sizeof( schedule ) ); +} + +static void SHA256Init( sha256Context_t &context ) { + context.state[ 0 ] = 0x6a09e667u; + context.state[ 1 ] = 0xbb67ae85u; + context.state[ 2 ] = 0x3c6ef372u; + context.state[ 3 ] = 0xa54ff53au; + context.state[ 4 ] = 0x510e527fu; + context.state[ 5 ] = 0x9b05688cu; + context.state[ 6 ] = 0x1f83d9abu; + context.state[ 7 ] = 0x5be0cd19u; + context.totalBytes = 0; + context.bufferedBytes = 0; + std::memset( context.buffer, 0, sizeof( context.buffer ) ); +} + +static void SHA256Update( sha256Context_t &context, const void *data, std::size_t dataBytes ) { + const std::uint8_t *cursor = static_cast( data ); + context.totalBytes += static_cast( dataBytes ); + + if ( context.bufferedBytes != 0 ) { + const std::size_t wanted = SHA256_BLOCK_BYTES - context.bufferedBytes; + const std::size_t copied = dataBytes < wanted ? dataBytes : wanted; + if ( copied != 0 ) { + std::memcpy( context.buffer + context.bufferedBytes, cursor, copied ); + context.bufferedBytes += copied; + cursor += copied; + dataBytes -= copied; + } + if ( context.bufferedBytes == SHA256_BLOCK_BYTES ) { + SHA256Transform( context, context.buffer ); + context.bufferedBytes = 0; + } + } + + while ( dataBytes >= SHA256_BLOCK_BYTES ) { + SHA256Transform( context, cursor ); + cursor += SHA256_BLOCK_BYTES; + dataBytes -= SHA256_BLOCK_BYTES; + } + if ( dataBytes != 0 ) { + std::memcpy( context.buffer, cursor, dataBytes ); + context.bufferedBytes = dataBytes; + } +} + +static void SHA256Final( sha256Context_t &context, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ) { + const std::uint64_t totalBits = context.totalBytes * 8u; + context.buffer[ context.bufferedBytes++ ] = 0x80u; + if ( context.bufferedBytes > 56 ) { + std::memset( context.buffer + context.bufferedBytes, 0, + SHA256_BLOCK_BYTES - context.bufferedBytes ); + SHA256Transform( context, context.buffer ); + context.bufferedBytes = 0; + } + std::memset( context.buffer + context.bufferedBytes, 0, 56 - context.bufferedBytes ); + for ( int index = 0; index < 8; ++index ) { + context.buffer[ 56 + index ] = static_cast( totalBits >> ( 56 - index * 8 ) ); + } + SHA256Transform( context, context.buffer ); + for ( int index = 0; index < 8; ++index ) { + WriteBigEndian32( digest + index * 4, context.state[ index ] ); + } + SecureZero( &context, sizeof( context ) ); +} + +struct hmacSHA256Prepared_t { + sha256Context_t inner; + sha256Context_t outer; +}; + +static void HMACPrepare( const void *key, std::size_t keyBytes, + hmacSHA256Prepared_t &prepared ) { + std::uint8_t normalizedKey[ SHA256_BLOCK_BYTES ] = {}; + if ( keyBytes > SHA256_BLOCK_BYTES ) { + SHA256( key, keyBytes, normalizedKey ); + } else if ( keyBytes != 0 ) { + std::memcpy( normalizedKey, key, keyBytes ); + } + + std::uint8_t innerPad[ SHA256_BLOCK_BYTES ]; + std::uint8_t outerPad[ SHA256_BLOCK_BYTES ]; + for ( std::size_t index = 0; index < SHA256_BLOCK_BYTES; ++index ) { + innerPad[ index ] = normalizedKey[ index ] ^ 0x36u; + outerPad[ index ] = normalizedKey[ index ] ^ 0x5cu; + } + SHA256Init( prepared.inner ); + SHA256Update( prepared.inner, innerPad, sizeof( innerPad ) ); + SHA256Init( prepared.outer ); + SHA256Update( prepared.outer, outerPad, sizeof( outerPad ) ); + SecureZero( normalizedKey, sizeof( normalizedKey ) ); + SecureZero( innerPad, sizeof( innerPad ) ); + SecureZero( outerPad, sizeof( outerPad ) ); +} + +static void HMACFinish( const hmacSHA256Prepared_t &prepared, + const void *data, std::size_t dataBytes, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ) { + sha256Context_t inner = prepared.inner; + sha256Context_t outer = prepared.outer; + std::uint8_t innerDigest[ SHA256_DIGEST_BYTES ]; + SHA256Update( inner, data, dataBytes ); + SHA256Final( inner, innerDigest ); + SHA256Update( outer, innerDigest, sizeof( innerDigest ) ); + SHA256Final( outer, digest ); + SecureZero( innerDigest, sizeof( innerDigest ) ); +} + +} // namespace + +void SHA256( const void *data, std::size_t dataBytes, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ) { + sha256Context_t context; + SHA256Init( context ); + if ( dataBytes != 0 ) { + SHA256Update( context, data, dataBytes ); + } + SHA256Final( context, digest ); +} + +void HMACSHA256( const void *key, std::size_t keyBytes, + const void *data, std::size_t dataBytes, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ) { + hmacSHA256Prepared_t prepared; + HMACPrepare( key, keyBytes, prepared ); + HMACFinish( prepared, data, dataBytes, digest ); + SecureZero( &prepared, sizeof( prepared ) ); +} + +bool PBKDF2HMACSHA256( const void *password, std::size_t passwordBytes, + const void *salt, std::size_t saltBytes, std::uint32_t iterations, + void *output, std::size_t outputBytes ) { + if ( ( passwordBytes != 0 && password == nullptr ) || + ( saltBytes != 0 && salt == nullptr ) || output == nullptr || + outputBytes == 0 || outputBytes > SHA256_DIGEST_BYTES || iterations == 0 ) { + return false; + } + + hmacSHA256Prepared_t prepared; + HMACPrepare( password, passwordBytes, prepared ); + sha256Context_t firstInner = prepared.inner; + const std::uint8_t blockIndex[ 4 ] = { 0, 0, 0, 1 }; + std::uint8_t iteration[ SHA256_DIGEST_BYTES ]; + std::uint8_t aggregate[ SHA256_DIGEST_BYTES ]; + + if ( saltBytes != 0 ) { + SHA256Update( firstInner, salt, saltBytes ); + } + SHA256Update( firstInner, blockIndex, sizeof( blockIndex ) ); + std::uint8_t firstDigest[ SHA256_DIGEST_BYTES ]; + SHA256Final( firstInner, firstDigest ); + sha256Context_t firstOuter = prepared.outer; + SHA256Update( firstOuter, firstDigest, sizeof( firstDigest ) ); + SHA256Final( firstOuter, iteration ); + std::memcpy( aggregate, iteration, sizeof( aggregate ) ); + SecureZero( firstDigest, sizeof( firstDigest ) ); + + for ( std::uint32_t round = 1; round < iterations; ++round ) { + std::uint8_t next[ SHA256_DIGEST_BYTES ]; + HMACFinish( prepared, iteration, sizeof( iteration ), next ); + for ( std::size_t index = 0; index < sizeof( aggregate ); ++index ) { + aggregate[ index ] ^= next[ index ]; + } + std::memcpy( iteration, next, sizeof( iteration ) ); + SecureZero( next, sizeof( next ) ); + } + + std::memcpy( output, aggregate, outputBytes ); + SecureZero( iteration, sizeof( iteration ) ); + SecureZero( aggregate, sizeof( aggregate ) ); + SecureZero( &prepared, sizeof( prepared ) ); + return true; +} + +bool ConstantTimeEquals( const void *left, const void *right, std::size_t bytes ) { + if ( bytes == 0 ) { + return true; + } + if ( left == nullptr || right == nullptr ) { + return false; + } + const std::uint8_t *leftBytes = static_cast( left ); + const std::uint8_t *rightBytes = static_cast( right ); + volatile std::uint8_t difference = 0; + for ( std::size_t index = 0; index < bytes; ++index ) { + difference |= leftBytes[ index ] ^ rightBytes[ index ]; + } + return difference == 0; +} + +void SecureZero( void *memory, std::size_t bytes ) { + volatile std::uint8_t *cursor = static_cast( memory ); + while ( bytes-- != 0 ) { + *cursor++ = 0; + } +} + +} // namespace idCrypto diff --git a/src/idlib/CryptoHash.h b/src/idlib/CryptoHash.h new file mode 100644 index 00000000..7031e718 --- /dev/null +++ b/src/idlib/CryptoHash.h @@ -0,0 +1,47 @@ +/* +=========================================================================== + +openQ4 cryptographic hash primitives +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __CRYPTOHASH_H__ +#define __CRYPTOHASH_H__ + +#include +#include + +namespace idCrypto { + +static constexpr std::size_t SHA256_DIGEST_BYTES = 32; +static constexpr std::size_t SHA256_BLOCK_BYTES = 64; + +// These routines implement the algorithms specified by FIPS 180-4, RFC 2104, +// and RFC 8018. They deliberately have no dependency on engine globals so the +// exact primitives can be exercised by the native safety test target. +void SHA256( const void *data, std::size_t dataBytes, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ); + +void HMACSHA256( const void *key, std::size_t keyBytes, + const void *data, std::size_t dataBytes, + std::uint8_t digest[ SHA256_DIGEST_BYTES ] ); + +// openQ4 only needs one SHA-256-sized PBKDF2 block. Keeping the interface +// bounded prevents accidental unbounded work or counter-wrap mistakes. +bool PBKDF2HMACSHA256( const void *password, std::size_t passwordBytes, + const void *salt, std::size_t saltBytes, std::uint32_t iterations, + void *output, std::size_t outputBytes ); + +bool ConstantTimeEquals( const void *left, const void *right, std::size_t bytes ); +void SecureZero( void *memory, std::size_t bytes ); + +} // namespace idCrypto + +#endif /* !__CRYPTOHASH_H__ */ diff --git a/src/idlib/PrivateCommand.h b/src/idlib/PrivateCommand.h new file mode 100644 index 00000000..8893fe8b --- /dev/null +++ b/src/idlib/PrivateCommand.h @@ -0,0 +1,76 @@ +/* +=========================================================================== + +openQ4 private command matching helpers +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __PRIVATECOMMAND_H__ +#define __PRIVATECOMMAND_H__ + +#include +#include + +namespace idPrivateCommand { + +static inline bool IsNameCharacter( const unsigned char value ) { + return ( value >= 'a' && value <= 'z' ) || + ( value >= 'A' && value <= 'Z' ) || + ( value >= '0' && value <= '9' ) || value == '_' || value == '.'; +} + +static inline unsigned char FoldASCII( const unsigned char value ) { + return value >= 'A' && value <= 'Z' ? + static_cast( value + ( 'a' - 'A' ) ) : value; +} + +// Matches a complete CVar-name token anywhere in a command line. Lengths are +// established before the scan so a command ending in a prefix of name never +// causes the comparison or right-boundary check to read beyond its NUL byte. +static inline bool ContainsBoundedCaseInsensitiveToken( const char *commandText, + const char *name ) { + if ( commandText == NULL || name == NULL || commandText[ 0 ] == '\0' || name[ 0 ] == '\0' ) { + return false; + } + + const std::size_t commandBytes = std::strlen( commandText ); + const std::size_t nameBytes = std::strlen( name ); + if ( nameBytes > commandBytes ) { + return false; + } + + for ( std::size_t offset = 0; offset <= commandBytes - nameBytes; ++offset ) { + bool equal = true; + for ( std::size_t index = 0; index < nameBytes; ++index ) { + if ( FoldASCII( static_cast( commandText[ offset + index ] ) ) != + FoldASCII( static_cast( name[ index ] ) ) ) { + equal = false; + break; + } + } + if ( !equal ) { + continue; + } + + const bool leftBounded = offset == 0 || + !IsNameCharacter( static_cast( commandText[ offset - 1 ] ) ); + const std::size_t rightOffset = offset + nameBytes; + const bool rightBounded = rightOffset == commandBytes || + !IsNameCharacter( static_cast( commandText[ rightOffset ] ) ); + if ( leftBounded && rightBounded ) { + return true; + } + } + return false; +} + +} // namespace idPrivateCommand + +#endif /* !__PRIVATECOMMAND_H__ */ diff --git a/src/idlib/Str.h b/src/idlib/Str.h index f8f91cb0..7d255159 100644 --- a/src/idlib/Str.h +++ b/src/idlib/Str.h @@ -198,6 +198,7 @@ class idStr { void Empty( void ); bool IsEmpty( void ) const; void Clear( void ); + void SecureClear( void ); void Append( const char a ); void Append( const idStr &text ); void Append( const char *text ); @@ -831,6 +832,18 @@ ID_INLINE void idStr::Clear( void ) { Init(); } +ID_INLINE void idStr::SecureClear( void ) { + if ( data != NULL ) { + const int bytes = data != baseBuffer ? alloced : static_cast( sizeof( baseBuffer ) ); + volatile char *cursor = data; + for ( int remaining = bytes; remaining > 0; --remaining ) { + *cursor++ = 0; + } + } + FreeData(); + Init(); +} + ID_INLINE void idStr::Append( const char a ) { EnsureAlloced( idStrAllocationDetail::SaturatingAdd( static_cast( len ), 2 ) ); data[ len ] = a; diff --git a/src/idlib/precompiled.h b/src/idlib/precompiled.h index 2944df25..fe4c1083 100644 --- a/src/idlib/precompiled.h +++ b/src/idlib/precompiled.h @@ -323,6 +323,19 @@ class BITT { #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/imagetools/BinaryImage.cpp b/src/imagetools/BinaryImage.cpp index fb8a7546..ce54068d 100644 --- a/src/imagetools/BinaryImage.cpp +++ b/src/imagetools/BinaryImage.cpp @@ -37,6 +37,7 @@ If you have questions concerning this license or the applicable additional terms */ #include "../renderer/Image.h" +#include "../idlib/CryptoHash.h" #include "DXT/DXTCodec.h" #include "Color/ColorSpace.h" @@ -72,6 +73,36 @@ static bool R_ShouldWriteGeneratedImages() { return image_writeGeneratedImages.GetBool() || cvarSystem->GetCVarBool( "com_makingBuild" ); } +/* +======================== +R_MakeCompactBinaryImageFileName + +The ordinary generated qpath remains the public/cache-package identity. This +fixed-length secondary identity is consulted only after that cache is absent or +invalid, and is written only after the ordinary savepath cannot be opened. This +lets deeply nested save roots populate a runtime cache without changing existing +generated-tree priority. The version covers both normalization and path layout; +bump it if either changes. +======================== +*/ +static void R_MakeCompactBinaryImageFileName( idStr &compactFileName, const char *logicalName ) { + idStr normalizedName = logicalName != NULL ? logicalName : ""; + normalizedName.BackSlashesToSlashes(); + normalizedName.ToLower(); + + std::uint8_t digest[ idCrypto::SHA256_DIGEST_BYTES ]; + idCrypto::SHA256( normalizedName.c_str(), normalizedName.Length(), digest ); + + static const char hexDigits[] = "0123456789abcdef"; + char digestHex[ 16 * 2 + 1 ]; + for ( int i = 0; i < 16; i++ ) { + digestHex[ i * 2 + 0 ] = hexDigits[ digest[ i ] >> 4 ]; + digestHex[ i * 2 + 1 ] = hexDigits[ digest[ i ] & 15 ]; + } + digestHex[ sizeof( digestHex ) - 1 ] = '\0'; + compactFileName = va( "generated/images/_compact/v1/%s.bimage", digestHex ); +} + static bool R_BinaryImageFormatIsBlockCompressed( textureFormat_t format ) { return format == FMT_DXT1 || format == FMT_DXT5 || format == FMT_BC7; } @@ -131,30 +162,40 @@ void idBinaryImage::Load2DFromMemory( int width, int height, const byte * pic_co fileData.height = height; fileData.numLevels = numLevels; - // note: this previously passed the renderer's vertex-cache TAG_TEMP enum - // as the allocator's debug tag byte — a port artifact, not a real tag - byte * pic = (byte *)Mem_Alloc( width * height * 4 ); - memcpy( pic, pic_const, width * height * 4 ); - - if ( colorFormat == CFM_YCOCG_DXT5 ) { - // convert the image data to YCoCg and use the YCoCgDXT5 compressor - idColorSpace::ConvertRGBToCoCg_Y( pic, pic, width, height ); - } else if ( colorFormat == CFM_NORMAL_DXT5 ) { - // Blah, HQ swizzles automatically, Fast doesn't - if ( !image_highQualityCompression.GetBool() ) { + // Work from the caller's buffer directly; take a mutable copy only when a + // color-format conversion has to rewrite the source pixels. 'pic' becomes + // owned at the first buffer this function allocates (the conversion copy or + // the first mip downsample). + const byte * pic = pic_const; + bool picOwned = false; + + const bool needsMutableCopy = + ( colorFormat == CFM_YCOCG_DXT5 ) || + ( colorFormat == CFM_NORMAL_DXT5 && !image_highQualityCompression.GetBool() ) || + ( colorFormat == CFM_GREEN_ALPHA ); + if ( needsMutableCopy ) { + byte * converted = (byte *)Mem_Alloc( width * height * 4 ); + memcpy( converted, pic_const, width * height * 4 ); + if ( colorFormat == CFM_YCOCG_DXT5 ) { + // convert the image data to YCoCg and use the YCoCgDXT5 compressor + idColorSpace::ConvertRGBToCoCg_Y( converted, converted, width, height ); + } else if ( colorFormat == CFM_NORMAL_DXT5 ) { + // Blah, HQ swizzles automatically, Fast doesn't for ( int i = 0; i < width * height; i++ ) { - pic[i*4+3] = pic[i*4+0]; - pic[i*4+0] = 0; - pic[i*4+2] = 0; + converted[i*4+3] = converted[i*4+0]; + converted[i*4+0] = 0; + converted[i*4+2] = 0; + } + } else { + for ( int i = 0; i < width * height; i++ ) { + converted[i*4+1] = converted[i*4+3]; + converted[i*4+0] = 0; + converted[i*4+2] = 0; + converted[i*4+3] = 0; } } - } else if ( colorFormat == CFM_GREEN_ALPHA ) { - for ( int i = 0; i < width * height; i++ ) { - pic[i*4+1] = pic[i*4+3]; - pic[i*4+0] = 0; - pic[i*4+2] = 0; - pic[i*4+3] = 0; - } + pic = converted; + picOwned = true; } int scaledWidth = width; @@ -162,27 +203,29 @@ void idBinaryImage::Load2DFromMemory( int width, int height, const byte * pic_co images.SetNum( numLevels ); for ( int level = 0; level < images.Num(); level++ ) { idBinaryImageData &img = images[ level ]; - byte *uploadPic = pic; + const byte *uploadPic = pic; if ( filterNeutralAlpha ) { - uploadPic = (byte *)Mem_Alloc( scaledWidth * scaledHeight * 4 ); - memcpy( uploadPic, pic, scaledWidth * scaledHeight * 4 ); - R_ApplyFilterNeutralAlpha( uploadPic, scaledWidth * scaledHeight ); + byte * filtered = (byte *)Mem_Alloc( scaledWidth * scaledHeight * 4 ); + memcpy( filtered, pic, scaledWidth * scaledHeight * 4 ); + R_ApplyFilterNeutralAlpha( filtered, scaledWidth * scaledHeight ); + uploadPic = filtered; } // Images that are going to be DXT compressed and aren't multiples of 4 need to be // padded out before compressing. - byte * dxtPic = uploadPic; + const byte * dxtPic = uploadPic; int dxtWidth = 0; int dxtHeight = 0; if ( textureFormat == FMT_DXT5 || textureFormat == FMT_DXT1 ) { if ( ( scaledWidth & 3 ) || ( scaledHeight & 3 ) ) { dxtWidth = ( scaledWidth + 3 ) & ~3; dxtHeight = ( scaledHeight + 3 ) & ~3; - dxtPic = (byte *)Mem_ClearedAlloc( dxtWidth*4*dxtHeight ); + byte * padded = (byte *)Mem_ClearedAlloc( dxtWidth*4*dxtHeight ); for ( int i = 0; i < scaledHeight; i++ ) { - memcpy( dxtPic + i*dxtWidth*4, uploadPic + i*scaledWidth*4, scaledWidth*4 ); + memcpy( padded + i*dxtWidth*4, uploadPic + i*scaledWidth*4, scaledWidth*4 ); } + dxtPic = padded; } else { dxtPic = uploadPic; dxtWidth = scaledWidth; @@ -256,36 +299,41 @@ void idBinaryImage::Load2DFromMemory( int width, int height, const byte * pic_co } else { fileData.format = textureFormat = FMT_RGBA8; img.Alloc( scaledWidth * scaledHeight * 4 ); - for ( int i = 0; i < img.dataSize; i++ ) { - img.data[ i ] = uploadPic[ i ]; - } + memcpy( img.data, uploadPic, img.dataSize ); } // if we had to pad to quads, free the padded version if ( uploadPic != dxtPic ) { - Mem_Free( dxtPic ); + Mem_Free( (void *)dxtPic ); dxtPic = NULL; } if ( uploadPic != pic ) { - Mem_Free( uploadPic ); + Mem_Free( (void *)uploadPic ); uploadPic = NULL; } - // downsample for the next level - byte * shrunk = NULL; - if ( gammaMips ) { - shrunk = R_MipMapWithGamma( pic, scaledWidth, scaledHeight ); - } else { - shrunk = R_MipMap( pic, scaledWidth, scaledHeight ); + // downsample for the next level; the final level has no next level to feed + if ( level + 1 < images.Num() ) { + byte * shrunk = NULL; + if ( gammaMips ) { + shrunk = R_MipMapWithGamma( pic, scaledWidth, scaledHeight ); + } else { + shrunk = R_MipMap( pic, scaledWidth, scaledHeight ); + } + if ( picOwned ) { + Mem_Free( (void *)pic ); + } + pic = shrunk; + picOwned = true; } - Mem_Free( pic ); - pic = shrunk; scaledWidth = Max( 1, scaledWidth >> 1 ); scaledHeight = Max( 1, scaledHeight >> 1 ); } - Mem_Free( pic ); + if ( picOwned ) { + Mem_Free( (void *)pic ); + } } /* @@ -315,10 +363,16 @@ static void PadImageTo4x4( const byte *src, int width, int height, byte dest[64] /* ======================== -idBinaryImage::Load2DFromCompressedData +idBinaryImage::Load2DFromOwnedCompressedData + +Takes ownership of fileBuffer, which must come from this binary's Mem_Alloc +family: Clear() releases it with Mem_Free (never hand over a +fileSystem->ReadFile buffer - in renderer-module builds that is a different +heap). Each level becomes a view into the buffer, mirroring +LoadFromGeneratedFile, so no per-level copies are made. ======================== */ -void idBinaryImage::Load2DFromCompressedData( int width, int height, int numLevels, textureFormat_t textureFormat, textureColor_t colorFormat, const byte *data, const int *levelOffsets, const int *levelSizes ) { +void idBinaryImage::Load2DFromOwnedCompressedData( int width, int height, int numLevels, textureFormat_t textureFormat, textureColor_t colorFormat, byte *fileBuffer, const int *levelOffsets, const int *levelSizes ) { Clear(); fileData.textureType = TT_2D; @@ -328,6 +382,8 @@ void idBinaryImage::Load2DFromCompressedData( int width, int height, int numLeve fileData.height = height; fileData.numLevels = numLevels; + loadedFileData = fileBuffer; + images.SetNum( numLevels ); int levelWidth = width; int levelHeight = height; @@ -337,8 +393,7 @@ void idBinaryImage::Load2DFromCompressedData( int width, int height, int numLeve img.destZ = 0; img.width = levelWidth; img.height = levelHeight; - img.Alloc( levelSizes[ level ] ); - memcpy( img.data, data + levelOffsets[ level ], levelSizes[ level ] ); + img.SetExternalData( loadedFileData + levelOffsets[ level ], levelSizes[ level ] ); levelWidth = Max( 1, levelWidth >> 1 ); levelHeight = Max( 1, levelHeight >> 1 ); @@ -400,18 +455,20 @@ void idBinaryImage::LoadCubeFromMemory( int width, const byte * pics[6], int num memcpy( img.data, pic, img.dataSize ); } - // downsample for the next level - byte * shrunk = NULL; - if ( gammaMips ) { - shrunk = R_MipMapWithGamma( pic, scaledWidth, scaledWidth ); - } else { - shrunk = R_MipMap( pic, scaledWidth, scaledWidth ); - } - if ( pic != orig ) { - Mem_Free( (void *)pic ); - pic = NULL; + // downsample for the next level; the final level has no next level to feed + if ( level + 1 < fileData.numLevels ) { + byte * shrunk = NULL; + if ( gammaMips ) { + shrunk = R_MipMapWithGamma( pic, scaledWidth, scaledWidth ); + } else { + shrunk = R_MipMap( pic, scaledWidth, scaledWidth ); + } + if ( pic != orig ) { + Mem_Free( (void *)pic ); + pic = NULL; + } + pic = shrunk; } - pic = shrunk; scaledWidth = Max( 1, scaledWidth >> 1 ); } @@ -436,22 +493,26 @@ bool idBinaryImage::WriteToFile( idFile *file, ID_TIME_T sourceFileTime ) { fileData.headerMagic = BIMAGE_MAGIC; fileData.sourceFileTime = sourceFileTime; - file->WriteBig( fileData.sourceFileTime ); - file->WriteBig( fileData.headerMagic ); - file->WriteBig( fileData.textureType ); - file->WriteBig( fileData.format ); - file->WriteBig( fileData.colorFormat ); - file->WriteBig( fileData.width ); - file->WriteBig( fileData.height ); - file->WriteBig( fileData.numLevels ); + if ( file->WriteBig( fileData.sourceFileTime ) != sizeof( fileData.sourceFileTime ) || + file->WriteBig( fileData.headerMagic ) != sizeof( fileData.headerMagic ) || + file->WriteBig( fileData.textureType ) != sizeof( fileData.textureType ) || + file->WriteBig( fileData.format ) != sizeof( fileData.format ) || + file->WriteBig( fileData.colorFormat ) != sizeof( fileData.colorFormat ) || + file->WriteBig( fileData.width ) != sizeof( fileData.width ) || + file->WriteBig( fileData.height ) != sizeof( fileData.height ) || + file->WriteBig( fileData.numLevels ) != sizeof( fileData.numLevels ) ) { + return false; + } for ( int i = 0; i < images.Num(); i++ ) { idBinaryImageData &img = images[ i ]; - file->WriteBig( img.level ); - file->WriteBig( img.destZ ); - file->WriteBig( img.width ); - file->WriteBig( img.height ); - file->WriteBig( img.dataSize ); + if ( file->WriteBig( img.level ) != sizeof( img.level ) || + file->WriteBig( img.destZ ) != sizeof( img.destZ ) || + file->WriteBig( img.width ) != sizeof( img.width ) || + file->WriteBig( img.height ) != sizeof( img.height ) || + file->WriteBig( img.dataSize ) != sizeof( img.dataSize ) ) { + return false; + } if ( file->Write( img.data, img.dataSize ) != img.dataSize ) { return false; } @@ -473,13 +534,20 @@ ID_TIME_T idBinaryImage::WriteGeneratedFile( ID_TIME_T sourceFileTime ) { MakeGeneratedFileName( binaryFileName ); // Write generated cache data to savepath so long image-program names stay under // Windows path limits even when fs_basepath points at "Program Files". - idFileLocal file( fileSystem->OpenFileWrite( binaryFileName, "fs_savepath" ) ); + idStr writeFileName = binaryFileName; + idFile *outputFile = fileSystem->OpenFileWrite( writeFileName, "fs_savepath" ); + if ( outputFile == NULL ) { + R_MakeCompactBinaryImageFileName( writeFileName, GetName() ); + outputFile = fileSystem->OpenFileWrite( writeFileName, "fs_savepath" ); + } + idFileLocal file( outputFile ); if ( file == NULL ) { - idLib::Warning( "idBinaryImage: Could not open file '%s'", binaryFileName.c_str() ); + idLib::Warning( "idBinaryImage: Could not open generated cache '%s' or compact fallback '%s'", + binaryFileName.c_str(), writeFileName.c_str() ); return FILE_NOT_FOUND_TIMESTAMP; } if ( image_showGeneratedImageWrites.GetBool() ) { - idLib::Printf( "Writing %s\n", binaryFileName.c_str() ); + idLib::Printf( "Writing %s\n", writeFileName.c_str() ); } if ( !WriteToFile( file, sourceFileTime ) ) { @@ -499,12 +567,16 @@ ID_TIME_T idBinaryImage::LoadFromGeneratedFile( ID_TIME_T sourceFileTime ) { idStr binaryFileName; MakeGeneratedFileName( binaryFileName ); idFileLocal bFile = fileSystem->OpenFileRead( binaryFileName ); - if ( bFile == NULL ) { - return FILE_NOT_FOUND_TIMESTAMP; - } - if ( LoadFromGeneratedFile( bFile, sourceFileTime, true ) ) { + if ( bFile != NULL && LoadFromGeneratedFile( bFile, sourceFileTime, true ) ) { return bFile->Timestamp(); } + + idStr compactFileName; + R_MakeCompactBinaryImageFileName( compactFileName, GetName() ); + idFileLocal compactFile = fileSystem->OpenFileRead( compactFileName ); + if ( compactFile != NULL && LoadFromGeneratedFile( compactFile, sourceFileTime, true ) ) { + return compactFile->Timestamp(); + } return FILE_NOT_FOUND_TIMESTAMP; } @@ -520,15 +592,31 @@ ID_TIME_T idBinaryImage::LoadFromGeneratedFileUnchecked() { idStr binaryFileName; MakeGeneratedFileName( binaryFileName ); idFileLocal bFile = fileSystem->OpenFileRead( binaryFileName ); - if ( bFile == NULL ) { - return FILE_NOT_FOUND_TIMESTAMP; - } - if ( LoadFromGeneratedFile( bFile, FILE_NOT_FOUND_TIMESTAMP, false ) ) { + if ( bFile != NULL && LoadFromGeneratedFile( bFile, FILE_NOT_FOUND_TIMESTAMP, false ) ) { return bFile->Timestamp(); } return FILE_NOT_FOUND_TIMESTAMP; } +/* +========================== +idBinaryImage::LoadFromCompactGeneratedFileUnchecked + +Loads the fixed-length recovery identity after the ordinary generated cache was +missing or rejected. The VFS read preserves pure-server directory policy; the +ordinary generated identity remains authoritative because callers probe it first. +========================== +*/ +ID_TIME_T idBinaryImage::LoadFromCompactGeneratedFileUnchecked() { + idStr compactFileName; + R_MakeCompactBinaryImageFileName( compactFileName, GetName() ); + idFileLocal compactFile = fileSystem->OpenFileRead( compactFileName ); + if ( compactFile != NULL && LoadFromGeneratedFile( compactFile, FILE_NOT_FOUND_TIMESTAMP, false ) ) { + return compactFile->Timestamp(); + } + return FILE_NOT_FOUND_TIMESTAMP; +} + /* ========================== idBinaryImage::LoadFromFile @@ -765,4 +853,3 @@ void idBinaryImage::GetGeneratedFileName( idStr & gfn, const char *name ) { const uint32_t crc = CRC32_BlockChecksum( normalizedName.c_str(), normalizedName.Length() ); gfn = va( "generated/images/_programs/%s_%08x.bimage", prefix.c_str(), static_cast( crc ) ); } - diff --git a/src/imagetools/BinaryImage.h b/src/imagetools/BinaryImage.h index 9cec88f9..ebd11a0b 100644 --- a/src/imagetools/BinaryImage.h +++ b/src/imagetools/BinaryImage.h @@ -49,12 +49,13 @@ class idBinaryImage { void SetName( const char *_name ) { imgName = _name; } void Load2DFromMemory( int width, int height, const byte * pic_const, int numLevels, textureFormat_t & textureFormat, textureColor_t & colorFormat, bool gammaMips, bool filterNeutralAlpha = false ); - void Load2DFromCompressedData( int width, int height, int numLevels, textureFormat_t textureFormat, textureColor_t colorFormat, const byte *data, const int *levelOffsets, const int *levelSizes ); + void Load2DFromOwnedCompressedData( int width, int height, int numLevels, textureFormat_t textureFormat, textureColor_t colorFormat, byte *fileBuffer, const int *levelOffsets, const int *levelSizes ); void LoadCubeFromMemory( int width, const byte * pics[6], int numLevels, textureFormat_t & textureFormat, bool gammaMips ); void Clear(); ID_TIME_T LoadFromGeneratedFile( ID_TIME_T sourceFileTime ); ID_TIME_T LoadFromGeneratedFileUnchecked(); + ID_TIME_T LoadFromCompactGeneratedFileUnchecked(); ID_TIME_T WriteGeneratedFile( ID_TIME_T sourceFileTime ); bool LoadFromFile( idFile *file, int dataBytes = -1 ); bool WriteToFile( idFile *file, ID_TIME_T sourceFileTime ); diff --git a/src/imagetools/Image_files.cpp b/src/imagetools/Image_files.cpp index 5e25c65b..830800d0 100644 --- a/src/imagetools/Image_files.cpp +++ b/src/imagetools/Image_files.cpp @@ -502,8 +502,20 @@ static void LoadJPG( const char *filename, unsigned char **pic, int *width, int fileSystem->CloseFile( f ); return; // just getting timestamp } - fbuffer = (byte *)Mem_ClearedAlloc( len + 4096 ); - f->Read( fbuffer, len ); + // Only the 4096-byte tail needs zeroing (fill_input_buffer over-reads + // past the payload); the body is fully overwritten by the read. Zeroing + // just the pad avoids a full-buffer clear on multi-MB JPEGs. The + // short-read check is load-bearing, not cosmetic: with an uncleared + // body a truncated file would otherwise leave uninitialized heap for + // libjpeg to parse. + fbuffer = (byte *)Mem_Alloc( len + 4096 ); + memset( fbuffer + len, 0, 4096 ); + if ( f->Read( fbuffer, len ) != len ) { + Mem_Free( fbuffer ); + fileSystem->CloseFile( f ); + common->Warning( "LoadJPG( %s ): truncated file\n", filename ); + return; + } fileSystem->CloseFile( f ); } @@ -556,9 +568,19 @@ static void LoadJPG( const char *filename, unsigned char **pic, int *width, int row_stride = cinfo.output_width * cinfo.output_components; if (cinfo.output_components!=4) { - common->DWarning( "JPG %s is unsupported color depth (%d)", + common->DWarning( "JPG %s is unsupported color depth (%d)", filename, cinfo.output_components); } + // output_width/height are JDIMENSION (unsigned); the product * 4 wraps a + // 32-bit computation before widening to the allocation size, so the scanline + // loop below would then write far past a tiny buffer. Reject oversize + // dimensions up front, mirroring the TGA loader (libjpeg permits dimensions + // well beyond 2^31/4 pixels, so this is reachable on crafted files). + if ( (int64)cinfo.output_width * cinfo.output_height * 4 > 0x7FFFFFFF ) { + jpeg_destroy_decompress( &cinfo ); + Mem_Free( fbuffer ); + common->Error( "LoadJPG( %s ): dimensions too large (%i x %i)\n", filename, cinfo.output_width, cinfo.output_height ); + } out = (byte *)R_StaticAlloc(cinfo.output_width*cinfo.output_height*4); *pic = out; @@ -821,7 +843,7 @@ static bool R_ParseDDSFileInfo( const byte *header, int headerBytes, int fileSiz return true; } -static bool R_ReadDDSFileInfo( const char *name, ddsFileInfo_t &info, ID_TIME_T *timestamp ) { +static bool R_ReadDDSFileInfoUncached( const char *name, ddsFileInfo_t &info, ID_TIME_T *timestamp ) { if ( timestamp != NULL ) { *timestamp = FILE_NOT_FOUND_TIMESTAMP; } @@ -846,6 +868,69 @@ static bool R_ReadDDSFileInfo( const char *name, ddsFileInfo_t &info, ID_TIME_T return true; } +/* +============= +DDS probe memoization + +A level load resolves the same DDS replacement candidates several times per +image (source selection, the staleness timestamp probe, generated-cache +validation, and the decode pass), and every probe is a real file open across +all search paths. Search-path contents cannot change during the synchronous +BeginLevelLoad..EndLevelLoad window, so per-candidate probe results are +memoized there. Outside the window every call probes the filesystem directly, +which keeps reloadImages and hot-reload workflows seeing freshly dropped +files. Image loading is single-threaded (see parseBuffer in +Image_program.cpp), so plain statics are safe here. +============= +*/ +struct ddsProbeCacheEntry_t { + idStr name; + bool found; + ddsFileInfo_t info; + ID_TIME_T timestamp; +}; + +static bool ddsProbeCacheActive = false; +static idHashIndex ddsProbeCacheHash; +static idList ddsProbeCacheEntries; + +void R_SetDDSProbeCacheActive( bool active ) { + ddsProbeCacheActive = active; + ddsProbeCacheHash.Free(); + ddsProbeCacheEntries.Clear(); +} + +static bool R_ReadDDSFileInfo( const char *name, ddsFileInfo_t &info, ID_TIME_T *timestamp ) { + if ( !ddsProbeCacheActive ) { + return R_ReadDDSFileInfoUncached( name, info, timestamp ); + } + const int hashKey = ddsProbeCacheHash.GenerateKey( name, false ); + for ( int i = ddsProbeCacheHash.First( hashKey ); i >= 0; i = ddsProbeCacheHash.Next( i ) ) { + const ddsProbeCacheEntry_t &entry = ddsProbeCacheEntries[ i ]; + if ( entry.name.Icmp( name ) == 0 ) { + if ( timestamp != NULL ) { + *timestamp = entry.found ? entry.timestamp : FILE_NOT_FOUND_TIMESTAMP; + } + if ( entry.found ) { + info = entry.info; + } + return entry.found; + } + } + ddsProbeCacheEntry_t entry; + entry.name = name; + entry.timestamp = FILE_NOT_FOUND_TIMESTAMP; + entry.found = R_ReadDDSFileInfoUncached( name, entry.info, &entry.timestamp ); + if ( timestamp != NULL ) { + *timestamp = entry.found ? entry.timestamp : FILE_NOT_FOUND_TIMESTAMP; + } + if ( entry.found ) { + info = entry.info; + } + ddsProbeCacheHash.Add( hashKey, ddsProbeCacheEntries.Append( entry ) ); + return entry.found; +} + static bool R_ImageNameHasDDSShadowPrefix( const idStr &name ) { return name.Length() >= 4 && name[0] == 'd' && name[1] == 'd' && name[2] == 's' && name[3] == '/'; } @@ -1206,11 +1291,27 @@ bool R_LoadPrecompressedDDS( const char *cname, idBinaryImage &image, ID_TIME_T return false; } - byte *buffer = NULL; - const int fileSize = fileSystem->ReadFile( name.c_str(), (void **)&buffer, timestamp ); - if ( buffer == NULL || fileSize < DDS_HEADER_BYTES ) { + // Read into a buffer this library owns rather than fileSystem->ReadFile: the + // mip payloads stay behind as views into it inside the idBinaryImage, and its + // eventual Mem_Free in idBinaryImage::Clear must pair with this binary's + // allocator (the renderer modules carry their own idlib heap). + idFile *ddsFile = fileSystem->OpenFileRead( name.c_str() ); + if ( ddsFile == NULL ) { + if ( timestamp != NULL ) { + *timestamp = FILE_NOT_FOUND_TIMESTAMP; + } + return false; + } + if ( timestamp != NULL ) { + *timestamp = ddsFile->Timestamp(); + } + const int fileSize = ddsFile->Length(); + byte *buffer = fileSize >= DDS_HEADER_BYTES ? (byte *)Mem_Alloc( fileSize ) : NULL; + const bool readOk = buffer != NULL && ddsFile->Read( buffer, fileSize ) == fileSize; + fileSystem->CloseFile( ddsFile ); + if ( !readOk ) { if ( buffer != NULL ) { - fileSystem->FreeFile( buffer ); + Mem_Free( buffer ); } return false; } @@ -1284,12 +1385,15 @@ bool R_LoadPrecompressedDDS( const char *cname, idBinaryImage &image, ID_TIME_T info.format == DDS_STORED_FORMAT_RXGB || ( usage == TD_BUMP && textureFormat == FMT_DXT5 ); const textureColor_t colorFormat = rxgbNormal ? CFM_NORMAL_DXT5 : CFM_DEFAULT; - image.Load2DFromCompressedData( selectedWidth, selectedHeight, selectedLevels, textureFormat, colorFormat, + image.Load2DFromOwnedCompressedData( selectedWidth, selectedHeight, selectedLevels, textureFormat, colorFormat, buffer, levelOffsets.Ptr() + firstLevel, levelSizes.Ptr() + firstLevel ); + buffer = NULL; // the binary image now owns the file buffer loaded = true; } while ( false ); - fileSystem->FreeFile( buffer ); + if ( buffer != NULL ) { + Mem_Free( buffer ); + } return loaded; } @@ -1580,8 +1684,17 @@ static void R_LoadImageInternal( const char *cname, byte **pic, int *width, int name.ExtractFileExtension( ext ); idStr preferredDDSName; - if ( ext != "dds" && R_ResolvePreferredDDSImageSource( name.c_str(), preferredDDSName, NULL, false, NULL ) ) { - LoadDDS( preferredDDSName.c_str(), pic, width, height, timestamp, decodeRXGBNormalMap ); + ID_TIME_T preferredDDSTime = FILE_NOT_FOUND_TIMESTAMP; + if ( ext != "dds" && R_ResolvePreferredDDSImageSource( name.c_str(), preferredDDSName, &preferredDDSTime, false, NULL ) ) { + if ( pic == NULL ) { + // timestamp-only probe: the resolve already opened this file and read + // its timestamp, so don't open it a second time + if ( timestamp != NULL ) { + *timestamp = preferredDDSTime; + } + } else { + LoadDDS( preferredDDSName.c_str(), pic, width, height, timestamp, decodeRXGBNormalMap ); + } if ( R_LoadImageSucceeded( pic, timestamp ) ) { return; } diff --git a/src/imagetools/Image_program.cpp b/src/imagetools/Image_program.cpp index 52f63ff6..2fa2fa34 100644 --- a/src/imagetools/Image_program.cpp +++ b/src/imagetools/Image_program.cpp @@ -639,7 +639,7 @@ static bool R_ParseImageProgram_r(idLexer& src, byte** pic, int* width, int* hei R_LoadImageProgram =================== */ -void R_LoadImageProgram(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamps, textureUsage_t* usage) { +bool R_LoadImageProgram(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamps, textureUsage_t* usage) { idLexer src; src.LoadMemory(name, idLib::SizeToInt(strlen(name), "R_LoadImageProgram"), name); @@ -650,9 +650,10 @@ void R_LoadImageProgram(const char* name, byte** pic, int* width, int* height, I *timestamps = 0; } - R_ParseImageProgram_r(src, pic, width, height, timestamps, usage); + const bool loaded = R_ParseImageProgram_r(src, pic, width, height, timestamps, usage); src.FreeSource(); + return loaded; } /* diff --git a/src/render_geo/RenderGeometry.h b/src/render_geo/RenderGeometry.h index 6a91b953..ca259b7f 100644 --- a/src/render_geo/RenderGeometry.h +++ b/src/render_geo/RenderGeometry.h @@ -397,6 +397,10 @@ void R_AllocStaticTriSurfShadowVerts( srfTriangles_t *tri, int numVerts ); void R_AllocStaticTriSurfSilTraceVerts( srfTriangles_t *tri, int numVerts ); void R_AllocStaticSkinToModelTransforms( srfTriangles_t *tri, int numTransforms ); #endif +bool R_AllocStaticGpuSkinningJointPalette( srfTriangles_t *tri, int numJoints ); +void R_ClearStaticGpuSkinningJointPalette( srfTriangles_t *tri ); +void R_ReferenceStaticGpuSkinning( srfTriangles_t *tri, const srfTriangles_t *reference ); +bool R_CopyStaticGpuSkinning( srfTriangles_t *tri, const srfTriangles_t *source ); void R_AllocStaticTriSurfPlanes( srfTriangles_t *tri, int numIndexes ); void R_ResizeStaticTriSurfVerts( srfTriangles_t *tri, int numVerts ); void R_ResizeStaticTriSurfIndexes( srfTriangles_t *tri, int numIndexes ); @@ -425,6 +429,7 @@ srfTriangles_t * R_MergeTriangles( const srfTriangles_t *tri1, const srfTriangle void R_DeriveTangents( srfTriangles_t *tri, bool allocFacePlanes = true ); deformInfo_t * R_BuildDeformInfo( int numVerts, const idDrawVert *verts, int numIndexes, const int *indexes, bool useUnsmoothedTangents ); +deformInfo_t * R_AllocDeformInfo( int numSourceVerts, int numOutputVerts, int numIndexes, int numMirroredVerts, int numDupVerts, int numSilEdges, bool allocDominantTris ); void R_FreeDeformInfo( deformInfo_t *deformInfo ); int R_DeformInfoMemoryUsed( deformInfo_t *deformInfo ); diff --git a/src/render_geo/RenderGeometryPolytope.cpp b/src/render_geo/RenderGeometryPolytope.cpp index cec1909f..07310300 100644 --- a/src/render_geo/RenderGeometryPolytope.cpp +++ b/src/render_geo/RenderGeometryPolytope.cpp @@ -85,7 +85,14 @@ srfTriangles_t *R_PolytopeSurface( int numPlanes, const idPlane *planes, idWindi for ( i = 0; i < numPlanes; i++ ) { idFixedWinding &w = planeWindings[i]; const int windingPointCount = w.GetNumPoints(); - if ( !windingPointCount ) { + // Match the counting pass's filter exactly: it budgeted verts only for + // windings with >2 points, so a winding that clipped down to 1-2 points + // must be skipped here too or its verts would be written past the + // exact-size allocation. A 1-2 point winding produces no triangles anyway. + if ( windingPointCount <= 2 ) { + if ( windings ) { + windings[i] = NULL; + } continue; } for ( j = 0 ; j < windingPointCount ; j++ ) { diff --git a/src/render_geo/RenderGeometryTriSurf.cpp b/src/render_geo/RenderGeometryTriSurf.cpp index 386f5b11..4529046a 100644 --- a/src/render_geo/RenderGeometryTriSurf.cpp +++ b/src/render_geo/RenderGeometryTriSurf.cpp @@ -31,6 +31,7 @@ If you have questions concerning this license or the applicable additional terms #include "RenderGeometry.h" #include "../imagetools/ImageTools.h" +#include "../renderer/GpuSkinning.h" /* ============================================================================== @@ -139,6 +140,7 @@ static idDynamicBlockAlloc triSilEdgeAllocator; static idDynamicBlockAlloc triDominantTrisAllocator; static idDynamicBlockAlloc triMirroredVertAllocator; static idDynamicBlockAlloc triDupVertAllocator; +static idDynamicBlockAlloc gpuSkinningJointPaletteAllocator; #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) static idDynamicBlockAlloc silTraceVertexAllocator; static idDynamicBlockAlloc skinToModelTransformAllocator; @@ -153,6 +155,7 @@ static idDynamicAlloc triSilEdgeAllocator; static idDynamicAlloc triDominantTrisAllocator; static idDynamicAlloc triMirroredVertAllocator; static idDynamicAlloc triDupVertAllocator; +static idDynamicAlloc gpuSkinningJointPaletteAllocator; #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) static idDynamicAlloc silTraceVertexAllocator; static idDynamicAlloc skinToModelTransformAllocator; @@ -182,6 +185,7 @@ void R_InitTriSurfData( void ) { triDominantTrisAllocator.Init(); triMirroredVertAllocator.Init(); triDupVertAllocator.Init(); + gpuSkinningJointPaletteAllocator.Init(); #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) silTraceVertexAllocator.Init(); skinToModelTransformAllocator.Init(); @@ -197,6 +201,7 @@ void R_InitTriSurfData( void ) { triDominantTrisAllocator.SetLockMemory( true ); triMirroredVertAllocator.SetLockMemory( true ); triDupVertAllocator.SetLockMemory( true ); + gpuSkinningJointPaletteAllocator.SetLockMemory( true ); #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) silTraceVertexAllocator.SetLockMemory( true ); skinToModelTransformAllocator.SetLockMemory( true ); @@ -229,6 +234,7 @@ void R_ShutdownTriSurfData( void ) { triDominantTrisAllocator.Shutdown(); triMirroredVertAllocator.Shutdown(); triDupVertAllocator.Shutdown(); + gpuSkinningJointPaletteAllocator.Shutdown(); #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) silTraceVertexAllocator.Shutdown(); skinToModelTransformAllocator.Shutdown(); @@ -281,6 +287,11 @@ void R_ShowTriSurfMemory_f( const idCmdArgs &args ) { common->Printf( "%6d kB dup vert memory (%d kB free in %d blocks, %d empty base blocks)\n", triDupVertAllocator.GetBaseBlockMemory() >> 10, triDupVertAllocator.GetFreeBlockMemory() >> 10, triDupVertAllocator.GetNumFreeBlocks(), triDupVertAllocator.GetNumEmptyBaseBlocks() ); + common->Printf( "%6d kB GPU joint palette memory (%d kB free in %d blocks, %d empty base blocks)\n", + gpuSkinningJointPaletteAllocator.GetBaseBlockMemory() >> 10, + gpuSkinningJointPaletteAllocator.GetFreeBlockMemory() >> 10, + gpuSkinningJointPaletteAllocator.GetNumFreeBlocks(), + gpuSkinningJointPaletteAllocator.GetNumEmptyBaseBlocks() ); common->Printf( "%6zu kB total triangle memory\n", ( srfTrianglesAllocator.GetAllocCount() * sizeof( srfTriangles_t ) + @@ -292,7 +303,8 @@ void R_ShowTriSurfMemory_f( const idCmdArgs &args ) { triSilEdgeAllocator.GetBaseBlockMemory() + triDominantTrisAllocator.GetBaseBlockMemory() + triMirroredVertAllocator.GetBaseBlockMemory() + - triDupVertAllocator.GetBaseBlockMemory() ) >> 10 ); + triDupVertAllocator.GetBaseBlockMemory() + + gpuSkinningJointPaletteAllocator.GetBaseBlockMemory() ) >> 10 ); } /* @@ -344,6 +356,10 @@ int R_TriSurfMemory( const srfTriangles_t *tri ) { if ( tri->dupVerts != NULL ) { total += tri->numDupVerts * sizeof( tri->dupVerts[0] ); } + if ( tri->gpuSkinningJointPaletteAlloc != NULL ) { + total += tri->numGpuSkinningJointPaletteAllocJoints + * GPU_SKINNING_JOINT_FLOATS * sizeof( tri->gpuSkinningJointPaletteAlloc[0] ); + } total += sizeof( *tri ); @@ -476,6 +492,14 @@ void R_ReallyFreeStaticTriSurf( srfTriangles_t *tri ) { triShadowVertexAllocator.Free( tri->shadowVertexes ); } + if ( tri->gpuSkinningJointPaletteAlloc != NULL ) { + gpuSkinningJointPaletteAllocator.Free( tri->gpuSkinningJointPaletteAlloc ); + tri->gpuSkinningJointPaletteAlloc = NULL; + tri->gpuSkinningJointPalette = NULL; + tri->numGpuSkinningJoints = 0; + tri->numGpuSkinningJointPaletteAllocJoints = 0; + } + #ifdef _DEBUG memset( tri, 0, sizeof( srfTriangles_t ) ); #endif @@ -583,6 +607,7 @@ srfTriangles_t *R_CopyStaticTriSurf( const srfTriangles_t *tri ) { if ( tri->numIndexes > 0 ) { memcpy( newTri->indexes, tri->indexes, tri->numIndexes * sizeof( newTri->indexes[0] ) ); } + R_CopyStaticGpuSkinning( newTri, tri ); return newTri; } @@ -605,6 +630,7 @@ void RenderGeo_FreeEmptyBaseBlocks( void ) { triDominantTrisAllocator.FreeEmptyBaseBlocks(); triMirroredVertAllocator.FreeEmptyBaseBlocks(); triDupVertAllocator.FreeEmptyBaseBlocks(); + gpuSkinningJointPaletteAllocator.FreeEmptyBaseBlocks(); #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) silTraceVertexAllocator.FreeEmptyBaseBlocks(); skinToModelTransformAllocator.FreeEmptyBaseBlocks(); @@ -668,6 +694,7 @@ void R_AllocStaticSkinToModelTransforms( srfTriangles_t *tri, int numTransforms if ( tri->skinToModelTransformsAlloc != NULL ) { if ( tri->numSkinToModelTransforms >= numTransforms ) { tri->skinToModelTransforms = tri->skinToModelTransformsAlloc; + tri->numSkinToModelTransforms = numTransforms; return; } @@ -680,6 +707,131 @@ void R_AllocStaticSkinToModelTransforms( srfTriangles_t *tri, int numTransforms } #endif +/* +================= +R_ClearStaticGpuSkinningJointPalette + +Clear the visible contract while retaining an owned allocation for reuse by a +later snapshot. This prevents stale eligibility without creating frame churn. +================= +*/ +void R_ClearStaticGpuSkinningJointPalette( srfTriangles_t *tri ) { + if ( tri == NULL ) { + return; + } + + tri->gpuSkinningBindPoseVerts = NULL; + tri->gpuSkinningVerts = NULL; + tri->numGpuSkinningVerts = 0; + tri->gpuSkinningJointPalette = NULL; + tri->numGpuSkinningJoints = 0; + tri->gpuSkinningPaletteGeneration = 0; + tri->gpuSkinningFallbackReason = 0; + tri->gpuSkinningSignedWeights = false; +} + +/* +================= +R_AllocStaticGpuSkinningJointPalette +================= +*/ +bool R_AllocStaticGpuSkinningJointPalette( srfTriangles_t *tri, int numJoints ) { + if ( tri == NULL || numJoints <= 0 || numJoints > GPU_SKINNING_MAX_JOINTS ) { + return false; + } + + if ( tri->gpuSkinningJointPaletteAlloc != NULL + && tri->numGpuSkinningJointPaletteAllocJoints < numJoints ) { + gpuSkinningJointPaletteAllocator.Free( tri->gpuSkinningJointPaletteAlloc ); + tri->gpuSkinningJointPaletteAlloc = NULL; + tri->numGpuSkinningJointPaletteAllocJoints = 0; + } + + if ( tri->gpuSkinningJointPaletteAlloc == NULL ) { + tri->gpuSkinningJointPaletteAlloc = gpuSkinningJointPaletteAllocator.Alloc( + numJoints * GPU_SKINNING_JOINT_FLOATS ); + if ( tri->gpuSkinningJointPaletteAlloc == NULL ) { + return false; + } + tri->numGpuSkinningJointPaletteAllocJoints = numJoints; + } + + tri->gpuSkinningJointPalette = tri->gpuSkinningJointPaletteAlloc; + tri->numGpuSkinningJoints = numJoints; + return true; +} + +/* +================= +R_ReferenceStaticGpuSkinning + +Reference immutable sidecars and a snapshot palette without taking ownership. +================= +*/ +void R_ReferenceStaticGpuSkinning( srfTriangles_t *tri, const srfTriangles_t *reference ) { + if ( tri == NULL ) { + return; + } + + if ( tri->gpuSkinningJointPaletteAlloc != NULL ) { + gpuSkinningJointPaletteAllocator.Free( tri->gpuSkinningJointPaletteAlloc ); + } + tri->gpuSkinningJointPaletteAlloc = NULL; + tri->numGpuSkinningJointPaletteAllocJoints = 0; + + if ( reference == NULL ) { + R_ClearStaticGpuSkinningJointPalette( tri ); + return; + } + + tri->gpuSkinningBindPoseVerts = reference->gpuSkinningBindPoseVerts; + tri->gpuSkinningVerts = reference->gpuSkinningVerts; + tri->numGpuSkinningVerts = reference->numGpuSkinningVerts; + tri->gpuSkinningJointPalette = reference->gpuSkinningJointPalette; + tri->numGpuSkinningJoints = reference->numGpuSkinningJoints; + tri->gpuSkinningPaletteGeneration = reference->gpuSkinningPaletteGeneration; + tri->gpuSkinningFallbackReason = reference->gpuSkinningFallbackReason; + tri->gpuSkinningSignedWeights = reference->gpuSkinningSignedWeights; +} + +/* +================= +R_CopyStaticGpuSkinning +================= +*/ +bool R_CopyStaticGpuSkinning( srfTriangles_t *tri, const srfTriangles_t *source ) { + if ( tri == NULL || source == NULL ) { + return false; + } + if ( tri == source ) { + return true; + } + + tri->gpuSkinningBindPoseVerts = source->gpuSkinningBindPoseVerts; + tri->gpuSkinningVerts = source->gpuSkinningVerts; + tri->numGpuSkinningVerts = source->numGpuSkinningVerts; + tri->gpuSkinningFallbackReason = source->gpuSkinningFallbackReason; + tri->gpuSkinningSignedWeights = source->gpuSkinningSignedWeights; + tri->gpuSkinningPaletteGeneration = source->gpuSkinningPaletteGeneration; + + if ( source->gpuSkinningJointPalette == NULL || source->numGpuSkinningJoints <= 0 ) { + tri->gpuSkinningJointPalette = NULL; + tri->numGpuSkinningJoints = 0; + return true; + } + + if ( !R_AllocStaticGpuSkinningJointPalette( tri, source->numGpuSkinningJoints ) ) { + R_ClearStaticGpuSkinningJointPalette( tri ); + return false; + } + + memcpy( tri->gpuSkinningJointPalette, source->gpuSkinningJointPalette, + source->numGpuSkinningJoints * GPU_SKINNING_JOINT_FLOATS + * sizeof( source->gpuSkinningJointPalette[0] ) ); + tri->gpuSkinningPaletteGeneration = source->gpuSkinningPaletteGeneration; + return true; +} + /* ================= R_AllocStaticTriSurfPlanes @@ -746,6 +898,7 @@ void R_ReferenceStaticTriSurfVerts( srfTriangles_t *tri, const srfTriangles_t *r : const_cast( reference ); ++tri->topAmbientSurface->referenceCount; tri->myID = ++triSurfReferenceId; + R_ReferenceStaticGpuSkinning( tri, reference ); } /* @@ -2379,6 +2532,47 @@ deformInfo_t *R_BuildDeformInfo( int numVerts, const idDrawVert *verts, int numI return deform; } +/* +=================== +R_AllocDeformInfo + +Allocates a deformInfo_t with the given counts without running any derivation. +Used by the generated-cache readers, whose payloads already carry the fully +range-validated derived arrays; re-deriving them only to overwrite the results +with the cached bytes wasted the cache hit. Array NULL-ness matches +R_BuildDeformInfo exactly: the allocators return NULL for Alloc(0), so +R_FreeDeformInfo and R_FreeStaticTriSurf behave identically. +If the derivation in R_BuildDeformInfo ever changes, bump +MD5_MODEL_GENERATED_CACHE_PARSER_VERSION and +STATIC_MODEL_GENERATED_CACHE_PARSER_VERSION so stale payloads regenerate. +=================== +*/ +deformInfo_t *R_AllocDeformInfo( int numSourceVerts, int numOutputVerts, int numIndexes, + int numMirroredVerts, int numDupVerts, int numSilEdges, + bool allocDominantTris ) { + deformInfo_t *deform = (deformInfo_t *)R_ClearedStaticAlloc( sizeof( *deform ) ); + + deform->numSourceVerts = numSourceVerts; + deform->numOutputVerts = numOutputVerts; + + deform->numIndexes = numIndexes; + deform->indexes = triIndexAllocator.Alloc( numIndexes ); + deform->silIndexes = triSilIndexAllocator.Alloc( numIndexes ); + + deform->numMirroredVerts = numMirroredVerts; + deform->mirroredVerts = triMirroredVertAllocator.Alloc( numMirroredVerts ); + + deform->numDupVerts = numDupVerts; + deform->dupVerts = triDupVertAllocator.Alloc( numDupVerts * 2 ); + + deform->numSilEdges = numSilEdges; + deform->silEdges = triSilEdgeAllocator.Alloc( numSilEdges ); + + deform->dominantTris = allocDominantTris ? triDominantTrisAllocator.Alloc( numOutputVerts ) : NULL; + + return deform; +} + /* =================== R_FreeDeformInfo diff --git a/src/renderer/AdvancedLightingCore.h b/src/renderer/AdvancedLightingCore.h new file mode 100644 index 00000000..15b7a859 --- /dev/null +++ b/src/renderer/AdvancedLightingCore.h @@ -0,0 +1,288 @@ +// Copyright (C) 2026 DarkMatter Productions +// + + +#ifndef __ADVANCED_LIGHTING_CORE_H__ +#define __ADVANCED_LIGHTING_CORE_H__ + +#include +#include + +/* +=============================================================================== + + Device-independent ownership rules for Milestone F lighting records. + + The GL clustered implementation supplies the storage and raster work, but the + transaction, generation, volume, and deterministic probe-selection rules stay + free of renderer and graphics-API types. That keeps malformed, overflow, and + stale-generation behaviour testable on every build host. + +=============================================================================== +*/ + +const int ADVANCED_LIGHTING_PROBES_PER_CLUSTER = 2; + +typedef enum advancedLightingRecordKind_e { + ADVANCED_LIGHTING_RECORD_PROBE = 0, + ADVANCED_LIGHTING_RECORD_DECAL +} advancedLightingRecordKind_t; + +typedef enum advancedLightingReject_e { + ADVANCED_LIGHTING_REJECT_NONE = 0, + ADVANCED_LIGHTING_REJECT_DISABLED, + ADVANCED_LIGHTING_REJECT_INVALID_GENERATION, + ADVANCED_LIGHTING_REJECT_INVALID_IDENTITY, + ADVANCED_LIGHTING_REJECT_INVALID_VOLUME, + ADVANCED_LIGHTING_REJECT_INVALID_KIND, + ADVANCED_LIGHTING_REJECT_CAPACITY, + ADVANCED_LIGHTING_REJECT_REFERENCE_CAPACITY, + ADVANCED_LIGHTING_REJECT_RESOURCES +} advancedLightingReject_t; + +typedef struct advancedLightingVolume_s { + float origin[3]; + float extents[3]; + std::uint32_t generation; + int stableId; +} advancedLightingVolume_t; + +typedef struct advancedLightingTransaction_s { + bool enabled; + bool sealed; + bool complete; + std::uint32_t generation; + int probeCapacity; + int decalCapacity; + int referenceCapacity; + int probeCount; + int decalCount; + int probeReferences; + int decalReferences; + int rejectedRecords; + advancedLightingReject_t firstReject; +} advancedLightingTransaction_t; + +typedef struct advancedLightingProbeCandidate_s { + int probeIndex; + int priority; + int stableId; + float weight; +} advancedLightingProbeCandidate_t; + +inline bool AdvancedLightingCore_Finite( float value ) { + return std::isfinite( value ); +} + +inline bool AdvancedLightingCore_ValidateVolume( + const advancedLightingVolume_t &volume, std::uint32_t generation, + advancedLightingReject_t &reject ) { + if ( volume.generation == 0 || volume.generation != generation ) { + reject = ADVANCED_LIGHTING_REJECT_INVALID_GENERATION; + return false; + } + if ( volume.stableId < 0 ) { + reject = ADVANCED_LIGHTING_REJECT_INVALID_IDENTITY; + return false; + } + for ( int axis = 0; axis < 3; ++axis ) { + if ( !AdvancedLightingCore_Finite( volume.origin[axis] ) + || !AdvancedLightingCore_Finite( volume.extents[axis] ) + || volume.extents[axis] <= 0.0f ) { + reject = ADVANCED_LIGHTING_REJECT_INVALID_VOLUME; + return false; + } + } + reject = ADVANCED_LIGHTING_REJECT_NONE; + return true; +} + +inline void AdvancedLightingCore_Begin( advancedLightingTransaction_t &state, + bool enabled, std::uint32_t generation, int probeCapacity, + int decalCapacity, int referenceCapacity ) { + state.enabled = enabled; + state.sealed = false; + state.complete = false; + state.generation = generation; + state.probeCapacity = probeCapacity > 0 ? probeCapacity : 0; + state.decalCapacity = decalCapacity > 0 ? decalCapacity : 0; + state.referenceCapacity = referenceCapacity > 0 ? referenceCapacity : 0; + state.probeCount = 0; + state.decalCount = 0; + state.probeReferences = 0; + state.decalReferences = 0; + state.rejectedRecords = 0; + state.firstReject = enabled + ? ADVANCED_LIGHTING_REJECT_NONE + : ADVANCED_LIGHTING_REJECT_DISABLED; +} + +inline void AdvancedLightingCore_Reject( advancedLightingTransaction_t &state, + advancedLightingReject_t reject ) { + state.rejectedRecords++; + if ( state.firstReject == ADVANCED_LIGHTING_REJECT_NONE ) { + state.firstReject = reject; + } +} + +inline bool AdvancedLightingCore_Admit( advancedLightingTransaction_t &state, + advancedLightingRecordKind_t kind, + const advancedLightingVolume_t &volume ) { + if ( !state.enabled || state.sealed ) { + AdvancedLightingCore_Reject( state, ADVANCED_LIGHTING_REJECT_DISABLED ); + return false; + } + if ( kind != ADVANCED_LIGHTING_RECORD_PROBE + && kind != ADVANCED_LIGHTING_RECORD_DECAL ) { + AdvancedLightingCore_Reject( state, ADVANCED_LIGHTING_REJECT_INVALID_KIND ); + return false; + } + advancedLightingReject_t reject = ADVANCED_LIGHTING_REJECT_NONE; + if ( !AdvancedLightingCore_ValidateVolume( volume, state.generation, reject ) ) { + AdvancedLightingCore_Reject( state, reject ); + return false; + } + int *count = kind == ADVANCED_LIGHTING_RECORD_PROBE + ? &state.probeCount : &state.decalCount; + const int capacity = kind == ADVANCED_LIGHTING_RECORD_PROBE + ? state.probeCapacity : state.decalCapacity; + if ( *count >= capacity ) { + AdvancedLightingCore_Reject( state, ADVANCED_LIGHTING_REJECT_CAPACITY ); + return false; + } + ( *count )++; + return true; +} + +inline bool AdvancedLightingCore_AddReference( + advancedLightingTransaction_t &state, + advancedLightingRecordKind_t kind ) { + if ( !state.enabled || state.sealed ) { + AdvancedLightingCore_Reject( state, ADVANCED_LIGHTING_REJECT_DISABLED ); + return false; + } + if ( kind != ADVANCED_LIGHTING_RECORD_PROBE + && kind != ADVANCED_LIGHTING_RECORD_DECAL ) { + AdvancedLightingCore_Reject( state, ADVANCED_LIGHTING_REJECT_INVALID_KIND ); + return false; + } + const int total = state.probeReferences + state.decalReferences; + if ( total >= state.referenceCapacity ) { + AdvancedLightingCore_Reject( + state, ADVANCED_LIGHTING_REJECT_REFERENCE_CAPACITY ); + return false; + } + if ( kind == ADVANCED_LIGHTING_RECORD_PROBE ) { + state.probeReferences++; + } else { + state.decalReferences++; + } + return true; +} + +inline bool AdvancedLightingCore_Seal( advancedLightingTransaction_t &state, + bool resourcesReady ) { + state.sealed = true; + if ( !resourcesReady && state.firstReject == ADVANCED_LIGHTING_REJECT_NONE ) { + state.firstReject = ADVANCED_LIGHTING_REJECT_RESOURCES; + } + state.complete = state.enabled && resourcesReady + && state.firstReject == ADVANCED_LIGHTING_REJECT_NONE; + return state.complete; +} + +inline bool AdvancedLightingCore_ProbeBefore( + const advancedLightingProbeCandidate_t &a, + const advancedLightingProbeCandidate_t &b ) { + if ( a.priority != b.priority ) { + return a.priority > b.priority; + } + if ( a.weight != b.weight ) { + return a.weight > b.weight; + } + if ( a.stableId != b.stableId ) { + return a.stableId < b.stableId; + } + return a.probeIndex < b.probeIndex; +} + +inline void AdvancedLightingCore_SelectProbe( + advancedLightingProbeCandidate_t selected[ADVANCED_LIGHTING_PROBES_PER_CLUSTER], + const advancedLightingProbeCandidate_t &candidate ) { + if ( candidate.probeIndex < 0 || candidate.stableId < 0 + || !AdvancedLightingCore_Finite( candidate.weight ) + || candidate.weight <= 0.0f ) { + return; + } + for ( int index = 0; index < ADVANCED_LIGHTING_PROBES_PER_CLUSTER; ++index ) { + if ( selected[index].probeIndex == candidate.probeIndex ) { + if ( AdvancedLightingCore_ProbeBefore( candidate, selected[index] ) ) { + selected[index] = candidate; + } + return; + } + } + for ( int index = 0; index < ADVANCED_LIGHTING_PROBES_PER_CLUSTER; ++index ) { + if ( selected[index].probeIndex < 0 + || AdvancedLightingCore_ProbeBefore( candidate, selected[index] ) ) { + for ( int move = ADVANCED_LIGHTING_PROBES_PER_CLUSTER - 1; + move > index; --move ) { + selected[move] = selected[move - 1]; + } + selected[index] = candidate; + return; + } + } +} + +inline float AdvancedLightingCore_ProbeWeight( + const advancedLightingVolume_t &volume, const float position[3], + float blendDistance ) { + if ( !AdvancedLightingCore_Finite( blendDistance ) || blendDistance <= 0.0f ) { + blendDistance = 0.001f; + } + float edgeDistance = volume.extents[0]; + for ( int axis = 0; axis < 3; ++axis ) { + if ( !AdvancedLightingCore_Finite( position[axis] ) ) { + return 0.0f; + } + const float normalized = std::fabs( position[axis] - volume.origin[axis] ) + / volume.extents[axis]; + if ( normalized >= 1.0f ) { + return 0.0f; + } + const float axisEdgeDistance = ( 1.0f - normalized ) * volume.extents[axis]; + edgeDistance = axis == 0 || axisEdgeDistance < edgeDistance + ? axisEdgeDistance : edgeDistance; + } + const float weight = edgeDistance / blendDistance; + return weight < 0.0f ? 0.0f : ( weight > 1.0f ? 1.0f : weight ); +} + +inline float AdvancedLightingCore_SphericalProbeWeight( + const float origin[3], float radius, const float position[3], + float blendDistance ) { + if ( !AdvancedLightingCore_Finite( radius ) || radius <= 0.0f + || !AdvancedLightingCore_Finite( blendDistance ) + || blendDistance <= 0.0f ) { + return 0.0f; + } + float distanceSquared = 0.0f; + for ( int axis = 0; axis < 3; ++axis ) { + if ( !AdvancedLightingCore_Finite( origin[axis] ) + || !AdvancedLightingCore_Finite( position[axis] ) ) { + return 0.0f; + } + const float delta = position[axis] - origin[axis]; + distanceSquared += delta * delta; + } + if ( !AdvancedLightingCore_Finite( distanceSquared ) + || distanceSquared >= radius * radius ) { + return 0.0f; + } + const float distance = std::sqrt( distanceSquared ); + const float weight = ( radius - distance ) / blendDistance; + return weight < 0.0f ? 0.0f : ( weight > 1.0f ? 1.0f : weight ); +} + +#endif /* !__ADVANCED_LIGHTING_CORE_H__ */ diff --git a/src/renderer/AdvancedScreenSpaceCore.h b/src/renderer/AdvancedScreenSpaceCore.h new file mode 100644 index 00000000..fea00b5b --- /dev/null +++ b/src/renderer/AdvancedScreenSpaceCore.h @@ -0,0 +1,114 @@ +// Copyright (C) 2026 DarkMatter Productions +// + + +#ifndef __ADVANCED_SCREEN_SPACE_CORE_H__ +#define __ADVANCED_SCREEN_SPACE_CORE_H__ + +#include + +/* +=============================================================================== + + Bounded, device-independent admission for the optional screen-space + advanced-lighting tail. The packed eight-float contract deliberately fits + the established OpenGL uniform path and the unused w components of Vulkan's + 256-byte temporal resolve block. + + All leaves are additionally controlled by the Milestone-F master. Invalid + or non-finite tuning input is clamped to a conservative value, and disabling + the master publishes an exact zero-feature packet. + +=============================================================================== +*/ + +const int ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES = 16; +const int ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS = 16; +const int ADVANCED_SCREEN_SPACE_SSGI_TAPS = 8; + +typedef enum advancedScreenSpaceEffectBits_e { + ADVANCED_SCREEN_SPACE_EFFECT_NONE = 0, + ADVANCED_SCREEN_SPACE_EFFECT_FROXEL = 1 << 0, + ADVANCED_SCREEN_SPACE_EFFECT_SSR = 1 << 1, + ADVANCED_SCREEN_SPACE_EFFECT_SSGI = 1 << 2 +} advancedScreenSpaceEffectBits_t; + +typedef struct advancedScreenSpaceConfig_s { + unsigned int effectMask; + float froxelDensity; + float froxelMaxDistance; + int froxelSlices; + float ssrIntensity; + float ssrMaxDistance; + int ssrSteps; + float ssgiIntensity; +} advancedScreenSpaceConfig_t; + +inline float AdvancedScreenSpaceCore_ClampFinite( float value, + float minimum, float maximum, float fallback ) { + if ( !std::isfinite( value ) ) { + value = fallback; + } + return value < minimum ? minimum : ( value > maximum ? maximum : value ); +} + +inline int AdvancedScreenSpaceCore_ClampInt( int value, + int minimum, int maximum ) { + return value < minimum ? minimum : ( value > maximum ? maximum : value ); +} + +inline advancedScreenSpaceConfig_t AdvancedScreenSpaceCore_Build( + bool masterEnabled, bool froxelEnabled, bool ssrEnabled, + bool ssgiEnabled, float froxelDensity, float froxelMaxDistance, + int froxelSlices, float ssrIntensity, float ssrMaxDistance, + int ssrSteps, float ssgiIntensity ) { + advancedScreenSpaceConfig_t config = {}; + if ( !masterEnabled ) { + return config; + } + if ( froxelEnabled ) { + config.effectMask |= ADVANCED_SCREEN_SPACE_EFFECT_FROXEL; + config.froxelDensity = AdvancedScreenSpaceCore_ClampFinite( + froxelDensity, 0.00001f, 0.01f, 0.00012f ); + config.froxelMaxDistance = AdvancedScreenSpaceCore_ClampFinite( + froxelMaxDistance, 64.0f, 8192.0f, 2048.0f ); + config.froxelSlices = AdvancedScreenSpaceCore_ClampInt( + froxelSlices, 4, ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES ); + } + if ( ssrEnabled ) { + config.effectMask |= ADVANCED_SCREEN_SPACE_EFFECT_SSR; + config.ssrIntensity = AdvancedScreenSpaceCore_ClampFinite( + ssrIntensity, 0.0f, 1.0f, 0.35f ); + config.ssrMaxDistance = AdvancedScreenSpaceCore_ClampFinite( + ssrMaxDistance, 32.0f, 2048.0f, 512.0f ); + config.ssrSteps = AdvancedScreenSpaceCore_ClampInt( + ssrSteps, 4, ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS ); + } + if ( ssgiEnabled ) { + config.effectMask |= ADVANCED_SCREEN_SPACE_EFFECT_SSGI; + config.ssgiIntensity = AdvancedScreenSpaceCore_ClampFinite( + ssgiIntensity, 0.0f, 1.0f, 0.25f ); + } + return config; +} + +inline bool AdvancedScreenSpaceCore_Requested( + const advancedScreenSpaceConfig_t &config ) { + return config.effectMask != ADVANCED_SCREEN_SPACE_EFFECT_NONE; +} + +// Layout: mask, froxel density/distance/slices, SSR intensity/distance/steps, +// SSGI intensity. Keep this stable across both native backends. +inline void AdvancedScreenSpaceCore_Pack( + const advancedScreenSpaceConfig_t &config, float packed[8] ) { + packed[0] = static_cast( config.effectMask ); + packed[1] = config.froxelDensity; + packed[2] = config.froxelMaxDistance; + packed[3] = static_cast( config.froxelSlices ); + packed[4] = config.ssrIntensity; + packed[5] = config.ssrMaxDistance; + packed[6] = static_cast( config.ssrSteps ); + packed[7] = config.ssgiIntensity; +} + +#endif /* !__ADVANCED_SCREEN_SPACE_CORE_H__ */ diff --git a/src/renderer/Cinematic.cpp b/src/renderer/Cinematic.cpp index 41a68be9..d081fc98 100644 --- a/src/renderer/Cinematic.cpp +++ b/src/renderer/Cinematic.cpp @@ -91,12 +91,13 @@ class idCinematicLocal : public idCinematic { bool looping; bool dirty; + int lastDecodeSerial; bool half; bool smootheddouble; bool inMemory; void RoQ_init(void); - void blitVQQuad32fs(byte** status, unsigned char* data); + void blitVQQuad32fs(byte** status, unsigned char* data, const unsigned char* dataEnd); void RoQShutdown(void); void RoQInterrupt(void); @@ -145,6 +146,10 @@ static unsigned short* vq2 = NULL; static unsigned short* vq4 = NULL; static unsigned short* vq8 = NULL; +// single monotonic serial shared by all cinematic instances in this binary; +// a (instance pointer, serial) pair therefore never repeats +static int cinematicFrameSerial = 0; + //=========================================== @@ -173,8 +178,12 @@ void idCinematic::InitCinematic(void) { ROQ_YY_tab[i] = (int)((i << 6) | (i >> 2)); } - // RoQInterrupt reads RoQFrameSize + 8 bytes (chunk payload plus next chunk header) - file = (byte*)Mem_Alloc(65536 + 8); + // RoQInterrupt reads RoQFrameSize + 8 bytes (chunk payload plus next chunk + // header). The extra 64-byte tail is slack: blitVQQuad32fs bounds its walk + // at the chunk-payload end, but a single decode iteration may over-advance + // the read cursor by a few bytes past that checkpoint, so keep those reads + // inside the allocation. + file = (byte*)Mem_Alloc(65536 + 8 + 64); vq2 = (word*)Mem_Alloc(256 * 16 * 4 * sizeof(word)); vq4 = (word*)Mem_Alloc(256 * 64 * 4 * sizeof(word)); vq8 = (word*)Mem_Alloc(256 * 256 * 4 * sizeof(word)); @@ -269,6 +278,7 @@ idCinematicLocal::idCinematicLocal idCinematicLocal::idCinematicLocal() { image = NULL; imageCapacity = 0; + lastDecodeSerial = 0; status = FMV_EOF; buf = NULL; iFile = NULL; @@ -478,6 +488,7 @@ cinData_t idCinematicLocal::ImageForTime(int thisTime) { cinData.imageHeight = CIN_HEIGHT; cinData.status = status; cinData.image = buf; + cinData.frameSerial = lastDecodeSerial; return cinData; } @@ -839,7 +850,7 @@ void idCinematicLocal::blit2_32(byte* src, byte* dst, int spl) { idCinematicLocal::blitVQQuad32fs ============== */ -void idCinematicLocal::blitVQQuad32fs(byte** status, unsigned char* data) { +void idCinematicLocal::blitVQQuad32fs(byte** status, unsigned char* data, const unsigned char* dataEnd) { unsigned short newd, celdata, code; unsigned int index, i; @@ -848,6 +859,13 @@ void idCinematicLocal::blitVQQuad32fs(byte** status, unsigned char* data) { index = 0; do { + // A well-formed VQ frame terminates on the NULL status sentinel before + // its cel stream is exhausted; a corrupt one can keep consuming codes, + // so stop once the read cursor reaches the chunk-payload end rather than + // walking off the shared file buffer. + if (data >= dataEnd) { + break; + } if (!newd) { newd = 7; celdata = data[0] + data[1] * 256; @@ -896,7 +914,16 @@ void idCinematicLocal::blitVQQuad32fs(byte** status, unsigned char* data) { data++; break; case 0x4000: // motion compensation - move4_32(status[index] + mcomp[(*data)], status[index], samplesPerLine); + { + // mcomp[] is built from attacker-controllable frame-header + // offsets; validate the 4x4 source block stays inside the + // decode image before reading it, else drop the block. + const ptrdiff_t srcOff = (status[index] - image) + mcomp[(*data)]; + const ptrdiff_t span4 = (ptrdiff_t)3 * samplesPerLine + 16; + if (image != NULL && srcOff >= 0 && (size_t)srcOff + (size_t)span4 <= imageCapacity) { + move4_32(image + srcOff, status[index], samplesPerLine); + } + } data++; break; } @@ -904,7 +931,15 @@ void idCinematicLocal::blitVQQuad32fs(byte** status, unsigned char* data) { } break; case 0x4000: // motion compensation - move8_32(status[index] + mcomp[(*data)], status[index], samplesPerLine); + { + // See the 4x4 case: bound the 8x8 motion-compensation source + // block against the decode image before reading it. + const ptrdiff_t srcOff = (status[index] - image) + mcomp[(*data)]; + const ptrdiff_t span8 = (ptrdiff_t)7 * samplesPerLine + 32; + if (image != NULL && srcOff >= 0 && (size_t)srcOff + (size_t)span8 <= imageCapacity) { + move8_32(image + srcOff, status[index], samplesPerLine); + } + } data++; index += 5; break; @@ -1506,16 +1541,24 @@ void idCinematicLocal::RoQInterrupt(void) { switch (roq_id) { case ROQ_QUAD_VQ: + // A ROQ_QUAD_INFO chunk (which allocates qStatus/image and leaves + // numQuads >= 0) must precede any VQ frame. A malformed stream that + // sends VQ first leaves numQuads at its -1 init value and would walk + // the uninitialized qStatus pointer arrays as blit destinations. + if (numQuads < 0) { + status = FMV_EOF; + return; + } if ((numQuads & 1)) { normalBuffer0 = t[1]; RoQPrepMcomp(roqF0, roqF1); - blitVQQuad32fs(qStatus[1], framedata); + blitVQQuad32fs(qStatus[1], framedata, framedata + RoQFrameSize); buf = image + screenDelta; } else { normalBuffer0 = t[0]; RoQPrepMcomp(roqF0, roqF1); - blitVQQuad32fs(qStatus[0], framedata); + blitVQQuad32fs(qStatus[0], framedata, framedata + RoQFrameSize); buf = image; } if (numQuads == 0) { // first frame @@ -1523,6 +1566,7 @@ void idCinematicLocal::RoQInterrupt(void) { } numQuads++; dirty = true; + lastDecodeSerial = ++cinematicFrameSerial; break; case ROQ_CODEBOOK: decodeCodeBook(framedata, (unsigned short)roq_flags); @@ -1553,6 +1597,7 @@ void idCinematicLocal::RoQInterrupt(void) { JPEGBlit(image, framedata, RoQFrameSize); memcpy(image + screenDelta, image, samplesPerLine * ysize); numQuads++; + lastDecodeSerial = ++cinematicFrameSerial; } break; default: diff --git a/src/renderer/Cinematic.h b/src/renderer/Cinematic.h index 4a6180c6..fb5d9ddc 100644 --- a/src/renderer/Cinematic.h +++ b/src/renderer/Cinematic.h @@ -56,6 +56,7 @@ typedef struct { int imageWidth, imageHeight; // will be a power of 2 const byte * image; // RGBA format, alpha will be 255 int status; + int frameSerial; // globally monotonic serial of the decode that produced `image`; 0 = producer does not participate, consumers must treat every call as new pixels } cinData_t; class idCinematic { diff --git a/src/renderer/ClassicCinematicPostDomain.cpp b/src/renderer/ClassicCinematicPostDomain.cpp new file mode 100644 index 00000000..5a4d5530 --- /dev/null +++ b/src/renderer/ClassicCinematicPostDomain.cpp @@ -0,0 +1,939 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicCinematicPostDomain.h" +#include "ClassicSubviewDomain.h" + +#include +#include +#include + +static_assert( static_cast( CLASSIC_CINEMATIC_POST_BACKEND_GL ) + == static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ), + "classic cinematic/post and subview GL backend ordinals must match" ); +static_assert( static_cast( CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ) + == static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ), + "classic cinematic/post and subview Vulkan backend ordinals must match" ); +static_assert( static_cast( CLASSIC_CINEMATIC_POST_BACKEND_COUNT ) + == static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT ), + "classic cinematic/post and subview backend counts must match" ); + +typedef struct classicCinematicPostDomainState_s { + classicCinematicPostDomainView_t views[ + CLASSIC_CINEMATIC_POST_DOMAIN_MAX_VIEWS ]; + int viewCount; + classicCinematicPostDomainStats_t stats; +} classicCinematicPostDomainState_t; + +static classicCinematicPostDomainState_t rg_classicCinematicPostDomain; + +static bool R_ClassicCinematicPostDomain_RangeFits( + int first, int count, int capacity ) { + return first >= 0 && count >= 0 && first <= capacity + && count <= capacity - first; +} + +static bool R_ClassicCinematicPostDomain_RangeContains( + int outerFirst, int outerCount, int first, int count ) { + if ( outerFirst < 0 || outerCount < 0 || first < outerFirst || count < 0 ) { + return false; + } + const int offset = first - outerFirst; + return offset <= outerCount && count <= outerCount - offset; +} + +static std::uint64_t R_ClassicCinematicPostDomain_HashBytes( + std::uint64_t hash, const void *data, size_t bytes ) { + const byte *source = static_cast( data ); + for ( size_t i = 0; i < bytes; ++i ) { + hash ^= source[ i ]; + hash *= UINT64_C( 1099511628211 ); + } + return hash; +} + +static std::uint64_t R_ClassicCinematicPostDomain_HashView( + const classicCinematicPostDomainView_t &view ) { + std::uint64_t hash = UINT64_C( 1469598103934665603 ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.scope, + sizeof( view.scope ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.scenePacketIndex, + sizeof( view.scenePacketIndex ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, + &view.specialRootScenePacketIndex, + sizeof( view.specialRootScenePacketIndex ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, + &view.specialNestingDepth, sizeof( view.specialNestingDepth ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, + &view.nestedInSpecialView, sizeof( view.nestedInSpecialView ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.passPacketIndex, + sizeof( view.passPacketIndex ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.firstSourceSurface, + sizeof( view.firstSourceSurface ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.sourceSurfaceCount, + sizeof( view.sourceSurfaceCount ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.packetDrawCount, + sizeof( view.packetDrawCount ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.cinematicStageCount, + sizeof( view.cinematicStageCount ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.currentRenderStageCount, + sizeof( view.currentRenderStageCount ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, &view.currentDepthStageCount, + sizeof( view.currentDepthStageCount ) ); + hash = R_ClassicCinematicPostDomain_HashBytes( hash, + &view.cinematicTimeMilliseconds, sizeof( view.cinematicTimeMilliseconds ) ); + return hash; +} + +static bool R_ClassicCinematicPostDomain_IsCurrentRenderImage( const idImage *image ) { + if ( image == NULL || globalImages == NULL ) { + return false; + } + if ( image == globalImages->currentRenderImage + || image == globalImages->originalCurrentRenderImage ) { + return true; + } + const char *name = image->GetName(); + return name != NULL && idStr::Icmpn( name, "_currentRender", 14 ) == 0; +} + +static bool R_ClassicCinematicPostDomain_IsCurrentDepthImage( const idImage *image ) { + if ( image == NULL || globalImages == NULL ) { + return false; + } + if ( image == globalImages->currentDepthImage ) { + return true; + } + const char *name = image->GetName(); + return name != NULL && idStr::Icmpn( name, "_currentDepth", 13 ) == 0; +} + +static bool R_ClassicCinematicPostDomain_IsRootCinematicView( + const viewDef_t *viewDef ) { + return viewDef != NULL && viewDef->viewEntitys == NULL + && !viewDef->isSubview && !viewDef->isMirror && !viewDef->isXraySubview + && !viewDef->isEditor && viewDef->superView == NULL + && viewDef->subviewSurface == NULL && viewDef->numClipPlanes == 0 + && viewDef->renderWorld == NULL && viewDef->viewLights == NULL + && viewDef->renderFlags == 0 && viewDef->renderView.globalMaterial == NULL + && viewDef->numOutlineDrawSurfs == 0; +} + +static bool R_ClassicCinematicPostDomain_IsAuthoredPostView( + const viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->viewEntitys == NULL || viewDef->isEditor + || viewDef->renderWorld == NULL || viewDef->renderView.viewID < 0 + || ( viewDef->renderFlags & RF_PORTAL_SKY ) != 0 + || viewDef->renderView.globalMaterial != NULL ) { + return false; + } + if ( viewDef->isSubview ) { + // A dynamic post/video tail inside a special view is admissible only + // when that view belongs to the independently sealed subview tree. The + // two domains will publish or roll back the shared root transaction + // together after every child range and capture edge completes. + return r_rendererSharedSubview.GetBool() + && viewDef->superView != NULL && viewDef->subviewSurface != NULL; + } + return !viewDef->isMirror && !viewDef->isXraySubview + && viewDef->superView == NULL && viewDef->subviewSurface == NULL + && viewDef->numClipPlanes == 0; +} + +static bool R_ClassicCinematicPostDomain_StageCounts( + const drawSurf_t *drawSurf, int &cinematicStages, int ¤tRenderStages, + int ¤tDepthStages ) { + if ( drawSurf == NULL || drawSurf->material == NULL ) { + return false; + } + const idMaterial *material = drawSurf->material; + const int stageCount = material->GetNumStages(); + for ( int stageIndex = 0; stageIndex < stageCount; ++stageIndex ) { + const shaderStage_t *stage = material->GetStage( stageIndex ); + if ( stage == NULL ) { + return false; + } + if ( stage->texture.cinematic != NULL ) { + cinematicStages++; + } + if ( material->TestMaterialFlag( MF_NEED_CURRENT_RENDER ) + || R_ClassicCinematicPostDomain_IsCurrentRenderImage( + stage->texture.image ) ) { + currentRenderStages++; + } + if ( R_ClassicCinematicPostDomain_IsCurrentDepthImage( + stage->texture.image ) ) { + currentDepthStages++; + } + } + return true; +} + +// Keep root 2D packet reconciliation in lockstep with the GUI packet builder. +// The dynamic stage walker remains free to retain legacy no-op behavior for +// non-drawable inputs, but every packet-admitted source must retain its exact +// original drawSurf identity and order before this domain can commit. +static bool R_ClassicCinematicPostDomain_IsRootGUIPacketEligible( + const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->geo != NULL + && drawSurf->geo->numIndexes > 0 && drawSurf->space != NULL + && drawSurf->material != NULL && drawSurf->material->HasAmbient() + && !drawSurf->material->IsPortalSky(); +} + +static int R_ClassicCinematicPostDomain_FindPass( + const idScenePacketFrame &packetFrame, const scenePacket_t &scene, + renderPassCategory_t category ) { + if ( !R_ClassicCinematicPostDomain_RangeFits( scene.firstPassPacket, + scene.passPacketCount, packetFrame.NumPasses() ) ) { + return -1; + } + for ( int passOffset = 0; passOffset < scene.passPacketCount; ++passOffset ) { + const int passIndex = scene.firstPassPacket + passOffset; + const passPacket_t &pass = packetFrame.Pass( passIndex ); + if ( pass.passCategory == category && !pass.commandOnly ) { + return passIndex; + } + } + return -1; +} + +static void R_ClassicCinematicPostDomain_Fail( + classicCinematicPostDomainView_t &view, + classicCinematicPostDomainFailure_t failure, int detail ) { + view.ready = false; + view.failure = failure; + view.failureDetail = detail; + if ( failure > CLASSIC_CINEMATIC_POST_FAILURE_NONE + && failure < CLASSIC_CINEMATIC_POST_FAILURE_COUNT ) { + rg_classicCinematicPostDomain.stats.failureCounts[ failure ]++; + } +} + +static void R_ClassicCinematicPostDomain_CommitView( + classicCinematicPostDomainView_t &view ) { + view.ready = true; + view.failure = CLASSIC_CINEMATIC_POST_FAILURE_NONE; + view.failureDetail = 0; + view.hash = R_ClassicCinematicPostDomain_HashView( view ); + rg_classicCinematicPostDomain.stats.readyViews++; + rg_classicCinematicPostDomain.stats.cinematicStages += view.cinematicStageCount; + rg_classicCinematicPostDomain.stats.currentRenderStages += view.currentRenderStageCount; + rg_classicCinematicPostDomain.stats.currentDepthStages += view.currentDepthStageCount; +} + +static bool R_ClassicCinematicPostDomain_AddRootCinematicView( + const idScenePacketFrame &packetFrame, int sceneIndex, + const scenePacket_t &scene ) { + const viewDef_t *viewDef = scene.viewDef; + if ( !R_ClassicCinematicPostDomain_IsRootCinematicView( viewDef ) ) { + return true; + } + if ( viewDef->numDrawSurfs <= 0 || viewDef->drawSurfs == NULL ) { + return true; + } + int cinematicStages = 0; + int currentRenderStages = 0; + int currentDepthStages = 0; + for ( int sourceIndex = 0; sourceIndex < viewDef->numDrawSurfs; ++sourceIndex ) { + const drawSurf_t *drawSurf = viewDef->drawSurfs[ sourceIndex ]; + if ( drawSurf == NULL || drawSurf->material == NULL + || !R_ClassicCinematicPostDomain_StageCounts( drawSurf, + cinematicStages, currentRenderStages, currentDepthStages ) ) { + return true; + } + if ( drawSurf->material->GetSort() >= SS_POST_PROCESS ) { + return true; + } + } + if ( cinematicStages <= 0 ) { + return true; + } + if ( rg_classicCinematicPostDomain.viewCount + >= CLASSIC_CINEMATIC_POST_DOMAIN_MAX_VIEWS ) { + rg_classicCinematicPostDomain.stats.overflow = true; + return false; + } + classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ + rg_classicCinematicPostDomain.viewCount++ ]; + memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.specialRootScenePacketIndex = -1; + view.scope = CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC; + view.scenePacketIndex = sceneIndex; + view.passPacketIndex = R_ClassicCinematicPostDomain_FindPass( packetFrame, + scene, RENDER_PASS_GUI ); + view.firstSourceSurface = 0; + view.sourceSurfaceCount = viewDef->numDrawSurfs; + view.cinematicStageCount = cinematicStages; + view.currentRenderStageCount = currentRenderStages; + view.currentDepthStageCount = currentDepthStages; + const double cinematicTime = 1000.0 * ( static_cast( viewDef->floatTime ) + + static_cast( viewDef->renderView.shaderParms[ 11 ] ) ); + if ( !std::isfinite( cinematicTime ) + || cinematicTime < static_cast( ( std::numeric_limits::min )() ) + || cinematicTime > static_cast( ( std::numeric_limits::max )() ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_CINEMATIC_CLOCK, 0 ); + return true; + } + view.cinematicTimeMilliseconds = static_cast( cinematicTime ); + if ( !R_ClassicCinematicPostDomain_RangeFits( scene.firstPassPacket, + scene.passPacketCount, packetFrame.NumPasses() ) + || !R_ClassicCinematicPostDomain_RangeFits( scene.firstDrawPacket, + scene.drawPacketCount, packetFrame.NumDrawPackets() ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SCENE_RANGE, sceneIndex ); + return true; + } + if ( view.passPacketIndex < 0 ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE, 0 ); + return true; + } + const passPacket_t &pass = packetFrame.Pass( view.passPacketIndex ); + if ( pass.packetCategory != SCENE_PACKET_CATEGORY_GUI + || !R_ClassicCinematicPostDomain_RangeFits( pass.firstDrawPacket, + pass.drawPacketCount, packetFrame.NumDrawPackets() ) + || !R_ClassicCinematicPostDomain_RangeContains( scene.firstDrawPacket, + scene.drawPacketCount, pass.firstDrawPacket, + pass.drawPacketCount ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE, view.passPacketIndex ); + return true; + } + view.packetDrawCount = pass.drawPacketCount; + int packetCursor = pass.firstDrawPacket; + const int packetEnd = packetCursor + pass.drawPacketCount; + for ( int sourceIndex = 0; sourceIndex < viewDef->numDrawSurfs; + sourceIndex++ ) { + const drawSurf_t *drawSurf = viewDef->drawSurfs[ sourceIndex ]; + if ( !R_ClassicCinematicPostDomain_IsRootGUIPacketEligible( drawSurf ) ) { + continue; + } + if ( packetCursor >= packetEnd ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + sourceIndex ); + return true; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetCursor++ ); + if ( packet.legacyDrawSurf != drawSurf + || packet.passCategory != RENDER_PASS_GUI + || packet.packetCategory != SCENE_PACKET_CATEGORY_GUI ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + sourceIndex ); + return true; + } + } + if ( packetCursor != packetEnd ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor ); + return true; + } + R_ClassicCinematicPostDomain_CommitView( view ); + rg_classicCinematicPostDomain.stats.rootCinematicViews++; + return true; +} + +static bool R_ClassicCinematicPostDomain_AddAuthoredPostView( + const idScenePacketFrame &packetFrame, int sceneIndex, + const scenePacket_t &scene ) { + const viewDef_t *viewDef = scene.viewDef; + if ( !R_ClassicCinematicPostDomain_IsAuthoredPostView( viewDef ) + || viewDef->numDrawSurfs <= 0 || viewDef->drawSurfs == NULL ) { + return true; + } + int firstSource = viewDef->numDrawSurfs; + for ( int sourceIndex = 0; sourceIndex < viewDef->numDrawSurfs; ++sourceIndex ) { + const drawSurf_t *drawSurf = viewDef->drawSurfs[ sourceIndex ]; + if ( drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->GetSort() >= SS_POST_PROCESS ) { + firstSource = sourceIndex; + break; + } + } + if ( firstSource >= viewDef->numDrawSurfs ) { + return true; + } + if ( rg_classicCinematicPostDomain.viewCount + >= CLASSIC_CINEMATIC_POST_DOMAIN_MAX_VIEWS ) { + rg_classicCinematicPostDomain.stats.overflow = true; + return false; + } + classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ + rg_classicCinematicPostDomain.viewCount++ ]; + memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.specialRootScenePacketIndex = -1; + view.scope = CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST; + view.scenePacketIndex = sceneIndex; + if ( viewDef->isSubview ) { + const classicSubviewDomainView_t *specialView = + R_ClassicSubviewDomain_FindView( viewDef ); + const classicSubviewDomainView_t *specialRoot = specialView != NULL + ? R_ClassicSubviewDomain_ViewByIndex( specialView->rootViewIndex ) + : NULL; + if ( specialView == NULL || specialRoot == NULL || !specialView->ready + || !specialRoot->ready || specialRoot->viewDef == NULL ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_MISSING_SPECIAL_VIEW, + sceneIndex ); + return true; + } + view.nestedInSpecialView = true; + view.specialRootViewDef = specialRoot->viewDef; + view.specialRootScenePacketIndex = specialRoot->scenePacketIndex; + view.specialNestingDepth = specialView->nestingDepth; + } + view.passPacketIndex = R_ClassicCinematicPostDomain_FindPass( packetFrame, + scene, RENDER_PASS_AUTHORED_POST ); + view.firstSourceSurface = firstSource; + view.sourceSurfaceCount = viewDef->numDrawSurfs - firstSource; + if ( !R_ClassicCinematicPostDomain_RangeFits( scene.firstPassPacket, + scene.passPacketCount, packetFrame.NumPasses() ) + || !R_ClassicCinematicPostDomain_RangeFits( scene.firstDrawPacket, + scene.drawPacketCount, packetFrame.NumDrawPackets() ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SCENE_RANGE, sceneIndex ); + return true; + } + if ( view.passPacketIndex < 0 ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE, 0 ); + return true; + } + const passPacket_t &pass = packetFrame.Pass( view.passPacketIndex ); + if ( pass.packetCategory != SCENE_PACKET_CATEGORY_POST_PROCESS + || !R_ClassicCinematicPostDomain_RangeFits( pass.firstDrawPacket, + pass.drawPacketCount, packetFrame.NumDrawPackets() ) + || !R_ClassicCinematicPostDomain_RangeContains( scene.firstDrawPacket, + scene.drawPacketCount, pass.firstDrawPacket, + pass.drawPacketCount ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE, view.passPacketIndex ); + return true; + } + view.packetDrawCount = pass.drawPacketCount; + int packetCursor = pass.firstDrawPacket; + const int packetEnd = packetCursor + pass.drawPacketCount; + for ( int sourceIndex = firstSource; sourceIndex < viewDef->numDrawSurfs; + ++sourceIndex ) { + const drawSurf_t *drawSurf = viewDef->drawSurfs[ sourceIndex ]; + if ( drawSurf == NULL || drawSurf->material == NULL + || drawSurf->material->GetSort() < SS_POST_PROCESS ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SOURCE_SURFACE, sourceIndex ); + return true; + } + if ( !R_ClassicCinematicPostDomain_StageCounts( drawSurf, + view.cinematicStageCount, view.currentRenderStageCount, + view.currentDepthStageCount ) ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SOURCE_SURFACE, sourceIndex ); + return true; + } + if ( drawSurf->material->HasAmbient() && !drawSurf->material->IsPortalSky() + && !drawSurf->material->SuppressInSubview() ) { + if ( packetCursor >= packetEnd ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + sourceIndex ); + return true; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetCursor++ ); + if ( packet.legacyDrawSurf != drawSurf + || packet.passCategory != RENDER_PASS_AUTHORED_POST + || packet.packetCategory != SCENE_PACKET_CATEGORY_POST_PROCESS ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + sourceIndex ); + return true; + } + } + } + if ( packetCursor != packetEnd ) { + R_ClassicCinematicPostDomain_Fail( view, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor ); + return true; + } + R_ClassicCinematicPostDomain_CommitView( view ); + rg_classicCinematicPostDomain.stats.authoredPostViews++; + return true; +} + +void R_ClassicCinematicPostDomain_ResetFrame( void ) { + memset( &rg_classicCinematicPostDomain, 0, + sizeof( rg_classicCinematicPostDomain ) ); + idStr::Copynz( rg_classicCinematicPostDomain.stats.status, "reset", + sizeof( rg_classicCinematicPostDomain.stats.status ) ); +} + +void R_ClassicCinematicPostDomain_PrepareFrame( + const idScenePacketFrame &packetFrame ) { + R_ClassicCinematicPostDomain_ResetFrame(); + classicCinematicPostDomainStats_t &stats = rg_classicCinematicPostDomain.stats; + stats.prepared = true; + stats.frameValid = packetFrame.Stats().frontEndDerived + || packetFrame.Stats().backendDerived; + stats.sourceScenes = packetFrame.NumScenes(); + if ( !stats.frameValid || packetFrame.Stats().overflow ) { + stats.overflow = packetFrame.Stats().overflow; + idStr::Copynz( stats.status, "packet frame unavailable", sizeof( stats.status ) ); + return; + } + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( scene.viewDef == NULL ) { + continue; + } + if ( !R_ClassicCinematicPostDomain_AddRootCinematicView( packetFrame, + sceneIndex, scene ) + || !R_ClassicCinematicPostDomain_AddAuthoredPostView( packetFrame, + sceneIndex, scene ) ) { + stats.overflow = true; + break; + } + } + stats.views = rg_classicCinematicPostDomain.viewCount; + for ( int viewIndex = 0; viewIndex < rg_classicCinematicPostDomain.viewCount; + ++viewIndex ) { + const classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( !view.ready ) { + stats.fallbackViews++; + } + if ( view.ready && view.nestedInSpecialView ) { + stats.nestedSpecialViews++; + stats.nestedCinematicStages += view.cinematicStageCount; + bool firstForRoot = true; + for ( int earlier = 0; earlier < viewIndex; ++earlier ) { + const classicCinematicPostDomainView_t &prior = + rg_classicCinematicPostDomain.views[ earlier ]; + if ( prior.ready && prior.nestedInSpecialView + && prior.specialRootViewDef == view.specialRootViewDef ) { + firstForRoot = false; + break; + } + } + if ( firstForRoot ) { + stats.nestedSpecialTransactions++; + } + } + stats.hash ^= view.hash; + } + idStr::Copynz( stats.status, stats.overflow ? "overflow" : "prepared", + sizeof( stats.status ) ); +} + +const classicCinematicPostDomainStats_t &R_ClassicCinematicPostDomain_Stats( void ) { + return rg_classicCinematicPostDomain.stats; +} + +static const classicCinematicPostDomainView_t *R_ClassicCinematicPostDomain_Find( + const viewDef_t *viewDef, classicCinematicPostDomainScope_t scope ) { + for ( int viewIndex = 0; viewIndex < rg_classicCinematicPostDomain.viewCount; + ++viewIndex ) { + const classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( view.viewDef == viewDef && view.scope == scope ) { + return &view; + } + } + return NULL; +} + +const classicCinematicPostDomainView_t *R_ClassicCinematicPostDomain_FindRootCinematicView( + const viewDef_t *viewDef ) { + return R_ClassicCinematicPostDomain_Find( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC ); +} + +const classicCinematicPostDomainView_t *R_ClassicCinematicPostDomain_FindAuthoredPostView( + const viewDef_t *viewDef ) { + return R_ClassicCinematicPostDomain_Find( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST ); +} + +bool R_ClassicCinematicPostDomain_ReadyForBackend( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend ) { + const classicCinematicPostDomainStats_t &stats = + rg_classicCinematicPostDomain.stats; + if ( !stats.prepared || !stats.frameValid || stats.overflow ) { + return false; + } + const classicCinematicPostDomainView_t *view = + R_ClassicCinematicPostDomain_Find( viewDef, scope ); + if ( view == NULL || !view->ready ) { + return false; + } + if ( backend < CLASSIC_CINEMATIC_POST_BACKEND_GL + || backend >= CLASSIC_CINEMATIC_POST_BACKEND_COUNT + || view->backendOutcome[ backend ] + == CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK ) { + return false; + } + if ( !view->nestedInSpecialView ) { + return true; + } + const classicSubviewDomainView_t *specialView = + R_ClassicSubviewDomain_FindView( viewDef ); + return specialView != NULL && specialView->ready + && specialView->viewDef == viewDef + && specialView->rootViewIndex >= 0 + && R_ClassicSubviewDomain_ViewByIndex( specialView->rootViewIndex ) != NULL + && R_ClassicSubviewDomain_ViewByIndex( + specialView->rootViewIndex )->viewDef == view->specialRootViewDef + && specialView->backendOutcome[ backend ] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + && R_ClassicSubviewDomain_ViewSemanticsMatch( *specialView ); +} + +static const classicSubviewDomainView_t * +R_ClassicCinematicPostDomain_SubviewRoot( const viewDef_t *memberViewDef ) { + const classicSubviewDomainView_t *member = + R_ClassicSubviewDomain_FindView( memberViewDef ); + return member != NULL && member->rootViewIndex >= 0 + ? R_ClassicSubviewDomain_ViewByIndex( member->rootViewIndex ) : NULL; +} + +static bool R_ClassicCinematicPostDomain_IsSubviewTransactionMember( + const classicCinematicPostDomainView_t &view, + const classicSubviewDomainView_t *root ) { + return root != NULL && view.nestedInSpecialView + && view.specialRootViewDef == root->viewDef; +} + +bool R_ClassicCinematicPostDomain_SubviewTransactionReady( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ) { + if ( backend < CLASSIC_CINEMATIC_POST_BACKEND_GL + || backend >= CLASSIC_CINEMATIC_POST_BACKEND_COUNT ) { + return false; + } + if ( r_rendererSharedCinematicPost.GetBool() ) { + const classicCinematicPostDomainStats_t &stats = + rg_classicCinematicPostDomain.stats; + if ( !stats.prepared || !stats.frameValid || stats.overflow ) { + return false; + } + } + const classicSubviewDomainView_t *root = + R_ClassicCinematicPostDomain_SubviewRoot( memberViewDef ); + if ( root == NULL ) { + return false; + } + for ( int viewIndex = 0; viewIndex < rg_classicCinematicPostDomain.viewCount; + ++viewIndex ) { + const classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( !R_ClassicCinematicPostDomain_IsSubviewTransactionMember( view, root ) ) { + continue; + } + if ( !view.ready || view.backendOutcome[ backend ] + == CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK ) { + return false; + } + } + return true; +} + +bool R_ClassicCinematicPostDomain_SubviewTransactionCompleted( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ) { + if ( !R_ClassicCinematicPostDomain_SubviewTransactionReady( memberViewDef, + backend ) ) { + return false; + } + const classicSubviewDomainView_t *root = + R_ClassicCinematicPostDomain_SubviewRoot( memberViewDef ); + for ( int viewIndex = 0; viewIndex < rg_classicCinematicPostDomain.viewCount; + ++viewIndex ) { + const classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( !R_ClassicCinematicPostDomain_IsSubviewTransactionMember( view, root ) ) { + continue; + } + // Publication is a second, non-failing pass. Require the complete + // transaction to remain unpublished and coverage-complete before any + // member can become visible as shared-owned. + if ( view.backendOutcome[ backend ] + != CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED + || !view.backendCompleted[ backend ] + || view.backendDrawnSurfaces[ backend ] != view.sourceSurfaceCount ) { + return false; + } + } + return true; +} + +static void R_ClassicCinematicPostDomain_RecordSingleFallback( + classicCinematicPostDomainView_t &view, + classicCinematicPostDomainBackend_t backend, + classicCinematicPostDomainFailure_t failure, int detail, + bool countDuplicate ) { + classicCinematicPostDomainBackendCoverage_t &coverage = + rg_classicCinematicPostDomain.stats.backend[ backend ]; + if ( view.backendOutcome[ backend ] != CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED ) { + if ( countDuplicate ) { + coverage.duplicateReports++; + } + return; + } + view.backendOutcome[ backend ] = CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK; + view.backendFailure[ backend ] = failure; + view.backendFailureDetail[ backend ] = detail; + coverage.fallbackViews++; + coverage.fallbackSurfaces += view.sourceSurfaceCount; +} + +static bool R_ClassicCinematicPostDomain_PublishSingleOwned( + classicCinematicPostDomainView_t &view, + classicCinematicPostDomainBackend_t backend ) { + classicCinematicPostDomainBackendCoverage_t &coverage = + rg_classicCinematicPostDomain.stats.backend[ backend ]; + if ( view.backendOutcome[ backend ] == CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK ) { + coverage.coverageMismatches++; + return false; + } + if ( view.backendOutcome[ backend ] == CLASSIC_CINEMATIC_POST_BACKEND_OWNED ) { + coverage.duplicateReports++; + return true; + } + if ( view.backendDrawnSurfaces[ backend ] != view.sourceSurfaceCount ) { + coverage.coverageMismatches++; + return false; + } + view.backendOutcome[ backend ] = CLASSIC_CINEMATIC_POST_BACKEND_OWNED; + coverage.ownedViews++; + coverage.ownedSurfaces += view.backendDrawnSurfaces[ backend ]; + return true; +} + +void R_ClassicCinematicPostDomain_RecordBackendFallback( + const viewDef_t *viewDef, classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend, + classicCinematicPostDomainFailure_t failure, int detail ) { + classicCinematicPostDomainView_t *view = const_cast( + R_ClassicCinematicPostDomain_Find( viewDef, scope ) ); + if ( view == NULL || backend < CLASSIC_CINEMATIC_POST_BACKEND_GL + || backend >= CLASSIC_CINEMATIC_POST_BACKEND_COUNT ) { + return; + } + R_ClassicCinematicPostDomain_RecordSingleFallback( *view, backend, + failure, detail, true ); + if ( view->nestedInSpecialView ) { + R_ClassicSubviewDomain_RecordBackendFallback( viewDef, + static_cast( backend ), + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CINEMATIC_POST_FALLBACK, + detail ); + } +} + +bool R_ClassicCinematicPostDomain_RecordOwned( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend, int drawnSurfaces ) { + classicCinematicPostDomainView_t *view = const_cast( + R_ClassicCinematicPostDomain_Find( viewDef, scope ) ); + if ( view == NULL || !view->ready || backend < CLASSIC_CINEMATIC_POST_BACKEND_GL + || backend >= CLASSIC_CINEMATIC_POST_BACKEND_COUNT + || drawnSurfaces != view->sourceSurfaceCount ) { + if ( view != NULL && backend >= CLASSIC_CINEMATIC_POST_BACKEND_GL + && backend < CLASSIC_CINEMATIC_POST_BACKEND_COUNT ) { + rg_classicCinematicPostDomain.stats.backend[ backend ].coverageMismatches++; + } + return false; + } + classicCinematicPostDomainBackendCoverage_t &coverage = + rg_classicCinematicPostDomain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] + == CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK ) { + coverage.coverageMismatches++; + return false; + } + if ( view->backendOutcome[ backend ] + == CLASSIC_CINEMATIC_POST_BACKEND_OWNED ) { + coverage.duplicateReports++; + return view->backendDrawnSurfaces[ backend ] == drawnSurfaces; + } + if ( view->backendCompleted[ backend ] ) { + coverage.duplicateReports++; + return view->backendDrawnSurfaces[ backend ] == drawnSurfaces; + } + view->backendDrawnSurfaces[ backend ] = drawnSurfaces; + view->backendCompleted[ backend ] = true; + // Dynamic work nested inside a special view has executed, but ownership is + // not visible until the outermost special-view transaction completes every + // descendant draw range and capture/direct edge. + return view->nestedInSpecialView + || R_ClassicCinematicPostDomain_PublishSingleOwned( *view, backend ); +} + +void R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend, + classicCinematicPostDomainFailure_t failure, int detail ) { + if ( backend < CLASSIC_CINEMATIC_POST_BACKEND_GL + || backend >= CLASSIC_CINEMATIC_POST_BACKEND_COUNT ) { + return; + } + const classicSubviewDomainView_t *root = + R_ClassicCinematicPostDomain_SubviewRoot( memberViewDef ); + for ( int viewIndex = 0; root != NULL + && viewIndex < rg_classicCinematicPostDomain.viewCount; ++viewIndex ) { + classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( R_ClassicCinematicPostDomain_IsSubviewTransactionMember( view, root ) ) { + R_ClassicCinematicPostDomain_RecordSingleFallback( view, backend, + failure, detail, false ); + } + } +} + +bool R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ) { + if ( !R_ClassicCinematicPostDomain_SubviewTransactionCompleted( + memberViewDef, backend ) ) { + return false; + } + const classicSubviewDomainView_t *root = + R_ClassicCinematicPostDomain_SubviewRoot( memberViewDef ); + for ( int viewIndex = 0; root != NULL + && viewIndex < rg_classicCinematicPostDomain.viewCount; ++viewIndex ) { + classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ viewIndex ]; + if ( R_ClassicCinematicPostDomain_IsSubviewTransactionMember( view, root ) + && !R_ClassicCinematicPostDomain_PublishSingleOwned( view, backend ) ) { + return false; + } + } + return true; +} + +const char *ClassicCinematicPostDomainFailure_Name( + classicCinematicPostDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_CINEMATIC_POST_FAILURE_NONE: return "none"; + case CLASSIC_CINEMATIC_POST_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_CINEMATIC_POST_FAILURE_SCENE_PACKET_OVERFLOW: return "scenePacketOverflow"; + case CLASSIC_CINEMATIC_POST_FAILURE_VIEW_POOL_OVERFLOW: return "viewPoolOverflow"; + case CLASSIC_CINEMATIC_POST_FAILURE_UNSUPPORTED_VIEW: return "unsupportedView"; + case CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SCENE_RANGE: return "invalidSceneRange"; + case CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE: return "invalidPassRange"; + case CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH: return "sourcePacketMismatch"; + case CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SOURCE_SURFACE: return "invalidSourceSurface"; + case CLASSIC_CINEMATIC_POST_FAILURE_CINEMATIC_CLOCK: return "cinematicClock"; + case CLASSIC_CINEMATIC_POST_FAILURE_MISSING_SPECIAL_VIEW: return "missingSpecialView"; + case CLASSIC_CINEMATIC_POST_FAILURE_SPECIAL_VIEW_TRANSACTION_REJECTED: return "specialViewTransactionRejected"; + case CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_SPECIAL_VIEW_INCOMPLETE: return "backendSpecialViewIncomplete"; + case CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_NOT_READY: return "backendNotReady"; + case CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED: return "backendRejected"; + case CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_COVERAGE_MISMATCH: return "backendCoverageMismatch"; + default: return "unknown"; + } +} + +bool RendererClassicCinematicPostDomain_RunSelfTest( void ) { + R_ClassicCinematicPostDomain_ResetFrame(); + const classicCinematicPostDomainStats_t &resetStats = + R_ClassicCinematicPostDomain_Stats(); + if ( CLASSIC_CINEMATIC_POST_DOMAIN_MAX_VIEWS != SCENE_PACKET_MAX_SCENES + || !R_ClassicCinematicPostDomain_RangeFits( 7, 1, 8 ) + || R_ClassicCinematicPostDomain_RangeFits( 8, 1, 8 ) + || R_ClassicCinematicPostDomain_RangeFits( + ( std::numeric_limits::max )(), 1, + ( std::numeric_limits::max )() ) + || !R_ClassicCinematicPostDomain_RangeContains( 2, 3, 4, 1 ) + || R_ClassicCinematicPostDomain_RangeContains( 2, 3, 5, 1 ) + || idStr::Cmp( ClassicCinematicPostDomainFailure_Name( + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH ), + "sourcePacketMismatch" ) + || idStr::Cmp( ClassicCinematicPostDomainFailure_Name( + CLASSIC_CINEMATIC_POST_FAILURE_CINEMATIC_CLOCK ), + "cinematicClock" ) + || idStr::Cmp( ClassicCinematicPostDomainFailure_Name( + CLASSIC_CINEMATIC_POST_FAILURE_SPECIAL_VIEW_TRANSACTION_REJECTED ), + "specialViewTransactionRejected" ) + || resetStats.prepared || resetStats.frameValid || resetStats.overflow + || resetStats.views != 0 || resetStats.readyViews != 0 + || R_ClassicCinematicPostDomain_ReadyForBackend( NULL, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_GL ) + || R_ClassicCinematicPostDomain_RecordOwned( NULL, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, 0 ) ) { + return false; + } + + viewDef_t viewDef; + memset( &viewDef, 0, sizeof( viewDef ) ); + classicCinematicPostDomainView_t &view = + rg_classicCinematicPostDomain.views[ 0 ]; + memset( &view, 0, sizeof( view ) ); + view.viewDef = &viewDef; + view.scope = CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC; + view.scenePacketIndex = 3; + view.passPacketIndex = 7; + view.firstSourceSurface = 0; + view.sourceSurfaceCount = 3; + view.packetDrawCount = 2; + view.cinematicStageCount = 1; + view.cinematicTimeMilliseconds = 1000; + view.ready = true; + const std::uint64_t firstHash = R_ClassicCinematicPostDomain_HashView( view ); + view.cinematicTimeMilliseconds++; + const bool clockHashSensitive = R_ClassicCinematicPostDomain_HashView( view ) + != firstHash; + view.cinematicTimeMilliseconds--; + view.nestedInSpecialView = true; + view.specialRootScenePacketIndex = 11; + view.specialNestingDepth = 2; + const bool nestingHashSensitive = + R_ClassicCinematicPostDomain_HashView( view ) != firstHash; + view.nestedInSpecialView = false; + view.specialRootScenePacketIndex = 0; + view.specialNestingDepth = 0; + view.hash = firstHash; + rg_classicCinematicPostDomain.viewCount = 1; + memset( &rg_classicCinematicPostDomain.stats, 0, + sizeof( rg_classicCinematicPostDomain.stats ) ); + + const bool owned = R_ClassicCinematicPostDomain_RecordOwned( &viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_GL, 3 ); + const bool duplicate = R_ClassicCinematicPostDomain_RecordOwned( &viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_GL, 3 ); + const bool mismatch = !R_ClassicCinematicPostDomain_RecordOwned( &viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, 2 ); + R_ClassicCinematicPostDomain_RecordBackendFallback( &viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_COVERAGE_MISMATCH, 2 ); + const classicCinematicPostDomainStats_t &stats = + R_ClassicCinematicPostDomain_Stats(); + const bool coverageContract = owned && duplicate && mismatch + && view.backendOutcome[ CLASSIC_CINEMATIC_POST_BACKEND_GL ] + == CLASSIC_CINEMATIC_POST_BACKEND_OWNED + && view.backendOutcome[ CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ] + == CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK + && !R_ClassicCinematicPostDomain_ReadyForBackend( &viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ) + && stats.backend[ CLASSIC_CINEMATIC_POST_BACKEND_GL ].ownedViews == 1 + && stats.backend[ CLASSIC_CINEMATIC_POST_BACKEND_GL ].duplicateReports == 1 + && stats.backend[ CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ] + .coverageMismatches == 1 + && stats.backend[ CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ].fallbackViews == 1; + R_ClassicCinematicPostDomain_ResetFrame(); + return clockHashSensitive && nestingHashSensitive && coverageContract; +} diff --git a/src/renderer/ClassicCinematicPostDomain.h b/src/renderer/ClassicCinematicPostDomain.h new file mode 100644 index 00000000..d8015b14 --- /dev/null +++ b/src/renderer/ClassicCinematicPostDomain.h @@ -0,0 +1,166 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_CINEMATIC_POST_DOMAIN_H__ +#define __CLASSIC_CINEMATIC_POST_DOMAIN_H__ + +#include "ScenePackets.h" + +/* +============================================================================== + + Shared transaction boundary for the two dynamic classic-frame islands that + cannot use the static fixed-material pass pools: + + * root 2D command views containing one or more videoMap/soundMap stages; + * the ordered SS_POST_PROCESS range in an ordinary root 3D view. + + Cinematics retain the established decoder, scratch-image upload, and exact + audio/video clock. Authored post stages retain their established feedback + capture and program-stage dispatch. The domain instead seals the complete + source range, packet correspondence, view identity, timing, and ordering + before either backend enters that mature dynamic-stage adapter. A rejected + transaction is wholly classic; a committed transaction never mixes a partial + shared range with the ordinary walker. + +============================================================================== +*/ + +const int CLASSIC_CINEMATIC_POST_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; + +enum classicCinematicPostDomainScope_t { + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC = 0, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_SCOPE_COUNT +}; + +enum classicCinematicPostDomainFailure_t { + CLASSIC_CINEMATIC_POST_FAILURE_NONE = 0, + CLASSIC_CINEMATIC_POST_FAILURE_UNAVAILABLE, + CLASSIC_CINEMATIC_POST_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_CINEMATIC_POST_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_CINEMATIC_POST_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_PASS_RANGE, + CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_CINEMATIC_POST_FAILURE_INVALID_SOURCE_SURFACE, + CLASSIC_CINEMATIC_POST_FAILURE_CINEMATIC_CLOCK, + CLASSIC_CINEMATIC_POST_FAILURE_MISSING_SPECIAL_VIEW, + CLASSIC_CINEMATIC_POST_FAILURE_SPECIAL_VIEW_TRANSACTION_REJECTED, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_SPECIAL_VIEW_INCOMPLETE, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_NOT_READY, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_CINEMATIC_POST_FAILURE_COUNT +}; + +enum classicCinematicPostDomainBackend_t { + CLASSIC_CINEMATIC_POST_BACKEND_GL = 0, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_BACKEND_COUNT +}; + +enum classicCinematicPostDomainBackendOutcome_t { + CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED = 0, + CLASSIC_CINEMATIC_POST_BACKEND_OWNED, + CLASSIC_CINEMATIC_POST_BACKEND_FALLBACK +}; + +typedef struct classicCinematicPostDomainView_s { + const viewDef_t *viewDef; + const viewDef_t *specialRootViewDef; + int specialRootScenePacketIndex; + int specialNestingDepth; + classicCinematicPostDomainScope_t scope; + int scenePacketIndex; + int passPacketIndex; + int firstSourceSurface; + int sourceSurfaceCount; + int packetDrawCount; + int cinematicStageCount; + int currentRenderStageCount; + int currentDepthStageCount; + int cinematicTimeMilliseconds; + bool ready; + classicCinematicPostDomainFailure_t failure; + bool nestedInSpecialView; + int failureDetail; + std::uint64_t hash; + classicCinematicPostDomainBackendOutcome_t backendOutcome[ + CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; + classicCinematicPostDomainFailure_t backendFailure[ + CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; + int backendFailureDetail[ + CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; + int backendDrawnSurfaces[ + CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; + bool backendCompleted[ CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; +} classicCinematicPostDomainView_t; + +typedef struct classicCinematicPostDomainBackendCoverage_s { + int ownedViews; + int fallbackViews; + int ownedSurfaces; + int fallbackSurfaces; + int coverageMismatches; + int duplicateReports; +} classicCinematicPostDomainBackendCoverage_t; + +typedef struct classicCinematicPostDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int views; + int rootCinematicViews; + int authoredPostViews; + int nestedSpecialViews; + int nestedSpecialTransactions; + int nestedCinematicStages; + int readyViews; + int fallbackViews; + int cinematicStages; + int currentRenderStages; + int currentDepthStages; + int failureCounts[ CLASSIC_CINEMATIC_POST_FAILURE_COUNT ]; + std::uint64_t hash; + classicCinematicPostDomainBackendCoverage_t backend[ + CLASSIC_CINEMATIC_POST_BACKEND_COUNT ]; + char status[ 96 ]; +} classicCinematicPostDomainStats_t; + +void R_ClassicCinematicPostDomain_ResetFrame( void ); +void R_ClassicCinematicPostDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicCinematicPostDomainStats_t &R_ClassicCinematicPostDomain_Stats( void ); +const classicCinematicPostDomainView_t *R_ClassicCinematicPostDomain_FindRootCinematicView( + const viewDef_t *viewDef ); +const classicCinematicPostDomainView_t *R_ClassicCinematicPostDomain_FindAuthoredPostView( + const viewDef_t *viewDef ); +bool R_ClassicCinematicPostDomain_RecordOwned( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend, int drawnSurfaces ); +void R_ClassicCinematicPostDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend, + classicCinematicPostDomainFailure_t failure, int detail ); +bool R_ClassicCinematicPostDomain_ReadyForBackend( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainBackend_t backend ); +bool R_ClassicCinematicPostDomain_SubviewTransactionReady( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ); +bool R_ClassicCinematicPostDomain_SubviewTransactionCompleted( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ); +bool R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend ); +void R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback( + const viewDef_t *memberViewDef, + classicCinematicPostDomainBackend_t backend, + classicCinematicPostDomainFailure_t failure, int detail ); +const char *ClassicCinematicPostDomainFailure_Name( + classicCinematicPostDomainFailure_t failure ); +bool RendererClassicCinematicPostDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_CINEMATIC_POST_DOMAIN_H__ */ diff --git a/src/renderer/ClassicDeformDomain.cpp b/src/renderer/ClassicDeformDomain.cpp new file mode 100644 index 00000000..3d8b63fe --- /dev/null +++ b/src/renderer/ClassicDeformDomain.cpp @@ -0,0 +1,1031 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicDeformDomain.h" + +#include +#include + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; +const std::uint32_t CONTRACT_HASH_VERSION = 1u; +static classicDeformDomainStats_t packetStats; + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + for ( int i = 0; i < 4; ++i ) { + HashByte( hash, value >> ( i * 8 ) ); + } +} + +static void HashU64( std::uint64_t &hash, std::uint64_t value ) { + for ( int i = 0; i < 8; ++i ) { + HashByte( hash, static_cast( value >> ( i * 8 ) ) ); + } +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), "float hash width mismatch" ); + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static std::uint64_t HashName( const char *name ) { + std::uint64_t hash = HASH_OFFSET; + if ( name == NULL ) { + HashByte( hash, 0u ); + return hash; + } + for ( const unsigned char *cursor = + reinterpret_cast( name ); *cursor != 0; + ++cursor ) { + HashByte( hash, *cursor ); + } + return hash; +} + +static bool FloatIsFinite( float value ) { + return std::isfinite( static_cast( value ) ); +} + +static classicDeformKind_t KindForMaterial( const idMaterial *material ) { + if ( material == NULL ) { + return CLASSIC_DEFORM_KIND_NONE; + } + switch ( material->Deform() ) { + case DFRM_NONE: return CLASSIC_DEFORM_KIND_NONE; + case DFRM_SPRITE: return CLASSIC_DEFORM_KIND_SPRITE; + case DFRM_RECTSPRITE: return CLASSIC_DEFORM_KIND_RECTSPRITE; + case DFRM_TUBE: return CLASSIC_DEFORM_KIND_TUBE; + case DFRM_FLARE: return CLASSIC_DEFORM_KIND_FLARE; + case DFRM_EXPAND: return CLASSIC_DEFORM_KIND_EXPAND; + case DFRM_MOVE: return CLASSIC_DEFORM_KIND_MOVE; + case DFRM_TURB: return CLASSIC_DEFORM_KIND_TURBULENT; + case DFRM_EYEBALL: return CLASSIC_DEFORM_KIND_EYEBALL; + case DFRM_PARTICLE: return CLASSIC_DEFORM_KIND_PARTICLE; + case DFRM_PARTICLE2: return CLASSIC_DEFORM_KIND_PARTICLE2; + default: return CLASSIC_DEFORM_KIND_UNKNOWN; + } +} + +static bool KindIsSupportedCPUDeform( classicDeformKind_t kind ) { + return kind >= CLASSIC_DEFORM_KIND_SPRITE + && kind <= CLASSIC_DEFORM_KIND_EYEBALL; +} + +static bool KindIsUnsupported( classicDeformKind_t kind ) { + return kind == CLASSIC_DEFORM_KIND_PARTICLE + || kind == CLASSIC_DEFORM_KIND_PARTICLE2 + || kind == CLASSIC_DEFORM_KIND_UNKNOWN; +} + +static int ParameterCountForKind( classicDeformKind_t kind ) { + switch ( kind ) { + case CLASSIC_DEFORM_KIND_FLARE: + case CLASSIC_DEFORM_KIND_EXPAND: + case CLASSIC_DEFORM_KIND_MOVE: + return 1; + case CLASSIC_DEFORM_KIND_TURBULENT: + return 3; + default: + return 0; + } +} + +static classicDeformCacheLifetime_t GeometryLifetime( + const srfTriangles_t *geometry ) { + if ( geometry == NULL ) { + return CLASSIC_DEFORM_CACHE_UNKNOWN; + } + if ( ( geometry->ambientCache != NULL + && geometry->ambientCache->tag == TAG_TEMP ) + || ( geometry->indexCache != NULL + && geometry->indexCache->tag == TAG_TEMP ) ) { + return CLASSIC_DEFORM_CACHE_FRAME_TEMP; + } + // deformedSurface is topology/lifetime ownership for generated models. It + // is deliberately not used as evidence that a material deform executed. + if ( geometry->tempAmbientCache || geometry->deformedSurface ) { + return CLASSIC_DEFORM_CACHE_DYNAMIC_BRIDGE; + } + if ( geometry->ambientCache != NULL || geometry->indexCache != NULL ) { + return CLASSIC_DEFORM_CACHE_STATIC; + } + if ( geometry->verts != NULL || geometry->indexes != NULL ) { + return CLASSIC_DEFORM_CACHE_CLIENT_MEMORY; + } + return CLASSIC_DEFORM_CACHE_UNKNOWN; +} + +static bool CacheHasBacking( const vertCache_t *cache ) { + return cache != NULL && ( cache->vbo != 0 || cache->virtMem != NULL ); +} + +static void CaptureGeometryState( const srfTriangles_t *geometry, + classicDeformGeometryState_t &state ) { + std::memset( &state, 0, sizeof( state ) ); + state.geometry = geometry; + state.ambientOffset = -1; + state.indexOffset = -1; + state.ambientTag = -1; + state.indexTag = -1; + state.captured = true; + state.lifetime = GeometryLifetime( geometry ); + if ( geometry == NULL ) { + state.cacheStateValid = true; + return; + } + + state.vertexCount = geometry->numVerts; + state.indexCount = geometry->numIndexes; + state.ambientCache = geometry->ambientCache; + state.indexCache = geometry->indexCache; + state.hasClientVertexData = geometry->verts != NULL; + state.hasClientIndexData = geometry->indexes != NULL; +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + state.hasPrimBatchMesh = geometry->primBatchMesh != NULL; +#endif + if ( state.ambientCache != NULL ) { + state.hasAmbientCache = true; + state.ambientBuffer = state.ambientCache->vbo; + state.ambientOffset = state.ambientCache->offset; + state.ambientBytes = state.ambientCache->size; + state.ambientTag = state.ambientCache->tag; + state.ambientCacheHasBacking = CacheHasBacking( state.ambientCache ); + } + if ( state.indexCache != NULL ) { + state.hasIndexCache = true; + state.indexBuffer = state.indexCache->vbo; + state.indexOffset = state.indexCache->offset; + state.indexBytes = state.indexCache->size; + state.indexTag = state.indexCache->tag; + state.indexCacheHasBacking = CacheHasBacking( state.indexCache ); + } + state.cacheStateValid = state.vertexCount >= 0 && state.indexCount >= 0 + && ( !state.hasAmbientCache + || ( !state.ambientCache->indexBuffer + && state.ambientBytes > 0 && state.ambientOffset >= 0 + && state.ambientCacheHasBacking ) ) + && ( !state.hasIndexCache + || ( state.indexCache->indexBuffer + && state.indexBytes > 0 && state.indexOffset >= 0 + && state.indexCacheHasBacking ) ); +} + +static bool GeometryStateMatches( const classicDeformGeometryState_t &a, + const classicDeformGeometryState_t &b ) { + return a.geometry == b.geometry + && a.ambientCache == b.ambientCache + && a.indexCache == b.indexCache + && a.vertexCount == b.vertexCount && a.indexCount == b.indexCount + && a.ambientBuffer == b.ambientBuffer + && a.indexBuffer == b.indexBuffer + && a.ambientOffset == b.ambientOffset + && a.indexOffset == b.indexOffset + && a.ambientBytes == b.ambientBytes + && a.indexBytes == b.indexBytes + && a.ambientTag == b.ambientTag && a.indexTag == b.indexTag + && a.lifetime == b.lifetime && a.captured == b.captured + && a.hasAmbientCache == b.hasAmbientCache + && a.hasIndexCache == b.hasIndexCache + && a.ambientCacheHasBacking == b.ambientCacheHasBacking + && a.indexCacheHasBacking == b.indexCacheHasBacking + && a.hasClientVertexData == b.hasClientVertexData + && a.hasClientIndexData == b.hasClientIndexData + && a.hasPrimBatchMesh == b.hasPrimBatchMesh + && a.cacheStateValid == b.cacheStateValid; +} + +static void HashGeometryState( std::uint64_t &hash, + const classicDeformGeometryState_t &state ) { + HashInt( hash, state.vertexCount ); + HashInt( hash, state.indexCount ); + HashInt( hash, state.ambientBytes ); + HashInt( hash, state.indexBytes ); + HashInt( hash, state.ambientTag ); + HashInt( hash, state.indexTag ); + HashInt( hash, state.lifetime ); + HashBool( hash, state.captured ); + HashBool( hash, state.hasAmbientCache ); + HashBool( hash, state.hasIndexCache ); + HashBool( hash, state.ambientCacheHasBacking ); + HashBool( hash, state.indexCacheHasBacking ); + HashBool( hash, state.hasClientVertexData ); + HashBool( hash, state.hasClientIndexData ); + HashBool( hash, state.hasPrimBatchMesh ); + HashBool( hash, state.cacheStateValid ); +} + +static bool MaterialIdentityMatches( const classicDeformRecord_t &record ) { + return record.sourceMaterial == record.resultMaterial + && record.sourceMaterialIndex == record.resultMaterialIndex + && record.sourceMaterialNameHash == record.resultMaterialNameHash; +} + +static bool CompletedOutputProvable( const classicDeformRecord_t &record ) { + const classicDeformGeometryState_t &result = record.resultGeometry; + const std::uint64_t vertexBytes = result.vertexCount > 0 + ? static_cast( result.vertexCount ) * sizeof( idDrawVert ) : 0; + const std::uint64_t indexBytes = result.indexCount > 0 + ? static_cast( result.indexCount ) * sizeof( glIndex_t ) : 0; + return record.role == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + && record.cpuFinalized && KindIsSupportedCPUDeform( record.kind ) + && record.parametersValid + && record.sourceMaterial != NULL && record.resultMaterial != NULL + && MaterialIdentityMatches( record ) + && record.sourceGeometry.captured && result.captured + && record.sourceGeometry.geometry != NULL && result.geometry != NULL + && result.geometry != record.sourceGeometry.geometry + && result.vertexCount > 0 && result.indexCount > 0 + && result.cacheStateValid && result.hasAmbientCache + && result.ambientCache != NULL && result.ambientCacheHasBacking + && result.ambientTag == TAG_TEMP + && static_cast( result.ambientBytes ) >= vertexBytes + && ( ( result.hasIndexCache && result.indexCache != NULL + && result.indexCacheHasBacking && result.indexTag == TAG_TEMP + && static_cast( result.indexBytes ) >= indexBytes ) + || result.hasClientIndexData ) + && result.lifetime == CLASSIC_DEFORM_CACHE_FRAME_TEMP; +} + +static bool EmptyOutputProvable( const classicDeformRecord_t &record ) { + const classicDeformGeometryState_t &result = record.resultGeometry; + return record.role == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + && record.cpuFinalized && KindIsSupportedCPUDeform( record.kind ) + && record.parametersValid + && record.sourceMaterial != NULL && record.resultMaterial != NULL + && MaterialIdentityMatches( record ) + && record.sourceGeometry.captured && result.captured + && record.sourceGeometry.geometry != NULL && result.geometry != NULL + && result.geometry != record.sourceGeometry.geometry + && result.vertexCount >= 0 && result.indexCount == 0 + && result.cacheStateValid + && !result.hasAmbientCache && result.ambientCache == NULL + && !result.hasIndexCache && result.indexCache == NULL + && result.hasClientIndexData + && result.lifetime == CLASSIC_DEFORM_CACHE_CLIENT_MEMORY; +} + +static std::uint64_t CaptureInputSemanticHash( const drawSurf_t *drawSurf ) { + std::uint64_t hash = HASH_OFFSET; + if ( drawSurf != NULL && drawSurf->space != NULL ) { + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, drawSurf->space->modelMatrix[ i ] ); + } + } else { + HashByte( hash, 0u ); + } + if ( tr.viewDef != NULL ) { + for ( int i = 0; i < 3; ++i ) { + HashFloat( hash, tr.viewDef->renderView.vieworg[ i ] ); + } + for ( int axis = 0; axis < 3; ++axis ) { + for ( int component = 0; component < 3; ++component ) { + HashFloat( hash, + tr.viewDef->renderView.viewaxis[ axis ][ component ] ); + } + } + HashBool( hash, tr.viewDef->isMirror ); + } else { + HashByte( hash, 0u ); + } + return hash; +} + +static void CaptureMaterialIdentity( const idMaterial *material, int &index, + std::uint64_t &nameHash ) { + index = material != NULL ? material->Index() : -1; + nameHash = HashName( material != NULL ? material->GetName() : NULL ); +} + +static void ClearParameters( classicDeformRecord_t &record ) { + record.parameterCount = 0; + record.parametersValid = true; + for ( int i = 0; i < CLASSIC_DEFORM_MAX_PARAMETERS; ++i ) { + record.parameterRegisters[ i ] = -1; + record.parameterValues[ i ] = 0.0f; + } +} + +static void CaptureParameters( const drawSurf_t *drawSurf, + classicDeformRecord_t &record ) { + ClearParameters( record ); + record.parameterCount = ParameterCountForKind( record.kind ); + if ( record.parameterCount == 0 ) { + return; + } + if ( drawSurf == NULL || drawSurf->material == NULL + || drawSurf->shaderRegisters == NULL ) { + record.parametersValid = false; + return; + } + const int registerCount = drawSurf->material->GetNumRegisters(); + for ( int i = 0; i < record.parameterCount; ++i ) { + const int registerIndex = drawSurf->material->GetDeformRegister( i ); + record.parameterRegisters[ i ] = registerIndex; + if ( registerIndex < 0 || registerIndex >= registerCount ) { + record.parametersValid = false; + continue; + } + record.parameterValues[ i ] = drawSurf->shaderRegisters[ registerIndex ]; + if ( !FloatIsFinite( record.parameterValues[ i ] ) ) { + record.parametersValid = false; + } + } +} + +static void InitializeRecordFromDrawSurf( const drawSurf_t *drawSurf, + classicDeformRole_t role, std::uint64_t frameToken, bool captureInputs, + classicDeformRecord_t &record ) { + std::memset( &record, 0, sizeof( record ) ); + record.role = role; + record.frameToken = frameToken; + record.sourceMaterialIndex = -1; + record.resultMaterialIndex = -1; + record.initialized = true; + record.cpuFinalized = false; + record.sourceMaterial = drawSurf != NULL ? drawSurf->material : NULL; + record.resultMaterial = record.sourceMaterial; + CaptureMaterialIdentity( record.sourceMaterial, record.sourceMaterialIndex, + record.sourceMaterialNameHash ); + record.resultMaterialIndex = record.sourceMaterialIndex; + record.resultMaterialNameHash = record.sourceMaterialNameHash; + record.kind = KindForMaterial( record.sourceMaterial ); + CaptureGeometryState( drawSurf != NULL ? drawSurf->geo : NULL, + record.sourceGeometry ); + record.resultGeometry = record.sourceGeometry; + ClearParameters( record ); + record.flareScale = record.kind == CLASSIC_DEFORM_KIND_FLARE + ? r_flareSize.GetFloat() : 0.0f; + if ( captureInputs && record.kind != CLASSIC_DEFORM_KIND_NONE ) { + CaptureParameters( drawSurf, record ); + const idDecl *deformDecl = record.sourceMaterial != NULL + ? record.sourceMaterial->GetDeformDecl() : NULL; + record.deformDeclNameHash = HashName( + deformDecl != NULL ? deformDecl->GetName() : NULL ); + record.inputSemanticHash = CaptureInputSemanticHash( drawSurf ); + } + + if ( role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE; + } else if ( record.kind == CLASSIC_DEFORM_KIND_NONE ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_NONE; + } else if ( r_skipDeforms.GetBool() ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_SKIPPED; + } else if ( KindIsUnsupported( record.kind ) ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_UNSUPPORTED; + } else { + // A requested deform remains failed until EndDrawSurf proves that the + // authoritative CPU path published a distinct usable result. + record.outcome = CLASSIC_DEFORM_OUTCOME_FAILED; + } +} + +static bool ValidateRecordInternal( const classicDeformRecord_t &record, + bool validateHash ) { + if ( !record.initialized || record.frameToken == 0 + || record.role <= CLASSIC_DEFORM_ROLE_UNKNOWN + || record.role >= CLASSIC_DEFORM_ROLE_COUNT + || record.kind < CLASSIC_DEFORM_KIND_NONE + || record.kind >= CLASSIC_DEFORM_KIND_COUNT + || record.outcome < CLASSIC_DEFORM_OUTCOME_NONE + || record.outcome >= CLASSIC_DEFORM_OUTCOME_COUNT + || record.parameterCount < 0 + || record.parameterCount > CLASSIC_DEFORM_MAX_PARAMETERS + || !FloatIsFinite( record.flareScale ) + || !record.sourceGeometry.captured + || !record.resultGeometry.captured + || !record.sourceGeometry.cacheStateValid + || !record.resultGeometry.cacheStateValid ) { + return false; + } + if ( record.kind != CLASSIC_DEFORM_KIND_FLARE + && record.flareScale != 0.0f ) { + return false; + } + if ( validateHash && ( record.semanticHash == 0 + || record.semanticHash + != R_ClassicDeformDomain_ComputeSemanticHash( record ) ) ) { + return false; + } + if ( record.parametersValid ) { + for ( int i = 0; i < record.parameterCount; ++i ) { + if ( record.parameterRegisters[ i ] < 0 + || !FloatIsFinite( record.parameterValues[ i ] ) ) { + return false; + } + } + } + + if ( record.outcome == CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE ) { + return record.role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + && !record.cpuFinalized + && record.sourceMaterial == record.resultMaterial + && GeometryStateMatches( record.sourceGeometry, + record.resultGeometry ); + } + if ( record.role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW ) { + return false; + } + + switch ( record.outcome ) { + case CLASSIC_DEFORM_OUTCOME_NONE: + return record.kind == CLASSIC_DEFORM_KIND_NONE + && record.sourceMaterial == record.resultMaterial + && GeometryStateMatches( record.sourceGeometry, + record.resultGeometry ); + case CLASSIC_DEFORM_OUTCOME_SKIPPED: + return record.kind != CLASSIC_DEFORM_KIND_NONE + && record.sourceMaterial == record.resultMaterial + && GeometryStateMatches( record.sourceGeometry, + record.resultGeometry ); + case CLASSIC_DEFORM_OUTCOME_COMPLETED: + return CompletedOutputProvable( record ); + case CLASSIC_DEFORM_OUTCOME_EMPTY: + return EmptyOutputProvable( record ); + case CLASSIC_DEFORM_OUTCOME_FAILED: + return KindIsSupportedCPUDeform( record.kind ) + && !CompletedOutputProvable( record ) + && !EmptyOutputProvable( record ); + case CLASSIC_DEFORM_OUTCOME_UNSUPPORTED: + return KindIsUnsupported( record.kind ) + && record.sourceMaterial == record.resultMaterial + && GeometryStateMatches( record.sourceGeometry, + record.resultGeometry ); + case CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE: + case CLASSIC_DEFORM_OUTCOME_COUNT: + default: + return false; + } +} + +static void SealRecord( classicDeformRecord_t &record ) { + record.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( record ); + record.valid = ValidateRecordInternal( record, true ); +} + +} // namespace + +std::uint64_t R_ClassicDeformDomain_CurrentFrameToken( void ) { + // Draw surfaces and their frame-temp caches can span all subviews in one + // renderer frame. A frame-only token remains stable across those view walks. + return static_cast( + static_cast( tr.frameCount ) ) + 1ull; +} + +void R_ClassicDeformDomain_BeginDrawSurf( drawSurf_t *drawSurf ) { + if ( drawSurf == NULL ) { + return; + } + // R_AddDrawSurf does not clear the whole structure, so always establish a + // safe sentinel. Full source capture is opt-in: without shared deformation + // ownership, scene packets keep their named legacy fallback and do not need + // model/view hashing on every ordinary finalized draw. + std::memset( &drawSurf->classicDeform, 0, + sizeof( drawSurf->classicDeform ) ); + const classicDeformKind_t kind = KindForMaterial( drawSurf->material ); + if ( kind == CLASSIC_DEFORM_KIND_NONE + || !r_rendererSharedDeform.GetBool() ) { + return; + } + InitializeRecordFromDrawSurf( drawSurf, + CLASSIC_DEFORM_ROLE_FINALIZED_DRAW, + R_ClassicDeformDomain_CurrentFrameToken(), + true, drawSurf->classicDeform ); + // EndDrawSurf owns the only normal-path seal. This avoids hashing the same + // record twice and keeps DFRM_NONE's ubiquitous path small. + drawSurf->classicDeform.valid = false; +} + +void R_ClassicDeformDomain_EndDrawSurf( drawSurf_t *drawSurf ) { + if ( drawSurf == NULL ) { + return; + } + const std::uint64_t frameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + if ( !drawSurf->classicDeform.initialized + || drawSurf->classicDeform.role + != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + || drawSurf->classicDeform.frameToken != frameToken ) { + // Never reconstruct a successful record after the source pointer has + // already been replaced. Missing/stale Begin state must remain fail-closed. + return; + } + + classicDeformRecord_t &record = drawSurf->classicDeform; + record.cpuFinalized = true; + record.resultMaterial = drawSurf->material; + CaptureMaterialIdentity( record.resultMaterial, record.resultMaterialIndex, + record.resultMaterialNameHash ); + CaptureGeometryState( drawSurf->geo, record.resultGeometry ); + + if ( record.kind == CLASSIC_DEFORM_KIND_NONE ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_NONE; + } else if ( r_skipDeforms.GetBool() ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_SKIPPED; + } else if ( KindIsUnsupported( record.kind ) ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_UNSUPPORTED; + } else if ( EmptyOutputProvable( record ) ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_EMPTY; + } else if ( CompletedOutputProvable( record ) ) { + record.outcome = CLASSIC_DEFORM_OUTCOME_COMPLETED; + } else { + record.outcome = CLASSIC_DEFORM_OUTCOME_FAILED; + } + SealRecord( record ); +} + +void R_ClassicDeformDomain_SnapshotDrawSurf( const drawSurf_t *drawSurf, + classicDeformRole_t role, std::uint64_t frameToken, + classicDeformRecord_t &record ) { + if ( frameToken == 0 ) { + frameToken = R_ClassicDeformDomain_CurrentFrameToken(); + } + if ( role <= CLASSIC_DEFORM_ROLE_UNKNOWN + || role >= CLASSIC_DEFORM_ROLE_COUNT ) { + role = CLASSIC_DEFORM_ROLE_OTHER; + } + + // Interaction/fog receiver and shadow-volume drawSurfs never pass through + // R_FinalizeDrawSurf. Do not inspect their uninitialized embedded slot or + // claim that their material deformation ran. + if ( role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW ) { + InitializeRecordFromDrawSurf( drawSurf, role, frameToken, false, record ); + SealRecord( record ); + return; + } + + if ( drawSurf != NULL + && drawSurf->classicDeform.initialized + && drawSurf->classicDeform.role + == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + && drawSurf->classicDeform.frameToken == frameToken + && R_ClassicDeformDomain_RecordMatchesDrawSurf( + drawSurf->classicDeform, drawSurf ) ) { + record = drawSurf->classicDeform; + return; + } + + const classicDeformKind_t kind = KindForMaterial( + drawSurf != NULL ? drawSurf->material : NULL ); + InitializeRecordFromDrawSurf( drawSurf, role, frameToken, + r_rendererSharedDeform.GetBool() + && kind != CLASSIC_DEFORM_KIND_NONE, record ); + SealRecord( record ); +} + +void R_ClassicDeformDomain_SnapshotDrawSurf( const drawSurf_t *drawSurf, + classicDeformRole_t role, classicDeformRecord_t &record ) { + R_ClassicDeformDomain_SnapshotDrawSurf( drawSurf, role, + R_ClassicDeformDomain_CurrentFrameToken(), record ); +} + +std::uint64_t R_ClassicDeformDomain_ComputeSemanticHash( + const classicDeformRecord_t &record ) { + std::uint64_t hash = HASH_OFFSET; + HashU32( hash, CONTRACT_HASH_VERSION ); + HashInt( hash, record.role ); + HashInt( hash, record.kind ); + HashInt( hash, record.outcome ); + HashInt( hash, record.sourceMaterialIndex ); + HashInt( hash, record.resultMaterialIndex ); + HashU64( hash, record.sourceMaterialNameHash ); + HashU64( hash, record.resultMaterialNameHash ); + HashU64( hash, record.deformDeclNameHash ); + HashU64( hash, record.inputSemanticHash ); + HashGeometryState( hash, record.sourceGeometry ); + HashGeometryState( hash, record.resultGeometry ); + HashInt( hash, record.parameterCount ); + for ( int i = 0; i < CLASSIC_DEFORM_MAX_PARAMETERS; ++i ) { + HashInt( hash, record.parameterRegisters[ i ] ); + HashFloat( hash, record.parameterValues[ i ] ); + } + HashFloat( hash, record.flareScale ); + HashBool( hash, record.parametersValid ); + HashBool( hash, record.cpuFinalized ); + return hash; +} + +bool R_ClassicDeformDomain_ValidateRecord( + const classicDeformRecord_t &record ) { + return record.valid && ValidateRecordInternal( record, true ); +} + +bool R_ClassicDeformDomain_RecordFreshForFrame( + const classicDeformRecord_t &record, std::uint64_t frameToken ) { + return frameToken != 0 && record.initialized + && record.frameToken == frameToken; +} + +bool R_ClassicDeformDomain_ValidateRecordForFrame( + const classicDeformRecord_t &record, std::uint64_t frameToken ) { + return R_ClassicDeformDomain_RecordFreshForFrame( record, frameToken ) + && R_ClassicDeformDomain_ValidateRecord( record ); +} + +bool R_ClassicDeformDomain_RecordMatchesDrawSurf( + const classicDeformRecord_t &record, const drawSurf_t *drawSurf ) { + if ( drawSurf == NULL + || !R_ClassicDeformDomain_ValidateRecordForFrame( record, + R_ClassicDeformDomain_CurrentFrameToken() ) + || record.resultMaterial != drawSurf->material ) { + return false; + } + classicDeformGeometryState_t current; + CaptureGeometryState( drawSurf->geo, current ); + return GeometryStateMatches( record.resultGeometry, current ); +} + +bool R_ClassicDeformDomain_SameProvenance( + const classicDeformRecord_t &a, const classicDeformRecord_t &b ) { + return R_ClassicDeformDomain_ValidateRecord( a ) + && R_ClassicDeformDomain_ValidateRecord( b ) + && a.frameToken == b.frameToken && a.semanticHash == b.semanticHash + && a.role == b.role && a.kind == b.kind && a.outcome == b.outcome + && a.cpuFinalized == b.cpuFinalized + && a.sourceMaterial == b.sourceMaterial + && a.resultMaterial == b.resultMaterial + && GeometryStateMatches( a.sourceGeometry, b.sourceGeometry ) + && GeometryStateMatches( a.resultGeometry, b.resultGeometry ); +} + +bool R_ClassicDeformDomain_HasCompletedOutput( + const classicDeformRecord_t &record ) { + return record.outcome == CLASSIC_DEFORM_OUTCOME_COMPLETED + && R_ClassicDeformDomain_ValidateRecord( record ) + && CompletedOutputProvable( record ); +} + +bool R_ClassicDeformDomain_HasEmptyOutput( + const classicDeformRecord_t &record ) { + return record.outcome == CLASSIC_DEFORM_OUTCOME_EMPTY + && R_ClassicDeformDomain_ValidateRecord( record ) + && EmptyOutputProvable( record ); +} + +bool R_ClassicDeformDomain_IsFailClosed( + const classicDeformRecord_t &record ) { + if ( !R_ClassicDeformDomain_ValidateRecord( record ) ) { + return true; + } + return record.kind != CLASSIC_DEFORM_KIND_NONE + && record.outcome != CLASSIC_DEFORM_OUTCOME_COMPLETED + && record.outcome != CLASSIC_DEFORM_OUTCOME_EMPTY + && record.outcome != CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE; +} + +void R_ClassicDeformDomain_BeginPacketFrame( std::uint64_t frameToken ) { + std::memset( &packetStats, 0, sizeof( packetStats ) ); + packetStats.frameToken = frameToken != 0 + ? frameToken : R_ClassicDeformDomain_CurrentFrameToken(); +} + +void R_ClassicDeformDomain_RecordPacket( + const classicDeformRecord_t &record ) { + if ( packetStats.frameToken == 0 + || packetStats.frameToken != record.frameToken ) { + R_ClassicDeformDomain_BeginPacketFrame( record.frameToken ); + } + packetStats.records++; + if ( packetStats.hash == 0 ) { + packetStats.hash = HASH_OFFSET; + } + const bool valid = R_ClassicDeformDomain_ValidateRecordForFrame( + record, packetStats.frameToken ); + HashBool( packetStats.hash, valid ); + HashInt( packetStats.hash, record.role ); + HashInt( packetStats.hash, record.outcome ); + HashU64( packetStats.hash, record.semanticHash ); + if ( !valid ) { + packetStats.invalid++; + return; + } + switch ( record.outcome ) { + case CLASSIC_DEFORM_OUTCOME_NONE: + packetStats.none++; + break; + case CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE: + packetStats.notApplicable++; + break; + case CLASSIC_DEFORM_OUTCOME_COMPLETED: + packetStats.completed++; + break; + case CLASSIC_DEFORM_OUTCOME_EMPTY: + packetStats.empty++; + break; + case CLASSIC_DEFORM_OUTCOME_SKIPPED: + packetStats.skipped++; + break; + case CLASSIC_DEFORM_OUTCOME_FAILED: + packetStats.failed++; + break; + case CLASSIC_DEFORM_OUTCOME_UNSUPPORTED: + packetStats.unsupported++; + break; + case CLASSIC_DEFORM_OUTCOME_COUNT: + default: + packetStats.invalid++; + break; + } +} + +const classicDeformDomainStats_t &R_ClassicDeformDomain_Stats( void ) { + return packetStats; +} + +const char *ClassicDeformRole_Name( classicDeformRole_t role ) { + switch ( role ) { + case CLASSIC_DEFORM_ROLE_UNKNOWN: return "unknown"; + case CLASSIC_DEFORM_ROLE_FINALIZED_DRAW: return "finalizedDraw"; + case CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER: return "interactionReceiver"; + case CLASSIC_DEFORM_ROLE_FOG_RECEIVER: return "fogReceiver"; + case CLASSIC_DEFORM_ROLE_SHADOW_VOLUME: return "shadowVolume"; + case CLASSIC_DEFORM_ROLE_OTHER: return "other"; + case CLASSIC_DEFORM_ROLE_COUNT: + default: return "invalid"; + } +} + +const char *ClassicDeformKind_Name( classicDeformKind_t kind ) { + switch ( kind ) { + case CLASSIC_DEFORM_KIND_NONE: return "none"; + case CLASSIC_DEFORM_KIND_SPRITE: return "sprite"; + case CLASSIC_DEFORM_KIND_RECTSPRITE: return "rectsprite"; + case CLASSIC_DEFORM_KIND_TUBE: return "tube"; + case CLASSIC_DEFORM_KIND_FLARE: return "flare"; + case CLASSIC_DEFORM_KIND_EXPAND: return "expand"; + case CLASSIC_DEFORM_KIND_MOVE: return "move"; + case CLASSIC_DEFORM_KIND_TURBULENT: return "turbulent"; + case CLASSIC_DEFORM_KIND_EYEBALL: return "eyeball"; + case CLASSIC_DEFORM_KIND_PARTICLE: return "particle"; + case CLASSIC_DEFORM_KIND_PARTICLE2: return "particle2"; + case CLASSIC_DEFORM_KIND_UNKNOWN: return "unknown"; + case CLASSIC_DEFORM_KIND_COUNT: + default: return "invalid"; + } +} + +const char *ClassicDeformOutcome_Name( classicDeformOutcome_t outcome ) { + switch ( outcome ) { + case CLASSIC_DEFORM_OUTCOME_NONE: return "none"; + case CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE: return "notApplicable"; + case CLASSIC_DEFORM_OUTCOME_SKIPPED: return "skipped"; + case CLASSIC_DEFORM_OUTCOME_COMPLETED: return "completed"; + case CLASSIC_DEFORM_OUTCOME_EMPTY: return "empty"; + case CLASSIC_DEFORM_OUTCOME_FAILED: return "failed"; + case CLASSIC_DEFORM_OUTCOME_UNSUPPORTED: return "unsupported"; + case CLASSIC_DEFORM_OUTCOME_COUNT: + default: return "invalid"; + } +} + +const char *ClassicDeformCacheLifetime_Name( + classicDeformCacheLifetime_t lifetime ) { + switch ( lifetime ) { + case CLASSIC_DEFORM_CACHE_UNKNOWN: return "unknown"; + case CLASSIC_DEFORM_CACHE_STATIC: return "static"; + case CLASSIC_DEFORM_CACHE_FRAME_TEMP: return "frameTemp"; + case CLASSIC_DEFORM_CACHE_CLIENT_MEMORY: return "clientMemory"; + case CLASSIC_DEFORM_CACHE_DYNAMIC_BRIDGE: return "dynamicBridge"; + case CLASSIC_DEFORM_CACHE_COUNT: + default: return "invalid"; + } +} + +bool RendererClassicDeformDomain_RunSelfTest( void ) { + if ( idStr::Cmp( ClassicDeformRole_Name( + CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER ), "interactionReceiver" ) + || idStr::Cmp( ClassicDeformKind_Name( + CLASSIC_DEFORM_KIND_RECTSPRITE ), "rectsprite" ) + || idStr::Cmp( ClassicDeformOutcome_Name( + CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE ), "notApplicable" ) + || idStr::Cmp( ClassicDeformCacheLifetime_Name( + CLASSIC_DEFORM_CACHE_FRAME_TEMP ), "frameTemp" ) ) { + return false; + } + + classicDeformRecord_t completed; + std::memset( &completed, 0, sizeof( completed ) ); + completed.role = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + completed.kind = CLASSIC_DEFORM_KIND_MOVE; + completed.outcome = CLASSIC_DEFORM_OUTCOME_COMPLETED; + completed.sourceMaterial = reinterpret_cast( + static_cast( 0x100 ) ); + completed.resultMaterial = completed.sourceMaterial; + completed.sourceGeometry.geometry = + reinterpret_cast( + static_cast( 0x200 ) ); + completed.resultGeometry.geometry = + reinterpret_cast( + static_cast( 0x300 ) ); + completed.resultGeometry.ambientCache = + reinterpret_cast( + static_cast( 0x400 ) ); + completed.sourceMaterialIndex = 7; + completed.resultMaterialIndex = 7; + completed.frameToken = 1; + completed.sourceMaterialNameHash = 0x1111ull; + completed.resultMaterialNameHash = 0x1111ull; + completed.deformDeclNameHash = 0x2222ull; + completed.inputSemanticHash = 0x3333ull; + completed.parameterCount = 1; + for ( int i = 0; i < CLASSIC_DEFORM_MAX_PARAMETERS; ++i ) { + completed.parameterRegisters[ i ] = -1; + } + completed.parameterRegisters[ 0 ] = 3; + completed.parameterValues[ 0 ] = 2.0f; + completed.parametersValid = true; + completed.cpuFinalized = true; + completed.initialized = true; + completed.sourceGeometry.vertexCount = 4; + completed.sourceGeometry.indexCount = 6; + completed.sourceGeometry.captured = true; + completed.sourceGeometry.hasClientVertexData = true; + completed.sourceGeometry.hasClientIndexData = true; + completed.sourceGeometry.cacheStateValid = true; + completed.sourceGeometry.lifetime = CLASSIC_DEFORM_CACHE_CLIENT_MEMORY; + completed.resultGeometry.vertexCount = 4; + completed.resultGeometry.indexCount = 6; + completed.resultGeometry.captured = true; + completed.resultGeometry.hasAmbientCache = true; + completed.resultGeometry.ambientCacheHasBacking = true; + completed.resultGeometry.ambientBuffer = 1; + completed.resultGeometry.ambientBytes = 4 * sizeof( idDrawVert ); + completed.resultGeometry.ambientOffset = 0; + completed.resultGeometry.ambientTag = TAG_TEMP; + completed.resultGeometry.hasClientIndexData = true; + completed.resultGeometry.lifetime = CLASSIC_DEFORM_CACHE_FRAME_TEMP; + completed.resultGeometry.cacheStateValid = true; + completed.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( completed ); + completed.valid = true; + if ( !R_ClassicDeformDomain_ValidateRecord( completed ) + || !R_ClassicDeformDomain_ValidateRecordForFrame( completed, 1 ) + || !R_ClassicDeformDomain_HasCompletedOutput( completed ) + || R_ClassicDeformDomain_IsFailClosed( completed ) ) { + return false; + } + classicDeformRecord_t driftedMaterial = completed; + driftedMaterial.resultMaterial = reinterpret_cast( + static_cast( 0x101 ) ); + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasCompletedOutput( driftedMaterial ) ) { + return false; + } + driftedMaterial = completed; + driftedMaterial.resultMaterialIndex++; + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasCompletedOutput( driftedMaterial ) ) { + return false; + } + driftedMaterial = completed; + driftedMaterial.resultMaterialNameHash++; + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasCompletedOutput( driftedMaterial ) ) { + return false; + } + + classicDeformRecord_t same = completed; + same.frameToken = 2; + same.resultGeometry.ambientBuffer = 99; + if ( R_ClassicDeformDomain_ComputeSemanticHash( same ) + != completed.semanticHash + || R_ClassicDeformDomain_SameProvenance( completed, same ) ) { + return false; + } + same = completed; + same.parameterValues[ 0 ] = 3.0f; + same.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( same ); + same.valid = true; + if ( same.semanticHash == completed.semanticHash + || R_ClassicDeformDomain_SameProvenance( completed, same ) ) { + return false; + } + + classicDeformRecord_t empty = completed; + empty.kind = CLASSIC_DEFORM_KIND_FLARE; + empty.outcome = CLASSIC_DEFORM_OUTCOME_EMPTY; + empty.flareScale = 1.0f; + empty.resultGeometry.geometry = + reinterpret_cast( + static_cast( 0x500 ) ); + empty.resultGeometry.ambientCache = NULL; + empty.resultGeometry.indexCache = NULL; + empty.resultGeometry.vertexCount = 16; + empty.resultGeometry.indexCount = 0; + empty.resultGeometry.hasAmbientCache = false; + empty.resultGeometry.hasIndexCache = false; + empty.resultGeometry.ambientCacheHasBacking = false; + empty.resultGeometry.indexCacheHasBacking = false; + empty.resultGeometry.ambientBuffer = 0; + empty.resultGeometry.ambientBytes = 0; + empty.resultGeometry.ambientOffset = -1; + empty.resultGeometry.ambientTag = -1; + empty.resultGeometry.indexOffset = -1; + empty.resultGeometry.indexTag = -1; + empty.resultGeometry.lifetime = CLASSIC_DEFORM_CACHE_CLIENT_MEMORY; + empty.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( empty ); + empty.valid = true; + if ( !R_ClassicDeformDomain_ValidateRecord( empty ) + || !R_ClassicDeformDomain_HasEmptyOutput( empty ) + || R_ClassicDeformDomain_IsFailClosed( empty ) ) { + return false; + } + driftedMaterial = empty; + driftedMaterial.resultMaterial = reinterpret_cast( + static_cast( 0x101 ) ); + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasEmptyOutput( driftedMaterial ) ) { + return false; + } + driftedMaterial = empty; + driftedMaterial.resultMaterialIndex++; + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasEmptyOutput( driftedMaterial ) ) { + return false; + } + driftedMaterial = empty; + driftedMaterial.resultMaterialNameHash++; + driftedMaterial.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( driftedMaterial ); + driftedMaterial.valid = true; + if ( R_ClassicDeformDomain_ValidateRecord( driftedMaterial ) + || R_ClassicDeformDomain_HasEmptyOutput( driftedMaterial ) ) { + return false; + } + + classicDeformRecord_t notApplicable = completed; + notApplicable.role = CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER; + notApplicable.outcome = CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE; + notApplicable.cpuFinalized = false; + notApplicable.resultGeometry = notApplicable.sourceGeometry; + notApplicable.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( notApplicable ); + notApplicable.valid = true; + if ( !R_ClassicDeformDomain_ValidateRecord( notApplicable ) + || R_ClassicDeformDomain_IsFailClosed( notApplicable ) ) { + return false; + } + + classicDeformRecord_t skipped = completed; + skipped.outcome = CLASSIC_DEFORM_OUTCOME_SKIPPED; + skipped.resultGeometry = skipped.sourceGeometry; + skipped.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( skipped ); + skipped.valid = true; + if ( !R_ClassicDeformDomain_ValidateRecord( skipped ) + || !R_ClassicDeformDomain_IsFailClosed( skipped ) ) { + return false; + } + + classicDeformRecord_t failed = completed; + failed.outcome = CLASSIC_DEFORM_OUTCOME_FAILED; + failed.resultGeometry = failed.sourceGeometry; + failed.semanticHash = R_ClassicDeformDomain_ComputeSemanticHash( failed ); + failed.valid = true; + if ( !R_ClassicDeformDomain_ValidateRecord( failed ) + || !R_ClassicDeformDomain_IsFailClosed( failed ) ) { + return false; + } + + classicDeformRecord_t unsupported = failed; + unsupported.kind = CLASSIC_DEFORM_KIND_PARTICLE; + unsupported.outcome = CLASSIC_DEFORM_OUTCOME_UNSUPPORTED; + unsupported.parameterCount = 0; + unsupported.parameterRegisters[ 0 ] = -1; + unsupported.parameterValues[ 0 ] = 0.0f; + unsupported.semanticHash = + R_ClassicDeformDomain_ComputeSemanticHash( unsupported ); + unsupported.valid = true; + return R_ClassicDeformDomain_ValidateRecord( unsupported ) + && R_ClassicDeformDomain_IsFailClosed( unsupported ); +} diff --git a/src/renderer/ClassicDeformDomain.h b/src/renderer/ClassicDeformDomain.h new file mode 100644 index 00000000..824753f3 --- /dev/null +++ b/src/renderer/ClassicDeformDomain.h @@ -0,0 +1,189 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_DEFORM_DOMAIN_H__ +#define __CLASSIC_DEFORM_DOMAIN_H__ + +#include + +class idMaterial; +struct drawSurf_s; +struct srfTriangles_s; +struct vertCache_s; + +/* +=============================================================================== + + Sealed front-end contract for classic material deformation. + + The established CPU deformers remain authoritative. This contract records + the source and published draw geometry around that call so scene-packet + consumers can distinguish completed material deformation from unrelated + dynamic-model ownership. Pointer fields are frame-local identity checks; the + semantic hash deliberately excludes addresses, frame tokens, and backend + buffer names. + +=============================================================================== +*/ + +const int CLASSIC_DEFORM_MAX_PARAMETERS = 4; + +enum classicDeformRole_t { + CLASSIC_DEFORM_ROLE_UNKNOWN = 0, + CLASSIC_DEFORM_ROLE_FINALIZED_DRAW, + CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER, + CLASSIC_DEFORM_ROLE_FOG_RECEIVER, + CLASSIC_DEFORM_ROLE_SHADOW_VOLUME, + CLASSIC_DEFORM_ROLE_OTHER, + CLASSIC_DEFORM_ROLE_COUNT +}; + +enum classicDeformKind_t { + CLASSIC_DEFORM_KIND_NONE = 0, + CLASSIC_DEFORM_KIND_SPRITE, + CLASSIC_DEFORM_KIND_RECTSPRITE, + CLASSIC_DEFORM_KIND_TUBE, + CLASSIC_DEFORM_KIND_FLARE, + CLASSIC_DEFORM_KIND_EXPAND, + CLASSIC_DEFORM_KIND_MOVE, + CLASSIC_DEFORM_KIND_TURBULENT, + CLASSIC_DEFORM_KIND_EYEBALL, + CLASSIC_DEFORM_KIND_PARTICLE, + CLASSIC_DEFORM_KIND_PARTICLE2, + CLASSIC_DEFORM_KIND_UNKNOWN, + CLASSIC_DEFORM_KIND_COUNT +}; + +enum classicDeformOutcome_t { + CLASSIC_DEFORM_OUTCOME_NONE = 0, + CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE, + CLASSIC_DEFORM_OUTCOME_SKIPPED, + CLASSIC_DEFORM_OUTCOME_COMPLETED, + CLASSIC_DEFORM_OUTCOME_EMPTY, + CLASSIC_DEFORM_OUTCOME_FAILED, + CLASSIC_DEFORM_OUTCOME_UNSUPPORTED, + CLASSIC_DEFORM_OUTCOME_COUNT +}; + +enum classicDeformCacheLifetime_t { + CLASSIC_DEFORM_CACHE_UNKNOWN = 0, + CLASSIC_DEFORM_CACHE_STATIC, + CLASSIC_DEFORM_CACHE_FRAME_TEMP, + CLASSIC_DEFORM_CACHE_CLIENT_MEMORY, + CLASSIC_DEFORM_CACHE_DYNAMIC_BRIDGE, + CLASSIC_DEFORM_CACHE_COUNT +}; + +typedef struct classicDeformGeometryState_s { + const srfTriangles_s *geometry; + const vertCache_s *ambientCache; + const vertCache_s *indexCache; + int vertexCount; + int indexCount; + unsigned int ambientBuffer; + unsigned int indexBuffer; + int ambientOffset; + int indexOffset; + int ambientBytes; + int indexBytes; + int ambientTag; + int indexTag; + classicDeformCacheLifetime_t lifetime; + bool captured; + bool hasAmbientCache; + bool hasIndexCache; + bool ambientCacheHasBacking; + bool indexCacheHasBacking; + bool hasClientVertexData; + bool hasClientIndexData; + bool hasPrimBatchMesh; + bool cacheStateValid; +} classicDeformGeometryState_t; + +typedef struct classicDeformRecord_s { + classicDeformRole_t role; + classicDeformKind_t kind; + classicDeformOutcome_t outcome; + const idMaterial *sourceMaterial; + const idMaterial *resultMaterial; + classicDeformGeometryState_t sourceGeometry; + classicDeformGeometryState_t resultGeometry; + int sourceMaterialIndex; + int resultMaterialIndex; + std::uint64_t frameToken; + std::uint64_t sourceMaterialNameHash; + std::uint64_t resultMaterialNameHash; + std::uint64_t deformDeclNameHash; + std::uint64_t inputSemanticHash; + int parameterCount; + int parameterRegisters[ CLASSIC_DEFORM_MAX_PARAMETERS ]; + float parameterValues[ CLASSIC_DEFORM_MAX_PARAMETERS ]; + float flareScale; + bool parametersValid; + bool cpuFinalized; + bool initialized; + bool valid; + std::uint64_t semanticHash; +} classicDeformRecord_t; + +typedef struct classicDeformDomainStats_s { + std::uint64_t frameToken; + int records; + int none; + int notApplicable; + int completed; + int empty; + int skipped; + int failed; + int unsupported; + int invalid; + std::uint64_t hash; +} classicDeformDomainStats_t; + +// Begin/End bracket only R_FinalizeDrawSurf's authoritative CPU call. Passes +// which reuse an unfinalized receiver seal NOT_APPLICABLE through Snapshot. +void R_ClassicDeformDomain_BeginDrawSurf( drawSurf_s *drawSurf ); +void R_ClassicDeformDomain_EndDrawSurf( drawSurf_s *drawSurf ); + +std::uint64_t R_ClassicDeformDomain_CurrentFrameToken( void ); +void R_ClassicDeformDomain_SnapshotDrawSurf( const drawSurf_s *drawSurf, + classicDeformRole_t role, std::uint64_t frameToken, + classicDeformRecord_t &record ); +void R_ClassicDeformDomain_SnapshotDrawSurf( const drawSurf_s *drawSurf, + classicDeformRole_t role, classicDeformRecord_t &record ); + +std::uint64_t R_ClassicDeformDomain_ComputeSemanticHash( + const classicDeformRecord_t &record ); +bool R_ClassicDeformDomain_ValidateRecord( + const classicDeformRecord_t &record ); +bool R_ClassicDeformDomain_ValidateRecordForFrame( + const classicDeformRecord_t &record, std::uint64_t frameToken ); +bool R_ClassicDeformDomain_RecordFreshForFrame( + const classicDeformRecord_t &record, std::uint64_t frameToken ); +bool R_ClassicDeformDomain_RecordMatchesDrawSurf( + const classicDeformRecord_t &record, const drawSurf_s *drawSurf ); +bool R_ClassicDeformDomain_SameProvenance( + const classicDeformRecord_t &a, const classicDeformRecord_t &b ); +bool R_ClassicDeformDomain_HasCompletedOutput( + const classicDeformRecord_t &record ); +bool R_ClassicDeformDomain_HasEmptyOutput( + const classicDeformRecord_t &record ); +bool R_ClassicDeformDomain_IsFailClosed( + const classicDeformRecord_t &record ); + +// Scene-packet diagnostics retain the last completed front-end frame so +// gfxInfo remains useful after the transient packet arena has been released. +void R_ClassicDeformDomain_BeginPacketFrame( std::uint64_t frameToken ); +void R_ClassicDeformDomain_RecordPacket( + const classicDeformRecord_t &record ); +const classicDeformDomainStats_t &R_ClassicDeformDomain_Stats( void ); + +const char *ClassicDeformRole_Name( classicDeformRole_t role ); +const char *ClassicDeformKind_Name( classicDeformKind_t kind ); +const char *ClassicDeformOutcome_Name( classicDeformOutcome_t outcome ); +const char *ClassicDeformCacheLifetime_Name( + classicDeformCacheLifetime_t lifetime ); + +bool RendererClassicDeformDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_DEFORM_DOMAIN_H__ */ diff --git a/src/renderer/ClassicFogBlendDomain.cpp b/src/renderer/ClassicFogBlendDomain.cpp new file mode 100644 index 00000000..d05f5f4b --- /dev/null +++ b/src/renderer/ClassicFogBlendDomain.cpp @@ -0,0 +1,2394 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicFogBlendDomain.h" + +#include +#include + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; + +typedef struct classicFogBlendDomainState_s { + classicFogBlendDomainView_t views[ CLASSIC_FOG_BLEND_DOMAIN_MAX_VIEWS ]; + classicFogBlendDomainLight_t lights[ CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHTS ]; + classicFogBlendDomainSurface_t surfaces[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_SURFACES ]; + classicFogBlendDomainLightStage_t lightStages[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES ]; + classicFogBlendDomainPrimitive_t primitives[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES ]; + classicFogBlendDomainTexture_t textures[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_TEXTURES ]; + classicFogBlendDomainStats_t stats; + std::uint32_t generation; + int viewCount; + int lightCount; + int surfaceCount; + int lightStageCount; + int primitiveCount; + int textureCount; +} classicFogBlendDomainState_t; + +typedef struct classicFogBlendCheckpoint_s { + int lightCount; + int surfaceCount; + int lightStageCount; + int primitiveCount; + int textureCount; +} classicFogBlendCheckpoint_t; + +typedef struct classicFogBlendBuildError_s { + classicFogBlendDomainFailure_t failure; + int detail; + int passPacketIndex; + int drawPacketIndex; + int lightOrdinal; + int receiverOrdinal; + int stageIndex; +} classicFogBlendBuildError_t; + +classicFogBlendDomainState_t domain; + +static bool RangeFits( int first, int count, int capacity ) { + return first >= 0 && count >= 0 && first <= capacity + && count <= capacity - first; +} + +static bool FloatIsFinite( float value ) { + return std::isfinite( value ) != 0; +} + +static bool FloatsAreFinite( const float *values, int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !FloatIsFinite( values[ i ] ) ) { + return false; + } + } + return true; +} + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + for ( int i = 0; i < 4; ++i ) { + HashByte( hash, value >> ( i * 8 ) ); + } +} + +static void HashU64( std::uint64_t &hash, std::uint64_t value ) { + for ( int i = 0; i < 8; ++i ) { + HashByte( hash, + static_cast( value >> ( i * 8 ) ) ); + } +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), + "float hash width mismatch" ); + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static std::uint64_t HashString( const char *text ) { + std::uint64_t hash = HASH_OFFSET; + if ( text != NULL ) { + for ( const unsigned char *cursor = + reinterpret_cast( text ); + *cursor != 0; ++cursor ) { + HashByte( hash, *cursor ); + } + } + return hash; +} + +static void SetError( classicFogBlendBuildError_t &error, + classicFogBlendDomainFailure_t failure, int detail = 0, + int passPacketIndex = -1, int drawPacketIndex = -1, + int lightOrdinal = -1, int receiverOrdinal = -1, + int stageIndex = -1 ) { + error.failure = failure; + error.detail = detail; + error.passPacketIndex = passPacketIndex; + error.drawPacketIndex = drawPacketIndex; + error.lightOrdinal = lightOrdinal; + error.receiverOrdinal = receiverOrdinal; + error.stageIndex = stageIndex; +} + +static void InitError( classicFogBlendBuildError_t &error ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONE ); +} + +static void InitTextureMatrix( float matrix[ 2 ][ 4 ] ) { + std::memset( matrix, 0, sizeof( float ) * 8 ); + matrix[ 0 ][ 0 ] = 1.0f; + matrix[ 1 ][ 1 ] = 1.0f; +} + +static void InitBlend( rendererBlendState_t &blend, + rendererBlendFactor_t source, rendererBlendFactor_t destination ) { + std::memset( &blend, 0, sizeof( blend ) ); + blend.enabled = true; + blend.sourceColor = source; + blend.destinationColor = destination; + blend.colorOperation = RENDERER_BLEND_OP_ADD; + blend.sourceAlpha = source; + blend.destinationAlpha = destination; + blend.alphaOperation = RENDERER_BLEND_OP_ADD; +} + +static void InitSurface( classicFogBlendDomainSurface_t &surface ) { + std::memset( &surface, 0, sizeof( surface ) ); + surface.drawPacketIndex = -1; + surface.lightIndex = -1; + surface.sourceOrdinal = -1; + surface.receiverOrdinal = -1; + surface.receiver = CLASSIC_FOG_BLEND_RECEIVER_GLOBAL; + surface.geometryRecordIndex = -1; + surface.instanceRecordIndex = -1; +} + +static void InitLightStage( classicFogBlendDomainLightStage_t &stage ) { + std::memset( &stage, 0, sizeof( stage ) ); + stage.lightIndex = -1; + stage.sourceStageIndex = -1; + stage.disposition = CLASSIC_FOG_BLEND_STAGE_DRAW; + stage.firstPrimitive = -1; + stage.cull = RENDERER_CULL_FRONT; + stage.colorWriteMask = RENDERER_COLOR_WRITE_RGBA; + stage.alphaTestCompareOperation = RENDERER_COMPARE_ALWAYS; + stage.depth.testEnabled = true; + stage.depth.writeEnabled = false; + stage.depth.compareOperation = RENDERER_COMPARE_EQUAL; + InitBlend( stage.blend, RENDERER_BLEND_ONE, RENDERER_BLEND_ZERO ); + InitTextureMatrix( stage.textureMatrix ); +} + +static void InitLight( classicFogBlendDomainLight_t &light ) { + std::memset( &light, 0, sizeof( light ) ); + light.sourceOrdinal = -1; + light.kind = CLASSIC_FOG_BLEND_LIGHT_FOG; + light.disposition = CLASSIC_FOG_BLEND_LIGHT_DRAW; + light.firstSurface = -1; + light.firstLightStage = -1; + light.firstPrimitive = -1; +} + +static void InitPrimitive( classicFogBlendDomainPrimitive_t &primitive ) { + std::memset( &primitive, 0, sizeof( primitive ) ); + primitive.lightIndex = -1; + primitive.lightStageIndex = -1; + primitive.surfaceIndex = -1; + primitive.kind = CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER; + primitive.receiver = CLASSIC_FOG_BLEND_RECEIVER_GLOBAL; + primitive.disposition = CLASSIC_FOG_BLEND_PRIMITIVE_DRAW; + primitive.geometryRecordIndex = -1; + primitive.instanceRecordIndex = -1; + primitive.depth.testEnabled = true; + primitive.depth.writeEnabled = false; + primitive.depth.compareOperation = RENDERER_COMPARE_EQUAL; + primitive.cull = RENDERER_CULL_FRONT; +} + +static void InitView( classicFogBlendDomainView_t &view, + const viewDef_t *viewDef, int scenePacketIndex ) { + std::memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scenePacketIndex = scenePacketIndex; + view.fogBlendPassPacketIndex = -1; + view.firstLight = -1; + view.firstSurface = -1; + view.firstLightStage = -1; + view.firstPrimitive = -1; + view.failure = CLASSIC_FOG_BLEND_FAILURE_UNAVAILABLE; + view.failurePassPacketIndex = -1; + view.failureDrawPacketIndex = -1; + view.failureLightOrdinal = -1; + view.failureReceiverOrdinal = -1; + view.failureStageIndex = -1; + for ( int backend = 0; backend < CLASSIC_FOG_BLEND_BACKEND_COUNT; + ++backend ) { + view.backendOutcome[ backend ] = + CLASSIC_FOG_BLEND_BACKEND_UNRECORDED; + view.backendFailure[ backend ] = CLASSIC_FOG_BLEND_FAILURE_NONE; + } + if ( viewDef != NULL ) { + view.viewportX1 = viewDef->viewport.x1; + view.viewportY1 = viewDef->viewport.y1; + view.viewportX2 = viewDef->viewport.x2; + view.viewportY2 = viewDef->viewport.y2; + view.scissorX1 = viewDef->scissor.x1; + view.scissorY1 = viewDef->scissor.y1; + view.scissorX2 = viewDef->scissor.x2; + view.scissorY2 = viewDef->scissor.y2; + std::memcpy( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ); + } +} + +static bool FailureIsOverflow( classicFogBlendDomainFailure_t failure ) { + return failure == CLASSIC_FOG_BLEND_FAILURE_SCENE_PACKET_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_VIEW_POOL_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_LIGHT_POOL_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_SURFACE_POOL_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_LIGHT_STAGE_POOL_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW + || failure == CLASSIC_FOG_BLEND_FAILURE_TEXTURE_POOL_OVERFLOW; +} + +static bool FailView( classicFogBlendDomainView_t &view, + const classicFogBlendCheckpoint_t &checkpoint, + const classicFogBlendBuildError_t &error ) { + domain.lightCount = checkpoint.lightCount; + domain.surfaceCount = checkpoint.surfaceCount; + domain.lightStageCount = checkpoint.lightStageCount; + domain.primitiveCount = checkpoint.primitiveCount; + domain.textureCount = checkpoint.textureCount; + view.ready = false; + view.failure = error.failure == CLASSIC_FOG_BLEND_FAILURE_NONE + ? CLASSIC_FOG_BLEND_FAILURE_UNAVAILABLE : error.failure; + view.failureDetail = error.detail; + view.failurePassPacketIndex = error.passPacketIndex; + view.failureDrawPacketIndex = error.drawPacketIndex; + view.failureLightOrdinal = error.lightOrdinal; + view.failureReceiverOrdinal = error.receiverOrdinal; + view.failureStageIndex = error.stageIndex; + view.firstLight = -1; + view.lightCount = 0; + view.firstSurface = -1; + view.surfaceCount = 0; + view.firstLightStage = -1; + view.lightStageCount = 0; + view.firstPrimitive = -1; + view.primitiveCount = 0; + view.drawablePrimitiveCount = 0; + view.noopPrimitiveCount = 0; + view.fogReceiverPrimitiveCount = 0; + view.fogFrustumPrimitiveCount = 0; + view.blendPrimitiveCount = 0; + view.materialDeformReceiverCount = 0; + view.hash = 0; + domain.stats.fallbackViews++; + if ( view.failure >= CLASSIC_FOG_BLEND_FAILURE_NONE + && view.failure < CLASSIC_FOG_BLEND_FAILURE_COUNT ) { + domain.stats.failureCounts[ view.failure ]++; + } + if ( FailureIsOverflow( view.failure ) ) { + domain.stats.overflow = true; + } + return false; +} + +static bool MaterialRequestsClassicDeform( const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->Deform() != DFRM_NONE; +} + +static int DeformContractFailureDetail( const drawPacket_t *packet ) { + if ( packet == NULL || !packet->hasClassicDeformRecord + || packet->classicDeformRecord == NULL ) { + return -1; + } + return static_cast( packet->classicDeformRecord->role ) + * CLASSIC_DEFORM_OUTCOME_COUNT + + static_cast( packet->classicDeformRecord->outcome ); +} + +static bool ValidateReceiverDeformContract( const drawSurf_t *drawSurf, + const drawPacket_t &packet, classicDeformRole_t &role, + classicDeformOutcome_t &outcome, std::uint64_t &semanticHash ) { + role = CLASSIC_DEFORM_ROLE_UNKNOWN; + outcome = CLASSIC_DEFORM_OUTCOME_NONE; + semanticHash = 0; + if ( !MaterialRequestsClassicDeform( drawSurf ) ) { + return true; + } + if ( !r_rendererSharedDeform.GetBool() || r_skipDeforms.GetBool() ) { + return false; + } + const std::uint64_t frameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + if ( !packet.hasClassicDeformRecord + || packet.classicDeformRecord == NULL + || packet.geometryRecord == NULL + || packet.classicDeformRecord + != &packet.geometryRecord->classicDeform + || !packet.geometryRecord->hasClassicDeformRecord ) { + return false; + } + const classicDeformRecord_t &record = *packet.classicDeformRecord; + if ( record.role != CLASSIC_DEFORM_ROLE_FOG_RECEIVER + || record.cpuFinalized + || record.outcome != CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE + || !R_ClassicDeformDomain_ValidateRecordForFrame( record, + frameToken ) + || !R_ClassicDeformDomain_RecordMatchesDrawSurf( record, drawSurf ) ) { + return false; + } + role = record.role; + outcome = record.outcome; + semanticHash = record.semanticHash; + return true; +} + +static std::uint64_t MakeTextureResourceId( int textureIndex ) { + if ( domain.generation == 0 || textureIndex < 0 + || textureIndex >= CLASSIC_FOG_BLEND_DOMAIN_MAX_TEXTURES ) { + return 0; + } + return ( static_cast( domain.generation ) << 32 ) + | static_cast( textureIndex + 1 ); +} + +static int TextureIndexFromResourceId( std::uint64_t resourceId ) { + if ( resourceId == 0 + || static_cast( resourceId >> 32 ) + != domain.generation ) { + return -1; + } + const std::uint32_t encoded = static_cast( resourceId ); + if ( encoded == 0 || encoded > CLASSIC_FOG_BLEND_DOMAIN_MAX_TEXTURES ) { + return -1; + } + return static_cast( encoded - 1 ); +} + +static bool ImageReady( const idImage *image, + classicFogBlendBuildError_t &error, int stageIndex ) { + if ( image == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( R_IsMutableRenderImage( image ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DYNAMIC_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( image->IsDefaulted() ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DEFAULTED_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( !image->IsLoaded() ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNLOADED_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static bool AddTexture( const idImage *image, std::uint64_t &resourceId, + classicFogBlendBuildError_t &error, int stageIndex ) { + resourceId = 0; + if ( !ImageReady( image, error, stageIndex ) ) { + return false; + } + for ( int i = 0; i < domain.textureCount; ++i ) { + if ( domain.textures[ i ].image == image + && domain.textures[ i ].storageGeneration + == image->GetStorageGeneration() ) { + resourceId = domain.textures[ i ].textureResourceId; + return true; + } + } + if ( domain.textureCount >= CLASSIC_FOG_BLEND_DOMAIN_MAX_TEXTURES ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_TEXTURE_POOL_OVERFLOW, + domain.textureCount, -1, -1, -1, -1, stageIndex ); + return false; + } + const int textureIndex = domain.textureCount++; + classicFogBlendDomainTexture_t &texture = domain.textures[ textureIndex ]; + std::memset( &texture, 0, sizeof( texture ) ); + texture.textureResourceId = MakeTextureResourceId( textureIndex ); + texture.image = image; + texture.storageGeneration = image->GetStorageGeneration(); + texture.nameHash = HashString( image->GetName() ); + texture.textureHandle = const_cast( image )->GetDeviceHandle(); + texture.filter = image->GetFilter(); + texture.repeat = image->GetRepeat(); + texture.loaded = image->IsLoaded(); + texture.defaulted = image->IsDefaulted(); + texture.mutableImage = R_IsMutableRenderImage( image ); + resourceId = texture.textureResourceId; + return resourceId != 0; +} + +static std::uint64_t StableTextureHash( std::uint64_t resourceId ) { + const int textureIndex = TextureIndexFromResourceId( resourceId ); + return textureIndex >= 0 && textureIndex < domain.textureCount + ? domain.textures[ textureIndex ].nameHash : 0; +} + +static bool ReadRegister( const float *registers, int registerCount, + int registerIndex, float &value, + classicFogBlendBuildError_t &error, int stageIndex ) { + if ( registers == NULL || registerCount <= 0 ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_SHADER_REGISTERS, + registerCount, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( registerIndex < 0 || registerIndex >= registerCount ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_REGISTER_OUT_OF_RANGE, + registerIndex, -1, -1, -1, -1, stageIndex ); + return false; + } + value = registers[ registerIndex ]; + if ( !FloatIsFinite( value ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + registerIndex, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static bool EvaluateStageColor( const shaderStage_t &source, + const float *registers, int registerCount, float color[ 4 ], + classicFogBlendBuildError_t &error, int stageIndex ) { + for ( int component = 0; component < 4; ++component ) { + if ( !ReadRegister( registers, registerCount, + source.color.registers[ component ], color[ component ], error, + stageIndex ) ) { + return false; + } + } + return true; +} + +static bool EvaluateTextureMatrix( const textureStage_t &texture, + const float *registers, int registerCount, float matrix[ 2 ][ 4 ], + classicFogBlendBuildError_t &error, int stageIndex ) { + InitTextureMatrix( matrix ); + if ( !texture.hasMatrix ) { + return true; + } + for ( int row = 0; row < 2; ++row ) { + float values[ 3 ]; + for ( int column = 0; column < 3; ++column ) { + if ( !ReadRegister( registers, registerCount, + texture.matrix[ row ][ column ], values[ column ], error, + stageIndex ) ) { + return false; + } + } + float translation = values[ 2 ]; + if ( translation < -40.0f || translation > 40.0f ) { + if ( static_cast( translation ) < -2147483648.0 + || static_cast( translation ) > 2147483647.0 ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + texture.matrix[ row ][ 2 ], -1, -1, -1, -1, + stageIndex ); + return false; + } + translation -= static_cast( translation ); + } + matrix[ row ][ 0 ] = values[ 0 ]; + matrix[ row ][ 1 ] = values[ 1 ]; + matrix[ row ][ 2 ] = 0.0f; + matrix[ row ][ 3 ] = translation; + } + return true; +} + +static bool MapSourceBlendFactor( int drawStateBits, + rendererBlendFactor_t &factor ) { + switch ( drawStateBits & GLS_SRCBLEND_BITS ) { + case GLS_SRCBLEND_ZERO: factor = RENDERER_BLEND_ZERO; return true; + case GLS_SRCBLEND_ONE: factor = RENDERER_BLEND_ONE; return true; + case GLS_SRCBLEND_DST_COLOR: factor = RENDERER_BLEND_DST_COLOR; return true; + case GLS_SRCBLEND_ONE_MINUS_DST_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_DST_COLOR; return true; + case GLS_SRCBLEND_SRC_ALPHA: factor = RENDERER_BLEND_SRC_ALPHA; return true; + case GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; return true; + case GLS_SRCBLEND_DST_ALPHA: factor = RENDERER_BLEND_DST_ALPHA; return true; + case GLS_SRCBLEND_ONE_MINUS_DST_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_DST_ALPHA; return true; + case GLS_SRCBLEND_ALPHA_SATURATE: + factor = RENDERER_BLEND_SRC_ALPHA_SATURATE; return true; + case GLS_SRCBLEND_SRC_COLOR: factor = RENDERER_BLEND_SRC_COLOR; return true; + case GLS_SRCBLEND_ONE_MINUS_SRC_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_SRC_COLOR; return true; + default: return false; + } +} + +static bool MapDestinationBlendFactor( int drawStateBits, + rendererBlendFactor_t &factor ) { + switch ( drawStateBits & GLS_DSTBLEND_BITS ) { + case GLS_DSTBLEND_ZERO: factor = RENDERER_BLEND_ZERO; return true; + case GLS_DSTBLEND_ONE: factor = RENDERER_BLEND_ONE; return true; + case GLS_DSTBLEND_SRC_COLOR: factor = RENDERER_BLEND_SRC_COLOR; return true; + case GLS_DSTBLEND_ONE_MINUS_SRC_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_SRC_COLOR; return true; + case GLS_DSTBLEND_SRC_ALPHA: factor = RENDERER_BLEND_SRC_ALPHA; return true; + case GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; return true; + case GLS_DSTBLEND_DST_ALPHA: factor = RENDERER_BLEND_DST_ALPHA; return true; + case GLS_DSTBLEND_ONE_MINUS_DST_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_DST_ALPHA; return true; + default: return false; + } +} + +static std::uint32_t ColorWriteMask( int drawStateBits ) { + std::uint32_t mask = RENDERER_COLOR_WRITE_RGBA; + if ( ( drawStateBits & GLS_REDMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_RED ); + } + if ( ( drawStateBits & GLS_GREENMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_GREEN ); + } + if ( ( drawStateBits & GLS_BLUEMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_BLUE ); + } + if ( ( drawStateBits & GLS_ALPHAMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_ALPHA ); + } + return mask; +} + +static bool SealBlendRenderState( int sourceBits, + classicFogBlendDomainLightStage_t &stage, + classicFogBlendBuildError_t &error, int stageIndex ) { + const int supportedStateBits = GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS + | GLS_DEPTHMASK | GLS_COLORMASK | GLS_ALPHAMASK + | GLS_DEPTHFUNC_ALWAYS | GLS_DEPTHFUNC_EQUAL | GLS_ATEST_BITS; + if ( ( sourceBits & ~supportedStateBits ) != 0 + || !MapSourceBlendFactor( sourceBits, stage.blend.sourceColor ) + || !MapDestinationBlendFactor( + sourceBits, stage.blend.destinationColor ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_RENDER_STATE, + sourceBits, -1, -1, -1, -1, stageIndex ); + return false; + } + stage.blend.enabled = ( sourceBits + & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ) ) != 0; + stage.blend.sourceAlpha = stage.blend.sourceColor; + stage.blend.destinationAlpha = stage.blend.destinationColor; + stage.blend.colorOperation = RENDERER_BLEND_OP_ADD; + stage.blend.alphaOperation = RENDERER_BLEND_OP_ADD; + stage.depth.testEnabled = true; + stage.depth.writeEnabled = false; + // RB_BlendLight ORs GLS_DEPTHFUNC_EQUAL after authored state. GL_State gives + // EQUAL precedence if an authored ALWAYS bit is also present. + stage.depth.compareOperation = RENDERER_COMPARE_EQUAL; + stage.cull = RENDERER_CULL_FRONT; + stage.colorWriteMask = ColorWriteMask( sourceBits ); + stage.alphaTestEnabled = false; + stage.alphaTestCompareOperation = RENDERER_COMPARE_ALWAYS; + stage.alphaTestValue = 0.0f; + switch ( sourceBits & GLS_ATEST_BITS ) { + case 0: + break; + case GLS_ATEST_EQ_255: + stage.alphaTestEnabled = true; + stage.alphaTestCompareOperation = RENDERER_COMPARE_EQUAL; + stage.alphaTestValue = 1.0f; + break; + case GLS_ATEST_LT_128: + stage.alphaTestEnabled = true; + stage.alphaTestCompareOperation = RENDERER_COMPARE_LESS; + stage.alphaTestValue = 0.5f; + break; + case GLS_ATEST_GE_128: + stage.alphaTestEnabled = true; + stage.alphaTestCompareOperation = RENDERER_COMPARE_GREATER_OR_EQUAL; + stage.alphaTestValue = 0.5f; + break; + default: + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_RENDER_STATE, + sourceBits, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static std::uint64_t HashSurface( + const classicFogBlendDomainSurface_t &surface, int lightArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, surface.lightIndex - lightArenaBase ); + HashInt( hash, surface.sourceOrdinal ); + HashInt( hash, surface.receiverOrdinal ); + HashInt( hash, surface.receiver ); + HashInt( hash, surface.vertexCount ); + HashInt( hash, surface.firstIndex ); + HashInt( hash, surface.indexCount ); + HashInt( hash, surface.vertexOffset ); + HashInt( hash, surface.scissorX1 ); + HashInt( hash, surface.scissorY1 ); + HashInt( hash, surface.scissorX2 ); + HashInt( hash, surface.scissorY2 ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, surface.modelMatrix[ i ] ); + HashFloat( hash, surface.modelViewMatrix[ i ] ); + } + HashBool( hash, surface.hasAmbientCache ); + HashBool( hash, surface.hasIndexCache ); + HashInt( hash, surface.deformRole ); + HashInt( hash, surface.deformOutcome ); + HashU64( hash, surface.deformContractHash ); + return hash; +} + +static std::uint64_t HashPrimitive( + const classicFogBlendDomainPrimitive_t &primitive, + int lightArenaBase, int stageArenaBase, int surfaceArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, primitive.lightIndex - lightArenaBase ); + HashInt( hash, primitive.lightStageIndex - stageArenaBase ); + HashInt( hash, primitive.surfaceIndex >= 0 + ? primitive.surfaceIndex - surfaceArenaBase : -1 ); + HashInt( hash, primitive.kind ); + HashInt( hash, primitive.receiver ); + HashInt( hash, primitive.disposition ); + HashInt( hash, primitive.vertexCount ); + HashInt( hash, primitive.firstIndex ); + HashInt( hash, primitive.indexCount ); + HashInt( hash, primitive.vertexOffset ); + HashInt( hash, primitive.scissorX1 ); + HashInt( hash, primitive.scissorY1 ); + HashInt( hash, primitive.scissorX2 ); + HashInt( hash, primitive.scissorY2 ); + HashBool( hash, primitive.depth.testEnabled ); + HashBool( hash, primitive.depth.writeEnabled ); + HashInt( hash, primitive.depth.compareOperation ); + HashInt( hash, primitive.cull ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, primitive.modelMatrix[ i ] ); + HashFloat( hash, primitive.modelViewMatrix[ i ] ); + } + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, + primitive.localLightProject[ plane ][ component ] ); + } + } + for ( int unit = 0; unit < 2; ++unit ) { + for ( int coordinate = 0; coordinate < 2; ++coordinate ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, + primitive.fogTexgen[ unit ][ coordinate ][ component ] ); + } + } + } + return hash; +} + +static std::uint64_t HashLightStage( + const classicFogBlendDomainLightStage_t &stage, int lightArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, stage.lightIndex - lightArenaBase ); + HashInt( hash, stage.sourceStageIndex ); + HashInt( hash, stage.disposition ); + HashInt( hash, stage.primitiveCount ); + HashInt( hash, stage.drawablePrimitiveCount ); + HashInt( hash, stage.noopPrimitiveCount ); + HashFloat( hash, stage.condition ); + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, stage.color[ component ] ); + } + for ( int row = 0; row < 2; ++row ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, stage.textureMatrix[ row ][ component ] ); + } + } + HashU64( hash, StableTextureHash( stage.projectionTextureResourceId ) ); + HashU64( hash, StableTextureHash( stage.falloffTextureResourceId ) ); + HashU64( hash, StableTextureHash( stage.fogTextureResourceId ) ); + HashU64( hash, StableTextureHash( stage.fogEnterTextureResourceId ) ); + HashBool( hash, stage.blend.enabled ); + HashInt( hash, stage.blend.sourceColor ); + HashInt( hash, stage.blend.destinationColor ); + HashInt( hash, stage.blend.colorOperation ); + HashInt( hash, stage.blend.sourceAlpha ); + HashInt( hash, stage.blend.destinationAlpha ); + HashInt( hash, stage.blend.alphaOperation ); + HashBool( hash, stage.depth.testEnabled ); + HashBool( hash, stage.depth.writeEnabled ); + HashInt( hash, stage.depth.compareOperation ); + HashInt( hash, stage.cull ); + HashU32( hash, stage.colorWriteMask ); + HashBool( hash, stage.alphaTestEnabled ); + HashInt( hash, stage.alphaTestCompareOperation ); + HashFloat( hash, stage.alphaTestValue ); + HashBool( hash, stage.hasTextureMatrix ); + HashBool( hash, stage.conditionIgnored ); + return hash; +} + +static std::uint64_t HashLight( const classicFogBlendDomainLight_t &light, + int lightArenaBase, int stageArenaBase, int surfaceArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, light.sourceOrdinal ); + HashInt( hash, light.kind ); + HashInt( hash, light.disposition ); + HashInt( hash, light.surfaceCount ); + for ( int receiver = 0; receiver < CLASSIC_FOG_BLEND_RECEIVER_COUNT; + ++receiver ) { + HashInt( hash, light.receiverSurfaceCount[ receiver ] ); + } + HashInt( hash, light.lightStageCount ); + HashInt( hash, light.activeLightStageCount ); + HashInt( hash, light.inactiveLightStageCount ); + HashInt( hash, light.noopLightStageCount ); + HashInt( hash, light.primitiveCount ); + HashInt( hash, light.drawablePrimitiveCount ); + HashInt( hash, light.noopPrimitiveCount ); + HashInt( hash, light.fogReceiverPrimitiveCount ); + HashInt( hash, light.fogFrustumPrimitiveCount ); + HashInt( hash, light.blendPrimitiveCount ); + HashInt( hash, light.scissorX1 ); + HashInt( hash, light.scissorY1 ); + HashInt( hash, light.scissorX2 ); + HashInt( hash, light.scissorY2 ); + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, light.lightProject[ plane ][ component ] ); + } + } + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, light.fogPlane[ component ] ); + HashFloat( hash, light.fogColor[ component ] ); + } + for ( int unit = 0; unit < 2; ++unit ) { + for ( int coordinate = 0; coordinate < 2; ++coordinate ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, + light.fogGlobalTexgen[ unit ][ coordinate ][ component ] ); + } + } + } + HashFloat( hash, light.fogDensity ); + HashFloat( hash, light.fogDistanceScale ); + HashU64( hash, StableTextureHash( light.falloffTextureResourceId ) ); + HashU64( hash, StableTextureHash( light.fogTextureResourceId ) ); + HashU64( hash, StableTextureHash( light.fogEnterTextureResourceId ) ); + HashBool( hash, light.globalChainPresent ); + HashInt( hash, light.materialDeformReceiverCount ); + HashU64( hash, light.deformContractHash ); + for ( int i = 0; i < light.surfaceCount; ++i ) { + HashU64( hash, domain.surfaces[ light.firstSurface + i ].hash ); + } + for ( int i = 0; i < light.lightStageCount; ++i ) { + HashU64( hash, domain.lightStages[ light.firstLightStage + i ].hash ); + } + for ( int i = 0; i < light.primitiveCount; ++i ) { + const classicFogBlendDomainPrimitive_t &primitive = + domain.primitives[ light.firstPrimitive + i ]; + // Recompute with view-local identities in case a caller constructed the + // record before every arena base was known. + HashU64( hash, HashPrimitive( primitive, lightArenaBase, + stageArenaBase, surfaceArenaBase ) ); + } + return hash; +} + +static std::uint64_t HashView( const classicFogBlendDomainView_t &view ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, view.lightCount ); + HashInt( hash, view.fogLightCount ); + HashInt( hash, view.blendLightCount ); + HashInt( hash, view.noopLightCount ); + HashInt( hash, view.surfaceCount ); + HashInt( hash, view.lightStageCount ); + HashInt( hash, view.activeLightStageCount ); + HashInt( hash, view.inactiveLightStageCount ); + HashInt( hash, view.noopLightStageCount ); + HashInt( hash, view.primitiveCount ); + HashInt( hash, view.drawablePrimitiveCount ); + HashInt( hash, view.noopPrimitiveCount ); + HashInt( hash, view.fogReceiverPrimitiveCount ); + HashInt( hash, view.fogFrustumPrimitiveCount ); + HashInt( hash, view.blendPrimitiveCount ); + HashInt( hash, view.packetDrawCount ); + HashInt( hash, view.materialDeformReceiverCount ); + HashInt( hash, view.viewportX1 ); + HashInt( hash, view.viewportY1 ); + HashInt( hash, view.viewportX2 ); + HashInt( hash, view.viewportY2 ); + HashInt( hash, view.scissorX1 ); + HashInt( hash, view.scissorY1 ); + HashInt( hash, view.scissorX2 ); + HashInt( hash, view.scissorY2 ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, view.projectionMatrix[ i ] ); + } + HashBool( hash, view.useScissor ); + HashBool( hash, view.skipBlendLights ); + for ( int i = 0; i < view.lightCount; ++i ) { + HashU64( hash, domain.lights[ view.firstLight + i ].hash ); + } + return hash; +} + +static bool SceneIsFogBlendCandidate( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene ) { + if ( scene.viewDef == NULL + || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || scene.viewDef->viewEntitys == NULL ) { + return false; + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) ) { + return true; + } + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + if ( packetFrame.Pass( scene.firstPassPacket + localPass ).passCategory + == RENDER_PASS_FOG_BLEND ) { + return true; + } + } + return false; +} + +static classicFogBlendDomainView_t *FindMutableView( + const viewDef_t *viewDef ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].viewDef == viewDef ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +static int ViewIndex( const classicFogBlendDomainView_t *view ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( view == &domain.views[ i ] ) { + return i; + } + } + return -1; +} + +static int LightIndex( const classicFogBlendDomainLight_t *light ) { + for ( int i = 0; i < domain.lightCount; ++i ) { + if ( light == &domain.lights[ i ] ) { + return i; + } + } + return -1; +} + +static bool ViewLightIsFogOrBlend( const viewLight_t *viewLight ) { + return viewLight != NULL && viewLight->lightShader != NULL + && ( viewLight->lightShader->IsFogLight() + || viewLight->lightShader->IsBlendLight() ); +} + +static bool ValidateDrawPacketIdentity( const drawPacket_t &packet, + int packetIndex, + const viewDef_t *viewDef, const viewLight_t *viewLight, + int lightOrdinal, sceneFogBlendReceiverClass_t receiverClass, + int receiverOrdinal, int sourceOrdinal, const drawSurf_t *drawSurf, + classicFogBlendBuildError_t &error ) { + if ( packet.passCategory != RENDER_PASS_FOG_BLEND + || packet.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || packet.viewDef != viewDef || packet.legacyDrawSurf != drawSurf + || packet.fogBlendLight != viewLight + || packet.fogBlendLightOrdinal != lightOrdinal + || packet.fogBlendReceiverClass != receiverClass + || packet.fogBlendReceiverOrdinal != receiverOrdinal + || packet.fogBlendSourceOrdinal != sourceOrdinal ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH, + sourceOrdinal, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + return true; +} + +static bool ValidateDrawPacket( const idScenePacketFrame &packetFrame, + const drawPacket_t &packet, int packetIndex, + const viewDef_t *viewDef, const viewLight_t *viewLight, + int lightOrdinal, sceneFogBlendReceiverClass_t receiverClass, + int receiverOrdinal, int sourceOrdinal, const drawSurf_t *drawSurf, + classicFogBlendBuildError_t &error ) { + if ( !ValidateDrawPacketIdentity( packet, packetIndex, viewDef, viewLight, + lightOrdinal, receiverClass, receiverOrdinal, sourceOrdinal, + drawSurf, error ) ) { + return false; + } + if ( drawSurf == NULL || drawSurf->geo == NULL || drawSurf->space == NULL + || packet.geometryRecordIndex < 0 + || packet.geometryRecordIndex >= packetFrame.NumGeometryRecords() + || packet.geometryRecord == NULL + || packet.geometryRecord + != &packetFrame.GeometryRecord( packet.geometryRecordIndex ) + || packet.geometryRecord->legacyGeometry != drawSurf->geo ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_GEOMETRY_RECORD, + packet.geometryRecordIndex, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( packet.instanceRecordIndex < 0 + || packet.instanceRecordIndex >= packetFrame.NumInstanceRecords() + || packet.instanceRecord == NULL + || packet.instanceRecord + != &packetFrame.InstanceRecord( packet.instanceRecordIndex ) + || packet.instanceRecord->legacySpace != drawSurf->space ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_INSTANCE_RECORD, + packet.instanceRecordIndex, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + + const geometryResourceRecord_t &geometry = *packet.geometryRecord; + const instanceRecord_t &instance = *packet.instanceRecord; + bool admittedMaterialDeform = false; + if ( MaterialRequestsClassicDeform( drawSurf ) + && r_rendererSharedDeform.GetBool() ) { + classicDeformRole_t role; + classicDeformOutcome_t outcome; + std::uint64_t semanticHash = 0; + if ( !ValidateReceiverDeformContract( drawSurf, packet, role, + outcome, semanticHash ) + || role != CLASSIC_DEFORM_ROLE_FOG_RECEIVER + || outcome != CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE + || semanticHash == 0 ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( &packet ), -1, packetIndex, + lightOrdinal, receiverOrdinal ); + return false; + } + admittedMaterialDeform = true; + } + if ( ( geometry.deformMode != GEOMETRY_DEFORM_NONE + && !admittedMaterialDeform ) + || drawSurf->geo->deformedSurface ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DEFORM, + geometry.deformMode, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( geometry.skinningMode != GEOMETRY_SKINNING_NONE + && geometry.skinningMode != GEOMETRY_SKINNING_CPU ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SKINNING, + geometry.skinningMode, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( geometry.hasPrimBatchMesh ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SPECIAL_SURFACE, + geometry.hasPrimBatchMesh ? 1 : 0, -1, packetIndex, + lightOrdinal, receiverOrdinal ); + return false; + } + if ( instance.weaponDepthHack || instance.modelDepthHack != 0.0f ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DEPTH_HACK, + instance.weaponDepthHack ? 1 : 2, -1, packetIndex, + lightOrdinal, receiverOrdinal ); + return false; + } + if ( instance.negativeScale ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NEGATIVE_SCALE, + 1, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + if ( !packet.hasGeometry || !packet.hasAmbientCache + || geometry.vertexCount <= 0 || geometry.indexCount <= 0 + || geometry.indexCount % 3 != 0 + || packet.vertexCount != geometry.vertexCount + || packet.indexCount != geometry.indexCount + || ( !packet.hasIndexCache && !geometry.hasClientIndexData ) + || !FloatsAreFinite( instance.modelMatrix, 16 ) + || !FloatsAreFinite( instance.modelViewMatrix, 16 ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_PACKET, + geometry.indexCount, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + return true; +} + +static bool AppendSurface( const idScenePacketFrame &packetFrame, + const classicFogBlendDomainView_t &view, + classicFogBlendDomainLight_t &light, int absoluteLightIndex, + const viewLight_t *viewLight, const drawSurf_t *drawSurf, + int lightOrdinal, classicFogBlendDomainReceiver_t receiver, + int receiverOrdinal, int sourceOrdinal, int passPacketIndex, + int packetIndex, int lightArenaBase, + classicFogBlendBuildError_t &error ) { + const sceneFogBlendReceiverClass_t packetReceiver = + receiver == CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + ? SCENE_FOG_BLEND_RECEIVER_GLOBAL + : SCENE_FOG_BLEND_RECEIVER_LOCAL; + const drawPacket_t &packet = packetFrame.DrawPacket( packetIndex ); + if ( !ValidateDrawPacket( packetFrame, packet, packetIndex, view.viewDef, + viewLight, lightOrdinal, packetReceiver, receiverOrdinal, + sourceOrdinal, drawSurf, error ) ) { + error.passPacketIndex = passPacketIndex; + return false; + } + if ( domain.surfaceCount >= CLASSIC_FOG_BLEND_DOMAIN_MAX_SURFACES ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SURFACE_POOL_OVERFLOW, + domain.surfaceCount, passPacketIndex, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + classicFogBlendDomainSurface_t &surface = + domain.surfaces[ domain.surfaceCount++ ]; + InitSurface( surface ); + surface.legacyDrawSurf = drawSurf; + surface.legacyGeometry = drawSurf->geo; + surface.drawPacketIndex = packetIndex; + surface.lightIndex = absoluteLightIndex; + surface.sourceOrdinal = sourceOrdinal; + surface.receiverOrdinal = receiverOrdinal; + surface.receiver = receiver; + surface.geometryRecordIndex = packet.geometryRecordIndex; + surface.instanceRecordIndex = packet.instanceRecordIndex; + surface.vertexCount = packet.vertexCount; + surface.firstIndex = packet.firstIndex; + surface.indexCount = packet.indexCount; + surface.vertexOffset = packet.vertexOffset; + surface.scissorX1 = packet.scissorX1; + surface.scissorY1 = packet.scissorY1; + surface.scissorX2 = packet.scissorX2; + surface.scissorY2 = packet.scissorY2; + std::memcpy( surface.modelMatrix, packet.instanceRecord->modelMatrix, + sizeof( surface.modelMatrix ) ); + std::memcpy( surface.modelViewMatrix, + packet.instanceRecord->modelViewMatrix, + sizeof( surface.modelViewMatrix ) ); + surface.hasAmbientCache = packet.hasAmbientCache; + surface.hasIndexCache = packet.hasIndexCache; + if ( MaterialRequestsClassicDeform( drawSurf ) ) { + surface.deformRole = packet.classicDeformRecord->role; + surface.deformOutcome = packet.classicDeformRecord->outcome; + surface.deformContractHash = packet.classicDeformRecord->semanticHash; + if ( light.materialDeformReceiverCount == 0 ) { + light.deformContractHash = HASH_OFFSET; + } + light.materialDeformReceiverCount++; + HashU64( light.deformContractHash, surface.deformContractHash ); + } + surface.hash = HashSurface( surface, lightArenaBase ); + light.surfaceCount++; + light.receiverSurfaceCount[ receiver ]++; + return true; +} + +static void LocalizePlane( const float modelMatrix[ 16 ], + const float globalPlane[ 4 ], float localPlane[ 4 ] ) { + idPlane source; + idPlane destination; + for ( int component = 0; component < 4; ++component ) { + source[ component ] = globalPlane[ component ]; + } + R_GlobalPlaneToLocal( modelMatrix, source, destination ); + std::memcpy( localPlane, destination.ToFloatPtr(), sizeof( float ) * 4 ); +} + +static void CopySurfaceToPrimitive( + const classicFogBlendDomainSurface_t &surface, + classicFogBlendDomainPrimitive_t &primitive ) { + primitive.legacyDrawSurf = surface.legacyDrawSurf; + primitive.legacyGeometry = surface.legacyGeometry; + primitive.geometryRecordIndex = surface.geometryRecordIndex; + primitive.instanceRecordIndex = surface.instanceRecordIndex; + primitive.vertexCount = surface.vertexCount; + primitive.firstIndex = surface.firstIndex; + primitive.indexCount = surface.indexCount; + primitive.vertexOffset = surface.vertexOffset; + primitive.scissorX1 = surface.scissorX1; + primitive.scissorY1 = surface.scissorY1; + primitive.scissorX2 = surface.scissorX2; + primitive.scissorY2 = surface.scissorY2; + std::memcpy( primitive.modelMatrix, surface.modelMatrix, + sizeof( primitive.modelMatrix ) ); + std::memcpy( primitive.modelViewMatrix, surface.modelViewMatrix, + sizeof( primitive.modelViewMatrix ) ); +} + +static bool AppendPrimitive( classicFogBlendDomainLight_t &light, + classicFogBlendDomainLightStage_t &stage, + classicFogBlendDomainPrimitive_t &source, int lightArenaBase, + int stageArenaBase, int surfaceArenaBase, + classicFogBlendBuildError_t &error ) { + if ( domain.primitiveCount >= CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW, + domain.primitiveCount, -1, -1, light.sourceOrdinal, + source.receiver, stage.sourceStageIndex ); + return false; + } + source.hash = HashPrimitive( source, lightArenaBase, stageArenaBase, + surfaceArenaBase ); + domain.primitives[ domain.primitiveCount++ ] = source; + light.primitiveCount++; + stage.primitiveCount++; + if ( source.disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + light.drawablePrimitiveCount++; + stage.drawablePrimitiveCount++; + } else { + light.noopPrimitiveCount++; + stage.noopPrimitiveCount++; + } + return true; +} + +static void SealFogTexgen( const classicFogBlendDomainLight_t &light, + const float modelMatrix[ 16 ], float texgen[ 2 ][ 2 ][ 4 ] ) { + LocalizePlane( modelMatrix, light.fogGlobalTexgen[ 0 ][ 0 ], + texgen[ 0 ][ 0 ] ); + texgen[ 0 ][ 0 ][ 3 ] += 0.5f; + std::memset( texgen[ 0 ][ 1 ], 0, sizeof( texgen[ 0 ][ 1 ] ) ); + texgen[ 0 ][ 1 ][ 3 ] = 0.5f; + LocalizePlane( modelMatrix, light.fogGlobalTexgen[ 1 ][ 0 ], + texgen[ 1 ][ 0 ] ); + LocalizePlane( modelMatrix, light.fogGlobalTexgen[ 1 ][ 1 ], + texgen[ 1 ][ 1 ] ); + texgen[ 1 ][ 1 ][ 3 ] += FOG_ENTER; +} + +static bool AppendFogReceiverPrimitive( + classicFogBlendDomainLight_t &light, + classicFogBlendDomainLightStage_t &stage, int absoluteLightIndex, + int absoluteStageIndex, int absoluteSurfaceIndex, int lightArenaBase, + int stageArenaBase, int surfaceArenaBase, + classicFogBlendBuildError_t &error ) { + const classicFogBlendDomainSurface_t &surface = + domain.surfaces[ absoluteSurfaceIndex ]; + classicFogBlendDomainPrimitive_t primitive; + InitPrimitive( primitive ); + CopySurfaceToPrimitive( surface, primitive ); + primitive.lightIndex = absoluteLightIndex; + primitive.lightStageIndex = absoluteStageIndex; + primitive.surfaceIndex = absoluteSurfaceIndex; + primitive.kind = CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER; + primitive.receiver = surface.receiver; + primitive.disposition = CLASSIC_FOG_BLEND_PRIMITIVE_DRAW; + primitive.depth = stage.depth; + primitive.cull = stage.cull; + SealFogTexgen( light, primitive.modelMatrix, primitive.fogTexgen ); + if ( !AppendPrimitive( light, stage, primitive, lightArenaBase, + stageArenaBase, surfaceArenaBase, error ) ) { + return false; + } + light.fogReceiverPrimitiveCount++; + return true; +} + +static bool AppendBlendPrimitive( + classicFogBlendDomainLight_t &light, + classicFogBlendDomainLightStage_t &stage, int absoluteLightIndex, + int absoluteStageIndex, int absoluteSurfaceIndex, + classicFogBlendDomainPrimitiveDisposition_t disposition, + int lightArenaBase, int stageArenaBase, int surfaceArenaBase, + classicFogBlendBuildError_t &error ) { + const classicFogBlendDomainSurface_t &surface = + domain.surfaces[ absoluteSurfaceIndex ]; + classicFogBlendDomainPrimitive_t primitive; + InitPrimitive( primitive ); + CopySurfaceToPrimitive( surface, primitive ); + primitive.lightIndex = absoluteLightIndex; + primitive.lightStageIndex = absoluteStageIndex; + primitive.surfaceIndex = absoluteSurfaceIndex; + primitive.kind = CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER; + primitive.receiver = surface.receiver; + primitive.disposition = disposition; + primitive.depth = stage.depth; + primitive.cull = stage.cull; + for ( int plane = 0; plane < 4; ++plane ) { + LocalizePlane( primitive.modelMatrix, light.lightProject[ plane ], + primitive.localLightProject[ plane ] ); + } + if ( !AppendPrimitive( light, stage, primitive, lightArenaBase, + stageArenaBase, surfaceArenaBase, error ) ) { + return false; + } + if ( disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + light.blendPrimitiveCount++; + } + return true; +} + +static bool PrepareFogLight( const classicFogBlendDomainView_t &view, + classicFogBlendDomainLight_t &light, int absoluteLightIndex, + const viewLight_t &viewLight, int lightArenaBase, int stageArenaBase, + int surfaceArenaBase, classicFogBlendBuildError_t &error ) { + if ( viewLight.lightShader == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_SHADER, + 0, -1, -1, light.sourceOrdinal ); + return false; + } + const int registerCount = viewLight.lightShader->GetNumRegisters(); + if ( viewLight.shaderRegisters == NULL || registerCount <= 0 ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_SHADER_REGISTERS, + registerCount, -1, -1, light.sourceOrdinal ); + return false; + } + if ( viewLight.lightShader->GetNumStages() <= 0 + || viewLight.lightShader->GetStage( 0 ) == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_STAGE, + 0, -1, -1, light.sourceOrdinal, -1, 0 ); + return false; + } + const srfTriangles_t *frustum = viewLight.frustumTris; + if ( frustum == NULL || frustum->ambientCache == NULL + || frustum->numVerts <= 0 || frustum->numIndexes <= 0 + || frustum->numIndexes % 3 != 0 + || ( frustum->indexCache == NULL && frustum->indexes == NULL ) + || frustum->deformedSurface || R_TriHasPrimBatchMesh( frustum ) + || !FloatsAreFinite( frustum->bounds[ 0 ].ToFloatPtr(), 3 ) + || !FloatsAreFinite( frustum->bounds[ 1 ].ToFloatPtr(), 3 ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_FOG_FRUSTUM_GEOMETRY, + frustum != NULL ? frustum->numIndexes : 0, -1, -1, + light.sourceOrdinal ); + return false; + } + if ( globalImages == NULL || globalImages->fogImage == NULL + || globalImages->fogEnterImage == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_RESOURCE, + 0, -1, -1, light.sourceOrdinal, -1, 0 ); + return false; + } + if ( domain.lightStageCount + >= CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_LIGHT_STAGE_POOL_OVERFLOW, + domain.lightStageCount, -1, -1, light.sourceOrdinal, -1, 0 ); + return false; + } + + const int absoluteStageIndex = domain.lightStageCount++; + classicFogBlendDomainLightStage_t &stage = + domain.lightStages[ absoluteStageIndex ]; + InitLightStage( stage ); + stage.lightIndex = absoluteLightIndex; + stage.sourceStageIndex = 0; + stage.disposition = CLASSIC_FOG_BLEND_STAGE_DRAW; + stage.firstPrimitive = domain.primitiveCount; + stage.condition = 1.0f; + stage.conditionIgnored = true; // RB_FogPass never tests stage 0 condition. + InitBlend( stage.blend, RENDERER_BLEND_SRC_ALPHA, + RENDERER_BLEND_ONE_MINUS_SRC_ALPHA ); + stage.depth.testEnabled = true; + stage.depth.writeEnabled = false; + stage.depth.compareOperation = RENDERER_COMPARE_EQUAL; + stage.cull = RENDERER_CULL_FRONT; + stage.colorWriteMask = RENDERER_COLOR_WRITE_RGBA; + stage.alphaTestEnabled = false; + stage.alphaTestCompareOperation = RENDERER_COMPARE_ALWAYS; + const shaderStage_t &sourceStage = *viewLight.lightShader->GetStage( 0 ); + if ( !EvaluateStageColor( sourceStage, viewLight.shaderRegisters, + registerCount, stage.color, error, 0 ) + || !AddTexture( globalImages->fogImage, + stage.fogTextureResourceId, error, 0 ) + || !AddTexture( globalImages->fogEnterImage, + stage.fogEnterTextureResourceId, error, 0 ) ) { + error.lightOrdinal = light.sourceOrdinal; + return false; + } + light.fogTextureResourceId = stage.fogTextureResourceId; + light.fogEnterTextureResourceId = stage.fogEnterTextureResourceId; + for ( int component = 0; component < 3; ++component ) { + light.fogColor[ component ] = stage.color[ component ]; + } + light.fogColor[ 3 ] = 1.0f; // glColor3fv leaves fog draw alpha at one. + light.fogDensity = stage.color[ 3 ]; + light.fogDistanceScale = light.fogDensity <= 1.0f + ? -0.5f / DEFAULT_FOG_DISTANCE : -0.5f / light.fogDensity; + if ( !FloatIsFinite( light.fogDistanceScale ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + 3, -1, -1, light.sourceOrdinal, -1, 0 ); + return false; + } + const float *worldModelView = view.viewDef->worldSpace.modelViewMatrix; + light.fogGlobalTexgen[ 0 ][ 0 ][ 0 ] = + light.fogDistanceScale * worldModelView[ 2 ]; + light.fogGlobalTexgen[ 0 ][ 0 ][ 1 ] = + light.fogDistanceScale * worldModelView[ 6 ]; + light.fogGlobalTexgen[ 0 ][ 0 ][ 2 ] = + light.fogDistanceScale * worldModelView[ 10 ]; + light.fogGlobalTexgen[ 0 ][ 0 ][ 3 ] = + light.fogDistanceScale * worldModelView[ 14 ]; + light.fogGlobalTexgen[ 0 ][ 1 ][ 0 ] = + light.fogDistanceScale * worldModelView[ 0 ]; + light.fogGlobalTexgen[ 0 ][ 1 ][ 1 ] = + light.fogDistanceScale * worldModelView[ 4 ]; + light.fogGlobalTexgen[ 0 ][ 1 ][ 2 ] = + light.fogDistanceScale * worldModelView[ 8 ]; + light.fogGlobalTexgen[ 0 ][ 1 ][ 3 ] = + light.fogDistanceScale * worldModelView[ 12 ]; + for ( int component = 0; component < 4; ++component ) { + light.fogGlobalTexgen[ 1 ][ 1 ][ component ] = + 0.001f * light.fogPlane[ component ]; + } + const float viewerDistance = + view.viewDef->renderView.vieworg[ 0 ] + * light.fogGlobalTexgen[ 1 ][ 1 ][ 0 ] + + view.viewDef->renderView.vieworg[ 1 ] + * light.fogGlobalTexgen[ 1 ][ 1 ][ 1 ] + + view.viewDef->renderView.vieworg[ 2 ] + * light.fogGlobalTexgen[ 1 ][ 1 ][ 2 ] + + light.fogGlobalTexgen[ 1 ][ 1 ][ 3 ]; + std::memset( light.fogGlobalTexgen[ 1 ][ 0 ], 0, + sizeof( light.fogGlobalTexgen[ 1 ][ 0 ] ) ); + light.fogGlobalTexgen[ 1 ][ 0 ][ 3 ] = FOG_ENTER + viewerDistance; + if ( !FloatsAreFinite( &light.fogGlobalTexgen[ 0 ][ 0 ][ 0 ], 16 ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + 4, -1, -1, light.sourceOrdinal, -1, 0 ); + return false; + } + + for ( int localSurfaceIndex = 0; + localSurfaceIndex < light.surfaceCount; ++localSurfaceIndex ) { + if ( !AppendFogReceiverPrimitive( light, stage, absoluteLightIndex, + absoluteStageIndex, light.firstSurface + localSurfaceIndex, + lightArenaBase, stageArenaBase, surfaceArenaBase, error ) ) { + return false; + } + } + + classicFogBlendDomainPrimitive_t cap; + InitPrimitive( cap ); + cap.legacyGeometry = frustum; + cap.lightIndex = absoluteLightIndex; + cap.lightStageIndex = absoluteStageIndex; + cap.kind = CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP; + cap.receiver = CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM; + cap.disposition = CLASSIC_FOG_BLEND_PRIMITIVE_DRAW; + cap.vertexCount = frustum->numVerts; + cap.firstIndex = 0; + cap.indexCount = frustum->numIndexes; + cap.vertexOffset = 0; + cap.scissorX1 = view.scissorX1; + cap.scissorY1 = view.scissorY1; + cap.scissorX2 = view.scissorX2; + cap.scissorY2 = view.scissorY2; + cap.depth.testEnabled = true; + cap.depth.writeEnabled = false; + cap.depth.compareOperation = RENDERER_COMPARE_LESS_OR_EQUAL; + cap.cull = RENDERER_CULL_BACK; + std::memcpy( cap.modelMatrix, view.viewDef->worldSpace.modelMatrix, + sizeof( cap.modelMatrix ) ); + std::memcpy( cap.modelViewMatrix, view.viewDef->worldSpace.modelViewMatrix, + sizeof( cap.modelViewMatrix ) ); + SealFogTexgen( light, cap.modelMatrix, cap.fogTexgen ); + if ( !AppendPrimitive( light, stage, cap, lightArenaBase, + stageArenaBase, surfaceArenaBase, error ) ) { + return false; + } + light.fogFrustumPrimitiveCount++; + light.lightStageCount = 1; + light.activeLightStageCount = 1; + stage.hash = HashLightStage( stage, lightArenaBase ); + return true; +} + +static bool PrepareBlendLight( classicFogBlendDomainLight_t &light, + int absoluteLightIndex, const viewLight_t &viewLight, + bool skipBlendLights, + int lightArenaBase, int stageArenaBase, int surfaceArenaBase, + classicFogBlendBuildError_t &error ) { + // Preserve RB_BlendLight's historically observable early-return ordering: + // a missing GLOBAL head suppresses the LOCAL chain before skip/state checks. + if ( !light.globalChainPresent ) { + light.disposition = + CLASSIC_FOG_BLEND_LIGHT_NOOP_MISSING_GLOBAL_CHAIN; + return true; + } + if ( skipBlendLights ) { + light.disposition = CLASSIC_FOG_BLEND_LIGHT_NOOP_SKIP_BLEND; + return true; + } + if ( viewLight.lightShader == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_SHADER, + 0, -1, -1, light.sourceOrdinal ); + return false; + } + const int registerCount = viewLight.lightShader->GetNumRegisters(); + if ( viewLight.shaderRegisters == NULL || registerCount <= 0 ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_SHADER_REGISTERS, + registerCount, -1, -1, light.sourceOrdinal ); + return false; + } + if ( !AddTexture( viewLight.falloffImage, + light.falloffTextureResourceId, error, -1 ) ) { + error.lightOrdinal = light.sourceOrdinal; + return false; + } + const int sourceStageCount = viewLight.lightShader->GetNumStages(); + for ( int sourceStageIndex = 0; sourceStageIndex < sourceStageCount; + ++sourceStageIndex ) { + const shaderStage_t *sourceStage = + viewLight.lightShader->GetStage( sourceStageIndex ); + if ( sourceStage == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_STAGE, + sourceStageIndex, -1, -1, light.sourceOrdinal, -1, + sourceStageIndex ); + return false; + } + if ( domain.lightStageCount + >= CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES ) { + SetError( error, + CLASSIC_FOG_BLEND_FAILURE_LIGHT_STAGE_POOL_OVERFLOW, + domain.lightStageCount, -1, -1, light.sourceOrdinal, -1, + sourceStageIndex ); + return false; + } + const int absoluteStageIndex = domain.lightStageCount++; + classicFogBlendDomainLightStage_t &stage = + domain.lightStages[ absoluteStageIndex ]; + InitLightStage( stage ); + stage.lightIndex = absoluteLightIndex; + stage.sourceStageIndex = sourceStageIndex; + stage.firstPrimitive = domain.primitiveCount; + stage.falloffTextureResourceId = light.falloffTextureResourceId; + if ( !ReadRegister( viewLight.shaderRegisters, registerCount, + sourceStage->conditionRegister, stage.condition, error, + sourceStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + return false; + } + const bool active = stage.condition != 0.0f; + if ( !active ) { + stage.disposition = + CLASSIC_FOG_BLEND_STAGE_NOOP_INACTIVE_CONDITION; + light.inactiveLightStageCount++; + light.noopLightStageCount++; + for ( int localSurfaceIndex = 0; + localSurfaceIndex < light.surfaceCount; + ++localSurfaceIndex ) { + if ( !AppendBlendPrimitive( light, stage, absoluteLightIndex, + absoluteStageIndex, + light.firstSurface + localSurfaceIndex, + CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_INACTIVE_STAGE, + lightArenaBase, stageArenaBase, surfaceArenaBase, + error ) ) { + return false; + } + } + stage.hash = HashLightStage( stage, lightArenaBase ); + light.lightStageCount++; + continue; + } + if ( sourceStage->newStage != NULL + || sourceStage->texture.cinematic != NULL + || sourceStage->texture.dynamic != DI_STATIC ) { + SetError( error, + sourceStage->newStage != NULL + ? CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_MATERIAL + : CLASSIC_FOG_BLEND_FAILURE_DYNAMIC_RESOURCE, + sourceStageIndex, -1, -1, light.sourceOrdinal, -1, + sourceStageIndex ); + return false; + } + stage.disposition = CLASSIC_FOG_BLEND_STAGE_DRAW; + stage.hasTextureMatrix = sourceStage->texture.hasMatrix; + if ( !EvaluateStageColor( *sourceStage, viewLight.shaderRegisters, + registerCount, stage.color, error, sourceStageIndex ) + || !EvaluateTextureMatrix( sourceStage->texture, + viewLight.shaderRegisters, registerCount, + stage.textureMatrix, error, sourceStageIndex ) + || !SealBlendRenderState( sourceStage->drawStateBits, + stage, error, sourceStageIndex ) + || !AddTexture( sourceStage->texture.image, + stage.projectionTextureResourceId, error, + sourceStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + return false; + } + for ( int localSurfaceIndex = 0; + localSurfaceIndex < light.surfaceCount; ++localSurfaceIndex ) { + if ( !AppendBlendPrimitive( light, stage, absoluteLightIndex, + absoluteStageIndex, + light.firstSurface + localSurfaceIndex, + CLASSIC_FOG_BLEND_PRIMITIVE_DRAW, lightArenaBase, + stageArenaBase, surfaceArenaBase, error ) ) { + return false; + } + } + stage.hash = HashLightStage( stage, lightArenaBase ); + light.lightStageCount++; + light.activeLightStageCount++; + } + return true; +} + +static bool PrepareView( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene, classicFogBlendDomainView_t &view ) { + const classicFogBlendCheckpoint_t checkpoint = { + domain.lightCount, domain.surfaceCount, domain.lightStageCount, + domain.primitiveCount, domain.textureCount + }; + classicFogBlendBuildError_t error; + InitError( error ); + const scenePacketFrameStats_t &packetStats = packetFrame.Stats(); + if ( packetStats.overflow ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SCENE_PACKET_OVERFLOW, + packetStats.overflowCause ); + return FailView( view, checkpoint, error ); + } + + const viewDef_t *viewDef = view.viewDef; + const int allowedRenderFlags = RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef == NULL || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || viewDef->viewEntitys == NULL || viewDef->renderWorld == NULL + || viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 + || viewDef->renderView.globalMaterial != NULL + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 + || !FloatsAreFinite( viewDef->projectionMatrix, 16 ) + || !FloatsAreFinite( viewDef->worldSpace.modelMatrix, 16 ) + || !FloatsAreFinite( viewDef->worldSpace.modelViewMatrix, 16 ) + || !FloatsAreFinite( viewDef->renderView.vieworg.ToFloatPtr(), 3 ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_VIEW, + viewDef != NULL ? viewDef->renderFlags : 0 ); + return FailView( view, checkpoint, error ); + } + // The classic outer early-out suppresses both fog and blend. Keep that + // exact behavior on the untouched fallback rather than preparing either + // light class independently. + if ( r_skipFogLights.GetBool() ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_STATE, 1 ); + return FailView( view, checkpoint, error ); + } + if ( r_showOverDraw.GetInteger() != 0 || r_singleTriangle.GetBool() ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_STATE, + r_showOverDraw.GetInteger() != 0 + ? 100 + r_showOverDraw.GetInteger() : 200 ); + return FailView( view, checkpoint, error ); + } + if ( viewDef->worldSpace.weaponDepthHack + || viewDef->worldSpace.modelDepthHack != 0.0f ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_DEPTH_HACK, 3 ); + return FailView( view, checkpoint, error ); + } + view.useScissor = r_useScissor.GetBool(); + view.skipBlendLights = r_skipBlendLights.GetBool(); + + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) + || !RangeFits( scene.firstDrawPacket, scene.drawPacketCount, + packetFrame.NumDrawPackets() ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_INVALID_SCENE_RANGE, + scene.passPacketCount ); + return FailView( view, checkpoint, error ); + } + const passPacket_t *fogBlendPass = NULL; + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + const int passPacketIndex = scene.firstPassPacket + localPass; + const passPacket_t &pass = packetFrame.Pass( passPacketIndex ); + if ( !RangeFits( pass.firstDrawPacket, pass.drawPacketCount, + packetFrame.NumDrawPackets() ) + || pass.firstDrawPacket < scene.firstDrawPacket + || pass.firstDrawPacket + > scene.firstDrawPacket + scene.drawPacketCount + || pass.drawPacketCount > scene.firstDrawPacket + + scene.drawPacketCount - pass.firstDrawPacket ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_RANGE, + pass.drawPacketCount, passPacketIndex ); + return FailView( view, checkpoint, error ); + } + if ( pass.passCategory != RENDER_PASS_FOG_BLEND ) { + continue; + } + if ( fogBlendPass != NULL || !pass.enabled || pass.commandOnly + || ( pass.packetCategory != SCENE_PACKET_CATEGORY_WORLD + && !( pass.drawPacketCount == 0 + && pass.packetCategory + == SCENE_PACKET_CATEGORY_COMMAND ) ) ) { + SetError( error, + CLASSIC_FOG_BLEND_FAILURE_INVALID_FOG_BLEND_PASS, + pass.passCategory, passPacketIndex ); + return FailView( view, checkpoint, error ); + } + fogBlendPass = &pass; + view.fogBlendPassPacketIndex = passPacketIndex; + } + if ( fogBlendPass == NULL ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_INVALID_FOG_BLEND_PASS, + -1 ); + return FailView( view, checkpoint, error ); + } + view.packetDrawCount = fogBlendPass->drawPacketCount; + const int packetEnd = fogBlendPass->firstDrawPacket + + fogBlendPass->drawPacketCount; + int packetCursor = fogBlendPass->firstDrawPacket; + int expectedSourceOrdinal = 0; + int sourceLightOrdinal = 0; + for ( const viewLight_t *viewLight = viewDef->viewLights; + viewLight != NULL; + viewLight = viewLight->next, ++sourceLightOrdinal ) { + if ( !ViewLightIsFogOrBlend( viewLight ) ) { + continue; + } + if ( domain.lightCount >= CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHTS ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_LIGHT_POOL_OVERFLOW, + domain.lightCount, view.fogBlendPassPacketIndex, + packetCursor, sourceLightOrdinal ); + return FailView( view, checkpoint, error ); + } + const int absoluteLightIndex = domain.lightCount++; + classicFogBlendDomainLight_t &light = + domain.lights[ absoluteLightIndex ]; + InitLight( light ); + light.sourceOrdinal = sourceLightOrdinal; + light.kind = viewLight->lightShader->IsFogLight() + ? CLASSIC_FOG_BLEND_LIGHT_FOG : CLASSIC_FOG_BLEND_LIGHT_BLEND; + light.firstSurface = domain.surfaceCount; + light.firstLightStage = domain.lightStageCount; + light.firstPrimitive = domain.primitiveCount; + light.scissorX1 = viewLight->scissorRect.x1; + light.scissorY1 = viewLight->scissorRect.y1; + light.scissorX2 = viewLight->scissorRect.x2; + light.scissorY2 = viewLight->scissorRect.y2; + light.globalChainPresent = viewLight->globalInteractions != NULL; + const bool noopBlend = + light.kind == CLASSIC_FOG_BLEND_LIGHT_BLEND + && ( !light.globalChainPresent || view.skipBlendLights ); + if ( light.kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + if ( !FloatsAreFinite( viewLight->fogPlane.ToFloatPtr(), 4 ) ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + 0, view.fogBlendPassPacketIndex, packetCursor, + sourceLightOrdinal ); + return FailView( view, checkpoint, error ); + } + std::memcpy( light.fogPlane, viewLight->fogPlane.ToFloatPtr(), + sizeof( light.fogPlane ) ); + } else if ( !noopBlend ) { + for ( int plane = 0; plane < 4; ++plane ) { + if ( !FloatsAreFinite( + viewLight->lightProject[ plane ].ToFloatPtr(), 4 ) ) { + SetError( error, + CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + plane, view.fogBlendPassPacketIndex, packetCursor, + sourceLightOrdinal ); + return FailView( view, checkpoint, error ); + } + std::memcpy( light.lightProject[ plane ], + viewLight->lightProject[ plane ].ToFloatPtr(), + sizeof( light.lightProject[ plane ] ) ); + } + } + + const drawSurf_t *chains[ 2 ] = { + viewLight->globalInteractions, viewLight->localInteractions + }; + const classicFogBlendDomainReceiver_t receivers[ 2 ] = { + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL, + CLASSIC_FOG_BLEND_RECEIVER_LOCAL + }; + for ( int chainIndex = 0; chainIndex < 2; ++chainIndex ) { + int receiverOrdinal = 0; + for ( const drawSurf_t *drawSurf = chains[ chainIndex ]; + drawSurf != NULL; + drawSurf = drawSurf->nextOnLight, ++receiverOrdinal ) { + if ( packetCursor < fogBlendPass->firstDrawPacket + || packetCursor >= packetEnd ) { + SetError( error, + CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH, + expectedSourceOrdinal, + view.fogBlendPassPacketIndex, packetCursor, + sourceLightOrdinal, receiverOrdinal ); + return FailView( view, checkpoint, error ); + } + const sceneFogBlendReceiverClass_t packetReceiver = + receivers[ chainIndex ] == + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + ? SCENE_FOG_BLEND_RECEIVER_GLOBAL + : SCENE_FOG_BLEND_RECEIVER_LOCAL; + if ( noopBlend ) { + const drawPacket_t &packet = + packetFrame.DrawPacket( packetCursor ); + if ( !ValidateDrawPacketIdentity( packet, packetCursor, + view.viewDef, viewLight, sourceLightOrdinal, + packetReceiver, receiverOrdinal, + expectedSourceOrdinal, drawSurf, error ) ) { + error.passPacketIndex = view.fogBlendPassPacketIndex; + return FailView( view, checkpoint, error ); + } + if ( MaterialRequestsClassicDeform( drawSurf ) ) { + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformHash = 0; + if ( !ValidateReceiverDeformContract( drawSurf, packet, + deformRole, deformOutcome, deformHash ) ) { + SetError( error, + CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( &packet ), + view.fogBlendPassPacketIndex, packetCursor, + sourceLightOrdinal, receiverOrdinal ); + return FailView( view, checkpoint, error ); + } + if ( deformHash != 0 + && deformRole + == CLASSIC_DEFORM_ROLE_FOG_RECEIVER + && deformOutcome + == CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE ) { + if ( light.materialDeformReceiverCount == 0 ) { + light.deformContractHash = HASH_OFFSET; + } + light.materialDeformReceiverCount++; + HashU64( light.deformContractHash, deformHash ); + } + } + } else if ( !AppendSurface( packetFrame, view, light, + absoluteLightIndex, viewLight, drawSurf, + sourceLightOrdinal, receivers[ chainIndex ], + receiverOrdinal, expectedSourceOrdinal, + view.fogBlendPassPacketIndex, packetCursor, + checkpoint.lightCount, error ) ) { + return FailView( view, checkpoint, error ); + } + packetCursor++; + expectedSourceOrdinal++; + } + } + + const bool prepared = light.kind == CLASSIC_FOG_BLEND_LIGHT_FOG + ? PrepareFogLight( view, light, absoluteLightIndex, *viewLight, + checkpoint.lightCount, checkpoint.lightStageCount, + checkpoint.surfaceCount, error ) + : PrepareBlendLight( light, absoluteLightIndex, *viewLight, + view.skipBlendLights, + checkpoint.lightCount, checkpoint.lightStageCount, + checkpoint.surfaceCount, error ); + if ( !prepared ) { + error.passPacketIndex = view.fogBlendPassPacketIndex; + return FailView( view, checkpoint, error ); + } + if ( light.kind == CLASSIC_FOG_BLEND_LIGHT_BLEND + && light.disposition != CLASSIC_FOG_BLEND_LIGHT_DRAW ) { + view.noopLightCount++; + } + light.hash = HashLight( light, checkpoint.lightCount, + checkpoint.lightStageCount, checkpoint.surfaceCount ); + } + if ( packetCursor != packetEnd ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor, view.fogBlendPassPacketIndex, + packetCursor ); + return FailView( view, checkpoint, error ); + } + + view.firstLight = checkpoint.lightCount; + view.lightCount = domain.lightCount - checkpoint.lightCount; + view.firstSurface = checkpoint.surfaceCount; + view.surfaceCount = domain.surfaceCount - checkpoint.surfaceCount; + view.firstLightStage = checkpoint.lightStageCount; + view.lightStageCount = domain.lightStageCount - checkpoint.lightStageCount; + view.firstPrimitive = checkpoint.primitiveCount; + view.primitiveCount = domain.primitiveCount - checkpoint.primitiveCount; + for ( int lightIndex = 0; lightIndex < view.lightCount; ++lightIndex ) { + const classicFogBlendDomainLight_t &light = + domain.lights[ view.firstLight + lightIndex ]; + if ( light.kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + view.fogLightCount++; + } else { + view.blendLightCount++; + } + view.activeLightStageCount += light.activeLightStageCount; + view.inactiveLightStageCount += light.inactiveLightStageCount; + view.noopLightStageCount += light.noopLightStageCount; + view.drawablePrimitiveCount += light.drawablePrimitiveCount; + view.noopPrimitiveCount += light.noopPrimitiveCount; + view.fogReceiverPrimitiveCount += light.fogReceiverPrimitiveCount; + view.fogFrustumPrimitiveCount += light.fogFrustumPrimitiveCount; + view.blendPrimitiveCount += light.blendPrimitiveCount; + view.materialDeformReceiverCount += + light.materialDeformReceiverCount; + for ( int receiver = 0; receiver < CLASSIC_FOG_BLEND_RECEIVER_COUNT; + ++receiver ) { + view.receiverSurfaceCount[ receiver ] + += light.receiverSurfaceCount[ receiver ]; + } + } + if ( view.drawablePrimitiveCount + view.noopPrimitiveCount + != view.primitiveCount + || view.fogReceiverPrimitiveCount + + view.fogFrustumPrimitiveCount + view.blendPrimitiveCount + != view.drawablePrimitiveCount ) { + SetError( error, CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH, + view.primitiveCount, view.fogBlendPassPacketIndex ); + return FailView( view, checkpoint, error ); + } + view.failure = CLASSIC_FOG_BLEND_FAILURE_NONE; + view.hash = HashView( view ); + view.ready = true; + domain.stats.readyViews++; + domain.stats.lights += view.lightCount; + domain.stats.fogLights += view.fogLightCount; + domain.stats.blendLights += view.blendLightCount; + domain.stats.noopLights += view.noopLightCount; + domain.stats.surfaces += view.surfaceCount; + domain.stats.lightStages += view.lightStageCount; + domain.stats.activeLightStages += view.activeLightStageCount; + domain.stats.inactiveLightStages += view.inactiveLightStageCount; + domain.stats.noopLightStages += view.noopLightStageCount; + domain.stats.primitives += view.primitiveCount; + domain.stats.drawablePrimitives += view.drawablePrimitiveCount; + domain.stats.noopPrimitives += view.noopPrimitiveCount; + domain.stats.fogReceiverPrimitives += view.fogReceiverPrimitiveCount; + domain.stats.fogFrustumPrimitives += view.fogFrustumPrimitiveCount; + domain.stats.blendPrimitives += view.blendPrimitiveCount; + domain.stats.materialDeformReceivers += + view.materialDeformReceiverCount; + for ( int receiver = 0; receiver < CLASSIC_FOG_BLEND_RECEIVER_COUNT; + ++receiver ) { + domain.stats.receiverSurfaces[ receiver ] + += view.receiverSurfaceCount[ receiver ]; + } + return true; +} + +static void RecordFallback( classicFogBlendDomainView_t *view, + classicFogBlendDomainBackend_t backend, + classicFogBlendDomainFailure_t failure, int detail, + int fogReceiverPrimitives, int fogFrustumPrimitives, + int blendPrimitives, int noopPrimitives, int noopLightStages, + int noopLights ) { + classicFogBlendDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view == NULL ) { + coverage.untrackedFallbacks++; + return; + } + const int viewIndex = ViewIndex( view ); + if ( viewIndex < 0 || view->backendOutcome[ backend ] + != CLASSIC_FOG_BLEND_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + view->backendOutcome[ backend ] = CLASSIC_FOG_BLEND_BACKEND_FALLBACK; + view->backendFailure[ backend ] = failure == CLASSIC_FOG_BLEND_FAILURE_NONE + ? CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED : failure; + view->backendFailureDetail[ backend ] = detail; + view->backendFogReceiverPrimitives[ backend ] = fogReceiverPrimitives; + view->backendFogFrustumPrimitives[ backend ] = fogFrustumPrimitives; + view->backendBlendPrimitives[ backend ] = blendPrimitives; + view->backendNoopPrimitives[ backend ] = noopPrimitives; + view->backendNoopLightStages[ backend ] = noopLightStages; + view->backendNoopLights[ backend ] = noopLights; + coverage.fallbackViewMask |= 1ull << viewIndex; + coverage.fallbackViews++; + coverage.fallbackLights += view->lightCount; + coverage.fallbackSurfaces += view->surfaceCount; + coverage.fallbackFogReceiverPrimitives + += view->fogReceiverPrimitiveCount; + coverage.fallbackFogFrustumPrimitives + += view->fogFrustumPrimitiveCount; + coverage.fallbackBlendPrimitives += view->blendPrimitiveCount; + coverage.fallbackNoopPrimitives += view->noopPrimitiveCount; + coverage.fallbackNoopLightStages += view->noopLightStageCount; + coverage.fallbackNoopLights += view->noopLightCount; + if ( view->backendFailure[ backend ] + == CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH ) { + coverage.coverageMismatches++; + } + if ( view->backendFailure[ backend ] >= CLASSIC_FOG_BLEND_FAILURE_NONE + && view->backendFailure[ backend ] + < CLASSIC_FOG_BLEND_FAILURE_COUNT ) { + domain.stats.failureCounts[ view->backendFailure[ backend ] ]++; + } +} + +} // namespace + +void R_ClassicFogBlendDomain_ResetFrame( void ) { + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.viewCount = 0; + domain.lightCount = 0; + domain.surfaceCount = 0; + domain.lightStageCount = 0; + domain.primitiveCount = 0; + domain.textureCount = 0; + domain.generation++; + if ( domain.generation == 0 ) { + domain.generation = 1; + } + idStr::Copynz( domain.stats.status, "empty", + sizeof( domain.stats.status ) ); +} + +void R_ClassicFogBlendDomain_PrepareFrame( + const idScenePacketFrame &packetFrame ) { + R_ClassicFogBlendDomain_ResetFrame(); + domain.stats.prepared = true; + domain.stats.sourceScenes = packetFrame.NumScenes(); + domain.stats.overflow = packetFrame.Stats().overflow; + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); + ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !SceneIsFogBlendCandidate( packetFrame, scene ) ) { + continue; + } + if ( FindMutableView( scene.viewDef ) != NULL ) { + continue; + } + domain.stats.fogBlendViews++; + if ( domain.viewCount >= CLASSIC_FOG_BLEND_DOMAIN_MAX_VIEWS ) { + domain.stats.overflow = true; + domain.stats.fallbackViews++; + domain.stats.failureCounts[ + CLASSIC_FOG_BLEND_FAILURE_VIEW_POOL_OVERFLOW ]++; + continue; + } + classicFogBlendDomainView_t &view = + domain.views[ domain.viewCount++ ]; + InitView( view, scene.viewDef, sceneIndex ); + PrepareView( packetFrame, scene, view ); + } + domain.stats.textures = domain.textureCount; + domain.stats.frameValid = !domain.stats.overflow + && domain.stats.fallbackViews == 0; + std::uint64_t frameHash = HASH_OFFSET; + HashInt( frameHash, domain.stats.sourceScenes ); + HashInt( frameHash, domain.stats.fogBlendViews ); + for ( int i = 0; i < domain.viewCount; ++i ) { + HashInt( frameHash, domain.views[ i ].scenePacketIndex ); + HashBool( frameHash, domain.views[ i ].ready ); + HashInt( frameHash, domain.views[ i ].failure ); + HashU64( frameHash, domain.views[ i ].hash ); + } + domain.stats.hash = frameHash; + if ( domain.stats.fogBlendViews == 0 ) { + idStr::Copynz( domain.stats.status, "empty", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.frameValid ) { + idStr::Copynz( domain.stats.status, "ready", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.readyViews == 0 ) { + idStr::Copynz( domain.stats.status, "fallback", + sizeof( domain.stats.status ) ); + } else { + idStr::Copynz( domain.stats.status, "mixed-view-fallback", + sizeof( domain.stats.status ) ); + } +} + +const classicFogBlendDomainStats_t &R_ClassicFogBlendDomain_Stats( void ) { + return domain.stats; +} + +int R_ClassicFogBlendDomain_NumViews( void ) { + return domain.viewCount; +} + +const classicFogBlendDomainView_t *R_ClassicFogBlendDomain_ViewByIndex( + int index ) { + return index >= 0 && index < domain.viewCount + ? &domain.views[ index ] : NULL; +} + +const classicFogBlendDomainView_t * + R_ClassicFogBlendDomain_ViewForScenePacket( int scenePacketIndex ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].scenePacketIndex == scenePacketIndex ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +const classicFogBlendDomainView_t *R_ClassicFogBlendDomain_FindView( + const viewDef_t *viewDef ) { + return FindMutableView( viewDef ); +} + +const classicFogBlendDomainLight_t *R_ClassicFogBlendDomain_ViewLight( + const classicFogBlendDomainView_t &view, int lightIndex ) { + if ( !view.ready || lightIndex < 0 || lightIndex >= view.lightCount + || !RangeFits( view.firstLight, view.lightCount, + domain.lightCount ) ) { + return NULL; + } + return &domain.lights[ view.firstLight + lightIndex ]; +} + +const classicFogBlendDomainLightStage_t *R_ClassicFogBlendDomain_LightStage( + const classicFogBlendDomainLight_t &light, int stageIndex ) { + if ( LightIndex( &light ) < 0 || stageIndex < 0 + || stageIndex >= light.lightStageCount + || !RangeFits( light.firstLightStage, light.lightStageCount, + domain.lightStageCount ) ) { + return NULL; + } + return &domain.lightStages[ light.firstLightStage + stageIndex ]; +} + +const classicFogBlendDomainPrimitive_t * + R_ClassicFogBlendDomain_ViewPrimitive( + const classicFogBlendDomainView_t &view, int primitiveIndex ) { + if ( !view.ready || primitiveIndex < 0 + || primitiveIndex >= view.primitiveCount + || !RangeFits( view.firstPrimitive, view.primitiveCount, + domain.primitiveCount ) ) { + return NULL; + } + return &domain.primitives[ view.firstPrimitive + primitiveIndex ]; +} + +const classicFogBlendDomainTexture_t *R_ClassicFogBlendDomain_ResolveTexture( + std::uint64_t textureResourceId ) { + const int textureIndex = TextureIndexFromResourceId( textureResourceId ); + if ( textureIndex < 0 || textureIndex >= domain.textureCount ) { + return NULL; + } + const classicFogBlendDomainTexture_t &texture = + domain.textures[ textureIndex ]; + if ( texture.textureResourceId != textureResourceId + || texture.image == NULL || !texture.loaded || texture.defaulted + || texture.mutableImage + || texture.image->GetStorageGeneration() + != texture.storageGeneration ) { + return NULL; + } + return &texture; +} + +bool R_ClassicFogBlendDomain_RecordOwned( const viewDef_t *viewDef, + classicFogBlendDomainBackend_t backend, int fogReceiverPrimitives, + int fogFrustumPrimitives, int blendPrimitives, int noopPrimitives, + int noopLightStages, int noopLights ) { + if ( backend < CLASSIC_FOG_BLEND_BACKEND_GL + || backend >= CLASSIC_FOG_BLEND_BACKEND_COUNT ) { + return false; + } + classicFogBlendDomainView_t *view = FindMutableView( viewDef ); + if ( view == NULL ) { + return false; + } + classicFogBlendDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] + != CLASSIC_FOG_BLEND_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return false; + } + if ( !view->ready ) { + RecordFallback( view, backend, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, view->failure, + fogReceiverPrimitives, fogFrustumPrimitives, blendPrimitives, + noopPrimitives, noopLightStages, noopLights ); + return false; + } + if ( fogReceiverPrimitives != view->fogReceiverPrimitiveCount + || fogFrustumPrimitives != view->fogFrustumPrimitiveCount + || blendPrimitives != view->blendPrimitiveCount + || noopPrimitives != view->noopPrimitiveCount + || noopLightStages != view->noopLightStageCount + || noopLights != view->noopLightCount ) { + const int detail = ( fogReceiverPrimitives + - view->fogReceiverPrimitiveCount ) * 100000 + + ( fogFrustumPrimitives + - view->fogFrustumPrimitiveCount ) * 10000 + + ( blendPrimitives - view->blendPrimitiveCount ) * 1000 + + ( noopPrimitives - view->noopPrimitiveCount ) * 100 + + ( noopLightStages - view->noopLightStageCount ) * 10 + + ( noopLights - view->noopLightCount ); + RecordFallback( view, backend, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, detail, + fogReceiverPrimitives, fogFrustumPrimitives, blendPrimitives, + noopPrimitives, noopLightStages, noopLights ); + return false; + } + const int viewIndex = ViewIndex( view ); + view->backendOutcome[ backend ] = CLASSIC_FOG_BLEND_BACKEND_OWNED; + view->backendFailure[ backend ] = CLASSIC_FOG_BLEND_FAILURE_NONE; + view->backendFailureDetail[ backend ] = 0; + view->backendFogReceiverPrimitives[ backend ] = fogReceiverPrimitives; + view->backendFogFrustumPrimitives[ backend ] = fogFrustumPrimitives; + view->backendBlendPrimitives[ backend ] = blendPrimitives; + view->backendNoopPrimitives[ backend ] = noopPrimitives; + view->backendNoopLightStages[ backend ] = noopLightStages; + view->backendNoopLights[ backend ] = noopLights; + coverage.ownedViewMask |= 1ull << viewIndex; + coverage.ownedViews++; + coverage.ownedLights += view->lightCount; + coverage.ownedSurfaces += view->surfaceCount; + coverage.ownedFogReceiverPrimitives += fogReceiverPrimitives; + coverage.ownedFogFrustumPrimitives += fogFrustumPrimitives; + coverage.ownedBlendPrimitives += blendPrimitives; + coverage.ownedNoopPrimitives += noopPrimitives; + coverage.ownedNoopLightStages += noopLightStages; + coverage.ownedNoopLights += noopLights; + return true; +} + +void R_ClassicFogBlendDomain_RecordBackendFallback( + const viewDef_t *viewDef, classicFogBlendDomainBackend_t backend, + classicFogBlendDomainFailure_t failure, int detail ) { + if ( backend < CLASSIC_FOG_BLEND_BACKEND_GL + || backend >= CLASSIC_FOG_BLEND_BACKEND_COUNT ) { + return; + } + RecordFallback( FindMutableView( viewDef ), backend, failure, detail, + 0, 0, 0, 0, 0, 0 ); +} + +const classicFogBlendDomainBackendCoverage_t & + R_ClassicFogBlendDomain_BackendCoverage( + classicFogBlendDomainBackend_t backend ) { + static const classicFogBlendDomainBackendCoverage_t empty = {}; + return backend >= CLASSIC_FOG_BLEND_BACKEND_GL + && backend < CLASSIC_FOG_BLEND_BACKEND_COUNT + ? domain.stats.backend[ backend ] : empty; +} + +const char *ClassicFogBlendDomainLightKind_Name( + classicFogBlendDomainLightKind_t kind ) { + switch ( kind ) { + case CLASSIC_FOG_BLEND_LIGHT_FOG: return "fog"; + case CLASSIC_FOG_BLEND_LIGHT_BLEND: return "blend"; + case CLASSIC_FOG_BLEND_LIGHT_KIND_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainReceiver_Name( + classicFogBlendDomainReceiver_t receiver ) { + switch ( receiver ) { + case CLASSIC_FOG_BLEND_RECEIVER_GLOBAL: return "global"; + case CLASSIC_FOG_BLEND_RECEIVER_LOCAL: return "local"; + case CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM: return "frustum"; + case CLASSIC_FOG_BLEND_RECEIVER_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainLightDisposition_Name( + classicFogBlendDomainLightDisposition_t disposition ) { + switch ( disposition ) { + case CLASSIC_FOG_BLEND_LIGHT_DRAW: return "draw"; + case CLASSIC_FOG_BLEND_LIGHT_NOOP_SKIP_BLEND: return "skip-blend"; + case CLASSIC_FOG_BLEND_LIGHT_NOOP_MISSING_GLOBAL_CHAIN: + return "missing-global-chain"; + case CLASSIC_FOG_BLEND_LIGHT_DISPOSITION_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainLightStageDisposition_Name( + classicFogBlendDomainLightStageDisposition_t disposition ) { + switch ( disposition ) { + case CLASSIC_FOG_BLEND_STAGE_DRAW: return "draw"; + case CLASSIC_FOG_BLEND_STAGE_NOOP_INACTIVE_CONDITION: + return "inactive-condition"; + case CLASSIC_FOG_BLEND_STAGE_NOOP_SKIP_BLEND: return "skip-blend"; + case CLASSIC_FOG_BLEND_STAGE_NOOP_MISSING_GLOBAL_CHAIN: + return "missing-global-chain"; + case CLASSIC_FOG_BLEND_STAGE_DISPOSITION_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainPrimitiveKind_Name( + classicFogBlendDomainPrimitiveKind_t kind ) { + switch ( kind ) { + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER: return "fog-receiver"; + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP: return "fog-cap"; + case CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER: return "blend-receiver"; + case CLASSIC_FOG_BLEND_PRIMITIVE_KIND_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainPrimitiveDisposition_Name( + classicFogBlendDomainPrimitiveDisposition_t disposition ) { + switch ( disposition ) { + case CLASSIC_FOG_BLEND_PRIMITIVE_DRAW: return "draw"; + case CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_INACTIVE_STAGE: + return "inactive-stage"; + case CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_SKIP_BLEND: return "skip-blend"; + case CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_MISSING_GLOBAL_CHAIN: + return "missing-global-chain"; + case CLASSIC_FOG_BLEND_PRIMITIVE_DISPOSITION_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainFailure_Name( + classicFogBlendDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_FOG_BLEND_FAILURE_NONE: return "none"; + case CLASSIC_FOG_BLEND_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_FOG_BLEND_FAILURE_SCENE_PACKET_OVERFLOW: return "scene-packet-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_VIEW_POOL_OVERFLOW: return "view-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_LIGHT_POOL_OVERFLOW: return "light-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_SURFACE_POOL_OVERFLOW: return "surface-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_LIGHT_STAGE_POOL_OVERFLOW: return "light-stage-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW: return "primitive-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_TEXTURE_POOL_OVERFLOW: return "texture-pool-overflow"; + case CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_VIEW: return "unsupported-view"; + case CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_STATE: return "unsupported-state"; + case CLASSIC_FOG_BLEND_FAILURE_INVALID_SCENE_RANGE: return "invalid-scene-range"; + case CLASSIC_FOG_BLEND_FAILURE_INVALID_FOG_BLEND_PASS: return "invalid-fog-blend-pass"; + case CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_RANGE: return "invalid-draw-range"; + case CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH: return "source-packet-mismatch"; + case CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_PACKET: return "invalid-draw-packet"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_GEOMETRY_RECORD: return "missing-geometry-record"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_INSTANCE_RECORD: return "missing-instance-record"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_SHADER: return "missing-light-shader"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_SHADER_REGISTERS: return "missing-shader-registers"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_STAGE: return "missing-light-stage"; + case CLASSIC_FOG_BLEND_FAILURE_REGISTER_OUT_OF_RANGE: return "register-out-of-range"; + case CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE: return "nonfinite-value"; + case CLASSIC_FOG_BLEND_FAILURE_FOG_FRUSTUM_GEOMETRY: return "fog-frustum-geometry"; + case CLASSIC_FOG_BLEND_FAILURE_DEFORM: return "deform"; + case CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT: return "deform-contract"; + case CLASSIC_FOG_BLEND_FAILURE_SKINNING: return "skinning"; + case CLASSIC_FOG_BLEND_FAILURE_SPECIAL_SURFACE: return "special-surface"; + case CLASSIC_FOG_BLEND_FAILURE_DEPTH_HACK: return "depth-hack"; + case CLASSIC_FOG_BLEND_FAILURE_NEGATIVE_SCALE: return "negative-scale"; + case CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_MATERIAL: return "unsupported-material"; + case CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_RENDER_STATE: return "unsupported-render-state"; + case CLASSIC_FOG_BLEND_FAILURE_DYNAMIC_RESOURCE: return "dynamic-resource"; + case CLASSIC_FOG_BLEND_FAILURE_MISSING_RESOURCE: return "missing-resource"; + case CLASSIC_FOG_BLEND_FAILURE_DEFAULTED_RESOURCE: return "defaulted-resource"; + case CLASSIC_FOG_BLEND_FAILURE_UNLOADED_RESOURCE: return "unloaded-resource"; + case CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY: return "backend-not-ready"; + case CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH: return "backend-coverage-mismatch"; + case CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED: return "backend-rejected"; + case CLASSIC_FOG_BLEND_FAILURE_COUNT: break; + } + return "unknown"; +} + +const char *ClassicFogBlendDomainBackend_Name( + classicFogBlendDomainBackend_t backend ) { + switch ( backend ) { + case CLASSIC_FOG_BLEND_BACKEND_GL: return "GL"; + case CLASSIC_FOG_BLEND_BACKEND_VULKAN: return "Vulkan"; + case CLASSIC_FOG_BLEND_BACKEND_COUNT: break; + } + return "unknown"; +} + +bool RendererClassicFogBlendDomain_RunSelfTest( void ) { + R_ClassicFogBlendDomain_ResetFrame(); + classicFogBlendDomainSurface_t deformHashSurface; + InitSurface( deformHashSurface ); + const std::uint64_t undeformedSurfaceHash = + HashSurface( deformHashSurface, 0 ); + deformHashSurface.deformRole = CLASSIC_DEFORM_ROLE_FOG_RECEIVER; + deformHashSurface.deformOutcome = + CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE; + deformHashSurface.deformContractHash = 0x91234ull; + if ( HashSurface( deformHashSurface, 0 ) == undeformedSurfaceHash + || idStr::Cmp( ClassicFogBlendDomainFailure_Name( + CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT ), + "deform-contract" ) ) { + return false; + } + viewDef_t viewDef; + std::memset( &viewDef, 0, sizeof( viewDef ) ); + domain.viewCount = 1; + classicFogBlendDomainView_t &view = domain.views[ 0 ]; + InitView( view, &viewDef, 7 ); + view.ready = true; + view.failure = CLASSIC_FOG_BLEND_FAILURE_NONE; + view.firstLight = 0; + view.lightCount = 1; + view.fogLightCount = 1; + view.firstLightStage = 0; + view.lightStageCount = 1; + view.activeLightStageCount = 1; + view.firstPrimitive = 0; + view.primitiveCount = 2; + view.drawablePrimitiveCount = 2; + view.fogReceiverPrimitiveCount = 1; + view.fogFrustumPrimitiveCount = 1; + domain.lightCount = 1; + domain.lightStageCount = 1; + domain.primitiveCount = 2; + InitLight( domain.lights[ 0 ] ); + domain.lights[ 0 ].firstLightStage = 0; + domain.lights[ 0 ].lightStageCount = 1; + domain.lights[ 0 ].firstPrimitive = 0; + domain.lights[ 0 ].primitiveCount = 2; + InitLightStage( domain.lightStages[ 0 ] ); + domain.lightStages[ 0 ].lightIndex = 0; + InitPrimitive( domain.primitives[ 0 ] ); + domain.primitives[ 0 ].lightIndex = 0; + domain.primitives[ 0 ].lightStageIndex = 0; + domain.primitives[ 0 ].kind = + CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER; + InitPrimitive( domain.primitives[ 1 ] ); + domain.primitives[ 1 ].lightIndex = 0; + domain.primitives[ 1 ].lightStageIndex = 0; + domain.primitives[ 1 ].kind = + CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP; + const std::uint64_t receiverHash = HashPrimitive( domain.primitives[ 0 ], + 0, 0, 0 ); + const std::uint64_t capHash = HashPrimitive( domain.primitives[ 1 ], + 0, 0, 0 ); + const bool accessors = R_ClassicFogBlendDomain_FindView( &viewDef ) == &view + && R_ClassicFogBlendDomain_ViewForScenePacket( 7 ) == &view + && R_ClassicFogBlendDomain_ViewLight( view, 0 ) == &domain.lights[ 0 ] + && R_ClassicFogBlendDomain_LightStage( domain.lights[ 0 ], 0 ) + == &domain.lightStages[ 0 ] + && R_ClassicFogBlendDomain_ViewPrimitive( view, 1 ) + == &domain.primitives[ 1 ]; + const bool stableOrder = receiverHash != capHash; + const bool owned = R_ClassicFogBlendDomain_RecordOwned( &viewDef, + CLASSIC_FOG_BLEND_BACKEND_GL, 1, 1, 0, 0, 0, 0 ); + const bool duplicateRejected = !R_ClassicFogBlendDomain_RecordOwned( + &viewDef, CLASSIC_FOG_BLEND_BACKEND_GL, 1, 1, 0, 0, 0, 0 ) + && domain.stats.backend[ CLASSIC_FOG_BLEND_BACKEND_GL ].duplicateReports + == 1; + + R_ClassicFogBlendDomain_ResetFrame(); + domain.viewCount = 1; + InitView( domain.views[ 0 ], &viewDef, 0 ); + domain.views[ 0 ].ready = true; + domain.views[ 0 ].failure = CLASSIC_FOG_BLEND_FAILURE_NONE; + domain.views[ 0 ].fogReceiverPrimitiveCount = 2; + const bool mismatchRejected = !R_ClassicFogBlendDomain_RecordOwned( + &viewDef, CLASSIC_FOG_BLEND_BACKEND_VULKAN, 1, 0, 0, 0, 0, 0 ) + && domain.views[ 0 ].backendFailure[ + CLASSIC_FOG_BLEND_BACKEND_VULKAN ] + == CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH + && domain.stats.backend[ + CLASSIC_FOG_BLEND_BACKEND_VULKAN ].coverageMismatches == 1; + + classicFogBlendDomainView_t rollbackView; + InitView( rollbackView, NULL, -1 ); + const classicFogBlendCheckpoint_t checkpoint = { 0, 0, 0, 0, 0 }; + domain.lightCount = 1; + domain.surfaceCount = 1; + domain.lightStageCount = 1; + domain.primitiveCount = 1; + domain.textureCount = 1; + classicFogBlendBuildError_t rollbackError; + InitError( rollbackError ); + SetError( rollbackError, + CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW, 9 ); + const bool rollbackRejected = !FailView( rollbackView, checkpoint, + rollbackError ) && domain.lightCount == 0 && domain.surfaceCount == 0 + && domain.lightStageCount == 0 && domain.primitiveCount == 0 + && domain.textureCount == 0 && rollbackView.firstLight == -1 + && rollbackView.failure + == CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW; + const bool names = idStr::Cmp( + ClassicFogBlendDomainBackend_Name( + CLASSIC_FOG_BLEND_BACKEND_VULKAN ), "Vulkan" ) == 0 + && idStr::Cmp( ClassicFogBlendDomainPrimitiveKind_Name( + CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP ), "fog-cap" ) == 0; + const bool passed = accessors && stableOrder && owned + && duplicateRejected && mismatchRejected && rollbackRejected && names; + R_ClassicFogBlendDomain_ResetFrame(); + return passed; +} diff --git a/src/renderer/ClassicFogBlendDomain.h b/src/renderer/ClassicFogBlendDomain.h new file mode 100644 index 00000000..a17a1a5c --- /dev/null +++ b/src/renderer/ClassicFogBlendDomain.h @@ -0,0 +1,448 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_FOG_BLEND_DOMAIN_H__ +#define __CLASSIC_FOG_BLEND_DOMAIN_H__ + +#include "ClassicDeformDomain.h" +#include "MaterialResourceTable.h" + +/* +=============================================================================== + + Shared whole-view ownership contract for the classic fog/blend phase. + + Preparation is transactional per ordinary root 3D view. A ready view owns + the complete source-ordered fog/blend light stream: authoritative GLOBAL then + LOCAL receiver chains, every authored blend stage (including inactive + stages), and the fog frustum cap after its receivers. Every stage register, + texture, render state, transform and texgen plane is sealed here. Retained + draw-surface and triangle pointers are geometry submission bridges only. + + Any unsupported view, surface, stage, geometry or resource rewinds all ranges + for that view so an unchanged classic backend can execute the complete phase + exactly once. Fog and blend are never split between shared and classic owners. + +=============================================================================== +*/ + +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHTS = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_SURFACES = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES = + SCENE_PACKET_MAX_DRAWS * 2; +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES = + SCENE_PACKET_MAX_DRAWS * 4; +const int CLASSIC_FOG_BLEND_DOMAIN_MAX_TEXTURES = + SCENE_PACKET_MAX_DRAWS * 2; + +enum classicFogBlendDomainLightKind_t { + CLASSIC_FOG_BLEND_LIGHT_FOG = 0, + CLASSIC_FOG_BLEND_LIGHT_BLEND, + CLASSIC_FOG_BLEND_LIGHT_KIND_COUNT +}; + +enum classicFogBlendDomainReceiver_t { + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL = 0, + CLASSIC_FOG_BLEND_RECEIVER_LOCAL, + CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM, + CLASSIC_FOG_BLEND_RECEIVER_COUNT +}; + +enum classicFogBlendDomainLightDisposition_t { + CLASSIC_FOG_BLEND_LIGHT_DRAW = 0, + CLASSIC_FOG_BLEND_LIGHT_NOOP_SKIP_BLEND, + CLASSIC_FOG_BLEND_LIGHT_NOOP_MISSING_GLOBAL_CHAIN, + CLASSIC_FOG_BLEND_LIGHT_DISPOSITION_COUNT +}; + +enum classicFogBlendDomainLightStageDisposition_t { + CLASSIC_FOG_BLEND_STAGE_DRAW = 0, + CLASSIC_FOG_BLEND_STAGE_NOOP_INACTIVE_CONDITION, + CLASSIC_FOG_BLEND_STAGE_NOOP_SKIP_BLEND, + CLASSIC_FOG_BLEND_STAGE_NOOP_MISSING_GLOBAL_CHAIN, + CLASSIC_FOG_BLEND_STAGE_DISPOSITION_COUNT +}; + +enum classicFogBlendDomainPrimitiveKind_t { + CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER = 0, + CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP, + CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER, + CLASSIC_FOG_BLEND_PRIMITIVE_KIND_COUNT +}; + +enum classicFogBlendDomainPrimitiveDisposition_t { + CLASSIC_FOG_BLEND_PRIMITIVE_DRAW = 0, + CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_INACTIVE_STAGE, + CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_SKIP_BLEND, + CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_MISSING_GLOBAL_CHAIN, + CLASSIC_FOG_BLEND_PRIMITIVE_DISPOSITION_COUNT +}; + +enum classicFogBlendDomainFailure_t { + CLASSIC_FOG_BLEND_FAILURE_NONE = 0, + CLASSIC_FOG_BLEND_FAILURE_UNAVAILABLE, + CLASSIC_FOG_BLEND_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_LIGHT_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_SURFACE_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_LIGHT_STAGE_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_PRIMITIVE_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_TEXTURE_POOL_OVERFLOW, + CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_STATE, + CLASSIC_FOG_BLEND_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_FOG_BLEND_FAILURE_INVALID_FOG_BLEND_PASS, + CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_RANGE, + CLASSIC_FOG_BLEND_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_FOG_BLEND_FAILURE_INVALID_DRAW_PACKET, + CLASSIC_FOG_BLEND_FAILURE_MISSING_GEOMETRY_RECORD, + CLASSIC_FOG_BLEND_FAILURE_MISSING_INSTANCE_RECORD, + CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_SHADER, + CLASSIC_FOG_BLEND_FAILURE_MISSING_SHADER_REGISTERS, + CLASSIC_FOG_BLEND_FAILURE_MISSING_LIGHT_STAGE, + CLASSIC_FOG_BLEND_FAILURE_REGISTER_OUT_OF_RANGE, + CLASSIC_FOG_BLEND_FAILURE_NONFINITE_VALUE, + CLASSIC_FOG_BLEND_FAILURE_FOG_FRUSTUM_GEOMETRY, + CLASSIC_FOG_BLEND_FAILURE_DEFORM, + CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT, + CLASSIC_FOG_BLEND_FAILURE_SKINNING, + CLASSIC_FOG_BLEND_FAILURE_SPECIAL_SURFACE, + CLASSIC_FOG_BLEND_FAILURE_DEPTH_HACK, + CLASSIC_FOG_BLEND_FAILURE_NEGATIVE_SCALE, + CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_MATERIAL, + CLASSIC_FOG_BLEND_FAILURE_UNSUPPORTED_RENDER_STATE, + CLASSIC_FOG_BLEND_FAILURE_DYNAMIC_RESOURCE, + CLASSIC_FOG_BLEND_FAILURE_MISSING_RESOURCE, + CLASSIC_FOG_BLEND_FAILURE_DEFAULTED_RESOURCE, + CLASSIC_FOG_BLEND_FAILURE_UNLOADED_RESOURCE, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + CLASSIC_FOG_BLEND_FAILURE_COUNT +}; + +enum classicFogBlendDomainBackend_t { + CLASSIC_FOG_BLEND_BACKEND_GL = 0, + CLASSIC_FOG_BLEND_BACKEND_VULKAN, + CLASSIC_FOG_BLEND_BACKEND_COUNT +}; + +enum classicFogBlendDomainBackendOutcome_t { + CLASSIC_FOG_BLEND_BACKEND_UNRECORDED = 0, + CLASSIC_FOG_BLEND_BACKEND_OWNED, + CLASSIC_FOG_BLEND_BACKEND_FALLBACK +}; + +typedef struct classicFogBlendDomainTexture_s { + std::uint64_t textureResourceId; + const idImage *image; + std::uint64_t storageGeneration; + std::uint64_t nameHash; + unsigned int textureHandle; + textureFilter_t filter; + textureRepeat_t repeat; + bool loaded; + bool defaulted; + bool mutableImage; +} classicFogBlendDomainTexture_t; + +typedef struct classicFogBlendDomainSurface_s { + // Geometry submission bridges only. Consumers must not follow either bridge + // to material stages, shader registers, or mutable front-end state. + const drawSurf_t *legacyDrawSurf; + const srfTriangles_t *legacyGeometry; + int drawPacketIndex; + int lightIndex; + int sourceOrdinal; + int receiverOrdinal; + classicFogBlendDomainReceiver_t receiver; + int geometryRecordIndex; + int instanceRecordIndex; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float modelMatrix[ 16 ]; + float modelViewMatrix[ 16 ]; + bool hasAmbientCache; + bool hasIndexCache; + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicFogBlendDomainSurface_t; + +typedef struct classicFogBlendDomainLightStage_s { + int lightIndex; + int sourceStageIndex; + classicFogBlendDomainLightStageDisposition_t disposition; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + float condition; + float color[ 4 ]; + float textureMatrix[ 2 ][ 4 ]; + std::uint64_t projectionTextureResourceId; + std::uint64_t falloffTextureResourceId; + std::uint64_t fogTextureResourceId; + std::uint64_t fogEnterTextureResourceId; + rendererBlendState_t blend; + rendererDepthState_t depth; + rendererCullMode_t cull; + std::uint32_t colorWriteMask; + bool alphaTestEnabled; + rendererCompareOp_t alphaTestCompareOperation; + float alphaTestValue; + bool hasTextureMatrix; + bool conditionIgnored; + std::uint64_t hash; +} classicFogBlendDomainLightStage_t; + +typedef struct classicFogBlendDomainLight_s { + int sourceOrdinal; + classicFogBlendDomainLightKind_t kind; + classicFogBlendDomainLightDisposition_t disposition; + int firstSurface; + int surfaceCount; + int receiverSurfaceCount[ CLASSIC_FOG_BLEND_RECEIVER_COUNT ]; + int firstLightStage; + int lightStageCount; + int activeLightStageCount; + int inactiveLightStageCount; + int noopLightStageCount; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int fogReceiverPrimitiveCount; + int fogFrustumPrimitiveCount; + int blendPrimitiveCount; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float lightProject[ 4 ][ 4 ]; + float fogPlane[ 4 ]; + float fogGlobalTexgen[ 2 ][ 2 ][ 4 ]; + float fogColor[ 4 ]; + float fogDensity; + float fogDistanceScale; + std::uint64_t falloffTextureResourceId; + std::uint64_t fogTextureResourceId; + std::uint64_t fogEnterTextureResourceId; + bool globalChainPresent; + int materialDeformReceiverCount; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicFogBlendDomainLight_t; + +typedef struct classicFogBlendDomainPrimitive_s { + // Geometry submission bridges only. All render semantics are available from + // this record, its LightStage, and its owning View. + const drawSurf_t *legacyDrawSurf; + const srfTriangles_t *legacyGeometry; + int lightIndex; + int lightStageIndex; + int surfaceIndex; + classicFogBlendDomainPrimitiveKind_t kind; + classicFogBlendDomainReceiver_t receiver; + classicFogBlendDomainPrimitiveDisposition_t disposition; + int geometryRecordIndex; + int instanceRecordIndex; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + // Depth/cull are repeated per primitive because the fog frustum cap changes + // from receiver EQUAL/front culling to LESS_OR_EQUAL/back culling while + // retaining its light stage's blend/color state. + rendererDepthState_t depth; + rendererCullMode_t cull; + float modelMatrix[ 16 ]; + float modelViewMatrix[ 16 ]; + float localLightProject[ 4 ][ 4 ]; + float fogTexgen[ 2 ][ 2 ][ 4 ]; + std::uint64_t hash; +} classicFogBlendDomainPrimitive_t; + +typedef struct classicFogBlendDomainBackendCoverage_s { + std::uint64_t ownedViewMask; + std::uint64_t fallbackViewMask; + int ownedViews; + int fallbackViews; + int ownedLights; + int fallbackLights; + int ownedSurfaces; + int fallbackSurfaces; + int ownedFogReceiverPrimitives; + int fallbackFogReceiverPrimitives; + int ownedFogFrustumPrimitives; + int fallbackFogFrustumPrimitives; + int ownedBlendPrimitives; + int fallbackBlendPrimitives; + int ownedNoopPrimitives; + int fallbackNoopPrimitives; + int ownedNoopLightStages; + int fallbackNoopLightStages; + int ownedNoopLights; + int fallbackNoopLights; + int coverageMismatches; + int duplicateReports; + int untrackedFallbacks; +} classicFogBlendDomainBackendCoverage_t; + +typedef struct classicFogBlendDomainView_s { + const viewDef_t *viewDef; + int scenePacketIndex; + int fogBlendPassPacketIndex; + int firstLight; + int lightCount; + int fogLightCount; + int blendLightCount; + int noopLightCount; + int firstSurface; + int surfaceCount; + int receiverSurfaceCount[ CLASSIC_FOG_BLEND_RECEIVER_COUNT ]; + int firstLightStage; + int lightStageCount; + int activeLightStageCount; + int inactiveLightStageCount; + int noopLightStageCount; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int fogReceiverPrimitiveCount; + int fogFrustumPrimitiveCount; + int blendPrimitiveCount; + int packetDrawCount; + int materialDeformReceiverCount; + int viewportX1; + int viewportY1; + int viewportX2; + int viewportY2; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float projectionMatrix[ 16 ]; + bool useScissor; + bool skipBlendLights; + bool ready; + classicFogBlendDomainFailure_t failure; + int failureDetail; + int failurePassPacketIndex; + int failureDrawPacketIndex; + int failureLightOrdinal; + int failureReceiverOrdinal; + int failureStageIndex; + std::uint64_t hash; + classicFogBlendDomainBackendOutcome_t backendOutcome[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + classicFogBlendDomainFailure_t backendFailure[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendFailureDetail[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendFogReceiverPrimitives[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendFogFrustumPrimitives[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendBlendPrimitives[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendNoopPrimitives[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendNoopLightStages[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + int backendNoopLights[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; +} classicFogBlendDomainView_t; + +typedef struct classicFogBlendDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int fogBlendViews; + int readyViews; + int fallbackViews; + int lights; + int fogLights; + int blendLights; + int noopLights; + int surfaces; + int receiverSurfaces[ CLASSIC_FOG_BLEND_RECEIVER_COUNT ]; + int lightStages; + int activeLightStages; + int inactiveLightStages; + int noopLightStages; + int primitives; + int drawablePrimitives; + int noopPrimitives; + int fogReceiverPrimitives; + int fogFrustumPrimitives; + int blendPrimitives; + int textures; + int materialDeformReceivers; + int failureCounts[ CLASSIC_FOG_BLEND_FAILURE_COUNT ]; + std::uint64_t hash; + classicFogBlendDomainBackendCoverage_t backend[ + CLASSIC_FOG_BLEND_BACKEND_COUNT ]; + char status[ 96 ]; +} classicFogBlendDomainStats_t; + +void R_ClassicFogBlendDomain_ResetFrame( void ); +void R_ClassicFogBlendDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicFogBlendDomainStats_t &R_ClassicFogBlendDomain_Stats( void ); +int R_ClassicFogBlendDomain_NumViews( void ); +const classicFogBlendDomainView_t *R_ClassicFogBlendDomain_ViewByIndex( int index ); +const classicFogBlendDomainView_t *R_ClassicFogBlendDomain_ViewForScenePacket( + int scenePacketIndex ); +const classicFogBlendDomainView_t *R_ClassicFogBlendDomain_FindView( + const viewDef_t *viewDef ); +const classicFogBlendDomainLight_t *R_ClassicFogBlendDomain_ViewLight( + const classicFogBlendDomainView_t &view, int lightIndex ); +const classicFogBlendDomainLightStage_t *R_ClassicFogBlendDomain_LightStage( + const classicFogBlendDomainLight_t &light, int stageIndex ); +const classicFogBlendDomainPrimitive_t *R_ClassicFogBlendDomain_ViewPrimitive( + const classicFogBlendDomainView_t &view, int primitiveIndex ); +const classicFogBlendDomainTexture_t *R_ClassicFogBlendDomain_ResolveTexture( + std::uint64_t textureResourceId ); +bool R_ClassicFogBlendDomain_RecordOwned( const viewDef_t *viewDef, + classicFogBlendDomainBackend_t backend, int fogReceiverPrimitives, + int fogFrustumPrimitives, int blendPrimitives, int noopPrimitives, + int noopLightStages, int noopLights ); +void R_ClassicFogBlendDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicFogBlendDomainBackend_t backend, + classicFogBlendDomainFailure_t failure, int detail ); +const classicFogBlendDomainBackendCoverage_t & + R_ClassicFogBlendDomain_BackendCoverage( + classicFogBlendDomainBackend_t backend ); +const char *ClassicFogBlendDomainLightKind_Name( + classicFogBlendDomainLightKind_t kind ); +const char *ClassicFogBlendDomainReceiver_Name( + classicFogBlendDomainReceiver_t receiver ); +const char *ClassicFogBlendDomainLightDisposition_Name( + classicFogBlendDomainLightDisposition_t disposition ); +const char *ClassicFogBlendDomainLightStageDisposition_Name( + classicFogBlendDomainLightStageDisposition_t disposition ); +const char *ClassicFogBlendDomainPrimitiveKind_Name( + classicFogBlendDomainPrimitiveKind_t kind ); +const char *ClassicFogBlendDomainPrimitiveDisposition_Name( + classicFogBlendDomainPrimitiveDisposition_t disposition ); +const char *ClassicFogBlendDomainFailure_Name( + classicFogBlendDomainFailure_t failure ); +const char *ClassicFogBlendDomainBackend_Name( + classicFogBlendDomainBackend_t backend ); +bool RendererClassicFogBlendDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_FOG_BLEND_DOMAIN_H__ */ diff --git a/src/renderer/ClassicGuiDomain.cpp b/src/renderer/ClassicGuiDomain.cpp new file mode 100644 index 00000000..9d76ef97 --- /dev/null +++ b/src/renderer/ClassicGuiDomain.cpp @@ -0,0 +1,1330 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicGuiDomain.h" + +#include + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; + +typedef struct classicGuiDomainState_s { + classicGuiDomainView_t views[ CLASSIC_GUI_DOMAIN_MAX_VIEWS ]; + classicGuiDomainDraw_t draws[ CLASSIC_GUI_DOMAIN_MAX_DRAWS ]; + rendererEvaluatedMaterialPass_t passes[ CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES ]; + classicGuiDomainStats_t stats; + int viewCount; + int drawCount; + int passCount; +} classicGuiDomainState_t; + +classicGuiDomainState_t domain; + +static bool RangeFits( int first, int count, int capacity ) { + return first >= 0 && count >= 0 && first <= capacity && count <= capacity - first; +} + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + for ( int i = 0; i < 4; ++i ) { + HashByte( hash, value >> ( i * 8 ) ); + } +} + +static void HashU64( std::uint64_t &hash, std::uint64_t value ) { + for ( int i = 0; i < 8; ++i ) { + HashByte( hash, static_cast( value >> ( i * 8 ) ) ); + } +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), "float hash width mismatch" ); + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static std::uint64_t HashPass( const rendererEvaluatedMaterialPass_t &pass ) { + std::uint64_t hash = HASH_OFFSET; + HashU32( hash, pass.order ); + HashInt( hash, pass.sourceStageIndex ); + HashInt( hash, pass.kind ); + HashInt( hash, pass.textureSemantic ); + // Generation is intentionally excluded so identical records hash equally + // across frames; the low bits retain record/binding identity. + HashU32( hash, static_cast( pass.textureResourceId ) ); + HashFloat( hash, pass.condition ); + HashBool( hash, pass.active ); + HashInt( hash, pass.disposition ); + for ( int i = 0; i < 4; ++i ) { + HashFloat( hash, pass.color[ i ] ); + } + for ( int i = 0; i < 6; ++i ) { + HashFloat( hash, pass.textureMatrix[ i ] ); + } + HashBool( hash, pass.blend.enabled ); + HashInt( hash, pass.blend.sourceColor ); + HashInt( hash, pass.blend.destinationColor ); + HashInt( hash, pass.blend.colorOperation ); + HashInt( hash, pass.blend.sourceAlpha ); + HashInt( hash, pass.blend.destinationAlpha ); + HashInt( hash, pass.blend.alphaOperation ); + HashBool( hash, pass.depth.testEnabled ); + HashBool( hash, pass.depth.writeEnabled ); + HashInt( hash, pass.depth.compareOperation ); + HashInt( hash, pass.cull ); + HashU32( hash, pass.colorWriteMask ); + HashBool( hash, pass.alphaTestEnabled ); + HashInt( hash, pass.alphaTestCompareOperation ); + HashFloat( hash, pass.alphaTest ); + HashInt( hash, pass.texgen ); + HashInt( hash, pass.vertexColor ); + HashBool( hash, pass.polygonOffsetEnabled ); + HashFloat( hash, pass.polygonOffsetFactor ); + HashFloat( hash, pass.polygonOffsetUnits ); + HashInt( hash, pass.programFamily ); + HashU32( hash, pass.programKey ); + return hash; +} + +static std::uint64_t HashDraw( const classicGuiDomainDraw_t &draw ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, draw.sourceSurfaceIndex ); + HashInt( hash, draw.sourceSurface ); + HashBool( hash, draw.packetBacked ); + HashInt( hash, draw.materialId ); + HashInt( hash, draw.vertexCount ); + HashInt( hash, draw.firstIndex ); + HashInt( hash, draw.indexCount ); + HashInt( hash, draw.vertexOffset ); + HashInt( hash, draw.scissorX1 ); + HashInt( hash, draw.scissorY1 ); + HashInt( hash, draw.scissorX2 ); + HashInt( hash, draw.scissorY2 ); + HashInt( hash, draw.deformRole ); + HashInt( hash, draw.deformOutcome ); + HashU64( hash, draw.deformContractHash ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, draw.modelViewMatrix[ i ] ); + } + HashInt( hash, draw.evaluatedPassCount ); + for ( int i = 0; i < draw.evaluatedPassCount; ++i ) { + HashU64( hash, HashPass( domain.passes[ draw.firstEvaluatedPass + i ] ) ); + } + return hash; +} + +static std::uint64_t HashView( const classicGuiDomainView_t &view ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, view.scope ); + HashInt( hash, view.sourceSurfaceCount ); + HashInt( hash, view.drawableSurfaceCount ); + HashInt( hash, view.noopSurfaceCount ); + HashInt( hash, view.evaluatedPassCount ); + HashInt( hash, view.drawablePassCount ); + HashInt( hash, view.noopPassCount ); + HashInt( hash, view.materialDeformSurfaceCount ); + HashInt( hash, view.completedDeformSurfaceCount ); + HashInt( hash, view.emptyDeformSurfaceCount ); + HashInt( hash, view.viewportX1 ); + HashInt( hash, view.viewportY1 ); + HashInt( hash, view.viewportX2 ); + HashInt( hash, view.viewportY2 ); + HashInt( hash, view.scissorX1 ); + HashInt( hash, view.scissorY1 ); + HashInt( hash, view.scissorX2 ); + HashInt( hash, view.scissorY2 ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, view.projectionMatrix[ i ] ); + } + for ( int i = 0; i < view.drawCount; ++i ) { + HashU64( hash, domain.draws[ view.firstDraw + i ].hash ); + } + return hash; +} + +static void InitDraw( classicGuiDomainDraw_t &draw ) { + std::memset( &draw, 0, sizeof( draw ) ); + draw.drawPacketIndex = -1; + draw.materialTableRecordIndex = -1; + draw.materialId = -1; + draw.firstGuiPass = -1; + draw.firstWorldPass = -1; + draw.firstEvaluatedPass = -1; +} + +static void InitView( classicGuiDomainView_t &view, const viewDef_t *viewDef, + int scenePacketIndex ) { + std::memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scope = viewDef != NULL && viewDef->viewEntitys != NULL + ? CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD + : CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D; + view.scenePacketIndex = scenePacketIndex; + view.guiPassPacketIndex = -1; + view.firstDraw = -1; + view.firstEvaluatedPass = -1; + view.failurePassPacketIndex = -1; + view.failureDrawPacketIndex = -1; + view.failureSourceSurfaceIndex = -1; + view.failureSourceStageIndex = -1; + view.guiPassFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE; + view.evaluationStatus = RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; + if ( viewDef != NULL ) { + view.viewportX1 = viewDef->viewport.x1; + view.viewportY1 = viewDef->viewport.y1; + view.viewportX2 = viewDef->viewport.x2; + view.viewportY2 = viewDef->viewport.y2; + view.scissorX1 = viewDef->scissor.x1; + view.scissorY1 = viewDef->scissor.y1; + view.scissorX2 = viewDef->scissor.x2; + view.scissorY2 = viewDef->scissor.y2; + std::memcpy( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ); + if ( view.scope == CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D ) { + view.sourceSurfaceCount = viewDef->numDrawSurfs; + } else if ( viewDef->drawSurfs != NULL ) { + for ( int i = 0; i < viewDef->numDrawSurfs; ++i ) { + if ( viewDef->drawSurfs[ i ] != NULL + && ( viewDef->drawSurfs[ i ]->dsFlags + & DSF_IN_WORLD_GUI ) != 0 ) { + view.sourceSurfaceCount++; + } + } + } + } +} + +static bool FailView( classicGuiDomainView_t &view, + classicGuiDomainFailure_t failure, int detail, + int sourceSurface = -1, int drawPacket = -1, int passPacket = -1, + int sourceStage = -1, + materialResourceGuiPassFailure_t guiFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE, + rendererMaterialPassEvaluationStatus_t evaluationStatus = RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + view.ready = false; + view.failure = failure; + view.failureDetail = detail; + view.guiPassFailure = guiFailure; + view.evaluationStatus = evaluationStatus; + view.failureSourceSurfaceIndex = sourceSurface; + view.failureDrawPacketIndex = drawPacket; + view.failurePassPacketIndex = passPacket; + view.failureSourceStageIndex = sourceStage; + view.firstDraw = -1; + view.drawCount = 0; + view.firstEvaluatedPass = -1; + view.evaluatedPassCount = 0; + view.activePassCount = 0; + view.drawablePassCount = 0; + view.inactivePassCount = 0; + view.activeNoopPassCount = 0; + view.noopPassCount = 0; + view.materialDeformSurfaceCount = 0; + view.completedDeformSurfaceCount = 0; + view.emptyDeformSurfaceCount = 0; + view.hash = 0; + domain.stats.fallbackViews++; + if ( failure >= CLASSIC_GUI_DOMAIN_FAILURE_NONE + && failure < CLASSIC_GUI_DOMAIN_FAILURE_COUNT ) { + domain.stats.failureCounts[ failure ]++; + } + if ( failure == CLASSIC_GUI_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW + || failure == CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_OVERFLOW + || failure == CLASSIC_GUI_DOMAIN_FAILURE_VIEW_POOL_OVERFLOW + || failure == CLASSIC_GUI_DOMAIN_FAILURE_DRAW_POOL_OVERFLOW + || failure == CLASSIC_GUI_DOMAIN_FAILURE_EVALUATED_PASS_POOL_OVERFLOW ) { + domain.stats.overflow = true; + } + return false; +} + +static bool MaterialRequestsClassicDeform( const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->Deform() != DFRM_NONE; +} + +static int DeformContractFailureDetail( const drawSurf_t *drawSurf ) { + if ( drawSurf == NULL ) { + return -1; + } + return static_cast( drawSurf->classicDeform.role ) + * CLASSIC_DEFORM_OUTCOME_COUNT + + static_cast( drawSurf->classicDeform.outcome ); +} + +static bool ValidateFinalizedDeformContract( const drawSurf_t *drawSurf, + const drawPacket_t *packet, classicDeformRole_t &role, + classicDeformOutcome_t &outcome, std::uint64_t &semanticHash ) { + role = CLASSIC_DEFORM_ROLE_UNKNOWN; + outcome = CLASSIC_DEFORM_OUTCOME_NONE; + semanticHash = 0; + if ( !MaterialRequestsClassicDeform( drawSurf ) ) { + return true; + } + // The deform switch is independent authorization. Without it the complete + // GUI view rolls back to the unchanged classic owner. + if ( !r_rendererSharedDeform.GetBool() ) { + return false; + } + const std::uint64_t frameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + const classicDeformRecord_t &record = drawSurf->classicDeform; + if ( record.role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + || !record.cpuFinalized + || !R_ClassicDeformDomain_ValidateRecordForFrame( record, + frameToken ) + || !R_ClassicDeformDomain_RecordMatchesDrawSurf( record, drawSurf ) + || ( !R_ClassicDeformDomain_HasCompletedOutput( record ) + && !R_ClassicDeformDomain_HasEmptyOutput( record ) ) ) { + return false; + } + if ( packet != NULL ) { + if ( !packet->hasClassicDeformRecord + || packet->classicDeformRecord == NULL + || packet->geometryRecord == NULL + || packet->classicDeformRecord + != &packet->geometryRecord->classicDeform + || !packet->geometryRecord->hasClassicDeformRecord + || !R_ClassicDeformDomain_ValidateRecordForFrame( + *packet->classicDeformRecord, frameToken ) + || !R_ClassicDeformDomain_SameProvenance( record, + *packet->classicDeformRecord ) ) { + return false; + } + } + role = record.role; + outcome = record.outcome; + semanticHash = record.semanticHash; + return true; +} + +static classicGuiDomainSourceSurface_t ClassifySourceSurface( + classicGuiDomainScope_t scope, const drawSurf_t *drawSurf, + bool &packetExpected ) { + packetExpected = false; + if ( drawSurf == NULL ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NULL_SURFACE; + } + if ( drawSurf->material == NULL ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_MISSING_MATERIAL; + } + if ( drawSurf->material->GetSort() >= SS_POST_PROCESS ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_POST_PROCESS; + } + if ( !drawSurf->material->HasAmbient() ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NO_AMBIENT; + } + if ( drawSurf->material->IsPortalSky() ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_PORTAL_SKY; + } + if ( drawSurf->geo == NULL || drawSurf->geo->numIndexes <= 0 ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY; + } + if ( drawSurf->space == NULL ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_MISSING_SPACE; + } + if ( drawSurf->space->weaponDepthHack || drawSurf->space->modelDepthHack != 0.0f ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DEPTH_HACK; + } + if ( drawSurf->decalColorCache != NULL || drawSurf->decalColorStageCount != 0 + || drawSurf->decalColorStride != 0 || drawSurf->decalColorOffset != 0 ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DECAL_COLOR_STREAM; + } + if ( drawSurf->material->SuppressInSubview() ) { + // The established GL root-2D walker suppresses this flag while the + // established Vulkan 2D walker draws it. Until that legacy divergence is + // resolved, neither result is a truthful shared semantic record. + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW; + } + if ( scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD + && ( ( drawSurf->dsFlags & DSF_IN_WORLD_GUI ) == 0 + || drawSurf->space->entityDef != NULL ) ) { + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_MISSING_SPACE; + } + packetExpected = true; + return CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE; +} + +static bool CountDisposition( classicGuiDomainDraw_t &draw, + const rendererEvaluatedMaterialPass_t &pass ) { + if ( pass.active ) { + draw.activePassCount++; + } + switch ( pass.disposition ) { + case RENDERER_MATERIAL_PASS_DRAW: + draw.drawablePassCount++; + return pass.active; + case RENDERER_MATERIAL_PASS_INACTIVE_CONDITION: + draw.inactivePassCount++; + draw.noopPassCount++; + return !pass.active; + case RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND: + case RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE: + case RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA: + draw.activeNoopPassCount++; + draw.noopPassCount++; + return pass.active; + default: + return false; + } +} + +static bool ValidateDrawPacket( const idScenePacketFrame &packetFrame, + int packetIndex, const viewDef_t *viewDef, const drawSurf_t *drawSurf, + classicGuiDomainScope_t scope, classicGuiDomainFailure_t &failure ) { + if ( packetIndex < 0 || packetIndex >= packetFrame.NumDrawPackets() ) { + failure = CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH; + return false; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetIndex ); + const scenePacketCategory_t expectedCategory = + scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD + ? SCENE_PACKET_CATEGORY_WORLD : SCENE_PACKET_CATEGORY_GUI; + if ( packet.passCategory != RENDER_PASS_GUI + || packet.packetCategory != expectedCategory + || packet.viewDef != viewDef || packet.legacyDrawSurf != drawSurf ) { + failure = CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH; + return false; + } + if ( packet.geometryRecordIndex < 0 + || packet.geometryRecordIndex >= packetFrame.NumGeometryRecords() + || packet.geometryRecord != &packetFrame.GeometryRecord( packet.geometryRecordIndex ) + || packet.geometryRecord->legacyGeometry != drawSurf->geo ) { + failure = CLASSIC_GUI_DOMAIN_FAILURE_MISSING_GEOMETRY_RECORD; + return false; + } + if ( packet.instanceRecordIndex < 0 + || packet.instanceRecordIndex >= packetFrame.NumInstanceRecords() + || packet.instanceRecord != &packetFrame.InstanceRecord( packet.instanceRecordIndex ) + || packet.instanceRecord->legacySpace != drawSurf->space ) { + failure = CLASSIC_GUI_DOMAIN_FAILURE_MISSING_INSTANCE_RECORD; + return false; + } + if ( packet.materialRecordIndex < 0 + || packet.materialRecordIndex >= packetFrame.NumMaterialRecords() + || packet.materialRecord != &packetFrame.MaterialRecord( packet.materialRecordIndex ) + || packet.materialRecord->material != drawSurf->material ) { + failure = CLASSIC_GUI_DOMAIN_FAILURE_MISSING_MATERIAL_RECORD; + return false; + } + return true; +} + +static bool R_ClassicGuiDomain_PrepareView( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene, classicGuiDomainView_t &view ) { + const scenePacketFrameStats_t &packetStats = packetFrame.Stats(); + const materialResourceTableStats_t &tableStats = R_MaterialResourceTable_Stats(); + if ( packetStats.overflow ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW, + packetStats.overflowCause ); + } + if ( !tableStats.prepared ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_NOT_PREPARED, 0 ); + } + if ( !tableStats.available ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_UNAVAILABLE, 0 ); + } + if ( tableStats.overflow ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_OVERFLOW, 0 ); + } + const viewDef_t *viewDef = view.viewDef; + const bool inWorld = view.scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD; + if ( viewDef == NULL + || ( !inWorld && viewDef->viewEntitys != NULL ) + || ( inWorld && ( viewDef->viewEntitys == NULL + || viewDef->renderWorld == NULL || r_skipAmbient.GetBool() ) ) + || viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_UNSUPPORTED_VIEW, 0 ); + } + if ( viewDef->numDrawSurfs < 0 + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_RANGE, + viewDef->numDrawSurfs ); + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_SCENE_RANGE, + scene.passPacketCount ); + } + const passPacket_t *guiPass = NULL; + int passPacketIndex = -1; + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + const int candidateIndex = scene.firstPassPacket + localPass; + const passPacket_t &candidate = packetFrame.Pass( candidateIndex ); + if ( candidate.passCategory != RENDER_PASS_GUI ) { + continue; + } + if ( guiPass != NULL || !candidate.enabled || candidate.commandOnly + || candidate.packetCategory != ( inWorld + ? SCENE_PACKET_CATEGORY_WORLD : SCENE_PACKET_CATEGORY_GUI ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_GUI_PASS, + candidate.passCategory, -1, -1, candidateIndex ); + } + guiPass = &candidate; + passPacketIndex = candidateIndex; + } + if ( guiPass == NULL || ( !inWorld && scene.passPacketCount != 1 ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_GUI_PASS, + scene.passPacketCount ); + } + view.guiPassPacketIndex = passPacketIndex; + if ( !RangeFits( scene.firstDrawPacket, scene.drawPacketCount, + packetFrame.NumDrawPackets() ) + || !RangeFits( guiPass->firstDrawPacket, guiPass->drawPacketCount, + packetFrame.NumDrawPackets() ) + || ( !inWorld && ( scene.firstDrawPacket != guiPass->firstDrawPacket + || scene.drawPacketCount != guiPass->drawPacketCount ) ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_RANGE, + guiPass->drawPacketCount, -1, -1, passPacketIndex ); + } + view.packetDrawCount = guiPass->drawPacketCount; + if ( !RangeFits( domain.drawCount, view.sourceSurfaceCount, + CLASSIC_GUI_DOMAIN_MAX_DRAWS ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_DRAW_POOL_OVERFLOW, + view.sourceSurfaceCount ); + } + + const int drawCheckpoint = domain.drawCount; + const int passCheckpoint = domain.passCount; + int stagedPassCount = 0; + int packetCursor = guiPass->firstDrawPacket; + const int packetEnd = guiPass->firstDrawPacket + guiPass->drawPacketCount; + int drawableSurfaces = 0; + int noopSurfaces = 0; + + int sourceSlot = 0; + for ( int sourceIndex = 0; sourceIndex < viewDef->numDrawSurfs; ++sourceIndex ) { + const drawSurf_t *source = viewDef->drawSurfs[ sourceIndex ]; + if ( inWorld && ( source == NULL + || ( source->dsFlags & DSF_IN_WORLD_GUI ) == 0 ) ) { + continue; + } + bool packetExpected = false; + const classicGuiDomainSourceSurface_t classification = + ClassifySourceSurface( view.scope, source, packetExpected ); + classicGuiDomainDraw_t &draw = domain.draws[ drawCheckpoint + sourceSlot++ ]; + InitDraw( draw ); + draw.sourceSurfaceIndex = sourceIndex; + draw.sourceSurface = classification; + draw.legacyDrawSurf = source; + + if ( classification == CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW + || classification == CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_MISSING_SPACE + || classification == CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DEPTH_HACK + || classification == CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DECAL_COLOR_STREAM + || classification == CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_POST_PROCESS ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_SURFACE_FALLBACK, + classification, sourceIndex ); + } + + const drawPacket_t *packet = NULL; + if ( packetExpected ) { + classicGuiDomainFailure_t packetFailure = CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_PACKET; + if ( packetCursor >= packetEnd + || !ValidateDrawPacket( packetFrame, packetCursor, viewDef, + source, view.scope, packetFailure ) ) { + return FailView( view, packetFailure, packetCursor, + sourceIndex, packetCursor, passPacketIndex ); + } + packet = &packetFrame.DrawPacket( packetCursor ); + draw.packetBacked = true; + draw.drawPacketIndex = packetCursor++; + draw.materialTableRecordIndex = packet->materialRecordIndex; + draw.vertexCount = packet->vertexCount; + draw.firstIndex = packet->firstIndex; + draw.indexCount = packet->indexCount; + draw.vertexOffset = packet->vertexOffset; + draw.scissorX1 = packet->scissorX1; + draw.scissorY1 = packet->scissorY1; + draw.scissorX2 = packet->scissorX2; + draw.scissorY2 = packet->scissorY2; + draw.hasIndexCache = packet->hasIndexCache; + draw.hasAmbientCache = packet->hasAmbientCache; + std::memcpy( draw.modelViewMatrix, packet->instanceRecord->modelViewMatrix, + sizeof( draw.modelViewMatrix ) ); + } + + if ( !ValidateFinalizedDeformContract( source, packet, + draw.deformRole, draw.deformOutcome, + draw.deformContractHash ) ) { + return FailView( view, + CLASSIC_GUI_DOMAIN_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( source ), sourceIndex, + draw.drawPacketIndex, passPacketIndex ); + } + + if ( classification != CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE ) { + noopSurfaces++; + draw.hash = HashDraw( draw ); + continue; + } + drawableSurfaces++; + if ( packet == NULL || !packet->hasShaderRegisters + || packet->instanceRecord == NULL + || !packet->instanceRecord->hasShaderRegisters + || packet->instanceRecord->legacyShaderRegisters == NULL + || packet->instanceRecord->legacyShaderRegisters != source->shaderRegisters ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MISSING_INSTANCE_RECORD, + 0, sourceIndex, draw.drawPacketIndex, passPacketIndex ); + } + if ( packet->instanceRecord->negativeScale ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_SURFACE_FALLBACK, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE, + sourceIndex, draw.drawPacketIndex, passPacketIndex ); + } + const materialResourceTableRecord_t *materialRecord = + R_MaterialResourceTable_RecordForIndex( packet->materialRecordIndex ); + if ( materialRecord == NULL ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MISSING_MATERIAL_RECORD, + packet->materialRecordIndex, sourceIndex, draw.drawPacketIndex, passPacketIndex ); + } + if ( materialRecord->sourceMaterialRecordIndex != packet->materialRecordIndex + || materialRecord->material != source->material + || materialRecord->tableGeneration == 0 + || ( view.tableGeneration != 0 + && view.tableGeneration != materialRecord->tableGeneration ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_STALE_MATERIAL_RECORD, + packet->materialRecordIndex, sourceIndex, draw.drawPacketIndex, passPacketIndex ); + } + if ( packet->instanceRecord->shaderRegisterCount != materialRecord->registerCount ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MISSING_INSTANCE_RECORD, + packet->instanceRecord->shaderRegisterCount, sourceIndex, + draw.drawPacketIndex, passPacketIndex ); + } + view.tableGeneration = materialRecord->tableGeneration; + draw.tableGeneration = materialRecord->tableGeneration; + draw.materialId = materialRecord->materialId; + rendererMaterialPassList_t compiled; + if ( inWorld ) { + draw.firstWorldPass = materialRecord->firstWorldPass; + draw.worldPassCount = materialRecord->worldPassCount; + if ( !materialRecord->worldDomainReferenced + || !materialRecord->worldPassEligible + || materialRecord->worldPassFailure + != MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + || materialRecord->firstWorldPass < 0 + || materialRecord->worldPassCount <= 0 + || !R_MaterialResourceTable_WorldPassEligible( *materialRecord ) ) { + return FailView( view, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_INELIGIBLE, + materialRecord->worldPassFailure, sourceIndex, + draw.drawPacketIndex, passPacketIndex, + materialRecord->worldPassFailureStage, + materialRecord->worldPassFailure ); + } + if ( !R_MaterialResourceTable_CopyWorldPassList( *materialRecord, + compiled ) + || compiled.count != static_cast( + materialRecord->worldPassCount ) ) { + return FailView( view, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_COPY_FAILED, + materialRecord->worldPassCount, sourceIndex, + draw.drawPacketIndex, passPacketIndex ); + } + } else { + draw.firstGuiPass = materialRecord->firstGuiPass; + draw.guiPassCount = materialRecord->guiPassCount; + if ( !materialRecord->guiDomainReferenced + || !materialRecord->guiPassEligible + || materialRecord->guiPassFailure + != MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + || materialRecord->firstGuiPass < 0 + || materialRecord->guiPassCount <= 0 + || !R_MaterialResourceTable_GuiPassEligible( *materialRecord ) ) { + return FailView( view, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_INELIGIBLE, + materialRecord->guiPassFailure, sourceIndex, + draw.drawPacketIndex, passPacketIndex, + materialRecord->guiPassFailureStage, + materialRecord->guiPassFailure ); + } + if ( !R_MaterialResourceTable_CopyGuiPassList( *materialRecord, + compiled ) + || compiled.count != static_cast( + materialRecord->guiPassCount ) ) { + return FailView( view, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_COPY_FAILED, + materialRecord->guiPassCount, sourceIndex, + draw.drawPacketIndex, passPacketIndex ); + } + } + rendererEvaluatedMaterialPassList_t evaluated; + const rendererMaterialPassEvaluationStatus_t evaluationStatus = + RendererContracts_EvaluateMaterialPassList( evaluated, compiled, + packet->instanceRecord->legacyShaderRegisters, + materialRecord->registerCount > 0 + ? static_cast( materialRecord->registerCount ) : 0u ); + if ( evaluationStatus != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS + || evaluated.count != compiled.count ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_EVALUATION_FAILED, + evaluationStatus, sourceIndex, draw.drawPacketIndex, passPacketIndex, + -1, MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE, evaluationStatus ); + } + if ( !RangeFits( passCheckpoint + stagedPassCount, + static_cast( evaluated.count ), + CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_EVALUATED_PASS_POOL_OVERFLOW, + static_cast( evaluated.count ), sourceIndex, draw.drawPacketIndex, + passPacketIndex ); + } + draw.firstEvaluatedPass = passCheckpoint + stagedPassCount; + draw.evaluatedPassCount = static_cast( evaluated.count ); + for ( int passIndex = 0; passIndex < draw.evaluatedPassCount; ++passIndex ) { + const rendererEvaluatedMaterialPass_t &pass = evaluated.passes[ passIndex ]; + const materialResourceTextureBinding_t *binding = + R_MaterialResourceTable_ResolveTextureResource( pass.textureResourceId ); + if ( binding == NULL || binding->textureResourceId != pass.textureResourceId + || !binding->loaded || binding->missing || binding->defaulted ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE, + passIndex, sourceIndex, draw.drawPacketIndex, passPacketIndex, + pass.sourceStageIndex ); + } + if ( !CountDisposition( draw, pass ) ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DISPOSITION, + pass.disposition, sourceIndex, draw.drawPacketIndex, passPacketIndex, + pass.sourceStageIndex ); + } + domain.passes[ draw.firstEvaluatedPass + passIndex ] = pass; + } + stagedPassCount += draw.evaluatedPassCount; + draw.hash = HashDraw( draw ); + } + + if ( sourceSlot != view.sourceSurfaceCount || packetCursor != packetEnd ) { + return FailView( view, CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor, sourceSlot, packetCursor, + passPacketIndex ); + } + + view.firstDraw = drawCheckpoint; + view.drawCount = view.sourceSurfaceCount; + view.drawableSurfaceCount = drawableSurfaces; + view.noopSurfaceCount = noopSurfaces; + view.firstEvaluatedPass = passCheckpoint; + view.evaluatedPassCount = stagedPassCount; + for ( int i = 0; i < view.drawCount; ++i ) { + const classicGuiDomainDraw_t &draw = domain.draws[ view.firstDraw + i ]; + view.activePassCount += draw.activePassCount; + view.drawablePassCount += draw.drawablePassCount; + view.inactivePassCount += draw.inactivePassCount; + view.activeNoopPassCount += draw.activeNoopPassCount; + view.noopPassCount += draw.noopPassCount; + if ( draw.deformContractHash != 0 ) { + view.materialDeformSurfaceCount++; + if ( draw.deformOutcome == CLASSIC_DEFORM_OUTCOME_COMPLETED ) { + view.completedDeformSurfaceCount++; + } else if ( draw.deformOutcome == CLASSIC_DEFORM_OUTCOME_EMPTY ) { + view.emptyDeformSurfaceCount++; + } + } + } + domain.drawCount += view.drawCount; + domain.passCount += view.evaluatedPassCount; + view.hash = HashView( view ); + view.failure = CLASSIC_GUI_DOMAIN_FAILURE_NONE; + view.ready = true; + domain.stats.readyViews++; + domain.stats.drawableSurfaces += view.drawableSurfaceCount; + domain.stats.noopSurfaces += view.noopSurfaceCount; + domain.stats.draws += view.drawCount; + domain.stats.evaluatedPasses += view.evaluatedPassCount; + domain.stats.activePasses += view.activePassCount; + domain.stats.drawablePasses += view.drawablePassCount; + domain.stats.inactivePasses += view.inactivePassCount; + domain.stats.activeNoopPasses += view.activeNoopPassCount; + domain.stats.noopPasses += view.noopPassCount; + domain.stats.materialDeformSurfaces += view.materialDeformSurfaceCount; + domain.stats.completedDeformSurfaces += view.completedDeformSurfaceCount; + domain.stats.emptyDeformSurfaces += view.emptyDeformSurfaceCount; + return true; +} + +static int ViewIndex( const classicGuiDomainView_t *view ) { + // O(1) equivalent of the linear pool scan: a pointer identifies a live view + // exactly when it is element [index] of the pool with index < viewCount. + const auto index = view - domain.views; + return index >= 0 && index < domain.viewCount + && view == &domain.views[ index ] ? static_cast( index ) : -1; +} + +static int DrawIndex( const classicGuiDomainDraw_t *draw ) { + const auto index = draw - domain.draws; + return index >= 0 && index < domain.drawCount + && draw == &domain.draws[ index ] ? static_cast( index ) : -1; +} + +static classicGuiDomainView_t *FindMutableView( const viewDef_t *viewDef ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].viewDef == viewDef ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +static bool SceneContainsGuiPass( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene ) { + if ( scene.viewDef == NULL ) { + return false; + } + if ( scene.viewDef->viewEntitys != NULL ) { + if ( !r_rendererSharedInWorldGui.GetBool() ) { + return false; + } + } else if ( !r_rendererSharedGui.GetBool() ) { + return false; + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) ) { + // Preserve a diagnostic descriptor for a malformed 2D draw scene. Valid + // scenes are identified by their GUI pass rather than the broad scene + // packet category, which is WORLD for ordinary 2D command lists. + return scene.viewDef->viewEntitys == NULL; + } + for ( int passIndex = 0; passIndex < scene.passPacketCount; ++passIndex ) { + if ( packetFrame.Pass( scene.firstPassPacket + passIndex ).passCategory + == RENDER_PASS_GUI ) { + return true; + } + } + return false; +} + +static void RecordFallback( classicGuiDomainView_t *view, + classicGuiDomainBackend_t backend, classicGuiDomainFailure_t failure, + int detail, int drawnPasses, int noopPasses ) { + classicGuiDomainBackendCoverage_t &coverage = domain.stats.backend[ backend ]; + if ( view == NULL ) { + coverage.untrackedFallbacks++; + return; + } + const int viewIndex = ViewIndex( view ); + if ( viewIndex < 0 || view->backendOutcome[ backend ] != CLASSIC_GUI_DOMAIN_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + view->backendOutcome[ backend ] = CLASSIC_GUI_DOMAIN_BACKEND_FALLBACK; + view->backendFailure[ backend ] = failure == CLASSIC_GUI_DOMAIN_FAILURE_NONE + ? CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED : failure; + view->backendFailureDetail[ backend ] = detail; + view->backendDrawnPasses[ backend ] = drawnPasses; + view->backendNoopPasses[ backend ] = noopPasses; + coverage.fallbackViewMask |= 1ull << viewIndex; + coverage.fallbackViews++; + coverage.fallbackSourceSurfaces += view->sourceSurfaceCount; + coverage.fallbackDrawablePasses += view->drawablePassCount; + coverage.fallbackNoopPasses += view->noopPassCount; + if ( failure == CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH ) { + coverage.coverageMismatches++; + } + if ( view->backendFailure[ backend ] >= CLASSIC_GUI_DOMAIN_FAILURE_NONE + && view->backendFailure[ backend ] < CLASSIC_GUI_DOMAIN_FAILURE_COUNT ) { + domain.stats.failureCounts[ view->backendFailure[ backend ] ]++; + } +} + +} // namespace + +void R_ClassicGuiDomain_ResetFrame( void ) { + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.viewCount = 0; + domain.drawCount = 0; + domain.passCount = 0; + idStr::Copynz( domain.stats.status, "empty", sizeof( domain.stats.status ) ); +} + +void R_ClassicGuiDomain_PrepareFrame( const idScenePacketFrame &packetFrame ) { + R_ClassicGuiDomain_ResetFrame(); + domain.stats.prepared = true; + domain.stats.sourceScenes = packetFrame.NumScenes(); + domain.stats.overflow = packetFrame.Stats().overflow; + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !SceneContainsGuiPass( packetFrame, scene ) ) { + continue; + } + if ( FindMutableView( scene.viewDef ) != NULL ) { + continue; + } + domain.stats.guiViews++; + if ( scene.viewDef != NULL && scene.viewDef->viewEntitys != NULL ) { + domain.stats.inWorldViews++; + } else { + domain.stats.rootViews++; + } + if ( domain.viewCount >= CLASSIC_GUI_DOMAIN_MAX_VIEWS ) { + domain.stats.overflow = true; + domain.stats.fallbackViews++; + domain.stats.failureCounts[ CLASSIC_GUI_DOMAIN_FAILURE_VIEW_POOL_OVERFLOW ]++; + continue; + } + classicGuiDomainView_t &view = domain.views[ domain.viewCount++ ]; + InitView( view, scene.viewDef, sceneIndex ); + if ( view.sourceSurfaceCount > 0 ) { + domain.stats.sourceSurfaces += view.sourceSurfaceCount; + } + R_ClassicGuiDomain_PrepareView( packetFrame, scene, view ); + } + + domain.stats.frameValid = !domain.stats.overflow + && domain.stats.fallbackViews == 0; + std::uint64_t frameHash = HASH_OFFSET; + HashInt( frameHash, domain.stats.sourceScenes ); + HashInt( frameHash, domain.stats.guiViews ); + HashInt( frameHash, domain.stats.rootViews ); + HashInt( frameHash, domain.stats.inWorldViews ); + for ( int i = 0; i < domain.viewCount; ++i ) { + HashInt( frameHash, domain.views[ i ].scenePacketIndex ); + HashInt( frameHash, domain.views[ i ].scope ); + HashBool( frameHash, domain.views[ i ].ready ); + HashInt( frameHash, domain.views[ i ].failure ); + HashU64( frameHash, domain.views[ i ].hash ); + } + domain.stats.hash = frameHash; + if ( domain.stats.guiViews == 0 ) { + idStr::Copynz( domain.stats.status, "empty", sizeof( domain.stats.status ) ); + } else if ( domain.stats.frameValid ) { + idStr::Copynz( domain.stats.status, "ready", sizeof( domain.stats.status ) ); + } else if ( domain.stats.readyViews == 0 ) { + idStr::Copynz( domain.stats.status, "fallback", sizeof( domain.stats.status ) ); + } else { + idStr::Copynz( domain.stats.status, "mixed-view-fallback", sizeof( domain.stats.status ) ); + } +} + +const classicGuiDomainStats_t &R_ClassicGuiDomain_Stats( void ) { + return domain.stats; +} + +int R_ClassicGuiDomain_NumViews( void ) { + return domain.viewCount; +} + +const classicGuiDomainView_t *R_ClassicGuiDomain_ViewByIndex( int index ) { + return index >= 0 && index < domain.viewCount ? &domain.views[ index ] : NULL; +} + +const classicGuiDomainView_t *R_ClassicGuiDomain_ViewForScenePacket( int scenePacketIndex ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].scenePacketIndex == scenePacketIndex ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +const classicGuiDomainView_t *R_ClassicGuiDomain_FindView( const viewDef_t *viewDef ) { + return FindMutableView( viewDef ); +} + +const classicGuiDomainView_t *R_ClassicGuiDomain_FindRootView( + const viewDef_t *viewDef ) { + classicGuiDomainView_t *view = FindMutableView( viewDef ); + return view != NULL && view->scope == CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D + ? view : NULL; +} + +const classicGuiDomainView_t *R_ClassicGuiDomain_FindInWorldView( + const viewDef_t *viewDef ) { + classicGuiDomainView_t *view = FindMutableView( viewDef ); + return view != NULL && view->scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD + ? view : NULL; +} + +const classicGuiDomainDraw_t *R_ClassicGuiDomain_ViewDraw( + const classicGuiDomainView_t &view, int drawIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || drawIndex < 0 + || drawIndex >= view.drawCount + || !RangeFits( view.firstDraw, view.drawCount, domain.drawCount ) ) { + return NULL; + } + return &domain.draws[ view.firstDraw + drawIndex ]; +} + +const rendererEvaluatedMaterialPass_t *R_ClassicGuiDomain_DrawPass( + const classicGuiDomainDraw_t &draw, int passIndex ) { + if ( DrawIndex( &draw ) < 0 || passIndex < 0 || passIndex >= draw.evaluatedPassCount + || !RangeFits( draw.firstEvaluatedPass, draw.evaluatedPassCount, + domain.passCount ) ) { + return NULL; + } + return &domain.passes[ draw.firstEvaluatedPass + passIndex ]; +} + +const materialResourceTextureBinding_t *R_ClassicGuiDomain_DrawPassTexture( + const classicGuiDomainDraw_t &draw, int passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicGuiDomain_DrawPass( draw, passIndex ); + return pass != NULL + ? R_MaterialResourceTable_ResolveTextureResource( pass->textureResourceId ) : NULL; +} + +bool R_ClassicGuiDomain_RecordOwned( const viewDef_t *viewDef, + classicGuiDomainBackend_t backend, int drawnPasses, int noopPasses ) { + if ( backend < CLASSIC_GUI_DOMAIN_BACKEND_GL + || backend >= CLASSIC_GUI_DOMAIN_BACKEND_COUNT ) { + return false; + } + classicGuiDomainView_t *view = FindMutableView( viewDef ); + if ( view == NULL ) { + return false; + } + classicGuiDomainBackendCoverage_t &coverage = domain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] != CLASSIC_GUI_DOMAIN_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return view->backendOutcome[ backend ] == CLASSIC_GUI_DOMAIN_BACKEND_OWNED + && view->backendDrawnPasses[ backend ] == drawnPasses + && view->backendNoopPasses[ backend ] == noopPasses; + } + if ( !view->ready ) { + RecordFallback( view, backend, CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + view->failure, drawnPasses, noopPasses ); + return false; + } + if ( drawnPasses != view->drawablePassCount || noopPasses != view->noopPassCount ) { + RecordFallback( view, backend, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH, + drawnPasses, drawnPasses, noopPasses ); + return false; + } + const int viewIndex = ViewIndex( view ); + view->backendOutcome[ backend ] = CLASSIC_GUI_DOMAIN_BACKEND_OWNED; + view->backendDrawnPasses[ backend ] = drawnPasses; + view->backendNoopPasses[ backend ] = noopPasses; + coverage.ownedViewMask |= 1ull << viewIndex; + coverage.ownedViews++; + coverage.ownedSourceSurfaces += view->sourceSurfaceCount; + coverage.ownedDrawablePasses += drawnPasses; + coverage.ownedNoopPasses += noopPasses; + return true; +} + +void R_ClassicGuiDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicGuiDomainBackend_t backend, classicGuiDomainFailure_t failure, + int detail ) { + if ( backend < CLASSIC_GUI_DOMAIN_BACKEND_GL + || backend >= CLASSIC_GUI_DOMAIN_BACKEND_COUNT ) { + return; + } + RecordFallback( FindMutableView( viewDef ), backend, failure, detail, 0, 0 ); +} + +const classicGuiDomainBackendCoverage_t &R_ClassicGuiDomain_BackendCoverage( + classicGuiDomainBackend_t backend ) { + static const classicGuiDomainBackendCoverage_t empty = {}; + return backend >= CLASSIC_GUI_DOMAIN_BACKEND_GL + && backend < CLASSIC_GUI_DOMAIN_BACKEND_COUNT + ? domain.stats.backend[ backend ] : empty; +} + +bool R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( + const viewDef_t *viewDef, classicGuiDomainBackend_t backend, + const drawSurf_t *drawSurf ) { + if ( backend < CLASSIC_GUI_DOMAIN_BACKEND_GL + || backend >= CLASSIC_GUI_DOMAIN_BACKEND_COUNT || drawSurf == NULL + || ( drawSurf->dsFlags & DSF_IN_WORLD_GUI ) == 0 ) { + return false; + } + const classicGuiDomainView_t *view = R_ClassicGuiDomain_FindInWorldView( + viewDef ); + if ( view == NULL || view->backendOutcome[ backend ] + != CLASSIC_GUI_DOMAIN_BACKEND_OWNED ) { + return false; + } + for ( int i = 0; i < view->drawCount; ++i ) { + const classicGuiDomainDraw_t *draw = R_ClassicGuiDomain_ViewDraw( + *view, i ); + if ( draw != NULL && draw->legacyDrawSurf == drawSurf ) { + return true; + } + } + return false; +} + +const char *ClassicGuiDomainSourceSurface_Name( + classicGuiDomainSourceSurface_t sourceSurface ) { + switch ( sourceSurface ) { + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE: return "drawable"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NULL_SURFACE: return "noopNullSurface"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: return "noopMissingMaterial"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NO_AMBIENT: return "noopNoAmbient"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_PORTAL_SKY: return "noopPortalSky"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: return "noopEmptyGeometry"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW: return "fallbackSuppressedInSubview"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_MISSING_SPACE: return "fallbackMissingSpace"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DEPTH_HACK: return "fallbackDepthHack"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DECAL_COLOR_STREAM: return "fallbackDecalColorStream"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE: return "fallbackNegativeScale"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_POST_PROCESS: return "fallbackPostProcess"; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicGuiDomainFailure_Name( classicGuiDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_GUI_DOMAIN_FAILURE_NONE: return "none"; + case CLASSIC_GUI_DOMAIN_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_GUI_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW: return "scenePacketOverflow"; + case CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_NOT_PREPARED: return "materialTableNotPrepared"; + case CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_OVERFLOW: return "materialTableOverflow"; + case CLASSIC_GUI_DOMAIN_FAILURE_VIEW_POOL_OVERFLOW: return "viewPoolOverflow"; + case CLASSIC_GUI_DOMAIN_FAILURE_DRAW_POOL_OVERFLOW: return "drawPoolOverflow"; + case CLASSIC_GUI_DOMAIN_FAILURE_EVALUATED_PASS_POOL_OVERFLOW: return "evaluatedPassPoolOverflow"; + case CLASSIC_GUI_DOMAIN_FAILURE_UNSUPPORTED_VIEW: return "unsupportedView"; + case CLASSIC_GUI_DOMAIN_FAILURE_INVALID_SCENE_RANGE: return "invalidSceneRange"; + case CLASSIC_GUI_DOMAIN_FAILURE_INVALID_GUI_PASS: return "invalidGuiPass"; + case CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_RANGE: return "invalidDrawRange"; + case CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_SURFACE_FALLBACK: return "sourceSurfaceFallback"; + case CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH: return "sourcePacketMismatch"; + case CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_PACKET: return "invalidDrawPacket"; + case CLASSIC_GUI_DOMAIN_FAILURE_DEFORM_CONTRACT: return "deformContract"; + case CLASSIC_GUI_DOMAIN_FAILURE_MISSING_GEOMETRY_RECORD: return "missingGeometryRecord"; + case CLASSIC_GUI_DOMAIN_FAILURE_MISSING_INSTANCE_RECORD: return "missingInstanceRecord"; + case CLASSIC_GUI_DOMAIN_FAILURE_MISSING_MATERIAL_RECORD: return "missingMaterialRecord"; + case CLASSIC_GUI_DOMAIN_FAILURE_STALE_MATERIAL_RECORD: return "staleMaterialRecord"; + case CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_INELIGIBLE: return "materialPassIneligible"; + case CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_COPY_FAILED: return "materialPassCopyFailed"; + case CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_EVALUATION_FAILED: return "materialPassEvaluationFailed"; + case CLASSIC_GUI_DOMAIN_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE: return "textureResourceUnavailable"; + case CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DISPOSITION: return "invalidDisposition"; + case CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY: return "backendNotReady"; + case CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH: return "backendCoverageMismatch"; + case CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED: return "backendRejected"; + case CLASSIC_GUI_DOMAIN_FAILURE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicGuiDomainBackend_Name( classicGuiDomainBackend_t backend ) { + switch ( backend ) { + case CLASSIC_GUI_DOMAIN_BACKEND_GL: return "GL"; + case CLASSIC_GUI_DOMAIN_BACKEND_VULKAN: return "Vulkan"; + case CLASSIC_GUI_DOMAIN_BACKEND_COUNT: + default: return "unknown"; + } +} + +bool RendererClassicGuiDomain_RunSelfTest( void ) { + if ( CLASSIC_GUI_DOMAIN_MAX_VIEWS != 64 + || CLASSIC_GUI_DOMAIN_MAX_DRAWS != 4096 + || CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES != 8192 + || !RangeFits( 8191, 1, 8192 ) || RangeFits( 8192, 1, 8192 ) + || RangeFits( -1, 1, 8192 ) + || idStr::Cmp( ClassicGuiDomainFailure_Name( + CLASSIC_GUI_DOMAIN_FAILURE_DEFORM_CONTRACT ), + "deformContract" ) ) { + return false; + } + rendererEvaluatedMaterialPass_t first; + std::memset( &first, 0, sizeof( first ) ); + first.sourceStageIndex = 7; + first.kind = RENDERER_MATERIAL_PASS_GUI; + first.textureResourceId = 0x123400020003ull; + first.condition = 1.0f; + first.active = true; + first.disposition = RENDERER_MATERIAL_PASS_DRAW; + first.color[ 0 ] = 1.0f; + first.textureMatrix[ 0 ] = 1.0f; + first.textureMatrix[ 4 ] = 1.0f; + first.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA; + first.blend.destinationColor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + first.depth.compareOperation = RENDERER_COMPARE_ALWAYS; + first.alphaTestCompareOperation = RENDERER_COMPARE_GREATER; + const std::uint64_t firstHash = HashPass( first ); + rendererEvaluatedMaterialPass_t same = first; + if ( firstHash == 0 || HashPass( same ) != firstHash ) { + return false; + } + same.color[ 0 ] = 0.5f; + if ( HashPass( same ) == firstHash ) { + return false; + } + same = first; + same.textureResourceId += 1ull << 32; + if ( HashPass( same ) != firstHash + || idStr::Cmp( ClassicGuiDomainFailure_Name( + CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH ), + "sourcePacketMismatch" ) ) { + return false; + } + classicGuiDomainDraw_t deformHashDraw; + InitDraw( deformHashDraw ); + const std::uint64_t undeformedHash = HashDraw( deformHashDraw ); + deformHashDraw.deformRole = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + deformHashDraw.deformOutcome = CLASSIC_DEFORM_OUTCOME_COMPLETED; + deformHashDraw.deformContractHash = 0x51234ull; + if ( HashDraw( deformHashDraw ) == undeformedHash ) { + return false; + } + + classicGuiDomainDraw_t dispositionDraw; + InitDraw( dispositionDraw ); + rendererEvaluatedMaterialPass_t dispositionPass = first; + dispositionPass.active = false; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_INACTIVE_CONDITION; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.active = true; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_DRAW; + if ( !CountDisposition( dispositionDraw, dispositionPass ) + || dispositionDraw.activePassCount != 4 + || dispositionDraw.drawablePassCount != 1 + || dispositionDraw.inactivePassCount != 1 + || dispositionDraw.activeNoopPassCount != 3 + || dispositionDraw.noopPassCount != 4 ) { + return false; + } + dispositionPass.disposition = + static_cast( 0x7fffffff ); + if ( CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + InitDraw( dispositionDraw ); + dispositionPass.active = false; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_DRAW; + if ( CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + InitDraw( dispositionDraw ); + dispositionPass.active = true; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_INACTIVE_CONDITION; + if ( CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + + const classicGuiDomainStats_t savedStats = domain.stats; + classicGuiDomainView_t rollbackView; + InitView( rollbackView, NULL, -1 ); + rollbackView.ready = true; + rollbackView.firstDraw = 17; + rollbackView.drawCount = 3; + rollbackView.firstEvaluatedPass = 29; + rollbackView.evaluatedPassCount = 5; + rollbackView.activePassCount = 4; + rollbackView.drawablePassCount = 2; + rollbackView.inactivePassCount = 1; + rollbackView.activeNoopPassCount = 1; + rollbackView.noopPassCount = 2; + rollbackView.hash = 1; + const bool rollbackResult = FailView( rollbackView, + CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH, 41, 2, 7, 3 ); + const bool rollbackOk = !rollbackResult && !rollbackView.ready + && rollbackView.failure == CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH + && rollbackView.failureDetail == 41 + && rollbackView.failureSourceSurfaceIndex == 2 + && rollbackView.failureDrawPacketIndex == 7 + && rollbackView.failurePassPacketIndex == 3 + && rollbackView.firstDraw == -1 && rollbackView.drawCount == 0 + && rollbackView.firstEvaluatedPass == -1 + && rollbackView.evaluatedPassCount == 0 + && rollbackView.activePassCount == 0 + && rollbackView.drawablePassCount == 0 + && rollbackView.inactivePassCount == 0 + && rollbackView.activeNoopPassCount == 0 + && rollbackView.noopPassCount == 0 && rollbackView.hash == 0; + domain.stats = savedStats; + if ( !rollbackOk ) { + return false; + } + + const int savedViewCount = domain.viewCount; + const int savedDrawCount = domain.drawCount; + const classicGuiDomainView_t savedView = domain.views[ 0 ]; + const classicGuiDomainDraw_t savedDraw = domain.draws[ 0 ]; + viewDef_t coverageView; + std::memset( &coverageView, 0, sizeof( coverageView ) ); + InitView( domain.views[ 0 ], &coverageView, 0 ); + domain.viewCount = 1; + domain.views[ 0 ].ready = true; + domain.views[ 0 ].sourceSurfaceCount = 4; + domain.views[ 0 ].drawablePassCount = 2; + domain.views[ 0 ].noopPassCount = 3; + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + const bool firstOwned = R_ClassicGuiDomain_RecordOwned( &coverageView, + CLASSIC_GUI_DOMAIN_BACKEND_GL, 2, 3 ); + const bool repeatedOwned = R_ClassicGuiDomain_RecordOwned( &coverageView, + CLASSIC_GUI_DOMAIN_BACKEND_GL, 2, 3 ); + const bool mismatchRejected = !R_ClassicGuiDomain_RecordOwned( &coverageView, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, 1, 3 ); + const bool coverageOk = firstOwned && repeatedOwned && mismatchRejected + && domain.views[ 0 ].backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_GL ] + == CLASSIC_GUI_DOMAIN_BACKEND_OWNED + && domain.views[ 0 ].backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_VULKAN ] + == CLASSIC_GUI_DOMAIN_BACKEND_FALLBACK + && domain.stats.backend[ CLASSIC_GUI_DOMAIN_BACKEND_GL ].ownedViews == 1 + && domain.stats.backend[ CLASSIC_GUI_DOMAIN_BACKEND_GL ].duplicateReports == 1 + && domain.stats.backend[ CLASSIC_GUI_DOMAIN_BACKEND_VULKAN ].fallbackViews == 1 + && domain.stats.backend[ CLASSIC_GUI_DOMAIN_BACKEND_VULKAN ].coverageMismatches == 1; + + viewEntity_t inWorldEntity; + std::memset( &inWorldEntity, 0, sizeof( inWorldEntity ) ); + drawSurf_t inWorldSurface; + std::memset( &inWorldSurface, 0, sizeof( inWorldSurface ) ); + inWorldSurface.dsFlags = DSF_IN_WORLD_GUI; + drawSurf_t *inWorldSurfaces[] = { &inWorldSurface }; + viewDef_t inWorldView; + std::memset( &inWorldView, 0, sizeof( inWorldView ) ); + inWorldView.viewEntitys = &inWorldEntity; + inWorldView.drawSurfs = inWorldSurfaces; + inWorldView.numDrawSurfs = 1; + classicGuiDomainView_t scopeView; + InitView( scopeView, &inWorldView, 1 ); + const bool scopeOk = scopeView.scope + == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD + && scopeView.sourceSurfaceCount == 1 + && HashView( scopeView ) != 0; + + InitView( domain.views[ 0 ], &inWorldView, 1 ); + domain.viewCount = 1; + domain.drawCount = 1; + domain.views[ 0 ].ready = true; + domain.views[ 0 ].firstDraw = 0; + domain.views[ 0 ].drawCount = 1; + domain.views[ 0 ].backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_GL ] = + CLASSIC_GUI_DOMAIN_BACKEND_OWNED; + InitDraw( domain.draws[ 0 ] ); + domain.draws[ 0 ].legacyDrawSurf = &inWorldSurface; + const bool ownershipQueryOk = + R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( &inWorldView, + CLASSIC_GUI_DOMAIN_BACKEND_GL, &inWorldSurface ) + && !R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( &inWorldView, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, &inWorldSurface ); + domain.views[ 0 ] = savedView; + domain.viewCount = savedViewCount; + domain.draws[ 0 ] = savedDraw; + domain.drawCount = savedDrawCount; + domain.stats = savedStats; + return coverageOk && scopeOk && ownershipQueryOk; +} diff --git a/src/renderer/ClassicGuiDomain.h b/src/renderer/ClassicGuiDomain.h new file mode 100644 index 00000000..6ef01b92 --- /dev/null +++ b/src/renderer/ClassicGuiDomain.h @@ -0,0 +1,258 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_GUI_DOMAIN_H__ +#define __CLASSIC_GUI_DOMAIN_H__ + +#include "ClassicDeformDomain.h" +#include "MaterialResourceTable.h" + +/* +=============================================================================== + + Shared ownership contract for classic fixed-function GUI work. Root 2D + command views remain whole-view transactions; GUI quads emitted onto 3D + surfaces are an independently sealed, provenance-tagged subset. + + Preparation is transactional per view. A view is ready only when every + source surface has been classified, every expected scene packet matches in + original order, every material pass has evaluated, and every opaque texture + resource has resolved. Failed views retain diagnostics but expose no partial + draw or pass range. + +=============================================================================== +*/ + +const int CLASSIC_GUI_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; +const int CLASSIC_GUI_DOMAIN_MAX_DRAWS = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES = SCENE_PACKET_MAX_DRAWS * 2; + +enum classicGuiDomainScope_t { + CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D = 0, + CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD, + CLASSIC_GUI_DOMAIN_SCOPE_COUNT +}; + +enum classicGuiDomainSourceSurface_t { + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE = 0, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NULL_SURFACE, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_MISSING_MATERIAL, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NO_AMBIENT, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_PORTAL_SKY, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_MISSING_SPACE, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DEPTH_HACK, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DECAL_COLOR_STREAM, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_POST_PROCESS, + CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_COUNT +}; + +enum classicGuiDomainFailure_t { + CLASSIC_GUI_DOMAIN_FAILURE_NONE = 0, + CLASSIC_GUI_DOMAIN_FAILURE_UNAVAILABLE, + CLASSIC_GUI_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_NOT_PREPARED, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_TABLE_OVERFLOW, + CLASSIC_GUI_DOMAIN_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_GUI_DOMAIN_FAILURE_DRAW_POOL_OVERFLOW, + CLASSIC_GUI_DOMAIN_FAILURE_EVALUATED_PASS_POOL_OVERFLOW, + CLASSIC_GUI_DOMAIN_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_GUI_DOMAIN_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_GUI_DOMAIN_FAILURE_INVALID_GUI_PASS, + CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_RANGE, + CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_SURFACE_FALLBACK, + CLASSIC_GUI_DOMAIN_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DRAW_PACKET, + CLASSIC_GUI_DOMAIN_FAILURE_DEFORM_CONTRACT, + CLASSIC_GUI_DOMAIN_FAILURE_MISSING_GEOMETRY_RECORD, + CLASSIC_GUI_DOMAIN_FAILURE_MISSING_INSTANCE_RECORD, + CLASSIC_GUI_DOMAIN_FAILURE_MISSING_MATERIAL_RECORD, + CLASSIC_GUI_DOMAIN_FAILURE_STALE_MATERIAL_RECORD, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_INELIGIBLE, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_COPY_FAILED, + CLASSIC_GUI_DOMAIN_FAILURE_MATERIAL_PASS_EVALUATION_FAILED, + CLASSIC_GUI_DOMAIN_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE, + CLASSIC_GUI_DOMAIN_FAILURE_INVALID_DISPOSITION, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + CLASSIC_GUI_DOMAIN_FAILURE_COUNT +}; + +enum classicGuiDomainBackend_t { + CLASSIC_GUI_DOMAIN_BACKEND_GL = 0, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, + CLASSIC_GUI_DOMAIN_BACKEND_COUNT +}; + +enum classicGuiDomainBackendOutcome_t { + CLASSIC_GUI_DOMAIN_BACKEND_UNRECORDED = 0, + CLASSIC_GUI_DOMAIN_BACKEND_OWNED, + CLASSIC_GUI_DOMAIN_BACKEND_FALLBACK +}; + +typedef struct classicGuiDomainBackendCoverage_s { + std::uint64_t ownedViewMask; + std::uint64_t fallbackViewMask; + int ownedViews; + int fallbackViews; + int ownedSourceSurfaces; + int fallbackSourceSurfaces; + int ownedDrawablePasses; + int fallbackDrawablePasses; + int ownedNoopPasses; + int fallbackNoopPasses; + int coverageMismatches; + int duplicateReports; + int untrackedFallbacks; +} classicGuiDomainBackendCoverage_t; + +typedef struct classicGuiDomainDraw_s { + int sourceSurfaceIndex; + classicGuiDomainSourceSurface_t sourceSurface; + // This is the sole legacy bridge retained by the domain. Consumers may use + // it for geometry submission only; material stages and shader registers are + // sealed into the evaluated pass range during preparation. + const drawSurf_t *legacyDrawSurf; + int drawPacketIndex; + int materialTableRecordIndex; + int materialId; + std::uint32_t tableGeneration; + int firstGuiPass; + int guiPassCount; + int firstWorldPass; + int worldPassCount; + int firstEvaluatedPass; + int evaluatedPassCount; + int activePassCount; + int drawablePassCount; + int inactivePassCount; + int activeNoopPassCount; + int noopPassCount; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float modelViewMatrix[ 16 ]; + bool packetBacked; + bool hasIndexCache; + bool hasAmbientCache; + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicGuiDomainDraw_t; + +typedef struct classicGuiDomainView_s { + const viewDef_t *viewDef; + classicGuiDomainScope_t scope; + int scenePacketIndex; + int guiPassPacketIndex; + std::uint32_t tableGeneration; + int firstDraw; + int drawCount; + int sourceSurfaceCount; + int drawableSurfaceCount; + int noopSurfaceCount; + int packetDrawCount; + int firstEvaluatedPass; + int evaluatedPassCount; + int activePassCount; + int drawablePassCount; + int inactivePassCount; + int activeNoopPassCount; + int noopPassCount; + int materialDeformSurfaceCount; + int completedDeformSurfaceCount; + int emptyDeformSurfaceCount; + int viewportX1; + int viewportY1; + int viewportX2; + int viewportY2; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float projectionMatrix[ 16 ]; + bool ready; + classicGuiDomainFailure_t failure; + int failureDetail; + materialResourceGuiPassFailure_t guiPassFailure; + rendererMaterialPassEvaluationStatus_t evaluationStatus; + int failurePassPacketIndex; + int failureDrawPacketIndex; + int failureSourceSurfaceIndex; + int failureSourceStageIndex; + std::uint64_t hash; + classicGuiDomainBackendOutcome_t backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; + classicGuiDomainFailure_t backendFailure[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; + int backendFailureDetail[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; + int backendDrawnPasses[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; + int backendNoopPasses[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; +} classicGuiDomainView_t; + +typedef struct classicGuiDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int guiViews; + int rootViews; + int inWorldViews; + int readyViews; + int fallbackViews; + int sourceSurfaces; + int drawableSurfaces; + int noopSurfaces; + int draws; + int evaluatedPasses; + int activePasses; + int drawablePasses; + int inactivePasses; + int activeNoopPasses; + int noopPasses; + int materialDeformSurfaces; + int completedDeformSurfaces; + int emptyDeformSurfaces; + int failureCounts[ CLASSIC_GUI_DOMAIN_FAILURE_COUNT ]; + std::uint64_t hash; + classicGuiDomainBackendCoverage_t backend[ CLASSIC_GUI_DOMAIN_BACKEND_COUNT ]; + char status[ 96 ]; +} classicGuiDomainStats_t; + +void R_ClassicGuiDomain_ResetFrame( void ); +void R_ClassicGuiDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicGuiDomainStats_t &R_ClassicGuiDomain_Stats( void ); +int R_ClassicGuiDomain_NumViews( void ); +const classicGuiDomainView_t *R_ClassicGuiDomain_ViewByIndex( int index ); +const classicGuiDomainView_t *R_ClassicGuiDomain_ViewForScenePacket( int scenePacketIndex ); +const classicGuiDomainView_t *R_ClassicGuiDomain_FindView( const viewDef_t *viewDef ); +const classicGuiDomainView_t *R_ClassicGuiDomain_FindRootView( const viewDef_t *viewDef ); +const classicGuiDomainView_t *R_ClassicGuiDomain_FindInWorldView( const viewDef_t *viewDef ); +const classicGuiDomainDraw_t *R_ClassicGuiDomain_ViewDraw( + const classicGuiDomainView_t &view, int drawIndex ); +const rendererEvaluatedMaterialPass_t *R_ClassicGuiDomain_DrawPass( + const classicGuiDomainDraw_t &draw, int passIndex ); +const materialResourceTextureBinding_t *R_ClassicGuiDomain_DrawPassTexture( + const classicGuiDomainDraw_t &draw, int passIndex ); +bool R_ClassicGuiDomain_RecordOwned( const viewDef_t *viewDef, + classicGuiDomainBackend_t backend, int drawnPasses, int noopPasses ); +void R_ClassicGuiDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicGuiDomainBackend_t backend, classicGuiDomainFailure_t failure, int detail ); +const classicGuiDomainBackendCoverage_t &R_ClassicGuiDomain_BackendCoverage( + classicGuiDomainBackend_t backend ); +bool R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( const viewDef_t *viewDef, + classicGuiDomainBackend_t backend, const drawSurf_t *drawSurf ); +const char *ClassicGuiDomainSourceSurface_Name( + classicGuiDomainSourceSurface_t sourceSurface ); +const char *ClassicGuiDomainFailure_Name( classicGuiDomainFailure_t failure ); +const char *ClassicGuiDomainBackend_Name( classicGuiDomainBackend_t backend ); +bool RendererClassicGuiDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_GUI_DOMAIN_H__ */ diff --git a/src/renderer/ClassicInteractionDomain.cpp b/src/renderer/ClassicInteractionDomain.cpp new file mode 100644 index 00000000..94ec2baf --- /dev/null +++ b/src/renderer/ClassicInteractionDomain.cpp @@ -0,0 +1,5340 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "CelShading.h" +#include "ClassicInteractionDomain.h" + +#include +#include +#include +#include +#include + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; + +typedef struct classicInteractionDomainState_s { + classicInteractionDomainView_t views[ CLASSIC_INTERACTION_DOMAIN_MAX_VIEWS ]; + classicInteractionDomainLight_t lights[ CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ]; + classicInteractionDomainSurface_t surfaces[ CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES ]; + classicInteractionDomainPrimitive_t primitives[ CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ]; + classicInteractionDomainShadowCaster_t shadowCasters[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + classicInteractionDomainShadowAlphaStage_t shadowAlphaStages[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES ]; + classicInteractionDomainShadowMapPass_t shadowMapPasses[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES ]; + classicInteractionDomainTexture_t textures[ CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES ]; + classicInteractionDomainStats_t stats; + std::uint32_t generation; + int viewCount; + int lightCount; + int surfaceCount; + int primitiveCount; + int shadowCasterCount; + int shadowAlphaStageCount; + int shadowMapPassCount; + int textureCount; +} classicInteractionDomainState_t; + +typedef struct classicInteractionCheckpoint_s { + int lightCount; + int surfaceCount; + int primitiveCount; + int shadowCasterCount; + int shadowAlphaStageCount; + int shadowMapPassCount; + int textureCount; +} classicInteractionCheckpoint_t; + +typedef struct classicInteractionBuildError_s { + classicInteractionDomainFailure_t failure; + int detail; + int passPacketIndex; + int drawPacketIndex; + int lightOrdinal; + int receiverOrdinal; + int stageIndex; +} classicInteractionBuildError_t; + +typedef struct classicInteractionWork_s { + int bumpStageIndex; + int diffuseStageIndex; + int specularStageIndex; + const idImage *bumpImage; + const idImage *diffuseImage; + const idImage *specularImage; + std::uint64_t bumpImageResourceId; + std::uint64_t diffuseImageResourceId; + std::uint64_t specularImageResourceId; + float diffuseColor[ 4 ]; + float specularColor[ 4 ]; + float bumpMatrix[ 2 ][ 4 ]; + float diffuseMatrix[ 2 ][ 4 ]; + float specularMatrix[ 2 ][ 4 ]; + rendererVertexColorMode_t vertexColor; +} classicInteractionWork_t; + +classicInteractionDomainState_t domain; + +// Per-frame memo of AddTexture linear-scan results. Entries are only ever +// copies of authoritative pool-scan results; any pool shrink (frame reset or +// whole-view rollback in FailView) empties the memo by bumping the stamp, so +// a memo hit can never observe rolled-back or stale state. +const int TEXTURE_LOOKUP_SIZE = CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES * 2; +static_assert( ( TEXTURE_LOOKUP_SIZE & ( TEXTURE_LOOKUP_SIZE - 1 ) ) == 0, + "texture lookup size must be a power of two" ); + +typedef struct classicInteractionTextureLookupSlot_s { + const idImage *image; + std::uint64_t storageGeneration; + int textureIndex; + std::uint32_t stamp; +} classicInteractionTextureLookupSlot_t; + +classicInteractionTextureLookupSlot_t textureLookup[ TEXTURE_LOOKUP_SIZE ]; +std::uint32_t textureLookupStamp; + +static void InvalidateTextureLookup( void ) { + textureLookupStamp++; + if ( textureLookupStamp == 0 ) { + std::memset( textureLookup, 0, sizeof( textureLookup ) ); + textureLookupStamp = 1; + } +} + +static int TextureLookupSlot( const idImage *image, + std::uint64_t storageGeneration ) { + std::uint64_t key = static_cast( + reinterpret_cast( image ) ); + key ^= storageGeneration * HASH_PRIME; + key *= HASH_PRIME; + int slot = static_cast( key & ( TEXTURE_LOOKUP_SIZE - 1 ) ); + for ( int probe = 0; probe < TEXTURE_LOOKUP_SIZE; ++probe ) { + const classicInteractionTextureLookupSlot_t &entry = + textureLookup[ slot ]; + if ( entry.stamp != textureLookupStamp + || ( entry.image == image + && entry.storageGeneration == storageGeneration ) ) { + return slot; + } + slot = ( slot + 1 ) & ( TEXTURE_LOOKUP_SIZE - 1 ); + } + return -1; +} + +static bool RangeFits( int first, int count, int capacity ) { + return first >= 0 && count >= 0 && first <= capacity + && count <= capacity - first; +} + +static bool FloatIsFinite( float value ) { + return std::isfinite( value ) != 0; +} + +static bool FloatsAreFinite( const float *values, int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !FloatIsFinite( values[ i ] ) ) { + return false; + } + } + return true; +} + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + hash ^= value; + hash *= HASH_PRIME; +} + +static void HashU64( std::uint64_t &hash, std::uint64_t value ) { + hash ^= value; + hash *= HASH_PRIME; +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), "float hash width mismatch" ); + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static std::uint64_t HashString( const char *text ) { + std::uint64_t hash = HASH_OFFSET; + if ( text != NULL ) { + for ( const unsigned char *cursor = + reinterpret_cast( text ); + *cursor != 0; ++cursor ) { + HashByte( hash, *cursor ); + } + } + return hash; +} + +static std::uint64_t MakeTextureResourceId( int textureIndex ) { + if ( domain.generation == 0 || textureIndex < 0 + || textureIndex >= CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES ) { + return 0; + } + return ( static_cast( domain.generation ) << 32 ) + | static_cast( textureIndex + 1 ); +} + +static int TextureIndexFromResourceId( std::uint64_t resourceId ) { + if ( resourceId == 0 + || static_cast( resourceId >> 32 ) + != domain.generation ) { + return -1; + } + const std::uint32_t encoded = static_cast( resourceId ); + if ( encoded == 0 || encoded > CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES ) { + return -1; + } + return static_cast( encoded - 1 ); +} + +static void SetError( classicInteractionBuildError_t &error, + classicInteractionDomainFailure_t failure, int detail = 0, + int passPacketIndex = -1, int drawPacketIndex = -1, + int lightOrdinal = -1, int receiverOrdinal = -1, + int stageIndex = -1 ) { + error.failure = failure; + error.detail = detail; + error.passPacketIndex = passPacketIndex; + error.drawPacketIndex = drawPacketIndex; + error.lightOrdinal = lightOrdinal; + error.receiverOrdinal = receiverOrdinal; + error.stageIndex = stageIndex; +} + +static void InitError( classicInteractionBuildError_t &error ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONE ); +} + +static void InitTextureMatrix( float matrix[ 2 ][ 4 ] ) { + std::memset( matrix, 0, sizeof( float ) * 8 ); + matrix[ 0 ][ 0 ] = 1.0f; + matrix[ 1 ][ 1 ] = 1.0f; +} + +static void InitWork( classicInteractionWork_t &work ) { + std::memset( &work, 0, sizeof( work ) ); + work.bumpStageIndex = -1; + work.diffuseStageIndex = -1; + work.specularStageIndex = -1; + work.vertexColor = RENDERER_VERTEX_COLOR_IGNORE; + InitTextureMatrix( work.bumpMatrix ); + InitTextureMatrix( work.diffuseMatrix ); + InitTextureMatrix( work.specularMatrix ); +} + +static void InitPrimitive( classicInteractionDomainPrimitive_t &primitive ) { + std::memset( &primitive, 0, sizeof( primitive ) ); + primitive.lightIndex = -1; + primitive.surfaceIndex = -1; + primitive.lightStageIndex = -1; + primitive.bumpStageIndex = -1; + primitive.diffuseStageIndex = -1; + primitive.specularStageIndex = -1; + primitive.receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + primitive.disposition = CLASSIC_INTERACTION_PRIMITIVE_NOOP_MISSING_BUMP; + primitive.depth = CLASSIC_INTERACTION_DEPTH_EQUAL; + primitive.cull = RENDERER_CULL_FRONT; + primitive.vertexColor = RENDERER_VERTEX_COLOR_IGNORE; + primitive.blend.enabled = true; + primitive.blend.sourceColor = RENDERER_BLEND_ONE; + primitive.blend.destinationColor = RENDERER_BLEND_ONE; + primitive.blend.colorOperation = RENDERER_BLEND_OP_ADD; + primitive.blend.sourceAlpha = RENDERER_BLEND_ONE; + primitive.blend.destinationAlpha = RENDERER_BLEND_ONE; + primitive.blend.alphaOperation = RENDERER_BLEND_OP_ADD; + InitTextureMatrix( primitive.bumpMatrix ); + InitTextureMatrix( primitive.diffuseMatrix ); + InitTextureMatrix( primitive.specularMatrix ); +} + +static void InitSurface( classicInteractionDomainSurface_t &surface ) { + std::memset( &surface, 0, sizeof( surface ) ); + surface.drawPacketIndex = -1; + surface.lightIndex = -1; + surface.sourceOrdinal = -1; + surface.receiverOrdinal = -1; + surface.receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + surface.materialTableRecordIndex = -1; + surface.materialId = -1; + surface.firstPrimitive = -1; +} + +static void InitShadowAlphaStage( + classicInteractionDomainShadowAlphaStage_t &stage ) { + std::memset( &stage, 0, sizeof( stage ) ); + stage.casterIndex = -1; + stage.stageIndex = -1; + InitTextureMatrix( stage.textureMatrix ); +} + +static void InitShadowCaster( + classicInteractionDomainShadowCaster_t &caster ) { + std::memset( &caster, 0, sizeof( caster ) ); + caster.drawPacketIndex = -1; + caster.lightIndex = -1; + caster.sourceOrdinal = -1; + caster.chainOrdinal = -1; + caster.geometryRecordIndex = -1; + caster.instanceRecordIndex = -1; + caster.materialTableRecordIndex = -1; + caster.materialId = -1; + caster.chain = CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + caster.disposition = CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY; + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_FULL; + caster.cull = RENDERER_CULL_FRONT; + caster.firstAlphaStage = -1; + caster.perfectHull = false; +} + +static void InitShadowMapPass( + classicInteractionDomainShadowMapPass_t &pass ) { + std::memset( &pass, 0, sizeof( pass ) ); + pass.lightIndex = -1; + pass.receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + pass.resourceOwner = CLASSIC_INTERACTION_RECEIVER_LOCAL; + pass.disposition = CLASSIC_INTERACTION_SHADOW_MAP_PASS_UNUSED; + pass.mode = CLASSIC_INTERACTION_SHADOW_NONE; + pass.lightClass = SHADOWMAP_LIGHT_PROJECTED; + R_ShadowMapResetProjectedLightState( pass.projected.state ); + pass.point.faceCount = 6; +} + +static void InitLight( classicInteractionDomainLight_t &light ) { + std::memset( &light, 0, sizeof( light ) ); + light.sourceOrdinal = -1; + light.firstSurface = -1; + light.firstPrimitive = -1; + for ( int chain = 0; chain < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; + ++chain ) { + light.firstShadowCaster[ chain ] = -1; + } + light.shadowMapPassIndex[ CLASSIC_INTERACTION_RECEIVER_LOCAL ] = -1; + light.shadowMapPassIndex[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = -1; +} + +static void InitView( classicInteractionDomainView_t &view, + const viewDef_t *viewDef, int scenePacketIndex ) { + std::memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scenePacketIndex = scenePacketIndex; + view.interactionPassPacketIndex = -1; + view.stencilShadowPassPacketIndex = -1; + view.shadowMapPassPacketIndex = -1; + view.firstLight = -1; + view.firstSurface = -1; + view.firstPrimitive = -1; + view.firstShadowCaster = -1; + view.firstShadowMapPass = -1; + view.stencilReference = 128; + view.shadowMode = CLASSIC_INTERACTION_SHADOW_NONE; + view.failurePassPacketIndex = -1; + view.failureDrawPacketIndex = -1; + view.failureLightOrdinal = -1; + view.failureReceiverOrdinal = -1; + view.failureStageIndex = -1; + if ( viewDef != NULL ) { + view.viewportX1 = viewDef->viewport.x1; + view.viewportY1 = viewDef->viewport.y1; + view.viewportX2 = viewDef->viewport.x2; + view.viewportY2 = viewDef->viewport.y2; + view.scissorX1 = viewDef->scissor.x1; + view.scissorY1 = viewDef->scissor.y1; + view.scissorX2 = viewDef->scissor.x2; + view.scissorY2 = viewDef->scissor.y2; + std::memcpy( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ); + } +} + +static bool FailureIsOverflow( classicInteractionDomainFailure_t failure ) { + return failure == CLASSIC_INTERACTION_FAILURE_SCENE_PACKET_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_VIEW_POOL_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_LIGHT_POOL_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_SURFACE_POOL_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_PRIMITIVE_POOL_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW + || failure + == CLASSIC_INTERACTION_FAILURE_SHADOW_ALPHA_STAGE_POOL_OVERFLOW + || failure == CLASSIC_INTERACTION_FAILURE_TEXTURE_POOL_OVERFLOW; +} + +static bool FailView( classicInteractionDomainView_t &view, + const classicInteractionCheckpoint_t &checkpoint, + const classicInteractionBuildError_t &error ) { + if ( checkpoint.textureCount < domain.textureCount ) { + InvalidateTextureLookup(); + } + domain.lightCount = checkpoint.lightCount; + domain.surfaceCount = checkpoint.surfaceCount; + domain.primitiveCount = checkpoint.primitiveCount; + domain.shadowCasterCount = checkpoint.shadowCasterCount; + domain.shadowAlphaStageCount = checkpoint.shadowAlphaStageCount; + domain.shadowMapPassCount = checkpoint.shadowMapPassCount; + domain.textureCount = checkpoint.textureCount; + view.ready = false; + view.failure = error.failure == CLASSIC_INTERACTION_FAILURE_NONE + ? CLASSIC_INTERACTION_FAILURE_UNAVAILABLE : error.failure; + view.failureDetail = error.detail; + view.failurePassPacketIndex = error.passPacketIndex; + view.failureDrawPacketIndex = error.drawPacketIndex; + view.failureLightOrdinal = error.lightOrdinal; + view.failureReceiverOrdinal = error.receiverOrdinal; + view.failureStageIndex = error.stageIndex; + view.firstLight = -1; + view.lightCount = 0; + view.firstSurface = -1; + view.surfaceCount = 0; + view.firstPrimitive = -1; + view.primitiveCount = 0; + view.drawablePrimitiveCount = 0; + view.noopPrimitiveCount = 0; + view.firstShadowCaster = -1; + view.shadowCasterCount = 0; + view.firstShadowMapPass = -1; + view.shadowMapPassCount = 0; + view.drawableShadowCasterCount = 0; + view.noopShadowCasterCount = 0; + view.logicalVolumeDrawCount = 0; + view.preloadVolumeDrawCount = 0; + view.shadowLightCount = 0; + view.hybridShadowPassCount = 0; + view.projectedShadowMapPassCount = 0; + view.csmShadowMapPassCount = 0; + view.pointShadowMapPassCount = 0; + view.projectedShadowLightCount = 0; + view.pointShadowLightCount = 0; + view.shadowPacketDrawCount = 0; + view.shadowMode = CLASSIC_INTERACTION_SHADOW_NONE; + view.activeLightStageCount = 0; + view.inactiveLightStageCount = 0; + view.activeSurfaceStageCount = 0; + view.inactiveSurfaceStageCount = 0; + view.materialDeformReceiverCount = 0; + view.completedDeformCasterCount = 0; + view.emptyDeformCasterCount = 0; + std::memset( view.receiverSurfaceCount, 0, + sizeof( view.receiverSurfaceCount ) ); + std::memset( view.receiverPrimitiveCount, 0, + sizeof( view.receiverPrimitiveCount ) ); + view.hash = 0; + domain.stats.fallbackViews++; + if ( view.failure >= CLASSIC_INTERACTION_FAILURE_NONE + && view.failure < CLASSIC_INTERACTION_FAILURE_COUNT ) { + domain.stats.failureCounts[ view.failure ]++; + } + if ( FailureIsOverflow( view.failure ) ) { + domain.stats.overflow = true; + } + return false; +} + +static bool MaterialRequestsClassicDeform( const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->Deform() != DFRM_NONE; +} + +static int DeformContractFailureDetail( const drawSurf_t *drawSurf, + const drawPacket_t *packet = NULL ) { + const classicDeformRecord_t *record = packet != NULL + && packet->hasClassicDeformRecord + && packet->classicDeformRecord != NULL ? packet->classicDeformRecord + : ( drawSurf != NULL ? &drawSurf->classicDeform : NULL ); + if ( record == NULL ) { + return -1; + } + return static_cast( record->role ) + * CLASSIC_DEFORM_OUTCOME_COUNT + + static_cast( record->outcome ); +} + +static bool ValidateDeformContract( const drawSurf_t *drawSurf, + const drawPacket_t *packet, classicDeformRole_t expectedRole, + classicDeformRole_t &role, classicDeformOutcome_t &outcome, + std::uint64_t &semanticHash ) { + role = CLASSIC_DEFORM_ROLE_UNKNOWN; + outcome = CLASSIC_DEFORM_OUTCOME_NONE; + semanticHash = 0; + if ( !MaterialRequestsClassicDeform( drawSurf ) ) { + return true; + } + if ( !r_rendererSharedDeform.GetBool() || r_skipDeforms.GetBool() ) { + return false; + } + const std::uint64_t frameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + const classicDeformRecord_t *packetRecord = packet != NULL + ? packet->classicDeformRecord : NULL; + const classicDeformRecord_t &record = + expectedRole == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + ? drawSurf->classicDeform + : ( packetRecord != NULL ? *packetRecord : drawSurf->classicDeform ); + if ( record.role != expectedRole + || !R_ClassicDeformDomain_ValidateRecordForFrame( record, + frameToken ) + || !R_ClassicDeformDomain_RecordMatchesDrawSurf( record, drawSurf ) ) { + return false; + } + if ( expectedRole == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW ) { + if ( !record.cpuFinalized + || ( !R_ClassicDeformDomain_HasCompletedOutput( record ) + && !R_ClassicDeformDomain_HasEmptyOutput( record ) ) ) { + return false; + } + } else if ( record.cpuFinalized + || record.outcome != CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE ) { + return false; + } + if ( packet != NULL ) { + if ( !packet->hasClassicDeformRecord + || packetRecord == NULL + || packet->geometryRecord == NULL + || packetRecord != &packet->geometryRecord->classicDeform + || !packet->geometryRecord->hasClassicDeformRecord + || !R_ClassicDeformDomain_ValidateRecordForFrame( + *packetRecord, frameToken ) + || !R_ClassicDeformDomain_SameProvenance( record, + *packetRecord ) ) { + return false; + } + } + role = record.role; + outcome = record.outcome; + semanticHash = record.semanticHash; + return true; +} + +static bool MatrixHasNegativeScale( const float matrix[ 16 ] ) { + const float determinant = + matrix[ 0 ] * ( matrix[ 5 ] * matrix[ 10 ] - matrix[ 9 ] * matrix[ 6 ] ) + - matrix[ 4 ] * ( matrix[ 1 ] * matrix[ 10 ] - matrix[ 9 ] * matrix[ 2 ] ) + + matrix[ 8 ] * ( matrix[ 1 ] * matrix[ 6 ] - matrix[ 5 ] * matrix[ 2 ] ); + return determinant < 0.0f; +} + +static bool ReadRegister( const float *registers, int registerCount, + int registerIndex, float &value, + classicInteractionBuildError_t &error, int stageIndex ) { + if ( registers == NULL || registerCount <= 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + registerCount, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( registerIndex < 0 || registerIndex >= registerCount ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_REGISTER_OUT_OF_RANGE, + registerIndex, -1, -1, -1, -1, stageIndex ); + return false; + } + value = registers[ registerIndex ]; + if ( !FloatIsFinite( value ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + registerIndex, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static bool EvaluateTextureMatrix( const textureStage_t &texture, + const float *registers, int registerCount, float matrix[ 2 ][ 4 ], + classicInteractionBuildError_t &error, int stageIndex ) { + InitTextureMatrix( matrix ); + if ( !texture.hasMatrix ) { + return true; + } + float values[ 2 ][ 3 ]; + for ( int row = 0; row < 2; ++row ) { + for ( int column = 0; column < 3; ++column ) { + if ( !ReadRegister( registers, registerCount, + texture.matrix[ row ][ column ], values[ row ][ column ], + error, stageIndex ) ) { + return false; + } + } + } + for ( int row = 0; row < 2; ++row ) { + float translation = values[ row ][ 2 ]; + if ( translation < -40.0f || translation > 40.0f ) { + if ( static_cast( translation ) < -2147483648.0 + || static_cast( translation ) > 2147483647.0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + texture.matrix[ row ][ 2 ], -1, -1, -1, -1, stageIndex ); + return false; + } + translation -= static_cast( translation ); + } + matrix[ row ][ 0 ] = values[ row ][ 0 ]; + matrix[ row ][ 1 ] = values[ row ][ 1 ]; + matrix[ row ][ 2 ] = 0.0f; + matrix[ row ][ 3 ] = translation; + } + return true; +} + +static bool EvaluateStageColor( const shaderStage_t &stage, + const float *registers, int registerCount, float color[ 4 ], + bool clampColor, classicInteractionBuildError_t &error, + int stageIndex ) { + for ( int component = 0; component < 4; ++component ) { + if ( !ReadRegister( registers, registerCount, + stage.color.registers[ component ], color[ component ], error, + stageIndex ) ) { + return false; + } + if ( clampColor ) { + color[ component ] = idMath::ClampFloat( 0.0f, 1.0f, + color[ component ] ); + } + } + return true; +} + +static bool EvaluateCondition( const shaderStage_t &stage, + const float *registers, int registerCount, bool &active, + classicInteractionBuildError_t &error, int stageIndex ) { + float condition = 0.0f; + if ( !ReadRegister( registers, registerCount, stage.conditionRegister, + condition, error, stageIndex ) ) { + return false; + } + active = condition != 0.0f; + return true; +} + +static rendererVertexColorMode_t ConvertVertexColor( + stageVertexColor_t vertexColor, + classicInteractionBuildError_t &error, int stageIndex ) { + switch ( vertexColor ) { + case SVC_IGNORE: return RENDERER_VERTEX_COLOR_IGNORE; + case SVC_MODULATE: return RENDERER_VERTEX_COLOR_MODULATE; + case SVC_INVERSE_MODULATE: return RENDERER_VERTEX_COLOR_INVERSE_MODULATE; + default: + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + vertexColor, -1, -1, -1, -1, stageIndex ); + return RENDERER_VERTEX_COLOR_IGNORE; + } +} + +static rendererCullMode_t ConvertCull( cullType_t cull, + classicInteractionBuildError_t &error ) { + switch ( cull ) { + case CT_FRONT_SIDED: return RENDERER_CULL_FRONT; + case CT_BACK_SIDED: return RENDERER_CULL_BACK; + case CT_TWO_SIDED: return RENDERER_CULL_NONE; + default: + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, cull ); + return RENDERER_CULL_FRONT; + } +} + +static bool ImageReady( const idImage *image, + classicInteractionBuildError_t &error, int stageIndex ) { + if ( image == NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( R_IsMutableRenderImage( image ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DYNAMIC_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( image->IsDefaulted() ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFAULTED_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( !image->IsLoaded() ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNLOADED_RESOURCE, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static bool AddTexture( const idImage *image, std::uint64_t &resourceId, + classicInteractionBuildError_t &error, int stageIndex ) { + resourceId = 0; + if ( !ImageReady( image, error, stageIndex ) ) { + return false; + } + const std::uint64_t liveStorageGeneration = image->GetStorageGeneration(); + const int lookupSlot = TextureLookupSlot( image, liveStorageGeneration ); + if ( lookupSlot >= 0 + && textureLookup[ lookupSlot ].stamp == textureLookupStamp ) { + resourceId = domain.textures[ + textureLookup[ lookupSlot ].textureIndex ].textureResourceId; + return true; + } + for ( int i = 0; i < domain.textureCount; ++i ) { + if ( domain.textures[ i ].image == image + && domain.textures[ i ].storageGeneration + == liveStorageGeneration ) { + if ( lookupSlot >= 0 ) { + textureLookup[ lookupSlot ].image = image; + textureLookup[ lookupSlot ].storageGeneration = + liveStorageGeneration; + textureLookup[ lookupSlot ].textureIndex = i; + textureLookup[ lookupSlot ].stamp = textureLookupStamp; + } + resourceId = domain.textures[ i ].textureResourceId; + return true; + } + } + if ( domain.textureCount >= CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_TEXTURE_POOL_OVERFLOW, + domain.textureCount, -1, -1, -1, -1, stageIndex ); + return false; + } + const int textureIndex = domain.textureCount++; + classicInteractionDomainTexture_t &texture = domain.textures[ textureIndex ]; + std::memset( &texture, 0, sizeof( texture ) ); + texture.textureResourceId = MakeTextureResourceId( textureIndex ); + texture.image = image; + texture.storageGeneration = image->GetStorageGeneration(); + texture.nameHash = HashString( image->GetName() ); + texture.textureHandle = const_cast( image )->GetDeviceHandle(); + texture.filter = image->GetFilter(); + texture.repeat = image->GetRepeat(); + texture.loaded = image->IsLoaded(); + texture.defaulted = image->IsDefaulted(); + texture.mutableImage = R_IsMutableRenderImage( image ); + if ( lookupSlot >= 0 ) { + textureLookup[ lookupSlot ].image = image; + textureLookup[ lookupSlot ].storageGeneration = liveStorageGeneration; + textureLookup[ lookupSlot ].textureIndex = textureIndex; + textureLookup[ lookupSlot ].stamp = textureLookupStamp; + } + resourceId = texture.textureResourceId; + return resourceId != 0; +} + +static bool TextureStageFixed( const shaderStage_t &stage, + classicInteractionBuildError_t &error, int stageIndex ) { + if ( stage.newStage != NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING, + 0, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( stage.texture.cinematic != NULL || stage.texture.dynamic != DI_STATIC ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DYNAMIC_RESOURCE, + stage.texture.dynamic, -1, -1, -1, -1, stageIndex ); + return false; + } + if ( stage.texture.texgen != TG_EXPLICIT ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + stage.texture.texgen, -1, -1, -1, -1, stageIndex ); + return false; + } + return true; +} + +static void BakeTextureMatrixIntoProjection( float projection[ 4 ][ 4 ], + const float textureRows[ 2 ][ 4 ] ) { + float textureMatrix[ 16 ]; + std::memset( textureMatrix, 0, sizeof( textureMatrix ) ); + textureMatrix[ 0 ] = textureRows[ 0 ][ 0 ]; + textureMatrix[ 4 ] = textureRows[ 0 ][ 1 ]; + textureMatrix[ 12 ] = textureRows[ 0 ][ 3 ]; + textureMatrix[ 1 ] = textureRows[ 1 ][ 0 ]; + textureMatrix[ 5 ] = textureRows[ 1 ][ 1 ]; + textureMatrix[ 13 ] = textureRows[ 1 ][ 3 ]; + textureMatrix[ 10 ] = 1.0f; + textureMatrix[ 15 ] = 1.0f; + + float generator[ 16 ]; + std::memset( generator, 0, sizeof( generator ) ); + generator[ 0 ] = projection[ 0 ][ 0 ]; + generator[ 4 ] = projection[ 0 ][ 1 ]; + generator[ 8 ] = projection[ 0 ][ 2 ]; + generator[ 12 ] = projection[ 0 ][ 3 ]; + generator[ 1 ] = projection[ 1 ][ 0 ]; + generator[ 5 ] = projection[ 1 ][ 1 ]; + generator[ 9 ] = projection[ 1 ][ 2 ]; + generator[ 13 ] = projection[ 1 ][ 3 ]; + generator[ 3 ] = projection[ 2 ][ 0 ]; + generator[ 7 ] = projection[ 2 ][ 1 ]; + generator[ 11 ] = projection[ 2 ][ 2 ]; + generator[ 15 ] = projection[ 2 ][ 3 ]; + + float finalMatrix[ 16 ]; + myGlMultMatrix( generator, textureMatrix, finalMatrix ); + projection[ 0 ][ 0 ] = finalMatrix[ 0 ]; + projection[ 0 ][ 1 ] = finalMatrix[ 4 ]; + projection[ 0 ][ 2 ] = finalMatrix[ 8 ]; + projection[ 0 ][ 3 ] = finalMatrix[ 12 ]; + projection[ 1 ][ 0 ] = finalMatrix[ 1 ]; + projection[ 1 ][ 1 ] = finalMatrix[ 5 ]; + projection[ 1 ][ 2 ] = finalMatrix[ 9 ]; + projection[ 1 ][ 3 ] = finalMatrix[ 13 ]; +} + +static std::uint64_t StableTextureHash( std::uint64_t resourceId ) { + const int textureIndex = TextureIndexFromResourceId( resourceId ); + return textureIndex >= 0 && textureIndex < domain.textureCount + ? domain.textures[ textureIndex ].nameHash : 0; +} + +static std::uint64_t HashPrimitive( + const classicInteractionDomainPrimitive_t &primitive, + int lightArenaBase, int surfaceArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + // Published indices remain absolute arena identities for backend validation, + // but hashes use view-local identities so unrelated earlier views cannot + // perturb otherwise identical content. + HashInt( hash, primitive.lightIndex - lightArenaBase ); + HashInt( hash, primitive.surfaceIndex - surfaceArenaBase ); + HashInt( hash, primitive.lightStageIndex ); + HashInt( hash, primitive.bumpStageIndex ); + HashInt( hash, primitive.diffuseStageIndex ); + HashInt( hash, primitive.specularStageIndex ); + HashInt( hash, primitive.receiver ); + HashInt( hash, primitive.disposition ); + HashInt( hash, primitive.depth ); + HashInt( hash, primitive.cull ); + HashInt( hash, primitive.vertexColor ); + HashBool( hash, primitive.blend.enabled ); + HashInt( hash, primitive.blend.sourceColor ); + HashInt( hash, primitive.blend.destinationColor ); + HashInt( hash, primitive.blend.colorOperation ); + HashInt( hash, primitive.blend.sourceAlpha ); + HashInt( hash, primitive.blend.destinationAlpha ); + HashInt( hash, primitive.blend.alphaOperation ); + HashInt( hash, primitive.vertexCount ); + HashInt( hash, primitive.firstIndex ); + HashInt( hash, primitive.indexCount ); + HashInt( hash, primitive.vertexOffset ); + HashInt( hash, primitive.scissorX1 ); + HashInt( hash, primitive.scissorY1 ); + HashInt( hash, primitive.scissorX2 ); + HashInt( hash, primitive.scissorY2 ); + HashBool( hash, primitive.ambientLight ); + HashBool( hash, primitive.polygonOffsetEnabled ); + HashFloat( hash, primitive.polygonOffsetFactor ); + HashFloat( hash, primitive.polygonOffsetUnits ); + HashU64( hash, StableTextureHash( primitive.lightImageResourceId ) ); + HashU64( hash, StableTextureHash( primitive.lightFalloffImageResourceId ) ); + HashU64( hash, StableTextureHash( primitive.bumpImageResourceId ) ); + HashU64( hash, StableTextureHash( primitive.diffuseImageResourceId ) ); + HashU64( hash, StableTextureHash( primitive.specularImageResourceId ) ); + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, primitive.diffuseColor[ component ] ); + HashFloat( hash, primitive.specularColor[ component ] ); + HashFloat( hash, primitive.flatDiffuseParams[ component ] ); + HashFloat( hash, primitive.localLightOrigin[ component ] ); + HashFloat( hash, primitive.localViewOrigin[ component ] ); + } + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, primitive.lightProjection[ plane ][ component ] ); + } + } + for ( int row = 0; row < 2; ++row ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, primitive.bumpMatrix[ row ][ component ] ); + HashFloat( hash, primitive.diffuseMatrix[ row ][ component ] ); + HashFloat( hash, primitive.specularMatrix[ row ][ component ] ); + } + } + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, primitive.modelMatrix[ i ] ); + HashFloat( hash, primitive.modelViewMatrix[ i ] ); + } + return hash; +} + +static std::uint64_t HashSurface( + const classicInteractionDomainSurface_t &surface, + int lightArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, surface.lightIndex - lightArenaBase ); + HashInt( hash, surface.sourceOrdinal ); + HashInt( hash, surface.receiverOrdinal ); + HashInt( hash, surface.receiver ); + HashInt( hash, surface.materialId ); + HashInt( hash, surface.primitiveCount ); + HashInt( hash, surface.drawablePrimitiveCount ); + HashInt( hash, surface.noopPrimitiveCount ); + HashInt( hash, surface.surfaceStageCount ); + HashInt( hash, surface.activeSurfaceStageCount ); + HashInt( hash, surface.inactiveSurfaceStageCount ); + HashInt( hash, surface.vertexCount ); + HashInt( hash, surface.firstIndex ); + HashInt( hash, surface.indexCount ); + HashInt( hash, surface.vertexOffset ); + HashInt( hash, surface.scissorX1 ); + HashInt( hash, surface.scissorY1 ); + HashInt( hash, surface.scissorX2 ); + HashInt( hash, surface.scissorY2 ); + HashInt( hash, surface.deformRole ); + HashInt( hash, surface.deformOutcome ); + HashU64( hash, surface.deformContractHash ); + for ( int i = 0; i < surface.primitiveCount; ++i ) { + HashU64( hash, domain.primitives[ surface.firstPrimitive + i ].hash ); + } + return hash; +} + +static std::uint64_t HashShadowAlphaStage( + const classicInteractionDomainShadowAlphaStage_t &stage ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, stage.stageIndex ); + HashInt( hash, stage.alphaTestMode ); + HashFloat( hash, stage.alphaTestValue ); + HashFloat( hash, stage.alphaScale ); + HashFloat( hash, stage.alphaHashMode ); + HashU64( hash, StableTextureHash( stage.textureResourceId ) ); + for ( int row = 0; row < 2; ++row ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, stage.textureMatrix[ row ][ component ] ); + } + } + return hash; +} + +static std::uint64_t HashShadowCaster( + const classicInteractionDomainShadowCaster_t &caster, + int lightArenaBase ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, caster.lightIndex - lightArenaBase ); + HashInt( hash, caster.sourceOrdinal ); + HashInt( hash, caster.chainOrdinal ); + HashInt( hash, caster.chain ); + HashInt( hash, caster.disposition ); + HashInt( hash, caster.indexSelection ); + HashInt( hash, caster.cull ); + HashInt( hash, caster.materialCoverage ); + HashInt( hash, caster.materialId ); + HashInt( hash, caster.alphaStageCount ); + HashInt( hash, caster.vertexCount ); + HashInt( hash, caster.totalIndexCount ); + HashInt( hash, caster.selectedIndexCount ); + HashInt( hash, caster.scissorX1 ); + HashInt( hash, caster.scissorY1 ); + HashInt( hash, caster.scissorX2 ); + HashInt( hash, caster.scissorY2 ); + HashFloat( hash, caster.depthMin ); + HashFloat( hash, caster.depthMax ); + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, caster.localLightOrigin[ component ] ); + } + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, caster.modelMatrix[ i ] ); + HashFloat( hash, caster.modelViewMatrix[ i ] ); + } + for ( int component = 0; component < 3; ++component ) { + HashFloat( hash, caster.boundsMin[ component ] ); + HashFloat( hash, caster.boundsMax[ component ] ); + } + HashBool( hash, caster.external ); + HashBool( hash, caster.preload ); + HashBool( hash, caster.ambientGeometry ); + HashBool( hash, caster.perfectHull ); + HashBool( hash, caster.dynamicCaster ); + HashBool( hash, caster.translucentCaster ); + HashInt( hash, caster.deformRole ); + HashInt( hash, caster.deformOutcome ); + HashU64( hash, caster.deformContractHash ); + for ( int i = 0; i < caster.alphaStageCount; ++i ) { + HashU64( hash, + domain.shadowAlphaStages[ caster.firstAlphaStage + i ].hash ); + } + return hash; +} + +static void HashProjectedShadowState( std::uint64_t &hash, + const classicInteractionDomainShadowProjectedState_t &projected ) { + const shadowMapProjectedLightState_t &state = projected.state; + HashBool( hash, state.valid ); + HashBool( hash, state.cascadeFallback ); + HashFloat( hash, state.projectionPad ); + HashFloat( hash, state.projectionScale ); + HashInt( hash, state.fallbackReason ); + HashInt( hash, state.requestedCascadeCount ); + HashInt( hash, state.fallbackCascade ); + HashInt( hash, state.cascadeCount ); + HashInt( hash, state.atlasDiv ); + HashInt( hash, state.tileSize ); + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, state.baseClipPlanes[ plane ][ component ] ); + } + } + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, state.baseShadowMatrix[ i ] ); + } + for ( int cascade = 0; cascade < SHADOWMAP_PROJECTED_MAX_CASCADES; + ++cascade ) { + HashFloat( hash, state.splitDepths[ cascade ] ); + HashFloat( hash, state.biasScale[ cascade ] ); + HashFloat( hash, state.texelDepthBias[ cascade ] ); + HashFloat( hash, state.worldTexelSize[ cascade ] ); + HashFloat( hash, state.sliceNear[ cascade ] ); + HashFloat( hash, state.sliceFar[ cascade ] ); + HashFloat( hash, state.depthRange[ cascade ] ); + HashFloat( hash, state.clipZExtent[ cascade ] ); + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, + state.clipPlanes[ cascade ][ plane ][ component ] ); + } + } + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, state.atlasRect[ cascade ][ component ] ); + } + } + HashBool( hash, projected.filter.distantSource ); + HashFloat( hash, projected.filter.filterScale ); + HashFloat( hash, projected.filter.filterRadius ); + HashInt( hash, projected.filter.filterTaps ); + HashInt( hash, projected.filter.filterMode ); + HashFloat( hash, projected.filter.pcssLightRadius ); + HashFloat( hash, projected.filter.pcssMaxRadius ); + HashFloat( hash, projected.filter.effectiveFilterRadius ); + HashFloat( hash, projected.constantBias ); + HashFloat( hash, projected.normalBias ); + HashFloat( hash, projected.normalOffsetScale ); + HashFloat( hash, projected.cascadeBlend ); + HashFloat( hash, projected.texelBiasScale ); + HashBool( hash, projected.depthCompare ); + HashBool( hash, projected.receiverPlaneBias ); +} + +static std::uint64_t HashShadowMapPass( + const classicInteractionDomainShadowMapPass_t &pass ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, pass.receiver ); + HashInt( hash, pass.resourceOwner ); + HashInt( hash, pass.disposition ); + HashInt( hash, pass.mode ); + HashInt( hash, pass.lightClass ); + HashInt( hash, pass.receiverMask ); + HashInt( hash, pass.mappedCasterCount ); + HashInt( hash, pass.supplementCasterCount ); + HashInt( hash, pass.drawableMappedCasters ); + HashInt( hash, pass.noopMappedCasters ); + HashInt( hash, pass.drawableSupplementCasters ); + HashInt( hash, pass.noopSupplementCasters ); + HashInt( hash, pass.casterSignature ); + HashInt( hash, pass.incompleteMapMask ); + HashInt( hash, pass.incompleteStencilMask ); + HashInt( hash, pass.hybridIncompleteMask ); + HashInt( hash, pass.prelightMapMissingMask ); + HashInt( hash, pass.prelightStencilRequiredMask ); + HashInt( hash, pass.prelightStencilReadyMask ); + HashBool( hash, pass.resourceAlias ); + HashBool( hash, pass.mapRequired ); + HashBool( hash, pass.mapComplete ); + HashBool( hash, pass.stencilComplete ); + HashBool( hash, pass.hybridComplete ); + HashBool( hash, pass.hasStaticCasters ); + HashBool( hash, pass.hasDynamicCasters ); + HashBool( hash, pass.hasAlphaCasters ); + HashBool( hash, pass.hasTranslucentCasters ); + HashBool( hash, pass.allowCacheReuse ); + HashBool( hash, pass.allowCacheUpdate ); + HashBool( hash, pass.allowScratch ); + HashBool( hash, pass.hashedAlpha ); + HashBool( hash, pass.stableAlphaHash ); + HashInt( hash, pass.casterCullMode ); + HashFloat( hash, pass.polygonFactor ); + HashFloat( hash, pass.polygonOffset ); + if ( pass.lightClass == SHADOWMAP_LIGHT_POINT ) { + HashBool( hash, pass.point.valid ); + HashInt( hash, pass.point.faceCount ); + HashInt( hash, pass.point.faceSize ); + for ( int i = 0; i < 4; ++i ) { + HashFloat( hash, pass.point.lightOrigin[ i ] ); + } + HashFloat( hash, pass.point.farDistance ); + HashFloat( hash, pass.point.constantBias ); + HashFloat( hash, pass.point.normalBias ); + HashFloat( hash, pass.point.normalOffsetScale ); + HashFloat( hash, pass.point.texelBiasScale ); + HashFloat( hash, pass.point.filterRadius ); + HashInt( hash, pass.point.filterTaps ); + HashInt( hash, pass.point.filterMode ); + HashBool( hash, pass.point.depthCompare ); + HashBool( hash, pass.point.highPrecision ); + } else { + HashProjectedShadowState( hash, pass.projected ); + } + return hash; +} + +static std::uint64_t HashLight( + const classicInteractionDomainLight_t &light ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, light.sourceOrdinal ); + HashInt( hash, light.surfaceCount ); + HashInt( hash, light.primitiveCount ); + HashInt( hash, light.drawablePrimitiveCount ); + HashInt( hash, light.noopPrimitiveCount ); + HashInt( hash, light.lightStageCount ); + HashInt( hash, light.activeLightStageCount ); + HashInt( hash, light.inactiveLightStageCount ); + for ( int receiver = 0; receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; + receiver++ ) { + HashInt( hash, light.receiverSurfaceCount[ receiver ] ); + HashInt( hash, light.receiverPrimitiveCount[ receiver ] ); + HashInt( hash, light.receiverShadowMode[ receiver ] ); + if ( receiver < 2 && light.shadowMapPassIndex[ receiver ] >= 0 ) { + HashU64( hash, domain.shadowMapPasses[ + light.shadowMapPassIndex[ receiver ] ].hash ); + } + } + for ( int chain = 0; chain < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; + ++chain ) { + HashInt( hash, light.shadowCasterCount[ chain ] ); + } + HashInt( hash, light.shadowCasterTotal ); + HashInt( hash, light.drawableShadowCasters ); + HashInt( hash, light.noopShadowCasters ); + HashInt( hash, light.logicalVolumeDraws ); + HashInt( hash, light.preloadVolumeDraws ); + HashInt( hash, light.scissorX1 ); + HashInt( hash, light.scissorY1 ); + HashInt( hash, light.scissorX2 ); + HashInt( hash, light.scissorY2 ); + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, light.globalLightOrigin[ component ] ); + HashFloat( hash, light.lightRadius[ component ] ); + } + for ( int plane = 0; plane < 4; ++plane ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, light.lightProject[ plane ][ component ] ); + } + } + HashBool( hash, light.pointLight ); + HashBool( hash, light.parallel ); + HashBool( hash, light.ambientLight ); + HashBool( hash, light.shadowClassified ); + HashBool( hash, light.clearStencil ); + for ( int chain = 0; chain < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; + ++chain ) { + for ( int i = 0; i < light.shadowCasterCount[ chain ]; ++i ) { + HashU64( hash, domain.shadowCasters[ + light.firstShadowCaster[ chain ] + i ].hash ); + } + } + for ( int i = 0; i < light.surfaceCount; ++i ) { + HashU64( hash, domain.surfaces[ light.firstSurface + i ].hash ); + } + return hash; +} + +static std::uint64_t HashView( const classicInteractionDomainView_t &view ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, view.lightCount ); + HashInt( hash, view.surfaceCount ); + HashInt( hash, view.primitiveCount ); + HashInt( hash, view.drawablePrimitiveCount ); + HashInt( hash, view.noopPrimitiveCount ); + HashInt( hash, view.shadowCasterCount ); + HashInt( hash, view.drawableShadowCasterCount ); + HashInt( hash, view.noopShadowCasterCount ); + HashInt( hash, view.logicalVolumeDrawCount ); + HashInt( hash, view.preloadVolumeDrawCount ); + HashInt( hash, view.shadowLightCount ); + HashInt( hash, view.shadowMapPassCount ); + HashInt( hash, view.hybridShadowPassCount ); + HashInt( hash, view.projectedShadowMapPassCount ); + HashInt( hash, view.csmShadowMapPassCount ); + HashInt( hash, view.pointShadowMapPassCount ); + HashInt( hash, view.projectedShadowLightCount ); + HashInt( hash, view.pointShadowLightCount ); + HashInt( hash, view.packetDrawCount ); + HashInt( hash, view.shadowPacketDrawCount ); + HashInt( hash, view.activeLightStageCount ); + HashInt( hash, view.inactiveLightStageCount ); + HashInt( hash, view.activeSurfaceStageCount ); + HashInt( hash, view.inactiveSurfaceStageCount ); + HashInt( hash, view.materialDeformReceiverCount ); + HashInt( hash, view.completedDeformCasterCount ); + HashInt( hash, view.emptyDeformCasterCount ); + for ( int receiver = 0; receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; + receiver++ ) { + HashInt( hash, view.receiverSurfaceCount[ receiver ] ); + HashInt( hash, view.receiverPrimitiveCount[ receiver ] ); + } + HashInt( hash, view.viewportX1 ); + HashInt( hash, view.viewportY1 ); + HashInt( hash, view.viewportX2 ); + HashInt( hash, view.viewportY2 ); + HashInt( hash, view.scissorX1 ); + HashInt( hash, view.scissorY1 ); + HashInt( hash, view.scissorX2 ); + HashInt( hash, view.scissorY2 ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, view.projectionMatrix[ i ] ); + } + HashFloat( hash, view.maxLightValue ); + HashFloat( hash, view.lightScale ); + HashFloat( hash, view.overBright ); + HashFloat( hash, view.shadowPolygonFactor ); + HashFloat( hash, view.shadowPolygonUnits ); + HashInt( hash, view.stencilReference ); + HashInt( hash, view.shadowMode ); + HashBool( hash, view.useScissor ); + HashBool( hash, view.useShadowVertexProgram ); + HashBool( hash, view.preferTwoSidedStencil ); + HashBool( hash, view.useDepthBounds ); + HashBool( hash, view.stencilTranslucentShadows ); + for ( int i = 0; i < view.lightCount; ++i ) { + HashU64( hash, domain.lights[ view.firstLight + i ].hash ); + } + return hash; +} + +static bool SurfacePacketEligible( const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->geo != NULL + && drawSurf->geo->numIndexes > 0 && drawSurf->space != NULL + && drawSurf->material != NULL + && drawSurf->material->ReceivesLighting() + && !drawSurf->material->IsPortalSky(); +} + +static bool ShadowPacketSurfaceEligible( const drawSurf_t *drawSurf ) { + if ( drawSurf == NULL || drawSurf->geo == NULL + || drawSurf->geo->numVerts <= 0 + || drawSurf->geo->numIndexes <= 0 ) { + return false; + } + const idMaterial *material = drawSurf->material; + if ( material == NULL ) { + return true; + } + if ( material->IsDedicatedCollisionSurface() + || material->HasGui() || material->HasSubview() ) { + return false; + } + return material->Coverage() == MC_TRANSLUCENT + || material->SurfaceCastsShadow(); +} + +static bool ViewLightHasInteractions( const viewLight_t *viewLight ) { + return viewLight != NULL && viewLight->lightShader != NULL + && !viewLight->lightShader->IsFogLight() + && !viewLight->lightShader->IsBlendLight() + && ( viewLight->localInteractions != NULL + || viewLight->globalInteractions != NULL + || viewLight->translucentInteractions != NULL ); +} + +static bool ViewLightContributesToClassicScale( const viewLight_t *viewLight ) { + // RB_DetermineLightScale deliberately considers every light with any + // interaction receiver chain. Fog and blend classification does not alter + // that membership, even though those lights are not owned by this domain. + return viewLight != NULL + && ( viewLight->localInteractions != NULL + || viewLight->globalInteractions != NULL + || viewLight->translucentInteractions != NULL ); +} + +static bool AccumulateClassicLightScaleValue( float authoredScale, + float component, float &maximum ) { + const float value = authoredScale * component; + if ( !FloatIsFinite( value ) ) { + return false; + } + if ( value > maximum ) { + maximum = value; + } + return true; +} + +static bool ViewLightHasMappedShadowState( const viewLight_t &viewLight ) { + return viewLight.globalShadowMapStencilSupplements != NULL + || viewLight.localShadowMapStencilSupplements != NULL + || viewLight.localShadowMapCasters != NULL + || viewLight.globalShadowMapCasters != NULL + || viewLight.localShadowMapDynamicCasters != NULL + || viewLight.globalShadowMapDynamicCasters != NULL + || viewLight.localTranslucentShadowMapCasters != NULL + || viewLight.globalTranslucentShadowMapCasters != NULL; +} + +static bool SceneShadowChainToDomain( sceneShadowCasterClass_t source, + classicInteractionDomainShadowChain_t &destination ) { + switch ( source ) { + case SCENE_SHADOW_CASTER_STENCIL_GLOBAL: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + return true; + case SCENE_SHADOW_CASTER_STENCIL_LOCAL: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + return true; + case SCENE_SHADOW_CASTER_MAP_GLOBAL_STATIC: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC; + return true; + case SCENE_SHADOW_CASTER_MAP_LOCAL_STATIC: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC; + return true; + case SCENE_SHADOW_CASTER_MAP_GLOBAL_DYNAMIC: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC; + return true; + case SCENE_SHADOW_CASTER_MAP_LOCAL_DYNAMIC: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC; + return true; + case SCENE_SHADOW_CASTER_MAP_GLOBAL_TRANSLUCENT: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT; + return true; + case SCENE_SHADOW_CASTER_MAP_LOCAL_TRANSLUCENT: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT; + return true; + case SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL; + return true; + case SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL: + destination = CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL; + return true; + case SCENE_SHADOW_CASTER_NONE: + case SCENE_SHADOW_CASTER_COUNT: + default: + return false; + } +} + +static bool ValidateShadowPacket( const idScenePacketFrame &packetFrame, + const drawPacket_t &packet, int packetIndex, const viewDef_t *viewDef, + const viewLight_t *viewLight, int lightOrdinal, + renderPassCategory_t passCategory, + sceneShadowCasterClass_t casterClass, int chainOrdinal, + int sourceOrdinal, const drawSurf_t *drawSurf, + classicInteractionBuildError_t &error ) { + if ( packet.passCategory != passCategory + || packet.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || packet.viewDef != viewDef || packet.legacyDrawSurf != drawSurf + || packet.shadowLight != viewLight + || packet.shadowLightOrdinal != lightOrdinal + || packet.shadowCasterClass != casterClass + || packet.shadowChainOrdinal != chainOrdinal + || packet.shadowSourceOrdinal != sourceOrdinal ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + sourceOrdinal, -1, packetIndex, lightOrdinal, chainOrdinal ); + return false; + } + if ( drawSurf == NULL || drawSurf->geo == NULL || drawSurf->space == NULL + || packet.geometryRecordIndex < 0 + || packet.geometryRecordIndex >= packetFrame.NumGeometryRecords() + || packet.geometryRecord == NULL + || packet.geometryRecord + != &packetFrame.GeometryRecord( packet.geometryRecordIndex ) + || packet.geometryRecord->legacyGeometry != drawSurf->geo + || packet.instanceRecordIndex < 0 + || packet.instanceRecordIndex >= packetFrame.NumInstanceRecords() + || packet.instanceRecord == NULL + || packet.instanceRecord + != &packetFrame.InstanceRecord( packet.instanceRecordIndex ) + || packet.instanceRecord->legacySpace != drawSurf->space ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + packet.geometryRecordIndex, -1, packetIndex, lightOrdinal, + chainOrdinal ); + return false; + } + const geometryResourceRecord_t &geometry = *packet.geometryRecord; + const instanceRecord_t &instance = *packet.instanceRecord; + const bool stencilPacket = passCategory == RENDER_PASS_STENCIL_SHADOW + || casterClass == SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL + || casterClass == SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL; + bool admittedMaterialDeform = false; + if ( !stencilPacket && MaterialRequestsClassicDeform( drawSurf ) + && r_rendererSharedDeform.GetBool() ) { + classicDeformRole_t role; + classicDeformOutcome_t outcome; + std::uint64_t semanticHash = 0; + if ( !ValidateDeformContract( drawSurf, &packet, + CLASSIC_DEFORM_ROLE_FINALIZED_DRAW, role, outcome, + semanticHash ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( drawSurf, &packet ), -1, packetIndex, + lightOrdinal, chainOrdinal ); + return false; + } + admittedMaterialDeform = true; + } + const srfTriangles_t *tri = drawSurf->geo; + const srfTriangles_t *casterTri = !stencilPacket + && tri->ambientSurface != NULL ? tri->ambientSurface : tri; + if ( !packet.hasGeometry || geometry.legacyGeometry == NULL + || ( geometry.deformMode != GEOMETRY_DEFORM_NONE + && !admittedMaterialDeform ) + || ( geometry.skinningMode != GEOMETRY_SKINNING_NONE + && geometry.skinningMode != GEOMETRY_SKINNING_CPU ) + || geometry.hasPrimBatchMesh || tri->deformedSurface + || casterTri == NULL || casterTri->numVerts <= 0 + || casterTri->numIndexes <= 0 || casterTri->numIndexes % 3 != 0 + || instance.weaponDepthHack || instance.modelDepthHack != 0.0f + || instance.negativeScale + || MatrixHasNegativeScale( instance.modelMatrix ) + || !FloatsAreFinite( instance.modelMatrix, 16 ) + || !FloatsAreFinite( instance.modelViewMatrix, 16 ) + || !FloatIsFinite( drawSurf->scissorRect.zmin ) + || !FloatIsFinite( drawSurf->scissorRect.zmax ) + || !FloatsAreFinite( casterTri->bounds[0].ToFloatPtr(), 3 ) + || !FloatsAreFinite( casterTri->bounds[1].ToFloatPtr(), 3 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + casterTri != NULL ? casterTri->numIndexes : 0, -1, packetIndex, + lightOrdinal, chainOrdinal ); + return false; + } + if ( stencilPacket ) { + if ( tri->shadowCache == NULL || tri->indexes == NULL + || tri->numShadowIndexesNoFrontCaps < 0 + || tri->numShadowIndexesNoFrontCaps > tri->numIndexes + || tri->numShadowIndexesNoCaps < 0 + || tri->numShadowIndexesNoCaps > tri->numIndexes + || tri->numShadowIndexesNoFrontCaps % 3 != 0 + || tri->numShadowIndexesNoCaps % 3 != 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + tri->numIndexes, -1, packetIndex, lightOrdinal, + chainOrdinal ); + return false; + } + } else { + if ( casterTri->ambientCache == NULL + || ( casterTri->indexCache == NULL + && casterTri->indexes == NULL ) + || drawSurf->material == NULL + || drawSurf->shaderRegisters == NULL + || packet.materialRecordIndex < 0 + || packet.materialRecordIndex + >= packetFrame.NumMaterialRecords() + || packet.materialRecord == NULL + || packet.materialRecord + != &packetFrame.MaterialRecord( + packet.materialRecordIndex ) + || packet.materialRecord->material != drawSurf->material + || !packet.hasShaderRegisters + || !instance.hasShaderRegisters + || instance.legacyShaderRegisters + != drawSurf->shaderRegisters ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + casterTri->numIndexes, -1, packetIndex, lightOrdinal, + chainOrdinal ); + return false; + } + } + return true; +} + +static bool PrepareStencilShadowChain( + const idScenePacketFrame &packetFrame, + const classicInteractionDomainView_t &view, + classicInteractionDomainLight_t &light, const drawSurf_t *chain, + sceneShadowCasterClass_t packetClass, + classicInteractionDomainShadowChain_t domainChain, + renderPassCategory_t passCategory, int passPacketIndex, + int &packetCursor, int packetEnd, + int lightArenaBase, classicInteractionBuildError_t &error ) { + if ( chain == NULL ) { + return true; + } + light.firstShadowCaster[ domainChain ] = domain.shadowCasterCount; + int chainOrdinal = 0; + for ( const drawSurf_t *drawSurf = chain; drawSurf != NULL; + drawSurf = drawSurf->nextOnLight, ++chainOrdinal ) { + if ( !ShadowPacketSurfaceEligible( drawSurf ) ) { + continue; + } + if ( packetCursor < 0 || packetCursor >= packetEnd ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + packetCursor, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetCursor ); + classicInteractionDomainShadowChain_t packetDomainChain; + if ( !SceneShadowChainToDomain( packet.shadowCasterClass, + packetDomainChain ) || packetDomainChain != domainChain ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + packet.shadowCasterClass, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + const int sourceOrdinal = packetCursor + - packetFrame.Pass( passPacketIndex ).firstDrawPacket; + if ( !ValidateShadowPacket( packetFrame, packet, packetCursor, + view.viewDef, light.legacyViewLight, light.sourceOrdinal, + passCategory, + packetClass, chainOrdinal, sourceOrdinal, drawSurf, error ) ) { + return false; + } + if ( domain.shadowCasterCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW, + domain.shadowCasterCount, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + + classicInteractionDomainShadowCaster_t caster; + InitShadowCaster( caster ); + caster.legacyDrawSurf = drawSurf; + caster.legacyViewLight = light.legacyViewLight; + caster.legacyCasterGeometry = drawSurf->geo; + caster.perfectHull = drawSurf->geo->perfectHull; + caster.drawPacketIndex = packetCursor; + caster.lightIndex = static_cast( &light - domain.lights ); + caster.sourceOrdinal = sourceOrdinal; + caster.chainOrdinal = chainOrdinal; + caster.geometryRecordIndex = packet.geometryRecordIndex; + caster.instanceRecordIndex = packet.instanceRecordIndex; + caster.chain = domainChain; + caster.vertexCount = drawSurf->geo->numVerts; + caster.totalIndexCount = drawSurf->geo->numIndexes; + caster.scissorX1 = drawSurf->scissorRect.x1; + caster.scissorY1 = drawSurf->scissorRect.y1; + caster.scissorX2 = drawSurf->scissorRect.x2; + caster.scissorY2 = drawSurf->scissorRect.y2; + caster.depthMin = idMath::ClampFloat( 0.0f, 1.0f, + drawSurf->scissorRect.zmin ); + caster.depthMax = idMath::ClampFloat( caster.depthMin, 1.0f, + drawSurf->scissorRect.zmax ); + std::memcpy( caster.modelMatrix, packet.instanceRecord->modelMatrix, + sizeof( caster.modelMatrix ) ); + std::memcpy( caster.modelViewMatrix, + packet.instanceRecord->modelViewMatrix, + sizeof( caster.modelViewMatrix ) ); + std::memcpy( caster.boundsMin, + drawSurf->geo->bounds[0].ToFloatPtr(), sizeof( caster.boundsMin ) ); + std::memcpy( caster.boundsMax, + drawSurf->geo->bounds[1].ToFloatPtr(), sizeof( caster.boundsMax ) ); + if ( view.useShadowVertexProgram ) { + idVec4 localLight; + R_GlobalPointToLocal( drawSurf->space->modelMatrix, + light.legacyViewLight->globalLightOrigin, + localLight.ToVec3() ); + localLight.w = 0.0f; + std::memcpy( caster.localLightOrigin, localLight.ToFloatPtr(), + sizeof( caster.localLightOrigin ) ); + } + + const srfTriangles_t *tri = drawSurf->geo; + const int externalSetting = r_useExternalShadows.GetInteger(); + if ( externalSetting == 0 ) { + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_FULL; + caster.selectedIndexCount = tri->numIndexes; + } else if ( externalSetting == 2 + || !( drawSurf->dsFlags & DSF_VIEW_INSIDE_SHADOW ) ) { + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS; + caster.selectedIndexCount = tri->numShadowIndexesNoCaps; + caster.external = externalSetting != 2; + } else if ( !light.legacyViewLight->viewInsideLight + && !( tri->shadowCapPlaneBits & SHADOW_CAP_INFINITE ) ) { + if ( light.legacyViewLight->viewSeesShadowPlaneBits + & tri->shadowCapPlaneBits ) { + caster.indexSelection = + CLASSIC_INTERACTION_SHADOW_INDEX_NO_FRONT_CAPS; + caster.selectedIndexCount = + tri->numShadowIndexesNoFrontCaps; + } else { + caster.indexSelection = + CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS; + caster.selectedIndexCount = tri->numShadowIndexesNoCaps; + } + caster.external = true; + } else { + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_FULL; + caster.selectedIndexCount = tri->numIndexes; + } + if ( caster.selectedIndexCount == tri->numIndexes ) { + caster.external = false; + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_FULL; + } + caster.preload = !caster.external && caster.selectedIndexCount > 0; + caster.disposition = caster.selectedIndexCount > 0 + ? CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + : CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY; + caster.hash = HashShadowCaster( caster, lightArenaBase ); + domain.shadowCasters[ domain.shadowCasterCount++ ] = caster; + light.shadowCasterCount[ domainChain ]++; + light.shadowCasterTotal++; + if ( caster.disposition == CLASSIC_INTERACTION_SHADOW_CASTER_DRAW ) { + light.drawableShadowCasters++; + light.logicalVolumeDraws++; + if ( caster.preload ) { + light.preloadVolumeDraws++; + } + } else { + light.noopShadowCasters++; + } + packetCursor++; + } + return true; +} + +static float ShadowMapAlphaHashMode( const float modelMatrix[ 16 ] ) { + if ( !r_shadowMapHashedAlpha.GetBool() ) { + return 0.0f; + } + if ( !r_shadowMapStableAlphaHash.GetBool() ) { + return 1.0f; + } + const float seedBase = + idMath::Floor( modelMatrix[ 12 ] * 0.5f ) * 0.06711056f + + idMath::Floor( modelMatrix[ 13 ] * 0.5f ) * 0.00583715f + + idMath::Floor( modelMatrix[ 14 ] * 0.5f ) * 0.01327111f; + return 2.0f + seedBase - idMath::Floor( seedBase ); +} + +static bool AppendEmptyMappedDeformCaster( + classicInteractionDomainLight_t &light, const drawSurf_t *drawSurf, + classicInteractionDomainShadowChain_t domainChain, int chainOrdinal, + int sourceOrdinal, int passPacketIndex, int packetCursor, + int lightArenaBase, classicInteractionBuildError_t &error ) { + classicDeformRole_t role; + classicDeformOutcome_t outcome; + std::uint64_t semanticHash = 0; + if ( !ValidateDeformContract( drawSurf, NULL, + CLASSIC_DEFORM_ROLE_FINALIZED_DRAW, role, outcome, + semanticHash ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( drawSurf ), passPacketIndex, + packetCursor, light.sourceOrdinal, chainOrdinal ); + return false; + } + if ( outcome != CLASSIC_DEFORM_OUTCOME_EMPTY ) { + return true; + } + const materialCoverage_t coverage = drawSurf != NULL + && drawSurf->material != NULL ? drawSurf->material->Coverage() + : MC_BAD; + if ( drawSurf == NULL || drawSurf->geo == NULL || drawSurf->space == NULL + || drawSurf->material == NULL + || ( coverage != MC_OPAQUE && coverage != MC_PERFORATED ) + || drawSurf->classicDeform.resultGeometry.indexCount != 0 + || domain.shadowCasterCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ) { + SetError( error, domain.shadowCasterCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS + ? CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW + : CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( drawSurf ), passPacketIndex, + packetCursor, light.sourceOrdinal, chainOrdinal ); + return false; + } + classicInteractionDomainShadowCaster_t caster; + InitShadowCaster( caster ); + caster.legacyDrawSurf = drawSurf; + caster.legacyViewLight = light.legacyViewLight; + caster.legacyCasterGeometry = drawSurf->geo; + caster.perfectHull = drawSurf->geo->perfectHull; + caster.lightIndex = static_cast( &light - domain.lights ); + caster.sourceOrdinal = sourceOrdinal; + caster.chainOrdinal = chainOrdinal; + caster.chain = domainChain; + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_AMBIENT; + caster.cull = ConvertCull( drawSurf->material->GetCullType(), error ); + if ( error.failure != CLASSIC_INTERACTION_FAILURE_NONE ) { + return false; + } + caster.materialCoverage = coverage; + caster.scissorX1 = drawSurf->scissorRect.x1; + caster.scissorY1 = drawSurf->scissorRect.y1; + caster.scissorX2 = drawSurf->scissorRect.x2; + caster.scissorY2 = drawSurf->scissorRect.y2; + caster.depthMin = idMath::ClampFloat( 0.0f, 1.0f, + drawSurf->scissorRect.zmin ); + caster.depthMax = idMath::ClampFloat( caster.depthMin, 1.0f, + drawSurf->scissorRect.zmax ); + std::memcpy( caster.modelMatrix, drawSurf->space->modelMatrix, + sizeof( caster.modelMatrix ) ); + std::memcpy( caster.modelViewMatrix, drawSurf->space->modelViewMatrix, + sizeof( caster.modelViewMatrix ) ); + std::memcpy( caster.boundsMin, drawSurf->geo->bounds[0].ToFloatPtr(), + sizeof( caster.boundsMin ) ); + std::memcpy( caster.boundsMax, drawSurf->geo->bounds[1].ToFloatPtr(), + sizeof( caster.boundsMax ) ); + caster.ambientGeometry = true; + caster.dynamicCaster = domainChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC + || domainChain == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC; + caster.deformRole = role; + caster.deformOutcome = outcome; + caster.deformContractHash = semanticHash; + caster.hash = HashShadowCaster( caster, lightArenaBase ); + domain.shadowCasters[ domain.shadowCasterCount++ ] = caster; + light.shadowCasterCount[ domainChain ]++; + light.shadowCasterTotal++; + light.noopShadowCasters++; + return true; +} + +static bool PrepareMappedShadowChain( + const idScenePacketFrame &packetFrame, + classicInteractionDomainView_t &view, + classicInteractionDomainLight_t &light, const drawSurf_t *chain, + sceneShadowCasterClass_t packetClass, + classicInteractionDomainShadowChain_t domainChain, + int passPacketIndex, int &packetCursor, int packetEnd, + int lightArenaBase, classicInteractionBuildError_t &error ) { + if ( chain == NULL ) { + return true; + } + if ( domainChain == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT + || domainChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT ) { + // Moment-map caster ownership remains outside this fixed-classic + // corridor. Reject the whole view instead of turning blended coverage + // into opaque depth or silently omitting it. + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + domainChain, passPacketIndex, packetCursor, + light.sourceOrdinal ); + return false; + } + + light.firstShadowCaster[ domainChain ] = domain.shadowCasterCount; + int chainOrdinal = 0; + for ( const drawSurf_t *drawSurf = chain; drawSurf != NULL; + drawSurf = drawSurf->nextOnLight, ++chainOrdinal ) { + if ( !ShadowPacketSurfaceEligible( drawSurf ) ) { + if ( MaterialRequestsClassicDeform( drawSurf ) ) { + // The validator owns both switch isolation and the explicit + // EMPTY proof. Never let an ineligible deform caster disappear + // from the chain merely because deform ownership is disabled. + if ( !AppendEmptyMappedDeformCaster( light, drawSurf, + domainChain, chainOrdinal, + packetCursor + - packetFrame.Pass( passPacketIndex ).firstDrawPacket, + passPacketIndex, packetCursor, lightArenaBase, + error ) ) { + return false; + } + } + continue; + } + if ( packetCursor < 0 || packetCursor >= packetEnd ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + packetCursor, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetCursor ); + classicInteractionDomainShadowChain_t packetDomainChain; + if ( !SceneShadowChainToDomain( packet.shadowCasterClass, + packetDomainChain ) || packetDomainChain != domainChain ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + packet.shadowCasterClass, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + const int sourceOrdinal = packetCursor + - packetFrame.Pass( passPacketIndex ).firstDrawPacket; + if ( !ValidateShadowPacket( packetFrame, packet, packetCursor, + view.viewDef, light.legacyViewLight, light.sourceOrdinal, + RENDER_PASS_SHADOW_MAP, packetClass, chainOrdinal, + sourceOrdinal, drawSurf, error ) ) { + return false; + } + if ( domain.shadowCasterCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW, + domain.shadowCasterCount, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + + const idMaterial *material = drawSurf->material; + const materialCoverage_t coverage = material->Coverage(); + if ( coverage != MC_OPAQUE && coverage != MC_PERFORATED + || ( material->Deform() != DFRM_NONE + && !r_rendererSharedDeform.GetBool() ) + || material->HasGui() || material->HasSubview() + || drawSurf->dynamicTexCoords != NULL + || drawSurf->texGenTransformAndViewOrg != NULL + || drawSurf->decalColorCache != NULL + || drawSurf->decalColorStageCount != 0 ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + coverage, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + const materialResourceTableRecord_t *tableRecord = + R_MaterialResourceTable_RecordForIndex( + packet.materialRecordIndex ); + if ( tableRecord == NULL || tableRecord->material != material + || tableRecord->sourceMaterialRecordIndex + != packet.materialRecordIndex + || tableRecord->tableGeneration == 0 + || ( view.tableGeneration != 0 + && view.tableGeneration + != tableRecord->tableGeneration ) + || packet.instanceRecord->shaderRegisterCount + != material->GetNumRegisters() + || tableRecord->registerCount + != material->GetNumRegisters() ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_MISSING_MATERIAL_RECORD, + packet.materialRecordIndex, passPacketIndex, packetCursor, + light.sourceOrdinal, chainOrdinal ); + return false; + } + view.tableGeneration = tableRecord->tableGeneration; + + const srfTriangles_t *casterGeometry = + drawSurf->geo->ambientSurface != NULL + ? drawSurf->geo->ambientSurface : drawSurf->geo; + classicInteractionDomainShadowCaster_t caster; + InitShadowCaster( caster ); + caster.legacyDrawSurf = drawSurf; + caster.legacyViewLight = light.legacyViewLight; + caster.legacyCasterGeometry = casterGeometry; + caster.perfectHull = casterGeometry->perfectHull; + caster.drawPacketIndex = packetCursor; + caster.lightIndex = static_cast( &light - domain.lights ); + caster.sourceOrdinal = sourceOrdinal; + caster.chainOrdinal = chainOrdinal; + caster.geometryRecordIndex = packet.geometryRecordIndex; + caster.instanceRecordIndex = packet.instanceRecordIndex; + caster.materialTableRecordIndex = packet.materialRecordIndex; + caster.materialId = tableRecord->materialId; + caster.tableGeneration = tableRecord->tableGeneration; + caster.chain = domainChain; + caster.indexSelection = CLASSIC_INTERACTION_SHADOW_INDEX_AMBIENT; + caster.cull = ConvertCull( material->GetCullType(), error ); + if ( error.failure != CLASSIC_INTERACTION_FAILURE_NONE ) { + return false; + } + caster.materialCoverage = coverage; + caster.vertexCount = casterGeometry->numVerts; + caster.totalIndexCount = casterGeometry->numIndexes; + caster.selectedIndexCount = casterGeometry->numIndexes; + caster.scissorX1 = drawSurf->scissorRect.x1; + caster.scissorY1 = drawSurf->scissorRect.y1; + caster.scissorX2 = drawSurf->scissorRect.x2; + caster.scissorY2 = drawSurf->scissorRect.y2; + caster.depthMin = idMath::ClampFloat( 0.0f, 1.0f, + drawSurf->scissorRect.zmin ); + caster.depthMax = idMath::ClampFloat( caster.depthMin, 1.0f, + drawSurf->scissorRect.zmax ); + std::memcpy( caster.modelMatrix, + packet.instanceRecord->modelMatrix, sizeof( caster.modelMatrix ) ); + std::memcpy( caster.modelViewMatrix, + packet.instanceRecord->modelViewMatrix, + sizeof( caster.modelViewMatrix ) ); + std::memcpy( caster.boundsMin, + casterGeometry->bounds[0].ToFloatPtr(), + sizeof( caster.boundsMin ) ); + std::memcpy( caster.boundsMax, + casterGeometry->bounds[1].ToFloatPtr(), + sizeof( caster.boundsMax ) ); + caster.ambientGeometry = true; + caster.dynamicCaster = domainChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC + || domainChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC; + if ( MaterialRequestsClassicDeform( drawSurf ) ) { + caster.deformRole = drawSurf->classicDeform.role; + caster.deformOutcome = drawSurf->classicDeform.outcome; + caster.deformContractHash = drawSurf->classicDeform.semanticHash; + } + + bool activeAlphaStage = false; + bool drawableAlphaStage = false; + if ( coverage == MC_PERFORATED ) { + caster.firstAlphaStage = domain.shadowAlphaStageCount; + const float *registers = drawSurf->shaderRegisters; + const int registerCount = material->GetNumRegisters(); + for ( int stageIndex = 0; + stageIndex < material->GetNumStages(); ++stageIndex ) { + const shaderStage_t *stage = material->GetStage( stageIndex ); + if ( stage == NULL || !stage->hasAlphaTest ) { + continue; + } + bool active = false; + if ( !EvaluateCondition( *stage, registers, registerCount, + active, error, stageIndex ) ) { + return false; + } + if ( !active ) { + continue; + } + activeAlphaStage = true; + float alphaScale = 0.0f; + if ( !ReadRegister( registers, registerCount, + stage->color.registers[3], alphaScale, error, + stageIndex ) ) { + return false; + } + if ( alphaScale <= 0.0f ) { + continue; + } + if ( stage->alphaTestMode != GL_LESS + && stage->alphaTestMode != GL_EQUAL + && stage->alphaTestMode != GL_GREATER + || !TextureStageFixed( *stage, error, stageIndex ) ) { + if ( error.failure == CLASSIC_INTERACTION_FAILURE_NONE ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + stage->alphaTestMode, passPacketIndex, + packetCursor, light.sourceOrdinal, + chainOrdinal, stageIndex ); + } + return false; + } + if ( domain.shadowAlphaStageCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_ALPHA_STAGE_POOL_OVERFLOW, + domain.shadowAlphaStageCount, passPacketIndex, + packetCursor, light.sourceOrdinal, + chainOrdinal, stageIndex ); + return false; + } + classicInteractionDomainShadowAlphaStage_t alpha; + InitShadowAlphaStage( alpha ); + alpha.casterIndex = domain.shadowCasterCount; + alpha.stageIndex = stageIndex; + alpha.alphaTestMode = stage->alphaTestMode; + if ( !ReadRegister( registers, registerCount, + stage->alphaTestRegister, alpha.alphaTestValue, + error, stageIndex ) + || !EvaluateTextureMatrix( stage->texture, + registers, registerCount, alpha.textureMatrix, + error, stageIndex ) + || !AddTexture( stage->texture.image, + alpha.textureResourceId, error, stageIndex ) ) { + return false; + } + alpha.alphaScale = alphaScale; + alpha.alphaHashMode = ShadowMapAlphaHashMode( + caster.modelMatrix ); + alpha.hash = HashShadowAlphaStage( alpha ); + domain.shadowAlphaStages[ + domain.shadowAlphaStageCount++ ] = alpha; + caster.alphaStageCount++; + drawableAlphaStage = true; + } + } + caster.disposition = coverage != MC_PERFORATED + || !activeAlphaStage || drawableAlphaStage + ? CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + : CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY; + if ( caster.disposition + == CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY ) { + // Match the classic perforated caster loop: an active alpha-test + // stage whose evaluated alpha scale is zero emits no indexes. Keep + // the full geometry identity in totalIndexCount, but seal the work + // count as zero so both backends consume the deliberate no-op. + caster.selectedIndexCount = 0; + } + caster.hash = HashShadowCaster( caster, lightArenaBase ); + domain.shadowCasters[ domain.shadowCasterCount++ ] = caster; + light.shadowCasterCount[ domainChain ]++; + light.shadowCasterTotal++; + if ( caster.disposition == CLASSIC_INTERACTION_SHADOW_CASTER_DRAW ) { + light.drawableShadowCasters++; + } else { + light.noopShadowCasters++; + } + packetCursor++; + } + return true; +} + +static int ShadowChainDrawableCount( + const classicInteractionDomainLight_t &light, + classicInteractionDomainShadowChain_t chain ) { + int count = 0; + for ( int i = 0; i < light.shadowCasterCount[ chain ]; ++i ) { + const classicInteractionDomainShadowCaster_t &caster = + domain.shadowCasters[ light.firstShadowCaster[ chain ] + i ]; + if ( caster.disposition == CLASSIC_INTERACTION_SHADOW_CASTER_DRAW ) { + count++; + } + } + return count; +} + +static int ShadowMapReceiverMask( + classicInteractionDomainReceiver_t receiver ) { + return receiver == CLASSIC_INTERACTION_RECEIVER_LOCAL + ? SHADOWMAP_RECEIVER_MASK_LOCAL : SHADOWMAP_RECEIVER_MASK_GLOBAL; +} + +static int ShadowMapProjectedTileSize( + const shadowMapLightClassification_t &classification ) { + const int atlasDiv = Max( 1, classification.atlasDiv ); + const int maxTextureSize = glConfig.maxTextureSize > 0 + ? glConfig.maxTextureSize : 4096; + const int atlasSize = idMath::ClampInt( 2048, 8192, + r_shadowMapAtlasSize.GetInteger() ); + const int maxTileSize = Min( maxTextureSize, atlasSize ) / atlasDiv; + return maxTileSize >= 128 + ? idMath::ClampInt( 128, maxTileSize, + r_shadowMapSize.GetInteger() ) : 0; +} + +static std::uint64_t MakeShadowMapResourcePlanId( int passIndex ) { + return domain.generation != 0 && passIndex >= 0 + ? ( static_cast( domain.generation ) << 32 ) + | static_cast( passIndex + 1 ) + : 0; +} + +static bool BuildShadowMapPasses( + classicInteractionDomainView_t &view, + classicInteractionDomainLight_t &light, + classicInteractionBuildError_t &error ) { + const viewLight_t *viewLight = light.legacyViewLight; + if ( viewLight == NULL || !r_useShadowMap.GetBool() + || !r_shadows.GetBool() || light.ambientLight + || !viewLight->lightShader->LightCastsShadows() ) { + return true; + } + + const shadowMapLightClassification_t classification = + R_ClassifyShadowMapLight( viewLight ); + const bool classSupported = !classification.pointLight + || r_shadowMapPointLights.GetBool(); + classicInteractionDomainShadowProjectedState_t projected; + std::memset( &projected, 0, sizeof( projected ) ); + R_ShadowMapResetProjectedLightState( projected.state ); + classicInteractionDomainShadowPointState_t point; + std::memset( &point, 0, sizeof( point ) ); + + if ( classSupported && classification.pointLight ) { + point.valid = true; + point.faceCount = 6; + const int maxTextureSize = glConfig.maxTextureSize > 0 + ? glConfig.maxTextureSize : 2048; + point.faceSize = idMath::ClampInt( 128, + Min( maxTextureSize, 2048 ), + r_shadowMapPointSize.GetInteger() ); + point.lightOrigin[0] = viewLight->globalLightOrigin[0]; + point.lightOrigin[1] = viewLight->globalLightOrigin[1]; + point.lightOrigin[2] = viewLight->globalLightOrigin[2]; + point.lightOrigin[3] = 1.0f; + point.farDistance = R_ShadowMapPointFarDistance( viewLight ); + const shadowMapPointReceiverSettings_t receiverSettings = + R_ShadowMapPointReceiverSettings( + point.farDistance, point.faceSize ); + point.constantBias = receiverSettings.constantBias; + point.normalBias = receiverSettings.normalBias; + point.normalOffsetScale = receiverSettings.normalOffsetScale; + point.texelBiasScale = receiverSettings.texelBiasScale; + point.filterRadius = + Max( 0.0f, r_shadowMapPointFilterRadius.GetFloat() ); + point.filterTaps = idMath::ClampInt( 1, 13, + r_shadowMapPointFilterTaps.GetInteger() ); + point.filterMode = idMath::ClampInt( 0, 1, + r_shadowMapPointFilterMode.GetInteger() ); + point.depthCompare = r_shadowMapPointDepthCompare.GetBool(); + point.highPrecision = r_shadowMapPointHighPrecision.GetBool(); + } else if ( classSupported ) { + const int tileSize = ShadowMapProjectedTileSize( classification ); + if ( tileSize <= 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + tileSize, view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal ); + return false; + } + R_BuildShadowMapProjectedLightState( viewLight, view.viewDef, + tileSize, projected.state ); + if ( !projected.state.valid + || projected.state.cascadeCount < 1 + || projected.state.cascadeCount + > SHADOWMAP_PROJECTED_MAX_CASCADES + || projected.state.atlasDiv < 1 + || projected.state.atlasDiv > 2 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + projected.state.fallbackReason, + view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal ); + return false; + } + projected.filter = R_ShadowMapProjectedFilterSettings( viewLight ); + projected.constantBias = r_shadowMapBias.GetFloat(); + projected.normalBias = r_shadowMapNormalBias.GetFloat(); + projected.normalOffsetScale = + Max( 0.0f, r_shadowMapNormalOffsetScale.GetFloat() ); + projected.cascadeBlend = + Max( 0.0f, r_shadowMapCascadeBlend.GetFloat() ); + projected.texelBiasScale = + Max( 0.0f, r_shadowMapTexelBiasScale.GetFloat() ); + projected.depthCompare = r_shadowMapDepthCompare.GetBool(); + projected.receiverPlaneBias = + r_shadowMapReceiverPlaneBias.GetBool(); + } + if ( classification.pointLight ) { + const float pointValues[] = { + point.lightOrigin[0], point.lightOrigin[1], point.lightOrigin[2], + point.farDistance, point.constantBias, point.normalBias, + point.normalOffsetScale, point.texelBiasScale, + point.filterRadius + }; + if ( !FloatsAreFinite( pointValues, + static_cast( sizeof( pointValues ) / sizeof( pointValues[0] ) ) ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 21, view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal ); + return false; + } + } else if ( classSupported ) { + const float projectedValues[] = { + projected.filter.filterScale, + projected.filter.filterRadius, + projected.filter.pcssLightRadius, + projected.filter.pcssMaxRadius, + projected.filter.effectiveFilterRadius, + projected.constantBias, projected.normalBias, + projected.normalOffsetScale, projected.cascadeBlend, + projected.texelBiasScale + }; + if ( !FloatsAreFinite( projectedValues, + static_cast( sizeof( projectedValues ) + / sizeof( projectedValues[0] ) ) ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 22, view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal ); + return false; + } + } + + const int globalStaticMapCount = light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC ]; + const int localStaticMapCount = light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC ]; + const int globalDynamicMapCount = light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC ]; + const int localDynamicMapCount = light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ]; + const int globalMapCount = globalStaticMapCount + + globalDynamicMapCount; + const int localMapCount = localStaticMapCount + + localDynamicMapCount; + const int globalSupplementCount = + light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL ]; + const int localSupplementCount = + light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ]; + const int globalStencilCount = + light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL ]; + const int localStencilCount = + light.shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL ]; + + for ( int receiverIndex = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiverIndex <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; + ++receiverIndex ) { + const classicInteractionDomainReceiver_t receiver = + static_cast( receiverIndex ); + const bool receiverNeeded = receiver + == CLASSIC_INTERACTION_RECEIVER_LOCAL + ? viewLight->localInteractions != NULL + : viewLight->globalInteractions != NULL + || ( r_shadowMapTranslucentReceivers.GetBool() + && viewLight->translucentInteractions != NULL ); + if ( !receiverNeeded ) { + continue; + } + const int receiverMask = ShadowMapReceiverMask( receiver ); + const int mappedCasterCount = globalMapCount + + ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + ? localMapCount : 0 ); + const int supplementCount = globalSupplementCount + + ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + ? localSupplementCount : 0 ); + const int fullStencilCount = globalStencilCount + + ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + ? localStencilCount : 0 ); + const int incompleteMapMask = + viewLight->shadowMapIncompleteMapMask + | viewLight->shadowMapPrelightMapMissingMask; + const int incompleteStencilMask = + viewLight->shadowMapIncompleteStencilMask + | ( viewLight->shadowMapPrelightStencilRequiredMask + & ~viewLight->shadowMapPrelightStencilReadyMask ); + const int hybridIncompleteMask = + viewLight->shadowMapHybridIncompleteMask + | viewLight->shadowMapPrelightMapMissingMask; + const bool mapComplete = + ( incompleteMapMask & receiverMask ) == 0; + const bool stencilComplete = fullStencilCount > 0 + && ( incompleteStencilMask & receiverMask ) == 0; + const bool hybridComplete = !mapComplete + && supplementCount > 0 + && ( hybridIncompleteMask & receiverMask ) == 0; + + if ( mappedCasterCount <= 0 || !classSupported + || ( !mapComplete && !hybridComplete ) ) { + if ( !stencilComplete && mappedCasterCount > 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + receiverMask, view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal, receiverIndex ); + return false; + } + continue; + } + if ( domain.shadowMapPassCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW, + domain.shadowMapPassCount, + view.shadowMapPassPacketIndex, -1, + light.sourceOrdinal, receiverIndex ); + return false; + } + + const int passIndex = domain.shadowMapPassCount++; + classicInteractionDomainShadowMapPass_t &pass = + domain.shadowMapPasses[ passIndex ]; + InitShadowMapPass( pass ); + pass.legacyViewLight = viewLight; + pass.lightIndex = static_cast( &light - domain.lights ); + pass.receiver = receiver; + pass.resourceOwner = receiver; + pass.disposition = hybridComplete + ? CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID + : CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED; + pass.mode = hybridComplete + ? CLASSIC_INTERACTION_SHADOW_HYBRID + : ( classification.pointLight + ? CLASSIC_INTERACTION_SHADOW_POINT + : CLASSIC_INTERACTION_SHADOW_PROJECTED ); + pass.lightClass = classification.lightClass; + pass.receiverMask = receiverMask; + pass.mappedCasterCount = mappedCasterCount; + pass.supplementCasterCount = supplementCount; + pass.casterSignature = viewLight->shadowMapCasterSignature; + pass.incompleteMapMask = viewLight->shadowMapIncompleteMapMask; + pass.incompleteStencilMask = + viewLight->shadowMapIncompleteStencilMask; + pass.hybridIncompleteMask = + viewLight->shadowMapHybridIncompleteMask; + pass.prelightMapMissingMask = + viewLight->shadowMapPrelightMapMissingMask; + pass.prelightStencilRequiredMask = + viewLight->shadowMapPrelightStencilRequiredMask; + pass.prelightStencilReadyMask = + viewLight->shadowMapPrelightStencilReadyMask; + pass.resourcePlanId = MakeShadowMapResourcePlanId( passIndex ); + pass.resourceGeneration = domain.generation; + // Publication itself selects mapped ownership for this receiver. A + // complete retained volume is rollback capability, not permission for a + // backend to silently substitute stencil under the same settings. + pass.mapRequired = true; + pass.mapComplete = mapComplete; + pass.stencilComplete = stencilComplete; + pass.hybridComplete = mapComplete || hybridComplete; + pass.hasStaticCasters = globalStaticMapCount + + ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + ? localStaticMapCount : 0 ) > 0; + pass.hasDynamicCasters = globalDynamicMapCount + + ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + ? localDynamicMapCount : 0 ) > 0; + pass.hasAlphaCasters = false; + for ( int chainIndex = + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC; + chainIndex + <= CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC; + ++chainIndex ) { + const classicInteractionDomainShadowChain_t chain = + static_cast( + chainIndex ); + if ( receiver == CLASSIC_INTERACTION_RECEIVER_LOCAL + && ( chain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC + || chain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ) ) { + continue; + } + pass.drawableMappedCasters += + ShadowChainDrawableCount( light, chain ); + pass.noopMappedCasters += + light.shadowCasterCount[ chain ] + - ShadowChainDrawableCount( light, chain ); + for ( int casterIndex = 0; + casterIndex < light.shadowCasterCount[ chain ]; + ++casterIndex ) { + if ( domain.shadowCasters[ + light.firstShadowCaster[ chain ] + + casterIndex ].alphaStageCount > 0 ) { + pass.hasAlphaCasters = true; + } + } + } + const classicInteractionDomainShadowChain_t supplementChains[2] = { + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + }; + for ( int supplementIndex = 0; + supplementIndex < ( receiver + == CLASSIC_INTERACTION_RECEIVER_GLOBAL ? 2 : 1 ); + ++supplementIndex ) { + const classicInteractionDomainShadowChain_t chain = + supplementChains[ supplementIndex ]; + pass.drawableSupplementCasters += + ShadowChainDrawableCount( light, chain ); + pass.noopSupplementCasters += + light.shadowCasterCount[ chain ] + - ShadowChainDrawableCount( light, chain ); + } + pass.allowCacheReuse = r_shadowMapStaticCache.GetBool() + && !pass.hasDynamicCasters && !pass.hasAlphaCasters + && ( classification.cascadeCount == 1 + || r_shadowMapCacheCSM.GetBool() ); + pass.allowCacheUpdate = true; + pass.allowScratch = true; + pass.hashedAlpha = r_shadowMapHashedAlpha.GetBool(); + pass.stableAlphaHash = r_shadowMapStableAlphaHash.GetBool(); + pass.casterCullMode = idMath::ClampInt( 0, 2, + r_shadowMapCasterCulling.GetInteger() ); + pass.polygonFactor = r_shadowMapPolygonFactor.GetFloat(); + pass.polygonOffset = r_shadowMapPolygonOffset.GetFloat(); + pass.projected = projected; + pass.point = point; + + if ( receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL + && localMapCount == 0 + && light.shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] >= 0 ) { + pass.resourceAlias = true; + pass.resourceOwner = CLASSIC_INTERACTION_RECEIVER_LOCAL; + } + pass.hash = HashShadowMapPass( pass ); + light.shadowMapPassIndex[ receiverIndex ] = passIndex; + light.receiverShadowMode[ receiverIndex ] = pass.mode; + } + + const int globalPassIndex = light.shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ]; + if ( r_shadowMapTranslucentReceivers.GetBool() + && viewLight->translucentInteractions != NULL + && globalPassIndex >= 0 ) { + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ] = + domain.shadowMapPasses[ globalPassIndex ].mode; + } + return true; +} + +static void RecomputePlannedStencilWork( + classicInteractionDomainLight_t &light ) { + light.logicalVolumeDraws = 0; + light.preloadVolumeDraws = 0; + const auto volumeMode = []( classicInteractionDomainShadowMode_t mode ) { + return mode == CLASSIC_INTERACTION_SHADOW_STENCIL + || mode == CLASSIC_INTERACTION_SHADOW_HYBRID; + }; + const auto chainFor = []( classicInteractionDomainShadowMode_t mode, + bool local ) { + if ( mode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + return local + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL; + } + return local ? CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + }; + const auto addChain = [&light]( + classicInteractionDomainShadowChain_t chain ) { + for ( int casterIndex = 0; + casterIndex < light.shadowCasterCount[ chain ]; + ++casterIndex ) { + const classicInteractionDomainShadowCaster_t &caster = + domain.shadowCasters[ light.firstShadowCaster[ chain ] + + casterIndex ]; + if ( caster.disposition + != CLASSIC_INTERACTION_SHADOW_CASTER_DRAW ) { + continue; + } + light.logicalVolumeDraws++; + if ( caster.preload ) { + light.preloadVolumeDraws++; + } + } + }; + + // Model the exact classic receiver order rather than counting each selected + // chain only once. Switching between full stencil and hybrid supplements + // clears the stencil state and can require a deliberate resubmission; both + // backends reconcile their committed logical/preload submissions to this + // backend-neutral plan. + classicInteractionDomainShadowMode_t preparedMode = + CLASSIC_INTERACTION_SHADOW_NONE; + bool preparedIncludesLocal = false; + const classicInteractionDomainShadowMode_t localMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_LOCAL ]; + if ( volumeMode( localMode ) ) { + addChain( chainFor( localMode, false ) ); + preparedMode = localMode; + preparedIncludesLocal = false; + } + const classicInteractionDomainShadowMode_t globalMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ]; + if ( volumeMode( globalMode ) ) { + if ( preparedMode != globalMode ) { + addChain( chainFor( globalMode, false ) ); + } + if ( preparedMode != globalMode || !preparedIncludesLocal ) { + addChain( chainFor( globalMode, true ) ); + } + preparedMode = globalMode; + preparedIncludesLocal = true; + } + const classicInteractionDomainShadowMode_t translucentMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ]; + if ( volumeMode( translucentMode ) ) { + if ( preparedMode != translucentMode ) { + addChain( chainFor( translucentMode, false ) ); + addChain( chainFor( translucentMode, true ) ); + } else if ( !preparedIncludesLocal ) { + addChain( chainFor( translucentMode, true ) ); + } + } + // An admitted shadow chain can contain only empty/no-op records. The + // classic path still clears before testing that receiver, otherwise stencil + // values left by the previous light could darken an otherwise unshadowed + // contribution. Keep clear ownership tied to the selected mode, not only to + // drawable volume count. + light.clearStencil = volumeMode( localMode ) || volumeMode( globalMode ) + || volumeMode( translucentMode ); +} + +static bool ValidateDrawPacket( const idScenePacketFrame &packetFrame, + const drawPacket_t &packet, int packetIndex, const viewDef_t *viewDef, + const viewLight_t *viewLight, int lightOrdinal, + sceneInteractionReceiverClass_t receiverClass, int receiverOrdinal, + int sourceOrdinal, const drawSurf_t *drawSurf, + classicInteractionBuildError_t &error ) { + if ( packet.passCategory != RENDER_PASS_ARB2_INTERACTION + || packet.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || packet.viewDef != viewDef || packet.legacyDrawSurf != drawSurf + || packet.interactionLight != viewLight + || packet.interactionLightOrdinal != lightOrdinal + || packet.interactionReceiverClass != receiverClass + || packet.interactionReceiverOrdinal != receiverOrdinal + || packet.interactionSourceOrdinal != sourceOrdinal ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + sourceOrdinal, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + if ( packet.geometryRecordIndex < 0 + || packet.geometryRecordIndex >= packetFrame.NumGeometryRecords() + || packet.geometryRecord == NULL + || packet.geometryRecord + != &packetFrame.GeometryRecord( packet.geometryRecordIndex ) + || packet.geometryRecord->legacyGeometry != drawSurf->geo ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_GEOMETRY_RECORD, + packet.geometryRecordIndex, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( packet.instanceRecordIndex < 0 + || packet.instanceRecordIndex >= packetFrame.NumInstanceRecords() + || packet.instanceRecord == NULL + || packet.instanceRecord + != &packetFrame.InstanceRecord( packet.instanceRecordIndex ) + || packet.instanceRecord->legacySpace != drawSurf->space ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_INSTANCE_RECORD, + packet.instanceRecordIndex, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( packet.materialRecordIndex < 0 + || packet.materialRecordIndex >= packetFrame.NumMaterialRecords() + || packet.materialRecord == NULL + || packet.materialRecord + != &packetFrame.MaterialRecord( packet.materialRecordIndex ) + || packet.materialRecord->material != drawSurf->material ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_MATERIAL_RECORD, + packet.materialRecordIndex, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + const geometryResourceRecord_t &geometry = *packet.geometryRecord; + if ( !packet.hasGeometry || packet.vertexCount <= 0 || packet.indexCount <= 0 + || geometry.vertexCount != packet.vertexCount + || geometry.indexCount != packet.indexCount + || geometry.legacyGeometry == NULL || !packet.hasAmbientCache + || geometry.legacyGeometry->ambientCache == NULL + || ( !packet.hasIndexCache && !geometry.hasClientIndexData ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_GEOMETRY_RECORD, + packet.indexCount, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + bool admittedMaterialDeform = false; + if ( MaterialRequestsClassicDeform( drawSurf ) + && r_rendererSharedDeform.GetBool() ) { + classicDeformRole_t role; + classicDeformOutcome_t outcome; + std::uint64_t semanticHash = 0; + if ( !ValidateDeformContract( drawSurf, &packet, + CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER, role, outcome, + semanticHash ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( drawSurf, &packet ), -1, packetIndex, + lightOrdinal, receiverOrdinal ); + return false; + } + admittedMaterialDeform = true; + } + if ( ( geometry.deformMode != GEOMETRY_DEFORM_NONE + && !admittedMaterialDeform ) + || ( drawSurf->geo != NULL && drawSurf->geo->deformedSurface ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFORM, + geometry.deformMode, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + // CPU-skinned surfaces already expose the authoritative deformed idDrawVert + // stream in the sealed ambient cache. GPU-palette surfaces require a + // different vertex contract and remain an atomic whole-view boundary. + if ( geometry.skinningMode != GEOMETRY_SKINNING_NONE + && geometry.skinningMode != GEOMETRY_SKINNING_CPU ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SKINNING, + geometry.skinningMode, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + if ( geometry.hasPrimBatchMesh ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SPECIAL_SURFACE, + 1, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + const instanceRecord_t &instance = *packet.instanceRecord; + if ( !packet.hasShaderRegisters || !instance.hasShaderRegisters + || instance.legacyShaderRegisters == NULL + || instance.legacyShaderRegisters != drawSurf->shaderRegisters ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + instance.shaderRegisterCount, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( instance.weaponDepthHack || instance.modelDepthHack != 0.0f ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEPTH_HACK, + instance.weaponDepthHack ? 1 : 2, -1, packetIndex, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( instance.negativeScale || MatrixHasNegativeScale( instance.modelMatrix ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NEGATIVE_SCALE, + 1, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + if ( !FloatsAreFinite( instance.modelMatrix, 16 ) + || !FloatsAreFinite( instance.modelViewMatrix, 16 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, packetIndex, lightOrdinal, receiverOrdinal ); + return false; + } + return true; +} + +static bool DetermineLightScale( const viewDef_t &viewDef, + classicInteractionDomainView_t &view, + classicInteractionBuildError_t &error ) { + const float authoredScale = r_lightScale.GetFloat(); + const float rendererMaximum = tr.backEndRendererMaxLight; + if ( !FloatIsFinite( authoredScale ) || !FloatIsFinite( rendererMaximum ) + || rendererMaximum <= 0.0f ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, 1 ); + return false; + } + float maximum = 1.0f; + int lightOrdinal = 0; + for ( const viewLight_t *viewLight = viewDef.viewLights; viewLight != NULL; + viewLight = viewLight->next, ++lightOrdinal ) { + if ( !ViewLightContributesToClassicScale( viewLight ) ) { + continue; + } + const idMaterial *shader = viewLight->lightShader; + if ( shader == NULL ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + 0, -1, -1, lightOrdinal ); + return false; + } + const float *registers = viewLight->shaderRegisters; + const int registerCount = shader->GetNumRegisters(); + if ( registers == NULL || registerCount <= 0 ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + registerCount, -1, -1, lightOrdinal ); + return false; + } + for ( int stageIndex = 0; stageIndex < shader->GetNumStages(); + ++stageIndex ) { + const shaderStage_t *stage = shader->GetStage( stageIndex ); + if ( stage == NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + stageIndex, -1, -1, lightOrdinal, -1, stageIndex ); + return false; + } + for ( int component = 0; component < 3; ++component ) { + float value = 0.0f; + if ( !ReadRegister( registers, registerCount, + stage->color.registers[ component ], value, error, + stageIndex ) ) { + error.lightOrdinal = lightOrdinal; + return false; + } + if ( !AccumulateClassicLightScaleValue( authoredScale, value, + maximum ) ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + component, -1, -1, lightOrdinal, -1, stageIndex ); + return false; + } + } + } + } + view.maxLightValue = maximum; + if ( maximum <= rendererMaximum ) { + view.lightScale = authoredScale; + view.overBright = 1.0f; + } else { + view.lightScale = authoredScale * rendererMaximum / maximum; + view.overBright = maximum / rendererMaximum; + } + if ( !FloatIsFinite( view.lightScale ) || !FloatIsFinite( view.overBright ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, 2 ); + return false; + } + return true; +} + +static bool ValidateLight( const viewLight_t &viewLight, int lightOrdinal, + classicInteractionBuildError_t &error ) { + const idMaterial *shader = viewLight.lightShader; + if ( shader == NULL || shader->IsFogLight() || shader->IsBlendLight() + || !shader->IsDrawn() || shader->GetNumStages() <= 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + 0, -1, -1, lightOrdinal ); + return false; + } + if ( viewLight.shaderRegisters == NULL || shader->GetNumRegisters() <= 0 ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + shader->GetNumRegisters(), -1, -1, lightOrdinal ); + return false; + } + if ( !FloatsAreFinite( viewLight.globalLightOrigin.ToFloatPtr(), 3 ) + || !FloatsAreFinite( viewLight.lightRadius.ToFloatPtr(), 3 ) + || !FloatsAreFinite( viewLight.lightProject[ 0 ].ToFloatPtr(), 16 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, -1, lightOrdinal ); + return false; + } + if ( !ImageReady( viewLight.falloffImage, error, -1 ) ) { + error.lightOrdinal = lightOrdinal; + return false; + } + for ( int stageIndex = 0; stageIndex < shader->GetNumStages(); + ++stageIndex ) { + const shaderStage_t *stage = shader->GetStage( stageIndex ); + if ( stage == NULL || !TextureStageFixed( *stage, error, stageIndex ) ) { + error.lightOrdinal = lightOrdinal; + return false; + } + } + return true; +} + +static bool ValidateSurfaceMaterial( const drawPacket_t &packet, + const drawSurf_t &drawSurf, int lightOrdinal, int receiverOrdinal, + classicInteractionDomainView_t &view, + const materialResourceTableRecord_t *&tableRecord, + classicInteractionBuildError_t &error ) { + const idMaterial *material = drawSurf.material; + if ( material == NULL || !material->IsDrawn() + || !material->ReceivesLighting() || material->IsPortalSky() + || material->SuppressInSubview() || material->HasGui() + || material->HasSubview() || material->GetSort() >= SS_POST_PROCESS ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + 0, -1, -1, lightOrdinal, receiverOrdinal ); + return false; + } + if ( ( material->Deform() != DFRM_NONE + && !r_rendererSharedDeform.GetBool() ) + || ( drawSurf.geo != NULL && drawSurf.geo->deformedSurface ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_DEFORM, + material->Deform(), -1, -1, lightOrdinal, receiverOrdinal ); + return false; + } + if ( ( drawSurf.dsFlags & ( DSF_BSE_EFFECT | DSF_OUTLINE_ONLY ) ) != 0 + || drawSurf.dynamicTexCoords != NULL + || drawSurf.texGenTransformAndViewOrg != NULL + || drawSurf.decalColorCache != NULL + || drawSurf.decalColorStageCount != 0 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SPECIAL_SURFACE, + drawSurf.dsFlags, -1, -1, lightOrdinal, receiverOrdinal ); + return false; + } + if ( RB_FlatDiffuseSurfaceActive( &drawSurf ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_FLAT_DIFFUSE, + 0, -1, -1, lightOrdinal, receiverOrdinal ); + return false; + } + tableRecord = R_MaterialResourceTable_RecordForIndex( + packet.materialRecordIndex ); + if ( tableRecord == NULL || tableRecord->material != material + || tableRecord->sourceMaterialRecordIndex + != packet.materialRecordIndex ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MISSING_MATERIAL_RECORD, + packet.materialRecordIndex, -1, -1, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( tableRecord->tableGeneration == 0 + || ( view.tableGeneration != 0 + && view.tableGeneration != tableRecord->tableGeneration ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_STALE_MATERIAL_RECORD, + tableRecord->tableGeneration, -1, -1, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( packet.instanceRecord == NULL + || packet.instanceRecord->shaderRegisterCount + != tableRecord->registerCount + || tableRecord->registerCount != material->GetNumRegisters() ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + tableRecord->registerCount, -1, -1, lightOrdinal, + receiverOrdinal ); + return false; + } + if ( tableRecord->hasPBR || tableRecord->hasCustomProgram + || tableRecord->hasCustomGLSL ) { + SetError( error, tableRecord->hasPBR + ? CLASSIC_INTERACTION_FAILURE_ENHANCED_MATERIAL + : CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING, + packet.materialRecordIndex, -1, -1, lightOrdinal, + receiverOrdinal ); + return false; + } + const float polygonFactor = r_offsetFactor.GetFloat(); + const float polygonUnits = r_offsetUnits.GetFloat() + * material->GetPolygonOffset(); + if ( !FloatIsFinite( polygonFactor ) || !FloatIsFinite( polygonUnits ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, -1, lightOrdinal, receiverOrdinal ); + return false; + } + for ( int stageIndex = 0; stageIndex < material->GetNumStages(); + ++stageIndex ) { + const shaderStage_t *stage = material->GetStage( stageIndex ); + if ( stage == NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + stageIndex, -1, -1, lightOrdinal, receiverOrdinal, + stageIndex ); + return false; + } + if ( stage->newStage != NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING, + stageIndex, -1, -1, lightOrdinal, receiverOrdinal, + stageIndex ); + return false; + } + if ( stage->lighting != SL_AMBIENT + && !TextureStageFixed( *stage, error, stageIndex ) ) { + error.lightOrdinal = lightOrdinal; + error.receiverOrdinal = receiverOrdinal; + return false; + } + } + view.tableGeneration = tableRecord->tableGeneration; + return true; +} + +static bool BuildBasePrimitive( classicInteractionDomainPrimitive_t &base, + const classicInteractionDomainView_t &view, + const classicInteractionDomainLight_t &light, + const classicInteractionDomainSurface_t &surface, + const drawPacket_t &packet, const shaderStage_t &lightStage, + int lightStageIndex, std::uint64_t lightImageResourceId, + std::uint64_t falloffImageResourceId, + const float lightTextureMatrix[ 2 ][ 4 ], + classicInteractionBuildError_t &error ) { + InitPrimitive( base ); + const drawSurf_t *drawSurf = surface.legacyDrawSurf; + const viewLight_t *viewLight = light.legacyViewLight; + if ( drawSurf == NULL || drawSurf->space == NULL || viewLight == NULL + || view.viewDef == NULL || packet.instanceRecord == NULL ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_PACKET, + 0, -1, surface.drawPacketIndex, light.sourceOrdinal, + surface.receiverOrdinal, lightStageIndex ); + return false; + } + base.legacyDrawSurf = drawSurf; + base.legacyViewLight = viewLight; + base.lightIndex = surface.lightIndex; + base.surfaceIndex = static_cast( &surface - domain.surfaces ); + base.lightStageIndex = lightStageIndex; + base.receiver = surface.receiver; + base.depth = surface.receiver == CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT + ? CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL + : CLASSIC_INTERACTION_DEPTH_EQUAL; + base.cull = ConvertCull( drawSurf->material->GetCullType(), error ); + if ( error.failure != CLASSIC_INTERACTION_FAILURE_NONE ) { + return false; + } + base.vertexCount = packet.vertexCount; + base.firstIndex = packet.firstIndex; + base.indexCount = packet.indexCount; + base.vertexOffset = packet.vertexOffset; + base.scissorX1 = packet.scissorX1; + base.scissorY1 = packet.scissorY1; + base.scissorX2 = packet.scissorX2; + base.scissorY2 = packet.scissorY2; + base.ambientLight = viewLight->lightShader->IsAmbientLight(); + base.polygonOffsetEnabled = + drawSurf->material->TestMaterialFlag( MF_POLYGONOFFSET ); + base.polygonOffsetFactor = r_offsetFactor.GetFloat(); + base.polygonOffsetUnits = r_offsetUnits.GetFloat() + * drawSurf->material->GetPolygonOffset(); + base.lightImageResourceId = lightImageResourceId; + base.lightFalloffImageResourceId = falloffImageResourceId; + std::memcpy( base.modelMatrix, packet.instanceRecord->modelMatrix, + sizeof( base.modelMatrix ) ); + std::memcpy( base.modelViewMatrix, packet.instanceRecord->modelViewMatrix, + sizeof( base.modelViewMatrix ) ); + + idVec3 localPoint; + R_GlobalPointToLocal( drawSurf->space->modelMatrix, + viewLight->globalLightOrigin, localPoint ); + base.localLightOrigin[ 0 ] = localPoint[ 0 ]; + base.localLightOrigin[ 1 ] = localPoint[ 1 ]; + base.localLightOrigin[ 2 ] = localPoint[ 2 ]; + base.localLightOrigin[ 3 ] = 0.0f; + R_GlobalPointToLocal( drawSurf->space->modelMatrix, + view.viewDef->renderView.vieworg, localPoint ); + base.localViewOrigin[ 0 ] = localPoint[ 0 ]; + base.localViewOrigin[ 1 ] = localPoint[ 1 ]; + base.localViewOrigin[ 2 ] = localPoint[ 2 ]; + base.localViewOrigin[ 3 ] = 1.0f; + for ( int planeIndex = 0; planeIndex < 4; ++planeIndex ) { + idPlane localPlane; + R_GlobalPlaneToLocal( drawSurf->space->modelMatrix, + viewLight->lightProject[ planeIndex ], localPlane ); + std::memcpy( base.lightProjection[ planeIndex ], localPlane.ToFloatPtr(), + sizeof( base.lightProjection[ planeIndex ] ) ); + } + if ( lightStage.texture.hasMatrix ) { + BakeTextureMatrixIntoProjection( base.lightProjection, + lightTextureMatrix ); + } + if ( !FloatsAreFinite( base.localLightOrigin, 4 ) + || !FloatsAreFinite( base.localViewOrigin, 4 ) + || !FloatsAreFinite( &base.lightProjection[ 0 ][ 0 ], 16 ) + || !FloatsAreFinite( base.modelViewMatrix, 16 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, surface.drawPacketIndex, light.sourceOrdinal, + surface.receiverOrdinal, lightStageIndex ); + return false; + } + return true; +} + +static bool PositiveRGB( const float color[ 4 ] ) { + return color[ 0 ] > 0.0f || color[ 1 ] > 0.0f || color[ 2 ] > 0.0f; +} + +static bool SubmitPrimitive( const classicInteractionDomainPrimitive_t &base, + const classicInteractionWork_t &work, + classicInteractionDomainSurface_t &surface, + int lightArenaBase, int surfaceArenaBase, + classicInteractionBuildError_t &error ) { + if ( domain.primitiveCount >= CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ) { + const int lightOrdinal = base.lightIndex >= 0 + && base.lightIndex < domain.lightCount + ? domain.lights[ base.lightIndex ].sourceOrdinal : -1; + SetError( error, CLASSIC_INTERACTION_FAILURE_PRIMITIVE_POOL_OVERFLOW, + domain.primitiveCount, -1, surface.drawPacketIndex, + lightOrdinal, + surface.receiverOrdinal, base.lightStageIndex ); + return false; + } + classicInteractionDomainPrimitive_t primitive = base; + primitive.bumpStageIndex = work.bumpStageIndex; + primitive.diffuseStageIndex = work.diffuseStageIndex; + primitive.specularStageIndex = work.specularStageIndex; + primitive.vertexColor = work.vertexColor; + std::memcpy( primitive.diffuseColor, work.diffuseColor, + sizeof( primitive.diffuseColor ) ); + std::memcpy( primitive.specularColor, work.specularColor, + sizeof( primitive.specularColor ) ); + std::memcpy( primitive.bumpMatrix, work.bumpMatrix, + sizeof( primitive.bumpMatrix ) ); + std::memcpy( primitive.diffuseMatrix, work.diffuseMatrix, + sizeof( primitive.diffuseMatrix ) ); + std::memcpy( primitive.specularMatrix, work.specularMatrix, + sizeof( primitive.specularMatrix ) ); + + if ( work.bumpImage == NULL ) { + primitive.disposition = + CLASSIC_INTERACTION_PRIMITIVE_NOOP_MISSING_BUMP; + } else { + const idImage *bumpImage = r_skipBump.GetBool() + ? ( globalImages != NULL ? globalImages->flatNormalMap : NULL ) + : work.bumpImage; + const idImage *diffuseImage = work.diffuseImage == NULL + || r_skipDiffuse.GetBool() + ? ( globalImages != NULL ? globalImages->blackImage : NULL ) + : work.diffuseImage; + const idImage *specularImage = work.specularImage == NULL + || r_skipSpecular.GetBool() || primitive.ambientLight + ? ( globalImages != NULL ? globalImages->blackImage : NULL ) + : work.specularImage; + if ( !AddTexture( bumpImage, primitive.bumpImageResourceId, error, + work.bumpStageIndex ) + || !AddTexture( diffuseImage, primitive.diffuseImageResourceId, + error, work.diffuseStageIndex ) + || !AddTexture( specularImage, + primitive.specularImageResourceId, error, + work.specularStageIndex ) ) { + error.drawPacketIndex = surface.drawPacketIndex; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + const classicInteractionDomainTexture_t *diffuseTexture = + R_ClassicInteractionDomain_ResolveTexture( + primitive.diffuseImageResourceId ); + const classicInteractionDomainTexture_t *specularTexture = + R_ClassicInteractionDomain_ResolveTexture( + primitive.specularImageResourceId ); + const bool diffuseBlack = diffuseTexture == NULL || globalImages == NULL + || diffuseTexture->image == globalImages->blackImage; + const bool specularBlack = specularTexture == NULL || globalImages == NULL + || specularTexture->image == globalImages->blackImage; + primitive.disposition = + ( PositiveRGB( primitive.diffuseColor ) && !diffuseBlack ) + || ( PositiveRGB( primitive.specularColor ) && !specularBlack ) + ? CLASSIC_INTERACTION_PRIMITIVE_DRAW + : CLASSIC_INTERACTION_PRIMITIVE_NOOP_BLACK; + } + if ( !FloatsAreFinite( primitive.diffuseColor, 4 ) + || !FloatsAreFinite( primitive.specularColor, 4 ) + || !FloatsAreFinite( &primitive.bumpMatrix[ 0 ][ 0 ], 8 ) + || !FloatsAreFinite( &primitive.diffuseMatrix[ 0 ][ 0 ], 8 ) + || !FloatsAreFinite( &primitive.specularMatrix[ 0 ][ 0 ], 8 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, surface.drawPacketIndex, -1, surface.receiverOrdinal, + base.lightStageIndex ); + return false; + } + primitive.hash = HashPrimitive( primitive, lightArenaBase, + surfaceArenaBase ); + domain.primitives[ domain.primitiveCount++ ] = primitive; + surface.primitiveCount++; + if ( primitive.disposition == CLASSIC_INTERACTION_PRIMITIVE_DRAW ) { + surface.drawablePrimitiveCount++; + } else { + surface.noopPrimitiveCount++; + } + return true; +} + +static bool PopulateLightRecord( classicInteractionDomainLight_t &light, + const viewLight_t &viewLight, int sourceOrdinal, + classicInteractionBuildError_t &error ) { + InitLight( light ); + light.legacyViewLight = &viewLight; + light.sourceOrdinal = sourceOrdinal; + light.firstSurface = domain.surfaceCount; + light.firstPrimitive = domain.primitiveCount; + light.scissorX1 = viewLight.scissorRect.x1; + light.scissorY1 = viewLight.scissorRect.y1; + light.scissorX2 = viewLight.scissorRect.x2; + light.scissorY2 = viewLight.scissorRect.y2; + light.globalLightOrigin[ 0 ] = viewLight.globalLightOrigin[ 0 ]; + light.globalLightOrigin[ 1 ] = viewLight.globalLightOrigin[ 1 ]; + light.globalLightOrigin[ 2 ] = viewLight.globalLightOrigin[ 2 ]; + light.globalLightOrigin[ 3 ] = 1.0f; + light.lightRadius[ 0 ] = viewLight.lightRadius[ 0 ]; + light.lightRadius[ 1 ] = viewLight.lightRadius[ 1 ]; + light.lightRadius[ 2 ] = viewLight.lightRadius[ 2 ]; + light.lightRadius[ 3 ] = 0.0f; + for ( int plane = 0; plane < 4; ++plane ) { + std::memcpy( light.lightProject[ plane ], + viewLight.lightProject[ plane ].ToFloatPtr(), + sizeof( light.lightProject[ plane ] ) ); + } + light.pointLight = viewLight.pointLight; + light.parallel = viewLight.parallel; + light.ambientLight = viewLight.lightShader->IsAmbientLight(); + light.shadowClassified = true; + light.lightStageCount = viewLight.lightShader->GetNumStages(); + for ( int stageIndex = 0; stageIndex < light.lightStageCount; ++stageIndex ) { + bool active = false; + if ( !EvaluateCondition( *viewLight.lightShader->GetStage( stageIndex ), + viewLight.shaderRegisters, + viewLight.lightShader->GetNumRegisters(), active, error, + stageIndex ) ) { + error.lightOrdinal = sourceOrdinal; + return false; + } + if ( active ) { + light.activeLightStageCount++; + } else { + light.inactiveLightStageCount++; + } + } + return true; +} + +static bool PrepareSurfacePrimitives( + const classicInteractionDomainView_t &view, + const classicInteractionDomainLight_t &light, + classicInteractionDomainSurface_t &surface, + const drawPacket_t &packet, + int lightArenaBase, int surfaceArenaBase, + classicInteractionBuildError_t &error ) { + const drawSurf_t *drawSurf = surface.legacyDrawSurf; + const viewLight_t *viewLight = light.legacyViewLight; + const idMaterial *surfaceShader = drawSurf->material; + const idMaterial *lightShader = viewLight->lightShader; + const float *surfaceRegisters = drawSurf->shaderRegisters; + const float *lightRegisters = viewLight->shaderRegisters; + const int surfaceRegisterCount = surfaceShader->GetNumRegisters(); + const int lightRegisterCount = lightShader->GetNumRegisters(); + surface.firstPrimitive = domain.primitiveCount; + + for ( int lightStageIndex = 0; + lightStageIndex < lightShader->GetNumStages(); ++lightStageIndex ) { + const shaderStage_t &lightStage = + *lightShader->GetStage( lightStageIndex ); + bool lightActive = false; + if ( !EvaluateCondition( lightStage, lightRegisters, + lightRegisterCount, lightActive, error, lightStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + if ( !lightActive ) { + continue; + } + + std::uint64_t lightImageResourceId = 0; + std::uint64_t falloffImageResourceId = 0; + if ( !AddTexture( lightStage.texture.image, lightImageResourceId, + error, lightStageIndex ) + || !AddTexture( viewLight->falloffImage, + falloffImageResourceId, error, lightStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + float lightTextureMatrix[ 2 ][ 4 ]; + if ( !EvaluateTextureMatrix( lightStage.texture, lightRegisters, + lightRegisterCount, lightTextureMatrix, error, + lightStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + float lightColor[ 4 ]; + if ( !EvaluateStageColor( lightStage, lightRegisters, + lightRegisterCount, lightColor, false, error, + lightStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + lightColor[ 0 ] *= view.lightScale; + lightColor[ 1 ] *= view.lightScale; + lightColor[ 2 ] *= view.lightScale; + if ( !FloatsAreFinite( lightColor, 4 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + 0, -1, surface.drawPacketIndex, light.sourceOrdinal, + surface.receiverOrdinal, lightStageIndex ); + return false; + } + + classicInteractionDomainPrimitive_t base; + if ( !BuildBasePrimitive( base, view, light, surface, packet, + lightStage, lightStageIndex, lightImageResourceId, + falloffImageResourceId, lightTextureMatrix, error ) ) { + return false; + } + classicInteractionWork_t work; + InitWork( work ); + for ( int surfaceStageIndex = 0; + surfaceStageIndex < surfaceShader->GetNumStages(); + ++surfaceStageIndex ) { + const shaderStage_t &surfaceStage = + *surfaceShader->GetStage( surfaceStageIndex ); + if ( surfaceStage.lighting == SL_AMBIENT ) { + continue; + } + surface.surfaceStageCount++; + bool surfaceActive = false; + if ( !EvaluateCondition( surfaceStage, surfaceRegisters, + surfaceRegisterCount, surfaceActive, error, + surfaceStageIndex ) ) { + error.lightOrdinal = light.sourceOrdinal; + error.receiverOrdinal = surface.receiverOrdinal; + return false; + } + if ( !surfaceActive ) { + surface.inactiveSurfaceStageCount++; + continue; + } + surface.activeSurfaceStageCount++; + switch ( surfaceStage.lighting ) { + case SL_BUMP: + if ( !SubmitPrimitive( base, work, surface, lightArenaBase, + surfaceArenaBase, error ) ) { + return false; + } + work.diffuseImage = NULL; + work.specularImage = NULL; + work.diffuseImageResourceId = 0; + work.specularImageResourceId = 0; + work.diffuseStageIndex = -1; + work.specularStageIndex = -1; + work.bumpImage = surfaceStage.texture.image; + work.bumpStageIndex = surfaceStageIndex; + if ( !EvaluateTextureMatrix( surfaceStage.texture, + surfaceRegisters, surfaceRegisterCount, work.bumpMatrix, + error, surfaceStageIndex ) ) { + return false; + } + break; + case SL_DIFFUSE: { + if ( work.diffuseImage != NULL + && !SubmitPrimitive( base, work, surface, + lightArenaBase, surfaceArenaBase, error ) ) { + return false; + } + work.diffuseImage = surfaceStage.texture.image; + work.diffuseStageIndex = surfaceStageIndex; + if ( !EvaluateTextureMatrix( surfaceStage.texture, + surfaceRegisters, surfaceRegisterCount, + work.diffuseMatrix, error, surfaceStageIndex ) + || !EvaluateStageColor( surfaceStage, + surfaceRegisters, surfaceRegisterCount, + work.diffuseColor, true, error, + surfaceStageIndex ) ) { + return false; + } + for ( int component = 0; component < 4; ++component ) { + work.diffuseColor[ component ] *= lightColor[ component ]; + } + work.vertexColor = ConvertVertexColor( + surfaceStage.vertexColor, error, surfaceStageIndex ); + if ( error.failure != CLASSIC_INTERACTION_FAILURE_NONE ) { + return false; + } + break; + } + case SL_SPECULAR: { + if ( work.specularImage != NULL + && !SubmitPrimitive( base, work, surface, + lightArenaBase, surfaceArenaBase, error ) ) { + return false; + } + work.specularImage = surfaceStage.texture.image; + work.specularStageIndex = surfaceStageIndex; + if ( !EvaluateTextureMatrix( surfaceStage.texture, + surfaceRegisters, surfaceRegisterCount, + work.specularMatrix, error, surfaceStageIndex ) + || !EvaluateStageColor( surfaceStage, + surfaceRegisters, surfaceRegisterCount, + work.specularColor, true, error, + surfaceStageIndex ) ) { + return false; + } + for ( int component = 0; component < 4; ++component ) { + work.specularColor[ component ] *= lightColor[ component ]; + } + work.vertexColor = ConvertVertexColor( + surfaceStage.vertexColor, error, surfaceStageIndex ); + if ( error.failure != CLASSIC_INTERACTION_FAILURE_NONE ) { + return false; + } + break; + } + case SL_AMBIENT: + default: + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + surfaceStage.lighting, -1, surface.drawPacketIndex, + light.sourceOrdinal, surface.receiverOrdinal, + surfaceStageIndex ); + return false; + } + } + if ( !SubmitPrimitive( base, work, surface, lightArenaBase, + surfaceArenaBase, error ) ) { + return false; + } + } + return true; +} + +static bool SceneIsInteractionCandidate( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene ) { + if ( scene.viewDef == NULL + || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || scene.viewDef->viewEntitys == NULL ) { + return false; + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) ) { + return true; + } + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + if ( packetFrame.Pass( scene.firstPassPacket + localPass ).passCategory + == RENDER_PASS_ARB2_INTERACTION ) { + return true; + } + } + return false; +} + +static classicInteractionDomainView_t *FindMutableView( + const viewDef_t *viewDef ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].viewDef == viewDef ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +static int ViewIndex( const classicInteractionDomainView_t *view ) { + if ( view == NULL ) { + return -1; + } + const ptrdiff_t index = view - domain.views; + if ( index < 0 || index >= domain.viewCount + || view != &domain.views[ index ] ) { + return -1; + } + return static_cast( index ); +} + +static int LightIndex( const classicInteractionDomainLight_t *light ) { + if ( light == NULL ) { + return -1; + } + const ptrdiff_t index = light - domain.lights; + if ( index < 0 || index >= domain.lightCount + || light != &domain.lights[ index ] ) { + return -1; + } + return static_cast( index ); +} + +static int SurfaceIndex( const classicInteractionDomainSurface_t *surface ) { + if ( surface == NULL ) { + return -1; + } + const ptrdiff_t index = surface - domain.surfaces; + if ( index < 0 || index >= domain.surfaceCount + || surface != &domain.surfaces[ index ] ) { + return -1; + } + return static_cast( index ); +} + +static int ShadowCasterIndex( + const classicInteractionDomainShadowCaster_t *caster ) { + if ( caster == NULL ) { + return -1; + } + const ptrdiff_t index = caster - domain.shadowCasters; + if ( index < 0 || index >= domain.shadowCasterCount + || caster != &domain.shadowCasters[ index ] ) { + return -1; + } + return static_cast( index ); +} + +static bool R_ClassicInteractionDomain_PrepareView( + const idScenePacketFrame &packetFrame, const scenePacket_t &scene, + classicInteractionDomainView_t &view ) { + const classicInteractionCheckpoint_t arenaCheckpoint = { + domain.lightCount, domain.surfaceCount, domain.primitiveCount, + domain.shadowCasterCount, domain.shadowAlphaStageCount, + domain.shadowMapPassCount, domain.textureCount + }; + classicInteractionBuildError_t error; + InitError( error ); + const scenePacketFrameStats_t &packetStats = packetFrame.Stats(); + const materialResourceTableStats_t &tableStats = + R_MaterialResourceTable_Stats(); + if ( packetStats.overflow ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SCENE_PACKET_OVERFLOW, + packetStats.overflowCause ); + return FailView( view, arenaCheckpoint, error ); + } + if ( !tableStats.prepared ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_NOT_PREPARED ); + return FailView( view, arenaCheckpoint, error ); + } + if ( !tableStats.available ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNAVAILABLE ); + return FailView( view, arenaCheckpoint, error ); + } + if ( tableStats.overflow ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_OVERFLOW ); + return FailView( view, arenaCheckpoint, error ); + } + + const viewDef_t *viewDef = view.viewDef; + const int allowedRenderFlags = RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef == NULL || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || viewDef->viewEntitys == NULL || viewDef->renderWorld == NULL + || viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 + || viewDef->renderView.globalMaterial != NULL + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 + || !FloatsAreFinite( viewDef->projectionMatrix, 16 ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_VIEW, + viewDef != NULL ? viewDef->renderFlags : 0 ); + return FailView( view, arenaCheckpoint, error ); + } + if ( r_skipInteractions.GetBool() ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, 1 ); + return FailView( view, arenaCheckpoint, error ); + } + if ( R_CelShadingAnyEnabled() ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_CEL_SHADING ); + return FailView( view, arenaCheckpoint, error ); + } + if ( r_enhancedMaterials.GetBool() || r_pbrMaterials.GetBool() + || r_pbrInferFromLegacyMaterials.GetBool() ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_ENHANCED_MATERIAL ); + return FailView( view, arenaCheckpoint, error ); + } + if ( r_useSimpleInteraction.GetBool() || r_testARBProgram.GetBool() + || r_appleARB2Interactions.GetInteger() != 0 + || glConfig.disableARB2Interactions ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_ALTERNATE_INTERACTION_PROGRAM ); + return FailView( view, arenaCheckpoint, error ); + } + if ( !FloatIsFinite( r_offsetFactor.GetFloat() ) + || !FloatIsFinite( r_offsetUnits.GetFloat() ) + || !FloatIsFinite( r_shadowPolygonFactor.GetFloat() ) + || !FloatIsFinite( r_shadowPolygonOffset.GetFloat() ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, 3 ); + return FailView( view, arenaCheckpoint, error ); + } + if ( r_showShadows.GetInteger() != 0 + || r_useExternalShadows.GetInteger() < 0 + || r_useExternalShadows.GetInteger() > 2 ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + r_showShadows.GetInteger() != 0 + ? r_showShadows.GetInteger() + : 100 + r_useExternalShadows.GetInteger() ); + return FailView( view, arenaCheckpoint, error ); + } + view.useScissor = r_useScissor.GetBool(); + view.useShadowVertexProgram = r_useShadowVertexProgram.GetBool(); + view.preferTwoSidedStencil = r_useTwoSidedStencil.GetBool(); + view.useDepthBounds = r_useDepthBoundsTest.GetBool(); + view.stencilTranslucentShadows = + r_stencilTranslucentShadows.GetBool(); + view.shadowPolygonFactor = r_shadowPolygonFactor.GetFloat(); + view.shadowPolygonUnits = -r_shadowPolygonOffset.GetFloat(); + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) + || !RangeFits( scene.firstDrawPacket, scene.drawPacketCount, + packetFrame.NumDrawPackets() ) ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_INVALID_SCENE_RANGE, + scene.passPacketCount ); + return FailView( view, arenaCheckpoint, error ); + } + + const passPacket_t *interactionPass = NULL; + const passPacket_t *stencilShadowPass = NULL; + const passPacket_t *shadowMapPass = NULL; + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + const int passPacketIndex = scene.firstPassPacket + localPass; + const passPacket_t &pass = packetFrame.Pass( passPacketIndex ); + if ( !RangeFits( pass.firstDrawPacket, pass.drawPacketCount, + packetFrame.NumDrawPackets() ) + || pass.firstDrawPacket < scene.firstDrawPacket + || pass.firstDrawPacket + > scene.firstDrawPacket + scene.drawPacketCount + || pass.drawPacketCount > scene.firstDrawPacket + + scene.drawPacketCount - pass.firstDrawPacket ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_RANGE, + pass.drawPacketCount, passPacketIndex ); + return FailView( view, arenaCheckpoint, error ); + } + if ( pass.passCategory == RENDER_PASS_ARB2_INTERACTION ) { + if ( interactionPass != NULL || !pass.enabled || pass.commandOnly + || ( pass.packetCategory != SCENE_PACKET_CATEGORY_WORLD + && !( pass.drawPacketCount == 0 + && pass.packetCategory + == SCENE_PACKET_CATEGORY_COMMAND ) ) ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_INVALID_INTERACTION_PASS, + pass.passCategory, passPacketIndex ); + return FailView( view, arenaCheckpoint, error ); + } + interactionPass = &pass; + view.interactionPassPacketIndex = passPacketIndex; + continue; + } + if ( pass.passCategory == RENDER_PASS_STENCIL_SHADOW + || pass.passCategory == RENDER_PASS_SHADOW_MAP ) { + const bool stencilPass = + pass.passCategory == RENDER_PASS_STENCIL_SHADOW; + const passPacket_t *&selected = stencilPass + ? stencilShadowPass : shadowMapPass; + if ( selected != NULL || !pass.enabled || pass.commandOnly + || pass.packetCategory != SCENE_PACKET_CATEGORY_WORLD ) { + SetError( error, + stencilPass + ? CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH + : CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + pass.passCategory, passPacketIndex ); + return FailView( view, arenaCheckpoint, error ); + } + selected = &pass; + if ( stencilPass ) { + view.stencilShadowPassPacketIndex = passPacketIndex; + } else { + view.shadowMapPassPacketIndex = passPacketIndex; + } + } + } + if ( interactionPass == NULL ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_INVALID_INTERACTION_PASS, -1 ); + return FailView( view, arenaCheckpoint, error ); + } + view.packetDrawCount = interactionPass->drawPacketCount; + view.shadowPacketDrawCount = stencilShadowPass != NULL + ? stencilShadowPass->drawPacketCount : 0; + if ( shadowMapPass != NULL ) { + view.shadowPacketDrawCount += shadowMapPass->drawPacketCount; + } + if ( !DetermineLightScale( *viewDef, view, error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + + int packetCursor = interactionPass->firstDrawPacket; + const int packetEnd = interactionPass->firstDrawPacket + + interactionPass->drawPacketCount; + int expectedSourceOrdinal = 0; + int shadowPacketCursor = stencilShadowPass != NULL + ? stencilShadowPass->firstDrawPacket : -1; + const int shadowPacketEnd = stencilShadowPass != NULL + ? stencilShadowPass->firstDrawPacket + + stencilShadowPass->drawPacketCount : -1; + int mapPacketCursor = shadowMapPass != NULL + ? shadowMapPass->firstDrawPacket : -1; + const int mapPacketEnd = shadowMapPass != NULL + ? shadowMapPass->firstDrawPacket + + shadowMapPass->drawPacketCount : -1; + int sourceLightOrdinal = 0; + for ( const viewLight_t *viewLight = viewDef->viewLights; viewLight != NULL; + viewLight = viewLight->next, ++sourceLightOrdinal ) { + if ( !ViewLightHasInteractions( viewLight ) ) { + continue; + } + if ( domain.lightCount >= CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_LIGHT_POOL_OVERFLOW, + domain.lightCount, view.interactionPassPacketIndex, -1, + sourceLightOrdinal ); + return FailView( view, arenaCheckpoint, error ); + } + if ( !ValidateLight( *viewLight, sourceLightOrdinal, error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + const int absoluteLightIndex = domain.lightCount++; + classicInteractionDomainLight_t &light = + domain.lights[ absoluteLightIndex ]; + if ( !PopulateLightRecord( light, *viewLight, sourceLightOrdinal, + error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + + const bool stencilShadowing = r_shadows.GetBool() + && viewLight->lightShader->LightCastsShadows() + && !viewLight->lightShader->IsAmbientLight() + && ( viewLight->globalShadows != NULL + || viewLight->localShadows != NULL ); + if ( stencilShadowing ) { + if ( stencilShadowPass == NULL ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + 0, -1, -1, sourceLightOrdinal ); + return FailView( view, arenaCheckpoint, error ); + } + if ( !PrepareStencilShadowChain( packetFrame, view, light, + viewLight->globalShadows, + SCENE_SHADOW_CASTER_STENCIL_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL, + RENDER_PASS_STENCIL_SHADOW, + view.stencilShadowPassPacketIndex, + shadowPacketCursor, shadowPacketEnd, + arenaCheckpoint.lightCount, error ) + || !PrepareStencilShadowChain( packetFrame, view, light, + viewLight->localShadows, + SCENE_SHADOW_CASTER_STENCIL_LOCAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL, + RENDER_PASS_STENCIL_SHADOW, + view.stencilShadowPassPacketIndex, + shadowPacketCursor, shadowPacketEnd, + arenaCheckpoint.lightCount, error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + light.clearStencil = true; + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] = + viewLight->globalShadows != NULL + ? CLASSIC_INTERACTION_SHADOW_STENCIL + : CLASSIC_INTERACTION_SHADOW_NONE; + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = + CLASSIC_INTERACTION_SHADOW_STENCIL; + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ] = + view.stencilTranslucentShadows + ? CLASSIC_INTERACTION_SHADOW_STENCIL + : CLASSIC_INTERACTION_SHADOW_NONE; + view.shadowMode = CLASSIC_INTERACTION_SHADOW_STENCIL; + } + + const bool mappedShadowing = shadowMapPass != NULL + && r_useShadowMap.GetBool() + && r_shadows.GetBool() + && viewLight->lightShader->LightCastsShadows() + && !viewLight->lightShader->IsAmbientLight() + && ViewLightHasMappedShadowState( *viewLight ); + if ( mappedShadowing ) { + if ( !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->globalShadowMapCasters, + SCENE_SHADOW_CASTER_MAP_GLOBAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->localShadowMapCasters, + SCENE_SHADOW_CASTER_MAP_LOCAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->globalShadowMapDynamicCasters, + SCENE_SHADOW_CASTER_MAP_GLOBAL_DYNAMIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->localShadowMapDynamicCasters, + SCENE_SHADOW_CASTER_MAP_LOCAL_DYNAMIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->globalTranslucentShadowMapCasters, + SCENE_SHADOW_CASTER_MAP_GLOBAL_TRANSLUCENT, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareMappedShadowChain( packetFrame, view, light, + viewLight->localTranslucentShadowMapCasters, + SCENE_SHADOW_CASTER_MAP_LOCAL_TRANSLUCENT, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareStencilShadowChain( packetFrame, view, light, + viewLight->globalShadowMapStencilSupplements, + SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL, + RENDER_PASS_SHADOW_MAP, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) + || !PrepareStencilShadowChain( packetFrame, view, light, + viewLight->localShadowMapStencilSupplements, + SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL, + RENDER_PASS_SHADOW_MAP, + view.shadowMapPassPacketIndex, mapPacketCursor, + mapPacketEnd, arenaCheckpoint.lightCount, error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + } + if ( !BuildShadowMapPasses( view, light, error ) ) { + return FailView( view, arenaCheckpoint, error ); + } + RecomputePlannedStencilWork( light ); + if ( light.shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] >= 0 + || light.shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] >= 0 ) { + const bool anyHybrid = + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] + == CLASSIC_INTERACTION_SHADOW_HYBRID + || light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] + == CLASSIC_INTERACTION_SHADOW_HYBRID; + view.shadowMode = anyHybrid + ? CLASSIC_INTERACTION_SHADOW_HYBRID + : ( light.pointLight + ? CLASSIC_INTERACTION_SHADOW_POINT + : CLASSIC_INTERACTION_SHADOW_PROJECTED ); + } + + const drawSurf_t *chains[ CLASSIC_INTERACTION_RECEIVER_COUNT ] = { + viewLight->localInteractions, + viewLight->globalInteractions, + r_skipTranslucent.GetBool() ? NULL + : viewLight->translucentInteractions + }; + const sceneInteractionReceiverClass_t packetReceivers[ + CLASSIC_INTERACTION_RECEIVER_COUNT ] = { + SCENE_INTERACTION_RECEIVER_LOCAL, + SCENE_INTERACTION_RECEIVER_GLOBAL, + SCENE_INTERACTION_RECEIVER_TRANSLUCENT + }; + for ( int receiverIndex = 0; + receiverIndex < CLASSIC_INTERACTION_RECEIVER_COUNT; + ++receiverIndex ) { + int receiverOrdinal = 0; + for ( const drawSurf_t *drawSurf = chains[ receiverIndex ]; + drawSurf != NULL; + drawSurf = drawSurf->nextOnLight, ++receiverOrdinal ) { + if ( !SurfacePacketEligible( drawSurf ) ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_PACKET, + receiverIndex, view.interactionPassPacketIndex, + packetCursor, sourceLightOrdinal, receiverOrdinal ); + return FailView( view, arenaCheckpoint, error ); + } + if ( packetCursor < interactionPass->firstDrawPacket + || packetCursor >= packetEnd ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + expectedSourceOrdinal, + view.interactionPassPacketIndex, packetCursor, + sourceLightOrdinal, receiverOrdinal ); + return FailView( view, arenaCheckpoint, error ); + } + const drawPacket_t &packet = + packetFrame.DrawPacket( packetCursor ); + if ( !ValidateDrawPacket( packetFrame, packet, packetCursor, + viewDef, viewLight, sourceLightOrdinal, + packetReceivers[ receiverIndex ], receiverOrdinal, + expectedSourceOrdinal, drawSurf, error ) ) { + error.passPacketIndex = view.interactionPassPacketIndex; + return FailView( view, arenaCheckpoint, error ); + } + if ( domain.surfaceCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SURFACE_POOL_OVERFLOW, + domain.surfaceCount, + view.interactionPassPacketIndex, packetCursor, + sourceLightOrdinal, receiverOrdinal ); + return FailView( view, arenaCheckpoint, error ); + } + const int absoluteSurfaceIndex = domain.surfaceCount++; + classicInteractionDomainSurface_t &surface = + domain.surfaces[ absoluteSurfaceIndex ]; + InitSurface( surface ); + surface.legacyDrawSurf = drawSurf; + surface.legacyViewLight = viewLight; + surface.drawPacketIndex = packetCursor; + surface.lightIndex = absoluteLightIndex; + surface.sourceOrdinal = expectedSourceOrdinal; + surface.receiverOrdinal = receiverOrdinal; + surface.receiver = static_cast< + classicInteractionDomainReceiver_t>( receiverIndex ); + surface.materialTableRecordIndex = packet.materialRecordIndex; + surface.vertexCount = packet.vertexCount; + surface.firstIndex = packet.firstIndex; + surface.indexCount = packet.indexCount; + surface.vertexOffset = packet.vertexOffset; + surface.scissorX1 = packet.scissorX1; + surface.scissorY1 = packet.scissorY1; + surface.scissorX2 = packet.scissorX2; + surface.scissorY2 = packet.scissorY2; + if ( MaterialRequestsClassicDeform( drawSurf ) ) { + surface.deformRole = packet.classicDeformRecord->role; + surface.deformOutcome = packet.classicDeformRecord->outcome; + surface.deformContractHash = + packet.classicDeformRecord->semanticHash; + } + const materialResourceTableRecord_t *tableRecord = NULL; + if ( !ValidateSurfaceMaterial( packet, *drawSurf, + sourceLightOrdinal, receiverOrdinal, view, tableRecord, + error ) ) { + error.passPacketIndex = view.interactionPassPacketIndex; + error.drawPacketIndex = packetCursor; + return FailView( view, arenaCheckpoint, error ); + } + surface.materialId = tableRecord->materialId; + surface.tableGeneration = tableRecord->tableGeneration; + if ( !PrepareSurfacePrimitives( view, light, surface, packet, + arenaCheckpoint.lightCount, + arenaCheckpoint.surfaceCount, error ) ) { + error.passPacketIndex = view.interactionPassPacketIndex; + error.drawPacketIndex = packetCursor; + error.lightOrdinal = sourceLightOrdinal; + error.receiverOrdinal = receiverOrdinal; + return FailView( view, arenaCheckpoint, error ); + } + surface.hash = HashSurface( surface, + arenaCheckpoint.lightCount ); + light.surfaceCount++; + light.primitiveCount += surface.primitiveCount; + light.drawablePrimitiveCount + += surface.drawablePrimitiveCount; + light.noopPrimitiveCount += surface.noopPrimitiveCount; + light.receiverSurfaceCount[ receiverIndex ]++; + light.receiverPrimitiveCount[ receiverIndex ] + += surface.primitiveCount; + packetCursor++; + expectedSourceOrdinal++; + } + } + light.hash = HashLight( light ); + } + if ( packetCursor != packetEnd ) { + SetError( error, CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor, view.interactionPassPacketIndex, + packetCursor ); + return FailView( view, arenaCheckpoint, error ); + } + if ( stencilShadowPass != NULL + && shadowPacketCursor != shadowPacketEnd ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + shadowPacketEnd - shadowPacketCursor, + view.stencilShadowPassPacketIndex, shadowPacketCursor ); + return FailView( view, arenaCheckpoint, error ); + } + if ( shadowMapPass != NULL && mapPacketCursor != mapPacketEnd ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + mapPacketEnd - mapPacketCursor, + view.shadowMapPassPacketIndex, mapPacketCursor ); + return FailView( view, arenaCheckpoint, error ); + } + + // Publish only after the full light/receiver/stage walk reconciles exactly. + // Until ready becomes true, accessors reject every staged arena range. + view.firstLight = arenaCheckpoint.lightCount; + view.lightCount = domain.lightCount - arenaCheckpoint.lightCount; + view.firstSurface = arenaCheckpoint.surfaceCount; + view.surfaceCount = domain.surfaceCount - arenaCheckpoint.surfaceCount; + view.firstPrimitive = arenaCheckpoint.primitiveCount; + view.primitiveCount = domain.primitiveCount - arenaCheckpoint.primitiveCount; + view.firstShadowCaster = arenaCheckpoint.shadowCasterCount; + view.shadowCasterCount = domain.shadowCasterCount + - arenaCheckpoint.shadowCasterCount; + view.firstShadowMapPass = arenaCheckpoint.shadowMapPassCount; + view.shadowMapPassCount = domain.shadowMapPassCount + - arenaCheckpoint.shadowMapPassCount; + for ( int i = 0; i < view.lightCount; ++i ) { + const classicInteractionDomainLight_t &light = + domain.lights[ view.firstLight + i ]; + view.drawablePrimitiveCount += light.drawablePrimitiveCount; + view.noopPrimitiveCount += light.noopPrimitiveCount; + view.drawableShadowCasterCount += light.drawableShadowCasters; + view.noopShadowCasterCount += light.noopShadowCasters; + view.logicalVolumeDrawCount += light.logicalVolumeDraws; + view.preloadVolumeDrawCount += light.preloadVolumeDraws; + if ( light.shadowCasterTotal > 0 ) { + view.shadowLightCount++; + } + bool mappedLight = false; + for ( int receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiver <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; ++receiver ) { + const int passIndex = light.shadowMapPassIndex[ receiver ]; + if ( passIndex < 0 ) { + continue; + } + mappedLight = true; + if ( domain.shadowMapPasses[ passIndex ].disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID ) { + view.hybridShadowPassCount++; + } + const classicInteractionDomainShadowMapPass_t &mapPass = + domain.shadowMapPasses[ passIndex ]; + if ( mapPass.lightClass == SHADOWMAP_LIGHT_POINT ) { + view.pointShadowMapPassCount++; + } else { + view.projectedShadowMapPassCount++; + if ( mapPass.projected.state.cascadeCount > 1 ) { + view.csmShadowMapPassCount++; + } + } + } + if ( mappedLight ) { + if ( light.pointLight ) { + view.pointShadowLightCount++; + } else { + view.projectedShadowLightCount++; + } + } + view.activeLightStageCount += light.activeLightStageCount; + view.inactiveLightStageCount += light.inactiveLightStageCount; + for ( int receiver = 0; + receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; ++receiver ) { + view.receiverSurfaceCount[ receiver ] + += light.receiverSurfaceCount[ receiver ]; + view.receiverPrimitiveCount[ receiver ] + += light.receiverPrimitiveCount[ receiver ]; + } + } + for ( int i = 0; i < view.surfaceCount; ++i ) { + const classicInteractionDomainSurface_t &surface = + domain.surfaces[ view.firstSurface + i ]; + view.activeSurfaceStageCount += surface.activeSurfaceStageCount; + view.inactiveSurfaceStageCount += surface.inactiveSurfaceStageCount; + if ( surface.deformContractHash != 0 ) { + view.materialDeformReceiverCount++; + } + } + for ( int i = 0; i < view.shadowCasterCount; ++i ) { + const classicInteractionDomainShadowCaster_t &caster = + domain.shadowCasters[ view.firstShadowCaster + i ]; + if ( caster.deformContractHash == 0 ) { + continue; + } + if ( caster.deformOutcome == CLASSIC_DEFORM_OUTCOME_COMPLETED ) { + view.completedDeformCasterCount++; + } else if ( caster.deformOutcome == CLASSIC_DEFORM_OUTCOME_EMPTY ) { + view.emptyDeformCasterCount++; + } + } + bool anyStencilMode = false; + bool anyProjectedMode = false; + bool anyPointMode = false; + bool anyHybridMode = false; + for ( int lightIndex = 0; lightIndex < view.lightCount; ++lightIndex ) { + const classicInteractionDomainLight_t &light = + domain.lights[ view.firstLight + lightIndex ]; + for ( int receiver = 0; + receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; ++receiver ) { + switch ( light.receiverShadowMode[ receiver ] ) { + case CLASSIC_INTERACTION_SHADOW_STENCIL: + anyStencilMode = true; + break; + case CLASSIC_INTERACTION_SHADOW_PROJECTED: + anyProjectedMode = true; + break; + case CLASSIC_INTERACTION_SHADOW_POINT: + anyPointMode = true; + break; + case CLASSIC_INTERACTION_SHADOW_HYBRID: + anyHybridMode = true; + break; + default: + break; + } + } + } + const int distinctShadowModes = ( anyStencilMode ? 1 : 0 ) + + ( anyProjectedMode ? 1 : 0 ) + ( anyPointMode ? 1 : 0 ) + + ( anyHybridMode ? 1 : 0 ); + if ( anyHybridMode || distinctShadowModes > 1 ) { + view.shadowMode = CLASSIC_INTERACTION_SHADOW_HYBRID; + } else if ( anyPointMode ) { + view.shadowMode = CLASSIC_INTERACTION_SHADOW_POINT; + } else if ( anyProjectedMode ) { + view.shadowMode = CLASSIC_INTERACTION_SHADOW_PROJECTED; + } else if ( anyStencilMode ) { + view.shadowMode = CLASSIC_INTERACTION_SHADOW_STENCIL; + } else { + view.shadowMode = CLASSIC_INTERACTION_SHADOW_NONE; + } + if ( view.drawablePrimitiveCount + view.noopPrimitiveCount + != view.primitiveCount + || view.drawableShadowCasterCount + + view.noopShadowCasterCount != view.shadowCasterCount ) { + SetError( error, + CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + view.primitiveCount ); + return FailView( view, arenaCheckpoint, error ); + } + view.failure = CLASSIC_INTERACTION_FAILURE_NONE; + view.hash = HashView( view ); + view.ready = true; + domain.stats.readyViews++; + domain.stats.lights += view.lightCount; + domain.stats.surfaces += view.surfaceCount; + domain.stats.primitives += view.primitiveCount; + domain.stats.drawablePrimitives += view.drawablePrimitiveCount; + domain.stats.noopPrimitives += view.noopPrimitiveCount; + domain.stats.shadowCasters += view.shadowCasterCount; + domain.stats.drawableShadowCasters += view.drawableShadowCasterCount; + domain.stats.noopShadowCasters += view.noopShadowCasterCount; + domain.stats.logicalVolumeDraws += view.logicalVolumeDrawCount; + domain.stats.preloadVolumeDraws += view.preloadVolumeDrawCount; + domain.stats.shadowLights += view.shadowLightCount; + domain.stats.shadowMapPasses += view.shadowMapPassCount; + domain.stats.hybridShadowPasses += view.hybridShadowPassCount; + domain.stats.projectedShadowMapPasses + += view.projectedShadowMapPassCount; + domain.stats.csmShadowMapPasses += view.csmShadowMapPassCount; + domain.stats.pointShadowMapPasses += view.pointShadowMapPassCount; + domain.stats.projectedShadowLights += view.projectedShadowLightCount; + domain.stats.pointShadowLights += view.pointShadowLightCount; + domain.stats.activeLightStages += view.activeLightStageCount; + domain.stats.inactiveLightStages += view.inactiveLightStageCount; + domain.stats.activeSurfaceStages += view.activeSurfaceStageCount; + domain.stats.inactiveSurfaceStages += view.inactiveSurfaceStageCount; + domain.stats.materialDeformReceivers += view.materialDeformReceiverCount; + domain.stats.completedDeformCasters += view.completedDeformCasterCount; + domain.stats.emptyDeformCasters += view.emptyDeformCasterCount; + for ( int receiver = 0; receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; + receiver++ ) { + domain.stats.receiverSurfaces[ receiver ] + += view.receiverSurfaceCount[ receiver ]; + domain.stats.receiverPrimitives[ receiver ] + += view.receiverPrimitiveCount[ receiver ]; + } + return true; +} + +static void RecordFallback( classicInteractionDomainView_t *view, + classicInteractionDomainBackend_t backend, + classicInteractionDomainFailure_t failure, int detail, + int drawnPrimitives, int noopPrimitives, + int submittedShadowCasters, int noopShadowCasters, + int logicalVolumeDraws, int preloadVolumeDraws, + int shadowMapPasses, int hybridPasses ) { + classicInteractionDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view == NULL ) { + coverage.untrackedFallbacks++; + return; + } + const int viewIndex = ViewIndex( view ); + if ( viewIndex < 0 || view->backendOutcome[ backend ] + != CLASSIC_INTERACTION_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + view->backendOutcome[ backend ] = CLASSIC_INTERACTION_BACKEND_FALLBACK; + view->backendFailure[ backend ] = failure == CLASSIC_INTERACTION_FAILURE_NONE + ? CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED : failure; + view->backendFailureDetail[ backend ] = detail; + view->backendDrawnPrimitives[ backend ] = drawnPrimitives; + view->backendNoopPrimitives[ backend ] = noopPrimitives; + view->backendShadowCasters[ backend ] = submittedShadowCasters; + view->backendNoopShadowCasters[ backend ] = noopShadowCasters; + view->backendLogicalVolumeDraws[ backend ] = logicalVolumeDraws; + view->backendPreloadVolumeDraws[ backend ] = preloadVolumeDraws; + view->backendShadowMapPasses[ backend ] = shadowMapPasses; + view->backendHybridPasses[ backend ] = hybridPasses; + coverage.fallbackViewMask |= 1ull << viewIndex; + coverage.fallbackViews++; + coverage.fallbackLights += view->lightCount; + coverage.fallbackSurfaces += view->surfaceCount; + coverage.fallbackDrawablePrimitives += view->drawablePrimitiveCount; + coverage.fallbackNoopPrimitives += view->noopPrimitiveCount; + coverage.fallbackShadowCasters += view->drawableShadowCasterCount; + coverage.fallbackNoopShadowCasters += view->noopShadowCasterCount; + coverage.fallbackLogicalVolumeDraws += view->logicalVolumeDrawCount; + coverage.fallbackPreloadVolumeDraws += view->preloadVolumeDrawCount; + coverage.fallbackShadowMapPasses += view->shadowMapPassCount; + coverage.fallbackHybridPasses += view->hybridShadowPassCount; + if ( failure == CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH ) { + coverage.coverageMismatches++; + } + if ( view->backendFailure[ backend ] >= CLASSIC_INTERACTION_FAILURE_NONE + && view->backendFailure[ backend ] + < CLASSIC_INTERACTION_FAILURE_COUNT ) { + domain.stats.failureCounts[ view->backendFailure[ backend ] ]++; + } +} + +} // namespace + +void R_ClassicInteractionDomain_ResetFrame( void ) { + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.viewCount = 0; + domain.lightCount = 0; + domain.surfaceCount = 0; + domain.primitiveCount = 0; + domain.shadowCasterCount = 0; + domain.shadowAlphaStageCount = 0; + domain.shadowMapPassCount = 0; + domain.textureCount = 0; + InvalidateTextureLookup(); + domain.generation++; + if ( domain.generation == 0 ) { + domain.generation = 1; + } + idStr::Copynz( domain.stats.status, "empty", + sizeof( domain.stats.status ) ); +} + +void R_ClassicInteractionDomain_PrepareFrame( + const idScenePacketFrame &packetFrame ) { + R_ClassicInteractionDomain_ResetFrame(); + domain.stats.prepared = true; + domain.stats.sourceScenes = packetFrame.NumScenes(); + domain.stats.overflow = packetFrame.Stats().overflow; + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); + ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !SceneIsInteractionCandidate( packetFrame, scene ) ) { + continue; + } + if ( FindMutableView( scene.viewDef ) != NULL ) { + continue; + } + domain.stats.interactionViews++; + if ( domain.viewCount >= CLASSIC_INTERACTION_DOMAIN_MAX_VIEWS ) { + domain.stats.overflow = true; + domain.stats.fallbackViews++; + domain.stats.failureCounts[ + CLASSIC_INTERACTION_FAILURE_VIEW_POOL_OVERFLOW ]++; + continue; + } + classicInteractionDomainView_t &view = + domain.views[ domain.viewCount++ ]; + InitView( view, scene.viewDef, sceneIndex ); + R_ClassicInteractionDomain_PrepareView( packetFrame, scene, view ); + } + + domain.stats.textures = domain.textureCount; + domain.stats.shadowAlphaStages = domain.shadowAlphaStageCount; + domain.stats.frameValid = !domain.stats.overflow + && domain.stats.fallbackViews == 0; + std::uint64_t frameHash = HASH_OFFSET; + HashInt( frameHash, domain.stats.sourceScenes ); + HashInt( frameHash, domain.stats.interactionViews ); + for ( int i = 0; i < domain.viewCount; ++i ) { + HashInt( frameHash, domain.views[ i ].scenePacketIndex ); + HashBool( frameHash, domain.views[ i ].ready ); + HashInt( frameHash, domain.views[ i ].failure ); + HashU64( frameHash, domain.views[ i ].hash ); + } + domain.stats.hash = frameHash; + if ( domain.stats.interactionViews == 0 ) { + idStr::Copynz( domain.stats.status, "empty", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.frameValid ) { + idStr::Copynz( domain.stats.status, "ready", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.readyViews == 0 ) { + idStr::Copynz( domain.stats.status, "fallback", + sizeof( domain.stats.status ) ); + } else { + idStr::Copynz( domain.stats.status, "mixed-view-fallback", + sizeof( domain.stats.status ) ); + } +} + +const classicInteractionDomainStats_t &R_ClassicInteractionDomain_Stats( + void ) { + return domain.stats; +} + +int R_ClassicInteractionDomain_NumViews( void ) { + return domain.viewCount; +} + +const classicInteractionDomainView_t *R_ClassicInteractionDomain_ViewByIndex( + int index ) { + return index >= 0 && index < domain.viewCount + ? &domain.views[ index ] : NULL; +} + +const classicInteractionDomainView_t *R_ClassicInteractionDomain_ViewForScenePacket( + int scenePacketIndex ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].scenePacketIndex == scenePacketIndex ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +const classicInteractionDomainView_t *R_ClassicInteractionDomain_FindView( + const viewDef_t *viewDef ) { + return FindMutableView( viewDef ); +} + +const classicInteractionDomainLight_t *R_ClassicInteractionDomain_ViewLight( + const classicInteractionDomainView_t &view, int lightIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || lightIndex < 0 + || lightIndex >= view.lightCount + || !RangeFits( view.firstLight, view.lightCount, + domain.lightCount ) ) { + return NULL; + } + return &domain.lights[ view.firstLight + lightIndex ]; +} + +const classicInteractionDomainSurface_t *R_ClassicInteractionDomain_ViewSurface( + const classicInteractionDomainView_t &view, int surfaceIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || surfaceIndex < 0 + || surfaceIndex >= view.surfaceCount + || !RangeFits( view.firstSurface, view.surfaceCount, + domain.surfaceCount ) ) { + return NULL; + } + return &domain.surfaces[ view.firstSurface + surfaceIndex ]; +} + +const classicInteractionDomainPrimitive_t *R_ClassicInteractionDomain_ViewPrimitive( + const classicInteractionDomainView_t &view, int primitiveIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || primitiveIndex < 0 + || primitiveIndex >= view.primitiveCount + || !RangeFits( view.firstPrimitive, view.primitiveCount, + domain.primitiveCount ) ) { + return NULL; + } + return &domain.primitives[ view.firstPrimitive + primitiveIndex ]; +} + +const classicInteractionDomainShadowCaster_t *R_ClassicInteractionDomain_ViewShadowCaster( + const classicInteractionDomainView_t &view, int casterIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || casterIndex < 0 + || casterIndex >= view.shadowCasterCount + || !RangeFits( view.firstShadowCaster, view.shadowCasterCount, + domain.shadowCasterCount ) ) { + return NULL; + } + return &domain.shadowCasters[ view.firstShadowCaster + casterIndex ]; +} + +const classicInteractionDomainShadowCaster_t *R_ClassicInteractionDomain_LightShadowCaster( + const classicInteractionDomainLight_t &light, + classicInteractionDomainShadowChain_t chain, int casterIndex ) { + if ( LightIndex( &light ) < 0 + || chain < CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL + || chain >= CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT + || casterIndex < 0 || casterIndex >= light.shadowCasterCount[ chain ] + || !RangeFits( light.firstShadowCaster[ chain ], + light.shadowCasterCount[ chain ], domain.shadowCasterCount ) ) { + return NULL; + } + return &domain.shadowCasters[ + light.firstShadowCaster[ chain ] + casterIndex ]; +} + +bool R_ClassicInteractionDomain_ShadowCasterNoopValid( + const classicInteractionDomainShadowCaster_t &caster ) { + if ( caster.disposition != CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY + || caster.selectedIndexCount != 0 || caster.alphaStageCount != 0 + || caster.firstAlphaStage != -1 + || ( caster.materialCoverage != MC_OPAQUE + && caster.materialCoverage != MC_PERFORATED ) ) { + return false; + } + const bool sealedEmptyDeform = caster.deformContractHash != 0 + && caster.deformRole == CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + && caster.deformOutcome == CLASSIC_DEFORM_OUTCOME_EMPTY + && caster.vertexCount == 0 && caster.totalIndexCount == 0; + const bool inactivePerforated = caster.deformContractHash == 0 + && caster.materialCoverage == MC_PERFORATED + && caster.vertexCount > 0 && caster.totalIndexCount > 0; + return sealedEmptyDeform || inactivePerforated; +} + +const classicInteractionDomainShadowAlphaStage_t *R_ClassicInteractionDomain_ShadowAlphaStage( + const classicInteractionDomainShadowCaster_t &caster, int stageIndex ) { + const int casterIndex = ShadowCasterIndex( &caster ); + if ( casterIndex < 0 || casterIndex >= domain.shadowCasterCount + || stageIndex < 0 || stageIndex >= caster.alphaStageCount + || !RangeFits( caster.firstAlphaStage, caster.alphaStageCount, + domain.shadowAlphaStageCount ) ) { + return NULL; + } + return &domain.shadowAlphaStages[ caster.firstAlphaStage + stageIndex ]; +} + +const classicInteractionDomainShadowMapPass_t *R_ClassicInteractionDomain_LightShadowMapPass( + const classicInteractionDomainLight_t &light, + classicInteractionDomainReceiver_t receiver ) { + if ( LightIndex( &light ) < 0 + || receiver < CLASSIC_INTERACTION_RECEIVER_LOCAL + || receiver > CLASSIC_INTERACTION_RECEIVER_GLOBAL ) { + return NULL; + } + const int passIndex = light.shadowMapPassIndex[ receiver ]; + if ( passIndex < 0 || passIndex >= domain.shadowMapPassCount ) { + return NULL; + } + const classicInteractionDomainShadowMapPass_t &pass = + domain.shadowMapPasses[ passIndex ]; + return pass.lightIndex == LightIndex( &light ) + && pass.receiver == receiver ? &pass : NULL; +} + +const classicInteractionDomainSurface_t *R_ClassicInteractionDomain_LightSurface( + const classicInteractionDomainLight_t &light, int surfaceIndex ) { + if ( LightIndex( &light ) < 0 || surfaceIndex < 0 + || surfaceIndex >= light.surfaceCount + || !RangeFits( light.firstSurface, light.surfaceCount, + domain.surfaceCount ) ) { + return NULL; + } + return &domain.surfaces[ light.firstSurface + surfaceIndex ]; +} + +const classicInteractionDomainPrimitive_t *R_ClassicInteractionDomain_SurfacePrimitive( + const classicInteractionDomainSurface_t &surface, int primitiveIndex ) { + if ( SurfaceIndex( &surface ) < 0 || primitiveIndex < 0 + || primitiveIndex >= surface.primitiveCount + || !RangeFits( surface.firstPrimitive, surface.primitiveCount, + domain.primitiveCount ) ) { + return NULL; + } + return &domain.primitives[ surface.firstPrimitive + primitiveIndex ]; +} + +const classicInteractionDomainTexture_t *R_ClassicInteractionDomain_ResolveTexture( + std::uint64_t textureResourceId ) { + const int textureIndex = TextureIndexFromResourceId( textureResourceId ); + if ( textureIndex < 0 || textureIndex >= domain.textureCount ) { + return NULL; + } + const classicInteractionDomainTexture_t &texture = + domain.textures[ textureIndex ]; + if ( texture.textureResourceId != textureResourceId + || texture.image == NULL || !texture.loaded || texture.defaulted + || texture.mutableImage + || texture.image->GetStorageGeneration() + != texture.storageGeneration ) { + return NULL; + } + return &texture; +} + +bool R_ClassicInteractionDomain_RecordOwned( const viewDef_t *viewDef, + classicInteractionDomainBackend_t backend, int drawnPrimitives, + int noopPrimitives, int submittedShadowCasters, + int noopShadowCasters, int logicalVolumeDraws, + int preloadVolumeDraws, int shadowMapPasses, + int hybridPasses ) { + if ( backend < CLASSIC_INTERACTION_BACKEND_GL + || backend >= CLASSIC_INTERACTION_BACKEND_COUNT ) { + return false; + } + classicInteractionDomainView_t *view = FindMutableView( viewDef ); + if ( view == NULL ) { + return false; + } + classicInteractionDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] + != CLASSIC_INTERACTION_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return view->backendOutcome[ backend ] + == CLASSIC_INTERACTION_BACKEND_OWNED + && view->backendDrawnPrimitives[ backend ] == drawnPrimitives + && view->backendNoopPrimitives[ backend ] == noopPrimitives + && view->backendShadowCasters[ backend ] + == submittedShadowCasters + && view->backendNoopShadowCasters[ backend ] + == noopShadowCasters + && view->backendLogicalVolumeDraws[ backend ] + == logicalVolumeDraws + && view->backendPreloadVolumeDraws[ backend ] + == preloadVolumeDraws + && view->backendShadowMapPasses[ backend ] + == shadowMapPasses + && view->backendHybridPasses[ backend ] == hybridPasses; + } + if ( !view->ready ) { + RecordFallback( view, backend, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + view->failure, drawnPrimitives, noopPrimitives, + submittedShadowCasters, noopShadowCasters, + logicalVolumeDraws, preloadVolumeDraws, + shadowMapPasses, hybridPasses ); + return false; + } + if ( drawnPrimitives != view->drawablePrimitiveCount + || noopPrimitives != view->noopPrimitiveCount + || submittedShadowCasters != view->drawableShadowCasterCount + || noopShadowCasters != view->noopShadowCasterCount + || logicalVolumeDraws != view->logicalVolumeDrawCount + || preloadVolumeDraws != view->preloadVolumeDrawCount + || shadowMapPasses != view->shadowMapPassCount + || hybridPasses != view->hybridShadowPassCount ) { + RecordFallback( view, backend, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + drawnPrimitives + noopPrimitives, drawnPrimitives, + noopPrimitives, submittedShadowCasters, noopShadowCasters, + logicalVolumeDraws, preloadVolumeDraws, + shadowMapPasses, hybridPasses ); + return false; + } + const int viewIndex = ViewIndex( view ); + view->backendOutcome[ backend ] = CLASSIC_INTERACTION_BACKEND_OWNED; + view->backendDrawnPrimitives[ backend ] = drawnPrimitives; + view->backendNoopPrimitives[ backend ] = noopPrimitives; + view->backendShadowCasters[ backend ] = submittedShadowCasters; + view->backendNoopShadowCasters[ backend ] = noopShadowCasters; + view->backendLogicalVolumeDraws[ backend ] = logicalVolumeDraws; + view->backendPreloadVolumeDraws[ backend ] = preloadVolumeDraws; + view->backendShadowMapPasses[ backend ] = shadowMapPasses; + view->backendHybridPasses[ backend ] = hybridPasses; + coverage.ownedViewMask |= 1ull << viewIndex; + coverage.ownedViews++; + coverage.ownedLights += view->lightCount; + coverage.ownedSurfaces += view->surfaceCount; + coverage.ownedDrawablePrimitives += drawnPrimitives; + coverage.ownedNoopPrimitives += noopPrimitives; + coverage.ownedShadowCasters += submittedShadowCasters; + coverage.ownedNoopShadowCasters += noopShadowCasters; + coverage.ownedLogicalVolumeDraws += logicalVolumeDraws; + coverage.ownedPreloadVolumeDraws += preloadVolumeDraws; + coverage.ownedShadowMapPasses += shadowMapPasses; + coverage.ownedHybridPasses += hybridPasses; + return true; +} + +void R_ClassicInteractionDomain_RecordBackendFallback( + const viewDef_t *viewDef, classicInteractionDomainBackend_t backend, + classicInteractionDomainFailure_t failure, int detail ) { + if ( backend < CLASSIC_INTERACTION_BACKEND_GL + || backend >= CLASSIC_INTERACTION_BACKEND_COUNT ) { + return; + } + RecordFallback( FindMutableView( viewDef ), backend, failure, detail, + 0, 0, 0, 0, 0, 0, 0, 0 ); +} + +const classicInteractionDomainBackendCoverage_t &R_ClassicInteractionDomain_BackendCoverage( + classicInteractionDomainBackend_t backend ) { + static const classicInteractionDomainBackendCoverage_t empty = {}; + return backend >= CLASSIC_INTERACTION_BACKEND_GL + && backend < CLASSIC_INTERACTION_BACKEND_COUNT + ? domain.stats.backend[ backend ] : empty; +} + +const char *ClassicInteractionDomainReceiver_Name( + classicInteractionDomainReceiver_t receiver ) { + switch ( receiver ) { + case CLASSIC_INTERACTION_RECEIVER_LOCAL: return "local"; + case CLASSIC_INTERACTION_RECEIVER_GLOBAL: return "global"; + case CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT: return "translucent"; + case CLASSIC_INTERACTION_RECEIVER_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainPrimitiveDisposition_Name( + classicInteractionDomainPrimitiveDisposition_t disposition ) { + switch ( disposition ) { + case CLASSIC_INTERACTION_PRIMITIVE_DRAW: return "draw"; + case CLASSIC_INTERACTION_PRIMITIVE_NOOP_INACTIVE_LIGHT: + return "noopInactiveLight"; + case CLASSIC_INTERACTION_PRIMITIVE_NOOP_INACTIVE_SURFACE: + return "noopInactiveSurface"; + case CLASSIC_INTERACTION_PRIMITIVE_NOOP_MISSING_BUMP: + return "noopMissingBump"; + case CLASSIC_INTERACTION_PRIMITIVE_NOOP_BLACK: return "noopBlack"; + case CLASSIC_INTERACTION_PRIMITIVE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainDepth_Name( + classicInteractionDomainDepth_t depth ) { + switch ( depth ) { + case CLASSIC_INTERACTION_DEPTH_EQUAL: return "equal"; + case CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL: return "lessOrEqual"; + case CLASSIC_INTERACTION_DEPTH_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainShadowMode_Name( + classicInteractionDomainShadowMode_t mode ) { + switch ( mode ) { + case CLASSIC_INTERACTION_SHADOW_NONE: return "none"; + case CLASSIC_INTERACTION_SHADOW_STENCIL: return "stencil"; + case CLASSIC_INTERACTION_SHADOW_PROJECTED: return "projected"; + case CLASSIC_INTERACTION_SHADOW_POINT: return "point"; + case CLASSIC_INTERACTION_SHADOW_HYBRID: return "hybrid"; + case CLASSIC_INTERACTION_SHADOW_MODE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainShadowChain_Name( + classicInteractionDomainShadowChain_t chain ) { + switch ( chain ) { + case CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL: + return "stencilGlobal"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL: + return "stencilLocal"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC: + return "mapGlobalStatic"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC: + return "mapLocalStatic"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC: + return "mapGlobalDynamic"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC: + return "mapLocalDynamic"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT: + return "mapGlobalTranslucent"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT: + return "mapLocalTranslucent"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL: + return "supplementGlobal"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL: + return "supplementLocal"; + case CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainFailure_Name( + classicInteractionDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_INTERACTION_FAILURE_NONE: return "none"; + case CLASSIC_INTERACTION_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_INTERACTION_FAILURE_SCENE_PACKET_OVERFLOW: + return "scenePacketOverflow"; + case CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_NOT_PREPARED: + return "materialTableNotPrepared"; + case CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_OVERFLOW: + return "materialTableOverflow"; + case CLASSIC_INTERACTION_FAILURE_VIEW_POOL_OVERFLOW: + return "viewPoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_LIGHT_POOL_OVERFLOW: + return "lightPoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_SURFACE_POOL_OVERFLOW: + return "surfacePoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_PRIMITIVE_POOL_OVERFLOW: + return "primitivePoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW: + return "shadowCasterPoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_SHADOW_ALPHA_STAGE_POOL_OVERFLOW: + return "shadowAlphaStagePoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_TEXTURE_POOL_OVERFLOW: + return "texturePoolOverflow"; + case CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_VIEW: + return "unsupportedView"; + case CLASSIC_INTERACTION_FAILURE_INVALID_SCENE_RANGE: + return "invalidSceneRange"; + case CLASSIC_INTERACTION_FAILURE_INVALID_INTERACTION_PASS: + return "invalidInteractionPass"; + case CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_RANGE: + return "invalidDrawRange"; + case CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH: + return "sourcePacketMismatch"; + case CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_PACKET: + return "invalidDrawPacket"; + case CLASSIC_INTERACTION_FAILURE_MISSING_GEOMETRY_RECORD: + return "missingGeometryRecord"; + case CLASSIC_INTERACTION_FAILURE_MISSING_INSTANCE_RECORD: + return "missingInstanceRecord"; + case CLASSIC_INTERACTION_FAILURE_MISSING_MATERIAL_RECORD: + return "missingMaterialRecord"; + case CLASSIC_INTERACTION_FAILURE_STALE_MATERIAL_RECORD: + return "staleMaterialRecord"; + case CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS: + return "missingShaderRegisters"; + case CLASSIC_INTERACTION_FAILURE_REGISTER_OUT_OF_RANGE: + return "registerOutOfRange"; + case CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE: + return "nonfiniteValue"; + case CLASSIC_INTERACTION_FAILURE_SHADOWS: return "shadows"; + case CLASSIC_INTERACTION_FAILURE_SHADOW_MAP: return "shadowMap"; + case CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH: + return "shadowPacketMismatch"; + case CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY: + return "shadowGeometry"; + case CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING: + return "customLighting"; + case CLASSIC_INTERACTION_FAILURE_DEFORM: return "deform"; + case CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT: + return "deformContract"; + case CLASSIC_INTERACTION_FAILURE_SKINNING: return "skinning"; + case CLASSIC_INTERACTION_FAILURE_SPECIAL_SURFACE: + return "specialSurface"; + case CLASSIC_INTERACTION_FAILURE_DEPTH_HACK: return "depthHack"; + case CLASSIC_INTERACTION_FAILURE_NEGATIVE_SCALE: + return "negativeScale"; + case CLASSIC_INTERACTION_FAILURE_FLAT_DIFFUSE: return "flatDiffuse"; + case CLASSIC_INTERACTION_FAILURE_ENHANCED_MATERIAL: + return "enhancedMaterial"; + case CLASSIC_INTERACTION_FAILURE_CEL_SHADING: return "celShading"; + case CLASSIC_INTERACTION_FAILURE_ALTERNATE_INTERACTION_PROGRAM: + return "alternateInteractionProgram"; + case CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL: + return "unsupportedMaterial"; + case CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE: + return "unsupportedState"; + case CLASSIC_INTERACTION_FAILURE_DYNAMIC_RESOURCE: + return "dynamicResource"; + case CLASSIC_INTERACTION_FAILURE_MISSING_RESOURCE: + return "missingResource"; + case CLASSIC_INTERACTION_FAILURE_DEFAULTED_RESOURCE: + return "defaultedResource"; + case CLASSIC_INTERACTION_FAILURE_UNLOADED_RESOURCE: + return "unloadedResource"; + case CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY: + return "backendNotReady"; + case CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH: + return "backendCoverageMismatch"; + case CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED: + return "backendRejected"; + case CLASSIC_INTERACTION_FAILURE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicInteractionDomainBackend_Name( + classicInteractionDomainBackend_t backend ) { + switch ( backend ) { + case CLASSIC_INTERACTION_BACKEND_GL: return "GL"; + case CLASSIC_INTERACTION_BACKEND_VULKAN: return "Vulkan"; + case CLASSIC_INTERACTION_BACKEND_COUNT: + default: return "unknown"; + } +} + +bool RendererClassicInteractionDomain_RunSelfTest( void ) { + classicInteractionDomainShadowCaster_t noopCaster; + InitShadowCaster( noopCaster ); + noopCaster.disposition = CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY; + noopCaster.materialCoverage = MC_OPAQUE; + noopCaster.deformRole = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + noopCaster.deformOutcome = CLASSIC_DEFORM_OUTCOME_EMPTY; + noopCaster.deformContractHash = 0x7a1234ull; + if ( !R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster ) ) { + return false; + } + noopCaster.deformRole = CLASSIC_DEFORM_ROLE_UNKNOWN; + if ( R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster ) ) { + return false; + } + noopCaster.deformRole = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + noopCaster.deformOutcome = CLASSIC_DEFORM_OUTCOME_COMPLETED; + if ( R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster ) ) { + return false; + } + noopCaster.deformOutcome = CLASSIC_DEFORM_OUTCOME_EMPTY; + noopCaster.deformContractHash = 0; + if ( R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster ) ) { + return false; + } + noopCaster.materialCoverage = MC_PERFORATED; + noopCaster.vertexCount = 4; + noopCaster.totalIndexCount = 6; + if ( !R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster ) ) { + return false; + } + + if ( CLASSIC_INTERACTION_DOMAIN_MAX_VIEWS != 64 + || CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS != 4096 + || CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES != 4096 + || CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES != 16384 + || CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES != 8192 + || CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS != 4096 + || CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES != 8192 + || CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES != 8192 + || !RangeFits( 16383, 1, 16384 ) + || RangeFits( 16384, 1, 16384 ) + || RangeFits( -1, 1, 16384 ) + || idStr::Cmp( ClassicInteractionDomainReceiver_Name( + CLASSIC_INTERACTION_RECEIVER_LOCAL ), "local" ) + || idStr::Cmp( ClassicInteractionDomainReceiver_Name( + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ), "translucent" ) + || idStr::Cmp( ClassicInteractionDomainDepth_Name( + CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL ), "lessOrEqual" ) + || idStr::Cmp( ClassicInteractionDomainShadowMode_Name( + CLASSIC_INTERACTION_SHADOW_STENCIL ), "stencil" ) + || idStr::Cmp( ClassicInteractionDomainShadowMode_Name( + CLASSIC_INTERACTION_SHADOW_HYBRID ), "hybrid" ) + || idStr::Cmp( ClassicInteractionDomainShadowChain_Name( + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ), + "supplementLocal" ) + || idStr::Cmp( ClassicInteractionDomainFailure_Name( + CLASSIC_INTERACTION_FAILURE_SHADOWS ), "shadows" ) + || idStr::Cmp( ClassicInteractionDomainFailure_Name( + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH ), + "shadowPacketMismatch" ) + || idStr::Cmp( ClassicInteractionDomainFailure_Name( + CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING ), + "customLighting" ) + || idStr::Cmp( ClassicInteractionDomainFailure_Name( + CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT ), + "deformContract" ) + || idStr::Cmp( ClassicInteractionDomainFailure_Name( + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH ), + "backendCoverageMismatch" ) ) { + return false; + } + + // Register evaluation preserves classic large-scroll wrapping and clamping. + shaderStage_t stage; + std::memset( &stage, 0, sizeof( stage ) ); + stage.texture.hasMatrix = true; + stage.texture.matrix[ 0 ][ 0 ] = 0; + stage.texture.matrix[ 0 ][ 1 ] = 1; + stage.texture.matrix[ 0 ][ 2 ] = 2; + stage.texture.matrix[ 1 ][ 0 ] = 3; + stage.texture.matrix[ 1 ][ 1 ] = 4; + stage.texture.matrix[ 1 ][ 2 ] = 5; + stage.color.registers[ 0 ] = 6; + stage.color.registers[ 1 ] = 7; + stage.color.registers[ 2 ] = 8; + stage.color.registers[ 3 ] = 9; + float registers[ 10 ] = { + 1.0f, 0.0f, 41.25f, 0.0f, 1.0f, -41.75f, + -1.0f, 0.25f, 2.0f, 1.0f + }; + classicInteractionBuildError_t evaluationError; + InitError( evaluationError ); + float matrix[ 2 ][ 4 ]; + float color[ 4 ]; + if ( !EvaluateTextureMatrix( stage.texture, registers, 10, matrix, + evaluationError, 4 ) + || matrix[ 0 ][ 3 ] != 0.25f || matrix[ 1 ][ 3 ] != -0.75f + || !EvaluateStageColor( stage, registers, 10, color, true, + evaluationError, 4 ) + || color[ 0 ] != 0.0f || color[ 1 ] != 0.25f + || color[ 2 ] != 1.0f || color[ 3 ] != 1.0f ) { + return false; + } + InitError( evaluationError ); + stage.conditionRegister = 10; + bool active = false; + if ( EvaluateCondition( stage, registers, 10, active, evaluationError, 4 ) + || evaluationError.failure + != CLASSIC_INTERACTION_FAILURE_REGISTER_OUT_OF_RANGE ) { + return false; + } + registers[ 0 ] = std::numeric_limits::quiet_NaN(); + InitError( evaluationError ); + stage.conditionRegister = 0; + if ( EvaluateCondition( stage, registers, 10, active, evaluationError, 4 ) + || evaluationError.failure + != CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE ) { + return false; + } + + // RB_DetermineLightScale membership is based solely on the presence of an + // interaction chain. Keep the shader deliberately unset here: a future + // fog/blend/material ownership filter must not leak into scale membership. + drawSurf_t scaleReceiver; + std::memset( &scaleReceiver, 0, sizeof( scaleReceiver ) ); + viewLight_t scaleLight; + std::memset( &scaleLight, 0, sizeof( scaleLight ) ); + scaleLight.localInteractions = &scaleReceiver; + if ( !ViewLightContributesToClassicScale( &scaleLight ) ) { + return false; + } + scaleLight.localInteractions = NULL; + scaleLight.globalInteractions = &scaleReceiver; + if ( !ViewLightContributesToClassicScale( &scaleLight ) ) { + return false; + } + scaleLight.globalInteractions = NULL; + scaleLight.translucentInteractions = &scaleReceiver; + if ( !ViewLightContributesToClassicScale( &scaleLight ) ) { + return false; + } + scaleLight.translucentInteractions = NULL; + if ( ViewLightContributesToClassicScale( &scaleLight ) ) { + return false; + } + // A bright fog/blend light follows this exact accumulator once its receiver + // chain admits it; ownership classification remains a separate decision. + float scaleMaximum = 1.0f; + if ( !AccumulateClassicLightScaleValue( 2.0f, 4.0f, scaleMaximum ) + || scaleMaximum != 8.0f + || !AccumulateClassicLightScaleValue( 2.0f, -100.0f, + scaleMaximum ) + || scaleMaximum != 8.0f ) { + return false; + } + + const classicInteractionDomainStats_t savedStats = domain.stats; + const std::uint32_t savedGeneration = domain.generation; + const int savedViewCount = domain.viewCount; + const int savedLightCount = domain.lightCount; + const int savedSurfaceCount = domain.surfaceCount; + const int savedPrimitiveCount = domain.primitiveCount; + const int savedShadowCasterCount = domain.shadowCasterCount; + const int savedShadowAlphaStageCount = domain.shadowAlphaStageCount; + const int savedShadowMapPassCount = domain.shadowMapPassCount; + const int savedTextureCount = domain.textureCount; + const classicInteractionDomainView_t savedView = domain.views[ 0 ]; + const classicInteractionDomainLight_t savedLight = domain.lights[ 0 ]; + const classicInteractionDomainSurface_t savedSurface = domain.surfaces[ 0 ]; + const classicInteractionDomainPrimitive_t savedPrimitive = + domain.primitives[ 0 ]; + const classicInteractionDomainTexture_t savedTexture = domain.textures[ 0 ]; + const classicInteractionDomainPrimitive_t savedCoveragePrimitive = + domain.primitives[ 1 ]; + const classicInteractionDomainLight_t savedShiftedLight = + domain.lights[ 3 ]; + const classicInteractionDomainSurface_t savedShiftedSurface = + domain.surfaces[ 5 ]; + const classicInteractionDomainPrimitive_t savedShiftedPrimitive = + domain.primitives[ 7 ]; + classicInteractionDomainShadowCaster_t savedShadowCasters[ 4 ]; + for ( int i = 0; i < 4; ++i ) { + savedShadowCasters[ i ] = domain.shadowCasters[ i ]; + } + classicInteractionDomainShadowMapPass_t savedMapPasses[ 4 ]; + for ( int i = 0; i < 4; ++i ) { + savedMapPasses[ i ] = domain.shadowMapPasses[ i ]; + } + classicInteractionDomainShadowAlphaStage_t alphaStageContract; + InitShadowAlphaStage( alphaStageContract ); + alphaStageContract.stageIndex = 3; + alphaStageContract.alphaTestMode = GL_GREATER; + alphaStageContract.alphaTestValue = 0.5f; + alphaStageContract.alphaScale = 1.0f; + alphaStageContract.hash = HashShadowAlphaStage( alphaStageContract ); + if ( alphaStageContract.hash == 0 ) { + return false; + } + classicInteractionDomainShadowCaster_t deformHashCaster; + InitShadowCaster( deformHashCaster ); + const std::uint64_t undeformedCasterHash = + HashShadowCaster( deformHashCaster, 0 ); + deformHashCaster.deformRole = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + deformHashCaster.deformOutcome = CLASSIC_DEFORM_OUTCOME_EMPTY; + deformHashCaster.deformContractHash = 0x81234ull; + if ( HashShadowCaster( deformHashCaster, 0 ) + == undeformedCasterHash ) { + return false; + } + + // Stable hashes exclude generation-scoped resource-id high bits. + domain.generation = 7; + domain.textureCount = 1; + std::memset( &domain.textures[ 0 ], 0, sizeof( domain.textures[ 0 ] ) ); + domain.textures[ 0 ].textureResourceId = MakeTextureResourceId( 0 ); + domain.textures[ 0 ].nameHash = HashString( "selftest/interaction" ); + classicInteractionDomainPrimitive_t first; + InitPrimitive( first ); + first.lightImageResourceId = domain.textures[ 0 ].textureResourceId; + first.diffuseColor[ 0 ] = 1.0f; + const std::uint64_t firstHash = HashPrimitive( first, 0, 0 ); + classicInteractionDomainPrimitive_t same = first; + if ( firstHash == 0 || HashPrimitive( same, 0, 0 ) != firstHash ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.lights[ 0 ] = savedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + same.diffuseColor[ 0 ] = 0.5f; + if ( HashPrimitive( same, 0, 0 ) == firstHash ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.lights[ 0 ] = savedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + domain.generation = 8; + domain.textures[ 0 ].textureResourceId = MakeTextureResourceId( 0 ); + first.lightImageResourceId = domain.textures[ 0 ].textureResourceId; + if ( HashPrimitive( first, 0, 0 ) != firstHash ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.lights[ 0 ] = savedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + + // Identical published content must hash identically even when unrelated + // earlier views move every arena checkpoint. Public indices intentionally + // remain arena-absolute; only their view-local identity enters the hashes. + domain.lightCount = 4; + domain.surfaceCount = 6; + domain.primitiveCount = 8; + classicInteractionDomainPrimitive_t originPrimitive; + InitPrimitive( originPrimitive ); + originPrimitive.lightIndex = 0; + originPrimitive.surfaceIndex = 0; + originPrimitive.lightStageIndex = 2; + originPrimitive.receiver = CLASSIC_INTERACTION_RECEIVER_GLOBAL; + originPrimitive.disposition = CLASSIC_INTERACTION_PRIMITIVE_DRAW; + originPrimitive.lightImageResourceId = + domain.textures[ 0 ].textureResourceId; + originPrimitive.diffuseColor[ 0 ] = 0.75f; + originPrimitive.hash = HashPrimitive( originPrimitive, 0, 0 ); + domain.primitives[ 0 ] = originPrimitive; + classicInteractionDomainPrimitive_t shiftedPrimitive = originPrimitive; + shiftedPrimitive.lightIndex = 3; + shiftedPrimitive.surfaceIndex = 5; + shiftedPrimitive.hash = HashPrimitive( shiftedPrimitive, 3, 5 ); + domain.primitives[ 7 ] = shiftedPrimitive; + + classicInteractionDomainSurface_t originSurface; + InitSurface( originSurface ); + originSurface.lightIndex = 0; + originSurface.sourceOrdinal = 11; + originSurface.receiverOrdinal = 2; + originSurface.receiver = CLASSIC_INTERACTION_RECEIVER_GLOBAL; + originSurface.materialId = 17; + originSurface.firstPrimitive = 0; + originSurface.primitiveCount = 1; + originSurface.drawablePrimitiveCount = 1; + originSurface.surfaceStageCount = 1; + originSurface.activeSurfaceStageCount = 1; + originSurface.hash = HashSurface( originSurface, 0 ); + classicInteractionDomainSurface_t deformSurface = originSurface; + deformSurface.deformRole = CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER; + deformSurface.deformOutcome = CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE; + deformSurface.deformContractHash = 0x71234ull; + if ( HashSurface( deformSurface, 0 ) == originSurface.hash ) { + return false; + } + domain.surfaces[ 0 ] = originSurface; + classicInteractionDomainSurface_t shiftedSurface = originSurface; + shiftedSurface.lightIndex = 3; + shiftedSurface.firstPrimitive = 7; + shiftedSurface.hash = HashSurface( shiftedSurface, 3 ); + domain.surfaces[ 5 ] = shiftedSurface; + + classicInteractionDomainLight_t originLight; + InitLight( originLight ); + originLight.sourceOrdinal = 4; + originLight.firstSurface = 0; + originLight.surfaceCount = 1; + originLight.firstPrimitive = 0; + originLight.primitiveCount = 1; + originLight.drawablePrimitiveCount = 1; + originLight.lightStageCount = 1; + originLight.activeLightStageCount = 1; + originLight.receiverSurfaceCount[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + originLight.receiverPrimitiveCount[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + originLight.hash = HashLight( originLight ); + domain.lights[ 0 ] = originLight; + classicInteractionDomainLight_t shiftedLight = originLight; + shiftedLight.firstSurface = 5; + shiftedLight.firstPrimitive = 7; + shiftedLight.hash = HashLight( shiftedLight ); + domain.lights[ 3 ] = shiftedLight; + + classicInteractionDomainView_t originView; + InitView( originView, NULL, -1 ); + originView.ready = true; + originView.firstLight = 0; + originView.lightCount = 1; + originView.firstSurface = 0; + originView.surfaceCount = 1; + originView.firstPrimitive = 0; + originView.primitiveCount = 1; + originView.drawablePrimitiveCount = 1; + originView.packetDrawCount = 1; + originView.activeLightStageCount = 1; + originView.activeSurfaceStageCount = 1; + originView.receiverSurfaceCount[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + originView.receiverPrimitiveCount[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + originView.maxLightValue = 8.0f; + originView.lightScale = 0.25f; + originView.overBright = 8.0f; + originView.hash = HashView( originView ); + classicInteractionDomainView_t shiftedView = originView; + shiftedView.firstLight = 3; + shiftedView.firstSurface = 5; + shiftedView.firstPrimitive = 7; + shiftedView.hash = HashView( shiftedView ); + const bool arenaHashInvariant = originPrimitive.hash == shiftedPrimitive.hash + && originSurface.hash == shiftedSurface.hash + && originLight.hash == shiftedLight.hash + && originView.hash == shiftedView.hash; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.lights[ 0 ] = savedLight; + domain.lights[ 3 ] = savedShiftedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.surfaces[ 5 ] = savedShiftedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.primitives[ 7 ] = savedShiftedPrimitive; + if ( !arenaHashInvariant ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.textures[ 0 ] = savedTexture; + return false; + } + + // Mixed receiver modes can require a deliberate volume resubmission after + // switching the stencil buffer between full volumes and hybrid supplements. + // The shared plan counts that exact LOCAL -> GLOBAL -> TRANSLUCENT sequence, + // while identical consecutive modes reuse the already prepared global half. + domain.shadowCasterCount = 4; + InitLight( domain.lights[ 0 ] ); + const classicInteractionDomainShadowChain_t planChains[ 4 ] = { + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + }; + for ( int i = 0; i < 4; ++i ) { + InitShadowCaster( domain.shadowCasters[ i ] ); + domain.shadowCasters[ i ].disposition = + CLASSIC_INTERACTION_SHADOW_CASTER_DRAW; + domain.shadowCasters[ i ].preload = ( i & 1 ) == 0; + domain.lights[ 0 ].firstShadowCaster[ planChains[ i ] ] = i; + domain.lights[ 0 ].shadowCasterCount[ planChains[ i ] ] = 1; + } + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] = + CLASSIC_INTERACTION_SHADOW_STENCIL; + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = + CLASSIC_INTERACTION_SHADOW_HYBRID; + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ] = + CLASSIC_INTERACTION_SHADOW_STENCIL; + RecomputePlannedStencilWork( domain.lights[ 0 ] ); + const bool mixedStencilPlan = domain.lights[ 0 ].logicalVolumeDraws == 5 + && domain.lights[ 0 ].preloadVolumeDraws == 3 + && domain.lights[ 0 ].clearStencil; + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] = + CLASSIC_INTERACTION_SHADOW_HYBRID; + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = + CLASSIC_INTERACTION_SHADOW_HYBRID; + domain.lights[ 0 ].receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ] = + CLASSIC_INTERACTION_SHADOW_HYBRID; + RecomputePlannedStencilWork( domain.lights[ 0 ] ); + const bool reusedHybridPlan = domain.lights[ 0 ].logicalVolumeDraws == 2 + && domain.lights[ 0 ].preloadVolumeDraws == 1; + for ( int i = 0; i < 4; ++i ) { + domain.shadowCasters[ i ].disposition = + CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY; + } + RecomputePlannedStencilWork( domain.lights[ 0 ] ); + const bool noopStencilClearPlan = + domain.lights[ 0 ].logicalVolumeDraws == 0 + && domain.lights[ 0 ].preloadVolumeDraws == 0 + && domain.lights[ 0 ].clearStencil; + domain.shadowCasterCount = savedShadowCasterCount; + domain.lights[ 0 ] = savedLight; + for ( int i = 0; i < 4; ++i ) { + domain.shadowCasters[ i ] = savedShadowCasters[ i ]; + } + if ( !mixedStencilPlan || !reusedHybridPlan || !noopStencilClearPlan ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.shadowAlphaStageCount = savedShadowAlphaStageCount; + domain.shadowMapPassCount = savedShadowMapPassCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + + // Mapped ownership records distinguish projected-single, CSM/parallel, + // point-cube and hybrid supplement semantics without backend handles. + domain.lightCount = 1; + domain.shadowMapPassCount = 4; + InitLight( domain.lights[ 0 ] ); + classicInteractionDomainShadowMapPass_t &projectedPass = + domain.shadowMapPasses[ 0 ]; + InitShadowMapPass( projectedPass ); + projectedPass.lightIndex = 0; + projectedPass.receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + projectedPass.resourceOwner = CLASSIC_INTERACTION_RECEIVER_LOCAL; + projectedPass.disposition = CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED; + projectedPass.mode = CLASSIC_INTERACTION_SHADOW_PROJECTED; + projectedPass.lightClass = SHADOWMAP_LIGHT_PROJECTED; + projectedPass.receiverMask = SHADOWMAP_RECEIVER_MASK_LOCAL; + projectedPass.mappedCasterCount = 1; + projectedPass.drawableMappedCasters = 1; + projectedPass.resourcePlanId = 0x0000000700000001ull; + projectedPass.resourceGeneration = 7; + projectedPass.mapComplete = true; + projectedPass.hybridComplete = true; + projectedPass.mapRequired = true; + projectedPass.projected.state.valid = true; + projectedPass.projected.state.cascadeCount = 1; + projectedPass.projected.state.atlasDiv = 1; + projectedPass.projected.state.tileSize = 1024; + projectedPass.hash = HashShadowMapPass( projectedPass ); + + classicInteractionDomainShadowMapPass_t &csmParallelPass = + domain.shadowMapPasses[ 1 ]; + csmParallelPass = projectedPass; + csmParallelPass.receiver = CLASSIC_INTERACTION_RECEIVER_GLOBAL; + csmParallelPass.resourceOwner = CLASSIC_INTERACTION_RECEIVER_LOCAL; + csmParallelPass.resourceAlias = true; + csmParallelPass.lightClass = SHADOWMAP_LIGHT_PARALLEL; + csmParallelPass.receiverMask = SHADOWMAP_RECEIVER_MASK_GLOBAL; + csmParallelPass.projected.state.cascadeCount = 4; + csmParallelPass.projected.state.atlasDiv = 2; + csmParallelPass.hash = HashShadowMapPass( csmParallelPass ); + + classicInteractionDomainShadowMapPass_t &pointCubePass = + domain.shadowMapPasses[ 2 ]; + InitShadowMapPass( pointCubePass ); + pointCubePass.lightIndex = 0; + pointCubePass.receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + pointCubePass.disposition = CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED; + pointCubePass.mode = CLASSIC_INTERACTION_SHADOW_POINT; + pointCubePass.lightClass = SHADOWMAP_LIGHT_POINT; + pointCubePass.point.valid = true; + pointCubePass.point.faceCount = 6; + pointCubePass.point.faceSize = 512; + pointCubePass.point.farDistance = 256.0f; + pointCubePass.mapComplete = true; + pointCubePass.hybridComplete = true; + pointCubePass.hash = HashShadowMapPass( pointCubePass ); + + classicInteractionDomainShadowMapPass_t &hybridPass = + domain.shadowMapPasses[ 3 ]; + hybridPass = pointCubePass; + hybridPass.receiver = CLASSIC_INTERACTION_RECEIVER_GLOBAL; + hybridPass.disposition = CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID; + hybridPass.mode = CLASSIC_INTERACTION_SHADOW_HYBRID; + hybridPass.mapComplete = false; + hybridPass.supplementCasterCount = 1; + hybridPass.drawableSupplementCasters = 1; + hybridPass.hash = HashShadowMapPass( hybridPass ); + domain.lights[ 0 ].shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] = 0; + domain.lights[ 0 ].shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + classicInteractionDomainShadowMapPass_t changedPass = projectedPass; + changedPass.projected.filter.filterRadius = 2.0f; + classicInteractionDomainShadowMapPass_t identityChangedPass = + projectedPass; + identityChangedPass.resourcePlanId = 0x0000006300000011ull; + identityChangedPass.resourceGeneration = 99; + classicInteractionDomainShadowMapPass_t supplementChangedPass = + projectedPass; + supplementChangedPass.supplementCasterCount = 1; + supplementChangedPass.drawableSupplementCasters = 1; + classicInteractionDomainShadowMapPass_t dynamicChangedPass = projectedPass; + dynamicChangedPass.hasDynamicCasters = true; + classicInteractionDomainShadowMapPass_t alphaChangedPass = projectedPass; + alphaChangedPass.hasAlphaCasters = true; + classicInteractionDomainShadowMapPass_t completenessChangedPass = + projectedPass; + completenessChangedPass.mapComplete = false; + completenessChangedPass.hybridComplete = false; + classicInteractionDomainView_t mappedViewContract; + InitView( mappedViewContract, NULL, -1 ); + mappedViewContract.shadowMapPassCount = 4; + mappedViewContract.hybridShadowPassCount = 1; + mappedViewContract.projectedShadowMapPassCount = 2; + mappedViewContract.csmShadowMapPassCount = 1; + mappedViewContract.pointShadowMapPassCount = 2; + const std::uint64_t mappedViewHash = HashView( mappedViewContract ); + const bool mappedContractOk = projectedPass.hash != 0 + && csmParallelPass.hash != 0 && pointCubePass.hash != 0 + && hybridPass.hash != 0 + && projectedPass.hash == HashShadowMapPass( projectedPass ) + && projectedPass.hash != HashShadowMapPass( changedPass ) + && projectedPass.hash == HashShadowMapPass( identityChangedPass ) + && projectedPass.hash != HashShadowMapPass( supplementChangedPass ) + && projectedPass.hash != HashShadowMapPass( dynamicChangedPass ) + && projectedPass.hash != HashShadowMapPass( alphaChangedPass ) + && projectedPass.hash != HashShadowMapPass( + completenessChangedPass ) + && R_ClassicInteractionDomain_LightShadowMapPass( + domain.lights[ 0 ], CLASSIC_INTERACTION_RECEIVER_LOCAL ) + == &projectedPass + && R_ClassicInteractionDomain_LightShadowMapPass( + domain.lights[ 0 ], CLASSIC_INTERACTION_RECEIVER_GLOBAL ) + == &csmParallelPass + && csmParallelPass.resourceAlias + && csmParallelPass.resourceOwner + == CLASSIC_INTERACTION_RECEIVER_LOCAL + && csmParallelPass.projected.state.cascadeCount == 4 + && pointCubePass.point.faceCount == 6 + && mappedViewHash != 0 + && hybridPass.disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID; + domain.lightCount = savedLightCount; + domain.shadowMapPassCount = savedShadowMapPassCount; + domain.lights[ 0 ] = savedLight; + for ( int i = 0; i < 4; ++i ) { + domain.shadowMapPasses[ i ] = savedMapPasses[ i ]; + } + if ( !mappedContractOk ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.shadowCasterCount = savedShadowCasterCount; + domain.shadowAlphaStageCount = savedShadowAlphaStageCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + + // A failed preparation rewinds every arena and publishes no partial range. + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.lightCount = 5; + domain.surfaceCount = 7; + domain.primitiveCount = 11; + domain.shadowCasterCount = 13; + domain.shadowAlphaStageCount = 17; + domain.shadowMapPassCount = 19; + domain.textureCount = 23; + classicInteractionDomainView_t rollbackView; + InitView( rollbackView, NULL, -1 ); + rollbackView.ready = true; + rollbackView.firstLight = 2; + rollbackView.lightCount = 3; + rollbackView.firstSurface = 3; + rollbackView.surfaceCount = 4; + rollbackView.firstPrimitive = 5; + rollbackView.primitiveCount = 6; + rollbackView.hash = 1; + const classicInteractionCheckpoint_t checkpoint = { + 2, 3, 5, 7, 11, 13, 17 + }; + classicInteractionBuildError_t rollbackError; + SetError( rollbackError, + CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + 41, 2, 7, 3, 4, 5 ); + const bool rollbackResult = FailView( rollbackView, checkpoint, + rollbackError ); + const bool rollbackOk = !rollbackResult && !rollbackView.ready + && rollbackView.failure + == CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH + && rollbackView.failureDetail == 41 + && rollbackView.failurePassPacketIndex == 2 + && rollbackView.failureDrawPacketIndex == 7 + && rollbackView.failureLightOrdinal == 3 + && rollbackView.failureReceiverOrdinal == 4 + && rollbackView.failureStageIndex == 5 + && rollbackView.firstLight == -1 && rollbackView.lightCount == 0 + && rollbackView.firstSurface == -1 + && rollbackView.surfaceCount == 0 + && rollbackView.firstPrimitive == -1 + && rollbackView.primitiveCount == 0 && rollbackView.hash == 0 + && domain.lightCount == 2 && domain.surfaceCount == 3 + && domain.primitiveCount == 5 + && domain.shadowCasterCount == 7 + && domain.shadowAlphaStageCount == 11 + && domain.shadowMapPassCount == 13 + && domain.textureCount == 17; + if ( !rollbackOk ) { + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.shadowCasterCount = savedShadowCasterCount; + domain.shadowAlphaStageCount = savedShadowAlphaStageCount; + domain.shadowMapPassCount = savedShadowMapPassCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.lights[ 0 ] = savedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.textures[ 0 ] = savedTexture; + return false; + } + + // Prove absolute arena identities and light -> surface -> primitive ordering. + viewDef_t coverageView; + std::memset( &coverageView, 0, sizeof( coverageView ) ); + domain.viewCount = 1; + domain.lightCount = 1; + domain.surfaceCount = 1; + domain.primitiveCount = 2; + domain.shadowCasterCount = 4; + domain.shadowAlphaStageCount = 0; + domain.shadowMapPassCount = 2; + InitView( domain.views[ 0 ], &coverageView, 0 ); + domain.views[ 0 ].ready = true; + domain.views[ 0 ].firstLight = 0; + domain.views[ 0 ].lightCount = 1; + domain.views[ 0 ].firstSurface = 0; + domain.views[ 0 ].surfaceCount = 1; + domain.views[ 0 ].firstPrimitive = 0; + domain.views[ 0 ].primitiveCount = 2; + domain.views[ 0 ].drawablePrimitiveCount = 1; + domain.views[ 0 ].noopPrimitiveCount = 1; + domain.views[ 0 ].firstShadowCaster = 0; + domain.views[ 0 ].shadowCasterCount = 4; + domain.views[ 0 ].drawableShadowCasterCount = 3; + domain.views[ 0 ].noopShadowCasterCount = 1; + domain.views[ 0 ].logicalVolumeDrawCount = 1; + domain.views[ 0 ].preloadVolumeDrawCount = 1; + domain.views[ 0 ].firstShadowMapPass = 0; + domain.views[ 0 ].shadowMapPassCount = 2; + domain.views[ 0 ].hybridShadowPassCount = 1; + domain.views[ 0 ].projectedShadowMapPassCount = 1; + domain.views[ 0 ].pointShadowMapPassCount = 1; + domain.views[ 0 ].shadowMode = CLASSIC_INTERACTION_SHADOW_HYBRID; + InitLight( domain.lights[ 0 ] ); + domain.lights[ 0 ].firstSurface = 0; + domain.lights[ 0 ].surfaceCount = 1; + InitSurface( domain.surfaces[ 0 ] ); + domain.surfaces[ 0 ].lightIndex = 0; + domain.surfaces[ 0 ].firstPrimitive = 0; + domain.surfaces[ 0 ].primitiveCount = 2; + InitPrimitive( domain.primitives[ 0 ] ); + InitPrimitive( domain.primitives[ 1 ] ); + domain.primitives[ 0 ].lightIndex = 0; + domain.primitives[ 0 ].surfaceIndex = 0; + domain.primitives[ 1 ].lightIndex = 0; + domain.primitives[ 1 ].surfaceIndex = 0; + InitShadowMapPass( domain.shadowMapPasses[ 0 ] ); + domain.shadowMapPasses[ 0 ].lightIndex = 0; + domain.shadowMapPasses[ 0 ].receiver = + CLASSIC_INTERACTION_RECEIVER_LOCAL; + domain.shadowMapPasses[ 0 ].disposition = + CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED; + domain.shadowMapPasses[ 0 ].mode = + CLASSIC_INTERACTION_SHADOW_PROJECTED; + InitShadowMapPass( domain.shadowMapPasses[ 1 ] ); + domain.shadowMapPasses[ 1 ].lightIndex = 0; + domain.shadowMapPasses[ 1 ].receiver = + CLASSIC_INTERACTION_RECEIVER_GLOBAL; + domain.shadowMapPasses[ 1 ].disposition = + CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID; + domain.shadowMapPasses[ 1 ].mode = CLASSIC_INTERACTION_SHADOW_HYBRID; + domain.lights[ 0 ].shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_LOCAL ] = 0; + domain.lights[ 0 ].shadowMapPassIndex[ + CLASSIC_INTERACTION_RECEIVER_GLOBAL ] = 1; + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + const bool ordered = R_ClassicInteractionDomain_ViewLight( + domain.views[ 0 ], 0 ) == &domain.lights[ 0 ] + && R_ClassicInteractionDomain_LightSurface( + domain.lights[ 0 ], 0 ) == &domain.surfaces[ 0 ] + && R_ClassicInteractionDomain_SurfacePrimitive( + domain.surfaces[ 0 ], 0 ) == &domain.primitives[ 0 ] + && R_ClassicInteractionDomain_SurfacePrimitive( + domain.surfaces[ 0 ], 1 ) == &domain.primitives[ 1 ] + && R_ClassicInteractionDomain_SurfacePrimitive( + domain.surfaces[ 0 ], 2 ) == NULL; + const bool firstOwned = R_ClassicInteractionDomain_RecordOwned( + &coverageView, CLASSIC_INTERACTION_BACKEND_GL, 1, 1, + 3, 1, 1, 1, 2, 1 ); + const bool repeatedOwned = R_ClassicInteractionDomain_RecordOwned( + &coverageView, CLASSIC_INTERACTION_BACKEND_GL, 1, 1, + 3, 1, 1, 1, 2, 1 ); + const bool mismatchRejected = !R_ClassicInteractionDomain_RecordOwned( + &coverageView, CLASSIC_INTERACTION_BACKEND_VULKAN, 1, 1, + 3, 1, 1, 1, 1, 0 ); + const bool coverageOk = ordered && firstOwned && repeatedOwned + && mismatchRejected + && domain.views[ 0 ].backendOutcome[ + CLASSIC_INTERACTION_BACKEND_GL ] + == CLASSIC_INTERACTION_BACKEND_OWNED + && domain.views[ 0 ].backendOutcome[ + CLASSIC_INTERACTION_BACKEND_VULKAN ] + == CLASSIC_INTERACTION_BACKEND_FALLBACK + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_GL ].ownedViews == 1 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_GL ].duplicateReports == 1 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_GL ].ownedShadowMapPasses == 2 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_GL ].ownedHybridPasses == 1 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_VULKAN ].fallbackViews == 1 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_VULKAN ].coverageMismatches == 1 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_VULKAN ].fallbackShadowMapPasses == 2 + && domain.stats.backend[ + CLASSIC_INTERACTION_BACKEND_VULKAN ].fallbackHybridPasses == 1; + + domain.stats = savedStats; + domain.generation = savedGeneration; + domain.viewCount = savedViewCount; + domain.lightCount = savedLightCount; + domain.surfaceCount = savedSurfaceCount; + domain.primitiveCount = savedPrimitiveCount; + domain.shadowCasterCount = savedShadowCasterCount; + domain.shadowAlphaStageCount = savedShadowAlphaStageCount; + domain.shadowMapPassCount = savedShadowMapPassCount; + domain.textureCount = savedTextureCount; + domain.views[ 0 ] = savedView; + domain.lights[ 0 ] = savedLight; + domain.surfaces[ 0 ] = savedSurface; + domain.primitives[ 0 ] = savedPrimitive; + domain.primitives[ 1 ] = savedCoveragePrimitive; + for ( int i = 0; i < 4; ++i ) { + domain.shadowCasters[ i ] = savedShadowCasters[ i ]; + } + for ( int i = 0; i < 4; ++i ) { + domain.shadowMapPasses[ i ] = savedMapPasses[ i ]; + } + domain.textures[ 0 ] = savedTexture; + return coverageOk; +} diff --git a/src/renderer/ClassicInteractionDomain.h b/src/renderer/ClassicInteractionDomain.h new file mode 100644 index 00000000..86eeef35 --- /dev/null +++ b/src/renderer/ClassicInteractionDomain.h @@ -0,0 +1,659 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_INTERACTION_DOMAIN_H__ +#define __CLASSIC_INTERACTION_DOMAIN_H__ + +#include "ClassicDeformDomain.h" +#include "MaterialResourceTable.h" +#include "ShadowMapProjected.h" + +/* +=============================================================================== + + Shared whole-view ownership contract for classic fixed interaction lighting. + + Preparation is transactional per ordinary root 3D view. A ready view owns + the complete fixed-classic light -> shadow caster -> local/global/translucent + receiver -> primitive stream in source order. Every value needed by a + backend is sealed here; retained legacy pointers are geometry/identity + bridges, not permission to reread material stages or shader registers. + + Unsupported shadow-map caster variants (currently translucent moments), + custom lighting, deforms, GPU-palette skinning, depth hacks, mutable resources, and + renderer-specific interaction variants are explicit whole-view rollback + boundaries. A failed view publishes no light, shadow, + surface, primitive, alpha-stage, or texture range, so the unchanged classic + renderer can execute it exactly once. + +=============================================================================== +*/ + +const int CLASSIC_INTERACTION_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; +const int CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES = SCENE_PACKET_MAX_DRAWS * 4; +const int CLASSIC_INTERACTION_DOMAIN_MAX_TEXTURES = SCENE_PACKET_MAX_DRAWS * 2; +const int CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES = + SCENE_PACKET_MAX_DRAWS * 2; +const int CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES = + CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS * 2; + +enum classicInteractionDomainReceiver_t { + CLASSIC_INTERACTION_RECEIVER_LOCAL = 0, + CLASSIC_INTERACTION_RECEIVER_GLOBAL, + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT, + CLASSIC_INTERACTION_RECEIVER_COUNT +}; + +enum classicInteractionDomainPrimitiveDisposition_t { + CLASSIC_INTERACTION_PRIMITIVE_DRAW = 0, + CLASSIC_INTERACTION_PRIMITIVE_NOOP_INACTIVE_LIGHT, + CLASSIC_INTERACTION_PRIMITIVE_NOOP_INACTIVE_SURFACE, + CLASSIC_INTERACTION_PRIMITIVE_NOOP_MISSING_BUMP, + CLASSIC_INTERACTION_PRIMITIVE_NOOP_BLACK, + CLASSIC_INTERACTION_PRIMITIVE_COUNT +}; + +enum classicInteractionDomainDepth_t { + CLASSIC_INTERACTION_DEPTH_EQUAL = 0, + CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL, + CLASSIC_INTERACTION_DEPTH_COUNT +}; + +enum classicInteractionDomainShadowMode_t { + CLASSIC_INTERACTION_SHADOW_NONE = 0, + CLASSIC_INTERACTION_SHADOW_STENCIL, + CLASSIC_INTERACTION_SHADOW_PROJECTED, + CLASSIC_INTERACTION_SHADOW_POINT, + CLASSIC_INTERACTION_SHADOW_HYBRID, + CLASSIC_INTERACTION_SHADOW_MODE_COUNT +}; + +enum classicInteractionDomainShadowChain_t { + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL = 0, + CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL, + CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT +}; + +enum classicInteractionDomainShadowDisposition_t { + CLASSIC_INTERACTION_SHADOW_CASTER_DRAW = 0, + CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY, + CLASSIC_INTERACTION_SHADOW_CASTER_DISPOSITION_COUNT +}; + +enum classicInteractionDomainShadowIndexSelection_t { + CLASSIC_INTERACTION_SHADOW_INDEX_FULL = 0, + CLASSIC_INTERACTION_SHADOW_INDEX_NO_FRONT_CAPS, + CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS, + CLASSIC_INTERACTION_SHADOW_INDEX_AMBIENT, + CLASSIC_INTERACTION_SHADOW_INDEX_SELECTION_COUNT +}; + +enum classicInteractionDomainShadowMapPassDisposition_t { + CLASSIC_INTERACTION_SHADOW_MAP_PASS_UNUSED = 0, + CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED, + CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID, + CLASSIC_INTERACTION_SHADOW_MAP_PASS_DISPOSITION_COUNT +}; + +enum classicInteractionDomainFailure_t { + CLASSIC_INTERACTION_FAILURE_NONE = 0, + CLASSIC_INTERACTION_FAILURE_UNAVAILABLE, + CLASSIC_INTERACTION_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_NOT_PREPARED, + CLASSIC_INTERACTION_FAILURE_MATERIAL_TABLE_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_LIGHT_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_SURFACE_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_PRIMITIVE_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_SHADOW_CASTER_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_SHADOW_ALPHA_STAGE_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_TEXTURE_POOL_OVERFLOW, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_INTERACTION_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_INTERACTION_FAILURE_INVALID_INTERACTION_PASS, + CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_RANGE, + CLASSIC_INTERACTION_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_INTERACTION_FAILURE_INVALID_DRAW_PACKET, + CLASSIC_INTERACTION_FAILURE_MISSING_GEOMETRY_RECORD, + CLASSIC_INTERACTION_FAILURE_MISSING_INSTANCE_RECORD, + CLASSIC_INTERACTION_FAILURE_MISSING_MATERIAL_RECORD, + CLASSIC_INTERACTION_FAILURE_STALE_MATERIAL_RECORD, + CLASSIC_INTERACTION_FAILURE_MISSING_SHADER_REGISTERS, + CLASSIC_INTERACTION_FAILURE_REGISTER_OUT_OF_RANGE, + CLASSIC_INTERACTION_FAILURE_NONFINITE_VALUE, + CLASSIC_INTERACTION_FAILURE_SHADOWS, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH, + CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING, + CLASSIC_INTERACTION_FAILURE_DEFORM, + CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT, + CLASSIC_INTERACTION_FAILURE_SKINNING, + CLASSIC_INTERACTION_FAILURE_SPECIAL_SURFACE, + CLASSIC_INTERACTION_FAILURE_DEPTH_HACK, + CLASSIC_INTERACTION_FAILURE_NEGATIVE_SCALE, + CLASSIC_INTERACTION_FAILURE_FLAT_DIFFUSE, + CLASSIC_INTERACTION_FAILURE_ENHANCED_MATERIAL, + CLASSIC_INTERACTION_FAILURE_CEL_SHADING, + CLASSIC_INTERACTION_FAILURE_ALTERNATE_INTERACTION_PROGRAM, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_MATERIAL, + CLASSIC_INTERACTION_FAILURE_UNSUPPORTED_STATE, + CLASSIC_INTERACTION_FAILURE_DYNAMIC_RESOURCE, + CLASSIC_INTERACTION_FAILURE_MISSING_RESOURCE, + CLASSIC_INTERACTION_FAILURE_DEFAULTED_RESOURCE, + CLASSIC_INTERACTION_FAILURE_UNLOADED_RESOURCE, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + CLASSIC_INTERACTION_FAILURE_COUNT +}; + +enum classicInteractionDomainBackend_t { + CLASSIC_INTERACTION_BACKEND_GL = 0, + CLASSIC_INTERACTION_BACKEND_VULKAN, + CLASSIC_INTERACTION_BACKEND_COUNT +}; + +enum classicInteractionDomainBackendOutcome_t { + CLASSIC_INTERACTION_BACKEND_UNRECORDED = 0, + CLASSIC_INTERACTION_BACKEND_OWNED, + CLASSIC_INTERACTION_BACKEND_FALLBACK +}; + +typedef struct classicInteractionDomainTexture_s { + std::uint64_t textureResourceId; + const idImage *image; + std::uint64_t storageGeneration; + std::uint64_t nameHash; + unsigned int textureHandle; + textureFilter_t filter; + textureRepeat_t repeat; + bool loaded; + bool defaulted; + bool mutableImage; +} classicInteractionDomainTexture_t; + +typedef struct classicInteractionDomainShadowAlphaStage_s { + int casterIndex; + int stageIndex; + int alphaTestMode; + float alphaTestValue; + float alphaScale; + float alphaHashMode; + float textureMatrix[ 2 ][ 4 ]; + std::uint64_t textureResourceId; + std::uint64_t hash; +} classicInteractionDomainShadowAlphaStage_t; + +typedef struct classicInteractionDomainShadowCaster_s { + // Identity/geometry bridges only after publication. + const drawSurf_t *legacyDrawSurf; + const viewLight_t *legacyViewLight; + const srfTriangles_t *legacyCasterGeometry; + int drawPacketIndex; + int lightIndex; + int sourceOrdinal; + int chainOrdinal; + int geometryRecordIndex; + int instanceRecordIndex; + int materialTableRecordIndex; + int materialId; + std::uint32_t tableGeneration; + classicInteractionDomainShadowChain_t chain; + classicInteractionDomainShadowDisposition_t disposition; + classicInteractionDomainShadowIndexSelection_t indexSelection; + rendererCullMode_t cull; + int materialCoverage; + int firstAlphaStage; + int alphaStageCount; + int vertexCount; + int totalIndexCount; + int selectedIndexCount; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float depthMin; + float depthMax; + float localLightOrigin[ 4 ]; + float modelMatrix[ 16 ]; + float modelViewMatrix[ 16 ]; + float boundsMin[ 3 ]; + float boundsMax[ 3 ]; + bool external; + bool preload; + bool ambientGeometry; + // Topology classification sealed from the actual caster geometry. + bool perfectHull; + bool dynamicCaster; + bool translucentCaster; + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicInteractionDomainShadowCaster_t; + +// The shared classifier and fitter build projected state exactly once. Both +// backends consume these sealed clip, crop, split, filtering and bias values. +typedef struct classicInteractionDomainShadowProjectedState_s { + shadowMapProjectedLightState_t state; + shadowMapProjectedFilterSettings_t filter; + float constantBias; + float normalBias; + float normalOffsetScale; + float cascadeBlend; + float texelBiasScale; + bool depthCompare; + bool receiverPlaneBias; +} classicInteractionDomainShadowProjectedState_t; + +typedef struct classicInteractionDomainShadowPointState_s { + bool valid; + int faceCount; + int faceSize; + float lightOrigin[ 4 ]; + float farDistance; + float constantBias; + float normalBias; + float normalOffsetScale; + float texelBiasScale; + float filterRadius; + int filterTaps; + int filterMode; + bool depthCompare; + bool highPrecision; +} classicInteractionDomainShadowPointState_t; + +// A semantic resource plan is immutable and backend-neutral. resourcePlanId +// is a frame identity rather than an API texture handle: GL and Vulkan reserve +// their own physical image only after proving that it implements this record. +typedef struct classicInteractionDomainShadowMapPass_s { + const viewLight_t *legacyViewLight; + int lightIndex; + classicInteractionDomainReceiver_t receiver; + classicInteractionDomainReceiver_t resourceOwner; + classicInteractionDomainShadowMapPassDisposition_t disposition; + classicInteractionDomainShadowMode_t mode; + shadowMapLightClass_t lightClass; + int receiverMask; + int mappedCasterCount; + int supplementCasterCount; + int drawableMappedCasters; + int noopMappedCasters; + int drawableSupplementCasters; + int noopSupplementCasters; + int casterSignature; + int incompleteMapMask; + int incompleteStencilMask; + int hybridIncompleteMask; + int prelightMapMissingMask; + int prelightStencilRequiredMask; + int prelightStencilReadyMask; + std::uint64_t resourcePlanId; + std::uint32_t resourceGeneration; + bool resourceAlias; + bool mapRequired; + bool mapComplete; + bool stencilComplete; + bool hybridComplete; + bool hasStaticCasters; + bool hasDynamicCasters; + bool hasAlphaCasters; + bool hasTranslucentCasters; + bool allowCacheReuse; + bool allowCacheUpdate; + bool allowScratch; + bool hashedAlpha; + bool stableAlphaHash; + int casterCullMode; + float polygonFactor; + float polygonOffset; + classicInteractionDomainShadowProjectedState_t projected; + classicInteractionDomainShadowPointState_t point; + std::uint64_t hash; +} classicInteractionDomainShadowMapPass_t; + +typedef struct classicInteractionDomainPrimitive_s { + // Geometry/identity bridges only. Backends must not follow these to stages + // or register arrays. + const drawSurf_t *legacyDrawSurf; + const viewLight_t *legacyViewLight; + int lightIndex; + int surfaceIndex; + int lightStageIndex; + int bumpStageIndex; + int diffuseStageIndex; + int specularStageIndex; + classicInteractionDomainReceiver_t receiver; + classicInteractionDomainPrimitiveDisposition_t disposition; + classicInteractionDomainDepth_t depth; + rendererCullMode_t cull; + rendererVertexColorMode_t vertexColor; + rendererBlendState_t blend; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + bool ambientLight; + bool polygonOffsetEnabled; + float polygonOffsetFactor; + float polygonOffsetUnits; + std::uint64_t lightImageResourceId; + std::uint64_t lightFalloffImageResourceId; + std::uint64_t bumpImageResourceId; + std::uint64_t diffuseImageResourceId; + std::uint64_t specularImageResourceId; + float diffuseColor[ 4 ]; + float specularColor[ 4 ]; + float flatDiffuseParams[ 4 ]; + float localLightOrigin[ 4 ]; + float localViewOrigin[ 4 ]; + float lightProjection[ 4 ][ 4 ]; + float bumpMatrix[ 2 ][ 4 ]; + float diffuseMatrix[ 2 ][ 4 ]; + float specularMatrix[ 2 ][ 4 ]; + float modelMatrix[ 16 ]; + float modelViewMatrix[ 16 ]; + std::uint64_t hash; +} classicInteractionDomainPrimitive_t; + +typedef struct classicInteractionDomainSurface_s { + const drawSurf_t *legacyDrawSurf; + const viewLight_t *legacyViewLight; + int drawPacketIndex; + int lightIndex; + int sourceOrdinal; + int receiverOrdinal; + classicInteractionDomainReceiver_t receiver; + int materialTableRecordIndex; + int materialId; + std::uint32_t tableGeneration; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int surfaceStageCount; + int activeSurfaceStageCount; + int inactiveSurfaceStageCount; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicInteractionDomainSurface_t; + +typedef struct classicInteractionDomainLight_s { + const viewLight_t *legacyViewLight; + int sourceOrdinal; + int firstSurface; + int surfaceCount; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int lightStageCount; + int activeLightStageCount; + int inactiveLightStageCount; + int receiverSurfaceCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int receiverPrimitiveCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int firstShadowCaster[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int shadowCasterCount[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + // LOCAL and GLOBAL own physical map resources. TRANSLUCENT, when enabled, + // samples the sealed GLOBAL pass and therefore has no third allocation. + int shadowMapPassIndex[ 2 ]; + classicInteractionDomainShadowMode_t receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int shadowCasterTotal; + int drawableShadowCasters; + int noopShadowCasters; + int logicalVolumeDraws; + int preloadVolumeDraws; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float globalLightOrigin[ 4 ]; + float lightRadius[ 4 ]; + float lightProject[ 4 ][ 4 ]; + bool pointLight; + bool parallel; + bool ambientLight; + bool shadowClassified; + bool clearStencil; + std::uint64_t hash; +} classicInteractionDomainLight_t; + +typedef struct classicInteractionDomainBackendCoverage_s { + std::uint64_t ownedViewMask; + std::uint64_t fallbackViewMask; + int ownedViews; + int fallbackViews; + int ownedLights; + int fallbackLights; + int ownedSurfaces; + int fallbackSurfaces; + int ownedDrawablePrimitives; + int fallbackDrawablePrimitives; + int ownedNoopPrimitives; + int ownedShadowCasters; + int ownedNoopShadowCasters; + int ownedLogicalVolumeDraws; + int ownedPreloadVolumeDraws; + int ownedShadowMapPasses; + int ownedHybridPasses; + int fallbackNoopPrimitives; + int fallbackShadowCasters; + int fallbackNoopShadowCasters; + int fallbackLogicalVolumeDraws; + int fallbackPreloadVolumeDraws; + int fallbackShadowMapPasses; + int fallbackHybridPasses; + int coverageMismatches; + int duplicateReports; + int untrackedFallbacks; +} classicInteractionDomainBackendCoverage_t; + +typedef struct classicInteractionDomainView_s { + const viewDef_t *viewDef; + int scenePacketIndex; + int interactionPassPacketIndex; + int stencilShadowPassPacketIndex; + int shadowMapPassPacketIndex; + std::uint32_t tableGeneration; + int firstLight; + int lightCount; + int firstSurface; + int surfaceCount; + int firstPrimitive; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int firstShadowCaster; + int shadowCasterCount; + int firstShadowMapPass; + int drawableShadowCasterCount; + int noopShadowCasterCount; + int logicalVolumeDrawCount; + int preloadVolumeDrawCount; + int shadowLightCount; + int shadowMapPassCount; + int hybridShadowPassCount; + int projectedShadowMapPassCount; + int csmShadowMapPassCount; + int pointShadowMapPassCount; + int projectedShadowLightCount; + int pointShadowLightCount; + int packetDrawCount; + int shadowPacketDrawCount; + int activeLightStageCount; + int inactiveLightStageCount; + int activeSurfaceStageCount; + int inactiveSurfaceStageCount; + int materialDeformReceiverCount; + int completedDeformCasterCount; + int emptyDeformCasterCount; + int receiverSurfaceCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int receiverPrimitiveCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int viewportX1; + int viewportY1; + int viewportX2; + int viewportY2; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float projectionMatrix[ 16 ]; + float maxLightValue; + float lightScale; + float overBright; + float shadowPolygonFactor; + float shadowPolygonUnits; + int stencilReference; + classicInteractionDomainShadowMode_t shadowMode; + bool useScissor; + bool useShadowVertexProgram; + bool preferTwoSidedStencil; + bool useDepthBounds; + bool stencilTranslucentShadows; + bool ready; + classicInteractionDomainFailure_t failure; + int failureDetail; + int failurePassPacketIndex; + int failureDrawPacketIndex; + int failureLightOrdinal; + int failureReceiverOrdinal; + int failureStageIndex; + std::uint64_t hash; + classicInteractionDomainBackendOutcome_t backendOutcome[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + classicInteractionDomainFailure_t backendFailure[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendFailureDetail[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendDrawnPrimitives[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendNoopPrimitives[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendShadowCasters[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendNoopShadowCasters[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendLogicalVolumeDraws[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendPreloadVolumeDraws[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendShadowMapPasses[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + int backendHybridPasses[ CLASSIC_INTERACTION_BACKEND_COUNT ]; +} classicInteractionDomainView_t; + +typedef struct classicInteractionDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int interactionViews; + int readyViews; + int fallbackViews; + int lights; + int surfaces; + int primitives; + int drawablePrimitives; + int noopPrimitives; + int shadowCasters; + int drawableShadowCasters; + int noopShadowCasters; + int logicalVolumeDraws; + int preloadVolumeDraws; + int shadowAlphaStages; + int shadowLights; + int shadowMapPasses; + int hybridShadowPasses; + int projectedShadowMapPasses; + int csmShadowMapPasses; + int pointShadowMapPasses; + int projectedShadowLights; + int pointShadowLights; + int textures; + int activeLightStages; + int inactiveLightStages; + int activeSurfaceStages; + int inactiveSurfaceStages; + int materialDeformReceivers; + int completedDeformCasters; + int emptyDeformCasters; + int receiverSurfaces[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int receiverPrimitives[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int failureCounts[ CLASSIC_INTERACTION_FAILURE_COUNT ]; + std::uint64_t hash; + classicInteractionDomainBackendCoverage_t backend[ CLASSIC_INTERACTION_BACKEND_COUNT ]; + char status[ 96 ]; +} classicInteractionDomainStats_t; + +void R_ClassicInteractionDomain_ResetFrame( void ); +void R_ClassicInteractionDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicInteractionDomainStats_t &R_ClassicInteractionDomain_Stats( void ); +int R_ClassicInteractionDomain_NumViews( void ); +const classicInteractionDomainView_t *R_ClassicInteractionDomain_ViewByIndex( int index ); +const classicInteractionDomainView_t *R_ClassicInteractionDomain_ViewForScenePacket( int scenePacketIndex ); +const classicInteractionDomainView_t *R_ClassicInteractionDomain_FindView( const viewDef_t *viewDef ); +const classicInteractionDomainLight_t *R_ClassicInteractionDomain_ViewLight( + const classicInteractionDomainView_t &view, int lightIndex ); +const classicInteractionDomainSurface_t *R_ClassicInteractionDomain_ViewSurface( + const classicInteractionDomainView_t &view, int surfaceIndex ); +const classicInteractionDomainPrimitive_t *R_ClassicInteractionDomain_ViewPrimitive( + const classicInteractionDomainView_t &view, int primitiveIndex ); +const classicInteractionDomainShadowCaster_t *R_ClassicInteractionDomain_ViewShadowCaster( + const classicInteractionDomainView_t &view, int casterIndex ); +const classicInteractionDomainShadowCaster_t *R_ClassicInteractionDomain_LightShadowCaster( + const classicInteractionDomainLight_t &light, + classicInteractionDomainShadowChain_t chain, int casterIndex ); +bool R_ClassicInteractionDomain_ShadowCasterNoopValid( + const classicInteractionDomainShadowCaster_t &caster ); +const classicInteractionDomainShadowAlphaStage_t *R_ClassicInteractionDomain_ShadowAlphaStage( + const classicInteractionDomainShadowCaster_t &caster, int stageIndex ); +const classicInteractionDomainShadowMapPass_t *R_ClassicInteractionDomain_LightShadowMapPass( + const classicInteractionDomainLight_t &light, + classicInteractionDomainReceiver_t receiver ); +const classicInteractionDomainSurface_t *R_ClassicInteractionDomain_LightSurface( + const classicInteractionDomainLight_t &light, int surfaceIndex ); +const classicInteractionDomainPrimitive_t *R_ClassicInteractionDomain_SurfacePrimitive( + const classicInteractionDomainSurface_t &surface, int primitiveIndex ); +const classicInteractionDomainTexture_t *R_ClassicInteractionDomain_ResolveTexture( + std::uint64_t textureResourceId ); +bool R_ClassicInteractionDomain_RecordOwned( const viewDef_t *viewDef, + classicInteractionDomainBackend_t backend, int drawnPrimitives, + int noopPrimitives, int submittedShadowCasters = 0, + int noopShadowCasters = 0, int logicalVolumeDraws = 0, + int preloadVolumeDraws = 0, int shadowMapPasses = 0, + int hybridPasses = 0 ); +void R_ClassicInteractionDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicInteractionDomainBackend_t backend, + classicInteractionDomainFailure_t failure, int detail ); +const classicInteractionDomainBackendCoverage_t &R_ClassicInteractionDomain_BackendCoverage( + classicInteractionDomainBackend_t backend ); +const char *ClassicInteractionDomainReceiver_Name( classicInteractionDomainReceiver_t receiver ); +const char *ClassicInteractionDomainPrimitiveDisposition_Name( + classicInteractionDomainPrimitiveDisposition_t disposition ); +const char *ClassicInteractionDomainDepth_Name( classicInteractionDomainDepth_t depth ); +const char *ClassicInteractionDomainShadowMode_Name( + classicInteractionDomainShadowMode_t mode ); +const char *ClassicInteractionDomainShadowChain_Name( + classicInteractionDomainShadowChain_t chain ); +const char *ClassicInteractionDomainFailure_Name( classicInteractionDomainFailure_t failure ); +const char *ClassicInteractionDomainBackend_Name( classicInteractionDomainBackend_t backend ); +bool RendererClassicInteractionDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_INTERACTION_DOMAIN_H__ */ diff --git a/src/renderer/ClassicSpecialFrameDomain.cpp b/src/renderer/ClassicSpecialFrameDomain.cpp new file mode 100644 index 00000000..686de9a3 --- /dev/null +++ b/src/renderer/ClassicSpecialFrameDomain.cpp @@ -0,0 +1,546 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicSpecialFrameDomain.h" + +#include + +typedef struct classicSpecialFrameDomainState_s { + classicSpecialFrameDomainView_t views[ + CLASSIC_SPECIAL_FRAME_DOMAIN_MAX_VIEWS ]; + int viewCount; + classicSpecialFrameDomainStats_t stats; +} classicSpecialFrameDomainState_t; + +static classicSpecialFrameDomainState_t rg_classicSpecialFrameDomain; + +static std::uint64_t R_ClassicSpecialFrameDomain_HashBytes( + std::uint64_t hash, const void *data, size_t bytes ) { + const byte *source = static_cast( data ); + for ( size_t i = 0; i < bytes; ++i ) { + hash ^= source[ i ]; + hash *= UINT64_C( 1099511628211 ); + } + return hash; +} + +static std::uint64_t R_ClassicSpecialFrameDomain_HashView( + const classicSpecialFrameDomainView_t &view ) { + std::uint64_t hash = UINT64_C( 1469598103934665603 ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, &view.scope, + sizeof( view.scope ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.scenePacketIndex, sizeof( view.scenePacketIndex ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.passPacketIndex, sizeof( view.passPacketIndex ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.sourceSurfaceCount, sizeof( view.sourceSurfaceCount ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.packetDrawCount, sizeof( view.packetDrawCount ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.passPacketCount, sizeof( view.passPacketCount ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.renderDemoVersion, sizeof( view.renderDemoVersion ) ); + hash = R_ClassicSpecialFrameDomain_HashBytes( hash, + &view.specialEffectsMask, sizeof( view.specialEffectsMask ) ); + return hash; +} + +static void R_ClassicSpecialFrameDomain_ResetStats( void ) { + memset( &rg_classicSpecialFrameDomain, 0, + sizeof( rg_classicSpecialFrameDomain ) ); + idStr::Copynz( rg_classicSpecialFrameDomain.stats.status, "empty", + sizeof( rg_classicSpecialFrameDomain.stats.status ) ); +} + +void R_ClassicSpecialFrameDomain_ResetFrame( void ) { + R_ClassicSpecialFrameDomain_ResetStats(); +} + +static void R_ClassicSpecialFrameDomain_Fail( + classicSpecialFrameDomainView_t &view, + classicSpecialFrameDomainFailure_t failure, int detail ) { + view.ready = false; + view.failure = failure; + view.failureDetail = detail; + if ( failure > CLASSIC_SPECIAL_FRAME_FAILURE_NONE + && failure < CLASSIC_SPECIAL_FRAME_FAILURE_COUNT ) { + rg_classicSpecialFrameDomain.stats.failureCounts[ failure ]++; + } +} + +static void R_ClassicSpecialFrameDomain_Commit( + classicSpecialFrameDomainView_t &view ) { + view.ready = true; + view.failure = CLASSIC_SPECIAL_FRAME_FAILURE_NONE; + view.failureDetail = 0; + view.hash = R_ClassicSpecialFrameDomain_HashView( view ); + rg_classicSpecialFrameDomain.stats.readyViews++; + rg_classicSpecialFrameDomain.stats.hash ^= + view.hash + UINT64_C( 0x9e3779b97f4a7c15 ) + + ( rg_classicSpecialFrameDomain.stats.hash << 6 ) + + ( rg_classicSpecialFrameDomain.stats.hash >> 2 ); +} + +static bool R_ClassicSpecialFrameDomain_AddRecord( + classicSpecialFrameDomainView_t *&result, const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, int sceneIndex ) { + result = NULL; + if ( rg_classicSpecialFrameDomain.viewCount + >= CLASSIC_SPECIAL_FRAME_DOMAIN_MAX_VIEWS ) { + rg_classicSpecialFrameDomain.stats.overflow = true; + return false; + } + classicSpecialFrameDomainView_t &view = + rg_classicSpecialFrameDomain.views[ + rg_classicSpecialFrameDomain.viewCount++ ]; + memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scope = scope; + view.scenePacketIndex = sceneIndex; + view.passPacketIndex = -1; + view.failure = CLASSIC_SPECIAL_FRAME_FAILURE_UNAVAILABLE; + result = &view; + rg_classicSpecialFrameDomain.stats.views++; + if ( scope == CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO ) { + rg_classicSpecialFrameDomain.stats.renderDemoViews++; + } else { + rg_classicSpecialFrameDomain.stats.ravenEffectsViews++; + } + return true; +} + +static bool R_ClassicSpecialFrameDomain_ValidateSceneRange( + const idScenePacketFrame &packetFrame, int sceneIndex, + const scenePacket_t &scene, classicSpecialFrameDomainView_t &view, + bool commandOnly ) { + if ( sceneIndex < 0 || sceneIndex >= packetFrame.NumScenes() + || scene.viewDef == NULL || scene.passPacketCount <= 0 + || scene.firstPassPacket < 0 + || scene.firstPassPacket + scene.passPacketCount + > packetFrame.NumPasses() || scene.drawPacketCount < 0 ) { + R_ClassicSpecialFrameDomain_Fail( view, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_SCENE_RANGE, sceneIndex ); + return false; + } + int packetDraws = 0; + for ( int passOffset = 0; passOffset < scene.passPacketCount; ++passOffset ) { + const int passIndex = scene.firstPassPacket + passOffset; + const passPacket_t &pass = packetFrame.Pass( passIndex ); + if ( pass.firstDrawPacket < 0 || pass.drawPacketCount < 0 + || pass.firstDrawPacket + pass.drawPacketCount + > packetFrame.NumDrawPackets() + || ( commandOnly && !pass.commandOnly ) ) { + R_ClassicSpecialFrameDomain_Fail( view, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_PASS_RANGE, passIndex ); + return false; + } + packetDraws += pass.drawPacketCount; + for ( int drawOffset = 0; drawOffset < pass.drawPacketCount; ++drawOffset ) { + const drawPacket_t &packet = packetFrame.DrawPacket( + pass.firstDrawPacket + drawOffset ); + if ( packet.viewDef != scene.viewDef || packet.legacyDrawSurf == NULL ) { + R_ClassicSpecialFrameDomain_Fail( view, + CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH, + pass.firstDrawPacket + drawOffset ); + return false; + } + } + } + if ( packetDraws != scene.drawPacketCount ) { + R_ClassicSpecialFrameDomain_Fail( view, + CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH, packetDraws ); + return false; + } + view.packetDrawCount = packetDraws; + view.passPacketCount = scene.passPacketCount; + return true; +} + +static bool R_ClassicSpecialFrameDomain_HasPass( + const idScenePacketFrame &packetFrame, const scenePacket_t &scene, + renderPassCategory_t category ) { + for ( int passOffset = 0; passOffset < scene.passPacketCount; ++passOffset ) { + const passPacket_t &pass = packetFrame.Pass( + scene.firstPassPacket + passOffset ); + if ( pass.passCategory == category ) { + return true; + } + } + return false; +} + +static void R_ClassicSpecialFrameDomain_AddRenderDemo( + const idScenePacketFrame &packetFrame, int sceneIndex, + const scenePacket_t &scene ) { + if ( !scene.renderDemoPlayback ) { + return; + } + classicSpecialFrameDomainView_t *view = NULL; + if ( !R_ClassicSpecialFrameDomain_AddRecord( view, scene.viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, sceneIndex ) ) { + return; + } + if ( view->viewDef == NULL || view->viewDef->viewEntitys == NULL + || view->viewDef->renderWorld == NULL || view->viewDef->isSubview + || view->viewDef->isMirror || view->viewDef->isXraySubview + || view->viewDef->isEditor || view->viewDef->superView != NULL + || view->viewDef->subviewSurface != NULL + || view->viewDef->numClipPlanes != 0 + || view->viewDef->numDrawSurfs <= 0 + || view->viewDef->drawSurfs == NULL ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_UNSUPPORTED_VIEW, 0 ); + return; + } + if ( session == NULL || session->readDemo == NULL + || session->rw != view->viewDef->renderWorld + || session->renderdemoVersion <= 0 ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_RENDER_DEMO_STATE, 0 ); + return; + } + if ( !R_ClassicSpecialFrameDomain_ValidateSceneRange( packetFrame, + sceneIndex, scene, *view, false ) ) { + return; + } + if ( !R_ClassicSpecialFrameDomain_HasPass( packetFrame, scene, + RENDER_PASS_DEPTH ) + || !R_ClassicSpecialFrameDomain_HasPass( packetFrame, scene, + RENDER_PASS_ARB2_INTERACTION ) + || !R_ClassicSpecialFrameDomain_HasPass( packetFrame, scene, + RENDER_PASS_AMBIENT ) ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_PASS_RANGE, 0 ); + return; + } + view->sourceSurfaceCount = view->viewDef->numDrawSurfs; + view->renderDemoVersion = session->renderdemoVersion; + view->passPacketIndex = scene.firstPassPacket; + R_ClassicSpecialFrameDomain_Commit( *view ); +} + +static void R_ClassicSpecialFrameDomain_AddRavenEffects( + const idScenePacketFrame &packetFrame, int sceneIndex, + const scenePacket_t &scene ) { + if ( scene.passPacketCount != 1 || scene.firstPassPacket < 0 + || scene.firstPassPacket >= packetFrame.NumPasses() ) { + return; + } + const passPacket_t &pass = packetFrame.Pass( scene.firstPassPacket ); + if ( pass.passCategory != RENDER_PASS_SPECIAL_EFFECTS ) { + return; + } + classicSpecialFrameDomainView_t *view = NULL; + if ( !R_ClassicSpecialFrameDomain_AddRecord( view, scene.viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, sceneIndex ) ) { + return; + } + if ( view->viewDef == NULL || view->viewDef->viewEntitys == NULL + || view->viewDef->renderWorld == NULL || view->viewDef->isSubview + || view->viewDef->isMirror || view->viewDef->isXraySubview + || view->viewDef->isEditor || view->viewDef->superView != NULL + || view->viewDef->subviewSurface != NULL + || view->viewDef->numClipPlanes != 0 + || view->viewDef->renderView.viewID < 0 ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_UNSUPPORTED_VIEW, 0 ); + return; + } + if ( !R_ClassicSpecialFrameDomain_ValidateSceneRange( packetFrame, + sceneIndex, scene, *view, true ) || !pass.commandOnly ) { + if ( view->failure == CLASSIC_SPECIAL_FRAME_FAILURE_UNAVAILABLE ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_PASS_RANGE, 0 ); + } + return; + } + view->specialEffectsMask = scene.specialEffectsMask + & ( SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL ); + if ( view->specialEffectsMask == 0 ) { + R_ClassicSpecialFrameDomain_Fail( *view, + CLASSIC_SPECIAL_FRAME_FAILURE_SPECIAL_EFFECT_STATE, 0 ); + return; + } + view->passPacketIndex = scene.firstPassPacket; + rg_classicSpecialFrameDomain.stats.specialEffectsMask + |= view->specialEffectsMask; + R_ClassicSpecialFrameDomain_Commit( *view ); +} + +void R_ClassicSpecialFrameDomain_PrepareFrame( + const idScenePacketFrame &packetFrame ) { + R_ClassicSpecialFrameDomain_ResetStats(); + classicSpecialFrameDomainStats_t &stats = rg_classicSpecialFrameDomain.stats; + stats.prepared = true; + stats.sourceScenes = packetFrame.NumScenes(); + if ( packetFrame.Stats().overflow || !packetFrame.ValidateSortKeys() ) { + stats.overflow = packetFrame.Stats().overflow; + stats.frameValid = false; + stats.failureCounts[ CLASSIC_SPECIAL_FRAME_FAILURE_SCENE_PACKET_OVERFLOW ]++; + idStr::Copynz( stats.status, "packet-invalid", sizeof( stats.status ) ); + return; + } + stats.frameValid = true; + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + R_ClassicSpecialFrameDomain_AddRenderDemo( packetFrame, sceneIndex, scene ); + R_ClassicSpecialFrameDomain_AddRavenEffects( packetFrame, sceneIndex, scene ); + } + if ( stats.overflow ) { + stats.frameValid = false; + idStr::Copynz( stats.status, "view-overflow", sizeof( stats.status ) ); + } else if ( stats.views == 0 ) { + idStr::Copynz( stats.status, "prepared", sizeof( stats.status ) ); + } else if ( stats.readyViews == stats.views ) { + idStr::Copynz( stats.status, "ready", sizeof( stats.status ) ); + } else { + idStr::Copynz( stats.status, "fallback", sizeof( stats.status ) ); + } +} + +const classicSpecialFrameDomainStats_t &R_ClassicSpecialFrameDomain_Stats( void ) { + return rg_classicSpecialFrameDomain.stats; +} + +static const classicSpecialFrameDomainView_t *R_ClassicSpecialFrameDomain_Find( + const viewDef_t *viewDef, classicSpecialFrameDomainScope_t scope ) { + for ( int i = 0; i < rg_classicSpecialFrameDomain.viewCount; ++i ) { + const classicSpecialFrameDomainView_t &view = + rg_classicSpecialFrameDomain.views[ i ]; + if ( view.viewDef == viewDef && view.scope == scope ) { + return &view; + } + } + return NULL; +} + +const classicSpecialFrameDomainView_t *R_ClassicSpecialFrameDomain_FindRenderDemoView( + const viewDef_t *viewDef ) { + return R_ClassicSpecialFrameDomain_Find( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO ); +} + +const classicSpecialFrameDomainView_t *R_ClassicSpecialFrameDomain_FindRavenEffectsView( + const viewDef_t *viewDef ) { + return R_ClassicSpecialFrameDomain_Find( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS ); +} + +bool R_ClassicSpecialFrameDomain_ReadyForBackend( const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend ) { + const classicSpecialFrameDomainView_t *view = + R_ClassicSpecialFrameDomain_Find( viewDef, scope ); + return view != NULL && view->ready + && backend >= CLASSIC_SPECIAL_FRAME_BACKEND_GL + && backend < CLASSIC_SPECIAL_FRAME_BACKEND_COUNT + && view->backendOutcome[ backend ] + != CLASSIC_SPECIAL_FRAME_BACKEND_FALLBACK; +} + +void R_ClassicSpecialFrameDomain_RecordBackendFallback( + const viewDef_t *viewDef, classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend, + classicSpecialFrameDomainFailure_t failure, int detail ) { + classicSpecialFrameDomainView_t *view = + const_cast( + R_ClassicSpecialFrameDomain_Find( viewDef, scope ) ); + if ( view == NULL || backend < CLASSIC_SPECIAL_FRAME_BACKEND_GL + || backend >= CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ) { + return; + } + classicSpecialFrameDomainBackendCoverage_t &coverage = + rg_classicSpecialFrameDomain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] + != CLASSIC_SPECIAL_FRAME_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + view->backendOutcome[ backend ] = CLASSIC_SPECIAL_FRAME_BACKEND_FALLBACK; + view->backendFailure[ backend ] = failure; + view->backendFailureDetail[ backend ] = detail; + coverage.fallbackViews++; + coverage.fallbackSurfaces += view->sourceSurfaceCount; + rg_classicSpecialFrameDomain.stats.fallbackViews++; +} + +bool R_ClassicSpecialFrameDomain_RecordOwned( const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend, int coverageValue ) { + classicSpecialFrameDomainView_t *view = + const_cast( + R_ClassicSpecialFrameDomain_Find( viewDef, scope ) ); + if ( view == NULL || !view->ready + || backend < CLASSIC_SPECIAL_FRAME_BACKEND_GL + || backend >= CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ) { + return false; + } + classicSpecialFrameDomainBackendCoverage_t &coverage = + rg_classicSpecialFrameDomain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] == CLASSIC_SPECIAL_FRAME_BACKEND_FALLBACK ) { + coverage.coverageMismatches++; + return false; + } + if ( scope == CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO + && coverageValue != view->sourceSurfaceCount ) { + coverage.coverageMismatches++; + return false; + } + if ( scope == CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS + && ( coverageValue <= 0 + || ( coverageValue & ~view->specialEffectsMask ) != 0 ) ) { + coverage.coverageMismatches++; + return false; + } + if ( view->backendOutcome[ backend ] == CLASSIC_SPECIAL_FRAME_BACKEND_OWNED ) { + coverage.duplicateReports++; + return true; + } + if ( scope == CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS ) { + view->backendCoverage[ backend ] |= coverageValue; + if ( ( view->backendCoverage[ backend ] & view->specialEffectsMask ) + != view->specialEffectsMask ) { + return true; + } + } else { + view->backendCoverage[ backend ] = coverageValue; + } + view->backendOutcome[ backend ] = CLASSIC_SPECIAL_FRAME_BACKEND_OWNED; + coverage.ownedViews++; + coverage.ownedSurfaces += scope == CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO + ? coverageValue : 1; + return true; +} + +void R_ClassicSpecialFrameDomain_FinalizeBackendFrame( + classicSpecialFrameDomainBackend_t backend ) { + if ( backend < CLASSIC_SPECIAL_FRAME_BACKEND_GL + || backend >= CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ) { + return; + } + for ( int i = 0; i < rg_classicSpecialFrameDomain.viewCount; ++i ) { + classicSpecialFrameDomainView_t &view = + rg_classicSpecialFrameDomain.views[ i ]; + if ( view.backendOutcome[ backend ] + == CLASSIC_SPECIAL_FRAME_BACKEND_UNRECORDED ) { + R_ClassicSpecialFrameDomain_RecordBackendFallback( view.viewDef, + view.scope, backend, + view.ready ? CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_REJECTED + : view.failure, + view.ready ? 0 : view.failureDetail ); + } + } +} + +const char *ClassicSpecialFrameDomainFailure_Name( + classicSpecialFrameDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_SPECIAL_FRAME_FAILURE_NONE: return "none"; + case CLASSIC_SPECIAL_FRAME_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_SPECIAL_FRAME_FAILURE_SCENE_PACKET_OVERFLOW: return "scenePacketOverflow"; + case CLASSIC_SPECIAL_FRAME_FAILURE_VIEW_POOL_OVERFLOW: return "viewPoolOverflow"; + case CLASSIC_SPECIAL_FRAME_FAILURE_UNSUPPORTED_VIEW: return "unsupportedView"; + case CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_SCENE_RANGE: return "invalidSceneRange"; + case CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_PASS_RANGE: return "invalidPassRange"; + case CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH: return "sourcePacketMismatch"; + case CLASSIC_SPECIAL_FRAME_FAILURE_RENDER_DEMO_STATE: return "renderDemoState"; + case CLASSIC_SPECIAL_FRAME_FAILURE_SPECIAL_EFFECT_STATE: return "specialEffectState"; + case CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_NOT_READY: return "backendNotReady"; + case CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_REJECTED: return "backendRejected"; + case CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_COVERAGE_MISMATCH: return "backendCoverageMismatch"; + default: return "unknown"; + } +} + +bool RendererClassicSpecialFrameDomain_RunSelfTest( void ) { + R_ClassicSpecialFrameDomain_ResetFrame(); + const classicSpecialFrameDomainStats_t &reset = + R_ClassicSpecialFrameDomain_Stats(); + if ( CLASSIC_SPECIAL_FRAME_DOMAIN_MAX_VIEWS != SCENE_PACKET_MAX_SCENES + || reset.prepared || reset.frameValid || reset.views != 0 + || idStr::Cmp( ClassicSpecialFrameDomainFailure_Name( + CLASSIC_SPECIAL_FRAME_FAILURE_RENDER_DEMO_STATE ), + "renderDemoState" ) + || idStr::Cmp( ClassicSpecialFrameDomainFailure_Name( + CLASSIC_SPECIAL_FRAME_FAILURE_SPECIAL_EFFECT_STATE ), + "specialEffectState" ) + || R_ClassicSpecialFrameDomain_ReadyForBackend( NULL, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_GL ) ) { + return false; + } + + viewDef_t demoView; + viewDef_t effectsView; + memset( &demoView, 0, sizeof( demoView ) ); + memset( &effectsView, 0, sizeof( effectsView ) ); + classicSpecialFrameDomainView_t &demo = + rg_classicSpecialFrameDomain.views[ 0 ]; + classicSpecialFrameDomainView_t &effects = + rg_classicSpecialFrameDomain.views[ 1 ]; + memset( &demo, 0, sizeof( demo ) ); + memset( &effects, 0, sizeof( effects ) ); + demo.viewDef = &demoView; + demo.scope = CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO; + demo.scenePacketIndex = 1; + demo.passPacketIndex = 2; + demo.sourceSurfaceCount = 3; + demo.packetDrawCount = 8; + demo.passPacketCount = 6; + demo.renderDemoVersion = 10; + demo.ready = true; + demo.hash = R_ClassicSpecialFrameDomain_HashView( demo ); + effects.viewDef = &effectsView; + effects.scope = CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS; + effects.scenePacketIndex = 4; + effects.passPacketIndex = 5; + effects.passPacketCount = 1; + effects.specialEffectsMask = SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL; + effects.ready = true; + const std::uint64_t effectsHash = R_ClassicSpecialFrameDomain_HashView( effects ); + effects.specialEffectsMask = SPECIAL_EFFECT_BLUR; + const bool hashSensitive = R_ClassicSpecialFrameDomain_HashView( effects ) + != effectsHash; + effects.specialEffectsMask = SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL; + effects.hash = effectsHash; + rg_classicSpecialFrameDomain.viewCount = 2; + memset( &rg_classicSpecialFrameDomain.stats, 0, + sizeof( rg_classicSpecialFrameDomain.stats ) ); + + const bool demoOwned = R_ClassicSpecialFrameDomain_RecordOwned( &demoView, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, 3 ); + const bool demoMismatch = !R_ClassicSpecialFrameDomain_RecordOwned( &demoView, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, 2 ); + const bool blurOwned = R_ClassicSpecialFrameDomain_RecordOwned( &effectsView, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, SPECIAL_EFFECT_BLUR ); + const bool alOwned = R_ClassicSpecialFrameDomain_RecordOwned( &effectsView, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, SPECIAL_EFFECT_AL ); + R_ClassicSpecialFrameDomain_RecordBackendFallback( &demoView, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_COVERAGE_MISMATCH, 2 ); + const classicSpecialFrameDomainStats_t &stats = + R_ClassicSpecialFrameDomain_Stats(); + const bool coverage = demoOwned && demoMismatch && blurOwned && alOwned + && demo.backendOutcome[ CLASSIC_SPECIAL_FRAME_BACKEND_GL ] + == CLASSIC_SPECIAL_FRAME_BACKEND_OWNED + && demo.backendOutcome[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ] + == CLASSIC_SPECIAL_FRAME_BACKEND_FALLBACK + && effects.backendOutcome[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ] + == CLASSIC_SPECIAL_FRAME_BACKEND_OWNED + && effects.backendCoverage[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ] + == ( SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL ) + && stats.backend[ CLASSIC_SPECIAL_FRAME_BACKEND_GL ].ownedViews == 1 + && stats.backend[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ].ownedViews == 1 + && stats.backend[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ].fallbackViews == 1 + && stats.backend[ CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ] + .coverageMismatches == 1; + R_ClassicSpecialFrameDomain_ResetFrame(); + return hashSensitive && coverage; +} diff --git a/src/renderer/ClassicSpecialFrameDomain.h b/src/renderer/ClassicSpecialFrameDomain.h new file mode 100644 index 00000000..6f1d7a0c --- /dev/null +++ b/src/renderer/ClassicSpecialFrameDomain.h @@ -0,0 +1,138 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_SPECIAL_FRAME_DOMAIN_H__ +#define __CLASSIC_SPECIAL_FRAME_DOMAIN_H__ + +#include "ScenePackets.h" + +/* +============================================================================== + + Render-demo playback and Raven's special-frame controller cannot be + classified solely from normal material passes. A demo is an immutable + session stream that can use an ordinary positive view id, while the Raven + controller is an RC_DRAW_SPECIAL_EFFECTS command whose depth/color work is + completed later in the matching world, resolve, and UI paths. + + This domain seals those two command/view islands before either backend uses + the existing complete executor. It never rewrites a decoder, recorded + world, special-effect resource, or resolved-target path. A failed record is + therefore wholly classic; a successful record reports only after the same + complete executor has finished its source range. + +============================================================================== +*/ + +const int CLASSIC_SPECIAL_FRAME_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; + +enum classicSpecialFrameDomainScope_t { + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO = 0, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_SCOPE_COUNT +}; + +enum classicSpecialFrameDomainFailure_t { + CLASSIC_SPECIAL_FRAME_FAILURE_NONE = 0, + CLASSIC_SPECIAL_FRAME_FAILURE_UNAVAILABLE, + CLASSIC_SPECIAL_FRAME_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_SPECIAL_FRAME_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_SPECIAL_FRAME_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_SPECIAL_FRAME_FAILURE_INVALID_PASS_RANGE, + CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_SPECIAL_FRAME_FAILURE_RENDER_DEMO_STATE, + CLASSIC_SPECIAL_FRAME_FAILURE_SPECIAL_EFFECT_STATE, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_NOT_READY, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_REJECTED, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_SPECIAL_FRAME_FAILURE_COUNT +}; + +enum classicSpecialFrameDomainBackend_t { + CLASSIC_SPECIAL_FRAME_BACKEND_GL = 0, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT +}; + +enum classicSpecialFrameDomainBackendOutcome_t { + CLASSIC_SPECIAL_FRAME_BACKEND_UNRECORDED = 0, + CLASSIC_SPECIAL_FRAME_BACKEND_OWNED, + CLASSIC_SPECIAL_FRAME_BACKEND_FALLBACK +}; + +typedef struct classicSpecialFrameDomainView_s { + const viewDef_t *viewDef; + classicSpecialFrameDomainScope_t scope; + int scenePacketIndex; + int passPacketIndex; + int sourceSurfaceCount; + int packetDrawCount; + int passPacketCount; + int renderDemoVersion; + int specialEffectsMask; + bool ready; + classicSpecialFrameDomainFailure_t failure; + int failureDetail; + std::uint64_t hash; + classicSpecialFrameDomainBackendOutcome_t backendOutcome[ + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ]; + classicSpecialFrameDomainFailure_t backendFailure[ + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ]; + int backendFailureDetail[ + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ]; + int backendCoverage[ + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ]; +} classicSpecialFrameDomainView_t; + +typedef struct classicSpecialFrameDomainBackendCoverage_s { + int ownedViews; + int fallbackViews; + int ownedSurfaces; + int fallbackSurfaces; + int coverageMismatches; + int duplicateReports; +} classicSpecialFrameDomainBackendCoverage_t; + +typedef struct classicSpecialFrameDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int views; + int renderDemoViews; + int ravenEffectsViews; + int readyViews; + int fallbackViews; + int specialEffectsMask; + int failureCounts[ CLASSIC_SPECIAL_FRAME_FAILURE_COUNT ]; + std::uint64_t hash; + classicSpecialFrameDomainBackendCoverage_t backend[ + CLASSIC_SPECIAL_FRAME_BACKEND_COUNT ]; + char status[ 96 ]; +} classicSpecialFrameDomainStats_t; + +void R_ClassicSpecialFrameDomain_ResetFrame( void ); +void R_ClassicSpecialFrameDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicSpecialFrameDomainStats_t &R_ClassicSpecialFrameDomain_Stats( void ); +const classicSpecialFrameDomainView_t *R_ClassicSpecialFrameDomain_FindRenderDemoView( + const viewDef_t *viewDef ); +const classicSpecialFrameDomainView_t *R_ClassicSpecialFrameDomain_FindRavenEffectsView( + const viewDef_t *viewDef ); +bool R_ClassicSpecialFrameDomain_ReadyForBackend( const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend ); +bool R_ClassicSpecialFrameDomain_RecordOwned( const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend, int coverage ); +void R_ClassicSpecialFrameDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicSpecialFrameDomainScope_t scope, + classicSpecialFrameDomainBackend_t backend, + classicSpecialFrameDomainFailure_t failure, int detail ); +void R_ClassicSpecialFrameDomain_FinalizeBackendFrame( + classicSpecialFrameDomainBackend_t backend ); +const char *ClassicSpecialFrameDomainFailure_Name( + classicSpecialFrameDomainFailure_t failure ); +bool RendererClassicSpecialFrameDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_SPECIAL_FRAME_DOMAIN_H__ */ diff --git a/src/renderer/ClassicSubviewDomain.cpp b/src/renderer/ClassicSubviewDomain.cpp new file mode 100644 index 00000000..09f058fb --- /dev/null +++ b/src/renderer/ClassicSubviewDomain.cpp @@ -0,0 +1,1334 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ClassicSubviewDomain.h" +#include "ClassicCinematicPostDomain.h" + +#include + +static_assert( static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ) + == static_cast( CLASSIC_CINEMATIC_POST_BACKEND_GL ), + "classic subview and cinematic/post GL backend ordinals must match" ); +static_assert( static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ) + == static_cast( CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ), + "classic subview and cinematic/post Vulkan backend ordinals must match" ); +static_assert( static_cast( CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT ) + == static_cast( CLASSIC_CINEMATIC_POST_BACKEND_COUNT ), + "classic subview and cinematic/post backend counts must match" ); + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; + +typedef struct classicSubviewDomainState_s { + classicSubviewDomainView_t views[CLASSIC_SUBVIEW_DOMAIN_MAX_VIEWS]; + classicSubviewDomainStats_t stats; + int viewCount; +} classicSubviewDomainState_t; + +classicSubviewDomainState_t domain; + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + for ( int i = 0; i < 4; ++i ) { + HashByte( hash, value >> ( i * 8 ) ); + } +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static bool SameFloat( float a, float b ) { + std::uint32_t aBits = 0; + std::uint32_t bBits = 0; + std::memcpy( &aBits, &a, sizeof( aBits ) ); + std::memcpy( &bBits, &b, sizeof( bBits ) ); + return aBits == bBits; +} + +static void HashVec3( std::uint64_t &hash, const idVec3 &value ) { + for ( int component = 0; component < 3; ++component ) { + HashFloat( hash, value[component] ); + } +} + +static bool SameVec3( const idVec3 &a, const idVec3 &b ) { + for ( int component = 0; component < 3; ++component ) { + if ( !SameFloat( a[component], b[component] ) ) { + return false; + } + } + return true; +} + +static void HashPlane( std::uint64_t &hash, const idPlane &value ) { + for ( int component = 0; component < 4; ++component ) { + HashFloat( hash, value[component] ); + } +} + +static bool SamePlane( const idPlane &a, const idPlane &b ) { + for ( int component = 0; component < 4; ++component ) { + if ( !SameFloat( a[component], b[component] ) ) { + return false; + } + } + return true; +} + +static void HashScreenRect( std::uint64_t &hash, const idScreenRect &value ) { + HashInt( hash, value.x1 ); + HashInt( hash, value.y1 ); + HashInt( hash, value.x2 ); + HashInt( hash, value.y2 ); +} + +static bool SameScreenRect( const idScreenRect &a, const idScreenRect &b ) { + return a.x1 == b.x1 && a.y1 == b.y1 && a.x2 == b.x2 && a.y2 == b.y2; +} + +static void InitView( classicSubviewDomainView_t &view, + const viewDef_t *viewDef, int scenePacketIndex ) { + std::memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scenePacketIndex = scenePacketIndex; + view.parentScenePacketIndex = -1; + view.parentViewIndex = -1; + view.rootViewIndex = -1; + view.nestingDepth = 0; + view.subtreeViewCount = 1; + view.capturePacketIndex = -1; + view.firstPassPacket = -1; + view.firstDrawPacket = -1; + view.captureTextureType = TT_DISABLED; + view.captureTextureFormat = FMT_NONE; + view.captureType = CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE; + view.failure = CLASSIC_SUBVIEW_DOMAIN_FAILURE_NONE; +} + +static void CaptureViewSemantics( classicSubviewDomainView_t &view, + const viewDef_t &viewDef ) { + view.semanticViewID = viewDef.renderView.viewID; + view.semanticRenderTime = viewDef.renderView.time; + view.semanticFloatTime = viewDef.floatTime; + view.semanticViewOrigin = viewDef.renderView.vieworg; + view.semanticViewAxis = viewDef.renderView.viewaxis; + view.semanticInitialViewAreaOrigin = viewDef.initialViewAreaOrigin; + view.semanticViewport = viewDef.viewport; + view.semanticScissor = viewDef.scissor; + view.semanticClipPlaneCount = viewDef.numClipPlanes; + view.semanticIsMirror = viewDef.isMirror; + view.semanticIsXraySubview = viewDef.isXraySubview; + for ( int plane = 0; plane < MAX_CLIP_PLANES; ++plane ) { + view.semanticClipPlanes[plane] = viewDef.clipPlanes[plane]; + } +} + +static bool ViewSemanticsMatch( const classicSubviewDomainView_t &view, + const viewDef_t &viewDef ) { + if ( view.semanticViewID != viewDef.renderView.viewID + || view.semanticRenderTime != viewDef.renderView.time + || !SameFloat( view.semanticFloatTime, viewDef.floatTime ) + || !SameVec3( view.semanticViewOrigin, viewDef.renderView.vieworg ) + || !SameVec3( view.semanticInitialViewAreaOrigin, + viewDef.initialViewAreaOrigin ) + || !SameScreenRect( view.semanticViewport, viewDef.viewport ) + || !SameScreenRect( view.semanticScissor, viewDef.scissor ) + || view.semanticClipPlaneCount != viewDef.numClipPlanes + || view.semanticIsMirror != viewDef.isMirror + || view.semanticIsXraySubview != viewDef.isXraySubview ) { + return false; + } + for ( int axis = 0; axis < 3; ++axis ) { + if ( !SameVec3( view.semanticViewAxis[axis], + viewDef.renderView.viewaxis[axis] ) ) { + return false; + } + } + for ( int plane = 0; plane < MAX_CLIP_PLANES; ++plane ) { + if ( !SamePlane( view.semanticClipPlanes[plane], viewDef.clipPlanes[plane] ) ) { + return false; + } + } + return true; +} + +static int ViewIndex( const classicSubviewDomainView_t *view ) { + if ( view == NULL || view < domain.views + || view >= domain.views + domain.viewCount ) { + return -1; + } + return static_cast( view - domain.views ); +} + +static classicSubviewDomainView_t *FindMutableView( const viewDef_t *viewDef ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[i].viewDef == viewDef ) { + return &domain.views[i]; + } + } + return NULL; +} + +static int FindViewIndex( const viewDef_t *viewDef ) { + return ViewIndex( FindMutableView( viewDef ) ); +} + +static bool FailView( classicSubviewDomainView_t &view, + classicSubviewDomainFailure_t failure, int detail ) { + if ( !view.ready && view.failure != CLASSIC_SUBVIEW_DOMAIN_FAILURE_NONE ) { + return false; + } + view.ready = false; + view.failure = failure; + view.failureDetail = detail; + view.hash = 0; + domain.stats.fallbackViews++; + if ( failure >= CLASSIC_SUBVIEW_DOMAIN_FAILURE_NONE + && failure < CLASSIC_SUBVIEW_DOMAIN_FAILURE_COUNT ) { + domain.stats.failureCounts[failure]++; + } + return false; +} + +static int FindScenePacket( const idScenePacketFrame &packetFrame, + const viewDef_t *viewDef ) { + for ( int i = 0; i < packetFrame.NumScenes(); ++i ) { + if ( packetFrame.Scene(i).viewDef == viewDef ) { + return i; + } + } + return -1; +} + +static bool ParentContainsSurface( const viewDef_t *parent, + const drawSurf_t *surface ) { + if ( parent == NULL || surface == NULL || parent->drawSurfs == NULL + || parent->numDrawSurfs <= 0 ) { + return false; + } + for ( int i = 0; i < parent->numDrawSurfs; ++i ) { + if ( parent->drawSurfs[i] == surface ) { + return true; + } + } + return false; +} + +static bool FindDirectKind( const drawSurf_t *surface, + classicSubviewDomainKind_t &kind ) { + kind = CLASSIC_SUBVIEW_DOMAIN_KIND_NONE; + if ( surface == NULL || surface->material == NULL + || !surface->material->HasSubview() + || surface->material->GetSort() != SS_SUBVIEW ) { + return false; + } + // SS_SUBVIEW is the classic direct mirror path: it publishes its child view + // straight into the parent target and deliberately has no copy command. + kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + return true; +} + +static bool FindCaptureKind( const drawSurf_t *surface, + const idImage *captureImage, classicSubviewDomainKind_t &kind ) { + kind = CLASSIC_SUBVIEW_DOMAIN_KIND_NONE; + if ( surface == NULL || surface->material == NULL || captureImage == NULL + || !surface->material->HasSubview() ) { + return false; + } + int matchingStages = 0; + const int stageCount = surface->material->GetNumStages(); + for ( int i = 0; i < stageCount; ++i ) { + const shaderStage_t *stage = surface->material->GetStage(i); + if ( stage == NULL ) { + return false; + } + classicSubviewDomainKind_t stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_NONE; + switch ( stage->texture.dynamic ) { + case DI_REMOTE_RENDER: + stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA; + break; + case DI_MIRROR_RENDER: + stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR; + break; + case DI_REFLECTION_RENDER: + stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION; + break; + case DI_REFRACTION_RENDER: + stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION; + break; + case DI_XRAY_RENDER: + stageKind = CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY; + break; + case DI_STATIC: + continue; + default: + // Other dynamic images are not subview capture producers. + return false; + } + if ( stage->texture.image != captureImage || matchingStages != 0 ) { + return false; + } + kind = stageKind; + matchingStages++; + } + return matchingStages == 1; +} + +static bool ClassifyCaptureTargetOptions( textureType_t textureType, + textureFormat_t textureFormat, int cubeFace, bool copyDepth, + classicSubviewDomainCaptureType_t &type ) { + // The packet must retain both transfer aspect and layer identity exactly. + type = CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE; + const bool isCube = textureType == TT_CUBIC; + if ( ( textureType != TT_2D && !isCube ) + || ( isCube && ( cubeFace < 0 || cubeFace >= 6 ) ) + || ( !isCube && cubeFace != 0 ) ) { + return false; + } + const bool isDepth = textureFormat == FMT_DEPTH + || textureFormat == FMT_DEPTH_STENCIL; + if ( copyDepth != isDepth ) { + return false; + } + if ( copyDepth ) { + type = isCube ? CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP + : CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D; + } else { + type = isCube ? CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP + : CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D; + } + return true; +} + +static bool ClassifyCaptureTarget( const sceneSubviewCapture_t &capture, + classicSubviewDomainCaptureType_t &type ) { + if ( capture.image == NULL || capture.width <= 0 || capture.height <= 0 ) { + type = CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE; + return false; + } + const idImageOpts &opts = capture.image->GetOpts(); + return ClassifyCaptureTargetOptions( opts.textureType, opts.format, + capture.cubeFace, capture.copyDepth, type ); +} + +static bool HasSupportedSpecialSemantics( const viewDef_t &viewDef, + classicSubviewDomainKind_t kind ) { + if ( viewDef.numClipPlanes < 0 || viewDef.numClipPlanes > MAX_CLIP_PLANES ) { + return false; + } + switch ( kind ) { + case CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR: + case CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR: + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION: + // The final cull parity may be false for a nested mirror, so the + // captured isMirror bit is authoritative rather than an eligibility + // requirement. The single portal plane is the required camera boundary. + return !viewDef.isXraySubview && viewDef.numClipPlanes == 1; + case CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY: + return viewDef.isXraySubview && viewDef.numClipPlanes == 0; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA: + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION: + return !viewDef.isMirror && !viewDef.isXraySubview + && viewDef.numClipPlanes == 0; + case CLASSIC_SUBVIEW_DOMAIN_KIND_NONE: + case CLASSIC_SUBVIEW_DOMAIN_KIND_COUNT: + default: + return false; + } +} + +static std::uint64_t HashView( const classicSubviewDomainView_t &view ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, view.scenePacketIndex ); + HashInt( hash, view.parentScenePacketIndex ); + HashInt( hash, view.parentViewIndex ); + HashInt( hash, view.rootViewIndex ); + HashInt( hash, view.nestingDepth ); + HashInt( hash, view.subtreeViewCount ); + HashInt( hash, view.capturePacketIndex ); + HashInt( hash, view.firstPassPacket ); + HashInt( hash, view.passPacketCount ); + HashInt( hash, view.firstDrawPacket ); + HashInt( hash, view.drawPacketCount ); + HashInt( hash, view.captureX ); + HashInt( hash, view.captureY ); + HashInt( hash, view.captureWidth ); + HashInt( hash, view.captureHeight ); + HashInt( hash, view.captureCubeFace ); + HashInt( hash, view.captureTextureType ); + HashInt( hash, view.captureTextureFormat ); + HashBool( hash, view.captureCopyDepth ); + HashInt( hash, view.kind ); + HashInt( hash, view.captureType ); + HashInt( hash, view.semanticViewID ); + HashInt( hash, view.semanticRenderTime ); + HashFloat( hash, view.semanticFloatTime ); + HashVec3( hash, view.semanticViewOrigin ); + for ( int axis = 0; axis < 3; ++axis ) { + HashVec3( hash, view.semanticViewAxis[axis] ); + } + HashVec3( hash, view.semanticInitialViewAreaOrigin ); + HashScreenRect( hash, view.semanticViewport ); + HashScreenRect( hash, view.semanticScissor ); + HashInt( hash, view.semanticClipPlaneCount ); + HashBool( hash, view.semanticIsMirror ); + HashBool( hash, view.semanticIsXraySubview ); + for ( int plane = 0; plane < MAX_CLIP_PLANES; ++plane ) { + HashPlane( hash, view.semanticClipPlanes[plane] ); + } + if ( view.viewDef != NULL ) { + HashInt( hash, view.viewDef->viewport.x1 ); + HashInt( hash, view.viewDef->viewport.y1 ); + HashInt( hash, view.viewDef->viewport.x2 ); + HashInt( hash, view.viewDef->viewport.y2 ); + HashInt( hash, view.viewDef->scissor.x1 ); + HashInt( hash, view.viewDef->scissor.y1 ); + HashInt( hash, view.viewDef->scissor.x2 ); + HashInt( hash, view.viewDef->scissor.y2 ); + HashBool( hash, view.viewDef->isSubview ); + HashBool( hash, view.viewDef->isMirror ); + HashBool( hash, view.viewDef->isXraySubview ); + } + return hash; +} + +static bool PrepareView( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene, classicSubviewDomainView_t &view ) { + const viewDef_t *viewDef = view.viewDef; + view.parentViewDef = viewDef != NULL ? viewDef->superView : NULL; + view.parentSurface = viewDef != NULL ? viewDef->subviewSurface : NULL; + if ( viewDef == NULL || scene.packetCategory != SCENE_PACKET_CATEGORY_SUBVIEW + || !viewDef->isSubview || viewDef->viewEntitys == NULL + || viewDef->renderWorld == NULL || viewDef->isEditor + || viewDef->renderView.viewID < 0 + || viewDef->renderView.globalMaterial != NULL + || viewDef->superView == NULL || viewDef->subviewSurface == NULL ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_VIEW, 0 ); + } + view.parentScenePacketIndex = FindScenePacket( packetFrame, view.parentViewDef ); + if ( view.parentScenePacketIndex < 0 ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SCENE, 0 ); + } + if ( !ParentContainsSurface( view.parentViewDef, view.parentSurface ) ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SURFACE, 0 ); + } + classicSubviewDomainKind_t directKind = CLASSIC_SUBVIEW_DOMAIN_KIND_NONE; + const bool isDirect = FindDirectKind( view.parentSurface, directKind ); + int captureIndex = -1; + for ( int i = 0; i < packetFrame.NumSubviewCaptures(); ++i ) { + const sceneSubviewCapture_t &candidate = packetFrame.SubviewCapture(i); + if ( candidate.viewScenePacketIndex != view.scenePacketIndex ) { + continue; + } + if ( captureIndex >= 0 ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_DUPLICATE_CAPTURE, i ); + } + captureIndex = i; + } + if ( isDirect ) { + if ( captureIndex >= 0 ) { + return FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNEXPECTED_CAPTURE, captureIndex ); + } + view.kind = directKind; + } else { + if ( captureIndex < 0 ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_CAPTURE, 0 ); + } + const sceneSubviewCapture_t &capture = packetFrame.SubviewCapture(captureIndex); + if ( capture.viewDef != viewDef || capture.image == NULL + || capture.width <= 0 || capture.height <= 0 ) { + return FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_INVALID_CAPTURE, + captureIndex ); + } + if ( !ClassifyCaptureTarget( capture, view.captureType ) ) { + return FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_CAPTURE_TARGET, + captureIndex ); + } + view.captureImage = capture.image; + view.capturePacketIndex = captureIndex; + view.captureX = capture.x; + view.captureY = capture.y; + view.captureWidth = capture.width; + view.captureHeight = capture.height; + view.captureCubeFace = capture.cubeFace; + view.captureTextureType = capture.image->GetOpts().textureType; + view.captureTextureFormat = capture.image->GetOpts().format; + view.captureCopyDepth = capture.copyDepth; + if ( capture.x != viewDef->viewport.x1 || capture.y != viewDef->viewport.y1 + || capture.width != viewDef->viewport.x2 - viewDef->viewport.x1 + 1 + || capture.height != viewDef->viewport.y2 - viewDef->viewport.y1 + 1 ) { + return FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_VIEWPORT_MISMATCH, captureIndex ); + } + if ( !FindCaptureKind( view.parentSurface, capture.image, view.kind ) ) { + return FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_SURFACE_MISMATCH, captureIndex ); + } + } + if ( !HasSupportedSpecialSemantics( *viewDef, view.kind ) ) { + return FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_SPECIAL_SEMANTICS, + static_cast( view.kind ) ); + } + CaptureViewSemantics( view, *viewDef ); + view.firstPassPacket = scene.firstPassPacket; + view.passPacketCount = scene.passPacketCount; + view.firstDrawPacket = scene.firstDrawPacket; + view.drawPacketCount = scene.drawPacketCount; + view.ready = true; + return true; +} + +static void ResolveNestedOwnership( void ) { + // R_RenderView emits descendants before their parent command. Recover that + // order from the immutable scene packet indices and make every nested chain + // one transaction. A child must never be admitted independently merely + // because its local camera and capture edge happened to validate. + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &view = domain.views[viewIndex]; + view.parentViewIndex = -1; + view.rootViewIndex = viewIndex; + view.nestingDepth = 0; + view.subtreeViewCount = 1; + if ( view.parentViewDef == NULL || !view.parentViewDef->isSubview ) { + continue; + } + const int parentIndex = FindViewIndex( view.parentViewDef ); + view.parentViewIndex = parentIndex; + if ( parentIndex < 0 ) { + FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_NESTED_PARENT, + view.parentScenePacketIndex ); + continue; + } + const classicSubviewDomainView_t &parent = domain.views[parentIndex]; + if ( parent.scenePacketIndex != view.parentScenePacketIndex + || view.scenePacketIndex >= parent.scenePacketIndex ) { + FailView( view, CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_COMMAND_ORDER, + parent.scenePacketIndex ); + } + } + + // Resolve the root/depth only across strict depth-first links. The packet + // ordering check above bounds this walk by the fixed view arena and rejects + // malformed cycles before a backend can consume any record. + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &view = domain.views[viewIndex]; + if ( !view.ready ) { + continue; + } + int rootIndex = viewIndex; + int depth = 0; + for ( int parentIndex = view.parentViewIndex; parentIndex >= 0; + parentIndex = domain.views[parentIndex].parentViewIndex ) { + if ( parentIndex >= domain.viewCount || depth >= domain.viewCount ) { + FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_COMMAND_ORDER, parentIndex ); + break; + } + rootIndex = parentIndex; + ++depth; + } + if ( view.ready ) { + view.rootViewIndex = rootIndex; + view.nestingDepth = depth; + } + } + + // A rejected nested link invalidates its entire parent/child component. + // Iterate to cover a malformed sibling and every ancestor/descendant without + // relying on front-end allocation order beyond the validated packet edge. + for ( int pass = 0; pass < domain.viewCount; ++pass ) { + bool changed = false; + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &view = domain.views[viewIndex]; + if ( view.ready && view.parentViewIndex >= 0 + && !domain.views[view.parentViewIndex].ready ) { + FailView( view, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_PARENT_FALLBACK, + view.parentViewIndex ); + changed = true; + } + } + for ( int parentIndex = domain.viewCount - 1; parentIndex >= 0; + --parentIndex ) { + classicSubviewDomainView_t &parent = domain.views[parentIndex]; + if ( !parent.ready ) { + continue; + } + for ( int childIndex = 0; childIndex < domain.viewCount; ++childIndex ) { + if ( domain.views[childIndex].parentViewIndex == parentIndex + && !domain.views[childIndex].ready ) { + FailView( parent, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CHILD_FALLBACK, + childIndex ); + changed = true; + break; + } + } + } + if ( !changed ) { + break; + } + } + + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &view = domain.views[viewIndex]; + view.subtreeViewCount = view.ready ? 1 : 0; + } + // Descendants precede parents in an admitted chain, so this pass gives every + // root its complete sealed component size without dynamic allocation. + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + const classicSubviewDomainView_t &view = domain.views[viewIndex]; + if ( view.ready && view.parentViewIndex >= 0 + && domain.views[view.parentViewIndex].ready ) { + domain.views[view.parentViewIndex].subtreeViewCount += + view.subtreeViewCount; + } + } +} + +static int TransactionRootIndex( const classicSubviewDomainView_t *view ) { + const int viewIndex = ViewIndex( view ); + if ( viewIndex < 0 ) { + return -1; + } + return view->rootViewIndex >= 0 && view->rootViewIndex < domain.viewCount + ? view->rootViewIndex : viewIndex; +} + +static bool IsTransactionMember( const classicSubviewDomainView_t &view, + int rootIndex ) { + return rootIndex >= 0 && view.ready + && ( view.rootViewIndex == rootIndex + || ( view.rootViewIndex < 0 && ViewIndex( &view ) == rootIndex ) ); +} + +static void RecordSingleFallback( classicSubviewDomainView_t &view, + classicSubviewDomainBackend_t backend, classicSubviewDomainFailure_t failure, + int detail ) { + classicSubviewDomainBackendCoverage_t &coverage = domain.stats.backend[backend]; + if ( view.backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + return; + } + const int index = ViewIndex( &view ); + view.backendOutcome[backend] = CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK; + view.backendFailure[backend] = failure; + view.backendFailureDetail[backend] = detail; + if ( index >= 0 ) { + coverage.fallbackViewMask |= 1ull << index; + } + coverage.fallbackViews++; + if ( view.nestingDepth > 0 ) { + coverage.fallbackNestedViews++; + } +} + +static void RecordFallback( classicSubviewDomainView_t *view, + classicSubviewDomainBackend_t backend, classicSubviewDomainFailure_t failure, + int detail ) { + if ( backend < CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL + || backend >= CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT ) { + return; + } + classicSubviewDomainBackendCoverage_t &coverage = domain.stats.backend[backend]; + if ( view == NULL ) { + coverage.untrackedFallbacks++; + return; + } + if ( view->backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + const int rootIndex = TransactionRootIndex( view ); + const bool nestedTransaction = rootIndex >= 0 + && domain.views[rootIndex].subtreeViewCount > 1; + if ( nestedTransaction ) { + coverage.fallbackNestedTransactions++; + } + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &member = domain.views[viewIndex]; + if ( IsTransactionMember( member, rootIndex ) ) { + RecordSingleFallback( member, backend, failure, + viewIndex == ViewIndex( view ) ? detail : rootIndex ); + } + } + if ( rootIndex >= 0 ) { + R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback( + domain.views[rootIndex].viewDef, + static_cast( backend ), + CLASSIC_CINEMATIC_POST_FAILURE_SPECIAL_VIEW_TRANSACTION_REJECTED, + detail ); + } +} + +static void PublishOwned( classicSubviewDomainView_t &view, + classicSubviewDomainBackend_t backend ) { + const int index = ViewIndex( &view ); + classicSubviewDomainBackendCoverage_t &coverage = domain.stats.backend[backend]; + view.backendOutcome[backend] = CLASSIC_SUBVIEW_DOMAIN_BACKEND_OWNED; + if ( index >= 0 ) { + coverage.ownedViewMask |= 1ull << index; + } + coverage.ownedViews++; + if ( view.nestingDepth > 0 ) { + coverage.ownedNestedViews++; + } + if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR ) { + coverage.ownedDirectMirrorViews++; + } else if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA ) { + coverage.ownedRemoteCameraViews++; + } else if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR ) { + coverage.ownedMirrorViews++; + } else if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION ) { + coverage.ownedReflectionViews++; + } else if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION ) { + coverage.ownedRefractionViews++; + } else if ( view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY ) { + coverage.ownedXrayViews++; + } + switch ( view.captureType ) { + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP: + coverage.ownedColorCubemapCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D: + coverage.ownedDepth2DCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP: + coverage.ownedDepthCubemapCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE: + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D: + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COUNT: + default: + break; + } +} + +static bool FinalizeTransaction( classicSubviewDomainView_t &view, + classicSubviewDomainBackend_t backend ) { + const int rootIndex = TransactionRootIndex( &view ); + if ( rootIndex < 0 ) { + return false; + } + int memberCount = 0; + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + const classicSubviewDomainView_t &member = domain.views[viewIndex]; + if ( !IsTransactionMember( member, rootIndex ) ) { + continue; + } + ++memberCount; + if ( member.backendOutcome[backend] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK ) { + return false; + } + if ( !member.backendCompleted[backend] ) { + RecordFallback( &view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE, + viewIndex ); + return false; + } + } + if ( memberCount != domain.views[rootIndex].subtreeViewCount ) { + RecordFallback( &view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE, + memberCount ); + return false; + } + if ( !R_ClassicCinematicPostDomain_SubviewTransactionCompleted( + domain.views[rootIndex].viewDef, + static_cast( backend ) ) ) { + RecordFallback( &view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE, + rootIndex ); + return false; + } + if ( !R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned( + domain.views[rootIndex].viewDef, + static_cast( backend ) ) ) { + RecordFallback( &view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE, + rootIndex ); + return false; + } + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &member = domain.views[viewIndex]; + if ( IsTransactionMember( member, rootIndex ) ) { + PublishOwned( member, backend ); + } + } + if ( memberCount > 1 ) { + domain.stats.backend[backend].ownedNestedTransactions++; + } + return true; +} + +} // namespace + +void R_ClassicSubviewDomain_ResetFrame( void ) { + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.viewCount = 0; + idStr::Copynz( domain.stats.status, "empty", sizeof( domain.stats.status ) ); +} + +void R_ClassicSubviewDomain_PrepareFrame( const idScenePacketFrame &packetFrame ) { + R_ClassicSubviewDomain_ResetFrame(); + domain.stats.prepared = true; + domain.stats.sourceScenes = packetFrame.NumScenes(); + domain.stats.capturePackets = packetFrame.NumSubviewCaptures(); + domain.stats.overflow = packetFrame.Stats().overflow; + if ( packetFrame.Stats().overflow ) { + domain.stats.failureCounts[CLASSIC_SUBVIEW_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW]++; + } + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene(sceneIndex); + if ( scene.packetCategory != SCENE_PACKET_CATEGORY_SUBVIEW ) { + continue; + } + domain.stats.subviewScenes++; + if ( domain.viewCount >= CLASSIC_SUBVIEW_DOMAIN_MAX_VIEWS ) { + domain.stats.overflow = true; + domain.stats.fallbackViews++; + domain.stats.failureCounts[CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_POOL_OVERFLOW]++; + continue; + } + classicSubviewDomainView_t &view = domain.views[domain.viewCount++]; + InitView( view, scene.viewDef, sceneIndex ); + PrepareView( packetFrame, scene, view ); + } + ResolveNestedOwnership(); + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + classicSubviewDomainView_t &view = domain.views[viewIndex]; + if ( view.ready ) { + view.hash = HashView( view ); + domain.stats.readyViews++; + if ( view.nestingDepth > 0 ) { + domain.stats.nestedViews++; + domain.stats.maxNestingDepth = Max( domain.stats.maxNestingDepth, + view.nestingDepth ); + } + if ( view.rootViewIndex == viewIndex && view.subtreeViewCount > 1 ) { + domain.stats.nestedTransactions++; + } + switch ( view.kind ) { + case CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR: + domain.stats.directMirrorViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA: + domain.stats.remoteCameraViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR: + domain.stats.mirrorViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION: + domain.stats.reflectionViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION: + domain.stats.refractionViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY: + domain.stats.xrayViews++; + break; + case CLASSIC_SUBVIEW_DOMAIN_KIND_NONE: + case CLASSIC_SUBVIEW_DOMAIN_KIND_COUNT: + default: + break; + } + switch ( view.captureType ) { + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP: + domain.stats.colorCubemapCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D: + domain.stats.depth2DCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP: + domain.stats.depthCubemapCaptures++; + break; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE: + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D: + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COUNT: + default: + break; + } + } + } + domain.stats.frameValid = !domain.stats.overflow + && domain.stats.fallbackViews == 0; + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, domain.stats.subviewScenes ); + HashInt( hash, domain.stats.capturePackets ); + HashInt( hash, domain.stats.nestedViews ); + HashInt( hash, domain.stats.nestedTransactions ); + HashInt( hash, domain.stats.maxNestingDepth ); + for ( int i = 0; i < domain.viewCount; ++i ) { + HashBool( hash, domain.views[i].ready ); + HashInt( hash, domain.views[i].failure ); + HashInt( hash, domain.views[i].failureDetail ); + HashInt( hash, domain.views[i].kind ); + HashInt( hash, static_cast( domain.views[i].hash ) ); + HashInt( hash, static_cast( domain.views[i].hash >> 32 ) ); + } + domain.stats.hash = hash; + if ( domain.stats.subviewScenes == 0 ) { + idStr::Copynz( domain.stats.status, "empty", sizeof( domain.stats.status ) ); + } else if ( domain.stats.frameValid ) { + idStr::Copynz( domain.stats.status, "ready", sizeof( domain.stats.status ) ); + } else if ( domain.stats.readyViews == 0 ) { + idStr::Copynz( domain.stats.status, "fallback", sizeof( domain.stats.status ) ); + } else { + idStr::Copynz( domain.stats.status, "mixed-view-fallback", sizeof( domain.stats.status ) ); + } +} + +const classicSubviewDomainStats_t &R_ClassicSubviewDomain_Stats( void ) { + return domain.stats; +} + +int R_ClassicSubviewDomain_NumViews( void ) { + return domain.viewCount; +} + +const classicSubviewDomainView_t *R_ClassicSubviewDomain_ViewByIndex( int index ) { + return index >= 0 && index < domain.viewCount ? &domain.views[index] : NULL; +} + +const classicSubviewDomainView_t *R_ClassicSubviewDomain_FindView( + const viewDef_t *viewDef ) { + return FindMutableView(viewDef); +} + +bool R_ClassicSubviewDomain_IsCaptureBacked( + const classicSubviewDomainView_t &view ) { + return view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA + || view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR + || view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION + || view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION + || view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY; +} + +bool R_ClassicSubviewDomain_IsDirect( const classicSubviewDomainView_t &view ) { + return view.kind == CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; +} + +bool R_ClassicSubviewDomain_ViewSemanticsMatch( + const classicSubviewDomainView_t &view ) { + return view.ready && view.viewDef != NULL + && ViewSemanticsMatch( view, *view.viewDef ); +} + +bool R_ClassicSubviewDomain_ReadyForBackend( + const classicSubviewDomainView_t &view, + classicSubviewDomainBackend_t backend ) { + if ( backend < CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL + || backend >= CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT + || !domain.stats.prepared || domain.stats.overflow + || !view.ready + || view.backendCompleted[backend] + || view.backendOutcome[backend] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + || !R_ClassicSubviewDomain_ViewSemanticsMatch( view ) ) { + return false; + } + const int rootIndex = TransactionRootIndex( &view ); + for ( int viewIndex = 0; viewIndex < domain.viewCount; ++viewIndex ) { + const classicSubviewDomainView_t &member = domain.views[viewIndex]; + if ( !IsTransactionMember( member, rootIndex ) ) { + continue; + } + if ( member.backendOutcome[backend] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + || !R_ClassicSubviewDomain_ViewSemanticsMatch( member ) + || ( R_ClassicSubviewDomain_IsCaptureBacked( member ) + && ( member.captureImage == NULL + || !member.captureImage->IsLoaded() ) ) ) { + return false; + } + } + return R_ClassicCinematicPostDomain_SubviewTransactionReady( view.viewDef, + static_cast( backend ) ); +} + +bool R_ClassicSubviewDomain_CaptureMatches( + const classicSubviewDomainView_t &view, const idImage *image, + int x, int y, int width, int height, int cubeFace, bool copyDepth ) { + return R_ClassicSubviewDomain_IsCaptureBacked( view ) + && R_ClassicSubviewDomain_ViewSemanticsMatch( view ) + && image == view.captureImage && x == view.captureX + && y == view.captureY && width == view.captureWidth + && height == view.captureHeight && cubeFace == view.captureCubeFace + && copyDepth == view.captureCopyDepth; +} + +bool R_ClassicSubviewDomain_RecordOwned( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend, const idImage *image, + int x, int y, int width, int height, int cubeFace, bool copyDepth ) { + if ( backend < CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL + || backend >= CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT ) { + return false; + } + classicSubviewDomainView_t *view = FindMutableView(viewDef); + if ( view == NULL || !view->ready ) { + RecordFallback( view, backend, CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY, 0 ); + return false; + } + if ( view->backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + || view->backendCompleted[backend] ) { + domain.stats.backend[backend].duplicateReports++; + return view->backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK + && R_ClassicSubviewDomain_CaptureMatches( *view, image, x, y, width, + height, cubeFace, copyDepth ); + } + if ( !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) ) { + domain.stats.backend[backend].coverageMismatches++; + RecordFallback( view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH, 0 ); + return false; + } + if ( !R_ClassicSubviewDomain_CaptureMatches( *view, image, x, y, width, + height, cubeFace, copyDepth ) ) { + domain.stats.backend[backend].coverageMismatches++; + RecordFallback( view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, 0 ); + return false; + } + view->backendCompleted[backend] = true; + return TransactionRootIndex( view ) != ViewIndex( view ) + || FinalizeTransaction( *view, backend ); +} + +bool R_ClassicSubviewDomain_RecordDirectOwned( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend ) { + if ( backend < CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL + || backend >= CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT ) { + return false; + } + classicSubviewDomainView_t *view = FindMutableView(viewDef); + if ( view == NULL || !view->ready || !R_ClassicSubviewDomain_IsDirect( *view ) ) { + RecordFallback( view, backend, CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY, 0 ); + return false; + } + if ( view->backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + || view->backendCompleted[backend] ) { + domain.stats.backend[backend].duplicateReports++; + return view->backendOutcome[backend] != CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK + && R_ClassicSubviewDomain_ViewSemanticsMatch( *view ); + } + if ( !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) ) { + domain.stats.backend[backend].coverageMismatches++; + RecordFallback( view, backend, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH, 0 ); + return false; + } + view->backendCompleted[backend] = true; + return TransactionRootIndex( view ) != ViewIndex( view ) + || FinalizeTransaction( *view, backend ); +} + +void R_ClassicSubviewDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend, classicSubviewDomainFailure_t failure, + int detail ) { + RecordFallback( FindMutableView(viewDef), backend, failure, detail ); +} + +const classicSubviewDomainBackendCoverage_t & + R_ClassicSubviewDomain_BackendCoverage( classicSubviewDomainBackend_t backend ) { + static const classicSubviewDomainBackendCoverage_t empty = {}; + return backend >= CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL + && backend < CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT + ? domain.stats.backend[backend] : empty; +} + +const char *ClassicSubviewDomainKind_Name( classicSubviewDomainKind_t kind ) { + switch ( kind ) { + case CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR: return "directMirror"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA: return "remoteCamera"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR: return "mirror"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION: return "reflection"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION: return "refraction"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY: return "xray"; + case CLASSIC_SUBVIEW_DOMAIN_KIND_NONE: + case CLASSIC_SUBVIEW_DOMAIN_KIND_COUNT: + default: return "none"; + } +} + +const char *ClassicSubviewDomainCaptureType_Name( + classicSubviewDomainCaptureType_t type ) { + switch ( type ) { + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D: return "color2D"; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP: return "colorCubemap"; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D: return "depth2D"; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP: return "depthCubemap"; + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE: + case CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COUNT: + default: return "none"; + } +} + +const char *ClassicSubviewDomainFailure_Name( classicSubviewDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_NONE: return "none"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW: return "scenePacketOverflow"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_POOL_OVERFLOW: return "capturePoolOverflow"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_VIEW: return "unsupportedView"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SCENE: return "missingParentScene"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SURFACE: return "missingParentSurface"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_PARENT_MATERIAL: return "unsupportedParentMaterial"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_CAPTURE: return "missingCapture"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_DUPLICATE_CAPTURE: return "duplicateCapture"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_INVALID_CAPTURE: return "invalidCapture"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_SURFACE_MISMATCH: return "captureSurfaceMismatch"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_VIEWPORT_MISMATCH: return "captureViewportMismatch"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_CAPTURE_TARGET: return "unsupportedCaptureTarget"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNEXPECTED_CAPTURE: return "unexpectedCapture"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_SPECIAL_SEMANTICS: return "unsupportedSpecialSemantics"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH: return "viewSemanticsMismatch"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_NESTED_PARENT: return "missingNestedParent"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_COMMAND_ORDER: return "nestedCommandOrder"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_PARENT_FALLBACK: return "nestedParentFallback"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CHILD_FALLBACK: return "nestedChildFallback"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CINEMATIC_POST_FALLBACK: return "nestedCinematicPostFallback"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY: return "backendNotReady"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED: return "backendRejected"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH: return "backendCaptureMismatch"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE: return "backendNestingIncomplete"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE: return "backendNestedCinematicPostIncomplete"; + case CLASSIC_SUBVIEW_DOMAIN_FAILURE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicSubviewDomainBackend_Name( classicSubviewDomainBackend_t backend ) { + switch ( backend ) { + case CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL: return "GL"; + case CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN: return "Vulkan"; + case CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT: + default: return "unknown"; + } +} + +bool RendererClassicSubviewDomain_RunSelfTest( void ) { + R_ClassicCinematicPostDomain_ResetFrame(); + if ( CLASSIC_SUBVIEW_DOMAIN_MAX_VIEWS != SCENE_PACKET_MAX_SUBVIEW_CAPTURES + || idStr::Cmp( ClassicSubviewDomainKind_Name( + CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA ), "remoteCamera" ) != 0 + || idStr::Cmp( ClassicSubviewDomainKind_Name( + CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR ), "directMirror" ) != 0 + || idStr::Cmp( ClassicSubviewDomainCaptureType_Name( + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP ), + "depthCubemap" ) != 0 + || idStr::Cmp( ClassicSubviewDomainFailure_Name( + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_CAPTURE_TARGET ), + "unsupportedCaptureTarget" ) != 0 + || idStr::Cmp( ClassicSubviewDomainFailure_Name( + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE ), + "backendNestingIncomplete" ) != 0 + || idStr::Cmp( ClassicSubviewDomainFailure_Name( + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE ), + "backendNestedCinematicPostIncomplete" ) != 0 + || idStr::Cmp( ClassicSubviewDomainBackend_Name( + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ), "Vulkan" ) != 0 ) { + return false; + } + // Prepare a valid empty companion domain so standalone subview cases also + // exercise the joint-domain vacuous-success contract, regardless of the + // caller's live cinematic/post CVar state. + idScenePacketFrame emptyCinematicFrame; + emptyCinematicFrame.Clear(); + emptyCinematicFrame.MarkBackendDerived(); + R_ClassicCinematicPostDomain_PrepareFrame( emptyCinematicFrame ); + R_ClassicSubviewDomain_ResetFrame(); + viewDef_t viewDef; + std::memset( &viewDef, 0, sizeof(viewDef) ); + InitView( domain.views[0], &viewDef, 7 ); + domain.viewCount = 1; + classicSubviewDomainView_t &view = domain.views[0]; + view.ready = true; + view.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA; + view.captureImage = reinterpret_cast( static_cast(0x1000) ); + view.captureX = 3; + view.captureY = 4; + view.captureWidth = 64; + view.captureHeight = 32; + view.captureCubeFace = 5; + view.captureTextureType = TT_CUBIC; + view.captureTextureFormat = FMT_DEPTH; + view.captureCopyDepth = true; + view.captureType = CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP; + ResolveNestedOwnership(); + classicSubviewDomainCaptureType_t classifiedType = + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE; + const bool cubeDepthTarget = ClassifyCaptureTargetOptions( TT_CUBIC, + FMT_DEPTH, 5, true, classifiedType ) + && classifiedType == CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP; + const bool depthStencilTarget = ClassifyCaptureTargetOptions( TT_2D, + FMT_DEPTH_STENCIL, 0, true, classifiedType ) + && classifiedType == CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D; + const bool invalidTargetRejected = + !ClassifyCaptureTargetOptions( TT_CUBIC, FMT_DEPTH, 6, true, + classifiedType ) + && !ClassifyCaptureTargetOptions( TT_2D, FMT_RGBA8, 0, true, + classifiedType ); + const std::uint64_t firstHash = HashView(view); + view.captureWidth++; + const bool hashSensitive = HashView(view) != firstHash; + view.captureWidth--; + std::memset( &domain.stats, 0, sizeof(domain.stats) ); + const bool owned = R_ClassicSubviewDomain_RecordOwned( &viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, view.captureImage, 3, 4, 64, 32, + 5, true ); + const bool duplicate = R_ClassicSubviewDomain_RecordOwned( &viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, view.captureImage, 3, 4, 64, 32, + 5, true ); + const bool mismatch = !R_ClassicSubviewDomain_RecordOwned( &viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, view.captureImage, 3, 4, 63, 32, + 5, true ); + const bool capturePassed = cubeDepthTarget && depthStencilTarget + && invalidTargetRejected && hashSensitive && owned && duplicate && mismatch + && view.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_OWNED + && view.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL].ownedViews == 1 + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + .ownedDepthCubemapCaptures == 1 + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL].duplicateReports == 1 + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN].coverageMismatches == 1; + R_ClassicSubviewDomain_ResetFrame(); + std::memset( &viewDef, 0, sizeof(viewDef) ); + viewDef.isSubview = true; + viewDef.numClipPlanes = 1; + InitView( domain.views[0], &viewDef, 11 ); + domain.viewCount = 1; + classicSubviewDomainView_t &directView = domain.views[0]; + directView.ready = true; + directView.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + CaptureViewSemantics( directView, viewDef ); + ResolveNestedOwnership(); + const bool directOwned = R_ClassicSubviewDomain_RecordDirectOwned( &viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + viewDef.scissor.x1++; + const bool semanticMismatch = !R_ClassicSubviewDomain_ViewSemanticsMatch( + directView ); + + // A nested direct pair publishes neither member until the outer root returns. + // The second run injects a root failure after the child completes and proves + // that the pending child is rolled back with the same transaction. + R_ClassicSubviewDomain_ResetFrame(); + domain.stats.prepared = true; + viewDef_t nestedRootDef; + viewDef_t nestedChildDef; + std::memset( &nestedRootDef, 0, sizeof(nestedRootDef) ); + std::memset( &nestedChildDef, 0, sizeof(nestedChildDef) ); + nestedRootDef.isSubview = true; + nestedChildDef.isSubview = true; + nestedRootDef.numClipPlanes = 1; + nestedChildDef.numClipPlanes = 1; + nestedChildDef.superView = &nestedRootDef; + InitView( domain.views[0], &nestedChildDef, 20 ); + InitView( domain.views[1], &nestedRootDef, 21 ); + domain.viewCount = 2; + classicSubviewDomainView_t &nestedChild = domain.views[0]; + classicSubviewDomainView_t &nestedRoot = domain.views[1]; + nestedChild.ready = true; + nestedChild.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + nestedChild.parentViewDef = &nestedRootDef; + nestedChild.parentScenePacketIndex = 21; + CaptureViewSemantics( nestedChild, nestedChildDef ); + nestedRoot.ready = true; + nestedRoot.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + CaptureViewSemantics( nestedRoot, nestedRootDef ); + ResolveNestedOwnership(); + const bool nestedTopology = nestedChild.ready && nestedRoot.ready + && nestedChild.parentViewIndex == 1 + && nestedChild.rootViewIndex == 1 + && nestedChild.nestingDepth == 1 + && nestedRoot.rootViewIndex == 1 + && nestedRoot.subtreeViewCount == 2; + const bool nestedChildReadyInitially = R_ClassicSubviewDomain_ReadyForBackend( + nestedChild, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + const bool nestedChildCompleted = R_ClassicSubviewDomain_RecordDirectOwned( + &nestedChildDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + const bool nestedDeferred = nestedChild.backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED; + const bool nestedChildNotReadyTwice = !R_ClassicSubviewDomain_ReadyForBackend( + nestedChild, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + const bool nestedChildDuplicate = R_ClassicSubviewDomain_RecordDirectOwned( + &nestedChildDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + const bool nestedDuplicateSealed = nestedChildDuplicate + && nestedChild.backendCompleted[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + && nestedChild.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + .duplicateReports == 1; + const bool nestedRootOwned = R_ClassicSubviewDomain_RecordDirectOwned( + &nestedRootDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ); + const bool nestedPublished = nestedRootOwned + && nestedChild.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_OWNED + && nestedRoot.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_OWNED + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + .ownedNestedViews == 1 + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + .ownedNestedTransactions == 1; + + R_ClassicSubviewDomain_ResetFrame(); + InitView( domain.views[0], &nestedChildDef, 20 ); + InitView( domain.views[1], &nestedRootDef, 21 ); + domain.viewCount = 2; + classicSubviewDomainView_t &rejectedChild = domain.views[0]; + classicSubviewDomainView_t &rejectedRoot = domain.views[1]; + rejectedChild.ready = true; + rejectedChild.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + rejectedChild.parentViewDef = &nestedRootDef; + rejectedChild.parentScenePacketIndex = 21; + CaptureViewSemantics( rejectedChild, nestedChildDef ); + rejectedRoot.ready = true; + rejectedRoot.kind = CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR; + CaptureViewSemantics( rejectedRoot, nestedRootDef ); + ResolveNestedOwnership(); + const bool rejectedChildCompleted = R_ClassicSubviewDomain_RecordDirectOwned( + &nestedChildDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ); + R_ClassicSubviewDomain_RecordBackendFallback( &nestedRootDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED, 9 ); + const bool nestedRollback = rejectedChildCompleted + && rejectedChild.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK + && rejectedRoot.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + .fallbackNestedViews == 1 + && domain.stats.backend[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + .fallbackNestedTransactions == 1; + const bool passed = capturePassed && directOwned && semanticMismatch + && nestedTopology && nestedChildReadyInitially + && nestedChildCompleted && nestedDeferred + && nestedChildNotReadyTwice && nestedDuplicateSealed && nestedPublished + && nestedRollback; + R_ClassicSubviewDomain_ResetFrame(); + R_ClassicCinematicPostDomain_ResetFrame(); + return passed; +} diff --git a/src/renderer/ClassicSubviewDomain.h b/src/renderer/ClassicSubviewDomain.h new file mode 100644 index 00000000..44baf4ad --- /dev/null +++ b/src/renderer/ClassicSubviewDomain.h @@ -0,0 +1,233 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_SUBVIEW_DOMAIN_H__ +#define __CLASSIC_SUBVIEW_DOMAIN_H__ + +#include "ImageOpts.h" +#include "ScenePackets.h" + +/* +=============================================================================== + + Shared special-subview transaction. + + A view is published only after its parent source surface, child scene packet, + and exact RC_COPY_RENDER destination agree. Direct SS_SUBVIEW mirrors seal + their complete camera/clip/scissor semantics and have no capture command; + dynamic remote, mirror, reflection, refraction, and x-ray surfaces retain an + exact color, cubemap-face, or depth-capture edge. A nested child also seals + its direct parent domain record, root transaction, depth, and depth-first + command order. Every capture target seals its image type, depth/color aspect, + and exact cube face before either backend uses its established transfer + implementation. Nested transaction ownership is published only after the + outermost special view completes every admitted descendant. + +=============================================================================== +*/ + +const int CLASSIC_SUBVIEW_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SUBVIEW_CAPTURES; + +enum classicSubviewDomainKind_t { + CLASSIC_SUBVIEW_DOMAIN_KIND_NONE = 0, + CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR, + CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA, + CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR, + CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION, + CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION, + CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY, + CLASSIC_SUBVIEW_DOMAIN_KIND_COUNT +}; + +enum classicSubviewDomainCaptureType_t { + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_NONE = 0, + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D, + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP, + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D, + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP, + CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COUNT +}; + +enum classicSubviewDomainFailure_t { + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NONE = 0, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNAVAILABLE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_POOL_OVERFLOW, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SCENE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_PARENT_SURFACE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_PARENT_MATERIAL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_CAPTURE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_DUPLICATE_CAPTURE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_INVALID_CAPTURE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_SURFACE_MISMATCH, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_VIEWPORT_MISMATCH, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_CAPTURE_TARGET, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNEXPECTED_CAPTURE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_SPECIAL_SEMANTICS, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_NESTED_PARENT, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_COMMAND_ORDER, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_PARENT_FALLBACK, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CHILD_FALLBACK, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CINEMATIC_POST_FALLBACK, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_COUNT +}; + +enum classicSubviewDomainBackend_t { + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL = 0, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT +}; + +enum classicSubviewDomainBackendOutcome_t { + CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED = 0, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_OWNED, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_FALLBACK +}; + +typedef struct classicSubviewDomainBackendCoverage_s { + std::uint64_t ownedViewMask; + std::uint64_t fallbackViewMask; + int ownedViews; + int fallbackViews; + int ownedDirectMirrorViews; + int ownedRemoteCameraViews; + int ownedMirrorViews; + int ownedReflectionViews; + int ownedRefractionViews; + int ownedXrayViews; + int ownedColorCubemapCaptures; + int ownedDepth2DCaptures; + int ownedDepthCubemapCaptures; + int ownedNestedViews; + int ownedNestedTransactions; + int fallbackNestedViews; + int fallbackNestedTransactions; + int coverageMismatches; + int duplicateReports; + int untrackedFallbacks; +} classicSubviewDomainBackendCoverage_t; + +typedef struct classicSubviewDomainView_s { + const viewDef_t *viewDef; + const viewDef_t *parentViewDef; + const drawSurf_t *parentSurface; + idImage *captureImage; + int scenePacketIndex; + int parentScenePacketIndex; + int parentViewIndex; + int rootViewIndex; + int nestingDepth; + int subtreeViewCount; + int capturePacketIndex; + int firstPassPacket; + int passPacketCount; + int firstDrawPacket; + int drawPacketCount; + int captureX; + int captureY; + int captureWidth; + int captureHeight; + int captureCubeFace; + textureType_t captureTextureType; + textureFormat_t captureTextureFormat; + bool captureCopyDepth; + int semanticViewID; + int semanticRenderTime; + float semanticFloatTime; + idVec3 semanticViewOrigin; + idMat3 semanticViewAxis; + idVec3 semanticInitialViewAreaOrigin; + idScreenRect semanticViewport; + idScreenRect semanticScissor; + idPlane semanticClipPlanes[MAX_CLIP_PLANES]; + int semanticClipPlaneCount; + bool semanticIsMirror; + bool semanticIsXraySubview; + classicSubviewDomainKind_t kind; + classicSubviewDomainCaptureType_t captureType; + bool ready; + classicSubviewDomainFailure_t failure; + int failureDetail; + std::uint64_t hash; + classicSubviewDomainBackendOutcome_t backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT]; + classicSubviewDomainFailure_t backendFailure[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT]; + int backendFailureDetail[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT]; + bool backendCompleted[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT]; +} classicSubviewDomainView_t; + +typedef struct classicSubviewDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int subviewScenes; + int capturePackets; + int readyViews; + int fallbackViews; + int directMirrorViews; + int remoteCameraViews; + int mirrorViews; + int reflectionViews; + int refractionViews; + int xrayViews; + int colorCubemapCaptures; + int depth2DCaptures; + int depthCubemapCaptures; + int nestedViews; + int nestedTransactions; + int maxNestingDepth; + int failureCounts[CLASSIC_SUBVIEW_DOMAIN_FAILURE_COUNT]; + std::uint64_t hash; + classicSubviewDomainBackendCoverage_t backend[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT]; + char status[96]; +} classicSubviewDomainStats_t; + +void R_ClassicSubviewDomain_ResetFrame( void ); +void R_ClassicSubviewDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicSubviewDomainStats_t &R_ClassicSubviewDomain_Stats( void ); +int R_ClassicSubviewDomain_NumViews( void ); +const classicSubviewDomainView_t *R_ClassicSubviewDomain_ViewByIndex( int index ); +const classicSubviewDomainView_t *R_ClassicSubviewDomain_FindView( + const viewDef_t *viewDef ); +bool R_ClassicSubviewDomain_CaptureMatches( + const classicSubviewDomainView_t &view, const idImage *image, + int x, int y, int width, int height, int cubeFace, bool copyDepth ); +bool R_ClassicSubviewDomain_IsCaptureBacked( + const classicSubviewDomainView_t &view ); +bool R_ClassicSubviewDomain_IsDirect( + const classicSubviewDomainView_t &view ); +bool R_ClassicSubviewDomain_ViewSemanticsMatch( + const classicSubviewDomainView_t &view ); +bool R_ClassicSubviewDomain_ReadyForBackend( + const classicSubviewDomainView_t &view, + classicSubviewDomainBackend_t backend ); +bool R_ClassicSubviewDomain_RecordOwned( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend, const idImage *image, + int x, int y, int width, int height, int cubeFace, bool copyDepth ); +bool R_ClassicSubviewDomain_RecordDirectOwned( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend ); +void R_ClassicSubviewDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicSubviewDomainBackend_t backend, classicSubviewDomainFailure_t failure, + int detail ); +const classicSubviewDomainBackendCoverage_t & + R_ClassicSubviewDomain_BackendCoverage( classicSubviewDomainBackend_t backend ); +const char *ClassicSubviewDomainKind_Name( classicSubviewDomainKind_t kind ); +const char *ClassicSubviewDomainCaptureType_Name( + classicSubviewDomainCaptureType_t type ); +const char *ClassicSubviewDomainFailure_Name( classicSubviewDomainFailure_t failure ); +const char *ClassicSubviewDomainBackend_Name( classicSubviewDomainBackend_t backend ); +bool RendererClassicSubviewDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_SUBVIEW_DOMAIN_H__ */ diff --git a/src/renderer/ClassicWorldAmbientDomain.cpp b/src/renderer/ClassicWorldAmbientDomain.cpp new file mode 100644 index 00000000..42aa07ca --- /dev/null +++ b/src/renderer/ClassicWorldAmbientDomain.cpp @@ -0,0 +1,1621 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "CelShading.h" +#include "ClassicWorldAmbientDomain.h" + +#include + +namespace { + +const std::uint64_t HASH_OFFSET = 1469598103934665603ull; +const std::uint64_t HASH_PRIME = 1099511628211ull; + +typedef struct classicWorldAmbientDomainState_s { + classicWorldAmbientDomainView_t views[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_VIEWS ]; + classicWorldAmbientDomainDraw_t draws[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS ]; + rendererEvaluatedMaterialPass_t passes[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ]; + classicWorldAmbientDomainStats_t stats; + int viewCount; + int drawCount; + int passCount; +} classicWorldAmbientDomainState_t; + +classicWorldAmbientDomainState_t domain; + +static bool RangeFits( int first, int count, int capacity ) { + return first >= 0 && count >= 0 && first <= capacity && count <= capacity - first; +} + +static void HashByte( std::uint64_t &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= HASH_PRIME; +} + +static void HashU32( std::uint64_t &hash, std::uint32_t value ) { + for ( int i = 0; i < 4; ++i ) { + HashByte( hash, value >> ( i * 8 ) ); + } +} + +static void HashU64( std::uint64_t &hash, std::uint64_t value ) { + for ( int i = 0; i < 8; ++i ) { + HashByte( hash, static_cast( value >> ( i * 8 ) ) ); + } +} + +static void HashInt( std::uint64_t &hash, int value ) { + HashU32( hash, static_cast( value ) ); +} + +static void HashBool( std::uint64_t &hash, bool value ) { + HashByte( hash, value ? 1u : 0u ); +} + +static void HashFloat( std::uint64_t &hash, float value ) { + std::uint32_t bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), "float hash width mismatch" ); + std::memcpy( &bits, &value, sizeof( bits ) ); + HashU32( hash, bits ); +} + +static std::uint64_t HashPass( const rendererEvaluatedMaterialPass_t &pass ) { + std::uint64_t hash = HASH_OFFSET; + HashU32( hash, pass.order ); + HashInt( hash, pass.sourceStageIndex ); + HashInt( hash, pass.kind ); + HashInt( hash, pass.textureSemantic ); + // The table generation is deliberately excluded so identical contracts hash + // equally across frames. Low bits retain record/binding identity. + HashU32( hash, static_cast( pass.textureResourceId ) ); + HashFloat( hash, pass.condition ); + HashBool( hash, pass.active ); + HashInt( hash, pass.disposition ); + for ( int i = 0; i < 4; ++i ) { + HashFloat( hash, pass.color[ i ] ); + } + for ( int i = 0; i < 6; ++i ) { + HashFloat( hash, pass.textureMatrix[ i ] ); + } + HashBool( hash, pass.blend.enabled ); + HashInt( hash, pass.blend.sourceColor ); + HashInt( hash, pass.blend.destinationColor ); + HashInt( hash, pass.blend.colorOperation ); + HashInt( hash, pass.blend.sourceAlpha ); + HashInt( hash, pass.blend.destinationAlpha ); + HashInt( hash, pass.blend.alphaOperation ); + HashBool( hash, pass.depth.testEnabled ); + HashBool( hash, pass.depth.writeEnabled ); + HashInt( hash, pass.depth.compareOperation ); + HashInt( hash, pass.cull ); + HashU32( hash, pass.colorWriteMask ); + HashBool( hash, pass.alphaTestEnabled ); + HashInt( hash, pass.alphaTestCompareOperation ); + HashFloat( hash, pass.alphaTest ); + HashInt( hash, pass.texgen ); + HashInt( hash, pass.vertexColor ); + HashBool( hash, pass.polygonOffsetEnabled ); + HashFloat( hash, pass.polygonOffsetFactor ); + HashFloat( hash, pass.polygonOffsetUnits ); + HashInt( hash, pass.programFamily ); + HashU32( hash, pass.programKey ); + return hash; +} + +static std::uint64_t HashDraw( const classicWorldAmbientDomainDraw_t &draw ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, draw.sourceSurfaceIndex ); + HashInt( hash, draw.sourceSurface ); + HashInt( hash, draw.phase ); + HashBool( hash, draw.packetBacked ); + HashBool( hash, draw.depthPrerequisite ); + HashInt( hash, draw.materialId ); + HashInt( hash, draw.vertexCount ); + HashInt( hash, draw.firstIndex ); + HashInt( hash, draw.indexCount ); + HashInt( hash, draw.vertexOffset ); + HashInt( hash, draw.scissorX1 ); + HashInt( hash, draw.scissorY1 ); + HashInt( hash, draw.scissorX2 ); + HashInt( hash, draw.scissorY2 ); + HashInt( hash, draw.deformRole ); + HashInt( hash, draw.deformOutcome ); + HashU64( hash, draw.deformContractHash ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, draw.modelViewMatrix[ i ] ); + } + HashInt( hash, draw.evaluatedPassCount ); + for ( int i = 0; i < draw.evaluatedPassCount; ++i ) { + HashU64( hash, HashPass( domain.passes[ draw.firstEvaluatedPass + i ] ) ); + } + return hash; +} + +static std::uint64_t HashView( const classicWorldAmbientDomainView_t &view ) { + std::uint64_t hash = HASH_OFFSET; + HashInt( hash, view.sourceSurfaceCount ); + HashInt( hash, view.drawableSurfaceCount ); + HashInt( hash, view.noopSurfaceCount ); + HashInt( hash, view.packetDrawCount ); + HashInt( hash, view.depthPacketDrawCount ); + HashInt( hash, view.evaluatedPassCount ); + HashInt( hash, view.drawablePassCount ); + HashInt( hash, view.noopPassCount ); + HashInt( hash, view.materialDeformSurfaceCount ); + HashInt( hash, view.completedDeformSurfaceCount ); + HashInt( hash, view.emptyDeformSurfaceCount ); + for ( int phase = 0; phase < CLASSIC_WORLD_AMBIENT_PHASE_COUNT; ++phase ) { + HashInt( hash, view.phaseSurfaceCount[ phase ] ); + HashInt( hash, view.phaseDrawableSurfaceCount[ phase ] ); + HashInt( hash, view.phaseDrawablePassCount[ phase ] ); + HashInt( hash, view.phaseNoopPassCount[ phase ] ); + } + HashInt( hash, view.viewportX1 ); + HashInt( hash, view.viewportY1 ); + HashInt( hash, view.viewportX2 ); + HashInt( hash, view.viewportY2 ); + HashInt( hash, view.scissorX1 ); + HashInt( hash, view.scissorY1 ); + HashInt( hash, view.scissorX2 ); + HashInt( hash, view.scissorY2 ); + for ( int i = 0; i < 16; ++i ) { + HashFloat( hash, view.projectionMatrix[ i ] ); + } + for ( int i = 0; i < view.drawCount; ++i ) { + HashU64( hash, domain.draws[ view.firstDraw + i ].hash ); + } + return hash; +} + +static void InitDraw( classicWorldAmbientDomainDraw_t &draw ) { + std::memset( &draw, 0, sizeof( draw ) ); + draw.phase = CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG; + draw.drawPacketIndex = -1; + draw.depthDrawPacketIndex = -1; + draw.materialTableRecordIndex = -1; + draw.materialId = -1; + draw.firstWorldPass = -1; + draw.firstEvaluatedPass = -1; +} + +static void InitView( classicWorldAmbientDomainView_t &view, + const viewDef_t *viewDef, int scenePacketIndex ) { + std::memset( &view, 0, sizeof( view ) ); + view.viewDef = viewDef; + view.scenePacketIndex = scenePacketIndex; + view.ambientPassPacketIndex = -1; + view.depthPassPacketIndex = -1; + view.firstDraw = -1; + view.firstEvaluatedPass = -1; + view.failurePassPacketIndex = -1; + view.failureDrawPacketIndex = -1; + view.failureSourceSurfaceIndex = -1; + view.failureSourceStageIndex = -1; + view.worldPassFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE; + view.evaluationStatus = RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; + if ( viewDef != NULL ) { + view.viewportX1 = viewDef->viewport.x1; + view.viewportY1 = viewDef->viewport.y1; + view.viewportX2 = viewDef->viewport.x2; + view.viewportY2 = viewDef->viewport.y2; + view.scissorX1 = viewDef->scissor.x1; + view.scissorY1 = viewDef->scissor.y1; + view.scissorX2 = viewDef->scissor.x2; + view.scissorY2 = viewDef->scissor.y2; + std::memcpy( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ); + view.sourceSurfaceCount = viewDef->numDrawSurfs; + } +} + +static bool FailView( classicWorldAmbientDomainView_t &view, + classicWorldAmbientDomainFailure_t failure, int detail, + int sourceSurface = -1, int drawPacket = -1, int passPacket = -1, + int sourceStage = -1, + materialResourceWorldPassFailure_t worldFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE, + rendererMaterialPassEvaluationStatus_t evaluationStatus = RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + view.ready = false; + view.failure = failure; + view.failureDetail = detail; + view.worldPassFailure = worldFailure; + view.evaluationStatus = evaluationStatus; + view.failureSourceSurfaceIndex = sourceSurface; + view.failureDrawPacketIndex = drawPacket; + view.failurePassPacketIndex = passPacket; + view.failureSourceStageIndex = sourceStage; + view.firstDraw = -1; + view.drawCount = 0; + view.firstEvaluatedPass = -1; + view.evaluatedPassCount = 0; + view.activePassCount = 0; + view.drawablePassCount = 0; + view.inactivePassCount = 0; + view.activeNoopPassCount = 0; + view.noopPassCount = 0; + view.materialDeformSurfaceCount = 0; + view.completedDeformSurfaceCount = 0; + view.emptyDeformSurfaceCount = 0; + for ( int phase = 0; phase < CLASSIC_WORLD_AMBIENT_PHASE_COUNT; ++phase ) { + view.phaseSurfaceCount[ phase ] = 0; + view.phaseDrawableSurfaceCount[ phase ] = 0; + view.phaseDrawablePassCount[ phase ] = 0; + view.phaseNoopPassCount[ phase ] = 0; + } + view.hash = 0; + domain.stats.fallbackViews++; + if ( failure >= CLASSIC_WORLD_AMBIENT_FAILURE_NONE + && failure < CLASSIC_WORLD_AMBIENT_FAILURE_COUNT ) { + domain.stats.failureCounts[ failure ]++; + } + if ( failure == CLASSIC_WORLD_AMBIENT_FAILURE_SCENE_PACKET_OVERFLOW + || failure == CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_OVERFLOW + || failure == CLASSIC_WORLD_AMBIENT_FAILURE_VIEW_POOL_OVERFLOW + || failure == CLASSIC_WORLD_AMBIENT_FAILURE_DRAW_POOL_OVERFLOW + || failure == CLASSIC_WORLD_AMBIENT_FAILURE_EVALUATED_PASS_POOL_OVERFLOW ) { + domain.stats.overflow = true; + } + return false; +} + +static bool MaterialRequestsClassicDeform( const drawSurf_t *drawSurf ) { + return drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->Deform() != DFRM_NONE; +} + +static int DeformContractFailureDetail( const drawSurf_t *drawSurf ) { + if ( drawSurf == NULL ) { + return -1; + } + return static_cast( drawSurf->classicDeform.role ) + * CLASSIC_DEFORM_OUTCOME_COUNT + + static_cast( drawSurf->classicDeform.outcome ); +} + +static bool ValidateFinalizedDeformContract( const drawSurf_t *drawSurf, + const drawPacket_t *packet, classicDeformRole_t &role, + classicDeformOutcome_t &outcome, std::uint64_t &semanticHash ) { + role = CLASSIC_DEFORM_ROLE_UNKNOWN; + outcome = CLASSIC_DEFORM_OUTCOME_NONE; + semanticHash = 0; + if ( !MaterialRequestsClassicDeform( drawSurf ) ) { + return true; + } + if ( !r_rendererSharedDeform.GetBool() ) { + return false; + } + const std::uint64_t frameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + const classicDeformRecord_t &record = drawSurf->classicDeform; + if ( record.role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW + || !record.cpuFinalized + || !R_ClassicDeformDomain_ValidateRecordForFrame( record, + frameToken ) + || !R_ClassicDeformDomain_RecordMatchesDrawSurf( record, drawSurf ) + || ( !R_ClassicDeformDomain_HasCompletedOutput( record ) + && !R_ClassicDeformDomain_HasEmptyOutput( record ) ) ) { + return false; + } + if ( packet != NULL ) { + if ( !packet->hasClassicDeformRecord + || packet->classicDeformRecord == NULL + || packet->geometryRecord == NULL + || packet->classicDeformRecord + != &packet->geometryRecord->classicDeform + || !packet->geometryRecord->hasClassicDeformRecord + || !R_ClassicDeformDomain_ValidateRecordForFrame( + *packet->classicDeformRecord, frameToken ) + || !R_ClassicDeformDomain_SameProvenance( record, + *packet->classicDeformRecord ) ) { + return false; + } + } + role = record.role; + outcome = record.outcome; + semanticHash = record.semanticHash; + return true; +} + +static bool MatrixHasNegativeScale( const float matrix[ 16 ] ) { + const float determinant = + matrix[ 0 ] * ( matrix[ 5 ] * matrix[ 10 ] - matrix[ 9 ] * matrix[ 6 ] ) + - matrix[ 4 ] * ( matrix[ 1 ] * matrix[ 10 ] - matrix[ 9 ] * matrix[ 2 ] ) + + matrix[ 8 ] * ( matrix[ 1 ] * matrix[ 6 ] - matrix[ 5 ] * matrix[ 2 ] ); + return determinant < 0.0f; +} + +static classicWorldAmbientPhase_t PhaseForSurface( const drawSurf_t *drawSurf ) { + const idMaterial *material = drawSurf != NULL ? drawSurf->material : NULL; + return material != NULL && material->GetSort() >= SS_MEDIUM + ? CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG + : CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG; +} + +static bool SourceSurfaceIsFallback( + classicWorldAmbientDomainSourceSurface_t classification ) { + return classification >= CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NOT_DRAWN + && classification < CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_COUNT; +} + +static bool SourceSurfaceHasPlayerVisibilityWork( const drawSurf_t *drawSurf ) { + if ( r_skipPlayerVisibilityEffects.GetBool() || drawSurf == NULL + || drawSurf->space == NULL || drawSurf->space->entityDef == NULL + || drawSurf->geo == NULL || drawSurf->geo->numIndexes <= 0 + || drawSurf->material == NULL + || ( drawSurf->dsFlags & DSF_BSE_EFFECT ) != 0 + || drawSurf->space->weaponDepthHack + || drawSurf->space->modelDepthHack != 0.0f ) { + return false; + } + const idMaterial *material = drawSurf->material; + if ( !material->IsDrawn() || material->IsPortalSky() + || material->SuppressInSubview() + || material->GetSort() >= SS_POST_PROCESS || material->HasGui() ) { + return false; + } + const renderEntity_t &entity = drawSurf->space->entityDef->parms; + return entity.brightSkinColor[ 3 ] > 0.0f + || entity.rimlightColor[ 3 ] > 0.0f + || ( entity.outlineColor[ 3 ] > 0.0f && entity.outlineWidth > 0.0f ); +} + +static classicWorldAmbientDomainSourceSurface_t ClassifySourceSurface( + const viewDef_t *viewDef, const drawSurf_t *drawSurf, + bool &packetExpected, classicWorldAmbientPhase_t &phase ) { + packetExpected = false; + phase = PhaseForSurface( drawSurf ); + if ( drawSurf == NULL ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NULL_SURFACE; + } + if ( ( drawSurf->dsFlags & ( DSF_BSE_EFFECT | DSF_OUTLINE_ONLY ) ) != 0 + || drawSurf->dynamicTexCoords != NULL + || drawSurf->texGenTransformAndViewOrg != NULL ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SPECIAL_SURFACE; + } + if ( drawSurf->geo != NULL && drawSurf->geo->deformedSurface ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM; + } + // Viewmodel/synthetic GUI transforms are domain boundaries even when their + // material would otherwise be an ambient no-op. Admitting them as no-ops + // would make packet omission hide a view-level ownership blocker. + if ( drawSurf->space != NULL ) { + if ( drawSurf->space->weaponDepthHack + || drawSurf->space->modelDepthHack != 0.0f ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEPTH_HACK; + } + if ( MatrixHasNegativeScale( drawSurf->space->modelMatrix ) ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE; + } + if ( viewDef != NULL && drawSurf->space->entityDef == NULL + && drawSurf->space != &viewDef->worldSpace ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SYNTHETIC_GUI_SPACE; + } + } + const idMaterial *material = drawSurf->material; + if ( material == NULL ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_MISSING_MATERIAL; + } + if ( material->Deform() != DFRM_NONE + && !r_rendererSharedDeform.GetBool() ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM; + } + if ( material->GetSort() >= SS_POST_PROCESS ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_POST_PROCESS; + } + if ( material->IsPortalSky() ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PORTAL_SKY; + } + if ( material->SuppressInSubview() ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW; + } + if ( material->HasGui() || material->GetSort() == SS_GUI + || material->GetSort() == SS_PREGUI ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_IN_WORLD_GUI; + } + if ( material->HasSubview() || material->GetSort() == SS_SUBVIEW ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUBVIEW; + } + if ( drawSurf->decalColorCache != NULL || drawSurf->decalColorStageCount != 0 + || drawSurf->decalColorStride != 0 || drawSurf->decalColorOffset != 0 + || ( material->GetSort() >= SS_DECAL && material->GetSort() < SS_FAR ) ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DECAL; + } + if ( SourceSurfaceHasPlayerVisibilityWork( drawSurf ) ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PLAYER_VISIBILITY; + } + if ( !material->HasAmbient() ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NO_AMBIENT; + } + if ( !material->IsDrawn() ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NOT_DRAWN; + } + if ( drawSurf->geo == NULL || drawSurf->geo->numVerts <= 0 + || drawSurf->geo->numIndexes <= 0 ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY; + } + if ( drawSurf->space == NULL ) { + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_MISSING_SPACE; + } + packetExpected = true; + return CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE; +} + +static bool CountDisposition( classicWorldAmbientDomainDraw_t &draw, + const rendererEvaluatedMaterialPass_t &pass ) { + if ( pass.active ) { + draw.activePassCount++; + } + switch ( pass.disposition ) { + case RENDERER_MATERIAL_PASS_DRAW: + draw.drawablePassCount++; + return pass.active; + case RENDERER_MATERIAL_PASS_INACTIVE_CONDITION: + draw.inactivePassCount++; + draw.noopPassCount++; + return !pass.active; + case RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND: + case RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE: + case RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA: + draw.activeNoopPassCount++; + draw.noopPassCount++; + return pass.active; + default: + return false; + } +} + +static bool ValidateAmbientDrawPacket( const idScenePacketFrame &packetFrame, + int packetIndex, const viewDef_t *viewDef, const drawSurf_t *drawSurf, + classicWorldAmbientDomainFailure_t &failure ) { + if ( packetIndex < 0 || packetIndex >= packetFrame.NumDrawPackets() ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH; + return false; + } + const drawPacket_t &packet = packetFrame.DrawPacket( packetIndex ); + if ( packet.passCategory != RENDER_PASS_AMBIENT + || packet.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || packet.viewDef != viewDef || packet.legacyDrawSurf != drawSurf ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH; + return false; + } + if ( packet.geometryRecordIndex < 0 + || packet.geometryRecordIndex >= packetFrame.NumGeometryRecords() + || packet.geometryRecord == NULL + || packet.geometryRecord != &packetFrame.GeometryRecord( packet.geometryRecordIndex ) + || packet.geometryRecord->legacyGeometry != drawSurf->geo ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_GEOMETRY_RECORD; + return false; + } + if ( packet.instanceRecordIndex < 0 + || packet.instanceRecordIndex >= packetFrame.NumInstanceRecords() + || packet.instanceRecord == NULL + || packet.instanceRecord != &packetFrame.InstanceRecord( packet.instanceRecordIndex ) + || packet.instanceRecord->legacySpace != drawSurf->space ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_INSTANCE_RECORD; + return false; + } + if ( packet.materialRecordIndex < 0 + || packet.materialRecordIndex >= packetFrame.NumMaterialRecords() + || packet.materialRecord == NULL + || packet.materialRecord != &packetFrame.MaterialRecord( packet.materialRecordIndex ) + || packet.materialRecord->material != drawSurf->material ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_MATERIAL_RECORD; + return false; + } + const geometryResourceRecord_t &geometry = *packet.geometryRecord; + if ( !packet.hasGeometry || packet.vertexCount <= 0 || packet.indexCount <= 0 + || geometry.vertexCount != packet.vertexCount + || geometry.indexCount != packet.indexCount + || ( geometry.deformMode != GEOMETRY_DEFORM_NONE + && !( r_rendererSharedDeform.GetBool() + && MaterialRequestsClassicDeform( drawSurf ) ) ) + || geometry.legacyGeometry == NULL || geometry.legacyGeometry->ambientCache == NULL + || !packet.hasAmbientCache + || ( !packet.hasIndexCache && !geometry.hasClientIndexData ) ) { + failure = geometry.deformMode != GEOMETRY_DEFORM_NONE + && !( r_rendererSharedDeform.GetBool() + && MaterialRequestsClassicDeform( drawSurf ) ) + ? CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK + : CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_GEOMETRY_RECORD; + return false; + } + const instanceRecord_t &instance = *packet.instanceRecord; + if ( ( instance.visibilityFlags & INSTANCE_VISIBILITY_WORLD ) == 0 + || ( instance.visibilityFlags & ( INSTANCE_VISIBILITY_GUI + | INSTANCE_VISIBILITY_VIEWMODEL | INSTANCE_VISIBILITY_SUBVIEW + | INSTANCE_VISIBILITY_REMOTE_CAMERA | INSTANCE_VISIBILITY_RENDER_DEMO ) ) != 0 + || instance.weaponDepthHack || instance.modelDepthHack != 0.0f ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK; + return false; + } + if ( instance.negativeScale ) { + failure = CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK; + return false; + } + return true; +} + +static bool ValidateDepthPrerequisite( const idScenePacketFrame &packetFrame, + const passPacket_t *depthPass, const viewDef_t *viewDef, + const drawSurf_t *drawSurf, const drawPacket_t &ambientPacket, + int &depthPacketCursor, int &depthPacketIndex ) { + depthPacketIndex = -1; + if ( drawSurf == NULL || drawSurf->material == NULL + || drawSurf->material->Coverage() == MC_TRANSLUCENT ) { + return true; + } + if ( depthPass == NULL || !depthPass->enabled || depthPass->commandOnly + || !RangeFits( depthPass->firstDrawPacket, depthPass->drawPacketCount, + packetFrame.NumDrawPackets() ) ) { + return false; + } + const int depthEnd = depthPass->firstDrawPacket + depthPass->drawPacketCount; + if ( depthPacketCursor < depthPass->firstDrawPacket + || depthPacketCursor > depthEnd ) { + return false; + } + for ( int packetIndex = depthPacketCursor; + packetIndex < depthEnd; ++packetIndex ) { + const drawPacket_t &packet = packetFrame.DrawPacket( packetIndex ); + if ( packet.legacyDrawSurf != drawSurf ) { + continue; + } + if ( packet.passCategory != RENDER_PASS_DEPTH + || packet.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || packet.viewDef != viewDef || packet.materialRecordIndex != ambientPacket.materialRecordIndex + || packet.geometryRecordIndex != ambientPacket.geometryRecordIndex + || packet.instanceRecordIndex != ambientPacket.instanceRecordIndex + || packet.materialRecord != ambientPacket.materialRecord + || packet.geometryRecord != ambientPacket.geometryRecord + || packet.instanceRecord != ambientPacket.instanceRecord + || packet.vertexCount != ambientPacket.vertexCount + || packet.firstIndex != ambientPacket.firstIndex + || packet.indexCount != ambientPacket.indexCount + || packet.vertexOffset != ambientPacket.vertexOffset + || !packet.hasGeometry || !packet.hasAmbientCache + || ( !packet.hasIndexCache && !packet.geometryRecord->hasClientIndexData ) ) { + return false; + } + depthPacketIndex = packetIndex; + depthPacketCursor = packetIndex + 1; + return true; + } + return false; +} + +static bool ForbiddenPassCategory( renderPassCategory_t category ) { + switch ( category ) { + case RENDER_PASS_STENCIL_SHADOW: + case RENDER_PASS_SHADOW_MAP: + case RENDER_PASS_ARB2_INTERACTION: + case RENDER_PASS_LIGHT_GRID: + case RENDER_PASS_DEFERRED_RESOLVE: + case RENDER_PASS_FORWARD_PLUS: + case RENDER_PASS_FOG_BLEND: + case RENDER_PASS_SSAO: + case RENDER_PASS_MOTION_BLUR: + case RENDER_PASS_BLOOM: + case RENDER_PASS_AUTHORED_POST: + case RENDER_PASS_SPECIAL_EFFECTS: + case RENDER_PASS_GUI: + case RENDER_PASS_PRESENT: + return true; + case RENDER_PASS_DEPTH: + case RENDER_PASS_AMBIENT: + return false; + default: + return true; + } +} + +static bool R_ClassicWorldAmbientDomain_PrepareView( + const idScenePacketFrame &packetFrame, const scenePacket_t &scene, + classicWorldAmbientDomainView_t &view ) { + const scenePacketFrameStats_t &packetStats = packetFrame.Stats(); + const materialResourceTableStats_t &tableStats = R_MaterialResourceTable_Stats(); + if ( packetStats.overflow ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_SCENE_PACKET_OVERFLOW, + packetStats.overflowCause ); + } + if ( !tableStats.prepared ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_NOT_PREPARED, 0 ); + } + if ( !tableStats.available ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_UNAVAILABLE, 0 ); + } + if ( tableStats.overflow ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_OVERFLOW, 0 ); + } + + const viewDef_t *viewDef = view.viewDef; + const int allowedRenderFlags = RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef == NULL || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || viewDef->viewEntitys == NULL || viewDef->renderWorld == NULL + || viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 + || viewDef->renderView.globalMaterial != NULL + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 || r_skipAmbient.GetBool() ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_UNSUPPORTED_VIEW, + viewDef != NULL ? viewDef->renderFlags : 0 ); + } + if ( r_portalsDistanceCull.GetBool() ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT, 1 ); + } + if ( r_forceAmbient.GetFloat() > 0.0f ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT, 2 ); + } + if ( R_CelShadingAnyEnabled() ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT, 3 ); + } + if ( viewDef->numDrawSurfs < 0 + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_RANGE, + viewDef->numDrawSurfs ); + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) + || !RangeFits( scene.firstDrawPacket, scene.drawPacketCount, + packetFrame.NumDrawPackets() ) ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_SCENE_RANGE, + scene.passPacketCount ); + } + + const passPacket_t *ambientPass = NULL; + const passPacket_t *depthPass = NULL; + for ( int localPass = 0; localPass < scene.passPacketCount; ++localPass ) { + const int passPacketIndex = scene.firstPassPacket + localPass; + const passPacket_t &pass = packetFrame.Pass( passPacketIndex ); + if ( !RangeFits( pass.firstDrawPacket, pass.drawPacketCount, + packetFrame.NumDrawPackets() ) + || pass.firstDrawPacket < scene.firstDrawPacket + || pass.firstDrawPacket + > scene.firstDrawPacket + scene.drawPacketCount + || pass.drawPacketCount > scene.firstDrawPacket + + scene.drawPacketCount - pass.firstDrawPacket ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_RANGE, + pass.drawPacketCount, -1, -1, passPacketIndex ); + } + if ( pass.passCategory == RENDER_PASS_AMBIENT ) { + if ( ambientPass != NULL || !pass.enabled || pass.commandOnly + || ( pass.packetCategory != SCENE_PACKET_CATEGORY_WORLD + && !( pass.drawPacketCount == 0 + && pass.packetCategory + == SCENE_PACKET_CATEGORY_COMMAND ) ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_AMBIENT_PASS, + pass.passCategory, -1, -1, passPacketIndex ); + } + ambientPass = &pass; + view.ambientPassPacketIndex = passPacketIndex; + continue; + } + if ( pass.passCategory == RENDER_PASS_DEPTH ) { + if ( depthPass != NULL || !pass.enabled || pass.commandOnly + || ( pass.packetCategory != SCENE_PACKET_CATEGORY_WORLD + && !( pass.drawPacketCount == 0 + && pass.packetCategory + == SCENE_PACKET_CATEGORY_COMMAND ) ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING, + pass.passCategory, -1, -1, passPacketIndex ); + } + depthPass = &pass; + view.depthPassPacketIndex = passPacketIndex; + view.depthPacketDrawCount = pass.drawPacketCount; + continue; + } + const bool hasWork = pass.commandOnly || pass.drawPacketCount > 0; + if ( hasWork && ForbiddenPassCategory( pass.passCategory ) ) { + return FailView( view, CLASSIC_WORLD_AMBIENT_FAILURE_FORBIDDEN_PASS, + pass.passCategory, -1, -1, passPacketIndex ); + } + } + if ( ambientPass == NULL ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_AMBIENT_PASS, -1 ); + } + view.packetDrawCount = ambientPass->drawPacketCount; + if ( !RangeFits( domain.drawCount, viewDef->numDrawSurfs, + CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_DRAW_POOL_OVERFLOW, + viewDef->numDrawSurfs ); + } + + const int drawCheckpoint = domain.drawCount; + const int passCheckpoint = domain.passCount; + int stagedPassCount = 0; + int packetCursor = ambientPass->firstDrawPacket; + const int packetEnd = ambientPass->firstDrawPacket + + ambientPass->drawPacketCount; + int depthPacketCursor = depthPass != NULL + ? depthPass->firstDrawPacket : 0; + int drawableSurfaces = 0; + int noopSurfaces = 0; + + for ( int sourceIndex = 0; sourceIndex < viewDef->numDrawSurfs; + ++sourceIndex ) { + const drawSurf_t *source = viewDef->drawSurfs[ sourceIndex ]; + bool packetExpected = false; + classicWorldAmbientPhase_t phase = CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG; + const classicWorldAmbientDomainSourceSurface_t classification = + ClassifySourceSurface( viewDef, source, packetExpected, phase ); + classicWorldAmbientDomainDraw_t &draw = + domain.draws[ drawCheckpoint + sourceIndex ]; + InitDraw( draw ); + draw.sourceSurfaceIndex = sourceIndex; + draw.sourceSurface = classification; + draw.phase = phase; + draw.legacyDrawSurf = source; + + if ( SourceSurfaceIsFallback( classification ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK, + classification, sourceIndex ); + } + + const drawPacket_t *packet = NULL; + if ( packetExpected ) { + classicWorldAmbientDomainFailure_t packetFailure = + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_PACKET; + if ( packetCursor >= packetEnd + || !ValidateAmbientDrawPacket( packetFrame, packetCursor, + viewDef, source, packetFailure ) ) { + return FailView( view, packetFailure, packetCursor, + sourceIndex, packetCursor, view.ambientPassPacketIndex ); + } + packet = &packetFrame.DrawPacket( packetCursor ); + draw.packetBacked = true; + draw.drawPacketIndex = packetCursor++; + draw.materialTableRecordIndex = packet->materialRecordIndex; + draw.vertexCount = packet->vertexCount; + draw.firstIndex = packet->firstIndex; + draw.indexCount = packet->indexCount; + draw.vertexOffset = packet->vertexOffset; + draw.scissorX1 = packet->scissorX1; + draw.scissorY1 = packet->scissorY1; + draw.scissorX2 = packet->scissorX2; + draw.scissorY2 = packet->scissorY2; + draw.hasIndexCache = packet->hasIndexCache; + draw.hasAmbientCache = packet->hasAmbientCache; + std::memcpy( draw.modelViewMatrix, + packet->instanceRecord->modelViewMatrix, + sizeof( draw.modelViewMatrix ) ); + } + + if ( !ValidateFinalizedDeformContract( source, packet, + draw.deformRole, draw.deformOutcome, + draw.deformContractHash ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_DEFORM_CONTRACT, + DeformContractFailureDetail( source ), sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + + if ( classification != CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE ) { + noopSurfaces++; + draw.hash = HashDraw( draw ); + continue; + } + drawableSurfaces++; + if ( packet == NULL || !packet->hasShaderRegisters + || packet->instanceRecord == NULL + || !packet->instanceRecord->hasShaderRegisters + || packet->instanceRecord->legacyShaderRegisters == NULL + || packet->instanceRecord->legacyShaderRegisters + != source->shaderRegisters ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_INSTANCE_RECORD, 0, + sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex ); + } + if ( packet->instanceRecord->negativeScale + || packet->instanceRecord->weaponDepthHack + || packet->instanceRecord->modelDepthHack != 0.0f ) { + const int detail = packet->instanceRecord->negativeScale + ? CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE + : CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEPTH_HACK; + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK, + detail, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex ); + } + if ( packet->geometryRecord == NULL + || ( packet->geometryRecord->deformMode + != GEOMETRY_DEFORM_NONE + && draw.deformContractHash == 0 ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM, + sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex ); + } + if ( packet->geometryRecord->skinningMode != GEOMETRY_SKINNING_NONE ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SKINNING, + sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex ); + } + + if ( !ValidateDepthPrerequisite( packetFrame, depthPass, viewDef, + source, *packet, depthPacketCursor, + draw.depthDrawPacketIndex ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING, + source->material->Coverage(), sourceIndex, + draw.drawPacketIndex, view.depthPassPacketIndex ); + } + draw.depthPrerequisite = source->material->Coverage() != MC_TRANSLUCENT; + + const materialResourceTableRecord_t *materialRecord = + R_MaterialResourceTable_RecordForIndex( + packet->materialRecordIndex ); + if ( materialRecord == NULL ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_MATERIAL_RECORD, + packet->materialRecordIndex, sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + if ( materialRecord->sourceMaterialRecordIndex + != packet->materialRecordIndex + || materialRecord->material != source->material + || materialRecord->tableGeneration == 0 + || ( view.tableGeneration != 0 + && view.tableGeneration != materialRecord->tableGeneration ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_STALE_MATERIAL_RECORD, + packet->materialRecordIndex, sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + if ( packet->instanceRecord->shaderRegisterCount + != materialRecord->registerCount ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_INSTANCE_RECORD, + packet->instanceRecord->shaderRegisterCount, sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + view.tableGeneration = materialRecord->tableGeneration; + draw.tableGeneration = materialRecord->tableGeneration; + draw.materialId = materialRecord->materialId; + draw.firstWorldPass = materialRecord->firstWorldPass; + draw.worldPassCount = materialRecord->worldPassCount; + if ( !materialRecord->worldDomainReferenced + || !materialRecord->worldPassEligible + || materialRecord->worldPassFailure + != MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + || materialRecord->firstWorldPass < 0 + || materialRecord->worldPassCount <= 0 + || !R_MaterialResourceTable_WorldPassEligible( + *materialRecord ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_INELIGIBLE, + materialRecord->worldPassFailure, sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex, + materialRecord->worldPassFailureStage, + materialRecord->worldPassFailure ); + } + int directWorldPassCount = 0; + const rendererMaterialPass_t *directWorldPasses = + R_MaterialResourceTable_WorldPasses( *materialRecord, + directWorldPassCount ); + if ( directWorldPasses == NULL + || directWorldPassCount != materialRecord->worldPassCount ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_COPY_FAILED, + directWorldPassCount, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex ); + } + + rendererMaterialPassList_t compiled; + if ( !R_MaterialResourceTable_CopyWorldPassList( *materialRecord, + compiled ) + || compiled.count != static_cast( + materialRecord->worldPassCount ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_COPY_FAILED, + materialRecord->worldPassCount, sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + rendererEvaluatedMaterialPassList_t evaluated; + const rendererMaterialPassEvaluationStatus_t evaluationStatus = + RendererContracts_EvaluateMaterialPassList( evaluated, compiled, + packet->instanceRecord->legacyShaderRegisters, + materialRecord->registerCount > 0 + ? static_cast( materialRecord->registerCount ) + : 0u ); + if ( evaluationStatus != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS + || evaluated.count != compiled.count ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_EVALUATION_FAILED, + evaluationStatus, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex, -1, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE, evaluationStatus ); + } + if ( !RangeFits( passCheckpoint + stagedPassCount, + static_cast( evaluated.count ), + CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_EVALUATED_PASS_POOL_OVERFLOW, + static_cast( evaluated.count ), sourceIndex, + draw.drawPacketIndex, view.ambientPassPacketIndex ); + } + draw.firstEvaluatedPass = passCheckpoint + stagedPassCount; + draw.evaluatedPassCount = static_cast( evaluated.count ); + for ( int passIndex = 0; passIndex < draw.evaluatedPassCount; + ++passIndex ) { + const rendererEvaluatedMaterialPass_t &pass = + evaluated.passes[ passIndex ]; + if ( pass.kind != RENDERER_MATERIAL_PASS_SURFACE + || pass.programFamily != RENDERER_PROGRAM_FIXED + || !pass.depth.testEnabled + || pass.texgen != RENDERER_TEXGEN_EXPLICIT ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_INELIGIBLE, + passIndex, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex, pass.sourceStageIndex ); + } + const materialResourceTextureBinding_t *binding = + R_MaterialResourceTable_ResolveTextureResource( + pass.textureResourceId ); + if ( binding == NULL + || binding->textureResourceId != pass.textureResourceId + || !binding->loaded || binding->missing + || binding->defaulted ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE, + passIndex, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex, pass.sourceStageIndex ); + } + if ( !CountDisposition( draw, pass ) ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DISPOSITION, + pass.disposition, sourceIndex, draw.drawPacketIndex, + view.ambientPassPacketIndex, pass.sourceStageIndex ); + } + domain.passes[ draw.firstEvaluatedPass + passIndex ] = pass; + } + stagedPassCount += draw.evaluatedPassCount; + draw.hash = HashDraw( draw ); + } + + if ( packetCursor != packetEnd ) { + return FailView( view, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH, + packetEnd - packetCursor, viewDef->numDrawSurfs, packetCursor, + view.ambientPassPacketIndex ); + } + + view.firstDraw = drawCheckpoint; + view.drawCount = viewDef->numDrawSurfs; + view.drawableSurfaceCount = drawableSurfaces; + view.noopSurfaceCount = noopSurfaces; + view.firstEvaluatedPass = passCheckpoint; + view.evaluatedPassCount = stagedPassCount; + for ( int i = 0; i < view.drawCount; ++i ) { + const classicWorldAmbientDomainDraw_t &draw = + domain.draws[ view.firstDraw + i ]; + const int phase = static_cast( draw.phase ); + view.phaseSurfaceCount[ phase ]++; + if ( draw.sourceSurface + == CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE ) { + view.phaseDrawableSurfaceCount[ phase ]++; + } + view.activePassCount += draw.activePassCount; + view.drawablePassCount += draw.drawablePassCount; + view.inactivePassCount += draw.inactivePassCount; + view.activeNoopPassCount += draw.activeNoopPassCount; + view.noopPassCount += draw.noopPassCount; + if ( draw.deformContractHash != 0 ) { + view.materialDeformSurfaceCount++; + if ( draw.deformOutcome == CLASSIC_DEFORM_OUTCOME_COMPLETED ) { + view.completedDeformSurfaceCount++; + } else if ( draw.deformOutcome == CLASSIC_DEFORM_OUTCOME_EMPTY ) { + view.emptyDeformSurfaceCount++; + } + } + view.phaseDrawablePassCount[ phase ] += draw.drawablePassCount; + view.phaseNoopPassCount[ phase ] += draw.noopPassCount; + } + domain.drawCount += view.drawCount; + domain.passCount += view.evaluatedPassCount; + view.hash = HashView( view ); + view.failure = CLASSIC_WORLD_AMBIENT_FAILURE_NONE; + view.ready = true; + domain.stats.readyViews++; + domain.stats.drawableSurfaces += view.drawableSurfaceCount; + domain.stats.noopSurfaces += view.noopSurfaceCount; + domain.stats.draws += view.drawCount; + domain.stats.evaluatedPasses += view.evaluatedPassCount; + domain.stats.activePasses += view.activePassCount; + domain.stats.drawablePasses += view.drawablePassCount; + domain.stats.inactivePasses += view.inactivePassCount; + domain.stats.activeNoopPasses += view.activeNoopPassCount; + domain.stats.noopPasses += view.noopPassCount; + domain.stats.materialDeformSurfaces += view.materialDeformSurfaceCount; + domain.stats.completedDeformSurfaces += view.completedDeformSurfaceCount; + domain.stats.emptyDeformSurfaces += view.emptyDeformSurfaceCount; + for ( int phase = 0; phase < CLASSIC_WORLD_AMBIENT_PHASE_COUNT; ++phase ) { + domain.stats.phaseDrawablePasses[ phase ] + += view.phaseDrawablePassCount[ phase ]; + domain.stats.phaseNoopPasses[ phase ] + += view.phaseNoopPassCount[ phase ]; + } + return true; +} + +static int ViewIndex( const classicWorldAmbientDomainView_t *view ) { + // O(1) equivalent of the linear pool scan: a pointer identifies a live view + // exactly when it is element [index] of the pool with index < viewCount. + const auto index = view - domain.views; + return index >= 0 && index < domain.viewCount + && view == &domain.views[ index ] ? static_cast( index ) : -1; +} + +static int DrawIndex( const classicWorldAmbientDomainDraw_t *draw ) { + const auto index = draw - domain.draws; + return index >= 0 && index < domain.drawCount + && draw == &domain.draws[ index ] ? static_cast( index ) : -1; +} + +static classicWorldAmbientDomainView_t *FindMutableView( + const viewDef_t *viewDef ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].viewDef == viewDef ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +static bool SceneIsWorldAmbientCandidate( const idScenePacketFrame &packetFrame, + const scenePacket_t &scene ) { + if ( scene.viewDef == NULL + || scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || scene.viewDef->viewEntitys == NULL ) { + return false; + } + if ( !RangeFits( scene.firstPassPacket, scene.passPacketCount, + packetFrame.NumPasses() ) ) { + return true; + } + for ( int passIndex = 0; passIndex < scene.passPacketCount; ++passIndex ) { + if ( packetFrame.Pass( scene.firstPassPacket + passIndex ).passCategory + == RENDER_PASS_AMBIENT ) { + return true; + } + } + // Supplemental world command scenes (post, present, special effects) may + // legitimately reuse the viewDef. They are outside this ambient owner. + return false; +} + +static void RecordFallback( classicWorldAmbientDomainView_t *view, + classicWorldAmbientDomainBackend_t backend, + classicWorldAmbientDomainFailure_t failure, int detail, + int preFogDrawnPasses, int postFogDrawnPasses, int noopPasses ) { + classicWorldAmbientDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view == NULL ) { + coverage.untrackedFallbacks++; + return; + } + const int viewIndex = ViewIndex( view ); + if ( viewIndex < 0 + || view->backendOutcome[ backend ] + != CLASSIC_WORLD_AMBIENT_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return; + } + view->backendOutcome[ backend ] = CLASSIC_WORLD_AMBIENT_BACKEND_FALLBACK; + view->backendFailure[ backend ] = failure == CLASSIC_WORLD_AMBIENT_FAILURE_NONE + ? CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED : failure; + view->backendFailureDetail[ backend ] = detail; + view->backendDrawnPasses[ backend ][ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] = + preFogDrawnPasses; + view->backendDrawnPasses[ backend ][ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] = + postFogDrawnPasses; + view->backendNoopPasses[ backend ] = noopPasses; + coverage.fallbackViewMask |= 1ull << viewIndex; + coverage.fallbackViews++; + coverage.fallbackSourceSurfaces += view->sourceSurfaceCount; + coverage.fallbackDrawablePasses += view->drawablePassCount; + coverage.fallbackNoopPasses += view->noopPassCount; + for ( int phase = 0; phase < CLASSIC_WORLD_AMBIENT_PHASE_COUNT; ++phase ) { + coverage.fallbackPhaseDrawablePasses[ phase ] + += view->phaseDrawablePassCount[ phase ]; + } + if ( failure == CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH ) { + coverage.coverageMismatches++; + } + if ( view->backendFailure[ backend ] >= CLASSIC_WORLD_AMBIENT_FAILURE_NONE + && view->backendFailure[ backend ] + < CLASSIC_WORLD_AMBIENT_FAILURE_COUNT ) { + domain.stats.failureCounts[ view->backendFailure[ backend ] ]++; + } +} + +} // namespace + +void R_ClassicWorldAmbientDomain_ResetFrame( void ) { + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + domain.viewCount = 0; + domain.drawCount = 0; + domain.passCount = 0; + idStr::Copynz( domain.stats.status, "empty", sizeof( domain.stats.status ) ); +} + +void R_ClassicWorldAmbientDomain_PrepareFrame( + const idScenePacketFrame &packetFrame ) { + R_ClassicWorldAmbientDomain_ResetFrame(); + domain.stats.prepared = true; + domain.stats.sourceScenes = packetFrame.NumScenes(); + domain.stats.overflow = packetFrame.Stats().overflow; + for ( int sceneIndex = 0; sceneIndex < packetFrame.NumScenes(); ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !SceneIsWorldAmbientCandidate( packetFrame, scene ) ) { + continue; + } + if ( FindMutableView( scene.viewDef ) != NULL ) { + continue; + } + domain.stats.worldViews++; + if ( domain.viewCount >= CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_VIEWS ) { + domain.stats.overflow = true; + domain.stats.fallbackViews++; + domain.stats.failureCounts[ + CLASSIC_WORLD_AMBIENT_FAILURE_VIEW_POOL_OVERFLOW ]++; + continue; + } + classicWorldAmbientDomainView_t &view = + domain.views[ domain.viewCount++ ]; + InitView( view, scene.viewDef, sceneIndex ); + if ( view.sourceSurfaceCount > 0 ) { + domain.stats.sourceSurfaces += view.sourceSurfaceCount; + } + R_ClassicWorldAmbientDomain_PrepareView( packetFrame, scene, view ); + } + + domain.stats.frameValid = !domain.stats.overflow + && domain.stats.fallbackViews == 0; + std::uint64_t frameHash = HASH_OFFSET; + HashInt( frameHash, domain.stats.sourceScenes ); + HashInt( frameHash, domain.stats.worldViews ); + for ( int i = 0; i < domain.viewCount; ++i ) { + HashInt( frameHash, domain.views[ i ].scenePacketIndex ); + HashBool( frameHash, domain.views[ i ].ready ); + HashInt( frameHash, domain.views[ i ].failure ); + HashU64( frameHash, domain.views[ i ].hash ); + } + domain.stats.hash = frameHash; + if ( domain.stats.worldViews == 0 ) { + idStr::Copynz( domain.stats.status, "empty", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.frameValid ) { + idStr::Copynz( domain.stats.status, "ready", + sizeof( domain.stats.status ) ); + } else if ( domain.stats.readyViews == 0 ) { + idStr::Copynz( domain.stats.status, "fallback", + sizeof( domain.stats.status ) ); + } else { + idStr::Copynz( domain.stats.status, "mixed-view-fallback", + sizeof( domain.stats.status ) ); + } +} + +const classicWorldAmbientDomainStats_t &R_ClassicWorldAmbientDomain_Stats( void ) { + return domain.stats; +} + +int R_ClassicWorldAmbientDomain_NumViews( void ) { + return domain.viewCount; +} + +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_ViewByIndex( + int index ) { + return index >= 0 && index < domain.viewCount ? &domain.views[ index ] : NULL; +} + +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_ViewForScenePacket( + int scenePacketIndex ) { + for ( int i = 0; i < domain.viewCount; ++i ) { + if ( domain.views[ i ].scenePacketIndex == scenePacketIndex ) { + return &domain.views[ i ]; + } + } + return NULL; +} + +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_FindView( + const viewDef_t *viewDef ) { + return FindMutableView( viewDef ); +} + +const classicWorldAmbientDomainDraw_t *R_ClassicWorldAmbientDomain_ViewDraw( + const classicWorldAmbientDomainView_t &view, int drawIndex ) { + if ( !view.ready || ViewIndex( &view ) < 0 || drawIndex < 0 + || drawIndex >= view.drawCount + || !RangeFits( view.firstDraw, view.drawCount, domain.drawCount ) ) { + return NULL; + } + return &domain.draws[ view.firstDraw + drawIndex ]; +} + +const rendererEvaluatedMaterialPass_t *R_ClassicWorldAmbientDomain_DrawPass( + const classicWorldAmbientDomainDraw_t &draw, int passIndex ) { + if ( DrawIndex( &draw ) < 0 || passIndex < 0 + || passIndex >= draw.evaluatedPassCount + || !RangeFits( draw.firstEvaluatedPass, draw.evaluatedPassCount, + domain.passCount ) ) { + return NULL; + } + return &domain.passes[ draw.firstEvaluatedPass + passIndex ]; +} + +const materialResourceTextureBinding_t *R_ClassicWorldAmbientDomain_DrawPassTexture( + const classicWorldAmbientDomainDraw_t &draw, int passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicWorldAmbientDomain_DrawPass( draw, passIndex ); + return pass != NULL + ? R_MaterialResourceTable_ResolveTextureResource( + pass->textureResourceId ) : NULL; +} + +bool R_ClassicWorldAmbientDomain_RecordOwned( const viewDef_t *viewDef, + classicWorldAmbientDomainBackend_t backend, int preFogDrawnPasses, + int postFogDrawnPasses, int noopPasses ) { + if ( backend < CLASSIC_WORLD_AMBIENT_BACKEND_GL + || backend >= CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ) { + return false; + } + classicWorldAmbientDomainView_t *view = FindMutableView( viewDef ); + if ( view == NULL ) { + return false; + } + classicWorldAmbientDomainBackendCoverage_t &coverage = + domain.stats.backend[ backend ]; + if ( view->backendOutcome[ backend ] + != CLASSIC_WORLD_AMBIENT_BACKEND_UNRECORDED ) { + coverage.duplicateReports++; + return view->backendOutcome[ backend ] + == CLASSIC_WORLD_AMBIENT_BACKEND_OWNED + && view->backendDrawnPasses[ backend ][ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] == preFogDrawnPasses + && view->backendDrawnPasses[ backend ][ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] == postFogDrawnPasses + && view->backendNoopPasses[ backend ] == noopPasses; + } + if ( !view->ready ) { + RecordFallback( view, backend, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + view->failure, preFogDrawnPasses, postFogDrawnPasses, + noopPasses ); + return false; + } + if ( preFogDrawnPasses != view->phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + || postFogDrawnPasses != view->phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + || noopPasses != view->noopPassCount ) { + RecordFallback( view, backend, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH, + preFogDrawnPasses + postFogDrawnPasses, + preFogDrawnPasses, postFogDrawnPasses, noopPasses ); + return false; + } + const int viewIndex = ViewIndex( view ); + view->backendOutcome[ backend ] = CLASSIC_WORLD_AMBIENT_BACKEND_OWNED; + view->backendDrawnPasses[ backend ][ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] = preFogDrawnPasses; + view->backendDrawnPasses[ backend ][ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] = postFogDrawnPasses; + view->backendNoopPasses[ backend ] = noopPasses; + coverage.ownedViewMask |= 1ull << viewIndex; + coverage.ownedViews++; + coverage.ownedSourceSurfaces += view->sourceSurfaceCount; + coverage.ownedDrawablePasses += preFogDrawnPasses + postFogDrawnPasses; + coverage.ownedNoopPasses += noopPasses; + coverage.ownedPhaseDrawablePasses[ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] += preFogDrawnPasses; + coverage.ownedPhaseDrawablePasses[ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] += postFogDrawnPasses; + return true; +} + +void R_ClassicWorldAmbientDomain_RecordBackendFallback( + const viewDef_t *viewDef, classicWorldAmbientDomainBackend_t backend, + classicWorldAmbientDomainFailure_t failure, int detail ) { + if ( backend < CLASSIC_WORLD_AMBIENT_BACKEND_GL + || backend >= CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ) { + return; + } + RecordFallback( FindMutableView( viewDef ), backend, failure, detail, + 0, 0, 0 ); +} + +const classicWorldAmbientDomainBackendCoverage_t &R_ClassicWorldAmbientDomain_BackendCoverage( + classicWorldAmbientDomainBackend_t backend ) { + static const classicWorldAmbientDomainBackendCoverage_t empty = {}; + return backend >= CLASSIC_WORLD_AMBIENT_BACKEND_GL + && backend < CLASSIC_WORLD_AMBIENT_BACKEND_COUNT + ? domain.stats.backend[ backend ] : empty; +} + +const char *ClassicWorldAmbientPhase_Name( classicWorldAmbientPhase_t phase ) { + switch ( phase ) { + case CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG: return "preFog"; + case CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG: return "postFog"; + case CLASSIC_WORLD_AMBIENT_PHASE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicWorldAmbientDomainSourceSurface_Name( + classicWorldAmbientDomainSourceSurface_t sourceSurface ) { + switch ( sourceSurface ) { + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE: return "drawable"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NULL_SURFACE: return "noopNullSurface"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: return "noopMissingMaterial"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NO_AMBIENT: return "noopNoAmbient"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: return "noopEmptyGeometry"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NOT_DRAWN: return "fallbackNotDrawn"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PORTAL_SKY: return "fallbackPortalSky"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW: return "fallbackSuppressedInSubview"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_IN_WORLD_GUI: return "fallbackInWorldGui"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUBVIEW: return "fallbackSubview"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_MISSING_SPACE: return "fallbackMissingSpace"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SYNTHETIC_GUI_SPACE: return "fallbackSyntheticGuiSpace"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SPECIAL_SURFACE: return "fallbackSpecialSurface"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM: return "fallbackDeform"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SKINNING: return "fallbackSkinning"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEPTH_HACK: return "fallbackDepthHack"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE: return "fallbackNegativeScale"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DECAL: return "fallbackDecal"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_POST_PROCESS: return "fallbackPostProcess"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PLAYER_VISIBILITY: return "fallbackPlayerVisibility"; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicWorldAmbientDomainFailure_Name( + classicWorldAmbientDomainFailure_t failure ) { + switch ( failure ) { + case CLASSIC_WORLD_AMBIENT_FAILURE_NONE: return "none"; + case CLASSIC_WORLD_AMBIENT_FAILURE_UNAVAILABLE: return "unavailable"; + case CLASSIC_WORLD_AMBIENT_FAILURE_SCENE_PACKET_OVERFLOW: return "scenePacketOverflow"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_NOT_PREPARED: return "materialTableNotPrepared"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_OVERFLOW: return "materialTableOverflow"; + case CLASSIC_WORLD_AMBIENT_FAILURE_VIEW_POOL_OVERFLOW: return "viewPoolOverflow"; + case CLASSIC_WORLD_AMBIENT_FAILURE_DRAW_POOL_OVERFLOW: return "drawPoolOverflow"; + case CLASSIC_WORLD_AMBIENT_FAILURE_EVALUATED_PASS_POOL_OVERFLOW: return "evaluatedPassPoolOverflow"; + case CLASSIC_WORLD_AMBIENT_FAILURE_UNSUPPORTED_VIEW: return "unsupportedView"; + case CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT: return "unpacketizedViewEffect"; + case CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_SCENE_RANGE: return "invalidSceneRange"; + case CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_AMBIENT_PASS: return "invalidAmbientPass"; + case CLASSIC_WORLD_AMBIENT_FAILURE_FORBIDDEN_PASS: return "forbiddenPass"; + case CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_RANGE: return "invalidDrawRange"; + case CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK: return "sourceSurfaceFallback"; + case CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH: return "sourcePacketMismatch"; + case CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_PACKET: return "invalidDrawPacket"; + case CLASSIC_WORLD_AMBIENT_FAILURE_DEFORM_CONTRACT: return "deformContract"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_GEOMETRY_RECORD: return "missingGeometryRecord"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_INSTANCE_RECORD: return "missingInstanceRecord"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_MATERIAL_RECORD: return "missingMaterialRecord"; + case CLASSIC_WORLD_AMBIENT_FAILURE_STALE_MATERIAL_RECORD: return "staleMaterialRecord"; + case CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING: return "depthPrerequisiteMissing"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_INELIGIBLE: return "materialPassIneligible"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_COPY_FAILED: return "materialPassCopyFailed"; + case CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_EVALUATION_FAILED: return "materialPassEvaluationFailed"; + case CLASSIC_WORLD_AMBIENT_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE: return "textureResourceUnavailable"; + case CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DISPOSITION: return "invalidDisposition"; + case CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY: return "backendNotReady"; + case CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH: return "backendCoverageMismatch"; + case CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED: return "backendRejected"; + case CLASSIC_WORLD_AMBIENT_FAILURE_COUNT: + default: return "unknown"; + } +} + +const char *ClassicWorldAmbientDomainBackend_Name( + classicWorldAmbientDomainBackend_t backend ) { + switch ( backend ) { + case CLASSIC_WORLD_AMBIENT_BACKEND_GL: return "GL"; + case CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN: return "Vulkan"; + case CLASSIC_WORLD_AMBIENT_BACKEND_COUNT: + default: return "unknown"; + } +} + +bool RendererClassicWorldAmbientDomain_RunSelfTest( void ) { + if ( CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_VIEWS != 64 + || CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS != 4096 + || CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES != 8192 + || !RangeFits( 8191, 1, 8192 ) || RangeFits( 8192, 1, 8192 ) + || RangeFits( -1, 1, 8192 ) + || idStr::Cmp( ClassicWorldAmbientPhase_Name( + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ), "preFog" ) + || idStr::Cmp( ClassicWorldAmbientDomainFailure_Name( + CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING ), + "depthPrerequisiteMissing" ) + || idStr::Cmp( ClassicWorldAmbientDomainSourceSurface_Name( + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SKINNING ), + "fallbackSkinning" ) + || idStr::Cmp( ClassicWorldAmbientDomainSourceSurface_Name( + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PLAYER_VISIBILITY ), + "fallbackPlayerVisibility" ) + || idStr::Cmp( ClassicWorldAmbientDomainFailure_Name( + CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT ), + "unpacketizedViewEffect" ) + || idStr::Cmp( ClassicWorldAmbientDomainFailure_Name( + CLASSIC_WORLD_AMBIENT_FAILURE_DEFORM_CONTRACT ), + "deformContract" ) + || ForbiddenPassCategory( RENDER_PASS_DEPTH ) + || ForbiddenPassCategory( RENDER_PASS_AMBIENT ) + || !ForbiddenPassCategory( RENDER_PASS_ARB2_INTERACTION ) + || !ForbiddenPassCategory( static_cast( + 0x7fffffff ) ) ) { + return false; + } + + rendererEvaluatedMaterialPass_t first; + std::memset( &first, 0, sizeof( first ) ); + first.sourceStageIndex = 7; + first.kind = RENDERER_MATERIAL_PASS_SURFACE; + first.textureResourceId = 0x123400020003ull; + first.condition = 1.0f; + first.active = true; + first.disposition = RENDERER_MATERIAL_PASS_DRAW; + first.color[ 0 ] = 1.0f; + first.textureMatrix[ 0 ] = 1.0f; + first.textureMatrix[ 4 ] = 1.0f; + first.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA; + first.blend.destinationColor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + first.depth.testEnabled = true; + first.depth.compareOperation = RENDERER_COMPARE_EQUAL; + first.alphaTestCompareOperation = RENDERER_COMPARE_GREATER; + first.programFamily = RENDERER_PROGRAM_FIXED; + const std::uint64_t firstHash = HashPass( first ); + rendererEvaluatedMaterialPass_t same = first; + if ( firstHash == 0 || HashPass( same ) != firstHash ) { + return false; + } + same.color[ 0 ] = 0.5f; + if ( HashPass( same ) == firstHash ) { + return false; + } + same = first; + same.textureResourceId += 1ull << 32; + if ( HashPass( same ) != firstHash ) { + return false; + } + classicWorldAmbientDomainDraw_t deformHashDraw; + InitDraw( deformHashDraw ); + const std::uint64_t undeformedHash = HashDraw( deformHashDraw ); + deformHashDraw.deformRole = CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + deformHashDraw.deformOutcome = CLASSIC_DEFORM_OUTCOME_COMPLETED; + deformHashDraw.deformContractHash = 0x61234ull; + if ( HashDraw( deformHashDraw ) == undeformedHash ) { + return false; + } + + classicWorldAmbientDomainDraw_t dispositionDraw; + InitDraw( dispositionDraw ); + rendererEvaluatedMaterialPass_t dispositionPass = first; + dispositionPass.active = false; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_INACTIVE_CONDITION; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.active = true; + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA; + if ( !CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + dispositionPass.disposition = RENDERER_MATERIAL_PASS_DRAW; + if ( !CountDisposition( dispositionDraw, dispositionPass ) + || dispositionDraw.activePassCount != 4 + || dispositionDraw.drawablePassCount != 1 + || dispositionDraw.inactivePassCount != 1 + || dispositionDraw.activeNoopPassCount != 3 + || dispositionDraw.noopPassCount != 4 ) { + return false; + } + dispositionPass.disposition = + static_cast( 0x7fffffff ); + if ( CountDisposition( dispositionDraw, dispositionPass ) ) { + return false; + } + + const classicWorldAmbientDomainStats_t savedStats = domain.stats; + classicWorldAmbientDomainView_t rollbackView; + InitView( rollbackView, NULL, -1 ); + rollbackView.ready = true; + rollbackView.firstDraw = 17; + rollbackView.drawCount = 3; + rollbackView.firstEvaluatedPass = 29; + rollbackView.evaluatedPassCount = 5; + rollbackView.drawablePassCount = 2; + rollbackView.phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] = 1; + rollbackView.phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] = 1; + rollbackView.noopPassCount = 3; + rollbackView.hash = 1; + const bool rollbackResult = FailView( rollbackView, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH, 41, + 2, 7, 3 ); + const bool rollbackOk = !rollbackResult && !rollbackView.ready + && rollbackView.failure + == CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH + && rollbackView.failureDetail == 41 + && rollbackView.failureSourceSurfaceIndex == 2 + && rollbackView.failureDrawPacketIndex == 7 + && rollbackView.failurePassPacketIndex == 3 + && rollbackView.firstDraw == -1 && rollbackView.drawCount == 0 + && rollbackView.firstEvaluatedPass == -1 + && rollbackView.evaluatedPassCount == 0 + && rollbackView.drawablePassCount == 0 + && rollbackView.noopPassCount == 0 && rollbackView.hash == 0; + domain.stats = savedStats; + if ( !rollbackOk ) { + return false; + } + + const int savedViewCount = domain.viewCount; + const classicWorldAmbientDomainView_t savedView = domain.views[ 0 ]; + viewDef_t coverageView; + std::memset( &coverageView, 0, sizeof( coverageView ) ); + InitView( domain.views[ 0 ], &coverageView, 0 ); + domain.viewCount = 1; + domain.views[ 0 ].ready = true; + domain.views[ 0 ].sourceSurfaceCount = 4; + domain.views[ 0 ].drawablePassCount = 3; + domain.views[ 0 ].phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] = 2; + domain.views[ 0 ].phaseDrawablePassCount[ + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] = 1; + domain.views[ 0 ].noopPassCount = 3; + std::memset( &domain.stats, 0, sizeof( domain.stats ) ); + const bool firstOwned = R_ClassicWorldAmbientDomain_RecordOwned( + &coverageView, CLASSIC_WORLD_AMBIENT_BACKEND_GL, 2, 1, 3 ); + const bool repeatedOwned = R_ClassicWorldAmbientDomain_RecordOwned( + &coverageView, CLASSIC_WORLD_AMBIENT_BACKEND_GL, 2, 1, 3 ); + const bool mismatchRejected = !R_ClassicWorldAmbientDomain_RecordOwned( + &coverageView, CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN, 1, 1, 3 ); + const bool coverageOk = firstOwned && repeatedOwned && mismatchRejected + && domain.views[ 0 ].backendOutcome[ + CLASSIC_WORLD_AMBIENT_BACKEND_GL ] + == CLASSIC_WORLD_AMBIENT_BACKEND_OWNED + && domain.views[ 0 ].backendOutcome[ + CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN ] + == CLASSIC_WORLD_AMBIENT_BACKEND_FALLBACK + && domain.stats.backend[ + CLASSIC_WORLD_AMBIENT_BACKEND_GL ].ownedViews == 1 + && domain.stats.backend[ + CLASSIC_WORLD_AMBIENT_BACKEND_GL ].duplicateReports == 1 + && domain.stats.backend[ + CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN ].fallbackViews == 1 + && domain.stats.backend[ + CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN ].coverageMismatches == 1; + domain.views[ 0 ] = savedView; + domain.viewCount = savedViewCount; + domain.stats = savedStats; + return coverageOk; +} diff --git a/src/renderer/ClassicWorldAmbientDomain.h b/src/renderer/ClassicWorldAmbientDomain.h new file mode 100644 index 00000000..a5fe0c08 --- /dev/null +++ b/src/renderer/ClassicWorldAmbientDomain.h @@ -0,0 +1,278 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __CLASSIC_WORLD_AMBIENT_DOMAIN_H__ +#define __CLASSIC_WORLD_AMBIENT_DOMAIN_H__ + +#include "ClassicDeformDomain.h" +#include "MaterialResourceTable.h" + +/* +=============================================================================== + + Shared whole-view ownership contract for classic fixed-function world + ambient/material work. + + Preparation is transactional per 3D view. A view is ready only when every + source surface is classified, the complete ambient packet span matches in + source order, every required opaque/perforated depth prerequisite is present, + every authored pass evaluates once, and every generation-scoped texture + resource resolves. Failed views expose diagnostics but no partial draw/pass + range. Interaction, shadow, light-grid, fog, deform, subview, GUI, special, + and post-process islands remain explicit whole-view rollback boundaries. + +=============================================================================== +*/ + +const int CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_VIEWS = SCENE_PACKET_MAX_SCENES; +const int CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS = SCENE_PACKET_MAX_DRAWS; +const int CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES = SCENE_PACKET_MAX_DRAWS * 2; + +enum classicWorldAmbientPhase_t { + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG = 0, + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG, + CLASSIC_WORLD_AMBIENT_PHASE_COUNT +}; + +enum classicWorldAmbientDomainSourceSurface_t { + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE = 0, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NULL_SURFACE, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_MISSING_MATERIAL, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NO_AMBIENT, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NOT_DRAWN, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PORTAL_SKY, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUPPRESSED_IN_SUBVIEW, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_IN_WORLD_GUI, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUBVIEW, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_MISSING_SPACE, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SYNTHETIC_GUI_SPACE, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SPECIAL_SURFACE, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SKINNING, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEPTH_HACK, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_NEGATIVE_SCALE, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DECAL, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_POST_PROCESS, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_PLAYER_VISIBILITY, + CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_COUNT +}; + +enum classicWorldAmbientDomainFailure_t { + CLASSIC_WORLD_AMBIENT_FAILURE_NONE = 0, + CLASSIC_WORLD_AMBIENT_FAILURE_UNAVAILABLE, + CLASSIC_WORLD_AMBIENT_FAILURE_SCENE_PACKET_OVERFLOW, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_NOT_PREPARED, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_TABLE_OVERFLOW, + CLASSIC_WORLD_AMBIENT_FAILURE_VIEW_POOL_OVERFLOW, + CLASSIC_WORLD_AMBIENT_FAILURE_DRAW_POOL_OVERFLOW, + CLASSIC_WORLD_AMBIENT_FAILURE_EVALUATED_PASS_POOL_OVERFLOW, + CLASSIC_WORLD_AMBIENT_FAILURE_UNSUPPORTED_VIEW, + CLASSIC_WORLD_AMBIENT_FAILURE_UNPACKETIZED_VIEW_EFFECT, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_SCENE_RANGE, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_AMBIENT_PASS, + CLASSIC_WORLD_AMBIENT_FAILURE_FORBIDDEN_PASS, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_RANGE, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK, + CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_PACKET_MISMATCH, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DRAW_PACKET, + CLASSIC_WORLD_AMBIENT_FAILURE_DEFORM_CONTRACT, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_GEOMETRY_RECORD, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_INSTANCE_RECORD, + CLASSIC_WORLD_AMBIENT_FAILURE_MISSING_MATERIAL_RECORD, + CLASSIC_WORLD_AMBIENT_FAILURE_STALE_MATERIAL_RECORD, + CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_INELIGIBLE, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_COPY_FAILED, + CLASSIC_WORLD_AMBIENT_FAILURE_MATERIAL_PASS_EVALUATION_FAILED, + CLASSIC_WORLD_AMBIENT_FAILURE_TEXTURE_RESOURCE_UNAVAILABLE, + CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_DISPOSITION, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + CLASSIC_WORLD_AMBIENT_FAILURE_COUNT +}; + +enum classicWorldAmbientDomainBackend_t { + CLASSIC_WORLD_AMBIENT_BACKEND_GL = 0, + CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN, + CLASSIC_WORLD_AMBIENT_BACKEND_COUNT +}; + +enum classicWorldAmbientDomainBackendOutcome_t { + CLASSIC_WORLD_AMBIENT_BACKEND_UNRECORDED = 0, + CLASSIC_WORLD_AMBIENT_BACKEND_OWNED, + CLASSIC_WORLD_AMBIENT_BACKEND_FALLBACK +}; + +typedef struct classicWorldAmbientDomainBackendCoverage_s { + std::uint64_t ownedViewMask; + std::uint64_t fallbackViewMask; + int ownedViews; + int fallbackViews; + int ownedSourceSurfaces; + int fallbackSourceSurfaces; + int ownedDrawablePasses; + int fallbackDrawablePasses; + int ownedNoopPasses; + int fallbackNoopPasses; + int ownedPhaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int fallbackPhaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int coverageMismatches; + int duplicateReports; + int untrackedFallbacks; +} classicWorldAmbientDomainBackendCoverage_t; + +typedef struct classicWorldAmbientDomainDraw_s { + int sourceSurfaceIndex; + classicWorldAmbientDomainSourceSurface_t sourceSurface; + classicWorldAmbientPhase_t phase; + // The sole legacy bridge retained by the domain. Consumers may use it only + // for sealed geometry submission; stage data/registers are never reread. + const drawSurf_t *legacyDrawSurf; + int drawPacketIndex; + int depthDrawPacketIndex; + int materialTableRecordIndex; + int materialId; + std::uint32_t tableGeneration; + int firstWorldPass; + int worldPassCount; + int firstEvaluatedPass; + int evaluatedPassCount; + int activePassCount; + int drawablePassCount; + int inactivePassCount; + int activeNoopPassCount; + int noopPassCount; + int vertexCount; + int firstIndex; + int indexCount; + int vertexOffset; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float modelViewMatrix[ 16 ]; + bool packetBacked; + bool depthPrerequisite; + bool hasIndexCache; + bool hasAmbientCache; + classicDeformRole_t deformRole; + classicDeformOutcome_t deformOutcome; + std::uint64_t deformContractHash; + std::uint64_t hash; +} classicWorldAmbientDomainDraw_t; + +typedef struct classicWorldAmbientDomainView_s { + const viewDef_t *viewDef; + int scenePacketIndex; + int ambientPassPacketIndex; + int depthPassPacketIndex; + std::uint32_t tableGeneration; + int firstDraw; + int drawCount; + int sourceSurfaceCount; + int drawableSurfaceCount; + int noopSurfaceCount; + int packetDrawCount; + int depthPacketDrawCount; + int firstEvaluatedPass; + int evaluatedPassCount; + int activePassCount; + int drawablePassCount; + int inactivePassCount; + int activeNoopPassCount; + int noopPassCount; + int materialDeformSurfaceCount; + int completedDeformSurfaceCount; + int emptyDeformSurfaceCount; + int phaseSurfaceCount[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int phaseDrawableSurfaceCount[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int phaseDrawablePassCount[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int phaseNoopPassCount[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int viewportX1; + int viewportY1; + int viewportX2; + int viewportY2; + int scissorX1; + int scissorY1; + int scissorX2; + int scissorY2; + float projectionMatrix[ 16 ]; + bool ready; + classicWorldAmbientDomainFailure_t failure; + int failureDetail; + materialResourceWorldPassFailure_t worldPassFailure; + rendererMaterialPassEvaluationStatus_t evaluationStatus; + int failurePassPacketIndex; + int failureDrawPacketIndex; + int failureSourceSurfaceIndex; + int failureSourceStageIndex; + std::uint64_t hash; + classicWorldAmbientDomainBackendOutcome_t backendOutcome[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ]; + classicWorldAmbientDomainFailure_t backendFailure[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ]; + int backendFailureDetail[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ]; + int backendDrawnPasses[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ][ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int backendNoopPasses[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ]; +} classicWorldAmbientDomainView_t; + +typedef struct classicWorldAmbientDomainStats_s { + bool prepared; + bool frameValid; + bool overflow; + int sourceScenes; + int worldViews; + int readyViews; + int fallbackViews; + int sourceSurfaces; + int drawableSurfaces; + int noopSurfaces; + int draws; + int evaluatedPasses; + int activePasses; + int drawablePasses; + int inactivePasses; + int activeNoopPasses; + int noopPasses; + int materialDeformSurfaces; + int completedDeformSurfaces; + int emptyDeformSurfaces; + int phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int failureCounts[ CLASSIC_WORLD_AMBIENT_FAILURE_COUNT ]; + std::uint64_t hash; + classicWorldAmbientDomainBackendCoverage_t backend[ CLASSIC_WORLD_AMBIENT_BACKEND_COUNT ]; + char status[ 96 ]; +} classicWorldAmbientDomainStats_t; + +void R_ClassicWorldAmbientDomain_ResetFrame( void ); +void R_ClassicWorldAmbientDomain_PrepareFrame( const idScenePacketFrame &packetFrame ); +const classicWorldAmbientDomainStats_t &R_ClassicWorldAmbientDomain_Stats( void ); +int R_ClassicWorldAmbientDomain_NumViews( void ); +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_ViewByIndex( int index ); +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_ViewForScenePacket( int scenePacketIndex ); +const classicWorldAmbientDomainView_t *R_ClassicWorldAmbientDomain_FindView( const viewDef_t *viewDef ); +const classicWorldAmbientDomainDraw_t *R_ClassicWorldAmbientDomain_ViewDraw( + const classicWorldAmbientDomainView_t &view, int drawIndex ); +const rendererEvaluatedMaterialPass_t *R_ClassicWorldAmbientDomain_DrawPass( + const classicWorldAmbientDomainDraw_t &draw, int passIndex ); +const materialResourceTextureBinding_t *R_ClassicWorldAmbientDomain_DrawPassTexture( + const classicWorldAmbientDomainDraw_t &draw, int passIndex ); +bool R_ClassicWorldAmbientDomain_RecordOwned( const viewDef_t *viewDef, + classicWorldAmbientDomainBackend_t backend, int preFogDrawnPasses, + int postFogDrawnPasses, int noopPasses ); +void R_ClassicWorldAmbientDomain_RecordBackendFallback( const viewDef_t *viewDef, + classicWorldAmbientDomainBackend_t backend, + classicWorldAmbientDomainFailure_t failure, int detail ); +const classicWorldAmbientDomainBackendCoverage_t &R_ClassicWorldAmbientDomain_BackendCoverage( + classicWorldAmbientDomainBackend_t backend ); +const char *ClassicWorldAmbientPhase_Name( classicWorldAmbientPhase_t phase ); +const char *ClassicWorldAmbientDomainSourceSurface_Name( + classicWorldAmbientDomainSourceSurface_t sourceSurface ); +const char *ClassicWorldAmbientDomainFailure_Name( + classicWorldAmbientDomainFailure_t failure ); +const char *ClassicWorldAmbientDomainBackend_Name( + classicWorldAmbientDomainBackend_t backend ); +bool RendererClassicWorldAmbientDomain_RunSelfTest( void ); + +#endif /* !__CLASSIC_WORLD_AMBIENT_DOMAIN_H__ */ diff --git a/src/renderer/GpuFrameTimingCore.h b/src/renderer/GpuFrameTimingCore.h new file mode 100644 index 00000000..3fd8a255 --- /dev/null +++ b/src/renderer/GpuFrameTimingCore.h @@ -0,0 +1,116 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __GPU_FRAME_TIMING_CORE_H__ +#define __GPU_FRAME_TIMING_CORE_H__ + +#include +#include + +// Renderer-API-independent state and timestamp math. Keeping this header free +// of GL/Vulkan/idlib types lets the native test exercise wrap, generation, and +// counter behavior without a graphics device. +typedef struct gpuFrameTimingCoreState_s { + bool supported; + bool valid; + int backend; + int frameNumber; + std::uint32_t generation; + std::uint32_t latencyFrames; + std::uint64_t elapsedMicroseconds; + std::uint64_t resolvedSamples; + std::uint64_t unavailableSamples; + std::uint64_t droppedSamples; + std::uint64_t resetCount; +} gpuFrameTimingCoreState_t; + +inline void GpuFrameTimingCore_Initialize( gpuFrameTimingCoreState_t &state ) { + state.supported = false; + state.valid = false; + state.backend = 0; + state.frameNumber = -1; + state.generation = 0; + state.latencyFrames = 0; + state.elapsedMicroseconds = 0; + state.resolvedSamples = 0; + state.unavailableSamples = 0; + state.droppedSamples = 0; + state.resetCount = 0; +} + +inline void GpuFrameTimingCore_Reset( gpuFrameTimingCoreState_t &state ) { + state.valid = false; + state.frameNumber = -1; + state.latencyFrames = 0; + state.elapsedMicroseconds = 0; + state.generation++; + state.resetCount++; +} + +inline void GpuFrameTimingCore_SetBackend( gpuFrameTimingCoreState_t &state, + int backend, bool supported ) { + if ( state.backend != backend || state.supported != supported ) { + state.backend = backend; + state.supported = supported; + GpuFrameTimingCore_Reset( state ); + } +} + +inline void GpuFrameTimingCore_RecordResolved( gpuFrameTimingCoreState_t &state, + int backend, int frameNumber, std::uint32_t generation, + std::uint64_t elapsedMicroseconds, std::uint32_t latencyFrames ) { + if ( !state.supported || state.backend != backend || state.generation != generation + || elapsedMicroseconds == 0 ) { + state.droppedSamples++; + return; + } + state.valid = true; + state.frameNumber = frameNumber; + state.latencyFrames = latencyFrames; + state.elapsedMicroseconds = elapsedMicroseconds; + state.resolvedSamples++; +} + +inline std::uint64_t GpuFrameTimingCore_TimestampDelta( std::uint64_t begin, + std::uint64_t end, std::uint32_t validBits ) { + if ( validBits == 0 ) { + return 0; + } + if ( validBits >= 64 ) { + return end - begin; + } + const std::uint64_t mask = ( std::uint64_t( 1 ) << validBits ) - 1; + return ( end - begin ) & mask; +} + +inline std::uint64_t GpuFrameTimingCore_TicksToMicroseconds( std::uint64_t ticks, + double nanosecondsPerTick ) { + if ( nanosecondsPerTick <= 0.0 ) { + return 0; + } + const long double microseconds = + static_cast( ticks ) * static_cast( nanosecondsPerTick ) / 1000.0L; + if ( microseconds >= static_cast( ( std::numeric_limits::max )() ) ) { + return ( std::numeric_limits::max )(); + } + return static_cast( microseconds + 0.5L ); +} + +inline std::uint32_t GpuFrameTimingCore_FrameLatency( int sampleFrame, int currentFrame ) { + if ( sampleFrame == -1 || currentFrame == sampleFrame ) { + return 0; + } + std::int64_t signedLatency = static_cast( currentFrame ) + - static_cast( sampleFrame ); + if ( signedLatency < 0 ) { + // Renderer frame numbers are exposed as signed ints for legacy ABI + // compatibility. Interpret a sign-boundary crossing as one 32-bit wrap. + signedLatency += ( std::int64_t( 1 ) << 32 ); + } + const std::uint64_t latency = static_cast( signedLatency ); + return latency > ( std::numeric_limits::max )() + ? ( std::numeric_limits::max )() + : static_cast( latency ); +} + +#endif /* !__GPU_FRAME_TIMING_CORE_H__ */ diff --git a/src/renderer/GpuSkinning.cpp b/src/renderer/GpuSkinning.cpp new file mode 100644 index 00000000..302cf131 --- /dev/null +++ b/src/renderer/GpuSkinning.cpp @@ -0,0 +1,598 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "GpuSkinning.h" + +#include +#include +#include +#include + +namespace { + gpuSkinningStats_t rg_gpuSkinningStats; + uint32 rg_gpuSkinningGeneration = 1; + bool rg_gpuSkinningInitialized = false; + + static ID_INLINE void R_GpuSkinning_SaturatingAdd( uint64 &value, uint64 amount ) { + const uint64 maxValue = ~static_cast( 0 ); + value = amount > maxValue - value ? maxValue : value + amount; + } + + static ID_INLINE idVec3 R_GpuSkinning_TransformVector( const float *matrix, + const idVec3 &vector, float w ) { + return idVec3( + matrix[0] * vector.x + matrix[1] * vector.y + matrix[2] * vector.z + matrix[3] * w, + matrix[4] * vector.x + matrix[5] * vector.y + matrix[6] * vector.z + matrix[7] * w, + matrix[8] * vector.x + matrix[9] * vector.y + matrix[10] * vector.z + matrix[11] * w ); + } + + static ID_INLINE bool R_GpuSkinning_BasisClose( const idVec3 &a, const idVec3 &b, + float epsilon ) { + return ( a - b ).LengthSqr() <= epsilon * epsilon; + } +} + +const char *R_GpuSkinning_FallbackName( gpuSkinningFallbackReason_t reason ) { + static const char *names[ GPU_SKINNING_FALLBACK_COUNT ] = { + "none", "disabled", "backendUnavailable", "missingBindPose", + "missingSkinVertices", "vertexCount", "residualWeights", "jointCount", + "jointIndex", "malformedWeights", "skinScale", "unsupportedMaterial", + "unsupportedPass", "stencilVolume", "decalOverlay", "paletteAllocation", + "stalePalette" + }; + return reason >= GPU_SKINNING_FALLBACK_NONE && reason < GPU_SKINNING_FALLBACK_COUNT + ? names[ reason ] : "invalid"; +} + +uint32 R_GpuSkinning_ContractGeneration( void ) { + return rg_gpuSkinningGeneration; +} + +uint64 R_GpuSkinning_ReadMicroseconds( void ) { + const std::chrono::steady_clock::duration elapsed = + std::chrono::steady_clock::now().time_since_epoch(); + return static_cast( std::chrono::duration_cast( + elapsed ).count() ); +} + +bool R_GpuSkinning_PackVertexExact( const gpuSkinningInfluence_t *influences, + int numInfluences, int numJoints, bool allowSignedWeights, + gpuSkinningVertex_t &packed, gpuSkinningPackResult_t &result ) { + memset( &packed, 0, sizeof( packed ) ); + memset( &result, 0, sizeof( result ) ); + result.reason = GPU_SKINNING_FALLBACK_NONE; + + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.packedVertices, 1 ); + if ( influences == NULL || numInfluences <= 0 ) { + result.reason = GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + return false; + } + if ( numJoints <= 0 || numJoints > GPU_SKINNING_MAX_JOINTS ) { + result.reason = GPU_SKINNING_FALLBACK_JOINT_COUNT; + return false; + } + + int packedIndex = 0; + for ( int i = 0; i < numInfluences; ++i ) { + const float weight = influences[i].weight; + if ( !std::isfinite( weight ) || ( !allowSignedWeights && weight < 0.0f ) ) { + result.reason = GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + return false; + } + const float absoluteWeight = idMath::Fabs( weight ); + result.totalAbsoluteWeight += absoluteWeight; + if ( absoluteWeight == 0.0f ) { + continue; + } + if ( influences[i].jointIndex >= static_cast( numJoints ) ) { + result.reason = GPU_SKINNING_FALLBACK_JOINT_INDEX; + return false; + } + + ++result.meaningfulInfluences; + result.maxJointIndex = Max( result.maxJointIndex, influences[i].jointIndex ); + if ( packedIndex < GPU_SKINNING_INFLUENCES ) { + packed.jointIndices[ packedIndex ] = influences[i].jointIndex; + packed.jointWeights[ packedIndex ] = weight; + ++packedIndex; + } else { + result.residualAbsoluteWeight += absoluteWeight; + } + } + + if ( result.totalAbsoluteWeight <= 0.0f ) { + result.reason = GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + return false; + } + if ( result.meaningfulInfluences > GPU_SKINNING_INFLUENCES ) { + result.reason = GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS; + return false; + } + + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.exactVertices, 1 ); + return true; +} + +bool R_GpuSkinning_DeformVertexCPU( const idDrawVert &bindPose, + const gpuSkinningVertex_t &skinVertex, const gpuSkinningJointPalette_t &palette, + idDrawVert &deformed ) { + if ( palette.matrices == NULL || palette.numJoints <= 0 + || palette.matrixStrideFloats != GPU_SKINNING_JOINT_FLOATS ) { + return false; + } + + deformed = bindPose; + idVec3 position( 0.0f, 0.0f, 0.0f ); + idVec3 normal( 0.0f, 0.0f, 0.0f ); + idVec3 tangent0( 0.0f, 0.0f, 0.0f ); + idVec3 tangent1( 0.0f, 0.0f, 0.0f ); + float totalAbsoluteWeight = 0.0f; + + for ( int i = 0; i < GPU_SKINNING_INFLUENCES; ++i ) { + const float weight = skinVertex.jointWeights[i]; + if ( weight == 0.0f ) { + continue; + } + if ( !std::isfinite( weight ) || skinVertex.jointIndices[i] >= static_cast( palette.numJoints ) ) { + return false; + } + const float *matrix = palette.matrices + + skinVertex.jointIndices[i] * palette.matrixStrideFloats; + for ( int valueIndex = 0; valueIndex < GPU_SKINNING_JOINT_FLOATS; ++valueIndex ) { + if ( !std::isfinite( matrix[valueIndex] ) ) { + return false; + } + } + position += weight * R_GpuSkinning_TransformVector( matrix, bindPose.xyz, 1.0f ); + normal += weight * R_GpuSkinning_TransformVector( matrix, bindPose.normal, 0.0f ); + tangent0 += weight * R_GpuSkinning_TransformVector( matrix, bindPose.tangents[0], 0.0f ); + tangent1 += weight * R_GpuSkinning_TransformVector( matrix, bindPose.tangents[1], 0.0f ); + totalAbsoluteWeight += idMath::Fabs( weight ); + } + if ( totalAbsoluteWeight <= 0.0f ) { + return false; + } + + deformed.xyz = position; + deformed.normal = normal; + deformed.tangents[0] = tangent0; + deformed.tangents[1] = tangent1; + return true; +} + +bool R_GpuSkinning_CompareVertexCPU( const idDrawVert &expected, + const idDrawVert &actual, float positionEpsilon, float basisEpsilon ) { + return positionEpsilon >= 0.0f && basisEpsilon >= 0.0f + && R_GpuSkinning_BasisClose( expected.xyz, actual.xyz, positionEpsilon ) + && R_GpuSkinning_BasisClose( expected.normal, actual.normal, basisEpsilon ) + && R_GpuSkinning_BasisClose( expected.tangents[0], actual.tangents[0], basisEpsilon ) + && R_GpuSkinning_BasisClose( expected.tangents[1], actual.tangents[1], basisEpsilon ); +} + +void R_GpuSkinning_ClearSurfaceContract( srfTriangles_s *tri, + gpuSkinningFallbackReason_t reason ) { + if ( tri == NULL ) { + return; + } + R_ClearStaticGpuSkinningJointPalette( tri ); + tri->gpuSkinningFallbackReason = reason; +} + +bool R_GpuSkinning_AttachSurfaceContract( srfTriangles_s *tri, + const idDrawVert *bindPoseVerts, const gpuSkinningVertex_t *skinVerts, + int numVerts, const float *jointMatrices, int numJoints, + int sourceMatrixStrideFloats, bool signedWeights, + gpuSkinningFallbackReason_t sourceFallback ) { + if ( tri == NULL ) { + return false; + } + R_ClearStaticGpuSkinningJointPalette( tri ); + if ( sourceFallback != GPU_SKINNING_FALLBACK_NONE ) { + R_GpuSkinning_ClearSurfaceContract( tri, sourceFallback ); + return false; + } + if ( bindPoseVerts == NULL ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_MISSING_BIND_POSE ); + return false; + } + if ( skinVerts == NULL ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES ); + return false; + } + if ( numVerts <= 0 || tri->numVerts != numVerts ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_VERTEX_COUNT ); + return false; + } + if ( jointMatrices == NULL || numJoints <= 0 || numJoints > GPU_SKINNING_MAX_JOINTS ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_JOINT_COUNT ); + return false; + } + if ( sourceMatrixStrideFloats < GPU_SKINNING_JOINT_FLOATS + || !R_AllocStaticGpuSkinningJointPalette( tri, numJoints ) ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION ); + return false; + } + + for ( int jointIndex = 0; jointIndex < numJoints; ++jointIndex ) { + const float *source = jointMatrices + jointIndex * sourceMatrixStrideFloats; + float *dest = tri->gpuSkinningJointPalette + jointIndex * GPU_SKINNING_JOINT_FLOATS; + for ( int valueIndex = 0; valueIndex < GPU_SKINNING_JOINT_FLOATS; ++valueIndex ) { + if ( !std::isfinite( source[valueIndex] ) ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS ); + return false; + } + dest[valueIndex] = source[valueIndex]; + } + } + + tri->gpuSkinningBindPoseVerts = bindPoseVerts; + tri->gpuSkinningVerts = skinVerts; + tri->numGpuSkinningVerts = numVerts; + tri->gpuSkinningPaletteGeneration = rg_gpuSkinningGeneration; + tri->gpuSkinningFallbackReason = GPU_SKINNING_FALLBACK_NONE; + tri->gpuSkinningSignedWeights = signedWeights; + return true; +} + +void R_GpuSkinning_ReferenceSurfaceContract( srfTriangles_s *tri, + const srfTriangles_s *reference ) { + if ( tri == reference ) { + return; + } + R_ReferenceStaticGpuSkinning( tri, reference ); +} + +gpuSkinningFallbackReason_t R_GpuSkinning_ValidateSurface( + const gpuSkinningSurface_t &surface ) { + if ( surface.fallbackReason != GPU_SKINNING_FALLBACK_NONE ) { + return surface.fallbackReason; + } + if ( surface.bindPoseVerts == NULL ) { + return GPU_SKINNING_FALLBACK_MISSING_BIND_POSE; + } + if ( surface.skinVerts == NULL ) { + return GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES; + } + if ( surface.numVerts <= 0 ) { + return GPU_SKINNING_FALLBACK_VERTEX_COUNT; + } + if ( surface.palette.matrices == NULL || surface.palette.numJoints <= 0 + || surface.palette.numJoints > GPU_SKINNING_MAX_JOINTS + || surface.palette.matrixStrideFloats != GPU_SKINNING_JOINT_FLOATS ) { + return GPU_SKINNING_FALLBACK_JOINT_COUNT; + } + if ( surface.palette.generation == 0 || surface.palette.generation != rg_gpuSkinningGeneration ) { + return GPU_SKINNING_FALLBACK_STALE_PALETTE; + } + for ( int jointIndex = 0; jointIndex < surface.palette.numJoints; ++jointIndex ) { + const float *matrix = surface.palette.matrices + + jointIndex * surface.palette.matrixStrideFloats; + for ( int valueIndex = 0; valueIndex < GPU_SKINNING_JOINT_FLOATS; ++valueIndex ) { + if ( !std::isfinite( matrix[valueIndex] ) ) { + return GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + } + } + } + + for ( int vertexIndex = 0; vertexIndex < surface.numVerts; ++vertexIndex ) { + float totalAbsoluteWeight = 0.0f; + for ( int influenceIndex = 0; influenceIndex < GPU_SKINNING_INFLUENCES; ++influenceIndex ) { + const float weight = surface.skinVerts[vertexIndex].jointWeights[influenceIndex]; + if ( !std::isfinite( weight ) || ( !surface.signedWeights && weight < 0.0f ) ) { + return GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + } + if ( weight != 0.0f + && surface.skinVerts[vertexIndex].jointIndices[influenceIndex] + >= static_cast( surface.palette.numJoints ) ) { + return GPU_SKINNING_FALLBACK_JOINT_INDEX; + } + totalAbsoluteWeight += idMath::Fabs( weight ); + } + if ( totalAbsoluteWeight <= 0.0f ) { + return GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + } + } + return GPU_SKINNING_FALLBACK_NONE; +} + +bool R_GpuSkinning_GetSurface( const srfTriangles_s *tri, + gpuSkinningSurface_t &surface ) { + memset( &surface, 0, sizeof( surface ) ); + if ( tri == NULL ) { + surface.fallbackReason = GPU_SKINNING_FALLBACK_VERTEX_COUNT; + return false; + } + surface.bindPoseVerts = tri->gpuSkinningBindPoseVerts; + surface.skinVerts = reinterpret_cast( tri->gpuSkinningVerts ); + surface.numVerts = tri->numGpuSkinningVerts; + surface.palette.matrices = tri->gpuSkinningJointPalette; + surface.palette.numJoints = tri->numGpuSkinningJoints; + surface.palette.matrixStrideFloats = GPU_SKINNING_JOINT_FLOATS; + surface.palette.generation = tri->gpuSkinningPaletteGeneration; + memset( &surface.palette.buffer, 0, sizeof( surface.palette.buffer ) ); + surface.fallbackReason = static_cast( tri->gpuSkinningFallbackReason ); + surface.signedWeights = tri->gpuSkinningSignedWeights; + surface.fallbackReason = R_GpuSkinning_ValidateSurface( surface ); + return surface.fallbackReason == GPU_SKINNING_FALLBACK_NONE; +} + +bool R_GpuSkinning_IsCandidate( const srfTriangles_s *tri ) { + return tri != NULL && ( tri->gpuSkinningBindPoseVerts != NULL + || tri->gpuSkinningVerts != NULL || tri->numGpuSkinningVerts > 0 + || tri->gpuSkinningFallbackReason != GPU_SKINNING_FALLBACK_NONE ); +} + +void R_GpuSkinning_RecordCpuSkinning( uint64 elapsedMicroseconds, + int numVerts, bool positionsOnly ) { + if ( numVerts <= 0 ) { + return; + } + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.cpuSkinVertices, numVerts ); + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.cpuSkinMicroseconds, elapsedMicroseconds ); + if ( positionsOnly ) { + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.cpuPositionOnlyVertices, numVerts ); + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.cpuPositionOnlyMicroseconds, elapsedMicroseconds ); + } +} + +void R_GpuSkinning_RecordFallback( gpuSkinningFallbackReason_t reason ) { + if ( reason > GPU_SKINNING_FALLBACK_NONE && reason < GPU_SKINNING_FALLBACK_COUNT ) { + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.fallbacks[ reason ], 1 ); + } +} + +void R_GpuSkinning_RecordBackendPrepared( int numVerts ) { + R_GpuSkinning_RecordBackendPrepared( numVerts, 0, 0 ); +} + +void R_GpuSkinning_RecordBackendPrepared( int numVerts, int numJoints, + uint64 paletteBytes ) { + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.backendPrepared, 1 ); + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.surfacesAdmitted, 1 ); + if ( numVerts > 0 ) { + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.preparedVertices, numVerts ); + } + if ( numJoints > 0 ) { + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.paletteUploads, 1 ); + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.paletteJoints, numJoints ); + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.paletteBytes, paletteBytes ); + } +} + +const gpuSkinningStats_t &R_GpuSkinning_GetStats( void ) { + return rg_gpuSkinningStats; +} + +void R_GpuSkinning_ContractInit( const renderBackendCaps_t &caps ) { + memset( &rg_gpuSkinningStats, 0, sizeof( rg_gpuSkinningStats ) ); + if ( ++rg_gpuSkinningGeneration == 0 ) { + rg_gpuSkinningGeneration = 1; + } + rg_gpuSkinningInitialized = true; + R_BackendGpuSkinning_Init( caps ); +} + +void R_GpuSkinning_ContractShutdown( void ) { + if ( rg_gpuSkinningInitialized ) { + R_BackendGpuSkinning_Shutdown(); + } + rg_gpuSkinningInitialized = false; + if ( ++rg_gpuSkinningGeneration == 0 ) { + rg_gpuSkinningGeneration = 1; + } +} + +bool R_GpuSkinning_PrepareAmbientCache( srfTriangles_s *tri, bool needsLighting ) { + if ( !R_GpuSkinning_IsCandidate( tri ) ) { + return false; + } + R_GpuSkinning_SaturatingAdd( rg_gpuSkinningStats.surfaceAttempts, 1 ); + if ( !r_gpuSkinning.GetBool() ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_DISABLED ); + return false; + } + gpuSkinningSurface_t surface; + if ( !R_GpuSkinning_GetSurface( tri, surface ) ) { + R_GpuSkinning_RecordFallback( surface.fallbackReason ); + return false; + } + if ( !R_BackendGpuSkinning_PrepareAmbientCache( tri, needsLighting ) ) { + // False may mean a backend intentionally retains the CPU ambient cache + // and performs GPU deformation later (Vulkan). The backend records a + // classified failure itself when it is genuinely unavailable. + return false; + } + R_GpuSkinning_RecordBackendPrepared( surface.numVerts, + surface.palette.numJoints, + static_cast( surface.palette.numJoints ) + * GPU_SKINNING_JOINT_FLOATS * sizeof( float ) ); + return true; +} + +void R_GpuSkinning_PrintGfxInfo( void ) { + common->Printf( + "GPU skinning: enabled=%d generation=%u packed=%llu exact=%llu attempts=%llu admitted=%llu prepared=%llu cpuSkinVerts=%llu cpuSkinUs=%llu positionOnlyVerts=%llu positionOnlyUs=%llu preparedVertices=%llu paletteUploads=%llu paletteJoints=%llu paletteBytes=%llu cumulative=1\n", + r_gpuSkinning.GetBool() ? 1 : 0, rg_gpuSkinningGeneration, + rg_gpuSkinningStats.packedVertices, rg_gpuSkinningStats.exactVertices, + rg_gpuSkinningStats.surfaceAttempts, rg_gpuSkinningStats.surfacesAdmitted, + rg_gpuSkinningStats.backendPrepared, rg_gpuSkinningStats.cpuSkinVertices, + rg_gpuSkinningStats.cpuSkinMicroseconds, rg_gpuSkinningStats.cpuPositionOnlyVertices, + rg_gpuSkinningStats.cpuPositionOnlyMicroseconds, + rg_gpuSkinningStats.preparedVertices, rg_gpuSkinningStats.paletteUploads, + rg_gpuSkinningStats.paletteJoints, rg_gpuSkinningStats.paletteBytes ); + common->Printf( + "GPU skinning fallbacks: disabled=%llu backendUnavailable=%llu missingBindPose=%llu missingSkinVertices=%llu vertexCount=%llu residualWeights=%llu jointCount=%llu jointIndex=%llu malformedWeights=%llu skinScale=%llu unsupportedMaterial=%llu unsupportedPass=%llu stencilVolume=%llu decalOverlay=%llu paletteAllocation=%llu stalePalette=%llu\n", + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_DISABLED ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_MISSING_BIND_POSE ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_VERTEX_COUNT ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_JOINT_COUNT ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_JOINT_INDEX ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_SKIN_SCALE ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_UNSUPPORTED_MATERIAL ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_STENCIL_VOLUME ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_DECAL_OVERLAY ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION ], + rg_gpuSkinningStats.fallbacks[ GPU_SKINNING_FALLBACK_STALE_PALETTE ] ); + R_BackendGpuSkinning_PrintGfxInfo(); +} + +bool R_GpuSkinning_RunSelfTest( void ) { + gpuSkinningInfluence_t influences[5]; + memset( influences, 0, sizeof( influences ) ); + gpuSkinningVertex_t packed; + gpuSkinningPackResult_t result; + for ( int i = 0; i < GPU_SKINNING_INFLUENCES; ++i ) { + influences[i].jointIndex = i; + influences[i].weight = 0.25f; + } + if ( !R_GpuSkinning_PackVertexExact( influences, 4, 4, false, packed, result ) + || result.meaningfulInfluences != 4 ) { + return false; + } + for ( int influenceCount = 1; influenceCount <= GPU_SKINNING_INFLUENCES; ++influenceCount ) { + memset( influences, 0, sizeof( influences ) ); + for ( int i = 0; i < influenceCount; ++i ) { + influences[i].jointIndex = i; + influences[i].weight = 1.0f / influenceCount; + } + if ( !R_GpuSkinning_PackVertexExact( influences, influenceCount, 4, + false, packed, result ) || result.meaningfulInfluences != influenceCount ) { + return false; + } + for ( int i = 0; i < influenceCount; ++i ) { + if ( packed.jointIndices[i] != static_cast( i ) + || packed.jointWeights[i] != influences[i].weight ) { + return false; + } + } + } + + memset( influences, 0, sizeof( influences ) ); + for ( int i = 0; i < 5; ++i ) { + influences[i].jointIndex = i % 4; + influences[i].weight = 0.2f; + } + influences[4].jointIndex = 0; influences[4].weight = 0.01f; + if ( R_GpuSkinning_PackVertexExact( influences, 5, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS ) { + return false; + } + + memset( influences, 0, sizeof( influences ) ); + if ( R_GpuSkinning_PackVertexExact( influences, 4, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS + || R_GpuSkinning_PackVertexExact( influences, 0, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS ) { + return false; + } + + influences[0].weight = std::numeric_limits::quiet_NaN(); + if ( R_GpuSkinning_PackVertexExact( influences, 1, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS ) { + return false; + } + influences[0].weight = -0.5f; + if ( R_GpuSkinning_PackVertexExact( influences, 1, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS ) { + return false; + } + if ( !R_GpuSkinning_PackVertexExact( influences, 1, 4, true, packed, result ) ) { + return false; + } + influences[0].jointIndex = 4; + influences[0].weight = 1.0f; + if ( R_GpuSkinning_PackVertexExact( influences, 1, 4, false, packed, result ) + || result.reason != GPU_SKINNING_FALLBACK_JOINT_INDEX ) { + return false; + } + influences[0].jointIndex = 0; + if ( R_GpuSkinning_PackVertexExact( influences, 1, GPU_SKINNING_MAX_JOINTS + 1, + false, packed, result ) || result.reason != GPU_SKINNING_FALLBACK_JOINT_COUNT ) { + return false; + } + + // MD5R stores the fourth weight implicitly and permits it to be signed. + influences[0].jointIndex = 0; influences[0].weight = 0.6f; + influences[1].jointIndex = 1; influences[1].weight = 0.5f; + influences[2].jointIndex = 2; influences[2].weight = 0.2f; + influences[3].jointIndex = 3; + influences[3].weight = 1.0f - influences[0].weight + - influences[1].weight - influences[2].weight; + if ( !R_GpuSkinning_PackVertexExact( influences, 4, 4, true, packed, result ) + || packed.jointWeights[3] >= 0.0f || packed.jointWeights[3] != influences[3].weight ) { + return false; + } + + float matrices[ GPU_SKINNING_JOINT_FLOATS * 4 ]; + memset( matrices, 0, sizeof( matrices ) ); + for ( int jointIndex = 0; jointIndex < 4; ++jointIndex ) { + float *matrix = matrices + jointIndex * GPU_SKINNING_JOINT_FLOATS; + matrix[0] = matrix[5] = matrix[10] = 1.0f; + matrix[3] = static_cast( jointIndex + 1 ); + } + gpuSkinningJointPalette_t palette; + memset( &palette, 0, sizeof( palette ) ); + palette.matrices = matrices; + palette.numJoints = 4; + palette.matrixStrideFloats = GPU_SKINNING_JOINT_FLOATS; + palette.generation = rg_gpuSkinningGeneration; + idDrawVert bindPose; + bindPose.Clear(); + memset( bindPose.color2, 0, sizeof( bindPose.color2 ) ); + bindPose.xyz.Set( 1.0f, 2.0f, 3.0f ); + bindPose.normal.Set( 0.0f, 0.0f, 1.0f ); + bindPose.tangents[0].Set( 1.0f, 0.0f, 0.0f ); + bindPose.tangents[1].Set( 0.0f, 1.0f, 0.0f ); + bindPose.st.Set( 0.375f, 0.625f ); + bindPose.SetColor( 0x12345678u ); + bindPose.color2[0] = 0x12; + bindPose.color2[1] = 0x34; + bindPose.color2[2] = 0x56; + bindPose.color2[3] = 0x78; + memset( &packed, 0, sizeof( packed ) ); + packed.jointIndices[0] = 1; + packed.jointWeights[0] = 1.0f; + idDrawVert deformed; + if ( !R_GpuSkinning_DeformVertexCPU( bindPose, packed, palette, deformed ) + || !R_GpuSkinning_BasisClose( deformed.xyz, idVec3( 3.0f, 2.0f, 3.0f ), 0.00001f ) + || deformed.GetColor() != bindPose.GetColor() + || memcmp( deformed.color2, bindPose.color2, sizeof( bindPose.color2 ) ) != 0 + || deformed.st != bindPose.st ) { + return false; + } + + gpuSkinningSurface_t surface; + memset( &surface, 0, sizeof( surface ) ); + surface.bindPoseVerts = &bindPose; + surface.skinVerts = &packed; + surface.numVerts = 1; + surface.palette = palette; + surface.fallbackReason = GPU_SKINNING_FALLBACK_NONE; + surface.signedWeights = false; + if ( R_GpuSkinning_ValidateSurface( surface ) != GPU_SKINNING_FALLBACK_NONE ) { + return false; + } + surface.palette.generation = rg_gpuSkinningGeneration + 1; + if ( R_GpuSkinning_ValidateSurface( surface ) != GPU_SKINNING_FALLBACK_STALE_PALETTE ) { + return false; + } + surface.palette.generation = rg_gpuSkinningGeneration; + matrices[GPU_SKINNING_JOINT_FLOATS] = std::numeric_limits::quiet_NaN(); + if ( R_GpuSkinning_ValidateSurface( surface ) != GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS + || R_GpuSkinning_DeformVertexCPU( bindPose, packed, palette, deformed ) ) { + return false; + } + matrices[GPU_SKINNING_JOINT_FLOATS] = 1.0f; + surface.palette.matrixStrideFloats = GPU_SKINNING_JOINT_FLOATS - 1; + if ( R_GpuSkinning_ValidateSurface( surface ) != GPU_SKINNING_FALLBACK_JOINT_COUNT ) { + return false; + } + + return R_GpuSkinning_CompareVertexCPU( bindPose, bindPose, 0.0f, 0.0f ); +} diff --git a/src/renderer/GpuSkinning.h b/src/renderer/GpuSkinning.h new file mode 100644 index 00000000..608271ec --- /dev/null +++ b/src/renderer/GpuSkinning.h @@ -0,0 +1,151 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __GPU_SKINNING_H__ +#define __GPU_SKINNING_H__ + +#include "RendererCaps.h" + +class idDrawVert; +struct srfTriangles_s; + +static const int GPU_SKINNING_INFLUENCES = 4; +static const int GPU_SKINNING_JOINT_FLOATS = 12; +static const int GPU_SKINNING_MAX_JOINTS = 65536; + +enum gpuSkinningFallbackReason_t { + GPU_SKINNING_FALLBACK_NONE = 0, + GPU_SKINNING_FALLBACK_DISABLED, + GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE, + GPU_SKINNING_FALLBACK_MISSING_BIND_POSE, + GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES, + GPU_SKINNING_FALLBACK_VERTEX_COUNT, + GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS, + GPU_SKINNING_FALLBACK_JOINT_COUNT, + GPU_SKINNING_FALLBACK_JOINT_INDEX, + GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS, + GPU_SKINNING_FALLBACK_SKIN_SCALE, + GPU_SKINNING_FALLBACK_UNSUPPORTED_MATERIAL, + GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS, + GPU_SKINNING_FALLBACK_STENCIL_VOLUME, + GPU_SKINNING_FALLBACK_DECAL_OVERLAY, + GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION, + GPU_SKINNING_FALLBACK_STALE_PALETTE, + GPU_SKINNING_FALLBACK_COUNT +}; + +typedef struct gpuSkinningVertex_s { + uint32 jointIndices[ GPU_SKINNING_INFLUENCES ]; + float jointWeights[ GPU_SKINNING_INFLUENCES ]; +} gpuSkinningVertex_t; + +typedef struct gpuSkinningInfluence_s { + uint32 jointIndex; + float weight; +} gpuSkinningInfluence_t; + +typedef struct gpuSkinningPackResult_s { + gpuSkinningFallbackReason_t reason; + int meaningfulInfluences; + uint32 maxJointIndex; + float totalAbsoluteWeight; + float residualAbsoluteWeight; +} gpuSkinningPackResult_t; + +// Backend objects are intentionally opaque to the shared front end. A zero +// generation is always invalid; context/device recreation advances generation. +typedef struct gpuSkinningBufferHandle_s { + uint32 generation; + uint32 frameSlot; + uint32 offsetBytes; + uint32 sizeBytes; + uint64 backendObject; +} gpuSkinningBufferHandle_t; + +typedef struct gpuSkinningJointPalette_s { + const float * matrices; + int numJoints; + int matrixStrideFloats; + uint32 generation; + gpuSkinningBufferHandle_t buffer; +} gpuSkinningJointPalette_t; + +typedef struct gpuSkinningSurface_s { + const idDrawVert * bindPoseVerts; + const gpuSkinningVertex_t *skinVerts; + int numVerts; + gpuSkinningJointPalette_t palette; + gpuSkinningFallbackReason_t fallbackReason; + bool signedWeights; +} gpuSkinningSurface_t; + +typedef struct gpuSkinningStats_s { + uint64 packedVertices; + uint64 exactVertices; + uint64 surfaceAttempts; + uint64 surfacesAdmitted; + uint64 backendPrepared; + uint64 preparedVertices; + uint64 paletteUploads; + uint64 paletteJoints; + uint64 paletteBytes; + uint64 cpuSkinVertices; + uint64 cpuSkinMicroseconds; + uint64 cpuPositionOnlyVertices; + uint64 cpuPositionOnlyMicroseconds; + uint64 fallbacks[ GPU_SKINNING_FALLBACK_COUNT ]; +} gpuSkinningStats_t; + +extern idCVar r_gpuSkinning; + +const char *R_GpuSkinning_FallbackName( gpuSkinningFallbackReason_t reason ); +uint32 R_GpuSkinning_ContractGeneration( void ); +uint64 R_GpuSkinning_ReadMicroseconds( void ); + +bool R_GpuSkinning_PackVertexExact( const gpuSkinningInfluence_t *influences, + int numInfluences, int numJoints, bool allowSignedWeights, + gpuSkinningVertex_t &packed, gpuSkinningPackResult_t &result ); + +bool R_GpuSkinning_DeformVertexCPU( const idDrawVert &bindPose, + const gpuSkinningVertex_t &skinVertex, const gpuSkinningJointPalette_t &palette, + idDrawVert &deformed ); +bool R_GpuSkinning_CompareVertexCPU( const idDrawVert &expected, + const idDrawVert &actual, float positionEpsilon, float basisEpsilon ); + +void R_GpuSkinning_ClearSurfaceContract( srfTriangles_s *tri, + gpuSkinningFallbackReason_t reason ); +bool R_GpuSkinning_AttachSurfaceContract( srfTriangles_s *tri, + const idDrawVert *bindPoseVerts, const gpuSkinningVertex_t *skinVerts, + int numVerts, const float *jointMatrices, int numJoints, + int sourceMatrixStrideFloats, bool signedWeights, + gpuSkinningFallbackReason_t sourceFallback = GPU_SKINNING_FALLBACK_NONE ); +void R_GpuSkinning_ReferenceSurfaceContract( srfTriangles_s *tri, + const srfTriangles_s *reference ); +bool R_GpuSkinning_GetSurface( const srfTriangles_s *tri, + gpuSkinningSurface_t &surface ); +bool R_GpuSkinning_IsCandidate( const srfTriangles_s *tri ); +gpuSkinningFallbackReason_t R_GpuSkinning_ValidateSurface( + const gpuSkinningSurface_t &surface ); + +void R_GpuSkinning_RecordCpuSkinning( uint64 elapsedMicroseconds, + int numVerts, bool positionsOnly ); +void R_GpuSkinning_RecordFallback( gpuSkinningFallbackReason_t reason ); +void R_GpuSkinning_RecordBackendPrepared( int numVerts ); +void R_GpuSkinning_RecordBackendPrepared( int numVerts, int numJoints, + uint64 paletteBytes ); +const gpuSkinningStats_t &R_GpuSkinning_GetStats( void ); + +void R_GpuSkinning_ContractInit( const renderBackendCaps_t &caps ); +void R_GpuSkinning_ContractShutdown( void ); +bool R_GpuSkinning_PrepareAmbientCache( srfTriangles_s *tri, bool needsLighting ); +void R_GpuSkinning_PrintGfxInfo( void ); +bool R_GpuSkinning_RunSelfTest( void ); + +// Implemented once by each backend module. The shared gate validates the +// surface before it reaches this seam; false requests the existing CPU cache. +void R_BackendGpuSkinning_Init( const renderBackendCaps_t &caps ); +void R_BackendGpuSkinning_Shutdown( void ); +bool R_BackendGpuSkinning_PrepareAmbientCache( srfTriangles_s *tri, bool needsLighting ); +void R_BackendGpuSkinning_PrintGfxInfo( void ); + +#endif diff --git a/src/renderer/Image.h b/src/renderer/Image.h index 4f9ac200..0e1dfed0 100644 --- a/src/renderer/Image.h +++ b/src/renderer/Image.h @@ -60,6 +60,11 @@ typedef enum { // Preserve retail Quake 4's explicit high-quality / uncompressed material // bucket without renumbering the existing generated-image cache keys. TD_HIGH_QUALITY, + // Appended openQ4 PBR usages keep all historical binary-image cache keys + // stable. Colour inputs receive gamma-correct mip generation; material + // data (ORM/metallic/roughness/AO) always remains linear. + TD_PBR_COLOR, + TD_MATERIAL_DATA, } textureUsage_t; typedef enum { @@ -150,8 +155,10 @@ class idImage { void GenerateCubeImage(const byte* pic[6], int size, textureFilter_t filter, textureUsage_t usage); - void CopyFramebuffer(int x, int y, int width, int height); - void CopyDepthbuffer(int x, int y, int width, int height); + bool CopyFramebuffer(int x, int y, int width, int height, + int cubeFace = 0); + bool CopyDepthbuffer(int x, int y, int width, int height, + int cubeFace = 0); void UploadScratch(const byte* pic, int width, int height); @@ -173,7 +180,9 @@ class idImage { int GetUploadHeight() const { return opts.height; } textureFilter_t GetFilter() const { return filter; } textureRepeat_t GetRepeat() const { return repeat; } + textureUsage_t GetUsage() const { return usage; } bool IsDefaulted() const { return defaulted; } + bool IsScratchImage() const { return scratchImage; } void SetReferencedOutsideLevelLoad() { referencedOutsideLevelLoad = true; } void SetReferencedInsideLevelLoad() { levelLoadReferenced = true; } @@ -248,6 +257,7 @@ class idImage { bool referencedOutsideLevelLoad; bool levelLoadReferenced; // for determining if it needs to be purged bool defaulted; // true if the default image was generated because a file couldn't be loaded + bool scratchImage; // storage is owned/mutated by a runtime render target or upload path ID_TIME_T sourceFileTime; // the most recent of all images used in creation, for reloadImages command ID_TIME_T binaryFileTime; // the time stamp of the binary file idStr loadedSourceName; // source or automatic DDS replacement used by the last successful load @@ -285,6 +295,7 @@ ID_INLINE idImage::idImage(const char* name) : imgName(name) { referencedOutsideLevelLoad = false; levelLoadReferenced = false; defaulted = false; + scratchImage = false; sourceFileTime = FILE_NOT_FOUND_TIMESTAMP; binaryFileTime = FILE_NOT_FOUND_TIMESTAMP; loadedSourceName.Clear(); @@ -292,6 +303,12 @@ ID_INLINE idImage::idImage(const char* name) : imgName(name) { useCount = 0; } +// Mutable renderer-owned images must never be treated as static PBR material +// resources. The name form catches targets before lazy allocation; the image +// form also catches arbitrary names registered through ScratchImage(). +bool R_IsMutableRenderImageName( const char *name ); +bool R_IsMutableRenderImage( const idImage *image ); + // data is RGBA bool R_WriteTGA(const char* filename, const byte* data, int width, int height, bool flipVertical = false, const char* basePath = "fs_savepath"); @@ -452,6 +469,9 @@ IMAGEFILES void R_LoadImage(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2); void R_LoadImageForUsage(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamp, bool makePowerOf2, textureUsage_t usage); bool R_ResolvePreferredDDSImageSource(const char* name, idStr& ddsName, ID_TIME_T* timestamp, bool allowPrecompressedDDS, bool* precompressedDDS); +// enables per-candidate DDS probe memoization for the duration of a level +// load; disabling also clears all memoized probe results +void R_SetDDSProbeCacheActive(bool active); bool R_LoadPrecompressedDDS(const char* name, idBinaryImage& image, ID_TIME_T* timestamp, textureUsage_t usage, const imageDownsizePolicy_t& downsizePolicy, bool useMipmaps); bool R_ImageDDS_RunSelfTest(); // pic is in top to bottom raster format @@ -465,6 +485,8 @@ IMAGEPROGRAM ==================================================================== */ -void R_LoadImageProgram(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamp, textureUsage_t* usage = NULL); +// Returns the parser's load result. Direct-file programs return false when the +// source is missing; a valid PK4-backed source may still have timestamp zero. +bool R_LoadImageProgram(const char* name, byte** pic, int* width, int* height, ID_TIME_T* timestamp, textureUsage_t* usage = NULL); const char* R_ParsePastImageProgram(idLexer& src); diff --git a/src/renderer/ImageManager.cpp b/src/renderer/ImageManager.cpp index 5dfdf5ac..cee56b4c 100644 --- a/src/renderer/ImageManager.cpp +++ b/src/renderer/ImageManager.cpp @@ -32,6 +32,32 @@ If you have questions concerning this license or the applicable additional terms #include "tr_local.h" +bool R_IsMutableRenderImageName( const char *name ) { + if ( name == NULL || name[0] == '\0' ) { + return false; + } + static const char *prefixes[] = { + "_cinematic", "_scratch", "_accum", + "_reflectionRender", "_refractionRender", + "_currentRender", "_originalCurrentRender", "_currentDepth", + "_forwardRenderResolved", "_postProcessAlbedo", + "_scenePreserveDepth", "_hdrScene", "_ssao", "_cel", + "_motionVector", "_bloom", "_hdrLum", "_underwaterDepth", + "_shadowMap", "_pointShadowMap", + "_translucentShadowMap", "_pointTranslucentShadowMap" + }; + for ( int i = 0; i < static_cast( sizeof( prefixes ) / sizeof( prefixes[0] ) ); ++i ) { + if ( idStr::Icmpn( name, prefixes[i], static_cast( strlen( prefixes[i] ) ) ) == 0 ) { + return true; + } + } + return !idStr::Icmp( name, "BlurTexture1" ) || !idStr::Icmp( name, "DepthTexture" ); +} + +bool R_IsMutableRenderImage( const idImage *image ) { + return image != NULL && ( image->IsScratchImage() || R_IsMutableRenderImageName( image->GetName() ) ); +} + // do this with a pointer, in case we want to make the actual manager // a private virtual subclass idImageManager imageManager; @@ -289,6 +315,22 @@ static bool R_IsQ4LightImageNamespace( const char *name ) { || idStr::Icmpn( name, "gfx/lights/", 11 ) == 0 ); } +static textureUsage_t R_ImageUsageForName( const char *name, textureUsage_t requestedUsage ) { + // Namespace remaps are a legacy convenience for generic callers only. + // Explicit PBR and high-quality usage classes are cache/storage contracts + // and must survive regardless of the source path chosen by an author. + if ( requestedUsage != TD_DEFAULT ) { + return requestedUsage; + } + if ( idStr::Icmpn( name, "fonts", 5 ) == 0 || idStr::Icmpn( name, "newfonts", 8 ) == 0 ) { + return TD_FONT; + } + if ( R_IsQ4LightImageNamespace( name ) ) { + return TD_LIGHT; + } + return requestedUsage; +} + static bool R_IsQ4PresentationImageNamespace( const char *name ) { return name != NULL && ( idStr::Icmpn( name, "gfx/guis/", 9 ) == 0 @@ -594,14 +636,7 @@ idImage *idImageManager::GetImageWithParameters( const char *_name, textureFilte declManager->MediaPrint( "DEFAULTED\n" ); return globalImages->defaultImage; } - if ( usage == TD_DEFAULT ) { - if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) { - usage = TD_FONT; - } - if ( R_IsQ4LightImageNamespace( _name ) ) { - usage = TD_LIGHT; - } - } + usage = R_ImageUsageForName( _name, usage ); // strip any .tga file extensions from anywhere in the _name, including image program parameters idStr name = _name; name.Replace( ".tga", "" ); @@ -648,12 +683,12 @@ idImage *idImageManager::ImageFromFile( const char *_name, textureFilter_t filte declManager->MediaPrint( "DEFAULTED\n" ); return globalImages->defaultImage; } - if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) { - usage = TD_FONT; - } - if ( R_IsQ4LightImageNamespace( _name ) ) { - usage = TD_LIGHT; - } + usage = R_ImageUsageForName( _name, usage ); + fileSystem->RecordLevelLoadResource( LEVEL_LOAD_RESOURCE_IMAGE, _name, + va( "filter=%d;repeat=%d;usage=%d;cube=%d;downsize=%d;flags=%u", + static_cast( filter ), static_cast( repeat ), + static_cast( usage ), static_cast( cubeMap ), + allowDownSize ? 1 : 0, flags ), 0, 1 ); // strip any .tga file extensions from anywhere in the _name, including image program parameters idStr name = _name; @@ -743,16 +778,7 @@ idImage *idImageManager::ImageHandleDeferred( const char *_name, textureFilter_t declManager->MediaPrint( "DEFAULTED\n" ); return globalImages->defaultImage; } - if ( usage == TD_DEFAULT ) { - // Keep the legacy convenience remap for generic callers, but preserve any - // explicit material-requested usage class such as TD_HIGH_QUALITY. - if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) { - usage = TD_FONT; - } - if ( R_IsQ4LightImageNamespace( _name ) ) { - usage = TD_LIGHT; - } - } + usage = R_ImageUsageForName( _name, usage ); idStr name = _name; name.Replace( ".tga", "" ); @@ -817,6 +843,7 @@ idImage * idImageManager::ScratchImage( const char *_name, idImageOpts *imgOpts, for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) { idImage * image = images[i]; if ( name.Icmp( image->GetName() ) == 0 ) { + image->scratchImage = true; image->usage = usage; image->levelLoadReferenced = true; image->referencedOutsideLevelLoad = true; @@ -839,6 +866,7 @@ idImage * idImageManager::ScratchImage( const char *_name, idImageOpts *imgOpts, // idImage* newImage = AllocImage( name ); if ( newImage != NULL ) { + newImage->scratchImage = true; newImage->usage = usage; newImage->levelLoadReferenced = true; newImage->referencedOutsideLevelLoad = true; @@ -901,6 +929,10 @@ ReloadImages =============== */ void idImageManager::ReloadImages( bool all ) { + // a reload exists to observe files dropped since the last level load; + // never let it consult (or leave behind) memoized probe results + R_SetDDSProbeCacheActive( false ); + for ( int i = 0 ; i < globalImages->images.Num() ; i++ ) { globalImages->images[ i ]->Reload( all ); } @@ -1090,6 +1122,10 @@ Frees all images used by the previous level void idImageManager::BeginLevelLoad() { insideLevelLoad = true; + // search paths are stable for the whole load, so DDS replacement probes + // can be memoized until EndLevelLoad finishes LoadLevelImages + R_SetDDSProbeCacheActive( true ); + for ( int i = 0 ; i < images.Num() ; i++ ) { idImage *image = images[ i ]; image->ClearUseCount(); @@ -1241,6 +1277,8 @@ void idImageManager::EndLevelLoad() { common->Printf( "%5i images loaded in %5.1f seconds\n", loadCount, (end-start) * 0.001 ); common->Printf( "----------------------------------------\n" ); //R_ListImages_f( idCmdArgs( "sorted sorted", false ) ); + + R_SetDDSProbeCacheActive( false ); } /* diff --git a/src/renderer/Image_load.cpp b/src/renderer/Image_load.cpp index 13182506..c337dd31 100644 --- a/src/renderer/Image_load.cpp +++ b/src/renderer/Image_load.cpp @@ -54,10 +54,114 @@ static bool R_ShouldSuppressMissingImageWarning( const char * imageName ) { return false; } +/* +======================== +R_FindMissingQ4StockImageFallback + +Some retail effect materials name images that were not shipped in any Quake 4 +PK4. Keep the authored name as the image identity so a loose/mod replacement +wins, but use the closest neutral stock shape when that primary lookup fails. +======================== +*/ +static const char *R_FindMissingQ4StockImageFallback( const char *imageName ) { + if ( imageName == NULL || imageName[0] == '\0' ) { + return NULL; + } + + idStr canonicalName = imageName; + canonicalName.BackSlashesToSlashes(); + canonicalName.StripFileExtension(); + + struct q4StockImageFallback_t { + const char *missingName; + const char *replacementName; + }; + static const q4StockImageFallback_t fallbacks[] = { + { "gfx/effects/fluids_drips/brown_bubble", + "gfx/effects/fluids_drips/bubble_alpha.tga" }, + { "gfx/effects/fluids_drips/brown_bubble_half", + "gfx/effects/fluids_drips/bubble_half.tga" }, + { "gfx/effects/fluids_drips/brown_splash_line", + "gfx/effects/fluids_drips/splash_line.tga" } + }; + + for ( int i = 0; i < (int)( sizeof( fallbacks ) / sizeof( fallbacks[0] ) ); i++ ) { + if ( canonicalName.Icmp( fallbacks[i].missingName ) == 0 ) { + return fallbacks[i].replacementName; + } + } + + return NULL; +} + +/* +======================== +R_SelectMissingQ4StockImageSource + +Resolve only the known incomplete retail declarations. Use the image-program +parser's load result rather than its timestamp: valid files inside retail PK4s +can report timestamp zero. The authored name always wins when present so adding +a loose/mod replacement immediately supersedes stock. +======================== +*/ +static bool R_SelectMissingQ4StockImageSource( const char *imageName, idStr &selectedSourceName, + ID_TIME_T &selectedSourceTime, bool &stockFallbackSelected ) { + const char *stockFallbackName = R_FindMissingQ4StockImageFallback( imageName ); + if ( stockFallbackName == NULL ) { + return false; + } + + selectedSourceName = imageName; + selectedSourceTime = 0; + stockFallbackSelected = false; + if ( R_LoadImageProgram( imageName, NULL, NULL, NULL, &selectedSourceTime ) ) { + return true; + } + + selectedSourceTime = 0; + if ( R_LoadImageProgram( stockFallbackName, NULL, NULL, NULL, &selectedSourceTime ) ) { + selectedSourceName = stockFallbackName; + stockFallbackSelected = true; + } + return true; +} + +/* +======================== +R_AddMissingQ4StockImageCacheIdentity + +The three logical retail names need separate authored and fallback cache paths. +Besides preventing equal timestamps from aliasing the two sources, this keeps +legacy unsalted .bimage files from being accepted for either selection. +======================== +*/ +static void R_AddMissingQ4StockImageCacheIdentity( idStr &generatedName, bool stockFallbackSelected ) { + idStr extension; + generatedName.ExtractFileExtension( extension ); + generatedName.StripFileExtension(); + generatedName += stockFallbackSelected ? "#q4stockfallback" : "#q4authored"; + if ( extension.Length() > 0 ) { + generatedName.SetFileExtension( extension ); + } +} + static unsigned int R_GetImageDownsizeSignature( const char *name, textureUsage_t usage, bool allowDownSize ); static void R_DownsizeLoadedImageData( const char *name, textureUsage_t usage, bool allowDownSize, byte *&pic, int &width, int &height ); static void R_DownsizeLoadedCubeImageData( const char *name, textureUsage_t usage, bool allowDownSize, byte *pics[6], int &size ); +static void R_LoadImageProgramForDeclaredUsage( const char *name, byte **pic, int *width, int *height, + ID_TIME_T *timestamp, textureUsage_t &usage ) { + // Classic image programs infer TD_BUMP for normal-producing operations. + // PBR semantics are explicit authoring contracts and form part of the image + // cache key, so decoding must not mutate them after lookup/name generation. + const textureUsage_t declaredUsage = usage; + textureUsage_t inferredUsage = usage; + R_LoadImageProgram( name, pic, width, height, timestamp, &inferredUsage ); + if ( declaredUsage != TD_PBR_COLOR && declaredUsage != TD_MATERIAL_DATA ) { + usage = inferredUsage; + } +} + /* ======================== idImage::DeriveOpts @@ -117,6 +221,16 @@ ID_INLINE void idImage::DeriveOpts() { opts.colorFormat = CFM_DEFAULT; opts.format = FMT_RGBA8; break; + case TD_PBR_COLOR: + opts.gammaMips = true; + opts.colorFormat = CFM_DEFAULT; + opts.format = FMT_RGBA8; + break; + case TD_MATERIAL_DATA: + opts.gammaMips = false; + opts.colorFormat = CFM_DEFAULT; + opts.format = FMT_RGBA8; + break; default: opts.gammaMips = false; opts.format = FMT_RGBA8; @@ -453,7 +567,7 @@ void idImage::ActuallyLoadImage( bool fromBackEnd ) { R_ResolvePreferredDDSImageSource( GetName(), preferredDDSName, &preferredDDSFileTime, true, &preferredDDSPrecompressed ); if ( preferredDDSImage && !fileSystem->InProductionMode() ) { ID_TIME_T originalSourceTime = FILE_NOT_FOUND_TIMESTAMP; - R_LoadImageProgram( GetName(), NULL, NULL, NULL, &originalSourceTime, &usage ); + R_LoadImageProgramForDeclaredUsage( GetName(), NULL, NULL, NULL, &originalSourceTime, usage ); if ( R_IsPreferredDDSStale( preferredDDSName, preferredDDSFileTime, originalSourceTime ) ) { if ( cvarSystem->GetCVarBool( "image_showPrecompressedTextures" ) ) { common->Printf( "Ignoring stale DDS replacement %s for %s\n", preferredDDSName.c_str(), GetName() ); @@ -470,7 +584,6 @@ void idImage::ActuallyLoadImage( bool fromBackEnd ) { if ( preferredDDSImage && cvarSystem->GetCVarBool( "image_showPrecompressedTextures" ) ) { common->Printf( "Using DDS replacement %s for %s\n", preferredDDSName.c_str(), GetName() ); } - const char *loadSourceName = preferredDDSImage ? preferredDDSName.c_str() : GetName(); const bool selectedDDSImage = explicitDDSImage || preferredDDSImage; const bool bypassGeneratedFile = explicitDDSImage || preferredDDSPrecompressed; idStr selectedSourceName = GetName(); @@ -490,6 +603,18 @@ void idImage::ActuallyLoadImage( bool fromBackEnd ) { } } } + bool q4StockImageCandidateResolved = false; + if ( !explicitDDSImage && !preferredDDSImage && cubeFiles == CF_2D ) { + bool stockFallbackSelected = false; + q4StockImageCandidateResolved = R_SelectMissingQ4StockImageSource( GetName(), selectedSourceName, + sourceFileTime, stockFallbackSelected ); + if ( q4StockImageCandidateResolved ) { + sourceFileTimeKnown = true; + R_AddMissingQ4StockImageCacheIdentity( generatedName, stockFallbackSelected ); + } + } + idStr selectedLoadSourceName = preferredDDSImage ? preferredDDSName : selectedSourceName; + const char *loadSourceName = selectedLoadSourceName.c_str(); idBinaryImage im( generatedName ); if ( bypassGeneratedFile ) { @@ -534,27 +659,44 @@ void idImage::ActuallyLoadImage( bool fromBackEnd ) { } } } - if ( binaryFileTime != FILE_NOT_FOUND_TIMESTAMP && !fileSystem->InProductionMode() ) { - if ( !sourceFileTimeKnown ) { - if ( cubeFiles != CF_2D ) { - R_LoadCubeImages( GetName(), cubeFiles, NULL, NULL, &sourceFileTime ); - } else if ( preferredDDSImage ) { - sourceFileTime = preferredDDSFileTime; - } else { - R_LoadImageProgram( GetName(), NULL, NULL, NULL, &sourceFileTime, &usage ); + const bool productionMode = fileSystem->InProductionMode(); + auto acceptGeneratedImage = [&]( ID_TIME_T candidateFileTime ) -> bool { + if ( candidateFileTime == FILE_NOT_FOUND_TIMESTAMP ) { + return false; + } + if ( !productionMode ) { + if ( !sourceFileTimeKnown ) { + if ( cubeFiles != CF_2D ) { + R_LoadCubeImages( GetName(), cubeFiles, NULL, NULL, &sourceFileTime ); + } else if ( preferredDDSImage ) { + sourceFileTime = preferredDDSFileTime; + } else { + R_LoadImageProgramForDeclaredUsage( GetName(), NULL, NULL, NULL, &sourceFileTime, usage ); + } + sourceFileTimeKnown = true; + } + if ( im.GetFileHeader().sourceFileTime != sourceFileTime ) { + im.Clear(); + return false; } - sourceFileTimeKnown = true; } - if ( im.GetFileHeader().sourceFileTime != sourceFileTime ) { + if ( !R_BinaryImageHeaderSupportedByRenderer( im.GetFileHeader() ) ) { im.Clear(); - binaryFileTime = FILE_NOT_FOUND_TIMESTAMP; + return false; } - } + if ( !productionMode && !R_GeneratedImageHeaderMatchesDerivedOpts( im.GetFileHeader(), opts, usage ) ) { + im.Clear(); + return false; + } + return true; + }; - const bool binaryImageAvailable = binaryFileTime != FILE_NOT_FOUND_TIMESTAMP && R_BinaryImageHeaderSupportedByRenderer( im.GetFileHeader() ); - if ( ( fileSystem->InProductionMode() && binaryImageAvailable ) || ( binaryImageAvailable - && R_GeneratedImageHeaderMatchesDerivedOpts( im.GetFileHeader(), opts, usage ) - ) ) { + bool generatedImageAccepted = acceptGeneratedImage( binaryFileTime ); + if ( !generatedImageAccepted && !bypassGeneratedFile ) { + binaryFileTime = im.LoadFromCompactGeneratedFileUnchecked(); + generatedImageAccepted = acceptGeneratedImage( binaryFileTime ); + } + if ( generatedImageAccepted ) { const bimageFile_t & header = im.GetFileHeader(); opts.width = header.width; opts.height = header.height; @@ -653,12 +795,22 @@ void idImage::ActuallyLoadImage( bool fromBackEnd ) { } // load the full specification, and perform any image program calculations - R_LoadImageProgram( fallbackLoadSourceName, &pic, &width, &height, &sourceFileTime, &usage ); + R_LoadImageProgramForDeclaredUsage( fallbackLoadSourceName, &pic, &width, &height, &sourceFileTime, usage ); if ( pic == NULL && preferredDDSImage && !preferredDDSPrecompressed ) { common->Warning( "Couldn't decode preferred DDS replacement %s for %s; falling back to original source", loadSourceName, GetName() ); selectedSourceName = GetName(); sourceFileTime = FILE_NOT_FOUND_TIMESTAMP; - R_LoadImageProgram( GetName(), &pic, &width, &height, &sourceFileTime, &usage ); + R_LoadImageProgramForDeclaredUsage( GetName(), &pic, &width, &height, &sourceFileTime, usage ); + } + if ( pic == NULL && !q4StockImageCandidateResolved ) { + const char *stockFallbackName = R_FindMissingQ4StockImageFallback( GetName() ); + if ( stockFallbackName != NULL ) { + sourceFileTime = FILE_NOT_FOUND_TIMESTAMP; + R_LoadImageProgramForDeclaredUsage( stockFallbackName, &pic, &width, &height, &sourceFileTime, usage ); + if ( pic != NULL ) { + selectedSourceName = stockFallbackName; + } + } } sourceFileTimeKnown = true; @@ -857,7 +1009,7 @@ been; anything else falls back to a general resample. */ static bool R_ImageUsageUsesGammaMips( textureUsage_t usage ) { // mirrors the gammaMips choices DeriveOpts makes for each usage - return usage == TD_FONT || usage == TD_LIGHT; + return usage == TD_FONT || usage == TD_LIGHT || usage == TD_PBR_COLOR; } static int R_CountExactHalvings( int width, int height, int scaledWidth, int scaledHeight ) { @@ -1021,13 +1173,41 @@ void R_PurgeFramebufferCopyFBOs( void ) { } } +// A cubemap is complete only when all six faces have matching storage. A +// framebuffer/depth copy writes a single sealed face, so resize every face +// first and then use the selected face as the transfer destination. +static void R_AllocateCopyTextureStorage( bool isCube, GLenum copyTarget, + GLint copyInternalFormat, int width, int height, GLenum copyDataFormat, + GLenum copyDataType ) { + if ( !isCube ) { + glTexImage2D( copyTarget, 0, copyInternalFormat, width, height, 0, + copyDataFormat, copyDataType, NULL ); + return; + } + for ( int face = 0; face < 6; ++face ) { + glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT + face, 0, + copyInternalFormat, width, height, 0, copyDataFormat, copyDataType, + NULL ); + } +} + /* ==================== CopyFramebuffer ==================== */ -void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { - R_BindTextureForDirectAccess( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D, texnum ); +bool idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight, + int cubeFace ) { + const bool isCube = opts.textureType == TT_CUBIC; + if ( imageWidth <= 0 || imageHeight <= 0 + || ( isCube && ( cubeFace < 0 || cubeFace >= 6 ) ) + || ( !isCube && cubeFace != 0 ) ) { + return false; + } + const GLenum textureTarget = isCube ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D; + const GLenum copyTarget = isCube + ? GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT + cubeFace : GL_TEXTURE_2D; + R_BindTextureForDirectAccess( textureTarget, texnum ); const bool readingFromRenderTexture = ( backEnd.renderTexture != NULL ) && ( backEnd.renderTexture->GetNumColorImages() > 0 ); const GLenum readAttachment = GL_COLOR_ATTACHMENT0; @@ -1053,19 +1233,21 @@ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { const GLuint copyFbo = r_copyFramebufferFbo; if ( needsStorageResize ) { - glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_RGBA8, imageWidth, imageHeight, 0, - dataFormat != 0 ? dataFormat : GL_RGBA, dataType != 0 ? dataType : GL_UNSIGNED_BYTE, NULL ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + R_AllocateCopyTextureStorage( isCube, copyTarget, + internalFormat != 0 ? internalFormat : GL_RGBA8, imageWidth, + imageHeight, dataFormat != 0 ? dataFormat : GL_RGBA, + dataType != 0 ? dataType : GL_UNSIGNED_BYTE ); + glTexParameterf( textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glTexParameterf( textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); } glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() ); glReadBuffer( readAttachment ); glBindFramebuffer( GL_DRAW_FRAMEBUFFER, copyFbo ); - glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texnum, 0 ); + glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, copyTarget, texnum, 0 ); glDrawBuffer( GL_COLOR_ATTACHMENT0 ); // glBlitFramebuffer obeys scissor state; ensure the copy is not clipped by a prior light scissor. @@ -1082,7 +1264,7 @@ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { glEnable( GL_SCISSOR_TEST ); } - glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0 ); + glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, copyTarget, 0, 0 ); glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo ); glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo ); @@ -1109,10 +1291,16 @@ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { glDisable( GL_SCISSOR_TEST ); } - if ( needsStorageResize ) { - glCopyTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_RGBA8, x, y, imageWidth, imageHeight, 0 ); + if ( needsStorageResize && !isCube ) { + glCopyTexImage2D( copyTarget, 0, internalFormat != 0 ? internalFormat : GL_RGBA8, x, y, imageWidth, imageHeight, 0 ); } else { - glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); + if ( needsStorageResize ) { + R_AllocateCopyTextureStorage( true, copyTarget, + internalFormat != 0 ? internalFormat : GL_RGBA8, imageWidth, + imageHeight, dataFormat != 0 ? dataFormat : GL_RGBA, + dataType != 0 ? dataType : GL_UNSIGNED_BYTE ); + } + glCopyTexSubImage2D( copyTarget, 0, 0, 0, x, y, imageWidth, imageHeight ); } if ( scissorWasEnabled ) { @@ -1124,14 +1312,15 @@ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { glReadBuffer( previousReadBuffer ); if ( needsStorageResize ) { - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + glTexParameterf( textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glTexParameterf( textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); } } // backEnd.pc.c_copyFrameBuffer++; + return true; } /* @@ -1139,8 +1328,18 @@ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) { CopyDepthbuffer ==================== */ -void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { - R_BindTextureForDirectAccess( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D, texnum ); +bool idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight, + int cubeFace ) { + const bool isCube = opts.textureType == TT_CUBIC; + if ( imageWidth <= 0 || imageHeight <= 0 + || ( isCube && ( cubeFace < 0 || cubeFace >= 6 ) ) + || ( !isCube && cubeFace != 0 ) ) { + return false; + } + const GLenum textureTarget = isCube ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D; + const GLenum copyTarget = isCube + ? GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT + cubeFace : GL_TEXTURE_2D; + R_BindTextureForDirectAccess( textureTarget, texnum ); // The destination must hold depth-renderable storage: it gets attached to // GL_DEPTH_ATTACHMENT for the blit path and receives GL_DEPTH_COMPONENT @@ -1192,12 +1391,15 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { const GLuint copyDepthFbo = r_copyDepthbufferFbo; if ( needsStorageResize ) { - glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, imageWidth, imageHeight, 0, - dataFormat != 0 ? dataFormat : GL_DEPTH_COMPONENT, dataType != 0 ? dataType : GL_FLOAT, NULL ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + R_AllocateCopyTextureStorage( isCube, copyTarget, + internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, + imageWidth, imageHeight, + dataFormat != 0 ? dataFormat : GL_DEPTH_COMPONENT, + dataType != 0 ? dataType : GL_FLOAT ); + glTexParameterf( textureTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); + glTexParameterf( textureTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); } if ( readingFromRenderTexture ) { @@ -1208,7 +1410,7 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { } glBindFramebuffer( GL_DRAW_FRAMEBUFFER, copyDepthFbo ); - glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texnum, 0 ); + glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, copyTarget, texnum, 0 ); glReadBuffer( GL_NONE ); glDrawBuffer( GL_NONE ); @@ -1226,13 +1428,13 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { glEnable( GL_SCISSOR_TEST ); } - glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0 ); + glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, copyTarget, 0, 0 ); glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo ); glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo ); glReadBuffer( previousReadBuffer ); glDrawBuffer( previousDrawBuffer ); - return; + return true; } if ( sourceDepthIsMSAA ) { @@ -1244,7 +1446,7 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { const int pixelCount = imageWidth * imageHeight; if ( pixelCount <= 0 ) { - return; + return false; } GLint previousReadFbo = 0; @@ -1281,17 +1483,24 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { // The readback buffer always holds GL_FLOAT depth values regardless of // the texture's preferred upload type. - if ( needsStorageResize ) { - glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, imageWidth, imageHeight, 0, + if ( needsStorageResize && !isCube ) { + glTexImage2D( copyTarget, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, imageWidth, imageHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, resolvedDepthBuffer.Ptr() ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); - glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); } else { - glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, imageWidth, imageHeight, + if ( needsStorageResize ) { + R_AllocateCopyTextureStorage( true, copyTarget, + internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, + imageWidth, imageHeight, GL_DEPTH_COMPONENT, GL_FLOAT ); + } + glTexSubImage2D( copyTarget, 0, 0, 0, imageWidth, imageHeight, GL_DEPTH_COMPONENT, GL_FLOAT, resolvedDepthBuffer.Ptr() ); } + if ( needsStorageResize ) { + glTexParameterf( textureTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); + glTexParameterf( textureTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameterf( textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + } glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo ); glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo ); @@ -1316,10 +1525,17 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { glDisable( GL_SCISSOR_TEST ); } - if ( needsStorageResize ) { - glCopyTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 ); + if ( needsStorageResize && !isCube ) { + glCopyTexImage2D( copyTarget, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 ); } else { - glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); + if ( needsStorageResize ) { + R_AllocateCopyTextureStorage( true, copyTarget, + internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT, + imageWidth, imageHeight, + dataFormat != 0 ? dataFormat : GL_DEPTH_COMPONENT, + dataType != 0 ? dataType : GL_FLOAT ); + } + glCopyTexSubImage2D( copyTarget, 0, 0, 0, x, y, imageWidth, imageHeight ); } if ( scissorWasEnabled ) { @@ -1332,6 +1548,7 @@ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { } //backEnd.pc.c_copyFrameBuffer++; + return true; } /* @@ -1535,7 +1752,11 @@ void idImage::Reload( bool force ) { } } else { // get the current values - R_LoadImageProgram( imgName, NULL, NULL, NULL, ¤t ); + bool stockFallbackSelected = false; + if ( !R_SelectMissingQ4StockImageSource( imgName, currentSourceName, + current, stockFallbackSelected ) ) { + R_LoadImageProgram( imgName, NULL, NULL, NULL, ¤t ); + } } } const bool sourceSelectionChanged = loadedSourceName.Length() == 0 || loadedSourceName.Icmp( currentSourceName ) != 0; diff --git a/src/renderer/Interaction.cpp b/src/renderer/Interaction.cpp index 23e051f5..49602a16 100644 --- a/src/renderer/Interaction.cpp +++ b/src/renderer/Interaction.cpp @@ -111,11 +111,19 @@ static bool R_TranslucentShadowMapMomentsSupportedForLight( const idRenderLightL // their translucent caster chains. Reject that tier explicitly so stale // OpenGL capability state can never admit casters Vulkan would omit; its // stock translucent shadow casters instead use binary stencil-parity depth. - const char *activeRenderApi = - cvarSystem != NULL - ? cvarSystem->GetCVarString( "r_actualRenderApi" ) - : ""; - if ( idStr::Icmp( activeRenderApi, "vulkan" ) == 0 ) { + // r_actualRenderApi is CVAR_ROM, set once at renderer-module selection; + // re-resolve the by-name lookup at most once per frame instead of per call. + static int cachedApiFrame = -1; + static bool cachedVulkanApi = false; + if ( cachedApiFrame != idLib::frameNumber ) { + const char *activeRenderApi = + cvarSystem != NULL + ? cvarSystem->GetCVarString( "r_actualRenderApi" ) + : ""; + cachedVulkanApi = idStr::Icmp( activeRenderApi, "vulkan" ) == 0; + cachedApiFrame = idLib::frameNumber; + } + if ( cachedVulkanApi ) { return false; } return r_shadowMapTranslucentMoments.GetBool() && @@ -129,17 +137,17 @@ static bool R_TranslucentShadowMapMomentsSupportedForLight( const idRenderLightL /* ================= -R_VulkanShadowMapsNeedPerSurfaceStencilVolumes +R_ShadowMapsNeedPerSurfaceStencilVolumes Optimized prelights combine every static-world caster for a light into one volume. A partial filtered shadow map needs only S\M as its stencil supplement, so the combined volume cannot be used without hardening casters already represented in the map. Keep the stock per-surface volume path for -Vulkan lights that can actually use mapped shadows; stencil-only and OpenGL -paths retain the optimized prelight behavior. +every backend light that can actually use mapped shadows; stencil-only paths +retain the optimized prelight behavior. ================= */ -bool R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( +bool R_ShadowMapsNeedPerSurfaceStencilVolumes( const idRenderLightLocal *lightDef ) { if ( lightDef == NULL || !r_shadows.GetBool() || !r_useShadowMap.GetBool() ) { @@ -149,11 +157,7 @@ bool R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( !r_shadowMapPointLights.GetBool() ) { return false; } - const char *activeRenderApi = - cvarSystem != NULL - ? cvarSystem->GetCVarString( "r_actualRenderApi" ) - : ""; - return idStr::Icmp( activeRenderApi, "vulkan" ) == 0; + return true; } typedef struct { @@ -529,7 +533,10 @@ static bool R_ShadowMapCasterIsDynamic( const idRenderEntityLocal *entityDef ) { return hasDynamicGeometry && entityDef->dynamicModelFrameCount >= recentFrame; } -static void R_RecordShadowMapCaster( viewLight_t *vLight, const idRenderEntityLocal *entityDef, const idMaterial *shader, const bool translucent, const bool expandedCaster ) { +static void R_RecordShadowMapCaster( viewLight_t *vLight, + const idRenderEntityLocal *entityDef, const idMaterial *shader, + const srfTriangles_t *casterTris, const bool translucent, + const bool expandedCaster ) { if ( vLight == NULL ) { return; } @@ -563,10 +570,27 @@ static void R_RecordShadowMapCaster( viewLight_t *vLight, const idRenderEntityLo hash = R_ShadowMapHashInt( hash, shader != NULL ? static_cast( shader->Coverage() ) : -1 ); hash = R_ShadowMapHashString( hash, shader != NULL ? shader->GetName() : NULL ); hash = R_ShadowMapHashInt( hash, shader != NULL ? shader->GetNumStages() : 0 ); + hash = R_ShadowMapHashInt( hash, + shader != NULL ? static_cast( shader->GetCullType() ) : -1 ); hash = R_ShadowMapHashInt( hash, translucent ? 1 : 0 ); hash = R_ShadowMapHashInt( hash, expandedCaster ? 1 : 0 ); + if ( casterTris != NULL ) { + // Automatic caster culling depends on these live geometry properties. + // Include them in the physical cache identity so model/decl reloads or + // topology changes cannot reuse depth rendered under another policy. + hash = R_ShadowMapHashInt( hash, casterTris->perfectHull ? 1 : 0 ); + hash = R_ShadowMapHashInt( hash, casterTris->numVerts ); + hash = R_ShadowMapHashInt( hash, casterTris->numIndexes ); + for ( int corner = 0; corner < 2; ++corner ) { + for ( int component = 0; component < 3; ++component ) { + hash = R_ShadowMapHashFloat( hash, + casterTris->bounds[ corner ][ component ] ); + } + } + } if ( entityDef != NULL ) { hash = R_ShadowMapHashInt( hash, entityDef->lastModifiedFrameNum ); + hash = R_ShadowMapHashInt( hash, entityDef->dynamicModelFrameCount ); for ( int i = 0; i < 16; i++ ) { hash = R_ShadowMapHashFloat( hash, entityDef->modelMatrix[i] ); } @@ -731,6 +755,27 @@ static bool R_ShouldSkipPointLightEmitterCaster( const idMaterial *shadowShader, return ambientTris->bounds.Expand( emitterBoundsPad ).ContainsPoint( localLightOrigin ); } +// Lifetime-cached wrapper: the emitter-panel test walks the surface geometry +// building a plane and bounds, and its inputs are invariant for the surface +// interaction's lifetime, so evaluate it at most once per interaction instead +// of per visible frame. Only point (non-parallel) lights can skip. +static bool R_CachedShouldSkipPointLightEmitterCaster( surfaceInteraction_t *sint, const viewLight_t *vLight, + const idMaterial *shadowShader, const idVec3 &localLightOrigin, const idVec3 &localLightRadius ) { + if ( sint == NULL ) { + return vLight->pointLight && !vLight->parallel && + R_ShouldSkipPointLightEmitterCaster( shadowShader, NULL, localLightOrigin, localLightRadius ); + } + if ( !( vLight->pointLight && !vLight->parallel ) ) { + return false; + } + if ( !sint->pointEmitterCasterVerdictValid ) { + sint->pointEmitterCasterSkip = R_ShouldSkipPointLightEmitterCaster( + shadowShader, sint->ambientTris, localLightOrigin, localLightRadius ); + sint->pointEmitterCasterVerdictValid = true; + } + return sint->pointEmitterCasterSkip; +} + static void R_BoundInteractionSurface( const srfTriangles_t *tri, const glIndex_t *indexes, int numIndexes, idBounds &bounds ) { if ( numIndexes <= 0 ) { bounds.Clear(); @@ -1652,7 +1697,7 @@ void idInteraction::CreateInteraction( const idRenderModel *model ) { R_LightHasRealPrelightModel( lightDef->parms ) && model->IsStaticWorldModel() && r_useOptimizedShadows.GetBool() && - !R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( lightDef ); + !R_ShadowMapsNeedPerSurfaceStencilVolumes( lightDef ); // A thin panel surrounding its owning point-light origin is excluded // from the point depth map. Probe its retail stencil path even when @@ -1691,6 +1736,8 @@ void idInteraction::CreateInteraction( const idRenderModel *model ) { // built once and kept so the per-light stencil fallback stays instant. const bool suppressDynamicShadowVolume = surfaceCanCastStencilShadowVolume && shadowLODAdmitted && + ( surfaceCanCastDedicatedShadowMap || + surfaceCanCastTranslucentShadowMap ) && model->IsDynamicModel() != DM_STATIC && !forcePointEmitterStencilGeneration && R_ShadowMapLightWillUseShadowMaps( lightDef ); @@ -1911,6 +1958,26 @@ void idInteraction::AddActiveInteraction( void ) { bool lightScissorsEmpty = lightScissor.IsEmpty(); + // Shadow-map admission policy that is invariant across this interaction's + // surfaces, hoisted out of the per-surface loop (mirrors CreateInteraction). + // translucentShadowMapSupported is only consumed when the caster policy is + // active and the interaction has shadows, so the extra guards keep the + // default stencil-only configuration from paying the by-name cvar lookup. + const bool shadowMapCasterPolicyActive = + r_useShadowMap.GetBool() && + ( !vLight->pointLight || vLight->parallel || r_shadowMapPointLights.GetBool() ); + const bool isViewOnlyEntity = + ( entityDef->parms.allowSurfaceInViewID != 0 && + entityDef->parms.allowSurfaceInViewID == tr.viewDef->renderView.viewID ) || + ( entityDef->parms.weaponDepthHackInViewID != 0 && + entityDef->parms.weaponDepthHackInViewID == tr.viewDef->renderView.viewID ); + const bool shadowMapsEnabled = r_shadows.GetBool() && r_useShadowMap.GetBool(); + const bool translucentShadowMapSupported = + interactionHasShadows && + shadowMapCasterPolicyActive && + shadowMapsEnabled && + R_TranslucentShadowMapMomentsSupportedForLight( lightDef ); + // for each surface of this entity / light interaction for ( int i = 0; i < numSurfaces; i++ ) { surfaceInteraction_t *sint = &surfaces[i]; @@ -1986,6 +2053,8 @@ void idInteraction::AddActiveInteraction( void ) { // surface's live cache when the interaction is later freed srfTriangles_t *frameTris = (srfTriangles_t *)R_FrameAlloc( sizeof( *frameTris ) ); *frameTris = *lightTris; + frameTris->gpuSkinningJointPaletteAlloc = NULL; + frameTris->numGpuSkinningJointPaletteAllocJoints = 0; lightTris->ambientCache = NULL; lightTris->tempAmbientCache = false; lightTris->indexCache = NULL; @@ -2076,9 +2145,6 @@ void idInteraction::AddActiveInteraction( void ) { const int shadowReceiverMask = materialNoSelfShadow ? SHADOWMAP_RECEIVER_MASK_GLOBAL : ( SHADOWMAP_RECEIVER_MASK_LOCAL | SHADOWMAP_RECEIVER_MASK_GLOBAL ); - const bool shadowMapCasterPolicyActive = - r_useShadowMap.GetBool() && - ( !vLight->pointLight || vLight->parallel || r_shadowMapPointLights.GetBool() ); bool admittedShadowMapCaster = false; bool linkedShadowMapCaster = false; @@ -2089,19 +2155,9 @@ void idInteraction::AddActiveInteraction( void ) { // (r_shadowMapPointLights 0 previously built full caster chains the // backend never consumed) if ( shadowMapCasterPolicyActive ) { - const bool isViewOnlyEntity = - ( entityDef->parms.allowSurfaceInViewID != 0 && - entityDef->parms.allowSurfaceInViewID == tr.viewDef->renderView.viewID ) || - ( entityDef->parms.weaponDepthHackInViewID != 0 && - entityDef->parms.weaponDepthHackInViewID == tr.viewDef->renderView.viewID ); const bool shadowMapNoSelfShadow = materialNoSelfShadow; - const bool shadowMapsEnabled = r_shadows.GetBool() && r_useShadowMap.GetBool(); - const bool translucentShadowMapSupported = - shadowMapsEnabled && - R_TranslucentShadowMapMomentsSupportedForLight( lightDef ); const bool skipPointLightEmitterCaster = - vLight->pointLight && !vLight->parallel && - R_ShouldSkipPointLightEmitterCaster( shadowShader, sint->ambientTris, localLightOrigin, lightDef->parms.lightRadius ); + R_CachedShouldSkipPointLightEmitterCaster( sint, vLight, shadowShader, localLightOrigin, lightDef->parms.lightRadius ); const bool sameSpectrumShadowMapCaster = R_ShadowMapShaderSpectrumMatchesLight( shadowShader, lightDef ); const bool allowShadowMapCaster = @@ -2142,7 +2198,8 @@ void idInteraction::AddActiveInteraction( void ) { if ( haveCasterGeometry ) { R_TouchShadowMapCache( casterTris->ambientCache ); R_TouchShadowMapCache( casterTris->indexCache ); - R_RecordShadowMapCaster( vLight, entityDef, shadowShader, false, shadowMapCasterOnly ); + R_RecordShadowMapCaster( vLight, entityDef, shadowShader, + casterTris, false, shadowMapCasterOnly ); // dynamic casters go to their own chains so cached static // tiles stay valid while they move (composed per frame) @@ -2165,7 +2222,8 @@ void idInteraction::AddActiveInteraction( void ) { if ( haveCasterGeometry ) { R_TouchShadowMapCache( casterTris->ambientCache ); R_TouchShadowMapCache( casterTris->indexCache ); - R_RecordShadowMapCaster( vLight, entityDef, shadowShader, true, shadowMapCasterOnly ); + R_RecordShadowMapCaster( vLight, entityDef, shadowShader, + casterTris, true, shadowMapCasterOnly ); if ( shadowMapNoSelfShadow ) { R_LinkShadowMapCasterSurf( &vLight->localTranslucentShadowMapCasters, @@ -2180,14 +2238,18 @@ void idInteraction::AddActiveInteraction( void ) { } srfTriangles_t *shadowTris = sint->shadowTris; + // Use this surface's cached stencil eligibility as provenance rather than + // the presence of a generated volume: eligible dynamic volumes may have + // been suppressed for mapped rendering, and generation/cache setup can + // fail. Conservative caster-only interactions must retain that same + // surface's stencil representation when map admission fails, without + // pulling unrelated or ineligible volumes into the hybrid chains. const bool mapMissingCasterNeedsStencil = shadowMapCasterPolicyActive && !sint->shadowStencilUsesPrelight && !linkedShadowMapCaster && ( admittedShadowMapCaster || - shadowTris != NULL ) && - ( !shadowMapCasterOnly || - admittedShadowMapCaster ); + sint->shadowStencilEligible ); const bool prelightMapMissingCasterNeedsStencil = shadowMapCasterPolicyActive && sint->shadowStencilUsesPrelight && diff --git a/src/renderer/Interaction.h b/src/renderer/Interaction.h index 99cd3375..64f83b36 100644 --- a/src/renderer/Interaction.h +++ b/src/renderer/Interaction.h @@ -73,6 +73,13 @@ typedef struct { bool shadowStencilEligible; bool shadowStencilUsesPrelight; + // Lifetime cache of the point-light emitter-panel caster verdict. Its inputs + // (shadow shader, ambientTris, the interaction's local light origin/radius) + // are all fixed for the surface interaction's lifetime; any change frees and + // rebuilds these surfaces, clearing the flag, so it never goes stale. + bool pointEmitterCasterVerdictValid; + bool pointEmitterCasterSkip; + int expCulled; // only for the experimental shadow buffer renderer srfCullInfo_t cullInfo; diff --git a/src/renderer/Material.cpp b/src/renderer/Material.cpp index 1e409367..3f38c036 100644 --- a/src/renderer/Material.cpp +++ b/src/renderer/Material.cpp @@ -31,6 +31,8 @@ If you have questions concerning this license or the applicable additional terms #include "tr_local.h" +#include + /* Any errors during parsing just set MF_DEFAULTED and return, rather than throwing @@ -68,6 +70,16 @@ typedef struct mtrParsingData_s { bool forceOverlays; } mtrParsingData_t; +static void R_ResetSpecularProbeMaterialInfo( specularProbeMaterialInfo_t &info ) { + memset( &info, 0, sizeof( info ) ); + info.cubeConvention = SPECULAR_PROBE_CUBE_NONE; + info.tint[0] = 1.0f; + info.tint[1] = 1.0f; + info.tint[2] = 1.0f; + info.intensity = 1.0f; + info.blendFraction = 0.25f; +} + // `glslPrograms` is an authored material-capability condition, while // glConfig.GLSLProgramAvailable also advertises the OpenGL renderer's broad // post-processing and modern-feature surface. Vulkan implements the stock @@ -280,6 +292,7 @@ void idMaterial::CommonInit() { constantRegisters = NULL; numStages = 0; numAmbientStages = 0; + hasCustomGLSLLightingStage = false; stages = NULL; editorImage = NULL; lightFalloffImage = NULL; @@ -294,6 +307,7 @@ void idMaterial::CommonInit() { unsmoothedTangents = false; gui = NULL; memset( deformRegisters, 0, sizeof( deformRegisters ) ); + deformDecl = NULL; editorAlpha = 1.0; spectrum = 0; polygonOffset = 0; @@ -310,6 +324,18 @@ void idMaterial::CommonInit() { globalUseCount = 0; portalImage = nullptr; // jmarshall end + memset( &pbrInfo, 0, sizeof( pbrInfo ) ); + pbrInfo.workflow = PBR_WORKFLOW_NONE; + pbrInfo.normalFormat = PBR_NORMAL_UNSPECIFIED; + pbrInfo.metallicRegister = -1; + pbrInfo.roughnessRegister = -1; + pbrInfo.aoRegister = -1; + pbrInfo.normalScaleRegister = -1; + pbrInfo.emissiveColorRegisters[0] = -1; + pbrInfo.emissiveColorRegisters[1] = -1; + pbrInfo.emissiveColorRegisters[2] = -1; + pbrInfo.autoLegacyFallback = true; + R_ResetSpecularProbeMaterialInfo( specularProbeInfo ); decalInfo.stayTime = 10000; decalInfo.maxAngle = 0.1f; @@ -424,6 +450,20 @@ void idMaterial::FreeData() { materialTypeArrayName.Clear(); MTAWidth = 0; MTAHeight = 0; + // Purged declarations remain queryable before their next parse. Do not + // leave stale PBR image pointers or register indices visible in that state. + memset( &pbrInfo, 0, sizeof( pbrInfo ) ); + pbrInfo.workflow = PBR_WORKFLOW_NONE; + pbrInfo.normalFormat = PBR_NORMAL_UNSPECIFIED; + pbrInfo.metallicRegister = -1; + pbrInfo.roughnessRegister = -1; + pbrInfo.aoRegister = -1; + pbrInfo.normalScaleRegister = -1; + pbrInfo.emissiveColorRegisters[0] = -1; + pbrInfo.emissiveColorRegisters[1] = -1; + pbrInfo.emissiveColorRegisters[2] = -1; + pbrInfo.autoLegacyFallback = true; + R_ResetSpecularProbeMaterialInfo( specularProbeInfo ); } /* @@ -450,6 +490,8 @@ idImage *idMaterial::GetEditorImage( void ) const { if ( !editorImage ) { editorImage = stages[0].texture.image; } + } else if ( pbrInfo.enabled && pbrInfo.albedo.image != NULL ) { + editorImage = pbrInfo.albedo.image; } else { editorImage = globalImages->defaultImage; } @@ -2483,6 +2525,701 @@ void idMaterial::ParseStage( idLexer &src, const textureRepeat_t trpDefault ) { } } +static bool R_IsUnsupportedPBRImageProgramToken( const idToken &token ) { + return R_IsMutableRenderImageName( token.c_str() ) + || !token.Icmp( "videoMap" ) || !token.Icmp( "soundMap" ) + || !token.Icmp( "mirrorRenderMap" ) || !token.Icmp( "remoteRenderMap" ) + || !token.Icmp( "reflectionRenderMap" ) || !token.Icmp( "refractionRenderMap" ) + || !token.Icmp( "xrayRenderMap" ) + || !token.Icmp( "cameraCubeMap" ) || !token.Icmp( "cubeMap" ) + || !token.Icmp( "program" ) || !token.Icmp( "vertexProgram" ) + || !token.Icmp( "fragmentProgram" ) || !token.Icmp( "fp20Program" ) + || !token.Icmp( "glslProgram" ) || !token.Icmp( "vertexParm" ) + || !token.Icmp( "fragmentParm" ) || !token.Icmp( "fragmentMap" ) + || !token.Icmp( "shaderParm" ) || !token.Icmp( "shaderTexture" ) + || !token.Icmp( "customLighting" ) + // Stage-only state is not an image name. Reject it here as well as + // when nested inside an otherwise valid image program. Keep image + // program operators such as add() and scale() available. + || !token.Icmp( "blend" ) || !token.Icmp( "map" ) + || !token.Icmp( "screen" ) || !token.Icmp( "screen2" ) || !token.Icmp( "glassWarp" ) + || !token.Icmp( "texGen" ) || !token.Icmp( "if" ) + || !token.Icmp( "alphaTest" ) || !token.Icmp( "alphaFunc" ) + || !token.Icmp( "scroll" ) || !token.Icmp( "translate" ) + || !token.Icmp( "centerScale" ) || !token.Icmp( "shear" ) || !token.Icmp( "rotate" ) + || !token.Icmp( "vertexColor" ) || !token.Icmp( "inverseVertexColor" ) + || !token.Icmp( "color" ) || !token.Icmp( "colored" ) + || !token.Icmp( "red" ) || !token.Icmp( "green" ) || !token.Icmp( "blue" ) + || !token.Icmp( "alpha" ) || !token.Icmp( "rgb" ) || !token.Icmp( "rgba" ) + || !token.Icmp( "maskRed" ) || !token.Icmp( "maskGreen" ) + || !token.Icmp( "maskBlue" ) || !token.Icmp( "maskAlpha" ) + || !token.Icmp( "maskColor" ) || !token.Icmp( "maskDepth" ) + || !token.Icmp( "privatePolygonOffset" ) || !token.Icmp( "polygonOffset" ) + || !token.Icmp( "ignoreAlphaTest" ); +} + +/* +================ +idMaterial::ParsePBRImage + +Parses one static image-program reference from a PBR metadata line. Dynamic +render/video maps and arbitrary shader state deliberately remain classic-stage +features until a modern pass owns their complete lifetime and fallback rules. +================ +*/ +bool idMaterial::ParsePBRImage( idLexer &src, pbrMaterialTexture_t &target, const int usage, const textureRepeat_t trpDefault ) { + if ( target.present ) { + src.Warning( "duplicate PBR image semantic in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + textureFilter_t filter = TF_DEFAULT; + textureRepeat_t repeat = trpDefault; + bool allowPicmip = true; + bool noMips = false; + bool highQuality = false; + bool forceHighQuality = false; + unsigned int imageFlags = 0; + idToken token; + bool haveImageToken = false; + + while ( src.ReadTokenOnLine( &token ) ) { + if ( !token.Icmp( "nearest" ) ) { + filter = TF_NEAREST; + continue; + } + if ( !token.Icmp( "linear" ) ) { + filter = TF_LINEAR; + continue; + } + if ( !token.Icmp( "clamp" ) ) { + repeat = TR_CLAMP; + continue; + } + if ( !token.Icmp( "noclamp" ) ) { + repeat = TR_REPEAT; + continue; + } + if ( !token.Icmp( "zeroclamp" ) ) { + repeat = TR_CLAMP_TO_ZERO; + continue; + } + if ( !token.Icmp( "alphazeroclamp" ) ) { + repeat = TR_CLAMP_TO_ZERO_ALPHA; + continue; + } + if ( !token.Icmp( "mirroredrepeat" ) ) { + repeat = TR_MIRRORED_REPEAT; + continue; + } + if ( !token.Icmp( "nopicmip" ) ) { + allowPicmip = false; + continue; + } + if ( !token.Icmp( "nomips" ) ) { + noMips = true; + imageFlags = R_ApplyMaterialNoMipFlags( imageFlags ); + continue; + } + if ( !token.Icmp( "forceHighQuality" ) ) { + highQuality = true; + forceHighQuality = true; + continue; + } + if ( !token.Icmp( "uncompressed" ) || !token.Icmp( "highquality" ) ) { + // TD_PBR_COLOR and TD_MATERIAL_DATA are already lossless RGBA8 + // identities. Retain the authoring hint for any generated classic + // fallback without discarding the PBR colour-vs-data mip semantics. + highQuality = true; + continue; + } + + if ( R_IsUnsupportedPBRImageProgramToken( token ) ) { + src.Warning( "dynamic or cube image token '%s' is not supported in the PBR block for '%s'", token.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + src.UnreadToken( &token ); + haveImageToken = true; + break; + } + + if ( !haveImageToken ) { + src.Warning( "PBR image semantic expects an image program in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + const char *parsedName = R_ParsePastImageProgram( src ); + if ( parsedName == NULL || parsedName[0] == '\0' ) { + src.Warning( "PBR image semantic has an empty image program in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + // R_ParsePastImageProgram accepts arbitrary leaf names, so validating only + // the first authored token would let a dynamic/render/program token hide + // inside add(), scale(), or another nested image operation. Re-lex the + // canonical expression and reject forbidden tokens at every nesting depth. + idLexer imageProgram; + imageProgram.LoadMemory( parsedName, idLib::SizeToInt( strlen( parsedName ), "ParsePBRImage validation" ), "pbrImageProgram" ); + imageProgram.SetFlags( LEXFL_NOFATALERRORS | LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES ); + idToken imageProgramToken; + while ( imageProgram.ReadToken( &imageProgramToken ) ) { + if ( R_IsUnsupportedPBRImageProgramToken( imageProgramToken ) ) { + src.Warning( "dynamic or cube image token '%s' is not supported in the PBR block for '%s'", imageProgramToken.c_str(), GetName() ); + imageProgram.FreeSource(); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + } + imageProgram.FreeSource(); + + idStr imageName = parsedName; + target.image = R_LoadMaterialImage( imageName.c_str(), filter, repeat, + static_cast( usage ), CF_2D, allowPicmip, imageFlags ); + if ( target.image == NULL ) { + target.image = globalImages->defaultImage; + } + if ( R_IsMutableRenderImage( target.image ) ) { + src.Warning( "mutable render image '%s' is not supported in the PBR block for '%s'", imageName.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + target.filter = static_cast( filter ); + target.repeat = static_cast( repeat ); + target.allowPicmip = allowPicmip; + target.noMips = noMips; + target.highQuality = highQuality; + target.forceHighQuality = forceHighQuality; + target.present = true; + return true; +} + +/* +================ +idMaterial::ParsePBRBlock +================ +*/ +bool idMaterial::ParsePBRBlock( idLexer &src, const textureRepeat_t trpDefault ) { + if ( pbrInfo.enabled ) { + src.Warning( "multiple PBR blocks in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !src.ExpectTokenString( "{" ) ) { + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + pbrInfo.enabled = true; + // Allocate PBR-only defaults only after an authored PBR block opts in. Doing + // this in ParseMaterial's common prologue would shift expression-register + // indices for every retail material even though no PBR metadata is present. + pbrInfo.metallicRegister = GetExpressionConstant( 0.0f ); + pbrInfo.roughnessRegister = GetExpressionConstant( 0.5f ); + pbrInfo.aoRegister = GetExpressionConstant( 1.0f ); + pbrInfo.normalScaleRegister = GetExpressionConstant( 1.0f ); + pbrInfo.emissiveColorRegisters[0] = GetExpressionConstant( 0.0f ); + pbrInfo.emissiveColorRegisters[1] = GetExpressionConstant( 0.0f ); + pbrInfo.emissiveColorRegisters[2] = GetExpressionConstant( 0.0f ); + idToken token; + while ( src.ReadToken( &token ) ) { + if ( token == "}" ) { + break; + } + + if ( !token.Icmp( "workflow" ) ) { + idToken value; + if ( !src.ReadTokenOnLine( &value ) ) { + src.Warning( "PBR workflow expects a value in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !value.Icmp( "metallicRoughness" ) ) { + pbrInfo.workflow = PBR_WORKFLOW_METALLIC_ROUGHNESS; + } else if ( !value.Icmp( "specularGlossiness" ) ) { + pbrInfo.workflow = PBR_WORKFLOW_SPECULAR_GLOSSINESS; + } else { + src.Warning( "unknown PBR workflow '%s' in material '%s'", value.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + continue; + } + + if ( !token.Icmp( "normalFormat" ) ) { + idToken value; + if ( !src.ReadTokenOnLine( &value ) ) { + src.Warning( "PBR normalFormat expects a value in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !value.Icmp( "quake4AGB" ) ) { + pbrInfo.normalFormat = PBR_NORMAL_QUAKE4_AGB; + } else if ( !value.Icmp( "tangentRG" ) ) { + pbrInfo.normalFormat = PBR_NORMAL_TANGENT_RG; + } else if ( !value.Icmp( "tangentXYZ" ) ) { + pbrInfo.normalFormat = PBR_NORMAL_TANGENT_XYZ; + } else { + src.Warning( "unknown PBR normalFormat '%s' in material '%s'", value.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + continue; + } + + if ( !token.Icmp( "albedoMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.albedo, TD_PBR_COLOR, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "normalMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.normal, TD_BUMP, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "ormMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.orm, TD_MATERIAL_DATA, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "metallicMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.metallic, TD_MATERIAL_DATA, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "roughnessMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.roughness, TD_MATERIAL_DATA, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "aoMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.ao, TD_MATERIAL_DATA, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "emissiveMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.emissive, TD_PBR_COLOR, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "legacyBumpMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.legacyBump, TD_BUMP, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "legacyDiffuseMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.legacyDiffuse, TD_DIFFUSE, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "legacySpecularMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.legacySpecular, TD_SPECULAR, trpDefault ) ) return false; + continue; + } + if ( !token.Icmp( "legacyEmissiveMap" ) ) { + if ( !ParsePBRImage( src, pbrInfo.legacyEmissive, TD_PBR_COLOR, trpDefault ) ) return false; + continue; + } + + if ( !token.Icmp( "metallic" ) ) { + pbrInfo.metallicRegister = ParseExpression( src ); + continue; + } + if ( !token.Icmp( "roughness" ) ) { + pbrInfo.roughnessRegister = ParseExpression( src ); + continue; + } + if ( !token.Icmp( "ao" ) ) { + pbrInfo.aoRegister = ParseExpression( src ); + continue; + } + if ( !token.Icmp( "normalScale" ) ) { + pbrInfo.normalScaleRegister = ParseExpression( src ); + continue; + } + if ( !token.Icmp( "emissiveColor" ) ) { + for ( int i = 0; i < 3; ++i ) { + pbrInfo.emissiveColorRegisters[i] = ParseExpression( src ); + if ( i < 2 ) { + idToken separator; + if ( src.ReadToken( &separator ) && separator != "," ) { + src.UnreadToken( &separator ); + } + } + } + continue; + } + if ( !token.Icmp( "autoLegacyFallback" ) ) { + idToken value; + if ( !src.ReadTokenOnLine( &value ) || ( value != "0" && value != "1" ) ) { + src.Warning( "PBR autoLegacyFallback expects 0 or 1 in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + pbrInfo.autoLegacyFallback = value == "1"; + continue; + } + + src.Warning( "unknown PBR parameter '%s' in material '%s'", token.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + if ( token != "}" ) { + src.Warning( "unterminated PBR block in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( pbrInfo.workflow == PBR_WORKFLOW_NONE ) { + src.Warning( "PBR material '%s' has no workflow", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !pbrInfo.albedo.present ) { + src.Warning( "PBR material '%s' has no albedoMap", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( pbrInfo.normal.present && pbrInfo.normalFormat == PBR_NORMAL_UNSPECIFIED ) { + src.Warning( "PBR normalMap in '%s' requires normalFormat", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( pbrInfo.orm.present && ( pbrInfo.metallic.present || pbrInfo.roughness.present || pbrInfo.ao.present ) ) { + src.Warning( "PBR material '%s' cannot combine ormMap with separate metallic/roughness/AO maps", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + pbrInfo.hasExplicitLegacyFallback = pbrInfo.legacyBump.present || + pbrInfo.legacyDiffuse.present || pbrInfo.legacySpecular.present || + pbrInfo.legacyEmissive.present; + return true; +} + +static bool R_ReadSpecularProbeNumbers( idLexer &src, const char *field, + const char *materialName, float *values, int valueCount ) { + for ( int index = 0; index < valueCount; ++index ) { + idToken value; + if ( !src.ReadTokenOnLine( &value ) || !value.IsNumeric() ) { + src.Warning( "openQ4SpecularProbe %s expects %d numeric value%s in material '%s'", + field, valueCount, valueCount == 1 ? "" : "s", materialName ); + return false; + } + values[index] = value.GetFloatValue(); + if ( !std::isfinite( values[index] ) ) { + src.Warning( "openQ4SpecularProbe %s is not finite in material '%s'", field, materialName ); + return false; + } + } + idToken extra; + if ( src.ReadTokenOnLine( &extra ) ) { + src.Warning( "openQ4SpecularProbe %s has an unexpected value '%s' in material '%s'", + field, extra.c_str(), materialName ); + return false; + } + return true; +} + +/* +================ +idMaterial::ParseSpecularProbeBlock + +Parses renderer-only metadata for an authored reflection/specular probe. The +contract is deliberately static and bounded: expressions, render targets, +image programs, and stage state are rejected. A complete temporary record is +committed only after the closing brace and every required field validate, so a +malformed block can never expose partially authored probe state. +================ +*/ +bool idMaterial::ParseSpecularProbeBlock( idLexer &src ) { + if ( specularProbeInfo.enabled ) { + src.Warning( "multiple openQ4SpecularProbe blocks in material '%s'", GetName() ); + R_ResetSpecularProbeMaterialInfo( specularProbeInfo ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !src.ExpectTokenString( "{" ) ) { + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + specularProbeMaterialInfo_t parsed; + R_ResetSpecularProbeMaterialInfo( parsed ); + bool cubeSeen = false; + bool tintSeen = false; + bool intensitySeen = false; + bool blendSeen = false; + bool prioritySeen = false; + idToken token; + while ( src.ReadToken( &token ) ) { + if ( token == "}" ) { + break; + } + + if ( !token.Icmp( "cubeMap" ) || !token.Icmp( "cameraCubeMap" ) ) { + if ( cubeSeen ) { + src.Warning( "openQ4SpecularProbe in material '%s' has multiple cube maps", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + idToken imageName; + idToken extra; + if ( !src.ReadTokenOnLine( &imageName ) || imageName.type == TT_NUMBER + || imageName.type == TT_PUNCTUATION || src.ReadTokenOnLine( &extra ) ) { + src.Warning( "openQ4SpecularProbe %s expects one static image name in material '%s'", + token.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( R_IsUnsupportedPBRImageProgramToken( imageName ) + || R_IsMutableRenderImageName( imageName.c_str() ) ) { + src.Warning( "mutable or dynamic probe image '%s' is not supported in material '%s'", + imageName.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + const bool cameraConvention = !token.Icmp( "cameraCubeMap" ); + parsed.cubeConvention = cameraConvention + ? SPECULAR_PROBE_CUBE_CAMERA : SPECULAR_PROBE_CUBE_NATIVE; + parsed.cubeImage = R_LoadMaterialImage( imageName.c_str(), TF_LINEAR, + TR_CLAMP, TD_HIGH_QUALITY, + cameraConvention ? CF_CAMERA : CF_NATIVE, false, IMAGEFLAG_NOMIPS ); + if ( parsed.cubeImage == NULL || R_IsMutableRenderImage( parsed.cubeImage ) + || ( parsed.cubeImage->IsLoaded() + && ( parsed.cubeImage->IsDefaulted() + || parsed.cubeImage->GetOpts().textureType != TT_CUBIC ) ) ) { + src.Warning( "probe cube image '%s' is unavailable or invalid in material '%s'", + imageName.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + cubeSeen = true; + continue; + } + + if ( !token.Icmp( "tint" ) ) { + if ( tintSeen || !R_ReadSpecularProbeNumbers( src, "tint", GetName(), parsed.tint, 3 ) ) { + src.Warning( "openQ4SpecularProbe tint is duplicated or invalid in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + for ( int component = 0; component < 3; ++component ) { + if ( parsed.tint[component] < 0.0f || parsed.tint[component] > 64.0f ) { + src.Warning( "openQ4SpecularProbe tint must be in [0,64] in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + } + tintSeen = true; + continue; + } + + if ( !token.Icmp( "intensity" ) ) { + if ( intensitySeen || !R_ReadSpecularProbeNumbers( src, "intensity", GetName(), &parsed.intensity, 1 ) + || parsed.intensity <= 0.0f || parsed.intensity > 64.0f ) { + src.Warning( "openQ4SpecularProbe intensity must be in (0,64] in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + intensitySeen = true; + continue; + } + + if ( !token.Icmp( "blendFraction" ) ) { + if ( blendSeen || !R_ReadSpecularProbeNumbers( src, "blendFraction", GetName(), &parsed.blendFraction, 1 ) + || parsed.blendFraction <= 0.0f || parsed.blendFraction > 1.0f ) { + src.Warning( "openQ4SpecularProbe blendFraction must be in (0,1] in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + blendSeen = true; + continue; + } + + if ( !token.Icmp( "priority" ) ) { + idToken value; + idToken extra; + if ( prioritySeen || !src.ReadTokenOnLine( &value ) + || value.type != TT_NUMBER || !( value.subtype & TT_INTEGER ) + || src.ReadTokenOnLine( &extra ) ) { + src.Warning( "openQ4SpecularProbe priority expects one integer in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + char *end = NULL; + const long parsedPriority = std::strtol( value.c_str(), &end, 10 ); + if ( end == value.c_str() || end == NULL || *end != '\0' + || parsedPriority < 0 || parsedPriority > 255 ) { + src.Warning( "openQ4SpecularProbe priority must be in [0,255] in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + parsed.priority = static_cast( parsedPriority ); + prioritySeen = true; + continue; + } + + src.Warning( "unknown openQ4SpecularProbe parameter '%s' in material '%s'", token.c_str(), GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + if ( token != "}" ) { + src.Warning( "unterminated openQ4SpecularProbe block in material '%s'", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + if ( !cubeSeen || parsed.cubeImage == NULL ) { + src.Warning( "openQ4SpecularProbe material '%s' has no cubeMap", GetName() ); + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + + parsed.enabled = true; + specularProbeInfo = parsed; + return true; +} + +/* +================ +idMaterial::AddPBRLegacyFallbackStages +================ +*/ +void idMaterial::AddPBRLegacyFallbackStages( const textureRepeat_t trpDefault ) { + if ( !pbrInfo.enabled ) { + return; + } + + bool hasBump = false; + bool hasDiffuse = false; + bool hasSpecular = false; + bool hasAmbient = false; + for ( int i = 0; i < numStages; ++i ) { + hasBump |= pd->parseStages[i].lighting == SL_BUMP; + hasDiffuse |= pd->parseStages[i].lighting == SL_DIFFUSE; + hasSpecular |= pd->parseStages[i].lighting == SL_SPECULAR; + hasAmbient |= pd->parseStages[i].lighting == SL_AMBIENT; + } + pbrInfo.hasAuthoredClassicFallback = hasBump && hasDiffuse; + + auto addStage = [&]( const char *blendName, const pbrMaterialTexture_t &texture ) -> bool { + if ( texture.image == NULL || numStages >= MAX_SHADER_STAGES ) { + SetMaterialFlag( MF_DEFAULTED ); + return false; + } + idStr buffer; + buffer = "blend "; + buffer.Append( blendName ); + buffer.Append( "\n" ); + switch ( static_cast( texture.filter ) ) { + case TF_NEAREST: buffer.Append( "nearest\n" ); break; + case TF_LINEAR: buffer.Append( "linear\n" ); break; + default: break; + } + const textureRepeat_t repeat = static_cast( texture.repeat ); + if ( repeat != trpDefault ) { + switch ( repeat ) { + case TR_REPEAT: buffer.Append( "noclamp\n" ); break; + case TR_MIRRORED_REPEAT: buffer.Append( "mirroredrepeat\n" ); break; + case TR_CLAMP: buffer.Append( "clamp\n" ); break; + case TR_CLAMP_TO_ZERO: buffer.Append( "zeroclamp\n" ); break; + case TR_CLAMP_TO_ZERO_ALPHA: buffer.Append( "alphazeroclamp\n" ); break; + default: break; + } + } + if ( !texture.allowPicmip ) buffer.Append( "nopicmip\n" ); + if ( texture.noMips ) buffer.Append( "nomips\n" ); + if ( texture.forceHighQuality ) { + buffer.Append( "forceHighQuality\n" ); + } else if ( texture.highQuality ) { + buffer.Append( "highquality\n" ); + } + buffer.Append( "map " ); + buffer.Append( texture.image->GetName() ); + buffer.Append( "\n}\n" ); + idLexer generated; + generated.LoadMemory( buffer.c_str(), buffer.Length(), "pbrLegacyFallback" ); + generated.SetFlags( LEXFL_NOFATALERRORS | LEXFL_NOSTRINGCONCAT | LEXFL_NOSTRINGESCAPECHARS | LEXFL_ALLOWPATHNAMES ); + ParseStage( generated, trpDefault ); + generated.FreeSource(); + return !TestMaterialFlag( MF_DEFAULTED ); + }; + auto defaultTexture = [&]( idImage *image ) -> pbrMaterialTexture_t { + pbrMaterialTexture_t texture; + memset( &texture, 0, sizeof( texture ) ); + texture.image = image; + texture.present = image != NULL; + texture.filter = static_cast( TF_DEFAULT ); + texture.repeat = static_cast( trpDefault ); + texture.allowPicmip = true; + return texture; + }; + + if ( !hasBump && pbrInfo.legacyBump.present ) { + if ( !addStage( "bumpmap", pbrInfo.legacyBump ) ) return; + hasBump = true; + pbrInfo.usesGeneratedLegacyFallback = true; + } + if ( !hasDiffuse && pbrInfo.legacyDiffuse.present ) { + if ( !addStage( "diffusemap", pbrInfo.legacyDiffuse ) ) return; + hasDiffuse = true; + pbrInfo.usesGeneratedLegacyFallback = true; + } + if ( !hasSpecular && pbrInfo.legacySpecular.present ) { + if ( !addStage( "specularmap", pbrInfo.legacySpecular ) ) return; + hasSpecular = true; + pbrInfo.usesGeneratedLegacyFallback = true; + } + if ( !hasAmbient && pbrInfo.legacyEmissive.present ) { + if ( !addStage( "add", pbrInfo.legacyEmissive ) ) return; + hasAmbient = true; + pbrInfo.usesGeneratedLegacyFallback = true; + } + + // An authored bump+diffuse interaction is already a complete classic + // fallback. Classic specular is optional, and a PBR emissive map must not + // silently inject an ambient stage into that authored path. + const bool hasUsableClassicInteraction = hasBump && hasDiffuse; + const bool allowApproximate = !hasUsableClassicInteraction + && pbrInfo.autoLegacyFallback + && r_pbrGeneratedLegacyFallback.GetBool(); + if ( allowApproximate ) { + if ( !hasBump ) { + // The classic Quake 4 interaction decoder understands only the A/G + // normal convention. Tangent RG/XYZ sources stay available to the + // future PBR shader, but their classic fallback must remain neutral. + const bool classicNormalCompatible = pbrInfo.normal.present + && pbrInfo.normalFormat == PBR_NORMAL_QUAKE4_AGB; + const pbrMaterialTexture_t texture = classicNormalCompatible + ? pbrInfo.normal + : defaultTexture( globalImages->flatNormalMap ); + if ( !addStage( "bumpmap", texture ) ) return; + hasBump = true; + pbrInfo.usesGeneratedLegacyFallback = true; + pbrInfo.usesApproximateLegacyFallback = true; + } + if ( !hasDiffuse ) { + const pbrMaterialTexture_t texture = pbrInfo.albedo.present ? pbrInfo.albedo : defaultTexture( globalImages->whiteImage ); + if ( !addStage( "diffusemap", texture ) ) return; + hasDiffuse = true; + pbrInfo.usesGeneratedLegacyFallback = true; + pbrInfo.usesApproximateLegacyFallback = true; + } + if ( !hasSpecular ) { + if ( !addStage( "specularmap", defaultTexture( globalImages->blackImage ) ) ) return; + hasSpecular = true; + pbrInfo.usesGeneratedLegacyFallback = true; + pbrInfo.usesApproximateLegacyFallback = true; + } + if ( !hasAmbient && pbrInfo.emissive.present ) { + if ( !addStage( "add", pbrInfo.emissive ) ) return; + pbrInfo.usesGeneratedLegacyFallback = true; + pbrInfo.usesApproximateLegacyFallback = true; + } + } + + if ( pbrInfo.usesApproximateLegacyFallback ) { + common->Warning( "PBR material '%s' uses an approximate generated classic fallback", GetName() ); + } +} + /* =============== idMaterial::ParseDeform @@ -2689,7 +3426,6 @@ void idMaterial::ParseMaterial( idLexer &src ) { for ( i = 0 ; i < numRegisters ; i++ ) { pd->registerIsTemporary[i] = true; // they aren't constants that can be folded } - numStages = 0; textureRepeat_t trpDefault = TR_REPEAT; // allow a global setting for repeat @@ -2977,6 +3713,18 @@ void idMaterial::ParseMaterial( idLexer &src ) { } continue; } + else if ( !token.Icmp( "pbr" ) || !token.Icmp( "physicallyBased" ) ) { + if ( !ParsePBRBlock( src, trpDefault ) ) { + return; + } + continue; + } + else if ( !token.Icmp( "openQ4SpecularProbe" ) ) { + if ( !ParseSpecularProbeBlock( src ) ) { + return; + } + continue; + } // diffusemap for stage shortcut else if ( !token.Icmp( "diffusemap" ) ) { str = R_ParsePastImageProgram( src ); @@ -3050,9 +3798,34 @@ void idMaterial::ParseMaterial( idLexer &src ) { } } + // PBR metadata never mutates an authored classic stage. For a PBR-only + // declaration, explicitly requested or development-only generated stages + // are added before the ordinary classic interaction completion/sort pass. + AddPBRLegacyFallbackStages( trpDefault ); + if ( TestMaterialFlag( MF_DEFAULTED ) ) { + return; + } + // add _flat or _white stages if needed AddImplicitStages(); + // A PBR declaration is still valid metadata when it deliberately omits a + // classic fallback, but the current renderer cannot draw it until a native + // PBR lighting path exists. Preserve that state explicitly instead of + // letting a zero-stage declaration disappear from authoring diagnostics. + if ( pbrInfo.enabled ) { + bool hasBump = false; + bool hasDiffuse = false; + for ( int i = 0; i < numStages; ++i ) { + hasBump |= pd->parseStages[i].lighting == SL_BUMP; + hasDiffuse |= pd->parseStages[i].lighting == SL_DIFFUSE; + } + pbrInfo.legacyFallbackMissing = !( hasBump && hasDiffuse ); + if ( pbrInfo.legacyFallbackMissing ) { + common->Warning( "PBR material '%s' has no usable classic bump+diffuse fallback", GetName() ); + } + } + // order the diffuse / bump / specular stages properly SortInteractionStages(); @@ -3309,6 +4082,15 @@ bool idMaterial::Parse( const char *text, const int textLength ) { memcpy( stages, pd->parseStages, numStages * sizeof( stages[0] ) ); } + hasCustomGLSLLightingStage = false; + for ( i = 0 ; i < numStages ; i++ ) { + const newShaderStage_t *newStage = stages[i].newStage; + if ( newStage != NULL && newStage->customLighting && newStage->glslProgram ) { + hasCustomGLSLLightingStage = true; + break; + } + } + if ( numOps ) { ops = (expOp_t *)R_StaticAlloc( numOps * sizeof( ops[0] ) ); memcpy( ops, pd->shaderOps, numOps * sizeof( ops[0] ) ); @@ -3401,6 +4183,21 @@ void idMaterial::Print() const { common->Printf( "%i = %i %s %i\n", op->c, op->a, opNames[ op->opType ], op->b ); } } + if ( pbrInfo.enabled ) { + common->Printf( + "PBR: workflow=%d normalFormat=%d albedo=%s normal=%s orm=%s generatedFallback=%d approximateFallback=%d missingFallback=%d\n", + static_cast( pbrInfo.workflow ), + static_cast( pbrInfo.normalFormat ), + pbrInfo.albedo.image != NULL ? pbrInfo.albedo.image->GetName() : "", + pbrInfo.normal.image != NULL ? pbrInfo.normal.image->GetName() : "", + pbrInfo.orm.image != NULL ? pbrInfo.orm.image->GetName() : "", + pbrInfo.usesGeneratedLegacyFallback ? 1 : 0, + pbrInfo.usesApproximateLegacyFallback ? 1 : 0, + pbrInfo.legacyFallbackMissing ? 1 : 0 ); + } + if ( specularProbeInfo.enabled && specularProbeInfo.cubeImage != NULL ) { + common->Printf( "Specular probe: cubeMap=%s\n", specularProbeInfo.cubeImage->GetName() ); + } } /* @@ -3431,6 +4228,21 @@ void idMaterial::AddReference() { if ( portalImage ) { portalImage->AddReference(); } + + pbrMaterialTexture_t *pbrTextures[] = { + &pbrInfo.albedo, &pbrInfo.normal, &pbrInfo.orm, &pbrInfo.metallic, + &pbrInfo.roughness, &pbrInfo.ao, &pbrInfo.emissive, + &pbrInfo.legacyBump, &pbrInfo.legacyDiffuse, + &pbrInfo.legacySpecular, &pbrInfo.legacyEmissive + }; + for ( unsigned int i = 0; i < sizeof( pbrTextures ) / sizeof( pbrTextures[0] ); ++i ) { + if ( pbrTextures[i]->present && pbrTextures[i]->image != NULL ) { + pbrTextures[i]->image->AddReference(); + } + } + if ( specularProbeInfo.enabled && specularProbeInfo.cubeImage != NULL ) { + specularProbeInfo.cubeImage->AddReference(); + } } /* @@ -3466,6 +4278,21 @@ void idMaterial::ResolveUse() { if ( portalImage != NULL ) { portalImage->AddUseCount( useCount ); } + + pbrMaterialTexture_t *pbrTextures[] = { + &pbrInfo.albedo, &pbrInfo.normal, &pbrInfo.orm, &pbrInfo.metallic, + &pbrInfo.roughness, &pbrInfo.ao, &pbrInfo.emissive, + &pbrInfo.legacyBump, &pbrInfo.legacyDiffuse, + &pbrInfo.legacySpecular, &pbrInfo.legacyEmissive + }; + for ( unsigned int i = 0; i < sizeof( pbrTextures ) / sizeof( pbrTextures[0] ); ++i ) { + if ( pbrTextures[i]->present && pbrTextures[i]->image != NULL ) { + pbrTextures[i]->image->AddUseCount( useCount ); + } + } + if ( specularProbeInfo.enabled && specularProbeInfo.cubeImage != NULL ) { + specularProbeInfo.cubeImage->AddUseCount( useCount ); + } } /* @@ -3847,6 +4674,9 @@ idMaterial::ImageName */ const char *idMaterial::ImageName( void ) const { if ( numStages == 0 ) { + if ( pbrInfo.enabled && pbrInfo.albedo.image != NULL ) { + return pbrInfo.albedo.image->GetName(); + } return "_scratch"; } idImage *image = stages[0].texture.image; @@ -3980,6 +4810,9 @@ static bool MaterialStagesHaveActiveCustomGLSLLighting( const shaderStage_t *sta } bool idMaterial::HasActiveCustomGLSLLighting( const float *registers ) const { + if ( !hasCustomGLSLLightingStage ) { + return false; + } return MaterialStagesHaveActiveCustomGLSLLighting( stages, numStages, registers ); } @@ -4036,6 +4869,9 @@ idMaterial::CanUseStockShadowMapReceiverForCustomGLSLLighting =================== */ bool idMaterial::CanUseStockShadowMapReceiverForCustomGLSLLighting( const float *registers ) const { + if ( !hasCustomGLSLLightingStage ) { + return false; + } return MaterialStagesHaveActiveCustomGLSLLighting( stages, numStages, registers ) && MaterialStagesHaveActiveStockLightingInteractions( stages, numStages, registers ); } @@ -4083,6 +4919,563 @@ bool R_MaterialCustomGLSLReceiverHelperSelfTest( void ) { && !MaterialStagesHaveActiveStockLightingInteractions( inactiveStockStages, 3, shaderRegisters ); } +/* +=================== +R_SpecularProbeMaterialParserSelfTest + +Uses the intrinsic normalization cubemap so the parser contract can be tested +without loose content. Invalid declarations must fail as a whole and expose no +probe metadata. +=================== +*/ +bool R_SpecularProbeMaterialParserSelfTest( void ) { + static const char validProbe[] = + "material _specular_probe_selftest_valid {\n" + " openQ4SpecularProbe {\n" + " cubeMap normalCubeMap\n" + " tint 0.75 1 1.25\n" + " intensity 2\n" + " blendFraction 0.4\n" + " priority 17\n" + " }\n" + " {\n" + " map _white\n" + " }\n" + "}\n"; + idDecl *validDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( validDecl == NULL ) { + return false; + } + idMaterial *valid = static_cast( validDecl ); + bool ok = valid->Parse( validProbe, + idLib::SizeToInt( sizeof( validProbe ) - 1, + "R_SpecularProbeMaterialParserSelfTest valid" ) ); + if ( ok ) { + const specularProbeMaterialInfo_t &info = valid->GetSpecularProbeInfo(); + ok = valid->HasSpecularProbe() + && info.cubeImage == globalImages->normalCubeMapImage + && info.cubeConvention == SPECULAR_PROBE_CUBE_NATIVE + && idMath::Fabs( info.tint[0] - 0.75f ) < 0.0001f + && idMath::Fabs( info.tint[1] - 1.0f ) < 0.0001f + && idMath::Fabs( info.tint[2] - 1.25f ) < 0.0001f + && idMath::Fabs( info.intensity - 2.0f ) < 0.0001f + && idMath::Fabs( info.blendFraction - 0.4f ) < 0.0001f + && info.priority == 17 + && valid->GetNumStages() == 1 + && valid->GetStage( 0 )->texture.image == globalImages->whiteImage; + } + DeclManager_FreeAllocatedDecl( validDecl ); + if ( !ok ) { + common->Printf( "RendererSpecularProbe material parser self-test: valid contract failed\n" ); + return false; + } + + static const char defaultPolicy[] = + "material _specular_probe_selftest_defaults {\n" + " openQ4SpecularProbe {\n" + " cubeMap normalCubeMap\n" + " }\n" + "}\n"; + idDecl *defaultDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( defaultDecl == NULL ) { + return false; + } + idMaterial *defaults = static_cast( defaultDecl ); + ok = defaults->Parse( defaultPolicy, + idLib::SizeToInt( sizeof( defaultPolicy ) - 1, + "R_SpecularProbeMaterialParserSelfTest defaults" ) ); + if ( ok ) { + const specularProbeMaterialInfo_t &info = defaults->GetSpecularProbeInfo(); + ok = info.enabled && info.tint[0] == 1.0f && info.tint[1] == 1.0f + && info.tint[2] == 1.0f && info.intensity == 1.0f + && info.blendFraction == 0.25f && info.priority == 0; + } + DeclManager_FreeAllocatedDecl( defaultDecl ); + if ( !ok ) { + common->Printf( "RendererSpecularProbe material parser self-test: default policy failed\n" ); + return false; + } + + auto rejectsProbeDeclaration = []( const char *declaration, const char *label ) -> bool { + idDecl *decl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( decl == NULL ) { + return false; + } + idMaterial *material = static_cast( decl ); + const bool accepted = material->Parse( declaration, + idLib::SizeToInt( strlen( declaration ), label ) ); + const bool leakedPartialContract = material->HasSpecularProbe(); + DeclManager_FreeAllocatedDecl( decl ); + return !accepted && !leakedPartialContract; + }; + static const char duplicateBlock[] = + "material _specular_probe_selftest_duplicate {\n" + " openQ4SpecularProbe { cubeMap normalCubeMap\n }\n" + " openQ4SpecularProbe { cubeMap normalCubeMap\n }\n" + "}\n"; + static const char missingCube[] = + "material _specular_probe_selftest_missing_cube {\n" + " openQ4SpecularProbe { intensity 1\n }\n" + "}\n"; + static const char twoDimensionalImage[] = + "material _specular_probe_selftest_2d {\n" + " openQ4SpecularProbe { cubeMap _white\n }\n" + "}\n"; + static const char mutableImage[] = + "material _specular_probe_selftest_mutable {\n" + " openQ4SpecularProbe { cubeMap _currentRender\n }\n" + "}\n"; + static const char imageProgram[] = + "material _specular_probe_selftest_program {\n" + " openQ4SpecularProbe { cubeMap add( normalCubeMap, normalCubeMap )\n }\n" + "}\n"; + static const char zeroIntensity[] = + "material _specular_probe_selftest_zero {\n" + " openQ4SpecularProbe { cubeMap normalCubeMap\n intensity 0\n }\n" + "}\n"; + static const char fractionalPriority[] = + "material _specular_probe_selftest_priority {\n" + " openQ4SpecularProbe { cubeMap normalCubeMap\n priority 1.5\n }\n" + "}\n"; + + return rejectsProbeDeclaration( duplicateBlock, "R_SpecularProbeMaterialParserSelfTest duplicate" ) + && rejectsProbeDeclaration( missingCube, "R_SpecularProbeMaterialParserSelfTest missing cube" ) + && rejectsProbeDeclaration( twoDimensionalImage, "R_SpecularProbeMaterialParserSelfTest 2D image" ) + && rejectsProbeDeclaration( mutableImage, "R_SpecularProbeMaterialParserSelfTest mutable image" ) + && rejectsProbeDeclaration( imageProgram, "R_SpecularProbeMaterialParserSelfTest image program" ) + && rejectsProbeDeclaration( zeroIntensity, "R_SpecularProbeMaterialParserSelfTest zero intensity" ) + && rejectsProbeDeclaration( fractionalPriority, "R_SpecularProbeMaterialParserSelfTest fractional priority" ); +} + +/* +=================== +R_PBRMaterialParserSelfTest + +Runtime parser test using only intrinsic images. It proves metadata parsing, +classic-stage non-interference, explicit fallback generation, and the two +important authoring failures without requiring repository content. +=================== +*/ +bool R_PBRMaterialParserSelfTest( void ) { + static const char dualAuthored[] = + "material _pbr_selftest_dual {\n" + " bumpmap _flat\n" + " diffusemap _white\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap heightmap( _white, 1 )\n" + " normalMap _flat\n" + " normalFormat tangentRG\n" + " ormMap smoothnormals( _flat )\n" + " emissiveMap _white\n" + " metallic 0.25\n" + " roughness 0.6\n" + " ao 0.9\n" + " normalScale 0.75\n" + " emissiveColor 0.1 0.2 0.3\n" + " }\n" + "}\n"; + + idDecl *dualDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( dualDecl == NULL ) { + return false; + } + idMaterial *dual = static_cast( dualDecl ); + bool ok = dual->Parse( dualAuthored, idLib::SizeToInt( sizeof( dualAuthored ) - 1, "R_PBRMaterialParserSelfTest dual" ) ); + if ( !ok ) { + common->Printf( "RendererPBRMaterial parser self-test: dual declaration did not parse\n" ); + } + if ( ok ) { + const pbrMaterialInfo_t &info = dual->GetPBRInfo(); + const int registerCount = dual->GetNumRegisters(); + float evaluatedRegisters[MAX_EXPRESSION_REGISTERS]; + float entityParms[MAX_ENTITY_SHADER_PARMS]; + viewDef_t evaluationView; + memset( evaluatedRegisters, 0, sizeof( evaluatedRegisters ) ); + memset( entityParms, 0, sizeof( entityParms ) ); + memset( &evaluationView, 0, sizeof( evaluationView ) ); + dual->EvaluateRegisters( evaluatedRegisters, entityParms, &evaluationView ); + auto registerMatches = [&]( int index, float expected ) -> bool { + return index >= 0 && index < registerCount + && idMath::Fabs( evaluatedRegisters[index] - expected ) < 0.0001f; + }; + const idImage *sameAlbedo = info.albedo.image != NULL ? globalImages->GetImageWithParameters( + info.albedo.image->GetName(), + static_cast( info.albedo.filter ), + static_cast( info.albedo.repeat ), + TD_PBR_COLOR, CF_2D, info.albedo.allowPicmip, 0 ) : NULL; + const idImage *sameORM = info.orm.image != NULL ? globalImages->GetImageWithParameters( + info.orm.image->GetName(), + static_cast( info.orm.filter ), + static_cast( info.orm.repeat ), + TD_MATERIAL_DATA, CF_2D, info.orm.allowPicmip, 0 ) : NULL; + ok = dual->HasPBR() + && info.workflow == PBR_WORKFLOW_METALLIC_ROUGHNESS + && info.normalFormat == PBR_NORMAL_TANGENT_RG + && info.albedo.present && info.normal.present && info.orm.present + && info.albedo.image->GetUsage() == TD_PBR_COLOR + && info.orm.image->GetUsage() == TD_MATERIAL_DATA + && sameAlbedo == info.albedo.image + && sameORM == info.orm.image + && registerMatches( info.metallicRegister, 0.25f ) + && registerMatches( info.roughnessRegister, 0.6f ) + && registerMatches( info.aoRegister, 0.9f ) + && registerMatches( info.normalScaleRegister, 0.75f ) + && registerMatches( info.emissiveColorRegisters[0], 0.1f ) + && registerMatches( info.emissiveColorRegisters[1], 0.2f ) + && registerMatches( info.emissiveColorRegisters[2], 0.3f ) + && info.hasAuthoredClassicFallback + && !info.usesGeneratedLegacyFallback + && dual->GetNumStages() == 2; + if ( !ok ) { + common->Printf( + "RendererPBRMaterial parser self-test: dual contract failed has=%d workflow=%d normal=%d maps=%d/%d/%d usage=%d/%d cache=%d/%d stages=%d authored=%d generated=%d registers=%d/%d/%d/%d/%d/%d/%d\n", + dual->HasPBR() ? 1 : 0, + static_cast( info.workflow ), + static_cast( info.normalFormat ), + info.albedo.present ? 1 : 0, + info.normal.present ? 1 : 0, + info.orm.present ? 1 : 0, + info.albedo.image != NULL ? static_cast( info.albedo.image->GetUsage() ) : -1, + info.orm.image != NULL ? static_cast( info.orm.image->GetUsage() ) : -1, + sameAlbedo == info.albedo.image ? 1 : 0, + sameORM == info.orm.image ? 1 : 0, + dual->GetNumStages(), + info.hasAuthoredClassicFallback ? 1 : 0, + info.usesGeneratedLegacyFallback ? 1 : 0, + registerMatches( info.metallicRegister, 0.25f ) ? 1 : 0, + registerMatches( info.roughnessRegister, 0.6f ) ? 1 : 0, + registerMatches( info.aoRegister, 0.9f ) ? 1 : 0, + registerMatches( info.normalScaleRegister, 0.75f ) ? 1 : 0, + registerMatches( info.emissiveColorRegisters[0], 0.1f ) ? 1 : 0, + registerMatches( info.emissiveColorRegisters[1], 0.2f ) ? 1 : 0, + registerMatches( info.emissiveColorRegisters[2], 0.3f ) ? 1 : 0 ); + } + } + DeclManager_FreeAllocatedDecl( dualDecl ); + if ( !ok ) { + return false; + } + + static const char explicitFallback[] = + "material _pbr_selftest_explicit {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " legacyBumpMap _flat\n" + " legacyDiffuseMap nearest clamp nopicmip nomips forceHighQuality makeIntensity( _white )\n" + " autoLegacyFallback 0\n" + " }\n" + "}\n"; + idDecl *explicitDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( explicitDecl == NULL ) { + return false; + } + idMaterial *explicitMaterial = static_cast( explicitDecl ); + const bool oldMakingBuild = com_makingBuild.GetBool(); + com_makingBuild.SetBool( true ); + ok = explicitMaterial->Parse( explicitFallback, idLib::SizeToInt( sizeof( explicitFallback ) - 1, "R_PBRMaterialParserSelfTest explicit" ) ); + com_makingBuild.SetBool( oldMakingBuild ); + if ( ok ) { + const pbrMaterialInfo_t &info = explicitMaterial->GetPBRInfo(); + const shaderStage_t *diffuseStage = NULL; + for ( int i = 0; i < explicitMaterial->GetNumStages(); ++i ) { + const shaderStage_t *stage = explicitMaterial->GetStage( i ); + if ( stage != NULL && stage->lighting == SL_DIFFUSE ) { + diffuseStage = stage; + break; + } + } + const idImage *expectedDiffuse = globalImages->GetImageWithParameters( + info.legacyDiffuse.image != NULL ? info.legacyDiffuse.image->GetName() : "", + TF_NEAREST, + TR_CLAMP, + TD_HIGH_QUALITY, + CF_2D, + false, + IMAGEFLAG_NOMIPS ); + ok = info.hasExplicitLegacyFallback + && info.usesGeneratedLegacyFallback + && !info.usesApproximateLegacyFallback + && info.legacyDiffuse.filter == static_cast( TF_NEAREST ) + && info.legacyDiffuse.repeat == static_cast( TR_CLAMP ) + && !info.legacyDiffuse.allowPicmip + && info.legacyDiffuse.noMips + && info.legacyDiffuse.highQuality + && info.legacyDiffuse.forceHighQuality + && explicitMaterial->GetNumStages() == 2 + && diffuseStage != NULL + && diffuseStage->texture.image == expectedDiffuse; + } + DeclManager_FreeAllocatedDecl( explicitDecl ); + if ( !ok ) { + return false; + } + + static const char missingFallback[] = + "material _pbr_selftest_missing_fallback {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " autoLegacyFallback 0\n" + " }\n" + "}\n"; + idDecl *missingDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( missingDecl == NULL ) { + return false; + } + idMaterial *missingMaterial = static_cast( missingDecl ); + ok = missingMaterial->Parse( missingFallback, idLib::SizeToInt( sizeof( missingFallback ) - 1, "R_PBRMaterialParserSelfTest missing fallback" ) ); + if ( ok ) { + const pbrMaterialInfo_t &info = missingMaterial->GetPBRInfo(); + ok = missingMaterial->HasPBR() + && info.legacyFallbackMissing + && !info.usesGeneratedLegacyFallback + && !info.usesApproximateLegacyFallback + && missingMaterial->GetNumStages() == 0; + } + DeclManager_FreeAllocatedDecl( missingDecl ); + if ( !ok ) { + return false; + } + + // A PBR-only declaration is still required to construct the complete ARB2 + // interaction contract when development fallback generation is enabled. Do + // not settle for validating only the normal stage: every low-end renderer + // needs the conventional bump/diffuse/specular trio. + static const char generatedClassicInteractionFallback[] = + "material _pbr_selftest_generated_classic_interaction {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " normalMap _flat\n" + " normalFormat tangentRG\n" + " }\n" + "}\n"; + auto validatesGeneratedClassicInteractionFallback = []( const char *declaration, const char *label ) -> bool { + idDecl *decl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( decl == NULL ) { + return false; + } + idMaterial *material = static_cast( decl ); + bool valid = material->Parse( declaration, idLib::SizeToInt( strlen( declaration ), label ) ); + if ( valid ) { + const pbrMaterialInfo_t &info = material->GetPBRInfo(); + const shaderStage_t *bumpStage = NULL; + const shaderStage_t *diffuseStage = NULL; + const shaderStage_t *specularStage = NULL; + for ( int i = 0; i < material->GetNumStages(); ++i ) { + const shaderStage_t *stage = material->GetStage( i ); + if ( stage == NULL ) { + continue; + } + if ( stage->lighting == SL_BUMP ) { + bumpStage = stage; + } else if ( stage->lighting == SL_DIFFUSE ) { + diffuseStage = stage; + } else if ( stage->lighting == SL_SPECULAR ) { + specularStage = stage; + } + } + valid = info.usesGeneratedLegacyFallback + && info.usesApproximateLegacyFallback + && !info.legacyFallbackMissing + && material->GetNumStages() == 3 + && bumpStage != NULL && bumpStage->texture.image == globalImages->flatNormalMap + && diffuseStage != NULL && diffuseStage->texture.image == info.albedo.image + && specularStage != NULL && specularStage->texture.image == globalImages->blackImage; + } + DeclManager_FreeAllocatedDecl( decl ); + return valid; + }; + + auto validatesGeneratedNormalFallback = []( const char *declaration, const char *label, bool expectReuse ) -> bool { + idDecl *decl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( decl == NULL ) { + return false; + } + idMaterial *material = static_cast( decl ); + bool valid = material->Parse( declaration, idLib::SizeToInt( strlen( declaration ), label ) ); + if ( valid ) { + const pbrMaterialInfo_t &info = material->GetPBRInfo(); + const shaderStage_t *bumpStage = NULL; + for ( int i = 0; i < material->GetNumStages(); ++i ) { + const shaderStage_t *stage = material->GetStage( i ); + if ( stage != NULL && stage->lighting == SL_BUMP ) { + bumpStage = stage; + break; + } + } + valid = info.usesApproximateLegacyFallback + && !info.legacyFallbackMissing + && info.normal.image != NULL + && bumpStage != NULL + && ( expectReuse + ? bumpStage->texture.image == info.normal.image + : bumpStage->texture.image == globalImages->flatNormalMap + && info.normal.image != globalImages->flatNormalMap ); + } + DeclManager_FreeAllocatedDecl( decl ); + return valid; + }; + static const char tangentNormalFallback[] = + "material _pbr_selftest_tangent_normal_fallback {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " normalMap makeIntensity( _white )\n" + " normalFormat tangentRG\n" + " }\n" + "}\n"; + static const char quake4NormalFallback[] = + "material _pbr_selftest_quake4_normal_fallback {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " normalMap makeIntensity( _white )\n" + " normalFormat quake4AGB\n" + " }\n" + "}\n"; + const bool oldGeneratedFallback = r_pbrGeneratedLegacyFallback.GetBool(); + r_pbrGeneratedLegacyFallback.SetBool( true ); + const bool generatedFallbacksValid = validatesGeneratedClassicInteractionFallback( + generatedClassicInteractionFallback, "R_PBRMaterialParserSelfTest complete ARB2 fallback" ) + && validatesGeneratedNormalFallback( + tangentNormalFallback, "R_PBRMaterialParserSelfTest tangent fallback", false ) + && validatesGeneratedNormalFallback( + quake4NormalFallback, "R_PBRMaterialParserSelfTest quake4 fallback", true ); + r_pbrGeneratedLegacyFallback.SetBool( oldGeneratedFallback ); + if ( !generatedFallbacksValid ) { + return false; + } + + static const char missingNormalFormat[] = + "material _pbr_selftest_bad_normal {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " normalMap _flat\n" + " }\n" + "}\n"; + idDecl *badNormalDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( badNormalDecl == NULL ) { + return false; + } + idMaterial *badNormal = static_cast( badNormalDecl ); + const bool badNormalAccepted = badNormal->Parse( missingNormalFormat, idLib::SizeToInt( sizeof( missingNormalFormat ) - 1, "R_PBRMaterialParserSelfTest normal" ) ); + DeclManager_FreeAllocatedDecl( badNormalDecl ); + if ( badNormalAccepted ) { + return false; + } + + static const char conflictingMaps[] = + "material _pbr_selftest_bad_maps {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " ormMap _white\n" + " roughnessMap _white\n" + " }\n" + "}\n"; + idDecl *badMapsDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( badMapsDecl == NULL ) { + return false; + } + idMaterial *badMaps = static_cast( badMapsDecl ); + const bool badMapsAccepted = badMaps->Parse( conflictingMaps, idLib::SizeToInt( sizeof( conflictingMaps ) - 1, "R_PBRMaterialParserSelfTest maps" ) ); + DeclManager_FreeAllocatedDecl( badMapsDecl ); + if ( badMapsAccepted ) { + return false; + } + + static const char fractionalFallback[] = + "material _pbr_selftest_bad_fallback_value {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " autoLegacyFallback 0.5\n" + " }\n" + "}\n"; + idDecl *fractionalDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( fractionalDecl == NULL ) { + return false; + } + idMaterial *fractionalMaterial = static_cast( fractionalDecl ); + const bool fractionalAccepted = fractionalMaterial->Parse( fractionalFallback, idLib::SizeToInt( sizeof( fractionalFallback ) - 1, "R_PBRMaterialParserSelfTest fallback value" ) ); + DeclManager_FreeAllocatedDecl( fractionalDecl ); + if ( fractionalAccepted ) { + return false; + } + + auto rejectsPBRDeclaration = []( const char *declaration, const char *label ) -> bool { + idDecl *decl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( decl == NULL ) { + return false; + } + idMaterial *material = static_cast( decl ); + const bool accepted = material->Parse( declaration, idLib::SizeToInt( strlen( declaration ), label ) ); + DeclManager_FreeAllocatedDecl( decl ); + return !accepted; + }; + static const char dynamicImageToken[] = + "material _pbr_selftest_dynamic_image {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap reflectionRenderMap\n" + " }\n" + "}\n"; + static const char shaderImageToken[] = + "material _pbr_selftest_shader_image {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap glslProgram\n" + " }\n" + "}\n"; + static const char nestedDynamicImageToken[] = + "material _pbr_selftest_nested_dynamic_image {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap add( _white, reflectionRenderMap )\n" + " }\n" + "}\n"; + static const char nestedShaderImageToken[] = + "material _pbr_selftest_nested_shader_image {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap add( _white, glslProgram )\n" + " }\n" + "}\n"; + static const char nestedSceneCaptureToken[] = + "material _pbr_selftest_nested_scene_capture {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap add( _white, _currentRender )\n" + " }\n" + "}\n"; + static const char nestedMutableRenderTargetToken[] = + "material _pbr_selftest_nested_mutable_target {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap add( _white, _reflectionRender )\n" + " }\n" + "}\n"; + static const char nestedStageStateToken[] = + "material _pbr_selftest_nested_stage_state {\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap add( _white, alphaTest )\n" + " }\n" + "}\n"; + const bool pbrRejectionsValid = rejectsPBRDeclaration( dynamicImageToken, "R_PBRMaterialParserSelfTest dynamic image" ) + && rejectsPBRDeclaration( shaderImageToken, "R_PBRMaterialParserSelfTest shader image" ) + && rejectsPBRDeclaration( nestedDynamicImageToken, "R_PBRMaterialParserSelfTest nested dynamic image" ) + && rejectsPBRDeclaration( nestedShaderImageToken, "R_PBRMaterialParserSelfTest nested shader image" ) + && rejectsPBRDeclaration( nestedSceneCaptureToken, "R_PBRMaterialParserSelfTest nested scene capture" ) + && rejectsPBRDeclaration( nestedMutableRenderTargetToken, "R_PBRMaterialParserSelfTest nested mutable target" ) + && rejectsPBRDeclaration( nestedStageStateToken, "R_PBRMaterialParserSelfTest nested stage state" ); + return pbrRejectionsValid && R_SpecularProbeMaterialParserSelfTest(); +} + /* =================== idMaterial::ReloadImages @@ -4110,4 +5503,19 @@ void idMaterial::ReloadImages( bool force ) const if ( portalImage ) { portalImage->Reload( force ); } + + const pbrMaterialTexture_t *pbrTextures[] = { + &pbrInfo.albedo, &pbrInfo.normal, &pbrInfo.orm, &pbrInfo.metallic, + &pbrInfo.roughness, &pbrInfo.ao, &pbrInfo.emissive, + &pbrInfo.legacyBump, &pbrInfo.legacyDiffuse, + &pbrInfo.legacySpecular, &pbrInfo.legacyEmissive + }; + for ( unsigned int i = 0; i < sizeof( pbrTextures ) / sizeof( pbrTextures[0] ); ++i ) { + if ( pbrTextures[i]->present && pbrTextures[i]->image != NULL ) { + pbrTextures[i]->image->Reload( force ); + } + } + if ( specularProbeInfo.enabled && specularProbeInfo.cubeImage != NULL ) { + specularProbeInfo.cubeImage->Reload( force ); + } } diff --git a/src/renderer/Material.h b/src/renderer/Material.h index d5326d83..faaf1be6 100644 --- a/src/renderer/Material.h +++ b/src/renderer/Material.h @@ -41,6 +41,81 @@ class idImage; class idCinematic; class idUserInterface; +// openQ4-only, opt-in physically based material metadata. This is kept +// separate from the classic stage list so retail Quake 4 materials retain +// their exact bump/diffuse/specular parsing and rendering contract. +typedef enum { + PBR_WORKFLOW_NONE = 0, + PBR_WORKFLOW_METALLIC_ROUGHNESS, + PBR_WORKFLOW_SPECULAR_GLOSSINESS +} pbrWorkflow_t; + +typedef enum { + PBR_NORMAL_UNSPECIFIED = -1, + PBR_NORMAL_QUAKE4_AGB = 0, + PBR_NORMAL_TANGENT_RG, + PBR_NORMAL_TANGENT_XYZ +} pbrNormalFormat_t; + +typedef struct { + idImage * image; + bool present; + int filter; // textureFilter_t, retained for classic fallback generation + int repeat; // textureRepeat_t, retained for classic fallback generation + bool allowPicmip; + bool noMips; + bool highQuality; + bool forceHighQuality; +} pbrMaterialTexture_t; + +typedef struct { + bool enabled; + pbrWorkflow_t workflow; + pbrNormalFormat_t normalFormat; + pbrMaterialTexture_t albedo; + pbrMaterialTexture_t normal; + pbrMaterialTexture_t orm; + pbrMaterialTexture_t metallic; + pbrMaterialTexture_t roughness; + pbrMaterialTexture_t ao; + pbrMaterialTexture_t emissive; + pbrMaterialTexture_t legacyBump; + pbrMaterialTexture_t legacyDiffuse; + pbrMaterialTexture_t legacySpecular; + pbrMaterialTexture_t legacyEmissive; + int metallicRegister; + int roughnessRegister; + int aoRegister; + int normalScaleRegister; + int emissiveColorRegisters[3]; + bool autoLegacyFallback; + bool hasAuthoredClassicFallback; + bool hasExplicitLegacyFallback; + bool usesGeneratedLegacyFallback; + bool usesApproximateLegacyFallback; + bool legacyFallbackMissing; +} pbrMaterialInfo_t; + +// An authored specular probe is renderer metadata carried by a light material. +// The light still supplies the spatial volume; this block supplies only the +// immutable image and ranking/blending policy. Keeping it out of renderLight_t +// preserves the game-module, save-game, demo, and network contracts. +typedef enum { + SPECULAR_PROBE_CUBE_NONE = 0, + SPECULAR_PROBE_CUBE_NATIVE, + SPECULAR_PROBE_CUBE_CAMERA +} specularProbeCubeConvention_t; + +typedef struct { + bool enabled; + idImage * cubeImage; + specularProbeCubeConvention_t cubeConvention; + float tint[3]; + float intensity; + float blendFraction; + int priority; +} specularProbeMaterialInfo_t; + // moved from image.h for default parm typedef enum { TF_LINEAR, @@ -505,6 +580,16 @@ class idMaterial : public idDecl { // get a specific stage const shaderStage_t* GetStage(const int index) const { assert(index >= 0 && index < numStages); return &stages[index]; } + // PBR metadata is consumed only by explicitly enabled modern renderer paths. + // Classic callers continue to use the unchanged stage list. + bool HasPBR(void) const { return pbrInfo.enabled; } + const pbrMaterialInfo_t& GetPBRInfo(void) const { return pbrInfo; } + + // openQ4-authored light-material metadata. Classic material stages and + // every public render-light/game ABI remain unchanged. + bool HasSpecularProbe(void) const { return specularProbeInfo.enabled && !TestMaterialFlag( MF_DEFAULTED ); } + const specularProbeMaterialInfo_t& GetSpecularProbeInfo(void) const { return specularProbeInfo; } + // Retarget a parsed stage without rewriting authored declaration text. // Runtime-generated images (for example the scalable console font atlas) // must not change the declaration checksum used by multiplayer handshakes. @@ -801,6 +886,10 @@ class idMaterial : public idDecl { void ParseShaderParm(idLexer& src, newShaderStage_t* newStage); void ParseShaderTexture(idLexer& src, newShaderStage_t* newStage); void ParseStage(idLexer& src, const textureRepeat_t trpDefault = TR_REPEAT); + bool ParsePBRBlock(idLexer& src, const textureRepeat_t trpDefault); + bool ParsePBRImage(idLexer& src, pbrMaterialTexture_t& target, const int usage, const textureRepeat_t trpDefault); + bool ParseSpecularProbeBlock(idLexer& src); + void AddPBRLegacyFallbackStages(const textureRepeat_t trpDefault); void ParseDeform(idLexer& src); void ParseDecalInfo(idLexer& src); bool CheckSurfaceParm(idToken* token); @@ -906,11 +995,20 @@ class idMaterial : public idDecl { bool suppressInSubview; bool portalSky; + pbrMaterialInfo_t pbrInfo; + specularProbeMaterialInfo_t specularProbeInfo; + // parse-time structural fact: some stage carries an active-able customLighting + // GLSL program (newStage with customLighting + glslProgram). Lets the hot + // per-surface interaction and shadow-receiver chain scans early-out without + // walking every stage; register conditions are still evaluated when set. + bool hasCustomGLSLLightingStage; int refCount; }; // Parser-free regression hook for custom GLSL receiver compatibility helpers. bool R_MaterialCustomGLSLReceiverHelperSelfTest( void ); +bool R_PBRMaterialParserSelfTest( void ); +bool R_SpecularProbeMaterialParserSelfTest( void ); typedef idList idMatList; diff --git a/src/renderer/MaterialResourceTable.cpp b/src/renderer/MaterialResourceTable.cpp index 89719b32..88cfac2a 100644 --- a/src/renderer/MaterialResourceTable.cpp +++ b/src/renderer/MaterialResourceTable.cpp @@ -11,6 +11,11 @@ const int MATERIAL_RESOURCE_TABLE_HASH_SIZE = MATERIAL_RESOURCE_TABLE_MAX_RECORD typedef struct materialResourceTableState_s { materialResourceTableRecord_t records[MATERIAL_RESOURCE_TABLE_MAX_RECORDS]; + rendererMaterialPass_t guiPasses[MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES]; + int guiPassCount; + rendererMaterialPass_t worldPasses[MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES]; + int worldPassCount; + unsigned int tableGeneration; materialResourceTableStats_t stats; renderBackendCaps_t caps; renderFeatureSet_t features; @@ -22,6 +27,18 @@ typedef struct materialResourceTableState_s { static materialResourceTableState_t rg_materialResourceTable; +static std::uint64_t R_MaterialResourceTable_MakeTextureResourceId( + int recordIndex, int bindingIndex ) { + if ( rg_materialResourceTable.tableGeneration == 0 + || recordIndex < 0 || recordIndex >= MATERIAL_RESOURCE_TABLE_MAX_RECORDS + || bindingIndex < 0 || bindingIndex >= MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS ) { + return 0; + } + return ( static_cast( rg_materialResourceTable.tableGeneration ) << 32 ) + | ( static_cast( recordIndex + 1 ) << 16 ) + | static_cast( bindingIndex + 1 ); +} + static int R_MaterialResourceTable_HashMaterial( const idMaterial *material ) { size_t key = reinterpret_cast( material ) >> 4; key *= 2654435761u; @@ -82,12 +99,57 @@ const char *MaterialResourceTextureSemantic_Name( materialResourceTextureSemanti return "gui"; case MATERIAL_RESOURCE_TEXTURE_POST_PROCESS: return "post"; + case MATERIAL_RESOURCE_TEXTURE_ALBEDO: + return "pbrAlbedo"; + case MATERIAL_RESOURCE_TEXTURE_NORMAL: + return "pbrNormal"; + case MATERIAL_RESOURCE_TEXTURE_ORM: + return "pbrORM"; + case MATERIAL_RESOURCE_TEXTURE_METALLIC: + return "pbrMetallic"; + case MATERIAL_RESOURCE_TEXTURE_ROUGHNESS: + return "pbrRoughness"; + case MATERIAL_RESOURCE_TEXTURE_AO: + return "pbrAO"; + case MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR: + return "pbrEmissive"; case MATERIAL_RESOURCE_TEXTURE_NONE: default: return "none"; } } +const char *MaterialResourcePBRFallbackReason_Name( materialResourcePBRFallbackReason_t reason ) { + switch ( reason ) { + case MATERIAL_RESOURCE_PBR_FALLBACK_NONE: + return "none"; + case MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED: + return "disabled"; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW: + return "unsupportedWorkflow"; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_MATERIAL_CLASS: + return "unsupportedMaterialClass"; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO: + return "missingAlbedo"; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_NORMAL_FORMAT: + return "missingNormalFormat"; + case MATERIAL_RESOURCE_PBR_FALLBACK_CONFLICTING_LAYOUT: + return "conflictingLayout"; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_TEXTURE_LAYOUT: + return "unsupportedTextureLayout"; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE: + return "missingImage"; + case MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE: + return "classicFeature"; + case MATERIAL_RESOURCE_PBR_FALLBACK_TOO_MANY_TEXTURES: + return "tooManyTextures"; + case MATERIAL_RESOURCE_PBR_FALLBACK_SHADER_PATH_UNAVAILABLE: + return "shaderPathUnavailable"; + default: + return "unknown"; + } +} + unsigned int MaterialResourceTextureSemantic_Bit( materialResourceTextureSemantic_t semantic ) { if ( semantic <= MATERIAL_RESOURCE_TEXTURE_NONE || semantic >= MATERIAL_RESOURCE_TEXTURE_COUNT ) { return 0; @@ -191,6 +253,70 @@ static void R_MaterialResourceTable_AddFallback( materialResourceTableRecord_t & } } +const char *MaterialResourceGuiPassFailure_Name( materialResourceGuiPassFailure_t reason ) { + switch ( reason ) { + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE: + return "none"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED: + return "notReferenced"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_COMPILED: + return "notCompiled"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL: + return "missingMaterial"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_DRAWN: + return "notDrawn"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NO_AMBIENT_STAGES: + return "noAmbientStages"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_NEEDS_CURRENT_RENDER: + return "needsCurrentRender"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_DECAL_SORT: + return "decalSort"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_POST_PROCESS_SORT: + return "postProcessSort"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_DYNAMIC_IMAGE: + return "dynamicImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_CINEMATIC_IMAGE: + return "cinematicImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_CURRENT_RENDER_IMAGE: + return "currentRenderImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_CUSTOM_PROGRAM: + return "customProgram"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_TEXGEN: + return "unsupportedTexgen"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_ALPHA_TEST: + return "unsupportedAlphaTest"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_IMAGE: + return "missingImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_DEFAULTED_IMAGE: + return "defaultedImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNLOADED_IMAGE: + return "unloadedImage"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_TEXTURE_BINDING_OVERFLOW: + return "textureBindingOverflow"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW: + return "passPoolOverflow"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_REGISTER: + return "invalidRegister"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE: + return "unsupportedState"; + case MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_PASS: + return "invalidPass"; + default: + return "unknown"; + } +} + +const char *MaterialResourceWorldPassFailure_Name( + materialResourceWorldPassFailure_t reason ) { + return MaterialResourceGuiPassFailure_Name( reason ); +} + +static void R_MaterialResourceTable_AddPBRFallback( materialResourceTableRecord_t &record, materialResourcePBRFallbackReason_t reason ) { + if ( record.pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_NONE ) { + record.pbrFallbackReason = reason; + } +} + static bool R_MaterialResourceTable_ImageIsPostProcess( const idImage *image ) { if ( image == NULL ) { return false; @@ -206,21 +332,7 @@ static bool R_MaterialResourceTable_ImageIsPostProcess( const idImage *image ) { } static bool R_MaterialResourceTable_ImageIsSceneCapture( const idImage *image ) { - if ( image == NULL ) { - return false; - } - if ( globalImages != NULL - && ( image == globalImages->currentRenderImage - || image == globalImages->originalCurrentRenderImage - || image == globalImages->currentDepthImage ) ) { - return true; - } - const char *name = image->GetName(); - if ( name == NULL ) { - return false; - } - return idStr::Icmpn( name, "_currentRender", 14 ) == 0 - || idStr::Icmpn( name, "_currentDepth", 13 ) == 0; + return R_IsMutableRenderImage( image ); } static bool R_MaterialResourceTable_TexgenIsScreenSpace( texgen_t texgen ) { @@ -372,6 +484,16 @@ static int R_MaterialResourceTable_SemanticSlot( materialResourceTextureSemantic return 4; case MATERIAL_RESOURCE_TEXTURE_POST_PROCESS: return 5; + case MATERIAL_RESOURCE_TEXTURE_ALBEDO: + case MATERIAL_RESOURCE_TEXTURE_NORMAL: + case MATERIAL_RESOURCE_TEXTURE_ORM: + case MATERIAL_RESOURCE_TEXTURE_METALLIC: + case MATERIAL_RESOURCE_TEXTURE_ROUGHNESS: + case MATERIAL_RESOURCE_TEXTURE_AO: + case MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR: + // Phase 3 records PBR handles but does not allocate visible shader units. + // The existing classic and shadow bindings therefore remain untouched. + return -1; default: return -1; } @@ -385,6 +507,89 @@ static const materialResourceTextureBinding_t *R_MaterialResourceTable_FindStage return NULL; } +static bool R_MaterialResourceTable_BindingsSampleSameImage( + const materialResourceTextureBinding_t &a, + const materialResourceTextureBinding_t &b ) { + if ( a.image == NULL || b.image == NULL ) { + return false; + } + const char *aName = a.image->GetName(); + const char *bName = b.image->GetName(); + return ( a.image == b.image + || ( aName != NULL && bName != NULL && aName[0] != '\0' && !idStr::Icmp( aName, bName ) ) ) + && a.filter == b.filter + && a.repeat == b.repeat; +} + +static bool R_MaterialResourceTable_BindingHasIdentityColor( + const materialResourceTableRecord_t &record, + const materialResourceTextureBinding_t &binding ) { + if ( record.material == NULL ) { + return false; + } + const float *constantRegisters = record.material->ConstantRegisters(); + if ( constantRegisters == NULL ) { + return false; + } + for ( int component = 0; component < 4; ++component ) { + const int registerIndex = binding.colorRegisters[component]; + if ( registerIndex < 0 || registerIndex >= record.material->GetNumRegisters() + || constantRegisters[registerIndex] != 1.0f ) { + return false; + } + } + return true; +} + +static const materialResourceTextureBinding_t *R_MaterialResourceTable_FindOnlySourceAlphaStage( + const materialResourceTableRecord_t &record ) { + const materialResourceTextureBinding_t *result = NULL; + for ( int bindingIndex = 0; bindingIndex < record.textureBindingCount; ++bindingIndex ) { + const materialResourceTextureBinding_t &binding = record.textures[bindingIndex]; + if ( binding.stageIndex < 0 || !R_MaterialResourceTable_IsAlphaBlend( binding.drawStateBits ) ) { + continue; + } + if ( result != NULL ) { + return NULL; + } + result = &binding; + } + return result; +} + +static bool R_MaterialResourceTable_HasPBRSourceAlphaBlendContract( const materialResourceTableRecord_t &record ) { + // The clustered owner samples PBR albedo once and applies source alpha from + // that texel. Admit an authored `blend blend` stage only when it is exactly + // the same image/sampler, a neutral-color ambient pass, and unmodified + // explicit UVs. Anything less would silently drop stage semantics. + if ( !record.hasPBR + || record.blendMode != MATERIAL_RESOURCE_BLEND_BLEND + || record.blendStageCount != 1 + || record.additiveStageCount != 0 + || record.filterStageCount != 0 + || record.alphaTest ) { + return false; + } + const materialResourceTextureBinding_t *albedo = + R_MaterialResourceTable_TextureBindingForSemantic( record, MATERIAL_RESOURCE_TEXTURE_ALBEDO ); + const materialResourceTextureBinding_t *alphaStage = + R_MaterialResourceTable_FindOnlySourceAlphaStage( record ); + return albedo != NULL + && alphaStage != NULL + && alphaStage->semantic == MATERIAL_RESOURCE_TEXTURE_EMISSIVE + && alphaStage->blendEnabled + && !alphaStage->depthWrite + && !alphaStage->colorMasked + && !alphaStage->hasConditionRegister + && !alphaStage->hasAlphaTest + && !alphaStage->hasTextureMatrix + && alphaStage->privatePolygonOffset == 0.0f + && alphaStage->texgen == static_cast( TG_EXPLICIT ) + && alphaStage->vertexColorMode == static_cast( SVC_IGNORE ) + && R_MaterialResourceTable_BindingsSampleSameImage( *albedo, *alphaStage ) + && R_MaterialResourceTable_BindingHasIdentityColor( record, *alphaStage ); +} + static bool R_MaterialResourceTable_ColorRegistersMatch( const materialResourceTextureBinding_t &a, const materialResourceTextureBinding_t &b ) { for ( int i = 0; i < 4; ++i ) { if ( a.colorRegisters[i] != b.colorRegisters[i] ) { @@ -403,6 +608,12 @@ static void R_MaterialResourceTable_AddStageColorFallback( materialResourceTable } static void R_MaterialResourceTable_ValidateStageColorContract( materialResourceTableRecord_t &record ) { + // The source-alpha PBR contract deliberately keeps its own alpha stage + // while the direct path samples the matching PBR albedo. Its distinct stage + // color registers are therefore expected, not an unsupported overlay. + if ( R_MaterialResourceTable_HasPBRSourceAlphaBlendContract( record ) ) { + return; + } const materialResourceTextureBinding_t *diffuse = R_MaterialResourceTable_FindStageBinding( record, MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); const materialResourceTextureBinding_t *emissive = R_MaterialResourceTable_FindStageBinding( record, MATERIAL_RESOURCE_TEXTURE_EMISSIVE ); if ( diffuse == NULL || emissive == NULL || R_MaterialResourceTable_ColorRegistersMatch( *diffuse, *emissive ) ) { @@ -434,6 +645,13 @@ static bool R_MaterialResourceTable_AmbientOverlayNeedsLegacyStage( const materi } static void R_MaterialResourceTable_ValidateAmbientOverlayContract( materialResourceTableRecord_t &record ) { + // `blend blend` source-alpha PBR declarations are expanded into an opaque + // interaction plus an emissive-labelled alpha stage by the material parser. + // Their direct PBR owner consumes that alpha stage as ordered transparency; + // it is not a separate glow overlay. + if ( R_MaterialResourceTable_HasPBRSourceAlphaBlendContract( record ) ) { + return; + } if ( record.materialClass != RENDER_MATERIAL_OPAQUE && record.materialClass != RENDER_MATERIAL_PERFORATED ) { return; } @@ -552,6 +770,27 @@ static void R_MaterialResourceTable_UpdateRecordSemanticFlags( materialResourceT case MATERIAL_RESOURCE_TEXTURE_POST_PROCESS: record.hasPostProcess |= present; break; + case MATERIAL_RESOURCE_TEXTURE_ALBEDO: + record.hasPBRAlbedo |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_NORMAL: + record.hasPBRNormal |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_ORM: + record.hasPBRORM |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_METALLIC: + record.hasPBRMetallic |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_ROUGHNESS: + record.hasPBRRoughness |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_AO: + record.hasPBRAO |= present; + break; + case MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR: + record.hasPBREmissive |= present; + break; default: break; } @@ -630,26 +869,41 @@ static bool R_MaterialResourceTable_HasSemanticBinding( const materialResourceTa return R_MaterialResourceTable_FindTextureBindingIndex( record, semantic ) >= 0; } -static void R_MaterialResourceTable_AddTextureBinding( +static bool R_MaterialResourceTable_IsPBRSemantic( materialResourceTextureSemantic_t semantic ) { + return semantic >= MATERIAL_RESOURCE_TEXTURE_ALBEDO && semantic < MATERIAL_RESOURCE_TEXTURE_COUNT; +} + +static int R_MaterialResourceTable_AddTextureBinding( materialResourceTableRecord_t &record, materialResourceTextureSemantic_t semantic, const idImage *image, const shaderStage_t *stage, int stageIndex ) { if ( semantic <= MATERIAL_RESOURCE_TEXTURE_NONE || semantic >= MATERIAL_RESOURCE_TEXTURE_COUNT || image == NULL ) { - record.hasMissingImage = true; - R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_MISSING_IMAGE, MATERIAL_RESOURCE_FALLBACK_FLAG_MISSING_IMAGE ); + if ( R_MaterialResourceTable_IsPBRSemantic( semantic ) ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE ); + } else { + record.hasMissingImage = true; + R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_MISSING_IMAGE, MATERIAL_RESOURCE_FALLBACK_FLAG_MISSING_IMAGE ); + } rg_materialResourceTable.stats.missingImages++; - return; + return -1; } - if ( R_MaterialResourceTable_HasSemanticBinding( record, semantic ) ) { + // Source-record convenience images remain one-per-semantic. Authored stage + // bindings do not: two GUI stages sampling the same semantic are distinct + // ordered operations and must retain their own image/state identity. + if ( stage == NULL && R_MaterialResourceTable_HasSemanticBinding( record, semantic ) ) { R_MaterialResourceTable_UpdateRecordSemanticFlags( record, semantic, image ); - return; + return R_MaterialResourceTable_FindTextureBindingIndex( record, semantic ); } if ( record.textureBindingCount >= MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS ) { - R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_TOO_MANY_TEXTURES, MATERIAL_RESOURCE_FALLBACK_FLAG_TOO_MANY_TEXTURES ); + if ( R_MaterialResourceTable_IsPBRSemantic( semantic ) ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_TOO_MANY_TEXTURES ); + } else { + R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_TOO_MANY_TEXTURES, MATERIAL_RESOURCE_FALLBACK_FLAG_TOO_MANY_TEXTURES ); + } rg_materialResourceTable.stats.unsupportedFeatures++; - return; + return -1; } const int bindingIndex = record.textureBindingCount++; @@ -657,12 +911,18 @@ static void R_MaterialResourceTable_AddTextureBinding( memset( &binding, 0, sizeof( binding ) ); binding.semantic = semantic; binding.image = image; + binding.textureResourceId = R_MaterialResourceTable_MakeTextureResourceId( + record.tableIndex, bindingIndex ); binding.textureHandle = image->IsLoaded() ? const_cast( image )->GetDeviceHandle() : 0; binding.filter = image->GetFilter(); binding.repeat = image->GetRepeat(); binding.classicUnit = R_MaterialResourceTable_SemanticSlot( semantic ); if ( binding.classicUnit >= rg_materialResourceTable.maxClassicTextureUnits ) { - R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_TOO_MANY_TEXTURES, MATERIAL_RESOURCE_FALLBACK_FLAG_TOO_MANY_TEXTURES ); + if ( R_MaterialResourceTable_IsPBRSemantic( semantic ) ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_TOO_MANY_TEXTURES ); + } else { + R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_TOO_MANY_TEXTURES, MATERIAL_RESOURCE_FALLBACK_FLAG_TOO_MANY_TEXTURES ); + } binding.classicUnit = -1; } binding.stageIndex = stageIndex; @@ -678,7 +938,8 @@ static void R_MaterialResourceTable_AddTextureBinding( binding.vertexColorMode = stage != NULL ? static_cast( stage->vertexColor ) : static_cast( SVC_IGNORE ); binding.privatePolygonOffset = stage != NULL ? stage->privatePolygonOffset : 0.0f; binding.blendEnabled = stage != NULL && R_MaterialResourceTable_BlendBits( stage->drawStateBits ) != 0; - binding.depthWrite = stage != NULL && ( stage->drawStateBits & GLS_DEPTHMASK ) != 0; + // GLS_DEPTHMASK is an inverse mask: set means depth writes are disabled. + binding.depthWrite = stage != NULL && ( stage->drawStateBits & GLS_DEPTHMASK ) == 0; binding.colorMasked = stage != NULL && ( stage->drawStateBits & GLS_COLORMASK ) != 0; for ( int i = 0; i < 4; ++i ) { binding.colorRegisters[i] = -1; @@ -711,7 +972,9 @@ static void R_MaterialResourceTable_AddTextureBinding( R_MaterialResourceTable_RecordDebugStringTruncation( "texture binding semantic" ); } - record.semanticBindingIndex[semantic] = bindingIndex; + if ( record.semanticBindingIndex[semantic] < 0 ) { + record.semanticBindingIndex[semantic] = bindingIndex; + } const unsigned int semanticBit = MaterialResourceTextureSemantic_Bit( semantic ); record.textureSemanticMask |= semanticBit; if ( binding.textureHandle != 0 ) { @@ -719,7 +982,9 @@ static void R_MaterialResourceTable_AddTextureBinding( if ( semantic == MATERIAL_RESOURCE_TEXTURE_DIFFUSE || semantic == MATERIAL_RESOURCE_TEXTURE_EMISSIVE || semantic == MATERIAL_RESOURCE_TEXTURE_GUI - || semantic == MATERIAL_RESOURCE_TEXTURE_POST_PROCESS ) { + || semantic == MATERIAL_RESOURCE_TEXTURE_POST_PROCESS + || semantic == MATERIAL_RESOURCE_TEXTURE_ALBEDO + || semantic == MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR ) { record.renderableColorTextureMask |= semanticBit; } } @@ -735,6 +1000,7 @@ static void R_MaterialResourceTable_AddTextureBinding( if ( binding.textureViewCandidate ) { rg_materialResourceTable.stats.textureViewDescriptors++; } + return bindingIndex; } static void R_MaterialResourceTable_BuildTextureArrayTable( void ) { @@ -748,6 +1014,16 @@ static void R_MaterialResourceTable_BuildTextureArrayTable( void ) { R_MaterialResourceTable_SeedTextureArrayFallbacks(); for ( int recordIndex = 0; recordIndex < rg_materialResourceTable.stats.records; ++recordIndex ) { materialResourceTableRecord_t &record = rg_materialResourceTable.records[recordIndex]; + // PBR records stay on the legacy/classic owner until a dedicated PBR + // shader path exists. Do not let their bindings consume the shared + // classic-modern texture table and perturb unrelated classic records. + if ( record.hasPBR ) { + for ( int bindingIndex = 0; bindingIndex < record.textureBindingCount; ++bindingIndex ) { + record.textures[bindingIndex].textureArrayCandidate = false; + record.textures[bindingIndex].textureArrayLayer = -1; + } + continue; + } for ( int bindingIndex = 0; bindingIndex < record.textureBindingCount; ++bindingIndex ) { materialResourceTextureBinding_t &binding = record.textures[bindingIndex]; if ( !binding.loaded || binding.textureHandle == 0 ) { @@ -887,6 +1163,499 @@ static void R_MaterialResourceTable_FinalizeRegisterRange( materialResourceTable } } +template +static void R_MaterialResourceTable_SetPassFailure( + materialResourceTableRecord_t &record, + materialResourceGuiPassFailure_t failure, + int sourceStageIndex ) { + if ( worldDomain ) { + record.firstWorldPass = -1; + record.worldPassCount = 0; + record.worldPassEligible = false; + record.worldPassFailure = failure; + record.worldPassFailureStage = sourceStageIndex; + } else { + record.firstGuiPass = -1; + record.guiPassCount = 0; + record.guiPassEligible = false; + record.guiPassFailure = failure; + record.guiPassFailureStage = sourceStageIndex; + } +} + +static int R_MaterialResourceTable_FindStageBindingIndex( + const materialResourceTableRecord_t &record, int sourceStageIndex ) { + for ( int i = 0; i < record.textureBindingCount; ++i ) { + if ( record.textures[i].stageIndex == sourceStageIndex ) { + return i; + } + } + return -1; +} + +static bool R_MaterialResourceTable_GuiRegisterValid( + const materialResourceTableRecord_t &record, int registerIndex ) { + return registerIndex >= 0 && registerIndex < record.registerCount; +} + +static bool R_MaterialResourceTable_MapSourceBlendFactor( + int drawStateBits, rendererBlendFactor_t &factor ) { + switch ( drawStateBits & GLS_SRCBLEND_BITS ) { + case GLS_SRCBLEND_ZERO: + factor = RENDERER_BLEND_ZERO; + return true; + case GLS_SRCBLEND_ONE: + factor = RENDERER_BLEND_ONE; + return true; + case GLS_SRCBLEND_DST_COLOR: + factor = RENDERER_BLEND_DST_COLOR; + return true; + case GLS_SRCBLEND_ONE_MINUS_DST_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_DST_COLOR; + return true; + case GLS_SRCBLEND_SRC_ALPHA: + factor = RENDERER_BLEND_SRC_ALPHA; + return true; + case GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + return true; + case GLS_SRCBLEND_DST_ALPHA: + factor = RENDERER_BLEND_DST_ALPHA; + return true; + case GLS_SRCBLEND_ONE_MINUS_DST_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_DST_ALPHA; + return true; + case GLS_SRCBLEND_ALPHA_SATURATE: + factor = RENDERER_BLEND_SRC_ALPHA_SATURATE; + return true; + case GLS_SRCBLEND_SRC_COLOR: + factor = RENDERER_BLEND_SRC_COLOR; + return true; + case GLS_SRCBLEND_ONE_MINUS_SRC_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_SRC_COLOR; + return true; + default: + return false; + } +} + +static bool R_MaterialResourceTable_MapDestinationBlendFactor( + int drawStateBits, rendererBlendFactor_t &factor ) { + switch ( drawStateBits & GLS_DSTBLEND_BITS ) { + case GLS_DSTBLEND_ZERO: + factor = RENDERER_BLEND_ZERO; + return true; + case GLS_DSTBLEND_ONE: + factor = RENDERER_BLEND_ONE; + return true; + case GLS_DSTBLEND_SRC_COLOR: + factor = RENDERER_BLEND_SRC_COLOR; + return true; + case GLS_DSTBLEND_ONE_MINUS_SRC_COLOR: + factor = RENDERER_BLEND_ONE_MINUS_SRC_COLOR; + return true; + case GLS_DSTBLEND_SRC_ALPHA: + factor = RENDERER_BLEND_SRC_ALPHA; + return true; + case GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + return true; + case GLS_DSTBLEND_DST_ALPHA: + factor = RENDERER_BLEND_DST_ALPHA; + return true; + case GLS_DSTBLEND_ONE_MINUS_DST_ALPHA: + factor = RENDERER_BLEND_ONE_MINUS_DST_ALPHA; + return true; + default: + return false; + } +} + +static bool R_MaterialResourceTable_MapGuiDepthState( + int drawStateBits, rendererDepthState_t &depth ) { + const int depthFunction = drawStateBits & ( GLS_DEPTHFUNC_ALWAYS | GLS_DEPTHFUNC_EQUAL ); + depth.testEnabled = false; // generated 2D views disable depth testing as a domain rule + depth.writeEnabled = ( drawStateBits & GLS_DEPTHMASK ) == 0; + switch ( depthFunction ) { + case GLS_DEPTHFUNC_LESS: + depth.compareOperation = RENDERER_COMPARE_LESS_OR_EQUAL; + return true; + case GLS_DEPTHFUNC_EQUAL: + depth.compareOperation = RENDERER_COMPARE_EQUAL; + return true; + case GLS_DEPTHFUNC_ALWAYS: + depth.compareOperation = RENDERER_COMPARE_ALWAYS; + return true; + default: + return false; + } +} + +static bool R_MaterialResourceTable_MapWorldDepthState( + int drawStateBits, rendererDepthState_t &depth ) { + const int depthFunction = drawStateBits + & ( GLS_DEPTHFUNC_ALWAYS | GLS_DEPTHFUNC_EQUAL ); + depth.testEnabled = true; + depth.writeEnabled = ( drawStateBits & GLS_DEPTHMASK ) == 0; + switch ( depthFunction ) { + case GLS_DEPTHFUNC_LESS: + depth.compareOperation = RENDERER_COMPARE_LESS_OR_EQUAL; + return true; + case GLS_DEPTHFUNC_EQUAL: + depth.compareOperation = RENDERER_COMPARE_EQUAL; + return true; + case GLS_DEPTHFUNC_ALWAYS: + depth.compareOperation = RENDERER_COMPARE_ALWAYS; + return true; + default: + return false; + } +} + +static bool R_MaterialResourceTable_MapCullMode( + int cullType, rendererCullMode_t &cull ) { + switch ( cullType ) { + case CT_FRONT_SIDED: + cull = RENDERER_CULL_FRONT; + return true; + case CT_BACK_SIDED: + cull = RENDERER_CULL_BACK; + return true; + case CT_TWO_SIDED: + cull = RENDERER_CULL_NONE; + return true; + default: + return false; + } +} + +static bool R_MaterialResourceTable_MapVertexColor( + stageVertexColor_t source, rendererVertexColorMode_t &destination ) { + switch ( source ) { + case SVC_IGNORE: + destination = RENDERER_VERTEX_COLOR_IGNORE; + return true; + case SVC_MODULATE: + destination = RENDERER_VERTEX_COLOR_MODULATE; + return true; + case SVC_INVERSE_MODULATE: + destination = RENDERER_VERTEX_COLOR_INVERSE_MODULATE; + return true; + default: + return false; + } +} + +static std::uint32_t R_MaterialResourceTable_ColorWriteMask( int drawStateBits ) { + std::uint32_t mask = RENDERER_COLOR_WRITE_RGBA; + if ( ( drawStateBits & GLS_REDMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_RED ); + } + if ( ( drawStateBits & GLS_GREENMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_GREEN ); + } + if ( ( drawStateBits & GLS_BLUEMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_BLUE ); + } + if ( ( drawStateBits & GLS_ALPHAMASK ) != 0 ) { + mask &= ~static_cast( RENDERER_COLOR_WRITE_ALPHA ); + } + return mask; +} + +template +static bool R_MaterialResourceTable_CompileOrderedPasses( + materialResourceTableRecord_t &record ) { + const bool domainReferenced = worldDomain + ? record.worldDomainReferenced : record.guiDomainReferenced; + R_MaterialResourceTable_SetPassFailure( + record, domainReferenced + ? MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_COMPILED + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED, -1 ); + if ( !domainReferenced ) { + return false; + } + const idMaterial *material = record.material; + if ( material == NULL ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL, -1 ); + return false; + } + if ( !material->IsDrawn() ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_DRAWN, -1 ); + return false; + } + if ( material->TestMaterialFlag( MF_NEED_CURRENT_RENDER ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_NEEDS_CURRENT_RENDER, -1 ); + return false; + } + // Decal-sorted surfaces are conditionally removed by r_skipDecals before + // the classic ambient walker. This first sealed contract deliberately + // carries no mutable debug-CVar disposition, so retain the whole view on + // the classic path for every sort value in the decal corridor. + if ( material->GetSort() >= SS_DECAL && material->GetSort() < SS_FAR ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_DECAL_SORT, -1 ); + return false; + } + // The classic walker splits post-process-sorted materials around scene + // capture and r_skipPostProcess handling. This first shared domain owns no + // part of that sequencing, so retain the complete view on the classic path. + if ( material->GetSort() >= SS_POST_PROCESS ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_POST_PROCESS_SORT, -1 ); + return false; + } + + rendererMaterialPassList_t compiled; + RendererContracts_ResetMaterialPassList( compiled ); + const int stageCount = material->GetNumStages(); + for ( int stageIndex = 0; stageIndex < stageCount; ++stageIndex ) { + const shaderStage_t *stage = material->GetStage( stageIndex ); + if ( stage == NULL || stage->lighting != SL_AMBIENT ) { + continue; + } + if ( stage->newStage != NULL ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_CUSTOM_PROGRAM, stageIndex ); + return false; + } + if ( stage->texture.cinematic != NULL ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_CINEMATIC_IMAGE, stageIndex ); + return false; + } + if ( stage->texture.dynamic != DI_STATIC ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_DYNAMIC_IMAGE, stageIndex ); + return false; + } + if ( stage->texture.texgen != TG_EXPLICIT ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_TEXGEN, stageIndex ); + return false; + } + // Classic fixed-function state carries a private stage offset into later + // stages when the material-level offset remains enabled. The sealed + // per-pass model restores each pass independently, so reject this combined + // authored state until the carry-forward transition is represented. + if ( record.hasMaterialPolygonOffset + && stage->privatePolygonOffset != 0.0f ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE, + stageIndex ); + return false; + } + // shaderStage_t::hasAlphaTest controls the classic depth/coverage pass; + // RB_STD_T_RenderShaderPasses does not apply it again to ambient color. + // The world domain proves the established depth packet prerequisite for + // every perforated/opaque surface, so its color pass must ignore this flag + // exactly like the classic walker. Generated 2D has no such prerequisite + // and remains ineligible. + if ( stage->hasAlphaTest && !worldDomain ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_ALPHA_TEST, + stageIndex ); + return false; + } + if ( stage->texture.image == NULL ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_IMAGE, stageIndex ); + return false; + } + if ( R_MaterialResourceTable_ImageIsPostProcess( stage->texture.image ) + || R_MaterialResourceTable_ImageIsSceneCapture( stage->texture.image ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_CURRENT_RENDER_IMAGE, stageIndex ); + return false; + } + if ( stage->texture.image->IsDefaulted() ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_DEFAULTED_IMAGE, stageIndex ); + return false; + } + if ( !stage->texture.image->IsLoaded() ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNLOADED_IMAGE, stageIndex ); + return false; + } + const int bindingIndex = R_MaterialResourceTable_FindStageBindingIndex( + record, stageIndex ); + if ( bindingIndex < 0 || bindingIndex >= record.textureBindingCount + || record.textures[bindingIndex].textureResourceId == 0 ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_TEXTURE_BINDING_OVERFLOW, stageIndex ); + return false; + } + + rendererMaterialPass_t pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = stageIndex; + pass.kind = worldDomain + ? RENDERER_MATERIAL_PASS_SURFACE : RENDERER_MATERIAL_PASS_GUI; + pass.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + pass.textureResourceId = record.textures[bindingIndex].textureResourceId; + if ( !R_MaterialResourceTable_GuiRegisterValid( record, stage->conditionRegister ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_REGISTER, stageIndex ); + return false; + } + pass.condition = RendererContracts_Register( stage->conditionRegister ); + for ( int i = 0; i < 4; ++i ) { + if ( !R_MaterialResourceTable_GuiRegisterValid( record, stage->color.registers[i] ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_REGISTER, stageIndex ); + return false; + } + pass.color[i] = RendererContracts_Register( stage->color.registers[i] ); + } + if ( stage->texture.hasMatrix ) { + for ( int row = 0; row < 2; ++row ) { + for ( int column = 0; column < 3; ++column ) { + const int matrixRegister = stage->texture.matrix[row][column]; + if ( !R_MaterialResourceTable_GuiRegisterValid( record, matrixRegister ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_REGISTER, stageIndex ); + return false; + } + pass.textureMatrix[row * 3 + column] = + RendererContracts_Register( matrixRegister ); + } + } + } + + const int supportedStateBits = GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS + | GLS_DEPTHMASK | GLS_COLORMASK | GLS_ALPHAMASK + | GLS_DEPTHFUNC_ALWAYS | GLS_DEPTHFUNC_EQUAL | GLS_ATEST_BITS; + if ( ( stage->drawStateBits & ~supportedStateBits ) != 0 + || !R_MaterialResourceTable_MapSourceBlendFactor( + stage->drawStateBits, pass.blend.sourceColor ) + || !R_MaterialResourceTable_MapDestinationBlendFactor( + stage->drawStateBits, pass.blend.destinationColor ) + || !( worldDomain + ? R_MaterialResourceTable_MapWorldDepthState( + stage->drawStateBits, pass.depth ) + : R_MaterialResourceTable_MapGuiDepthState( + stage->drawStateBits, pass.depth ) ) + || !R_MaterialResourceTable_MapCullMode( record.cullType, pass.cull ) + || !R_MaterialResourceTable_MapVertexColor( + stage->vertexColor, pass.vertexColor ) ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE, stageIndex ); + return false; + } + pass.blend.enabled = R_MaterialResourceTable_BlendBits( + stage->drawStateBits ) != 0; + pass.blend.destinationAlpha = pass.blend.destinationColor; + pass.blend.sourceAlpha = pass.blend.sourceColor; + pass.blend.colorOperation = RENDERER_BLEND_OP_ADD; + pass.blend.alphaOperation = RENDERER_BLEND_OP_ADD; + pass.colorWriteMask = R_MaterialResourceTable_ColorWriteMask( + stage->drawStateBits ); + + pass.alphaTestEnabled = false; + switch ( stage->drawStateBits & GLS_ATEST_BITS ) { + case 0: + break; + case GLS_ATEST_EQ_255: + pass.alphaTestEnabled = true; + pass.alphaTestCompareOperation = RENDERER_COMPARE_EQUAL; + pass.alphaTest = RendererContracts_Constant( 1.0f ); + break; + case GLS_ATEST_LT_128: + pass.alphaTestEnabled = true; + pass.alphaTestCompareOperation = RENDERER_COMPARE_LESS; + pass.alphaTest = RendererContracts_Constant( 0.5f ); + break; + case GLS_ATEST_GE_128: + pass.alphaTestEnabled = true; + pass.alphaTestCompareOperation = RENDERER_COMPARE_GREATER_OR_EQUAL; + pass.alphaTest = RendererContracts_Constant( 0.5f ); + break; + default: + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE, stageIndex ); + return false; + } + pass.texgen = RENDERER_TEXGEN_EXPLICIT; + const float offsetScale = stage->privatePolygonOffset != 0.0f + ? stage->privatePolygonOffset + : ( record.hasMaterialPolygonOffset ? record.polygonOffset : 0.0f ); + pass.polygonOffsetEnabled = offsetScale != 0.0f; + if ( pass.polygonOffsetEnabled ) { + pass.polygonOffsetFactor = RendererContracts_Constant( + r_offsetFactor.GetFloat() ); + pass.polygonOffsetUnits = RendererContracts_Constant( + r_offsetUnits.GetFloat() * offsetScale ); + } + pass.programFamily = worldDomain + ? RENDERER_PROGRAM_FIXED : RENDERER_PROGRAM_GUI; + pass.programKey = 0; + if ( !RendererContracts_AppendMaterialPass( compiled, pass ) ) { + R_MaterialResourceTable_SetPassFailure( + record, compiled.overflowed + ? MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_PASS, + stageIndex ); + return false; + } + } + + if ( compiled.count == 0 ) { + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_NO_AMBIENT_STAGES, -1 ); + return false; + } + const int poolCount = worldDomain ? rg_materialResourceTable.worldPassCount + : rg_materialResourceTable.guiPassCount; + const int poolCapacity = worldDomain ? MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES + : MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES; + if ( poolCount < 0 || poolCount > poolCapacity + || compiled.count > static_cast( + poolCapacity - poolCount ) ) { + if ( worldDomain ) { + rg_materialResourceTable.stats.worldPassPoolOverflows++; + } else { + rg_materialResourceTable.stats.guiPassPoolOverflows++; + } + R_MaterialResourceTable_SetPassFailure( + record, MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW, -1 ); + return false; + } + + if ( worldDomain ) { + record.firstWorldPass = rg_materialResourceTable.worldPassCount; + record.worldPassCount = static_cast( compiled.count ); + memcpy( &rg_materialResourceTable.worldPasses[record.firstWorldPass], + compiled.passes, sizeof( compiled.passes[0] ) * compiled.count ); + rg_materialResourceTable.worldPassCount += record.worldPassCount; + record.worldPassEligible = true; + record.worldPassFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE; + record.worldPassFailureStage = -1; + } else { + record.firstGuiPass = rg_materialResourceTable.guiPassCount; + record.guiPassCount = static_cast( compiled.count ); + memcpy( &rg_materialResourceTable.guiPasses[record.firstGuiPass], + compiled.passes, sizeof( compiled.passes[0] ) * compiled.count ); + rg_materialResourceTable.guiPassCount += record.guiPassCount; + record.guiPassEligible = true; + record.guiPassFailure = MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE; + record.guiPassFailureStage = -1; + } + return true; +} + +static bool R_MaterialResourceTable_CompileGuiPasses( + materialResourceTableRecord_t &record ) { + return R_MaterialResourceTable_CompileOrderedPasses( record ); +} + +static bool R_MaterialResourceTable_CompileWorldPasses( + materialResourceTableRecord_t &record ) { + return R_MaterialResourceTable_CompileOrderedPasses( record ); +} + static void R_MaterialResourceTable_ScanMaterialStages( materialResourceTableRecord_t &record, const materialResourceRecord_t &sourceRecord ) { const idMaterial *material = sourceRecord.material; if ( material == NULL ) { @@ -978,11 +1747,10 @@ static void R_MaterialResourceTable_ScanMaterialStages( materialResourceTableRec } const materialResourceTextureSemantic_t semantic = R_MaterialResourceTable_StageSemantic( *stage, record.materialClass, record.needsCurrentRender ); - if ( !R_MaterialResourceTable_HasSemanticBinding( record, semantic ) ) { - R_MaterialResourceTable_AddTextureBinding( record, semantic, stage->texture.image, stage, i ); - } + const int stageBindingIndex = R_MaterialResourceTable_AddTextureBinding( + record, semantic, stage->texture.image, stage, i ); if ( stage->hasAlphaTest && record.shadowAlphaBindingIndex < 0 ) { - record.shadowAlphaBindingIndex = R_MaterialResourceTable_FindTextureBindingIndex( record, semantic ); + record.shadowAlphaBindingIndex = stageBindingIndex; } } @@ -1023,6 +1791,192 @@ static void R_MaterialResourceTable_AddSourceImages( materialResourceTableRecord } } +static void R_MaterialResourceTable_CountPBR( const materialResourceTableRecord_t &record ) { + if ( !record.hasPBR ) { + rg_materialResourceTable.stats.classicRecords++; + return; + } + materialResourceTableStats_t &stats = rg_materialResourceTable.stats; + stats.pbrRecords++; + stats.pbrResourceReadyRecords += record.pbrResourceReady ? 1 : 0; + stats.pbrModernReadyRecords += record.pbrModernReady ? 1 : 0; + stats.pbrPackedMapRecords += record.pbrPackedMaterialData ? 1 : 0; + stats.pbrSeparateMapRecords += record.pbrSeparateMaterialData ? 1 : 0; + stats.pbrAuthoredClassicFallbackRecords += record.pbrHasAuthoredClassicFallback ? 1 : 0; + stats.pbrExplicitGeneratedFallbackRecords += record.pbrHasExplicitLegacyFallback && record.pbrUsesGeneratedLegacyFallback ? 1 : 0; + stats.pbrGeneratedFallbackRecords += record.pbrUsesGeneratedLegacyFallback ? 1 : 0; + stats.pbrApproximateFallbackRecords += record.pbrUsesApproximateLegacyFallback ? 1 : 0; + stats.pbrMissingLegacyFallbackRecords += record.pbrLegacyFallbackMissing ? 1 : 0; + stats.pbrMissingAlbedoMapRecords += record.hasPBRAlbedo ? 0 : 1; + stats.pbrMissingNormalMapRecords += record.hasPBRNormal ? 0 : 1; + stats.pbrMissingORMMapRecords += record.hasPBRORM ? 0 : 1; + if ( record.pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_NONE ) { + return; + } + stats.pbrFallbackRecords++; + switch ( record.pbrFallbackReason ) { + case MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED: + stats.pbrFallbackDisabled++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW: + stats.pbrFallbackUnsupportedWorkflow++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_MATERIAL_CLASS: + stats.pbrFallbackUnsupportedMaterialClass++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO: + stats.pbrFallbackMissingAlbedo++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_NORMAL_FORMAT: + stats.pbrFallbackMissingNormalFormat++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_CONFLICTING_LAYOUT: + stats.pbrFallbackConflictingLayout++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_TEXTURE_LAYOUT: + stats.pbrFallbackUnsupportedTextureLayout++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE: + stats.pbrFallbackMissingImage++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE: + stats.pbrFallbackClassicFeature++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_TOO_MANY_TEXTURES: + stats.pbrFallbackTooManyTextures++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_SHADER_PATH_UNAVAILABLE: + stats.pbrFallbackShaderPathUnavailable++; + break; + case MATERIAL_RESOURCE_PBR_FALLBACK_NONE: + default: + break; + } +} + +static void R_MaterialResourceTable_AddPBRSourceImages( materialResourceTableRecord_t &record, const materialResourceRecord_t &sourceRecord ) { + if ( !sourceRecord.hasPBR ) { + return; + } + record.hasPBR = true; + record.pbrWorkflow = sourceRecord.pbrWorkflow; + record.pbrNormalFormat = sourceRecord.pbrNormalFormat; + record.pbrHasAuthoredClassicFallback = sourceRecord.pbrHasAuthoredClassicFallback; + record.pbrHasExplicitLegacyFallback = sourceRecord.pbrHasExplicitLegacyFallback; + record.pbrUsesGeneratedLegacyFallback = sourceRecord.pbrUsesGeneratedLegacyFallback; + record.pbrUsesApproximateLegacyFallback = sourceRecord.pbrUsesApproximateLegacyFallback; + record.pbrLegacyFallbackMissing = sourceRecord.pbrLegacyFallbackMissing; + record.pbrMetallicRegister = sourceRecord.pbrMetallicRegister; + record.pbrRoughnessRegister = sourceRecord.pbrRoughnessRegister; + record.pbrAORegister = sourceRecord.pbrAORegister; + record.pbrNormalScaleRegister = sourceRecord.pbrNormalScaleRegister; + memcpy( record.pbrEmissiveColorRegisters, sourceRecord.pbrEmissiveColorRegisters, sizeof( record.pbrEmissiveColorRegisters ) ); + + if ( sourceRecord.pbrAlbedoImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_ALBEDO, sourceRecord.pbrAlbedoImage, NULL, -1 ); + } + if ( sourceRecord.pbrNormalImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_NORMAL, sourceRecord.pbrNormalImage, NULL, -1 ); + } + if ( sourceRecord.pbrORMImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_ORM, sourceRecord.pbrORMImage, NULL, -1 ); + } + if ( sourceRecord.pbrMetallicImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_METALLIC, sourceRecord.pbrMetallicImage, NULL, -1 ); + } + if ( sourceRecord.pbrRoughnessImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_ROUGHNESS, sourceRecord.pbrRoughnessImage, NULL, -1 ); + } + if ( sourceRecord.pbrAOImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_AO, sourceRecord.pbrAOImage, NULL, -1 ); + } + if ( sourceRecord.pbrEmissiveImage != NULL ) { + R_MaterialResourceTable_AddTextureBinding( record, MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR, sourceRecord.pbrEmissiveImage, NULL, -1 ); + } + record.pbrPackedMaterialData = sourceRecord.pbrORMImage != NULL; + record.pbrSeparateMaterialData = sourceRecord.pbrMetallicImage != NULL + || sourceRecord.pbrRoughnessImage != NULL + || sourceRecord.pbrAOImage != NULL; +} + +static bool R_MaterialResourceTable_PBRBindingReady( const materialResourceTableRecord_t &record, materialResourceTextureSemantic_t semantic, bool required ) { + const int index = R_MaterialResourceTable_FindTextureBindingIndex( record, semantic ); + if ( index < 0 ) { + return !required; + } + const materialResourceTextureBinding_t &binding = record.textures[index]; + return binding.image != NULL + && binding.loaded + && !binding.defaulted + && !R_IsMutableRenderImage( binding.image ); +} + +static void R_MaterialResourceTable_FinalizePBRContract( materialResourceTableRecord_t &record ) { + if ( !record.hasPBR ) { + return; + } + + if ( !record.hasPBRAlbedo ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO ); + } + if ( record.hasPBRNormal && record.pbrNormalFormat == PBR_NORMAL_UNSPECIFIED ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_NORMAL_FORMAT ); + } + if ( record.pbrPackedMaterialData && record.pbrSeparateMaterialData ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_CONFLICTING_LAYOUT ); + } + if ( record.pbrWorkflow != PBR_WORKFLOW_METALLIC_ROUGHNESS ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW ); + } + // Translucent PBR materials stay in the ordered forward path. Their blend + // contract is checked again by the executor before a draw is promoted, so + // uncommon multi-stage or unsupported blend expressions still fall back to + // the authored classic stages. + if ( record.materialClass != RENDER_MATERIAL_OPAQUE + && record.materialClass != RENDER_MATERIAL_PERFORATED + && record.materialClass != RENDER_MATERIAL_TRANSLUCENT ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_MATERIAL_CLASS ); + } + const bool sourceAlphaIntent = record.materialClass == RENDER_MATERIAL_TRANSLUCENT + || record.blendMode == MATERIAL_RESOURCE_BLEND_BLEND + || record.blendStageCount > 0; + if ( sourceAlphaIntent && !R_MaterialResourceTable_HasPBRSourceAlphaBlendContract( record ) ) { + // A transparent PBR draw has no correct partial promotion: if its sole + // alpha stage cannot be proven equivalent to direct albedo sampling, keep + // the complete material on its classic fallback owner. + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE ); + } + if ( !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_ALBEDO, true ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_NORMAL, record.hasPBRNormal ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_ORM, record.hasPBRORM ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_METALLIC, record.hasPBRMetallic ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_ROUGHNESS, record.hasPBRRoughness ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_AO, record.hasPBRAO ) + || !R_MaterialResourceTable_PBRBindingReady( record, MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR, record.hasPBREmissive ) ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE ); + } + if ( record.fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE ); + } + + // Resource readiness describes parsed, resident PBR inputs independently of + // whether this frame is allowed to give them visible ownership. + record.pbrResourceReady = record.pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_NONE; + if ( !record.pbrResourceReady ) { + record.pbrModernReady = false; + return; + } + if ( !r_rendererModernQuality.GetBool() || !r_pbrMaterials.GetBool() ) { + R_MaterialResourceTable_AddPBRFallback( record, MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED ); + record.pbrModernReady = false; + return; + } + // The direct corridor reserves units 0..6 for albedo, normal, packed ORM or + // separate metallic/roughness/AO data, and emissive. The shadow set begins + // above the material texture-array range, so this remains valid on GL 3.3. + record.pbrModernReady = true; +} + static void R_MaterialResourceTable_FinalizeShadowContract( materialResourceTableRecord_t &record ) { record.shadowFallbackFlags |= record.fallbackFlags; record.shadowCasterSupported = record.castsShadow && record.fallbackReason == MATERIAL_RESOURCE_FALLBACK_NONE; @@ -1040,7 +1994,10 @@ static void R_MaterialResourceTable_FinalizeShadowContract( materialResourceTabl } } -static bool R_MaterialResourceTable_AddRecordFromSource( const materialResourceRecord_t &sourceRecord, int sourceIndex, bool scanMaterialStages ) { +static bool R_MaterialResourceTable_AddRecordFromSource( + const materialResourceRecord_t &sourceRecord, int sourceIndex, + bool scanMaterialStages, bool guiDomainReferenced = false, + bool worldDomainReferenced = false ) { if ( rg_materialResourceTable.stats.records >= MATERIAL_RESOURCE_TABLE_MAX_RECORDS ) { rg_materialResourceTable.stats.overflow = true; R_MaterialResourceTable_SetStatus( "material table overflow" ); @@ -1053,8 +2010,12 @@ static bool R_MaterialResourceTable_AddRecordFromSource( const materialResourceR record.semanticBindingIndex[i] = -1; } record.tableIndex = rg_materialResourceTable.stats.records; + record.tableGeneration = rg_materialResourceTable.tableGeneration; record.sourceMaterialRecordIndex = sourceIndex; - record.materialId = sourceRecord.material != NULL ? sourceRecord.material->Index() : -1; + // The scene-packet record owns the stable decl index. Synthetic validation + // decls allocated through declManager intentionally have no idDeclBase and + // therefore must never be asked for Index() directly. + record.materialId = sourceRecord.material != NULL ? sourceRecord.resourceTableIndex : -1; record.material = sourceRecord.material; if ( !R_MaterialResourceTable_CopyDebugString( record.materialName, sizeof( record.materialName ), sourceRecord.material != NULL ? sourceRecord.material->GetName() : "" ) ) { R_MaterialResourceTable_RecordDebugStringTruncation( "material record name" ); @@ -1080,6 +2041,18 @@ static bool R_MaterialResourceTable_AddRecordFromSource( const materialResourceR record.hasMaterialPolygonOffset = sourceRecord.material != NULL && sourceRecord.material->TestMaterialFlag( MF_POLYGONOFFSET ); record.polygonOffset = sourceRecord.material != NULL ? sourceRecord.material->GetPolygonOffset() : 0.0f; record.shadowAlphaBindingIndex = -1; + record.firstGuiPass = -1; + record.guiDomainReferenced = guiDomainReferenced; + record.guiPassFailure = guiDomainReferenced + ? MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_COMPILED + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED; + record.guiPassFailureStage = -1; + record.firstWorldPass = -1; + record.worldDomainReferenced = worldDomainReferenced; + record.worldPassFailure = worldDomainReferenced + ? MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_COMPILED + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED; + record.worldPassFailureStage = -1; record.registerStart = 0; record.registerCount = sourceRecord.material != NULL ? sourceRecord.material->GetNumRegisters() : 0; record.fallbackReason = MATERIAL_RESOURCE_FALLBACK_NONE; @@ -1091,12 +2064,19 @@ static bool R_MaterialResourceTable_AddRecordFromSource( const materialResourceR if ( scanMaterialStages ) { R_MaterialResourceTable_ScanMaterialStages( record, sourceRecord ); R_MaterialResourceTable_AddSourceImages( record, sourceRecord ); + if ( record.guiDomainReferenced ) { + R_MaterialResourceTable_CompileGuiPasses( record ); + } + if ( record.worldDomainReferenced ) { + R_MaterialResourceTable_CompileWorldPasses( record ); + } } else if ( sourceRecord.material == NULL ) { R_MaterialResourceTable_AddSourceImages( record, sourceRecord ); R_MaterialResourceTable_AddFallback( record, MATERIAL_RESOURCE_FALLBACK_MISSING_MATERIAL, MATERIAL_RESOURCE_FALLBACK_FLAG_MISSING_MATERIAL ); } else { R_MaterialResourceTable_AddSourceImages( record, sourceRecord ); } + R_MaterialResourceTable_AddPBRSourceImages( record, sourceRecord ); if ( R_MaterialResourceTable_RecordNeedsSurfaceImage( record ) && !record.hasDiffuse && !record.hasEmissive @@ -1108,14 +2088,34 @@ static bool R_MaterialResourceTable_AddRecordFromSource( const materialResourceR } R_MaterialResourceTable_ValidateStageColorContract( record ); R_MaterialResourceTable_ValidateAmbientOverlayContract( record ); + R_MaterialResourceTable_FinalizePBRContract( record ); if ( !scanMaterialStages ) { record.shadowCasterSupported = record.castsShadow && record.fallbackReason == MATERIAL_RESOURCE_FALLBACK_NONE; record.shadowFallbackFlags = record.fallbackFlags; } R_MaterialResourceTable_FinalizeShadowContract( record ); R_MaterialResourceTable_FinalizeRegisterRange( record ); + if ( record.guiDomainReferenced ) { + rg_materialResourceTable.stats.guiDomainReferencedRecords++; + if ( record.guiPassEligible ) { + rg_materialResourceTable.stats.guiPassEligibleRecords++; + rg_materialResourceTable.stats.guiPasses += record.guiPassCount; + } else { + rg_materialResourceTable.stats.guiPassFallbackRecords++; + } + } + if ( record.worldDomainReferenced ) { + rg_materialResourceTable.stats.worldDomainReferencedRecords++; + if ( record.worldPassEligible ) { + rg_materialResourceTable.stats.worldPassEligibleRecords++; + rg_materialResourceTable.stats.worldPasses += record.worldPassCount; + } else { + rg_materialResourceTable.stats.worldPassFallbackRecords++; + } + } R_MaterialResourceTable_CountClass( record ); R_MaterialResourceTable_CountFallbacks( record ); + R_MaterialResourceTable_CountPBR( record ); R_MaterialResourceTable_HashInsert( record.material, rg_materialResourceTable.stats.records ); rg_materialResourceTable.stats.records++; @@ -1134,6 +2134,13 @@ static void R_MaterialResourceTable_ResetFrameStats( void ) { memset( rg_materialResourceTable.textureArrayTable, 0, sizeof( rg_materialResourceTable.textureArrayTable ) ); memset( rg_materialResourceTable.materialHash, 0, sizeof( rg_materialResourceTable.materialHash ) ); rg_materialResourceTable.textureArrayTableCount = 0; + rg_materialResourceTable.guiPassCount = 0; + rg_materialResourceTable.worldPassCount = 0; + rg_materialResourceTable.tableGeneration++; + if ( rg_materialResourceTable.tableGeneration == 0 ) { + // Resource id zero and generation zero are both permanently invalid. + rg_materialResourceTable.tableGeneration = 1; + } memset( &rg_materialResourceTable.stats, 0, sizeof( rg_materialResourceTable.stats ) ); rg_materialResourceTable.stats.initialized = initialized; rg_materialResourceTable.stats.available = available; @@ -1146,6 +2153,7 @@ static void R_MaterialResourceTable_ResetFrameStats( void ) { void R_MaterialResourceTable_Init( const renderBackendCaps_t &caps, const renderFeatureSet_t &features ) { memset( &rg_materialResourceTable, 0, sizeof( rg_materialResourceTable ) ); + rg_materialResourceTable.tableGeneration = 1; rg_materialResourceTable.caps = caps; rg_materialResourceTable.features = features; rg_materialResourceTable.maxClassicTextureUnits = caps.maxTextureImageUnits > 0 ? Min( caps.maxTextureImageUnits, MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS ) : MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS; @@ -1170,17 +2178,35 @@ void R_MaterialResourceTable_PrepareFrame( const idScenePacketFrame &packetFrame rg_materialResourceTable.stats.prepared = true; const int materialRecordCount = packetFrame.NumMaterialRecords(); const int drawPacketCount = packetFrame.NumDrawPackets(); + bool guiDomainReferenced[SCENE_PACKET_MAX_MATERIAL_RECORDS]; + bool worldDomainReferenced[SCENE_PACKET_MAX_MATERIAL_RECORDS]; + memset( guiDomainReferenced, 0, sizeof( guiDomainReferenced ) ); + memset( worldDomainReferenced, 0, sizeof( worldDomainReferenced ) ); rg_materialResourceTable.stats.sourceMaterialRecords = materialRecordCount; for ( int i = 0; i < drawPacketCount; ++i ) { - if ( packetFrame.DrawPacket( i ).materialRecordIndex >= 0 ) { + const drawPacket_t &drawPacket = packetFrame.DrawPacket( i ); + if ( drawPacket.materialRecordIndex >= 0 ) { rg_materialResourceTable.stats.drawPacketReferences++; + if ( drawPacket.passCategory == RENDER_PASS_GUI + && drawPacket.packetCategory == SCENE_PACKET_CATEGORY_GUI + && drawPacket.materialRecordIndex < materialRecordCount ) { + guiDomainReferenced[drawPacket.materialRecordIndex] = true; + } + if ( ( drawPacket.passCategory == RENDER_PASS_AMBIENT + || drawPacket.passCategory == RENDER_PASS_GUI ) + && drawPacket.packetCategory == SCENE_PACKET_CATEGORY_WORLD + && drawPacket.materialRecordIndex < materialRecordCount ) { + worldDomainReferenced[drawPacket.materialRecordIndex] = true; + } } } if ( !rg_materialResourceTable.stats.available ) { return; } for ( int i = 0; i < materialRecordCount; ++i ) { - R_MaterialResourceTable_AddRecordFromSource( packetFrame.MaterialRecord( i ), i, true ); + R_MaterialResourceTable_AddRecordFromSource( + packetFrame.MaterialRecord( i ), i, true, guiDomainReferenced[i], + worldDomainReferenced[i] ); } R_MaterialResourceTable_BuildTextureArrayTable(); if ( rg_materialResourceTable.stats.overflow ) { @@ -1206,6 +2232,123 @@ const materialResourceTextureBinding_t *R_MaterialResourceTable_TextureBindingFo return bindingIndex >= 0 ? &record.textures[bindingIndex] : NULL; } +bool R_MaterialResourceTable_GuiPassEligible( + const materialResourceTableRecord_t &record ) { + return record.guiDomainReferenced + && record.guiPassEligible + && record.guiPassFailure == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + && record.tableGeneration == rg_materialResourceTable.tableGeneration + && record.tableIndex >= 0 + && record.tableIndex < rg_materialResourceTable.stats.records + && &rg_materialResourceTable.records[record.tableIndex] == &record + && record.firstGuiPass >= 0 + && record.guiPassCount > 0 + && record.guiPassCount + <= static_cast( RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) + && record.firstGuiPass <= rg_materialResourceTable.guiPassCount + && record.guiPassCount <= rg_materialResourceTable.guiPassCount - record.firstGuiPass; +} + +const rendererMaterialPass_t *R_MaterialResourceTable_GuiPasses( + const materialResourceTableRecord_t &record, int &count ) { + count = 0; + if ( !R_MaterialResourceTable_GuiPassEligible( record ) ) { + return NULL; + } + count = record.guiPassCount; + return &rg_materialResourceTable.guiPasses[record.firstGuiPass]; +} + +bool R_MaterialResourceTable_CopyGuiPassList( + const materialResourceTableRecord_t &record, + rendererMaterialPassList_t &destination ) { + RendererContracts_ResetMaterialPassList( destination ); + int count = 0; + const rendererMaterialPass_t *passes = R_MaterialResourceTable_GuiPasses( + record, count ); + if ( passes == NULL || count <= 0 + || count > static_cast( RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) ) { + return false; + } + memcpy( destination.passes, passes, sizeof( destination.passes[0] ) * count ); + destination.count = static_cast( count ); + return true; +} + +bool R_MaterialResourceTable_WorldPassEligible( + const materialResourceTableRecord_t &record ) { + return record.worldDomainReferenced + && record.worldPassEligible + && record.worldPassFailure == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + && record.tableGeneration == rg_materialResourceTable.tableGeneration + && record.tableIndex >= 0 + && record.tableIndex < rg_materialResourceTable.stats.records + && &rg_materialResourceTable.records[record.tableIndex] == &record + && record.firstWorldPass >= 0 + && record.worldPassCount > 0 + && record.worldPassCount + <= static_cast( RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) + && record.firstWorldPass <= rg_materialResourceTable.worldPassCount + && record.worldPassCount + <= rg_materialResourceTable.worldPassCount - record.firstWorldPass; +} + +const rendererMaterialPass_t *R_MaterialResourceTable_WorldPasses( + const materialResourceTableRecord_t &record, int &count ) { + count = 0; + if ( !R_MaterialResourceTable_WorldPassEligible( record ) ) { + return NULL; + } + count = record.worldPassCount; + return &rg_materialResourceTable.worldPasses[record.firstWorldPass]; +} + +bool R_MaterialResourceTable_CopyWorldPassList( + const materialResourceTableRecord_t &record, + rendererMaterialPassList_t &destination ) { + RendererContracts_ResetMaterialPassList( destination ); + int count = 0; + const rendererMaterialPass_t *passes = R_MaterialResourceTable_WorldPasses( + record, count ); + if ( passes == NULL || count <= 0 + || count > static_cast( RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) ) { + return false; + } + memcpy( destination.passes, passes, + sizeof( destination.passes[0] ) * count ); + destination.count = static_cast( count ); + return true; +} + +const materialResourceTextureBinding_t *R_MaterialResourceTable_ResolveTextureResource( + std::uint64_t textureResourceId ) { + if ( textureResourceId == 0 ) { + return NULL; + } + const unsigned int generation = static_cast( textureResourceId >> 32 ); + const unsigned int encodedRecord = static_cast( + ( textureResourceId >> 16 ) & 0xffffu ); + const unsigned int encodedBinding = static_cast( + textureResourceId & 0xffffu ); + if ( generation == 0 || generation != rg_materialResourceTable.tableGeneration + || encodedRecord == 0 || encodedBinding == 0 ) { + return NULL; + } + const int recordIndex = static_cast( encodedRecord - 1 ); + const int bindingIndex = static_cast( encodedBinding - 1 ); + if ( recordIndex < 0 || recordIndex >= rg_materialResourceTable.stats.records ) { + return NULL; + } + const materialResourceTableRecord_t &record = + rg_materialResourceTable.records[recordIndex]; + if ( record.tableGeneration != generation || record.tableIndex != recordIndex + || bindingIndex < 0 || bindingIndex >= record.textureBindingCount ) { + return NULL; + } + const materialResourceTextureBinding_t &binding = record.textures[bindingIndex]; + return binding.textureResourceId == textureResourceId ? &binding : NULL; +} + const materialResourceTableRecord_t *R_MaterialResourceTable_FindRecordForMaterial( const idMaterial *material ) { if ( material == NULL ) { return NULL; @@ -1237,11 +2380,12 @@ int R_MaterialResourceTable_TextureArrayTableIndexForHandle( unsigned int textur void R_MaterialResourceTable_PrintGfxInfo( void ) { const materialResourceTableStats_t &stats = R_MaterialResourceTable_Stats(); common->Printf( - "Material resource table: initialized=%d available=%d prepared=%d records=%d source=%d draws=%d textures=%d classic=%d arrays=%d table=%d/%d desc=%d overflow=%d views=%d bindless=%d/%d fallback=%d missing=%d unsupported=%d custom=%d/%d dynamic=%d current=%d texgen=%d(screen=%d sky=%d) condition=%d stageColor=%d matrix=%d vertexColor=%d offset=%d debugTrunc=%d source='%s' status='%s'\n", + "Material resource table: initialized=%d available=%d prepared=%d records=%d classicRecords=%d source=%d draws=%d textures=%d classicTextures=%d arrays=%d table=%d/%d desc=%d overflow=%d views=%d bindless=%d/%d fallback=%d missing=%d unsupported=%d custom=%d/%d dynamic=%d current=%d texgen=%d(screen=%d sky=%d) condition=%d stageColor=%d matrix=%d vertexColor=%d offset=%d debugTrunc=%d source='%s' status='%s'\n", stats.initialized ? 1 : 0, stats.available ? 1 : 0, stats.prepared ? 1 : 0, stats.records, + stats.classicRecords, stats.sourceMaterialRecords, stats.drawPacketReferences, stats.textureBindings, @@ -1272,15 +2416,75 @@ void R_MaterialResourceTable_PrintGfxInfo( void ) { stats.debugStringTruncations, stats.debugStringTruncationSource, stats.lastFailure ); + common->Printf( + "PBR material resources: records=%d resourceReady=%d modernReady=%d packed=%d separate=%d fallback=%d disabled=%d workflow=%d class=%d albedo=%d normalFormat=%d conflict=%d layout=%d image=%d classic=%d units=%d shader=%d authored=%d explicitGenerated=%d generated=%d approximate=%d missingFallback=%d mapsMissing=%d/%d/%d\n", + stats.pbrRecords, + stats.pbrResourceReadyRecords, + stats.pbrModernReadyRecords, + stats.pbrPackedMapRecords, + stats.pbrSeparateMapRecords, + stats.pbrFallbackRecords, + stats.pbrFallbackDisabled, + stats.pbrFallbackUnsupportedWorkflow, + stats.pbrFallbackUnsupportedMaterialClass, + stats.pbrFallbackMissingAlbedo, + stats.pbrFallbackMissingNormalFormat, + stats.pbrFallbackConflictingLayout, + stats.pbrFallbackUnsupportedTextureLayout, + stats.pbrFallbackMissingImage, + stats.pbrFallbackClassicFeature, + stats.pbrFallbackTooManyTextures, + stats.pbrFallbackShaderPathUnavailable, + stats.pbrAuthoredClassicFallbackRecords, + stats.pbrExplicitGeneratedFallbackRecords, + stats.pbrGeneratedFallbackRecords, + stats.pbrApproximateFallbackRecords, + stats.pbrMissingLegacyFallbackRecords, + stats.pbrMissingAlbedoMapRecords, + stats.pbrMissingNormalMapRecords, + stats.pbrMissingORMMapRecords ); + common->Printf( + "GUI material passes: referenced=%d eligible=%d fallback=%d passes=%d/%d poolOverflow=%d\n", + stats.guiDomainReferencedRecords, + stats.guiPassEligibleRecords, + stats.guiPassFallbackRecords, + stats.guiPasses, + MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES, + stats.guiPassPoolOverflows ); + common->Printf( + "World ambient material passes: referenced=%d eligible=%d fallback=%d passes=%d/%d poolOverflow=%d\n", + stats.worldDomainReferencedRecords, + stats.worldPassEligibleRecords, + stats.worldPassFallbackRecords, + stats.worldPasses, + MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES, + stats.worldPassPoolOverflows ); +} + +bool R_MaterialResourceTable_ClassicModernPathEligible( const materialResourceTableRecord_t &record ) { + return !record.hasPBR; +} + +bool R_MaterialResourceTable_PBRModernPathEligible( const materialResourceTableRecord_t &record ) { + return r_rendererModernQuality.GetBool() + && record.hasPBR + && record.pbrModernReady + && record.pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_NONE; +} + +bool R_MaterialResourceTable_PBRTransparentPathEligible( const materialResourceTableRecord_t &record ) { + return R_MaterialResourceTable_PBRModernPathEligible( record ) + && R_MaterialResourceTable_HasPBRSourceAlphaBlendContract( record ); } void R_MaterialResourceTable_DumpLatest( void ) { const materialResourceTableStats_t &stats = R_MaterialResourceTable_Stats(); common->Printf( - "MaterialResourceTable dump: prepared=%d available=%d records=%d source=%d draws=%d textures=%d table=%d/%d desc=%d overflow=%d fallback=%d missing=%d defaulted=%d unsupported=%d debugTrunc=%d source='%s' status='%s'\n", + "MaterialResourceTable dump: prepared=%d available=%d records=%d classicRecords=%d source=%d draws=%d textures=%d table=%d/%d desc=%d overflow=%d fallback=%d missing=%d defaulted=%d unsupported=%d debugTrunc=%d source='%s' status='%s'\n", stats.prepared ? 1 : 0, stats.available ? 1 : 0, stats.records, + stats.classicRecords, stats.sourceMaterialRecords, stats.drawPacketReferences, stats.textureBindings, @@ -1295,10 +2499,39 @@ void R_MaterialResourceTable_DumpLatest( void ) { stats.debugStringTruncations, stats.debugStringTruncationSource, stats.lastFailure ); + common->Printf( + "PBR summary: records=%d resourceReady=%d modernReady=%d packed=%d separate=%d fallback=%d approximate=%d missingFallback=%d mapsMissing=%d/%d/%d\n", + stats.pbrRecords, + stats.pbrResourceReadyRecords, + stats.pbrModernReadyRecords, + stats.pbrPackedMapRecords, + stats.pbrSeparateMapRecords, + stats.pbrFallbackRecords, + stats.pbrApproximateFallbackRecords, + stats.pbrMissingLegacyFallbackRecords, + stats.pbrMissingAlbedoMapRecords, + stats.pbrMissingNormalMapRecords, + stats.pbrMissingORMMapRecords ); + common->Printf( + "GUI pass summary: referenced=%d eligible=%d fallback=%d passes=%d/%d poolOverflow=%d\n", + stats.guiDomainReferencedRecords, + stats.guiPassEligibleRecords, + stats.guiPassFallbackRecords, + stats.guiPasses, + MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES, + stats.guiPassPoolOverflows ); + common->Printf( + "World ambient pass summary: referenced=%d eligible=%d fallback=%d passes=%d/%d poolOverflow=%d\n", + stats.worldDomainReferencedRecords, + stats.worldPassEligibleRecords, + stats.worldPassFallbackRecords, + stats.worldPasses, + MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES, + stats.worldPassPoolOverflows ); for ( int i = 0; i < stats.records; ++i ) { const materialResourceTableRecord_t &record = rg_materialResourceTable.records[i]; common->Printf( - " material[%d] source=%d id=%d name='%s' class=%s blend=%s sort=%s/%.2f cull=%d regs=%d+%d stageRegs=%d+%d stages=%d/%d alpha=%d textures=%d fallback=%s flags=0x%x current=%d capture=%d gui=%d subview=%d light=%d shadow=%d/%d matrix=%d vertexColor=%d dynamic=%d screenTexgen=%d skyTexgen=%d custom=%d/%d offset=%.2f twoSided=%d backsides=%d shadowFlags=0x%x\n", + " material[%d] source=%d id=%d name='%s' class=%s blend=%s sort=%s/%.2f cull=%d regs=%d+%d stageRegs=%d+%d stages=%d/%d blendStages=%d additiveStages=%d filterStages=%d alpha=%d textures=%d fallback=%s flags=0x%x current=%d capture=%d gui=%d subview=%d light=%d shadow=%d/%d matrix=%d vertexColor=%d dynamic=%d screenTexgen=%d skyTexgen=%d custom=%d/%d offset=%.2f twoSided=%d backsides=%d shadowFlags=0x%x\n", record.tableIndex, record.sourceMaterialRecordIndex, record.materialId, @@ -1314,6 +2547,9 @@ void R_MaterialResourceTable_DumpLatest( void ) { record.stageRegisterCount, record.evaluatedStageCount, record.stageCount, + record.blendStageCount, + record.additiveStageCount, + record.filterStageCount, record.alphaTest ? 1 : 0, record.textureBindingCount, MaterialResourceFallbackReason_Name( record.fallbackReason ), @@ -1334,8 +2570,58 @@ void R_MaterialResourceTable_DumpLatest( void ) { record.hasCustomGLSL ? 1 : 0, record.polygonOffset, record.twoSided ? 1 : 0, - record.shouldCreateBackSides ? 1 : 0, - record.shadowFallbackFlags ); + record.shouldCreateBackSides ? 1 : 0, + record.shadowFallbackFlags ); + if ( record.guiDomainReferenced ) { + common->Printf( + " guiPass eligible=%d span=%d+%d failure=%s stage=%d generation=%u\n", + record.guiPassEligible ? 1 : 0, + record.firstGuiPass, + record.guiPassCount, + MaterialResourceGuiPassFailure_Name( record.guiPassFailure ), + record.guiPassFailureStage, + record.tableGeneration ); + } + if ( record.worldDomainReferenced ) { + common->Printf( + " worldPass eligible=%d span=%d+%d failure=%s stage=%d generation=%u\n", + record.worldPassEligible ? 1 : 0, + record.firstWorldPass, + record.worldPassCount, + MaterialResourceWorldPassFailure_Name( record.worldPassFailure ), + record.worldPassFailureStage, + record.tableGeneration ); + } + if ( record.hasPBR ) { + common->Printf( + " pbr workflow=%d normalFormat=%d resourceReady=%d modernReady=%d fallback=%s packed=%d separate=%d maps=a%d n%d orm%d m%d r%d ao%d e%d authored=%d explicit=%d generated=%d approximate=%d missingFallback=%d regs=%d/%d/%d/%d emit=%d,%d,%d\n", + record.pbrWorkflow, + record.pbrNormalFormat, + record.pbrResourceReady ? 1 : 0, + record.pbrModernReady ? 1 : 0, + MaterialResourcePBRFallbackReason_Name( record.pbrFallbackReason ), + record.pbrPackedMaterialData ? 1 : 0, + record.pbrSeparateMaterialData ? 1 : 0, + record.hasPBRAlbedo ? 1 : 0, + record.hasPBRNormal ? 1 : 0, + record.hasPBRORM ? 1 : 0, + record.hasPBRMetallic ? 1 : 0, + record.hasPBRRoughness ? 1 : 0, + record.hasPBRAO ? 1 : 0, + record.hasPBREmissive ? 1 : 0, + record.pbrHasAuthoredClassicFallback ? 1 : 0, + record.pbrHasExplicitLegacyFallback ? 1 : 0, + record.pbrUsesGeneratedLegacyFallback ? 1 : 0, + record.pbrUsesApproximateLegacyFallback ? 1 : 0, + record.pbrLegacyFallbackMissing ? 1 : 0, + record.pbrMetallicRegister, + record.pbrRoughnessRegister, + record.pbrAORegister, + record.pbrNormalScaleRegister, + record.pbrEmissiveColorRegisters[0], + record.pbrEmissiveColorRegisters[1], + record.pbrEmissiveColorRegisters[2] ); + } for ( int bindingIndex = 0; bindingIndex < record.textureBindingCount; ++bindingIndex ) { const materialResourceTextureBinding_t &binding = record.textures[bindingIndex]; common->Printf( @@ -1368,6 +2654,815 @@ void R_MaterialResourceTable_DumpLatest( void ) { } } +static bool R_MaterialResourceTable_RunGuiPassContractSelfTest( void ) { + if ( globalImages == NULL ) { + common->Printf( "RendererMaterialResourceTable GUI pass self-test skipped: images unavailable\n" ); + return true; + } + static const char declaration[] = + "material _gui_resource_table_selftest {\n" + " twoSided\n" + " polygonOffset 2\n" + " {\n" + " if parm0\n" + " blend gl_src_alpha, gl_one_minus_src_alpha\n" + " map _white\n" + " color parm0, parm1, parm2, parm3\n" + " translate parm4, parm5\n" + " scale parm6, parm7\n" + " vertexColor\n" + " maskRed\n" + " maskAlpha\n" + " maskDepth\n" + " }\n" + " {\n" + " blend gl_dst_alpha, gl_one\n" + " map _black\n" + " inverseVertexColor\n" + " maskGreen\n" + " }\n" + "}\n"; + + idDecl *materialDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( materialDecl == NULL ) { + common->Printf( "RendererMaterialResourceTable self-test failed: GUI declaration allocation\n" ); + return false; + } + idMaterial *material = static_cast( materialDecl ); + if ( !material->Parse( declaration, + idLib::SizeToInt( sizeof( declaration ) - 1, + "GUI resource-table self-test" ) ) ) { + common->Printf( "RendererMaterialResourceTable self-test failed: GUI declaration parse\n" ); + DeclManager_FreeAllocatedDecl( materialDecl ); + return false; + } + // The parser assigns decal sort to material-level polygon offset by default. + // This fixture isolates the supported offset state from the separately tested + // decal-sort exclusion, as a 2D draw can reference an otherwise opaque + // material regardless of where that material was first classified. + const float parsedSort = material->GetSort(); + material->SetSort( SS_OPAQUE ); + + const int savedMaxClassicTextureUnits = rg_materialResourceTable.maxClassicTextureUnits; + rg_materialResourceTable.maxClassicTextureUnits = Max( + savedMaxClassicTextureUnits, MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS ); + materialResourceRecord_t source; + memset( &source, 0, sizeof( source ) ); + source.material = material; + source.diffuseImage = globalImages->whiteImage; + source.resourceTableIndex = 300; + // A material can be deduplicated from a world draw before a GUI draw. The + // packet reference, not the first observed material class, owns this domain. + source.permutation.materialClass = RENDER_MATERIAL_OPAQUE; + source.permutation.alphaMode = MC_PERFORATED; + + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool unreferencedAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 299, true, false ); + const materialResourceTableRecord_t *unreferencedRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + const bool unreferencedRejected = unreferencedAdded + && unreferencedRecord != NULL + && !unreferencedRecord->guiDomainReferenced + && !unreferencedRecord->guiPassEligible + && unreferencedRecord->firstGuiPass == -1 + && unreferencedRecord->guiPassCount == 0 + && unreferencedRecord->guiPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED + && !unreferencedRecord->worldDomainReferenced + && !unreferencedRecord->worldPassEligible + && unreferencedRecord->firstWorldPass == -1 + && unreferencedRecord->worldPassCount == 0 + && unreferencedRecord->worldPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.worldPassCount == 0 + && rg_materialResourceTable.stats.guiDomainReferencedRecords == 0 + && rg_materialResourceTable.stats.guiPassEligibleRecords == 0 + && rg_materialResourceTable.stats.guiPassFallbackRecords == 0 + && rg_materialResourceTable.stats.worldDomainReferencedRecords == 0 + && rg_materialResourceTable.stats.worldPassEligibleRecords == 0 + && rg_materialResourceTable.stats.worldPassFallbackRecords == 0; + + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool added = R_MaterialResourceTable_AddRecordFromSource( + source, 300, true, true, true ); + const materialResourceTableRecord_t *record = + R_MaterialResourceTable_RecordForIndex( 0 ); + int passCount = 0; + const rendererMaterialPass_t *passes = record != NULL + ? R_MaterialResourceTable_GuiPasses( *record, passCount ) : NULL; + rendererMaterialPassList_t copiedPasses; + const bool copied = record != NULL + && R_MaterialResourceTable_CopyGuiPassList( *record, copiedPasses ); + int worldPassCount = 0; + const rendererMaterialPass_t *worldPasses = record != NULL + ? R_MaterialResourceTable_WorldPasses( *record, worldPassCount ) : NULL; + rendererMaterialPassList_t copiedWorldPasses; + const bool worldCopied = record != NULL + && R_MaterialResourceTable_CopyWorldPassList( + *record, copiedWorldPasses ); + const materialResourceTextureBinding_t *firstBinding = + passes != NULL && passCount > 0 + ? R_MaterialResourceTable_ResolveTextureResource( + passes[0].textureResourceId ) : NULL; + const materialResourceTextureBinding_t *secondBinding = + passes != NULL && passCount > 1 + ? R_MaterialResourceTable_ResolveTextureResource( + passes[1].textureResourceId ) : NULL; + const std::uint64_t staleResourceId = passes != NULL && passCount > 0 + ? passes[0].textureResourceId : 0; + const std::uint32_t expectedFirstColorMask = RENDERER_COLOR_WRITE_GREEN + | RENDERER_COLOR_WRITE_BLUE; + const bool recordContract = added && record != NULL + && record->guiDomainReferenced + && R_MaterialResourceTable_GuiPassEligible( *record ) + && record->guiPassFailure == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + && record->firstGuiPass == 0 && record->guiPassCount == 2 + && record->textureBindingCount == 3 + && record->materialClass == RENDER_MATERIAL_OPAQUE + && record->semanticBindingIndex[MATERIAL_RESOURCE_TEXTURE_EMISSIVE] == 0 + && R_MaterialResourceTable_TextureBindingForSemantic( + *record, MATERIAL_RESOURCE_TEXTURE_EMISSIVE ) == &record->textures[0]; + const bool orderedResourceContract = recordContract && passes != NULL + && passCount == 2 && copied && copiedPasses.count == 2 + && passes[0].order == 0 && passes[1].order == 1 + && passes[0].sourceStageIndex == 0 && passes[1].sourceStageIndex == 1 + && passes[0].kind == RENDERER_MATERIAL_PASS_GUI + && passes[1].kind == RENDERER_MATERIAL_PASS_GUI + && passes[0].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && passes[1].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && passes[0].textureResourceId != passes[1].textureResourceId + && firstBinding == &record->textures[0] + && secondBinding == &record->textures[1] + && firstBinding->stageIndex == 0 && secondBinding->stageIndex == 1 + && !firstBinding->depthWrite; + const bool registerContract = orderedResourceContract + && passes[0].condition.source == RENDERER_REGISTER_INDEX + && passes[0].condition.index == material->GetStage( 0 )->conditionRegister + && passes[0].color[0].source == RENDERER_REGISTER_INDEX + && passes[0].color[0].index == material->GetStage( 0 )->color.registers[0] + && passes[0].textureMatrix[0].source == RENDERER_REGISTER_INDEX + && passes[0].textureMatrix[5].source == RENDERER_REGISTER_INDEX; + const bool blendDepthContract = registerContract && passes[0].blend.enabled + && passes[0].blend.sourceColor == RENDERER_BLEND_SRC_ALPHA + && passes[0].blend.destinationColor == RENDERER_BLEND_ONE_MINUS_SRC_ALPHA + && !passes[0].depth.testEnabled && !passes[0].depth.writeEnabled + && passes[0].depth.compareOperation == RENDERER_COMPARE_LESS_OR_EQUAL + && passes[0].cull == RENDERER_CULL_NONE; + const bool colorAlphaContract = blendDepthContract + && passes[0].colorWriteMask == expectedFirstColorMask + && !passes[0].alphaTestEnabled + && passes[0].alphaTestCompareOperation == RENDERER_COMPARE_GREATER + && passes[0].alphaTest.source == RENDERER_REGISTER_CONSTANT + && passes[0].alphaTest.constantValue == 0.5f; + const bool firstStateContract = colorAlphaContract + && passes[0].vertexColor == RENDERER_VERTEX_COLOR_MODULATE + && passes[0].polygonOffsetEnabled + && passes[0].polygonOffsetFactor.source == RENDERER_REGISTER_CONSTANT + && passes[0].polygonOffsetUnits.source == RENDERER_REGISTER_CONSTANT + && passes[0].programFamily == RENDERER_PROGRAM_GUI + && RendererContracts_ValidateMaterialPass( passes[0] ); + const bool secondStateContract = firstStateContract && passes[1].blend.enabled + && passes[1].blend.sourceColor == RENDERER_BLEND_DST_ALPHA + && passes[1].blend.destinationColor == RENDERER_BLEND_ONE + && passes[1].vertexColor == RENDERER_VERTEX_COLOR_INVERSE_MODULATE + && RendererContracts_ValidateMaterialPass( passes[1] ); + const bool worldRecordContract = secondStateContract + && record->worldDomainReferenced + && R_MaterialResourceTable_WorldPassEligible( *record ) + && record->worldPassFailure == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + && record->firstWorldPass == 0 && record->worldPassCount == 2 + && worldPasses != NULL && worldPassCount == 2 + && worldCopied && copiedWorldPasses.count == 2; + rendererDepthState_t expectedWorldDepth[ 2 ]; + const bool worldFixtureDepthMapped = material->GetNumStages() >= 2 + && R_MaterialResourceTable_MapWorldDepthState( + material->GetStage( 0 )->drawStateBits, expectedWorldDepth[ 0 ] ) + && R_MaterialResourceTable_MapWorldDepthState( + material->GetStage( 1 )->drawStateBits, expectedWorldDepth[ 1 ] ); + const bool worldKindProgramDepthContract = worldRecordContract + && worldFixtureDepthMapped + && worldPasses[0].order == 0 && worldPasses[1].order == 1 + && worldPasses[0].sourceStageIndex == 0 + && worldPasses[1].sourceStageIndex == 1 + && worldPasses[0].kind == RENDERER_MATERIAL_PASS_SURFACE + && worldPasses[1].kind == RENDERER_MATERIAL_PASS_SURFACE + && worldPasses[0].programFamily == RENDERER_PROGRAM_FIXED + && worldPasses[1].programFamily == RENDERER_PROGRAM_FIXED + && worldPasses[0].textureResourceId == passes[0].textureResourceId + && worldPasses[1].textureResourceId == passes[1].textureResourceId + && worldPasses[0].depth.testEnabled + == expectedWorldDepth[0].testEnabled + && worldPasses[0].depth.writeEnabled + == expectedWorldDepth[0].writeEnabled + && worldPasses[0].depth.compareOperation + == expectedWorldDepth[0].compareOperation + && worldPasses[1].depth.testEnabled + == expectedWorldDepth[1].testEnabled + && worldPasses[1].depth.writeEnabled + == expectedWorldDepth[1].writeEnabled + && worldPasses[1].depth.compareOperation + == expectedWorldDepth[1].compareOperation + && RendererContracts_ValidateMaterialPass( worldPasses[0] ) + && RendererContracts_ValidateMaterialPass( worldPasses[1] ); + rendererDepthState_t lessDepth; + rendererDepthState_t equalDepth; + rendererDepthState_t alwaysDepth; + const bool worldDepthMappingContract = worldKindProgramDepthContract + && R_MaterialResourceTable_MapWorldDepthState( + GLS_DEPTHFUNC_LESS | GLS_DEPTHMASK, lessDepth ) + && lessDepth.testEnabled && !lessDepth.writeEnabled + && lessDepth.compareOperation == RENDERER_COMPARE_LESS_OR_EQUAL + && R_MaterialResourceTable_MapWorldDepthState( + GLS_DEPTHFUNC_EQUAL, equalDepth ) + && equalDepth.testEnabled && equalDepth.writeEnabled + && equalDepth.compareOperation == RENDERER_COMPARE_EQUAL + && R_MaterialResourceTable_MapWorldDepthState( + GLS_DEPTHFUNC_ALWAYS, alwaysDepth ) + && alwaysDepth.testEnabled && alwaysDepth.writeEnabled + && alwaysDepth.compareOperation == RENDERER_COMPARE_ALWAYS; + const bool statsContract = worldDepthMappingContract + && rg_materialResourceTable.stats.guiPasses == 2 + && rg_materialResourceTable.stats.guiDomainReferencedRecords == 1 + && rg_materialResourceTable.stats.guiPassEligibleRecords == 1 + && rg_materialResourceTable.stats.guiPassFallbackRecords == 0 + && rg_materialResourceTable.stats.worldPasses == 2 + && rg_materialResourceTable.stats.worldDomainReferencedRecords == 1 + && rg_materialResourceTable.stats.worldPassEligibleRecords == 1 + && rg_materialResourceTable.stats.worldPassFallbackRecords == 0; + bool ok = unreferencedRejected && statsContract; + const bool initialEligible = record != NULL && record->guiPassEligible; + const materialResourceGuiPassFailure_t initialFailure = record != NULL + ? record->guiPassFailure : MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL; + const int initialBindingCount = record != NULL ? record->textureBindingCount : -1; + + // The authored alpha-test flag belongs to the separate classic depth/coverage + // walk, not the ambient color walk. Generated 2D has no paired prerequisite + // and rejects it, while world ambient keeps both color stages and relies on + // the domain's exact classic depth-packet proof. + shaderStage_t *alphaStage = material->GetNumStages() > 0 + ? const_cast( material->GetStage( 0 ) ) : NULL; + bool alphaDomainContract = false; + if ( alphaStage != NULL ) { + alphaStage->hasAlphaTest = true; + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool alphaAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 301, true, true, true ); + const materialResourceTableRecord_t *alphaRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + alphaDomainContract = alphaAdded && alphaRecord != NULL + && !alphaRecord->guiPassEligible + && alphaRecord->firstGuiPass == -1 + && alphaRecord->guiPassCount == 0 + && alphaRecord->guiPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_ALPHA_TEST + && alphaRecord->guiPassFailureStage == 0 + && alphaRecord->worldPassEligible + && alphaRecord->firstWorldPass == 0 + && alphaRecord->worldPassCount == 2 + && alphaRecord->worldPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE + && alphaRecord->worldPassFailureStage == -1 + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.worldPassCount == 2; + alphaStage->hasAlphaTest = false; + } + + // A private offset layered over a material offset has classic carry-forward + // semantics that this first independent-pass contract does not encode. + bool combinedPolygonOffsetRejected = false; + if ( alphaStage != NULL ) { + alphaStage->privatePolygonOffset = 3.0f; + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool combinedOffsetAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 305, true, true, true ); + const materialResourceTableRecord_t *combinedOffsetRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + combinedPolygonOffsetRejected = combinedOffsetAdded + && combinedOffsetRecord != NULL + && !combinedOffsetRecord->guiPassEligible + && combinedOffsetRecord->firstGuiPass == -1 + && combinedOffsetRecord->guiPassCount == 0 + && combinedOffsetRecord->guiPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE + && combinedOffsetRecord->guiPassFailureStage == 0 + && !combinedOffsetRecord->worldPassEligible + && combinedOffsetRecord->firstWorldPass == -1 + && combinedOffsetRecord->worldPassCount == 0 + && combinedOffsetRecord->worldPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE + && combinedOffsetRecord->worldPassFailureStage == 0 + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.worldPassCount == 0; + alphaStage->privatePolygonOffset = 0.0f; + } + + // Post-process sorting is coupled to scene capture and skip controls in the + // classic walker. Prove it cannot leak a partially compiled shared list. + const float initialSort = material->GetSort(); + material->SetSort( SS_POST_PROCESS ); + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool postProcessAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 303, true, true, true ); + const materialResourceTableRecord_t *postProcessRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + const bool postProcessRejected = postProcessAdded && postProcessRecord != NULL + && !postProcessRecord->guiPassEligible + && postProcessRecord->firstGuiPass == -1 + && postProcessRecord->guiPassCount == 0 + && postProcessRecord->guiPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_POST_PROCESS_SORT + && postProcessRecord->guiPassFailureStage == -1 + && !postProcessRecord->worldPassEligible + && postProcessRecord->firstWorldPass == -1 + && postProcessRecord->worldPassCount == 0 + && postProcessRecord->worldPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_POST_PROCESS_SORT + && postProcessRecord->worldPassFailureStage == -1 + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.worldPassCount == 0; + material->SetSort( initialSort ); + + // Decal-sort admission would bypass r_skipDecals in the shared consumers. + // Reject it atomically until that cvar is part of the sealed view contract. + material->SetSort( SS_DECAL ); + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool decalAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 304, true, true, true ); + const materialResourceTableRecord_t *decalRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + const bool decalRejected = decalAdded && decalRecord != NULL + && !decalRecord->guiPassEligible + && decalRecord->firstGuiPass == -1 + && decalRecord->guiPassCount == 0 + && decalRecord->guiPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_DECAL_SORT + && decalRecord->guiPassFailureStage == -1 + && !decalRecord->worldPassEligible + && decalRecord->firstWorldPass == -1 + && decalRecord->worldPassCount == 0 + && decalRecord->worldPassFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_DECAL_SORT + && decalRecord->worldPassFailureStage == -1 + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.worldPassCount == 0; + material->SetSort( initialSort ); + + // A new frame generation invalidates every old opaque resource id. Leave one + // slot free and prove that a two-stage material fails without committing its + // first pass or exposing a partially eligible record. + const bool staleRejected = staleResourceId != 0 + && R_MaterialResourceTable_ResolveTextureResource( staleResourceId ) == NULL; + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + rg_materialResourceTable.guiPassCount = MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES - 1; + const bool overflowAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 302, true, true ); + const materialResourceTableRecord_t *overflowRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + int overflowPassCount = -1; + const rendererMaterialPass_t *overflowPasses = overflowRecord != NULL + ? R_MaterialResourceTable_GuiPasses( *overflowRecord, overflowPassCount ) : NULL; + const materialResourceGuiPassFailure_t guiOverflowFailure = overflowRecord != NULL + ? overflowRecord->guiPassFailure + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL; + const bool guiOverflowContract = overflowAdded && overflowRecord != NULL + && !overflowRecord->guiPassEligible + && overflowRecord->firstGuiPass == -1 + && overflowRecord->guiPassCount == 0 + && overflowRecord->guiPassFailure == MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW + && overflowPasses == NULL && overflowPassCount == 0 + && rg_materialResourceTable.guiPassCount == MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES - 1 + && rg_materialResourceTable.stats.guiPasses == 0 + && rg_materialResourceTable.stats.guiPassEligibleRecords == 0 + && rg_materialResourceTable.stats.guiPassFallbackRecords == 1 + && rg_materialResourceTable.stats.guiPassPoolOverflows == 1; + + // The world ambient pool is separately bounded. Leave one slot free and + // prove a repeated two-stage material rolls back without affecting the GUI + // pool or exposing a partial world list. + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + rg_materialResourceTable.worldPassCount = + MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES - 1; + const bool worldOverflowAdded = R_MaterialResourceTable_AddRecordFromSource( + source, 306, true, false, true ); + const materialResourceTableRecord_t *worldOverflowRecord = + R_MaterialResourceTable_RecordForIndex( 0 ); + int worldOverflowPassCount = -1; + const rendererMaterialPass_t *worldOverflowPasses = worldOverflowRecord != NULL + ? R_MaterialResourceTable_WorldPasses( + *worldOverflowRecord, worldOverflowPassCount ) : NULL; + const materialResourceWorldPassFailure_t worldOverflowFailure = + worldOverflowRecord != NULL ? worldOverflowRecord->worldPassFailure + : MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL; + const bool worldOverflowContract = worldOverflowAdded + && worldOverflowRecord != NULL + && !worldOverflowRecord->worldPassEligible + && worldOverflowRecord->firstWorldPass == -1 + && worldOverflowRecord->worldPassCount == 0 + && worldOverflowFailure + == MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW + && worldOverflowPasses == NULL && worldOverflowPassCount == 0 + && rg_materialResourceTable.worldPassCount + == MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES - 1 + && rg_materialResourceTable.guiPassCount == 0 + && rg_materialResourceTable.stats.worldPasses == 0 + && rg_materialResourceTable.stats.worldPassEligibleRecords == 0 + && rg_materialResourceTable.stats.worldPassFallbackRecords == 1 + && rg_materialResourceTable.stats.worldPassPoolOverflows == 1; + ok &= alphaDomainContract && combinedPolygonOffsetRejected + && postProcessRejected && decalRejected && staleRejected + && guiOverflowContract && worldOverflowContract; + + if ( !ok ) { + common->Printf( + "RendererMaterialResourceTable self-test failed: ordered pass contract unreferenced=%d added=%d record=%d eligible=%d failure=%s guiPasses=%d worldPasses=%d bindings=%d copied=%d/%d resolved=%d/%d groups=%d/%d/%d/%d/%d/%d/%d/%d/%d/%d alphaDomain=%d combinedOffsetRejected=%d postProcessRejected=%d decalRejected=%d stale=%d guiOverflow=%d/%s worldOverflow=%d/%s pool=%d/%d\n", + unreferencedRejected ? 1 : 0, + added ? 1 : 0, + record != NULL ? 1 : 0, + initialEligible ? 1 : 0, + MaterialResourceGuiPassFailure_Name( initialFailure ), + passCount, + worldPassCount, + initialBindingCount, + copied ? 1 : 0, + worldCopied ? 1 : 0, + firstBinding != NULL ? 1 : 0, + secondBinding != NULL ? 1 : 0, + recordContract ? 1 : 0, + orderedResourceContract ? 1 : 0, + registerContract ? 1 : 0, + blendDepthContract ? 1 : 0, + colorAlphaContract ? 1 : 0, + firstStateContract ? 1 : 0, + secondStateContract ? 1 : 0, + worldRecordContract ? 1 : 0, + worldKindProgramDepthContract ? 1 : 0, + statsContract ? 1 : 0, + alphaDomainContract ? 1 : 0, + combinedPolygonOffsetRejected ? 1 : 0, + postProcessRejected ? 1 : 0, + decalRejected ? 1 : 0, + staleRejected ? 1 : 0, + guiOverflowContract ? 1 : 0, + MaterialResourceGuiPassFailure_Name( guiOverflowFailure ), + worldOverflowContract ? 1 : 0, + MaterialResourceWorldPassFailure_Name( worldOverflowFailure ), + rg_materialResourceTable.guiPassCount, + rg_materialResourceTable.worldPassCount ); + } + for ( int i = 0; i < rg_materialResourceTable.stats.records; ++i ) { + rg_materialResourceTable.records[i].material = NULL; + } + material->SetSort( parsedSort ); + DeclManager_FreeAllocatedDecl( materialDecl ); + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.maxClassicTextureUnits = savedMaxClassicTextureUnits; + if ( ok ) { + common->Printf( "RendererMaterialResourceTable ordered GUI/world pass contract self-test passed\n" ); + } + return ok; +} + +static bool R_MaterialResourceTable_RunPBRContractSelfTest( void ) { + if ( globalImages == NULL ) { + common->Printf( "RendererMaterialResourceTable PBR self-test skipped: images unavailable\n" ); + return true; + } + static const char declaration[] = + "material _pbr_resource_table_selftest {\n" + " bumpmap _flat\n" + " diffusemap _white\n" + " specularmap _black\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " normalMap _flat\n" + " normalFormat tangentRG\n" + " ormMap _white\n" + " metallic 0.2\n" + " roughness 0.7\n" + " ao 1.0\n" + " }\n" + "}\n"; + + idDecl *materialDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( materialDecl == NULL ) { + common->Printf( "RendererMaterialResourceTable self-test failed: PBR declaration allocation\n" ); + return false; + } + idMaterial *material = static_cast( materialDecl ); + if ( !material->Parse( declaration, idLib::SizeToInt( sizeof( declaration ) - 1, "PBR resource-table self-test" ) ) ) { + common->Printf( "RendererMaterialResourceTable self-test failed: PBR declaration parse\n" ); + DeclManager_FreeAllocatedDecl( materialDecl ); + return false; + } + const int savedMaxClassicTextureUnits = rg_materialResourceTable.maxClassicTextureUnits; + // Vulkan can invoke this CPU-only contract before the classic material table + // is initialized. Scope the same minimum unit budget used by this synthetic + // bump/diffuse/specular material, then restore the live backend state. + rg_materialResourceTable.maxClassicTextureUnits = Max( savedMaxClassicTextureUnits, 3 ); + const pbrMaterialInfo_t &pbr = material->GetPBRInfo(); + materialResourceRecord_t source; + memset( &source, 0, sizeof( source ) ); + source.material = material; + source.diffuseImage = globalImages->whiteImage; + source.normalImage = globalImages->flatNormalMap; + source.specularImage = globalImages->blackImage; + source.hasPBR = true; + source.pbrWorkflow = static_cast( pbr.workflow ); + source.pbrNormalFormat = static_cast( pbr.normalFormat ); + source.pbrAlbedoImage = pbr.albedo.image; + source.pbrNormalImage = pbr.normal.image; + source.pbrORMImage = pbr.orm.image; + source.pbrHasAuthoredClassicFallback = pbr.hasAuthoredClassicFallback; + source.pbrHasExplicitLegacyFallback = pbr.hasExplicitLegacyFallback; + source.pbrUsesGeneratedLegacyFallback = pbr.usesGeneratedLegacyFallback; + source.pbrUsesApproximateLegacyFallback = pbr.usesApproximateLegacyFallback; + source.pbrLegacyFallbackMissing = pbr.legacyFallbackMissing; + source.pbrMetallicRegister = pbr.metallicRegister; + source.pbrRoughnessRegister = pbr.roughnessRegister; + source.pbrAORegister = pbr.aoRegister; + source.pbrNormalScaleRegister = pbr.normalScaleRegister; + memcpy( source.pbrEmissiveColorRegisters, pbr.emissiveColorRegisters, sizeof( source.pbrEmissiveColorRegisters ) ); + source.permutation.materialClass = RENDER_MATERIAL_OPAQUE; + source.permutation.alphaMode = MC_OPAQUE; + source.resourceTableIndex = 200; + + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.stats.prepared = true; + const bool added = R_MaterialResourceTable_AddRecordFromSource( source, 200, true ); + + materialResourceRecord_t separateSource = source; + separateSource.pbrORMImage = NULL; + separateSource.pbrMetallicImage = globalImages->whiteImage; + separateSource.pbrRoughnessImage = globalImages->whiteImage; + separateSource.pbrAOImage = globalImages->whiteImage; + separateSource.resourceTableIndex = 201; + const bool separateAdded = R_MaterialResourceTable_AddRecordFromSource( separateSource, 201, true ); + + materialResourceRecord_t scalarSource = source; + scalarSource.pbrNormalImage = NULL; + scalarSource.pbrNormalFormat = PBR_NORMAL_UNSPECIFIED; + scalarSource.pbrORMImage = NULL; + scalarSource.resourceTableIndex = 202; + const bool scalarAdded = R_MaterialResourceTable_AddRecordFromSource( scalarSource, 202, true ); + + materialResourceRecord_t explicitSource = source; + explicitSource.pbrHasAuthoredClassicFallback = false; + explicitSource.pbrHasExplicitLegacyFallback = true; + explicitSource.pbrUsesGeneratedLegacyFallback = true; + explicitSource.pbrUsesApproximateLegacyFallback = false; + explicitSource.resourceTableIndex = 203; + const bool explicitAdded = R_MaterialResourceTable_AddRecordFromSource( explicitSource, 203, true ); + + materialResourceRecord_t unsupportedSource = source; + unsupportedSource.pbrWorkflow = PBR_WORKFLOW_SPECULAR_GLOSSINESS; + unsupportedSource.resourceTableIndex = 204; + const bool unsupportedAdded = R_MaterialResourceTable_AddRecordFromSource( unsupportedSource, 204, true ); + + materialResourceRecord_t missingFallbackSource = source; + missingFallbackSource.pbrHasAuthoredClassicFallback = false; + missingFallbackSource.pbrLegacyFallbackMissing = true; + missingFallbackSource.resourceTableIndex = 205; + const bool missingFallbackAdded = R_MaterialResourceTable_AddRecordFromSource( missingFallbackSource, 205, true ); + + materialResourceRecord_t missingAlbedoSource = source; + missingAlbedoSource.pbrAlbedoImage = NULL; + missingAlbedoSource.resourceTableIndex = 206; + const bool missingAlbedoAdded = R_MaterialResourceTable_AddRecordFromSource( missingAlbedoSource, 206, true ); + + idImageOpts mutableImageOpts = globalImages->whiteImage->GetOpts(); + idImage *mutableImage = globalImages->ScratchImage( + "_pbr_resource_table_mutable_selftest", + &mutableImageOpts, + TF_DEFAULT, + TR_REPEAT, + TD_PBR_COLOR ); + materialResourceRecord_t mutableImageSource = source; + mutableImageSource.pbrAlbedoImage = mutableImage; + mutableImageSource.resourceTableIndex = 207; + const bool mutableImageAdded = mutableImage != NULL + && R_MaterialResourceTable_AddRecordFromSource( mutableImageSource, 207, true ); + + // Authored legacy-map metadata is not an explicit-generated fallback when + // the complete classic interaction already made stage generation redundant. + materialResourceRecord_t redundantExplicitSource = source; + redundantExplicitSource.pbrHasExplicitLegacyFallback = true; + redundantExplicitSource.pbrUsesGeneratedLegacyFallback = false; + redundantExplicitSource.resourceTableIndex = 208; + const bool redundantExplicitAdded = R_MaterialResourceTable_AddRecordFromSource( redundantExplicitSource, 208, true ); + + // A source-alpha PBR surface is valid only through the ordered transparent + // forward path; retain it here so that path can perform blend validation. + materialResourceRecord_t transparentSource = source; + transparentSource.permutation.materialClass = RENDER_MATERIAL_TRANSLUCENT; + transparentSource.permutation.alphaMode = MC_TRANSLUCENT; + transparentSource.resourceTableIndex = 209; + const bool transparentAdded = R_MaterialResourceTable_AddRecordFromSource( transparentSource, 209, true ); + + R_MaterialResourceTable_BuildTextureArrayTable(); + const materialResourceTableRecord_t *record = R_MaterialResourceTable_RecordForIndex( 0 ); + const materialResourceTableRecord_t *separateRecord = R_MaterialResourceTable_RecordForIndex( 1 ); + const materialResourceTableRecord_t *scalarRecord = R_MaterialResourceTable_RecordForIndex( 2 ); + const materialResourceTableRecord_t *explicitRecord = R_MaterialResourceTable_RecordForIndex( 3 ); + const materialResourceTableRecord_t *unsupportedRecord = R_MaterialResourceTable_RecordForIndex( 4 ); + const materialResourceTableRecord_t *missingFallbackRecord = R_MaterialResourceTable_RecordForIndex( 5 ); + const materialResourceTableRecord_t *missingAlbedoRecord = R_MaterialResourceTable_RecordForIndex( 6 ); + const materialResourceTableRecord_t *mutableImageRecord = R_MaterialResourceTable_RecordForIndex( 7 ); + const materialResourceTableRecord_t *redundantExplicitRecord = R_MaterialResourceTable_RecordForIndex( 8 ); + const materialResourceTableRecord_t *transparentRecord = R_MaterialResourceTable_RecordForIndex( 9 ); + const materialResourceTableStats_t &stats = R_MaterialResourceTable_Stats(); + const bool pbrEnabled = r_rendererModernQuality.GetBool() && r_pbrMaterials.GetBool(); + const materialResourcePBRFallbackReason_t expectedReason = pbrEnabled + ? MATERIAL_RESOURCE_PBR_FALLBACK_NONE + : MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED; + const materialResourcePBRFallbackReason_t expectedSeparateReason = pbrEnabled + ? MATERIAL_RESOURCE_PBR_FALLBACK_NONE + : MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED; + materialResourceTableRecord_t sourceAlphaContract; + R_MaterialResourceTable_InitSelfTestRecord( sourceAlphaContract ); + sourceAlphaContract.material = material; + sourceAlphaContract.hasPBR = true; + sourceAlphaContract.pbrModernReady = pbrEnabled; + sourceAlphaContract.pbrFallbackReason = expectedReason; + sourceAlphaContract.blendMode = MATERIAL_RESOURCE_BLEND_BLEND; + sourceAlphaContract.blendStageCount = 1; + const materialResourceTextureBinding_t *sourceAlphaAlbedo = record != NULL + ? R_MaterialResourceTable_TextureBindingForSemantic( *record, MATERIAL_RESOURCE_TEXTURE_ALBEDO ) + : NULL; + const shaderStage_t *identityStage = material->GetNumStages() > 0 ? material->GetStage( 0 ) : NULL; + if ( sourceAlphaAlbedo != NULL && identityStage != NULL ) { + sourceAlphaContract.textureBindingCount = 2; + sourceAlphaContract.semanticBindingIndex[MATERIAL_RESOURCE_TEXTURE_ALBEDO] = 0; + sourceAlphaContract.semanticBindingIndex[MATERIAL_RESOURCE_TEXTURE_EMISSIVE] = 1; + sourceAlphaContract.textures[0] = *sourceAlphaAlbedo; + sourceAlphaContract.textures[0].semantic = MATERIAL_RESOURCE_TEXTURE_ALBEDO; + materialResourceTextureBinding_t &alphaStage = sourceAlphaContract.textures[1]; + alphaStage = *sourceAlphaAlbedo; + alphaStage.semantic = MATERIAL_RESOURCE_TEXTURE_EMISSIVE; + alphaStage.stageIndex = 0; + alphaStage.drawStateBits = GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA | GLS_DEPTHMASK; + alphaStage.blendEnabled = true; + alphaStage.depthWrite = false; + alphaStage.colorMasked = false; + alphaStage.hasConditionRegister = false; + alphaStage.hasAlphaTest = false; + alphaStage.hasTextureMatrix = false; + alphaStage.privatePolygonOffset = 0.0f; + alphaStage.texgen = static_cast( TG_EXPLICIT ); + alphaStage.vertexColorMode = static_cast( SVC_IGNORE ); + memcpy( alphaStage.colorRegisters, identityStage->color.registers, sizeof( alphaStage.colorRegisters ) ); + } + const bool sourceAlphaContractAccepted = R_MaterialResourceTable_PBRTransparentPathEligible( sourceAlphaContract ) == pbrEnabled; + materialResourceTableRecord_t complexSourceAlpha = sourceAlphaContract; + complexSourceAlpha.additiveStageCount = 1; + const bool sourceAlphaComplexBlendRejected = !R_MaterialResourceTable_PBRTransparentPathEligible( complexSourceAlpha ); + materialResourceTableRecord_t mismatchedAlbedoSourceAlpha = sourceAlphaContract; + mismatchedAlbedoSourceAlpha.textures[1].image = globalImages->blackImage; + const bool sourceAlphaMismatchedAlbedoRejected = !R_MaterialResourceTable_PBRTransparentPathEligible( mismatchedAlbedoSourceAlpha ); + materialResourceTableRecord_t wrongPassSourceAlpha = sourceAlphaContract; + wrongPassSourceAlpha.textures[1].semantic = MATERIAL_RESOURCE_TEXTURE_DIFFUSE; + const bool sourceAlphaWrongPassRejected = !R_MaterialResourceTable_PBRTransparentPathEligible( wrongPassSourceAlpha ); + materialResourceTableRecord_t tintedSourceAlpha = sourceAlphaContract; + tintedSourceAlpha.textures[1].colorRegisters[0] = EXP_REG_PARM0; + const bool sourceAlphaTintedRejected = !R_MaterialResourceTable_PBRTransparentPathEligible( tintedSourceAlpha ); + materialResourceTableRecord_t transformedUVSourceAlpha = sourceAlphaContract; + transformedUVSourceAlpha.textures[1].texgen = static_cast( TG_SCREEN ); + const bool sourceAlphaTransformedUVRejected = !R_MaterialResourceTable_PBRTransparentPathEligible( transformedUVSourceAlpha ); + bool pbrBindingsExcludedFromClassicTable = stats.records == 10; + for ( int recordIndex = 0; recordIndex < stats.records; ++recordIndex ) { + const materialResourceTableRecord_t *testRecord = R_MaterialResourceTable_RecordForIndex( recordIndex ); + pbrBindingsExcludedFromClassicTable &= testRecord != NULL && testRecord->hasPBR; + if ( testRecord != NULL ) { + for ( int i = 0; i < testRecord->textureBindingCount; ++i ) { + pbrBindingsExcludedFromClassicTable &= !testRecord->textures[i].textureArrayCandidate + && testRecord->textures[i].textureArrayLayer == -1; + } + } + } + const bool ok = added && separateAdded && scalarAdded && explicitAdded + && unsupportedAdded && missingFallbackAdded && missingAlbedoAdded && mutableImageAdded && redundantExplicitAdded && transparentAdded + && record != NULL + && record->hasPBR + && record->pbrResourceReady + && record->pbrModernReady == pbrEnabled + && !R_MaterialResourceTable_ClassicModernPathEligible( *record ) + && R_MaterialResourceTable_PBRModernPathEligible( *record ) == pbrEnabled + && record->pbrFallbackReason == expectedReason + && record->pbrPackedMaterialData + && !record->pbrSeparateMaterialData + && record->pbrHasAuthoredClassicFallback + && R_MaterialResourceTable_TextureBindingForSemantic( *record, MATERIAL_RESOURCE_TEXTURE_ALBEDO ) != NULL + && R_MaterialResourceTable_TextureBindingForSemantic( *record, MATERIAL_RESOURCE_TEXTURE_NORMAL ) != NULL + && R_MaterialResourceTable_TextureBindingForSemantic( *record, MATERIAL_RESOURCE_TEXTURE_ORM ) != NULL + && separateRecord != NULL && separateRecord->pbrResourceReady + && !separateRecord->pbrPackedMaterialData && separateRecord->pbrSeparateMaterialData + && separateRecord->pbrModernReady == pbrEnabled + && R_MaterialResourceTable_PBRModernPathEligible( *separateRecord ) == pbrEnabled + && separateRecord->pbrFallbackReason == expectedSeparateReason + && scalarRecord != NULL && scalarRecord->pbrResourceReady + && !scalarRecord->pbrPackedMaterialData && !scalarRecord->pbrSeparateMaterialData + && !scalarRecord->hasPBRNormal && !scalarRecord->hasPBRORM + && explicitRecord != NULL && explicitRecord->pbrHasExplicitLegacyFallback + && explicitRecord->pbrUsesGeneratedLegacyFallback + && !explicitRecord->pbrUsesApproximateLegacyFallback + && unsupportedRecord != NULL + && unsupportedRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW + && missingFallbackRecord != NULL && missingFallbackRecord->pbrLegacyFallbackMissing + && missingAlbedoRecord != NULL && !missingAlbedoRecord->hasPBRAlbedo + && missingAlbedoRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO + && mutableImageRecord != NULL && mutableImageRecord->hasPBRAlbedo + && !mutableImageRecord->pbrResourceReady + && mutableImageRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE + && redundantExplicitRecord != NULL && redundantExplicitRecord->pbrHasExplicitLegacyFallback + && !redundantExplicitRecord->pbrUsesGeneratedLegacyFallback + && transparentRecord != NULL + && transparentRecord->materialClass == RENDER_MATERIAL_TRANSLUCENT + && transparentRecord->blendMode == MATERIAL_RESOURCE_BLEND_BLEND + && !transparentRecord->pbrResourceReady + && !transparentRecord->pbrModernReady + && !R_MaterialResourceTable_PBRModernPathEligible( *transparentRecord ) + && transparentRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE + && sourceAlphaContractAccepted + && sourceAlphaComplexBlendRejected + && sourceAlphaMismatchedAlbedoRejected + && sourceAlphaWrongPassRejected + && sourceAlphaTintedRejected + && sourceAlphaTransformedUVRejected + && stats.pbrRecords == 10 + && stats.classicRecords == 0 + && stats.pbrResourceReadyRecords == 6 + && stats.pbrModernReadyRecords == ( pbrEnabled ? 5 : 0 ) + && stats.pbrPackedMapRecords == 8 + && stats.pbrSeparateMapRecords == 1 + && stats.pbrAuthoredClassicFallbackRecords == 8 + && stats.pbrExplicitGeneratedFallbackRecords == 1 + && stats.pbrGeneratedFallbackRecords == 1 + && stats.pbrApproximateFallbackRecords == 0 + && stats.pbrMissingLegacyFallbackRecords == 1 + && stats.pbrMissingAlbedoMapRecords == 1 + && stats.pbrMissingNormalMapRecords == 1 + && stats.pbrMissingORMMapRecords == 2 + && stats.pbrFallbackMissingImage == 1 + && stats.pbrFallbackRecords == ( pbrEnabled ? 4 : 10 ) + && stats.pbrFallbackUnsupportedTextureLayout == 0 + && pbrBindingsExcludedFromClassicTable + && stats.textureArrayTableDescriptors == 0; + if ( !ok ) { + common->Printf( + "RendererMaterialResourceTable self-test failed: PBR contract added=%d/%d/%d/%d/%d/%d/%d/%d/%d/%d sourceAlpha=%d/%d/%d/%d/%d record=%d resource=%d modern=%d fallback=%s records=%d packed=%d separate=%d missingFallback=%d mapsMissing=%d/%d/%d\n", + added ? 1 : 0, + separateAdded ? 1 : 0, + scalarAdded ? 1 : 0, + explicitAdded ? 1 : 0, + unsupportedAdded ? 1 : 0, + missingFallbackAdded ? 1 : 0, + missingAlbedoAdded ? 1 : 0, + mutableImageAdded ? 1 : 0, + redundantExplicitAdded ? 1 : 0, + transparentAdded ? 1 : 0, + sourceAlphaContractAccepted ? 1 : 0, + sourceAlphaComplexBlendRejected ? 1 : 0, + sourceAlphaMismatchedAlbedoRejected ? 1 : 0, + sourceAlphaWrongPassRejected ? 1 : 0, + sourceAlphaTintedRejected ? 1 : 0, + sourceAlphaTransformedUVRejected ? 1 : 0, + record != NULL ? 1 : 0, + record != NULL && record->pbrResourceReady ? 1 : 0, + record != NULL && record->pbrModernReady ? 1 : 0, + record != NULL ? MaterialResourcePBRFallbackReason_Name( record->pbrFallbackReason ) : "missing", + stats.pbrRecords, + stats.pbrPackedMapRecords, + stats.pbrSeparateMapRecords, + stats.pbrMissingLegacyFallbackRecords, + stats.pbrMissingAlbedoMapRecords, + stats.pbrMissingNormalMapRecords, + stats.pbrMissingORMMapRecords ); + } + // The table owns no declaration lifetime. Remove every retained synthetic + // pointer and invalidate the hash before releasing the temporary material, + // including failure exits that callers may inspect afterward. + for ( int i = 0; i < stats.records; ++i ) { + rg_materialResourceTable.records[i].material = NULL; + } + DeclManager_FreeAllocatedDecl( materialDecl ); + R_MaterialResourceTable_ResetFrameStats(); + rg_materialResourceTable.maxClassicTextureUnits = savedMaxClassicTextureUnits; + if ( ok ) { + common->Printf( "RendererMaterialResourceTable PBR contract self-test passed\n" ); + } + return ok; +} + static bool R_MaterialResourceTable_RunSyntheticRecordSelfTest( void ) { R_MaterialResourceTable_ResetFrameStats(); rg_materialResourceTable.stats.prepared = true; @@ -1409,6 +3504,7 @@ static bool R_MaterialResourceTable_RunSyntheticRecordSelfTest( void ) { const materialResourceTableStats_t &stats = R_MaterialResourceTable_Stats(); if ( stats.records != 6 + || stats.classicRecords != 6 || stats.opaqueRecords != 2 || stats.perforatedRecords != 1 || stats.translucentRecords != 1 @@ -1417,8 +3513,9 @@ static bool R_MaterialResourceTable_RunSyntheticRecordSelfTest( void ) { || stats.alphaTestRecords != 1 || stats.fallbackMissingImage <= 0 ) { common->Printf( - "RendererMaterialResourceTable self-test failed: synthetic counts records=%d opaque=%d perforated=%d translucent=%d gui=%d post=%d alpha=%d missingFallback=%d\n", + "RendererMaterialResourceTable self-test failed: synthetic counts records=%d classic=%d opaque=%d perforated=%d translucent=%d gui=%d post=%d alpha=%d missingFallback=%d\n", stats.records, + stats.classicRecords, stats.opaqueRecords, stats.perforatedRecords, stats.translucentRecords, @@ -1459,11 +3556,21 @@ static bool R_MaterialResourceTable_RunSyntheticRecordSelfTest( void ) { } bool RendererMaterialResourceTable_RunSelfTest( void ) { - if ( !rg_materialResourceTable.stats.initialized || !rg_materialResourceTable.stats.available ) { - common->Printf( "RendererMaterialResourceTable self-test skipped: material resource table unavailable\n" ); + if ( !R_MaterialResourceTable_RunPBRContractSelfTest() ) { + return false; + } + if ( !rg_materialResourceTable.stats.initialized ) { + common->Printf( "RendererMaterialResourceTable full self-test skipped: material resource table uninitialized\n" ); + return true; + } + if ( !rg_materialResourceTable.stats.available ) { + common->Printf( "RendererMaterialResourceTable full self-test skipped: scene packets unavailable\n" ); return true; } + if ( !R_MaterialResourceTable_RunGuiPassContractSelfTest() ) { + return false; + } if ( !R_MaterialResourceTable_RunSyntheticRecordSelfTest() ) { return false; } @@ -1511,13 +3618,15 @@ bool RendererMaterialResourceTable_RunSelfTest( void ) { const int expectedRecords = tr.defaultMaterial != NULL ? 1 : 0; if ( stats.sourceMaterialRecords != materialRecordCount || stats.records != expectedRecords + || stats.classicRecords != expectedRecords || stats.drawPacketReferences != ( tr.defaultMaterial != NULL ? drawPacketCount : 0 ) || stats.overflow ) { common->Printf( - "RendererMaterialResourceTable self-test failed: packet build mismatch source=%d/%d records=%d expected=%d drawRefs=%d overflow=%d\n", + "RendererMaterialResourceTable self-test failed: packet build mismatch source=%d/%d records=%d classic=%d expected=%d drawRefs=%d overflow=%d\n", stats.sourceMaterialRecords, materialRecordCount, stats.records, + stats.classicRecords, expectedRecords, stats.drawPacketReferences, stats.overflow ? 1 : 0 ); diff --git a/src/renderer/MaterialResourceTable.h b/src/renderer/MaterialResourceTable.h index e1d2d28e..97138e3a 100644 --- a/src/renderer/MaterialResourceTable.h +++ b/src/renderer/MaterialResourceTable.h @@ -5,6 +5,7 @@ #define __MATERIAL_RESOURCE_TABLE_H__ #include "RendererCaps.h" +#include "RendererContracts.h" #include "ScenePackets.h" /* @@ -21,8 +22,19 @@ */ const int MATERIAL_RESOURCE_TABLE_MAX_RECORDS = SCENE_PACKET_MAX_MATERIAL_RECORDS; -const int MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS = 8; +// GL 3.3 guarantees at least 16 fragment texture units. Keep enough record +// slots for a dual-authored material's classic fallback plus the initial PBR +// semantics. The descriptor-array capacity remains 12 so units 12..15 stay +// reserved for the existing shadow contract. +const int MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS = 16; const int MATERIAL_RESOURCE_TABLE_TEXTURE_ARRAY_CAPACITY = 12; +// Authored fixed-function GUI stages are pooled once per material record rather +// than embedding MAX_SHADER_STAGES in every table record. Exhaustion rejects +// the affected GUI domain atomically and leaves the classic renderer in charge. +const int MATERIAL_RESOURCE_TABLE_MAX_GUI_PASSES = SCENE_PACKET_MAX_DRAWS; +// World ambient stages use a distinct pool so preparing an eligible 3D view +// cannot consume or expose any part of the generated-2D GUI pass domain. +const int MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES = SCENE_PACKET_MAX_DRAWS; enum materialResourceBlendMode_t { MATERIAL_RESOURCE_BLEND_OPAQUE = 0, @@ -42,9 +54,33 @@ enum materialResourceTextureSemantic_t { MATERIAL_RESOURCE_TEXTURE_EMISSIVE, MATERIAL_RESOURCE_TEXTURE_GUI, MATERIAL_RESOURCE_TEXTURE_POST_PROCESS, + MATERIAL_RESOURCE_TEXTURE_ALBEDO, + MATERIAL_RESOURCE_TEXTURE_NORMAL, + MATERIAL_RESOURCE_TEXTURE_ORM, + MATERIAL_RESOURCE_TEXTURE_METALLIC, + MATERIAL_RESOURCE_TEXTURE_ROUGHNESS, + MATERIAL_RESOURCE_TEXTURE_AO, + MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR, MATERIAL_RESOURCE_TEXTURE_COUNT }; +enum materialResourcePBRFallbackReason_t { + MATERIAL_RESOURCE_PBR_FALLBACK_NONE = 0, + MATERIAL_RESOURCE_PBR_FALLBACK_DISABLED, + MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW, + MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_MATERIAL_CLASS, + MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO, + MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_NORMAL_FORMAT, + MATERIAL_RESOURCE_PBR_FALLBACK_CONFLICTING_LAYOUT, + // Retained for malformed/over-capacity layouts. Packed ORM and separate + // metallic/roughness/AO inputs are both valid in the direct PBR corridor. + MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_TEXTURE_LAYOUT, + MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE, + MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE, + MATERIAL_RESOURCE_PBR_FALLBACK_TOO_MANY_TEXTURES, + MATERIAL_RESOURCE_PBR_FALLBACK_SHADER_PATH_UNAVAILABLE +}; + enum materialResourceSortGroup_t { MATERIAL_RESOURCE_SORT_UNKNOWN = 0, MATERIAL_RESOURCE_SORT_SUBVIEW, @@ -96,9 +132,41 @@ enum materialResourceFallbackFlags_t { MATERIAL_RESOURCE_FALLBACK_FLAG_STAGE_COLOR = 1 << 16 }; +enum materialResourceGuiPassFailure_t { + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NONE = 0, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_REFERENCED, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_COMPILED, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_MATERIAL, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NOT_DRAWN, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NO_AMBIENT_STAGES, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_NEEDS_CURRENT_RENDER, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_DECAL_SORT, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_POST_PROCESS_SORT, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_DYNAMIC_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_CINEMATIC_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_CURRENT_RENDER_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_CUSTOM_PROGRAM, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_TEXGEN, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_ALPHA_TEST, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_MISSING_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_DEFAULTED_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNLOADED_IMAGE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_TEXTURE_BINDING_OVERFLOW, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_PASS_POOL_OVERFLOW, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_REGISTER, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_UNSUPPORTED_STATE, + MATERIAL_RESOURCE_GUI_PASS_FAILURE_INVALID_PASS +}; + +// GUI and world ambient compilation reject the same authored fixed-function +// features. Keep one failure vocabulary while publishing a domain-specific +// type and name helper to prevent consumers from depending on GUI ownership. +typedef materialResourceGuiPassFailure_t materialResourceWorldPassFailure_t; + typedef struct materialResourceTextureBinding_s { materialResourceTextureSemantic_t semantic; const idImage *image; + std::uint64_t textureResourceId; unsigned int textureHandle; textureFilter_t filter; textureRepeat_t repeat; @@ -136,6 +204,7 @@ typedef struct materialResourceTextureBinding_s { typedef struct materialResourceTableRecord_s { int tableIndex; + unsigned int tableGeneration; int sourceMaterialRecordIndex; int materialId; const idMaterial *material; @@ -173,6 +242,31 @@ typedef struct materialResourceTableRecord_s { bool hasSpecular; bool hasEmissive; bool hasPostProcess; + bool hasPBR; + bool pbrResourceReady; + bool pbrModernReady; + int pbrWorkflow; + int pbrNormalFormat; + materialResourcePBRFallbackReason_t pbrFallbackReason; + bool hasPBRAlbedo; + bool hasPBRNormal; + bool hasPBRORM; + bool hasPBRMetallic; + bool hasPBRRoughness; + bool hasPBRAO; + bool hasPBREmissive; + bool pbrPackedMaterialData; + bool pbrSeparateMaterialData; + bool pbrHasAuthoredClassicFallback; + bool pbrHasExplicitLegacyFallback; + bool pbrUsesGeneratedLegacyFallback; + bool pbrUsesApproximateLegacyFallback; + bool pbrLegacyFallbackMissing; + int pbrMetallicRegister; + int pbrRoughnessRegister; + int pbrAORegister; + int pbrNormalScaleRegister; + int pbrEmissiveColorRegisters[3]; bool hasConditionRegisters; bool hasTextureMatrix; bool hasVertexColor; @@ -197,6 +291,18 @@ typedef struct materialResourceTableRecord_s { unsigned int textureSemanticMask; unsigned int loadedTextureSemanticMask; unsigned int renderableColorTextureMask; + int firstGuiPass; + int guiPassCount; + bool guiDomainReferenced; + bool guiPassEligible; + materialResourceGuiPassFailure_t guiPassFailure; + int guiPassFailureStage; + int firstWorldPass; + int worldPassCount; + bool worldDomainReferenced; + bool worldPassEligible; + materialResourceWorldPassFailure_t worldPassFailure; + int worldPassFailureStage; int semanticBindingIndex[MATERIAL_RESOURCE_TEXTURE_COUNT]; int textureBindingCount; materialResourceTextureBinding_t textures[MATERIAL_RESOURCE_TABLE_MAX_TEXTURE_BINDINGS]; @@ -215,6 +321,7 @@ typedef struct materialResourceTableStats_s { int sourceMaterialRecords; int drawPacketReferences; int records; + int classicRecords; int opaqueRecords; int perforatedRecords; int translucentRecords; @@ -254,6 +361,41 @@ typedef struct materialResourceTableStats_s { int fallbackVertexColor; int fallbackPolygonOffset; int fallbackTooManyTextures; + int guiPasses; + int guiDomainReferencedRecords; + int guiPassEligibleRecords; + int guiPassFallbackRecords; + int guiPassPoolOverflows; + int worldPasses; + int worldDomainReferencedRecords; + int worldPassEligibleRecords; + int worldPassFallbackRecords; + int worldPassPoolOverflows; + int pbrRecords; + int pbrResourceReadyRecords; + int pbrModernReadyRecords; + int pbrPackedMapRecords; + int pbrSeparateMapRecords; + int pbrAuthoredClassicFallbackRecords; + int pbrExplicitGeneratedFallbackRecords; + int pbrGeneratedFallbackRecords; + int pbrApproximateFallbackRecords; + int pbrMissingLegacyFallbackRecords; + int pbrMissingAlbedoMapRecords; + int pbrMissingNormalMapRecords; + int pbrMissingORMMapRecords; + int pbrFallbackRecords; + int pbrFallbackDisabled; + int pbrFallbackUnsupportedWorkflow; + int pbrFallbackUnsupportedMaterialClass; + int pbrFallbackMissingAlbedo; + int pbrFallbackMissingNormalFormat; + int pbrFallbackConflictingLayout; + int pbrFallbackUnsupportedTextureLayout; + int pbrFallbackMissingImage; + int pbrFallbackClassicFeature; + int pbrFallbackTooManyTextures; + int pbrFallbackShaderPathUnavailable; int debugStringTruncations; char debugStringTruncationSource[64]; char lastFailure[96]; @@ -265,13 +407,28 @@ void R_MaterialResourceTable_PrepareFrame( const idScenePacketFrame &packetFrame const materialResourceTableStats_t &R_MaterialResourceTable_Stats( void ); const materialResourceTableRecord_t *R_MaterialResourceTable_RecordForIndex( int tableIndex ); const materialResourceTableRecord_t *R_MaterialResourceTable_FindRecordForMaterial( const idMaterial *material ); +bool R_MaterialResourceTable_ClassicModernPathEligible( const materialResourceTableRecord_t &record ); +// PBR remains a separate ownership corridor: callers must never treat this as +// permission to run the classic material shader path for a PBR record. +bool R_MaterialResourceTable_PBRModernPathEligible( const materialResourceTableRecord_t &record ); +bool R_MaterialResourceTable_PBRTransparentPathEligible( const materialResourceTableRecord_t &record ); const unsigned int *R_MaterialResourceTable_TextureArrayTable( int &count ); int R_MaterialResourceTable_TextureArrayTableIndexForHandle( unsigned int textureHandle ); const char *MaterialResourceBlendMode_Name( materialResourceBlendMode_t blendMode ); const char *MaterialResourceTextureSemantic_Name( materialResourceTextureSemantic_t semantic ); unsigned int MaterialResourceTextureSemantic_Bit( materialResourceTextureSemantic_t semantic ); const materialResourceTextureBinding_t *R_MaterialResourceTable_TextureBindingForSemantic( const materialResourceTableRecord_t &record, materialResourceTextureSemantic_t semantic ); +bool R_MaterialResourceTable_GuiPassEligible( const materialResourceTableRecord_t &record ); +const rendererMaterialPass_t *R_MaterialResourceTable_GuiPasses( const materialResourceTableRecord_t &record, int &count ); +bool R_MaterialResourceTable_CopyGuiPassList( const materialResourceTableRecord_t &record, rendererMaterialPassList_t &destination ); +bool R_MaterialResourceTable_WorldPassEligible( const materialResourceTableRecord_t &record ); +const rendererMaterialPass_t *R_MaterialResourceTable_WorldPasses( const materialResourceTableRecord_t &record, int &count ); +bool R_MaterialResourceTable_CopyWorldPassList( const materialResourceTableRecord_t &record, rendererMaterialPassList_t &destination ); +const materialResourceTextureBinding_t *R_MaterialResourceTable_ResolveTextureResource( std::uint64_t textureResourceId ); const char *MaterialResourceFallbackReason_Name( materialResourceFallbackReason_t reason ); +const char *MaterialResourcePBRFallbackReason_Name( materialResourcePBRFallbackReason_t reason ); +const char *MaterialResourceGuiPassFailure_Name( materialResourceGuiPassFailure_t reason ); +const char *MaterialResourceWorldPassFailure_Name( materialResourceWorldPassFailure_t reason ); void R_MaterialResourceTable_PrintGfxInfo( void ); void R_MaterialResourceTable_DumpLatest( void ); bool RendererMaterialResourceTable_RunSelfTest( void ); diff --git a/src/renderer/Model.cpp b/src/renderer/Model.cpp index 0d49ed72..7472ac30 100644 --- a/src/renderer/Model.cpp +++ b/src/renderer/Model.cpp @@ -35,11 +35,1099 @@ If you have questions concerning this license or the applicable additional terms #include "Model_lwo.h" #include "Model_ma.h" +#include + idCVar idRenderModelStatic::r_mergeModelSurfaces( "r_mergeModelSurfaces", "1", CVAR_BOOL|CVAR_RENDERER, "combine model surfaces with the same material" ); idCVar idRenderModelStatic::r_slopVertex( "r_slopVertex", "0.01", CVAR_RENDERER, "merge xyz coordinates this far apart" ); idCVar idRenderModelStatic::r_slopTexCoord( "r_slopTexCoord", "0.001", CVAR_RENDERER, "merge texture coordinates this far apart" ); idCVar idRenderModelStatic::r_slopNormal( "r_slopNormal", "0.02", CVAR_RENDERER, "merge normals that dot less than this" ); +namespace { + static const uint64_t RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES = 512ULL * 1024ULL * 1024ULL; + static const uint64_t RENDER_MODEL_CACHE_MAX_ALLOCATION_BYTES = 512ULL * 1024ULL * 1024ULL; +} + +bool R_RenderModelCacheFloatIsFinite( float value ) { + return std::isfinite( value ); +} + +idRenderModelCacheReader::idRenderModelCacheReader( idFile &source ) : + file( source ), + transferBytes( 0 ), + allocationBytes( 0 ), + valid( true ) { +} + +bool idRenderModelCacheReader::ReadBytes( void *data, int length ) { + if ( !valid || length < 0 || ( length > 0 && data == NULL ) + || static_cast( length ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes ) { + valid = false; + return false; + } + if ( length > 0 && file.Read( data, length ) != length ) { + valid = false; + return false; + } + transferBytes += static_cast( length ); + return true; +} + +bool idRenderModelCacheReader::ReadInt( int &value ) { + if ( !valid || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.ReadInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheReader::ReadUnsignedInt( unsigned int &value ) { + if ( !valid || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.ReadUnsignedInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheReader::ReadUnsigned64( uint64_t &value ) { + unsigned int low = 0; + unsigned int high = 0; + if ( !ReadUnsignedInt( low ) || !ReadUnsignedInt( high ) ) { + return false; + } + value = static_cast( low ) | ( static_cast( high ) << 32 ); + return true; +} + +bool idRenderModelCacheReader::ReadByte( byte &value ) { + if ( !valid || transferBytes >= RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES + || file.ReadUnsignedChar( value ) != sizeof( value ) ) { + valid = false; + return false; + } + ++transferBytes; + return true; +} + +bool idRenderModelCacheReader::ReadBool( bool &value ) { + byte encoded = 0; + if ( !ReadByte( encoded ) || encoded > 1 ) { + valid = false; + return false; + } + value = ( encoded != 0 ); + return true; +} + +bool idRenderModelCacheReader::ReadFloat( float &value ) { + if ( !valid || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.ReadFloat( value ) != sizeof( value ) || !R_RenderModelCacheFloatIsFinite( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheReader::ReadVec2( idVec2 &value ) { + return ReadFloat( value.x ) && ReadFloat( value.y ); +} + +bool idRenderModelCacheReader::ReadVec3( idVec3 &value ) { + return ReadFloat( value.x ) && ReadFloat( value.y ) && ReadFloat( value.z ); +} + +bool idRenderModelCacheReader::ReadVec4( idVec4 &value ) { + return ReadFloat( value.x ) && ReadFloat( value.y ) && ReadFloat( value.z ) && ReadFloat( value.w ); +} + +bool idRenderModelCacheReader::ReadFloatArray( float *values, int count ) { + if ( count < 0 || ( count > 0 && values == NULL ) + || static_cast( count ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES / sizeof( float ) ) { + valid = false; + return false; + } + if ( !ReadBytes( values, count * static_cast( sizeof( float ) ) ) ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + values[i] = LittleFloat( values[i] ); + if ( !R_RenderModelCacheFloatIsFinite( values[i] ) ) { + valid = false; + return false; + } + } + return true; +} + +bool idRenderModelCacheReader::ReadIntArray( int *values, int count ) { + if ( count < 0 || ( count > 0 && values == NULL ) + || static_cast( count ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES / sizeof( int ) ) { + valid = false; + return false; + } + if ( !ReadBytes( values, count * static_cast( sizeof( int ) ) ) ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + values[i] = LittleLong( values[i] ); + } + return true; +} + +bool idRenderModelCacheReader::ReadBounds( idBounds &value, bool allowCleared ) { + if ( !ReadVec3( value[0] ) || !ReadVec3( value[1] ) ) { + return false; + } + if ( !allowCleared || !value.IsCleared() ) { + for ( int axis = 0; axis < 3; ++axis ) { + if ( value[0][axis] > value[1][axis] ) { + valid = false; + return false; + } + } + } + return true; +} + +bool idRenderModelCacheReader::ReadString( idStr &value, int maxLength ) { + int length = 0; + if ( maxLength < 0 || !ReadInt( length ) || length < 0 || length > maxLength + || !Reserve( length + 1, sizeof( char ) ) ) { + valid = false; + value.Clear(); + return false; + } + if ( length == 0 ) { + value.Clear(); + return true; + } + value.Fill( ' ', length ); + if ( !ReadBytes( &value[0], length ) ) { + value.Clear(); + return false; + } + for ( int i = 0; i < length; ++i ) { + if ( value[i] == '\0' ) { + valid = false; + value.Clear(); + return false; + } + } + return true; +} + +bool idRenderModelCacheReader::ReadCount( int &value, int maxCount, size_t elementSize ) { + if ( maxCount < 0 || !ReadInt( value ) || value < 0 || value > maxCount + || ( elementSize > 0 && !Reserve( value, elementSize ) ) ) { + valid = false; + return false; + } + return true; +} + +bool idRenderModelCacheReader::Reserve( int count, size_t elementSize ) { + if ( !valid || count < 0 || ( count > 0 && elementSize > RENDER_MODEL_CACHE_MAX_ALLOCATION_BYTES / static_cast( count ) ) ) { + valid = false; + return false; + } + const uint64_t bytes = static_cast( count ) * static_cast( elementSize ); + if ( bytes > RENDER_MODEL_CACHE_MAX_ALLOCATION_BYTES - allocationBytes ) { + valid = false; + return false; + } + allocationBytes += bytes; + return true; +} + +idRenderModelCacheWriter::idRenderModelCacheWriter( idFile &destination ) : + file( destination ), + transferBytes( 0 ), + valid( true ) { +} + +bool idRenderModelCacheWriter::WriteBytes( const void *data, int length ) { + if ( !valid || length < 0 || ( length > 0 && data == NULL ) + || static_cast( length ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes ) { + valid = false; + return false; + } + if ( length > 0 && file.Write( data, length ) != length ) { + valid = false; + return false; + } + transferBytes += static_cast( length ); + return true; +} + +bool idRenderModelCacheWriter::WriteInt( int value ) { + if ( !valid || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.WriteInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheWriter::WriteUnsignedInt( unsigned int value ) { + if ( !valid || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.WriteUnsignedInt( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheWriter::WriteUnsigned64( uint64_t value ) { + return WriteUnsignedInt( static_cast( value & 0xffffffffULL ) ) + && WriteUnsignedInt( static_cast( value >> 32 ) ); +} + +bool idRenderModelCacheWriter::WriteByte( byte value ) { + if ( !valid || transferBytes >= RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES + || file.WriteUnsignedChar( value ) != sizeof( value ) ) { + valid = false; + return false; + } + ++transferBytes; + return true; +} + +bool idRenderModelCacheWriter::WriteBool( bool value ) { + return WriteByte( value ? 1 : 0 ); +} + +bool idRenderModelCacheWriter::WriteFloat( float value ) { + if ( !R_RenderModelCacheFloatIsFinite( value ) || !valid + || sizeof( value ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES - transferBytes + || file.WriteFloat( value ) != sizeof( value ) ) { + valid = false; + return false; + } + transferBytes += sizeof( value ); + return true; +} + +bool idRenderModelCacheWriter::WriteVec2( const idVec2 &value ) { + return WriteFloat( value.x ) && WriteFloat( value.y ); +} + +bool idRenderModelCacheWriter::WriteVec3( const idVec3 &value ) { + return WriteFloat( value.x ) && WriteFloat( value.y ) && WriteFloat( value.z ); +} + +bool idRenderModelCacheWriter::WriteVec4( const idVec4 &value ) { + return WriteFloat( value.x ) && WriteFloat( value.y ) && WriteFloat( value.z ) && WriteFloat( value.w ); +} + +bool idRenderModelCacheWriter::WriteFloatArray( const float *values, int count ) { + if ( count < 0 || ( count > 0 && values == NULL ) + || static_cast( count ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES / sizeof( float ) ) { + valid = false; + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !R_RenderModelCacheFloatIsFinite( values[i] ) ) { + valid = false; + return false; + } + } + return WriteBytes( values, count * static_cast( sizeof( float ) ) ); +} + +bool idRenderModelCacheWriter::WriteIntArray( const int *values, int count ) { + if ( count < 0 || ( count > 0 && values == NULL ) + || static_cast( count ) > RENDER_MODEL_CACHE_MAX_TRANSFER_BYTES / sizeof( int ) ) { + valid = false; + return false; + } + return WriteBytes( values, count * static_cast( sizeof( int ) ) ); +} + +bool idRenderModelCacheWriter::WriteBounds( const idBounds &value ) { + return WriteVec3( value[0] ) && WriteVec3( value[1] ); +} + +bool idRenderModelCacheWriter::WriteString( const char *value, int maxLength ) { + if ( value == NULL ) { + value = ""; + } + const size_t length = strlen( value ); + if ( maxLength < 0 || length > static_cast( maxLength ) || length > 0x7fffffffU ) { + valid = false; + return false; + } + return WriteInt( static_cast( length ) ) + && WriteBytes( value, static_cast( length ) ); +} + +bool R_TryReadGeneratedRenderModelCache( idRenderModel &model, const char *sourcePath, + unsigned int parserVersion, const char *settingsKey ) { + idRenderModelStatic *cacheModel = dynamic_cast( &model ); + if ( sourcePath == NULL || sourcePath[0] == '\0' || parserVersion == 0 + || cacheModel == NULL + || cacheModel->LevelLoadCachePayloadType() == RENDER_MODEL_CACHE_UNSUPPORTED ) { + return false; + } + fileSystem->RecordLevelLoadResource( LEVEL_LOAD_RESOURCE_RENDER_MODEL, + sourcePath, settingsKey != NULL ? settingsKey : "", 0u, 2u ); + idFile *cache = fileSystem->OpenGeneratedCacheRead( GENERATED_CACHE_RENDER_MODEL, + sourcePath, parserVersion, settingsKey != NULL ? settingsKey : "" ); + if ( cache == NULL ) { + return false; + } + const bool decoded = cacheModel->ReadLevelLoadCachePayload( *cache ); + const bool consumedPayload = decoded && cache->Tell() == cache->Length(); + fileSystem->CloseFile( cache ); + if ( !consumedPayload ) { + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, + sourcePath, parserVersion, settingsKey != NULL ? settingsKey : "" ); + } + return consumedPayload; +} + +void R_WriteGeneratedRenderModelCache( const idRenderModel &model, const char *sourcePath, + unsigned int parserVersion, const char *settingsKey ) { + const idRenderModelStatic *cacheModel = dynamic_cast( &model ); + if ( sourcePath == NULL || sourcePath[0] == '\0' || parserVersion == 0 + || cacheModel == NULL + || cacheModel->LevelLoadCachePayloadType() == RENDER_MODEL_CACHE_UNSUPPORTED ) { + return; + } + idFile *payload = fileSystem->GetNewFileMemory(); + if ( payload == NULL ) { + return; + } + if ( cacheModel->WriteLevelLoadCachePayload( *payload ) ) { + const int payloadLength = payload->Length(); + const char *payloadData = payload->GetDataPtr(); + if ( payloadLength > 0 && payloadData != NULL ) { + fileSystem->WriteGeneratedCache( GENERATED_CACHE_RENDER_MODEL, + sourcePath, parserVersion, settingsKey != NULL ? settingsKey : "", + payloadData, static_cast( payloadLength ) ); + } + } + fileSystem->CloseFile( payload ); +} + +namespace { + static const unsigned int STATIC_MODEL_CACHE_MAGIC = 0x5334514fU; // "OQ4S" + static const int STATIC_MODEL_CACHE_VERSION = 2; + static const unsigned int STATIC_MODEL_GENERATED_CACHE_PARSER_VERSION = 0x00010002U; // bump when R_BuildDeformInfo's derivation changes: cache hits no longer cross-check against a fresh rebuild + static const int MODEL_CACHE_MAX_NAME = 4096; + static const int MODEL_CACHE_MAX_MATERIAL = 4096; + static const int MODEL_CACHE_MAX_SURFACES = 65536; + static const int MODEL_CACHE_MAX_SURFACE_VERTS = 1 << 20; + static const int MODEL_CACHE_MAX_SURFACE_INDEXES = 1 << 24; + static const int MODEL_CACHE_MAX_SIL_EDGES = 1 << 23; + static const int MODEL_CACHE_MAX_SKIN_TRANSFORMS = 65536; + static const int MODEL_CACHE_MAX_MATERIALIZED_VERTS = 1 << 18; + static const int MODEL_CACHE_MAX_MATERIALIZED_INDEXES = 1 << 20; + + // the bulk array codecs stream these structures as raw little-endian bytes; + // pin their layouts so a change breaks the build instead of the cache format + static_assert( sizeof( glIndex_t ) == sizeof( int ), "cache bulk IO streams glIndex_t as int" ); + static_assert( sizeof( silEdge_t ) == 4 * sizeof( int ), "cache bulk IO streams silEdge_t as 4 ints" ); + static_assert( sizeof( dominantTri_t ) == 2 * sizeof( int ) + 3 * sizeof( float ), "cache bulk IO streams dominantTri_t as 2 ints + 3 floats" ); + static_assert( sizeof( idPlane ) == 4 * sizeof( float ), "cache bulk IO streams idPlane as 4 floats" ); + static_assert( sizeof( idVec4 ) == 4 * sizeof( float ), "cache bulk IO streams idVec4 as 4 floats" ); + static_assert( sizeof( shadowCache_t ) == sizeof( idVec4 ), "cache bulk IO streams shadowCache_t as idVec4" ); +#if defined( _MD5R_SUPPORT ) + static_assert( sizeof( rvSilTraceVertT ) == sizeof( idVec4 ), "cache bulk IO streams rvSilTraceVertT as idVec4" ); +#endif + + struct renderModelCacheTriStage_t { + idBounds bounds; + int surfaceFlags; + bool generateNormals; + bool tangentsCalculated; + bool facePlanesCalculated; + bool perfectHull; + bool deformedSurface; + int numVerts; + idList verts; + int numIndexes; + idList indexes; + idList silIndexes; + idList mirroredVerts; + idList dupVerts; + idList silEdges; + idList facePlanes; + idList dominantTris; + int numShadowIndexesNoFrontCaps; + int numShadowIndexesNoCaps; + int shadowCapPlaneBits; + idList shadowVertexes; + idList silTraceVertexes; + int numSkinToModelTransforms; + idList skinToModelTransforms; + + renderModelCacheTriStage_t() : + surfaceFlags( 0 ), + generateNormals( false ), + tangentsCalculated( false ), + facePlanesCalculated( false ), + perfectHull( false ), + deformedSurface( false ), + numVerts( 0 ), + numIndexes( 0 ), + numShadowIndexesNoFrontCaps( 0 ), + numShadowIndexesNoCaps( 0 ), + shadowCapPlaneBits( 0 ), + numSkinToModelTransforms( 0 ) { + bounds.Clear(); + } + }; + + struct renderModelCacheSurfaceStage_t { + int id; + idStr materialName; + bool materialUnsmoothedTangents; + bool materialCreatesBackSides; + bool materialCastsShadow; + bool materialDiscrete; + bool hasGeometry; + renderModelCacheTriStage_t geometry; + + renderModelCacheSurfaceStage_t() : + id( 0 ), + materialUnsmoothedTangents( false ), + materialCreatesBackSides( false ), + materialCastsShadow( false ), + materialDiscrete( false ), + hasGeometry( false ) {} + }; + + static bool R_ModelCacheBoundsCanWrite( const idBounds &bounds ) { + for ( int axis = 0; axis < 3; ++axis ) { + if ( !R_RenderModelCacheFloatIsFinite( bounds[0][axis] ) + || !R_RenderModelCacheFloatIsFinite( bounds[1][axis] ) ) { + return false; + } + } + if ( bounds.IsCleared() ) { + return true; + } + for ( int axis = 0; axis < 3; ++axis ) { + if ( bounds[0][axis] > bounds[1][axis] ) { + return false; + } + } + return true; + } + + // an idDrawVert in the cache stream: 14 floats (xyz, st, normal, tangents) + // followed by the 8 color/color2 bytes; chunked so a whole batch of vertexes + // moves through one idFile call instead of 22 per vertex + static const int MODEL_CACHE_DRAW_VERT_STREAM_FLOATS = 14; + static const int MODEL_CACHE_DRAW_VERT_STREAM_BYTES = MODEL_CACHE_DRAW_VERT_STREAM_FLOATS * static_cast( sizeof( float ) ) + 8; + static const int MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS = 64; + + static bool R_ModelCacheWriteDrawVertArray( idRenderModelCacheWriter &writer, const idDrawVert *verts, int count ) { + byte chunk[MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS * MODEL_CACHE_DRAW_VERT_STREAM_BYTES]; + if ( count < 0 || ( count > 0 && verts == NULL ) ) { + return false; + } + for ( int base = 0; base < count; base += MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS ) { + const int chunkRecords = Min( count - base, MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS ); + byte *record = chunk; + for ( int i = 0; i < chunkRecords; ++i, record += MODEL_CACHE_DRAW_VERT_STREAM_BYTES ) { + const idDrawVert &vert = verts[base + i]; + float streamFloats[MODEL_CACHE_DRAW_VERT_STREAM_FLOATS]; + streamFloats[0] = vert.xyz.x; + streamFloats[1] = vert.xyz.y; + streamFloats[2] = vert.xyz.z; + streamFloats[3] = vert.st.x; + streamFloats[4] = vert.st.y; + streamFloats[5] = vert.normal.x; + streamFloats[6] = vert.normal.y; + streamFloats[7] = vert.normal.z; + streamFloats[8] = vert.tangents[0].x; + streamFloats[9] = vert.tangents[0].y; + streamFloats[10] = vert.tangents[0].z; + streamFloats[11] = vert.tangents[1].x; + streamFloats[12] = vert.tangents[1].y; + streamFloats[13] = vert.tangents[1].z; + for ( int component = 0; component < MODEL_CACHE_DRAW_VERT_STREAM_FLOATS; ++component ) { + if ( !R_RenderModelCacheFloatIsFinite( streamFloats[component] ) ) { + // same acceptance as WriteFloat: the payload is never published + return false; + } + streamFloats[component] = LittleFloat( streamFloats[component] ); + } + memcpy( record, streamFloats, sizeof( streamFloats ) ); + memcpy( record + sizeof( streamFloats ), vert.color, sizeof( vert.color ) ); + memcpy( record + sizeof( streamFloats ) + sizeof( vert.color ), vert.color2, sizeof( vert.color2 ) ); + } + if ( !writer.WriteBytes( chunk, chunkRecords * MODEL_CACHE_DRAW_VERT_STREAM_BYTES ) ) { + return false; + } + } + return true; + } + + static bool R_ModelCacheReadDrawVertArray( idRenderModelCacheReader &reader, idDrawVert *verts, int count ) { + byte chunk[MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS * MODEL_CACHE_DRAW_VERT_STREAM_BYTES]; + if ( count < 0 || ( count > 0 && verts == NULL ) ) { + return false; + } + for ( int base = 0; base < count; base += MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS ) { + const int chunkRecords = Min( count - base, MODEL_CACHE_DRAW_VERT_CHUNK_RECORDS ); + if ( !reader.ReadBytes( chunk, chunkRecords * MODEL_CACHE_DRAW_VERT_STREAM_BYTES ) ) { + return false; + } + const byte *record = chunk; + for ( int i = 0; i < chunkRecords; ++i, record += MODEL_CACHE_DRAW_VERT_STREAM_BYTES ) { + float streamFloats[MODEL_CACHE_DRAW_VERT_STREAM_FLOATS]; + memcpy( streamFloats, record, sizeof( streamFloats ) ); + for ( int component = 0; component < MODEL_CACHE_DRAW_VERT_STREAM_FLOATS; ++component ) { + streamFloats[component] = LittleFloat( streamFloats[component] ); + if ( !R_RenderModelCacheFloatIsFinite( streamFloats[component] ) ) { + // same acceptance as ReadFloat: the caller rejects the whole payload + return false; + } + } + idDrawVert &vert = verts[base + i]; + vert.xyz.Set( streamFloats[0], streamFloats[1], streamFloats[2] ); + vert.st.Set( streamFloats[3], streamFloats[4] ); + vert.normal.Set( streamFloats[5], streamFloats[6], streamFloats[7] ); + vert.tangents[0].Set( streamFloats[8], streamFloats[9], streamFloats[10] ); + vert.tangents[1].Set( streamFloats[11], streamFloats[12], streamFloats[13] ); + memcpy( vert.color, record + sizeof( streamFloats ), sizeof( vert.color ) ); + memcpy( vert.color2, record + sizeof( streamFloats ) + sizeof( vert.color ), sizeof( vert.color2 ) ); + } + } + return true; + } + + static bool R_ModelCacheWriteTri( idRenderModelCacheWriter &writer, const srfTriangles_t &tri ) { + const bool hasDerivedTopology = tri.silIndexes != NULL || tri.numMirroredVerts > 0 + || tri.numDupVerts > 0 || tri.numSilEdges > 0 || tri.dominantTris != NULL; + if ( !R_ModelCacheBoundsCanWrite( tri.bounds ) + || tri.gpuSkinningBindPoseVerts != NULL || tri.gpuSkinningVerts != NULL + || tri.numGpuSkinningVerts != 0 || tri.gpuSkinningJointPalette != NULL + || tri.gpuSkinningJointPaletteAlloc != NULL || tri.numGpuSkinningJoints != 0 + || tri.numGpuSkinningJointPaletteAllocJoints != 0 + || tri.gpuSkinningPaletteGeneration != 0 || tri.gpuSkinningFallbackReason != 0 + || tri.gpuSkinningSignedWeights + || tri.surfaceFlags < 0 || ( tri.surfaceFlags & ~STF_SOFT_PARTICLE_CANDIDATE ) != 0 + || tri.numVerts < 0 || tri.numVerts > MODEL_CACHE_MAX_SURFACE_VERTS + || ( tri.numVerts > 0 && tri.verts == NULL ) + || tri.numIndexes < 0 || tri.numIndexes > MODEL_CACHE_MAX_SURFACE_INDEXES + || ( tri.numIndexes % 3 ) != 0 || tri.deformedSurface + || tri.numMirroredVerts < 0 || tri.numMirroredVerts > tri.numVerts + || tri.numDupVerts < 0 || tri.numDupVerts > tri.numVerts + || tri.numSilEdges < 0 || tri.numSilEdges > MODEL_CACHE_MAX_SIL_EDGES + || tri.numShadowIndexesNoFrontCaps < 0 || tri.numShadowIndexesNoFrontCaps > tri.numIndexes + || tri.numShadowIndexesNoCaps < 0 || tri.numShadowIndexesNoCaps > tri.numShadowIndexesNoFrontCaps + || tri.shadowCapPlaneBits < 0 || tri.shadowCapPlaneBits > 127 + || ( hasDerivedTopology && ( tri.numVerts > MODEL_CACHE_MAX_MATERIALIZED_VERTS + || tri.numIndexes > MODEL_CACHE_MAX_MATERIALIZED_INDEXES ) ) ) { + return false; + } + + if ( !writer.WriteBounds( tri.bounds ) || !writer.WriteInt( tri.surfaceFlags ) + || !writer.WriteBool( tri.generateNormals ) || !writer.WriteBool( tri.tangentsCalculated ) + || !writer.WriteBool( tri.facePlanesCalculated ) || !writer.WriteBool( tri.perfectHull ) + || !writer.WriteBool( tri.deformedSurface ) || !writer.WriteInt( tri.numVerts ) + || !writer.WriteBool( tri.verts != NULL ) ) { + return false; + } + if ( tri.verts != NULL ) { + if ( !R_ModelCacheWriteDrawVertArray( writer, tri.verts, tri.numVerts ) ) { + return false; + } + } + + if ( !writer.WriteInt( tri.numIndexes ) || !writer.WriteBool( tri.indexes != NULL ) ) { + return false; + } + if ( tri.indexes != NULL ) { + for ( int i = 0; i < tri.numIndexes; ++i ) { + if ( tri.indexes[i] < 0 || tri.indexes[i] >= tri.numVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( tri.indexes, tri.numIndexes ) ) { + return false; + } + } else if ( tri.numIndexes != 0 ) { + return false; + } + + if ( !writer.WriteBool( tri.silIndexes != NULL ) ) { + return false; + } + if ( tri.silIndexes != NULL ) { + for ( int i = 0; i < tri.numIndexes; ++i ) { + if ( tri.silIndexes[i] < 0 || tri.silIndexes[i] >= tri.numVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( tri.silIndexes, tri.numIndexes ) ) { + return false; + } + } + + if ( !writer.WriteInt( tri.numMirroredVerts ) ) { + return false; + } + const int sourceVertCount = tri.numVerts - tri.numMirroredVerts; + for ( int i = 0; i < tri.numMirroredVerts; ++i ) { + if ( tri.mirroredVerts == NULL || tri.mirroredVerts[i] < 0 || tri.mirroredVerts[i] >= sourceVertCount ) { + return false; + } + } + if ( !writer.WriteIntArray( tri.mirroredVerts, tri.numMirroredVerts ) ) { + return false; + } + + if ( !writer.WriteInt( tri.numDupVerts ) ) { + return false; + } + for ( int i = 0; i < tri.numDupVerts * 2; ++i ) { + if ( tri.dupVerts == NULL || tri.dupVerts[i] < 0 || tri.dupVerts[i] >= tri.numVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( tri.dupVerts, tri.numDupVerts * 2 ) ) { + return false; + } + + if ( !writer.WriteInt( tri.numSilEdges ) ) { + return false; + } + if ( tri.numSilEdges > 0 && tri.silEdges == NULL ) { + return false; + } + const int numPlanes = tri.numIndexes / 3; + for ( int i = 0; i < tri.numSilEdges; ++i ) { + const silEdge_t &edge = tri.silEdges[i]; + if ( edge.p1 < 0 || edge.p1 >= numPlanes + || edge.p2 < 0 || edge.p2 > numPlanes || edge.v1 < 0 || edge.v1 >= tri.numVerts + || edge.v2 < 0 || edge.v2 >= tri.numVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( reinterpret_cast( tri.silEdges ), tri.numSilEdges * 4 ) ) { + return false; + } + + if ( !writer.WriteBool( tri.facePlanes != NULL ) ) { + return false; + } + if ( tri.facePlanes != NULL ) { + if ( !writer.WriteFloatArray( reinterpret_cast( tri.facePlanes ), numPlanes * 4 ) ) { + return false; + } + } + + if ( !writer.WriteBool( tri.dominantTris != NULL ) ) { + return false; + } + if ( tri.dominantTris != NULL ) { + for ( int i = 0; i < tri.numVerts; ++i ) { + const dominantTri_t &dominant = tri.dominantTris[i]; + if ( dominant.v2 < 0 || dominant.v2 >= tri.numVerts || dominant.v3 < 0 || dominant.v3 >= tri.numVerts + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[0] ) + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[1] ) + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[2] ) ) { + return false; + } + } + if ( !writer.WriteBytes( tri.dominantTris, tri.numVerts * static_cast( sizeof( dominantTri_t ) ) ) ) { + return false; + } + } + + if ( !writer.WriteInt( tri.numShadowIndexesNoFrontCaps ) || !writer.WriteInt( tri.numShadowIndexesNoCaps ) + || !writer.WriteInt( tri.shadowCapPlaneBits ) || !writer.WriteBool( tri.shadowVertexes != NULL ) ) { + return false; + } + if ( tri.shadowVertexes != NULL ) { + if ( !writer.WriteFloatArray( reinterpret_cast( tri.shadowVertexes ), tri.numVerts * 4 ) ) { + return false; + } + } + + bool hasSilTraceVerts = false; +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + hasSilTraceVerts = tri.silTraceVerts != NULL; +#endif + if ( !writer.WriteBool( hasSilTraceVerts ) ) { + return false; + } +#if defined( _MD5R_SUPPORT ) + if ( hasSilTraceVerts ) { + if ( !writer.WriteFloatArray( reinterpret_cast( tri.silTraceVerts ), tri.numVerts * 4 ) ) { + return false; + } + } +#elif defined( Q4SDK_MD5R ) + if ( hasSilTraceVerts ) { + return false; + } +#endif + + int numSkinToModelTransforms = 0; +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + numSkinToModelTransforms = tri.numSkinToModelTransforms; +#endif + if ( numSkinToModelTransforms < 0 || numSkinToModelTransforms > MODEL_CACHE_MAX_SKIN_TRANSFORMS + || !writer.WriteInt( numSkinToModelTransforms ) ) { + return false; + } + if ( numSkinToModelTransforms > 0 ) { +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + if ( tri.skinToModelTransforms == NULL ) { + return false; + } + const int floatCount = numSkinToModelTransforms * 16; + if ( !writer.WriteFloatArray( tri.skinToModelTransforms, floatCount ) ) { + return false; + } +#else + return false; +#endif + } + return writer.IsValid(); + } + + static bool R_ModelCacheReadTriStage( idRenderModelCacheReader &reader, renderModelCacheTriStage_t &tri ) { + bool hasVerts = false; + bool hasIndexes = false; + bool hasSilIndexes = false; + bool hasFacePlanes = false; + bool hasDominantTris = false; + bool hasShadowVertexes = false; + bool hasSilTraceVertexes = false; + + if ( !reader.ReadBounds( tri.bounds, true ) || !reader.ReadInt( tri.surfaceFlags ) + || tri.surfaceFlags < 0 || ( tri.surfaceFlags & ~STF_SOFT_PARTICLE_CANDIDATE ) != 0 + || !reader.ReadBool( tri.generateNormals ) || !reader.ReadBool( tri.tangentsCalculated ) + || !reader.ReadBool( tri.facePlanesCalculated ) || !reader.ReadBool( tri.perfectHull ) + || !reader.ReadBool( tri.deformedSurface ) || tri.deformedSurface + || !reader.ReadCount( tri.numVerts, MODEL_CACHE_MAX_SURFACE_VERTS ) || !reader.ReadBool( hasVerts ) + || ( tri.numVerts > 0 && !hasVerts ) ) { + return false; + } + if ( hasVerts ) { + if ( !reader.Reserve( tri.numVerts, sizeof( idDrawVert ) ) ) { + return false; + } + tri.verts.SetNum( tri.numVerts ); + if ( !R_ModelCacheReadDrawVertArray( reader, tri.verts.Ptr(), tri.numVerts ) ) { + return false; + } + } + + if ( !reader.ReadCount( tri.numIndexes, MODEL_CACHE_MAX_SURFACE_INDEXES ) + || ( tri.numIndexes % 3 ) != 0 || !reader.ReadBool( hasIndexes ) + || ( tri.numIndexes > 0 && !hasIndexes ) ) { + return false; + } + if ( hasIndexes ) { + if ( !reader.Reserve( tri.numIndexes, sizeof( glIndex_t ) ) ) { + return false; + } + tri.indexes.SetNum( tri.numIndexes ); + if ( !reader.ReadIntArray( tri.indexes.Ptr(), tri.numIndexes ) ) { + return false; + } + for ( int i = 0; i < tri.numIndexes; ++i ) { + if ( tri.indexes[i] < 0 || tri.indexes[i] >= tri.numVerts ) { + return false; + } + } + } + + if ( !reader.ReadBool( hasSilIndexes ) ) { + return false; + } + if ( hasSilIndexes ) { + if ( !hasVerts || !reader.Reserve( tri.numIndexes, sizeof( glIndex_t ) ) ) { + return false; + } + tri.silIndexes.SetNum( tri.numIndexes ); + if ( !reader.ReadIntArray( tri.silIndexes.Ptr(), tri.numIndexes ) ) { + return false; + } + for ( int i = 0; i < tri.numIndexes; ++i ) { + if ( tri.silIndexes[i] < 0 || tri.silIndexes[i] >= tri.numVerts ) { + return false; + } + } + } + + int mirroredCount = 0; + if ( !reader.ReadCount( mirroredCount, tri.numVerts, sizeof( int ) ) ) { + return false; + } + tri.mirroredVerts.SetNum( mirroredCount ); + if ( !reader.ReadIntArray( tri.mirroredVerts.Ptr(), mirroredCount ) ) { + return false; + } + const int sourceVertCount = tri.numVerts - mirroredCount; + for ( int i = 0; i < mirroredCount; ++i ) { + if ( tri.mirroredVerts[i] < 0 || tri.mirroredVerts[i] >= sourceVertCount ) { + return false; + } + } + + int dupCount = 0; + if ( !reader.ReadCount( dupCount, tri.numVerts ) || dupCount > 0x3fffffff + || !reader.Reserve( dupCount * 2, sizeof( int ) ) ) { + return false; + } + tri.dupVerts.SetNum( dupCount * 2 ); + if ( !reader.ReadIntArray( tri.dupVerts.Ptr(), tri.dupVerts.Num() ) ) { + return false; + } + for ( int i = 0; i < tri.dupVerts.Num(); ++i ) { + if ( tri.dupVerts[i] < 0 || tri.dupVerts[i] >= tri.numVerts ) { + return false; + } + } + + int silEdgeCount = 0; + if ( !reader.ReadCount( silEdgeCount, MODEL_CACHE_MAX_SIL_EDGES, sizeof( silEdge_t ) ) ) { + return false; + } + tri.silEdges.SetNum( silEdgeCount ); + if ( !reader.ReadIntArray( reinterpret_cast( tri.silEdges.Ptr() ), silEdgeCount * 4 ) ) { + return false; + } + const int numPlanes = tri.numIndexes / 3; + for ( int i = 0; i < silEdgeCount; ++i ) { + const silEdge_t &edge = tri.silEdges[i]; + if ( edge.p1 < 0 || edge.p1 >= numPlanes || edge.p2 < 0 || edge.p2 > numPlanes + || edge.v1 < 0 || edge.v1 >= tri.numVerts || edge.v2 < 0 || edge.v2 >= tri.numVerts ) { + return false; + } + } + + if ( !reader.ReadBool( hasFacePlanes ) ) { + return false; + } + if ( hasFacePlanes ) { + if ( !reader.Reserve( numPlanes, sizeof( idPlane ) ) ) { + return false; + } + tri.facePlanes.SetNum( numPlanes ); + if ( !reader.ReadFloatArray( reinterpret_cast( tri.facePlanes.Ptr() ), numPlanes * 4 ) ) { + return false; + } + } + + if ( !reader.ReadBool( hasDominantTris ) ) { + return false; + } + if ( hasDominantTris ) { + if ( !hasVerts || !reader.Reserve( tri.numVerts, sizeof( dominantTri_t ) ) ) { + return false; + } + tri.dominantTris.SetNum( tri.numVerts ); + if ( !reader.ReadBytes( tri.dominantTris.Ptr(), tri.numVerts * static_cast( sizeof( dominantTri_t ) ) ) ) { + return false; + } + for ( int i = 0; i < tri.numVerts; ++i ) { + dominantTri_t &dominant = tri.dominantTris[i]; + dominant.v2 = LittleLong( dominant.v2 ); + dominant.v3 = LittleLong( dominant.v3 ); + if ( dominant.v2 < 0 || dominant.v2 >= tri.numVerts || dominant.v3 < 0 || dominant.v3 >= tri.numVerts ) { + return false; + } + for ( int scale = 0; scale < 3; ++scale ) { + dominant.normalizationScale[scale] = LittleFloat( dominant.normalizationScale[scale] ); + if ( !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[scale] ) ) { + return false; + } + } + } + } + + if ( !reader.ReadInt( tri.numShadowIndexesNoFrontCaps ) || tri.numShadowIndexesNoFrontCaps < 0 + || tri.numShadowIndexesNoFrontCaps > tri.numIndexes || !reader.ReadInt( tri.numShadowIndexesNoCaps ) + || tri.numShadowIndexesNoCaps < 0 || tri.numShadowIndexesNoCaps > tri.numShadowIndexesNoFrontCaps + || !reader.ReadInt( tri.shadowCapPlaneBits ) || tri.shadowCapPlaneBits < 0 || tri.shadowCapPlaneBits > 127 + || !reader.ReadBool( hasShadowVertexes ) ) { + return false; + } + if ( hasShadowVertexes ) { + if ( !reader.Reserve( tri.numVerts, sizeof( idVec4 ) ) ) { + return false; + } + tri.shadowVertexes.SetNum( tri.numVerts ); + if ( !reader.ReadFloatArray( reinterpret_cast( tri.shadowVertexes.Ptr() ), tri.numVerts * 4 ) ) { + return false; + } + } + + if ( !reader.ReadBool( hasSilTraceVertexes ) ) { + return false; + } + if ( hasSilTraceVertexes ) { + if ( !reader.Reserve( tri.numVerts, sizeof( idVec4 ) ) ) { + return false; + } + tri.silTraceVertexes.SetNum( tri.numVerts ); + if ( !reader.ReadFloatArray( reinterpret_cast( tri.silTraceVertexes.Ptr() ), tri.numVerts * 4 ) ) { + return false; + } + } + + if ( !reader.ReadCount( tri.numSkinToModelTransforms, MODEL_CACHE_MAX_SKIN_TRANSFORMS ) + || tri.numSkinToModelTransforms > 0x07ffffff + || !reader.Reserve( tri.numSkinToModelTransforms * 16, sizeof( float ) ) ) { + return false; + } + tri.skinToModelTransforms.SetNum( tri.numSkinToModelTransforms * 16 ); + if ( !reader.ReadFloatArray( tri.skinToModelTransforms.Ptr(), tri.skinToModelTransforms.Num() ) ) { + return false; + } + + const bool hasDerivedTopology = hasSilIndexes || mirroredCount > 0 || dupCount > 0 || silEdgeCount > 0 || hasDominantTris; + if ( hasDerivedTopology && ( !hasVerts || !hasIndexes || tri.numVerts <= 0 || tri.numIndexes <= 0 ) ) { + return false; + } + return reader.IsValid(); + } + + static bool R_ModelCacheMaterializeTri( const renderModelCacheTriStage_t &source, srfTriangles_t *&outTri ) { + outTri = NULL; + srfTriangles_t *tri = R_AllocStaticTriSurf(); + if ( tri == NULL ) { + return false; + } + + tri->bounds = source.bounds; + tri->surfaceFlags = source.surfaceFlags; + tri->generateNormals = source.generateNormals; + tri->tangentsCalculated = source.tangentsCalculated; + tri->facePlanesCalculated = source.facePlanesCalculated; + tri->perfectHull = source.perfectHull; + tri->deformedSurface = false; + tri->numVerts = source.numVerts; + tri->numIndexes = source.numIndexes; + + if ( source.verts.Num() > 0 ) { + R_AllocStaticTriSurfVerts( tri, source.numVerts ); + memcpy( tri->verts, source.verts.Ptr(), source.numVerts * sizeof( tri->verts[0] ) ); + } + if ( source.indexes.Num() > 0 ) { + R_AllocStaticTriSurfIndexes( tri, source.numIndexes ); + memcpy( tri->indexes, source.indexes.Ptr(), source.numIndexes * sizeof( tri->indexes[0] ) ); + } + + const bool hasDerivedTopology = source.silIndexes.Num() > 0 || source.mirroredVerts.Num() > 0 + || source.dupVerts.Num() > 0 || source.silEdges.Num() > 0 || source.dominantTris.Num() > 0; + if ( hasDerivedTopology ) { + if ( source.silIndexes.Num() != source.numIndexes + || source.numVerts > MODEL_CACHE_MAX_MATERIALIZED_VERTS + || source.numIndexes > MODEL_CACHE_MAX_MATERIALIZED_INDEXES ) { + R_FreeStaticTriSurf( tri ); + return false; + } + + // R_ModelCacheReadTriStage fully range-validated the derived topology + // (and numVerts == sourceVertCount + mirroredVerts.Num() by construction); + // allocate the arrays directly instead of re-deriving them just to + // overwrite the results with the cached contents. + const int sourceVertCount = source.numVerts - source.mirroredVerts.Num(); + deformInfo_t *deform = R_AllocDeformInfo( sourceVertCount, source.numVerts, + source.numIndexes, source.mirroredVerts.Num(), source.dupVerts.Num() / 2, + source.silEdges.Num(), source.dominantTris.Num() > 0 ); + if ( deform == NULL ) { + R_FreeStaticTriSurf( tri ); + return false; + } + + tri->silIndexes = deform->silIndexes; + deform->silIndexes = NULL; + memcpy( tri->silIndexes, source.silIndexes.Ptr(), source.numIndexes * sizeof( tri->silIndexes[0] ) ); + + tri->numMirroredVerts = deform->numMirroredVerts; + tri->mirroredVerts = deform->mirroredVerts; + deform->mirroredVerts = NULL; + if ( tri->numMirroredVerts > 0 ) { + memcpy( tri->mirroredVerts, source.mirroredVerts.Ptr(), tri->numMirroredVerts * sizeof( tri->mirroredVerts[0] ) ); + } + + tri->numDupVerts = deform->numDupVerts; + tri->dupVerts = deform->dupVerts; + deform->dupVerts = NULL; + if ( tri->numDupVerts > 0 ) { + memcpy( tri->dupVerts, source.dupVerts.Ptr(), tri->numDupVerts * 2 * sizeof( tri->dupVerts[0] ) ); + } + + tri->dominantTris = deform->dominantTris; + deform->dominantTris = NULL; + if ( source.dominantTris.Num() > 0 ) { + memcpy( tri->dominantTris, source.dominantTris.Ptr(), source.numVerts * sizeof( tri->dominantTris[0] ) ); + } + + tri->numSilEdges = deform->numSilEdges; + tri->silEdges = deform->silEdges; + deform->silEdges = NULL; + R_FreeDeformInfo( deform ); + if ( tri->numSilEdges > 0 ) { + memcpy( tri->silEdges, source.silEdges.Ptr(), tri->numSilEdges * sizeof( tri->silEdges[0] ) ); + } + } + + if ( source.facePlanes.Num() > 0 ) { + R_AllocStaticTriSurfPlanes( tri, source.numIndexes ); + memcpy( tri->facePlanes, source.facePlanes.Ptr(), source.facePlanes.Num() * sizeof( tri->facePlanes[0] ) ); + } + if ( source.shadowVertexes.Num() > 0 ) { + R_AllocStaticTriSurfShadowVerts( tri, source.numVerts ); + for ( int i = 0; i < source.numVerts; ++i ) { + tri->shadowVertexes[i].xyz = source.shadowVertexes[i]; + } + } + + if ( source.silTraceVertexes.Num() > 0 ) { +#if defined( _MD5R_SUPPORT ) + R_AllocStaticTriSurfSilTraceVerts( tri, source.numVerts ); + for ( int i = 0; i < source.numVerts; ++i ) { + tri->silTraceVerts[i].xyzw = source.silTraceVertexes[i]; + } +#else + R_FreeStaticTriSurf( tri ); + return false; +#endif + } + if ( source.numSkinToModelTransforms > 0 ) { +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + R_AllocStaticSkinToModelTransforms( tri, source.numSkinToModelTransforms ); + memcpy( tri->skinToModelTransforms, source.skinToModelTransforms.Ptr(), + source.skinToModelTransforms.Num() * sizeof( source.skinToModelTransforms[0] ) ); +#else + R_FreeStaticTriSurf( tri ); + return false; +#endif + } + + tri->numShadowIndexesNoFrontCaps = source.numShadowIndexesNoFrontCaps; + tri->numShadowIndexesNoCaps = source.numShadowIndexesNoCaps; + tri->shadowCapPlaneBits = source.shadowCapPlaneBits; + tri->ambientViewCount = 0; + tri->ambientSurface = NULL; + tri->nextDeferredFree = NULL; + tri->indexCache = NULL; + tri->ambientCache = NULL; + tri->lightingCache = NULL; + tri->shadowCache = NULL; + outTri = tri; + return true; + } +} + /* ================ idRenderModel::BoundsFromJoints @@ -158,6 +1246,173 @@ idRenderModelStatic::~idRenderModelStatic() { PurgeModel(); } +renderModelCacheType_t idRenderModelStatic::LevelLoadCachePayloadType() const { + // Dynamic subclasses share the static model's surface container but have + // additional runtime state that this payload intentionally does not encode. + return IsDynamicModel() == DM_STATIC ? RENDER_MODEL_CACHE_STATIC : RENDER_MODEL_CACHE_UNSUPPORTED; +} + +bool idRenderModelStatic::WriteLevelLoadCachePayload( idFile &file ) const { + if ( defaulted || purged || !R_ModelCacheBoundsCanWrite( bounds ) + || surfaces.Num() < 0 || surfaces.Num() > MODEL_CACHE_MAX_SURFACES ) { + return false; + } + + idRenderModelCacheWriter writer( file ); + if ( !writer.WriteUnsignedInt( STATIC_MODEL_CACHE_MAGIC ) || !writer.WriteInt( STATIC_MODEL_CACHE_VERSION ) + || !writer.WriteString( name.c_str(), MODEL_CACHE_MAX_NAME ) + || !writer.WriteUnsigned64( static_cast( static_cast( timeStamp ) ) ) + || !writer.WriteBounds( bounds ) || !writer.WriteBool( isStaticWorldModel ) + || !writer.WriteBool( defaulted ) || !writer.WriteBool( purged ) || !writer.WriteBool( fastLoad ) + || !writer.WriteBool( reloadable ) || !writer.WriteBool( procSky ) + || !writer.WriteInt( surfaces.Num() ) ) { + return false; + } + + for ( int i = 0; i < surfaces.Num(); ++i ) { + const modelSurface_t &surface = surfaces[i]; + const char *materialName = ( surface.shader != NULL && surface.shader->GetName() != NULL ) + ? surface.shader->GetName() : ""; + if ( !writer.WriteInt( surface.id ) || !writer.WriteString( materialName, MODEL_CACHE_MAX_MATERIAL ) + || !writer.WriteBool( surface.shader != NULL && surface.shader->UseUnsmoothedTangents() ) + || !writer.WriteBool( surface.shader != NULL && surface.shader->ShouldCreateBackSides() ) + || !writer.WriteBool( surface.shader != NULL && surface.shader->SurfaceCastsShadow() ) + || !writer.WriteBool( surface.shader != NULL && surface.shader->IsDiscrete() ) + || !writer.WriteBool( surface.geometry != NULL ) ) { + return false; + } + if ( surface.geometry != NULL && !R_ModelCacheWriteTri( writer, *surface.geometry ) ) { + return false; + } + } + return writer.IsValid(); +} + +bool idRenderModelStatic::ReadLevelLoadCachePayload( idFile &file ) { + idRenderModelCacheReader reader( file ); + unsigned int magic = 0; + int version = 0; + idStr decodedName; + uint64_t encodedTimestamp = 0; + idBounds decodedBounds; + bool decodedStaticWorld = false; + bool decodedDefaulted = false; + bool decodedPurged = false; + bool decodedFastLoad = false; + bool decodedReloadable = false; + bool decodedProcSky = false; + int surfaceCount = 0; + + if ( !reader.ReadUnsignedInt( magic ) || magic != STATIC_MODEL_CACHE_MAGIC + || !reader.ReadInt( version ) || version != STATIC_MODEL_CACHE_VERSION + || !reader.ReadString( decodedName, MODEL_CACHE_MAX_NAME ) || decodedName.IsEmpty() + || !reader.ReadUnsigned64( encodedTimestamp ) || !reader.ReadBounds( decodedBounds, true ) + || !reader.ReadBool( decodedStaticWorld ) || !reader.ReadBool( decodedDefaulted ) + || !reader.ReadBool( decodedPurged ) || !reader.ReadBool( decodedFastLoad ) + || !reader.ReadBool( decodedReloadable ) || !reader.ReadBool( decodedProcSky ) + || decodedDefaulted || decodedPurged + || !reader.ReadCount( surfaceCount, MODEL_CACHE_MAX_SURFACES, sizeof( renderModelCacheSurfaceStage_t ) ) ) { + return false; + } + + idList decodedSurfaces; + decodedSurfaces.SetNum( surfaceCount ); + for ( int i = 0; i < surfaceCount; ++i ) { + renderModelCacheSurfaceStage_t &surface = decodedSurfaces[i]; + if ( !reader.ReadInt( surface.id ) || !reader.ReadString( surface.materialName, MODEL_CACHE_MAX_MATERIAL ) + || !reader.ReadBool( surface.materialUnsmoothedTangents ) + || !reader.ReadBool( surface.materialCreatesBackSides ) + || !reader.ReadBool( surface.materialCastsShadow ) + || !reader.ReadBool( surface.materialDiscrete ) + || !reader.ReadBool( surface.hasGeometry ) + || ( surface.materialName.IsEmpty() && ( surface.materialUnsmoothedTangents + || surface.materialCreatesBackSides || surface.materialCastsShadow || surface.materialDiscrete ) ) ) { + return false; + } + if ( surface.hasGeometry && ( surface.materialName.IsEmpty() + || !R_ModelCacheReadTriStage( reader, surface.geometry ) ) ) { + return false; + } + } + if ( !reader.IsValid() ) { + return false; + } + + const ID_TIME_T decodedTimestamp = static_cast( static_cast( encodedTimestamp ) ); + if ( static_cast( static_cast( decodedTimestamp ) ) != encodedTimestamp ) { + return false; + } + + idRenderModelStatic staged; + staged.name = decodedName; + staged.timeStamp = decodedTimestamp; + staged.bounds = decodedBounds; + staged.isStaticWorldModel = decodedStaticWorld; + staged.defaulted = false; + staged.purged = false; + staged.fastLoad = decodedFastLoad; + staged.reloadable = decodedReloadable; + staged.procSky = decodedProcSky; + staged.levelLoadReferenced = false; + staged.overlaysAdded = 0; + staged.lastModifiedFrame = 0; + staged.lastArchivedFrame = 0; + staged.shadowHull = NULL; + staged.surfaces.SetNum( surfaceCount ); + for ( int i = 0; i < surfaceCount; ++i ) { + staged.surfaces[i].id = 0; + staged.surfaces[i].shader = NULL; + staged.surfaces[i].geometry = NULL; + staged.surfaces[i].mOriginalSurfaceName = NULL; + } + + for ( int i = 0; i < surfaceCount; ++i ) { + const renderModelCacheSurfaceStage_t &sourceSurface = decodedSurfaces[i]; + modelSurface_t &surface = staged.surfaces[i]; + surface.id = sourceSurface.id; + surface.shader = sourceSurface.materialName.IsEmpty() + ? NULL : declManager->FindMaterial( sourceSurface.materialName.c_str() ); + surface.geometry = NULL; + surface.mOriginalSurfaceName = NULL; + if ( !sourceSurface.materialName.IsEmpty() && ( surface.shader == NULL + || idStr::Icmp( surface.shader->GetName(), sourceSurface.materialName.c_str() ) != 0 + || surface.shader->UseUnsmoothedTangents() != sourceSurface.materialUnsmoothedTangents + || surface.shader->ShouldCreateBackSides() != sourceSurface.materialCreatesBackSides + || surface.shader->SurfaceCastsShadow() != sourceSurface.materialCastsShadow + || surface.shader->IsDiscrete() != sourceSurface.materialDiscrete ) ) { + return false; + } + if ( sourceSurface.hasGeometry && ( surface.shader == NULL + || ( !decodedFastLoad && sourceSurface.geometry.verts.Num() > 0 + && sourceSurface.materialUnsmoothedTangents + != ( sourceSurface.geometry.dominantTris.Num() > 0 ) ) + || !R_ModelCacheMaterializeTri( sourceSurface.geometry, surface.geometry ) ) ) { + return false; + } + } + + SwapLevelLoadCacheState( staged ); + return true; +} + +void idRenderModelStatic::SwapLevelLoadCacheState( idRenderModelStatic &other ) { + surfaces.Swap( other.surfaces ); + idSwap( bounds, other.bounds ); + idSwap( overlaysAdded, other.overlaysAdded ); + idSwap( lastModifiedFrame, other.lastModifiedFrame ); + idSwap( lastArchivedFrame, other.lastArchivedFrame ); + idSwap( name, other.name ); + idSwap( shadowHull, other.shadowHull ); + idSwap( isStaticWorldModel, other.isStaticWorldModel ); + idSwap( defaulted, other.defaulted ); + idSwap( purged, other.purged ); + idSwap( fastLoad, other.fastLoad ); + idSwap( reloadable, other.reloadable ); + idSwap( procSky, other.procSky ); + idSwap( levelLoadReferenced, other.levelLoadReferenced ); + idSwap( timeStamp, other.timeStamp ); +} + /* ============== idRenderModelStatic::Print @@ -365,8 +1620,27 @@ idRenderModelStatic::InitFromFile void idRenderModelStatic::InitFromFile( const char *fileName ) { bool loaded; idStr extension; + const bool requestedFastLoad = fastLoad; InitEmpty( fileName ); + char cacheSettingsBuffer[1024]; + idStr::snPrintf( cacheSettingsBuffer, sizeof( cacheSettingsBuffer ), + "static-payload=2;merge=%s;slopv=%s;slopt=%s;slopn=%s;silremap=%s;binary=%s;force-md5r=%s;convert-static=%s;fast=%d", + r_mergeModelSurfaces.GetString(), r_slopVertex.GetString(), r_slopTexCoord.GetString(), + r_slopNormal.GetString(), cvarSystem->GetCVarString( "r_useSilRemap" ), + cvarSystem->GetCVarString( "com_binaryRead" ), cvarSystem->GetCVarString( "r_forceConvertMD5R" ), + cvarSystem->GetCVarString( "r_convertStaticToMD5R" ), requestedFastLoad ? 1 : 0 ); + const idStr cacheSettings = cacheSettingsBuffer; + if ( R_TryReadGeneratedRenderModelCache( *this, fileName, + STATIC_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ) ) { + if ( name.Icmp( fileName ) == 0 && fastLoad == requestedFastLoad ) { + return; + } + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, fileName, + STATIC_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); + InitEmpty( fileName ); + fastLoad = requestedFastLoad; + } // FIXME: load new .proc map format @@ -400,6 +1674,8 @@ void idRenderModelStatic::InitFromFile( const char *fileName ) { // create the bounds for culling and dynamic surface creation FinishSurfaces(); + R_WriteGeneratedRenderModelCache( *this, fileName, + STATIC_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); } /* @@ -1309,6 +2585,57 @@ bool idRenderModelStatic::ConvertLWOToModelSurfaces( const struct st_lwObject *l } } + // bucket the polygons by surface up front so each surface pass only visits + // its own polygons instead of rescanning the whole layer + idList surfOrder; + for ( lwoSurf = lwo->surf; lwoSurf; lwoSurf = lwoSurf->next ) { + surfOrder.Append( lwoSurf ); + } + idList surfPolyCount; + surfPolyCount.SetNum( surfOrder.Num() ); + memset( surfPolyCount.Ptr(), 0, surfOrder.Num() * sizeof( surfPolyCount[0] ) ); + idList polySurfOrdinal; + polySurfOrdinal.SetNum( layer->polygon.count ); + int lastOrdinal = -1; + for ( j = 0; j < layer->polygon.count; j++ ) { + const lwSurface *polySurf = layer->polygon.pol[j].surf; + int ordinal = -1; + if ( lastOrdinal >= 0 && surfOrder[lastOrdinal] == polySurf ) { + // polygons are typically grouped by surface + ordinal = lastOrdinal; + } else { + for ( k = 0; k < surfOrder.Num(); k++ ) { + if ( surfOrder[k] == polySurf ) { + ordinal = k; + break; + } + } + } + // unmatched polygons keep ordinal -1 and are dropped, exactly as the + // old all-surfaces scan skipped them + polySurfOrdinal[j] = ordinal; + if ( ordinal >= 0 ) { + surfPolyCount[ordinal]++; + lastOrdinal = ordinal; + } + } + idList surfPolyStart; + surfPolyStart.SetNum( surfOrder.Num() ); + int polyStart = 0; + for ( k = 0; k < surfOrder.Num(); k++ ) { + surfPolyStart[k] = polyStart; + polyStart += surfPolyCount[k]; + } + idList surfPolyIndexes; + surfPolyIndexes.SetNum( polyStart ); + idList surfPolyFill( surfPolyStart ); + for ( j = 0; j < layer->polygon.count; j++ ) { + // ascending j order preserves the original within-surface polygon order + if ( polySurfOrdinal[j] >= 0 ) { + surfPolyIndexes[ surfPolyFill[ polySurfOrdinal[j] ]++ ] = j; + } + } + // build the surfaces for ( lwoSurf = lwo->surf, i = 0; lwoSurf; lwoSurf = lwoSurf->next, i++ ) { im1 = declManager->FindMaterial( lwoSurf->name ); @@ -1325,7 +2652,8 @@ bool idRenderModelStatic::ConvertLWOToModelSurfaces( const struct st_lwObject *l // we need to find out how many unique vertex / texcoord combinations there are // the maximum possible number of combined vertexes is the number of indexes - mvTable = (matchVert_t *)R_ClearedStaticAlloc( layer->polygon.count * 3 * sizeof( mvTable[0] ) ); + const int surfTriIndexCount = surfPolyCount[i] * 3; + mvTable = (matchVert_t *)R_ClearedStaticAlloc( surfTriIndexCount * sizeof( mvTable[0] ) ); // we will have a hash chain based on the xyz values mvHash = (matchVert_t **)R_ClearedStaticAlloc( layer->point.count * sizeof( mvHash[0] ) ); @@ -1334,7 +2662,7 @@ bool idRenderModelStatic::ConvertLWOToModelSurfaces( const struct st_lwObject *l tri = R_AllocStaticTriSurf(); tri->numVerts = 0; tri->numIndexes = 0; - R_AllocStaticTriSurfIndexes( tri, layer->polygon.count * 3 ); + R_AllocStaticTriSurfIndexes( tri, surfTriIndexCount > 0 ? surfTriIndexCount : 1 ); tri->generateNormals = !normalsParsed; // find all the unique combinations @@ -1344,11 +2672,12 @@ bool idRenderModelStatic::ConvertLWOToModelSurfaces( const struct st_lwObject *l } else { normalEpsilon = 1.0f - r_slopNormal.GetFloat(); } - for ( j = 0; j < layer->polygon.count; j++ ) { - lwPolygon *poly = &layer->polygon.pol[j]; + const int surfPolyEnd = surfPolyStart[i] + surfPolyCount[i]; + for ( int p = surfPolyStart[i]; p < surfPolyEnd; p++ ) { + lwPolygon *poly = &layer->polygon.pol[ surfPolyIndexes[p] ]; if ( poly->surf != lwoSurf ) { - continue; + continue; // defensive: buckets are per surface } if ( poly->nverts != 3 ) { diff --git a/src/renderer/Model.h b/src/renderer/Model.h index 20ec4c74..4d8972ad 100644 --- a/src/renderer/Model.h +++ b/src/renderer/Model.h @@ -22,6 +22,7 @@ #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) class rvMesh; #endif +struct gpuSkinningVertex_s; // RAVEN END // shared between the renderer, game, and Maya export DLL @@ -192,6 +193,19 @@ typedef struct srfTriangles_s { char description[64]; #endif // RAVEN END + + // Immutable model-owned source data plus a snapshot-owned canonical joint + // palette. The allocation pointer is non-NULL only for the owning tri. + const idDrawVert * gpuSkinningBindPoseVerts; + const gpuSkinningVertex_s *gpuSkinningVerts; + int numGpuSkinningVerts; + float * gpuSkinningJointPalette; + float * gpuSkinningJointPaletteAlloc; + int numGpuSkinningJoints; + int numGpuSkinningJointPaletteAllocJoints; + unsigned int gpuSkinningPaletteGeneration; + int gpuSkinningFallbackReason; + bool gpuSkinningSignedWeights; } srfTriangles_t; static const int STF_SOFT_PARTICLE_CANDIDATE = 1 << 0; diff --git a/src/renderer/ModelDecal.cpp b/src/renderer/ModelDecal.cpp index 8be25c03..257a604d 100644 --- a/src/renderer/ModelDecal.cpp +++ b/src/renderer/ModelDecal.cpp @@ -691,6 +691,9 @@ void idRenderModelDecal::AddDecalDrawSurf( viewEntity_t *space ) { // be able to reorganize the index list srfTriangles_t *newTri = (srfTriangles_t *)R_FrameAlloc( sizeof( *newTri ) ); *newTri = tri; + newTri->gpuSkinningJointPaletteAlloc = NULL; + newTri->numGpuSkinningJointPaletteAllocJoints = 0; + R_ClearStaticGpuSkinningJointPalette( newTri ); // Snapshot the vertices plus any per-stage color blocks into one transient // upload so the draw surf matches Quake 4's frame-temp decal submission. diff --git a/src/renderer/ModelOverlay.cpp b/src/renderer/ModelOverlay.cpp index 4aa4a13b..4b8e80be 100644 --- a/src/renderer/ModelOverlay.cpp +++ b/src/renderer/ModelOverlay.cpp @@ -411,6 +411,7 @@ void idRenderModelOverlay::AddOverlaySurfacesToModel( idRenderModel *baseModel ) } newTri = newSurf->geometry; + R_GpuSkinning_ClearSurfaceContract( newTri, GPU_SKINNING_FALLBACK_NONE ); numVerts = numIndexes = 0; for ( i = 0; i < materials[k]->surfaces.Num(); i++ ) { diff --git a/src/renderer/Model_beam.cpp b/src/renderer/Model_beam.cpp index e1658e08..9cce862a 100644 --- a/src/renderer/Model_beam.cpp +++ b/src/renderer/Model_beam.cpp @@ -69,7 +69,10 @@ idRenderModel *idRenderModelBeam::InstantiateDynamicModel( const struct renderEn srfTriangles_t *tri; modelSurface_t surf; - if ( cachedModel ) { + // Match the sprite/md5 dynamic models: keep the cached snapshot for reuse + // unless caching is disabled. Deleting it unconditionally made the reuse + // branch below dead code and rebuilt the whole static model every view. + if ( cachedModel && !r_useCachedDynamicModels.GetBool() ) { delete cachedModel; cachedModel = NULL; } @@ -181,6 +184,12 @@ idRenderModel *idRenderModelBeam::InstantiateDynamicModel( const struct renderEn R_BoundTriSurf( tri ); + // The beam verts are recomputed every view from the camera, but the reused + // snapshot keeps its persistent static ambientCache, which R_CreateAmbientCache + // short-circuits on. Free the vertex caches so the rewritten verts re-upload; + // a no-op on a freshly allocated tri. + R_FreeStaticTriSurfVertexCaches( tri ); + staticModel->bounds = tri->bounds; return staticModel; diff --git a/src/renderer/Model_local.h b/src/renderer/Model_local.h index 3232e08d..b82e7acb 100644 --- a/src/renderer/Model_local.h +++ b/src/renderer/Model_local.h @@ -29,6 +29,17 @@ If you have questions concerning this license or the applicable additional terms #ifndef __MODEL_LOCAL_H__ #define __MODEL_LOCAL_H__ +#include "GpuSkinning.h" + +// Cache payload capabilities belong to renderer-owned concrete models. Keep +// them out of the public idRenderModel ABI shared with game modules. +typedef enum { + RENDER_MODEL_CACHE_UNSUPPORTED = 0, + RENDER_MODEL_CACHE_STATIC = 1, + RENDER_MODEL_CACHE_MD5 = 2, + RENDER_MODEL_CACHE_MD5R = 3 +} renderModelCacheType_t; + /* =============================================================================== @@ -37,6 +48,70 @@ If you have questions concerning this license or the applicable additional terms =============================================================================== */ +// Checked primitive I/O shared by the concrete render-model cache codecs. It +// deliberately avoids idFile::ReadString because cache strings must be capped +// before allocating their backing storage. +class idRenderModelCacheReader { +public: + idRenderModelCacheReader( idFile &source ); + + bool ReadBytes( void *data, int length ); + bool ReadInt( int &value ); + bool ReadUnsignedInt( unsigned int &value ); + bool ReadUnsigned64( uint64_t &value ); + bool ReadByte( byte &value ); + bool ReadBool( bool &value ); + bool ReadFloat( float &value ); + bool ReadVec2( idVec2 &value ); + bool ReadVec3( idVec3 &value ); + bool ReadVec4( idVec4 &value ); + bool ReadFloatArray( float *values, int count ); + bool ReadIntArray( int *values, int count ); + bool ReadBounds( idBounds &value, bool allowCleared = false ); + bool ReadString( idStr &value, int maxLength ); + bool ReadCount( int &value, int maxCount, size_t elementSize = 0 ); + bool Reserve( int count, size_t elementSize ); + bool IsValid() const { return valid; } + +private: + idFile & file; + uint64_t transferBytes; + uint64_t allocationBytes; + bool valid; +}; + +class idRenderModelCacheWriter { +public: + idRenderModelCacheWriter( idFile &destination ); + + bool WriteBytes( const void *data, int length ); + bool WriteInt( int value ); + bool WriteUnsignedInt( unsigned int value ); + bool WriteUnsigned64( uint64_t value ); + bool WriteByte( byte value ); + bool WriteBool( bool value ); + bool WriteFloat( float value ); + bool WriteVec2( const idVec2 &value ); + bool WriteVec3( const idVec3 &value ); + bool WriteVec4( const idVec4 &value ); + bool WriteFloatArray( const float *values, int count ); + bool WriteIntArray( const int *values, int count ); + bool WriteBounds( const idBounds &value ); + bool WriteString( const char *value, int maxLength ); + bool IsValid() const { return valid; } + +private: + idFile & file; + uint64_t transferBytes; + bool valid; +}; + +bool R_RenderModelCacheFloatIsFinite( float value ); +bool R_TryReadGeneratedRenderModelCache( idRenderModel &model, const char *sourcePath, + unsigned int parserVersion, const char *settingsKey ); +void R_WriteGeneratedRenderModelCache( const idRenderModel &model, const char *sourcePath, + unsigned int parserVersion, const char *settingsKey ); + class idRenderModelStatic : public idRenderModel { public: // the inherited public interface @@ -85,6 +160,9 @@ class idRenderModelStatic : public idRenderModel { virtual void SetBounds( const idBounds &newBounds ) { bounds = newBounds; } virtual void ReadFromDemoFile( class idDemoFile *f ); virtual void WriteToDemoFile( class idDemoFile *f ); + virtual renderModelCacheType_t LevelLoadCachePayloadType() const; + virtual bool WriteLevelLoadCachePayload( idFile &file ) const; + virtual bool ReadLevelLoadCachePayload( idFile &file ); virtual float DepthHack() const; virtual int GetSurfaceMask( const char *surface ) const; @@ -113,6 +191,8 @@ class idRenderModelStatic : public idRenderModel { int overlaysAdded; protected: + void SwapLevelLoadCacheState( idRenderModelStatic &other ); + int lastModifiedFrame; int lastArchivedFrame; @@ -150,7 +230,7 @@ class idMD5Mesh { ~idMD5Mesh(); void ParseMesh( idLexer &parser, int numJoints, const idJointMat *joints ); - void UpdateSurface( const struct renderEntity_s *ent, const idJointMat *joints, modelSurface_t *surf, bool calculateTangents = true ); + void UpdateSurface( const struct renderEntity_s *ent, const idJointMat *joints, modelSurface_t *surf, bool calculateTangents = true, bool allowGpuSkinning = true ); idBounds CalcBounds( const idJointMat *joints ); int NearestJoint( int a, int b, int c ) const; int NumVerts( void ) const; @@ -170,8 +250,13 @@ class idMD5Mesh { struct deformInfo_s * deformInfo; // used to create srfTriangles_t from base frames and new vertexes int surfaceNum; // number of the static surface created for this mesh float currentTime; // animation LOD timer + idList gpuBindPoseVerts; // immutable output-vertex bind pose + idList gpuSkinningVerts; // dedicated four-weight stream + int gpuSkinningNumJoints; + gpuSkinningFallbackReason_t gpuSkinningFallback; bool UpdateLod( const struct renderEntity_s *ent, const struct viewEntity_s *viewEnt, const modelSurface_t *surf ); + void BuildGpuSkinningSidecar( int numJoints ); void TransformVerts( idDrawVert *verts, const idJointMat *joints ); void TransformScaledVerts( idDrawVert *verts, const idJointMat *joints, float scale ); }; @@ -203,6 +288,9 @@ class idRenderModelMD5 : public idRenderModelStatic { virtual const idJointMat * GetSkinSpaceToLocalMats( void ) const; virtual int NearestJoint( int surfaceNum, int a, int b, int c ) const; virtual int GetSurfaceMask( const char *surface ) const; + virtual renderModelCacheType_t LevelLoadCachePayloadType() const; + virtual bool WriteLevelLoadCachePayload( idFile &file ) const; + virtual bool ReadLevelLoadCachePayload( idFile &file ); private: idList joints; @@ -316,7 +404,9 @@ struct rvMD5RMesh { numDrawIndices( 0 ), numDrawPrimitives( 0 ), numTransforms( 0 ), - deformInfo( NULL ) { + deformInfo( NULL ), + gpuSkinningSourceVerts( 0 ), + gpuSkinningFallback( GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES ) { bounds.Clear(); } @@ -344,6 +434,10 @@ struct rvMD5RMesh { idList primBatches; struct deformInfo_s * deformInfo; idList baseDrawVerts; + idList gpuBindPoseVerts; + idList gpuSkinningVerts; + int gpuSkinningSourceVerts; + gpuSkinningFallbackReason_t gpuSkinningFallback; }; struct rvMD5RVertexFormatDesc { @@ -478,6 +572,9 @@ class rvRenderModelMD5R : public idRenderModelStatic { virtual int NearestJoint( int surfaceNum, int a, int b, int c ) const; virtual int GetSurfaceMask( const char *surface ) const; virtual int Memory() const; + virtual renderModelCacheType_t LevelLoadCachePayloadType() const; + virtual bool WriteLevelLoadCachePayload( idFile &file ) const; + virtual bool ReadLevelLoadCachePayload( idFile &file ); bool InitFromMD5Model( const idRenderModelMD5 &sourceModel ); bool InitFromStaticModel( const idRenderModelStatic &sourceModel, rvMD5RSource_t sourceType ); @@ -527,7 +624,8 @@ class rvRenderModelMD5R : public idRenderModelStatic { void ParseJoint( Lexer &parser, int jointIndex, idJointQuat &worldPose ); void BuildLevelsOfDetail(); bool BuildDynamicMeshTemplate( rvMD5RMesh &mesh ); - bool UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJointMat *entJoints, modelSurface_t &surface, bool calculateTangents, float skinScale ) const; + void BuildGpuSkinningSidecar( rvMD5RMesh &mesh ) const; + bool UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJointMat *entJoints, modelSurface_t &surface, bool calculateTangents, float skinScale, bool allowGpuSkinning ) const; bool GenerateDynamicSurface( idRenderModelStatic &staticModel, rvMD5RMesh &mesh, const renderEntity_s &ent, const idJointMat *entJoints, dword surfMask ); bool CopyPrimBatchTriangles( const rvMD5RMesh &mesh, idDrawVert *destDrawVerts, glIndex_t *destIndices, const rvSilTraceVertT *silTraceVerts ) const; bool GenerateStaticSurfaces(); diff --git a/src/renderer/Model_md5.cpp b/src/renderer/Model_md5.cpp index 5deab46c..aefa9158 100644 --- a/src/renderer/Model_md5.cpp +++ b/src/renderer/Model_md5.cpp @@ -35,6 +35,93 @@ If you have questions concerning this license or the applicable additional terms static const char *MD5_SnapshotName = "_MD5_Snapshot_"; static const int MD5_BackSideSurfaceIdOffset = 1000; +namespace { + static const unsigned int MD5_MODEL_CACHE_MAGIC = 0x4d34514fU; // "OQ4M" + static const int MD5_MODEL_CACHE_VERSION = 1; + static const unsigned int MD5_MODEL_GENERATED_CACHE_PARSER_VERSION = 0x00020002U; // bump when R_BuildDeformInfo's derivation changes: cache hits no longer cross-check against a fresh rebuild + static const int MD5_CACHE_MAX_JOINTS = 65536; + static const int MD5_CACHE_MAX_MESHES = 65536; + static const int MD5_CACHE_MAX_VERTS = 1 << 20; + static const int MD5_CACHE_MAX_WEIGHTS = 1 << 24; + static const int MD5_CACHE_MAX_INDEXES = 1 << 24; + static const int MD5_CACHE_MAX_SIL_EDGES = 1 << 23; + static const int MD5_CACHE_MAX_NAME = 4096; + static const int MD5_CACHE_MAX_MATERIALIZED_VERTS = 1 << 18; + static const int MD5_CACHE_MAX_MATERIALIZED_WEIGHTS = 1 << 20; + static const int MD5_CACHE_MAX_MATERIALIZED_INDEXES = 1 << 20; + + // the bulk array codecs stream these structures as raw little-endian bytes; + // pin their layouts so a change breaks the build instead of the cache format + static_assert( sizeof( glIndex_t ) == sizeof( int ), "cache bulk IO streams glIndex_t as int" ); + static_assert( sizeof( silEdge_t ) == 4 * sizeof( int ), "cache bulk IO streams silEdge_t as 4 ints" ); + static_assert( sizeof( dominantTri_t ) == 2 * sizeof( int ) + 3 * sizeof( float ), "cache bulk IO streams dominantTri_t as 2 ints + 3 floats" ); + static_assert( sizeof( idVec2 ) == 2 * sizeof( float ), "cache bulk IO streams idVec2 as 2 floats" ); + static_assert( sizeof( idVec4 ) == 4 * sizeof( float ), "cache bulk IO streams idVec4 as 4 floats" ); + + struct md5CacheDeformStage_t { + int numSourceVerts; + int numOutputVerts; + idList indexes; + idList silIndexes; + idList mirroredVerts; + idList dupVerts; + idList silEdges; + idList dominantTris; + + md5CacheDeformStage_t() : numSourceVerts( 0 ), numOutputVerts( 0 ) {} + }; + + struct md5CacheMeshStage_t { + idStr materialName; + idList texCoords; + int numWeights; + idList weights; + idList scaledBaseVectors; + idList baseVectors; + idList scaledWeights; + idList weightIndex; + int numTris; + int surfaceNum; + md5CacheDeformStage_t deform; + + md5CacheMeshStage_t() : numWeights( 0 ), numTris( 0 ), surfaceNum( 0 ) {} + }; + + static bool R_MD5CacheWriteJointQuat( idRenderModelCacheWriter &writer, const idJointQuat &pose ) { + return writer.WriteFloat( pose.q.x ) && writer.WriteFloat( pose.q.y ) + && writer.WriteFloat( pose.q.z ) && writer.WriteFloat( pose.q.w ) + && writer.WriteVec3( pose.t ); + } + + static bool R_MD5CacheReadJointQuat( idRenderModelCacheReader &reader, idJointQuat &pose ) { + pose.w = 0.0f; + return reader.ReadFloat( pose.q.x ) && reader.ReadFloat( pose.q.y ) + && reader.ReadFloat( pose.q.z ) && reader.ReadFloat( pose.q.w ) + && reader.ReadVec3( pose.t ); + } + + static bool R_MD5CacheWriteJointMat( idRenderModelCacheWriter &writer, const idJointMat &mat ) { + const float *values = mat.ToFloatPtr(); + for ( int i = 0; i < 12; ++i ) { + if ( !writer.WriteFloat( values[i] ) ) { + return false; + } + } + return true; + } + + static bool R_MD5CacheReadJointMat( idRenderModelCacheReader &reader, idJointMat &mat ) { + float *values = mat.ToFloatPtr(); + for ( int i = 0; i < 12; ++i ) { + if ( !reader.ReadFloat( values[i] ) ) { + return false; + } + } + return true; + } + +} + /* ==================== R_CopyAndReverseTriangles @@ -77,6 +164,7 @@ static void R_CopyAndReverseTriangles( const srfTriangles_t *src, srfTriangles_t for ( int i = 0; i < tri->numVerts; ++i ) { tri->verts[i].normal = vec3_origin - tri->verts[i].normal; } + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ); for ( int i = 0; i < tri->numIndexes; i += 3 ) { tri->indexes[i + 0] = src->indexes[i + 1]; @@ -112,6 +200,597 @@ idRenderModelMD5::idRenderModelMD5() { viewEnt = NULL; } +renderModelCacheType_t idRenderModelMD5::LevelLoadCachePayloadType() const { + return RENDER_MODEL_CACHE_MD5; +} + +bool idRenderModelMD5::WriteLevelLoadCachePayload( idFile &file ) const { + if ( purged || defaulted || joints.Num() < 0 || joints.Num() > MD5_CACHE_MAX_JOINTS + || defaultPose.Num() != joints.Num() || skinSpaceToLocalMats.Num() != joints.Num() + || meshes.Num() < 0 || meshes.Num() > MD5_CACHE_MAX_MESHES ) { + return false; + } + + idRenderModelCacheWriter writer( file ); + if ( !writer.WriteUnsignedInt( MD5_MODEL_CACHE_MAGIC ) || !writer.WriteInt( MD5_MODEL_CACHE_VERSION ) + || !idRenderModelStatic::WriteLevelLoadCachePayload( file ) || !writer.WriteInt( joints.Num() ) ) { + return false; + } + + for ( int i = 0; i < joints.Num(); ++i ) { + int parentIndex = -1; + if ( joints[i].parent != NULL ) { + parentIndex = static_cast( joints[i].parent - joints.Ptr() ); + } + if ( joints[i].name.IsEmpty() || parentIndex < -1 || parentIndex >= i + || !writer.WriteString( joints[i].name.c_str(), MD5_CACHE_MAX_NAME ) + || !writer.WriteInt( parentIndex ) ) { + return false; + } + } + + if ( !writer.WriteInt( defaultPose.Num() ) ) { + return false; + } + for ( int i = 0; i < defaultPose.Num(); ++i ) { + if ( !R_MD5CacheWriteJointQuat( writer, defaultPose[i] ) ) { + return false; + } + } + if ( !writer.WriteInt( skinSpaceToLocalMats.Num() ) ) { + return false; + } + for ( int i = 0; i < skinSpaceToLocalMats.Num(); ++i ) { + if ( !R_MD5CacheWriteJointMat( writer, skinSpaceToLocalMats[i] ) ) { + return false; + } + } + + if ( !writer.WriteInt( meshes.Num() ) ) { + return false; + } + for ( int meshIndex = 0; meshIndex < meshes.Num(); ++meshIndex ) { + const idMD5Mesh &mesh = meshes[meshIndex]; + if ( mesh.shader == NULL || mesh.shader->GetName() == NULL || mesh.texCoords.Num() <= 0 + || mesh.texCoords.Num() > MD5_CACHE_MAX_MATERIALIZED_VERTS || mesh.numWeights <= 0 + || mesh.numWeights > MD5_CACHE_MAX_MATERIALIZED_WEIGHTS || mesh.weights == NULL + || mesh.scaledBaseVectors == NULL || mesh.scaledWeights == NULL || mesh.weightIndex == NULL + || mesh.baseVectors == NULL || mesh.deformInfo == NULL || mesh.numTris < 0 + || mesh.numTris > MD5_CACHE_MAX_MATERIALIZED_INDEXES / 3 || mesh.surfaceNum < -1 + || mesh.surfaceNum > MD5_CACHE_MAX_MESHES + || ( mesh.deformInfo->numMirroredVerts > 0 && mesh.deformInfo->mirroredVerts == NULL ) ) { + return false; + } + + idList firstWeight; + idList weightsPerVert; + firstWeight.SetNum( mesh.texCoords.Num() ); + weightsPerVert.SetNum( mesh.texCoords.Num() ); + int weightCursor = 0; + for ( int vertIndex = 0; vertIndex < mesh.texCoords.Num(); ++vertIndex ) { + if ( weightCursor >= mesh.numWeights ) { + return false; + } + const int nextBytes = mesh.weights[weightCursor].nextVertexOffset; + if ( nextBytes <= 0 || ( nextBytes % static_cast( sizeof( jointWeight_t ) ) ) != 0 ) { + return false; + } + const int vertexWeightCount = nextBytes / sizeof( jointWeight_t ); + if ( vertexWeightCount <= 0 || vertexWeightCount > mesh.numWeights - weightCursor ) { + return false; + } + firstWeight[vertIndex] = weightCursor; + weightsPerVert[vertIndex] = vertexWeightCount; + for ( int j = 0; j < vertexWeightCount; ++j ) { + if ( mesh.weights[weightCursor + j].nextVertexOffset != ( vertexWeightCount - j ) * sizeof( jointWeight_t ) ) { + return false; + } + } + weightCursor += vertexWeightCount; + } + if ( weightCursor != mesh.numWeights || mesh.deformInfo->numSourceVerts != mesh.texCoords.Num() + || mesh.deformInfo->numOutputVerts < mesh.deformInfo->numSourceVerts + || mesh.deformInfo->numOutputVerts > MD5_CACHE_MAX_MATERIALIZED_VERTS + || mesh.deformInfo->numMirroredVerts < 0 + || mesh.deformInfo->numMirroredVerts != mesh.deformInfo->numOutputVerts - mesh.deformInfo->numSourceVerts ) { + return false; + } + + int storedWeightCount = mesh.numWeights; + for ( int mirrorIndex = 0; mirrorIndex < mesh.deformInfo->numMirroredVerts; ++mirrorIndex ) { + const int sourceVert = mesh.deformInfo->mirroredVerts[mirrorIndex]; + if ( sourceVert < 0 || sourceVert >= weightsPerVert.Num() + || weightsPerVert[sourceVert] > MD5_CACHE_MAX_WEIGHTS - storedWeightCount ) { + return false; + } + storedWeightCount += weightsPerVert[sourceVert]; + } + if ( storedWeightCount > MD5_CACHE_MAX_MATERIALIZED_WEIGHTS * 2 ) { + return false; + } + + if ( !writer.WriteString( mesh.shader->GetName(), MD5_CACHE_MAX_NAME ) + || !writer.WriteInt( mesh.texCoords.Num() ) ) { + return false; + } + if ( !writer.WriteFloatArray( reinterpret_cast( mesh.texCoords.Ptr() ), mesh.texCoords.Num() * 2 ) ) { + return false; + } + if ( !writer.WriteInt( mesh.numWeights ) || !writer.WriteInt( storedWeightCount ) ) { + return false; + } + for ( int i = 0; i < storedWeightCount; ++i ) { + const jointWeight_t &weight = mesh.weights[i]; + if ( weight.weight < 0.0f || weight.jointMatOffset < 0 + || ( weight.jointMatOffset % static_cast( sizeof( idJointMat ) ) ) != 0 + || weight.jointMatOffset / static_cast( sizeof( idJointMat ) ) >= joints.Num() + || weight.nextVertexOffset <= 0 + || !writer.WriteFloat( weight.weight ) || !writer.WriteInt( weight.jointMatOffset ) + || !writer.WriteInt( weight.nextVertexOffset ) || !writer.WriteVec4( mesh.scaledBaseVectors[i] ) ) { + return false; + } + } + for ( int i = 0; i < mesh.numWeights; ++i ) { + if ( !writer.WriteVec4( mesh.scaledWeights[i] ) + || !writer.WriteInt( mesh.weightIndex[i * 2 + 0] ) + || !writer.WriteInt( mesh.weightIndex[i * 2 + 1] ) ) { + return false; + } + } + + const deformInfo_t &deform = *mesh.deformInfo; + if ( deform.numIndexes != mesh.numTris * 3 || deform.numIndexes < 0 || deform.numIndexes > MD5_CACHE_MAX_INDEXES + || deform.numMirroredVerts < 0 || deform.numMirroredVerts > deform.numOutputVerts + || deform.numDupVerts < 0 || deform.numDupVerts > deform.numOutputVerts + || deform.numSilEdges < 0 || deform.numSilEdges > MD5_CACHE_MAX_SIL_EDGES + || ( deform.numIndexes > 0 && ( deform.indexes == NULL || deform.silIndexes == NULL ) ) + || ( deform.numMirroredVerts > 0 && deform.mirroredVerts == NULL ) + || ( deform.numDupVerts > 0 && deform.dupVerts == NULL ) + || ( deform.numSilEdges > 0 && deform.silEdges == NULL ) + || !writer.WriteInt( mesh.numTris ) || !writer.WriteInt( mesh.surfaceNum ) + || !writer.WriteInt( deform.numSourceVerts ) || !writer.WriteInt( deform.numOutputVerts ) + || !writer.WriteInt( deform.numIndexes ) ) { + return false; + } + for ( int i = 0; i < deform.numIndexes; ++i ) { + if ( deform.indexes[i] < 0 || deform.indexes[i] >= deform.numOutputVerts + || deform.silIndexes[i] < 0 || deform.silIndexes[i] >= deform.numOutputVerts ) { + return false; + } + } + idList indexPairs; + indexPairs.SetNum( deform.numIndexes * 2 ); + for ( int i = 0; i < deform.numIndexes; ++i ) { + indexPairs[i * 2 + 0] = deform.indexes[i]; + indexPairs[i * 2 + 1] = deform.silIndexes[i]; + } + if ( !writer.WriteIntArray( indexPairs.Ptr(), indexPairs.Num() ) ) { + return false; + } + if ( !writer.WriteInt( deform.numMirroredVerts ) ) { + return false; + } + for ( int i = 0; i < deform.numMirroredVerts; ++i ) { + if ( deform.mirroredVerts[i] < 0 || deform.mirroredVerts[i] >= deform.numSourceVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( deform.mirroredVerts, deform.numMirroredVerts ) ) { + return false; + } + if ( !writer.WriteInt( deform.numDupVerts ) ) { + return false; + } + for ( int i = 0; i < deform.numDupVerts * 2; ++i ) { + if ( deform.dupVerts[i] < 0 || deform.dupVerts[i] >= deform.numOutputVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( deform.dupVerts, deform.numDupVerts * 2 ) ) { + return false; + } + if ( !writer.WriteInt( deform.numSilEdges ) ) { + return false; + } + const int planeCount = deform.numIndexes / 3; + for ( int i = 0; i < deform.numSilEdges; ++i ) { + const silEdge_t &edge = deform.silEdges[i]; + if ( edge.p1 < 0 || edge.p1 >= planeCount || edge.p2 < 0 || edge.p2 > planeCount + || edge.v1 < 0 || edge.v1 >= deform.numOutputVerts || edge.v2 < 0 || edge.v2 >= deform.numOutputVerts ) { + return false; + } + } + if ( !writer.WriteIntArray( reinterpret_cast( deform.silEdges ), deform.numSilEdges * 4 ) ) { + return false; + } + if ( !writer.WriteBool( deform.dominantTris != NULL ) ) { + return false; + } + if ( deform.dominantTris != NULL ) { + for ( int i = 0; i < deform.numOutputVerts; ++i ) { + const dominantTri_t &dominant = deform.dominantTris[i]; + if ( dominant.v2 < 0 || dominant.v2 >= deform.numOutputVerts + || dominant.v3 < 0 || dominant.v3 >= deform.numOutputVerts + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[0] ) + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[1] ) + || !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[2] ) ) { + return false; + } + } + if ( !writer.WriteBytes( deform.dominantTris, deform.numOutputVerts * static_cast( sizeof( dominantTri_t ) ) ) ) { + return false; + } + } + if ( deform.numOutputVerts > 0x1fffffff || !writer.WriteInt( deform.numOutputVerts * 4 ) ) { + return false; + } + if ( !writer.WriteFloatArray( reinterpret_cast( mesh.baseVectors ), deform.numOutputVerts * 16 ) ) { + return false; + } + } + return writer.IsValid(); +} + +bool idRenderModelMD5::ReadLevelLoadCachePayload( idFile &file ) { + idRenderModelCacheReader reader( file ); + unsigned int magic = 0; + int version = 0; + if ( !reader.ReadUnsignedInt( magic ) || magic != MD5_MODEL_CACHE_MAGIC + || !reader.ReadInt( version ) || version != MD5_MODEL_CACHE_VERSION ) { + return false; + } + + idRenderModelMD5 staged; + if ( !staged.idRenderModelStatic::ReadLevelLoadCachePayload( file ) || staged.surfaces.Num() != 0 ) { + return false; + } + + int jointCount = 0; + if ( !reader.ReadCount( jointCount, MD5_CACHE_MAX_JOINTS ) + || !reader.Reserve( jointCount, sizeof( idStr ) + sizeof( int ) + sizeof( idJointQuat ) + sizeof( idJointMat ) ) ) { + return false; + } + idList jointNames; + idList jointParents; + idList decodedPose; + idList decodedSkinMats; + jointNames.SetNum( jointCount ); + jointParents.SetNum( jointCount ); + for ( int i = 0; i < jointCount; ++i ) { + if ( !reader.ReadString( jointNames[i], MD5_CACHE_MAX_NAME ) || jointNames[i].IsEmpty() + || !reader.ReadInt( jointParents[i] ) || jointParents[i] < -1 || jointParents[i] >= i ) { + return false; + } + } + + int poseCount = 0; + if ( !reader.ReadCount( poseCount, MD5_CACHE_MAX_JOINTS ) || poseCount != jointCount ) { + return false; + } + decodedPose.SetNum( poseCount ); + for ( int i = 0; i < poseCount; ++i ) { + if ( !R_MD5CacheReadJointQuat( reader, decodedPose[i] ) ) { + return false; + } + } + int skinMatCount = 0; + if ( !reader.ReadCount( skinMatCount, MD5_CACHE_MAX_JOINTS ) || skinMatCount != jointCount ) { + return false; + } + decodedSkinMats.SetNum( skinMatCount ); + for ( int i = 0; i < skinMatCount; ++i ) { + if ( !R_MD5CacheReadJointMat( reader, decodedSkinMats[i] ) ) { + return false; + } + } + + int meshCount = 0; + if ( !reader.ReadCount( meshCount, MD5_CACHE_MAX_MESHES, sizeof( md5CacheMeshStage_t ) ) ) { + return false; + } + idList decodedMeshes; + decodedMeshes.SetNum( meshCount ); + for ( int meshIndex = 0; meshIndex < meshCount; ++meshIndex ) { + md5CacheMeshStage_t &mesh = decodedMeshes[meshIndex]; + int texCoordCount = 0; + int storedWeightCount = 0; + if ( !reader.ReadString( mesh.materialName, MD5_CACHE_MAX_NAME ) || mesh.materialName.IsEmpty() + || !reader.ReadCount( texCoordCount, MD5_CACHE_MAX_VERTS, sizeof( idVec2 ) ) || texCoordCount <= 0 + || texCoordCount > MD5_CACHE_MAX_MATERIALIZED_VERTS ) { + return false; + } + mesh.texCoords.SetNum( texCoordCount ); + if ( !reader.ReadFloatArray( reinterpret_cast( mesh.texCoords.Ptr() ), texCoordCount * 2 ) ) { + return false; + } + if ( !reader.ReadCount( mesh.numWeights, MD5_CACHE_MAX_WEIGHTS ) || mesh.numWeights <= 0 + || mesh.numWeights > MD5_CACHE_MAX_MATERIALIZED_WEIGHTS + || !reader.ReadCount( storedWeightCount, MD5_CACHE_MAX_WEIGHTS ) || storedWeightCount < mesh.numWeights + || storedWeightCount > MD5_CACHE_MAX_MATERIALIZED_WEIGHTS * 2 + || !reader.Reserve( storedWeightCount, sizeof( jointWeight_t ) + sizeof( idVec4 ) ) ) { + return false; + } + mesh.weights.SetNum( storedWeightCount ); + mesh.scaledBaseVectors.SetNum( storedWeightCount ); + for ( int i = 0; i < storedWeightCount; ++i ) { + jointWeight_t &weight = mesh.weights[i]; + if ( !reader.ReadFloat( weight.weight ) || weight.weight < 0.0f + || !reader.ReadInt( weight.jointMatOffset ) || weight.jointMatOffset < 0 + || ( weight.jointMatOffset % static_cast( sizeof( idJointMat ) ) ) != 0 + || weight.jointMatOffset / static_cast( sizeof( idJointMat ) ) >= jointCount + || !reader.ReadInt( weight.nextVertexOffset ) || weight.nextVertexOffset <= 0 + || ( weight.nextVertexOffset % static_cast( sizeof( jointWeight_t ) ) ) != 0 + || !reader.ReadVec4( mesh.scaledBaseVectors[i] ) ) { + return false; + } + } + + if ( mesh.numWeights > 0x3fffffff || !reader.Reserve( mesh.numWeights, sizeof( idVec4 ) + sizeof( int ) * 2 ) ) { + return false; + } + mesh.scaledWeights.SetNum( mesh.numWeights ); + mesh.weightIndex.SetNum( mesh.numWeights * 2 ); + for ( int i = 0; i < mesh.numWeights; ++i ) { + if ( !reader.ReadVec4( mesh.scaledWeights[i] ) + || !reader.ReadInt( mesh.weightIndex[i * 2 + 0] ) + || mesh.weightIndex[i * 2 + 0] < 0 + || ( mesh.weightIndex[i * 2 + 0] % static_cast( sizeof( idJointMat ) ) ) != 0 + || mesh.weightIndex[i * 2 + 0] / static_cast( sizeof( idJointMat ) ) >= jointCount + || !reader.ReadInt( mesh.weightIndex[i * 2 + 1] ) + || ( mesh.weightIndex[i * 2 + 1] != 0 && mesh.weightIndex[i * 2 + 1] != 1 ) ) { + return false; + } + } + + if ( !reader.ReadCount( mesh.numTris, MD5_CACHE_MAX_INDEXES / 3 ) + || !reader.ReadInt( mesh.surfaceNum ) || mesh.surfaceNum < -1 || mesh.surfaceNum > MD5_CACHE_MAX_MESHES + || !reader.ReadCount( mesh.deform.numSourceVerts, MD5_CACHE_MAX_VERTS ) + || mesh.deform.numSourceVerts != texCoordCount + || !reader.ReadCount( mesh.deform.numOutputVerts, MD5_CACHE_MAX_VERTS ) + || mesh.deform.numOutputVerts < mesh.deform.numSourceVerts + || mesh.deform.numOutputVerts > MD5_CACHE_MAX_MATERIALIZED_VERTS ) { + return false; + } + + int deformIndexCount = 0; + if ( !reader.ReadCount( deformIndexCount, MD5_CACHE_MAX_INDEXES ) + || deformIndexCount != mesh.numTris * 3 + || deformIndexCount > MD5_CACHE_MAX_MATERIALIZED_INDEXES + || !reader.Reserve( deformIndexCount, sizeof( glIndex_t ) * 2 ) ) { + return false; + } + mesh.deform.indexes.SetNum( deformIndexCount ); + mesh.deform.silIndexes.SetNum( deformIndexCount ); + idList indexPairs; + indexPairs.SetNum( deformIndexCount * 2 ); + if ( !reader.ReadIntArray( indexPairs.Ptr(), indexPairs.Num() ) ) { + return false; + } + for ( int i = 0; i < deformIndexCount; ++i ) { + mesh.deform.indexes[i] = indexPairs[i * 2 + 0]; + mesh.deform.silIndexes[i] = indexPairs[i * 2 + 1]; + if ( mesh.deform.indexes[i] < 0 || mesh.deform.indexes[i] >= mesh.deform.numOutputVerts + || mesh.deform.silIndexes[i] < 0 || mesh.deform.silIndexes[i] >= mesh.deform.numOutputVerts ) { + return false; + } + } + + int mirroredCount = 0; + if ( !reader.ReadCount( mirroredCount, mesh.deform.numOutputVerts, sizeof( int ) ) + || mirroredCount != mesh.deform.numOutputVerts - mesh.deform.numSourceVerts ) { + return false; + } + mesh.deform.mirroredVerts.SetNum( mirroredCount ); + if ( !reader.ReadIntArray( mesh.deform.mirroredVerts.Ptr(), mirroredCount ) ) { + return false; + } + for ( int i = 0; i < mirroredCount; ++i ) { + if ( mesh.deform.mirroredVerts[i] < 0 + || mesh.deform.mirroredVerts[i] >= mesh.deform.numSourceVerts ) { + return false; + } + } + + int dupCount = 0; + if ( !reader.ReadCount( dupCount, mesh.deform.numOutputVerts ) || dupCount > 0x3fffffff + || !reader.Reserve( dupCount * 2, sizeof( int ) ) ) { + return false; + } + mesh.deform.dupVerts.SetNum( dupCount * 2 ); + if ( !reader.ReadIntArray( mesh.deform.dupVerts.Ptr(), mesh.deform.dupVerts.Num() ) ) { + return false; + } + for ( int i = 0; i < mesh.deform.dupVerts.Num(); ++i ) { + if ( mesh.deform.dupVerts[i] < 0 + || mesh.deform.dupVerts[i] >= mesh.deform.numOutputVerts ) { + return false; + } + } + + int silEdgeCount = 0; + if ( !reader.ReadCount( silEdgeCount, MD5_CACHE_MAX_SIL_EDGES, sizeof( silEdge_t ) ) ) { + return false; + } + mesh.deform.silEdges.SetNum( silEdgeCount ); + if ( !reader.ReadIntArray( reinterpret_cast( mesh.deform.silEdges.Ptr() ), silEdgeCount * 4 ) ) { + return false; + } + const int planeCount = deformIndexCount / 3; + for ( int i = 0; i < silEdgeCount; ++i ) { + const silEdge_t &edge = mesh.deform.silEdges[i]; + if ( edge.p1 < 0 || edge.p1 >= planeCount || edge.p2 < 0 || edge.p2 > planeCount + || edge.v1 < 0 || edge.v1 >= mesh.deform.numOutputVerts + || edge.v2 < 0 || edge.v2 >= mesh.deform.numOutputVerts ) { + return false; + } + } + + bool hasDominantTris = false; + if ( !reader.ReadBool( hasDominantTris ) ) { + return false; + } + if ( hasDominantTris ) { + if ( !reader.Reserve( mesh.deform.numOutputVerts, sizeof( dominantTri_t ) ) ) { + return false; + } + mesh.deform.dominantTris.SetNum( mesh.deform.numOutputVerts ); + if ( !reader.ReadBytes( mesh.deform.dominantTris.Ptr(), + mesh.deform.numOutputVerts * static_cast( sizeof( dominantTri_t ) ) ) ) { + return false; + } + for ( int i = 0; i < mesh.deform.numOutputVerts; ++i ) { + dominantTri_t &dominant = mesh.deform.dominantTris[i]; + dominant.v2 = LittleLong( dominant.v2 ); + dominant.v3 = LittleLong( dominant.v3 ); + if ( dominant.v2 < 0 || dominant.v2 >= mesh.deform.numOutputVerts + || dominant.v3 < 0 || dominant.v3 >= mesh.deform.numOutputVerts ) { + return false; + } + for ( int scale = 0; scale < 3; ++scale ) { + dominant.normalizationScale[scale] = LittleFloat( dominant.normalizationScale[scale] ); + if ( !R_RenderModelCacheFloatIsFinite( dominant.normalizationScale[scale] ) ) { + return false; + } + } + } + } + + int baseVectorCount = 0; + if ( mesh.deform.numOutputVerts > 0x1fffffff + || !reader.ReadCount( baseVectorCount, MD5_CACHE_MAX_VERTS * 4, sizeof( idVec4 ) ) + || baseVectorCount != mesh.deform.numOutputVerts * 4 ) { + return false; + } + mesh.baseVectors.SetNum( baseVectorCount ); + if ( !reader.ReadFloatArray( reinterpret_cast( mesh.baseVectors.Ptr() ), baseVectorCount * 4 ) ) { + return false; + } + + idList weightsPerVert; + weightsPerVert.SetNum( texCoordCount ); + int cursor = 0; + for ( int vertIndex = 0; vertIndex < texCoordCount; ++vertIndex ) { + if ( cursor >= mesh.numWeights ) { + return false; + } + const int groupCount = mesh.weights[cursor].nextVertexOffset / sizeof( jointWeight_t ); + if ( groupCount <= 0 || groupCount > mesh.numWeights - cursor ) { + return false; + } + weightsPerVert[vertIndex] = groupCount; + for ( int j = 0; j < groupCount; ++j ) { + if ( mesh.weights[cursor + j].nextVertexOffset != ( groupCount - j ) * sizeof( jointWeight_t ) ) { + return false; + } + } + cursor += groupCount; + } + if ( cursor != mesh.numWeights ) { + return false; + } + int expectedStoredWeights = mesh.numWeights; + for ( int i = 0; i < mirroredCount; ++i ) { + const int groupCount = weightsPerVert[mesh.deform.mirroredVerts[i]]; + if ( groupCount > MD5_CACHE_MAX_WEIGHTS - expectedStoredWeights ) { + return false; + } + expectedStoredWeights += groupCount; + if ( cursor + groupCount > mesh.weights.Num() ) { + return false; + } + for ( int j = 0; j < groupCount; ++j ) { + if ( mesh.weights[cursor + j].nextVertexOffset != ( groupCount - j ) * sizeof( jointWeight_t ) ) { + return false; + } + } + cursor += groupCount; + } + if ( expectedStoredWeights != mesh.weights.Num() || cursor != mesh.weights.Num() ) { + return false; + } + + int completedWeightVertices = 0; + for ( int i = 0; i < mesh.numWeights; ++i ) { + completedWeightVertices += mesh.weightIndex[i * 2 + 1]; + } + if ( completedWeightVertices != texCoordCount || mesh.weightIndex[mesh.numWeights * 2 - 1] != 1 ) { + return false; + } + } + if ( !reader.IsValid() ) { + return false; + } + + staged.joints.SetNum( jointCount ); + for ( int i = 0; i < jointCount; ++i ) { + staged.joints[i].name = jointNames[i]; + staged.joints[i].parent = ( jointParents[i] >= 0 ) ? &staged.joints[jointParents[i]] : NULL; + } + staged.defaultPose.Swap( decodedPose ); + staged.skinSpaceToLocalMats.Swap( decodedSkinMats ); + staged.meshes.SetNum( meshCount ); + for ( int meshIndex = 0; meshIndex < meshCount; ++meshIndex ) { + const md5CacheMeshStage_t &source = decodedMeshes[meshIndex]; + idMD5Mesh &mesh = staged.meshes[meshIndex]; + mesh.shader = declManager->FindMaterial( source.materialName.c_str() ); + if ( mesh.shader == NULL || idStr::Icmp( mesh.shader->GetName(), source.materialName.c_str() ) != 0 + || mesh.shader->UseUnsmoothedTangents() != ( source.deform.dominantTris.Num() > 0 ) ) { + return false; + } + + // The reader fully range-validated every derived array and enforced + // numOutputVerts == numSourceVerts + mirroredVerts.Num(); allocate the + // deform info directly instead of re-deriving it just to overwrite it. + deformInfo_t *deform = R_AllocDeformInfo( source.deform.numSourceVerts, source.deform.numOutputVerts, + source.deform.indexes.Num(), source.deform.mirroredVerts.Num(), + source.deform.dupVerts.Num() / 2, source.deform.silEdges.Num(), + source.deform.dominantTris.Num() > 0 ); + if ( deform == NULL ) { + return false; + } + memcpy( deform->indexes, source.deform.indexes.Ptr(), deform->numIndexes * sizeof( deform->indexes[0] ) ); + memcpy( deform->silIndexes, source.deform.silIndexes.Ptr(), deform->numIndexes * sizeof( deform->silIndexes[0] ) ); + if ( deform->numMirroredVerts > 0 ) { + memcpy( deform->mirroredVerts, source.deform.mirroredVerts.Ptr(), deform->numMirroredVerts * sizeof( deform->mirroredVerts[0] ) ); + } + if ( deform->numDupVerts > 0 ) { + memcpy( deform->dupVerts, source.deform.dupVerts.Ptr(), deform->numDupVerts * 2 * sizeof( deform->dupVerts[0] ) ); + } + if ( deform->numSilEdges > 0 ) { + memcpy( deform->silEdges, source.deform.silEdges.Ptr(), deform->numSilEdges * sizeof( deform->silEdges[0] ) ); + } + if ( deform->dominantTris != NULL ) { + memcpy( deform->dominantTris, source.deform.dominantTris.Ptr(), deform->numOutputVerts * sizeof( deform->dominantTris[0] ) ); + } + + mesh.texCoords = source.texCoords; + mesh.numWeights = source.numWeights; + mesh.weights = static_cast( Mem_Alloc16( source.weights.Num() * sizeof( mesh.weights[0] ) ) ); + mesh.scaledBaseVectors = static_cast( Mem_Alloc16( source.scaledBaseVectors.Num() * sizeof( mesh.scaledBaseVectors[0] ) ) ); + mesh.baseVectors = static_cast( Mem_Alloc16( source.baseVectors.Num() * sizeof( mesh.baseVectors[0] ) ) ); + mesh.scaledWeights = static_cast( Mem_Alloc16( source.scaledWeights.Num() * sizeof( mesh.scaledWeights[0] ) ) ); + mesh.weightIndex = static_cast( Mem_Alloc16( source.weightIndex.Num() * sizeof( mesh.weightIndex[0] ) ) ); + memcpy( mesh.weights, source.weights.Ptr(), source.weights.Num() * sizeof( mesh.weights[0] ) ); + memcpy( mesh.scaledBaseVectors, source.scaledBaseVectors.Ptr(), source.scaledBaseVectors.Num() * sizeof( mesh.scaledBaseVectors[0] ) ); + memcpy( mesh.baseVectors, source.baseVectors.Ptr(), source.baseVectors.Num() * sizeof( mesh.baseVectors[0] ) ); + memcpy( mesh.scaledWeights, source.scaledWeights.Ptr(), source.scaledWeights.Num() * sizeof( mesh.scaledWeights[0] ) ); + memcpy( mesh.weightIndex, source.weightIndex.Ptr(), source.weightIndex.Num() * sizeof( mesh.weightIndex[0] ) ); + mesh.numTris = source.numTris; + mesh.surfaceNum = source.surfaceNum; + mesh.currentTime = 0.0f; + mesh.deformInfo = deform; + mesh.BuildGpuSkinningSidecar( jointCount ); + } + + SwapLevelLoadCacheState( staged ); + joints.Swap( staged.joints ); + defaultPose.Swap( staged.defaultPose ); + skinSpaceToLocalMats.Swap( staged.skinSpaceToLocalMats ); + meshes.Swap( staged.meshes ); + viewEnt = NULL; + return true; +} + /* ==================== idMD5Mesh::idMD5Mesh @@ -128,6 +807,8 @@ idMD5Mesh::idMD5Mesh() { deformInfo = NULL; surfaceNum = 0; currentTime = 0.0f; + gpuSkinningNumJoints = 0; + gpuSkinningFallback = GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES; } /* @@ -197,9 +878,16 @@ void idMD5Mesh::ParseMesh( idLexer &parser, int numJoints, const idJointMat *joi firstWeightForVertex[i] = parser.ParseInt(); numWeightsForVertex[i] = parser.ParseInt(); - if ( !numWeightsForVertex[i] ) { + if ( numWeightsForVertex[i] <= 0 ) { + // A negative count would skip the fill loop below, leaving count at + // 0 and driving the weightIndex[count*2-1] write out of bounds. parser.Error( "Vertex without any joint weights." ); } + if ( firstWeightForVertex[i] < 0 ) { + // A negative base index reads tempWeights below its buffer and can + // also mask the maxweight range check. + parser.Error( "Vertex with negative first weight index." ); + } numWeights += numWeightsForVertex[i]; if ( numWeightsForVertex[i] + firstWeightForVertex[i] > maxweight ) { @@ -229,7 +917,11 @@ void idMD5Mesh::ParseMesh( idLexer &parser, int numJoints, const idJointMat *joi parser.Error( "Invalid size: %d", count ); } if ( maxweight > count ) { - parser.Warning( "Vertices reference out of range weights in model (%d of %d weights).", maxweight, count ); + // This was a warning, but the insertion-sort and fill loops below index + // tempWeights[firstWeight + j] directly (idList::operator[] is unchecked + // in release), so an out-of-range reference is an OOB read/write, not a + // cosmetic issue. Stock models never trip this. + parser.Error( "Vertices reference out of range weights in model (%d of %d weights).", maxweight, count ); } tempWeights.SetNum( count ); @@ -356,7 +1048,7 @@ void idMD5Mesh::ParseMesh( idLexer &parser, int numJoints, const idJointMat *joi baseVectors = (idVec4 *)Mem_Alloc16( deformInfo->numOutputVerts * 4 * sizeof( baseVectors[0] ) ); modelSurface_t tempSurf; memset( &tempSurf, 0, sizeof( tempSurf ) ); - UpdateSurface( NULL, joints, &tempSurf, false ); + UpdateSurface( NULL, joints, &tempSurf, false, false ); R_DeriveTangents( tempSurf.geometry, true ); for ( i = 0; i < deformInfo->numOutputVerts; ++i ) { @@ -368,6 +1060,76 @@ void idMD5Mesh::ParseMesh( idLexer &parser, int numJoints, const idJointMat *joi } R_FreeStaticTriSurf( tempSurf.geometry ); + BuildGpuSkinningSidecar( numJoints ); +} + +/* +==================== +idMD5Mesh::BuildGpuSkinningSidecar + +Build an immutable output-vertex stream. More than four non-zero influences is +an exactness failure, not an invitation to silently truncate retail content. +==================== +*/ +void idMD5Mesh::BuildGpuSkinningSidecar( int numJoints ) { + gpuBindPoseVerts.Clear(); + gpuSkinningVerts.Clear(); + gpuSkinningNumJoints = numJoints; + gpuSkinningFallback = GPU_SKINNING_FALLBACK_NONE; + if ( deformInfo == NULL || baseVectors == NULL || weights == NULL + || deformInfo->numOutputVerts <= 0 ) { + gpuSkinningFallback = GPU_SKINNING_FALLBACK_MISSING_BIND_POSE; + return; + } + + gpuBindPoseVerts.SetNum( deformInfo->numOutputVerts ); + gpuSkinningVerts.SetNum( deformInfo->numOutputVerts ); + idList influences; + int weightCursor = 0; + for ( int vertexIndex = 0; vertexIndex < deformInfo->numOutputVerts; ++vertexIndex ) { + idDrawVert &bindPose = gpuBindPoseVerts[vertexIndex]; + bindPose.Clear(); + memset( bindPose.color2, 0, sizeof( bindPose.color2 ) ); + bindPose.xyz = baseVectors[vertexIndex * 4 + 0].ToVec3(); + bindPose.normal = baseVectors[vertexIndex * 4 + 1].ToVec3(); + bindPose.tangents[0] = baseVectors[vertexIndex * 4 + 2].ToVec3(); + bindPose.tangents[1] = baseVectors[vertexIndex * 4 + 3].ToVec3(); + int textureVertex = vertexIndex; + if ( textureVertex >= texCoords.Num() ) { + const int mirrorIndex = textureVertex - texCoords.Num(); + textureVertex = mirrorIndex >= 0 && mirrorIndex < deformInfo->numMirroredVerts + ? deformInfo->mirroredVerts[mirrorIndex] : -1; + } + if ( textureVertex >= 0 && textureVertex < texCoords.Num() ) { + bindPose.st = texCoords[textureVertex]; + } + + const int groupCount = weights[weightCursor].nextVertexOffset / sizeof( weights[0] ); + if ( groupCount <= 0 ) { + gpuSkinningFallback = GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS; + break; + } + influences.SetNum( groupCount ); + for ( int influenceIndex = 0; influenceIndex < groupCount; ++influenceIndex ) { + const jointWeight_t &weight = weights[weightCursor + influenceIndex]; + influences[influenceIndex].jointIndex = weight.jointMatOffset / sizeof( idJointMat ); + influences[influenceIndex].weight = weight.weight; + } + + gpuSkinningPackResult_t result; + if ( !R_GpuSkinning_PackVertexExact( influences.Ptr(), influences.Num(), numJoints, + false, gpuSkinningVerts[vertexIndex], result ) + && gpuSkinningFallback == GPU_SKINNING_FALLBACK_NONE ) { + gpuSkinningFallback = result.reason; + } + weightCursor += groupCount; + } + + if ( gpuSkinningFallback != GPU_SKINNING_FALLBACK_NONE ) { + // Keep immutable source arrays for diagnostics, but admission remains + // fail-closed for the whole mesh. + return; + } } /* @@ -420,7 +1182,7 @@ bool idMD5Mesh::UpdateLod( const struct renderEntity_s *ent, const struct viewEn idMD5Mesh::UpdateSurface ==================== */ -void idMD5Mesh::UpdateSurface( const struct renderEntity_s *ent, const idJointMat *entJoints, modelSurface_t *surf, bool calculateTangents ) { +void idMD5Mesh::UpdateSurface( const struct renderEntity_s *ent, const idJointMat *entJoints, modelSurface_t *surf, bool calculateTangents, bool allowGpuSkinning ) { int i, base; srfTriangles_t *tri; @@ -444,6 +1206,7 @@ void idMD5Mesh::UpdateSurface( const struct renderEntity_s *ent, const idJointMa } tri = surf->geometry; + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_NONE ); // note that some of the data is references, and should not be freed tri->deformedSurface = true; @@ -475,12 +1238,33 @@ void idMD5Mesh::UpdateSurface( const struct renderEntity_s *ent, const idJointMa } const bool useLegacySkinScale = ( ent != NULL && ent->shaderParms[ SHADERPARM_MD5_SKINSCALE ] != 0.0f ); + bool gpuSkinningContractAttached = false; + if ( useLegacySkinScale ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_SKIN_SCALE ); + } else if ( !allowGpuSkinning ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ); + } else if ( !r_gpuSkinning.GetBool() ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_DISABLED ); + } else if ( !r_useNewSkinning.GetBool() ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ); + } else { + gpuSkinningContractAttached = R_GpuSkinning_AttachSurfaceContract( + tri, gpuBindPoseVerts.Ptr(), gpuSkinningVerts.Ptr(), gpuBindPoseVerts.Num(), + entJoints != NULL ? entJoints[0].ToFloatPtr() : NULL, gpuSkinningNumJoints, + GPU_SKINNING_JOINT_FLOATS, false, gpuSkinningFallback ); + } + + const uint64 cpuSkinStart = R_GpuSkinning_ReadMicroseconds(); if ( useLegacySkinScale ) { TransformScaledVerts( tri->verts, entJoints, ent->shaderParms[ SHADERPARM_MD5_SKINSCALE ] ); tri->tangentsCalculated = false; } else if ( scaledBaseVectors != NULL && weights != NULL ) { - if ( r_useNewSkinning.GetBool() && calculateTangents && baseVectors != NULL ) { + if ( gpuSkinningContractAttached ) { + SIMDProcessor->TransformVertsNew( tri->verts, deformInfo->numOutputVerts, tri->bounds, + entJoints, scaledBaseVectors, weights, numWeights ); + tri->tangentsCalculated = false; + } else if ( r_useNewSkinning.GetBool() && calculateTangents && baseVectors != NULL ) { if ( r_useFastSkinning.GetBool() ) { SIMDProcessor->TransformVertsAndTangentsFast( tri->verts, deformInfo->numOutputVerts, tri->bounds, entJoints, baseVectors, weights, numWeights ); @@ -523,10 +1307,16 @@ void idMD5Mesh::UpdateSurface( const struct renderEntity_s *ent, const idJointMa // R_DeriveTangents() to get normals, tangents, and face planes. If it only // needs shadows generated, it will only have to generate face planes. If it only // has ambient drawing, or is culled, no additional work will be necessary - if ( !tri->tangentsCalculated && !r_useDeferredTangents.GetBool() ) { + if ( !tri->tangentsCalculated && !r_useDeferredTangents.GetBool() && !gpuSkinningContractAttached ) { // set face planes, vertex normals, tangents R_DeriveTangents( tri ); } + + const uint64 cpuSkinEnd = R_GpuSkinning_ReadMicroseconds(); + const uint64 elapsedMicroseconds = cpuSkinEnd >= cpuSkinStart + ? cpuSkinEnd - cpuSkinStart : 0; + R_GpuSkinning_RecordCpuSkinning( elapsedMicroseconds, tri->numVerts, + gpuSkinningContractAttached ); } /* @@ -685,13 +1475,33 @@ void idRenderModelMD5::LoadModel() { idJointQuat *pose; idMD5Joint *joint; idJointMat *poseMat3; + const idStr sourceName = name; if ( !purged ) { PurgeModel(); } purged = false; + char cacheSettingsBuffer[1024]; + idStr::snPrintf( cacheSettingsBuffer, sizeof( cacheSettingsBuffer ), + "md5-payload=2;merge=%s;slopv=%s;slopt=%s;slopn=%s;silremap=%s;binary=%s;force-md5r=%s;convert-md5=%s", + r_mergeModelSurfaces.GetString(), r_slopVertex.GetString(), r_slopTexCoord.GetString(), + r_slopNormal.GetString(), cvarSystem->GetCVarString( "r_useSilRemap" ), + cvarSystem->GetCVarString( "com_binaryRead" ), cvarSystem->GetCVarString( "r_forceConvertMD5R" ), + cvarSystem->GetCVarString( "r_convertMD5toMD5R" ) ); + const idStr cacheSettings = cacheSettingsBuffer; + if ( R_TryReadGeneratedRenderModelCache( *this, sourceName.c_str(), + MD5_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ) ) { + if ( name.Icmp( sourceName ) == 0 ) { + return; + } + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, sourceName.c_str(), + MD5_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); + PurgeModel(); + name = sourceName; + purged = false; + } - if ( !parser.LoadFile( name ) ) { + if ( !parser.LoadFile( sourceName ) ) { MakeDefaultModel(); return; } @@ -764,7 +1574,9 @@ void idRenderModelMD5::LoadModel() { CalculateBounds( poseMat3 ); // set the timestamp for reloadmodels - fileSystem->ReadFile( name, NULL, &timeStamp ); + fileSystem->ReadFile( sourceName, NULL, &timeStamp ); + R_WriteGeneratedRenderModelCache( *this, sourceName.c_str(), + MD5_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); } /* @@ -946,6 +1758,7 @@ idRenderModel *idRenderModelMD5::InstantiateDynamicModel( const struct renderEnt idRenderModelStatic *staticModel; const bool collisionOnly = ( surfMask & SURF_COLLISION ) != 0; const idJointMat * entJoints = ent->joints; + bool gpuJointPaletteReady = false; if ( cachedModel && !r_useCachedDynamicModels.GetBool() ) { delete cachedModel; @@ -973,6 +1786,7 @@ idRenderModel *idRenderModelMD5::InstantiateDynamicModel( const struct renderEnt idJointMat *transformedJoints = (idJointMat *)_alloca16( joints.Num() * sizeof( transformedJoints[0] ) ); SIMDProcessor->MultiplyJoints( transformedJoints, ent->joints, skinSpaceToLocalMats.Ptr(), joints.Num() ); entJoints = transformedJoints; + gpuJointPaletteReady = true; } if ( cachedModel ) { @@ -1047,7 +1861,8 @@ idRenderModel *idRenderModelMD5::InstantiateDynamicModel( const struct renderEnt } if ( collisionOnly || mesh->UpdateLod( ent, viewEnt, surf ) ) { - mesh->UpdateSurface( ent, entJoints, surf, !collisionOnly ); + mesh->UpdateSurface( ent, entJoints, surf, !collisionOnly, + !collisionOnly && gpuJointPaletteReady ); } srfTriangles_t *frontTri = surf->geometry; diff --git a/src/renderer/Model_md5r.cpp b/src/renderer/Model_md5r.cpp index f8241946..1b094a70 100644 --- a/src/renderer/Model_md5r.cpp +++ b/src/renderer/Model_md5r.cpp @@ -107,6 +107,21 @@ static ID_TIME_T R_MD5R_ReadBinaryAwareTimestamp( const idStr &filename, idStr * return sourceTimeStamp; } +#ifndef ID_DEDICATED +static void R_MD5R_BuildGeneratedCacheSettings( idStr &settings, rvMD5RSource_t source ) { + char buffer[1024]; + idStr::snPrintf( buffer, sizeof( buffer ), + "md5r-payload=2;source=%d;merge=%s;slopv=%s;slopt=%s;slopn=%s;silremap=%s;binary=%s;force-md5r=%s;convert-md5=%s;convert-static=%s;packed=%d;deferred-tangents=%s", + static_cast( source ), cvarSystem->GetCVarString( "r_mergeModelSurfaces" ), + cvarSystem->GetCVarString( "r_slopVertex" ), cvarSystem->GetCVarString( "r_slopTexCoord" ), + cvarSystem->GetCVarString( "r_slopNormal" ), cvarSystem->GetCVarString( "r_useSilRemap" ), + cvarSystem->GetCVarString( "com_binaryRead" ), cvarSystem->GetCVarString( "r_forceConvertMD5R" ), + cvarSystem->GetCVarString( "r_convertMD5toMD5R" ), cvarSystem->GetCVarString( "r_convertStaticToMD5R" ), + R_MD5R_UsePackedRuntimeSurfaces() ? 1 : 0, cvarSystem->GetCVarString( "r_useDeferredTangents" ) ); + settings = buffer; +} +#endif + /* ======================== R_MD5R_GetDefaultLODRange @@ -168,6 +183,7 @@ static void R_MD5R_CopyAndReverseTriangles( const srfTriangles_t *src, srfTriang for ( int i = 0; i < tri->numVerts; ++i ) { tri->verts[ i ].normal = vec3_origin - tri->verts[ i ].normal; } + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ); for ( int i = 0; i < tri->numIndexes; i += 3 ) { tri->indexes[ i + 0 ] = src->indexes[ i + 1 ]; @@ -1927,10 +1943,11 @@ bool R_MD5R_CreateLightTris( const int drawIndex0 = batchDrawSource[ triNum * 3 + 0 ]; const int drawIndex1 = batchDrawSource[ triNum * 3 + 1 ]; const int drawIndex2 = batchDrawSource[ triNum * 3 + 2 ]; + const int drawVertexEnd = primBatch.drawGeoSpec.vertexStart + primBatch.drawGeoSpec.vertexCount; if ( drawIndex0 < 0 || drawIndex1 < 0 || drawIndex2 < 0 - || drawIndex0 >= primBatch.drawGeoSpec.vertexCount - || drawIndex1 >= primBatch.drawGeoSpec.vertexCount - || drawIndex2 >= primBatch.drawGeoSpec.vertexCount ) { + || drawIndex0 < primBatch.drawGeoSpec.vertexStart || drawIndex0 >= drawVertexEnd + || drawIndex1 < primBatch.drawGeoSpec.vertexStart || drawIndex1 >= drawVertexEnd + || drawIndex2 < primBatch.drawGeoSpec.vertexStart || drawIndex2 >= drawVertexEnd ) { R_ResizeStaticTriSurfIndexes( destTri, 0 ); destTri->numIndexes = 0; return false; @@ -1939,9 +1956,9 @@ bool R_MD5R_CreateLightTris( destSilTraceIndices[ emittedIndexCount + 0 ] = silTraceIndex0; destSilTraceIndices[ emittedIndexCount + 1 ] = silTraceIndex1; destSilTraceIndices[ emittedIndexCount + 2 ] = silTraceIndex2; - batchIndices[ emittedIndexCount + 0 ] = drawIndex0 + primBatch.drawGeoSpec.vertexStart; - batchIndices[ emittedIndexCount + 1 ] = drawIndex1 + primBatch.drawGeoSpec.vertexStart; - batchIndices[ emittedIndexCount + 2 ] = drawIndex2 + primBatch.drawGeoSpec.vertexStart; + batchIndices[ emittedIndexCount + 0 ] = drawIndex0; + batchIndices[ emittedIndexCount + 1 ] = drawIndex1; + batchIndices[ emittedIndexCount + 2 ] = drawIndex2; emittedIndexCount += 3; } @@ -2015,6 +2032,621 @@ void rvRenderModelMD5R::RemoveFromList( rvRenderModelMD5R &model ) { } } +namespace { + static const unsigned int MD5R_MODEL_CACHE_MAGIC = 0x5234514fU; // "OQ4R" + static const int MD5R_MODEL_CACHE_VERSION = 2; +#ifndef ID_DEDICATED + static const unsigned int MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION = 0x00030002U; +#endif + static const int MD5R_CACHE_MAX_NAME = 4096; + static const int MD5R_CACHE_MAX_JOINTS = 65536; + static const int MD5R_CACHE_MAX_BUFFERS = 65536; + static const int MD5R_CACHE_MAX_VERTICES = 1 << 22; + static const int MD5R_CACHE_MAX_INDICES = 1 << 24; + static const int MD5R_CACHE_MAX_SIL_EDGES = 1 << 23; + static const int MD5R_CACHE_MAX_LODS = 65536; + static const int MD5R_CACHE_MAX_MESHES = 65536; + static const int MD5R_CACHE_MAX_PRIM_BATCHES = 1 << 20; + static const int MD5R_CACHE_MAX_TRANSFORMS = 65536; + static const int MD5R_CACHE_MAX_MATERIALIZED_VERTICES = 1 << 18; + static const int MD5R_CACHE_MAX_MATERIALIZED_INDICES = 1 << 20; + + static bool R_MD5RCacheValidTokenType( int tokenType ) { + switch ( tokenType ) { + case MD5R_VERTEX_DATA_INT: + case MD5R_VERTEX_DATA_FLOAT: + case MD5R_VERTEX_DATA_FLOAT16: + case MD5R_VERTEX_DATA_UINT: + case MD5R_VERTEX_DATA_INTN: + case MD5R_VERTEX_DATA_UINTN: + case MD5R_VERTEX_DATA_COLOR: + case MD5R_VERTEX_DATA_UBYTE: + case MD5R_VERTEX_DATA_BYTE: + case MD5R_VERTEX_DATA_UBYTEN: + case MD5R_VERTEX_DATA_BYTEN: + case MD5R_VERTEX_DATA_SHORT: + case MD5R_VERTEX_DATA_USHORT: + case MD5R_VERTEX_DATA_SHORTN: + case MD5R_VERTEX_DATA_USHORTN: + case MD5R_VERTEX_DATA_UDEC_10_10_10: + case MD5R_VERTEX_DATA_DEC_10_10_10: + case MD5R_VERTEX_DATA_UDEC_10_10_10N: + case MD5R_VERTEX_DATA_DEC_10_10_10N: + case MD5R_VERTEX_DATA_UDEC_10_11_11: + case MD5R_VERTEX_DATA_DEC_10_11_11: + case MD5R_VERTEX_DATA_UDEC_10_11_11N: + case MD5R_VERTEX_DATA_DEC_10_11_11N: + case MD5R_VERTEX_DATA_UDEC_11_11_10: + case MD5R_VERTEX_DATA_DEC_11_11_10: + case MD5R_VERTEX_DATA_UDEC_11_11_10N: + case MD5R_VERTEX_DATA_DEC_11_11_10N: + return true; + default: + return false; + } + } + + static bool R_MD5RCacheWriteVertexFormat( idRenderModelCacheWriter &writer, const rvMD5RVertexFormatDesc &format ) { + if ( !writer.WriteBool( format.hasPosition ) || !writer.WriteBool( format.positionSwizzled ) + || !writer.WriteInt( format.positionDim ) || !writer.WriteInt( format.positionTokenType ) + || !writer.WriteBool( format.hasBlendIndex ) || !writer.WriteInt( format.blendIndexTokenType ) + || !writer.WriteBool( format.hasBlendWeight ) || !writer.WriteInt( format.blendWeightDim ) + || !writer.WriteInt( format.blendWeightTransformCount ) || !writer.WriteInt( format.blendWeightTokenType ) + || !writer.WriteBool( format.hasNormal ) || !writer.WriteInt( format.normalTokenType ) + || !writer.WriteBool( format.hasTangent ) || !writer.WriteInt( format.tangentTokenType ) + || !writer.WriteBool( format.hasBinormal ) || !writer.WriteInt( format.binormalTokenType ) + || !writer.WriteBool( format.hasDiffuseColor ) || !writer.WriteInt( format.diffuseColorTokenType ) + || !writer.WriteBool( format.hasSpecularColor ) || !writer.WriteInt( format.specularColorTokenType ) + || !writer.WriteBool( format.hasPointSize ) || !writer.WriteInt( format.pointSizeTokenType ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !writer.WriteBool( format.hasTexCoord[i] ) || !writer.WriteInt( format.texCoordDim[i] ) + || !writer.WriteInt( format.texCoordTokenType[i] ) ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheReadVertexFormat( idRenderModelCacheReader &reader, rvMD5RVertexFormatDesc &format ) { + if ( !reader.ReadBool( format.hasPosition ) || !reader.ReadBool( format.positionSwizzled ) + || !reader.ReadInt( format.positionDim ) || !reader.ReadInt( format.positionTokenType ) + || !reader.ReadBool( format.hasBlendIndex ) || !reader.ReadInt( format.blendIndexTokenType ) + || !reader.ReadBool( format.hasBlendWeight ) || !reader.ReadInt( format.blendWeightDim ) + || !reader.ReadInt( format.blendWeightTransformCount ) || !reader.ReadInt( format.blendWeightTokenType ) + || !reader.ReadBool( format.hasNormal ) || !reader.ReadInt( format.normalTokenType ) + || !reader.ReadBool( format.hasTangent ) || !reader.ReadInt( format.tangentTokenType ) + || !reader.ReadBool( format.hasBinormal ) || !reader.ReadInt( format.binormalTokenType ) + || !reader.ReadBool( format.hasDiffuseColor ) || !reader.ReadInt( format.diffuseColorTokenType ) + || !reader.ReadBool( format.hasSpecularColor ) || !reader.ReadInt( format.specularColorTokenType ) + || !reader.ReadBool( format.hasPointSize ) || !reader.ReadInt( format.pointSizeTokenType ) ) { + return false; + } + if ( ( format.hasPosition && ( format.positionDim < 1 || format.positionDim > 4 || !R_MD5RCacheValidTokenType( format.positionTokenType ) ) ) + || ( format.hasBlendIndex && !R_MD5RCacheValidTokenType( format.blendIndexTokenType ) ) + || ( format.hasBlendWeight && ( format.blendWeightDim < 1 || format.blendWeightDim > 4 + || format.blendWeightTransformCount < 1 || format.blendWeightTransformCount > 4 + || format.blendWeightTransformCount > format.blendWeightDim + 1 + || !R_MD5RCacheValidTokenType( format.blendWeightTokenType ) ) ) + || ( format.hasNormal && !R_MD5RCacheValidTokenType( format.normalTokenType ) ) + || ( format.hasTangent && !R_MD5RCacheValidTokenType( format.tangentTokenType ) ) + || ( format.hasBinormal && !R_MD5RCacheValidTokenType( format.binormalTokenType ) ) + || ( format.hasDiffuseColor && !R_MD5RCacheValidTokenType( format.diffuseColorTokenType ) ) + || ( format.hasSpecularColor && !R_MD5RCacheValidTokenType( format.specularColorTokenType ) ) + || ( format.hasPointSize && !R_MD5RCacheValidTokenType( format.pointSizeTokenType ) ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !reader.ReadBool( format.hasTexCoord[i] ) || !reader.ReadInt( format.texCoordDim[i] ) + || !reader.ReadInt( format.texCoordTokenType[i] ) + || ( format.hasTexCoord[i] && ( format.texCoordDim[i] < 1 || format.texCoordDim[i] > 4 + || !R_MD5RCacheValidTokenType( format.texCoordTokenType[i] ) ) ) ) { + return false; + } + } + return true; + } + + template + static bool R_MD5RCacheValidOptionalVertexList( const idList &list, int numVertices ) { + return list.Num() == 0 || list.Num() == numVertices; + } + + static bool R_MD5RCacheVertexDataMatchesFormat( const rvMD5RVertexBufferDesc &buffer ) { + const rvMD5RVertexFormatDesc &format = buffer.hasLoadVertexFormat + ? buffer.loadVertexFormat : buffer.vertexFormat; + if ( buffer.rebuildOnLoad ) { + return true; + } + if ( ( buffer.positions.Num() == buffer.numVertices ) != format.hasPosition + || ( buffer.blendIndices.Num() == buffer.numVertices ) != format.hasBlendIndex + || ( buffer.blendWeights.Num() == buffer.numVertices ) != format.hasBlendWeight + || ( buffer.normals.Num() == buffer.numVertices ) != format.hasNormal + || ( buffer.tangents.Num() == buffer.numVertices ) != format.hasTangent + || ( buffer.binormals.Num() == buffer.numVertices ) != format.hasBinormal + || ( buffer.diffuseColors.Num() == buffer.numVertices ) != format.hasDiffuseColor + || ( buffer.specularColors.Num() == buffer.numVertices ) != format.hasSpecularColor + || ( buffer.pointSizes.Num() == buffer.numVertices ) != format.hasPointSize ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( ( buffer.texCoords[i].Num() == buffer.numVertices ) != format.hasTexCoord[i] ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheWriteVec4List( idRenderModelCacheWriter &writer, const idList &list ) { + if ( !writer.WriteInt( list.Num() ) ) return false; + for ( int i = 0; i < list.Num(); ++i ) if ( !writer.WriteVec4( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheWriteVec3List( idRenderModelCacheWriter &writer, const idList &list ) { + if ( !writer.WriteInt( list.Num() ) ) return false; + for ( int i = 0; i < list.Num(); ++i ) if ( !writer.WriteVec3( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheWriteDwordList( idRenderModelCacheWriter &writer, const idList &list ) { + if ( !writer.WriteInt( list.Num() ) ) return false; + for ( int i = 0; i < list.Num(); ++i ) if ( !writer.WriteUnsignedInt( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheWriteFloatList( idRenderModelCacheWriter &writer, const idList &list ) { + if ( !writer.WriteInt( list.Num() ) ) return false; + for ( int i = 0; i < list.Num(); ++i ) if ( !writer.WriteFloat( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheReadVec4List( idRenderModelCacheReader &reader, idList &list, int maxCount ) { + int count = 0; + if ( !reader.ReadCount( count, maxCount, sizeof( idVec4 ) ) ) return false; + list.SetNum( count ); + for ( int i = 0; i < count; ++i ) if ( !reader.ReadVec4( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheReadVec3List( idRenderModelCacheReader &reader, idList &list, int maxCount ) { + int count = 0; + if ( !reader.ReadCount( count, maxCount, sizeof( idVec3 ) ) ) return false; + list.SetNum( count ); + for ( int i = 0; i < count; ++i ) if ( !reader.ReadVec3( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheReadDwordList( idRenderModelCacheReader &reader, idList &list, int maxCount ) { + int count = 0; + if ( !reader.ReadCount( count, maxCount, sizeof( dword ) ) ) return false; + list.SetNum( count ); + for ( int i = 0; i < count; ++i ) if ( !reader.ReadUnsignedInt( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheReadFloatList( idRenderModelCacheReader &reader, idList &list, int maxCount ) { + int count = 0; + if ( !reader.ReadCount( count, maxCount, sizeof( float ) ) ) return false; + list.SetNum( count ); + for ( int i = 0; i < count; ++i ) if ( !reader.ReadFloat( list[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheWriteJointQuat( idRenderModelCacheWriter &writer, const idJointQuat &pose ) { + return writer.WriteFloat( pose.q.x ) && writer.WriteFloat( pose.q.y ) + && writer.WriteFloat( pose.q.z ) && writer.WriteFloat( pose.q.w ) && writer.WriteVec3( pose.t ); + } + + static bool R_MD5RCacheReadJointQuat( idRenderModelCacheReader &reader, idJointQuat &pose ) { + pose.w = 0.0f; + return reader.ReadFloat( pose.q.x ) && reader.ReadFloat( pose.q.y ) + && reader.ReadFloat( pose.q.z ) && reader.ReadFloat( pose.q.w ) && reader.ReadVec3( pose.t ); + } + + static bool R_MD5RCacheWriteJointMat( idRenderModelCacheWriter &writer, const idJointMat &mat ) { + for ( int i = 0; i < 12; ++i ) if ( !writer.WriteFloat( mat.ToFloatPtr()[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheReadJointMat( idRenderModelCacheReader &reader, idJointMat &mat ) { + for ( int i = 0; i < 12; ++i ) if ( !reader.ReadFloat( mat.ToFloatPtr()[i] ) ) return false; + return true; + } + + static bool R_MD5RCacheCanWriteVertexFormat( const rvMD5RVertexFormatDesc &format ) { + if ( ( format.hasPosition && ( format.positionDim < 1 || format.positionDim > 4 || !R_MD5RCacheValidTokenType( format.positionTokenType ) ) ) + || ( format.hasBlendIndex && !R_MD5RCacheValidTokenType( format.blendIndexTokenType ) ) + || ( format.hasBlendWeight && ( format.blendWeightDim < 1 || format.blendWeightDim > 4 + || format.blendWeightTransformCount < 1 || format.blendWeightTransformCount > 4 + || format.blendWeightTransformCount > format.blendWeightDim + 1 + || !R_MD5RCacheValidTokenType( format.blendWeightTokenType ) ) ) + || ( format.hasNormal && !R_MD5RCacheValidTokenType( format.normalTokenType ) ) + || ( format.hasTangent && !R_MD5RCacheValidTokenType( format.tangentTokenType ) ) + || ( format.hasBinormal && !R_MD5RCacheValidTokenType( format.binormalTokenType ) ) + || ( format.hasDiffuseColor && !R_MD5RCacheValidTokenType( format.diffuseColorTokenType ) ) + || ( format.hasSpecularColor && !R_MD5RCacheValidTokenType( format.specularColorTokenType ) ) + || ( format.hasPointSize && !R_MD5RCacheValidTokenType( format.pointSizeTokenType ) ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( format.hasTexCoord[i] && ( format.texCoordDim[i] < 1 || format.texCoordDim[i] > 4 + || !R_MD5RCacheValidTokenType( format.texCoordTokenType[i] ) ) ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheWriteVertexBuffer( idRenderModelCacheWriter &writer, const rvMD5RVertexBufferDesc &buffer ) { + if ( buffer.numVertices <= 0 || buffer.numVertices > MD5R_CACHE_MAX_VERTICES + || !buffer.hasVertexFormat || !R_MD5RCacheCanWriteVertexFormat( buffer.vertexFormat ) + || !R_MD5RCacheCanWriteVertexFormat( buffer.loadVertexFormat ) + || !R_MD5RCacheValidOptionalVertexList( buffer.positions, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.blendIndices, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.blendWeights, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.normals, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.tangents, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.binormals, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.diffuseColors, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.specularColors, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.pointSizes, buffer.numVertices ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !R_MD5RCacheValidOptionalVertexList( buffer.texCoords[i], buffer.numVertices ) ) { + return false; + } + } + if ( !R_MD5RCacheVertexDataMatchesFormat( buffer ) ) { + return false; + } + + if ( !writer.WriteInt( buffer.numVertices ) || !writer.WriteBool( buffer.systemMemory ) + || !writer.WriteBool( buffer.videoMemory ) || !writer.WriteBool( buffer.soA ) + || !writer.WriteBool( buffer.hasVertexFormat ) || !writer.WriteBool( buffer.hasLoadVertexFormat ) + || !writer.WriteBool( buffer.rebuildOnLoad ) + || !R_MD5RCacheWriteVertexFormat( writer, buffer.vertexFormat ) + || !R_MD5RCacheWriteVertexFormat( writer, buffer.loadVertexFormat ) + || !R_MD5RCacheWriteVec4List( writer, buffer.positions ) + || !R_MD5RCacheWriteDwordList( writer, buffer.blendIndices ) + || !R_MD5RCacheWriteVec4List( writer, buffer.blendWeights ) + || !R_MD5RCacheWriteVec3List( writer, buffer.normals ) + || !R_MD5RCacheWriteVec3List( writer, buffer.tangents ) + || !R_MD5RCacheWriteVec3List( writer, buffer.binormals ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !R_MD5RCacheWriteVec4List( writer, buffer.texCoords[i] ) ) { + return false; + } + } + return R_MD5RCacheWriteDwordList( writer, buffer.diffuseColors ) + && R_MD5RCacheWriteDwordList( writer, buffer.specularColors ) + && R_MD5RCacheWriteFloatList( writer, buffer.pointSizes ); + } + + static bool R_MD5RCacheReadVertexBuffer( idRenderModelCacheReader &reader, rvMD5RVertexBufferDesc &buffer ) { + buffer = rvMD5RVertexBufferDesc(); + if ( !reader.ReadCount( buffer.numVertices, MD5R_CACHE_MAX_VERTICES ) || buffer.numVertices <= 0 + || !reader.ReadBool( buffer.systemMemory ) || !reader.ReadBool( buffer.videoMemory ) + || !reader.ReadBool( buffer.soA ) || !reader.ReadBool( buffer.hasVertexFormat ) + || !reader.ReadBool( buffer.hasLoadVertexFormat ) || !reader.ReadBool( buffer.rebuildOnLoad ) + || !buffer.hasVertexFormat || !R_MD5RCacheReadVertexFormat( reader, buffer.vertexFormat ) + || !R_MD5RCacheReadVertexFormat( reader, buffer.loadVertexFormat ) + || !R_MD5RCacheReadVec4List( reader, buffer.positions, buffer.numVertices ) + || !R_MD5RCacheReadDwordList( reader, buffer.blendIndices, buffer.numVertices ) + || !R_MD5RCacheReadVec4List( reader, buffer.blendWeights, buffer.numVertices ) + || !R_MD5RCacheReadVec3List( reader, buffer.normals, buffer.numVertices ) + || !R_MD5RCacheReadVec3List( reader, buffer.tangents, buffer.numVertices ) + || !R_MD5RCacheReadVec3List( reader, buffer.binormals, buffer.numVertices ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !R_MD5RCacheReadVec4List( reader, buffer.texCoords[i], buffer.numVertices ) ) { + return false; + } + } + if ( !R_MD5RCacheReadDwordList( reader, buffer.diffuseColors, buffer.numVertices ) + || !R_MD5RCacheReadDwordList( reader, buffer.specularColors, buffer.numVertices ) + || !R_MD5RCacheReadFloatList( reader, buffer.pointSizes, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.positions, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.blendIndices, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.blendWeights, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.normals, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.tangents, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.binormals, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.diffuseColors, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.specularColors, buffer.numVertices ) + || !R_MD5RCacheValidOptionalVertexList( buffer.pointSizes, buffer.numVertices ) ) { + return false; + } + for ( int i = 0; i < 7; ++i ) { + if ( !R_MD5RCacheValidOptionalVertexList( buffer.texCoords[i], buffer.numVertices ) ) { + return false; + } + } + return R_MD5RCacheVertexDataMatchesFormat( buffer ); + } + + static bool R_MD5RCacheWriteIndexBuffer( idRenderModelCacheWriter &writer, const rvMD5RIndexBufferDesc &buffer ) { + if ( buffer.numIndices <= 0 || buffer.numIndices > MD5R_CACHE_MAX_INDICES + || ( buffer.bitDepth != 16 && buffer.bitDepth != 32 ) || buffer.indices.Num() != buffer.numIndices + || !writer.WriteInt( buffer.numIndices ) || !writer.WriteInt( buffer.bitDepth ) + || !writer.WriteBool( buffer.systemMemory ) || !writer.WriteBool( buffer.videoMemory ) ) { + return false; + } + for ( int i = 0; i < buffer.numIndices; ++i ) { + const unsigned int index = static_cast( buffer.indices[i] ); + if ( index >= static_cast( MD5R_CACHE_MAX_VERTICES ) + || ( buffer.bitDepth == 16 && index > 0xffffU ) + || !writer.WriteInt( static_cast( index ) ) ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheReadIndexBuffer( idRenderModelCacheReader &reader, rvMD5RIndexBufferDesc &buffer ) { + buffer = rvMD5RIndexBufferDesc(); + if ( !reader.ReadCount( buffer.numIndices, MD5R_CACHE_MAX_INDICES, sizeof( glIndex_t ) ) || buffer.numIndices <= 0 + || !reader.ReadInt( buffer.bitDepth ) || ( buffer.bitDepth != 16 && buffer.bitDepth != 32 ) + || !reader.ReadBool( buffer.systemMemory ) || !reader.ReadBool( buffer.videoMemory ) ) { + return false; + } + buffer.indices.SetNum( buffer.numIndices ); + for ( int i = 0; i < buffer.numIndices; ++i ) { + int index = 0; + if ( !reader.ReadInt( index ) || index < 0 || index >= MD5R_CACHE_MAX_VERTICES + || ( buffer.bitDepth == 16 && index > 0xffff ) ) { + return false; + } + buffer.indices[i] = static_cast( index ); + } + return true; + } + + static bool R_MD5RCacheWriteGeometrySpec( idRenderModelCacheWriter &writer, const rvMD5RGeometrySpec &spec ) { + return writer.WriteInt( spec.vertexStart ) && writer.WriteInt( spec.vertexCount ) + && writer.WriteInt( spec.indexStart ) && writer.WriteInt( spec.primitiveCount ); + } + + static bool R_MD5RCacheReadGeometrySpec( idRenderModelCacheReader &reader, rvMD5RGeometrySpec &spec ) { + return reader.ReadInt( spec.vertexStart ) && reader.ReadInt( spec.vertexCount ) + && reader.ReadInt( spec.indexStart ) && reader.ReadInt( spec.primitiveCount ) + && spec.vertexStart >= 0 && spec.vertexCount >= 0 && spec.vertexCount <= MD5R_CACHE_MAX_VERTICES + && spec.indexStart >= 0 && spec.primitiveCount >= 0 + && spec.primitiveCount <= MD5R_CACHE_MAX_INDICES / 3; + } + + static bool R_MD5RCacheValidBufferPair( int vertexBuffer, int indexBuffer, int numVertexBuffers, int numIndexBuffers ) { + return ( vertexBuffer == -1 && indexBuffer == -1 ) + || ( vertexBuffer >= 0 && vertexBuffer < numVertexBuffers && indexBuffer >= 0 && indexBuffer < numIndexBuffers ); + } + + static bool R_MD5RCacheValidateGeometrySpec( + const rvMD5RGeometrySpec &spec, + const rvMD5RVertexBufferDesc &vertexBuffer, + const rvMD5RIndexBufferDesc *indexBuffer, + bool indicesAreLocal ) { + if ( spec.vertexStart < 0 || spec.vertexCount < 0 || spec.vertexStart > vertexBuffer.numVertices + || spec.vertexCount > vertexBuffer.numVertices - spec.vertexStart || spec.indexStart < 0 + || spec.primitiveCount < 0 || spec.primitiveCount > MD5R_CACHE_MAX_INDICES / 3 ) { + return false; + } + const int indexCount = spec.primitiveCount * 3; + if ( indexCount == 0 ) { + return spec.indexStart == 0 || ( indexBuffer != NULL && spec.indexStart <= indexBuffer->numIndices ); + } + if ( indexBuffer == NULL || indexBuffer->indices.Num() != indexBuffer->numIndices + || spec.indexStart > indexBuffer->numIndices || indexCount > indexBuffer->numIndices - spec.indexStart ) { + return false; + } + const int firstIndex = indicesAreLocal ? 0 : spec.vertexStart; + const int indexEnd = indicesAreLocal ? spec.vertexCount : spec.vertexStart + spec.vertexCount; + for ( int i = 0; i < indexCount; ++i ) { + const unsigned int index = static_cast( indexBuffer->indices[spec.indexStart + i] ); + if ( index < static_cast( firstIndex ) || index >= static_cast( indexEnd ) ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheGeometrySpecIsZero( const rvMD5RGeometrySpec &spec ) { + return spec.vertexStart == 0 && spec.vertexCount == 0 && spec.indexStart == 0 && spec.primitiveCount == 0; + } + + static bool R_MD5RCacheBoundsCanWrite( const idBounds &bounds ) { + for ( int axis = 0; axis < 3; ++axis ) { + if ( !R_RenderModelCacheFloatIsFinite( bounds[0][axis] ) + || !R_RenderModelCacheFloatIsFinite( bounds[1][axis] ) ) { + return false; + } + } + if ( bounds.IsCleared() ) { + return true; + } + for ( int axis = 0; axis < 3; ++axis ) { + if ( bounds[0][axis] > bounds[1][axis] ) { + return false; + } + } + return true; + } + + static bool R_MD5RCacheValidateSkinningRange( const rvMD5RPrimBatch &batch, + const rvMD5RVertexBufferDesc &buffer, const rvMD5RGeometrySpec &spec, + int jointCount ) { + if ( jointCount <= 0 ) { + return batch.transformPalette.Num() == 0; + } + const int localTransformCount = batch.numTransforms > 0 ? batch.numTransforms : 1; + if ( localTransformCount > 256 + || ( batch.transformPalette.Num() == 0 && localTransformCount > jointCount ) ) { + return false; + } + if ( buffer.blendIndices.Num() == 0 ) { + return true; + } + if ( buffer.blendIndices.Num() != buffer.numVertices ) { + return false; + } + const rvMD5RVertexFormatDesc &format = buffer.hasLoadVertexFormat + ? buffer.loadVertexFormat : buffer.vertexFormat; + const int influenceCount = format.hasBlendWeight ? format.blendWeightTransformCount : 1; + if ( influenceCount < 1 || influenceCount > 4 ) { + return false; + } + for ( int vertexIndex = spec.vertexStart; vertexIndex < spec.vertexStart + spec.vertexCount; ++vertexIndex ) { + const dword packedIndices = buffer.blendIndices[vertexIndex]; + for ( int influenceIndex = 0; influenceIndex < influenceCount; ++influenceIndex ) { + const unsigned int localIndex = ( packedIndices >> ( influenceIndex * 8 ) ) & 0xffU; + if ( localIndex >= static_cast( localTransformCount ) ) { + return false; + } + } + } + return true; + } + + static bool R_MD5RCacheValidateMesh( + const rvMD5RMesh &mesh, + const idList &vertexBuffers, + const idList &indexBuffers, + const idList &silEdges, + int jointCount, + int lodCount ) { + if ( mesh.materialName.IsEmpty() || mesh.materialName.Length() > MD5R_CACHE_MAX_NAME + || mesh.levelOfDetail < -1 || mesh.levelOfDetail >= lodCount + || mesh.meshIdentifier < 0 || mesh.meshIdentifier > 0x3fffffff + || !R_MD5RCacheValidBufferPair( mesh.silTraceVertexBuffer, mesh.silTraceIndexBuffer, vertexBuffers.Num(), indexBuffers.Num() ) + || !R_MD5RCacheValidBufferPair( mesh.drawVertexBuffer, mesh.drawIndexBuffer, vertexBuffers.Num(), indexBuffers.Num() ) + || !R_MD5RCacheValidBufferPair( mesh.shadowVolVertexBuffer, mesh.shadowVolIndexBuffer, vertexBuffers.Num(), indexBuffers.Num() ) + || mesh.primBatches.Num() < 0 || mesh.primBatches.Num() > MD5R_CACHE_MAX_PRIM_BATCHES ) { + return false; + } + + int numSilTraceVertices = 0; + int numSilTraceIndices = 0; + int numSilTracePrimitives = 0; + int numSilEdges = 0; + int numDrawVertices = 0; + int numDrawIndices = 0; + int numDrawPrimitives = 0; + int numTransforms = 0; + int expectedSilEdgeStart = mesh.primBatches.Num() > 0 ? mesh.primBatches[0].silEdgeStart : 0; + for ( int batchIndex = 0; batchIndex < mesh.primBatches.Num(); ++batchIndex ) { + const rvMD5RPrimBatch &batch = mesh.primBatches[batchIndex]; + if ( batch.numTransforms < 0 || batch.numTransforms > MD5R_CACHE_MAX_TRANSFORMS + || ( batch.transformPalette.Num() != 0 && batch.transformPalette.Num() != batch.numTransforms ) + || batch.numShadowPrimitivesNoCaps < 0 || batch.shadowCapPlaneBits < 0 || batch.shadowCapPlaneBits > 127 + || batch.silEdgeStart < 0 || batch.silEdgeCount < 0 + || batch.silEdgeStart > silEdges.Num() || batch.silEdgeCount > silEdges.Num() - batch.silEdgeStart + || ( mesh.numSilEdges > 0 && batch.silEdgeStart != expectedSilEdgeStart ) ) { + return false; + } + for ( int transformIndex = 0; transformIndex < batch.transformPalette.Num(); ++transformIndex ) { + if ( batch.transformPalette[transformIndex] < 0 || batch.transformPalette[transformIndex] >= jointCount ) { + return false; + } + } + + if ( batch.hasSilTraceGeoSpec ) { + if ( mesh.silTraceVertexBuffer < 0 + || !R_MD5RCacheValidateGeometrySpec( batch.silTraceGeoSpec, + vertexBuffers[mesh.silTraceVertexBuffer], &indexBuffers[mesh.silTraceIndexBuffer], true ) + || !R_MD5RCacheValidateSkinningRange( batch, vertexBuffers[mesh.silTraceVertexBuffer], + batch.silTraceGeoSpec, jointCount ) ) { + return false; + } + } else if ( !R_MD5RCacheGeometrySpecIsZero( batch.silTraceGeoSpec ) ) { + return false; + } + if ( batch.hasDrawGeoSpec ) { + if ( mesh.drawVertexBuffer < 0 + || !R_MD5RCacheValidateGeometrySpec( batch.drawGeoSpec, + vertexBuffers[mesh.drawVertexBuffer], &indexBuffers[mesh.drawIndexBuffer], false ) + || !R_MD5RCacheValidateSkinningRange( batch, vertexBuffers[mesh.drawVertexBuffer], + batch.drawGeoSpec, jointCount ) ) { + return false; + } + } else if ( !R_MD5RCacheGeometrySpecIsZero( batch.drawGeoSpec ) ) { + return false; + } + if ( batch.hasShadowGeoSpec ) { + if ( mesh.shadowVolVertexBuffer < 0 + || !R_MD5RCacheValidateGeometrySpec( batch.shadowVolGeoSpec, + vertexBuffers[mesh.shadowVolVertexBuffer], + batch.shadowVolGeoSpec.primitiveCount > 0 ? &indexBuffers[mesh.shadowVolIndexBuffer] : NULL, false ) + || !R_MD5RCacheValidateSkinningRange( batch, vertexBuffers[mesh.shadowVolVertexBuffer], + batch.shadowVolGeoSpec, jointCount ) + || batch.numShadowPrimitivesNoCaps > batch.shadowVolGeoSpec.primitiveCount ) { + return false; + } + } else if ( !R_MD5RCacheGeometrySpecIsZero( batch.shadowVolGeoSpec ) + || batch.numShadowPrimitivesNoCaps != 0 || batch.shadowCapPlaneBits != 0 ) { + return false; + } + + if ( batch.silEdgeCount > 0 ) { + if ( !batch.hasSilTraceGeoSpec || batch.silTraceGeoSpec.vertexCount <= 0 + || batch.silTraceGeoSpec.primitiveCount <= 0 ) { + return false; + } + for ( int edgeIndex = 0; edgeIndex < batch.silEdgeCount; ++edgeIndex ) { + const silEdge_t &edge = silEdges[batch.silEdgeStart + edgeIndex]; + if ( edge.p1 < 0 || edge.p1 >= batch.silTraceGeoSpec.primitiveCount + // The standard silhouette builder uses planeCount as the + // dangling/open-edge sentinel for p2. Preserve that exact + // representation while still rejecting out-of-batch planes. + || edge.p2 < 0 || edge.p2 > batch.silTraceGeoSpec.primitiveCount + || edge.v1 < 0 || edge.v1 >= batch.silTraceGeoSpec.vertexCount + || edge.v2 < 0 || edge.v2 >= batch.silTraceGeoSpec.vertexCount ) { + return false; + } + } + } + expectedSilEdgeStart += batch.silEdgeCount; + + const int silTraceIndexes = batch.silTraceGeoSpec.primitiveCount * 3; + const int drawIndexes = batch.drawGeoSpec.primitiveCount * 3; + if ( ( batch.hasSilTraceGeoSpec && ( batch.silTraceGeoSpec.vertexCount > MD5R_CACHE_MAX_VERTICES - numSilTraceVertices + || silTraceIndexes > MD5R_CACHE_MAX_INDICES - numSilTraceIndices + || batch.silTraceGeoSpec.primitiveCount > MD5R_CACHE_MAX_INDICES / 3 - numSilTracePrimitives ) ) + || batch.silEdgeCount > MD5R_CACHE_MAX_SIL_EDGES - numSilEdges + || ( batch.hasDrawGeoSpec && ( batch.drawGeoSpec.vertexCount > MD5R_CACHE_MAX_VERTICES - numDrawVertices + || drawIndexes > MD5R_CACHE_MAX_INDICES - numDrawIndices + || batch.drawGeoSpec.primitiveCount > MD5R_CACHE_MAX_INDICES / 3 - numDrawPrimitives ) ) + || batch.numTransforms > MD5R_CACHE_MAX_TRANSFORMS - numTransforms ) { + return false; + } + if ( batch.hasSilTraceGeoSpec ) { + numSilTraceVertices += batch.silTraceGeoSpec.vertexCount; + numSilTraceIndices += silTraceIndexes; + numSilTracePrimitives += batch.silTraceGeoSpec.primitiveCount; + } + numSilEdges += batch.silEdgeCount; + if ( batch.hasDrawGeoSpec ) { + numDrawVertices += batch.drawGeoSpec.vertexCount; + numDrawIndices += drawIndexes; + numDrawPrimitives += batch.drawGeoSpec.primitiveCount; + } + numTransforms += batch.numTransforms; + } + + return mesh.numSilTraceVertices == numSilTraceVertices + && mesh.numSilTraceIndices == numSilTraceIndices + && mesh.numSilTracePrimitives == numSilTracePrimitives + && mesh.numSilEdges == numSilEdges + && mesh.numDrawVertices == numDrawVertices + && mesh.numDrawIndices == numDrawIndices + && mesh.numDrawPrimitives == numDrawPrimitives + && mesh.numTransforms == numTransforms; + } +} + /* ======================== rvRenderModelMD5R::rvRenderModelMD5R @@ -2042,6 +2674,502 @@ rvRenderModelMD5R::~rvRenderModelMD5R() { RemoveFromList( *this ); } +renderModelCacheType_t rvRenderModelMD5R::LevelLoadCachePayloadType() const { + return RENDER_MODEL_CACHE_MD5R; +} + +bool rvRenderModelMD5R::WriteLevelLoadCachePayload( idFile &file ) const { + const idList &resolvedVertexBuffers = GetVertexBuffers(); + const idList &resolvedIndexBuffers = GetIndexBuffers(); + const idList &resolvedSilEdges = GetSilhouetteEdges(); + if ( defaulted || purged || name.IsEmpty() || name.Length() > MD5R_CACHE_MAX_NAME + || !R_MD5RCacheBoundsCanWrite( bounds ) || md5rVersion != MD5R_VERSION + || ( metadataOnly && !geometrySectionsSkipped ) + || source < MD5R_SOURCE_FILE || source > MD5R_SOURCE_PROC + || resolvedVertexBuffers.Num() < 0 || resolvedVertexBuffers.Num() > MD5R_CACHE_MAX_BUFFERS + || resolvedIndexBuffers.Num() < 0 || resolvedIndexBuffers.Num() > MD5R_CACHE_MAX_BUFFERS + || resolvedSilEdges.Num() < 0 || resolvedSilEdges.Num() > MD5R_CACHE_MAX_SIL_EDGES + || lods.Num() < 0 || lods.Num() > MD5R_CACHE_MAX_LODS + || meshes.Num() < 0 || meshes.Num() > MD5R_CACHE_MAX_MESHES + || joints.Num() < 0 || joints.Num() > MD5R_CACHE_MAX_JOINTS + || defaultPose.Num() != joints.Num() || skinSpaceToLocalMats.Num() != joints.Num() ) { + return false; + } + + idRenderModelCacheWriter writer( file ); + if ( !writer.WriteUnsignedInt( MD5R_MODEL_CACHE_MAGIC ) || !writer.WriteInt( MD5R_MODEL_CACHE_VERSION ) + || !writer.WriteString( name.c_str(), MD5R_CACHE_MAX_NAME ) + || !writer.WriteUnsigned64( static_cast( static_cast( timeStamp ) ) ) + || !writer.WriteBounds( bounds ) || !writer.WriteBool( isStaticWorldModel ) + || !writer.WriteBool( defaulted ) || !writer.WriteBool( purged ) || !writer.WriteBool( fastLoad ) + || !writer.WriteBool( reloadable ) || !writer.WriteBool( procSky ) + || !writer.WriteString( commandLine.c_str(), MD5R_CACHE_MAX_NAME ) + || !writer.WriteInt( md5rVersion ) || !writer.WriteBool( metadataOnly ) + || !writer.WriteBool( geometrySectionsSkipped ) || !writer.WriteBool( hasSky ) + || !writer.WriteInt( static_cast( source ) ) || !writer.WriteInt( joints.Num() ) ) { + return false; + } + + for ( int jointIndex = 0; jointIndex < joints.Num(); ++jointIndex ) { + int parentIndex = -1; + if ( joints[jointIndex].parent != NULL ) { + for ( int candidate = 0; candidate < jointIndex; ++candidate ) { + if ( joints[jointIndex].parent == &joints[candidate] ) { + parentIndex = candidate; + break; + } + } + if ( parentIndex < 0 ) { + return false; + } + } + if ( joints[jointIndex].name.IsEmpty() + || !writer.WriteString( joints[jointIndex].name.c_str(), MD5R_CACHE_MAX_NAME ) + || !writer.WriteInt( parentIndex ) ) { + return false; + } + } + if ( !writer.WriteInt( defaultPose.Num() ) ) { + return false; + } + for ( int jointIndex = 0; jointIndex < defaultPose.Num(); ++jointIndex ) { + if ( !R_MD5RCacheWriteJointQuat( writer, defaultPose[jointIndex] ) ) { + return false; + } + } + if ( !writer.WriteInt( skinSpaceToLocalMats.Num() ) ) { + return false; + } + for ( int jointIndex = 0; jointIndex < skinSpaceToLocalMats.Num(); ++jointIndex ) { + if ( !R_MD5RCacheWriteJointMat( writer, skinSpaceToLocalMats[jointIndex] ) ) { + return false; + } + } + + if ( !writer.WriteInt( resolvedVertexBuffers.Num() ) ) { + return false; + } + for ( int bufferIndex = 0; bufferIndex < resolvedVertexBuffers.Num(); ++bufferIndex ) { + if ( !R_MD5RCacheWriteVertexBuffer( writer, resolvedVertexBuffers[bufferIndex] ) ) { + return false; + } + } + if ( !writer.WriteInt( resolvedIndexBuffers.Num() ) ) { + return false; + } + for ( int bufferIndex = 0; bufferIndex < resolvedIndexBuffers.Num(); ++bufferIndex ) { + if ( !R_MD5RCacheWriteIndexBuffer( writer, resolvedIndexBuffers[bufferIndex] ) ) { + return false; + } + } + + if ( !writer.WriteInt( resolvedSilEdges.Num() ) ) { + return false; + } + for ( int edgeIndex = 0; edgeIndex < resolvedSilEdges.Num(); ++edgeIndex ) { + const silEdge_t &edge = resolvedSilEdges[edgeIndex]; + if ( edge.p1 < 0 || edge.p1 > MD5R_CACHE_MAX_INDICES / 3 + || edge.p2 < 0 || edge.p2 > MD5R_CACHE_MAX_INDICES / 3 + || edge.v1 < 0 || edge.v1 >= MD5R_CACHE_MAX_VERTICES + || edge.v2 < 0 || edge.v2 >= MD5R_CACHE_MAX_VERTICES + || !writer.WriteInt( edge.p1 ) || !writer.WriteInt( edge.p2 ) + || !writer.WriteInt( edge.v1 ) || !writer.WriteInt( edge.v2 ) ) { + return false; + } + } + + if ( !writer.WriteInt( lods.Num() ) ) { + return false; + } + float previousLODRange = 0.0f; + for ( int lodIndex = 0; lodIndex < lods.Num(); ++lodIndex ) { + const rvMD5RLevelOfDetail &lod = lods[lodIndex]; + const float expectedSquared = lod.rangeEnd * lod.rangeEnd; + if ( !R_RenderModelCacheFloatIsFinite( lod.rangeEnd ) || lod.rangeEnd < 0.0f + || ( lodIndex > 0 && lod.rangeEnd < previousLODRange ) + || !R_RenderModelCacheFloatIsFinite( lod.rangeEndSquared ) + || lod.rangeEndSquared != expectedSquared + || !writer.WriteFloat( lod.rangeEnd ) || !writer.WriteFloat( lod.rangeEndSquared ) ) { + return false; + } + previousLODRange = lod.rangeEnd; + } + + if ( !writer.WriteInt( meshes.Num() ) ) { + return false; + } + int totalPrimBatches = 0; + for ( int meshIndex = 0; meshIndex < meshes.Num(); ++meshIndex ) { + const rvMD5RMesh &mesh = meshes[meshIndex]; + const idMaterial *material = mesh.material; + if ( mesh.primBatches.Num() > MD5R_CACHE_MAX_PRIM_BATCHES - totalPrimBatches + || material == NULL || idStr::Icmp( material->GetName(), mesh.materialName.c_str() ) != 0 + || !R_MD5RCacheBoundsCanWrite( mesh.bounds ) + || !R_MD5RCacheValidateMesh( mesh, resolvedVertexBuffers, resolvedIndexBuffers, + resolvedSilEdges, joints.Num(), lods.Num() ) ) { + return false; + } + totalPrimBatches += mesh.primBatches.Num(); + if ( !writer.WriteString( mesh.materialName.c_str(), MD5R_CACHE_MAX_NAME ) + || !writer.WriteBool( material->UseUnsmoothedTangents() ) + || !writer.WriteBool( material->ShouldCreateBackSides() ) + || !writer.WriteBool( material->SurfaceCastsShadow() ) + || !writer.WriteBool( material->IsDiscrete() ) + || !writer.WriteBounds( mesh.bounds ) || !writer.WriteInt( mesh.levelOfDetail ) + || !writer.WriteInt( mesh.meshIdentifier ) || !writer.WriteInt( mesh.silTraceVertexBuffer ) + || !writer.WriteInt( mesh.silTraceIndexBuffer ) || !writer.WriteInt( mesh.drawVertexBuffer ) + || !writer.WriteInt( mesh.drawIndexBuffer ) || !writer.WriteInt( mesh.shadowVolVertexBuffer ) + || !writer.WriteInt( mesh.shadowVolIndexBuffer ) || !writer.WriteInt( mesh.numSilTraceVertices ) + || !writer.WriteInt( mesh.numSilTraceIndices ) || !writer.WriteInt( mesh.numSilTracePrimitives ) + || !writer.WriteInt( mesh.numSilEdges ) || !writer.WriteInt( mesh.numDrawVertices ) + || !writer.WriteInt( mesh.numDrawIndices ) || !writer.WriteInt( mesh.numDrawPrimitives ) + || !writer.WriteInt( mesh.numTransforms ) || !writer.WriteInt( mesh.primBatches.Num() ) ) { + return false; + } + for ( int batchIndex = 0; batchIndex < mesh.primBatches.Num(); ++batchIndex ) { + const rvMD5RPrimBatch &batch = mesh.primBatches[batchIndex]; + if ( !writer.WriteInt( batch.numTransforms ) || !writer.WriteInt( batch.transformPalette.Num() ) ) { + return false; + } + for ( int transformIndex = 0; transformIndex < batch.transformPalette.Num(); ++transformIndex ) { + if ( !writer.WriteInt( batch.transformPalette[transformIndex] ) ) { + return false; + } + } + if ( !R_MD5RCacheWriteGeometrySpec( writer, batch.silTraceGeoSpec ) + || !R_MD5RCacheWriteGeometrySpec( writer, batch.drawGeoSpec ) + || !R_MD5RCacheWriteGeometrySpec( writer, batch.shadowVolGeoSpec ) + || !writer.WriteInt( batch.numShadowPrimitivesNoCaps ) + || !writer.WriteInt( batch.shadowCapPlaneBits ) || !writer.WriteInt( batch.silEdgeStart ) + || !writer.WriteInt( batch.silEdgeCount ) || !writer.WriteBool( batch.hasSilTraceGeoSpec ) + || !writer.WriteBool( batch.hasDrawGeoSpec ) || !writer.WriteBool( batch.hasShadowGeoSpec ) ) { + return false; + } + } + } + return writer.IsValid(); +} + +bool rvRenderModelMD5R::ReadLevelLoadCachePayload( idFile &file ) { + idRenderModelCacheReader reader( file ); + unsigned int magic = 0; + int version = 0; + idStr decodedName; + uint64_t encodedTimestamp = 0; + idBounds decodedBounds; + bool decodedStaticWorld = false; + bool decodedDefaulted = false; + bool decodedPurged = false; + bool decodedFastLoad = false; + bool decodedReloadable = false; + bool decodedProcSky = false; + idStr decodedCommandLine; + int decodedMD5RVersion = 0; + bool decodedMetadataOnly = false; + bool decodedGeometrySectionsSkipped = false; + bool decodedHasSky = false; + int decodedSource = 0; + if ( !reader.ReadUnsignedInt( magic ) || magic != MD5R_MODEL_CACHE_MAGIC + || !reader.ReadInt( version ) || version != MD5R_MODEL_CACHE_VERSION + || !reader.ReadString( decodedName, MD5R_CACHE_MAX_NAME ) || decodedName.IsEmpty() + || !reader.ReadUnsigned64( encodedTimestamp ) || !reader.ReadBounds( decodedBounds, true ) + || !reader.ReadBool( decodedStaticWorld ) || !reader.ReadBool( decodedDefaulted ) + || !reader.ReadBool( decodedPurged ) || !reader.ReadBool( decodedFastLoad ) + || !reader.ReadBool( decodedReloadable ) || !reader.ReadBool( decodedProcSky ) + || !reader.ReadString( decodedCommandLine, MD5R_CACHE_MAX_NAME ) + || !reader.ReadInt( decodedMD5RVersion ) || decodedMD5RVersion != MD5R_VERSION + || !reader.ReadBool( decodedMetadataOnly ) || !reader.ReadBool( decodedGeometrySectionsSkipped ) + || !reader.ReadBool( decodedHasSky ) || !reader.ReadInt( decodedSource ) + || decodedDefaulted || decodedPurged || ( decodedMetadataOnly && !decodedGeometrySectionsSkipped ) + || decodedSource < MD5R_SOURCE_FILE || decodedSource > MD5R_SOURCE_PROC ) { + return false; + } + const ID_TIME_T decodedTimestamp = static_cast( static_cast( encodedTimestamp ) ); + if ( static_cast( static_cast( decodedTimestamp ) ) != encodedTimestamp ) { + return false; + } + + rvRenderModelMD5R staged; + staged.name = decodedName; + staged.timeStamp = decodedTimestamp; + staged.bounds = decodedBounds; + staged.isStaticWorldModel = decodedStaticWorld; + staged.defaulted = false; + staged.purged = false; + staged.fastLoad = decodedFastLoad; + staged.reloadable = decodedReloadable; + staged.procSky = decodedProcSky; + staged.commandLine = decodedCommandLine; + staged.md5rVersion = decodedMD5RVersion; + staged.metadataOnly = decodedMetadataOnly; + staged.geometrySectionsSkipped = decodedGeometrySectionsSkipped; + staged.hasSky = decodedHasSky; + staged.source = static_cast( decodedSource ); + + int jointCount = 0; + if ( !reader.ReadCount( jointCount, MD5R_CACHE_MAX_JOINTS ) + || !reader.Reserve( jointCount, sizeof( idMD5Joint ) + sizeof( int ) + sizeof( idJointQuat ) + sizeof( idJointMat ) ) ) { + return false; + } + idList jointNames; + idList jointParents; + jointNames.SetNum( jointCount ); + jointParents.SetNum( jointCount ); + for ( int jointIndex = 0; jointIndex < jointCount; ++jointIndex ) { + if ( !reader.ReadString( jointNames[jointIndex], MD5R_CACHE_MAX_NAME ) || jointNames[jointIndex].IsEmpty() + || !reader.ReadInt( jointParents[jointIndex] ) + || jointParents[jointIndex] < -1 || jointParents[jointIndex] >= jointIndex ) { + return false; + } + } + int poseCount = 0; + if ( !reader.ReadCount( poseCount, MD5R_CACHE_MAX_JOINTS ) || poseCount != jointCount ) { + return false; + } + staged.defaultPose.SetNum( poseCount ); + for ( int jointIndex = 0; jointIndex < poseCount; ++jointIndex ) { + if ( !R_MD5RCacheReadJointQuat( reader, staged.defaultPose[jointIndex] ) ) { + return false; + } + } + int skinMatCount = 0; + if ( !reader.ReadCount( skinMatCount, MD5R_CACHE_MAX_JOINTS ) || skinMatCount != jointCount ) { + return false; + } + staged.skinSpaceToLocalMats.SetNum( skinMatCount ); + for ( int jointIndex = 0; jointIndex < skinMatCount; ++jointIndex ) { + if ( !R_MD5RCacheReadJointMat( reader, staged.skinSpaceToLocalMats[jointIndex] ) ) { + return false; + } + } + staged.joints.SetNum( jointCount ); + for ( int jointIndex = 0; jointIndex < jointCount; ++jointIndex ) { + staged.joints[jointIndex].name = jointNames[jointIndex]; + staged.joints[jointIndex].parent = ( jointParents[jointIndex] >= 0 ) + ? &staged.joints[jointParents[jointIndex]] : NULL; + } + + int vertexBufferCount = 0; + if ( !reader.ReadCount( vertexBufferCount, MD5R_CACHE_MAX_BUFFERS, sizeof( rvMD5RVertexBufferDesc ) ) ) { + return false; + } + staged.vertexBuffers.SetNum( vertexBufferCount ); + for ( int bufferIndex = 0; bufferIndex < vertexBufferCount; ++bufferIndex ) { + if ( !R_MD5RCacheReadVertexBuffer( reader, staged.vertexBuffers[bufferIndex] ) ) { + return false; + } + } + int indexBufferCount = 0; + if ( !reader.ReadCount( indexBufferCount, MD5R_CACHE_MAX_BUFFERS, sizeof( rvMD5RIndexBufferDesc ) ) ) { + return false; + } + staged.indexBuffers.SetNum( indexBufferCount ); + for ( int bufferIndex = 0; bufferIndex < indexBufferCount; ++bufferIndex ) { + if ( !R_MD5RCacheReadIndexBuffer( reader, staged.indexBuffers[bufferIndex] ) ) { + return false; + } + + } + + int silEdgeCount = 0; + if ( !reader.ReadCount( silEdgeCount, MD5R_CACHE_MAX_SIL_EDGES, sizeof( silEdge_t ) ) ) { + return false; + } + staged.silEdges.SetNum( silEdgeCount ); + for ( int edgeIndex = 0; edgeIndex < silEdgeCount; ++edgeIndex ) { + silEdge_t &edge = staged.silEdges[edgeIndex]; + if ( !reader.ReadInt( edge.p1 ) || !reader.ReadInt( edge.p2 ) + || !reader.ReadInt( edge.v1 ) || !reader.ReadInt( edge.v2 ) + || edge.p1 < 0 || edge.p1 > MD5R_CACHE_MAX_INDICES / 3 + || edge.p2 < 0 || edge.p2 > MD5R_CACHE_MAX_INDICES / 3 + || edge.v1 < 0 || edge.v1 >= MD5R_CACHE_MAX_VERTICES + || edge.v2 < 0 || edge.v2 >= MD5R_CACHE_MAX_VERTICES ) { + return false; + } + } + + int lodCount = 0; + if ( !reader.ReadCount( lodCount, MD5R_CACHE_MAX_LODS, sizeof( rvMD5RLevelOfDetail ) ) ) { + return false; + } + staged.lods.SetNum( lodCount ); + float previousLODRange = 0.0f; + for ( int lodIndex = 0; lodIndex < lodCount; ++lodIndex ) { + rvMD5RLevelOfDetail &lod = staged.lods[lodIndex]; + if ( !reader.ReadFloat( lod.rangeEnd ) || lod.rangeEnd < 0.0f + || ( lodIndex > 0 && lod.rangeEnd < previousLODRange ) + || !reader.ReadFloat( lod.rangeEndSquared ) + || lod.rangeEndSquared != lod.rangeEnd * lod.rangeEnd ) { + return false; + } + previousLODRange = lod.rangeEnd; + } + + int meshCount = 0; + if ( !reader.ReadCount( meshCount, MD5R_CACHE_MAX_MESHES, sizeof( rvMD5RMesh ) ) ) { + return false; + } + staged.meshes.SetNum( meshCount ); + idList decodedMaterialFlags; + decodedMaterialFlags.SetNum( meshCount ); + int totalPrimBatches = 0; + for ( int meshIndex = 0; meshIndex < meshCount; ++meshIndex ) { + rvMD5RMesh &mesh = staged.meshes[meshIndex]; + mesh = rvMD5RMesh(); + bool materialUnsmoothedTangents = false; + bool materialCreatesBackSides = false; + bool materialCastsShadow = false; + bool materialDiscrete = false; + if ( !reader.ReadString( mesh.materialName, MD5R_CACHE_MAX_NAME ) || mesh.materialName.IsEmpty() + || !reader.ReadBool( materialUnsmoothedTangents ) + || !reader.ReadBool( materialCreatesBackSides ) + || !reader.ReadBool( materialCastsShadow ) || !reader.ReadBool( materialDiscrete ) + || !reader.ReadBounds( mesh.bounds, true ) || !reader.ReadInt( mesh.levelOfDetail ) + || !reader.ReadInt( mesh.meshIdentifier ) || !reader.ReadInt( mesh.silTraceVertexBuffer ) + || !reader.ReadInt( mesh.silTraceIndexBuffer ) || !reader.ReadInt( mesh.drawVertexBuffer ) + || !reader.ReadInt( mesh.drawIndexBuffer ) || !reader.ReadInt( mesh.shadowVolVertexBuffer ) + || !reader.ReadInt( mesh.shadowVolIndexBuffer ) + || !reader.ReadCount( mesh.numSilTraceVertices, MD5R_CACHE_MAX_VERTICES ) + || !reader.ReadCount( mesh.numSilTraceIndices, MD5R_CACHE_MAX_INDICES ) + || !reader.ReadCount( mesh.numSilTracePrimitives, MD5R_CACHE_MAX_INDICES / 3 ) + || !reader.ReadCount( mesh.numSilEdges, MD5R_CACHE_MAX_SIL_EDGES ) + || !reader.ReadCount( mesh.numDrawVertices, MD5R_CACHE_MAX_VERTICES ) + || !reader.ReadCount( mesh.numDrawIndices, MD5R_CACHE_MAX_INDICES ) + || !reader.ReadCount( mesh.numDrawPrimitives, MD5R_CACHE_MAX_INDICES / 3 ) + || !reader.ReadCount( mesh.numTransforms, MD5R_CACHE_MAX_TRANSFORMS ) ) { + return false; + } + decodedMaterialFlags[meshIndex] = static_cast( ( materialUnsmoothedTangents ? 1 : 0 ) + | ( materialCreatesBackSides ? 2 : 0 ) | ( materialCastsShadow ? 4 : 0 ) + | ( materialDiscrete ? 8 : 0 ) ); + int primBatchCount = 0; + if ( !reader.ReadCount( primBatchCount, MD5R_CACHE_MAX_PRIM_BATCHES, sizeof( rvMD5RPrimBatch ) ) + || primBatchCount > MD5R_CACHE_MAX_PRIM_BATCHES - totalPrimBatches ) { + return false; + } + totalPrimBatches += primBatchCount; + mesh.primBatches.SetNum( primBatchCount ); + for ( int batchIndex = 0; batchIndex < primBatchCount; ++batchIndex ) { + rvMD5RPrimBatch &batch = mesh.primBatches[batchIndex]; + int paletteCount = 0; + if ( !reader.ReadCount( batch.numTransforms, MD5R_CACHE_MAX_TRANSFORMS ) + || !reader.ReadCount( paletteCount, MD5R_CACHE_MAX_TRANSFORMS, sizeof( int ) ) + || ( paletteCount != 0 && paletteCount != batch.numTransforms ) ) { + return false; + } + batch.transformPalette.SetNum( paletteCount ); + for ( int transformIndex = 0; transformIndex < paletteCount; ++transformIndex ) { + if ( !reader.ReadInt( batch.transformPalette[transformIndex] ) + || batch.transformPalette[transformIndex] < 0 + || batch.transformPalette[transformIndex] >= jointCount ) { + return false; + } + } + if ( !R_MD5RCacheReadGeometrySpec( reader, batch.silTraceGeoSpec ) + || !R_MD5RCacheReadGeometrySpec( reader, batch.drawGeoSpec ) + || !R_MD5RCacheReadGeometrySpec( reader, batch.shadowVolGeoSpec ) + || !reader.ReadInt( batch.numShadowPrimitivesNoCaps ) || batch.numShadowPrimitivesNoCaps < 0 + || !reader.ReadInt( batch.shadowCapPlaneBits ) || batch.shadowCapPlaneBits < 0 || batch.shadowCapPlaneBits > 127 + || !reader.ReadInt( batch.silEdgeStart ) || batch.silEdgeStart < 0 + || !reader.ReadInt( batch.silEdgeCount ) || batch.silEdgeCount < 0 + || !reader.ReadBool( batch.hasSilTraceGeoSpec ) + || !reader.ReadBool( batch.hasDrawGeoSpec ) || !reader.ReadBool( batch.hasShadowGeoSpec ) ) { + return false; + } + } + if ( !R_MD5RCacheValidateMesh( mesh, staged.vertexBuffers, staged.indexBuffers, + staged.silEdges, jointCount, staged.lods.Num() ) ) { + return false; + } + } + if ( !reader.IsValid() ) { + return false; + } + + const bool hasGeometrySections = staged.vertexBuffers.Num() > 0 || staged.indexBuffers.Num() > 0 || staged.meshes.Num() > 0; + if ( decodedMetadataOnly ) { + if ( !hasGeometrySections ) { + return false; + } + } else { + for ( int meshIndex = 0; meshIndex < staged.meshes.Num(); ++meshIndex ) { + if ( staged.meshes[meshIndex].numDrawVertices > MD5R_CACHE_MAX_MATERIALIZED_VERTICES + || staged.meshes[meshIndex].numDrawIndices > MD5R_CACHE_MAX_MATERIALIZED_INDICES ) { + return false; + } + } + } + + for ( int meshIndex = 0; meshIndex < staged.meshes.Num(); ++meshIndex ) { + staged.meshes[meshIndex].renderModel = &staged; + staged.meshes[meshIndex].material = declManager->FindMaterial( staged.meshes[meshIndex].materialName.c_str() ); + const byte materialFlags = decodedMaterialFlags[meshIndex]; + if ( staged.meshes[meshIndex].material == NULL + || idStr::Icmp( staged.meshes[meshIndex].material->GetName(), + staged.meshes[meshIndex].materialName.c_str() ) != 0 + || staged.meshes[meshIndex].material->UseUnsmoothedTangents() != ( ( materialFlags & 1 ) != 0 ) + || staged.meshes[meshIndex].material->ShouldCreateBackSides() != ( ( materialFlags & 2 ) != 0 ) + || staged.meshes[meshIndex].material->SurfaceCastsShadow() != ( ( materialFlags & 4 ) != 0 ) + || staged.meshes[meshIndex].material->IsDiscrete() != ( ( materialFlags & 8 ) != 0 ) ) { + return false; + } + } + staged.BuildLevelsOfDetail(); + if ( !decodedMetadataOnly ) { + bool materializedMetadataOnly = false; + bool materializedGeometrySkipped = false; + if ( staged.joints.Num() == 0 ) { + if ( hasGeometrySections && !staged.GenerateStaticSurfaces() ) { + staged.PurgeModel(); + return false; + } + materializedGeometrySkipped = staged.geometrySectionsSkipped; + } else if ( hasGeometrySections ) { + int builtTemplates = 0; + for ( int meshIndex = 0; meshIndex < staged.meshes.Num(); ++meshIndex ) { + if ( staged.BuildDynamicMeshTemplate( staged.meshes[meshIndex] ) ) { + ++builtTemplates; + } + } + materializedMetadataOnly = ( builtTemplates == 0 ); + materializedGeometrySkipped = ( builtTemplates != staged.meshes.Num() ); + } + if ( materializedMetadataOnly != decodedMetadataOnly + || materializedGeometrySkipped != decodedGeometrySectionsSkipped ) { + staged.PurgeModel(); + return false; + } + } + + staged.bounds = decodedBounds; + staged.metadataOnly = decodedMetadataOnly; + staged.geometrySectionsSkipped = decodedGeometrySectionsSkipped; + SwapLevelLoadCacheState( staged ); + vertexBuffers.Swap( staged.vertexBuffers ); + indexBuffers.Swap( staged.indexBuffers ); + silEdges.Swap( staged.silEdges ); + lods.Swap( staged.lods ); + allLODMeshes.Swap( staged.allLODMeshes ); + meshes.Swap( staged.meshes ); + joints.Swap( staged.joints ); + defaultPose.Swap( staged.defaultPose ); + skinSpaceToLocalMats.Swap( staged.skinSpaceToLocalMats ); + idSwap( commandLine, staged.commandLine ); + idSwap( md5rVersion, staged.md5rVersion ); + idSwap( metadataOnly, staged.metadataOnly ); + idSwap( geometrySectionsSkipped, staged.geometrySectionsSkipped ); + idSwap( hasSky, staged.hasSky ); + idSwap( source, staged.source ); + sharedVertexBuffers = NULL; + sharedIndexBuffers = NULL; + sharedSilEdges = NULL; + for ( int meshIndex = 0; meshIndex < meshes.Num(); ++meshIndex ) { + meshes[meshIndex].renderModel = this; + } + staged.PurgeModel(); + return true; +} + /* ======================== rvRenderModelMD5R::GetVertexBuffers @@ -2100,6 +3228,26 @@ bool rvRenderModelMD5R::InitFromMD5Model( const idRenderModelMD5 &sourceModel ) return false; } +#ifndef ID_DEDICATED + idStr cacheSettings; + R_MD5R_BuildGeneratedCacheSettings( cacheSettings, MD5R_SOURCE_MD5 ); + if ( R_TryReadGeneratedRenderModelCache( *this, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ) ) { + if ( name.Icmp( sourceModel.Name() ) == 0 && source == MD5R_SOURCE_MD5 + && timeStamp == sourceModel.Timestamp() ) { + return true; + } + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); + PurgeModel(); + idRenderModelStatic::InitEmpty( sourceModel.Name() ); + source = MD5R_SOURCE_MD5; + reloadable = true; + purged = false; + md5rVersion = MD5R_VERSION; + } +#endif + joints = sourceModel.joints; defaultPose = sourceModel.defaultPose; skinSpaceToLocalMats = sourceModel.skinSpaceToLocalMats; @@ -2295,6 +3443,12 @@ bool rvRenderModelMD5R::InitFromMD5Model( const idRenderModelMD5 &sourceModel ) return false; } } + timeStamp = sourceModel.Timestamp(); + +#ifndef ID_DEDICATED + R_WriteGeneratedRenderModelCache( *this, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); +#endif return true; } @@ -2305,7 +3459,43 @@ rvRenderModelMD5R::InitFromStaticModel ======================== */ bool rvRenderModelMD5R::InitFromStaticModel( const idRenderModelStatic &sourceModel, rvMD5RSource_t sourceType ) { - return InitFromStaticModelInternal( sourceModel, sourceType, NULL, NULL, NULL ); + if ( sourceType == MD5R_SOURCE_PROC ) { + return InitFromStaticModelInternal( sourceModel, sourceType, NULL, NULL, NULL ); + } + if ( sourceModel.IsDefaultModel() ) { + return false; + } + +#ifndef ID_DEDICATED + PurgeModel(); + idRenderModelStatic::InitEmpty( sourceModel.Name() ); + source = sourceType; + reloadable = true; + purged = false; + md5rVersion = MD5R_VERSION; + idStr cacheSettings; + R_MD5R_BuildGeneratedCacheSettings( cacheSettings, sourceType ); + if ( R_TryReadGeneratedRenderModelCache( *this, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ) ) { + if ( name.Icmp( sourceModel.Name() ) == 0 && source == sourceType + && timeStamp == sourceModel.Timestamp() ) { + return true; + } + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); + } +#endif + + if ( !InitFromStaticModelInternal( sourceModel, sourceType, NULL, NULL, NULL ) ) { + return false; + } + timeStamp = sourceModel.Timestamp(); + +#ifndef ID_DEDICATED + R_WriteGeneratedRenderModelCache( *this, sourceModel.Name(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); +#endif + return true; } /* @@ -3523,7 +4713,6 @@ static int R_MD5R_GetDominantBlendJoint( R_MD5R_GetPackedTransformCount ======================== */ -#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) static int R_MD5R_GetPackedTransformCount( const rvMD5RMesh &mesh ) { int totalTransformCount = 0; @@ -3534,6 +4723,8 @@ static int R_MD5R_GetPackedTransformCount( const rvMD5RMesh &mesh ) { return totalTransformCount; } +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + /* ======================== R_MD5R_CanUsePackedDynamicSurface @@ -3638,7 +4829,8 @@ static bool R_MD5R_UpdatePackedDynamicSurface( const idList &silEdges, int numJoints, modelSurface_t &surface, - bool calculateTangents ) { + bool calculateTangents, + bool skipCpuTangents ) { if ( entJoints == NULL || !R_MD5R_CanUsePackedDynamicSurface( mesh, vertexBuffers, indexBuffers ) ) { return false; } @@ -3771,7 +4963,8 @@ static bool R_MD5R_UpdatePackedDynamicSurface( tri->bounds = bounds; - if ( calculateTangents && !tri->tangentsCalculated && !r_useDeferredTangents.GetBool() ) { + if ( calculateTangents && !skipCpuTangents + && !tri->tangentsCalculated && !r_useDeferredTangents.GetBool() ) { R_DeriveTangents( tri ); } @@ -3829,20 +5022,199 @@ bool rvRenderModelMD5R::BuildDynamicMeshTemplate( rvMD5RMesh &mesh ) { return true; } +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) +static bool R_MD5R_UpdateSkinToModelTransforms( const rvMD5RMesh &mesh, + const idJointMat *entJoints, int numJoints, srfTriangles_t *tri ) { + if ( entJoints == NULL || tri == NULL ) { + return false; + } + const int totalTransformCount = R_MD5R_GetPackedTransformCount( mesh ); + if ( totalTransformCount <= 0 || totalTransformCount > GPU_SKINNING_MAX_JOINTS ) { + return false; + } + R_AllocStaticSkinToModelTransforms( tri, totalTransformCount ); + if ( tri->skinToModelTransforms == NULL ) { + return false; + } + + int transformBase = 0; + for ( int primBatchIndex = 0; primBatchIndex < mesh.primBatches.Num(); ++primBatchIndex ) { + const rvMD5RPrimBatch &primBatch = mesh.primBatches[primBatchIndex]; + const int primBatchTransformCount = Max( primBatch.numTransforms, 1 ); + for ( int transformIndex = 0; transformIndex < primBatchTransformCount; ++transformIndex ) { + int jointIndex = 0; + if ( !R_MD5R_ResolveBlendJoint( primBatch, transformIndex, numJoints, jointIndex ) ) { + return false; + } + R_MD5R_WriteSkinToModelTransform( entJoints[jointIndex], + tri->skinToModelTransforms + ( transformBase + transformIndex ) * 16 ); + } + transformBase += primBatchTransformCount; + } + return transformBase == totalTransformCount; +} + +static bool R_MD5R_AttachGpuSkinningContract( const rvMD5RMesh &mesh, + srfTriangles_t *tri, float skinScale, bool allowGpuSkinning ) { + if ( tri == NULL ) { + return false; + } + if ( skinScale != 0.0f ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_SKIN_SCALE ); + return false; + } + if ( !allowGpuSkinning ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_UNSUPPORTED_PASS ); + return false; + } + if ( !r_gpuSkinning.GetBool() ) { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_DISABLED ); + return false; + } + + int sidecarVertexCount = mesh.gpuBindPoseVerts.Num(); + if ( tri->numVerts == mesh.gpuSkinningSourceVerts ) { + sidecarVertexCount = mesh.gpuSkinningSourceVerts; + } + return R_GpuSkinning_AttachSurfaceContract( tri, mesh.gpuBindPoseVerts.Ptr(), + mesh.gpuSkinningVerts.Ptr(), sidecarVertexCount, + tri->skinToModelTransforms, tri->numSkinToModelTransforms, 16, true, + mesh.gpuSkinningFallback ); +} +#endif + +/* +======================== +rvRenderModelMD5R::BuildGpuSkinningSidecar + +Normalize retail per-primitive local blend indices into one immutable surface +stream. Signed implicit weights are intentionally preserved. +======================== +*/ +void rvRenderModelMD5R::BuildGpuSkinningSidecar( rvMD5RMesh &mesh ) const { + mesh.gpuBindPoseVerts.Clear(); + mesh.gpuSkinningVerts.Clear(); + mesh.gpuSkinningSourceVerts = 0; + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_NONE; + + const idList &vertexBuffers = GetVertexBuffers(); + if ( mesh.deformInfo == NULL || mesh.baseDrawVerts.Num() != mesh.deformInfo->numSourceVerts + || mesh.drawVertexBuffer < 0 || mesh.drawVertexBuffer >= vertexBuffers.Num() ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_MISSING_BIND_POSE; + return; + } + const rvMD5RVertexBufferDesc &drawVertexBuffer = vertexBuffers[mesh.drawVertexBuffer]; + const int totalTransformCount = R_MD5R_GetPackedTransformCount( mesh ); + if ( totalTransformCount <= 0 || totalTransformCount > GPU_SKINNING_MAX_JOINTS ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_JOINT_COUNT; + return; + } + + mesh.gpuSkinningSourceVerts = mesh.deformInfo->numSourceVerts; + mesh.gpuBindPoseVerts.SetNum( mesh.deformInfo->numOutputVerts ); + mesh.gpuSkinningVerts.SetNum( mesh.deformInfo->numOutputVerts ); + for ( int vertexIndex = 0; vertexIndex < mesh.gpuSkinningSourceVerts; ++vertexIndex ) { + mesh.gpuBindPoseVerts[vertexIndex] = mesh.baseDrawVerts[vertexIndex]; + } + + int destVertexBase = 0; + int transformBase = 0; + for ( int primBatchIndex = 0; primBatchIndex < mesh.primBatches.Num(); ++primBatchIndex ) { + const rvMD5RPrimBatch &primBatch = mesh.primBatches[primBatchIndex]; + const int primBatchTransformCount = Max( primBatch.numTransforms, 1 ); + if ( primBatch.hasDrawGeoSpec ) { + for ( int localVertexIndex = 0; localVertexIndex < primBatch.drawGeoSpec.vertexCount; ++localVertexIndex ) { + const int destVertexIndex = destVertexBase + localVertexIndex; + const int sourceVertexIndex = primBatch.drawGeoSpec.vertexStart + localVertexIndex; + if ( destVertexIndex < 0 || destVertexIndex >= mesh.gpuSkinningSourceVerts + || sourceVertexIndex < 0 || sourceVertexIndex >= drawVertexBuffer.numVertices ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_VERTEX_COUNT; + return; + } + + const bool hasBlendIndices = drawVertexBuffer.blendIndices.Num() == drawVertexBuffer.numVertices; + const bool hasBlendWeights = drawVertexBuffer.blendWeights.Num() == drawVertexBuffer.numVertices; + const dword packedBlendIndices = hasBlendIndices + ? drawVertexBuffer.blendIndices[sourceVertexIndex] : 0u; + idVec4 blendWeights; + blendWeights.Zero(); + blendWeights.x = 1.0f; + if ( hasBlendWeights ) { + blendWeights = drawVertexBuffer.blendWeights[sourceVertexIndex]; + } + const int implicitWeightIndex = R_MD5R_GetImplicitBlendWeightIndex( drawVertexBuffer ); + gpuSkinningInfluence_t influences[GPU_SKINNING_INFLUENCES]; + for ( int influenceIndex = 0; influenceIndex < GPU_SKINNING_INFLUENCES; ++influenceIndex ) { + const int localTransformIndex = hasBlendIndices + ? R_MD5R_GetBlendIndex( packedBlendIndices, influenceIndex ) : 0; + influences[influenceIndex].jointIndex = transformBase + Max( localTransformIndex, 0 ); + influences[influenceIndex].weight = R_MD5R_GetSkinningBlendWeight( + blendWeights, influenceIndex, implicitWeightIndex ); + if ( influences[influenceIndex].weight != 0.0f + && ( localTransformIndex < 0 || localTransformIndex >= primBatchTransformCount ) ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_JOINT_INDEX; + return; + } + } + + gpuSkinningPackResult_t packResult; + if ( !R_GpuSkinning_PackVertexExact( influences, GPU_SKINNING_INFLUENCES, + totalTransformCount, true, mesh.gpuSkinningVerts[destVertexIndex], packResult ) + && mesh.gpuSkinningFallback == GPU_SKINNING_FALLBACK_NONE ) { + mesh.gpuSkinningFallback = packResult.reason; + } + } + destVertexBase += primBatch.drawGeoSpec.vertexCount; + } + transformBase += primBatchTransformCount; + } + + if ( destVertexBase != mesh.gpuSkinningSourceVerts || transformBase != totalTransformCount ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_VERTEX_COUNT; + return; + } + + for ( int mirrorIndex = 0; mirrorIndex < mesh.deformInfo->numMirroredVerts; ++mirrorIndex ) { + const int destVertexIndex = mesh.gpuSkinningSourceVerts + mirrorIndex; + const int sourceVertexIndex = mesh.deformInfo->mirroredVerts[mirrorIndex]; + if ( destVertexIndex < 0 || destVertexIndex >= mesh.gpuBindPoseVerts.Num() + || sourceVertexIndex < 0 || sourceVertexIndex >= mesh.gpuSkinningSourceVerts ) { + mesh.gpuSkinningFallback = GPU_SKINNING_FALLBACK_VERTEX_COUNT; + return; + } + mesh.gpuBindPoseVerts[destVertexIndex] = mesh.gpuBindPoseVerts[sourceVertexIndex]; + mesh.gpuSkinningVerts[destVertexIndex] = mesh.gpuSkinningVerts[sourceVertexIndex]; + } +} + /* ======================== rvRenderModelMD5R::UpdateDynamicSurface ======================== */ -bool rvRenderModelMD5R::UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJointMat *entJoints, modelSurface_t &surface, bool calculateTangents, float skinScale ) const { +bool rvRenderModelMD5R::UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJointMat *entJoints, modelSurface_t &surface, bool calculateTangents, float skinScale, bool allowGpuSkinning ) const { const idList &vertexBuffers = GetVertexBuffers(); const idList &indexBuffers = GetIndexBuffers(); const idList &silEdges = GetSilhouetteEdges(); + const uint64 cpuSkinStart = R_GpuSkinning_ReadMicroseconds(); #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + const bool skipPackedCpuTangents = allowGpuSkinning && r_gpuSkinning.GetBool() + && mesh.gpuSkinningFallback == GPU_SKINNING_FALLBACK_NONE + && mesh.gpuBindPoseVerts.Num() == mesh.numDrawVertices + && mesh.gpuSkinningVerts.Num() == mesh.numDrawVertices; if ( skinScale == 0.0f && R_MD5R_UsePackedRuntimeSurfaces() - && R_MD5R_UpdatePackedDynamicSurface( mesh, entJoints, vertexBuffers, indexBuffers, silEdges, joints.Num(), surface, calculateTangents ) ) { + && R_MD5R_UpdatePackedDynamicSurface( mesh, entJoints, vertexBuffers, + indexBuffers, silEdges, joints.Num(), surface, calculateTangents, + skipPackedCpuTangents ) ) { + const bool gpuContractAttached = R_MD5R_AttachGpuSkinningContract( + mesh, surface.geometry, skinScale, allowGpuSkinning ); + const uint64 cpuSkinEnd = R_GpuSkinning_ReadMicroseconds(); + const uint64 elapsedMicroseconds = cpuSkinEnd >= cpuSkinStart + ? cpuSkinEnd - cpuSkinStart : 0; + R_GpuSkinning_RecordCpuSkinning( elapsedMicroseconds, + surface.geometry != NULL ? surface.geometry->numVerts : 0, gpuContractAttached ); return true; } #endif @@ -3882,6 +5254,7 @@ bool rvRenderModelMD5R::UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJo tri = R_AllocStaticTriSurf(); surface.geometry = tri; } + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_NONE ); tri->deformedSurface = true; tri->generateNormals = true; @@ -3903,6 +5276,18 @@ bool rvRenderModelMD5R::UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJo R_AllocStaticTriSurfVerts( tri, tri->numVerts ); } + bool gpuContractAttached = false; +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + if ( R_MD5R_UpdateSkinToModelTransforms( mesh, entJoints, joints.Num(), tri ) ) { + gpuContractAttached = R_MD5R_AttachGpuSkinningContract( + mesh, tri, skinScale, allowGpuSkinning ); + } else { + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_JOINT_COUNT ); + } +#else + R_GpuSkinning_ClearSurfaceContract( tri, GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); +#endif + int destVertexBase = 0; for ( int primBatchIndex = 0; primBatchIndex < mesh.primBatches.Num(); ++primBatchIndex ) { const rvMD5RPrimBatch &primBatch = mesh.primBatches[ primBatchIndex ]; @@ -3946,9 +5331,16 @@ bool rvRenderModelMD5R::UpdateDynamicSurface( const rvMD5RMesh &mesh, const idJo R_BoundTriSurf( tri ); if ( calculateTangents && !R_MD5R_DeferDynamicTangents() ) { - R_DeriveTangents( tri ); + if ( !gpuContractAttached ) { + R_DeriveTangents( tri ); + } } + const uint64 cpuSkinEnd = R_GpuSkinning_ReadMicroseconds(); + const uint64 elapsedMicroseconds = cpuSkinEnd >= cpuSkinStart + ? cpuSkinEnd - cpuSkinStart : 0; + R_GpuSkinning_RecordCpuSkinning( elapsedMicroseconds, tri->numVerts, gpuContractAttached ); + return true; } @@ -4016,6 +5408,11 @@ bool rvRenderModelMD5R::GenerateDynamicSurface( idRenderModelStatic &staticModel return false; } } + if ( mesh.deformInfo != NULL + && ( mesh.gpuBindPoseVerts.Num() != mesh.deformInfo->numOutputVerts + || mesh.gpuSkinningVerts.Num() != mesh.deformInfo->numOutputVerts ) ) { + BuildGpuSkinningSidecar( mesh ); + } modelSurface_t *surface = NULL; int surfaceNum = -1; @@ -4033,7 +5430,7 @@ bool rvRenderModelMD5R::GenerateDynamicSurface( idRenderModelStatic &staticModel surface->mOriginalSurfaceName = NULL; } - if ( !UpdateDynamicSurface( mesh, entJoints, *surface, !collisionOnly, skinScale ) ) { + if ( !UpdateDynamicSurface( mesh, entJoints, *surface, !collisionOnly, skinScale, !collisionOnly ) ) { staticModel.DeleteSurfaceWithId( mesh.meshIdentifier ); staticModel.DeleteSurfaceWithId( mesh.meshIdentifier + MD5R_BackSideSurfaceIdOffset ); mesh.surfaceNum = -1; @@ -4153,38 +5550,76 @@ surfaces without the retail rvMesh runtime. */ void rvRenderModelMD5R::LoadModel() { const rvMD5RSource_t loadSource = source; + const idStr modelName = name; + idStr cacheSource = modelName; + if ( loadSource == MD5R_SOURCE_FILE ) { + idStr resolvedSource; + R_MD5R_ReadBinaryAwareTimestamp( modelName, &resolvedSource ); + if ( !resolvedSource.IsEmpty() ) { + cacheSource = resolvedSource; + } + } +#ifndef ID_DEDICATED + idStr cacheSettings; + R_MD5R_BuildGeneratedCacheSettings( cacheSettings, loadSource ); +#endif PurgeModel(); purged = false; reloadable = true; + source = loadSource; +#ifndef ID_DEDICATED + if ( loadSource != MD5R_SOURCE_PROC + && R_TryReadGeneratedRenderModelCache( *this, cacheSource.c_str(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ) ) { + if ( name.Icmp( modelName ) == 0 && source == loadSource ) { + return; + } + fileSystem->DiscardGeneratedCache( GENERATED_CACHE_RENDER_MODEL, cacheSource.c_str(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); + PurgeModel(); + name = modelName; + purged = false; + reloadable = true; + source = loadSource; + } +#endif if ( loadSource == MD5R_SOURCE_MD5 ) { idAutoPtr sourceModel( new idRenderModelMD5 ); - sourceModel->InitFromFile( name.c_str() ); + sourceModel->InitFromFile( modelName.c_str() ); if ( sourceModel->IsDefaultModel() || !InitFromMD5Model( *sourceModel ) ) { MakeDefaultModel(); } - fileSystem->ReadFile( name.c_str(), NULL, &timeStamp ); + fileSystem->ReadFile( modelName.c_str(), NULL, &timeStamp ); +#ifndef ID_DEDICATED + R_WriteGeneratedRenderModelCache( *this, cacheSource.c_str(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); +#endif return; } if ( loadSource == MD5R_SOURCE_LWO_ASE_FLT ) { idAutoPtr sourceModel( new idRenderModelStatic ); - sourceModel->InitFromFile( name.c_str() ); + sourceModel->InitFromFile( modelName.c_str() ); if ( sourceModel->IsDefaultModel() || !InitFromStaticModel( *sourceModel, MD5R_SOURCE_LWO_ASE_FLT ) ) { MakeDefaultModel(); } - fileSystem->ReadFile( name.c_str(), NULL, &timeStamp ); + fileSystem->ReadFile( modelName.c_str(), NULL, &timeStamp ); +#ifndef ID_DEDICATED + R_WriteGeneratedRenderModelCache( *this, cacheSource.c_str(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); +#endif return; } source = MD5R_SOURCE_FILE; - idAutoPtr parser( LexerFactory::MakeLexer( name.c_str(), LEXFL_ALLOWPATHNAMES | LEXFL_NOSTRINGESCAPECHARS ) ); + idAutoPtr parser( LexerFactory::MakeLexer( modelName.c_str(), LEXFL_ALLOWPATHNAMES | LEXFL_NOSTRINGESCAPECHARS ) ); // The filename constructor loads either the compiled companion or the // canonical ASCII source. Calling LoadFile again breaks the ASCII delegate // with "another script already loaded". @@ -4292,7 +5727,14 @@ void rvRenderModelMD5R::LoadModel() { } } - timeStamp = R_MD5R_ReadBinaryAwareTimestamp( name ); + timeStamp = R_MD5R_ReadBinaryAwareTimestamp( modelName, &cacheSource ); + if ( cacheSource.IsEmpty() ) { + cacheSource = modelName; + } +#ifndef ID_DEDICATED + R_WriteGeneratedRenderModelCache( *this, cacheSource.c_str(), + MD5R_MODEL_GENERATED_CACHE_PARSER_VERSION, cacheSettings.c_str() ); +#endif } /* @@ -4308,6 +5750,10 @@ void rvRenderModelMD5R::PurgeModel() { meshes[ meshIndex ].deformInfo = NULL; } meshes[ meshIndex ].baseDrawVerts.Clear(); + meshes[ meshIndex ].gpuBindPoseVerts.Clear(); + meshes[ meshIndex ].gpuSkinningVerts.Clear(); + meshes[ meshIndex ].gpuSkinningSourceVerts = 0; + meshes[ meshIndex ].gpuSkinningFallback = GPU_SKINNING_FALLBACK_MISSING_SKIN_VERTICES; } vertexBuffers.Clear(); indexBuffers.Clear(); @@ -4497,15 +5943,13 @@ idRenderModel *rvRenderModelMD5R::InstantiateDynamicModel( const renderEntity_s } } - idList activeMeshes; - activeMeshes.SetNum( meshes.Num() ); - for ( int meshIndex = 0; meshIndex < activeMeshes.Num(); ++meshIndex ) { - activeMeshes[ meshIndex ] = false; - } + const int meshCount = meshes.Num(); + bool *activeMeshes = (bool *)_alloca( ( meshCount > 0 ? meshCount : 1 ) * sizeof( activeMeshes[0] ) ); + memset( activeMeshes, 0, meshCount * sizeof( activeMeshes[0] ) ); for ( int lodMeshIndex = 0; lodMeshIndex < allLODMeshes.Num(); ++lodMeshIndex ) { const int meshIndex = allLODMeshes[ lodMeshIndex ]; - if ( meshIndex >= 0 && meshIndex < activeMeshes.Num() ) { + if ( meshIndex >= 0 && meshIndex < meshCount ) { activeMeshes[ meshIndex ] = true; } } @@ -4513,7 +5957,7 @@ idRenderModel *rvRenderModelMD5R::InstantiateDynamicModel( const renderEntity_s if ( selectedLOD >= 0 && selectedLOD < lods.Num() ) { for ( int lodMeshIndex = 0; lodMeshIndex < lods[ selectedLOD ].meshIndexes.Num(); ++lodMeshIndex ) { const int meshIndex = lods[ selectedLOD ].meshIndexes[ lodMeshIndex ]; - if ( meshIndex >= 0 && meshIndex < activeMeshes.Num() ) { + if ( meshIndex >= 0 && meshIndex < meshCount ) { activeMeshes[ meshIndex ] = true; } } diff --git a/src/renderer/Model_sprite.cpp b/src/renderer/Model_sprite.cpp index c8614fc9..170734b2 100644 --- a/src/renderer/Model_sprite.cpp +++ b/src/renderer/Model_sprite.cpp @@ -175,6 +175,13 @@ idRenderModel * idRenderModelSprite::InstantiateDynamicModel( const struct rende R_BoundTriSurf( tri ); + // The reused snapshot keeps its persistent static ambientCache, and + // R_CreateAmbientCache short-circuits on a non-NULL cache, so without this + // the first frame's quad (size/vertex color) would render forever. Freeing + // the vertex caches after the rewrite forces a re-upload; it is a no-op on a + // freshly allocated tri. + R_FreeStaticTriSurfVertexCaches( tri ); + staticModel->bounds = tri->bounds; return staticModel; diff --git a/src/renderer/ModernClusteredLighting.cpp b/src/renderer/ModernClusteredLighting.cpp index f5d0e6b5..68db27c3 100644 --- a/src/renderer/ModernClusteredLighting.cpp +++ b/src/renderer/ModernClusteredLighting.cpp @@ -3,15 +3,19 @@ #include "tr_local.h" #include "ModernClusteredLighting.h" +#include "AdvancedLightingCore.h" #include "GLDebugScope.h" #include "GLStateCache.h" #include "ModernLightImageAtlas.h" #include "ModernShadowPlanner.h" +#include "ModernSpecularProbeAtlas.h" #include "RendererBenchmarks.h" #include "RendererMetrics.h" #include "ShadowMapProjected.h" #include "ShadowMapArb2Parity.h" +#include + const int MODERN_CLUSTER_MAX_GRIDS = 16; const int MODERN_CLUSTER_MAX_LIGHTS_UBO = 256; const int MODERN_CLUSTER_MAX_LIGHTS_SSBO = 1024; @@ -29,6 +33,7 @@ const int MODERN_CLUSTER_UBO_BINDING_PARAMS = 3; const int MODERN_CLUSTER_UBO_BINDING_LIGHTS = 4; const int MODERN_CLUSTER_UBO_BINDING_INDICES = 5; const int MODERN_CLUSTER_UBO_BINDING_SHADOW_DESCRIPTORS = 6; +const int MODERN_CLUSTER_UBO_BINDING_SPECULAR_PROBES = 7; const int MODERN_CLUSTER_SSBO_BINDING_LIGHTS = 6; const int MODERN_CLUSTER_SSBO_BINDING_INDICES = 7; const int MODERN_CLUSTER_SSBO_BINDING_SHADOW_DESCRIPTORS = 8; @@ -42,6 +47,11 @@ const int MODERN_CLUSTER_LIGHT_FLAG_SHADOW_CASCADE = 1 << 6; const int MODERN_CLUSTER_LIGHT_FLAG_SHADOW_POINT = 1 << 7; const int MODERN_CLUSTER_LIGHT_FLAG_BLEND = 1 << 8; +static_assert( RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER == ADVANCED_LIGHTING_PROBES_PER_CLUSTER, + "cluster probe selection width must match the device-independent contract" ); +static_assert( RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS == 32, + "cluster probe producer must match the fixed shader ABI" ); + typedef struct modernClusterGridRecord_s { const viewDef_t * viewDef; char debugName[64]; @@ -59,6 +69,8 @@ typedef struct modernClusterGridRecord_s { int indexGroupsPerCluster; int firstLight; int lightCount; + int firstProbe; + int probeCount; int width; int height; float nearZ; @@ -87,14 +99,42 @@ typedef struct modernClusterLightRecord_s { idPlane viewLightProject[4]; idScreenRect scissor; bool fullDepthRange; + // Cluster bin bounds computed in the counting pass and reused in the fill + // pass (CPU-CSR path); not uploaded to the GPU. Zero-initialized, which the + // compute-binning path leaves untouched. + int binTileMinX; + int binTileMaxX; + int binTileMinY; + int binTileMaxY; + int binSliceMinZ; + int binSliceMaxZ; } modernClusterLightRecord_t; +typedef struct modernSpecularProbeRecord_s { + int sceneIndex; + int stableLightIdentity; + int priority; + idVec3 cameraOrigin; + idVec3 axisX; + idVec3 axisY; + idVec3 axisZ; + idVec3 tint; + float intensity; + float radius; + float blendFraction; + float depthMin; + float depthMax; + idScreenRect scissor; + modernSpecularProbeAtlasPlacement_t placement; +} modernSpecularProbeRecord_t; + typedef struct modernClusterRecord_s { int firstLightIndex; int lightCount; int uploadedLightCount; int writeLightCount; bool overflow; + advancedLightingProbeCandidate_t probes[RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER]; } modernClusterRecord_t; typedef struct modernClusterGridGpuParams_s { @@ -145,6 +185,19 @@ assert_sizeof( modernClusterLightGpuRecord_t, 12 * 4 * sizeof( float ) ); assert_offsetof( modernClusterLightGpuRecord_t, flags, 64 ); assert_offsetof( modernClusterLightGpuRecord_t, projectQ, 144 ); +typedef struct modernSpecularProbeGpuRecord_s { + float positionRadius[4]; + float tintIntensity[4]; + float axisXPriority[4]; + float axisYBlend[4]; + float axisZSlot[4]; + float identity[4]; +} modernSpecularProbeGpuRecord_t; + +assert_sizeof( modernSpecularProbeGpuRecord_t, 6 * 4 * sizeof( float ) ); +assert_offsetof( modernSpecularProbeGpuRecord_t, tintIntensity, 16 ); +assert_offsetof( modernSpecularProbeGpuRecord_t, identity, 80 ); + typedef struct modernClusterIndexGpuRecord_s { GLuint indices[4]; } modernClusterIndexGpuRecord_t; @@ -199,14 +252,17 @@ typedef struct modernClusterDebugPixel_s { typedef struct modernClusterFrameData_s { idList grids; idList lights; + idList probes; idList clusters; idList clusterLightIndices; idList lightGpuRecords; + idList probeGpuRecords; idList indexGpuRecords; idList shadowDescriptors; idList shadowGpuRecords; int gridCount; int lightCount; + int probeCount; int shadowDescriptorCount; int clusterCount; int clusterRecordCount; @@ -217,18 +273,73 @@ typedef struct modernClusterFrameData_s { int indexRecordCapacity; int shadowDescriptorCapacity; bool shaderStoragePath; + bool probeSetComplete; + unsigned int probeFrameGeneration; } modernClusterFrameData_t; +typedef struct modernClusterDecalRecord_s { + rendererClusteredDecalSource_t source; + int gridIndex; + int tileMinX; + int tileMaxX; + int tileMinY; + int tileMaxY; + int sliceMinZ; + int sliceMaxZ; + int clusterReferenceCount; + unsigned long long hash; +} modernClusterDecalRecord_t; + +typedef struct modernClusterDecalClusterRecord_s { + int firstReference; + int referenceCount; + int writeCount; +} modernClusterDecalClusterRecord_t; + +typedef struct modernClusterDecalViewRecord_s { + const viewDef_t * viewDef; + int gridIndex; + int recordCount; + int firstCommandOrder; + int lastCommandOrder; + int clusterReferences; + unsigned long long gridHash; + unsigned long long sourceOrderHash; + bool owned; +} modernClusterDecalViewRecord_t; + +typedef struct modernClusterDecalFrameData_s { + idList records; + idList clusters; + idList clusterReferences; + idList views; + unsigned int generation; + unsigned long long sourceOrderHash; + int viewCount; + bool valid; +} modernClusterDecalFrameData_t; + +typedef struct modernClusterDecalRejectInfo_s { + rendererClusteredDecalReject_t reason; + int sourceIndex; + int viewIndex; +} modernClusterDecalRejectInfo_t; + static void R_ModernClusteredLighting_ViewPlaneForLightProject( const viewDef_t *viewDef, const idPlane &worldPlane, float out[4] ); static rendererClusteredLightingStats_t rg_clusteredLightingStats; static modernClusterFrameData_t rg_clusteredLightingFrame; +static rendererClusteredDecalStats_t rg_clusteredDecalStats; +static modernClusterDecalFrameData_t rg_clusteredDecalFrame; +static modernClusterDecalFrameData_t rg_clusteredDecalPrepareScratch; +static modernClusterDecalFrameData_t rg_clusteredDecalSealScratch; static renderBackendCaps_t rg_clusteredLightingCaps; static renderFeatureSet_t rg_clusteredLightingFeatures; static GLuint rg_clusteredLightingParamsUBO = 0; static GLuint rg_clusteredLightingLightsUBO = 0; static GLuint rg_clusteredLightingIndicesUBO = 0; static GLuint rg_clusteredLightingShadowDescriptorsUBO = 0; +static GLuint rg_clusteredLightingSpecularProbesUBO = 0; static GLuint rg_clusteredLightingLightsSSBO = 0; static GLuint rg_clusteredLightingIndicesSSBO = 0; static GLuint rg_clusteredLightingShadowDescriptorsSSBO = 0; @@ -248,6 +359,77 @@ static bool rg_clusteredLightingAvailable = false; static bool rg_clusteredLightingShaderStorageAvailable = false; static int rg_clusteredLightingBoundGridIndex = -1; +// The clustered front end is shared by the GL and Vulkan renderer modules, +// while authored probe upload/sampling is deliberately GL-only in Milestone F. +// Keep the shared producer linked on Vulkan but fail the leaf closed there. +static modernSpecularProbeAtlasReject_t R_ModernClusteredLighting_AcquireProbeAtlas( + const idImage *image, modernSpecularProbeAtlasPlacement_t *placement ) { +#if defined( OPENQ4_RENDERER_VK_MODULE ) + ( void )image; + if ( placement != NULL ) { + ModernSpecularProbeAtlas_ClearPlacement( *placement ); + } + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_UNAVAILABLE; +#else + return R_ModernSpecularProbeAtlas_Acquire( image, placement ); +#endif +} + +static bool R_ModernClusteredLighting_ProbeAtlasReady( void ) { +#if defined( OPENQ4_RENDERER_VK_MODULE ) + return false; +#else + return R_ModernSpecularProbeAtlas_Ready(); +#endif +} + +static bool R_ModernClusteredLighting_ProbeAtlasFrameReady( void ) { +#if defined( OPENQ4_RENDERER_VK_MODULE ) + return false; +#else + return R_ModernSpecularProbeAtlas_FrameReady(); +#endif +} + +static std::uint64_t R_ModernClusteredLighting_ProbeAtlasFrameGeneration( void ) { +#if defined( OPENQ4_RENDERER_VK_MODULE ) + return 0; +#else + return R_ModernSpecularProbeAtlas_Stats().frameGeneration; +#endif +} + +static void R_ModernClusteredLighting_ClearDecalStorage( modernClusterDecalFrameData_t &frame ) { + frame.records.SetNum( 0, false ); + frame.clusters.SetNum( 0, false ); + frame.clusterReferences.SetNum( 0, false ); + frame.views.SetNum( 0, false ); + frame.generation = 0; + frame.sourceOrderHash = 0; + frame.viewCount = 0; + frame.valid = false; +} + +static void R_ModernClusteredLighting_ResetDecalTransaction( void ) { + R_ModernClusteredLighting_ClearDecalStorage( rg_clusteredDecalFrame ); + memset( &rg_clusteredDecalStats, 0, sizeof( rg_clusteredDecalStats ) ); + rg_clusteredDecalStats.recordCapacity = RENDERER_CLUSTER_DECAL_MAX_RECORDS; + rg_clusteredDecalStats.referenceCapacity = RENDERER_CLUSTER_DECAL_MAX_REFERENCES; + rg_clusteredDecalStats.lastReject = RENDERER_CLUSTER_DECAL_REJECT_NONE; + rg_clusteredDecalStats.lastRejectSource = -1; + rg_clusteredDecalStats.lastRejectView = -1; + idStr::Copynz( rg_clusteredDecalStats.status, "empty", sizeof( rg_clusteredDecalStats.status ) ); +} + +void R_ModernClusteredLighting_ResetDecalsForFrame( void ) { + R_ModernClusteredLighting_ResetDecalTransaction(); +} + +static unsigned int R_ModernClusteredLighting_CurrentDecalGeneration( void ) { + unsigned int generation = static_cast( tr.frameCount ) + 1u; + return generation != 0u ? generation : 1u; +} + static void R_ModernClusteredLighting_SetStatus( rendererClusteredLightingStats_t &stats, const char *status ) { idStr::Copynz( stats.status, status ? status : "unknown", sizeof( stats.status ) ); } @@ -275,7 +457,7 @@ static bool R_ModernClusteredLighting_UseShaderStoragePath( void ) { && glBindBufferBase != NULL; } -static bool R_ModernClusteredLighting_UseComputeBinningPath( void ) { +static bool R_ModernClusteredLighting_ComputeBinningCapable( void ) { return R_ModernClusteredLighting_UseShaderStoragePath() && rg_clusteredLightingCaps.hasCompute && rg_clusteredLightingComputeProgram != 0 @@ -283,6 +465,14 @@ static bool R_ModernClusteredLighting_UseComputeBinningPath( void ) { && glMemoryBarrier != NULL; } +static bool R_ModernClusteredLighting_UseComputeBinningPath( void ) { + // Header z/w carry authored-probe ownership. The existing compute builder + // writes all four header lanes, so probe frames stay on deterministic CPU + // CSR until the compute contract can preserve those lanes exactly. + return R_ModernClusteredLighting_ComputeBinningCapable() + && rg_clusteredLightingFrame.probeCount == 0; +} + static int R_ModernClusteredLighting_LightCapacity( void ) { return R_ModernClusteredLighting_UseShaderStoragePath() ? MODERN_CLUSTER_MAX_LIGHTS_SSBO : MODERN_CLUSTER_MAX_LIGHTS_UBO; } @@ -316,26 +506,31 @@ static void R_ModernClusteredLighting_UpdateBuffer( GLenum target, GLuint buffer static void R_ModernClusteredLighting_BindGpuBuffers( bool useShaderStorage ) { if ( useShaderStorage ) { R_GLStateCache().BindBufferBase( GL_UNIFORM_BUFFER, MODERN_CLUSTER_UBO_BINDING_PARAMS, rg_clusteredLightingParamsUBO ); + R_GLStateCache().BindBufferBase( GL_UNIFORM_BUFFER, MODERN_CLUSTER_UBO_BINDING_SPECULAR_PROBES, rg_clusteredLightingSpecularProbesUBO ); const GLuint ssboBuffers[3] = { rg_clusteredLightingLightsSSBO, rg_clusteredLightingIndicesSSBO, rg_clusteredLightingShadowDescriptorsSSBO }; R_GLStateCache().BindBuffersBase( GL_SHADER_STORAGE_BUFFER, MODERN_CLUSTER_SSBO_BINDING_LIGHTS, 3, ssboBuffers ); return; } - const GLuint uboBuffers[4] = { rg_clusteredLightingParamsUBO, rg_clusteredLightingLightsUBO, rg_clusteredLightingIndicesUBO, rg_clusteredLightingShadowDescriptorsUBO }; - R_GLStateCache().BindBuffersBase( GL_UNIFORM_BUFFER, MODERN_CLUSTER_UBO_BINDING_PARAMS, 4, uboBuffers ); + const GLuint uboBuffers[5] = { rg_clusteredLightingParamsUBO, rg_clusteredLightingLightsUBO, rg_clusteredLightingIndicesUBO, rg_clusteredLightingShadowDescriptorsUBO, rg_clusteredLightingSpecularProbesUBO }; + R_GLStateCache().BindBuffersBase( GL_UNIFORM_BUFFER, MODERN_CLUSTER_UBO_BINDING_PARAMS, 5, uboBuffers ); } static void R_ModernClusteredLighting_ResetFrameData( void ) { + R_ModernClusteredLighting_ResetDecalTransaction(); rg_clusteredLightingFrame.grids.SetNum( 0, false ); rg_clusteredLightingFrame.lights.SetNum( 0, false ); + rg_clusteredLightingFrame.probes.SetNum( 0, false ); rg_clusteredLightingFrame.clusters.SetNum( 0, false ); rg_clusteredLightingFrame.clusterLightIndices.SetNum( 0, false ); rg_clusteredLightingFrame.lightGpuRecords.SetNum( 0, false ); + rg_clusteredLightingFrame.probeGpuRecords.SetNum( 0, false ); rg_clusteredLightingFrame.indexGpuRecords.SetNum( 0, false ); rg_clusteredLightingFrame.shadowDescriptors.SetNum( 0, false ); rg_clusteredLightingFrame.shadowGpuRecords.SetNum( 0, false ); rg_clusteredLightingFrame.gridCount = 0; rg_clusteredLightingFrame.lightCount = 0; + rg_clusteredLightingFrame.probeCount = 0; rg_clusteredLightingFrame.shadowDescriptorCount = 0; rg_clusteredLightingFrame.clusterCount = 0; rg_clusteredLightingFrame.clusterRecordCount = 0; @@ -346,6 +541,8 @@ static void R_ModernClusteredLighting_ResetFrameData( void ) { rg_clusteredLightingFrame.lightCapacity = R_ModernClusteredLighting_LightCapacity(); rg_clusteredLightingFrame.indexRecordCapacity = R_ModernClusteredLighting_IndexRecordCapacity(); rg_clusteredLightingFrame.shadowDescriptorCapacity = R_ModernClusteredLighting_ShadowDescriptorCapacity(); + rg_clusteredLightingFrame.probeSetComplete = true; + rg_clusteredLightingFrame.probeFrameGeneration = 0; rg_clusteredLightingBoundGridIndex = -1; } @@ -441,22 +638,6 @@ static float R_ModernClusteredLighting_LightRadius( const viewLight_t *vLight ) return idMath::ClampFloat( 1.0f, 32768.0f, radius ); } -static float R_ModernClusteredLighting_PointShadowFar( const viewLight_t *vLight ) { - if ( vLight == NULL ) { - return 1.0f; - } - - idVec3 adjustedRadius = vLight->lightRadius; - if ( vLight->lightDef != NULL ) { - const renderLight_t &parms = vLight->lightDef->parms; - adjustedRadius[0] = parms.lightRadius[0] + idMath::Fabs( parms.lightCenter[0] ); - adjustedRadius[1] = parms.lightRadius[1] + idMath::Fabs( parms.lightCenter[1] ); - adjustedRadius[2] = parms.lightRadius[2] + idMath::Fabs( parms.lightCenter[2] ); - } - - return Max( adjustedRadius.Length() * r_shadowMapPointFarScale.GetFloat(), 1.0f ); -} - static rendererModernLightType_t R_ModernClusteredLighting_ClassifyLight( const viewLight_t *vLight ) { if ( vLight == NULL ) { return RENDERER_MODERN_LIGHT_SPECIAL; @@ -581,7 +762,7 @@ static int R_ModernClusteredLighting_ShadowDescriptorFlags( const modernShadowLi if ( shadow.modernReceiverSamplingReady ) { flags |= RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_SAMPLING_READY; } - if ( shadow.arb2AtlasSlotReady ) { + if ( shadow.arb2AtlasSlotReady || shadow.arb2PointCubeReady ) { flags |= RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_ATLAS_SLOT; } if ( shadow.stableCascadeReady ) { @@ -660,17 +841,18 @@ static void R_ModernClusteredLighting_CopyPlannerShadowDescriptor( rendererModer dst.projectedSkippedSampleCount = src.projectedSkippedSampleCount; dst.flags = R_ModernClusteredLighting_ShadowDescriptorFlags( src ); dst.updateFrame = src.updateFrame; - dst.atlasContentFrame = src.arb2AtlasContentFrame; + dst.atlasContentFrame = src.pointLight + ? src.arb2PointCubeContentFrame : src.arb2AtlasContentFrame; dst.atlasCellX = src.arb2AtlasCellX; dst.atlasCellY = src.arb2AtlasCellY; dst.atlasCellSpan = src.arb2AtlasCellSpan; - dst.atlasSlotValid = src.arb2AtlasSlotReady; + dst.atlasSlotValid = src.arb2AtlasSlotReady || src.arb2PointCubeReady; memcpy( dst.shadowMatrix, src.shadowMatrix, sizeof( dst.shadowMatrix ) ); for ( int cascadeIndex = 0; cascadeIndex < RENDERER_MODERN_SHADOW_DESCRIPTOR_MAX_CASCADES; ++cascadeIndex ) { R_ModernClusteredLighting_SetIdentityMatrix( dst.viewShadowMatrix[cascadeIndex] ); } if ( src.pointLight ) { - dst.projection[0] = R_ModernClusteredLighting_PointShadowFar( src.viewLight ); + dst.projection[0] = R_ShadowMapPointFarDistance( src.viewLight ); dst.projection[1] = 2.0f / static_cast( Max( 1, src.resolution ) ); dst.projection[2] = static_cast( src.depthFormat ); dst.projection[3] = static_cast( src.compareMode ); @@ -785,13 +967,28 @@ static void R_ModernClusteredLighting_ApplyShadowDescriptor( modernClusterLightR record.shadowFallbackReason = shadow->fallbackReason; stats.shadowDescriptorCount = Max( stats.shadowDescriptorCount, shadow->descriptorIndex + 1 ); - // per-light gating (5c): a projected light is GPU-consumable only when - // its descriptor references a live persistent-atlas cell; a mapped light - // without one is blocked individually instead of poisoning the frame + // Per-light physical-resource gating (5c): projected lights require a live + // persistent-atlas cell and point lights require the exact cache cube which + // is currently bound. Missing provenance blocks only that light. + const bool mappedOrReused = shadow->policy == MODERN_SHADOW_POLICY_MAPPED + || shadow->policy == MODERN_SHADOW_POLICY_CACHE_REUSE; + bool projectedSlotReady = shadow->arb2AtlasSlotReady; + if ( !shadow->pointLight && mappedOrReused + && shadow->modernReceiverSamplingReady && projectedSlotReady ) { + projectedSlotReady = RB_ShadowMapProjectedAtlasSlotMarkUsed( + shadow->lightDefIndex, shadow->arb2AtlasSignature, + shadow->arb2AtlasStorageGeneration, + shadow->arb2AtlasCellX, shadow->arb2AtlasCellY, + shadow->arb2AtlasCellSpan ); + } const bool projectedSlotBlocked = !shadow->pointLight - && ( shadow->policy == MODERN_SHADOW_POLICY_MAPPED || shadow->policy == MODERN_SHADOW_POLICY_CACHE_REUSE ) + && mappedOrReused && shadow->modernReceiverSamplingReady - && !shadow->arb2AtlasSlotReady; + && !projectedSlotReady; + const bool pointCubeBlocked = shadow->pointLight + && mappedOrReused + && shadow->modernReceiverSamplingReady + && !shadow->arb2PointCubeReady; bool mappedShadowForModernReceiver = false; if ( shadow->policy == MODERN_SHADOW_POLICY_MAPPED && !shadow->modernReceiverSamplingReady ) { @@ -801,7 +998,7 @@ static void R_ModernClusteredLighting_ApplyShadowDescriptor( modernClusterLightR record.shadowPolicy = MODERN_SHADOW_POLICY_NONE; record.shadowFallbackReason = MODERN_SHADOW_FALLBACK_RECEIVER_SAMPLING_UNAVAILABLE; stats.shadowReceiverBlockedLights++; - } else if ( projectedSlotBlocked ) { + } else if ( projectedSlotBlocked || pointCubeBlocked ) { record.shadowDescriptorIndex = -1; record.shadowPolicy = MODERN_SHADOW_POLICY_NONE; record.shadowFallbackReason = MODERN_SHADOW_FALLBACK_RESOURCE_UNAVAILABLE; @@ -828,10 +1025,8 @@ static void R_ModernClusteredLighting_ApplyShadowDescriptor( modernClusterLightR stats.shadowSkippedLights++; } - if ( mappedShadowForModernReceiver && !shadow->pointLight && shadow->arb2AtlasSlotReady ) { - // the atlas cell this light record will present must not idle-expire - // under the GPU; this is the closest CPU point to actual consumption - RB_ShadowMapProjectedAtlasSlotMarkUsed( shadow->lightDefIndex ); + if ( mappedShadowForModernReceiver && shadow->pointLight && shadow->arb2PointCubeReady ) { + RB_ShadowMapPointCubeMarkUsed( shadow->lightDefIndex ); } if ( mappedShadowForModernReceiver && shadow->mapType == MODERN_SHADOW_MAP_CASCADE ) { @@ -854,6 +1049,11 @@ static const idMaterial *R_ModernClusteredLighting_ResolvedLightShader( const vi return NULL; } +static bool R_ModernClusteredLighting_IsSpecularProbe( const viewLight_t *vLight ) { + const idMaterial *lightShader = R_ModernClusteredLighting_ResolvedLightShader( vLight ); + return lightShader != NULL && lightShader->HasSpecularProbe(); +} + static bool R_ModernClusteredLighting_UsePerStageDescriptors( const viewLight_t *vLight, const idMaterial *lightShader ) { if ( vLight == NULL || lightShader == NULL || vLight->shaderRegisters == NULL ) { return false; @@ -887,6 +1087,9 @@ static int R_ModernClusteredLighting_CountViewLights( const viewDef_t *viewDef ) } for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { const idMaterial *lightShader = R_ModernClusteredLighting_ResolvedLightShader( vLight ); + if ( lightShader != NULL && lightShader->HasSpecularProbe() ) { + continue; + } if ( R_ModernClusteredLighting_UsePerStageDescriptors( vLight, lightShader ) ) { const int stageCount = lightShader->GetNumStages(); for ( int stageIndex = 0; stageIndex < stageCount; ++stageIndex ) { @@ -967,6 +1170,8 @@ static void R_ModernClusteredLighting_InitGrid( modernClusterGridRecord_t &grid, grid.indexGroupsPerCluster = R_ModernClusteredLighting_CeilDiv( requestedLightsPerCluster, 4 ); grid.firstLight = rg_clusteredLightingFrame.lightCount; grid.lightCount = 0; + grid.firstProbe = rg_clusteredLightingFrame.probeCount; + grid.probeCount = 0; grid.nearZ = viewDef != NULL && viewDef->renderView.cramZNear ? 0.25f : Max( 0.01f, r_znear.GetFloat() ); grid.farZ = 4096.0f; R_ModernClusteredLighting_FormatDebugString( grid.debugName, sizeof( grid.debugName ), "scene%d_%dx%dx%d", sceneIndex, grid.tileCountX, grid.tileCountY, grid.sliceCountZ ); @@ -976,6 +1181,615 @@ static int R_ModernClusteredLighting_ClusterIndex( const modernClusterGridRecord return grid.clusterOffset + ( sliceZ * grid.tileCountY + tileY ) * grid.tileCountX + tileX; } +static const unsigned long long MODERN_CLUSTER_DECAL_HASH_OFFSET = 1469598103934665603ull; +static const unsigned long long MODERN_CLUSTER_DECAL_HASH_PRIME = 1099511628211ull; + +static void R_ModernClusteredLighting_DecalHashByte( unsigned long long &hash, unsigned int value ) { + hash ^= value & 0xffu; + hash *= MODERN_CLUSTER_DECAL_HASH_PRIME; +} + +static void R_ModernClusteredLighting_DecalHashU32( unsigned long long &hash, unsigned int value ) { + for ( int byteIndex = 0; byteIndex < 4; ++byteIndex ) { + R_ModernClusteredLighting_DecalHashByte( hash, value >> ( byteIndex * 8 ) ); + } +} + +static void R_ModernClusteredLighting_DecalHashU64( unsigned long long &hash, unsigned long long value ) { + for ( int byteIndex = 0; byteIndex < 8; ++byteIndex ) { + R_ModernClusteredLighting_DecalHashByte( hash, static_cast( value >> ( byteIndex * 8 ) ) ); + } +} + +static void R_ModernClusteredLighting_DecalHashInt( unsigned long long &hash, int value ) { + R_ModernClusteredLighting_DecalHashU32( hash, static_cast( value ) ); +} + +static unsigned int R_ModernClusteredLighting_DecalFloatBits( float value ) { + unsigned int bits = 0; + memcpy( &bits, &value, sizeof( bits ) ); + return bits; +} + +static void R_ModernClusteredLighting_DecalHashFloat( unsigned long long &hash, float value ) { + R_ModernClusteredLighting_DecalHashU32( hash, R_ModernClusteredLighting_DecalFloatBits( value ) ); +} + +static unsigned long long R_ModernClusteredLighting_DecalGridHash( const modernClusterGridRecord_t &grid ) { + unsigned long long hash = MODERN_CLUSTER_DECAL_HASH_OFFSET; + R_ModernClusteredLighting_DecalHashInt( hash, grid.sceneIndex ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.tileCountX ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.tileCountY ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.sliceCountZ ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.clusterOffset ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.clusterCount ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.width ); + R_ModernClusteredLighting_DecalHashInt( hash, grid.height ); + R_ModernClusteredLighting_DecalHashFloat( hash, grid.nearZ ); + R_ModernClusteredLighting_DecalHashFloat( hash, grid.farZ ); + return hash; +} + +static unsigned long long R_ModernClusteredLighting_DecalSourceHash( const rendererClusteredDecalSource_t &source ) { + // Pointer values deliberately stay out of the stable hash. Seal compares + // both pointers exactly, while the immutable resource IDs make this hash + // deterministic across executions. + unsigned long long hash = MODERN_CLUSTER_DECAL_HASH_OFFSET; + R_ModernClusteredLighting_DecalHashInt( hash, source.commandIndex ); + R_ModernClusteredLighting_DecalHashInt( hash, source.commandOrder ); + R_ModernClusteredLighting_DecalHashU32( hash, source.materialStableId ); + R_ModernClusteredLighting_DecalHashU32( hash, source.geometryStableId ); + R_ModernClusteredLighting_DecalHashU32( hash, source.instanceStableId ); + R_ModernClusteredLighting_DecalHashInt( hash, source.screenX1 ); + R_ModernClusteredLighting_DecalHashInt( hash, source.screenY1 ); + R_ModernClusteredLighting_DecalHashInt( hash, source.screenX2 ); + R_ModernClusteredLighting_DecalHashInt( hash, source.screenY2 ); + R_ModernClusteredLighting_DecalHashFloat( hash, source.depthMin ); + R_ModernClusteredLighting_DecalHashFloat( hash, source.depthMax ); + R_ModernClusteredLighting_DecalHashU32( hash, source.generation ); + return hash; +} + +static int R_ModernClusteredLighting_ExactGridIndexForView( const viewDef_t *viewDef ) { + if ( viewDef == NULL ) { + return -1; + } + for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { + if ( rg_clusteredLightingFrame.grids[gridIndex].viewDef == viewDef ) { + return gridIndex; + } + } + return -1; +} + +static void R_ModernClusteredLighting_SetDecalReject( modernClusterDecalRejectInfo_t &reject, + rendererClusteredDecalReject_t reason, int sourceIndex, int viewIndex ) { + if ( reject.reason == RENDERER_CLUSTER_DECAL_REJECT_NONE ) { + reject.reason = reason; + reject.sourceIndex = sourceIndex; + reject.viewIndex = viewIndex; + } +} + +static bool R_ModernClusteredLighting_DecalSourceIdentityEqual( + const rendererClusteredDecalSource_t &a, const rendererClusteredDecalSource_t &b ) { + return a.viewDef == b.viewDef + && a.sourceSurface == b.sourceSurface + && a.commandIndex == b.commandIndex + && a.commandOrder == b.commandOrder + && a.materialStableId == b.materialStableId + && a.geometryStableId == b.geometryStableId + && a.instanceStableId == b.instanceStableId + && a.screenX1 == b.screenX1 + && a.screenY1 == b.screenY1 + && a.screenX2 == b.screenX2 + && a.screenY2 == b.screenY2 + && R_ModernClusteredLighting_DecalFloatBits( a.depthMin ) == R_ModernClusteredLighting_DecalFloatBits( b.depthMin ) + && R_ModernClusteredLighting_DecalFloatBits( a.depthMax ) == R_ModernClusteredLighting_DecalFloatBits( b.depthMax ) + && a.generation == b.generation; +} + +static bool R_ModernClusteredLighting_BuildDecalCandidate( + const rendererClusteredDecalSource_t *sources, int sourceCount, unsigned int generation, + modernClusterDecalFrameData_t &candidate, modernClusterDecalRejectInfo_t &reject ) { + R_ModernClusteredLighting_ClearDecalStorage( candidate ); + reject.reason = RENDERER_CLUSTER_DECAL_REJECT_NONE; + reject.sourceIndex = -1; + reject.viewIndex = -1; + + if ( sourceCount < 0 ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INVALID_ARGUMENT, -1, -1 ); + return false; + } + if ( sourceCount > RENDERER_CLUSTER_DECAL_MAX_RECORDS ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY, RENDERER_CLUSTER_DECAL_MAX_RECORDS, -1 ); + return false; + } + if ( sourceCount > 0 && sources == NULL ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INVALID_ARGUMENT, 0, -1 ); + return false; + } + if ( rg_clusteredLightingFrame.gridCount <= 0 + || rg_clusteredLightingFrame.grids.Num() != rg_clusteredLightingFrame.gridCount + || rg_clusteredLightingFrame.clusterCount <= 0 + || rg_clusteredLightingFrame.clusters.Num() != rg_clusteredLightingFrame.clusterCount ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, -1, -1 ); + return false; + } + + candidate.generation = generation; + candidate.views.SetNum( rg_clusteredLightingFrame.gridCount, false ); + for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { + modernClusterDecalViewRecord_t &view = candidate.views[gridIndex]; + memset( &view, 0, sizeof( view ) ); + view.viewDef = rg_clusteredLightingFrame.grids[gridIndex].viewDef; + view.gridIndex = gridIndex; + view.firstCommandOrder = -1; + view.lastCommandOrder = -1; + view.gridHash = R_ModernClusteredLighting_DecalGridHash( rg_clusteredLightingFrame.grids[gridIndex] ); + view.sourceOrderHash = MODERN_CLUSTER_DECAL_HASH_OFFSET; + R_ModernClusteredLighting_DecalHashU64( view.sourceOrderHash, view.gridHash ); + } + + int totalReferences = 0; + for ( int sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex ) { + const rendererClusteredDecalSource_t &source = sources[sourceIndex]; + const int gridIndex = R_ModernClusteredLighting_ExactGridIndexForView( source.viewDef ); + if ( gridIndex < 0 ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, sourceIndex, -1 ); + return false; + } + const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[gridIndex]; + modernClusterDecalViewRecord_t &view = candidate.views[gridIndex]; + if ( source.generation != generation ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION, sourceIndex, gridIndex ); + return false; + } + if ( source.sourceSurface == NULL + || source.materialStableId == RENDERER_CLUSTER_DECAL_INVALID_STABLE_ID + || source.geometryStableId == RENDERER_CLUSTER_DECAL_INVALID_STABLE_ID + || source.instanceStableId == RENDERER_CLUSTER_DECAL_INVALID_STABLE_ID ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, sourceIndex, gridIndex ); + return false; + } + if ( source.commandIndex < 0 || source.commandOrder < 0 + || ( view.recordCount > 0 && source.commandOrder <= view.lastCommandOrder ) ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_ORDER, sourceIndex, gridIndex ); + return false; + } + for ( int previousIndex = 0; previousIndex < candidate.records.Num(); ++previousIndex ) { + const modernClusterDecalRecord_t &previous = candidate.records[previousIndex]; + if ( previous.gridIndex == gridIndex + && ( previous.source.commandIndex == source.commandIndex + || previous.source.sourceSurface == source.sourceSurface ) ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_ORDER, sourceIndex, gridIndex ); + return false; + } + } + if ( source.screenX1 < 0 || source.screenY1 < 0 + || source.screenX2 < source.screenX1 || source.screenY2 < source.screenY1 + || source.screenX2 >= grid.width || source.screenY2 >= grid.height ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_RECT, sourceIndex, gridIndex ); + return false; + } + if ( !std::isfinite( static_cast( source.depthMin ) ) + || !std::isfinite( static_cast( source.depthMax ) ) + || source.depthMin <= 0.0f || source.depthMax < source.depthMin ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_DEPTH, sourceIndex, gridIndex ); + return false; + } + + modernClusterDecalRecord_t record; + memset( &record, 0, sizeof( record ) ); + record.source = source; + record.gridIndex = gridIndex; + record.tileMinX = idMath::ClampInt( 0, grid.tileCountX - 1, ( source.screenX1 * grid.tileCountX ) / Max( 1, grid.width ) ); + record.tileMaxX = idMath::ClampInt( 0, grid.tileCountX - 1, ( source.screenX2 * grid.tileCountX ) / Max( 1, grid.width ) ); + record.tileMinY = idMath::ClampInt( 0, grid.tileCountY - 1, ( source.screenY1 * grid.tileCountY ) / Max( 1, grid.height ) ); + record.tileMaxY = idMath::ClampInt( 0, grid.tileCountY - 1, ( source.screenY2 * grid.tileCountY ) / Max( 1, grid.height ) ); + record.sliceMinZ = R_ModernClusteredLighting_DepthSliceForZ( grid, source.depthMin ); + record.sliceMaxZ = R_ModernClusteredLighting_DepthSliceForZ( grid, source.depthMax ); + const long long referenceCount = + static_cast( record.tileMaxX + 1 - record.tileMinX ) + * static_cast( record.tileMaxY + 1 - record.tileMinY ) + * static_cast( record.sliceMaxZ + 1 - record.sliceMinZ ); + if ( referenceCount <= 0 + || referenceCount > RENDERER_CLUSTER_DECAL_MAX_REFERENCES + || static_cast( totalReferences ) + referenceCount > RENDERER_CLUSTER_DECAL_MAX_REFERENCES ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY, sourceIndex, gridIndex ); + return false; + } + record.clusterReferenceCount = static_cast( referenceCount ); + record.hash = R_ModernClusteredLighting_DecalSourceHash( source ); + candidate.records.Append( record ); + totalReferences += record.clusterReferenceCount; + + if ( view.recordCount == 0 ) { + view.firstCommandOrder = source.commandOrder; + candidate.viewCount++; + } + view.lastCommandOrder = source.commandOrder; + view.recordCount++; + view.clusterReferences += record.clusterReferenceCount; + R_ModernClusteredLighting_DecalHashU64( view.sourceOrderHash, record.hash ); + } + + candidate.clusters.SetNum( rg_clusteredLightingFrame.clusterCount, false ); + if ( candidate.clusters.Num() > 0 ) { + memset( candidate.clusters.Ptr(), 0, sizeof( modernClusterDecalClusterRecord_t ) * candidate.clusters.Num() ); + } + for ( int recordIndex = 0; recordIndex < candidate.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &record = candidate.records[recordIndex]; + const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[record.gridIndex]; + for ( int z = record.sliceMinZ; z <= record.sliceMaxZ; ++z ) { + for ( int y = record.tileMinY; y <= record.tileMaxY; ++y ) { + for ( int x = record.tileMinX; x <= record.tileMaxX; ++x ) { + const int clusterIndex = R_ModernClusteredLighting_ClusterIndex( grid, x, y, z ); + if ( clusterIndex < 0 || clusterIndex >= candidate.clusters.Num() ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INTERNAL, recordIndex, record.gridIndex ); + return false; + } + candidate.clusters[clusterIndex].referenceCount++; + } + } + } + } + + int referenceOffset = 0; + for ( int clusterIndex = 0; clusterIndex < candidate.clusters.Num(); ++clusterIndex ) { + modernClusterDecalClusterRecord_t &cluster = candidate.clusters[clusterIndex]; + cluster.firstReference = referenceOffset; + referenceOffset += cluster.referenceCount; + if ( referenceOffset > RENDERER_CLUSTER_DECAL_MAX_REFERENCES ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY, -1, -1 ); + return false; + } + } + if ( referenceOffset != totalReferences ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INTERNAL, -1, -1 ); + return false; + } + candidate.clusterReferences.SetNum( referenceOffset, false ); + for ( int recordIndex = 0; recordIndex < candidate.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &record = candidate.records[recordIndex]; + const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[record.gridIndex]; + for ( int z = record.sliceMinZ; z <= record.sliceMaxZ; ++z ) { + for ( int y = record.tileMinY; y <= record.tileMaxY; ++y ) { + for ( int x = record.tileMinX; x <= record.tileMaxX; ++x ) { + const int clusterIndex = R_ModernClusteredLighting_ClusterIndex( grid, x, y, z ); + modernClusterDecalClusterRecord_t &cluster = candidate.clusters[clusterIndex]; + const int destination = cluster.firstReference + cluster.writeCount; + if ( cluster.writeCount >= cluster.referenceCount + || destination < 0 || destination >= candidate.clusterReferences.Num() ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INTERNAL, recordIndex, record.gridIndex ); + return false; + } + candidate.clusterReferences[destination] = recordIndex; + cluster.writeCount++; + } + } + } + } + for ( int clusterIndex = 0; clusterIndex < candidate.clusters.Num(); ++clusterIndex ) { + if ( candidate.clusters[clusterIndex].writeCount != candidate.clusters[clusterIndex].referenceCount ) { + R_ModernClusteredLighting_SetDecalReject( reject, RENDERER_CLUSTER_DECAL_REJECT_INTERNAL, -1, -1 ); + return false; + } + } + + candidate.sourceOrderHash = MODERN_CLUSTER_DECAL_HASH_OFFSET; + R_ModernClusteredLighting_DecalHashU32( candidate.sourceOrderHash, generation ); + for ( int viewIndex = 0; viewIndex < candidate.views.Num(); ++viewIndex ) { + modernClusterDecalViewRecord_t &view = candidate.views[viewIndex]; + if ( view.recordCount <= 0 ) { + continue; + } + R_ModernClusteredLighting_DecalHashInt( view.sourceOrderHash, view.recordCount ); + R_ModernClusteredLighting_DecalHashInt( view.sourceOrderHash, view.firstCommandOrder ); + R_ModernClusteredLighting_DecalHashInt( view.sourceOrderHash, view.lastCommandOrder ); + R_ModernClusteredLighting_DecalHashInt( candidate.sourceOrderHash, viewIndex ); + R_ModernClusteredLighting_DecalHashU64( candidate.sourceOrderHash, view.sourceOrderHash ); + } + candidate.valid = true; + return true; +} + +const char *R_ModernClusteredLighting_DecalRejectName( rendererClusteredDecalReject_t reject ) { + switch ( reject ) { + case RENDERER_CLUSTER_DECAL_REJECT_NONE: return "none"; + case RENDERER_CLUSTER_DECAL_REJECT_NOT_PREPARED: return "not-prepared"; + case RENDERER_CLUSTER_DECAL_REJECT_INVALID_ARGUMENT: return "invalid-argument"; + case RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE: return "resource-unavailable"; + case RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY: return "record-capacity"; + case RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY: return "reference-capacity"; + case RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION: return "stale-generation"; + case RENDERER_CLUSTER_DECAL_REJECT_STALE_IDENTITY: return "stale-identity"; + case RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_RECT: return "malformed-rect"; + case RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_DEPTH: return "malformed-depth"; + case RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_ORDER: return "malformed-order"; + case RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE: return "view-incomplete"; + case RENDERER_CLUSTER_DECAL_REJECT_ORDER_MISMATCH: return "order-mismatch"; + case RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH: return "hash-mismatch"; + case RENDERER_CLUSTER_DECAL_REJECT_INTERNAL: return "internal"; + default: return "unknown"; + } +} + +static bool R_ModernClusteredLighting_RejectDecals( rendererClusteredDecalReject_t reason, + int sourceIndex, int viewIndex, bool seal, int submittedRecords, unsigned int generation ) { + R_ModernClusteredLighting_ClearDecalStorage( rg_clusteredDecalFrame ); + memset( &rg_clusteredDecalStats, 0, sizeof( rg_clusteredDecalStats ) ); + rg_clusteredDecalStats.generation = generation; + rg_clusteredDecalStats.submittedRecords = Max( 0, submittedRecords ); + rg_clusteredDecalStats.recordCapacity = RENDERER_CLUSTER_DECAL_MAX_RECORDS; + rg_clusteredDecalStats.referenceCapacity = RENDERER_CLUSTER_DECAL_MAX_REFERENCES; + rg_clusteredDecalStats.prepareRejects = seal ? 0 : 1; + rg_clusteredDecalStats.sealRejects = seal ? 1 : 0; + rg_clusteredDecalStats.lastReject = reason; + rg_clusteredDecalStats.lastRejectSource = sourceIndex; + rg_clusteredDecalStats.lastRejectView = viewIndex; + idStr::snPrintf( rg_clusteredDecalStats.status, sizeof( rg_clusteredDecalStats.status ), + "%s-rejected:%s", seal ? "seal" : "prepare", R_ModernClusteredLighting_DecalRejectName( reason ) ); + return false; +} + +bool R_ModernClusteredLighting_RejectDecalsForFrame( rendererClusteredDecalReject_t reason, + int sourceIndex, int submittedRecords, unsigned int generation ) { + if ( reason == RENDERER_CLUSTER_DECAL_REJECT_NONE ) { + reason = RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE; + } + return R_ModernClusteredLighting_RejectDecals( + reason, sourceIndex, -1, false, submittedRecords, generation ); +} + +bool R_ModernClusteredLighting_PrepareDecals( const rendererClusteredDecalSource_t *sources, + int sourceCount, unsigned int generation ) { + R_ModernClusteredLighting_ResetDecalTransaction(); + modernClusterDecalFrameData_t &candidate = rg_clusteredDecalPrepareScratch; + modernClusterDecalRejectInfo_t reject; + if ( !R_ModernClusteredLighting_BuildDecalCandidate( sources, sourceCount, generation, candidate, reject ) ) { + return R_ModernClusteredLighting_RejectDecals( reject.reason, reject.sourceIndex, + reject.viewIndex, false, sourceCount, generation ); + } + rg_clusteredDecalStats.prepared = true; + rg_clusteredDecalStats.csrReady = true; + rg_clusteredDecalStats.generation = generation; + rg_clusteredDecalStats.submittedRecords = sourceCount; + rg_clusteredDecalStats.stagedRecords = candidate.records.Num(); + rg_clusteredDecalStats.viewCount = candidate.viewCount; + rg_clusteredDecalStats.clusterCount = candidate.clusters.Num(); + rg_clusteredDecalStats.clusterReferences = candidate.clusterReferences.Num(); + rg_clusteredDecalStats.sourceOrderHash = candidate.sourceOrderHash; + rg_clusteredDecalFrame.generation = candidate.generation; + rg_clusteredDecalFrame.sourceOrderHash = candidate.sourceOrderHash; + rg_clusteredDecalFrame.viewCount = candidate.viewCount; + rg_clusteredDecalFrame.valid = candidate.valid; + rg_clusteredDecalFrame.records.Swap( candidate.records ); + rg_clusteredDecalFrame.clusters.Swap( candidate.clusters ); + rg_clusteredDecalFrame.clusterReferences.Swap( candidate.clusterReferences ); + rg_clusteredDecalFrame.views.Swap( candidate.views ); + idStr::Copynz( rg_clusteredDecalStats.status, "prepared", sizeof( rg_clusteredDecalStats.status ) ); + return true; +} + +static rendererClusteredDecalReject_t R_ModernClusteredLighting_DecalRecordMismatch( + const modernClusterDecalRecord_t &prepared, const modernClusterDecalRecord_t &sealed ) { + if ( prepared.source.generation != sealed.source.generation ) { + return RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION; + } + if ( prepared.source.viewDef != sealed.source.viewDef + || prepared.source.sourceSurface != sealed.source.sourceSurface + || prepared.source.materialStableId != sealed.source.materialStableId + || prepared.source.geometryStableId != sealed.source.geometryStableId + || prepared.source.instanceStableId != sealed.source.instanceStableId ) { + return RENDERER_CLUSTER_DECAL_REJECT_STALE_IDENTITY; + } + if ( prepared.source.commandIndex != sealed.source.commandIndex + || prepared.source.commandOrder != sealed.source.commandOrder ) { + return RENDERER_CLUSTER_DECAL_REJECT_ORDER_MISMATCH; + } + return RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH; +} + +bool R_ModernClusteredLighting_SealDecals( const rendererClusteredDecalSource_t *sources, + int sourceCount, unsigned int generation ) { + if ( !rg_clusteredDecalStats.prepared || !rg_clusteredDecalFrame.valid ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_NOT_PREPARED, + -1, -1, true, sourceCount, generation ); + } + if ( generation != rg_clusteredDecalFrame.generation ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION, + -1, -1, true, sourceCount, generation ); + } + + modernClusterDecalFrameData_t &candidate = rg_clusteredDecalSealScratch; + modernClusterDecalRejectInfo_t reject; + if ( !R_ModernClusteredLighting_BuildDecalCandidate( sources, sourceCount, generation, candidate, reject ) ) { + return R_ModernClusteredLighting_RejectDecals( reject.reason, reject.sourceIndex, + reject.viewIndex, true, sourceCount, generation ); + } + if ( candidate.records.Num() != rg_clusteredDecalFrame.records.Num() + || candidate.views.Num() != rg_clusteredDecalFrame.views.Num() ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + -1, -1, true, sourceCount, generation ); + } + // Compare exact source identity before aggregate view hashes so stale + // pointer/resource identity receives a useful diagnostic rather than being + // collapsed into a generic hash mismatch. + for ( int recordIndex = 0; recordIndex < candidate.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &prepared = rg_clusteredDecalFrame.records[recordIndex]; + const modernClusterDecalRecord_t &sealed = candidate.records[recordIndex]; + if ( !R_ModernClusteredLighting_DecalSourceIdentityEqual( prepared.source, sealed.source ) + || prepared.gridIndex != sealed.gridIndex || prepared.hash != sealed.hash ) { + return R_ModernClusteredLighting_RejectDecals( + R_ModernClusteredLighting_DecalRecordMismatch( prepared, sealed ), + recordIndex, prepared.gridIndex, true, sourceCount, generation ); + } + } + for ( int viewIndex = 0; viewIndex < candidate.views.Num(); ++viewIndex ) { + const modernClusterDecalViewRecord_t &prepared = rg_clusteredDecalFrame.views[viewIndex]; + const modernClusterDecalViewRecord_t &sealed = candidate.views[viewIndex]; + if ( prepared.viewDef != sealed.viewDef || prepared.gridIndex != sealed.gridIndex + || prepared.gridHash != sealed.gridHash ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_STALE_IDENTITY, + -1, viewIndex, true, sourceCount, generation ); + } + if ( prepared.recordCount != sealed.recordCount ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + -1, viewIndex, true, sourceCount, generation ); + } + if ( prepared.firstCommandOrder != sealed.firstCommandOrder + || prepared.lastCommandOrder != sealed.lastCommandOrder ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_ORDER_MISMATCH, + -1, viewIndex, true, sourceCount, generation ); + } + if ( prepared.sourceOrderHash != sealed.sourceOrderHash + || prepared.clusterReferences != sealed.clusterReferences ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH, + -1, viewIndex, true, sourceCount, generation ); + } + } + if ( candidate.clusters.Num() != rg_clusteredDecalFrame.clusters.Num() + || candidate.clusterReferences.Num() != rg_clusteredDecalFrame.clusterReferences.Num() + || candidate.sourceOrderHash != rg_clusteredDecalFrame.sourceOrderHash ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH, + -1, -1, true, sourceCount, generation ); + } + for ( int clusterIndex = 0; clusterIndex < candidate.clusters.Num(); ++clusterIndex ) { + const modernClusterDecalClusterRecord_t &prepared = rg_clusteredDecalFrame.clusters[clusterIndex]; + const modernClusterDecalClusterRecord_t &sealed = candidate.clusters[clusterIndex]; + if ( prepared.firstReference != sealed.firstReference + || prepared.referenceCount != sealed.referenceCount + || prepared.writeCount != sealed.writeCount ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH, + -1, -1, true, sourceCount, generation ); + } + } + for ( int referenceIndex = 0; referenceIndex < candidate.clusterReferences.Num(); ++referenceIndex ) { + if ( candidate.clusterReferences[referenceIndex] != rg_clusteredDecalFrame.clusterReferences[referenceIndex] ) { + return R_ModernClusteredLighting_RejectDecals( RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH, + -1, -1, true, sourceCount, generation ); + } + } + + int ownedViews = 0; + for ( int viewIndex = 0; viewIndex < rg_clusteredDecalFrame.views.Num(); ++viewIndex ) { + modernClusterDecalViewRecord_t &view = rg_clusteredDecalFrame.views[viewIndex]; + view.owned = view.recordCount > 0; + if ( view.owned ) { + ownedViews++; + } + } + rg_clusteredDecalStats.sealed = true; + rg_clusteredDecalStats.ownershipReady = true; + rg_clusteredDecalStats.publishedRecords = rg_clusteredDecalFrame.records.Num(); + rg_clusteredDecalStats.ownedViews = ownedViews; + idStr::Copynz( rg_clusteredDecalStats.status, "sealed", sizeof( rg_clusteredDecalStats.status ) ); + return true; +} + +const rendererClusteredDecalStats_t &R_ModernClusteredLighting_DecalStats( void ) { + return rg_clusteredDecalStats; +} + +bool R_ModernClusteredLighting_DecalViewStats( const viewDef_t *viewDef, + rendererClusteredDecalViewStats_t &stats ) { + memset( &stats, 0, sizeof( stats ) ); + stats.viewDef = viewDef; + stats.firstCommandOrder = -1; + stats.lastCommandOrder = -1; + if ( !rg_clusteredDecalStats.prepared || !rg_clusteredDecalFrame.valid || viewDef == NULL ) { + return false; + } + for ( int viewIndex = 0; viewIndex < rg_clusteredDecalFrame.views.Num(); ++viewIndex ) { + const modernClusterDecalViewRecord_t &view = rg_clusteredDecalFrame.views[viewIndex]; + if ( view.viewDef != viewDef ) { + continue; + } + stats.prepared = true; + stats.owned = rg_clusteredDecalStats.ownershipReady && view.owned; + stats.recordCount = view.recordCount; + stats.firstCommandOrder = view.firstCommandOrder; + stats.lastCommandOrder = view.lastCommandOrder; + stats.clusterReferences = view.clusterReferences; + stats.sourceOrderHash = view.sourceOrderHash; + return true; + } + return false; +} + +bool R_ModernClusteredLighting_DecalOwnsSurface( const viewDef_t *viewDef, const void *sourceSurface ) { + if ( !r_rendererModernQuality.GetBool() + || !r_rendererClusteredDecals.GetBool() + || r_skipDecals.GetBool() + || r_showOverDraw.GetInteger() != 0 + || r_rendererSharedWorldAmbient.GetBool() + || !rg_clusteredDecalStats.ownershipReady + || rg_clusteredDecalStats.generation != R_ModernClusteredLighting_CurrentDecalGeneration() + || viewDef == NULL || sourceSurface == NULL ) { + return false; + } + const int viewIndex = R_ModernClusteredLighting_ExactGridIndexForView( viewDef ); + if ( viewIndex < 0 || viewIndex >= rg_clusteredDecalFrame.views.Num() + || !rg_clusteredDecalFrame.views[viewIndex].owned ) { + return false; + } + for ( int recordIndex = 0; recordIndex < rg_clusteredDecalFrame.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &record = rg_clusteredDecalFrame.records[recordIndex]; + if ( record.gridIndex == viewIndex && record.source.sourceSurface == sourceSurface ) { + return true; + } + } + return false; +} + +bool R_ModernClusteredLighting_DecalOwnsCommand( const viewDef_t *viewDef, int commandIndex ) { + if ( !r_rendererModernQuality.GetBool() + || !r_rendererClusteredDecals.GetBool() + || r_skipDecals.GetBool() + || r_showOverDraw.GetInteger() != 0 + || r_rendererSharedWorldAmbient.GetBool() + || !rg_clusteredDecalStats.ownershipReady + || rg_clusteredDecalStats.generation != R_ModernClusteredLighting_CurrentDecalGeneration() + || viewDef == NULL || commandIndex < 0 ) { + return false; + } + const int viewIndex = R_ModernClusteredLighting_ExactGridIndexForView( viewDef ); + if ( viewIndex < 0 || viewIndex >= rg_clusteredDecalFrame.views.Num() + || !rg_clusteredDecalFrame.views[viewIndex].owned ) { + return false; + } + for ( int recordIndex = 0; recordIndex < rg_clusteredDecalFrame.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &record = rg_clusteredDecalFrame.records[recordIndex]; + if ( record.gridIndex == viewIndex && record.source.commandIndex == commandIndex ) { + return true; + } + } + return false; +} + +int R_ModernClusteredLighting_DecalSealedCommandForSurface( const viewDef_t *viewDef, + const void *sourceSurface, bool &duplicate ) { + duplicate = false; + if ( !R_ModernClusteredLighting_DecalOwnsSurface( viewDef, sourceSurface ) ) { + return -1; + } + const int viewIndex = R_ModernClusteredLighting_ExactGridIndexForView( viewDef ); + int commandIndex = -1; + for ( int recordIndex = 0; recordIndex < rg_clusteredDecalFrame.records.Num(); ++recordIndex ) { + const modernClusterDecalRecord_t &record = rg_clusteredDecalFrame.records[recordIndex]; + if ( record.gridIndex != viewIndex || record.source.sourceSurface != sourceSurface ) { + continue; + } + if ( commandIndex >= 0 ) { + duplicate = true; + return -1; + } + commandIndex = record.source.commandIndex; + } + return commandIndex; +} + static void R_ModernClusteredLighting_CountClusterReference( modernClusterRecord_t &cluster, rendererClusteredLightingStats_t &stats ) { cluster.lightCount++; stats.lightReferences++; @@ -994,13 +1808,31 @@ static void R_ModernClusteredLighting_FillClusterReference( modernClusterRecord_ cluster.writeLightCount++; } -static void R_ModernClusteredLighting_BinLight( const modernClusterGridRecord_t &grid, int lightIndex, const modernClusterLightRecord_t &light, rendererClusteredLightingStats_t &stats, bool fillReferences ) { - const int tileMinX = idMath::ClampInt( 0, grid.tileCountX - 1, ( light.scissor.x1 * grid.tileCountX ) / Max( 1, grid.width ) ); - const int tileMaxX = idMath::ClampInt( 0, grid.tileCountX - 1, ( light.scissor.x2 * grid.tileCountX ) / Max( 1, grid.width ) ); - const int tileMinY = idMath::ClampInt( 0, grid.tileCountY - 1, ( light.scissor.y1 * grid.tileCountY ) / Max( 1, grid.height ) ); - const int tileMaxY = idMath::ClampInt( 0, grid.tileCountY - 1, ( light.scissor.y2 * grid.tileCountY ) / Max( 1, grid.height ) ); - const int sliceMinZ = light.fullDepthRange ? 0 : R_ModernClusteredLighting_DepthSliceForZ( grid, light.depthMin ); - const int sliceMaxZ = light.fullDepthRange ? grid.sliceCountZ - 1 : R_ModernClusteredLighting_DepthSliceForZ( grid, light.depthMax ); +static void R_ModernClusteredLighting_BinLight( const modernClusterGridRecord_t &grid, int lightIndex, modernClusterLightRecord_t &light, rendererClusteredLightingStats_t &stats, bool fillReferences ) { + int tileMinX, tileMaxX, tileMinY, tileMaxY, sliceMinZ, sliceMaxZ; + if ( fillReferences ) { + // The counting pass (which always runs first on this CPU path) already + // derived these bounds from the same immutable grid/light inputs. + tileMinX = light.binTileMinX; + tileMaxX = light.binTileMaxX; + tileMinY = light.binTileMinY; + tileMaxY = light.binTileMaxY; + sliceMinZ = light.binSliceMinZ; + sliceMaxZ = light.binSliceMaxZ; + } else { + tileMinX = idMath::ClampInt( 0, grid.tileCountX - 1, ( light.scissor.x1 * grid.tileCountX ) / Max( 1, grid.width ) ); + tileMaxX = idMath::ClampInt( 0, grid.tileCountX - 1, ( light.scissor.x2 * grid.tileCountX ) / Max( 1, grid.width ) ); + tileMinY = idMath::ClampInt( 0, grid.tileCountY - 1, ( light.scissor.y1 * grid.tileCountY ) / Max( 1, grid.height ) ); + tileMaxY = idMath::ClampInt( 0, grid.tileCountY - 1, ( light.scissor.y2 * grid.tileCountY ) / Max( 1, grid.height ) ); + sliceMinZ = light.fullDepthRange ? 0 : R_ModernClusteredLighting_DepthSliceForZ( grid, light.depthMin ); + sliceMaxZ = light.fullDepthRange ? grid.sliceCountZ - 1 : R_ModernClusteredLighting_DepthSliceForZ( grid, light.depthMax ); + light.binTileMinX = tileMinX; + light.binTileMaxX = tileMaxX; + light.binTileMinY = tileMinY; + light.binTileMaxY = tileMaxY; + light.binSliceMinZ = sliceMinZ; + light.binSliceMaxZ = sliceMaxZ; + } for ( int z = sliceMinZ; z <= sliceMaxZ; ++z ) { for ( int y = tileMinY; y <= tileMaxY; ++y ) { @@ -1260,12 +2092,19 @@ static bool R_ModernClusteredLighting_AppendLightRecord( modernClusterGridRecord } else if ( !R_ModernClusteredLighting_FormatDebugString( record.debugName, sizeof( record.debugName ), "%s:%d:%s", R_ModernClusteredLighting_TypeName( record.type ), record.lightDefIndex, shaderName != NULL ? shaderName : "" ) ) { R_ModernClusteredLighting_RecordDebugStringTruncation( stats, "cluster light debugName" ); } - } else if ( stageIndex >= 0 ) { - if ( !R_ModernClusteredLighting_FormatDebugString( record.debugName, sizeof( record.debugName ), "%s:%d:s%d", R_ModernClusteredLighting_TypeName( record.type ), record.lightDefIndex, stageIndex ) ) { - R_ModernClusteredLighting_RecordDebugStringTruncation( stats, "cluster stage light shortName" ); + } else if ( r_shadowMapReport.GetInteger() >= 2 ) { + // The only consumer of debugName outside the metrics/clusterDebug overlays + // is the shadow-map join report (also gated on r_shadowMapReport >= 2), and + // it substitutes "" for an empty name. When no diagnostic wants + // names, skip the per-record vsnPrintf entirely; debugName stays empty from + // the record memset. + if ( stageIndex >= 0 ) { + if ( !R_ModernClusteredLighting_FormatDebugString( record.debugName, sizeof( record.debugName ), "%s:%d:s%d", R_ModernClusteredLighting_TypeName( record.type ), record.lightDefIndex, stageIndex ) ) { + R_ModernClusteredLighting_RecordDebugStringTruncation( stats, "cluster stage light shortName" ); + } + } else if ( !R_ModernClusteredLighting_FormatDebugString( record.debugName, sizeof( record.debugName ), "%s:%d", R_ModernClusteredLighting_TypeName( record.type ), record.lightDefIndex ) ) { + R_ModernClusteredLighting_RecordDebugStringTruncation( stats, "cluster light shortName" ); } - } else if ( !R_ModernClusteredLighting_FormatDebugString( record.debugName, sizeof( record.debugName ), "%s:%d", R_ModernClusteredLighting_TypeName( record.type ), record.lightDefIndex ) ) { - R_ModernClusteredLighting_RecordDebugStringTruncation( stats, "cluster light shortName" ); } R_ModernClusteredLighting_FillDescriptor( record, grid, vLight, lightStage ); @@ -1280,6 +2119,9 @@ static bool R_ModernClusteredLighting_AddLight( modernClusterGridRecord_t &grid, if ( vLight == NULL ) { return false; } + if ( R_ModernClusteredLighting_IsSpecularProbe( vLight ) ) { + return false; + } if ( !R_ModernClusteredLighting_LightAreaVisible( grid.viewDef, vLight ) ) { stats.culledLights++; return false; @@ -1316,6 +2158,252 @@ static bool R_ModernClusteredLighting_AddLight( modernClusterGridRecord_t &grid, return R_ModernClusteredLighting_AppendLightRecord( grid, vLight, scissor, type, NULL, -1, R_ModernClusteredLighting_LightColor( vLight ), stats ); } +static bool R_ModernClusteredLighting_FiniteVec3( const idVec3 &value ) { + return std::isfinite( value.x ) && std::isfinite( value.y ) + && std::isfinite( value.z ); +} + +static bool R_ModernClusteredLighting_NormalizeProbeAxis( idVec3 &axis ) { + if ( !R_ModernClusteredLighting_FiniteVec3( axis ) ) { + return false; + } + const float lengthSquared = axis * axis; + if ( !std::isfinite( lengthSquared ) || lengthSquared <= 0.000001f ) { + return false; + } + axis *= 1.0f / std::sqrt( lengthSquared ); + return R_ModernClusteredLighting_FiniteVec3( axis ); +} + +static void R_ModernClusteredLighting_ProbeAxisInView( const viewDef_t *viewDef, + const idVec3 &worldAxis, idVec3 &viewAxis ) { + if ( viewDef == NULL ) { + viewAxis = worldAxis; + return; + } + viewAxis.x = worldAxis * viewDef->renderView.viewaxis[1]; + viewAxis.y = worldAxis * viewDef->renderView.viewaxis[2]; + viewAxis.z = worldAxis * viewDef->renderView.viewaxis[0]; +} + +static unsigned int R_ModernClusteredLighting_ProbeGenerationExact( std::uint64_t generation ) { + unsigned int exact = static_cast( generation & 0x00ffffffull ); + return exact != 0 ? exact : 1u; +} + +static unsigned int R_ModernClusteredLighting_PublishedProbeFrameGeneration( + bool recordsComplete, int uploadedRecords, int totalRecords, + bool atlasReady, bool atlasFrameReady, std::uint64_t frameGeneration ) { + if ( !recordsComplete || totalRecords <= 0 || uploadedRecords != totalRecords + || !atlasReady || !atlasFrameReady || frameGeneration == 0 ) { + return 0u; + } + return R_ModernClusteredLighting_ProbeGenerationExact( frameGeneration ); +} + +static bool R_ModernClusteredLighting_AppendSpecularProbe( modernClusterGridRecord_t &grid, + const viewLight_t *vLight, rendererClusteredLightingStats_t &stats ) { + if ( vLight == NULL || !R_ModernClusteredLighting_IsSpecularProbe( vLight ) ) { + return false; + } + if ( !R_ModernClusteredLighting_LightAreaVisible( grid.viewDef, vLight ) ) { + return false; + } + idScreenRect scissor = vLight->scissorRect; + if ( scissor.IsEmpty() ) { + return false; + } + R_ModernClusteredLighting_ClampRectToView( scissor, grid.viewDef ); + if ( scissor.IsEmpty() ) { + return false; + } + if ( rg_clusteredLightingFrame.probeCount >= RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS ) { + stats.probeOverflow++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + + const idMaterial *material = R_ModernClusteredLighting_ResolvedLightShader( vLight ); + if ( material == NULL || !material->HasSpecularProbe() ) { + stats.probeRejectedMaterial++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + const specularProbeMaterialInfo_t &info = material->GetSpecularProbeInfo(); + bool materialValid = info.enabled && info.cubeImage != NULL + && info.cubeConvention != SPECULAR_PROBE_CUBE_NONE + && std::isfinite( info.intensity ) && info.intensity > 0.0f && info.intensity <= 64.0f + && std::isfinite( info.blendFraction ) && info.blendFraction > 0.0f && info.blendFraction <= 1.0f + && info.priority >= 0 && info.priority <= 255; + for ( int component = 0; component < 3; ++component ) { + materialValid = materialValid && std::isfinite( info.tint[component] ) + && info.tint[component] >= 0.0f && info.tint[component] <= 64.0f; + } + if ( !materialValid ) { + stats.probeRejectedMaterial++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + + const idVec3 radii = vLight->lightRadius; + const float radiusMin = Min( radii.x, Min( radii.y, radii.z ) ); + const float radiusMax = Max( radii.x, Max( radii.y, radii.z ) ); + const int stableIdentity = vLight->lightDef != NULL ? vLight->lightDef->index : -1; + if ( !vLight->pointLight || vLight->parallel || stableIdentity < 0 + || !R_ModernClusteredLighting_FiniteVec3( vLight->globalLightOrigin ) + || !R_ModernClusteredLighting_FiniteVec3( radii ) + || radiusMin <= 0.0f || radiusMax > 32768.0f + || radiusMax - radiusMin > Max( 0.01f, radiusMax * 0.01f ) ) { + stats.probeRejectedVolume++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + + idVec3 axisX; + idVec3 axisY; + idVec3 axisZ; + R_ModernClusteredLighting_ProbeAxisInView( grid.viewDef, vLight->lightAxis[0], axisX ); + R_ModernClusteredLighting_ProbeAxisInView( grid.viewDef, vLight->lightAxis[1], axisY ); + R_ModernClusteredLighting_ProbeAxisInView( grid.viewDef, vLight->lightAxis[2], axisZ ); + if ( !R_ModernClusteredLighting_NormalizeProbeAxis( axisX ) + || !R_ModernClusteredLighting_NormalizeProbeAxis( axisY ) + || !R_ModernClusteredLighting_NormalizeProbeAxis( axisZ ) ) { + stats.probeRejectedVolume++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + const float determinant = + ( axisX.y * axisY.z - axisX.z * axisY.y ) * axisZ.x + + ( axisX.z * axisY.x - axisX.x * axisY.z ) * axisZ.y + + ( axisX.x * axisY.y - axisX.y * axisY.x ) * axisZ.z; + if ( idMath::Fabs( axisX * axisY ) >= 0.01f + || idMath::Fabs( axisX * axisZ ) >= 0.01f + || idMath::Fabs( axisY * axisZ ) >= 0.01f + || idMath::Fabs( determinant ) <= 0.99f ) { + stats.probeRejectedVolume++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + + modernSpecularProbeAtlasPlacement_t placement; + const modernSpecularProbeAtlasReject_t atlasReject = + R_ModernClusteredLighting_AcquireProbeAtlas( info.cubeImage, &placement ); + if ( atlasReject != MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE + || !placement.valid + || placement.slot < 0 || placement.slot >= MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES + || placement.faceSize != MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE + || placement.sourceStorageGeneration == 0 + || placement.residencyGeneration == 0 ) { + stats.probeRejectedAtlas++; + rg_clusteredLightingFrame.probeSetComplete = false; + return false; + } + + rg_clusteredLightingFrame.probes.SetNum( rg_clusteredLightingFrame.probeCount + 1, false ); + modernSpecularProbeRecord_t &record = rg_clusteredLightingFrame.probes[rg_clusteredLightingFrame.probeCount]; + memset( &record, 0, sizeof( record ) ); + record.sceneIndex = grid.sceneIndex; + record.stableLightIdentity = stableIdentity & 0x00ffffff; + record.priority = info.priority; + R_ModernClusteredLighting_CameraPoint( grid.viewDef, vLight->globalLightOrigin, record.cameraOrigin ); + record.axisX = axisX; + record.axisY = axisY; + record.axisZ = axisZ; + record.tint.Set( info.tint[0], info.tint[1], info.tint[2] ); + record.intensity = info.intensity; + record.radius = ( radii.x + radii.y + radii.z ) / 3.0f; + record.blendFraction = info.blendFraction; + record.depthMin = idMath::ClampFloat( grid.nearZ, grid.farZ, record.cameraOrigin.z - record.radius ); + record.depthMax = idMath::ClampFloat( grid.nearZ, grid.farZ, record.cameraOrigin.z + record.radius ); + record.scissor = scissor; + record.placement = placement; + grid.probeCount++; + rg_clusteredLightingFrame.probeCount++; + stats.probeCount = rg_clusteredLightingFrame.probeCount; + return true; +} + +static void R_ModernClusteredLighting_ClusterCenter( const modernClusterGridRecord_t &grid, + int tileX, int tileY, int sliceZ, float center[3] ) { + const float nearZ = Max( 0.01f, grid.nearZ ); + const float farZ = Max( nearZ + 1.0f, grid.farZ ); + const float sliceFraction = ( static_cast( sliceZ ) + 0.5f ) + / static_cast( Max( 1, grid.sliceCountZ ) ); + const float z = nearZ * std::exp( std::log( farZ / nearZ ) * sliceFraction ); + const float ndcX = ( static_cast( tileX ) + 0.5f ) + / static_cast( Max( 1, grid.tileCountX ) ) * 2.0f - 1.0f; + const float ndcY = 1.0f - ( static_cast( tileY ) + 0.5f ) + / static_cast( Max( 1, grid.tileCountY ) ) * 2.0f; + center[0] = 0.0f; + center[1] = 0.0f; + center[2] = z; + if ( grid.viewDef != NULL ) { + const float projectionX = grid.viewDef->projectionMatrix[0]; + const float projectionY = grid.viewDef->projectionMatrix[5]; + if ( idMath::Fabs( projectionX ) > 0.0001f ) { + center[0] = -( ndcX + grid.viewDef->projectionMatrix[8] ) * z / projectionX; + } + if ( idMath::Fabs( projectionY ) > 0.0001f ) { + center[1] = ( ndcY + grid.viewDef->projectionMatrix[9] ) * z / projectionY; + } + } +} + +static void R_ModernClusteredLighting_BinSpecularProbes( rendererClusteredLightingStats_t &stats ) { + stats.probeReferences = 0; + for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { + const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[gridIndex]; + for ( int localProbeIndex = 0; localProbeIndex < grid.probeCount; ++localProbeIndex ) { + const int probeIndex = grid.firstProbe + localProbeIndex; + if ( probeIndex < 0 || probeIndex >= rg_clusteredLightingFrame.probeCount ) { + continue; + } + const modernSpecularProbeRecord_t &probe = rg_clusteredLightingFrame.probes[probeIndex]; + const int tileMinX = idMath::ClampInt( 0, grid.tileCountX - 1, ( probe.scissor.x1 * grid.tileCountX ) / Max( 1, grid.width ) ); + const int tileMaxX = idMath::ClampInt( 0, grid.tileCountX - 1, ( probe.scissor.x2 * grid.tileCountX ) / Max( 1, grid.width ) ); + const int tileMinY = idMath::ClampInt( 0, grid.tileCountY - 1, ( probe.scissor.y1 * grid.tileCountY ) / Max( 1, grid.height ) ); + const int tileMaxY = idMath::ClampInt( 0, grid.tileCountY - 1, ( probe.scissor.y2 * grid.tileCountY ) / Max( 1, grid.height ) ); + const int sliceMinZ = R_ModernClusteredLighting_DepthSliceForZ( grid, probe.depthMin ); + const int sliceMaxZ = R_ModernClusteredLighting_DepthSliceForZ( grid, probe.depthMax ); + const float origin[3] = { probe.cameraOrigin.x, probe.cameraOrigin.y, probe.cameraOrigin.z }; + const float blendDistance = probe.radius * probe.blendFraction; + for ( int z = sliceMinZ; z <= sliceMaxZ; ++z ) { + for ( int y = tileMinY; y <= tileMaxY; ++y ) { + for ( int x = tileMinX; x <= tileMaxX; ++x ) { + const int clusterIndex = R_ModernClusteredLighting_ClusterIndex( grid, x, y, z ); + if ( clusterIndex < 0 || clusterIndex >= rg_clusteredLightingFrame.clusterCount ) { + continue; + } + float center[3]; + R_ModernClusteredLighting_ClusterCenter( grid, x, y, z, center ); + advancedLightingProbeCandidate_t candidate; + candidate.probeIndex = probeIndex; + candidate.stableId = probe.stableLightIdentity; + const float sphericalWeight = AdvancedLightingCore_SphericalProbeWeight( + origin, probe.radius, center, blendDistance ); + // A cluster AABB can intersect a probe while its center lies exactly + // outside the sphere. Keep that conservative reference, but rank it + // below every positive center contributor so an authored high-priority + // edge cannot crowd a valid third probe out of the bounded top-two set. + candidate.priority = sphericalWeight > 0.0f ? probe.priority : -1; + candidate.weight = sphericalWeight > 0.0f + ? sphericalWeight + : static_cast( probe.priority + 1 ) * 0.000001f; + AdvancedLightingCore_SelectProbe( rg_clusteredLightingFrame.clusters[clusterIndex].probes, candidate ); + } + } + } + } + } + for ( int clusterIndex = 0; clusterIndex < rg_clusteredLightingFrame.clusterCount; ++clusterIndex ) { + for ( int candidateIndex = 0; candidateIndex < RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER; ++candidateIndex ) { + if ( rg_clusteredLightingFrame.clusters[clusterIndex].probes[candidateIndex].probeIndex >= 0 ) { + stats.probeReferences++; + } + } + } +} + static void R_ModernClusteredLighting_BinFrameReferences( rendererClusteredLightingStats_t &stats, bool fillReferences ) { for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[gridIndex]; @@ -1437,7 +2525,19 @@ static void R_ModernClusteredLighting_FillClusterIndexList( rendererClusteredLig static void R_ModernClusteredLighting_FinalizeClusterStats( rendererClusteredLightingStats_t &stats ) { R_ModernClusteredLighting_BuildClusterLayout( stats ); - R_ModernClusteredLighting_FillClusterIndexList( stats ); + // The CPU fill pass exists solely to populate clusterLightIndices for the + // non-compute upload branch. When compute binning owns the CSR the GPU + // rebuilds the flat list from scratch and the upload path never reads the + // CPU-filled contents (headers are zero-seeded and flat records seeded to + // 0xffffffff in R_ModernClusteredLighting_UploadBuffers), so skip the + // second full binning pass. stats.maxLightsInCluster already comes from + // the counting pass and stats.uploadedReferences from the layout pass, and + // the fill pass reproduces both bit-identically when it does run. The + // predicate is stable through this frame's upload: it depends only on + // init-time GL state and probeCount, which is final before finalize. + if ( !R_ModernClusteredLighting_UseComputeBinningPath() ) { + R_ModernClusteredLighting_FillClusterIndexList( stats ); + } stats.lossless = stats.overflowReferences == 0 && stats.overflowLights == 0 && stats.unsampledSpillReferences == 0 @@ -1461,6 +2561,9 @@ static void R_ModernClusteredLighting_BuildFrame( const idScenePacketFrame &pack stats.lightCapacity = rg_clusteredLightingFrame.lightCapacity; stats.indexRecordCapacity = rg_clusteredLightingFrame.indexRecordCapacity; stats.shadowDescriptorCapacity = rg_clusteredLightingFrame.shadowDescriptorCapacity; + stats.probeCapacity = RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; + stats.probeUBOBytes = sizeof( modernSpecularProbeGpuRecord_t ) * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; + stats.probeBufferReady = rg_clusteredLightingSpecularProbesUBO != 0; const int sceneCount = packetFrame.NumScenes(); stats.sceneCount = sceneCount; R_ModernClusteredLighting_SetStatus( stats, requested ? "unavailable" : "off" ); @@ -1509,6 +2612,10 @@ static void R_ModernClusteredLighting_BuildFrame( const idScenePacketFrame &pack modernClusterRecord_t &cluster = rg_clusteredLightingFrame.clusters[rg_clusteredLightingFrame.clusterCount + i]; memset( &cluster, 0, sizeof( cluster ) ); cluster.firstLightIndex = -1; + for ( int probeIndex = 0; probeIndex < RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER; ++probeIndex ) { + cluster.probes[probeIndex].probeIndex = -1; + cluster.probes[probeIndex].stableId = -1; + } } rg_clusteredLightingFrame.gridCount++; rg_clusteredLightingFrame.clusterCount += grid.clusterCount; @@ -1526,6 +2633,8 @@ static void R_ModernClusteredLighting_BuildFrame( const idScenePacketFrame &pack for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[gridIndex]; + grid.firstLight = rg_clusteredLightingFrame.lightCount; + grid.firstProbe = rg_clusteredLightingFrame.probeCount; if ( grid.sceneIndex < 0 || grid.sceneIndex >= packetFrame.NumScenes() ) { continue; } @@ -1533,13 +2642,26 @@ static void R_ModernClusteredLighting_BuildFrame( const idScenePacketFrame &pack if ( scene.viewDef == NULL ) { continue; } + const bool probesRequested = r_rendererModernQuality.GetBool() + && r_rendererReflectionProbes.GetBool(); for ( const viewLight_t *vLight = scene.viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { + if ( R_ModernClusteredLighting_IsSpecularProbe( vLight ) ) { + if ( probesRequested ) { + R_ModernClusteredLighting_AppendSpecularProbe( grid, vLight, stats ); + } + continue; + } R_ModernClusteredLighting_AddLight( grid, vLight, stats ); } if ( grid.lightCount > 0 ) { stats.scenesWithLights++; } } + stats.probeCount = rg_clusteredLightingFrame.probeCount; + stats.probeCpuCSRForced = rg_clusteredLightingFrame.probeCount > 0 + && R_ModernClusteredLighting_ComputeBinningCapable(); + stats.computeBinningReady = R_ModernClusteredLighting_UseComputeBinningPath(); + R_ModernClusteredLighting_BinSpecularProbes( stats ); R_ModernClusteredLighting_BuildShadowDescriptors( packetFrame, stats ); R_ModernClusteredLighting_BinFrameReferences( stats, false ); stats.buildMsec = Sys_Milliseconds() - startMsec; @@ -1898,6 +3020,14 @@ static void R_ModernClusteredLighting_DispatchComputeBinning( rendererClusteredL } } +static void R_ModernClusteredLighting_FillProbeHeader( + modernClusterIndexGpuRecord_t &header, const modernClusterRecord_t &cluster ) { + header.indices[2] = cluster.probes[0].probeIndex >= 0 + ? static_cast( cluster.probes[0].probeIndex ) : 0xffffffffu; + header.indices[3] = cluster.probes[1].probeIndex >= 0 + ? static_cast( cluster.probes[1].probeIndex ) : 0xffffffffu; +} + static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingStats_t &stats ) { rg_clusteredLightingBoundGridIndex = -1; stats.paramsUBOBytes = sizeof( modernClusterGridGpuParams_t ); @@ -1908,15 +3038,21 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt stats.lightsUBOBytes = sizeof( modernClusterLightGpuRecord_t ) * lightCapacity; stats.indicesUBOBytes = sizeof( modernClusterIndexGpuRecord_t ) * indexRecordCapacity; stats.shadowDescriptorBytes = sizeof( modernClusterShadowDescriptorGpuRecord_t ) * shadowDescriptorCapacity; + stats.probeUBOBytes = sizeof( modernSpecularProbeGpuRecord_t ) * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; stats.shaderStorageReady = useShaderStorage; stats.computeBinningReady = R_ModernClusteredLighting_UseComputeBinningPath(); stats.shadowDescriptorBufferReady = false; + stats.probeBufferReady = false; stats.shadowDescriptorCapacity = shadowDescriptorCapacity; + stats.probeCapacity = RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; stats.shadowDescriptorCount = rg_clusteredLightingFrame.shadowDescriptorCount; + stats.probeCount = rg_clusteredLightingFrame.probeCount; stats.clusterRecordCount = rg_clusteredLightingFrame.clusterRecordCount; stats.flatIndexRecordCount = rg_clusteredLightingFrame.flatIndexRecordCount; stats.flatIndexReferenceCapacity = rg_clusteredLightingFrame.flatIndexReferenceCapacity; - if ( !stats.requested || !stats.frameValid || rg_clusteredLightingParamsUBO == 0 || glBufferSubData == NULL || glBindBufferBase == NULL ) { + if ( !stats.requested || !stats.frameValid || rg_clusteredLightingParamsUBO == 0 + || rg_clusteredLightingSpecularProbesUBO == 0 + || glBufferSubData == NULL || glBindBufferBase == NULL ) { return false; } if ( useShaderStorage ) { @@ -1928,9 +3064,18 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt } const int uploadedLights = Min( rg_clusteredLightingFrame.lightCount, lightCapacity ); + const int uploadedProbes = Min( rg_clusteredLightingFrame.probeCount, RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS ); const int uploadedShadowDescriptors = Min( rg_clusteredLightingFrame.shadowDescriptorCount, shadowDescriptorCapacity ); stats.uploadedLights = uploadedLights; + stats.uploadedProbes = uploadedProbes; stats.uploadedShadowDescriptors = uploadedShadowDescriptors; + stats.probeFrameGeneration = R_ModernClusteredLighting_PublishedProbeFrameGeneration( + rg_clusteredLightingFrame.probeSetComplete, uploadedProbes, + rg_clusteredLightingFrame.probeCount, R_ModernClusteredLighting_ProbeAtlasReady(), + R_ModernClusteredLighting_ProbeAtlasFrameReady(), + R_ModernClusteredLighting_ProbeAtlasFrameGeneration() ); + stats.probeFrameReady = stats.probeFrameGeneration != 0u; + rg_clusteredLightingFrame.probeFrameGeneration = stats.probeFrameGeneration; stats.uploadedClusters = Min( rg_clusteredLightingFrame.clusterCount, MODERN_CLUSTER_MAX_CLUSTERS ); stats.uploadedGridIndexRecords = Min( rg_clusteredLightingFrame.indexRecordCount, indexRecordCapacity ); modernClusterGridGpuParams_t params; @@ -1941,7 +3086,13 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt } idList &lightRecords = rg_clusteredLightingFrame.lightGpuRecords; - const int lightUploadRecords = useShaderStorage ? Max( uploadedLights, 1 ) : lightCapacity; + // Prefix-sized fills and uploads on both paths: the full-capacity UBO/SSBO + // storage allocated at init keeps its binding ranges, glBufferSubData just + // writes this frame's prefix. Records beyond the counts are unreachable by + // contract (headers carry first/count, flat light indices are filtered + // against uploadedLights at fill time, and the GLSL fetch helpers clamp to + // capacity) - the SSBO path has always worked this way over a stale tail. + const int lightUploadRecords = Max( uploadedLights, 1 ); lightRecords.SetNum( lightUploadRecords, false ); memset( lightRecords.Ptr(), 0, sizeof( modernClusterLightGpuRecord_t ) * lightUploadRecords ); for ( int i = 0; i < uploadedLights; ++i ) { @@ -1982,23 +3133,58 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt memcpy( dst.projectionRect, src.descriptor.projectionAtlasRect, sizeof( dst.projectionRect ) ); } + idList &probeRecords = rg_clusteredLightingFrame.probeGpuRecords; + probeRecords.SetNum( RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS, false ); + memset( probeRecords.Ptr(), 0, + sizeof( modernSpecularProbeGpuRecord_t ) * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS ); + for ( int i = 0; i < uploadedProbes; ++i ) { + const modernSpecularProbeRecord_t &src = rg_clusteredLightingFrame.probes[i]; + modernSpecularProbeGpuRecord_t &dst = probeRecords[i]; + dst.positionRadius[0] = src.cameraOrigin.x; + dst.positionRadius[1] = src.cameraOrigin.y; + dst.positionRadius[2] = src.cameraOrigin.z; + dst.positionRadius[3] = src.radius; + dst.tintIntensity[0] = src.tint.x; + dst.tintIntensity[1] = src.tint.y; + dst.tintIntensity[2] = src.tint.z; + dst.tintIntensity[3] = src.intensity; + dst.axisXPriority[0] = src.axisX.x; + dst.axisXPriority[1] = src.axisX.y; + dst.axisXPriority[2] = src.axisX.z; + dst.axisXPriority[3] = static_cast( src.priority ); + dst.axisYBlend[0] = src.axisY.x; + dst.axisYBlend[1] = src.axisY.y; + dst.axisYBlend[2] = src.axisY.z; + dst.axisYBlend[3] = src.blendFraction; + dst.axisZSlot[0] = src.axisZ.x; + dst.axisZSlot[1] = src.axisZ.y; + dst.axisZSlot[2] = src.axisZ.z; + dst.axisZSlot[3] = static_cast( src.placement.slot ); + dst.identity[0] = static_cast( src.stableLightIdentity ); + dst.identity[1] = static_cast( R_ModernClusteredLighting_ProbeGenerationExact( src.placement.sourceStorageGeneration ) ); + dst.identity[2] = static_cast( R_ModernClusteredLighting_ProbeGenerationExact( src.placement.residencyGeneration ) ); + dst.identity[3] = static_cast( stats.probeFrameGeneration ); + } + idList &shadowRecords = rg_clusteredLightingFrame.shadowGpuRecords; - const int shadowUploadRecords = useShaderStorage ? Max( uploadedShadowDescriptors, 1 ) : shadowDescriptorCapacity; + const int shadowUploadRecords = Max( uploadedShadowDescriptors, 1 ); shadowRecords.SetNum( shadowUploadRecords, false ); - memset( shadowRecords.Ptr(), 0, sizeof( modernClusterShadowDescriptorGpuRecord_t ) * shadowUploadRecords ); + // FillShadowDescriptorGpuRecord memsets each record it fills, so only clear + // the padding tail (present only when no shadow descriptors were uploaded). + if ( shadowUploadRecords > uploadedShadowDescriptors ) { + memset( shadowRecords.Ptr() + uploadedShadowDescriptors, 0, + sizeof( modernClusterShadowDescriptorGpuRecord_t ) * ( shadowUploadRecords - uploadedShadowDescriptors ) ); + } for ( int i = 0; i < uploadedShadowDescriptors; ++i ) { R_ModernClusteredLighting_FillShadowDescriptorGpuRecord( shadowRecords[i], rg_clusteredLightingFrame.shadowDescriptors[i] ); } idList &indexRecords = rg_clusteredLightingFrame.indexGpuRecords; - const int indexUploadRecords = useShaderStorage ? Max( stats.uploadedGridIndexRecords, 1 ) : indexRecordCapacity; + const int indexUploadRecords = Max( stats.uploadedGridIndexRecords, 1 ); indexRecords.SetNum( indexUploadRecords, false ); - for ( int i = 0; i < indexUploadRecords; ++i ) { - indexRecords[i].indices[0] = 0xffffffffu; - indexRecords[i].indices[1] = 0xffffffffu; - indexRecords[i].indices[2] = 0xffffffffu; - indexRecords[i].indices[3] = 0xffffffffu; - } + // The record is exactly four GLuint lanes; 0xff in every byte is the + // all-lanes-invalid (0xffffffff) sentinel, so one memset seeds them all. + memset( indexRecords.Ptr(), 0xff, sizeof( modernClusterIndexGpuRecord_t ) * indexUploadRecords ); stats.uploadedReferences = 0; const bool seedComputeBinning = R_ModernClusteredLighting_UseComputeBinningPath(); for ( int gridIndex = 0; gridIndex < rg_clusteredLightingFrame.gridCount; ++gridIndex ) { @@ -2021,8 +3207,7 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt } else { indexRecords[headerRecord].indices[0] = static_cast( Max( 0, cluster.firstLightIndex ) ); indexRecords[headerRecord].indices[1] = static_cast( Max( 0, cluster.uploadedLightCount ) ); - indexRecords[headerRecord].indices[2] = cluster.overflow ? 1u : 0u; - indexRecords[headerRecord].indices[3] = 0u; + R_ModernClusteredLighting_FillProbeHeader( indexRecords[headerRecord], cluster ); } } } @@ -2048,6 +3233,9 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt && stats.uploadedGridIndexRecords == rg_clusteredLightingFrame.indexRecordCount; R_ModernClusteredLighting_UpdateBuffer( GL_UNIFORM_BUFFER, rg_clusteredLightingParamsUBO, 0, sizeof( params ), ¶ms ); + R_ModernClusteredLighting_UpdateBuffer( GL_UNIFORM_BUFFER, rg_clusteredLightingSpecularProbesUBO, 0, + sizeof( modernSpecularProbeGpuRecord_t ) * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS, + probeRecords.Ptr() ); rg_clusteredLightingBoundGridIndex = rg_clusteredLightingFrame.gridCount > 0 ? 0 : -1; if ( useShaderStorage ) { R_ModernClusteredLighting_UpdateBuffer( GL_SHADER_STORAGE_BUFFER, rg_clusteredLightingLightsSSBO, 0, sizeof( modernClusterLightGpuRecord_t ) * lightUploadRecords, lightRecords.Ptr() ); @@ -2065,9 +3253,10 @@ static bool R_ModernClusteredLighting_UploadBuffers( rendererClusteredLightingSt R_ModernClusteredLighting_SetStatus( stats, stats.lossless ? "uploaded-csr" : "uploaded-csr-lossy" ); } - stats.bufferUploads += 4; + stats.bufferUploads += 5; stats.buffersReady = true; stats.shadowDescriptorBufferReady = true; + stats.probeBufferReady = true; stats.uboFallbackReady = !useShaderStorage; return true; } @@ -2170,6 +3359,8 @@ static void R_ModernClusteredLighting_BuildGridParams( const modernClusterGridRe params.projectionDepth[0] = -1.0f; params.projectionDepth[1] = -2.0f * Max( 0.25f, grid.nearZ ); } + params.projectionDepth[2] = static_cast( stats.uploadedProbes ); + params.projectionDepth[3] = static_cast( stats.probeFrameGeneration ); } void R_ModernClusteredLighting_Init( const renderBackendCaps_t &caps, const renderFeatureSet_t &features ) { @@ -2187,10 +3378,12 @@ void R_ModernClusteredLighting_Init( const renderBackendCaps_t &caps, const rend const int lightBytes = sizeof( modernClusterLightGpuRecord_t ) * MODERN_CLUSTER_MAX_LIGHTS_UBO; const int indexBytes = sizeof( modernClusterIndexGpuRecord_t ) * MODERN_CLUSTER_MAX_INDEX_RECORDS_UBO; const int shadowDescriptorBytes = sizeof( modernClusterShadowDescriptorGpuRecord_t ) * MODERN_CLUSTER_MAX_SHADOW_DESCRIPTORS_UBO; + const int probeBytes = sizeof( modernSpecularProbeGpuRecord_t ) * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; if ( !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, paramsBytes, rg_clusteredLightingParamsUBO, "Modern clustered lighting grid params UBO" ) || !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, lightBytes, rg_clusteredLightingLightsUBO, "Modern clustered lighting light records UBO" ) || !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, indexBytes, rg_clusteredLightingIndicesUBO, "Modern clustered lighting indices UBO" ) || - !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, shadowDescriptorBytes, rg_clusteredLightingShadowDescriptorsUBO, "Modern clustered lighting shadow descriptors UBO" ) ) { + !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, shadowDescriptorBytes, rg_clusteredLightingShadowDescriptorsUBO, "Modern clustered lighting shadow descriptors UBO" ) || + !R_ModernClusteredLighting_CreateBuffer( GL_UNIFORM_BUFFER, probeBytes, rg_clusteredLightingSpecularProbesUBO, "Modern clustered lighting specular probe records UBO" ) ) { R_ModernClusteredLighting_Shutdown(); R_ModernClusteredLighting_SetStatus( rg_clusteredLightingStats, "buffer-init-failed" ); return; @@ -2227,22 +3420,25 @@ void R_ModernClusteredLighting_Init( const renderBackendCaps_t &caps, const rend rg_clusteredLightingStats.initialized = true; rg_clusteredLightingStats.buffersReady = true; rg_clusteredLightingStats.shadowDescriptorBufferReady = true; + rg_clusteredLightingStats.probeBufferReady = true; rg_clusteredLightingStats.shaderStorageReady = R_ModernClusteredLighting_UseShaderStoragePath(); rg_clusteredLightingStats.computeBinningReady = R_ModernClusteredLighting_UseComputeBinningPath(); rg_clusteredLightingStats.uboFallbackReady = !rg_clusteredLightingStats.shaderStorageReady; rg_clusteredLightingStats.lightCapacity = R_ModernClusteredLighting_LightCapacity(); rg_clusteredLightingStats.indexRecordCapacity = R_ModernClusteredLighting_IndexRecordCapacity(); rg_clusteredLightingStats.shadowDescriptorCapacity = R_ModernClusteredLighting_ShadowDescriptorCapacity(); + rg_clusteredLightingStats.probeCapacity = RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; rg_clusteredLightingStats.debugOverlayReady = rg_clusteredLightingDebugProgram != 0 && rg_clusteredLightingDebugVAO != 0; rg_clusteredLightingStats.paramsUBOBytes = paramsBytes; rg_clusteredLightingStats.lightsUBOBytes = sizeof( modernClusterLightGpuRecord_t ) * rg_clusteredLightingStats.lightCapacity; rg_clusteredLightingStats.indicesUBOBytes = sizeof( modernClusterIndexGpuRecord_t ) * rg_clusteredLightingStats.indexRecordCapacity; rg_clusteredLightingStats.shadowDescriptorBytes = sizeof( modernClusterShadowDescriptorGpuRecord_t ) * rg_clusteredLightingStats.shadowDescriptorCapacity; + rg_clusteredLightingStats.probeUBOBytes = probeBytes; R_ModernClusteredLighting_SetStatus( rg_clusteredLightingStats, "initialized" ); } void R_ModernClusteredLighting_Shutdown( void ) { - GLuint buffers[7]; + GLuint buffers[8]; int numBuffers = 0; if ( rg_clusteredLightingParamsUBO != 0 ) { buffers[numBuffers++] = rg_clusteredLightingParamsUBO; @@ -2256,6 +3452,9 @@ void R_ModernClusteredLighting_Shutdown( void ) { if ( rg_clusteredLightingShadowDescriptorsUBO != 0 ) { buffers[numBuffers++] = rg_clusteredLightingShadowDescriptorsUBO; } + if ( rg_clusteredLightingSpecularProbesUBO != 0 ) { + buffers[numBuffers++] = rg_clusteredLightingSpecularProbesUBO; + } if ( rg_clusteredLightingLightsSSBO != 0 ) { buffers[numBuffers++] = rg_clusteredLightingLightsSSBO; } @@ -2284,6 +3483,7 @@ void R_ModernClusteredLighting_Shutdown( void ) { rg_clusteredLightingLightsUBO = 0; rg_clusteredLightingIndicesUBO = 0; rg_clusteredLightingShadowDescriptorsUBO = 0; + rg_clusteredLightingSpecularProbesUBO = 0; rg_clusteredLightingLightsSSBO = 0; rg_clusteredLightingIndicesSSBO = 0; rg_clusteredLightingShadowDescriptorsSSBO = 0; @@ -2492,6 +3692,40 @@ void R_ModernClusteredLighting_PrintGfxInfo( void ) { rg_clusteredLightingStats.debugStringTruncationSource, rg_clusteredLightingStats.buildMsec, rg_clusteredLightingStats.bufferUploads ); + common->Printf( + "Modern clustered specular probes: records=%d/%d uploaded=%d refs=%d buffer=%d bytes=%d frameReady=%d generation=%u cpuCSRForced=%d rejected(material=%d volume=%d atlas=%d overflow=%d)\n", + rg_clusteredLightingStats.probeCount, + rg_clusteredLightingStats.probeCapacity, + rg_clusteredLightingStats.uploadedProbes, + rg_clusteredLightingStats.probeReferences, + rg_clusteredLightingStats.probeBufferReady ? 1 : 0, + rg_clusteredLightingStats.probeUBOBytes, + rg_clusteredLightingStats.probeFrameReady ? 1 : 0, + rg_clusteredLightingStats.probeFrameGeneration, + rg_clusteredLightingStats.probeCpuCSRForced ? 1 : 0, + rg_clusteredLightingStats.probeRejectedMaterial, + rg_clusteredLightingStats.probeRejectedVolume, + rg_clusteredLightingStats.probeRejectedAtlas, + rg_clusteredLightingStats.probeOverflow ); + common->Printf( + "Modern clustered decals: status=%s prepared=%d sealed=%d owned=%d generation=%u records=%d/%d refs=%d/%d views=%d/%d rejects=%d/%d last=%s source=%d view=%d hash=%llu\n", + rg_clusteredDecalStats.status, + rg_clusteredDecalStats.prepared ? 1 : 0, + rg_clusteredDecalStats.sealed ? 1 : 0, + rg_clusteredDecalStats.ownershipReady ? 1 : 0, + rg_clusteredDecalStats.generation, + rg_clusteredDecalStats.publishedRecords, + rg_clusteredDecalStats.recordCapacity, + rg_clusteredDecalStats.clusterReferences, + rg_clusteredDecalStats.referenceCapacity, + rg_clusteredDecalStats.ownedViews, + rg_clusteredDecalStats.viewCount, + rg_clusteredDecalStats.prepareRejects, + rg_clusteredDecalStats.sealRejects, + R_ModernClusteredLighting_DecalRejectName( rg_clusteredDecalStats.lastReject ), + rg_clusteredDecalStats.lastRejectSource, + rg_clusteredDecalStats.lastRejectView, + rg_clusteredDecalStats.sourceOrderHash ); } const rendererClusteredLightingStats_t &R_ModernClusteredLighting_Stats( void ) { @@ -2522,13 +3756,16 @@ bool R_ModernClusteredLighting_FrameLossless( void ) { && rg_clusteredLightingStats.overflowLights == 0 && rg_clusteredLightingStats.overflowReferences == 0 && rg_clusteredLightingStats.unsampledSpillReferences == 0 + && rg_clusteredLightingStats.shadowAtlasSlotBlockedLights == 0 && rg_clusteredLightingStats.uploadedLights == rg_clusteredLightingStats.lightCount && rg_clusteredLightingStats.uploadedReferences == rg_clusteredLightingStats.lightReferences && rg_clusteredLightingStats.uploadedShadowDescriptors == rg_clusteredLightingStats.shadowDescriptorCount; } bool R_ModernClusteredLighting_BindGridForView( const viewDef_t *viewDef ) { - if ( !rg_clusteredLightingStats.frameValid || !rg_clusteredLightingStats.buffersReady || rg_clusteredLightingParamsUBO == 0 || glBufferSubData == NULL || glBindBufferBase == NULL ) { + if ( !rg_clusteredLightingStats.frameValid || !rg_clusteredLightingStats.buffersReady + || rg_clusteredLightingParamsUBO == 0 || rg_clusteredLightingSpecularProbesUBO == 0 + || glBufferSubData == NULL || glBindBufferBase == NULL ) { rg_clusteredLightingStats.gridBindFailures++; return false; } @@ -2544,9 +3781,11 @@ bool R_ModernClusteredLighting_BindGridForView( const viewDef_t *viewDef ) { R_ModernClusteredLighting_UpdateBuffer( GL_UNIFORM_BUFFER, rg_clusteredLightingParamsUBO, 0, sizeof( params ), ¶ms ); rg_clusteredLightingBoundGridIndex = gridIndex; rg_clusteredLightingStats.gridSwitches++; + // grid switches are the only stats mutation on this path; snapshotting the + // ~460-byte struct per accepted forward+ draw was pure memcpy overhead + R_RendererMetrics_RecordClusteredLighting( rg_clusteredLightingStats ); } R_ModernClusteredLighting_BindGpuBuffers( R_ModernClusteredLighting_UseShaderStoragePath() ); - R_RendererMetrics_RecordClusteredLighting( rg_clusteredLightingStats ); return true; } @@ -2576,6 +3815,11 @@ int R_ModernClusteredLighting_ShadowDescriptorUboBlockBytes( void ) { return static_cast( sizeof( modernClusterShadowDescriptorGpuRecord_t ) ) * MODERN_CLUSTER_MAX_SHADOW_DESCRIPTORS_UBO; } +int R_ModernClusteredLighting_ProbeUboBlockBytes( void ) { + return static_cast( sizeof( modernSpecularProbeGpuRecord_t ) ) + * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS; +} + static void R_ModernClusteredLighting_SetupSelfTestView( viewDef_t &view, viewLight_t *lights, idRenderLightLocal *lightDefs, int lightCount ) { memset( &view, 0, sizeof( view ) ); view.renderView.width = 1280; @@ -2617,7 +3861,317 @@ static void R_ModernClusteredLighting_SetupSelfTestView( viewDef_t &view, viewLi view.viewLights = lightCount > 0 ? &lights[0] : NULL; } +static void R_ModernClusteredLighting_SetupSelfTestDecal( rendererClusteredDecalSource_t &source, + const viewDef_t *viewDef, const void *surface, int commandIndex, int commandOrder, + unsigned int generation ) { + memset( &source, 0, sizeof( source ) ); + source.viewDef = viewDef; + source.sourceSurface = surface; + source.commandIndex = commandIndex; + source.commandOrder = commandOrder; + source.materialStableId = static_cast( 100 + commandIndex ); + source.geometryStableId = static_cast( 200 + commandIndex ); + source.instanceStableId = static_cast( 300 + commandIndex ); + source.screenX1 = 48 + commandIndex * 8; + source.screenY1 = 64 + commandIndex * 4; + source.screenX2 = source.screenX1 + 95; + source.screenY2 = source.screenY1 + 63; + source.depthMin = 24.0f + static_cast( commandIndex * 8 ); + source.depthMax = source.depthMin + 128.0f; + source.generation = generation; +} + +static bool R_ModernClusteredLighting_DecalOwnershipIsZero( const viewDef_t *viewDef, + const void *surface, int commandIndex ) { + const rendererClusteredDecalStats_t &stats = R_ModernClusteredLighting_DecalStats(); + return !stats.ownershipReady + && stats.publishedRecords == 0 + && stats.ownedViews == 0 + && !R_ModernClusteredLighting_DecalOwnsSurface( viewDef, surface ) + && !R_ModernClusteredLighting_DecalOwnsCommand( viewDef, commandIndex ); +} + +static bool R_ModernClusteredLighting_RunDecalSelfTest( viewDef_t &view ) { + if ( rg_clusteredLightingFrame.gridCount != 1 || rg_clusteredLightingFrame.clusterCount <= 0 ) { + common->Printf( "RendererClusterGrid self-test failed: decal grid unavailable\n" ); + return false; + } + + struct decalGateRestore_t { + bool oldMaster; + bool oldLeaf; + bool oldSharedAmbient; + decalGateRestore_t() + : oldMaster( r_rendererModernQuality.GetBool() ), + oldLeaf( r_rendererClusteredDecals.GetBool() ), + oldSharedAmbient( r_rendererSharedWorldAmbient.GetBool() ) { + r_rendererModernQuality.SetBool( true ); + r_rendererClusteredDecals.SetBool( true ); + r_rendererSharedWorldAmbient.SetBool( false ); + } + ~decalGateRestore_t() { + r_rendererModernQuality.SetBool( oldMaster ); + r_rendererClusteredDecals.SetBool( oldLeaf ); + r_rendererSharedWorldAmbient.SetBool( oldSharedAmbient ); + } + } restoreDecalGates; + + const unsigned int generation = R_ModernClusteredLighting_CurrentDecalGeneration(); + int surfaceIdentities[4] = { 0, 1, 2, 3 }; + rendererClusteredDecalSource_t sources[3]; + for ( int sourceIndex = 0; sourceIndex < 3; ++sourceIndex ) { + R_ModernClusteredLighting_SetupSelfTestDecal( sources[sourceIndex], &view, + &surfaceIdentities[sourceIndex], sourceIndex, 10 + sourceIndex * 10, generation ); + } + + if ( !R_ModernClusteredLighting_PrepareDecals( sources, 3, generation ) ) { + const rendererClusteredDecalStats_t &failed = R_ModernClusteredLighting_DecalStats(); + common->Printf( "RendererClusterGrid self-test failed: decal prepare rejected (%s source=%d view=%d)\n", + R_ModernClusteredLighting_DecalRejectName( failed.lastReject ), + failed.lastRejectSource, failed.lastRejectView ); + return false; + } + const rendererClusteredDecalStats_t prepared = R_ModernClusteredLighting_DecalStats(); + rendererClusteredDecalViewStats_t preparedView; + if ( !prepared.prepared || prepared.sealed || prepared.ownershipReady || !prepared.csrReady + || prepared.stagedRecords != 3 || prepared.publishedRecords != 0 + || prepared.viewCount != 1 || prepared.clusterReferences <= 0 + || prepared.clusterReferences > RENDERER_CLUSTER_DECAL_MAX_REFERENCES + || prepared.sourceOrderHash == 0 + || !R_ModernClusteredLighting_DecalViewStats( &view, preparedView ) + || !preparedView.prepared || preparedView.owned || preparedView.recordCount != 3 + || preparedView.firstCommandOrder != 10 || preparedView.lastCommandOrder != 30 + || preparedView.clusterReferences != prepared.clusterReferences + || preparedView.sourceOrderHash == 0 + || R_ModernClusteredLighting_DecalOwnsSurface( &view, &surfaceIdentities[0] ) + || R_ModernClusteredLighting_DecalOwnsCommand( &view, sources[0].commandIndex ) ) { + common->Printf( + "RendererClusterGrid self-test failed: decal prepared contract invalid (prepared=%d sealed=%d owned=%d csr=%d staged=%d published=%d views=%d refs=%d hash=%llu viewPrepared=%d viewOwned=%d viewCount=%d order=%d..%d viewRefs=%d viewHash=%llu)\n", + prepared.prepared ? 1 : 0, prepared.sealed ? 1 : 0, + prepared.ownershipReady ? 1 : 0, prepared.csrReady ? 1 : 0, + prepared.stagedRecords, prepared.publishedRecords, prepared.viewCount, + prepared.clusterReferences, prepared.sourceOrderHash, + preparedView.prepared ? 1 : 0, preparedView.owned ? 1 : 0, + preparedView.recordCount, preparedView.firstCommandOrder, + preparedView.lastCommandOrder, preparedView.clusterReferences, + preparedView.sourceOrderHash ); + return false; + } + for ( int clusterIndex = 0; clusterIndex < rg_clusteredDecalFrame.clusters.Num(); ++clusterIndex ) { + const modernClusterDecalClusterRecord_t &cluster = rg_clusteredDecalFrame.clusters[clusterIndex]; + int previousRecord = -1; + for ( int localReference = 0; localReference < cluster.referenceCount; ++localReference ) { + const int referenceIndex = cluster.firstReference + localReference; + if ( referenceIndex < 0 || referenceIndex >= rg_clusteredDecalFrame.clusterReferences.Num() + || rg_clusteredDecalFrame.clusterReferences[referenceIndex] <= previousRecord ) { + common->Printf( "RendererClusterGrid self-test failed: decal CSR order invalid (cluster=%d ref=%d)\n", + clusterIndex, referenceIndex ); + return false; + } + previousRecord = rg_clusteredDecalFrame.clusterReferences[referenceIndex]; + } + } + idList referenceSnapshot = rg_clusteredDecalFrame.clusterReferences; + const unsigned long long preparedHash = prepared.sourceOrderHash; + if ( !R_ModernClusteredLighting_SealDecals( sources, 3, generation ) ) { + const rendererClusteredDecalStats_t &failed = R_ModernClusteredLighting_DecalStats(); + common->Printf( "RendererClusterGrid self-test failed: decal seal rejected (%s source=%d view=%d)\n", + R_ModernClusteredLighting_DecalRejectName( failed.lastReject ), + failed.lastRejectSource, failed.lastRejectView ); + return false; + } + const rendererClusteredDecalStats_t sealed = R_ModernClusteredLighting_DecalStats(); + rendererClusteredDecalViewStats_t sealedView; + if ( !sealed.sealed || !sealed.ownershipReady || sealed.publishedRecords != 3 + || sealed.ownedViews != 1 || sealed.sourceOrderHash != preparedHash + || !R_ModernClusteredLighting_DecalViewStats( &view, sealedView ) + || !sealedView.owned || sealedView.recordCount != 3 + || !R_ModernClusteredLighting_DecalOwnsSurface( &view, &surfaceIdentities[0] ) + || !R_ModernClusteredLighting_DecalOwnsSurface( &view, &surfaceIdentities[2] ) + || !R_ModernClusteredLighting_DecalOwnsCommand( &view, sources[1].commandIndex ) + || R_ModernClusteredLighting_DecalOwnsSurface( &view, &surfaceIdentities[3] ) + || R_ModernClusteredLighting_DecalOwnsCommand( &view, 999 ) ) { + common->Printf( "RendererClusterGrid self-test failed: decal sealed membership invalid (sealed=%d ready=%d published=%d ownedViews=%d hash=%llu)\n", + sealed.sealed ? 1 : 0, sealed.ownershipReady ? 1 : 0, + sealed.publishedRecords, sealed.ownedViews, sealed.sourceOrderHash ); + return false; + } + + if ( !R_ModernClusteredLighting_PrepareDecals( sources, 3, generation ) + || R_ModernClusteredLighting_DecalStats().sourceOrderHash != preparedHash + || rg_clusteredDecalFrame.clusterReferences.Num() != referenceSnapshot.Num() ) { + common->Printf( "RendererClusterGrid self-test failed: decal deterministic rebuild rejected\n" ); + return false; + } + for ( int referenceIndex = 0; referenceIndex < referenceSnapshot.Num(); ++referenceIndex ) { + if ( rg_clusteredDecalFrame.clusterReferences[referenceIndex] != referenceSnapshot[referenceIndex] ) { + common->Printf( "RendererClusterGrid self-test failed: decal deterministic CSR mismatch at %d\n", referenceIndex ); + return false; + } + } + if ( !R_ModernClusteredLighting_SealDecals( sources, 3, generation ) ) { + common->Printf( "RendererClusterGrid self-test failed: decal deterministic reseal rejected\n" ); + return false; + } + + if ( R_ModernClusteredLighting_PrepareDecals( &sources[0], RENDERER_CLUSTER_DECAL_MAX_RECORDS + 1, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: decal record overflow did not fail closed\n" ); + return false; + } + + const modernClusterGridRecord_t &grid = rg_clusteredLightingFrame.grids[0]; + const int referenceOverflowCount = RENDERER_CLUSTER_DECAL_MAX_REFERENCES / Max( 1, grid.clusterCount ) + 1; + if ( referenceOverflowCount <= 0 || referenceOverflowCount > RENDERER_CLUSTER_DECAL_MAX_RECORDS ) { + common->Printf( "RendererClusterGrid self-test failed: decal reference overflow fixture invalid (%d records, %d clusters)\n", + referenceOverflowCount, grid.clusterCount ); + return false; + } + idList overflowSurfaceIdentities; + idList referenceOverflowSources; + overflowSurfaceIdentities.SetNum( referenceOverflowCount, false ); + referenceOverflowSources.SetNum( referenceOverflowCount, false ); + for ( int sourceIndex = 0; sourceIndex < referenceOverflowCount; ++sourceIndex ) { + overflowSurfaceIdentities[sourceIndex] = sourceIndex; + R_ModernClusteredLighting_SetupSelfTestDecal( referenceOverflowSources[sourceIndex], &view, + &overflowSurfaceIdentities[sourceIndex], sourceIndex, sourceIndex, generation ); + referenceOverflowSources[sourceIndex].screenX1 = 0; + referenceOverflowSources[sourceIndex].screenY1 = 0; + referenceOverflowSources[sourceIndex].screenX2 = grid.width - 1; + referenceOverflowSources[sourceIndex].screenY2 = grid.height - 1; + referenceOverflowSources[sourceIndex].depthMin = Max( 0.01f, grid.nearZ ); + referenceOverflowSources[sourceIndex].depthMax = grid.farZ; + } + if ( R_ModernClusteredLighting_PrepareDecals( referenceOverflowSources.Ptr(), referenceOverflowCount, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &overflowSurfaceIdentities[0], 0 ) ) { + common->Printf( "RendererClusterGrid self-test failed: decal reference overflow did not fail closed (records=%d clusters=%d reject=%s)\n", + referenceOverflowCount, grid.clusterCount, + R_ModernClusteredLighting_DecalRejectName( R_ModernClusteredLighting_DecalStats().lastReject ) ); + return false; + } + + rendererClusteredDecalSource_t staleSources[3]; + memcpy( staleSources, sources, sizeof( staleSources ) ); + staleSources[1].geometryStableId++; + if ( !R_ModernClusteredLighting_PrepareDecals( sources, 3, generation ) + || R_ModernClusteredLighting_SealDecals( staleSources, 3, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_STALE_IDENTITY + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: stale decal identity did not fail closed (reject=%s)\n", + R_ModernClusteredLighting_DecalRejectName( R_ModernClusteredLighting_DecalStats().lastReject ) ); + return false; + } + + rendererClusteredDecalSource_t malformed = sources[0]; + malformed.screenX2 = malformed.screenX1 - 1; + if ( R_ModernClusteredLighting_PrepareDecals( &malformed, 1, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_RECT + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: malformed decal rect did not fail closed\n" ); + return false; + } + malformed = sources[0]; + malformed.depthMin = malformed.depthMax + 1.0f; + if ( R_ModernClusteredLighting_PrepareDecals( &malformed, 1, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_DEPTH + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: malformed decal depth did not fail closed\n" ); + return false; + } + + if ( !R_ModernClusteredLighting_PrepareDecals( sources, 3, generation ) + || R_ModernClusteredLighting_SealDecals( sources, 3, generation + 1 ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: stale decal generation did not fail closed\n" ); + return false; + } + if ( !R_ModernClusteredLighting_PrepareDecals( sources, 3, generation ) + || R_ModernClusteredLighting_SealDecals( sources, 2, generation ) + || R_ModernClusteredLighting_DecalStats().lastReject != RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE + || !R_ModernClusteredLighting_DecalOwnershipIsZero( &view, &surfaceIdentities[0], sources[0].commandIndex ) ) { + common->Printf( "RendererClusterGrid self-test failed: incomplete decal seal retained ownership (reject=%s)\n", + R_ModernClusteredLighting_DecalRejectName( R_ModernClusteredLighting_DecalStats().lastReject ) ); + return false; + } + + R_ModernClusteredLighting_ResetDecalTransaction(); + return true; +} + +static bool R_ModernClusteredLighting_RunProbeRecordSelfTest( void ) { + if ( sizeof( modernSpecularProbeGpuRecord_t ) != 96 + || R_ModernClusteredLighting_ProbeUboBlockBytes() + != 96 * RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS ) { + common->Printf( "RendererClusterGrid self-test failed: specular probe std140 size drifted\n" ); + return false; + } + + modernClusterRecord_t cluster; + memset( &cluster, 0, sizeof( cluster ) ); + for ( int index = 0; index < RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER; ++index ) { + cluster.probes[index].probeIndex = -1; + cluster.probes[index].stableId = -1; + } + modernClusterIndexGpuRecord_t header; + memset( &header, 0, sizeof( header ) ); + R_ModernClusteredLighting_FillProbeHeader( header, cluster ); + if ( header.indices[2] != 0xffffffffu || header.indices[3] != 0xffffffffu ) { + common->Printf( "RendererClusterGrid self-test failed: empty probe header was not sentinel-filled\n" ); + return false; + } + cluster.probes[0].probeIndex = 4; + cluster.probes[1].probeIndex = 7; + R_ModernClusteredLighting_FillProbeHeader( header, cluster ); + if ( header.indices[2] != 4u || header.indices[3] != 7u ) { + common->Printf( "RendererClusterGrid self-test failed: top-two probe header order drifted\n" ); + return false; + } + + if ( R_ModernClusteredLighting_PublishedProbeFrameGeneration( + false, 2, 2, true, true, 17 ) != 0u + || R_ModernClusteredLighting_PublishedProbeFrameGeneration( + true, 1, 2, true, true, 17 ) != 0u + || R_ModernClusteredLighting_PublishedProbeFrameGeneration( + true, 2, 2, true, false, 17 ) != 0u + || R_ModernClusteredLighting_PublishedProbeFrameGeneration( + true, 2, 2, true, true, 0 ) != 0u + || R_ModernClusteredLighting_PublishedProbeFrameGeneration( + true, 2, 2, true, true, 17 ) != 17u ) { + common->Printf( "RendererClusterGrid self-test failed: probe generation gate did not fail closed\n" ); + return false; + } + + modernClusterGridRecord_t grid; + memset( &grid, 0, sizeof( grid ) ); + grid.tileCountX = 1; + grid.tileCountY = 1; + grid.sliceCountZ = 1; + grid.clusterCount = 1; + grid.width = 1; + grid.height = 1; + grid.nearZ = 1.0f; + grid.farZ = 64.0f; + grid.maxLightsPerCluster = 1; + rendererClusteredLightingStats_t stats; + memset( &stats, 0, sizeof( stats ) ); + stats.uploadedProbes = 2; + stats.probeFrameGeneration = 17; + modernClusterGridGpuParams_t params; + R_ModernClusteredLighting_BuildGridParams( grid, stats, params ); + if ( params.projectionDepth[2] != 2.0f || params.projectionDepth[3] != 17.0f ) { + common->Printf( "RendererClusterGrid self-test failed: probe count/generation grid ABI drifted\n" ); + return false; + } + return true; +} + bool RendererClusterGrid_RunSelfTest( void ) { + if ( !R_ModernClusteredLighting_RunProbeRecordSelfTest() ) { + return false; + } if ( !r_rendererModernExecutor.GetBool() && r_rendererClusterDebug.GetInteger() <= 0 ) { common->Printf( "RendererClusterGrid self-test passed (disabled)\n" ); return true; @@ -2693,13 +4247,19 @@ bool RendererClusterGrid_RunSelfTest( void ) { common->Printf( "RendererClusterGrid self-test failed: depth slices not monotonic (%d %d %d)\n", sliceA, sliceB, sliceC ); return false; } + if ( !R_ModernClusteredLighting_RunDecalSelfTest( view ) ) { + return false; + } if ( rg_clusteredLightingAvailable && rg_clusteredLightingInitialized ) { if ( !R_ModernClusteredLighting_UploadBuffers( stats ) ) { common->Printf( "RendererClusterGrid self-test failed: clustered CSR upload unavailable\n" ); return false; } - if ( !stats.buffersReady || !stats.shadowDescriptorBufferReady || !stats.lossless || !stats.csrReady || stats.uploadedLights != 6 || stats.uploadedClusters <= 0 || stats.uploadedReferences != stats.lightReferences || stats.uploadedGridIndexRecords <= 0 || stats.shadowDescriptorCapacity <= 0 || stats.shadowDescriptorBytes <= 0 || ( stats.computeBinningReady && ( !stats.computeBinningExecuted || stats.computeBinningDispatches <= 0 ) ) ) { + if ( !stats.buffersReady || !stats.shadowDescriptorBufferReady || !stats.probeBufferReady + || stats.probeUBOBytes != R_ModernClusteredLighting_ProbeUboBlockBytes() + || stats.uploadedProbes != 0 || stats.probeFrameGeneration != 0 + || !stats.lossless || !stats.csrReady || stats.uploadedLights != 6 || stats.uploadedClusters <= 0 || stats.uploadedReferences != stats.lightReferences || stats.uploadedGridIndexRecords <= 0 || stats.shadowDescriptorCapacity <= 0 || stats.shadowDescriptorBytes <= 0 || ( stats.computeBinningReady && ( !stats.computeBinningExecuted || stats.computeBinningDispatches <= 0 ) ) ) { common->Printf( "RendererClusterGrid self-test failed: upload stats invalid (buffers=%d shadowBuffer=%d lossless=%d csr=%d lights=%d clusters=%d refs=%d/%d indexRecords=%d shadowDesc=%d/%d bytes=%d compute=%d/%d dispatch=%d)\n", stats.buffersReady ? 1 : 0, stats.shadowDescriptorBufferReady ? 1 : 0, stats.lossless ? 1 : 0, stats.csrReady ? 1 : 0, stats.uploadedLights, stats.uploadedClusters, stats.uploadedReferences, stats.lightReferences, stats.uploadedGridIndexRecords, stats.uploadedShadowDescriptors, stats.shadowDescriptorCapacity, stats.shadowDescriptorBytes, stats.computeBinningReady ? 1 : 0, stats.computeBinningExecuted ? 1 : 0, stats.computeBinningDispatches ); return false; } diff --git a/src/renderer/ModernClusteredLighting.h b/src/renderer/ModernClusteredLighting.h index 0c5eb7b5..22228c43 100644 --- a/src/renderer/ModernClusteredLighting.h +++ b/src/renderer/ModernClusteredLighting.h @@ -27,6 +27,85 @@ enum rendererModernLightType_t { static const int RENDERER_MODERN_SHADOW_DESCRIPTOR_MAX_TILES = 6; static const int RENDERER_MODERN_SHADOW_DESCRIPTOR_MAX_CASCADES = 4; +static const int RENDERER_CLUSTER_SPECULAR_PROBE_MAX_RECORDS = 32; +static const int RENDERER_CLUSTER_SPECULAR_PROBES_PER_CLUSTER = 2; +static const int RENDERER_CLUSTER_DECAL_MAX_RECORDS = 1024; +static const int RENDERER_CLUSTER_DECAL_MAX_REFERENCES = 65536; +static const unsigned int RENDERER_CLUSTER_DECAL_INVALID_STABLE_ID = 0xffffffffu; + +// CPU-only source contract for the isolated clustered-decal transaction. The +// screen rectangle is inclusive and view-local. Stable IDs must describe the +// immutable material/geometry/instance resources behind sourceSurface; the +// pointer itself is used only as exact within-frame membership identity. +typedef struct rendererClusteredDecalSource_s { + const viewDef_t * viewDef; + const void * sourceSurface; + int commandIndex; + int commandOrder; + unsigned int materialStableId; + unsigned int geometryStableId; + unsigned int instanceStableId; + int screenX1; + int screenY1; + int screenX2; + int screenY2; + float depthMin; + float depthMax; + unsigned int generation; +} rendererClusteredDecalSource_t; + +enum rendererClusteredDecalReject_t { + RENDERER_CLUSTER_DECAL_REJECT_NONE = 0, + RENDERER_CLUSTER_DECAL_REJECT_NOT_PREPARED, + RENDERER_CLUSTER_DECAL_REJECT_INVALID_ARGUMENT, + RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, + RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY, + RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY, + RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION, + RENDERER_CLUSTER_DECAL_REJECT_STALE_IDENTITY, + RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_RECT, + RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_DEPTH, + RENDERER_CLUSTER_DECAL_REJECT_MALFORMED_ORDER, + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + RENDERER_CLUSTER_DECAL_REJECT_ORDER_MISMATCH, + RENDERER_CLUSTER_DECAL_REJECT_HASH_MISMATCH, + RENDERER_CLUSTER_DECAL_REJECT_INTERNAL +}; + +typedef struct rendererClusteredDecalStats_s { + bool prepared; + bool sealed; + bool ownershipReady; + bool csrReady; + unsigned int generation; + int submittedRecords; + int stagedRecords; + int publishedRecords; + int viewCount; + int ownedViews; + int clusterCount; + int clusterReferences; + int recordCapacity; + int referenceCapacity; + int prepareRejects; + int sealRejects; + rendererClusteredDecalReject_t lastReject; + int lastRejectSource; + int lastRejectView; + unsigned long long sourceOrderHash; + char status[96]; +} rendererClusteredDecalStats_t; + +typedef struct rendererClusteredDecalViewStats_s { + const viewDef_t * viewDef; + bool prepared; + bool owned; + int recordCount; + int firstCommandOrder; + int lastCommandOrder; + int clusterReferences; + unsigned long long sourceOrderHash; +} rendererClusteredDecalViewStats_t; enum rendererModernShadowDescriptorFlag_t { RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_MAPPED = 1 << 0, @@ -46,9 +125,9 @@ enum rendererModernShadowDescriptorFlag_t { RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_PROJECTED_STATE_READY = 1 << 14, RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_PROJECTED_FALLBACK = 1 << 15, RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_RECEIVER_PLANE_BIAS = 1 << 16, - // projectedAtlasRect references a live cell of ARB2's persistent shadow - // atlas (signature-current, static content complete) - the only state in - // which the modern receiver may sample a projected shadow + // The descriptor references a signature-current, static-complete physical + // ARB2 resource: a persistent-atlas cell for projected lights or the exact + // singular cache cube currently bound for a point light. RENDERER_MODERN_SHADOW_DESCRIPTOR_FLAG_ATLAS_SLOT = 1 << 17 }; @@ -76,7 +155,7 @@ typedef struct rendererModernShadowDescriptor_s { int projectedSkippedSampleCount; int flags; // freshness (I7): frame the planner built this descriptor, and the frame - // the referenced atlas cell's static content was last rendered + // the referenced atlas cell or point cube was last rendered int updateFrame; int atlasContentFrame; int atlasCellX; @@ -151,6 +230,9 @@ typedef struct rendererClusteredLightingStats_s { bool csrReady; bool computeBinningReady; bool computeBinningExecuted; + bool probeBufferReady; + bool probeFrameReady; + bool probeCpuCSRForced; bool lossless; bool overflow; int gridCount; @@ -171,6 +253,15 @@ typedef struct rendererClusteredLightingStats_s { int shadowDescriptorCapacity; int shadowReceiverBlockedLights; int shadowAtlasSlotBlockedLights; + int probeCount; + int uploadedProbes; + int probeCapacity; + int probeReferences; + int probeOverflow; + int probeRejectedMaterial; + int probeRejectedVolume; + int probeRejectedAtlas; + unsigned int probeFrameGeneration; int culledLights; int clippedLights; int overflowLights; @@ -210,6 +301,7 @@ typedef struct rendererClusteredLightingStats_s { int lightsUBOBytes; int indicesUBOBytes; int shadowDescriptorBytes; + int probeUBOBytes; int debugMode; int debugOverlayDraws; int debugStringTruncations; @@ -220,6 +312,7 @@ typedef struct rendererClusteredLightingStats_s { void R_ModernClusteredLighting_Init( const renderBackendCaps_t &caps, const renderFeatureSet_t &features ); void R_ModernClusteredLighting_Shutdown( void ); void R_ModernClusteredLighting_PrepareFrame( const idScenePacketFrame &packetFrame, bool requested ); +void R_ModernClusteredLighting_ResetDecalsForFrame( void ); void R_ModernClusteredLighting_DrawDebugOverlay( void ); void R_ModernClusteredLighting_PrintGfxInfo( void ); const rendererClusteredLightingStats_t &R_ModernClusteredLighting_Stats( void ); @@ -232,6 +325,27 @@ const rendererModernShadowDescriptor_t *R_ModernClusteredLighting_ShadowDescript // linked-size expectation for the shadow-descriptor UBO block, for std140 // layout-drift introspection against the driver (M5) int R_ModernClusteredLighting_ShadowDescriptorUboBlockBytes( void ); +// linked-size expectation for the fixed std140 authored-probe block +int R_ModernClusteredLighting_ProbeUboBlockBytes( void ); + +// Two-phase, whole-frame CPU transaction. Both calls must occur after the +// ordinary cluster grids have been prepared, and SealDecals must receive the +// same authoritative list. No membership query succeeds before an exact seal. +bool R_ModernClusteredLighting_PrepareDecals( const rendererClusteredDecalSource_t *sources, int sourceCount, unsigned int generation ); +bool R_ModernClusteredLighting_SealDecals( const rendererClusteredDecalSource_t *sources, int sourceCount, unsigned int generation ); +// Explicitly aborts the current frame transaction before classic ownership is +// queried. This is used when the authoritative executor preflight discovers a +// resource or identity that cannot be represented by the sealed source list. +bool R_ModernClusteredLighting_RejectDecalsForFrame( rendererClusteredDecalReject_t reason, int sourceIndex, int submittedRecords, unsigned int generation ); +const rendererClusteredDecalStats_t &R_ModernClusteredLighting_DecalStats( void ); +bool R_ModernClusteredLighting_DecalViewStats( const viewDef_t *viewDef, rendererClusteredDecalViewStats_t &stats ); +bool R_ModernClusteredLighting_DecalOwnsSurface( const viewDef_t *viewDef, const void *sourceSurface ); +bool R_ModernClusteredLighting_DecalOwnsCommand( const viewDef_t *viewDef, int commandIndex ); +// Sealed-ownership resolution: returns the exact submit-plan command index the +// seal recorded for sourceSurface in this view, -1 when the surface is not +// owned, and sets duplicate when more than one sealed record claims it. +int R_ModernClusteredLighting_DecalSealedCommandForSurface( const viewDef_t *viewDef, const void *sourceSurface, bool &duplicate ); +const char *R_ModernClusteredLighting_DecalRejectName( rendererClusteredDecalReject_t reject ); bool RendererClusterGrid_RunSelfTest( void ); #endif /* !__MODERN_CLUSTERED_LIGHTING_H__ */ diff --git a/src/renderer/ModernGLDrawPlan.cpp b/src/renderer/ModernGLDrawPlan.cpp index 1a6e862d..5ac17b34 100644 --- a/src/renderer/ModernGLDrawPlan.cpp +++ b/src/renderer/ModernGLDrawPlan.cpp @@ -48,6 +48,46 @@ const char *ModernGLDrawPlanPipeline_Name( modernGLDrawPlanPipeline_t pipeline ) } } +static const srfTriangles_t *R_ModernGLDrawPlan_SourceAmbientSurface( + const drawPacket_t &packet ) { + if ( packet.legacyDrawSurf == NULL || packet.legacyDrawSurf->geo == NULL ) { + return NULL; + } + const srfTriangles_t *geometry = packet.legacyDrawSurf->geo; + return geometry->ambientSurface != NULL ? geometry->ambientSurface : geometry; +} + +static bool R_ModernGLDrawPlan_SameStableSurface( + const drawPacket_t &a, const drawPacket_t &b ) { + const srfTriangles_t *aSource = R_ModernGLDrawPlan_SourceAmbientSurface( a ); + const srfTriangles_t *bSource = R_ModernGLDrawPlan_SourceAmbientSurface( b ); + // Per-light interaction receivers are separate drawSurf allocations and can + // contain a culled index subset. Their ambientSurface pointer is the stable + // front-end identity shared with the ambient draw that owns clustered PBR. + return aSource != NULL + && aSource == bSource + && a.viewDef == b.viewDef + && a.space == b.space + && a.materialRecord == b.materialRecord + && a.materialRecordIndex == b.materialRecordIndex; +} + +static const drawPacket_t *R_ModernGLDrawPlan_FindPBRAmbientSurfaceOwner( + const idScenePacketFrame &packetFrame, const drawPacket_t &interaction ) { + if ( interaction.passCategory != RENDER_PASS_ARB2_INTERACTION ) { + return NULL; + } + const int drawPacketCount = packetFrame.NumDrawPackets(); + for ( int packetIndex = 0; packetIndex < drawPacketCount; ++packetIndex ) { + const drawPacket_t &candidate = packetFrame.DrawPacket( packetIndex ); + if ( candidate.passCategory == RENDER_PASS_AMBIENT + && R_ModernGLDrawPlan_SameStableSurface( interaction, candidate ) ) { + return &candidate; + } + } + return NULL; +} + static void R_ModernGLDrawPlan_SetStatus( modernGLDrawPlanStats_t &stats, const char *status ) { idStr::Copynz( stats.status, status ? status : "unknown", sizeof( stats.status ) ); } @@ -213,12 +253,29 @@ static bool R_ModernGLDrawPlan_ShouldUseForwardPlus( const modernGLDrawPlanBuild || materialRecord.materialClass == RENDER_MATERIAL_SHADOW_ONLY ) { return false; } + const bool pbrModernEligible = R_MaterialResourceTable_PBRModernPathEligible( materialRecord ); + // A clustered PBR shader evaluates the complete light list. Per-light ARB2 + // packets are never shader owners; the one ambient packet keeps stable + // surface ordering, including source-alpha ordering. + if ( pbrModernEligible && draw.passCategory != RENDER_PASS_AMBIENT ) { + return false; + } if ( R_ModernGLDrawPlan_IsForwardPlusDecalDraw( draw, materialRecord ) ) { pipeline = MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT; shaderKind = MODERN_GL_SHADER_TRANSPARENT_FORWARD; return true; } if ( draw.passCategory == RENDER_PASS_ARB2_INTERACTION || draw.passCategory == RENDER_PASS_AMBIENT ) { + // Conventional Quake 4 source-alpha materials keep an opaque material + // class, so use the explicit PBR blend contract before the opaque route. + if ( R_MaterialResourceTable_PBRTransparentPathEligible( materialRecord ) ) { + pipeline = MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT; + // The PBR clustered-forward shader carries the complete direct-light, + // IBL, and debug contract. The transparent pipeline supplies the + // source-alpha blend state and ordered submission semantics. + shaderKind = MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE; + return true; + } if ( materialRecord.alphaTest || materialRecord.materialClass == RENDER_MATERIAL_PERFORATED ) { pipeline = MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_ALPHA_TEST; shaderKind = MODERN_GL_SHADER_CLUSTERED_FORWARD_ALPHA_TEST; @@ -238,6 +295,37 @@ static bool R_ModernGLDrawPlan_ShouldUseForwardPlus( const modernGLDrawPlanBuild return false; } +static bool R_ModernGLDrawPlan_PBRInteractionHasReadyClusteredOwner( + const modernGLDrawPlanBuildContext_t &context, + const idScenePacketFrame &packetFrame, + const drawPacket_t &interaction, + const materialResourceTableRecord_t &materialRecord ) { + const drawPacket_t *owner = R_ModernGLDrawPlan_FindPBRAmbientSurfaceOwner( packetFrame, interaction ); + if ( owner == NULL || !R_ModernGLDrawPlan_HasGraphPass( context, RENDER_PASS_AMBIENT ) ) { + return false; + } + if ( RB_FlatDiffuseSurfaceActive( owner->legacyDrawSurf ) + || R_ModernGLDrawPlan_NeedsLegacySoftParticlePath( *owner ) + || owner->geometryRecord == NULL + || owner->geometryRecordIndex < 0 + || R_ModernGLDrawPlan_GeometryFallbackReason( *owner ) != GEOMETRY_RESOURCE_FALLBACK_NONE + || owner->instanceRecord == NULL + || owner->instanceRecordIndex < 0 ) { + return false; + } + modernGLDrawPlanPipeline_t ownerPipeline = MODERN_GL_DRAW_PLAN_PIPELINE_NONE; + modernGLShaderProgramKind_t ownerShader = MODERN_GL_SHADER_FLAT_MATERIAL; + if ( !R_ModernGLDrawPlan_ShouldUseForwardPlus( + context, *owner, materialRecord, ownerPipeline, ownerShader ) ) { + return false; + } + const modernGLShaderProgramInfo_t *program = + ( ownerShader >= 0 && ownerShader < MODERN_GL_SHADER_PROGRAM_KIND_COUNT ) + ? context.programs[ownerShader] + : NULL; + return program != NULL && program->program != 0 && program->linked; +} + bool idModernGLDrawPlan::AddEntry( const drawPacket_t &draw, int drawPacketIndex, const materialResourceTableRecord_t &materialRecord, modernGLDrawPlanPipeline_t pipeline, const modernGLShaderProgramInfo_t &program ) { if ( numEntries >= MODERN_GL_DRAW_PLAN_MAX_ENTRIES ) { stats.overflow = true; @@ -257,10 +345,14 @@ bool idModernGLDrawPlan::AddEntry( const drawPacket_t &draw, int drawPacketIndex entry.modelViewMatrixLocation = program.modelViewMatrixLocation; entry.debugColorLocation = program.debugColorLocation; entry.localParamsLocation = program.localParamsLocation; + entry.pbrIBLLocation = program.pbrIBLLocation; entry.mainTextureLocation = program.mainTextureLocation; entry.normalTextureLocation = program.normalTextureLocation; entry.specularTextureLocation = program.specularTextureLocation; entry.emissiveTextureLocation = program.emissiveTextureLocation; + entry.metallicTextureLocation = program.metallicTextureLocation; + entry.roughnessTextureLocation = program.roughnessTextureLocation; + entry.aoTextureLocation = program.aoTextureLocation; entry.textureIndicesLocation = program.textureIndicesLocation; entry.textureTableModeLocation = program.textureTableModeLocation; entry.materialFlagsLocation = program.materialFlagsLocation; @@ -298,6 +390,13 @@ bool idModernGLDrawPlan::AddEntry( const drawPacket_t &draw, int drawPacketIndex && R_ModernGLDrawPlan_IsForwardPlusDecalDraw( draw, materialRecord ) ) { stats.forwardPlusDecalDraws++; } + if ( materialRecord.hasPBR + && draw.passCategory == RENDER_PASS_AMBIENT + && ( pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_OPAQUE + || pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_ALPHA_TEST + || pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT ) ) { + stats.pbrClusteredSurfaceOwners++; + } if ( entry.glslVersion > stats.highestGLSLVersion ) { stats.highestGLSLVersion = entry.glslVersion; } @@ -345,7 +444,10 @@ bool idModernGLDrawPlan::Build( const idScenePacketFrame &packetFrame, const idR || r_rendererModernGBufferDebug.GetInteger() > 0 || r_rendererModernDeferred.GetBool() || r_rendererModernDeferredDebug.GetInteger() > 0; - context.forwardPlusRequested = context.modernVisibleRequested || r_rendererForwardPlus.GetBool(); + context.forwardPlusRequested = context.modernVisibleRequested + || r_rendererForwardPlus.GetBool() + || ( r_rendererModernQuality.GetBool() + && r_rendererClusteredDecals.GetBool() ); for ( int kind = 0; kind < MODERN_GL_SHADER_PROGRAM_KIND_COUNT; ++kind ) { context.programs[kind] = R_ModernGLShaderLibrary_FindProgram( static_cast( kind ), shaderStats.highestGLSLVersion ); } @@ -401,6 +503,23 @@ bool idModernGLDrawPlan::Build( const idScenePacketFrame &packetFrame, const idR stats.missingMaterialTableDraws++; continue; } + const bool classicModernEligible = R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ); + const bool pbrModernEligible = R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ); + if ( !classicModernEligible && !pbrModernEligible ) { + stats.fallbackDraws++; + stats.materialFallbackDraws++; + continue; + } + if ( pbrModernEligible + && draw.passCategory == RENDER_PASS_ARB2_INTERACTION + && R_ModernGLDrawPlan_PBRInteractionHasReadyClusteredOwner( + context, packetFrame, draw, *materialRecord ) ) { + // The ambient packet is the one complete clustered lighting owner for + // this stable surface. Consuming the per-light packet is intentional, + // not a legacy fallback. + stats.pbrClusteredConsumedInteractions++; + continue; + } const bool forwardPlusCandidate = R_ModernGLDrawPlan_ShouldUseForwardPlus( context, draw, *materialRecord, pipeline, shaderKind ); if ( materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE && ( !forwardPlusCandidate || !R_ModernGLDrawPlan_MaterialFallbackAllowedForForwardPlus( draw, *materialRecord ) ) ) { @@ -409,7 +528,30 @@ bool idModernGLDrawPlan::Build( const idScenePacketFrame &packetFrame, const idR continue; } - if ( !forwardPlusCandidate && draw.passCategory == RENDER_PASS_AMBIENT && R_ModernGLDrawPlan_ShouldUseGBuffer( context, *materialRecord ) ) { + const bool pbrGBufferCandidate = + !forwardPlusCandidate + && draw.passCategory == RENDER_PASS_AMBIENT + && R_ModernGLDrawPlan_ShouldUseGBuffer( context, *materialRecord ); + // Depth-only packets do not evaluate a material lighting model. PBR + // records therefore use the regular depth owner (including alpha-test + // sampling where applicable) before their G-buffer or forward shading + // pass; rejecting them here leaves a visible PBR frame with an empty + // scene-depth texture. + const bool pbrDepthCandidate = + !forwardPlusCandidate + && R_ModernGLDrawPlan_IsDepthPipeline( pipeline ); + // A PBR record is never compatible with the generic flat-material + // diagnostic program. It must have an explicit PBR G-buffer or + // clustered-forward owner, except for the lighting-independent depth + // prepass above; otherwise leave the complete draw with the classic + // renderer rather than submitting a partial material model. + if ( materialRecord->hasPBR && !forwardPlusCandidate && !pbrGBufferCandidate && !pbrDepthCandidate ) { + stats.fallbackDraws++; + stats.materialFallbackDraws++; + continue; + } + + if ( pbrGBufferCandidate ) { pipeline = MODERN_GL_DRAW_PLAN_PIPELINE_GBUFFER; shaderKind = ( materialRecord->alphaTest || materialRecord->materialClass == RENDER_MATERIAL_PERFORATED ) ? MODERN_GL_SHADER_GBUFFER_ALPHA_TEST diff --git a/src/renderer/ModernGLDrawPlan.h b/src/renderer/ModernGLDrawPlan.h index 2e7f496c..3f45e449 100644 --- a/src/renderer/ModernGLDrawPlan.h +++ b/src/renderer/ModernGLDrawPlan.h @@ -36,10 +36,14 @@ typedef struct modernGLDrawPlanEntry_s { int modelViewMatrixLocation; int debugColorLocation; int localParamsLocation; + int pbrIBLLocation; int mainTextureLocation; int normalTextureLocation; int specularTextureLocation; int emissiveTextureLocation; + int metallicTextureLocation; + int roughnessTextureLocation; + int aoTextureLocation; int textureIndicesLocation; int textureTableModeLocation; int materialFlagsLocation; @@ -82,6 +86,8 @@ typedef struct modernGLDrawPlanStats_s { int vertexOnlyDraws; int pipelineDraws[MODERN_GL_DRAW_PLAN_PIPELINE_COUNT]; int forwardPlusDecalDraws; + int pbrClusteredSurfaceOwners; + int pbrClusteredConsumedInteractions; int pipelineBatches; int geometryBatches; int textureSetBatches; diff --git a/src/renderer/ModernGLExecutor.cpp b/src/renderer/ModernGLExecutor.cpp index ddb2ca2d..00f1af7d 100644 --- a/src/renderer/ModernGLExecutor.cpp +++ b/src/renderer/ModernGLExecutor.cpp @@ -11,12 +11,16 @@ #include "ModernGLShaderLibrary.h" #include "ModernGLSubmitPlan.h" #include "ModernLightImageAtlas.h" +#include "ModernSpecularProbeAtlas.h" #include "ModernShadowPlanner.h" #include "RenderGraphResources.h" #include "RendererBootstrap.h" +#include "RendererContracts.h" #include "RendererMetrics.h" #include "RendererUpload.h" +#include + typedef struct modernGLFrameConstants_s { float viewport[4]; float frame[4]; @@ -170,27 +174,11 @@ enum modernGLDrawVertAttribute_t { const GLuint MODERN_GL_DRAWVERT_BINDING_INDEX = 0; const GLuint MODERN_GL_DRAW_RECORD_BINDING_INDEX = 1; +const GLuint MODERN_GL_DECAL_COLOR_BINDING_INDEX = 2; const GLuint MODERN_GL_DRAW_RECORD_ATTR_INDEX = 12; const GLuint MODERN_GL_DRAW_RECORD_SSBO_BINDING = 4; const GLuint MODERN_GL_GPU_BUCKET_SSBO_BINDING = 5; -typedef struct modernGLDrawVertAttributeDesc_s { - modernGLDrawVertAttribute_t attribute; - GLint components; - GLenum type; - GLboolean normalized; - GLuint relativeOffset; -} modernGLDrawVertAttributeDesc_t; - -static const modernGLDrawVertAttributeDesc_t rg_modernGLDrawVertAttributes[] = { - { MODERN_GL_DRAWVERT_ATTR_POSITION, 3, GL_FLOAT, GL_FALSE, DRAWVERT_XYZ_OFFSET }, - { MODERN_GL_DRAWVERT_ATTR_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, DRAWVERT_COLOR_OFFSET }, - { MODERN_GL_DRAWVERT_ATTR_TEXCOORD0, 2, GL_FLOAT, GL_FALSE, DRAWVERT_ST_OFFSET }, - { MODERN_GL_DRAWVERT_ATTR_TANGENT0, 3, GL_FLOAT, GL_FALSE, DRAWVERT_TANGENT0_OFFSET }, - { MODERN_GL_DRAWVERT_ATTR_TANGENT1, 3, GL_FLOAT, GL_FALSE, DRAWVERT_TANGENT1_OFFSET }, - { MODERN_GL_DRAWVERT_ATTR_NORMAL, 3, GL_FLOAT, GL_FALSE, DRAWVERT_NORMAL_OFFSET } -}; - typedef struct modernGLVertexBindingSourceCache_s { bool valid; GLuint vertexBuffer; @@ -226,7 +214,15 @@ static const char *R_ModernGLExecutor_FrameModeName( int mode ) { } static bool R_ModernGLExecutor_ModernVisibleRequested( void ) { - return r_rendererModernVisible.GetBool() || RendererBootstrap_ShouldAutoPromoteModernVisible(); + // These diagnostics intentionally remove the 3D backend work while retaining + // classic 2D drawing. Never let the side pipeline recreate or later compose + // a world frame over that GUI, including when shared GUI ownership is enabled. + return !r_skipRender.GetBool() && !r_skipRenderContext.GetBool() + && !r_rendererSharedWorldAmbient.GetBool() + && !r_rendererSharedWorldInteraction.GetBool() + && !r_rendererSharedDeform.GetBool() + && ( r_rendererModernVisible.GetBool() + || RendererBootstrap_ShouldAutoPromoteModernVisible() ); } bool R_ModernGLExecutor_ModernVisibleRequestedForPost( void ) { @@ -396,11 +392,18 @@ const int MODERN_GL_MATERIAL_TEXTURE_MAIN = 0; const int MODERN_GL_MATERIAL_TEXTURE_NORMAL = 1; const int MODERN_GL_MATERIAL_TEXTURE_SPECULAR = 2; const int MODERN_GL_MATERIAL_TEXTURE_EMISSIVE = 3; -const int MODERN_GL_MATERIAL_TEXTURE_COUNT = 4; +const int MODERN_GL_MATERIAL_TEXTURE_METALLIC = 4; +const int MODERN_GL_MATERIAL_TEXTURE_ROUGHNESS = 5; +const int MODERN_GL_MATERIAL_TEXTURE_AO = 6; +const int MODERN_GL_MATERIAL_TEXTURE_COUNT = 7; +// uTextureIndices is a uvec4. Separate PBR maps deliberately use direct +// samplers rather than extending that legacy table ABI. +const int MODERN_GL_MATERIAL_TEXTURE_TABLE_COUNT = 4; const int MODERN_GL_CLUSTER_UBO_BINDING_PARAMS = 3; const int MODERN_GL_CLUSTER_UBO_BINDING_LIGHTS = 4; const int MODERN_GL_CLUSTER_UBO_BINDING_INDICES = 5; const int MODERN_GL_CLUSTER_UBO_BINDING_SHADOW_DESCRIPTORS = 6; +const int MODERN_GL_CLUSTER_UBO_BINDING_SPECULAR_PROBES = 7; const int MODERN_GL_VISIBLE_COMPOSITE_TEXTURE_COUNT = 2; static const char *rg_modernGLGBufferAttachmentNames[MODERN_GL_GBUFFER_ATTACHMENT_COUNT] = { @@ -428,9 +431,94 @@ static const char *rg_modernGLDeferredTextureUniforms[MODERN_GL_DEFERRED_TEXTURE static const char *rg_modernGLShadowProjectedMomentUniform = "uModernTranslucentShadowMoments[0]"; static const char *rg_modernGLShadowPointMomentUniform = "uModernPointTranslucentShadowMoments[0]"; +// Shadow uniform locations and sampler-unit assignments are link-time program +// state, so they are memoized per program object and cleared alongside the +// cluster-block binding memo on shader-library relink and executor shutdown. +// The bindings snapshot is only valid within one revision of the shared shadow +// state: the classic per-light path (RB_ShadowMapSelectPointCacheEntry, +// RB_SharedWorldInteractionGLActivateMapResource) mutates +// g_shadowMapDepthImage/g_activePointShadowMapCache during RB_DrawView, so the +// revision is bumped every PrepareFrame and on every decal-overlay entry. +struct modernGLShadowUniformLocations_t { + GLuint program; + GLint projectedAtlas; + GLint pointAtlas; + GLint projectedMoments; + GLint pointMoments; + GLint resourceState; + GLint samplerState; + GLint momentState; + GLint contractState; + GLint lightImageAtlas; + GLint specularProbeAtlas; + bool samplerUnitsAssigned; + bool samplerReady; + int stateRevision; + int stateFrameCount; +}; + +const int MODERN_GL_SHADOW_UNIFORM_PROGRAM_CAPACITY = 32; +static modernGLShadowUniformLocations_t rg_modernGLShadowUniformPrograms[MODERN_GL_SHADOW_UNIFORM_PROGRAM_CAPACITY]; +static int rg_modernGLShadowUniformProgramCount = 0; + +static int rg_modernGLShadowBindingsRevision = 0; +static int rg_modernGLShadowSnapshotRevision = -1; +static int rg_modernGLShadowSnapshotFrameCount = -1; +static rendererShadowTextureBindings_t rg_modernGLShadowBindingsSnapshot; +static GLuint rg_modernGLShadowCompareCleared[MODERN_GL_SHADOW_TEXTURE_UNIT_COUNT]; + static modernGLExecutorStats_t rg_modernGLExecutorStats; +// False for the first modern side-pipeline pass after a render-world change. +// The classic backend refreshes the newly scoped cache later in that frame; +// until then, bind only complete placeholders and advertise no shadow atlas. +static bool rg_modernGLShadowTextureBindingsCurrent = false; static idModernGLDrawPlan rg_modernGLDrawPlan; static idModernGLSubmitPlan rg_modernGLSubmitPlan; +// Last uniform values uploaded by R_ModernGLExecutor_SubmitCommand for the +// currently bound program. GLSL uniform state persists per program object, but +// the memo only trusts values within one same-program run inside one modern +// pass: every program switch re-primes it, and every pass restore, shader +// relink (InvalidatePlans) and Shutdown forgets it. glDepthRange is tracked +// here too (not in idGLStateCache) because the legacy backend writes it raw +// (RB_EnterWeaponDepthHack/RB_LeaveDepthHack, render tools) between passes. +struct modernGLSubmitUniformMemo_t { + bool valid; + GLuint program; + bool drawRecordZeroed; + bool debugColorValid; + bool localParamsValid; + bool pbrIBLValid; + bool materialFlagsValid; + bool materialEnhancementValid; + float debugColor[4]; + float localParams[4]; + float pbrIBL[4]; + float materialFlags[4]; + float materialEnhancement[4]; +}; +static modernGLSubmitUniformMemo_t rg_modernGLSubmitUniformMemo; +static bool rg_modernGLDepthRangeKnown = false; +static bool rg_modernGLDepthRangeHalf = false; + +static void R_ModernGLExecutor_InvalidateSubmitStateMemos( void ) { + rg_modernGLSubmitUniformMemo.valid = false; + rg_modernGLDepthRangeKnown = false; +} + +static bool R_ModernGLExecutor_SubmitUniform4fChanged( bool &cachedValid, float cached[4], const float value[4] ) { + if ( rg_modernGLSubmitUniformMemo.valid && cachedValid + && cached[0] == value[0] && cached[1] == value[1] + && cached[2] == value[2] && cached[3] == value[3] ) { + return false; + } + cached[0] = value[0]; + cached[1] = value[1]; + cached[2] = value[2]; + cached[3] = value[3]; + cachedValid = true; + return true; +} + static renderBackendCaps_t rg_modernGLExecutorCaps; static renderFeatureSet_t rg_modernGLExecutorFeatures; static GLuint rg_modernGLExecutorVAO = 0; @@ -478,7 +566,6 @@ static int rg_modernGLExecutorVertexInputFormatSetups = 0; static modernGLVertexInputCache_t rg_modernGLVertexInputCache; static modernGLGpuDrivenBucket_t rg_modernGLGpuDrivenBuckets[MODERN_GL_DRAW_PLAN_MAX_ENTRIES]; static int rg_modernGLGpuDrivenBucketCount = 0; -static modernGLStreamBufferBinding_t rg_modernGLFrameUBOStream; static modernGLGpuDrivenStreamBindings_t rg_modernGLGpuDrivenStreamBindings; static modernGLPendingGpuValidationReadback_t rg_modernGLPendingValidationReadbacks[MODERN_GL_GPU_VALIDATION_PENDING_READBACKS]; static int rg_modernGLExecutorUniformBufferAlignment = 256; @@ -553,6 +640,8 @@ static void R_ModernGLExecutor_CopyDrawPlanStats( modernGLExecutorStats_t &stats stats.drawPlanDepthDraws = drawPlanStats.depthDraws; stats.drawPlanMaterialDraws = drawPlanStats.materialDraws; stats.drawPlanFallbackDraws = drawPlanStats.fallbackDraws; + stats.drawPlanPBRClusteredSurfaceOwners = drawPlanStats.pbrClusteredSurfaceOwners; + stats.drawPlanPBRClusteredConsumedInteractions = drawPlanStats.pbrClusteredConsumedInteractions; stats.drawPlanGeometryFallbackDraws = drawPlanStats.geometryFallbackDraws; stats.drawPlanGeometryDeformFallbackDraws = drawPlanStats.geometryDeformFallbackDraws; stats.drawPlanGeometrySkinnedFallbackDraws = drawPlanStats.geometrySkinnedFallbackDraws; @@ -1650,10 +1739,14 @@ static void R_ModernGLExecutor_CountModernVisibleOwner( const renderGraphPass_t modernOwned = stats.forwardPlusRequested; break; case RENDER_PASS_LIGHT_GRID: - // The graph models baked light-grid dependencies for planning, but the - // current deferred/Forward+ shaders do not sample the packed atlas data. - // Keep this pass legacy-owned until that parity exists. - modernOwned = false; + // The front end always declares the light-grid pass, even when it emitted + // no receiver draws. An empty pass has no legacy contribution to preserve, + // so it is vacuously ownable; real draws remain legacy-owned until the + // modern shaders consume the packed atlas with matching parity. + modernOwned = pass.drawPacketCount == 0; + if ( modernOwned ) { + stats.modernVisibleLightGridModernPasses++; + } break; case RENDER_PASS_GUI: modernOwned = stats.modernVisibleGuiProgramReady; @@ -1789,18 +1882,6 @@ static bool R_ModernGLExecutor_DrawPacketUsesLegacySidecarView( const drawPacket || R_ModernGLExecutor_ViewDefUsesLegacySidecar( draw.viewDef ); } -static int R_ModernGLExecutor_CountPassDraws( const idScenePacketFrame &packetFrame, renderPassCategory_t category ) { - int count = 0; - const int drawPacketCount = packetFrame.NumDrawPackets(); - for ( int i = 0; i < drawPacketCount; ++i ) { - const drawPacket_t &draw = packetFrame.DrawPacket( i ); - if ( draw.passCategory == category && !R_ModernGLExecutor_DrawPacketUsesLegacySidecarView( draw ) ) { - count++; - } - } - return count; -} - static bool R_ModernGLExecutor_DrawPacketUsesLegacyFeedbackSurface( const drawPacket_t &draw ) { if ( R_ModernGLExecutor_DrawPacketUsesLegacySidecarView( draw ) ) { return true; @@ -1859,7 +1940,11 @@ static void R_ModernGLExecutor_RecordPacketFallbackBlockers( const idScenePacket continue; } - const int viewIndex = R_ModernGLExecutor_ViewIndexForViewDef( packetFrame, draw.viewDef ); + // viewIndex is consumed only by SetOwnershipBlocker, which keeps the first + // blocker; skip the per-packet scene scan once one is recorded + const int viewIndex = stats.modernVisibleOwnershipBlocker[0] == '\0' + ? R_ModernGLExecutor_ViewIndexForViewDef( packetFrame, draw.viewDef ) + : -1; const bool legacyFeedbackSurface = R_ModernGLExecutor_DrawPacketUsesLegacyFeedbackSurface( draw ); if ( RB_FlatDiffuseSurfaceActive( draw.legacyDrawSurf ) ) { // The modern material shaders currently collapse authored ambient @@ -1899,6 +1984,12 @@ static void R_ModernGLExecutor_RecordPacketFallbackBlockers( const idScenePacket R_ModernGLExecutor_SetOwnershipBlocker( stats, "draw", viewIndex, draw.passCategory, i, "material", "missing-material-record" ); continue; } + if ( !R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ) + && !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) { + stats.modernVisibleMaterialFallbackDraws++; + R_ModernGLExecutor_SetOwnershipBlocker( stats, "material", viewIndex, draw.passCategory, materialRecord->materialId, materialRecord->materialName, "pbr-material-not-modern-ready" ); + continue; + } if ( R_ModernGLExecutor_DrawPacketNeedsLegacySceneGui( draw, *materialRecord ) && !legacyFeedbackSurface ) { stats.modernVisibleMaterialFallbackDraws++; R_ModernGLExecutor_SetOwnershipBlocker( stats, "material", viewIndex, draw.passCategory, materialRecord->materialId, materialRecord->materialName, "legacy-scene-gui" ); @@ -2295,9 +2386,31 @@ static void R_ModernGLExecutor_AnalyzeModernVisibleOwnershipReadiness( const idS // built the frame, so it can only record which lighting domains the front // end is asking for. The verdicts themselves are decided per light in // R_ModernGLExecutor_ClassifyModernVisibleLighting once descriptors exist. - stats.modernVisibleInteractionPasses = R_ModernGLExecutor_CountPassDraws( packetFrame, RENDER_PASS_ARB2_INTERACTION ) > 0 ? 1 : 0; - stats.modernVisibleFogBlendPasses = R_ModernGLExecutor_CountPassDraws( packetFrame, RENDER_PASS_FOG_BLEND ) > 0 ? 1 : 0; - stats.modernVisibleLightGridPasses = R_ModernGLExecutor_CountPassDraws( packetFrame, RENDER_PASS_LIGHT_GRID ) > 0 ? 1 : 0; + int interactionDraws = 0; + int fogBlendDraws = 0; + int lightGridDraws = 0; + const int passDrawPacketCount = packetFrame.NumDrawPackets(); + for ( int i = 0; i < passDrawPacketCount; ++i ) { + const drawPacket_t &draw = packetFrame.DrawPacket( i ); + if ( draw.passCategory != RENDER_PASS_ARB2_INTERACTION + && draw.passCategory != RENDER_PASS_FOG_BLEND + && draw.passCategory != RENDER_PASS_LIGHT_GRID ) { + continue; + } + if ( R_ModernGLExecutor_DrawPacketUsesLegacySidecarView( draw ) ) { + continue; + } + if ( draw.passCategory == RENDER_PASS_ARB2_INTERACTION ) { + interactionDraws++; + } else if ( draw.passCategory == RENDER_PASS_FOG_BLEND ) { + fogBlendDraws++; + } else { + lightGridDraws++; + } + } + stats.modernVisibleInteractionPasses = interactionDraws > 0 ? 1 : 0; + stats.modernVisibleFogBlendPasses = fogBlendDraws > 0 ? 1 : 0; + stats.modernVisibleLightGridPasses = lightGridDraws > 0 ? 1 : 0; if ( graph.FindPass( RENDER_PASS_ARB2_INTERACTION ) >= 0 ) { stats.modernVisibleInteractionPasses = Max( stats.modernVisibleInteractionPasses, 1 ); } @@ -2595,15 +2708,6 @@ static void R_ModernGLExecutor_BindUniformBuffer( GLuint buffer ) { } static void R_ModernGLExecutor_BindFrameUniformBufferBase( modernGLExecutorStats_t &stats ) { - if ( rg_modernGLFrameUBOStream.valid && glBindBufferRange != NULL ) { - R_GLStateCache().BindBufferRange( - GL_UNIFORM_BUFFER, - 0, - rg_modernGLFrameUBOStream.allocation.vbo, - static_cast( rg_modernGLFrameUBOStream.allocation.offset ), - rg_modernGLFrameUBOStream.size ); - return; - } if ( rg_modernGLExecutorFrameUBO == 0 ) { return; } @@ -2620,7 +2724,6 @@ static void R_ModernGLExecutor_BindFrameUniformBufferBase( modernGLExecutorStats } static void R_ModernGLExecutor_UpdateFrameUBO( modernGLExecutorStats_t &stats ) { - R_ModernGLExecutor_ResetStreamBinding( rg_modernGLFrameUBOStream ); if ( !stats.enabled || !stats.available || !stats.initialized || !stats.frameUBOReady ) { return; } @@ -2640,10 +2743,11 @@ static void R_ModernGLExecutor_UpdateFrameUBO( modernGLExecutorStats_t &stats ) constants.capabilities[2] = rg_modernGLExecutorCaps.hasUBO ? 1.0f : 0.0f; constants.capabilities[3] = rg_modernGLExecutorCaps.hasVAO ? 1.0f : 0.0f; - if ( glBindBufferRange != NULL && R_ModernGLExecutor_StreamBufferData( &constants, sizeof( constants ), rg_modernGLExecutorUniformBufferAlignment, rg_modernGLFrameUBOStream, stats ) ) { - stats.frameUBOStreamed = true; - stats.uploadManagerFrameUBOBytes += static_cast( sizeof( constants ) ); - } else if ( rg_modernGLExecutorLowOverheadReady && glNamedBufferSubData != NULL ) { + // Keep frame constants in their dedicated UBO. The upload-manager VBO ring + // is valid for vertex data, but binding its short ranges as UBOs generated + // GL_INVALID_VALUE on GL 3.3/4.1 drivers. This is a tiny, once-per-frame + // update, so the dedicated buffer is both portable and ample. + if ( rg_modernGLExecutorLowOverheadReady && glNamedBufferSubData != NULL ) { glNamedBufferSubData( rg_modernGLExecutorFrameUBO, 0, sizeof( constants ), &constants ); stats.lowOverheadDSAUpdates++; R_ModernGLExecutor_RecordUploadFallback( stats, sizeof( constants ) ); @@ -2663,9 +2767,63 @@ static const GLvoid *R_ModernGLExecutor_BufferOffset( int offset ) { return reinterpret_cast( static_cast( offset ) ); } +static bool R_ModernGLExecutor_TranslateLegacyVertexAttribute( + const rendererVertexAttributeDesc_t &source, GLuint &attribute, + GLint &components, GLenum &type, GLboolean &normalized ) { + switch ( source.semantic ) { + case RENDERER_VERTEX_SEMANTIC_POSITION: + attribute = MODERN_GL_DRAWVERT_ATTR_POSITION; + break; + case RENDERER_VERTEX_SEMANTIC_COLOR0: + attribute = MODERN_GL_DRAWVERT_ATTR_COLOR; + break; + case RENDERER_VERTEX_SEMANTIC_TEXCOORD0: + attribute = MODERN_GL_DRAWVERT_ATTR_TEXCOORD0; + break; + case RENDERER_VERTEX_SEMANTIC_TANGENT0: + attribute = MODERN_GL_DRAWVERT_ATTR_TANGENT0; + break; + case RENDERER_VERTEX_SEMANTIC_TANGENT1: + attribute = MODERN_GL_DRAWVERT_ATTR_TANGENT1; + break; + case RENDERER_VERTEX_SEMANTIC_NORMAL: + attribute = MODERN_GL_DRAWVERT_ATTR_NORMAL; + break; + default: + return false; + } + switch ( source.format ) { + case RENDERER_VERTEX_FORMAT_FLOAT32X2: + components = 2; + type = GL_FLOAT; + normalized = GL_FALSE; + return true; + case RENDERER_VERTEX_FORMAT_FLOAT32X3: + components = 3; + type = GL_FLOAT; + normalized = GL_FALSE; + return true; + case RENDERER_VERTEX_FORMAT_FLOAT32X4: + components = 4; + type = GL_FLOAT; + normalized = GL_FALSE; + return true; + case RENDERER_VERTEX_FORMAT_UNORM8X4: + components = 4; + type = GL_UNSIGNED_BYTE; + normalized = GL_TRUE; + return true; + default: + return false; + } +} + static bool R_ModernGLExecutor_DrawVertLayoutSupported( int vertexStride, int ambientCacheOffset ) { - return vertexStride >= static_cast( sizeof( idDrawVert ) ) - && vertexStride >= DRAWVERT_SIZE + const rendererVertexLayoutDesc_t &layout = RendererContracts_LegacyDrawVertLayout(); + return RendererContracts_ValidateVertexLayout( layout ) + && layout.bindingCount == 1 + && vertexStride >= static_cast( layout.bindings[ 0 ].stride ) + && vertexStride >= static_cast( sizeof( idDrawVert ) ) && ambientCacheOffset >= 0; } @@ -2683,11 +2841,22 @@ static bool R_ModernGLExecutor_ConfigureDrawVertVertexBindingFormat( modernGLExe if ( glEnableVertexAttribArray == NULL || glVertexAttribFormat == NULL || glVertexAttribBinding == NULL ) { return false; } - for ( int i = 0; i < static_cast( sizeof( rg_modernGLDrawVertAttributes ) / sizeof( rg_modernGLDrawVertAttributes[0] ) ); ++i ) { - const modernGLDrawVertAttributeDesc_t &desc = rg_modernGLDrawVertAttributes[i]; - const GLuint attribute = static_cast( desc.attribute ); + const rendererVertexLayoutDesc_t &layout = RendererContracts_LegacyDrawVertLayout(); + if ( !RendererContracts_ValidateVertexLayout( layout ) ) { + return false; + } + for ( std::uint32_t i = 0; i < layout.attributeCount; ++i ) { + const rendererVertexAttributeDesc_t &desc = layout.attributes[ i ]; + GLuint attribute = 0; + GLint components = 0; + GLenum type = GL_NONE; + GLboolean normalized = GL_FALSE; + if ( !R_ModernGLExecutor_TranslateLegacyVertexAttribute( + desc, attribute, components, type, normalized ) ) { + return false; + } glEnableVertexAttribArray( attribute ); - glVertexAttribFormat( attribute, desc.components, desc.type, desc.normalized, desc.relativeOffset ); + glVertexAttribFormat( attribute, components, type, normalized, desc.offset ); glVertexAttribBinding( attribute, MODERN_GL_DRAWVERT_BINDING_INDEX ); } if ( glVertexBindingDivisor != NULL ) { @@ -2702,17 +2871,6 @@ static bool R_ModernGLExecutor_ConfigureDrawVertVertexBindingFormat( modernGLExe return true; } -static void R_ModernGLExecutor_SetDrawVertFloatAttrib( modernGLDrawVertAttribute_t attribute, int components, int vertexStride, int offset ) { - glEnableVertexAttribArray( static_cast( attribute ) ); - glVertexAttribPointer( - static_cast( attribute ), - components, - GL_FLOAT, - GL_FALSE, - vertexStride, - R_ModernGLExecutor_BufferOffset( offset ) ); -} - static bool R_ModernGLExecutor_BindDrawVertVertexBindingSource( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats, GLintptr baseOffsetOverride ) { if ( !R_ModernGLExecutor_ConfigureDrawVertVertexBindingFormat( &stats ) || glBindVertexBuffer == NULL ) { return false; @@ -2747,19 +2905,22 @@ static bool R_ModernGLExecutor_BindDrawVertLegacyLayout( const modernGLSubmitCom R_GLStateCache().BindBuffer( GL_ARRAY_BUFFER, command.vertexBuffer ); const int baseOffset = command.ambientCacheOffset; - R_ModernGLExecutor_SetDrawVertFloatAttrib( MODERN_GL_DRAWVERT_ATTR_POSITION, 3, command.vertexStride, baseOffset + DRAWVERT_XYZ_OFFSET ); - glEnableVertexAttribArray( MODERN_GL_DRAWVERT_ATTR_COLOR ); - glVertexAttribPointer( - MODERN_GL_DRAWVERT_ATTR_COLOR, - 4, - GL_UNSIGNED_BYTE, - GL_TRUE, - command.vertexStride, - R_ModernGLExecutor_BufferOffset( baseOffset + DRAWVERT_COLOR_OFFSET ) ); - R_ModernGLExecutor_SetDrawVertFloatAttrib( MODERN_GL_DRAWVERT_ATTR_TEXCOORD0, 2, command.vertexStride, baseOffset + DRAWVERT_ST_OFFSET ); - R_ModernGLExecutor_SetDrawVertFloatAttrib( MODERN_GL_DRAWVERT_ATTR_TANGENT0, 3, command.vertexStride, baseOffset + DRAWVERT_TANGENT0_OFFSET ); - R_ModernGLExecutor_SetDrawVertFloatAttrib( MODERN_GL_DRAWVERT_ATTR_TANGENT1, 3, command.vertexStride, baseOffset + DRAWVERT_TANGENT1_OFFSET ); - R_ModernGLExecutor_SetDrawVertFloatAttrib( MODERN_GL_DRAWVERT_ATTR_NORMAL, 3, command.vertexStride, baseOffset + DRAWVERT_NORMAL_OFFSET ); + const rendererVertexLayoutDesc_t &layout = RendererContracts_LegacyDrawVertLayout(); + for ( std::uint32_t i = 0; i < layout.attributeCount; ++i ) { + const rendererVertexAttributeDesc_t &desc = layout.attributes[ i ]; + GLuint attribute = 0; + GLint components = 0; + GLenum type = GL_NONE; + GLboolean normalized = GL_FALSE; + if ( !R_ModernGLExecutor_TranslateLegacyVertexAttribute( + desc, attribute, components, type, normalized ) ) { + return false; + } + glEnableVertexAttribArray( attribute ); + glVertexAttribPointer( attribute, components, type, normalized, + command.vertexStride, R_ModernGLExecutor_BufferOffset( + baseOffset + static_cast( desc.offset ) ) ); + } rg_modernGLVertexInputCache.legacyLayoutValid = true; rg_modernGLVertexInputCache.legacyVertexBuffer = static_cast( command.vertexBuffer ); rg_modernGLVertexInputCache.legacyVertexStride = command.vertexStride; @@ -2780,6 +2941,60 @@ static bool R_ModernGLExecutor_BindDrawVertLayout( const modernGLSubmitCommand_t return R_ModernGLExecutor_BindDrawVertLegacyLayout( command, stats ); } +static bool R_ModernGLExecutor_BindClusteredDecalColorStream( + const modernGLSubmitCommand_t &command, const drawSurf_t *surface ) { + if ( surface == NULL || surface->decalColorOffset < 0 + || command.ambientCacheOffset < 0 + || surface->decalColorOffset > idMath::INT_MAX - command.ambientCacheOffset ) { + return false; + } + const int colorBase = command.ambientCacheOffset + surface->decalColorOffset; + if ( rg_modernGLExecutorVertexBindingReady ) { + if ( glVertexAttribFormat == NULL || glVertexAttribBinding == NULL + || glBindVertexBuffer == NULL ) { + return false; + } + glVertexAttribFormat( MODERN_GL_DRAWVERT_ATTR_COLOR, 4, + GL_UNSIGNED_BYTE, GL_TRUE, 0 ); + glVertexAttribBinding( MODERN_GL_DRAWVERT_ATTR_COLOR, + MODERN_GL_DECAL_COLOR_BINDING_INDEX ); + if ( glVertexBindingDivisor != NULL ) { + glVertexBindingDivisor( MODERN_GL_DECAL_COLOR_BINDING_INDEX, 0 ); + } + glBindVertexBuffer( MODERN_GL_DECAL_COLOR_BINDING_INDEX, + static_cast( command.vertexBuffer ), + static_cast( colorBase ), 4 ); + return true; + } + if ( glVertexAttribPointer == NULL ) { + return false; + } + R_GLStateCache().BindBuffer( GL_ARRAY_BUFFER, command.vertexBuffer ); + glVertexAttribPointer( MODERN_GL_DRAWVERT_ATTR_COLOR, 4, + GL_UNSIGNED_BYTE, GL_TRUE, 0, + R_ModernGLExecutor_BufferOffset( colorBase ) ); + return true; +} + +static void R_ModernGLExecutor_RestoreDrawVertColorStream( + const modernGLSubmitCommand_t &command ) { + const GLuint colorOffset = static_cast( offsetof( idDrawVert, color ) ); + if ( rg_modernGLExecutorVertexBindingReady ) { + glVertexAttribFormat( MODERN_GL_DRAWVERT_ATTR_COLOR, 4, + GL_UNSIGNED_BYTE, GL_TRUE, colorOffset ); + glVertexAttribBinding( MODERN_GL_DRAWVERT_ATTR_COLOR, + MODERN_GL_DRAWVERT_BINDING_INDEX ); + glBindVertexBuffer( MODERN_GL_DECAL_COLOR_BINDING_INDEX, + 0, 0, 4 ); + return; + } + R_GLStateCache().BindBuffer( GL_ARRAY_BUFFER, command.vertexBuffer ); + glVertexAttribPointer( MODERN_GL_DRAWVERT_ATTR_COLOR, 4, + GL_UNSIGNED_BYTE, GL_TRUE, command.vertexStride, + R_ModernGLExecutor_BufferOffset( + command.ambientCacheOffset + static_cast( colorOffset ) ) ); +} + static bool R_ModernGLExecutor_BindDrawVertLayoutForIndirect( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats ) { if ( !rg_modernGLExecutorVertexBindingReady || !R_ModernGLExecutor_DrawVertLayoutSupported( command.vertexStride, 0 ) ) { return false; @@ -2842,16 +3057,19 @@ static bool R_ModernGLExecutor_CommandEffectiveScissor( const modernGLSubmitComm static bool R_ModernGLExecutor_DepthResourceReady( const char *name, const renderGraphResourceHandle_t *&handle ); static void R_ModernGLExecutor_RecordMetrics( const modernGLExecutorStats_t &stats ); -static void R_ModernGLExecutor_SetSubmitScissor( const modernGLSubmitCommand_t &command, const viewDef_t *viewDef ) { +static void R_ModernGLExecutor_SetSubmitScissor( const modernGLSubmitCommand_t &command, + const viewDef_t *viewDef, bool useLegacyViewportOffset ) { if ( viewDef == NULL ) { R_GLStateCache().SetViewport( 0, 0, glConfig.vidWidth, glConfig.vidHeight ); R_GLStateCache().SetScissor( 0, 0, glConfig.vidWidth, glConfig.vidHeight ); return; } + const int viewportOffsetX = useLegacyViewportOffset ? tr.viewportOffset[0] : 0; + const int viewportOffsetY = useLegacyViewportOffset ? tr.viewportOffset[1] : 0; R_GLStateCache().SetViewport( - viewDef->viewport.x1, - viewDef->viewport.y1, + viewportOffsetX + viewDef->viewport.x1, + viewportOffsetY + viewDef->viewport.y1, viewDef->viewport.x2 + 1 - viewDef->viewport.x1, viewDef->viewport.y2 + 1 - viewDef->viewport.y1 ); @@ -2868,20 +3086,24 @@ static void R_ModernGLExecutor_SetSubmitScissor( const modernGLSubmitCommand_t & } R_GLStateCache().SetScissor( - viewDef->viewport.x1 + scissorX1, - viewDef->viewport.y1 + scissorY1, + viewportOffsetX + viewDef->viewport.x1 + scissorX1, + viewportOffsetY + viewDef->viewport.y1 + scissorY1, Max( 1, scissorX2 - scissorX1 + 1 ), Max( 1, scissorY2 - scissorY1 + 1 ) ); } static void R_ModernGLExecutor_ApplyCommandDepthRange( const modernGLSubmitCommand_t &command ) { - if ( command.modelDepthHack != 0.0f ) { - glDepthRange( 0.0, 1.0 ); - } else if ( command.weaponDepthHack ) { + const bool half = command.modelDepthHack == 0.0f && command.weaponDepthHack; + if ( rg_modernGLDepthRangeKnown && rg_modernGLDepthRangeHalf == half ) { + return; + } + if ( half ) { glDepthRange( 0.0, 0.5 ); } else { glDepthRange( 0.0, 1.0 ); } + rg_modernGLDepthRangeKnown = true; + rg_modernGLDepthRangeHalf = half; } static void R_ModernGLExecutor_ApplyCommandCullState( const modernGLSubmitCommand_t &command ) { @@ -2905,9 +3127,16 @@ static const materialResourceTableRecord_t *R_ModernGLExecutor_MaterialRecordFor return command.materialRecord != NULL ? command.materialRecord : R_MaterialResourceTable_RecordForIndex( command.materialTableIndex ); } +static bool R_ModernGLExecutor_CommandUsesPBR( const modernGLSubmitCommand_t &command ) { + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + return materialRecord != NULL && R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ); +} + +static bool R_ModernGLExecutor_IsExactSingleStageClusteredDecal( const drawPacket_t &draw ); static const materialResourceTextureBinding_t *R_ModernGLExecutor_FindTextureBinding( const materialResourceTableRecord_t &record, materialResourceTextureSemantic_t semantic ); static float R_ModernGLExecutor_ShaderRegisterValue( const modernGLSubmitCommand_t &command, int registerIndex, float fallbackValue ); static bool R_ModernGLExecutor_CommandMaterialColor( const modernGLSubmitCommand_t &command, float color[4] ); +static void R_ModernGLExecutor_PBREmissiveColorForCommand( const modernGLSubmitCommand_t &command, float color[3] ); static void R_ModernGLExecutor_MaterialFlagsForCommand( const modernGLSubmitCommand_t &command, float flags[4] ); static void R_ModernGLExecutor_MaterialEnhancementForCommand( const modernGLSubmitCommand_t &command, float enhancement[4] ); static void R_ModernGLExecutor_BindTextureGroup( GLuint first, GLsizei count, const GLuint *textures, modernGLExecutorStats_t &stats ); @@ -2915,6 +3144,18 @@ static bool R_ModernGLExecutor_CommandUsesShadowTextures( const modernGLSubmitCo static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernGLExecutorStats_t &stats ); static void R_ModernGLExecutor_DebugColorForCommand( const modernGLSubmitCommand_t &command, float color[4] ) { + const drawPacket_t *draw = command.drawPlanEntry != NULL ? command.drawPlanEntry->drawPacket : NULL; + if ( command.forwardPlusDecal && draw != NULL + && R_ModernGLExecutor_IsExactSingleStageClusteredDecal( *draw ) ) { + // Projected decals already bake the stage register color and depth fade + // into idDrawVert::color. The transparent shader consumes that stream; + // keep its uniform tint neutral so the authored color is applied once. + color[0] = 1.0f; + color[1] = 1.0f; + color[2] = 1.0f; + color[3] = 1.0f; + return; + } float materialColor[4]; switch ( command.shaderKind ) { case MODERN_GL_SHADER_DEPTH: @@ -2944,6 +3185,11 @@ static void R_ModernGLExecutor_DebugColorForCommand( const modernGLSubmitCommand break; case MODERN_GL_SHADER_GBUFFER_OPAQUE: case MODERN_GL_SHADER_GBUFFER_ALPHA_TEST: + if ( R_ModernGLExecutor_CommandUsesPBR( command ) ) { + R_ModernGLExecutor_PBREmissiveColorForCommand( command, color ); + color[3] = static_cast( r_pbrDebug.GetInteger() ); + break; + } if ( R_ModernGLExecutor_CommandMaterialColor( command, materialColor ) ) { memcpy( color, materialColor, sizeof( float ) * 4 ); } else { @@ -2961,6 +3207,11 @@ static void R_ModernGLExecutor_DebugColorForCommand( const modernGLSubmitCommand break; case MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE: case MODERN_GL_SHADER_CLUSTERED_FORWARD_ALPHA_TEST: + if ( R_ModernGLExecutor_CommandUsesPBR( command ) ) { + R_ModernGLExecutor_PBREmissiveColorForCommand( command, color ); + color[3] = static_cast( r_pbrDebug.GetInteger() ); + break; + } if ( R_ModernGLExecutor_CommandMaterialColor( command, materialColor ) ) { memcpy( color, materialColor, sizeof( float ) * 4 ); } else { @@ -2971,6 +3222,11 @@ static void R_ModernGLExecutor_DebugColorForCommand( const modernGLSubmitCommand } break; case MODERN_GL_SHADER_TRANSPARENT_FORWARD: + if ( R_ModernGLExecutor_CommandUsesPBR( command ) ) { + R_ModernGLExecutor_PBREmissiveColorForCommand( command, color ); + color[3] = static_cast( r_pbrDebug.GetInteger() ); + break; + } if ( R_ModernGLExecutor_CommandMaterialColor( command, materialColor ) ) { memcpy( color, materialColor, sizeof( float ) * 4 ); } else { @@ -3012,6 +3268,9 @@ static void R_ModernGLExecutor_SetDebugColor( const modernGLSubmitCommand_t &com } float color[4]; R_ModernGLExecutor_DebugColorForCommand( command, color ); + if ( !R_ModernGLExecutor_SubmitUniform4fChanged( rg_modernGLSubmitUniformMemo.debugColorValid, rg_modernGLSubmitUniformMemo.debugColor, color ) ) { + return; + } glUniform4f( command.debugColorLocation, color[0], color[1], color[2], color[3] ); } @@ -3053,6 +3312,19 @@ static void R_ModernGLExecutor_LocalParamsForCommand( const modernGLSubmitComman params[1] = 0.0f; params[2] = 0.0f; params[3] = 0.0f; + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + if ( materialRecord != NULL && R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) + && ( command.shaderKind == MODERN_GL_SHADER_GBUFFER_OPAQUE + || command.shaderKind == MODERN_GL_SHADER_GBUFFER_ALPHA_TEST + || command.shaderKind == MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE + || command.shaderKind == MODERN_GL_SHADER_CLUSTERED_FORWARD_ALPHA_TEST + || command.shaderKind == MODERN_GL_SHADER_TRANSPARENT_FORWARD ) ) { + params[0] = idMath::ClampFloat( 0.0f, 1.0f, R_ModernGLExecutor_ShaderRegisterValue( command, materialRecord->pbrMetallicRegister, 0.0f ) ); + params[1] = idMath::ClampFloat( 0.02f, 1.0f, R_ModernGLExecutor_ShaderRegisterValue( command, materialRecord->pbrRoughnessRegister, 0.5f ) ); + params[2] = idMath::ClampFloat( 0.0f, 1.0f, R_ModernGLExecutor_ShaderRegisterValue( command, materialRecord->pbrAORegister, 1.0f ) ); + params[3] = Max( 0.0f, R_ModernGLExecutor_ShaderRegisterValue( command, materialRecord->pbrNormalScaleRegister, 1.0f ) ); + return; + } switch ( command.shaderKind ) { case MODERN_GL_SHADER_DEPTH: case MODERN_GL_SHADER_SHADOW_DEPTH: { @@ -3074,6 +3346,9 @@ static void R_ModernGLExecutor_LocalParamsForCommand( const modernGLSubmitComman case MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE: case MODERN_GL_SHADER_LIGHT_GRID: params[0] = 1.0f; + if ( command.shaderKind == MODERN_GL_SHADER_DEFERRED_LIGHT_RESOLVE ) { + params[3] = static_cast( r_pbrDebug.GetInteger() ); + } break; case MODERN_GL_SHADER_TRANSPARENT_FORWARD: case MODERN_GL_SHADER_FOG_BLEND: @@ -3099,9 +3374,30 @@ static void R_ModernGLExecutor_SetLocalParams( const modernGLSubmitCommand_t &co } float params[4]; R_ModernGLExecutor_LocalParamsForCommand( command, params ); + if ( !R_ModernGLExecutor_SubmitUniform4fChanged( rg_modernGLSubmitUniformMemo.localParamsValid, rg_modernGLSubmitUniformMemo.localParams, params ) ) { + return; + } glUniform4f( command.localParamsLocation, params[0], params[1], params[2], params[3] ); } +static void R_ModernGLExecutor_SetPBRIBL( int location, bool useMemo = false ) { + if ( location < 0 ) { + return; + } + // The analytic environment is deliberately a renderer-global lighting term. + // It is consumed only by the PBR branches in the deferred/forward shaders, + // so stock materials and PBR-disabled runs retain their existing output. + const float value[4] = { + r_pbrIBL.GetBool() ? 1.0f : 0.0f, + idMath::ClampFloat( 0.0f, 4.0f, r_pbrIBLIntensity.GetFloat() ), + 0.0f, + 0.0f }; + if ( useMemo && !R_ModernGLExecutor_SubmitUniform4fChanged( rg_modernGLSubmitUniformMemo.pbrIBLValid, rg_modernGLSubmitUniformMemo.pbrIBL, value ) ) { + return; + } + glUniform4f( location, value[0], value[1], value[2], value[3] ); +} + static bool R_ModernGLExecutor_IsDepthPipeline( modernGLDrawPlanPipeline_t pipeline ) { return pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_DEPTH || pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_SHADOW_DEPTH; } @@ -3619,11 +3915,14 @@ static void R_ModernGLExecutor_UpdateGpuDrivenBuffers( modernGLExecutorStats_t & static modernGLDrawRecord_t drawRecords[MODERN_GL_GPU_DRIVEN_MAX_RECORDS]; static GLfloat drawRecordIndices[MODERN_GL_GPU_DRIVEN_MAX_RECORDS]; static modernGLGpuDrivenBucketRecord_t bucketRecords[MODERN_GL_GPU_DRIVEN_MAX_BUCKETS]; - memset( sceneRecords, 0, sizeof( sceneRecords ) ); - memset( indirectRecords, 0, sizeof( indirectRecords ) ); - memset( drawRecords, 0, sizeof( drawRecords ) ); - memset( drawRecordIndices, 0, sizeof( drawRecordIndices ) ); - memset( bucketRecords, 0, sizeof( bucketRecords ) ); + // Only the used prefix of each array is uploaded (the byte sizes below are + // count-based), so full-capacity clears are dead work: every uploaded + // sceneRecords/drawRecords/drawRecordIndices entry is fully rewritten per + // command below (BuildDrawRecord zeroes each draw record itself), the + // indirectRecords prefix is cleared once its count is known (the compute + // shader is the only writer of live entries), and the bucketRecords prefix + // is cleared right before the header fill so its GPU-written counters + // upload as zeros. const renderGraphResourceHandle_t *sceneHiZ = NULL; const bool useHiZForIndirect = @@ -3643,10 +3942,19 @@ static void R_ModernGLExecutor_UpdateGpuDrivenBuffers( modernGLExecutorStats_t & cpuReference.clusterBins = clusteredStats.frameValid ? clusteredStats.activeClusters : 0; const int sceneRecordCount = Min( rg_modernGLSubmitPlan.NumCommands(), MODERN_GL_GPU_DRIVEN_MAX_RECORDS ); + if ( sceneRecordCount == 0 ) { + // drawRecordBytes/drawRecordIndexBytes below upload Max( 1, ... ) entries; + // keep the guard entry zeroed when no command writes it + memset( &drawRecords[0], 0, sizeof( drawRecords[0] ) ); + drawRecordIndices[0] = 0.0f; + } int indirectRecordCount = 0; for ( int i = 0; i < sceneRecordCount; ++i ) { const modernGLSubmitCommand_t &command = rg_modernGLSubmitPlan.Command( i ); - modernGLSubmitCommand_t indirectCommand = command; + // the effective command only diverges from the plan command when a client + // index upload succeeds; defer the ~450 byte struct copy to that rare path + modernGLSubmitCommand_t patchedCommand; + const modernGLSubmitCommand_t *effectiveCommand = &command; modernGLGpuSceneRecord_t &record = sceneRecords[i]; R_ModernGLExecutor_BuildDrawRecord( command, drawRecords[i] ); drawRecordIndices[i] = static_cast( i ); @@ -3658,15 +3966,18 @@ static void R_ModernGLExecutor_UpdateGpuDrivenBuffers( modernGLExecutorStats_t & if ( command.clientIndexData != NULL && command.clientIndexBytes > 0 ) { rendererUploadAllocation_t indexUpload; if ( R_RendererUpload_AllocFrameTemp( const_cast( command.clientIndexData ), command.clientIndexBytes, 4, indexUpload ) ) { - indirectCommand.indexBuffer = indexUpload.vbo; - indirectCommand.indexCacheOffset = indexUpload.offset; - indirectCommand.uploadIndexBuffer = false; - indirectCommand.clientIndexData = NULL; - indirectCommand.clientIndexBytes = 0; + patchedCommand = command; + patchedCommand.indexBuffer = indexUpload.vbo; + patchedCommand.indexCacheOffset = indexUpload.offset; + patchedCommand.uploadIndexBuffer = false; + patchedCommand.clientIndexData = NULL; + patchedCommand.clientIndexBytes = 0; + effectiveCommand = &patchedCommand; uploadReady = true; } } } + const modernGLSubmitCommand_t &indirectCommand = *effectiveCommand; const bool canSeedIndirect = visible && uploadReady && R_ModernGLExecutor_CommandCanSeedIndirect( indirectCommand ); int bucketIndex = -1; if ( canSeedIndirect ) { @@ -3741,6 +4052,9 @@ static void R_ModernGLExecutor_UpdateGpuDrivenBuffers( modernGLExecutorStats_t & record.indirect[3] = static_cast( i ); } + // zero the uploaded bucket prefix: header[] is rewritten below, but the + // counters[] halves must reach the GPU as zeros for the compute pass + memset( bucketRecords, 0, Max( 1, rg_modernGLGpuDrivenBucketCount ) * sizeof( bucketRecords[0] ) ); for ( int i = 0; i < rg_modernGLGpuDrivenBucketCount; ++i ) { const modernGLGpuDrivenBucket_t &bucket = rg_modernGLGpuDrivenBuckets[i]; bucketRecords[i].header[0] = static_cast( bucket.firstIndirect ); @@ -3749,6 +4063,10 @@ static void R_ModernGLExecutor_UpdateGpuDrivenBuffers( modernGLExecutorStats_t & bucketRecords[i].header[3] = 0u; } + // live indirect commands are written by the compute shader, never the CPU; + // only the uploaded prefix needs to be zeroed + memset( indirectRecords, 0, Max( 1, indirectRecordCount ) * sizeof( indirectRecords[0] ) ); + GLuint validationCounters[MODERN_GL_GPU_DRIVEN_VALIDATION_COUNTERS] = { 0 }; const GLsizeiptr sceneBytes = static_cast( sceneRecordCount * sizeof( modernGLGpuSceneRecord_t ) ); const GLsizeiptr indirectBytes = static_cast( Max( 1, indirectRecordCount ) * sizeof( modernGLDrawElementsIndirectCommand_t ) ); @@ -3927,12 +4245,22 @@ static bool R_ModernGLExecutor_CommandMaterialColor( const modernGLSubmitCommand static GLuint R_ModernGLExecutor_TextureForCommandSemantic( const modernGLSubmitCommand_t &command, materialResourceTextureSemantic_t semantic ) { switch ( semantic ) { case MATERIAL_RESOURCE_TEXTURE_BUMP: + case MATERIAL_RESOURCE_TEXTURE_NORMAL: return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_NORMAL] ); case MATERIAL_RESOURCE_TEXTURE_SPECULAR: + case MATERIAL_RESOURCE_TEXTURE_ORM: return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_SPECULAR] ); case MATERIAL_RESOURCE_TEXTURE_EMISSIVE: + case MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR: return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_EMISSIVE] ); + case MATERIAL_RESOURCE_TEXTURE_METALLIC: + return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_METALLIC] ); + case MATERIAL_RESOURCE_TEXTURE_ROUGHNESS: + return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_ROUGHNESS] ); + case MATERIAL_RESOURCE_TEXTURE_AO: + return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_AO] ); case MATERIAL_RESOURCE_TEXTURE_DIFFUSE: + case MATERIAL_RESOURCE_TEXTURE_ALBEDO: case MATERIAL_RESOURCE_TEXTURE_GUI: case MATERIAL_RESOURCE_TEXTURE_POST_PROCESS: return static_cast( command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_MAIN] ); @@ -3968,7 +4296,7 @@ static int R_ModernGLExecutor_MainTextureTableIndexForCommand( const modernGLSub return command.materialTextureTableIndices[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_MAIN]; } -static bool R_ModernGLExecutor_TextureTableIndicesForCommand( const modernGLSubmitCommand_t &command, GLuint indices[MODERN_GL_MATERIAL_TEXTURE_COUNT] ) { +static bool R_ModernGLExecutor_TextureTableIndicesForCommand( const modernGLSubmitCommand_t &command, GLuint indices[MODERN_GL_MATERIAL_TEXTURE_TABLE_COUNT] ) { const materialResourceTableStats_t &materialStats = R_MaterialResourceTable_Stats(); if ( !materialStats.textureArrayTableReady || command.textureIndicesLocation < 0 || command.textureTableModeLocation < 0 || glUniform1ui == NULL || glUniform4ui == NULL ) { return false; @@ -3977,7 +4305,7 @@ static bool R_ModernGLExecutor_TextureTableIndicesForCommand( const modernGLSubm indices[MODERN_GL_MATERIAL_TEXTURE_NORMAL] = static_cast( R_ModernGLExecutor_TextureTableIndexForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_BUMP ) ); indices[MODERN_GL_MATERIAL_TEXTURE_SPECULAR] = static_cast( R_ModernGLExecutor_TextureTableIndexForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_SPECULAR ) ); indices[MODERN_GL_MATERIAL_TEXTURE_EMISSIVE] = static_cast( R_ModernGLExecutor_TextureTableIndexForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_EMISSIVE ) ); - for ( int i = 0; i < MODERN_GL_MATERIAL_TEXTURE_COUNT; ++i ) { + for ( int i = 0; i < MODERN_GL_MATERIAL_TEXTURE_TABLE_COUNT; ++i ) { if ( indices[i] == static_cast( -1 ) || static_cast( indices[i] ) < 0 || static_cast( indices[i] ) >= materialStats.textureArrayTableTextures ) { return false; } @@ -4031,7 +4359,31 @@ static float R_ModernGLExecutor_AlphaReferenceForCommand( const modernGLSubmitCo return idMath::ClampFloat( 0.0f, 1.0f, R_ModernGLExecutor_ShaderRegisterValue( command, command.materialAlphaTestRegister, 0.5f ) ); } +static void R_ModernGLExecutor_PBREmissiveColorForCommand( const modernGLSubmitCommand_t &command, float color[3] ) { + color[0] = 0.0f; + color[1] = 0.0f; + color[2] = 0.0f; + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + if ( materialRecord == NULL || !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) { + return; + } + for ( int i = 0; i < 3; ++i ) { + color[i] = Max( 0.0f, R_ModernGLExecutor_ShaderRegisterValue( command, materialRecord->pbrEmissiveColorRegisters[i], 0.0f ) ); + } +} + static void R_ModernGLExecutor_MaterialFlagsForCommand( const modernGLSubmitCommand_t &command, float flags[4] ) { + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + if ( materialRecord != NULL && R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) { + flags[0] = materialRecord->hasPBRNormal ? 1.0f : 0.0f; + flags[1] = materialRecord->hasPBRORM ? 1.0f : ( materialRecord->pbrSeparateMaterialData ? -1.0f : 0.0f ); + flags[2] = materialRecord->hasPBREmissive ? 1.0f : 0.0f; + // Keep a disjoint PBR marker while retaining the normal encoding in the + // same per-draw uniform. The extra offset also preserves the optional, + // unspecified normal format without colliding with classic materials. + flags[3] = 8.0f + static_cast( materialRecord->pbrNormalFormat + 1 ); + return; + } flags[0] = R_ModernGLExecutor_CommandHasTextureSemantic( command, MATERIAL_RESOURCE_TEXTURE_BUMP ) ? 1.0f : 0.0f; flags[1] = R_ModernGLExecutor_CommandHasTextureSemantic( command, MATERIAL_RESOURCE_TEXTURE_SPECULAR ) ? 1.0f : 0.0f; flags[2] = R_ModernGLExecutor_CommandHasTextureSemantic( command, MATERIAL_RESOURCE_TEXTURE_EMISSIVE ) ? 1.0f : 0.0f; @@ -4043,7 +4395,18 @@ static bool R_ModernGLExecutor_EnhancedMaterialShadingActive( void ) { } static void R_ModernGLExecutor_MaterialEnhancementForCommand( const modernGLSubmitCommand_t &command, float enhancement[4] ) { - (void)command; + if ( R_ModernGLExecutor_CommandUsesPBR( command ) ) { + // The PBR normal scale lives in uLocalParams.w. Keep the transparent + // decal bit separate so a non-decal source-alpha surface cannot be + // mistaken for a decal merely because its normal scale is non-zero. + enhancement[0] = command.forwardPlusDecal ? 1.0f : 0.0f; + enhancement[1] = 1.0f; + enhancement[2] = 1.0f; + // PBR owns the scalar slots in uLocalParams, so retain an authored + // alpha-test threshold in the otherwise PBR-neutral enhancement slot. + enhancement[3] = R_ModernGLExecutor_AlphaReferenceForCommand( command ); + return; + } const bool active = R_ModernGLExecutor_EnhancedMaterialShadingActive(); enhancement[0] = active ? 1.0f : 0.0f; enhancement[1] = active ? idMath::ClampFloat( 0.5f, 2.0f, r_enhancedMaterialNormalScale.GetFloat() ) : 1.0f; @@ -4057,6 +4420,9 @@ static void R_ModernGLExecutor_SetMaterialFlags( const modernGLSubmitCommand_t & } float flags[4]; R_ModernGLExecutor_MaterialFlagsForCommand( command, flags ); + if ( !R_ModernGLExecutor_SubmitUniform4fChanged( rg_modernGLSubmitUniformMemo.materialFlagsValid, rg_modernGLSubmitUniformMemo.materialFlags, flags ) ) { + return; + } glUniform4f( command.materialFlagsLocation, flags[0], flags[1], flags[2], flags[3] ); } @@ -4066,6 +4432,9 @@ static void R_ModernGLExecutor_SetMaterialEnhancement( const modernGLSubmitComma } float enhancement[4]; R_ModernGLExecutor_MaterialEnhancementForCommand( command, enhancement ); + if ( !R_ModernGLExecutor_SubmitUniform4fChanged( rg_modernGLSubmitUniformMemo.materialEnhancementValid, rg_modernGLSubmitUniformMemo.materialEnhancement, enhancement ) ) { + return; + } glUniform4f( command.materialEnhancementLocation, enhancement[0], enhancement[1], enhancement[2], enhancement[3] ); } @@ -4076,12 +4445,32 @@ static bool R_ModernGLExecutor_CommandUsesShadowTextures( const modernGLSubmitCo || command.shaderKind == MODERN_GL_SHADER_TRANSPARENT_FORWARD; } -static void R_ModernGLExecutor_BindMaterialTextures( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats ) { - GLuint textureIndices[MODERN_GL_MATERIAL_TEXTURE_COUNT]; +static void R_ModernGLExecutor_BindMaterialTextures( const modernGLSubmitCommand_t &command, + modernGLExecutorStats_t &stats, bool forceClassicTextureObjectSampling = false ) { + GLuint textureIndices[MODERN_GL_MATERIAL_TEXTURE_TABLE_COUNT]; + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + if ( forceClassicTextureObjectSampling ) { + // Inline clustered decals replace an exact classic stage. The shared + // low-overhead sampler is intentionally linear/clamp and would override + // the source image's authored filter/repeat state, so use the image's + // texture-object parameters and explicitly remove any prior sampler. + R_ModernGLExecutor_SetTextureTableMode( command, false ); + const GLuint textureHandle = R_ModernGLExecutor_TextureForCommand( command ); + R_GLStateCache().BindTexture( + MODERN_GL_MATERIAL_TEXTURE_MAIN, GL_TEXTURE_2D, textureHandle ); + if ( glBindSampler != NULL ) { + R_GLStateCache().BindSampler( MODERN_GL_MATERIAL_TEXTURE_MAIN, 0 ); + } + R_GLStateCache().ActiveTextureUnit( MODERN_GL_MATERIAL_TEXTURE_MAIN ); + return; + } + const bool pbrSeparateMaterialData = materialRecord != NULL + && R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) + && materialRecord->pbrSeparateMaterialData; const bool reserveShadowTextureUnits = R_ModernClusteredLighting_NumShadowDescriptors() > 0 && R_ModernGLExecutor_CommandUsesShadowTextures( command ); - if ( !reserveShadowTextureUnits && R_ModernGLExecutor_TextureTableIndicesForCommand( command, textureIndices ) && R_ModernGLExecutor_BindMaterialTextureTable( stats ) ) { + if ( !pbrSeparateMaterialData && !reserveShadowTextureUnits && R_ModernGLExecutor_TextureTableIndicesForCommand( command, textureIndices ) && R_ModernGLExecutor_BindMaterialTextureTable( stats ) ) { R_ModernGLExecutor_SetTextureTableMode( command, true ); glUniform4ui( command.textureIndicesLocation, @@ -4125,6 +4514,24 @@ static void R_ModernGLExecutor_BindMaterialTextures( const modernGLSubmitCommand stats.lowOverheadClassicTextureBinds++; } } + if ( command.metallicTextureLocation >= 0 ) { + const GLuint textureHandle = R_ModernGLExecutor_TextureForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_METALLIC ); + if ( R_GLStateCache().BindTexture( MODERN_GL_MATERIAL_TEXTURE_METALLIC, GL_TEXTURE_2D, textureHandle ) ) { + stats.lowOverheadClassicTextureBinds++; + } + } + if ( command.roughnessTextureLocation >= 0 ) { + const GLuint textureHandle = R_ModernGLExecutor_TextureForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_ROUGHNESS ); + if ( R_GLStateCache().BindTexture( MODERN_GL_MATERIAL_TEXTURE_ROUGHNESS, GL_TEXTURE_2D, textureHandle ) ) { + stats.lowOverheadClassicTextureBinds++; + } + } + if ( command.aoTextureLocation >= 0 ) { + const GLuint textureHandle = R_ModernGLExecutor_TextureForCommandSemantic( command, MATERIAL_RESOURCE_TEXTURE_AO ); + if ( R_GLStateCache().BindTexture( MODERN_GL_MATERIAL_TEXTURE_AO, GL_TEXTURE_2D, textureHandle ) ) { + stats.lowOverheadClassicTextureBinds++; + } + } R_GLStateCache().ActiveTextureUnit( 0 ); } @@ -4170,6 +4577,16 @@ static void R_ModernGLExecutor_ResetClusterBlockBindingCache( void ) { rg_modernGLClusterBlockBoundProgramCount = 0; } +// same lifetime rules as the cluster-block memo above: uniform locations and +// sampler-unit assignments die with the shader-library generation, and relink +// resets sampler uniforms to zero, so both reset sites must clear this too +static void R_ModernGLExecutor_ResetShadowUniformCache( void ) { + rg_modernGLShadowUniformProgramCount = 0; + rg_modernGLShadowSnapshotRevision = -1; + rg_modernGLShadowSnapshotFrameCount = -1; + rg_modernGLShadowBindingsRevision++; +} + static void R_ModernGLExecutor_BindClusterUniformBlocks( GLuint program ) { for ( int i = 0; i < rg_modernGLClusterBlockBoundProgramCount; ++i ) { if ( rg_modernGLClusterBlockBoundPrograms[i] == program ) { @@ -4180,12 +4597,15 @@ static void R_ModernGLExecutor_BindClusterUniformBlocks( GLuint program ) { R_ModernGLExecutor_SetUniformBlockBinding( program, "ModernClusterLightRecords", MODERN_GL_CLUSTER_UBO_BINDING_LIGHTS ); R_ModernGLExecutor_SetUniformBlockBinding( program, "ModernClusterIndexRecords", MODERN_GL_CLUSTER_UBO_BINDING_INDICES ); R_ModernGLExecutor_SetUniformBlockBinding( program, "ModernClusterShadowDescriptors", MODERN_GL_CLUSTER_UBO_BINDING_SHADOW_DESCRIPTORS ); + R_ModernGLExecutor_SetUniformBlockBinding( program, "ModernSpecularProbeRecords", MODERN_GL_CLUSTER_UBO_BINDING_SPECULAR_PROBES ); if ( rg_modernGLClusterBlockBoundProgramCount < MODERN_GL_CLUSTER_BLOCK_BOUND_PROGRAM_CAPACITY ) { rg_modernGLClusterBlockBoundPrograms[rg_modernGLClusterBlockBoundProgramCount++] = program; } } -static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats, bool recordSubmitStats ) { +static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &command, + modernGLExecutorStats_t &stats, bool recordSubmitStats, + const drawSurf_t *clusteredDecalSurface = NULL ) { if ( command.drawPlanEntry == NULL || command.viewDef == NULL ) { R_ModernGLExecutor_CountSubmittedFallback( stats, recordSubmitStats ); return false; @@ -4218,11 +4638,24 @@ static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &com } R_GLStateCache().UseProgram( command.program ); - if ( command.drawRecordModeLocation >= 0 && glUniform1ui != NULL ) { - glUniform1ui( command.drawRecordModeLocation, 0 ); + if ( !rg_modernGLSubmitUniformMemo.valid || rg_modernGLSubmitUniformMemo.program != command.program ) { + rg_modernGLSubmitUniformMemo.valid = true; + rg_modernGLSubmitUniformMemo.program = command.program; + rg_modernGLSubmitUniformMemo.drawRecordZeroed = false; + rg_modernGLSubmitUniformMemo.debugColorValid = false; + rg_modernGLSubmitUniformMemo.localParamsValid = false; + rg_modernGLSubmitUniformMemo.pbrIBLValid = false; + rg_modernGLSubmitUniformMemo.materialFlagsValid = false; + rg_modernGLSubmitUniformMemo.materialEnhancementValid = false; } - if ( command.drawRecordCountLocation >= 0 && glUniform1ui != NULL ) { - glUniform1ui( command.drawRecordCountLocation, 0 ); + if ( !rg_modernGLSubmitUniformMemo.drawRecordZeroed ) { + if ( command.drawRecordModeLocation >= 0 && glUniform1ui != NULL ) { + glUniform1ui( command.drawRecordModeLocation, 0 ); + } + if ( command.drawRecordCountLocation >= 0 && glUniform1ui != NULL ) { + glUniform1ui( command.drawRecordCountLocation, 0 ); + } + rg_modernGLSubmitUniformMemo.drawRecordZeroed = true; } R_ModernGLExecutor_BindFrameUniformBufferBase( stats ); if ( R_ModernGLExecutor_CommandUsesClusteredLighting( command ) ) { @@ -4234,20 +4667,29 @@ static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &com } R_ModernGLExecutor_SetDebugColor( command ); R_ModernGLExecutor_SetLocalParams( command ); + R_ModernGLExecutor_SetPBRIBL( command.pbrIBLLocation, true ); R_ModernGLExecutor_SetMaterialFlags( command ); R_ModernGLExecutor_SetMaterialEnhancement( command ); - R_ModernGLExecutor_BindMaterialTextures( command, stats ); + R_ModernGLExecutor_BindMaterialTextures( + command, stats, clusteredDecalSurface != NULL ); if ( R_ModernGLExecutor_CommandUsesShadowTextures( command ) ) { R_ModernGLExecutor_BindModernShadowTextures( command.program, stats ); } R_ModernGLExecutor_ApplyCommandDepthRange( command ); R_ModernGLExecutor_ApplyCommandCullState( command ); - R_ModernGLExecutor_SetSubmitScissor( command, command.viewDef ); + R_ModernGLExecutor_SetSubmitScissor( + command, command.viewDef, clusteredDecalSurface != NULL ); if ( !R_ModernGLExecutor_BindDrawVertLayout( command, stats ) ) { R_ModernGLExecutor_CountSubmittedFallback( stats, recordSubmitStats ); return false; } + if ( clusteredDecalSurface != NULL + && !R_ModernGLExecutor_BindClusteredDecalColorStream( + command, clusteredDecalSurface ) ) { + R_ModernGLExecutor_CountSubmittedFallback( stats, recordSubmitStats ); + return false; + } if ( command.indexed ) { R_GLStateCache().BindBuffer( GL_ELEMENT_ARRAY_BUFFER, indexBuffer ); @@ -4259,6 +4701,9 @@ static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &com } else { glDrawArrays( GL_TRIANGLES, 0, command.vertexCount ); } + if ( clusteredDecalSurface != NULL ) { + R_ModernGLExecutor_RestoreDrawVertColorStream( command ); + } if ( recordSubmitStats ) { stats.submittedDraws++; @@ -4275,6 +4720,7 @@ static bool R_ModernGLExecutor_SubmitCommand( const modernGLSubmitCommand_t &com } static void R_ModernGLExecutor_SoftRestoreForNextModernPass( modernGLExecutorStats_t &stats ) { + R_ModernGLExecutor_InvalidateSubmitStateMemos(); R_ModernGLExecutor_DisableDrawRecordIndexAttribute(); R_GLStateCache().ActiveTextureUnit( 0 ); R_GLStateCache().SetColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); @@ -4314,6 +4760,7 @@ static void R_ModernGLExecutor_RestoreLegacyStateInvariants( void ) { static void R_ModernGLExecutor_FullRestoreForLegacyHandoff( modernGLExecutorStats_t &stats, const char *reason, bool force ) { (void)reason; + R_ModernGLExecutor_InvalidateSubmitStateMemos(); if ( !force && !rg_modernGLExecutorModernStateDirty ) { return; } @@ -4419,7 +4866,7 @@ static void R_ModernGLExecutor_SubmitGpuDrivenIndirect( modernGLExecutorStats_t R_ModernGLExecutor_BindMaterialTextures( command, stats ); R_ModernGLExecutor_ApplyCommandDepthRange( command ); R_ModernGLExecutor_ApplyCommandCullState( command ); - R_ModernGLExecutor_SetSubmitScissor( command, command.viewDef ); + R_ModernGLExecutor_SetSubmitScissor( command, command.viewDef, false ); if ( !R_ModernGLExecutor_BindDrawVertLayoutForIndirect( command, stats ) ) { stats.gpuDrivenIndirectFallbacks += bucket.commandCount; continue; @@ -4464,7 +4911,10 @@ static void R_ModernGLExecutor_CountVisibleDepthFallback( const modernGLSubmitCo static bool R_ModernGLExecutor_VisibleDepthMaterialSupported( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats ) { const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); - if ( materialRecord == NULL || materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE ) { + if ( materialRecord == NULL + || ( !R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ) + && !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) + || materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE ) { stats.visibleDepthMaterialFallbackDraws++; return false; } @@ -4484,7 +4934,9 @@ static bool R_ModernGLExecutor_VisibleDepthMaterialSupported( const modernGLSubm stats.visibleDepthAlphaTestFallbackDraws++; return false; } - const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( *materialRecord, MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); + const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( + *materialRecord, + R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ? MATERIAL_RESOURCE_TEXTURE_ALBEDO : MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); if ( binding == NULL || binding->textureHandle == 0 ) { stats.visibleDepthAlphaTestFallbackDraws++; return false; @@ -4679,7 +5131,14 @@ static void R_ModernGLExecutor_DetachHiZReductionMip( void ) { } static void R_ModernGLExecutor_RestoreAfterHiZBuild( modernGLExecutorStats_t &stats, const char *reason ) { + R_ModernGLExecutor_InvalidateSubmitStateMemos(); if ( rg_modernGLExecutorSoftPassHandoffs ) { + // Detaching the reduction mip leaves the private Hi-Z FBO incomplete. + // A soft handoff must still publish a valid neutral target for tiers that + // do not execute an intervening indirect pass before diagnostic submit. + R_GLStateCache().BindFramebuffer( GL_FRAMEBUFFER, 0 ); + glReadBuffer( GL_BACK ); + glDrawBuffer( GL_BACK ); R_GLStateCache().ActiveTextureUnit( 0 ); R_GLStateCache().SetViewport( 0, 0, Max( 1, glConfig.vidWidth ), Max( 1, glConfig.vidHeight ) ); R_GLStateCache().SetScissor( 0, 0, Max( 1, glConfig.vidWidth ), Max( 1, glConfig.vidHeight ) ); @@ -5030,7 +5489,10 @@ static bool R_ModernGLExecutor_MaterialContractPromotable( const materialResourc static bool R_ModernGLExecutor_GBufferMaterialSupported( const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats ) { const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); - if ( materialRecord == NULL || materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE ) { + if ( materialRecord == NULL + || ( !R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ) + && !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) + || materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE ) { stats.opaqueGBufferMaterialFallbackDraws++; return false; } @@ -5043,7 +5505,9 @@ static bool R_ModernGLExecutor_GBufferMaterialSupported( const modernGLSubmitCom return false; } if ( materialRecord->alphaTest || materialRecord->materialClass == RENDER_MATERIAL_PERFORATED ) { - const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( *materialRecord, MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); + const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( + *materialRecord, + R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ? MATERIAL_RESOURCE_TEXTURE_ALBEDO : MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); if ( binding == NULL || binding->textureHandle == 0 ) { stats.opaqueGBufferTextureFallbackDraws++; return false; @@ -5213,32 +5677,78 @@ static int R_ModernGLExecutor_ShadowTextureUnitLimit( void ) { } static bool R_ModernGLExecutor_ShadowTextureUnitsReady( void ) { - // the light-image atlas sits on the unit directly above the shadow set, so - // it has to fit under the same limit or its binding would be dropped while - // the shader still declares the sampler - return R_ModernGLExecutor_ShadowTextureUnitLimit() > MODERN_GL_LIGHT_IMAGE_ATLAS_TEXTURE_UNIT; -} - -static bool R_ModernGLExecutor_SetShadowSamplerUniforms( GLuint program ) { - if ( program == 0 || glGetUniformLocation == NULL || glUniform1i == NULL ) { - return false; - } - const GLint projectedAtlas = glGetUniformLocation( program, "uModernShadowAtlas" ); - const GLint pointAtlas = glGetUniformLocation( program, "uModernPointShadowAtlas" ); - const GLint projectedMoments = glGetUniformLocation( program, rg_modernGLShadowProjectedMomentUniform ); - const GLint pointMoments = glGetUniformLocation( program, rg_modernGLShadowPointMomentUniform ); - const GLint resourceState = glGetUniformLocation( program, "uModernShadowResourceState" ); - const GLint samplerState = glGetUniformLocation( program, "uModernShadowSamplerState" ); - const GLint momentState = glGetUniformLocation( program, "uModernShadowMomentState" ); + // The light-image and authored specular-probe atlases sit directly above + // the shadow set. Both must fit because clustered PBR variants declare both + // samplers even when a particular frame falls back to analytic indirect. + return R_ModernGLExecutor_ShadowTextureUnitLimit() > MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT; +} + +// resolves (and memoizes) the shadow-set uniform locations for a program; the +// shader library resolves the same names at link time but discards them, so +// re-querying by name per draw was redundant driver traffic. Over-capacity +// programs fall open to a per-call scratch resolve, matching the pre-memo cost. +static modernGLShadowUniformLocations_t *R_ModernGLExecutor_ShadowUniformLocations( GLuint program ) { + if ( program == 0 || glGetUniformLocation == NULL ) { + return NULL; + } + for ( int i = 0; i < rg_modernGLShadowUniformProgramCount; ++i ) { + if ( rg_modernGLShadowUniformPrograms[i].program == program ) { + return &rg_modernGLShadowUniformPrograms[i]; + } + } + static modernGLShadowUniformLocations_t scratch; + modernGLShadowUniformLocations_t *loc = &scratch; + if ( rg_modernGLShadowUniformProgramCount < MODERN_GL_SHADOW_UNIFORM_PROGRAM_CAPACITY ) { + loc = &rg_modernGLShadowUniformPrograms[rg_modernGLShadowUniformProgramCount++]; + } + loc->program = program; + loc->projectedAtlas = glGetUniformLocation( program, "uModernShadowAtlas" ); + loc->pointAtlas = glGetUniformLocation( program, "uModernPointShadowAtlas" ); + loc->projectedMoments = glGetUniformLocation( program, rg_modernGLShadowProjectedMomentUniform ); + loc->pointMoments = glGetUniformLocation( program, rg_modernGLShadowPointMomentUniform ); + loc->resourceState = glGetUniformLocation( program, "uModernShadowResourceState" ); + loc->samplerState = glGetUniformLocation( program, "uModernShadowSamplerState" ); + loc->momentState = glGetUniformLocation( program, "uModernShadowMomentState" ); + loc->contractState = glGetUniformLocation( program, "uModernShadowContractState" ); + loc->lightImageAtlas = glGetUniformLocation( program, "uModernLightImageAtlas" ); + loc->specularProbeAtlas = glGetUniformLocation( program, "uModernSpecularProbeAtlas" ); + loc->samplerUnitsAssigned = false; + loc->samplerReady = loc->projectedAtlas >= 0 && loc->pointAtlas >= 0 + && loc->projectedMoments >= 0 && loc->pointMoments >= 0 + && loc->resourceState >= 0 && loc->samplerState >= 0 && loc->momentState >= 0; + loc->stateRevision = -1; + loc->stateFrameCount = -1; + return loc; +} + +static bool R_ModernGLExecutor_SetShadowSamplerUniforms( GLuint program, modernGLShadowUniformLocations_t *loc ) { + if ( program == 0 || glGetUniformLocation == NULL || glUniform1i == NULL || loc == NULL ) { + return false; + } + // sampler uniforms are persistent program-object state carrying + // compile-time-constant unit numbers, so assign them once per program; the + // memo is cleared on relink and shutdown, which are the only events that + // reset them + if ( loc->samplerUnitsAssigned ) { + return loc->samplerReady; + } + const GLint projectedAtlas = loc->projectedAtlas; + const GLint pointAtlas = loc->pointAtlas; + const GLint projectedMoments = loc->projectedMoments; + const GLint pointMoments = loc->pointMoments; // The light-image atlas sampler is independent of the shadow set and must be // assigned before the shadow bail-out below. A program that declares it but // is missing any shadow uniform would otherwise keep the default unit 0 and // sample whatever happens to be bound there, which the driver reports as // "State(s) are invalid: program texture usage". - const GLint lightImageAtlas = glGetUniformLocation( program, "uModernLightImageAtlas" ); + const GLint lightImageAtlas = loc->lightImageAtlas; if ( lightImageAtlas >= 0 ) { glUniform1i( lightImageAtlas, MODERN_GL_LIGHT_IMAGE_ATLAS_TEXTURE_UNIT ); } + const GLint specularProbeAtlas = loc->specularProbeAtlas; + if ( specularProbeAtlas >= 0 ) { + glUniform1i( specularProbeAtlas, MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT ); + } // Same rule as the light-image atlas above, for the shadow set itself: a // sampler this program actually declares has to be pointed at its own unit // whether or not the rest of the set survived compilation. GLSL drops @@ -5270,10 +5780,8 @@ static bool R_ModernGLExecutor_SetShadowSamplerUniforms( GLuint program ) { glUniform1iv( pointMoments, RENDERER_SHADOW_TEXTURE_MOMENT_COUNT, pointMomentUnits ); } } - if ( projectedAtlas < 0 || pointAtlas < 0 || projectedMoments < 0 || pointMoments < 0 || resourceState < 0 || samplerState < 0 || momentState < 0 ) { - return false; - } - return true; + loc->samplerUnitsAssigned = true; + return loc->samplerReady; } static int R_ModernGLExecutor_CountActualShadowTextures( const rendererShadowTextureBindings_t &bindings ) { @@ -5324,7 +5832,6 @@ static void R_ModernGLExecutor_BindShadowTextureSlot( const rendererShadowTextur // above avoids. Substitute a complete texture of the matching target; // uModernShadowResourceState still tells the shader not to read it. const GLuint texture = boundTexture != 0 ? boundTexture : R_ModernGLExecutor_ShadowSlotPlaceholderTexture( target ); - R_GLStateCache().ActiveTextureUnit( unit ); if ( R_GLStateCache().BindTexture( unit, target, texture ) ) { stats.lowOverheadClassicTextureBinds++; } @@ -5334,8 +5841,54 @@ static void R_ModernGLExecutor_BindShadowTextureSlot( const rendererShadowTextur // only reset compare mode on a real shadow texture; the placeholder is a // shared global image and must not have its sampler state rewritten if ( boundTexture != 0 && ( target == GL_TEXTURE_2D || target == GL_TEXTURE_CUBE_MAP ) ) { - glTexParameteri( target, GL_TEXTURE_COMPARE_MODE, GL_NONE ); + // the clear is memoized per (slot texture, bindings revision), never per + // rebind: classic receivers re-enable hardware depth-compare on these + // same texture objects between modern blocks while the state cache still + // records them as bound, and every revision bump re-clears through the + // snapshot memset + const int slot = unit - MODERN_GL_SHADOW_TEXTURE_UNIT_PROJECTED_ATLAS; + const bool alreadyCleared = slot >= 0 && slot < MODERN_GL_SHADOW_TEXTURE_UNIT_COUNT + && rg_modernGLShadowCompareCleared[slot] == boundTexture; + if ( !alreadyCleared ) { + // glTexParameteri targets the active unit's binding and BindTexture + // only switches units on a cache miss, so switch explicitly here + R_GLStateCache().ActiveTextureUnit( unit ); + glTexParameteri( target, GL_TEXTURE_COMPARE_MODE, GL_NONE ); + if ( slot >= 0 && slot < MODERN_GL_SHADOW_TEXTURE_UNIT_COUNT ) { + rg_modernGLShadowCompareCleared[slot] = boundTexture; + } + } + } +} + +// The classic point path owns separate cubes per cached light while modern +// GL exposes one samplerCube. Advertise that binding only when this frame's +// planner has matched a descriptor to the exact selected cache cube. +static bool R_ModernGLExecutor_PointCubeDescriptorReady( + const rendererShadowTextureBindings_t &bindings ) { + if ( !bindings.pointAtlasReady ) { + return false; + } + const int descriptorCount = R_ModernShadowPlanner_NumDescriptors(); + for ( int descriptorIndex = 0; descriptorIndex < descriptorCount; ++descriptorIndex ) { + const modernShadowLightDescriptor_t *descriptor = + R_ModernShadowPlanner_DescriptorByIndex( descriptorIndex ); + if ( descriptor != NULL && descriptor->pointLight + && descriptor->arb2PointCubeReady + && bindings.pointAtlasLightIndex == descriptor->lightDefIndex + && bindings.pointAtlasSignature + == descriptor->arb2PointCubeSignature + && bindings.pointAtlasContentFrame + == descriptor->arb2PointCubeContentFrame + && bindings.pointAtlas.width == descriptor->resolution + && bindings.pointAtlas.height == descriptor->resolution + && descriptor->modernReceiverSamplingReady + && ( descriptor->policy == MODERN_SHADOW_POLICY_MAPPED + || descriptor->policy == MODERN_SHADOW_POLICY_CACHE_REUSE ) ) { + return true; + } } + return false; } static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernGLExecutorStats_t &stats ) { @@ -5347,15 +5900,49 @@ static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernG return false; } - rendererShadowTextureBindings_t bindings; - RB_ShadowMapTextureBindings( bindings ); - // the persistent atlas is the texture the descriptor slot rects index - // into; the per-light alias only stands in when no atlas exists yet - if ( bindings.projectedPersistentAtlasReady ) { - bindings.projectedAtlas = bindings.projectedPersistentAtlas; - bindings.projectedAtlasReady = true; + // the rebuilt bindings (and the descriptor scan behind exactPointCubeReady) + // are constant within one bindings revision of one frame, so rebuild the + // snapshot only when either changes; every downstream read aliases it + rendererShadowTextureBindings_t &bindings = rg_modernGLShadowBindingsSnapshot; + if ( rg_modernGLShadowSnapshotRevision != rg_modernGLShadowBindingsRevision + || rg_modernGLShadowSnapshotFrameCount != tr.frameCount ) { + RB_ShadowMapTextureBindings( bindings ); + const bool exactPointCubeReady = + R_ModernGLExecutor_PointCubeDescriptorReady( bindings ); + if ( !rg_modernGLShadowTextureBindingsCurrent ) { + bindings.projectedAtlas.ready = false; + bindings.projectedPersistentAtlas.ready = false; + bindings.pointAtlas.ready = false; + bindings.projectedAtlasReady = false; + bindings.projectedPersistentAtlasReady = false; + bindings.pointAtlasReady = false; + for ( int i = 0; i < RENDERER_SHADOW_TEXTURE_MOMENT_COUNT; ++i ) { + bindings.projectedMoments[i].ready = false; + bindings.pointMoments[i].ready = false; + } + bindings.projectedMomentsReady = false; + bindings.pointMomentsReady = false; + } + if ( !exactPointCubeReady ) { + bindings.pointAtlas.ready = false; + bindings.pointAtlasReady = false; + for ( int i = 0; i < RENDERER_SHADOW_TEXTURE_MOMENT_COUNT; ++i ) { + bindings.pointMoments[i].ready = false; + } + bindings.pointMomentsReady = false; + } + // the persistent atlas is the texture the descriptor slot rects index + // into; the per-light alias only stands in when no atlas exists yet + if ( bindings.projectedPersistentAtlasReady ) { + bindings.projectedAtlas = bindings.projectedPersistentAtlas; + bindings.projectedAtlasReady = true; + } + rg_modernGLShadowSnapshotRevision = rg_modernGLShadowBindingsRevision; + rg_modernGLShadowSnapshotFrameCount = tr.frameCount; + memset( rg_modernGLShadowCompareCleared, 0, sizeof( rg_modernGLShadowCompareCleared ) ); } - const bool samplerReady = R_ModernGLExecutor_SetShadowSamplerUniforms( program ); + modernGLShadowUniformLocations_t *loc = R_ModernGLExecutor_ShadowUniformLocations( program ); + const bool samplerReady = R_ModernGLExecutor_SetShadowSamplerUniforms( program, loc ); stats.shadowTextureBindingsReady = stats.shadowTextureBindingsReady || samplerReady; stats.shadowTextureProjectedAtlasReady = stats.shadowTextureProjectedAtlasReady || bindings.projectedAtlasReady; stats.shadowTexturePointAtlasReady = stats.shadowTexturePointAtlasReady || bindings.pointAtlasReady; @@ -5364,11 +5951,16 @@ static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernG const int actualTextures = R_ModernGLExecutor_CountActualShadowTextures( bindings ); stats.shadowTextureActualTextures = Max( stats.shadowTextureActualTextures, actualTextures ); - if ( glUniform4f != NULL && program != 0 && glGetUniformLocation != NULL ) { - const GLint resourceState = glGetUniformLocation( program, "uModernShadowResourceState" ); - const GLint samplerState = glGetUniformLocation( program, "uModernShadowSamplerState" ); - const GLint momentState = glGetUniformLocation( program, "uModernShadowMomentState" ); - const GLint contractState = glGetUniformLocation( program, "uModernShadowContractState" ); + // the state vec4s only depend on the snapshot, the frame stamp, and cvars + // that cannot change during backend execution, so re-upload them at most + // once per (program, bindings revision, frame) + if ( glUniform4f != NULL && loc != NULL + && ( loc->stateRevision != rg_modernGLShadowSnapshotRevision + || loc->stateFrameCount != tr.frameCount ) ) { + const GLint resourceState = loc->resourceState; + const GLint samplerState = loc->samplerState; + const GLint momentState = loc->momentState; + const GLint contractState = loc->contractState; if ( contractState >= 0 ) { // y carries the current frame's modular stamp; a bound descriptor // buffer whose freshness.x disagrees was uploaded in another @@ -5403,6 +5995,8 @@ static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernG bindings.translucentMinVariance, bindings.translucentBleedReduction ); } + loc->stateRevision = rg_modernGLShadowSnapshotRevision; + loc->stateFrameCount = tr.frameCount; } R_ModernGLExecutor_BindShadowTextureSlot( bindings.projectedAtlas, GL_TEXTURE_2D, MODERN_GL_SHADOW_TEXTURE_UNIT_PROJECTED_ATLAS, stats ); @@ -5428,6 +6022,24 @@ static bool R_ModernGLExecutor_BindModernShadowTextures( GLuint program, modernG } } + // An incomplete binding would invalidate the whole program even though the + // shader's record checks choose analytic indirect. Bind the real atlas only + // after its whole-frame transaction is ready; otherwise bind a complete 2D + // placeholder whose dimensions deliberately fail the shader's atlas check. + { + const GLuint atlasTexture = R_ModernSpecularProbeAtlas_Ready() + && R_ModernSpecularProbeAtlas_FrameReady() + ? static_cast( R_ModernSpecularProbeAtlas_Texture() ) + : R_ModernGLExecutor_ShadowSlotPlaceholderTexture( GL_TEXTURE_2D ); + if ( atlasTexture != 0 ) { + R_GLStateCache().ActiveTextureUnit( MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT ); + R_GLStateCache().BindTexture( MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT, GL_TEXTURE_2D, atlasTexture ); + if ( glBindSampler != NULL ) { + R_GLStateCache().BindSampler( MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT, 0 ); + } + } + } + R_GLStateCache().ActiveTextureUnit( 0 ); return samplerReady; } @@ -5612,13 +6224,14 @@ static void R_ModernGLExecutor_SubmitDeferredResolve( modernGLExecutorStats_t &s glUniformMatrix4fv( program->modelViewProjectionLocation, 1, GL_FALSE, identity ); } if ( program->debugColorLocation >= 0 ) { - glUniform4f( program->debugColorLocation, 1.0f, 1.0f, 1.0f, 1.0f ); + glUniform4f( program->debugColorLocation, 1.0f, 1.0f, 1.0f, static_cast( r_pbrDebug.GetInteger() ) ); } if ( program->localParamsLocation >= 0 ) { const float overflowPressure = clusterStats.clusterCount > 0 ? idMath::ClampFloat( 0.0f, 1.0f, static_cast( clusterStats.overflowClusters ) / static_cast( clusterStats.clusterCount ) ) : 0.0f; const float fallbackPressure = stats.deferredResolveUnsupportedLightFallbacks > 0 ? 1.0f : 0.0f; glUniform4f( program->localParamsLocation, 1.0f, static_cast( stats.deferredResolveDebugMode ), fallbackPressure, overflowPressure ); } + R_ModernGLExecutor_SetPBRIBL( program->pbrIBLLocation ); for ( int i = 0; i < MODERN_GL_DEFERRED_TEXTURE_COUNT; ++i ) { R_ModernGLExecutor_SetSamplerUniform( program->program, rg_modernGLDeferredTextureUniforms[i], i ); } @@ -5687,6 +6300,11 @@ static bool R_ModernGLExecutor_ForwardPlusMaterialSupported( const modernGLSubmi stats.forwardPlusMaterialFallbackDraws++; return false; } + if ( !R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ) + && !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) { + stats.forwardPlusMaterialFallbackDraws++; + return false; + } const bool decalMaterial = command.forwardPlusDecal; if ( materialRecord->fallbackReason != MATERIAL_RESOURCE_FALLBACK_NONE && ( !decalMaterial || !R_ModernGLExecutor_DecalFallbackAllowedForForwardPlus( *materialRecord, command.drawPlanEntry != NULL ? command.drawPlanEntry->drawPacket : NULL ) ) ) { @@ -5710,7 +6328,9 @@ static bool R_ModernGLExecutor_ForwardPlusMaterialSupported( const modernGLSubmi } if ( command.pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_ALPHA_TEST || command.pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT ) { - const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( *materialRecord, MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); + const materialResourceTextureBinding_t *binding = R_ModernGLExecutor_FindTextureBinding( + *materialRecord, + R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ? MATERIAL_RESOURCE_TEXTURE_ALBEDO : MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); if ( binding == NULL || binding->textureHandle == 0 ) { stats.forwardPlusTextureFallbackDraws++; return false; @@ -5909,6 +6529,16 @@ static void R_ModernGLExecutor_SubmitForwardPlus( modernGLExecutorStats_t &stats if ( transparent ) { const bool decalMaterial = command.forwardPlusDecal; + // A fully transparent PBR frame has no opaque depth producer. In + // that case sceneDepth contains only its clear value, and treating it + // as an occluder can reject every ordered translucent draw on some + // drivers. Ordered source-alpha surfaces are safe to composite without + // depth in this transparent-only case; mixed frames retain the normal + // modern opaque-depth test. + const bool transparentOnlyFrame = !decalMaterial + && stats.visibleDepthDraws == 0 + && stats.opaqueGBufferDraws == 0; + R_GLStateCache().SetDepthTestEnabled( !transparentOnlyFrame ); R_GLStateCache().SetBlendEnabled( true ); if ( command.blendMode == MATERIAL_RESOURCE_BLEND_ADD ) { R_GLStateCache().SetBlendFunc( GL_ONE, GL_ONE ); @@ -5923,6 +6553,7 @@ static void R_ModernGLExecutor_SubmitForwardPlus( modernGLExecutorStats_t &stats } R_GLStateCache().SetDepthMask( GL_FALSE ); } else { + R_GLStateCache().SetDepthTestEnabled( true ); R_GLStateCache().SetBlendEnabled( false ); R_GLStateCache().SetDepthMask( GL_FALSE ); } @@ -5974,137 +6605,680 @@ static void R_ModernGLExecutor_SubmitForwardPlus( modernGLExecutorStats_t &stats } } -static bool R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed( const modernGLExecutorStats_t &stats, const viewDef_t *viewDef ) { - if ( viewDef == NULL || viewDef->viewEntitys == NULL || viewDef->isSubview ) { +static bool R_ModernGLExecutor_IsExactSingleStageClusteredDecalSurface( + const viewDef_t *viewDef, const drawSurf_t *surface ) { + const idMaterial *material = surface != NULL ? surface->material : NULL; + const srfTriangles_t *tri = surface != NULL ? surface->geo : NULL; + if ( viewDef == NULL || viewDef->isSubview || viewDef->viewEntitys == NULL + || surface == NULL || material == NULL || tri == NULL + || tri->numVerts <= 0 || tri->numIndexes <= 0 + || material->GetSort() < SS_DECAL || material->GetSort() >= SS_MEDIUM + || material->IsPortalSky() || material->SuppressInSubview() + || material->Coverage() != MC_TRANSLUCENT + || material->GetNumStages() != 1 + || surface->shaderRegisters == NULL + || surface->decalColorCache == NULL + || surface->decalColorCache != tri->ambientCache + || surface->decalColorOffset != tri->numVerts * static_cast( sizeof( idDrawVert ) ) + || surface->decalColorStride != tri->numVerts * 4 + || surface->decalColorStageCount != 1 ) { return false; } - if ( r_skipDecals.GetBool() ) { + + const shaderStage_t *stage = material->GetStage( 0 ); + if ( stage == NULL || stage->lighting != SL_AMBIENT + || stage->newStage != NULL + || stage->vertexColor != SVC_MODULATE + || stage->texture.image == NULL + || stage->texture.image->GetOpts().textureType != TT_2D + || stage->texture.cinematic != NULL + || stage->texture.dynamic != DI_STATIC + || stage->texture.texgen != TG_EXPLICIT + || stage->texture.hasMatrix + || stage->hasAlphaTest + || surface->shaderRegisters[stage->conditionRegister] == 0.0f ) { return false; } - if ( stats.modernVisibleCanReplaceFrame ) { + + const int blendBits = stage->drawStateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ); + const bool supportedBlend = + blendBits == ( GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA ) + || blendBits == ( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE ) + || blendBits == ( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO ) + || blendBits == ( GLS_SRCBLEND_ZERO | GLS_DSTBLEND_SRC_COLOR ) + || blendBits == ( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_SRC_COLOR ) + || blendBits == ( GLS_SRCBLEND_ZERO | GLS_DSTBLEND_ONE_MINUS_SRC_COLOR ); + const int supportedStateBits = GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS | GLS_DEPTHMASK; + if ( !supportedBlend + || ( stage->drawStateBits & GLS_DEPTHMASK ) == 0 + || ( stage->drawStateBits & ~supportedStateBits ) != 0 ) { return false; } - if ( !stats.forwardPlusRequested || stats.pipelineForwardPlusDecalCommands <= 0 ) { + + const float *regs = surface->shaderRegisters; + if ( blendBits == ( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE ) + && regs[stage->color.registers[0]] <= 0.0f + && regs[stage->color.registers[1]] <= 0.0f + && regs[stage->color.registers[2]] <= 0.0f ) { return false; } - if ( !stats.enabled || !stats.available || !stats.submitPlanReady || !rg_modernGLExecutorInitialized || rg_modernGLExecutorVAO == 0 ) { + if ( blendBits == ( GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA ) + && regs[stage->color.registers[3]] <= 0.0f ) { return false; } - return ( stats.modernVisibleRequested && stats.modernVisibleBlockedByLegacy ) || r_rendererForwardPlus.GetBool(); + return true; } -static bool R_ModernGLExecutor_CommandMatchesForwardPlusDecalOverlayView( const modernGLSubmitCommand_t &command, const viewDef_t *viewDef ) { - if ( command.viewDef == viewDef ) { +static bool R_ModernGLExecutor_IsExactSingleStageClusteredDecal( const drawPacket_t &draw ) { + return draw.passCategory == RENDER_PASS_AMBIENT + && draw.packetCategory == SCENE_PACKET_CATEGORY_WORLD + && R_ModernGLExecutor_IsExactSingleStageClusteredDecalSurface( + draw.viewDef, draw.legacyDrawSurf ); +} + +static bool R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( const drawPacket_t &draw ) { + return R_ModernGLExecutor_IsExactSingleStageClusteredDecal( draw ); +} + +// Classic ambient submission enables the material-wide offset first, then +// RB_PrepareStageTexturing replaces its units with privatePolygonOffset when +// the stage supplies one. Clustered decals own exactly one ambient stage, so +// preserve that precedence explicitly before suppressing the classic draw. +static bool R_ModernGLExecutor_ClusteredDecalPolygonOffset( + const materialResourceTableRecord_t *materialRecord, + const shaderStage_t *stage, float &polygonOffset ) { + polygonOffset = 0.0f; + if ( stage != NULL && stage->privatePolygonOffset != 0.0f ) { + polygonOffset = stage->privatePolygonOffset; return true; } - if ( command.viewDef == NULL || viewDef == NULL ) { - return false; + if ( materialRecord != NULL && materialRecord->hasMaterialPolygonOffset ) { + polygonOffset = materialRecord->polygonOffset; + return true; } + return false; +} - const viewDef_t *commandView = command.viewDef; - if ( commandView->isSubview != viewDef->isSubview || commandView->renderWorld != viewDef->renderWorld ) { - return false; - } - if ( commandView->viewEntitys != viewDef->viewEntitys ) { +static bool R_ModernGLExecutor_ClusteredDecalDepthRange( const modernGLSubmitCommand_t &command, + float &depthMin, float &depthMax ) { + if ( command.viewDef == NULL + || !command.visibilityBoundsValid + || !command.visibilityScreenRectValid + || command.visibilityFrustumRejected + || command.visibilityNearPlaneClipped + || command.visibilityDepthMin < 0.0f + || command.visibilityDepthMax > 1.0f + || command.visibilityDepthMax < command.visibilityDepthMin ) { return false; } - if ( !commandView->viewport.Equals( viewDef->viewport ) || !commandView->scissor.Equals( viewDef->scissor ) ) { + + const float projectionZ = command.viewDef->projectionMatrix[10]; + const float projectionW = command.viewDef->projectionMatrix[14]; + const float ndcMin = command.visibilityDepthMin * 2.0f - 1.0f; + const float ndcMax = command.visibilityDepthMax * 2.0f - 1.0f; + const float denominatorMin = projectionZ + ndcMin; + const float denominatorMax = projectionZ + ndcMax; + if ( !std::isfinite( static_cast( projectionZ ) ) + || !std::isfinite( static_cast( projectionW ) ) + || !std::isfinite( static_cast( denominatorMin ) ) + || !std::isfinite( static_cast( denominatorMax ) ) + || idMath::Fabs( denominatorMin ) <= 1.0e-6f + || idMath::Fabs( denominatorMax ) <= 1.0e-6f ) { return false; } - const renderView_t &commandRenderView = commandView->renderView; - const renderView_t &overlayRenderView = viewDef->renderView; - if ( commandRenderView.viewID != overlayRenderView.viewID - || commandRenderView.x != overlayRenderView.x - || commandRenderView.y != overlayRenderView.y - || commandRenderView.width != overlayRenderView.width - || commandRenderView.height != overlayRenderView.height ) { + const float viewDepthA = projectionW / denominatorMin; + const float viewDepthB = projectionW / denominatorMax; + if ( !std::isfinite( static_cast( viewDepthA ) ) + || !std::isfinite( static_cast( viewDepthB ) ) + || viewDepthA <= 0.0f || viewDepthB <= 0.0f ) { return false; } - - const float viewEpsilon = 0.001f; - return commandRenderView.vieworg.Compare( overlayRenderView.vieworg, viewEpsilon ) - && commandRenderView.viewaxis.Compare( overlayRenderView.viewaxis, viewEpsilon ); + depthMin = Min( viewDepthA, viewDepthB ); + depthMax = Max( viewDepthA, viewDepthB ); + return true; } -static void R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay( modernGLExecutorStats_t &stats ) { - R_ModernGLExecutor_FullRestoreForLegacyHandoff( stats, "forward+ decal overlay", true ); - if ( backEnd.renderTexture != NULL ) { - backEnd.renderTexture->MakeCurrent(); - R_GLStateCache_InvalidateAll( "forward+ decal overlay render target restore" ); +static bool R_ModernGLExecutor_ClusteredDecalCommandReady( + const modernGLSubmitCommand_t &command, modernGLExecutorStats_t &stats, + rendererClusteredDecalSource_t &source, int commandIndex, unsigned int generation ) { + const modernGLDrawPlanEntry_t *entry = command.drawPlanEntry; + const drawPacket_t *draw = entry != NULL ? entry->drawPacket : NULL; + const geometryResourceRecord_t *geometry = draw != NULL ? draw->geometryRecord : NULL; + const instanceRecord_t *instance = draw != NULL ? draw->instanceRecord : NULL; + if ( draw == NULL + || !R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( *draw ) + || !command.forwardPlusDecal + || command.viewDef != draw->viewDef + || command.pipeline != MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT + || command.passCategory != RENDER_PASS_AMBIENT + || command.program == 0 || command.vertexBuffer == 0 + || command.modelViewProjectionLocation < 0 + || command.modelDepthHack != 0.0f || command.weaponDepthHack + || command.negativeScale + || command.originalSubmitOrder < 0 + || command.materialStableId == RENDERER_CLUSTER_DECAL_INVALID_STABLE_ID + || command.geometryRecordIndex < 0 || command.instanceRecordIndex < 0 + || geometry == NULL || instance == NULL + || geometry->recordIndex != command.geometryRecordIndex + || instance->recordIndex != command.instanceRecordIndex + || geometry->fallbackReason != GEOMETRY_RESOURCE_FALLBACK_NONE + || !geometry->hasAmbientVertexBuffer || geometry->ambientVertexBuffer == 0 + || command.vertexBuffer != static_cast( geometry->ambientVertexBuffer ) + || command.ambientCacheOffset != geometry->ambientCacheOffset + || command.vertexStride != geometry->vertexStride + || !instance->hasModelMatrix + || !R_ModernGLExecutor_DrawVertLayoutSupported( command.vertexStride, command.ambientCacheOffset ) + || ( command.indexed && ( command.uploadIndexBuffer || command.indexBuffer == 0 || command.indexCount <= 0 ) ) + || ( !command.indexed && command.vertexCount <= 0 ) ) { + return false; + } + + modernGLExecutorStats_t materialPreflightStats = stats; + const drawSurf_t *surface = draw->legacyDrawSurf; + const idMaterial *material = surface != NULL ? surface->material : NULL; + const shaderStage_t *stage = material != NULL ? material->GetStage( 0 ) : NULL; + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + const materialResourceTextureBinding_t *binding = materialRecord != NULL + ? R_ModernGLExecutor_PrimaryColorBinding( *materialRecord, command.shaderKind ) + : NULL; + if ( surface == NULL || material == NULL || stage == NULL + || materialRecord == NULL || materialRecord->material != material + || materialRecord->stageCount != 1 || materialRecord->evaluatedStageCount != 1 + || materialRecord->hasPBR || materialRecord->textureBindingCount != 1 + || binding == NULL || binding->stageIndex != 0 + || binding->image != stage->texture.image || binding->textureHandle == 0 + || binding->image->GetOpts().textureType != TT_2D + || command.mainTextureLocation < 0 + || R_ModernGLExecutor_TextureForCommand( command ) != binding->textureHandle + || binding->drawStateBits != stage->drawStateBits + || binding->vertexColorMode != SVC_MODULATE + || binding->hasTextureMatrix || binding->depthWrite || binding->colorMasked + || !binding->loaded || binding->defaulted || binding->missing ) { + return false; + } + if ( surface->decalColorOffset < 0 || surface->decalColorStride <= 0 + || surface->decalColorOffset > geometry->ambientCacheBytes + || surface->decalColorStride > geometry->ambientCacheBytes - surface->decalColorOffset + || command.ambientCacheOffset < 0 + || surface->decalColorOffset > idMath::INT_MAX - command.ambientCacheOffset ) { + return false; } -} - -void R_ModernGLExecutor_SubmitForwardPlusDecalOverlay( const viewDef_t *viewDef ) { - modernGLExecutorStats_t &stats = rg_modernGLExecutorStats; - if ( !R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed( stats, viewDef ) ) { - return; + for ( int component = 0; component < 4; ++component ) { + if ( binding->colorRegisters[component] != stage->color.registers[component] ) { + return false; + } } - int submittedDecals = 0; - idGLDebugScope debugScope( "ModernGLExecutor forward+ decal overlay" ); - R_GLStateCache_InvalidateAll( "forward+ decal overlay" ); - R_GLStateCache().BindVertexArray( rg_modernGLExecutorVAO ); - R_GLStateCache().SetScissorTestEnabled( true ); - R_GLStateCache().SetDepthTestEnabled( true ); - R_GLStateCache().SetDepthFunc( GL_LEQUAL ); - R_GLStateCache().SetDepthMask( GL_FALSE ); - R_GLStateCache().SetStencilTestEnabled( false ); - R_GLStateCache().SetCullFaceEnabled( false ); - R_GLStateCache().SetColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); + // SubmitCommand cannot allocate for this subset (client-index uploads are + // rejected above). Pre-configure or fully translate the only remaining + // fallible vertex-layout branch before ownership can be sealed. + if ( rg_modernGLExecutorVertexBindingReady ) { + if ( glBindVertexBuffer == NULL + || !R_ModernGLExecutor_ConfigureDrawVertVertexBindingFormat( &materialPreflightStats ) ) { + return false; + } + } else { + if ( glVertexAttribPointer == NULL ) { + return false; + } + const rendererVertexLayoutDesc_t &layout = RendererContracts_LegacyDrawVertLayout(); + for ( std::uint32_t attributeIndex = 0; attributeIndex < layout.attributeCount; ++attributeIndex ) { + GLuint attribute = 0; + GLint components = 0; + GLenum type = GL_NONE; + GLboolean normalized = GL_FALSE; + if ( !R_ModernGLExecutor_TranslateLegacyVertexAttribute( + layout.attributes[attributeIndex], attribute, components, type, normalized ) ) { + return false; + } + } + } + if ( !R_ModernGLExecutor_ForwardPlusMaterialSupported( command, materialPreflightStats ) ) { + return false; + } + + const int viewWidth = command.viewDef->viewport.x2 >= command.viewDef->viewport.x1 + ? command.viewDef->viewport.x2 + 1 - command.viewDef->viewport.x1 + : command.viewDef->renderView.width; + const int viewHeight = command.viewDef->viewport.y2 >= command.viewDef->viewport.y1 + ? command.viewDef->viewport.y2 + 1 - command.viewDef->viewport.y1 + : command.viewDef->renderView.height; + if ( viewWidth <= 0 || viewHeight <= 0 + || command.visibilityScreenX1 < 0 || command.visibilityScreenY1 < 0 + || command.visibilityScreenX2 < command.visibilityScreenX1 + || command.visibilityScreenY2 < command.visibilityScreenY1 + || command.visibilityScreenX2 >= viewWidth + || command.visibilityScreenY2 >= viewHeight ) { + return false; + } + + float depthMin = 0.0f; + float depthMax = 0.0f; + if ( !R_ModernGLExecutor_ClusteredDecalDepthRange( command, depthMin, depthMax ) ) { + return false; + } + + memset( &source, 0, sizeof( source ) ); + source.viewDef = command.viewDef; + source.sourceSurface = draw->legacyDrawSurf; + source.commandIndex = commandIndex; + source.commandOrder = command.originalSubmitOrder; + source.materialStableId = command.materialStableId; + source.geometryStableId = static_cast( command.geometryRecordIndex ); + source.instanceStableId = static_cast( command.instanceRecordIndex ); + source.screenX1 = command.visibilityScreenX1; + source.screenY1 = command.visibilityScreenY1; + source.screenX2 = command.visibilityScreenX2; + source.screenY2 = command.visibilityScreenY2; + source.depthMin = depthMin; + source.depthMax = depthMax; + source.generation = generation; + return true; +} + +static void R_ModernGLExecutor_RejectClusteredDecals( + rendererClusteredDecalReject_t reason, int sourceIndex, + int submittedRecords, unsigned int generation ) { + R_ModernClusteredLighting_RejectDecalsForFrame( + reason, sourceIndex, submittedRecords, generation ); +} + +static bool R_ModernGLExecutor_IsRootWorldDrawScene( + const idScenePacketFrame &packetFrame, const scenePacket_t &scene ) { + if ( scene.packetCategory != SCENE_PACKET_CATEGORY_WORLD + || scene.viewDef == NULL || scene.viewDef->isSubview + || scene.viewDef->viewEntitys == NULL ) { + return false; + } + for ( int passOffset = 0; passOffset < scene.passPacketCount; ++passOffset ) { + const passPacket_t &pass = packetFrame.Pass( scene.firstPassPacket + passOffset ); + if ( pass.passCategory == RENDER_PASS_AMBIENT && !pass.commandOnly ) { + return true; + } + } + return false; +} + +static void R_ModernGLExecutor_PrepareClusteredDecalTransaction( + const idScenePacketFrame &packetFrame, bool requested, modernGLExecutorStats_t &stats ) { + if ( !requested || r_skipDecals.GetBool() || r_showOverDraw.GetInteger() != 0 + || stats.modernVisibleCanReplaceFrame ) { + return; + } + + unsigned int generation = static_cast( tr.frameCount ) + 1u; + if ( generation == 0u ) { + generation = 1u; + } + + // The classic root-view draw-surface lists are the source of truth. Count + // every admitted root draw scene before looking at packets so a second view + // with an eligible decal but a missing packet forces the whole frame back to + // classic ownership. + int sourceInventoryCount = 0; + const int sceneCount = packetFrame.NumScenes(); + for ( int sceneIndex = 0; sceneIndex < sceneCount; ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !R_ModernGLExecutor_IsRootWorldDrawScene( packetFrame, scene ) ) { + continue; + } + const viewDef_t *viewDef = scene.viewDef; + for ( int surfaceIndex = 0; surfaceIndex < viewDef->numDrawSurfs; ++surfaceIndex ) { + if ( R_ModernGLExecutor_IsExactSingleStageClusteredDecalSurface( + viewDef, viewDef->drawSurfs[surfaceIndex] ) ) { + sourceInventoryCount++; + } + } + } + int authoritativeCount = 0; + const int drawPacketCount = packetFrame.NumDrawPackets(); + for ( int drawIndex = 0; drawIndex < drawPacketCount; ++drawIndex ) { + if ( R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( packetFrame.DrawPacket( drawIndex ) ) ) { + authoritativeCount++; + } + } + if ( sourceInventoryCount <= 0 && authoritativeCount <= 0 ) { + return; + } + if ( sourceInventoryCount != authoritativeCount ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + -1, authoritativeCount, generation ); + return; + } + const scenePacketFrameStats_t &packetStats = packetFrame.Stats(); + if ( packetStats.overflow || packetStats.clippedDrawPackets > 0 ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + -1, authoritativeCount, generation ); + return; + } + if ( sourceInventoryCount > RENDERER_CLUSTER_DECAL_MAX_RECORDS ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY, + RENDERER_CLUSTER_DECAL_MAX_RECORDS, sourceInventoryCount, generation ); + return; + } + + const rendererClusteredLightingStats_t &clusterStats = R_ModernClusteredLighting_Stats(); + if ( !stats.enabled || !stats.available || !stats.initialized + || !stats.vaoReady || !stats.frameUBOReady || !stats.shaderLibraryReady + || !stats.submitPlanReady || !rg_modernGLExecutorInitialized + || rg_modernGLExecutorVAO == 0 + || !clusterStats.requested || !clusterStats.frameValid || !clusterStats.buffersReady + || !R_ModernClusteredLighting_FrameLossless() ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, + -1, authoritativeCount, generation ); + return; + } + + // Packet capacity is not the authority boundary. Reconcile each root draw + // scene independently against its exact classic draw-surface inventory so a + // missing, duplicated, or clipped packet can never turn a prefix into + // ownership. Scene-local ranges also fail closed if a view pointer is reused. + for ( int sceneIndex = 0; sceneIndex < sceneCount; ++sceneIndex ) { + const scenePacket_t &scene = packetFrame.Scene( sceneIndex ); + if ( !R_ModernGLExecutor_IsRootWorldDrawScene( packetFrame, scene ) ) { + continue; + } + const viewDef_t *viewDef = scene.viewDef; + int exactSurfaceCount = 0; + int exactPacketCount = 0; + const int sceneDrawEnd = scene.firstDrawPacket + scene.drawPacketCount; + for ( int packetIndex = scene.firstDrawPacket; packetIndex < sceneDrawEnd; ++packetIndex ) { + const drawPacket_t &candidate = packetFrame.DrawPacket( packetIndex ); + if ( R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( candidate ) ) { + exactPacketCount++; + } + } + for ( int surfaceIndex = 0; surfaceIndex < viewDef->numDrawSurfs; ++surfaceIndex ) { + const drawSurf_t *surface = viewDef->drawSurfs[surfaceIndex]; + if ( !R_ModernGLExecutor_IsExactSingleStageClusteredDecalSurface( viewDef, surface ) ) { + continue; + } + exactSurfaceCount++; + int matchingPackets = 0; + for ( int packetIndex = scene.firstDrawPacket; packetIndex < sceneDrawEnd; ++packetIndex ) { + const drawPacket_t &candidate = packetFrame.DrawPacket( packetIndex ); + if ( R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( candidate ) + && candidate.legacyDrawSurf == surface ) { + matchingPackets++; + } + } + if ( matchingPackets != 1 ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + surfaceIndex, authoritativeCount, generation ); + return; + } + } + if ( exactSurfaceCount != exactPacketCount ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + sceneIndex, exactPacketCount, generation ); + return; + } + } + + // Every authoritative source must have exactly one command before any + // ownership can be published. A missing or duplicate command makes the + // complete frame transaction classic. const int commandCount = rg_modernGLSubmitPlan.NumCommands(); - for ( int i = 0; i < commandCount; ++i ) { - const modernGLSubmitCommand_t &command = rg_modernGLSubmitPlan.Command( i ); - if ( !R_ModernGLExecutor_CommandMatchesForwardPlusDecalOverlayView( command, viewDef ) - || !command.forwardPlusDecal ) { + static int decalPacketCommandCounts[SCENE_PACKET_MAX_DRAWS]; + memset( decalPacketCommandCounts, 0, sizeof( decalPacketCommandCounts ) ); + for ( int commandIndex = 0; commandIndex < commandCount; ++commandIndex ) { + const modernGLSubmitCommand_t &command = rg_modernGLSubmitPlan.Command( commandIndex ); + if ( !command.forwardPlusDecal || command.drawPlanEntry == NULL ) { continue; } - if ( !R_ModernGLExecutor_CommandVisibleForModernPath( command, &stats, false ) ) { + const int planPacketIndex = command.drawPlanEntry->drawPacketIndex; + if ( planPacketIndex < 0 || planPacketIndex >= drawPacketCount ) { continue; } - if ( !R_ModernGLExecutor_ForwardPlusMaterialSupported( command, stats ) ) { - R_ModernGLExecutor_CountForwardPlusFallback( command, stats ); + const drawPacket_t &draw = packetFrame.DrawPacket( planPacketIndex ); + if ( command.drawPlanEntry->drawPacket == &draw ) { + decalPacketCommandCounts[planPacketIndex]++; + } + } + for ( int drawIndex = 0; drawIndex < drawPacketCount; ++drawIndex ) { + if ( !R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( packetFrame.DrawPacket( drawIndex ) ) ) { continue; } - - R_GLStateCache().SetBlendEnabled( true ); - if ( command.blendMode == MATERIAL_RESOURCE_BLEND_ADD ) { - R_GLStateCache().SetBlendFunc( GL_ONE, GL_ONE ); - } else if ( command.blendMode == MATERIAL_RESOURCE_BLEND_FILTER ) { - R_GLStateCache().SetBlendFunc( GL_ZERO, GL_ONE_MINUS_SRC_COLOR ); - } else { - R_GLStateCache().SetBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + if ( decalPacketCommandCounts[drawIndex] != 1 ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + drawIndex, authoritativeCount, generation ); + return; } + } - const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); - const bool applyPolygonOffset = materialRecord != NULL && materialRecord->hasMaterialPolygonOffset; - if ( applyPolygonOffset ) { - glEnable( GL_POLYGON_OFFSET_FILL ); - glPolygonOffset( r_offsetFactor.GetFloat(), r_offsetUnits.GetFloat() * materialRecord->polygonOffset ); + rendererClusteredDecalSource_t sources[RENDERER_CLUSTER_DECAL_MAX_RECORDS]; + int sourceCount = 0; + for ( int commandIndex = 0; commandIndex < commandCount; ++commandIndex ) { + const modernGLSubmitCommand_t &command = rg_modernGLSubmitPlan.Command( commandIndex ); + const drawPacket_t *draw = command.drawPlanEntry != NULL + ? command.drawPlanEntry->drawPacket : NULL; + if ( !command.forwardPlusDecal || draw == NULL + || !R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( *draw ) ) { + continue; } - const bool submitted = R_ModernGLExecutor_SubmitCommand( command, stats, false ); - if ( applyPolygonOffset ) { - glDisable( GL_POLYGON_OFFSET_FILL ); + if ( sourceCount >= RENDERER_CLUSTER_DECAL_MAX_RECORDS + || !R_ModernGLExecutor_ClusteredDecalCommandReady( + command, stats, sources[sourceCount], commandIndex, generation ) ) { + R_ModernGLExecutor_RejectClusteredDecals( + sourceCount >= RENDERER_CLUSTER_DECAL_MAX_RECORDS + ? RENDERER_CLUSTER_DECAL_REJECT_RECORD_CAPACITY + : RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, + commandIndex, authoritativeCount, generation ); + return; } - if ( !submitted ) { - R_ModernGLExecutor_CountForwardPlusFallback( command, stats ); - continue; + sourceCount++; + } + if ( sourceCount != authoritativeCount ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_VIEW_INCOMPLETE, + sourceCount, authoritativeCount, generation ); + return; + } + for ( int sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex ) { + bool firstSourceForView = true; + for ( int previous = 0; previous < sourceIndex; ++previous ) { + if ( sources[previous].viewDef == sources[sourceIndex].viewDef ) { + firstSourceForView = false; + break; + } } + if ( firstSourceForView + && !R_ModernClusteredLighting_BindGridForView( sources[sourceIndex].viewDef ) ) { + R_ModernGLExecutor_RejectClusteredDecals( + RENDERER_CLUSTER_DECAL_REJECT_RESOURCE_UNAVAILABLE, + sourceIndex, sourceCount, generation ); + return; + } + } - const int area = R_ModernGLExecutor_ForwardPlusCommandArea( command ); - stats.forwardPlusDraws++; - stats.forwardPlusTransparentDraws++; - stats.forwardPlusOverdrawEstimate += area; - submittedDecals++; + if ( !R_ModernClusteredLighting_PrepareDecals( sources, sourceCount, generation ) ) { + return; } + R_ModernClusteredLighting_SealDecals( sources, sourceCount, generation ); +} - if ( submittedDecals > 0 ) { - stats.forwardPlusExecuted = true; - R_ModernGLExecutor_SetStatus( stats, "forward-plus-decal-overlay" ); - R_ModernGLExecutor_RecordMetrics( stats ); +static bool R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed( const modernGLExecutorStats_t &stats, const viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->viewEntitys == NULL || viewDef->isSubview ) { + return false; + } + if ( r_skipDecals.GetBool() + || r_showOverDraw.GetInteger() != 0 + || !r_rendererModernQuality.GetBool() + || !r_rendererClusteredDecals.GetBool() + || r_rendererSharedWorldAmbient.GetBool() ) { + return false; + } + if ( stats.modernVisibleCanReplaceFrame ) { + return false; + } + if ( stats.pipelineForwardPlusDecalCommands <= 0 ) { + return false; + } + if ( !stats.enabled || !stats.available || !stats.submitPlanReady || !rg_modernGLExecutorInitialized || rg_modernGLExecutorVAO == 0 ) { + return false; + } + rendererClusteredDecalViewStats_t viewStats; + return R_ModernClusteredLighting_DecalViewStats( viewDef, viewStats ) + && viewStats.prepared && viewStats.owned && viewStats.recordCount > 0; +} + +static void R_ModernGLExecutor_RestoreLegacyViewRect( const viewDef_t *viewDef ) { + if ( viewDef == NULL ) { + return; + } + const int viewportX = tr.viewportOffset[0] + viewDef->viewport.x1; + const int viewportY = tr.viewportOffset[1] + viewDef->viewport.y1; + R_GLStateCache().SetViewport( + viewportX, + viewportY, + viewDef->viewport.x2 + 1 - viewDef->viewport.x1, + viewDef->viewport.y2 + 1 - viewDef->viewport.y1 ); + R_GLStateCache().SetScissor( + viewportX + viewDef->scissor.x1, + viewportY + viewDef->scissor.y1, + viewDef->scissor.x2 + 1 - viewDef->scissor.x1, + viewDef->scissor.y2 + 1 - viewDef->scissor.y1 ); + // Match RB_BeginDrawingView exactly. This real GL restore is required even + // when r_useScissor is disabled because the modern submit narrowed the box. + backEnd.currentScissor = viewDef->scissor; +} + +static void R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay( + modernGLExecutorStats_t &stats, const viewDef_t *viewDef ) { + R_ModernGLExecutor_FullRestoreForLegacyHandoff( stats, "forward+ decal overlay", true ); + if ( backEnd.renderTexture != NULL ) { + backEnd.renderTexture->MakeCurrent(); + R_GLStateCache_InvalidateAll( "forward+ decal overlay render target restore" ); + } + R_ModernGLExecutor_RestoreLegacyViewRect( viewDef ); +} + +bool R_ModernGLExecutor_SubmitForwardPlusDecalSurface( + const viewDef_t *viewDef, const drawSurf_t *sourceSurface ) { + modernGLExecutorStats_t &stats = rg_modernGLExecutorStats; + if ( sourceSurface == NULL + || !R_ModernClusteredLighting_DecalOwnsSurface( viewDef, sourceSurface ) ) { + return false; + } + if ( !R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed( stats, viewDef ) ) { + common->Error( "Modern clustered decal sealed ownership lost its executor gate" ); + return false; + } + rendererClusteredDecalViewStats_t viewStats; + if ( !R_ModernClusteredLighting_DecalViewStats( viewDef, viewStats ) || !viewStats.owned ) { + common->Error( "Modern clustered decal sealed ownership lost its exact view" ); + return false; + } + + bool duplicateSealedCommand = false; + const int commandIndex = R_ModernClusteredLighting_DecalSealedCommandForSurface( + viewDef, sourceSurface, duplicateSealedCommand ); + if ( duplicateSealedCommand ) { + common->Error( "Modern clustered decal surface has duplicate sealed commands" ); + return false; + } + if ( commandIndex < 0 || commandIndex >= rg_modernGLSubmitPlan.NumCommands() ) { + common->Error( "Modern clustered decal surface has no sealed command" ); + return false; + } + const modernGLSubmitCommand_t &command = rg_modernGLSubmitPlan.Command( commandIndex ); + const drawPacket_t *draw = command.drawPlanEntry != NULL + ? command.drawPlanEntry->drawPacket : NULL; + if ( draw == NULL || draw->legacyDrawSurf != sourceSurface + || !R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw( *draw ) + || !R_ModernClusteredLighting_BindGridForView( viewDef ) ) { + common->Error( "Modern clustered decal sealed command lost submit readiness" ); + return false; + } + const shaderStage_t *stage = sourceSurface->material != NULL + ? sourceSurface->material->GetStage( 0 ) : NULL; + if ( stage == NULL ) { + common->Error( "Modern clustered decal sealed command lost its material stage" ); + return false; + } + const int blendBits = stage->drawStateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ); + + idGLDebugScope debugScope( "ModernGLExecutor forward+ decal surface" ); + R_GLStateCache_InvalidateAll( "forward+ decal overlay" ); + rg_modernGLShadowBindingsRevision++; // classic per-light shadow state may have moved since the sidecar block + R_GLStateCache().BindVertexArray( rg_modernGLExecutorVAO ); + R_GLStateCache().SetScissorTestEnabled( true ); + R_GLStateCache().SetDepthTestEnabled( true ); + R_GLStateCache().SetDepthFunc( GL_LEQUAL ); + R_GLStateCache().SetDepthMask( GL_FALSE ); + R_GLStateCache().SetStencilTestEnabled( false ); + R_GLStateCache().SetCullFaceEnabled( false ); + R_GLStateCache().SetColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); + R_GLStateCache().SetBlendEnabled( true ); + if ( blendBits == ( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE ) ) { + R_GLStateCache().SetBlendFunc( GL_ONE, GL_ONE ); + } else if ( blendBits == ( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO ) ) { + R_GLStateCache().SetBlendFunc( GL_DST_COLOR, GL_ZERO ); + } else if ( blendBits == ( GLS_SRCBLEND_ZERO | GLS_DSTBLEND_SRC_COLOR ) ) { + R_GLStateCache().SetBlendFunc( GL_ZERO, GL_SRC_COLOR ); + } else if ( blendBits == ( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_SRC_COLOR ) ) { + R_GLStateCache().SetBlendFunc( GL_DST_COLOR, GL_SRC_COLOR ); + } else if ( blendBits == ( GLS_SRCBLEND_ZERO | GLS_DSTBLEND_ONE_MINUS_SRC_COLOR ) ) { + R_GLStateCache().SetBlendFunc( GL_ZERO, GL_ONE_MINUS_SRC_COLOR ); + } else { + R_GLStateCache().SetBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); } - R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay( stats ); + + const materialResourceTableRecord_t *materialRecord = R_ModernGLExecutor_MaterialRecordForCommand( command ); + float polygonOffset = 0.0f; + const bool applyPolygonOffset = R_ModernGLExecutor_ClusteredDecalPolygonOffset( + materialRecord, stage, polygonOffset ); + if ( applyPolygonOffset ) { + glEnable( GL_POLYGON_OFFSET_FILL ); + glPolygonOffset( r_offsetFactor.GetFloat(), r_offsetUnits.GetFloat() * polygonOffset ); + } + const bool submitted = R_ModernGLExecutor_SubmitCommand( + command, stats, false, sourceSurface ); + if ( applyPolygonOffset ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + } + if ( !submitted ) { + common->Error( "Modern clustered decal command failed after complete preflight" ); + return false; + } + + const int area = R_ModernGLExecutor_ForwardPlusCommandArea( command ); + stats.forwardPlusDraws++; + stats.forwardPlusTransparentDraws++; + stats.forwardPlusOverdrawEstimate += area; + stats.forwardPlusExecuted = true; + R_ModernGLExecutor_SetStatus( stats, "forward-plus-decal-overlay" ); + R_ModernGLExecutor_RecordMetrics( stats ); + R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay( stats, viewDef ); + return true; +} + +static bool R_ModernGLExecutor_SharedGuiOwnsCommand( + const modernGLSubmitCommand_t &command ) { + if ( command.pipeline != MODERN_GL_DRAW_PLAN_PIPELINE_GUI ) { + return false; + } + if ( r_rendererSharedGui.GetBool() ) { + // Root 2D ownership consumes the complete legacy GUI command view. + return true; + } + if ( !r_rendererSharedInWorldGui.GetBool() ) { + return false; + } + const drawPacket_t *drawPacket = command.drawPlanEntry != NULL + ? command.drawPlanEntry->drawPacket : NULL; + // The in-world corridor must never hide root UI from the aggregate path. + return drawPacket != NULL + && drawPacket->packetCategory == SCENE_PACKET_CATEGORY_WORLD; } static void R_ModernGLExecutor_SubmitPlan( modernGLExecutorStats_t &stats ) { @@ -6113,6 +7287,12 @@ static void R_ModernGLExecutor_SubmitPlan( modernGLExecutorStats_t &stats ) { } idGLDebugScope debugScope( "ModernGLExecutor diagnostic submit" ); + // Diagnostic draws intentionally write neither color nor depth, but they + // still require a complete framebuffer. Own a neutral target explicitly + // instead of inheriting whichever modern pass happened to run last. + R_GLStateCache().BindFramebuffer( GL_FRAMEBUFFER, 0 ); + glReadBuffer( GL_BACK ); + glDrawBuffer( GL_BACK ); R_GLStateCache().BindVertexArray( rg_modernGLExecutorVAO ); R_GLStateCache().SetScissorTestEnabled( true ); R_GLStateCache().SetDepthTestEnabled( true ); @@ -6128,6 +7308,9 @@ static void R_ModernGLExecutor_SubmitPlan( modernGLExecutorStats_t &stats ) { if ( command.pipeline == MODERN_GL_DRAW_PLAN_PIPELINE_GBUFFER || R_ModernGLExecutor_IsForwardPlusPipeline( command.pipeline ) ) { continue; } + if ( R_ModernGLExecutor_SharedGuiOwnsCommand( command ) ) { + continue; + } if ( !R_ModernGLExecutor_CommandVisibleForModernPath( command, &stats, false ) ) { continue; } @@ -6142,7 +7325,12 @@ static void R_ModernGLExecutor_SubmitPlan( modernGLExecutorStats_t &stats ) { } static void R_ModernGLExecutor_SubmitModernGui( modernGLExecutorStats_t &stats ) { - if ( !stats.modernVisibleRequested || stats.modernVisibleGuiReadyDraws <= 0 || !stats.submitPlanReady || rg_modernGLExecutorVAO == 0 ) { + // The shared classic-GUI domain executes at the source RC_DRAW_VIEW handoff. + // Never replay the older aggregate GUI commands as well: they flatten stage + // ordering and would double-submit a view that the shared owner consumed. + if ( r_rendererSharedGui.GetBool() + || !stats.modernVisibleRequested || stats.modernVisibleGuiReadyDraws <= 0 + || !stats.submitPlanReady || rg_modernGLExecutorVAO == 0 ) { return; } @@ -6165,6 +7353,9 @@ static void R_ModernGLExecutor_SubmitModernGui( modernGLExecutorStats_t &stats ) if ( command.pipeline != MODERN_GL_DRAW_PLAN_PIPELINE_GUI ) { continue; } + if ( R_ModernGLExecutor_SharedGuiOwnsCommand( command ) ) { + continue; + } if ( R_ModernGLExecutor_SubmitCommand( command, stats, false ) ) { submittedGuiDraws++; } else { @@ -6297,7 +7488,7 @@ static bool R_ModernGLExecutor_ModernVisibleShadowReceiversReady( const modernSh if ( descriptor->policy == MODERN_SHADOW_POLICY_MAPPED || descriptor->policy == MODERN_SHADOW_POLICY_CACHE_REUSE ) { if ( !descriptor->modernReceiverSamplingReady ) { blockedLights++; - } else if ( descriptor->pointLight ) { + } else if ( descriptor->pointLight && descriptor->arb2PointCubeReady ) { consumableLights++; bool seen = false; for ( int i = 0; i < distinctPointLightDefCount; ++i ) { @@ -6314,7 +7505,7 @@ static bool R_ModernGLExecutor_ModernVisibleShadowReceiversReady( const modernSh } consumablePointLights++; } - } else if ( descriptor->arb2AtlasSlotReady ) { + } else if ( !descriptor->pointLight && descriptor->arb2AtlasSlotReady ) { consumableLights++; consumableProjectedLights++; } else { @@ -6870,12 +8061,12 @@ void R_ModernGLExecutor_Init( const renderBackendCaps_t &caps, const renderFeatu R_ModernShadowPlanner_Init( caps, features ); R_ModernClusteredLighting_Init( caps, features ); R_ModernLightImageAtlas_Init( caps, features ); + R_ModernSpecularProbeAtlas_Init( caps, features ); rg_modernGLExecutorLowOverheadReady = R_ModernGLExecutor_CanUseLowOverhead( caps, features ); rg_modernGLExecutorVertexBindingReady = R_ModernGLExecutor_CanUseVertexBinding( caps, features ); rg_modernGLExecutorVertexInputFormatSetups = 0; R_ModernGLExecutor_ResetVertexInputCache(); R_ModernGLExecutor_QueryStreamingAlignments(); - R_ModernGLExecutor_ResetStreamBinding( rg_modernGLFrameUBOStream ); R_ModernGLExecutor_ResetGpuDrivenStreamBindings(); R_ModernGLExecutor_ResetStats( rg_modernGLExecutorStats, r_rendererModernExecutor.GetBool() ); @@ -6993,7 +8184,6 @@ void R_ModernGLExecutor_Init( const renderBackendCaps_t &caps, const renderFeatu void R_ModernGLExecutor_Shutdown( void ) { R_ModernGLExecutor_ClearPendingGpuValidationReadbacks(); - R_ModernGLExecutor_ResetStreamBinding( rg_modernGLFrameUBOStream ); R_ModernGLExecutor_ResetGpuDrivenStreamBindings(); rg_modernGLDrawPlan.Clear(); rg_modernGLSubmitPlan.Clear(); @@ -7015,6 +8205,8 @@ void R_ModernGLExecutor_Shutdown( void ) { } R_ModernGLShaderLibrary_Shutdown(); R_ModernGLExecutor_ResetClusterBlockBindingCache(); + R_ModernGLExecutor_InvalidateSubmitStateMemos(); + R_ModernGLExecutor_ResetShadowUniformCache(); rg_modernGLExecutorVAO = 0; rg_modernGLExecutorFrameUBO = 0; @@ -7034,6 +8226,7 @@ void R_ModernGLExecutor_Shutdown( void ) { memset( &rg_modernGLExecutorCaps, 0, sizeof( rg_modernGLExecutorCaps ) ); memset( &rg_modernGLExecutorFeatures, 0, sizeof( rg_modernGLExecutorFeatures ) ); R_ModernLightImageAtlas_Shutdown(); + R_ModernSpecularProbeAtlas_Shutdown(); R_ModernClusteredLighting_Shutdown(); R_ModernShadowPlanner_Shutdown(); R_GLStateCache_Shutdown(); @@ -7042,6 +8235,11 @@ void R_ModernGLExecutor_Shutdown( void ) { } void R_ModernGLExecutor_SkipFrame( void ) { + // Decal ownership stores command/surface addresses from the command stream. + // Revoke it even when the rest of the dormant executor reset is latched so + // no empty or side-pipeline-disabled frame can observe stale addresses. + R_ModernClusteredLighting_ResetDecalsForFrame(); + // the dormant-pipeline bookkeeping (ownership-table reason strings, a // ~2 KB stats reset, and a several-hundred-scalar metrics fan-out) only // needs refreshing once after any executor activity (the latch is @@ -7073,11 +8271,49 @@ void R_ModernGLExecutor_InvalidatePlans( void ) { rg_modernGLDrawPlan.Clear(); rg_modernGLSubmitPlan.Clear(); R_ModernGLExecutor_ResetClusterBlockBindingCache(); + R_ModernGLExecutor_InvalidateSubmitStateMemos(); + R_ModernGLExecutor_ResetShadowUniformCache(); +} + +static const viewDef_t *R_ModernGLExecutor_ShadowCacheOwnerView( + const idScenePacketFrame &packetFrame ) { + const viewDef_t *fallback = NULL; + // Command streams place the root world view after any capture-backed + // subviews. Prefer that root, but retain a subview/render-demo fallback for + // streams which contain no ordinary main view. + for ( int i = packetFrame.NumScenes() - 1; i >= 0; --i ) { + const viewDef_t *viewDef = packetFrame.Scene( i ).viewDef; + if ( viewDef == NULL || viewDef->renderWorld == NULL + || viewDef->viewEntitys == NULL ) { + continue; + } + if ( fallback == NULL ) { + fallback = viewDef; + } + if ( !viewDef->isSubview && viewDef->superView == NULL + && viewDef->subviewSurface == NULL + && viewDef->renderView.viewID >= 0 ) { + return viewDef; + } + } + return fallback; } void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, const idRenderGraph &graph ) { + const viewDef_t *shadowCacheOwnerView = + R_ModernGLExecutor_ShadowCacheOwnerView( packetFrame ); + rg_modernGLShadowTextureBindingsCurrent = shadowCacheOwnerView != NULL + && !RB_ShadowMapPrepareCacheView( shadowCacheOwnerView ); + rg_modernGLShadowBindingsRevision++; R_ModernGLExecutor_ResetPassOwnershipTable( "frame-start" ); // also clears the skip latch const bool modernVisibleRequested = R_ModernGLExecutor_ModernVisibleRequested(); + const bool specularProbeLeafRequested = + r_rendererModernQuality.GetBool() + && r_rendererReflectionProbes.GetBool(); + const bool clusteredDecalLeafRequested = + r_rendererModernQuality.GetBool() + && r_rendererClusteredDecals.GetBool() + && !r_rendererSharedWorldAmbient.GetBool(); // SSAO samples the final scene/depth post target; do not request a deferred sidecar just for it. const bool ssaoDeferredSidecarRequested = false; const bool visibleDepthSidecarRequested = r_rendererModernVisibleDepth.GetBool() || r_rendererModernDepthDebug.GetInteger() > 0 || R_ModernGLExecutor_ShadowMapSidecarRequested() || ssaoDeferredSidecarRequested; @@ -7090,6 +8326,8 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con opaqueGBufferSidecarRequested || deferredResolveSidecarRequested || forwardPlusSidecarRequested || + specularProbeLeafRequested || + clusteredDecalLeafRequested || r_rendererModernSubmit.GetBool() || gpuDrivenValidationRequested || r_rendererClusterDebug.GetInteger() > 0; @@ -7129,6 +8367,8 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con opaqueGBufferSidecarRequested || deferredResolveSidecarRequested || forwardPlusSidecarRequested || + specularProbeLeafRequested || + clusteredDecalLeafRequested || r_rendererModernSubmit.GetBool() || gpuDrivenValidationRequested; @@ -7184,6 +8424,7 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con const bool gpuDrivenWorkRequested = gpuDrivenValidationRequested || r_rendererModernSubmit.GetBool(); const bool gpuDrivenHiZPreludeRequested = gpuDrivenWorkRequested && r_rendererOcclusion.GetBool() && r_rendererHiZ.GetBool(); const bool forwardPlusDecalSidecarRequested = + clusteredDecalLeafRequested && modernVisibleRequested && !visibleReplacementCanConsume && rg_modernGLExecutorStats.pipelineForwardPlusDecalCommands > 0; @@ -7217,6 +8458,8 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con const bool clusteredLightingRequested = rg_modernGLExecutorStats.deferredResolveRequested || rg_modernGLExecutorStats.forwardPlusRequested || + specularProbeLeafRequested || + clusteredDecalLeafRequested || gpuDrivenValidationRequested || // modern-visible bring-up needs the descriptor set even on frames it // cannot own, because per-light ownership classification is the only @@ -7228,15 +8471,19 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con ( clusteredLightingRequested || rg_modernGLExecutorStats.visibleDepthRequested || R_ModernGLExecutor_ShadowMapSidecarRequested() ); R_ModernLightImageAtlas_BeginFrame(); + R_ModernSpecularProbeAtlas_BeginFrame(); R_ModernShadowPlanner_PrepareFrame( packetFrame, shadowPlanningRequested ); const modernShadowPlannerStats_t &shadowStats = R_ModernShadowPlanner_Stats(); rg_modernGLExecutorStats.visibilityShadowCasterTested += shadowStats.visibilityCasterTests; rg_modernGLExecutorStats.visibilityShadowCasterRejected += shadowStats.visibilityCasterRejected; rg_modernGLExecutorStats.visibilityShadowCasterSavedDraws += shadowStats.visibilityCasterSavedDraws; R_ModernClusteredLighting_PrepareFrame( packetFrame, clusteredLightingRequested ); + R_ModernGLExecutor_PrepareClusteredDecalTransaction( + packetFrame, clusteredDecalLeafRequested, rg_modernGLExecutorStats ); // the clustered build only reserved atlas cells; copy them in here, where // binding textures is safe R_ModernLightImageAtlas_FlushUploads(); + R_ModernSpecularProbeAtlas_FlushUploads(); R_ModernGLExecutor_ClassifyModernVisibleLighting( packetFrame, rg_modernGLExecutorStats ); R_ModernGLExecutor_UpdateFrameUBO( rg_modernGLExecutorStats ); { @@ -7672,6 +8919,26 @@ bool R_ModernGLExecutor_LegacyPassCanSkip( renderPassCategory_t category ) { } bool R_ModernGLExecutor_LegacyPassCanSkipForView( renderPassCategory_t category, const viewDef_t *viewDef ) { + // Shared world-ambient ownership is decided transactionally at the source + // view. The older aggregate executor must never suppress that classic + // rollback before the shared domain has completed backend preflight. + if ( r_rendererSharedWorldAmbient.GetBool() + && category == RENDER_PASS_AMBIENT ) { + return false; + } + if ( r_rendererSharedWorldInteraction.GetBool() + && ( category == RENDER_PASS_ARB2_INTERACTION + || category == RENDER_PASS_STENCIL_SHADOW + || category == RENDER_PASS_SHADOW_MAP ) ) { + return false; + } + // The shared fog/blend phase makes one atomic decision for every fog and + // blend light in the view. Keep the aggregate modern executor from + // suppressing the classic rollback before that phase finishes preflight. + if ( r_rendererSharedWorldFogBlend.GetBool() + && category == RENDER_PASS_FOG_BLEND ) { + return false; + } if ( !R_ModernGLExecutor_LegacyPassCanSkip( category ) ) { return false; } @@ -7964,7 +9231,8 @@ static bool R_ModernGLExecutor_ComposeVisibleSceneToTarget( bool useGpuTimer, const char *status ) { modernGLExecutorStats_t &stats = rg_modernGLExecutorStats; - if ( !stats.modernVisibleRequested ) { + if ( !stats.modernVisibleRequested + || !R_ModernGLExecutor_ModernVisibleRequested() ) { return false; } @@ -8197,7 +9465,7 @@ bool R_ModernGLExecutor_ModernVisiblePostProcessHandoffActive( void ) { void R_ModernGLExecutor_PrintGfxInfo( void ) { common->Printf( - "Modern GL executor: %s, cvar=%d, submitCvar=%d, gpuValidation=%d, visibleDepthCvar=%d, depthDebug=%d, opaqueCvar=%d, gbufferDebug=%d, deferredCvar=%d, deferredDebug=%d, VAO=%d, frameUBO=%d, shaderLibrary=%d, shaderPrograms=%d, highestGLSL=%d, drawPlan=%d, planDraws=%d, depth=%d, materialFamily=%d, planFallback=%d, batches=%d, submitPlan=%d, submitDraws=%d, submitFallback=%d, missingVBO=%d, missingIBO=%d, indexUpload=%d, submitted=%d/%d upload=%d fallback=%d, visibleDepth(req=%d exec=%d res=%d/%d draws=%d alpha=%d skinned=%d shadow=%d fallback=%d/%d stencil=%d mismatch=%d clears=%d resolves=%d overlay=%d/%d), gbuffer(req=%d exec=%d res=%d mrt=%d draws=%d fallback=%d alpha=%d skinned=%d clear=%d depth=%d/%d att=%d bpp=%d bw=%dKB overlay=%d/%d), deferred(req=%d exec=%d res=%d out=%d program=%d cluster=%d pixels=%d lights=%d point=%d projected=%d lightGrid=%d reads=%d fallback=%d unsupported=%d fog=%d special=%d overflow=%d clears=%d overlay=%d/%d), submitBatches(program=%d vbo=%d ibo=%d), gpuDriven=%d ssbo=%d indirect=%d validation=%d compute=%d sceneRecords=%d indirectRecords=%d gpuBytes(scene=%d indirect=%d validation=%d) dispatches=%d source=%d eligible=%d generated=%d culled=%d visible=%d cpu=%d/%d/%d gpu=%d/%d/%d clusters=%d/%d mismatches=%d readbacks=%d indirectExec=%d multiDraw=%d indirectCalls=%d, lowOverhead=%d dsa=%d multiBind=%d dsaUpdates=%d multiBindBatches=%d, restores=%d/%d, upload(frame=%d gpu=%d/%d fallback=%d/%d stream=%d/%d deferred=%d skipped=%d), legacyFallback=%d\n", + "Modern GL executor: %s, cvar=%d, submitCvar=%d, gpuValidation=%d, visibleDepthCvar=%d, depthDebug=%d, opaqueCvar=%d, gbufferDebug=%d, deferredCvar=%d, deferredDebug=%d, VAO=%d, frameUBO=%d, shaderLibrary=%d, shaderPrograms=%d, highestGLSL=%d, drawPlan=%d, planDraws=%d, depth=%d, materialFamily=%d, planFallback=%d, pbrOwners=%d, pbrConsumed=%d, batches=%d, submitPlan=%d, submitDraws=%d, submitFallback=%d, missingVBO=%d, missingIBO=%d, indexUpload=%d, submitted=%d/%d upload=%d fallback=%d, visibleDepth(req=%d exec=%d res=%d/%d draws=%d alpha=%d skinned=%d shadow=%d fallback=%d/%d stencil=%d mismatch=%d clears=%d resolves=%d overlay=%d/%d), gbuffer(req=%d exec=%d res=%d mrt=%d draws=%d fallback=%d alpha=%d skinned=%d clear=%d depth=%d/%d att=%d bpp=%d bw=%dKB overlay=%d/%d), deferred(req=%d exec=%d res=%d out=%d program=%d cluster=%d pixels=%d lights=%d point=%d projected=%d lightGrid=%d reads=%d fallback=%d unsupported=%d fog=%d special=%d overflow=%d clears=%d overlay=%d/%d), submitBatches(program=%d vbo=%d ibo=%d), gpuDriven=%d ssbo=%d indirect=%d validation=%d compute=%d sceneRecords=%d indirectRecords=%d gpuBytes(scene=%d indirect=%d validation=%d) dispatches=%d source=%d eligible=%d generated=%d culled=%d visible=%d cpu=%d/%d/%d gpu=%d/%d/%d clusters=%d/%d mismatches=%d readbacks=%d indirectExec=%d multiDraw=%d indirectCalls=%d, lowOverhead=%d dsa=%d multiBind=%d dsaUpdates=%d multiBindBatches=%d, restores=%d/%d, upload(frame=%d gpu=%d/%d fallback=%d/%d stream=%d/%d deferred=%d skipped=%d), legacyFallback=%d\n", rg_modernGLExecutorStats.available ? "available" : "unavailable", r_rendererModernExecutor.GetBool() ? 1 : 0, r_rendererModernSubmit.GetBool() ? 1 : 0, @@ -8218,6 +9486,8 @@ void R_ModernGLExecutor_PrintGfxInfo( void ) { rg_modernGLExecutorStats.drawPlanDepthDraws, rg_modernGLExecutorStats.drawPlanMaterialDraws, rg_modernGLExecutorStats.drawPlanFallbackDraws, + rg_modernGLExecutorStats.drawPlanPBRClusteredSurfaceOwners, + rg_modernGLExecutorStats.drawPlanPBRClusteredConsumedInteractions, rg_modernGLExecutorStats.drawPlanStateBatches, rg_modernGLExecutorStats.submitPlanReady ? 1 : 0, rg_modernGLExecutorStats.submitPlanDraws, @@ -8566,6 +9836,7 @@ void R_ModernGLExecutor_PrintGfxInfo( void ) { rg_modernGLExecutorStats.modernVisibleCinematicCompatibilityPasses ); R_ModernGLExecutor_PrintLightingOwnership( rg_modernGLExecutorStats ); R_ModernLightImageAtlas_PrintGfxInfo(); + R_ModernSpecularProbeAtlas_PrintGfxInfo(); R_GLStateCache_PrintGfxInfo(); R_ModernGLShaderLibrary_PrintGfxInfo(); } @@ -8607,6 +9878,42 @@ static bool R_ModernGLExecutor_DrawVertLayoutSelfTest( void ) { return true; } +static bool R_ModernGLExecutor_ClusteredDecalPolygonOffsetSelfTest( void ) { + materialResourceTableRecord_t materialRecord; + shaderStage_t stage; + memset( &materialRecord, 0, sizeof( materialRecord ) ); + memset( &stage, 0, sizeof( stage ) ); + + float polygonOffset = 99.0f; + const bool emptyRejected = !R_ModernGLExecutor_ClusteredDecalPolygonOffset( + &materialRecord, &stage, polygonOffset ) && polygonOffset == 0.0f; + + materialRecord.hasMaterialPolygonOffset = true; + materialRecord.polygonOffset = 2.0f; + const bool materialAccepted = R_ModernGLExecutor_ClusteredDecalPolygonOffset( + &materialRecord, &stage, polygonOffset ) && polygonOffset == 2.0f; + + stage.privatePolygonOffset = -3.0f; + const bool privateOverridesMaterial = R_ModernGLExecutor_ClusteredDecalPolygonOffset( + &materialRecord, &stage, polygonOffset ) && polygonOffset == -3.0f; + + materialRecord.hasMaterialPolygonOffset = false; + stage.privatePolygonOffset = 4.0f; + const bool privateAcceptedAlone = R_ModernGLExecutor_ClusteredDecalPolygonOffset( + &materialRecord, &stage, polygonOffset ) && polygonOffset == 4.0f; + + if ( !emptyRejected || !materialAccepted || !privateOverridesMaterial || !privateAcceptedAlone ) { + common->Printf( + "RendererModernGLExecutor self-test failed: clustered-decal polygon-offset precedence mismatch (empty=%d material=%d override=%d private=%d)\n", + emptyRejected ? 1 : 0, + materialAccepted ? 1 : 0, + privateOverridesMaterial ? 1 : 0, + privateAcceptedAlone ? 1 : 0 ); + return false; + } + return true; +} + static bool R_ModernGLExecutor_InitSelfTestTriangleGeometry( srfTriangles_t &geometry, vertCache_t &ambientCache, vertCache_t &indexCache, const char *selfTestName ) { memset( &geometry, 0, sizeof( geometry ) ); geometry.numVerts = 3; @@ -8663,6 +9970,7 @@ struct rendererModernGLSelfTestSurfaceScene_t { vertCache_t indexCache; viewEntity_t viewEntity; viewDef_t worldView; + float shaderRegisters[MAX_EXPRESSION_REGISTERS]; idDecl *ownedMaterialDecl; const idMaterial *ownedOpaqueMaterial; @@ -8725,6 +10033,52 @@ struct rendererModernGLSelfTestSurfaceScene_t { return ownedOpaqueMaterial; } + const idMaterial *InitPBROpaqueMaterial( const char *selfTestName ) { + if ( ownedOpaqueMaterial != NULL ) { + return ownedOpaqueMaterial; + } + if ( declManager == NULL ) { + common->Printf( "%s self-test failed: decl manager unavailable for synthetic PBR material\n", selfTestName ? selfTestName : "Renderer" ); + return NULL; + } + + ownedMaterialDecl = declManager->AllocateDecl( DECL_MATERIAL ); + idMaterial *material = static_cast( ownedMaterialDecl ); + const char *definition = + "{\n" + "\tbumpmap _flat\n" + "\tdiffusemap _white\n" + "\tspecularmap _black\n" + "\tpbr {\n" + "\t\tworkflow metallicRoughness\n" + "\t\talbedoMap _white\n" + "\t\tnormalMap _flat\n" + "\t\tnormalFormat tangentRG\n" + "\t\temissiveMap _white\n" + "\t\tmetallic 0.75\n" + "\t\troughness 0.35\n" + "\t\tao 0.8\n" + "\t\tnormalScale 0.9\n" + "\t\temissiveColor 0.1 0.2 0.3\n" + "\t}\n" + "}\n"; + if ( material == NULL || !material->Parse( definition, static_cast( strlen( definition ) ), true ) ) { + common->Printf( "%s self-test failed: could not parse synthetic PBR material\n", selfTestName ? selfTestName : "Renderer" ); + DeclManager_FreeAllocatedDecl( ownedMaterialDecl ); + ownedMaterialDecl = NULL; + return NULL; + } + if ( !material->HasPBR() || !material->IsDrawn() || material->Coverage() == MC_TRANSLUCENT || material->GetSort() >= SS_POST_PROCESS ) { + common->Printf( "%s self-test failed: synthetic PBR material is not opaque-compatible\n", selfTestName ? selfTestName : "Renderer" ); + DeclManager_FreeAllocatedDecl( ownedMaterialDecl ); + ownedMaterialDecl = NULL; + return NULL; + } + + ownedOpaqueMaterial = material; + return ownedOpaqueMaterial; + } + bool Init( const char *selfTestName, const int drawSurfCount, const idMaterial *materialOverride = NULL ) { if ( drawSurfCount <= 0 || drawSurfCount > static_cast( sizeof( drawSurfs ) / sizeof( drawSurfs[0] ) ) ) { common->Printf( "%s self-test failed: invalid synthetic draw-surface count %d\n", selfTestName ? selfTestName : "Renderer", drawSurfCount ); @@ -8734,16 +10088,27 @@ struct rendererModernGLSelfTestSurfaceScene_t { memset( drawSurfPtrs, 0, sizeof( drawSurfPtrs ) ); memset( &viewEntity, 0, sizeof( viewEntity ) ); memset( &worldView, 0, sizeof( worldView ) ); + memset( shaderRegisters, 0, sizeof( shaderRegisters ) ); if ( !R_ModernGLExecutor_InitSelfTestTriangleGeometry( geometry, ambientCache, indexCache, selfTestName ) ) { return false; } const idMaterial *material = materialOverride != NULL ? materialOverride : tr.defaultMaterial; + if ( material != NULL ) { + if ( material->GetNumRegisters() > MAX_EXPRESSION_REGISTERS ) { + common->Printf( "%s self-test failed: synthetic material register count exceeds capacity\n", selfTestName ? selfTestName : "Renderer" ); + return false; + } + float shaderParms[MAX_ENTITY_SHADER_PARMS]; + memset( shaderParms, 0, sizeof( shaderParms ) ); + material->EvaluateRegisters( shaderRegisters, shaderParms, &worldView ); + } for ( int i = 0; i < drawSurfCount; ++i ) { drawSurfs[i].geo = &geometry; drawSurfs[i].space = &viewEntity; if ( material != NULL ) { drawSurfs[i].material = material; drawSurfs[i].sort = material->GetSort(); + drawSurfs[i].shaderRegisters = shaderRegisters; } drawSurfPtrs[i] = &drawSurfs[i]; } @@ -8872,14 +10237,12 @@ static bool R_ModernGLExecutor_RunFrameSelfTest( void ) { return false; } R_ModernGLExecutor_UpdateFrameUBO( stats ); - const bool uploadStreamExercised = stats.frameUBOStreamed && stats.uploadManagerFrameUBOBytes > 0; - if ( ( stats.lowOverheadDSAUpdates <= 0 && !uploadStreamExercised ) || ( stats.lowOverheadMultiBindBatches <= 0 && !uploadStreamExercised ) ) { + R_ModernGLExecutor_BindFrameUniformBufferBase( stats ); + if ( stats.lowOverheadDSAUpdates <= 0 || stats.lowOverheadMultiBindBatches <= 0 ) { common->Printf( - "RendererModernGLExecutor self-test failed: GL45 DSA/multi-bind/upload-stream path was not exercised (dsa=%d multiBind=%d stream=%d bytes=%d)\n", + "RendererModernGLExecutor self-test failed: GL45 dedicated-frame-UBO DSA/multi-bind path was not exercised (dsa=%d multiBind=%d)\n", stats.lowOverheadDSAUpdates, - stats.lowOverheadMultiBindBatches, - uploadStreamExercised ? 1 : 0, - stats.uploadManagerFrameUBOBytes ); + stats.lowOverheadMultiBindBatches ); return false; } } @@ -8919,6 +10282,12 @@ bool RendererModernGLExecutor_RunSelfTest( void ) { if ( !R_ModernGLExecutor_DrawVertLayoutSelfTest() ) { return false; } + if ( !R_ModernGLExecutor_ClusteredDecalPolygonOffsetSelfTest() ) { + return false; + } + if ( !RendererSpecularProbeAtlas_RunSelfTest() ) { + return false; + } if ( !RendererModernGLShaderLibrary_RunSelfTest() ) { return false; } @@ -9752,9 +11121,175 @@ bool RendererGBuffer_RunSelfTest( void ) { return true; } +bool RendererPBRVisible_RunSelfTest( void ) { + const modernGLShaderLibraryStats_t &shaderStats = R_ModernGLShaderLibrary_Stats(); + if ( !shaderStats.available ) { + common->Printf( "RendererPBRVisible self-test passed (shader library unavailable)\n" ); + return true; + } + const modernGLShaderProgramInfo_t *opaqueProgram = R_ModernGLShaderLibrary_FindProgram( MODERN_GL_SHADER_GBUFFER_OPAQUE, shaderStats.highestGLSLVersion ); + const modernGLShaderProgramInfo_t *deferredProgram = R_ModernGLShaderLibrary_FindProgram( MODERN_GL_SHADER_DEFERRED_LIGHT_RESOLVE, shaderStats.highestGLSLVersion ); + const modernGLShaderProgramInfo_t *forwardProgram = R_ModernGLShaderLibrary_FindProgram( MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE, shaderStats.highestGLSLVersion ); + if ( opaqueProgram == NULL || opaqueProgram->program == 0 || !opaqueProgram->linked + || deferredProgram == NULL || deferredProgram->program == 0 || !deferredProgram->linked + || forwardProgram == NULL || forwardProgram->program == 0 || !forwardProgram->linked ) { + common->Printf( "RendererPBRVisible self-test failed: PBR shader corridor unavailable\n" ); + return false; + } + + struct rendererPBRVisibleCVarRestore_t { + idCVar &cvar; + bool oldValue; + rendererPBRVisibleCVarRestore_t( idCVar &value ) : cvar( value ), oldValue( value.GetBool() ) {} + ~rendererPBRVisibleCVarRestore_t() { cvar.SetBool( oldValue ); } + }; + rendererPBRVisibleCVarRestore_t restorePBR( r_pbrMaterials ); + rendererPBRVisibleCVarRestore_t restoreModernQuality( r_rendererModernQuality ); + rendererPBRVisibleCVarRestore_t restoreOpaque( r_rendererModernOpaque ); + rendererPBRVisibleCVarRestore_t restoreForwardPlus( r_rendererForwardPlus ); + r_pbrMaterials.SetBool( true ); + r_rendererModernQuality.SetBool( true ); + r_rendererModernOpaque.SetBool( true ); + r_rendererForwardPlus.SetBool( false ); + + idScenePacketFrame packetFrame; + idRenderGraph graph; + rendererModernGLSelfTestSurfaceScene_t scene; + const idMaterial *pbrMaterial = scene.InitPBROpaqueMaterial( "RendererPBRVisible" ); + if ( pbrMaterial == NULL || !R_ModernGLExecutor_BuildGBufferSelfTestFrame( packetFrame, graph, scene, pbrMaterial ) ) { + common->Printf( "RendererPBRVisible self-test failed: could not build PBR packet frame\n" ); + return false; + } + const materialResourceTableRecord_t *record = R_MaterialResourceTable_FindRecordForMaterial( pbrMaterial ); + if ( record == NULL || !record->hasPBR || !record->pbrResourceReady || !record->pbrModernReady + || !R_MaterialResourceTable_PBRModernPathEligible( *record ) + || record->pbrFallbackReason != MATERIAL_RESOURCE_PBR_FALLBACK_NONE ) { + common->Printf( "RendererPBRVisible self-test failed: PBR material-resource admission mismatch\n" ); + return false; + } + r_rendererModernQuality.SetBool( false ); + if ( R_MaterialResourceTable_PBRModernPathEligible( *record ) ) { + common->Printf( "RendererPBRVisible self-test failed: modern-quality rollback admitted PBR\n" ); + return false; + } + r_rendererModernQuality.SetBool( true ); + + idModernGLDrawPlan drawPlan; + drawPlan.Build( packetFrame, graph ); + idModernGLSubmitPlan submitPlan; + submitPlan.Build( drawPlan ); + const int expectedDraws = packetFrame.NumDrawPackets(); + if ( drawPlan.Stats().materialDraws != expectedDraws || submitPlan.Stats().materialReadyDraws != expectedDraws || submitPlan.NumCommands() != expectedDraws ) { + common->Printf( "RendererPBRVisible self-test failed: PBR draw/submit admission mismatch\n" ); + return false; + } + for ( int i = 0; i < submitPlan.NumCommands(); ++i ) { + const modernGLSubmitCommand_t &command = submitPlan.Command( i ); + float params[4]; + float flags[4]; + R_ModernGLExecutor_LocalParamsForCommand( command, params ); + R_ModernGLExecutor_MaterialFlagsForCommand( command, flags ); + if ( command.pipeline != MODERN_GL_DRAW_PLAN_PIPELINE_GBUFFER + || command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_MAIN] == 0 + || command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_NORMAL] == 0 + || command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_SPECULAR] == 0 + || command.materialTextureHandles[MODERN_GL_SUBMIT_MATERIAL_TEXTURE_EMISSIVE] == 0 + || idMath::Fabs( params[0] - 0.75f ) > 0.001f + || idMath::Fabs( params[1] - 0.35f ) > 0.001f + || idMath::Fabs( params[2] - 0.8f ) > 0.001f + || idMath::Fabs( params[3] - 0.9f ) > 0.001f + || flags[0] != 1.0f || flags[1] != 0.0f || flags[2] != 1.0f || flags[3] != 10.0f ) { + common->Printf( "RendererPBRVisible self-test failed: PBR shader-input packing mismatch\n" ); + return false; + } + } + + // One clustered shader invocation consumes the complete light list. Model + // two per-light interaction packets for each surface and prove that the + // stable ambient packet is the sole ordered surface owner. + r_rendererForwardPlus.SetBool( true ); + idScenePacketFrame clusteredFrame; + if ( !clusteredFrame.AddScene( &scene.worldView, true ) + || !clusteredFrame.AddPass( RENDER_PASS_ARB2_INTERACTION, true ) ) { + common->Printf( "RendererPBRVisible self-test failed: clustered interaction frame setup\n" ); + return false; + } + drawSurf_t interactionDrawSurfs[4]; + srfTriangles_t interactionTris[4]; + for ( int lightIndex = 0; lightIndex < 2; ++lightIndex ) { + for ( int surfaceIndex = 0; surfaceIndex < 2; ++surfaceIndex ) { + const int interactionIndex = lightIndex * 2 + surfaceIndex; + const drawSurf_t &ambientDrawSurf = scene.drawSurfs[surfaceIndex]; + interactionDrawSurfs[interactionIndex] = ambientDrawSurf; + interactionTris[interactionIndex] = *ambientDrawSurf.geo; + interactionTris[interactionIndex].ambientSurface = + const_cast( ambientDrawSurf.geo ); + if ( interactionTris[interactionIndex].numIndexes > 3 ) { + interactionTris[interactionIndex].numIndexes -= 3; + } + interactionDrawSurfs[interactionIndex].geo = + &interactionTris[interactionIndex]; + if ( !clusteredFrame.AddInteractionDrawPacket( + &interactionDrawSurfs[interactionIndex], interactionIndex, + NULL, lightIndex, SCENE_INTERACTION_RECEIVER_GLOBAL, surfaceIndex ) ) { + common->Printf( "RendererPBRVisible self-test failed: clustered interaction packet setup\n" ); + return false; + } + } + } + if ( !clusteredFrame.AddPass( RENDER_PASS_AMBIENT, true ) ) { + common->Printf( "RendererPBRVisible self-test failed: clustered ambient frame setup\n" ); + return false; + } + for ( int surfaceIndex = 0; surfaceIndex < 2; ++surfaceIndex ) { + if ( !clusteredFrame.AddDrawPacket( + &scene.drawSurfs[surfaceIndex], RENDER_PASS_AMBIENT, surfaceIndex ) ) { + common->Printf( "RendererPBRVisible self-test failed: clustered ambient packet setup\n" ); + return false; + } + } + clusteredFrame.FinishScene(); + idRenderGraph clusteredGraph; + R_RenderGraph_BuildFromScenePackets( clusteredFrame, clusteredGraph ); + R_MaterialResourceTable_PrepareFrame( clusteredFrame ); + idModernGLDrawPlan clusteredDrawPlan; + clusteredDrawPlan.Build( clusteredFrame, clusteredGraph ); + idModernGLSubmitPlan clusteredSubmitPlan; + clusteredSubmitPlan.Build( clusteredDrawPlan ); + const modernGLDrawPlanStats_t &clusteredStats = clusteredDrawPlan.Stats(); + if ( clusteredStats.sourceDrawPackets != 6 + || clusteredStats.materialDraws != 2 + || clusteredStats.pbrClusteredSurfaceOwners != 2 + || clusteredStats.pbrClusteredConsumedInteractions != 4 + || clusteredStats.fallbackDraws != 0 + || clusteredSubmitPlan.NumCommands() != 2 ) { + common->Printf( + "RendererPBRVisible self-test failed: clustered owner dedupe mismatch (source=%d material=%d owners=%d consumed=%d fallback=%d commands=%d)\n", + clusteredStats.sourceDrawPackets, + clusteredStats.materialDraws, + clusteredStats.pbrClusteredSurfaceOwners, + clusteredStats.pbrClusteredConsumedInteractions, + clusteredStats.fallbackDraws, + clusteredSubmitPlan.NumCommands() ); + return false; + } + for ( int commandIndex = 0; commandIndex < clusteredSubmitPlan.NumCommands(); ++commandIndex ) { + const modernGLSubmitCommand_t &command = clusteredSubmitPlan.Command( commandIndex ); + if ( command.passCategory != RENDER_PASS_AMBIENT + || command.pipeline != MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_OPAQUE ) { + common->Printf( "RendererPBRVisible self-test failed: clustered command is not the ambient surface owner\n" ); + return false; + } + } + + common->Printf( "RendererPBRVisible self-test passed (draws=%d gbuffer=1 deferred=1 forward=1 owners=2 consumed=4 packed=0 scalar=1)\n", expectedDraws ); + return true; +} + static bool R_ModernGLExecutor_ShadowBindingContractReady( const modernGLShaderProgramInfo_t *program, const char *selfTestName ) { - if ( program == NULL || program->program == 0 || !program->linked || !program->reflection.usesShadowTextures ) { - common->Printf( "%s self-test failed: shadow binding program contract unavailable\n", selfTestName ); + if ( program == NULL || program->program == 0 || !program->linked + || !program->reflection.usesShadowTextures || !program->reflection.usesSpecularProbes ) { + common->Printf( "%s self-test failed: shadow/probe binding program contract unavailable\n", selfTestName ); return false; } const char *shadowBindingUniforms[] = { @@ -9774,6 +11309,33 @@ static bool R_ModernGLExecutor_ShadowBindingContractReady( const modernGLShaderP return false; } } + const GLint specularProbeAtlasLocation = glGetUniformLocation != NULL + ? glGetUniformLocation( program->program, "uModernSpecularProbeAtlas" ) + : -1; + if ( specularProbeAtlasLocation < 0 ) { + common->Printf( "%s self-test failed: missing specular-probe atlas sampler\n", selfTestName ); + return false; + } + if ( glGetUniformBlockIndex == NULL || glGetActiveUniformBlockiv == NULL ) { + common->Printf( "%s self-test failed: specular-probe UBO introspection unavailable\n", selfTestName ); + return false; + } + const GLuint specularProbeBlockIndex = glGetUniformBlockIndex( program->program, "ModernSpecularProbeRecords" ); + if ( specularProbeBlockIndex == GL_INVALID_INDEX ) { + common->Printf( "%s self-test failed: missing specular-probe UBO block\n", selfTestName ); + return false; + } + GLint specularProbeBlockBytes = 0; + glGetActiveUniformBlockiv( program->program, specularProbeBlockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &specularProbeBlockBytes ); + const int expectedSpecularProbeBlockBytes = R_ModernClusteredLighting_ProbeUboBlockBytes(); + if ( specularProbeBlockBytes <= 0 || specularProbeBlockBytes != expectedSpecularProbeBlockBytes ) { + common->Printf( + "%s self-test failed: specular-probe UBO layout drift (linked=%d bytes, cpu=%d bytes)\n", + selfTestName, + specularProbeBlockBytes, + expectedSpecularProbeBlockBytes ); + return false; + } // std140 layout introspection (M5): the CPU GPU-record struct and the // GLSL shadow-descriptor block are hand-synced; ask the driver for the // linked block size on the UBO path and reject drift instead of letting diff --git a/src/renderer/ModernGLExecutor.h b/src/renderer/ModernGLExecutor.h index 241fb80b..1319bc7e 100644 --- a/src/renderer/ModernGLExecutor.h +++ b/src/renderer/ModernGLExecutor.h @@ -278,6 +278,8 @@ typedef struct modernGLExecutorStats_s { int drawPlanDepthDraws; int drawPlanMaterialDraws; int drawPlanFallbackDraws; + int drawPlanPBRClusteredSurfaceOwners; + int drawPlanPBRClusteredConsumedInteractions; int drawPlanGeometryFallbackDraws; int drawPlanGeometryDeformFallbackDraws; int drawPlanGeometrySkinnedFallbackDraws; @@ -489,7 +491,7 @@ void R_ModernGLExecutor_PrepareFrame( const idScenePacketFrame &packetFrame, con void R_ModernGLExecutor_DrawDepthDebugOverlay( void ); void R_ModernGLExecutor_DrawGBufferDebugOverlay( void ); void R_ModernGLExecutor_DrawDeferredDebugOverlay( void ); -void R_ModernGLExecutor_SubmitForwardPlusDecalOverlay( const viewDef_t *viewDef ); +bool R_ModernGLExecutor_SubmitForwardPlusDecalSurface( const viewDef_t *viewDef, const drawSurf_t *sourceSurface ); void R_ModernGLExecutor_ComposeVisibleSceneForPost( void ); void R_ModernGLExecutor_ComposeVisibleFrame( void ); const modernGLExecutorStats_t &R_ModernGLExecutor_Stats( void ); @@ -503,6 +505,7 @@ bool RendererModernGLExecutor_RunSelfTest( void ); bool RendererGpuDriven_RunSelfTest( void ); bool RendererVisiblePath_RunSelfTest( void ); bool RendererGBuffer_RunSelfTest( void ); +bool RendererPBRVisible_RunSelfTest( void ); bool RendererDeferredResolve_RunSelfTest( void ); bool RendererForwardPlus_RunSelfTest( void ); bool RendererModernVisible_RunSelfTest( void ); diff --git a/src/renderer/ModernGLShaderLibrary.cpp b/src/renderer/ModernGLShaderLibrary.cpp index 57993e3d..3c4910a9 100644 --- a/src/renderer/ModernGLShaderLibrary.cpp +++ b/src/renderer/ModernGLShaderLibrary.cpp @@ -5,6 +5,25 @@ #include "GLDebugScope.h" #include "MaterialResourceTable.h" #include "ModernGLShaderLibrary.h" +#include "ModernSpecularProbeAtlas.h" + +static_assert( MODERN_SPECULAR_PROBE_ATLAS_SIZE == 2048, + "authored-probe GLSL atlas-size ABI drift" ); +static_assert( MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE == 256, + "authored-probe GLSL face-size ABI drift" ); +static_assert( MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW == 8, + "authored-probe GLSL cell-grid ABI drift" ); +static_assert( MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT == 6, + "authored-probe GLSL face-count ABI drift" ); +static_assert( MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES == 8, + "authored-probe GLSL slot-count ABI drift" ); +static_assert( MODERN_SPECULAR_PROBE_FACE_POSITIVE_X == 0 + && MODERN_SPECULAR_PROBE_FACE_NEGATIVE_X == 1 + && MODERN_SPECULAR_PROBE_FACE_POSITIVE_Y == 2 + && MODERN_SPECULAR_PROBE_FACE_NEGATIVE_Y == 3 + && MODERN_SPECULAR_PROBE_FACE_POSITIVE_Z == 4 + && MODERN_SPECULAR_PROBE_FACE_NEGATIVE_Z == 5, + "authored-probe GLSL face-order ABI drift" ); static modernGLShaderLibraryStats_t rg_modernGLShaderLibraryStats; static modernGLShaderProgramInfo_t rg_modernGLShaderPrograms[MODERN_GL_SHADER_MAX_PROGRAMS]; @@ -305,6 +324,9 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "uniform sampler2D uNormalTexture;\n" "uniform sampler2D uSpecularTexture;\n" "uniform sampler2D uEmissiveTexture;\n" + "uniform sampler2D uMetallicTexture;\n" + "uniform sampler2D uRoughnessTexture;\n" + "uniform sampler2D uAOTexture;\n" "#define MODERN_HAS_TEXTURE_TABLE %d\n" "#define MODERN_MATERIAL_TEXTURE_TABLE_SIZE %d\n" "#if MODERN_HAS_TEXTURE_TABLE\n" @@ -338,7 +360,10 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " if (uTextureTableMode != 0u) { return texture(uMaterialTextures[int(ModernTextureTableIndex(uTextureIndices.w))], uv); }\n" "#endif\n" " return texture(uEmissiveTexture, uv);\n" - "}\n", + "}\n" + "vec4 ModernSampleMetallicTexture(vec2 uv) { return texture(uMetallicTexture, uv); }\n" + "vec4 ModernSampleRoughnessTexture(vec2 uv) { return texture(uRoughnessTexture, uv); }\n" + "vec4 ModernSampleAOTexture(vec2 uv) { return texture(uAOTexture, uv); }\n", hasTextureTable, MATERIAL_RESOURCE_TABLE_TEXTURE_ARRAY_CAPACITY, MATERIAL_RESOURCE_TABLE_TEXTURE_ARRAY_CAPACITY ); @@ -419,6 +444,12 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "vec3 ModernMaterialTangentNormal(void) {\n" " if (uMaterialFlags.x <= 0.5) { return vec3(0.0, 0.0, 1.0); }\n" " vec4 bumpSample = ModernSampleNormalTexture(vTexCoord);\n" + " if (uMaterialFlags.w >= 7.5) {\n" + " int normalFormat = int(floor(uMaterialFlags.w - 9.0 + 0.5));\n" + " if (normalFormat == 1) { vec2 xy = (bumpSample.rg * 2.0 - 1.0) * max(uLocalParams.w, 0.0); float z = sqrt(max(1.0 - dot(xy, xy), 0.0)); return ModernSafeNormal(vec3(xy, z)); }\n" + " if (normalFormat == 2) { return ModernSafeNormal((bumpSample.rgb * 2.0 - 1.0) * vec3(max(uLocalParams.w, 0.0), max(uLocalParams.w, 0.0), 1.0)); }\n" + " return ModernSafeNormal(ModernDecodeClassicNormal(bumpSample));\n" + " }\n" " return uMaterialEnhancement.x > 0.5 ? ModernDecodeEnhancedNormal(bumpSample) : ModernDecodeClassicNormal(bumpSample);\n" "}\n" "vec3 ModernMaterialNormal(void) {\n" @@ -439,6 +470,17 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "vec3 ModernEmissiveColor(void) {\n" " return uMaterialFlags.z > 0.5 ? ModernSampleEmissiveTexture(vTexCoord).rgb : vec3(0.0);\n" "}\n" + "bool ModernIsPBRMaterial(void) { return uMaterialFlags.w >= 7.5; }\n" + "vec3 ModernPBRBaseColor(void) { return pow(max(ModernSampleMainTexture(vTexCoord).rgb, vec3(0.0)), vec3(2.2)); }\n" + "vec3 ModernPBRMaterialData(void) {\n" + " vec3 orm = uMaterialFlags.y > 0.5 ? ModernSampleSpecularTexture(vTexCoord).rgb : vec3(1.0);\n" + " if (uMaterialFlags.y < -0.5) { orm = vec3(ModernSampleAOTexture(vTexCoord).r, ModernSampleRoughnessTexture(vTexCoord).r, ModernSampleMetallicTexture(vTexCoord).r); }\n" + " return vec3(clamp(orm.b * uLocalParams.x, 0.0, 1.0), clamp(orm.g * uLocalParams.y, 0.02, 1.0), clamp(orm.r * uLocalParams.z, 0.0, 1.0));\n" + "}\n" + "vec3 ModernPBREmissiveColor(void) {\n" + " vec3 texel = uMaterialFlags.z > 0.5 ? ModernSampleEmissiveTexture(vTexCoord).rgb : vec3(0.0);\n" + " return pow(max(texel, vec3(0.0)), vec3(2.2)) * max(uDebugColor.rgb, vec3(0.0));\n" + "}\n" "float ModernNormalLightScale(void) {\n" " vec3 lightDir = normalize(vec3(0.25, 0.35, 1.0));\n" " return clamp(dot(ModernMaterialNormal(), lightDir) * 0.5 + 0.5, 0.18, 1.0);\n" @@ -453,6 +495,12 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "#define MODERN_CLUSTER_UBO_MAX_LIGHTS 256\n" "#define MODERN_CLUSTER_UBO_MAX_INDEX_RECORDS 1024\n" "#define MODERN_CLUSTER_UBO_MAX_SHADOW_DESCRIPTORS 64\n" + "#define MODERN_SPECULAR_PROBE_MAX_RECORDS 32\n" + "#define MODERN_SPECULAR_PROBE_ATLAS_SIZE 2048\n" + "#define MODERN_SPECULAR_PROBE_FACE_SIZE 256\n" + "#define MODERN_SPECULAR_PROBE_CELLS_PER_ROW 8\n" + "#define MODERN_SPECULAR_PROBE_FACE_COUNT 6\n" + "#define MODERN_SPECULAR_PROBE_ATLAS_SLOTS 8\n" "struct ModernClusterLightRecord {\n" " vec4 positionRadius;\n" " vec4 worldOriginRadius;\n" @@ -482,6 +530,14 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " vec4 projection;\n" " vec4 projectedAtlasRect[4];\n" "};\n" + "struct ModernSpecularProbeRecord {\n" + " vec4 positionRadius;\n" + " vec4 tintIntensity;\n" + " vec4 axisXPriority;\n" + " vec4 axisYBlend;\n" + " vec4 axisZSlot;\n" + " vec4 identity;\n" + "};\n" "layout(std140) uniform ModernClusterGridParams {\n" " vec4 grid;\n" " vec4 depth;\n" @@ -493,7 +549,11 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " vec4 projection;\n" " vec4 projectionDepth;\n" "} uClusterGrid;\n" + "layout(std140) uniform ModernSpecularProbeRecords {\n" + " ModernSpecularProbeRecord probes[MODERN_SPECULAR_PROBE_MAX_RECORDS];\n" + "} uModernSpecularProbes;\n" "uniform sampler2D uModernLightImageAtlas;\n" + "uniform sampler2D uModernSpecularProbeAtlas;\n" "uniform sampler2D uModernShadowAtlas;\n" "uniform samplerCube uModernPointShadowAtlas;\n" "uniform sampler2D uModernTranslucentShadowMoments[3];\n" @@ -502,6 +562,10 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "uniform vec4 uModernShadowSamplerState;\n" "uniform vec4 uModernShadowMomentState;\n" "uniform vec4 uModernShadowContractState;\n" + "// x=enabled, y=intensity. This is deliberately global rather than\n" + "// material data: it is a stable lighting environment shared by every\n" + "// explicitly admitted PBR record in the current view.\n" + "uniform vec4 uPBRIBL;\n" "#if MODERN_HAS_SHADER_STORAGE\n" "layout(std430, binding = 6) readonly buffer ModernLightRecords {\n" " ModernClusterLightRecord lights[];\n" @@ -729,6 +793,172 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " float shading = ambient ? 1.0 : (ndotl + spec);\n" " attenuation = supported * max(max(shaped.r, shaped.g), shaped.b) * shading;\n" " return light.colorType.rgb * shaped * shading * supported;\n" + "}\n" + "vec3 ModernClusterEvaluatePBRLight(ModernClusterLightRecord light, vec3 viewPosition, vec3 normal, vec3 baseColor, float metallic, float roughness, out float attenuation) {\n" + " int type = int(floor(light.colorType.w + 0.5));\n" + " bool projected = type == 1; bool point = type == 0; bool ambient = type == 3;\n" + " vec3 toLight = light.positionRadius.xyz - viewPosition; float dist = length(toLight);\n" + " vec3 lightDir = dist > 0.0001 ? toLight / dist : vec3(0.0, 0.0, 1.0);\n" + " float radius = max(light.positionRadius.w, 1.0); float radial = clamp(1.0 - dist / radius, 0.0, 1.0); radial *= radial;\n" + " vec3 projection = projected ? ModernClusterProjectionColor(light, viewPosition) : vec3(1.0);\n" + " vec3 radiance = light.colorType.rgb * projection * ModernClusterFalloffColor(light, viewPosition, radial);\n" + " if (ambient) { attenuation = max(max(radiance.r, radiance.g), radiance.b); return radiance * baseColor; }\n" + " if (!(point || projected)) { attenuation = 0.0; return vec3(0.0); }\n" + " vec3 viewDir = length(viewPosition) > 0.0001 ? normalize(-viewPosition) : vec3(0.0, 0.0, -1.0); vec3 halfDir = normalize(lightDir + viewDir);\n" + " float ndotl = max(dot(normal, lightDir), 0.0); float ndotv = max(dot(normal, viewDir), 0.0);\n" + " float ndoth = max(dot(normal, halfDir), 0.0); float vdoth = max(dot(viewDir, halfDir), 0.0);\n" + " float alpha = max(roughness * roughness, 0.0004); float alpha2 = alpha * alpha;\n" + " float denom = max(3.14159265 * pow(max(ndoth * ndoth * (alpha2 - 1.0) + 1.0, 0.0001), 2.0), 0.0001); float distribution = alpha2 / denom;\n" + " float k = (roughness + 1.0); k = k * k * 0.125; float visibility = 1.0 / max((ndotv * (1.0 - k) + k) * (ndotl * (1.0 - k) + k), 0.0001);\n" + " vec3 f0 = mix(vec3(0.04), baseColor, metallic); vec3 fresnel = f0 + (vec3(1.0) - f0) * pow(1.0 - vdoth, 5.0);\n" + " vec3 specular = distribution * visibility * fresnel; vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic) * baseColor * (1.0 / 3.14159265);\n" + " attenuation = max(max(radiance.r, radiance.g), radiance.b) * ndotl; return (diffuse + specular) * radiance * ndotl;\n" + "}\n" + "vec3 ModernPBRAnalyticEnvironment(vec3 direction) {\n" + " // A neutral, analytic studio hemisphere provides stable PBR indirect\n" + " // light without requiring replacement cubemaps or a probe format.\n" + " float up = smoothstep(-0.55, 0.70, normalize(direction).y);\n" + " vec3 ground = vec3(0.025, 0.022, 0.020);\n" + " vec3 sky = vec3(0.46, 0.53, 0.68);\n" + " vec3 environment = mix(ground, sky, up);\n" + " vec3 keyDirection = normalize(vec3(-0.35, 0.62, 0.70));\n" + " environment += vec3(1.30, 1.16, 0.96) * pow(max(dot(normalize(direction), keyDirection), 0.0), 48.0);\n" + " // The current modern corridor is scene-referred and has no mandatory\n" + " // tonemap handoff, so retain a calibrated HDR range here rather than\n" + " // making default PBR metals disappear into the legacy black floor.\n" + " return environment * 4.0;\n" + "}\n" + "bool ModernSpecularProbeExactInteger(float value, float minimumValue, float maximumValue) {\n" + " return !isnan(value) && !isinf(value) && value >= minimumValue && value <= maximumValue && value == floor(value);\n" + "}\n" + "bool ModernSpecularProbeFiniteVec3(vec3 value) {\n" + " return !any(isnan(value)) && !any(isinf(value));\n" + "}\n" + "bool ModernSpecularProbeAtlasReady(void) {\n" + " return all(equal(textureSize(uModernSpecularProbeAtlas, 0), ivec2(MODERN_SPECULAR_PROBE_ATLAS_SIZE)));\n" + "}\n" + "bool ModernSpecularProbeCount(out uint probeCount) {\n" + " probeCount = 0u;\n" + " float countValue = uClusterGrid.projectionDepth.z;\n" + " if (!ModernSpecularProbeExactInteger(countValue, 0.0, float(MODERN_SPECULAR_PROBE_MAX_RECORDS))) { return false; }\n" + " probeCount = uint(countValue);\n" + " return probeCount > 0u;\n" + "}\n" + "bool ModernSpecularProbeRecordReady(ModernSpecularProbeRecord probe) {\n" + " float frameGeneration = uClusterGrid.projectionDepth.w;\n" + " if (!ModernSpecularProbeExactInteger(frameGeneration, 1.0, 16777215.0)) { return false; }\n" + " if (!ModernSpecularProbeExactInteger(probe.identity.x, 0.0, 16777215.0)\n" + " || !ModernSpecularProbeExactInteger(probe.identity.y, 1.0, 16777215.0)\n" + " || !ModernSpecularProbeExactInteger(probe.identity.z, 1.0, 16777215.0)\n" + " || !ModernSpecularProbeExactInteger(probe.identity.w, 1.0, 16777215.0)\n" + " || probe.identity.w != frameGeneration) { return false; }\n" + " if (!ModernSpecularProbeFiniteVec3(probe.positionRadius.xyz) || isnan(probe.positionRadius.w) || isinf(probe.positionRadius.w) || probe.positionRadius.w <= 0.0) { return false; }\n" + " if (!ModernSpecularProbeFiniteVec3(probe.tintIntensity.rgb) || any(lessThan(probe.tintIntensity.rgb, vec3(0.0)))\n" + " || any(greaterThan(probe.tintIntensity.rgb, vec3(64.0)))\n" + " || isnan(probe.tintIntensity.w) || isinf(probe.tintIntensity.w)\n" + " || probe.tintIntensity.w <= 0.0 || probe.tintIntensity.w > 64.0) { return false; }\n" + " if (!ModernSpecularProbeFiniteVec3(probe.axisXPriority.xyz)\n" + " || !ModernSpecularProbeFiniteVec3(probe.axisYBlend.xyz)\n" + " || !ModernSpecularProbeFiniteVec3(probe.axisZSlot.xyz)) { return false; }\n" + " if (!ModernSpecularProbeExactInteger(probe.axisXPriority.w, 0.0, 255.0)\n" + " || isnan(probe.axisYBlend.w) || isinf(probe.axisYBlend.w)\n" + " || probe.axisYBlend.w <= 0.0 || probe.axisYBlend.w > 1.0\n" + " || !ModernSpecularProbeExactInteger(probe.axisZSlot.w, 0.0, float(MODERN_SPECULAR_PROBE_ATLAS_SLOTS - 1))) { return false; }\n" + " float axisXLength2 = dot(probe.axisXPriority.xyz, probe.axisXPriority.xyz);\n" + " float axisYLength2 = dot(probe.axisYBlend.xyz, probe.axisYBlend.xyz);\n" + " float axisZLength2 = dot(probe.axisZSlot.xyz, probe.axisZSlot.xyz);\n" + " if (axisXLength2 <= 0.000001 || axisYLength2 <= 0.000001 || axisZLength2 <= 0.000001) { return false; }\n" + " vec3 axisX = probe.axisXPriority.xyz * inversesqrt(axisXLength2);\n" + " vec3 axisY = probe.axisYBlend.xyz * inversesqrt(axisYLength2);\n" + " vec3 axisZ = probe.axisZSlot.xyz * inversesqrt(axisZLength2);\n" + " float determinant = dot(cross(axisX, axisY), axisZ);\n" + " return abs(dot(axisX, axisY)) < 0.01 && abs(dot(axisX, axisZ)) < 0.01\n" + " && abs(dot(axisY, axisZ)) < 0.01 && abs(determinant) > 0.99;\n" + "}\n" + "vec3 ModernSpecularProbeSampleAtlas(int slot, vec3 direction) {\n" + " vec3 d = normalize(direction);\n" + " vec3 ad = abs(d);\n" + " int face = 0;\n" + " vec2 faceCoordinate = vec2(0.0);\n" + " float majorAxis = 1.0;\n" + " if (ad.x >= ad.y && ad.x >= ad.z) {\n" + " majorAxis = ad.x;\n" + " if (d.x >= 0.0) { face = 0; faceCoordinate = vec2(-d.z, -d.y); }\n" + " else { face = 1; faceCoordinate = vec2(d.z, -d.y); }\n" + " } else if (ad.y >= ad.z) {\n" + " majorAxis = ad.y;\n" + " if (d.y >= 0.0) { face = 2; faceCoordinate = vec2(d.x, d.z); }\n" + " else { face = 3; faceCoordinate = vec2(d.x, -d.z); }\n" + " } else {\n" + " majorAxis = ad.z;\n" + " if (d.z >= 0.0) { face = 4; faceCoordinate = vec2(d.x, -d.y); }\n" + " else { face = 5; faceCoordinate = vec2(-d.x, -d.y); }\n" + " }\n" + " vec2 faceUV = clamp(faceCoordinate / max(majorAxis, 0.000001) * 0.5 + 0.5, vec2(0.0), vec2(1.0));\n" + " int cell = slot * MODERN_SPECULAR_PROBE_FACE_COUNT + face;\n" + " ivec2 cellIndex = ivec2(cell % MODERN_SPECULAR_PROBE_CELLS_PER_ROW, cell / MODERN_SPECULAR_PROBE_CELLS_PER_ROW);\n" + " vec2 atlasTexel = vec2(cellIndex * MODERN_SPECULAR_PROBE_FACE_SIZE) + vec2(0.5)\n" + " + faceUV * float(MODERN_SPECULAR_PROBE_FACE_SIZE - 1);\n" + " vec2 atlasUV = atlasTexel / float(MODERN_SPECULAR_PROBE_ATLAS_SIZE);\n" + " return pow(max(textureLod(uModernSpecularProbeAtlas, atlasUV, 0.0).rgb, vec3(0.0)), vec3(2.2));\n" + "}\n" + "bool ModernSpecularProbeContribution(uint probeIndex, uint probeCount, vec3 viewPosition, vec3 reflectionDirection, out vec3 radiance, out float weight) {\n" + " radiance = vec3(0.0); weight = 0.0;\n" + " if (probeIndex == 0xffffffffu || probeIndex >= probeCount || probeIndex >= uint(MODERN_SPECULAR_PROBE_MAX_RECORDS)) { return false; }\n" + " ModernSpecularProbeRecord probe = uModernSpecularProbes.probes[int(probeIndex)];\n" + " if (!ModernSpecularProbeRecordReady(probe)) { return false; }\n" + " float distanceToProbe = length(viewPosition - probe.positionRadius.xyz);\n" + " float radius = probe.positionRadius.w;\n" + " if (isnan(distanceToProbe) || isinf(distanceToProbe) || distanceToProbe > radius) { return false; }\n" + " float blendWidth = max(radius * probe.axisYBlend.w, 0.000001);\n" + " weight = clamp((radius - distanceToProbe) / blendWidth, 0.0, 1.0);\n" + " if (weight <= 0.0) { return false; }\n" + " vec3 localDirection = vec3(\n" + " dot(reflectionDirection, normalize(probe.axisXPriority.xyz)),\n" + " dot(reflectionDirection, normalize(probe.axisYBlend.xyz)),\n" + " dot(reflectionDirection, normalize(probe.axisZSlot.xyz)));\n" + " if (dot(localDirection, localDirection) <= 0.000001) { return false; }\n" + " int slot = int(probe.axisZSlot.w);\n" + " radiance = ModernSpecularProbeSampleAtlas(slot, localDirection) * probe.tintIntensity.rgb * probe.tintIntensity.w;\n" + " return true;\n" + "}\n" + "bool ModernSpecularProbeEnvironment(uvec4 clusterRange, vec3 viewPosition, vec3 reflectionDirection, out vec3 environment, out float coverage) {\n" + " environment = vec3(0.0); coverage = 0.0;\n" + " uint probeCount = 0u;\n" + " if (!ModernSpecularProbeAtlasReady() || !ModernSpecularProbeCount(probeCount)) { return false; }\n" + " float totalWeight = 0.0;\n" + " for (int candidate = 0; candidate < 2; ++candidate) {\n" + " uint probeIndex = candidate == 0 ? clusterRange.z : clusterRange.w;\n" + " if (candidate == 1 && probeIndex == clusterRange.z) { continue; }\n" + " vec3 radiance = vec3(0.0); float weight = 0.0;\n" + " if (!ModernSpecularProbeContribution(probeIndex, probeCount, viewPosition, reflectionDirection, radiance, weight)) { continue; }\n" + " environment += radiance * weight; totalWeight += weight;\n" + " }\n" + " if (totalWeight <= 0.000001) { environment = vec3(0.0); return false; }\n" + " environment /= totalWeight;\n" + " coverage = clamp(totalWeight, 0.0, 1.0);\n" + " return true;\n" + "}\n" + "vec3 ModernPBRIndirect(vec3 viewPosition, vec3 normal, vec3 baseColor, float metallic, float roughness, float ao, uvec4 clusterRange) {\n" + " vec3 n = normalize(normal);\n" + " vec3 viewDir = length(viewPosition) > 0.0001 ? normalize(-viewPosition) : vec3(0.0, 0.0, -1.0);\n" + " float ndotv = clamp(dot(n, viewDir), 0.0, 1.0);\n" + " vec3 f0 = mix(vec3(0.04), baseColor, metallic);\n" + " vec3 fresnel = f0 + (max(vec3(1.0 - roughness), f0) - f0) * pow(1.0 - ndotv, 5.0);\n" + " vec3 legacyIndirect = baseColor * vec3(0.12) * ao * (1.0 - metallic);\n" + " vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic) * baseColor * ModernPBRAnalyticEnvironment(n);\n" + " vec3 reflection = reflect(-viewDir, n);\n" + " vec3 analyticPrefiltered = ModernPBRAnalyticEnvironment(normalize(mix(reflection, n, roughness * roughness)));\n" + " vec3 prefiltered = analyticPrefiltered;\n" + " vec3 probeEnvironment = vec3(0.0); float probeCoverage = 0.0;\n" + " if (ModernSpecularProbeEnvironment(clusterRange, viewPosition, reflection, probeEnvironment, probeCoverage)) {\n" + " vec3 analyticSharp = ModernPBRAnalyticEnvironment(normalize(reflection));\n" + " vec3 edgeBlendedProbe = mix(analyticSharp, probeEnvironment, probeCoverage);\n" + " prefiltered = mix(edgeBlendedProbe, analyticPrefiltered, clamp(roughness, 0.0, 1.0));\n" + " }\n" + " vec3 specular = prefiltered * fresnel * mix(1.0, 0.28, roughness);\n" + " vec3 analyticIndirect = (diffuse + specular) * ao * max(uPBRIBL.y, 0.0);\n" + " return mix(legacyIndirect, analyticIndirect, clamp(uPBRIBL.x, 0.0, 1.0));\n" "}\n"; const char *shadowPolicyHeader = "#define MODERN_SHADOW_MAP_PROJECTED 1.0\n" @@ -963,7 +1193,7 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " if (policy == MODERN_SHADOW_POLICY_MAPPED && descriptorPolicy != MODERN_SHADOW_POLICY_MAPPED) { return ModernClusterShadowBrokenVisibility(); }\n" " if (policy == MODERN_SHADOW_POLICY_CACHE_REUSE && descriptorPolicy != MODERN_SHADOW_POLICY_CACHE_REUSE) { return ModernClusterShadowBrokenVisibility(); }\n" " float mapType = floor(descriptor.identity.w + 0.5);\n" - " if (mapType == MODERN_SHADOW_MAP_POINT) { return ModernClusterSamplePointShadow(descriptor, light, viewPosition, normal); }\n" + " if (mapType == MODERN_SHADOW_MAP_POINT && ModernClusterShadowFlag(descriptor, MODERN_SHADOW_FLAG_ATLAS_SLOT) && descriptor.freshness.y > 0.5) { return ModernClusterSamplePointShadow(descriptor, light, viewPosition, normal); }\n" " if ((mapType == MODERN_SHADOW_MAP_PROJECTED || mapType == MODERN_SHADOW_MAP_CASCADE) && ModernClusterShadowFlag(descriptor, MODERN_SHADOW_FLAG_PROJECTED_STATE_READY) && ModernClusterShadowFlag(descriptor, MODERN_SHADOW_FLAG_ATLAS_SLOT) && descriptor.freshness.y > 0.5) { return ModernClusterSampleProjectedShadow(descriptor, light, viewPosition, normal); }\n" " return ModernClusterShadowBrokenVisibility();\n" "}\n"; @@ -1019,6 +1249,12 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "vec3 ModernMaterialTangentNormal(void) {\n" " if (uMaterialFlags.x <= 0.5) { return vec3(0.0, 0.0, 1.0); }\n" " vec4 bumpSample = ModernSampleNormalTexture(vTexCoord);\n" + " if (uMaterialFlags.w >= 7.5) {\n" + " int normalFormat = int(floor(uMaterialFlags.w - 9.0 + 0.5));\n" + " if (normalFormat == 1) { vec2 xy = (bumpSample.rg * 2.0 - 1.0) * max(uLocalParams.w, 0.0); float z = sqrt(max(1.0 - dot(xy, xy), 0.0)); return ModernSafeNormal(vec3(xy, z)); }\n" + " if (normalFormat == 2) { return ModernSafeNormal((bumpSample.rgb * 2.0 - 1.0) * vec3(max(uLocalParams.w, 0.0), max(uLocalParams.w, 0.0), 1.0)); }\n" + " return ModernSafeNormal(ModernDecodeClassicNormal(bumpSample));\n" + " }\n" " return uMaterialEnhancement.x > 0.5 ? ModernDecodeEnhancedNormal(bumpSample) : ModernDecodeClassicNormal(bumpSample);\n" "}\n" "vec3 ModernMaterialNormal(void) {\n" @@ -1035,14 +1271,22 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "}\n" "void main() {\n" " vec4 texel = ModernSampleMainTexture(vTexCoord);\n" - " vec3 baseColor = texel.rgb * max(uDebugColor.rgb, vec3(0.0));\n" + " bool pbr = uMaterialFlags.w >= 7.5;\n" + " vec3 baseColor = pbr ? pow(max(texel.rgb, vec3(0.0)), vec3(2.2)) : texel.rgb * max(uDebugColor.rgb, vec3(0.0));\n" " vec3 normal = ModernMaterialNormal();\n" " float specular = ModernSpecularStrength();\n" - " vec3 emissive = uMaterialFlags.z > 0.5 ? ModernSampleEmissiveTexture(vTexCoord).rgb : vec3(uLocalParams.w);\n" + " vec3 orm = uMaterialFlags.y > 0.5 ? ModernSampleSpecularTexture(vTexCoord).rgb : vec3(1.0);\n" + " if (pbr && uMaterialFlags.y < -0.5) { orm = vec3(ModernSampleAOTexture(vTexCoord).r, ModernSampleRoughnessTexture(vTexCoord).r, ModernSampleMetallicTexture(vTexCoord).r); }\n" + " float metallic = pbr ? clamp(orm.b * uLocalParams.x, 0.0, 1.0) : 0.04;\n" + " float roughness = pbr ? clamp(orm.g * uLocalParams.y, 0.02, 1.0) : specular;\n" + " float ao = pbr ? clamp(orm.r, 0.0, 1.0) * clamp(uLocalParams.z, 0.0, 1.0) : uLocalParams.z;\n" + " vec3 emissive = uMaterialFlags.z > 0.5 ? ModernSampleEmissiveTexture(vTexCoord).rgb : vec3(0.0);\n" + " if (pbr) { emissive = pow(max(emissive, vec3(0.0)), vec3(2.2)) * max(uDebugColor.rgb, vec3(0.0)); } else { emissive += vec3(uLocalParams.w); }\n" " out_Albedo = vec4(baseColor, 1.0);\n" " out_Normal = vec4(normal * 0.5 + 0.5, vTangentSign * 0.5 + 0.5);\n" - " out_Material = vec4(0.04, specular, uLocalParams.z, ModernMaterialFresnel());\n" - " out_Emissive = vec4(emissive, 1.0);\n" + " out_Material = pbr ? vec4(metallic, roughness, ao, 0.0) : vec4(0.04, specular, uLocalParams.z, ModernMaterialFresnel());\n" + " // Emissive alpha is an explicit layout tag; material alpha remains classic Fresnel data.\n" + " out_Emissive = vec4(emissive, pbr ? 1.0 : 0.0);\n" "}\n", glslVersion, hasDrawRecords, @@ -1101,6 +1345,12 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "vec3 ModernMaterialTangentNormal(void) {\n" " if (uMaterialFlags.x <= 0.5) { return vec3(0.0, 0.0, 1.0); }\n" " vec4 bumpSample = ModernSampleNormalTexture(vTexCoord);\n" + " if (uMaterialFlags.w >= 7.5) {\n" + " int normalFormat = int(floor(uMaterialFlags.w - 9.0 + 0.5));\n" + " if (normalFormat == 1) { vec2 xy = (bumpSample.rg * 2.0 - 1.0) * max(uLocalParams.w, 0.0); float z = sqrt(max(1.0 - dot(xy, xy), 0.0)); return ModernSafeNormal(vec3(xy, z)); }\n" + " if (normalFormat == 2) { return ModernSafeNormal((bumpSample.rgb * 2.0 - 1.0) * vec3(max(uLocalParams.w, 0.0), max(uLocalParams.w, 0.0), 1.0)); }\n" + " return ModernSafeNormal(ModernDecodeClassicNormal(bumpSample));\n" + " }\n" " return uMaterialEnhancement.x > 0.5 ? ModernDecodeEnhancedNormal(bumpSample) : ModernDecodeClassicNormal(bumpSample);\n" "}\n" "vec3 ModernMaterialNormal(void) {\n" @@ -1117,14 +1367,22 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "}\n" "void main() {\n" " vec4 texel = ModernSampleMainTexture(vTexCoord);\n" - " if (texel.a < max(uLocalParams.x, 0.001)) { discard; }\n" + " bool pbr = uMaterialFlags.w >= 7.5;\n" + " if (texel.a < (pbr ? max(uMaterialEnhancement.w, 0.001) : max(uLocalParams.x, 0.001))) { discard; }\n" " vec3 normal = ModernMaterialNormal();\n" " float specular = ModernSpecularStrength();\n" + " vec3 orm = uMaterialFlags.y > 0.5 ? ModernSampleSpecularTexture(vTexCoord).rgb : vec3(1.0);\n" + " if (pbr && uMaterialFlags.y < -0.5) { orm = vec3(ModernSampleAOTexture(vTexCoord).r, ModernSampleRoughnessTexture(vTexCoord).r, ModernSampleMetallicTexture(vTexCoord).r); }\n" + " float metallic = pbr ? clamp(orm.b * uLocalParams.x, 0.0, 1.0) : 0.04;\n" + " float roughness = pbr ? clamp(orm.g * uLocalParams.y, 0.02, 1.0) : specular;\n" + " float ao = pbr ? clamp(orm.r, 0.0, 1.0) * clamp(uLocalParams.z, 0.0, 1.0) : uLocalParams.z;\n" " vec3 emissive = uMaterialFlags.z > 0.5 ? ModernSampleEmissiveTexture(vTexCoord).rgb : vec3(0.0);\n" - " out_Albedo = vec4(texel.rgb * max(uDebugColor.rgb, vec3(0.0)), 1.0);\n" + " if (pbr) { emissive = pow(max(emissive, vec3(0.0)), vec3(2.2)) * max(uDebugColor.rgb, vec3(0.0)); }\n" + " out_Albedo = vec4(pbr ? pow(max(texel.rgb, vec3(0.0)), vec3(2.2)) : texel.rgb * max(uDebugColor.rgb, vec3(0.0)), 1.0);\n" " out_Normal = vec4(normal * 0.5 + 0.5, vTangentSign * 0.5 + 0.5);\n" - " out_Material = vec4(0.04, specular, uLocalParams.z, ModernMaterialFresnel());\n" - " out_Emissive = vec4(emissive, 1.0);\n" + " out_Material = pbr ? vec4(metallic, roughness, ao, 0.0) : vec4(0.04, specular, uLocalParams.z, ModernMaterialFresnel());\n" + " // Emissive alpha is an explicit layout tag; material alpha remains classic Fresnel data.\n" + " out_Emissive = vec4(emissive, pbr ? 1.0 : 0.0);\n" "}\n", glslVersion, hasDrawRecords, @@ -1155,7 +1413,10 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " // light math runs in the cluster basis (M2)\n" " vec3 normal = ModernClusterFromEyeSpace(normalize(texture(uGBufferNormal, vTexCoord).xyz * 2.0 - 1.0));\n" " vec4 material = texture(uGBufferMaterial, vTexCoord);\n" - " vec3 lightGrid = texture(uGBufferEmissive, vTexCoord).rgb;\n" + " vec4 emissiveRecord = texture(uGBufferEmissive, vTexCoord);\n" + " bool pbr = emissiveRecord.a > 0.5;\n" + " vec3 emissive = emissiveRecord.rgb;\n" + " vec3 lightGrid = pbr ? vec3(0.0) : emissive;\n" " float rawDepth = texture(uSceneDepth, vTexCoord).r;\n" " vec3 viewPosition = ModernClusterViewPositionFromDepth(vTexCoord, rawDepth);\n" " ivec3 grid = ivec3(max(uClusterGrid.grid.xyz, vec3(1.0)));\n" @@ -1180,7 +1441,7 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " float inY = step(light.scissorDepth.y, pixel.y) * step(pixel.y, light.scissorDepth.w);\n" " float shadowVisibility = ModernClusterShadowVisibility(light, viewPosition, normal);\n" " float attenuation = 0.0;\n" - " vec3 contribution = ModernClusterEvaluateLight(light, viewPosition, normal, material.g, material.a, attenuation);\n" + " vec3 contribution = pbr ? ModernClusterEvaluatePBRLight(light, viewPosition, normal, albedo.rgb, material.r, material.g, attenuation) : ModernClusterEvaluateLight(light, viewPosition, normal, material.g, material.a, attenuation);\n" " attenuation = supported ? attenuation * inX * inY * shadowVisibility : 0.0;\n" " lightAccum += contribution * inX * inY * shadowVisibility;\n" " contributingLights += attenuation > 0.001 ? 1 : 0;\n" @@ -1190,11 +1451,21 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " float debugMode = floor(uLocalParams.y + 0.5);\n" " float overflowPressure = clamp(uLocalParams.w, 0.0, 1.0);\n" " float shadowBindingProbe = dot(ModernClusterShadowResourceProbe(), vec4(0.000001));\n" - " vec3 lit = albedo.rgb * (vec3(0.12) + lightGrid + lightAccum * (0.35 + material.g)) * max(uDebugColor.rgb, vec3(0.0)) * exposure + vec3(shadowBindingProbe);\n" + " vec3 pbrIndirect = ModernPBRIndirect(viewPosition, normal, albedo.rgb, material.r, material.g, material.b, clusterRange);\n" + " vec3 lit = pbr ? pbrIndirect + lightAccum + emissive : albedo.rgb * (vec3(0.12) + lightGrid + lightAccum * (0.35 + material.g)) * max(uDebugColor.rgb, vec3(0.0)) * exposure;\n" + " lit += vec3(shadowBindingProbe);\n" " // fog and blend lights are compositing operations, not additive\n" " // contributions, so they are applied over the accumulated result\n" " lit = ModernClusterApplyFogAndBlend(lit, clusterRange, clusterLightCount, viewPosition);\n" - " if (debugMode == 1.0) {\n" + " float pbrDebug = floor(uDebugColor.a + 0.5);\n" + " if (pbr && pbrDebug == 1.0) { out_Color = vec4(albedo.rgb, 1.0); }\n" + " else if (pbr && pbrDebug == 2.0) { out_Color = vec4(normal * 0.5 + 0.5, 1.0); }\n" + " else if (pbr && pbrDebug == 3.0) { out_Color = vec4(vec3(material.r), 1.0); }\n" + " else if (pbr && pbrDebug == 4.0) { out_Color = vec4(vec3(material.g), 1.0); }\n" + " else if (pbr && pbrDebug == 5.0) { out_Color = vec4(vec3(material.b), 1.0); }\n" + " else if (pbr && pbrDebug == 6.0) { out_Color = vec4(emissive, 1.0); }\n" + " else if (pbrDebug == 7.0) { out_Color = vec4(pbr ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 1.0), 1.0); }\n" + " else if (debugMode == 1.0) {\n" " out_Color = vec4(clamp(lightAccum, vec3(0.0), vec3(1.0)), 1.0);\n" " } else if (debugMode == 2.0) {\n" " out_Color = vec4(fract(vec3(float(tileX) * 0.173, float(tileY) * 0.271, float(sliceZ) * 0.067)), 1.0);\n" @@ -1225,14 +1496,17 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "%s" "void main() {\n" " vec4 texel = ModernSampleMainTexture(vTexCoord);\n" - " if (%d != 0 && texel.a < max(uLocalParams.x, 0.001)) { discard; }\n" + " bool pbr = ModernIsPBRMaterial();\n" + " if (%d != 0 && texel.a < (pbr ? max(uMaterialEnhancement.w, 0.001) : max(uLocalParams.x, 0.001))) { discard; }\n" " // vViewPosition/normals are GL eye space; convert once so every\n" " // cluster consumer (slice, light math, shadows) shares the\n" " // cluster basis instead of mixing conventions (M2)\n" " vec3 clusterPosition = ModernClusterFromEyeSpace(vViewPosition);\n" " vec3 materialNormal = ModernClusterFromEyeSpace(ModernMaterialNormal());\n" " float specular = ModernSpecularStrength();\n" - " vec3 emissive = ModernEmissiveColor();\n" + " vec3 pbrData = ModernPBRMaterialData();\n" + " vec3 baseColor = pbr ? ModernPBRBaseColor() : texel.rgb * max(uDebugColor.rgb, vec3(0.0));\n" + " vec3 emissive = pbr ? ModernPBREmissiveColor() : ModernEmissiveColor();\n" " ivec3 grid = ivec3(max(uClusterGrid.grid.xyz, vec3(1.0)));\n" " vec2 viewport = max(uClusterGrid.viewport.xy, vec2(1.0));\n" " vec2 normalizedPixel = clamp(gl_FragCoord.xy / viewport, vec2(0.0), vec2(0.999));\n" @@ -1255,14 +1529,16 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " float inY = step(light.scissorDepth.y, gl_FragCoord.y) * step(gl_FragCoord.y, light.scissorDepth.w);\n" " float shadowVisibility = ModernClusterShadowVisibility(light, clusterPosition, materialNormal);\n" " float attenuation = 0.0;\n" - " vec3 contribution = ModernClusterEvaluateLight(light, clusterPosition, materialNormal, specular, ModernMaterialFresnel(), attenuation);\n" + " vec3 contribution = pbr ? ModernClusterEvaluatePBRLight(light, clusterPosition, materialNormal, baseColor, pbrData.x, pbrData.y, attenuation) : ModernClusterEvaluateLight(light, clusterPosition, materialNormal, specular, ModernMaterialFresnel(), attenuation);\n" " lightAccum += supported ? contribution * inX * inY * shadowVisibility : vec3(0.0);\n" " scannedLights++;\n" " }\n" " float lightScale = clamp(0.18 + uLocalParams.y + float(scannedLights) * 0.02, 0.18, 2.5);\n" " float shadowBindingProbe = dot(ModernClusterShadowResourceProbe(), vec4(0.000001));\n" - " vec3 lit = texel.rgb * max(uDebugColor.rgb, vec3(0.0)) * (lightScale + lightAccum * (0.30 + specular * 0.25)) + emissive + vec3(shadowBindingProbe);\n" - " out_Color = vec4(ModernSceneReferredColor(lit), texel.a * uDebugColor.a);\n" + " vec3 lit = pbr ? ModernPBRIndirect(clusterPosition, materialNormal, baseColor, pbrData.x, pbrData.y, pbrData.z, clusterRange) + lightAccum + emissive : baseColor * (lightScale + lightAccum * (0.30 + specular * 0.25)) + emissive;\n" + " int pbrDebug = int(floor(uDebugColor.a + 0.5));\n" + " if (pbr && pbrDebug == 1) lit = baseColor; else if (pbr && pbrDebug == 2) lit = materialNormal * 0.5 + 0.5; else if (pbr && pbrDebug == 3) lit = vec3(pbrData.x); else if (pbr && pbrDebug == 4) lit = vec3(pbrData.y); else if (pbr && pbrDebug == 5) lit = vec3(pbrData.z); else if (pbr && pbrDebug == 6) lit = emissive; else if (pbrDebug == 7) lit = pbr ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 1.0);\n" + " out_Color = vec4(ModernSceneReferredColor(lit + vec3(shadowBindingProbe)), texel.a);\n" "}\n", glslVersion, hasShaderStorage, @@ -1284,14 +1560,20 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern "%s" "void main() {\n" " vec4 texel = ModernSampleMainTexture(vTexCoord);\n" - " float decalMode = step(0.5, uLocalParams.w);\n" + " bool pbr = ModernIsPBRMaterial();\n" + " float decalMode = pbr ? step(0.5, uMaterialEnhancement.x) : step(0.5, uLocalParams.w);\n" " vec4 vertexTint = clamp(vVertexColor, vec4(0.0), vec4(1.0));\n" - " vec4 materialColor = texel * vertexTint * max(uDebugColor, vec4(0.0));\n" + // PBR materials without an authored vertex-color contract must take + // transparency from their albedo alpha. World vertices commonly carry + // an unset alpha channel, so multiplying it here makes valid + // source-alpha PBR surfaces disappear completely. + " vec4 materialColor = pbr ? vec4(ModernPBRBaseColor() * vertexTint.rgb, texel.a) : texel * vertexTint * max(uDebugColor, vec4(0.0));\n" " vec3 baseColor = materialColor.rgb;\n" " vec3 clusterPosition = ModernClusterFromEyeSpace(vViewPosition);\n" " vec3 materialNormal = ModernClusterFromEyeSpace(ModernMaterialNormal());\n" " float specular = ModernSpecularStrength();\n" - " vec3 emissive = ModernEmissiveColor();\n" + " vec3 pbrData = ModernPBRMaterialData();\n" + " vec3 emissive = pbr ? ModernPBREmissiveColor() : ModernEmissiveColor();\n" " ivec3 grid = ivec3(max(uClusterGrid.grid.xyz, vec3(1.0)));\n" " int maxLights = int(max(uClusterGrid.grid.w, 1.0));\n" " vec2 viewport = max(uClusterGrid.viewport.xy, vec2(1.0));\n" @@ -1310,12 +1592,14 @@ static void R_ModernGLShaderLibrary_BuildFragmentSource( int glslVersion, modern " int type = int(floor(light.colorType.w + 0.5));\n" " float shadowVisibility = ModernClusterShadowVisibility(light, clusterPosition, materialNormal);\n" " float attenuation = 0.0;\n" - " vec3 contribution = ModernClusterEvaluateLight(light, clusterPosition, materialNormal, specular, ModernMaterialFresnel(), attenuation);\n" + " vec3 contribution = pbr ? ModernClusterEvaluatePBRLight(light, clusterPosition, materialNormal, baseColor, pbrData.x, pbrData.y, attenuation) : ModernClusterEvaluateLight(light, clusterPosition, materialNormal, specular, ModernMaterialFresnel(), attenuation);\n" " if (type == 0 || type == 1 || type == 3) { lightAccum += contribution * shadowVisibility; }\n" " }\n" " float shadowBindingProbe = dot(ModernClusterShadowResourceProbe(), vec4(0.000001));\n" - " vec3 transparentColor = baseColor + lightAccum + emissive + vec3(shadowBindingProbe);\n" + " vec3 transparentColor = pbr ? ModernPBRIndirect(clusterPosition, materialNormal, baseColor, pbrData.x, pbrData.y, pbrData.z, clusterRange) + lightAccum + emissive + vec3(shadowBindingProbe) : baseColor + lightAccum + emissive + vec3(shadowBindingProbe);\n" " transparentColor = ModernClusterApplyFogAndBlend(transparentColor, clusterRange, clusterLightCount, clusterPosition);\n" + " int pbrDebug = int(floor(uDebugColor.a + 0.5));\n" + " if (pbr && pbrDebug == 1) transparentColor = baseColor; else if (pbr && pbrDebug == 2) transparentColor = materialNormal * 0.5 + 0.5; else if (pbr && pbrDebug == 3) transparentColor = vec3(pbrData.x); else if (pbr && pbrDebug == 4) transparentColor = vec3(pbrData.y); else if (pbr && pbrDebug == 5) transparentColor = vec3(pbrData.z); else if (pbr && pbrDebug == 6) transparentColor = emissive; else if (pbrDebug == 7) transparentColor = pbr ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 1.0);\n" " out_Color = vec4(ModernSceneReferredColor(mix(transparentColor, baseColor, decalMode)), materialColor.a);\n" "}\n", glslVersion, @@ -1479,6 +1763,21 @@ static bool R_ModernGLShaderLibrary_KindUsesMaterialTextures( modernGLShaderProg || kind == MODERN_GL_SHADER_FOG_BLEND; } +static bool R_ModernGLShaderLibrary_KindUsesPBRMaterialData( modernGLShaderProgramKind_t kind ) { + return kind == MODERN_GL_SHADER_GBUFFER_OPAQUE + || kind == MODERN_GL_SHADER_GBUFFER_ALPHA_TEST + || kind == MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE + || kind == MODERN_GL_SHADER_CLUSTERED_FORWARD_ALPHA_TEST + || kind == MODERN_GL_SHADER_TRANSPARENT_FORWARD; +} + +static bool R_ModernGLShaderLibrary_KindUsesPBRIBL( modernGLShaderProgramKind_t kind ) { + return kind == MODERN_GL_SHADER_DEFERRED_LIGHT_RESOLVE + || kind == MODERN_GL_SHADER_CLUSTERED_FORWARD_OPAQUE + || kind == MODERN_GL_SHADER_CLUSTERED_FORWARD_ALPHA_TEST + || kind == MODERN_GL_SHADER_TRANSPARENT_FORWARD; +} + static bool R_ModernGLShaderLibrary_KindUsesMaterialTextureTable( modernGLShaderProgramKind_t kind ) { return kind != MODERN_GL_SHADER_DEFERRED_LIGHT_RESOLVE; } @@ -1495,6 +1794,10 @@ static bool R_ModernGLShaderLibrary_KindUsesShadowTextures( modernGLShaderProgra || kind == MODERN_GL_SHADER_TRANSPARENT_FORWARD; } +static bool R_ModernGLShaderLibrary_KindUsesSpecularProbes( modernGLShaderProgramKind_t kind ) { + return R_ModernGLShaderLibrary_KindUsesPBRIBL( kind ); +} + static bool R_ModernGLShaderLibrary_KindUsesDrawVertColor( modernGLShaderProgramKind_t kind ) { (void)kind; @@ -1574,6 +1877,7 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.reflection.usesModelViewMatrix = R_ModernGLShaderLibrary_KindUsesDrawVertTangentSpace( info.kind ); info.reflection.usesDebugColor = R_ModernGLShaderLibrary_KindUsesDebugColor( info.kind ); info.reflection.usesLocalParams = R_ModernGLShaderLibrary_KindUsesLocalParams( info.kind ); + info.reflection.usesPBRIBL = R_ModernGLShaderLibrary_KindUsesPBRIBL( info.kind ); info.reflection.usesMainTexture = R_ModernGLShaderLibrary_KindUsesMainTexture( info.kind ); info.reflection.usesMaterialTextures = R_ModernGLShaderLibrary_KindUsesMaterialTextures( info.kind ); info.reflection.usesMaterialTextureTable = info.glslVersion >= 430 && info.reflection.usesMainTexture && R_ModernGLShaderLibrary_KindUsesMaterialTextureTable( info.kind ); @@ -1582,6 +1886,7 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.reflection.usesDrawRecords = info.glslVersion >= 430 && info.kind != MODERN_GL_SHADER_DEFERRED_LIGHT_RESOLVE; info.reflection.usesSceneDepthTexture = R_ModernGLShaderLibrary_KindUsesSceneDepthTexture( info.kind ); info.reflection.usesShadowTextures = R_ModernGLShaderLibrary_KindUsesShadowTextures( info.kind ); + info.reflection.usesSpecularProbes = R_ModernGLShaderLibrary_KindUsesSpecularProbes( info.kind ); info.reflection.usesTexCoord = info.reflection.usesMainTexture; info.reflection.usesDrawVertColor = R_ModernGLShaderLibrary_KindUsesDrawVertColor( info.kind ); info.reflection.usesDrawVertTangentSpace = R_ModernGLShaderLibrary_KindUsesDrawVertTangentSpace( info.kind ); @@ -1594,10 +1899,14 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.reflection.modelViewMatrixLocation = glGetUniformLocation( info.program, "uModelViewMatrix" ); info.reflection.debugColorLocation = glGetUniformLocation( info.program, "uDebugColor" ); info.reflection.localParamsLocation = glGetUniformLocation( info.program, "uLocalParams" ); + info.reflection.pbrIBLLocation = glGetUniformLocation( info.program, "uPBRIBL" ); info.reflection.mainTextureLocation = glGetUniformLocation( info.program, "uMainTexture" ); info.reflection.normalTextureLocation = glGetUniformLocation( info.program, "uNormalTexture" ); info.reflection.specularTextureLocation = glGetUniformLocation( info.program, "uSpecularTexture" ); info.reflection.emissiveTextureLocation = glGetUniformLocation( info.program, "uEmissiveTexture" ); + info.reflection.metallicTextureLocation = glGetUniformLocation( info.program, "uMetallicTexture" ); + info.reflection.roughnessTextureLocation = glGetUniformLocation( info.program, "uRoughnessTexture" ); + info.reflection.aoTextureLocation = glGetUniformLocation( info.program, "uAOTexture" ); info.reflection.textureIndicesLocation = glGetUniformLocation( info.program, "uTextureIndices" ); info.reflection.textureTableModeLocation = glGetUniformLocation( info.program, "uTextureTableMode" ); info.reflection.materialTextureTableLocation = glGetUniformLocation( info.program, "uMaterialTextures[0]" ); @@ -1606,6 +1915,10 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.reflection.drawRecordModeLocation = glGetUniformLocation( info.program, "uDrawRecordMode" ); info.reflection.drawRecordCountLocation = glGetUniformLocation( info.program, "uDrawRecordCount" ); info.reflection.sceneDepthTextureLocation = glGetUniformLocation( info.program, "uSceneDepth" ); + info.reflection.specularProbeAtlasLocation = glGetUniformLocation( info.program, "uModernSpecularProbeAtlas" ); + const GLuint specularProbeBlockIndex = glGetUniformBlockIndex( info.program, "ModernSpecularProbeRecords" ); + info.reflection.specularProbeBlockIndex = specularProbeBlockIndex == GL_INVALID_INDEX + ? -1 : static_cast( specularProbeBlockIndex ); const GLint shadowAtlasLocation = glGetUniformLocation( info.program, "uModernShadowAtlas" ); const GLint pointShadowAtlasLocation = glGetUniformLocation( info.program, "uModernPointShadowAtlas" ); const GLint translucentShadowMomentsLocation = glGetUniformLocation( info.program, "uModernTranslucentShadowMoments[0]" ); @@ -1626,6 +1939,20 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t GL_UNIFORM_BLOCK, true, info.reflection.frameBlockIndex >= 0 ); + if ( info.reflection.usesSpecularProbes ) { + R_ModernGLShaderLibrary_AddReflectionRecord( + info.reflection.uniformBlocks, + info.reflection.uniformBlockCount, + "ModernSpecularProbeRecords", + MODERN_GL_SHADER_RESOURCE_UNIFORM_BLOCK, + info.reflection.specularProbeBlockIndex, + -1, + 7, + 32, + GL_UNIFORM_BLOCK, + true, + info.reflection.specularProbeBlockIndex >= 0 ); + } R_ModernGLShaderLibrary_AddReflectionRecord( info.reflection.uniforms, info.reflection.uniformCount, @@ -1794,6 +2121,20 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t true, shadowMomentStateLocation >= 0 ); } + if ( info.reflection.usesSpecularProbes ) { + R_ModernGLShaderLibrary_AddReflectionRecord( + info.reflection.samplers, + info.reflection.samplerCount, + "uModernSpecularProbeAtlas", + MODERN_GL_SHADER_RESOURCE_SAMPLER, + -1, + info.reflection.specularProbeAtlasLocation, + MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT, + 1, + GL_SAMPLER_2D, + true, + info.reflection.specularProbeAtlasLocation >= 0 ); + } if ( info.reflection.usesMaterialTextures ) { R_ModernGLShaderLibrary_AddReflectionRecord( info.reflection.samplers, @@ -1831,6 +2172,25 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t GL_SAMPLER_2D, true, info.reflection.emissiveTextureLocation >= 0 ); + if ( R_ModernGLShaderLibrary_KindUsesPBRMaterialData( info.kind ) ) { + R_ModernGLShaderLibrary_AddReflectionRecord( info.reflection.samplers, info.reflection.samplerCount, "uMetallicTexture", MODERN_GL_SHADER_RESOURCE_SAMPLER, -1, info.reflection.metallicTextureLocation, 4, 1, GL_SAMPLER_2D, true, info.reflection.metallicTextureLocation >= 0 ); + R_ModernGLShaderLibrary_AddReflectionRecord( info.reflection.samplers, info.reflection.samplerCount, "uRoughnessTexture", MODERN_GL_SHADER_RESOURCE_SAMPLER, -1, info.reflection.roughnessTextureLocation, 5, 1, GL_SAMPLER_2D, true, info.reflection.roughnessTextureLocation >= 0 ); + R_ModernGLShaderLibrary_AddReflectionRecord( info.reflection.samplers, info.reflection.samplerCount, "uAOTexture", MODERN_GL_SHADER_RESOURCE_SAMPLER, -1, info.reflection.aoTextureLocation, 6, 1, GL_SAMPLER_2D, true, info.reflection.aoTextureLocation >= 0 ); + } + } + if ( info.reflection.usesPBRIBL ) { + R_ModernGLShaderLibrary_AddReflectionRecord( + info.reflection.uniforms, + info.reflection.uniformCount, + "uPBRIBL", + MODERN_GL_SHADER_RESOURCE_UNIFORM, + -1, + info.reflection.pbrIBLLocation, + -1, + 1, + GL_FLOAT_VEC4, + true, + info.reflection.pbrIBLLocation >= 0 ); } if ( info.reflection.usesMaterialTextureTable ) { R_ModernGLShaderLibrary_AddReflectionRecord( @@ -2059,10 +2419,14 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.modelViewMatrixLocation = info.reflection.modelViewMatrixLocation; info.debugColorLocation = info.reflection.debugColorLocation; info.localParamsLocation = info.reflection.localParamsLocation; + info.pbrIBLLocation = info.reflection.pbrIBLLocation; info.mainTextureLocation = info.reflection.mainTextureLocation; info.normalTextureLocation = info.reflection.normalTextureLocation; info.specularTextureLocation = info.reflection.specularTextureLocation; info.emissiveTextureLocation = info.reflection.emissiveTextureLocation; + info.metallicTextureLocation = info.reflection.metallicTextureLocation; + info.roughnessTextureLocation = info.reflection.roughnessTextureLocation; + info.aoTextureLocation = info.reflection.aoTextureLocation; info.textureIndicesLocation = info.reflection.textureIndicesLocation; info.textureTableModeLocation = info.reflection.textureTableModeLocation; info.materialTextureTableLocation = info.reflection.materialTextureTableLocation; @@ -2071,6 +2435,8 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t info.drawRecordModeLocation = info.reflection.drawRecordModeLocation; info.drawRecordCountLocation = info.reflection.drawRecordCountLocation; info.sceneDepthTextureLocation = info.reflection.sceneDepthTextureLocation; + info.specularProbeAtlasLocation = info.reflection.specularProbeAtlasLocation; + info.specularProbeBlockIndex = info.reflection.specularProbeBlockIndex; if ( info.frameBlockIndex < 0 || info.modelViewProjectionLocation < 0 ) { common->Warning( "Modern GL program '%s' is missing required reflected bindings", info.name ); @@ -2121,6 +2487,20 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t common->Warning( "Modern GL program '%s' is missing material texture samplers", info.name ); return false; } + if ( info.reflection.usesPBRIBL && info.pbrIBLLocation < 0 ) { + common->Warning( "Modern GL PBR program '%s' is missing uPBRIBL", info.name ); + return false; + } + if ( info.reflection.usesSpecularProbes + && ( info.specularProbeAtlasLocation < 0 || info.specularProbeBlockIndex < 0 ) ) { + common->Warning( "Modern GL PBR program '%s' is missing authored specular-probe bindings", info.name ); + return false; + } + if ( R_ModernGLShaderLibrary_KindUsesPBRMaterialData( info.kind ) + && ( info.metallicTextureLocation < 0 || info.roughnessTextureLocation < 0 || info.aoTextureLocation < 0 ) ) { + common->Warning( "Modern GL PBR program '%s' is missing separate material-map samplers", info.name ); + return false; + } if ( info.reflection.usesMaterialTextureTable && ( info.textureIndicesLocation < 0 || info.textureTableModeLocation < 0 || info.materialTextureTableLocation < 0 ) ) { common->Warning( "Modern GL program '%s' is missing material texture table bindings", info.name ); @@ -2144,7 +2524,9 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t } glUniformBlockBinding( info.program, static_cast( info.frameBlockIndex ), 0 ); - if ( info.reflection.usesMainTexture || info.reflection.usesMaterialTextures || info.reflection.usesSceneDepthTexture || info.reflection.usesShadowTextures ) { + if ( info.reflection.usesMainTexture || info.reflection.usesMaterialTextures + || info.reflection.usesSceneDepthTexture || info.reflection.usesShadowTextures + || info.reflection.usesSpecularProbes ) { glUseProgram( info.program ); if ( info.reflection.usesMainTexture ) { glUniform1i( info.mainTextureLocation, 0 ); @@ -2156,6 +2538,15 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t glUniform1i( info.normalTextureLocation, 1 ); glUniform1i( info.specularTextureLocation, 2 ); glUniform1i( info.emissiveTextureLocation, 3 ); + if ( info.metallicTextureLocation >= 0 ) { + glUniform1i( info.metallicTextureLocation, 4 ); + } + if ( info.roughnessTextureLocation >= 0 ) { + glUniform1i( info.roughnessTextureLocation, 5 ); + } + if ( info.aoTextureLocation >= 0 ) { + glUniform1i( info.aoTextureLocation, 6 ); + } } if ( info.reflection.usesShadowTextures ) { glUniform1i( shadowAtlasLocation, MODERN_GL_SHADOW_TEXTURE_UNIT_PROJECTED_ATLAS ); @@ -2174,6 +2565,9 @@ static bool R_ModernGLShaderLibrary_ReflectProgram( modernGLShaderProgramInfo_t glUniform4f( shadowSamplerStateLocation, 0.0f, 0.0f, 0.0f, 0.0f ); glUniform4f( shadowMomentStateLocation, 0.0f, 0.0f, 0.0f, 0.0f ); } + if ( info.reflection.usesSpecularProbes ) { + glUniform1i( info.specularProbeAtlasLocation, MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT ); + } if ( info.reflection.usesMaterialTextureTable && glUniform1iv != NULL ) { GLint tableUnits[MATERIAL_RESOURCE_TABLE_TEXTURE_ARRAY_CAPACITY]; for ( int i = 0; i < MATERIAL_RESOURCE_TABLE_TEXTURE_ARRAY_CAPACITY; ++i ) { @@ -2287,6 +2681,7 @@ static bool R_ModernGLShaderLibrary_CreateProgram( int glslVersion, modernGLShad info.modelViewMatrixLocation = -1; info.debugColorLocation = -1; info.localParamsLocation = -1; + info.pbrIBLLocation = -1; info.mainTextureLocation = -1; info.normalTextureLocation = -1; info.specularTextureLocation = -1; @@ -2299,6 +2694,8 @@ static bool R_ModernGLShaderLibrary_CreateProgram( int glslVersion, modernGLShad info.drawRecordModeLocation = -1; info.drawRecordCountLocation = -1; info.sceneDepthTextureLocation = -1; + info.specularProbeAtlasLocation = -1; + info.specularProbeBlockIndex = -1; idStr::snPrintf( info.name, sizeof( info.name ), @@ -2622,6 +3019,15 @@ bool RendererModernGLShaderLibrary_RunSelfTest( void ) { common->Printf( "RendererModernGLShaderLibrary self-test failed: local-param reflection mismatch for %s\n", program->name ); return false; } + if ( program->reflection.usesPBRIBL && program->pbrIBLLocation < 0 ) { + common->Printf( "RendererModernGLShaderLibrary self-test failed: PBR IBL reflection mismatch for %s\n", program->name ); + return false; + } + if ( program->reflection.usesSpecularProbes + && ( program->specularProbeAtlasLocation < 0 || program->specularProbeBlockIndex < 0 ) ) { + common->Printf( "RendererModernGLShaderLibrary self-test failed: authored specular-probe reflection mismatch for %s\n", program->name ); + return false; + } if ( program->reflection.usesMainTexture && program->mainTextureLocation < 0 ) { common->Printf( "RendererModernGLShaderLibrary self-test failed: sampler reflection mismatch for %s\n", program->name ); return false; diff --git a/src/renderer/ModernGLShaderLibrary.h b/src/renderer/ModernGLShaderLibrary.h index 212246eb..1f09a869 100644 --- a/src/renderer/ModernGLShaderLibrary.h +++ b/src/renderer/ModernGLShaderLibrary.h @@ -75,10 +75,14 @@ typedef struct modernGLShaderReflection_s { int modelViewMatrixLocation; int debugColorLocation; int localParamsLocation; + int pbrIBLLocation; int mainTextureLocation; int normalTextureLocation; int specularTextureLocation; int emissiveTextureLocation; + int metallicTextureLocation; + int roughnessTextureLocation; + int aoTextureLocation; int textureIndicesLocation; int textureTableModeLocation; int materialTextureTableLocation; @@ -87,6 +91,8 @@ typedef struct modernGLShaderReflection_s { int drawRecordModeLocation; int drawRecordCountLocation; int sceneDepthTextureLocation; + int specularProbeAtlasLocation; + int specularProbeBlockIndex; int positionAttribute; int colorAttribute; int texCoordAttribute; @@ -111,6 +117,7 @@ typedef struct modernGLShaderReflection_s { bool usesModelViewMatrix; bool usesDebugColor; bool usesLocalParams; + bool usesPBRIBL; bool usesMainTexture; bool usesMaterialTextures; bool usesMaterialTextureTable; @@ -119,6 +126,7 @@ typedef struct modernGLShaderReflection_s { bool usesDrawRecords; bool usesSceneDepthTexture; bool usesShadowTextures; + bool usesSpecularProbes; bool usesTexCoord; bool usesDrawVertColor; bool usesDrawVertTangentSpace; @@ -139,11 +147,15 @@ typedef struct modernGLShaderProgramInfo_s { int modelViewMatrixLocation; int debugColorLocation; int localParamsLocation; + int pbrIBLLocation; int mainTextureLocation; int sceneDepthTextureLocation; int normalTextureLocation; int specularTextureLocation; int emissiveTextureLocation; + int metallicTextureLocation; + int roughnessTextureLocation; + int aoTextureLocation; int textureIndicesLocation; int textureTableModeLocation; int materialTextureTableLocation; @@ -151,6 +163,8 @@ typedef struct modernGLShaderProgramInfo_s { int materialEnhancementLocation; int drawRecordModeLocation; int drawRecordCountLocation; + int specularProbeAtlasLocation; + int specularProbeBlockIndex; bool linked; char name[64]; } modernGLShaderProgramInfo_t; diff --git a/src/renderer/ModernGLSubmitPlan.cpp b/src/renderer/ModernGLSubmitPlan.cpp index 5c1a3f33..57193f09 100644 --- a/src/renderer/ModernGLSubmitPlan.cpp +++ b/src/renderer/ModernGLSubmitPlan.cpp @@ -3,6 +3,7 @@ #include "tr_local.h" #include "ModernGLSubmitPlan.h" +#include "RendererContracts.h" idModernGLSubmitPlan::idModernGLSubmitPlan() : numCommands( 0 ) { @@ -61,7 +62,11 @@ void R_ModernGLSubmitCommand_BuildModelViewProjection( modernGLSubmitCommand_t & } projectionMatrix[14] *= 0.25f; } - myGlMultMatrix( command.modelViewMatrix, projectionMatrix, command.modelViewProjectionMatrix ); + float canonicalModelViewProjection[ 16 ]; + myGlMultMatrix( command.modelViewMatrix, projectionMatrix, canonicalModelViewProjection ); + RendererContracts_ConvertClipMatrix( command.modelViewProjectionMatrix, + canonicalModelViewProjection, RendererContracts_GLClipSpace(), + RendererContracts_GLClipSpace() ); } static bool R_ModernGLSubmitPlan_ScissorEquals( const modernGLSubmitCommand_t &a, const modernGLSubmitCommand_t &b ) { @@ -72,6 +77,12 @@ static bool R_ModernGLSubmitPlan_ScissorEquals( const modernGLSubmitCommand_t &a } static modernGLSubmitCommand_t rg_modernGLSubmitPlanSortScratch[MODERN_GL_SUBMIT_PLAN_MAX_COMMANDS]; +// The comparator ends with the unique originalSubmitOrder, so the sorted +// permutation is a strict total order: sorting an index array through the +// same comparator and applying it with two struct copies per element yields +// the byte-identical command order without O(n log n) struct moves. +static int rg_modernGLSubmitPlanSortOrder[MODERN_GL_SUBMIT_PLAN_MAX_COMMANDS]; +static int rg_modernGLSubmitPlanSortOrderScratch[MODERN_GL_SUBMIT_PLAN_MAX_COMMANDS]; static int R_ModernGLSubmitPlan_CompareInt( int a, int b ) { return ( a > b ) - ( a < b ); @@ -208,47 +219,47 @@ static bool R_ModernGLSubmitPlan_RangeAlreadySorted( const modernGLSubmitCommand return true; } -static void R_ModernGLSubmitPlan_InsertionSortRange( modernGLSubmitCommand_t *commands, int left, int right ) { +static void R_ModernGLSubmitPlan_InsertionSortRange( const modernGLSubmitCommand_t *commands, int *order, int left, int right ) { for ( int i = left + 1; i < right; ++i ) { - const modernGLSubmitCommand_t key = commands[i]; + const int key = order[i]; int j = i; - while ( j > left && R_ModernGLSubmitPlan_CompareSortKey( commands[j - 1], key ) > 0 ) { - commands[j] = commands[j - 1]; + while ( j > left && R_ModernGLSubmitPlan_CompareSortKey( commands[order[j - 1]], commands[key] ) > 0 ) { + order[j] = order[j - 1]; j--; } - commands[j] = key; + order[j] = key; } } -static void R_ModernGLSubmitPlan_StableSortRange( modernGLSubmitCommand_t *commands, int left, int right ) { +static void R_ModernGLSubmitPlan_StableSortRange( const modernGLSubmitCommand_t *commands, int *order, int left, int right ) { if ( right - left <= 1 ) { return; } if ( right - left <= MODERN_GL_SUBMIT_PLAN_INSERTION_SORT_THRESHOLD ) { - R_ModernGLSubmitPlan_InsertionSortRange( commands, left, right ); + R_ModernGLSubmitPlan_InsertionSortRange( commands, order, left, right ); return; } const int mid = left + ( right - left ) / 2; - R_ModernGLSubmitPlan_StableSortRange( commands, left, mid ); - R_ModernGLSubmitPlan_StableSortRange( commands, mid, right ); - if ( R_ModernGLSubmitPlan_CompareSortKey( commands[mid - 1], commands[mid] ) <= 0 ) { + R_ModernGLSubmitPlan_StableSortRange( commands, order, left, mid ); + R_ModernGLSubmitPlan_StableSortRange( commands, order, mid, right ); + if ( R_ModernGLSubmitPlan_CompareSortKey( commands[order[mid - 1]], commands[order[mid]] ) <= 0 ) { return; } for ( int i = left; i < right; ++i ) { - rg_modernGLSubmitPlanSortScratch[i] = commands[i]; + rg_modernGLSubmitPlanSortOrderScratch[i] = order[i]; } int a = left; int b = mid; for ( int out = left; out < right; ++out ) { if ( a >= mid ) { - commands[out] = rg_modernGLSubmitPlanSortScratch[b++]; + order[out] = rg_modernGLSubmitPlanSortOrderScratch[b++]; } else if ( b >= right ) { - commands[out] = rg_modernGLSubmitPlanSortScratch[a++]; - } else if ( R_ModernGLSubmitPlan_CompareSortKey( rg_modernGLSubmitPlanSortScratch[a], rg_modernGLSubmitPlanSortScratch[b] ) <= 0 ) { - commands[out] = rg_modernGLSubmitPlanSortScratch[a++]; + order[out] = rg_modernGLSubmitPlanSortOrderScratch[a++]; + } else if ( R_ModernGLSubmitPlan_CompareSortKey( commands[rg_modernGLSubmitPlanSortOrderScratch[a]], commands[rg_modernGLSubmitPlanSortOrderScratch[b]] ) <= 0 ) { + order[out] = rg_modernGLSubmitPlanSortOrderScratch[a++]; } else { - commands[out] = rg_modernGLSubmitPlanSortScratch[b++]; + order[out] = rg_modernGLSubmitPlanSortOrderScratch[b++]; } } } @@ -322,7 +333,16 @@ static void R_ModernGLSubmitPlan_SortCommands( modernGLSubmitCommand_t *commands if ( index - start > 1 ) { stats.sortSpans++; if ( !R_ModernGLSubmitPlan_RangeAlreadySorted( commands, start, index ) ) { - R_ModernGLSubmitPlan_StableSortRange( commands, start, index ); + for ( int i = start; i < index; ++i ) { + rg_modernGLSubmitPlanSortOrder[i] = i; + } + R_ModernGLSubmitPlan_StableSortRange( commands, rg_modernGLSubmitPlanSortOrder, start, index ); + for ( int i = start; i < index; ++i ) { + rg_modernGLSubmitPlanSortScratch[i] = commands[i]; + } + for ( int i = start; i < index; ++i ) { + commands[i] = rg_modernGLSubmitPlanSortScratch[rg_modernGLSubmitPlanSortOrder[i]]; + } sortedAnySpan = true; } } @@ -657,6 +677,8 @@ static void R_ModernGLSubmitPlan_SetTextureCacheSlot( static void R_ModernGLSubmitPlan_FillMaterialTextureCache( modernGLSubmitCommand_t &command, const materialResourceTableRecord_t *materialRecord ) { R_ModernGLSubmitPlan_ResetMaterialTextureCache( command ); + const bool pbrModernMaterial = materialRecord != NULL + && R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ); if ( materialRecord != NULL ) { command.materialLoadedTextureSemanticMask = materialRecord->loadedTextureSemanticMask; command.materialAlphaTestRegister = materialRecord->alphaTestRegister; @@ -666,23 +688,46 @@ static void R_ModernGLSubmitPlan_FillMaterialTextureCache( modernGLSubmitCommand R_ModernGLSubmitPlan_SetTextureCacheSlot( command, MODERN_GL_SUBMIT_MATERIAL_TEXTURE_MAIN, - R_ModernGLSubmitPlan_PrimaryTextureBinding( materialRecord ), - MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); + pbrModernMaterial + ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_ALBEDO ) + : R_ModernGLSubmitPlan_PrimaryTextureBinding( materialRecord ), + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_ALBEDO : MATERIAL_RESOURCE_TEXTURE_DIFFUSE ); R_ModernGLSubmitPlan_SetTextureCacheSlot( command, MODERN_GL_SUBMIT_MATERIAL_TEXTURE_NORMAL, - materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_BUMP ) : NULL, - MATERIAL_RESOURCE_TEXTURE_BUMP ); + materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_NORMAL : MATERIAL_RESOURCE_TEXTURE_BUMP ) : NULL, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_NORMAL : MATERIAL_RESOURCE_TEXTURE_BUMP ); R_ModernGLSubmitPlan_SetTextureCacheSlot( command, MODERN_GL_SUBMIT_MATERIAL_TEXTURE_SPECULAR, - materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_SPECULAR ) : NULL, - MATERIAL_RESOURCE_TEXTURE_SPECULAR ); + materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_ORM : MATERIAL_RESOURCE_TEXTURE_SPECULAR ) : NULL, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_ORM : MATERIAL_RESOURCE_TEXTURE_SPECULAR ); R_ModernGLSubmitPlan_SetTextureCacheSlot( command, MODERN_GL_SUBMIT_MATERIAL_TEXTURE_EMISSIVE, - materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_EMISSIVE ) : NULL, - MATERIAL_RESOURCE_TEXTURE_EMISSIVE ); + materialRecord != NULL ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR : MATERIAL_RESOURCE_TEXTURE_EMISSIVE ) : NULL, + pbrModernMaterial ? MATERIAL_RESOURCE_TEXTURE_EMISSIVE_PBR : MATERIAL_RESOURCE_TEXTURE_EMISSIVE ); + R_ModernGLSubmitPlan_SetTextureCacheSlot( + command, + MODERN_GL_SUBMIT_MATERIAL_TEXTURE_METALLIC, + pbrModernMaterial && materialRecord->pbrSeparateMaterialData + ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_METALLIC ) : NULL, + MATERIAL_RESOURCE_TEXTURE_METALLIC ); + R_ModernGLSubmitPlan_SetTextureCacheSlot( + command, + MODERN_GL_SUBMIT_MATERIAL_TEXTURE_ROUGHNESS, + pbrModernMaterial && materialRecord->pbrSeparateMaterialData + ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_ROUGHNESS ) : NULL, + MATERIAL_RESOURCE_TEXTURE_ROUGHNESS ); + R_ModernGLSubmitPlan_SetTextureCacheSlot( + command, + MODERN_GL_SUBMIT_MATERIAL_TEXTURE_AO, + pbrModernMaterial && materialRecord->pbrSeparateMaterialData + ? R_MaterialResourceTable_TextureBindingForSemantic( *materialRecord, MATERIAL_RESOURCE_TEXTURE_AO ) : NULL, + MATERIAL_RESOURCE_TEXTURE_AO ); } bool idModernGLSubmitPlan::AddCommand( const modernGLDrawPlanEntry_t &entry ) { @@ -734,11 +779,18 @@ bool idModernGLSubmitPlan::AddCommand( const modernGLDrawPlanEntry_t &entry ) { stats.fallbackDraws++; return false; } + const materialResourceTableRecord_t *materialRecord = R_MaterialResourceTable_RecordForIndex( entry.materialTableIndex ); + if ( materialRecord != NULL + && !R_MaterialResourceTable_ClassicModernPathEligible( *materialRecord ) + && !R_MaterialResourceTable_PBRModernPathEligible( *materialRecord ) ) { + stats.fallbackDraws++; + return false; + } modernGLSubmitCommand_t &command = commands[numCommands]; memset( &command, 0, sizeof( command ) ); command.drawPlanEntry = &entry; - command.materialRecord = R_MaterialResourceTable_RecordForIndex( entry.materialTableIndex ); + command.materialRecord = materialRecord; command.viewDef = draw->viewDef; command.passCategory = entry.passCategory; command.pipeline = entry.pipeline; @@ -754,10 +806,14 @@ bool idModernGLSubmitPlan::AddCommand( const modernGLDrawPlanEntry_t &entry ) { command.modelViewMatrixLocation = entry.modelViewMatrixLocation; command.debugColorLocation = entry.debugColorLocation; command.localParamsLocation = entry.localParamsLocation; + command.pbrIBLLocation = entry.pbrIBLLocation; command.mainTextureLocation = entry.mainTextureLocation; command.normalTextureLocation = entry.normalTextureLocation; command.specularTextureLocation = entry.specularTextureLocation; command.emissiveTextureLocation = entry.emissiveTextureLocation; + command.metallicTextureLocation = entry.metallicTextureLocation; + command.roughnessTextureLocation = entry.roughnessTextureLocation; + command.aoTextureLocation = entry.aoTextureLocation; command.textureIndicesLocation = entry.textureIndicesLocation; command.textureTableModeLocation = entry.textureTableModeLocation; command.materialFlagsLocation = entry.materialFlagsLocation; @@ -774,7 +830,6 @@ bool idModernGLSubmitPlan::AddCommand( const modernGLDrawPlanEntry_t &entry ) { command.instanceRecordIndex = entry.instanceRecordIndex; command.originalSubmitOrder = numCommands; command.sortBucket = -1; - const materialResourceTableRecord_t *materialRecord = command.materialRecord; command.blendMode = materialRecord != NULL ? materialRecord->blendMode : MATERIAL_RESOURCE_BLEND_OPAQUE; command.cullType = materialRecord != NULL ? materialRecord->cullType : CT_FRONT_SIDED; command.materialStableId = entry.materialStableId; @@ -1084,10 +1139,15 @@ bool RendererModernGLSubmitPlan_RunSelfTest( void ) { return true; } - idScenePacketFrame packetFrame; + // This fixture keeps four fixed-capacity packet arenas alive at once. Keep + // them off the finite render-thread stack; sealed geometry contracts make + // the arenas deliberately substantial even though each fixture uses only a + // handful of records. + idAutoPtr packetFrame( new idScenePacketFrame ); idRenderGraph graph; idModernGLDrawPlan drawPlan; - R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, true, TAG_USED, false, drawPlan, packetFrame, graph ); + R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, true, TAG_USED, false, + drawPlan, *packetFrame, graph ); idModernGLSubmitPlan submitPlan; submitPlan.Build( drawPlan ); @@ -1148,14 +1208,17 @@ bool RendererModernGLSubmitPlan_RunSelfTest( void ) { } idModernGLDrawPlan missingCacheDrawPlan; - idScenePacketFrame missingCachePacketFrame; + idAutoPtr missingCachePacketFrame( + new idScenePacketFrame ); idRenderGraph missingCacheGraph; - R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( false, false, TAG_FREE, false, missingCacheDrawPlan, missingCachePacketFrame, missingCacheGraph ); + R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( false, false, TAG_FREE, false, + missingCacheDrawPlan, *missingCachePacketFrame, missingCacheGraph ); idModernGLSubmitPlan missingCacheSubmitPlan; missingCacheSubmitPlan.Build( missingCacheDrawPlan ); const modernGLDrawPlanStats_t &missingDrawStats = missingCacheDrawPlan.Stats(); const modernGLSubmitPlanStats_t &fallbackStats = missingCacheSubmitPlan.Stats(); - if ( missingDrawStats.sourceDrawPackets != missingCachePacketFrame.NumDrawPackets() + if ( missingDrawStats.sourceDrawPackets + != missingCachePacketFrame->NumDrawPackets() || missingDrawStats.plannedDraws != 0 || missingDrawStats.geometryFallbackDraws != expectedGeometryFallbackDraws || missingDrawStats.geometryVertexBufferFallbackDraws != expectedGeometryFallbackDraws @@ -1167,9 +1230,11 @@ bool RendererModernGLSubmitPlan_RunSelfTest( void ) { } idModernGLDrawPlan tempIndexDrawPlan; - idScenePacketFrame tempIndexPacketFrame; + idAutoPtr tempIndexPacketFrame( + new idScenePacketFrame ); idRenderGraph tempIndexGraph; - R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, true, TAG_TEMP, false, tempIndexDrawPlan, tempIndexPacketFrame, tempIndexGraph ); + R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, true, TAG_TEMP, false, + tempIndexDrawPlan, *tempIndexPacketFrame, tempIndexGraph ); idModernGLSubmitPlan tempIndexSubmitPlan; tempIndexSubmitPlan.Build( tempIndexDrawPlan ); const modernGLSubmitPlanStats_t &tempIndexStats = tempIndexSubmitPlan.Stats(); @@ -1183,9 +1248,11 @@ bool RendererModernGLSubmitPlan_RunSelfTest( void ) { } idModernGLDrawPlan uploadIndexDrawPlan; - idScenePacketFrame uploadIndexPacketFrame; + idAutoPtr uploadIndexPacketFrame( + new idScenePacketFrame ); idRenderGraph uploadIndexGraph; - R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, false, TAG_FREE, true, uploadIndexDrawPlan, uploadIndexPacketFrame, uploadIndexGraph ); + R_ModernGLSubmitPlan_BuildSelfTestDrawPlan( true, false, TAG_FREE, true, + uploadIndexDrawPlan, *uploadIndexPacketFrame, uploadIndexGraph ); idModernGLSubmitPlan uploadIndexSubmitPlan; uploadIndexSubmitPlan.Build( uploadIndexDrawPlan ); const modernGLSubmitPlanStats_t &uploadStats = uploadIndexSubmitPlan.Stats(); diff --git a/src/renderer/ModernGLSubmitPlan.h b/src/renderer/ModernGLSubmitPlan.h index 7a58214a..bb802b01 100644 --- a/src/renderer/ModernGLSubmitPlan.h +++ b/src/renderer/ModernGLSubmitPlan.h @@ -11,7 +11,13 @@ const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_MAIN = 0; const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_NORMAL = 1; const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_SPECULAR = 2; const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_EMISSIVE = 3; -const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_COUNT = 4; +// PBR's separate metallic/roughness/AO layout uses these direct samplers. +// They stay below the texture-array/shadow range, so this fits the GL 3.3 +// floor without overlapping a cubemap shadow sampler. +const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_METALLIC = 4; +const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_ROUGHNESS = 5; +const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_AO = 6; +const int MODERN_GL_SUBMIT_MATERIAL_TEXTURE_COUNT = 7; typedef struct modernGLSubmitCommand_s { const modernGLDrawPlanEntry_t *drawPlanEntry; @@ -31,10 +37,14 @@ typedef struct modernGLSubmitCommand_s { int modelViewMatrixLocation; int debugColorLocation; int localParamsLocation; + int pbrIBLLocation; int mainTextureLocation; int normalTextureLocation; int specularTextureLocation; int emissiveTextureLocation; + int metallicTextureLocation; + int roughnessTextureLocation; + int aoTextureLocation; int textureIndicesLocation; int textureTableModeLocation; int materialFlagsLocation; diff --git a/src/renderer/ModernLightImageAtlas.cpp b/src/renderer/ModernLightImageAtlas.cpp index 6816bed1..9a94690b 100644 --- a/src/renderer/ModernLightImageAtlas.cpp +++ b/src/renderer/ModernLightImageAtlas.cpp @@ -44,6 +44,8 @@ typedef struct modernLightAtlasEntry_s { unsigned int sourceTexnum; // re-upload when the source is reloaded int cell; int lastUsedFrame; + int uploadWidth; // re-upload when a reload changes dims under a recycled GL name + int uploadHeight; bool resident; bool pendingUpload; // reserved this frame, not yet copied in float rect[4]; @@ -249,6 +251,8 @@ static int R_ModernLightImageAtlas_ClaimEntry( void ) { rg_modernLightAtlasEntries[oldest].resident = false; rg_modernLightAtlasEntries[oldest].image = NULL; rg_modernLightAtlasEntries[oldest].sourceTexnum = 0; + rg_modernLightAtlasEntries[oldest].uploadWidth = 0; + rg_modernLightAtlasEntries[oldest].uploadHeight = 0; rg_modernLightAtlasEntries[oldest].cell = oldest; } return oldest; @@ -312,6 +316,10 @@ void R_ModernLightImageAtlas_FlushUploads( void ) { } R_GLStateCache().ActiveTextureUnit( 0 ); + // Diagnostic resident/live counts are refreshed once here (after all Acquires + // for the frame) instead of on every Acquire; the values read by gfxInfo are + // identical since no Acquire runs between the last Acquire and this flush. + R_ModernLightImageAtlas_RefreshCounts(); } modernLightAtlasReject_t R_ModernLightImageAtlas_Acquire( const idImage *image, float rect[4] ) { @@ -346,14 +354,19 @@ modernLightAtlasReject_t R_ModernLightImageAtlas_Acquire( const idImage *image, } int entryIndex = R_ModernLightImageAtlas_FindEntry( image ); - const bool stale = entryIndex >= 0 && rg_modernLightAtlasEntries[entryIndex].sourceTexnum != R_ModernLightImageAtlas_SourceHandle( image ); + // A reload can recycle the same GL name with different dimensions, which + // would pass a texnum-only staleness check while the resident texels and the + // rect (scaled for the old dims) are wrong, so compare dimensions too. + const bool stale = entryIndex >= 0 + && ( rg_modernLightAtlasEntries[entryIndex].sourceTexnum != R_ModernLightImageAtlas_SourceHandle( image ) + || rg_modernLightAtlasEntries[entryIndex].uploadWidth != sourceWidth + || rg_modernLightAtlasEntries[entryIndex].uploadHeight != sourceHeight ); if ( entryIndex >= 0 && !stale ) { rg_modernLightAtlasEntries[entryIndex].lastUsedFrame = rg_modernLightAtlasFrame; if ( rect != NULL ) { memcpy( rect, rg_modernLightAtlasEntries[entryIndex].rect, sizeof( float ) * 4 ); } rg_modernLightAtlasStats.cacheHits++; - R_ModernLightImageAtlas_RefreshCounts(); return MODERN_LIGHT_ATLAS_REJECT_NONE; } @@ -368,6 +381,8 @@ modernLightAtlasReject_t R_ModernLightImageAtlas_Acquire( const idImage *image, modernLightAtlasEntry_t &entry = rg_modernLightAtlasEntries[entryIndex]; entry.image = image; entry.sourceTexnum = R_ModernLightImageAtlas_SourceHandle( image ); + entry.uploadWidth = sourceWidth; + entry.uploadHeight = sourceHeight; entry.cell = entryIndex; entry.resident = true; entry.lastUsedFrame = rg_modernLightAtlasFrame; @@ -382,7 +397,6 @@ modernLightAtlasReject_t R_ModernLightImageAtlas_Acquire( const idImage *image, if ( rect != NULL ) { memcpy( rect, entry.rect, sizeof( float ) * 4 ); } - R_ModernLightImageAtlas_RefreshCounts(); return MODERN_LIGHT_ATLAS_REJECT_NONE; } diff --git a/src/renderer/ModernShadowPlanner.cpp b/src/renderer/ModernShadowPlanner.cpp index e4cdb8fd..cd0da9fd 100644 --- a/src/renderer/ModernShadowPlanner.cpp +++ b/src/renderer/ModernShadowPlanner.cpp @@ -69,8 +69,17 @@ static renderBackendCaps_t rg_modernShadowPlannerCaps; static renderFeatureSet_t rg_modernShadowPlannerFeatures; static modernShadowPlannerStats_t rg_modernShadowPlannerStats; static idList rg_modernShadowPlannerDescriptors; +// Index of descriptor slots by viewLight pointer, rebuilt in lockstep with the +// list each PrepareFrame so per-light lookups from the hot draw paths are O(1) +// instead of a linear scan. +static idHashIndex rg_modernShadowPlannerDescriptorHash; + +static ID_INLINE int R_ModernShadowPlanner_DescriptorKey( const viewLight_t *viewLight ) { + return ( int )( ( ( uintptr_t )viewLight ) >> 4 ); +} static modernShadowPlannerFairnessHistory_t rg_modernShadowPlannerFairnessHistory[MODERN_SHADOW_FAIRNESS_HISTORY_SLOTS]; static int rg_modernShadowPlannerFairnessEpoch = 0; +static int rg_modernShadowPlannerFairnessHighWater = 0; // count of ever-allocated (valid) fairness slots; valid slots always form the prefix [0, highWater) static bool rg_modernShadowPlannerInitialized = false; static void R_ModernShadowPlanner_SetStatus( modernShadowPlannerStats_t &stats, const char *status ) { @@ -80,6 +89,7 @@ static void R_ModernShadowPlanner_SetStatus( modernShadowPlannerStats_t &stats, static void R_ModernShadowPlanner_ResetFairnessHistory( void ) { memset( rg_modernShadowPlannerFairnessHistory, 0, sizeof( rg_modernShadowPlannerFairnessHistory ) ); rg_modernShadowPlannerFairnessEpoch = 0; + rg_modernShadowPlannerFairnessHighWater = 0; } static const idRenderLightLocal *R_ModernShadowPlanner_FairnessLightDef( const modernShadowLightDescriptor_t &descriptor ) { @@ -102,14 +112,22 @@ static modernShadowPlannerFairnessHistory_t *R_ModernShadowPlanner_FindFairnessH return NULL; } + // Valid slots always form the prefix [0, highWater): slots become valid only + // via allocation, which reuses a stale slot inside the prefix or claims the + // slot at the prefix end, and only ResetFairnessHistory clears slots. The + // scan is therefore bounded by the high-water mark and stays byte-for-byte + // equivalent to a full scan over all MODERN_SHADOW_FAIRNESS_HISTORY_SLOTS. + const int scanLimit = Min( rg_modernShadowPlannerFairnessHighWater, MODERN_SHADOW_FAIRNESS_HISTORY_SLOTS ); modernShadowPlannerFairnessHistory_t *oldest = &rg_modernShadowPlannerFairnessHistory[0]; - for ( int historyIndex = 0; historyIndex < MODERN_SHADOW_FAIRNESS_HISTORY_SLOTS; ++historyIndex ) { + bool oldestFromFreeSlot = false; + for ( int historyIndex = 0; historyIndex < scanLimit; ++historyIndex ) { modernShadowPlannerFairnessHistory_t &history = rg_modernShadowPlannerFairnessHistory[historyIndex]; if ( R_ModernShadowPlanner_FairnessHistoryMatches( history, descriptor ) ) { return &history; } if ( allocate && ( !history.valid || rg_modernShadowPlannerFairnessEpoch - history.lastSeenEpoch > MODERN_SHADOW_FAIRNESS_STALE_EPOCHS ) ) { oldest = &history; + oldestFromFreeSlot = true; break; } if ( history.lastSeenEpoch < oldest->lastSeenEpoch ) { @@ -121,6 +139,11 @@ static modernShadowPlannerFairnessHistory_t *R_ModernShadowPlanner_FindFairnessH return NULL; } + if ( !oldestFromFreeSlot && scanLimit < MODERN_SHADOW_FAIRNESS_HISTORY_SLOTS ) { + // the first invalid slot sits exactly at the prefix end; this is where the + // original full scan's first-free-slot break would have landed + oldest = &rg_modernShadowPlannerFairnessHistory[scanLimit]; + } memset( oldest, 0, sizeof( *oldest ) ); oldest->valid = true; oldest->lightDef = R_ModernShadowPlanner_FairnessLightDef( descriptor ); @@ -129,6 +152,10 @@ static modernShadowPlannerFairnessHistory_t *R_ModernShadowPlanner_FindFairnessH oldest->lastPolicy = -1; oldest->lastFallbackReason = -1; oldest->lastSeenEpoch = rg_modernShadowPlannerFairnessEpoch; + const int oldestIndex = static_cast( oldest - rg_modernShadowPlannerFairnessHistory ); + if ( oldestIndex >= rg_modernShadowPlannerFairnessHighWater ) { + rg_modernShadowPlannerFairnessHighWater = oldestIndex + 1; + } return oldest; } @@ -293,6 +320,7 @@ static void R_ModernShadowPlanner_ApplyArb2CacheEstimate( modernShadowLightDescr descriptor.arb2UnshadowedPasses = estimate.unshadowedPasses; descriptor.arb2CacheablePassMask = estimate.cacheablePassMask; descriptor.arb2CacheHitPassMask = estimate.cacheHitPassMask; + descriptor.arb2CacheKeyHitMask = estimate.cacheKeyHitMask; descriptor.arb2CacheMissPassMask = estimate.cacheMissPassMask; descriptor.arb2FreshUpdatePassMask = estimate.freshUpdatePassMask; descriptor.arb2BudgetFallbackPassMask = estimate.budgetFallbackPassMask; @@ -304,29 +332,90 @@ static void R_ModernShadowPlanner_ApplyArb2CacheEstimate( modernShadowLightDescr estimate.cacheMissPasses == 0; } -// Resolves the light's persistent-atlas placement (5c). The slot is -// consumable by modern receivers only when the GLOBAL-pass cache entry's -// signature matches this frame's content (the estimate replayed it) and the -// static tiles are the light's complete content - dynamic casters are -// composed over scratch per frame (5b) and never reach the atlas cell. -static void R_ModernShadowPlanner_ResolveArb2AtlasSlot( modernShadowLightDescriptor_t &descriptor, const viewLight_t *vLight ) { +// Modern clustered lighting owns one shadow resource per light, not separate +// LOCAL/GLOBAL receiver passes and not a stencil-composited hybrid. A cached +// GLOBAL map is therefore consumable only when it represents every active +// receiver exactly and contains every parity-relevant caster by itself. +static bool R_ModernShadowPlanner_Arb2SingleResourceComplete( + const viewLight_t *vLight ) { + if ( vLight == NULL ) { + return false; + } + unsigned int activeReceiverMask = 0; + if ( vLight->localInteractions != NULL ) { + activeReceiverMask |= SHADOWMAP_RECEIVER_MASK_LOCAL; + } + if ( vLight->globalInteractions != NULL + || vLight->translucentInteractions != NULL ) { + activeReceiverMask |= SHADOWMAP_RECEIVER_MASK_GLOBAL; + } + const unsigned int incompleteMapMask = static_cast( + vLight->shadowMapIncompleteMapMask + | vLight->shadowMapHybridIncompleteMask + | vLight->shadowMapPrelightMapMissingMask ); + if ( ( incompleteMapMask & activeReceiverMask ) != 0 ) { + return false; + } + if ( ( activeReceiverMask & SHADOWMAP_RECEIVER_MASK_LOCAL ) != 0 + && ( vLight->localShadowMapCasters != NULL + || vLight->localShadowMapDynamicCasters != NULL + || vLight->localTranslucentShadowMapCasters != NULL ) ) { + return false; + } + // Supplement chains should imply an incomplete-map bit. Keep this + // independent check fail-closed if frontend bookkeeping ever diverges. + if ( ( activeReceiverMask & SHADOWMAP_RECEIVER_MASK_LOCAL ) != 0 + && vLight->globalShadowMapStencilSupplements != NULL ) { + return false; + } + if ( ( activeReceiverMask & SHADOWMAP_RECEIVER_MASK_GLOBAL ) != 0 + && ( vLight->globalShadowMapStencilSupplements != NULL + || vLight->localShadowMapStencilSupplements != NULL ) ) { + return false; + } + return activeReceiverMask != 0; +} + +// Resolves the physical ARB2 resource which a modern receiver can sample. +// Projected lights reference a persistent-atlas cell; point lights reference +// the one exact cache cube currently selected by classic GL. Both require a +// current GLOBAL-pass signature and complete static content. +static void R_ModernShadowPlanner_ResolveArb2AtlasSlot( modernShadowLightDescriptor_t &descriptor, const viewLight_t *vLight, const viewDef_t *viewDef ) { descriptor.arb2AtlasSlotReady = false; descriptor.arb2AtlasCellX = -1; descriptor.arb2AtlasCellY = -1; descriptor.arb2AtlasCellSpan = 0; + descriptor.arb2AtlasSignature = 0; + descriptor.arb2AtlasStorageGeneration = 0; descriptor.arb2AtlasContentFrame = -1; + descriptor.arb2PointCubeReady = false; + descriptor.arb2PointCubeSignature = 0; + descriptor.arb2PointCubeContentFrame = -1; memset( descriptor.arb2AtlasCascadeRect, 0, sizeof( descriptor.arb2AtlasCascadeRect ) ); - if ( descriptor.pointLight || vLight == NULL || descriptor.lightDefIndex < 0 ) { - return; - } - shadowMapArb2AtlasSlot_t slot; - if ( !RB_ShadowMapProjectedAtlasSlotForLight( descriptor.lightDefIndex, slot ) ) { + if ( vLight == NULL || descriptor.lightDefIndex < 0 ) { return; } const bool contentCurrent = descriptor.arb2CacheEstimateValid - && ( descriptor.arb2CacheHitPassMask & SHADOWMAP_ARB2_CACHE_PASS_GLOBAL ) != 0; + && ( descriptor.arb2CacheKeyHitMask & SHADOWMAP_ARB2_CACHE_PASS_GLOBAL ) != 0; const bool staticContentComplete = vLight->shadowMapDynamicCasterCount == 0; - if ( !contentCurrent || !staticContentComplete ) { + if ( !contentCurrent || !staticContentComplete + || !R_ModernShadowPlanner_Arb2SingleResourceComplete( vLight ) ) { + return; + } + if ( descriptor.pointLight ) { + shadowMapArb2PointCube_t cube; + if ( !RB_ShadowMapPointCubeForLight( vLight, viewDef, cube ) + || !cube.valid || cube.size != descriptor.resolution + || cube.lastUpdatedFrame < 0 || cube.lastUpdatedFrame > tr.frameCount ) { + return; + } + descriptor.arb2PointCubeSignature = cube.signature; + descriptor.arb2PointCubeContentFrame = cube.lastUpdatedFrame; + descriptor.arb2PointCubeReady = true; + return; + } + shadowMapArb2AtlasSlot_t slot; + if ( !RB_ShadowMapProjectedAtlasSlotForLight( vLight, viewDef, slot ) ) { return; } if ( slot.cascadeCount != Max( 1, descriptor.cascadeCount ) ) { @@ -335,6 +424,8 @@ static void R_ModernShadowPlanner_ResolveArb2AtlasSlot( modernShadowLightDescrip descriptor.arb2AtlasCellX = slot.cellX; descriptor.arb2AtlasCellY = slot.cellY; descriptor.arb2AtlasCellSpan = slot.cellSpan; + descriptor.arb2AtlasSignature = slot.signature; + descriptor.arb2AtlasStorageGeneration = slot.storageGeneration; descriptor.arb2AtlasContentFrame = slot.lastUpdatedFrame; const int cascadeLimit = Min( MODERN_SHADOW_DESCRIPTOR_MAX_CASCADES, SHADOWMAP_PROJECTED_MAX_CASCADES ); for ( int i = 0; i < cascadeLimit; ++i ) { @@ -353,9 +444,12 @@ static void R_ModernShadowPlanner_ResolveArb2AtlasSlot( modernShadowLightDescrip static bool R_ModernShadowPlanner_CanIsolateArb2CacheOwnership( const modernShadowLightDescriptor_t &descriptor ) { // With modern receiver sampling live, cache reuse stays budget-exempt // only when the receivers can actually consume the cached tiles - the - // persistent-atlas slot (5c) makes that possible; without it the light - // must compete for a mapped update instead of silently losing shadows. - if ( descriptor.modernReceiverSamplingReady && !descriptor.arb2AtlasSlotReady ) { + // persistent projected-atlas slot or exact point cube makes that possible; + // without it the light must compete for an update instead of silently + // sampling another light's resource. + const bool physicalResourceReady = descriptor.pointLight + ? descriptor.arb2PointCubeReady : descriptor.arb2AtlasSlotReady; + if ( descriptor.modernReceiverSamplingReady && !physicalResourceReady ) { return false; } return descriptor.arb2CacheEstimateValid @@ -856,8 +950,41 @@ static void R_ModernShadowPlanner_InitDescriptorContract( modernShadowLightDescr descriptor.updateFrame = tr.frameCount; descriptor.casterCount = R_ModernShadowPlanner_TotalCasterCount( descriptor ); descriptor.receiverCount = R_ModernShadowPlanner_TotalReceiverCount( descriptor ); - descriptor.bias[0] = descriptor.pointLight ? r_shadowMapPointBias.GetFloat() : r_shadowMapBias.GetFloat(); - descriptor.bias[1] = descriptor.pointLight ? r_shadowMapPointNormalBias.GetFloat() : r_shadowMapNormalBias.GetFloat(); + if ( descriptor.pointLight ) { + const float pointFar = R_ShadowMapPointFarDistance( vLight ); + const shadowMapPointReceiverSettings_t basePointReceiverSettings = + R_ClampShadowMapPointReceiverSettings( + pointFar, descriptor.resolution, + r_shadowMapPointBias.GetFloat(), + r_shadowMapPointNormalBias.GetFloat(), + r_shadowMapTexelBiasScale.GetFloat(), + 0.0f, + r_shadowMapPointMaxWorldBias.GetFloat() ); + shadowMapPointReceiverSettings_t pointReceiverSettings = + basePointReceiverSettings; +#ifndef OPENQ4_RENDERER_VK_MODULE + // Modern GL always samples the manual color-depth cube, even when the + // classic receiver requested and obtained a hardware comparison cube. + // Preserve its storage-quantization floor as well as the shared + // world-space cap so mixed classic/modern materials cannot disagree. + pointReceiverSettings = + R_ShadowMapPointStorageAdjustedReceiverSettings( + basePointReceiverSettings, pointFar, + descriptor.resolution, + false, + r_shadowMapPointHighPrecision.GetBool() ); +#endif + descriptor.bias[0] = pointReceiverSettings.constantBias; + descriptor.bias[1] = pointReceiverSettings.normalBias; + descriptor.texelDepthBias[0] = + pointReceiverSettings.texelBiasScale + / static_cast( Max( 1, descriptor.resolution ) ); + // The modern point receiver does not yet apply geometric normal offset; + // its bounded depth-only contract above keeps that limitation explicit. + } else { + descriptor.bias[0] = r_shadowMapBias.GetFloat(); + descriptor.bias[1] = r_shadowMapNormalBias.GetFloat(); + } descriptor.bias[2] = r_shadowMapPolygonFactor.GetFloat(); descriptor.bias[3] = r_shadowMapPolygonOffset.GetFloat(); descriptor.casterPassReady = descriptor.casterCount > 0; @@ -919,13 +1046,12 @@ static bool R_ModernShadowPlanner_ValidateProjectedDescriptor( modernShadowLight } if ( descriptor.arb2AtlasSlotReady ) { - valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_ATLAS_SLOT, descriptor.arb2AtlasCellSpan > 0 && descriptor.arb2AtlasCellX >= 0 && descriptor.arb2AtlasCellY >= 0, stage, "persistent-atlas slot cell placement is invalid" ); + valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_ATLAS_SLOT, descriptor.arb2AtlasCellSpan > 0 && descriptor.arb2AtlasCellX >= 0 && descriptor.arb2AtlasCellY >= 0 && descriptor.arb2AtlasStorageGeneration != 0, stage, "persistent-atlas slot provenance is invalid" ); for ( int cascadeIndex = 0; cascadeIndex < activeCascadeCount; ++cascadeIndex ) { valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_ATLAS_SLOT, R_ModernShadowPlanner_AtlasRectMinMaxReady( descriptor.arb2AtlasCascadeRect[cascadeIndex] ), stage, "persistent-atlas slot rectangle is outside normalized bounds" ); } valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_FRESHNESS, descriptor.arb2AtlasContentFrame >= 0 && descriptor.arb2AtlasContentFrame <= tr.frameCount, stage, "persistent-atlas slot content frame is invalid" ); } - return valid; } @@ -952,6 +1078,10 @@ static bool R_ModernShadowPlanner_ValidateDescriptor( modernShadowLightDescripto } else { valid &= R_ModernShadowPlanner_ValidateProjectedDescriptor( descriptor, classification, stage ); } + if ( descriptor.arb2PointCubeReady ) { + valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_ATLAS_SLOT, descriptor.pointLight, stage, "point-cube provenance attached to a projected light" ); + valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_FRESHNESS, descriptor.arb2PointCubeContentFrame >= 0 && descriptor.arb2PointCubeContentFrame <= tr.frameCount, stage, "point-cube content frame is invalid" ); + } if ( descriptor.mapType == MODERN_SHADOW_MAP_CASCADE ) { valid &= R_ModernShadowPlanner_CheckDescriptorInvariant( descriptor, MODERN_SHADOW_DESCRIPTOR_INVARIANT_MAP_COUNTS, !descriptor.pointLight && descriptor.cascadeCount >= 2 && descriptor.tileCount == descriptor.cascadeCount, stage, "cascade descriptors must use one tile per cascade" ); @@ -980,6 +1110,12 @@ static void R_ModernShadowPlanner_SetDescriptorInvariantPolicy( modernShadowLigh descriptor.cacheReuseCandidate = false; descriptor.atlasTileReady = false; descriptor.arb2AtlasSlotReady = false; + descriptor.arb2PointCubeReady = false; + descriptor.arb2AtlasSignature = 0; + descriptor.arb2AtlasStorageGeneration = 0; + descriptor.arb2PointCubeSignature = 0; + descriptor.arb2AtlasContentFrame = -1; + descriptor.arb2PointCubeContentFrame = -1; } static modernShadowFallbackReason_t R_ModernShadowPlanner_SupportReason( const viewLight_t *vLight ) { @@ -1085,7 +1221,7 @@ static void R_ModernShadowPlanner_InitDescriptor( modernShadowLightDescriptor_t descriptor.budgetClass = R_ModernShadowPlanner_BudgetClassForDescriptor( descriptor ); R_ModernShadowPlanner_InitDescriptorContract( descriptor, vLight, viewDef ); R_ModernShadowPlanner_ApplyArb2CacheEstimate( descriptor, vLight, viewDef ); - R_ModernShadowPlanner_ResolveArb2AtlasSlot( descriptor, vLight ); + R_ModernShadowPlanner_ResolveArb2AtlasSlot( descriptor, vLight, viewDef ); descriptor.estimatedPixels = descriptor.resolution * descriptor.resolution * Max( 1, descriptor.tileCount ); const int scissorArea = vLight != NULL ? R_ModernShadowPlanner_ScissorArea( vLight->scissorRect ) : 0; descriptor.priority = @@ -1123,21 +1259,20 @@ static bool R_ModernShadowPlanner_CandidateBetter( const modernShadowLightDescri return candidate.lightDefIndex < best.lightDefIndex; } -static int R_ModernShadowPlanner_FindBestCandidate( const bool *selected ) { - int best = -1; - for ( int i = 0; i < rg_modernShadowPlannerDescriptors.Num(); ++i ) { - if ( selected[i] ) { - continue; - } - const modernShadowLightDescriptor_t &candidate = rg_modernShadowPlannerDescriptors[i]; - if ( candidate.fallbackReason != MODERN_SHADOW_FALLBACK_NONE ) { - continue; - } - if ( best < 0 || R_ModernShadowPlanner_CandidateBetter( candidate, rg_modernShadowPlannerDescriptors[best] ) ) { - best = i; - } +// Total order over descriptor indices matching the repeated best-candidate +// rescan: strictly-better wins, and a full tie keeps the lower descriptor index +// (what the linear scan's "only replace on strictly better" produces). The +// explicit index tie-break makes the result independent of the unstable sort. +static int R_ModernShadowPlanner_CompareCandidateIndices( const int *a, const int *b ) { + const modernShadowLightDescriptor_t &A = rg_modernShadowPlannerDescriptors[*a]; + const modernShadowLightDescriptor_t &B = rg_modernShadowPlannerDescriptors[*b]; + if ( R_ModernShadowPlanner_CandidateBetter( A, B ) ) { + return -1; + } + if ( R_ModernShadowPlanner_CandidateBetter( B, A ) ) { + return 1; } - return best; + return *a - *b; } static void R_ModernShadowPlanner_AssignAtlasTiles( modernShadowLightDescriptor_t &descriptor, int firstTile, int tilesPerRow ) { @@ -1183,8 +1318,6 @@ static void R_ModernShadowPlanner_SetBudgetMissPolicy( modernShadowLightDescript } static void R_ModernShadowPlanner_SelectMappedLights( modernShadowPlannerStats_t &stats ) { - bool selected[MODERN_SHADOW_PLAN_MAX_LIGHTS]; - memset( selected, 0, sizeof( selected ) ); modernShadowPlannerBudgetUse_t budgetUse; memset( &budgetUse, 0, sizeof( budgetUse ) ); int mappedLights = 0; @@ -1192,12 +1325,22 @@ static void R_ModernShadowPlanner_SelectMappedLights( modernShadowPlannerStats_t int usedPixels = 0; const int tilePixels = Max( 1, stats.shadowMapSize ) * Max( 1, stats.shadowMapSize ); const int atlasSlotsPerRow = Max( 1, static_cast( idMath::Ceil( idMath::Sqrt( static_cast( stats.maxAtlasTiles ) ) ) ) ); - for ( ;; ) { - const int candidateIndex = R_ModernShadowPlanner_FindBestCandidate( selected ); - if ( candidateIndex < 0 ) { - break; + + // Collect the selectable candidates once and order them exactly as the + // repeated best-candidate rescan would, then process in that fixed order. + // Processing a candidate only mutates that candidate's own keys, so a + // pre-sorted order is identical to the O(selectable * total) rescan. + static idList selectionOrder; + selectionOrder.SetNum( 0, false ); + for ( int i = 0; i < rg_modernShadowPlannerDescriptors.Num(); ++i ) { + if ( rg_modernShadowPlannerDescriptors[i].fallbackReason == MODERN_SHADOW_FALLBACK_NONE ) { + selectionOrder.Append( i ); } - selected[candidateIndex] = true; + } + selectionOrder.Sort( R_ModernShadowPlanner_CompareCandidateIndices ); + + for ( int orderIndex = 0; orderIndex < selectionOrder.Num(); ++orderIndex ) { + const int candidateIndex = selectionOrder[orderIndex]; modernShadowLightDescriptor_t &candidate = rg_modernShadowPlannerDescriptors[candidateIndex]; const modernShadowBudgetClass_t budgetClass = R_ModernShadowPlanner_BudgetClassForDescriptor( candidate ); candidate.budgetClass = budgetClass; @@ -1642,6 +1785,7 @@ void R_ModernShadowPlanner_PrepareFrame( const idScenePacketFrame &packetFrame, const bool initialized = rg_modernShadowPlannerInitialized; const bool available = initialized && rg_modernShadowPlannerFeatures.scenePackets; rg_modernShadowPlannerDescriptors.SetNum( 0, false ); + rg_modernShadowPlannerDescriptorHash.Clear(); memset( &rg_modernShadowPlannerStats, 0, sizeof( rg_modernShadowPlannerStats ) ); rg_modernShadowPlannerStats.initialized = initialized; rg_modernShadowPlannerStats.available = available; @@ -1694,22 +1838,34 @@ void R_ModernShadowPlanner_PrepareFrame( const idScenePacketFrame &packetFrame, rg_modernShadowPlannerStats.overflow = true; break; } - modernShadowLightDescriptor_t descriptor; + // Build the descriptor in place. idList::Alloc grows by granularity + // and returns a reference to the new slot, avoiding the ~1.4KB stack + // temporary and the whole-struct Append copy (InitDescriptor memsets + // the slot first, so a recycled slot cannot leak stale bytes). + const int descriptorIndex = rg_modernShadowPlannerDescriptors.Num(); + modernShadowLightDescriptor_t &descriptor = rg_modernShadowPlannerDescriptors.Alloc(); R_ModernShadowPlanner_InitDescriptor( descriptor, vLight, scene.viewDef, sceneIndex, - rg_modernShadowPlannerDescriptors.Num(), + descriptorIndex, rg_modernShadowPlannerStats.shadowMapSize, rg_modernShadowPlannerStats.translucentEnabled ); - rg_modernShadowPlannerDescriptors.Append( descriptor ); } if ( rg_modernShadowPlannerStats.overflow ) { break; } } + // Index the finalized descriptor list by viewLight pointer for the O(1) + // DescriptorForLight lookups the hot draw paths make; the slots are appended + // once above and never reordered afterward. + for ( int i = 0; i < rg_modernShadowPlannerDescriptors.Num(); ++i ) { + rg_modernShadowPlannerDescriptorHash.Add( + R_ModernShadowPlanner_DescriptorKey( rg_modernShadowPlannerDescriptors[i].viewLight ), i ); + } + R_ModernShadowPlanner_SelectMappedLights( rg_modernShadowPlannerStats ); R_ModernShadowPlanner_ValidatePreparedDescriptors(); R_ModernShadowPlanner_UpdateFairnessHistory( rg_modernShadowPlannerStats ); @@ -1833,8 +1989,13 @@ const modernShadowLightDescriptor_t *R_ModernShadowPlanner_DescriptorForLight( c if ( viewLight == NULL ) { return NULL; } - for ( int i = 0; i < rg_modernShadowPlannerDescriptors.Num(); ++i ) { - if ( rg_modernShadowPlannerDescriptors[i].viewLight == viewLight ) { + // The hash is rebuilt in lockstep with the descriptor list each PrepareFrame, + // so a lookup here is exact; the index guard is defensive only. + const int key = R_ModernShadowPlanner_DescriptorKey( viewLight ); + for ( int i = rg_modernShadowPlannerDescriptorHash.First( key ); i != -1; + i = rg_modernShadowPlannerDescriptorHash.Next( i ) ) { + if ( i < rg_modernShadowPlannerDescriptors.Num() + && rg_modernShadowPlannerDescriptors[i].viewLight == viewLight ) { return &rg_modernShadowPlannerDescriptors[i]; } } diff --git a/src/renderer/ModernShadowPlanner.h b/src/renderer/ModernShadowPlanner.h index 148245ff..7dba011b 100644 --- a/src/renderer/ModernShadowPlanner.h +++ b/src/renderer/ModernShadowPlanner.h @@ -4,6 +4,8 @@ #ifndef __MODERN_SHADOW_PLANNER_H__ #define __MODERN_SHADOW_PLANNER_H__ +#include + #include "RendererCaps.h" #include "ScenePackets.h" @@ -169,13 +171,22 @@ typedef struct modernShadowLightDescriptor_s { // resolved persistent-atlas placement (5c): true when the light's GLOBAL // cache entry holds live cells whose signature matches this frame's // content and no dynamic casters are missing from the static tiles - - // the state in which modern receivers may sample the cell directly + // the state in which modern receivers may sample the cell directly. The + // storage generation closes the export-to-consumption recreation window. bool arb2AtlasSlotReady; int arb2AtlasCellX; int arb2AtlasCellY; int arb2AtlasCellSpan; + int arb2AtlasSignature; + std::uint64_t arb2AtlasStorageGeneration; int arb2AtlasContentFrame; float arb2AtlasCascadeRect[MODERN_SHADOW_DESCRIPTOR_MAX_CASCADES][4]; + // Exact provenance for the singular point cube bound by modern GL. This + // is separate from projected atlas placement because each cached point + // light owns a distinct cube and only the currently selected one is bound. + bool arb2PointCubeReady; + int arb2PointCubeSignature; + int arb2PointCubeContentFrame; bool atlasTileReady; bool casterPassReady; bool cutoutCasterReady; @@ -195,6 +206,7 @@ typedef struct modernShadowLightDescriptor_s { int arb2UnshadowedPasses; unsigned int arb2CacheablePassMask; unsigned int arb2CacheHitPassMask; + unsigned int arb2CacheKeyHitMask; unsigned int arb2CacheMissPassMask; unsigned int arb2FreshUpdatePassMask; unsigned int arb2BudgetFallbackPassMask; diff --git a/src/renderer/ModernSpecularProbeAtlas.cpp b/src/renderer/ModernSpecularProbeAtlas.cpp new file mode 100644 index 00000000..6b76a736 --- /dev/null +++ b/src/renderer/ModernSpecularProbeAtlas.cpp @@ -0,0 +1,743 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "ModernSpecularProbeAtlas.h" +#include "GLStateCache.h" + +/* +================================================================================ + + Authored reflection cubemaps are copied into a fixed 2D face atlas so the + future clustered path needs one portable sampler rather than bindless handles + or a sampler-cube array. Acquire is CPU-only. FlushUploads owns the bounded + readback/upload batch and never binds a framebuffer, program, VAO, or viewport. + + Residency is keyed by image identity, device handle, and image storage + generation. LRU selection uses frame generations with a stable slot-index tie + break, and no slot used in the current frame may be evicted. + +================================================================================ +*/ + +typedef struct modernSpecularProbeAtlasEntry_s { + const idImage * image; + unsigned int sourceHandle; + std::uint64_t sourceStorageGeneration; + std::uint64_t residencyGeneration; + std::uint64_t lastUsedFrame; + int slot; + int faceSize; + bool resident; + bool pendingUpload; + bool uploaded; + modernSpecularProbeAtlasPlacement_t placement; +} modernSpecularProbeAtlasEntry_t; + +static bool rg_modernSpecularProbeAtlasAvailable = false; +static bool rg_modernSpecularProbeAtlasInitialized = false; +static GLuint rg_modernSpecularProbeAtlasTexture = 0; +static modernSpecularProbeAtlasEntry_t + rg_modernSpecularProbeAtlasEntries[MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES]; +static std::uint64_t rg_modernSpecularProbeAtlasGenerationCounter = 0; +static std::uint64_t rg_modernSpecularProbeAtlasGeneration = 0; +static std::uint64_t rg_modernSpecularProbeAtlasNextResidencyGeneration = 0; +static std::uint64_t rg_modernSpecularProbeAtlasFrame = 0; +static modernSpecularProbeAtlasStats_t rg_modernSpecularProbeAtlasStats; + +const char *ModernSpecularProbeAtlasReject_Name( + modernSpecularProbeAtlasReject_t reject ) { + switch ( reject ) { + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE: return "none"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_OUTPUT: return "null-output"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_IMAGE: return "null-image"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_UNAVAILABLE: return "unavailable"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_LOADED: return "image-not-loaded"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_DEFAULTED: return "image-defaulted"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_MUTABLE: return "mutable-image"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_CUBE: return "not-cubemap"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NON_SQUARE: return "non-square-cubemap"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_OVERSIZED: return "cubemap-oversized"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_INVALID_STORAGE: return "invalid-storage"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_ATLAS_FULL: return "atlas-full"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED: return "source-changed"; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_UPLOAD_UNAVAILABLE:return "upload-unavailable"; + default: return "unknown"; + } +} + +static std::uint64_t R_ModernSpecularProbeAtlas_AdvanceGeneration( + std::uint64_t &generation ) { + generation++; + if ( generation == 0 ) { + generation = 1; + } + return generation; +} + +static void R_ModernSpecularProbeAtlas_SetStatus( const char *status ) { + idStr::Copynz( rg_modernSpecularProbeAtlasStats.status, + status != NULL ? status : "unknown", + sizeof( rg_modernSpecularProbeAtlasStats.status ) ); +} + +static void R_ModernSpecularProbeAtlas_ResetEntries( void ) { + memset( rg_modernSpecularProbeAtlasEntries, 0, + sizeof( rg_modernSpecularProbeAtlasEntries ) ); + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + rg_modernSpecularProbeAtlasEntries[slot].slot = slot; + ModernSpecularProbeAtlas_ClearPlacement( + rg_modernSpecularProbeAtlasEntries[slot].placement ); + } +} + +static void R_ModernSpecularProbeAtlas_RefreshCounts( void ) { + int resident = 0; + int live = 0; + int pending = 0; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + const modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + if ( !entry.resident ) { + continue; + } + resident++; + if ( entry.lastUsedFrame == rg_modernSpecularProbeAtlasFrame ) { + live++; + } + if ( entry.pendingUpload ) { + pending++; + } + } + rg_modernSpecularProbeAtlasStats.residentEntries = resident; + rg_modernSpecularProbeAtlasStats.liveEntries = live; + rg_modernSpecularProbeAtlasStats.pendingEntries = pending; +} + +static modernSpecularProbeAtlasReject_t R_ModernSpecularProbeAtlas_RecordReject( + modernSpecularProbeAtlasReject_t reject ) { + switch ( reject ) { + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_OUTPUT: + rg_modernSpecularProbeAtlasStats.rejectedNullOutput++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_IMAGE: + rg_modernSpecularProbeAtlasStats.rejectedNullImage++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_UNAVAILABLE: + rg_modernSpecularProbeAtlasStats.rejectedUnavailable++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_LOADED: + rg_modernSpecularProbeAtlasStats.rejectedNotLoaded++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_DEFAULTED: + rg_modernSpecularProbeAtlasStats.rejectedDefaulted++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_MUTABLE: + rg_modernSpecularProbeAtlasStats.rejectedMutable++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_CUBE: + rg_modernSpecularProbeAtlasStats.rejectedNotCube++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_NON_SQUARE: + rg_modernSpecularProbeAtlasStats.rejectedNonSquare++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_OVERSIZED: + rg_modernSpecularProbeAtlasStats.rejectedOversized++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_INVALID_STORAGE: + rg_modernSpecularProbeAtlasStats.rejectedInvalidStorage++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_ATLAS_FULL: + rg_modernSpecularProbeAtlasStats.rejectedAtlasFull++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED: + rg_modernSpecularProbeAtlasStats.rejectedSourceChanged++; + break; + case MODERN_SPECULAR_PROBE_ATLAS_REJECT_UPLOAD_UNAVAILABLE: + rg_modernSpecularProbeAtlasStats.rejectedUploadUnavailable++; + break; + default: + break; + } + idStr::Copynz( rg_modernSpecularProbeAtlasStats.lastReject, + ModernSpecularProbeAtlasReject_Name( reject ), + sizeof( rg_modernSpecularProbeAtlasStats.lastReject ) ); + rg_modernSpecularProbeAtlasStats.frameLossless = false; + rg_modernSpecularProbeAtlasStats.frameReady = false; + return reject; +} + +static unsigned int R_ModernSpecularProbeAtlas_SourceHandle( + const idImage *image ) { + return image != NULL + ? const_cast( image )->GetDeviceHandle() : 0; +} + +static modernSpecularProbeAtlasReject_t +R_ModernSpecularProbeAtlas_ValidateSource( const idImage *image ) { + if ( image == NULL ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_IMAGE; + } + if ( !rg_modernSpecularProbeAtlasAvailable + || !rg_modernSpecularProbeAtlasInitialized ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_UNAVAILABLE; + } + if ( !image->IsLoaded() ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_LOADED; + } + if ( image->IsDefaulted() ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_DEFAULTED; + } + if ( R_IsMutableRenderImage( image ) ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_MUTABLE; + } + if ( image->GetOpts().textureType != TT_CUBIC ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_CUBE; + } + const int width = image->GetUploadWidth(); + const int height = image->GetUploadHeight(); + if ( width <= 0 || height <= 0 || width != height ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NON_SQUARE; + } + if ( width > MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_OVERSIZED; + } + if ( R_ModernSpecularProbeAtlas_SourceHandle( image ) == 0 + || image->GetStorageGeneration() == 0 ) { + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_INVALID_STORAGE; + } + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE; +} + +static int R_ModernSpecularProbeAtlas_FindEntry( const idImage *image ) { + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + if ( rg_modernSpecularProbeAtlasEntries[slot].resident + && rg_modernSpecularProbeAtlasEntries[slot].image == image ) { + return slot; + } + } + return -1; +} + +static int R_ModernSpecularProbeAtlas_SelectSlot( + const modernSpecularProbeAtlasEntry_t *entries, + std::uint64_t currentFrame ) { + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + if ( !entries[slot].resident ) { + return slot; + } + } + + int oldest = -1; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + if ( entries[slot].lastUsedFrame == currentFrame ) { + continue; + } + if ( oldest < 0 + || entries[slot].lastUsedFrame < entries[oldest].lastUsedFrame ) { + oldest = slot; + } + } + return oldest; +} + +static bool R_ModernSpecularProbeAtlas_FingerprintMatches( + const modernSpecularProbeAtlasEntry_t &entry ) { + return entry.image != NULL + && entry.image->IsLoaded() + && !entry.image->IsDefaulted() + && !R_IsMutableRenderImage( entry.image ) + && entry.image->GetOpts().textureType == TT_CUBIC + && entry.image->GetUploadWidth() == entry.faceSize + && entry.image->GetUploadHeight() == entry.faceSize + && R_ModernSpecularProbeAtlas_SourceHandle( entry.image ) + == entry.sourceHandle + && entry.image->GetStorageGeneration() + == entry.sourceStorageGeneration; +} + +void R_ModernSpecularProbeAtlas_Init( const renderBackendCaps_t &caps, + const renderFeatureSet_t &features ) { + R_ModernSpecularProbeAtlas_Shutdown(); + memset( &rg_modernSpecularProbeAtlasStats, 0, + sizeof( rg_modernSpecularProbeAtlasStats ) ); + R_ModernSpecularProbeAtlas_ResetEntries(); + + rg_modernSpecularProbeAtlasStats.atlasSize = + MODERN_SPECULAR_PROBE_ATLAS_SIZE; + rg_modernSpecularProbeAtlasStats.faceSize = + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE; + rg_modernSpecularProbeAtlasStats.capacity = + MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; + idStr::Copynz( rg_modernSpecularProbeAtlasStats.lastReject, "none", + sizeof( rg_modernSpecularProbeAtlasStats.lastReject ) ); + + const bool textureLimitReady = + caps.maxTextureSize >= MODERN_SPECULAR_PROBE_ATLAS_SIZE; + const bool samplerContractReady = + caps.maxTextureImageUnits > MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT; + rg_modernSpecularProbeAtlasStats.available = features.modernBaseline + && textureLimitReady && samplerContractReady; + if ( !rg_modernSpecularProbeAtlasStats.available ) { + R_ModernSpecularProbeAtlas_SetStatus( "capability-unavailable" ); + return; + } + rg_modernSpecularProbeAtlasAvailable = true; + + if ( glGenTextures == NULL || glDeleteTextures == NULL + || glGetTexImage == NULL || glTexSubImage2D == NULL ) { + R_ModernSpecularProbeAtlas_SetStatus( "entry-points-missing" ); + return; + } + + glGenTextures( 1, &rg_modernSpecularProbeAtlasTexture ); + if ( rg_modernSpecularProbeAtlasTexture != 0 ) { + R_GLStateCache().ActiveTextureUnit( 0 ); + R_GLStateCache().BindTexture( 0, GL_TEXTURE_2D, + rg_modernSpecularProbeAtlasTexture ); + byte *zeroed = static_cast( Mem_ClearedAlloc( + MODERN_SPECULAR_PROBE_ATLAS_SIZE + * MODERN_SPECULAR_PROBE_ATLAS_SIZE * 4 ) ); + glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, + MODERN_SPECULAR_PROBE_ATLAS_SIZE, + MODERN_SPECULAR_PROBE_ATLAS_SIZE, 0, + GL_RGBA, GL_UNSIGNED_BYTE, zeroed ); + Mem_Free( zeroed ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); + rg_modernSpecularProbeAtlasStats.textureReady = true; + } + + rg_modernSpecularProbeAtlasInitialized = + rg_modernSpecularProbeAtlasStats.textureReady; + rg_modernSpecularProbeAtlasStats.initialized = + rg_modernSpecularProbeAtlasInitialized; + if ( rg_modernSpecularProbeAtlasInitialized ) { + rg_modernSpecularProbeAtlasGeneration = + R_ModernSpecularProbeAtlas_AdvanceGeneration( + rg_modernSpecularProbeAtlasGenerationCounter ); + rg_modernSpecularProbeAtlasNextResidencyGeneration = 0; + rg_modernSpecularProbeAtlasStats.atlasGeneration = + rg_modernSpecularProbeAtlasGeneration; + } + R_ModernSpecularProbeAtlas_SetStatus( + rg_modernSpecularProbeAtlasInitialized ? "ready" : "incomplete" ); +} + +void R_ModernSpecularProbeAtlas_Shutdown( void ) { + if ( rg_modernSpecularProbeAtlasTexture != 0 && glDeleteTextures != NULL ) { + glDeleteTextures( 1, &rg_modernSpecularProbeAtlasTexture ); + } + rg_modernSpecularProbeAtlasTexture = 0; + rg_modernSpecularProbeAtlasAvailable = false; + rg_modernSpecularProbeAtlasInitialized = false; + rg_modernSpecularProbeAtlasGeneration = 0; + rg_modernSpecularProbeAtlasNextResidencyGeneration = 0; + rg_modernSpecularProbeAtlasFrame = 0; + R_ModernSpecularProbeAtlas_ResetEntries(); + memset( &rg_modernSpecularProbeAtlasStats, 0, + sizeof( rg_modernSpecularProbeAtlasStats ) ); + idStr::Copynz( rg_modernSpecularProbeAtlasStats.lastReject, "none", + sizeof( rg_modernSpecularProbeAtlasStats.lastReject ) ); + R_ModernSpecularProbeAtlas_SetStatus( "off" ); +} + +void R_ModernSpecularProbeAtlas_BeginFrame( void ) { + R_ModernSpecularProbeAtlas_AdvanceGeneration( + rg_modernSpecularProbeAtlasFrame ); + rg_modernSpecularProbeAtlasStats.frameGeneration = + rg_modernSpecularProbeAtlasFrame; + rg_modernSpecularProbeAtlasStats.acquires = 0; + rg_modernSpecularProbeAtlasStats.cacheHits = 0; + rg_modernSpecularProbeAtlasStats.uploadedEntries = 0; + rg_modernSpecularProbeAtlasStats.uploadedFaces = 0; + rg_modernSpecularProbeAtlasStats.reloads = 0; + rg_modernSpecularProbeAtlasStats.evictions = 0; + rg_modernSpecularProbeAtlasStats.rejectedNullOutput = 0; + rg_modernSpecularProbeAtlasStats.rejectedNullImage = 0; + rg_modernSpecularProbeAtlasStats.rejectedUnavailable = 0; + rg_modernSpecularProbeAtlasStats.rejectedNotLoaded = 0; + rg_modernSpecularProbeAtlasStats.rejectedDefaulted = 0; + rg_modernSpecularProbeAtlasStats.rejectedMutable = 0; + rg_modernSpecularProbeAtlasStats.rejectedNotCube = 0; + rg_modernSpecularProbeAtlasStats.rejectedNonSquare = 0; + rg_modernSpecularProbeAtlasStats.rejectedOversized = 0; + rg_modernSpecularProbeAtlasStats.rejectedInvalidStorage = 0; + rg_modernSpecularProbeAtlasStats.rejectedAtlasFull = 0; + rg_modernSpecularProbeAtlasStats.rejectedSourceChanged = 0; + rg_modernSpecularProbeAtlasStats.rejectedUploadUnavailable = 0; + idStr::Copynz( rg_modernSpecularProbeAtlasStats.lastReject, "none", + sizeof( rg_modernSpecularProbeAtlasStats.lastReject ) ); + rg_modernSpecularProbeAtlasStats.frameLossless = + rg_modernSpecularProbeAtlasInitialized; + rg_modernSpecularProbeAtlasStats.frameReady = + rg_modernSpecularProbeAtlasInitialized; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + if ( rg_modernSpecularProbeAtlasEntries[slot].resident + && rg_modernSpecularProbeAtlasEntries[slot].pendingUpload ) { + rg_modernSpecularProbeAtlasStats.frameReady = false; + break; + } + } + R_ModernSpecularProbeAtlas_RefreshCounts(); +} + +modernSpecularProbeAtlasReject_t R_ModernSpecularProbeAtlas_Acquire( + const idImage *image, + modernSpecularProbeAtlasPlacement_t *placement ) { + if ( placement != NULL ) { + ModernSpecularProbeAtlas_ClearPlacement( *placement ); + } + rg_modernSpecularProbeAtlasStats.acquires++; + if ( placement == NULL ) { + return R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_OUTPUT ); + } + + const modernSpecularProbeAtlasReject_t validation = + R_ModernSpecularProbeAtlas_ValidateSource( image ); + if ( validation != MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE ) { + return R_ModernSpecularProbeAtlas_RecordReject( validation ); + } + + const unsigned int sourceHandle = + R_ModernSpecularProbeAtlas_SourceHandle( image ); + const std::uint64_t sourceStorageGeneration = + image->GetStorageGeneration(); + const int faceSize = image->GetUploadWidth(); + int slot = R_ModernSpecularProbeAtlas_FindEntry( image ); + if ( slot >= 0 ) { + modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + const bool stale = entry.sourceHandle != sourceHandle + || entry.sourceStorageGeneration != sourceStorageGeneration + || entry.faceSize != faceSize; + if ( !stale ) { + entry.lastUsedFrame = rg_modernSpecularProbeAtlasFrame; + *placement = entry.placement; + rg_modernSpecularProbeAtlasStats.cacheHits++; + if ( entry.pendingUpload || !entry.uploaded ) { + rg_modernSpecularProbeAtlasStats.frameReady = false; + } + R_ModernSpecularProbeAtlas_RefreshCounts(); + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE; + } + if ( entry.lastUsedFrame == rg_modernSpecularProbeAtlasFrame ) { + return R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED ); + } + rg_modernSpecularProbeAtlasStats.reloads++; + } else { + slot = R_ModernSpecularProbeAtlas_SelectSlot( + rg_modernSpecularProbeAtlasEntries, + rg_modernSpecularProbeAtlasFrame ); + if ( slot < 0 ) { + return R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_ATLAS_FULL ); + } + if ( rg_modernSpecularProbeAtlasEntries[slot].resident ) { + rg_modernSpecularProbeAtlasStats.evictions++; + } + } + + modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + memset( &entry, 0, sizeof( entry ) ); + entry.image = image; + entry.sourceHandle = sourceHandle; + entry.sourceStorageGeneration = sourceStorageGeneration; + entry.residencyGeneration = + R_ModernSpecularProbeAtlas_AdvanceGeneration( + rg_modernSpecularProbeAtlasNextResidencyGeneration ); + entry.lastUsedFrame = rg_modernSpecularProbeAtlasFrame; + entry.slot = slot; + entry.faceSize = faceSize; + entry.resident = true; + entry.pendingUpload = true; + entry.uploaded = false; + if ( !ModernSpecularProbeAtlas_BuildPlacement( + slot, faceSize, entry.placement ) ) { + entry.resident = false; + return R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_INVALID_STORAGE ); + } + entry.placement.atlasGeneration = + rg_modernSpecularProbeAtlasGeneration; + entry.placement.residencyGeneration = entry.residencyGeneration; + entry.placement.sourceStorageGeneration = sourceStorageGeneration; + *placement = entry.placement; + rg_modernSpecularProbeAtlasStats.frameReady = false; + R_ModernSpecularProbeAtlas_RefreshCounts(); + return MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE; +} + +void R_ModernSpecularProbeAtlas_FlushUploads( void ) { + if ( !rg_modernSpecularProbeAtlasInitialized ) { + return; + } + R_ModernSpecularProbeAtlas_RefreshCounts(); + if ( rg_modernSpecularProbeAtlasStats.pendingEntries == 0 ) { + rg_modernSpecularProbeAtlasStats.frameReady = + rg_modernSpecularProbeAtlasStats.frameLossless; + return; + } + if ( glGetTexImage == NULL || glTexSubImage2D == NULL ) { + R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_UPLOAD_UNAVAILABLE ); + return; + } + + static byte scratch[ + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE + * MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE * 4]; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + if ( !entry.resident || !entry.pendingUpload ) { + continue; + } + const modernSpecularProbeAtlasReject_t validation = + R_ModernSpecularProbeAtlas_ValidateSource( entry.image ); + if ( validation != MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE + || !R_ModernSpecularProbeAtlas_FingerprintMatches( entry ) ) { + entry.resident = false; + entry.pendingUpload = false; + entry.uploaded = false; + R_ModernSpecularProbeAtlas_RecordReject( + validation != MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE + ? validation + : MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED ); + continue; + } + + for ( int face = 0; + face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + R_GLStateCache().ActiveTextureUnit( 0 ); + R_GLStateCache().BindTexture( 0, GL_TEXTURE_CUBE_MAP, + entry.sourceHandle ); + glGetTexImage( GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, + GL_RGBA, GL_UNSIGNED_BYTE, scratch ); + + const int cell = entry.placement.faceCells[face]; + const int destinationX = + ( cell % MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW ) + * MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE; + const int destinationY = + ( cell / MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW ) + * MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE; + R_GLStateCache().BindTexture( 0, GL_TEXTURE_2D, + rg_modernSpecularProbeAtlasTexture ); + glTexSubImage2D( GL_TEXTURE_2D, 0, + destinationX, destinationY, entry.faceSize, entry.faceSize, + GL_RGBA, GL_UNSIGNED_BYTE, scratch ); + } + + if ( !R_ModernSpecularProbeAtlas_FingerprintMatches( entry ) ) { + entry.resident = false; + entry.pendingUpload = false; + entry.uploaded = false; + R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED ); + continue; + } + entry.pendingUpload = false; + entry.uploaded = true; + rg_modernSpecularProbeAtlasStats.uploadedEntries++; + rg_modernSpecularProbeAtlasStats.uploadedFaces += + MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; + } + + R_GLStateCache().ActiveTextureUnit( 0 ); + R_ModernSpecularProbeAtlas_RefreshCounts(); + bool allLiveUploaded = true; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + const modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + if ( entry.resident + && entry.lastUsedFrame == rg_modernSpecularProbeAtlasFrame + && ( entry.pendingUpload || !entry.uploaded + || !R_ModernSpecularProbeAtlas_FingerprintMatches( entry ) ) ) { + allLiveUploaded = false; + break; + } + } + rg_modernSpecularProbeAtlasStats.frameReady = + rg_modernSpecularProbeAtlasStats.frameLossless && allLiveUploaded; +} + +unsigned int R_ModernSpecularProbeAtlas_Texture( void ) { + return rg_modernSpecularProbeAtlasTexture; +} + +bool R_ModernSpecularProbeAtlas_Ready( void ) { + return rg_modernSpecularProbeAtlasInitialized; +} + +bool R_ModernSpecularProbeAtlas_FrameReady( void ) { + if ( !rg_modernSpecularProbeAtlasStats.frameReady ) { + return false; + } + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + const modernSpecularProbeAtlasEntry_t &entry = + rg_modernSpecularProbeAtlasEntries[slot]; + if ( entry.resident + && entry.lastUsedFrame == rg_modernSpecularProbeAtlasFrame + && !R_ModernSpecularProbeAtlas_FingerprintMatches( entry ) ) { + R_ModernSpecularProbeAtlas_RecordReject( + MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED ); + return false; + } + } + return true; +} + +const modernSpecularProbeAtlasStats_t &R_ModernSpecularProbeAtlas_Stats( void ) { + return rg_modernSpecularProbeAtlasStats; +} + +void R_ModernSpecularProbeAtlas_PrintGfxInfo( void ) { + const modernSpecularProbeAtlasStats_t &stats = + rg_modernSpecularProbeAtlasStats; + common->Printf( + "Modern specular probe atlas: %s lastReject=%s available=%d init=%d texture=%d frameLossless=%d frameReady=%d size=%d face=%d capacity=%d resident=%d live=%d pending=%d generation=%llu/%llu acquires=%d hits=%d uploads=%d/%d reloads=%d evictions=%d rejected(output=%d null=%d unavailable=%d notLoaded=%d defaulted=%d mutable=%d notCube=%d nonSquare=%d oversized=%d storage=%d full=%d changed=%d upload=%d)\n", + stats.status, + stats.lastReject, + stats.available ? 1 : 0, + stats.initialized ? 1 : 0, + stats.textureReady ? 1 : 0, + stats.frameLossless ? 1 : 0, + stats.frameReady ? 1 : 0, + stats.atlasSize, + stats.faceSize, + stats.capacity, + stats.residentEntries, + stats.liveEntries, + stats.pendingEntries, + static_cast( stats.atlasGeneration ), + static_cast( stats.frameGeneration ), + stats.acquires, + stats.cacheHits, + stats.uploadedEntries, + stats.uploadedFaces, + stats.reloads, + stats.evictions, + stats.rejectedNullOutput, + stats.rejectedNullImage, + stats.rejectedUnavailable, + stats.rejectedNotLoaded, + stats.rejectedDefaulted, + stats.rejectedMutable, + stats.rejectedNotCube, + stats.rejectedNonSquare, + stats.rejectedOversized, + stats.rejectedInvalidStorage, + stats.rejectedAtlasFull, + stats.rejectedSourceChanged, + stats.rejectedUploadUnavailable ); +} + +/* +=================== +RendererSpecularProbeAtlas_RunSelfTest + +Exercises only deterministic CPU contracts: fixed face order, texel-centre +rectangles, disjoint bounded placements, stable LRU tie-breaking, and non-zero +generation rollover. No image, renderer resource, or GL context is required. +=================== +*/ +bool RendererSpecularProbeAtlas_RunSelfTest( void ) { + modernSpecularProbeAtlasPlacement_t placements[ + MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES]; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + if ( !ModernSpecularProbeAtlas_BuildPlacement( slot, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE, placements[slot] ) ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: slot %d did not pack\n", slot ); + return false; + } + for ( int face = 0; face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + const int expectedCell = + slot * MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT + face; + const float *rect = placements[slot].faceRects[face]; + if ( placements[slot].faceCells[face] != expectedCell + || rect[0] < 0.0f || rect[1] < 0.0f + || rect[0] + rect[2] > 1.0f + || rect[1] + rect[3] > 1.0f ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: slot %d face %d escaped packing\n", slot, face ); + return false; + } + for ( int priorSlot = 0; priorSlot <= slot; ++priorSlot ) { + const int priorFaceLimit = priorSlot == slot ? face : + MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; + for ( int priorFace = 0; priorFace < priorFaceLimit; ++priorFace ) { + const float *other = placements[priorSlot].faceRects[priorFace]; + const bool disjoint = rect[0] + rect[2] < other[0] + || other[0] + other[2] < rect[0] + || rect[1] + rect[3] < other[1] + || other[1] + other[3] < rect[1]; + if ( !disjoint ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: slot %d face %d overlaps slot %d face %d\n", slot, face, priorSlot, priorFace ); + return false; + } + } + } + } + } + + modernSpecularProbeAtlasPlacement_t invalid; + invalid.valid = true; + if ( ModernSpecularProbeAtlas_BuildPlacement( -1, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE, invalid ) || invalid.valid + || invalid.faceCells[0] != -1 || invalid.faceRects[0][0] != 0.0f ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: invalid placement was not cleared\n" ); + return false; + } + + modernSpecularProbeAtlasEntry_t lru[ + MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES]; + memset( lru, 0, sizeof( lru ) ); + if ( R_ModernSpecularProbeAtlas_SelectSlot( lru, 10 ) != 0 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: first free slot is not deterministic\n" ); + return false; + } + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + lru[slot].resident = true; + lru[slot].lastUsedFrame = slot < 2 ? 9 : 3; + } + if ( R_ModernSpecularProbeAtlas_SelectSlot( lru, 10 ) != 2 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: equal-age LRU did not choose the lowest slot\n" ); + return false; + } + lru[2].lastUsedFrame = 10; + if ( R_ModernSpecularProbeAtlas_SelectSlot( lru, 10 ) != 3 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: a live slot was selected for eviction\n" ); + return false; + } + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + lru[slot].lastUsedFrame = 10; + } + if ( R_ModernSpecularProbeAtlas_SelectSlot( lru, 10 ) != -1 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: all-live atlas did not fail closed\n" ); + return false; + } + + std::uint64_t generation = 0; + if ( R_ModernSpecularProbeAtlas_AdvanceGeneration( generation ) != 1 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: zero generation was published\n" ); + return false; + } + generation = ~static_cast( 0 ); + if ( R_ModernSpecularProbeAtlas_AdvanceGeneration( generation ) != 1 ) { + common->Printf( "RendererSpecularProbeAtlas self-test failed: wrapped generation was zero\n" ); + return false; + } + + common->Printf( + "RendererSpecularProbeAtlas self-test passed (entries=%d faces=%d atlas=%d face=%d)\n", + MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES, + MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT, + MODERN_SPECULAR_PROBE_ATLAS_SIZE, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE ); + return true; +} diff --git a/src/renderer/ModernSpecularProbeAtlas.h b/src/renderer/ModernSpecularProbeAtlas.h new file mode 100644 index 00000000..08b00a54 --- /dev/null +++ b/src/renderer/ModernSpecularProbeAtlas.h @@ -0,0 +1,184 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __MODERN_SPECULAR_PROBE_ATLAS_H__ +#define __MODERN_SPECULAR_PROBE_ATLAS_H__ + +#include "RendererCaps.h" + +#include + +class idImage; + +/* +=============================================================================== + + Bounded six-face residency for authored specular probes. + + Each cubemap owns six consecutive 256x256 cells in a fixed 2048x2048 + RGBA8 2D atlas. UV rectangles address texel centres, so bilinear filtering + cannot cross into an adjacent face even though no gutter pixels are needed. + The fixed face order is +X, -X, +Y, -Y, +Z, -Z. + + The placement helper below is graphics-API independent. Cluster record and + shader work can therefore pin the exact packing contract without creating a + GL context or depending on pointer values. + +=============================================================================== +*/ + +static const int MODERN_SPECULAR_PROBE_ATLAS_SIZE = 2048; +static const int MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE = 256; +static const int MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW = 8; +static const int MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT = 6; +static const int MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES = 8; +static const int MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT = 21; + +typedef enum modernSpecularProbeAtlasFace_e { + MODERN_SPECULAR_PROBE_FACE_POSITIVE_X = 0, + MODERN_SPECULAR_PROBE_FACE_NEGATIVE_X, + MODERN_SPECULAR_PROBE_FACE_POSITIVE_Y, + MODERN_SPECULAR_PROBE_FACE_NEGATIVE_Y, + MODERN_SPECULAR_PROBE_FACE_POSITIVE_Z, + MODERN_SPECULAR_PROBE_FACE_NEGATIVE_Z +} modernSpecularProbeAtlasFace_t; + +typedef struct modernSpecularProbeAtlasPlacement_s { + bool valid; + int slot; + int faceSize; + int faceCells[MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT]; + std::uint64_t atlasGeneration; + std::uint64_t residencyGeneration; + std::uint64_t sourceStorageGeneration; + float faceRects[MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT][4]; +} modernSpecularProbeAtlasPlacement_t; + +inline void ModernSpecularProbeAtlas_ClearPlacement( + modernSpecularProbeAtlasPlacement_t &placement ) { + placement.valid = false; + placement.slot = -1; + placement.faceSize = 0; + placement.atlasGeneration = 0; + placement.residencyGeneration = 0; + placement.sourceStorageGeneration = 0; + for ( int face = 0; face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + placement.faceCells[face] = -1; + for ( int component = 0; component < 4; ++component ) { + placement.faceRects[face][component] = 0.0f; + } + } +} + +inline bool ModernSpecularProbeAtlas_BuildPlacement( int slot, int faceSize, + modernSpecularProbeAtlasPlacement_t &placement ) { + ModernSpecularProbeAtlas_ClearPlacement( placement ); + if ( slot < 0 || slot >= MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES + || faceSize <= 0 || faceSize > MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE ) { + return false; + } + + const float inverseAtlas = + 1.0f / static_cast( MODERN_SPECULAR_PROBE_ATLAS_SIZE ); + for ( int face = 0; face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + const int cell = slot * MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT + face; + const int cellX = ( cell % MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW ) + * MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE; + const int cellY = ( cell / MODERN_SPECULAR_PROBE_ATLAS_CELLS_PER_ROW ) + * MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE; + placement.faceCells[face] = cell; + placement.faceRects[face][0] = + ( static_cast( cellX ) + 0.5f ) * inverseAtlas; + placement.faceRects[face][1] = + ( static_cast( cellY ) + 0.5f ) * inverseAtlas; + placement.faceRects[face][2] = + static_cast( faceSize - 1 ) * inverseAtlas; + placement.faceRects[face][3] = + static_cast( faceSize - 1 ) * inverseAtlas; + } + placement.valid = true; + placement.slot = slot; + placement.faceSize = faceSize; + return true; +} + +typedef enum modernSpecularProbeAtlasReject_e { + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NONE = 0, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_OUTPUT, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NULL_IMAGE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_UNAVAILABLE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_LOADED, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_DEFAULTED, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_MUTABLE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NOT_CUBE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_NON_SQUARE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_OVERSIZED, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_INVALID_STORAGE, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_ATLAS_FULL, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_SOURCE_CHANGED, + MODERN_SPECULAR_PROBE_ATLAS_REJECT_UPLOAD_UNAVAILABLE +} modernSpecularProbeAtlasReject_t; + +typedef struct modernSpecularProbeAtlasStats_s { + bool available; + bool initialized; + bool textureReady; + bool frameLossless; + bool frameReady; + int atlasSize; + int faceSize; + int capacity; + int residentEntries; + int liveEntries; + int pendingEntries; + int acquires; + int cacheHits; + int uploadedEntries; + int uploadedFaces; + int reloads; + int evictions; + int rejectedNullOutput; + int rejectedNullImage; + int rejectedUnavailable; + int rejectedNotLoaded; + int rejectedDefaulted; + int rejectedMutable; + int rejectedNotCube; + int rejectedNonSquare; + int rejectedOversized; + int rejectedInvalidStorage; + int rejectedAtlasFull; + int rejectedSourceChanged; + int rejectedUploadUnavailable; + std::uint64_t atlasGeneration; + std::uint64_t frameGeneration; + char status[96]; + char lastReject[64]; +} modernSpecularProbeAtlasStats_t; + +void R_ModernSpecularProbeAtlas_Init( const renderBackendCaps_t &caps, + const renderFeatureSet_t &features ); +void R_ModernSpecularProbeAtlas_Shutdown( void ); +void R_ModernSpecularProbeAtlas_BeginFrame( void ); + +// Acquire performs no GL transfer. It reserves or reuses six cells and fills a +// generation-stamped placement. Every rejection clears the placement and +// poisons FrameReady for this frame, preventing partial visual consumption. +modernSpecularProbeAtlasReject_t R_ModernSpecularProbeAtlas_Acquire( + const idImage *image, modernSpecularProbeAtlasPlacement_t *placement ); + +// Transfers all pending cube faces at a caller-owned synchronization point. +// FrameReady becomes true only when every live placement is resident, uploaded, +// and still matches its source storage generation. +void R_ModernSpecularProbeAtlas_FlushUploads( void ); + +const char *ModernSpecularProbeAtlasReject_Name( + modernSpecularProbeAtlasReject_t reject ); +unsigned int R_ModernSpecularProbeAtlas_Texture( void ); +bool R_ModernSpecularProbeAtlas_Ready( void ); +bool R_ModernSpecularProbeAtlas_FrameReady( void ); +const modernSpecularProbeAtlasStats_t &R_ModernSpecularProbeAtlas_Stats( void ); +void R_ModernSpecularProbeAtlas_PrintGfxInfo( void ); +bool RendererSpecularProbeAtlas_RunSelfTest( void ); + +#endif /* !__MODERN_SPECULAR_PROBE_ATLAS_H__ */ diff --git a/src/renderer/OpenGL/GpuSkinningGL.cpp b/src/renderer/OpenGL/GpuSkinningGL.cpp new file mode 100644 index 00000000..71e0d3fd --- /dev/null +++ b/src/renderer/OpenGL/GpuSkinningGL.cpp @@ -0,0 +1,309 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +/* +=============================================================================== + + OpenGL GPU animation adapter. + + The shared contract deliberately exposes no OpenGL object names. This + adapter uploads one immutable bind-pose stream, the exact four-weight stream, + and the current joint palette into the renderer's bounded frame ring. A + compute dispatch writes an ordinary idDrawVert stream, so every existing + ambient, interaction, subview, view-model, and shadow-map draw keeps using + the established vertex ABI. Stencil volumes continue to use their CPU + shadow cache and never enter this path. + +=============================================================================== +*/ + +#include "../tr_local.h" +#include "../GLStateCache.h" +#include "../GpuSkinning.h" +#include "../RendererUpload.h" + +#include + +namespace { + +static const int GPU_SKINNING_GL_WORKGROUP_SIZE = 64; + +struct gpuSkinningGLState_t { + GLuint program; + GLint sourceWordOffset; + GLint skinWordOffset; + GLint jointWordOffset; + GLint outputWordOffset; + GLint vertexCount; + GLint jointCount; + bool available; +}; + +static gpuSkinningGLState_t gpuSkinningGL; + +static GLuint R_GpuSkinningGL_CompileShader( const char *source ) { + if ( glCreateShader == NULL || glShaderSource == NULL || glCompileShader == NULL + || glGetShaderiv == NULL || glDeleteShader == NULL ) { + return 0; + } + + GLuint shader = glCreateShader( GL_COMPUTE_SHADER ); + if ( shader == 0 ) { + return 0; + } + glShaderSource( shader, 1, &source, NULL ); + glCompileShader( shader ); + + GLint compiled = GL_FALSE; + glGetShaderiv( shader, GL_COMPILE_STATUS, &compiled ); + if ( compiled != GL_TRUE ) { + char log[2048]; + memset( log, 0, sizeof( log ) ); + if ( glGetShaderInfoLog != NULL ) { + glGetShaderInfoLog( shader, sizeof( log ) - 1, NULL, log ); + } + common->Warning( "GPU skinning: OpenGL compute shader compilation failed: %s", log ); + glDeleteShader( shader ); + return 0; + } + return shader; +} + +static GLuint R_GpuSkinningGL_CreateProgram( void ) { + static const char *computeSource = + "#version 430\n" + "layout(local_size_x = 64) in;\n" + "layout(std430, binding = 0) readonly buffer SourceWords { uint words[]; } sourceData;\n" + "layout(std430, binding = 1) readonly buffer SkinWords { uint words[]; } skinData;\n" + "layout(std430, binding = 2) readonly buffer JointWords { uint words[]; } jointData;\n" + "layout(std430, binding = 3) writeonly buffer OutputWords { uint words[]; } outputData;\n" + "uniform uint uSourceWordOffset;\n" + "uniform uint uSkinWordOffset;\n" + "uniform uint uJointWordOffset;\n" + "uniform uint uOutputWordOffset;\n" + "uniform uint uVertexCount;\n" + "uniform uint uJointCount;\n" + "vec4 LoadJointRow(uint jointIndex, uint row) {\n" + " uint base = uJointWordOffset + jointIndex * 12u + row * 4u;\n" + " return vec4(uintBitsToFloat(jointData.words[base + 0u]),\n" + " uintBitsToFloat(jointData.words[base + 1u]),\n" + " uintBitsToFloat(jointData.words[base + 2u]),\n" + " uintBitsToFloat(jointData.words[base + 3u]));\n" + "}\n" + "vec3 TransformBasis(vec4 row0, vec4 row1, vec4 row2, vec3 value, float w) {\n" + " vec4 source = vec4(value, w);\n" + " return vec3(dot(row0, source), dot(row1, source), dot(row2, source));\n" + "}\n" + "void StoreVec3(uint base, uint member, vec3 value) {\n" + " outputData.words[base + member + 0u] = floatBitsToUint(value.x);\n" + " outputData.words[base + member + 1u] = floatBitsToUint(value.y);\n" + " outputData.words[base + member + 2u] = floatBitsToUint(value.z);\n" + "}\n" + "void main() {\n" + " uint vertex = gl_GlobalInvocationID.x;\n" + " if (vertex >= uVertexCount) { return; }\n" + " uint sourceBase = uSourceWordOffset + vertex * 16u;\n" + " uint outputBase = uOutputWordOffset + vertex * 16u;\n" + " for (uint word = 0u; word < 16u; ++word) {\n" + " outputData.words[outputBase + word] = sourceData.words[sourceBase + word];\n" + " }\n" + " uint skinBase = uSkinWordOffset + vertex * 8u;\n" + " vec4 row0 = vec4(0.0);\n" + " vec4 row1 = vec4(0.0);\n" + " vec4 row2 = vec4(0.0);\n" + " for (uint influence = 0u; influence < 4u; ++influence) {\n" + " float weight = uintBitsToFloat(skinData.words[skinBase + 4u + influence]);\n" + " if (weight == 0.0) { continue; }\n" + " uint jointIndex = skinData.words[skinBase + influence];\n" + " if (jointIndex >= uJointCount) { return; }\n" + " row0 += LoadJointRow(jointIndex, 0u) * weight;\n" + " row1 += LoadJointRow(jointIndex, 1u) * weight;\n" + " row2 += LoadJointRow(jointIndex, 2u) * weight;\n" + " }\n" + " vec3 position = vec3(uintBitsToFloat(sourceData.words[sourceBase + 0u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 1u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 2u]));\n" + " vec3 normal = vec3(uintBitsToFloat(sourceData.words[sourceBase + 4u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 5u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 6u]));\n" + " vec3 tangent0 = vec3(uintBitsToFloat(sourceData.words[sourceBase + 8u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 9u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 10u]));\n" + " vec3 tangent1 = vec3(uintBitsToFloat(sourceData.words[sourceBase + 11u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 12u]),\n" + " uintBitsToFloat(sourceData.words[sourceBase + 13u]));\n" + " StoreVec3(outputBase, 0u, TransformBasis(row0, row1, row2, position, 1.0));\n" + " StoreVec3(outputBase, 4u, TransformBasis(row0, row1, row2, normal, 0.0));\n" + " StoreVec3(outputBase, 8u, TransformBasis(row0, row1, row2, tangent0, 0.0));\n" + " StoreVec3(outputBase, 11u, TransformBasis(row0, row1, row2, tangent1, 0.0));\n" + "}\n"; + + if ( glCreateProgram == NULL || glAttachShader == NULL || glLinkProgram == NULL + || glGetProgramiv == NULL || glDeleteProgram == NULL ) { + return 0; + } + + GLuint shader = R_GpuSkinningGL_CompileShader( computeSource ); + if ( shader == 0 ) { + return 0; + } + GLuint program = glCreateProgram(); + if ( program == 0 ) { + glDeleteShader( shader ); + return 0; + } + glAttachShader( program, shader ); + glLinkProgram( program ); + if ( glDetachShader != NULL ) { + glDetachShader( program, shader ); + } + glDeleteShader( shader ); + + GLint linked = GL_FALSE; + glGetProgramiv( program, GL_LINK_STATUS, &linked ); + if ( linked != GL_TRUE ) { + char log[2048]; + memset( log, 0, sizeof( log ) ); + if ( glGetProgramInfoLog != NULL ) { + glGetProgramInfoLog( program, sizeof( log ) - 1, NULL, log ); + } + common->Warning( "GPU skinning: OpenGL compute program link failed: %s", log ); + glDeleteProgram( program ); + return 0; + } + return program; +} + +static bool R_GpuSkinningGL_AllocationValid( const rendererUploadAllocation_t &allocation ) { + return allocation.vbo != 0 && allocation.offset >= 0 && allocation.size > 0 + && ( allocation.offset & 3 ) == 0; +} + +} // namespace + +void R_BackendGpuSkinning_Init( const renderBackendCaps_t &caps ) { + memset( &gpuSkinningGL, 0, sizeof( gpuSkinningGL ) ); + if ( !caps.hasCompute || !caps.hasSSBO || glBindBufferBase == NULL + || glDispatchCompute == NULL || glMemoryBarrier == NULL + || glUniform1ui == NULL || glGetUniformLocation == NULL ) { + return; + } + + gpuSkinningGL.program = R_GpuSkinningGL_CreateProgram(); + if ( gpuSkinningGL.program == 0 ) { + return; + } + gpuSkinningGL.sourceWordOffset = glGetUniformLocation( gpuSkinningGL.program, "uSourceWordOffset" ); + gpuSkinningGL.skinWordOffset = glGetUniformLocation( gpuSkinningGL.program, "uSkinWordOffset" ); + gpuSkinningGL.jointWordOffset = glGetUniformLocation( gpuSkinningGL.program, "uJointWordOffset" ); + gpuSkinningGL.outputWordOffset = glGetUniformLocation( gpuSkinningGL.program, "uOutputWordOffset" ); + gpuSkinningGL.vertexCount = glGetUniformLocation( gpuSkinningGL.program, "uVertexCount" ); + gpuSkinningGL.jointCount = glGetUniformLocation( gpuSkinningGL.program, "uJointCount" ); + gpuSkinningGL.available = gpuSkinningGL.sourceWordOffset >= 0 + && gpuSkinningGL.skinWordOffset >= 0 && gpuSkinningGL.jointWordOffset >= 0 + && gpuSkinningGL.outputWordOffset >= 0 && gpuSkinningGL.vertexCount >= 0 + && gpuSkinningGL.jointCount >= 0; + if ( !gpuSkinningGL.available ) { + R_BackendGpuSkinning_Shutdown(); + } +} + +void R_BackendGpuSkinning_Shutdown( void ) { + if ( gpuSkinningGL.program != 0 && glDeleteProgram != NULL ) { + glDeleteProgram( gpuSkinningGL.program ); + } + memset( &gpuSkinningGL, 0, sizeof( gpuSkinningGL ) ); +} + +bool R_BackendGpuSkinning_PrepareAmbientCache( srfTriangles_t *tri, bool needsLighting ) { + (void)needsLighting; + if ( tri == NULL ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_VERTEX_COUNT ); + return false; + } + if ( tri->ambientCache != NULL ) { + return true; + } + if ( !gpuSkinningGL.available ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + return false; + } + + gpuSkinningSurface_t surface; + if ( !R_GpuSkinning_GetSurface( tri, surface ) ) { + R_GpuSkinning_RecordFallback( surface.fallbackReason ); + return false; + } + const gpuSkinningFallbackReason_t validation = R_GpuSkinning_ValidateSurface( surface ); + if ( validation != GPU_SKINNING_FALLBACK_NONE ) { + R_GpuSkinning_RecordFallback( validation ); + return false; + } + if ( surface.numVerts <= 0 || surface.numVerts > ( std::numeric_limits::max )() / static_cast( sizeof( idDrawVert ) ) + || surface.palette.matrixStrideFloats != GPU_SKINNING_JOINT_FLOATS ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_VERTEX_COUNT ); + return false; + } + if ( surface.palette.numJoints <= 0 ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_JOINT_COUNT ); + return false; + } + + const int sourceBytes = surface.numVerts * static_cast( sizeof( idDrawVert ) ); + const int skinBytes = surface.numVerts * static_cast( sizeof( gpuSkinningVertex_t ) ); + if ( surface.palette.numJoints > ( std::numeric_limits::max )() / ( GPU_SKINNING_JOINT_FLOATS * static_cast( sizeof( float ) ) ) ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_JOINT_COUNT ); + return false; + } + const int jointBytes = surface.palette.numJoints * GPU_SKINNING_JOINT_FLOATS * static_cast( sizeof( float ) ); + + rendererUploadAllocation_t sourceAllocation; + rendererUploadAllocation_t skinAllocation; + rendererUploadAllocation_t jointAllocation; + if ( !R_RendererUpload_AllocFrameTemp( const_cast( surface.bindPoseVerts ), sourceBytes, 16, sourceAllocation ) + || !R_RendererUpload_AllocFrameTemp( const_cast( surface.skinVerts ), skinBytes, 16, skinAllocation ) + || !R_RendererUpload_AllocFrameTemp( const_cast( surface.palette.matrices ), jointBytes, 16, jointAllocation ) + || !R_GpuSkinningGL_AllocationValid( sourceAllocation ) + || !R_GpuSkinningGL_AllocationValid( skinAllocation ) + || !R_GpuSkinningGL_AllocationValid( jointAllocation ) ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION ); + return false; + } + + vertCache_t *outputCache = vertexCache.AllocFrameTemp( + const_cast( surface.bindPoseVerts ), sourceBytes ); + if ( outputCache == NULL || outputCache->vbo == 0 || outputCache->offset < 0 + || ( outputCache->offset & 3 ) != 0 ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION ); + return false; + } + + R_GLStateCache().UseProgram( gpuSkinningGL.program ); + R_GLStateCache().BindBufferBase( GL_SHADER_STORAGE_BUFFER, 0, sourceAllocation.vbo ); + R_GLStateCache().BindBufferBase( GL_SHADER_STORAGE_BUFFER, 1, skinAllocation.vbo ); + R_GLStateCache().BindBufferBase( GL_SHADER_STORAGE_BUFFER, 2, jointAllocation.vbo ); + R_GLStateCache().BindBufferBase( GL_SHADER_STORAGE_BUFFER, 3, outputCache->vbo ); + glUniform1ui( gpuSkinningGL.sourceWordOffset, static_cast( sourceAllocation.offset / 4 ) ); + glUniform1ui( gpuSkinningGL.skinWordOffset, static_cast( skinAllocation.offset / 4 ) ); + glUniform1ui( gpuSkinningGL.jointWordOffset, static_cast( jointAllocation.offset / 4 ) ); + glUniform1ui( gpuSkinningGL.outputWordOffset, static_cast( outputCache->offset / 4 ) ); + glUniform1ui( gpuSkinningGL.vertexCount, static_cast( surface.numVerts ) ); + glUniform1ui( gpuSkinningGL.jointCount, static_cast( surface.palette.numJoints ) ); + glDispatchCompute( static_cast( + ( surface.numVerts + GPU_SKINNING_GL_WORKGROUP_SIZE - 1 ) / GPU_SKINNING_GL_WORKGROUP_SIZE ), 1, 1 ); + glMemoryBarrier( GL_SHADER_STORAGE_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT + | GL_BUFFER_UPDATE_BARRIER_BIT ); + for ( GLuint binding = 0; binding < 4; ++binding ) { + R_GLStateCache().BindBufferBase( GL_SHADER_STORAGE_BUFFER, binding, 0 ); + } + R_GLStateCache().UseProgram( 0 ); + + tri->ambientCache = outputCache; + tri->tempAmbientCache = true; + return true; +} + +void R_BackendGpuSkinning_PrintGfxInfo( void ) { + common->Printf( "GPU skinning compute (OpenGL): %s\n", + gpuSkinningGL.available ? "available" : "unavailable" ); +} diff --git a/src/renderer/RenderModuleAPI.h b/src/renderer/RenderModuleAPI.h index 7eeeb7ab..98ae99fd 100644 --- a/src/renderer/RenderModuleAPI.h +++ b/src/renderer/RenderModuleAPI.h @@ -46,9 +46,18 @@ // surface kind so the engine creates the window for the right API, and // the window services gain SDL-mediated Vulkan instance-extension and // surface creation (the module never links SDL) -// Version 7 keeps stale renderer modules from consuming the extended -// renderEntity_t presentation contract (flat diffuse colour and sweep flags). -#define RENDER_API_VERSION 7 +// 7 - Extended renderEntity_t presentation contract (flat diffuse colour +// and sweep flags) +// 8 - Append-only idRenderSystem slot for coherent full-frame capture with +// CPU center-crop/resampling to caller-selected output dimensions +// 9 - Append-only idRenderSystem slots for backend-neutral delayed whole-frame +// GPU timing and session-discontinuity resets (OpenGL and Vulkan) +// 10 - Shared idFile/idFileSystem ABI adds engine-owned memory staging, +// semantic preload records, and source-authoritative generated caches +// 11 - Append-only idRenderSystem slots publish frame-latched scene/native +// output state and enqueue the backend-neutral temporal presentation +// resolve requested by game modules +#define RENDER_API_VERSION 11 #define RENDER_API_ENTRY_POINT "GetRenderAPI" class idSys; diff --git a/src/renderer/RenderSystem.cpp b/src/renderer/RenderSystem.cpp index a99e4779..aa5e5d0a 100644 --- a/src/renderer/RenderSystem.cpp +++ b/src/renderer/RenderSystem.cpp @@ -33,6 +33,7 @@ If you have questions concerning this license or the applicable additional terms #include "RendererMetrics.h" #include "RendererUpload.h" #include "ScenePackets.h" +#include "TemporalPresentation.h" #include "smaa/AreaTex.h" #include "smaa/SearchTex.h" @@ -246,10 +247,10 @@ static void R_PerformanceCounters( void ) { tr.pc.c_shadowViewEntities, tr.pc.c_viewLights ); } if ( r_showUpdates.GetBool() ) { - common->Printf( "entityUpdates:%i entityRefs:%i lightUpdates:%i lightRefs:%i snapshotsReused:%i\n", + common->Printf( "entityUpdates:%i entityRefs:%i lightUpdates:%i lightRefs:%i snapshotsReused:%i updatesElided:%i\n", tr.pc.c_entityUpdates, tr.pc.c_entityReferences, tr.pc.c_lightUpdates, tr.pc.c_lightReferences, - tr.pc.c_entitySnapshotsReused ); + tr.pc.c_entitySnapshotsReused, tr.pc.c_entityUpdatesElided ); } if ( r_showMemory.GetBool() ) { int m1 = frameData ? frameData->memoryHighwater : 0; @@ -503,7 +504,7 @@ void R_AddSpecialEffects( viewDef_t *parms ) { cmd->commandId = RC_DRAW_SPECIAL_EFFECTS; cmd->viewDef = parms; if ( R_ScenePackets_FrontEndCaptureRequired() ) { - R_ScenePackets_AddSpecialEffects( parms ); + R_ScenePackets_AddSpecialEffects( parms, activeMask ); } } @@ -729,7 +730,8 @@ void idRenderSystemLocal::CaptureDepthRenderToImage( const char *imageName ) { cmd->cubeFace = 0; cmd->copyDepth = true; if ( R_ScenePackets_FrontEndCaptureRequired() ) { - R_ScenePackets_AddCopyRender(); + R_ScenePackets_AddCopyRender( image, cmd->x, cmd->y, + cmd->imageWidth, cmd->imageHeight, cmd->cubeFace, cmd->copyDepth ); } guiModel->Clear(); @@ -1297,6 +1299,7 @@ void idRenderSystemLocal::BeginFrame( int windowWidth, int windowHeight ) { if ( !glConfig.isInitialized ) { return; } + R_RendererMetrics_MarkCpuFrameBegin(); // determine which back end we will use SetBackEndRenderer(); @@ -1407,7 +1410,10 @@ void idRenderSystemLocal::BeginFrame( int windowWidth, int windowHeight ) { // is handled by the main-scene render target path so the back buffer stays at // the display size. const int screenFraction = idMath::ClampInt( 10, 200, r_screenFraction.GetInteger() ); - if ( screenFraction < 100 && r_resolutionScaleMode.GetInteger() == 0 ) { + if ( !R_TemporalPresentation_DynamicResolutionRequested() + && !R_TemporalPresentation_TemporalAARequested() + && !R_TemporalPresentation_ScreenSpaceEffectsRequested() + && screenFraction < 100 && r_resolutionScaleMode.GetInteger() == 0 ) { int w = SCREEN_WIDTH * screenFraction / 100.0f; int h = SCREEN_HEIGHT * screenFraction / 100.0f; CropRenderSize( w, h ); @@ -1417,6 +1423,7 @@ void idRenderSystemLocal::BeginFrame( int windowWidth, int windowHeight ) { // this is the ONLY place this is modified frameCount++; R_RendererMetrics_BeginFrame( frameCount ); + R_TemporalPresentation_BeginFrame( windowWidth, windowHeight, takingScreenshot ); R_RendererUpload_BeginFrame( frameCount ); if ( R_ScenePackets_FrontEndCaptureRequired() ) { R_ScenePackets_BeginFrame(); @@ -1742,7 +1749,8 @@ void idRenderSystemLocal::CaptureRenderToImage( const char *imageName ) { cmd->cubeFace = 0; cmd->copyDepth = false; if ( R_ScenePackets_FrontEndCaptureRequired() ) { - R_ScenePackets_AddCopyRender(); + R_ScenePackets_AddCopyRender( image, cmd->x, cmd->y, + cmd->imageWidth, cmd->imageHeight, cmd->cubeFace, cmd->copyDepth ); } guiModel->Clear(); @@ -1755,40 +1763,160 @@ CaptureRenderToFile ============== */ void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha ) { + CaptureRenderToFile( fileName, fixAlpha, 0, 0 ); +} + +static const int MAX_CAPTURE_DIMENSION = 16384; +static const int64 MAX_CAPTURE_PIXELS = 33554432; + +/* +============== +R_ResampleCaptureToAspectRGBA + +Preserves the bottom-up row order returned by glReadPixels. R_WriteTGA's +flipVertical argument then describes those rows exactly as it does for an +unscaled capture. +============== +*/ +static byte *R_ResampleCaptureToAspectRGBA( const byte *source, int sourceWidth, int sourceHeight, + int outputWidth, int outputHeight ) { + int cropX = 0; + int cropY = 0; + int cropWidth = sourceWidth; + int cropHeight = sourceHeight; + + const int64 sourceAspectProduct = (int64)sourceWidth * outputHeight; + const int64 outputAspectProduct = (int64)sourceHeight * outputWidth; + if ( sourceAspectProduct > outputAspectProduct ) { + cropWidth = (int)( (int64)sourceHeight * outputWidth / outputHeight ); + cropWidth = Max( cropWidth, 1 ); + cropX = ( sourceWidth - cropWidth ) / 2; + } else if ( sourceAspectProduct < outputAspectProduct ) { + cropHeight = (int)( (int64)sourceWidth * outputHeight / outputWidth ); + cropHeight = Max( cropHeight, 1 ); + cropY = ( sourceHeight - cropHeight ) / 2; + } + + const size_t outputPixels = (size_t)outputWidth * (size_t)outputHeight; + byte *output = (byte *)R_StaticAlloc( outputPixels * 4 ); + const int cropMaxX = cropX + cropWidth - 1; + const int cropMaxY = cropY + cropHeight - 1; + + for ( int y = 0; y < outputHeight; y++ ) { + const double sourceY = cropY + ( ( y + 0.5 ) * cropHeight / outputHeight ) - 0.5; + int y0 = (int)floor( sourceY ); + double yFraction = sourceY - y0; + if ( y0 < cropY ) { + y0 = cropY; + yFraction = 0.0; + } else if ( y0 >= cropMaxY ) { + y0 = cropMaxY; + yFraction = 0.0; + } + const int y1 = Min( y0 + 1, cropMaxY ); + byte *outputRow = output + (size_t)y * (size_t)outputWidth * 4; + + for ( int x = 0; x < outputWidth; x++ ) { + const double sourceX = cropX + ( ( x + 0.5 ) * cropWidth / outputWidth ) - 0.5; + int x0 = (int)floor( sourceX ); + double xFraction = sourceX - x0; + if ( x0 < cropX ) { + x0 = cropX; + xFraction = 0.0; + } else if ( x0 >= cropMaxX ) { + x0 = cropMaxX; + xFraction = 0.0; + } + const int x1 = Min( x0 + 1, cropMaxX ); + + const byte *topLeft = source + ( (size_t)y0 * sourceWidth + x0 ) * 4; + const byte *topRight = source + ( (size_t)y0 * sourceWidth + x1 ) * 4; + const byte *bottomLeft = source + ( (size_t)y1 * sourceWidth + x0 ) * 4; + const byte *bottomRight = source + ( (size_t)y1 * sourceWidth + x1 ) * 4; + byte *destination = outputRow + x * 4; + + for ( int channel = 0; channel < 4; channel++ ) { + const double top = topLeft[channel] + ( topRight[channel] - topLeft[channel] ) * xFraction; + const double bottom = bottomLeft[channel] + ( bottomRight[channel] - bottomLeft[channel] ) * xFraction; + destination[channel] = (byte)( top + ( bottom - top ) * yFraction + 0.5 ); + } + } + } + + return output; +} + +void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha, + int outputWidth, int outputHeight ) { if ( !glConfig.isInitialized ) { return; } renderCrop_t *rc = &renderCrops[currentRenderCrop]; + const bool resample = outputWidth != 0 || outputHeight != 0; + const int64 sourcePixelCount = (int64)rc->width * rc->height; + if ( rc->width < 1 || rc->height < 1 || + rc->width > MAX_CAPTURE_DIMENSION || rc->height > MAX_CAPTURE_DIMENSION || + sourcePixelCount > MAX_CAPTURE_PIXELS ) { + common->Warning( "CaptureRenderToFile: invalid source dimensions %d x %d", rc->width, rc->height ); + return; + } + const int64 outputPixelCount = (int64)outputWidth * outputHeight; + if ( resample && ( outputWidth < 1 || outputHeight < 1 || + outputWidth > MAX_CAPTURE_DIMENSION || outputHeight > MAX_CAPTURE_DIMENSION || + outputPixelCount > MAX_CAPTURE_PIXELS ) ) { + common->Warning( "CaptureRenderToFile: invalid output dimensions %d x %d", outputWidth, outputHeight ); + return; + } guiModel->EmitFullScreen(); guiModel->Clear(); const bool wasTakingScreenshot = tr.takingScreenshot; tr.takingScreenshot = true; + R_TemporalPresentation_MarkCurrentFrameCapture( "synchronous readback" ); R_IssueRenderCommands(); glReadBuffer( GL_BACK ); - // include extra space for OpenGL padding to word boundaries - int c = ( rc->width + 3 ) * rc->height; - byte *data = (byte *)R_StaticAlloc( c * 3 ); + // GL_RGB readback rows use the default four-byte pack alignment. Convert + // each row independently so the padding never becomes pixel data. + const int sourceStride = ( rc->width * 3 + 3 ) & ~3; + const size_t sourceBytes = (size_t)sourceStride * (size_t)rc->height; + byte *data = (byte *)R_StaticAlloc( sourceBytes ); + memset( data, 0, sourceBytes ); glReadPixels( rc->x, rc->y, rc->width, rc->height, GL_RGB, GL_UNSIGNED_BYTE, data ); tr.takingScreenshot = wasTakingScreenshot; - byte *data2 = (byte *)R_StaticAlloc( c * 4 ); + byte *data2 = (byte *)R_StaticAlloc( (size_t)sourcePixelCount * 4 ); - for ( int i = 0 ; i < c ; i++ ) { - data2[ i * 4 ] = data[ i * 3 ]; - data2[ i * 4 + 1 ] = data[ i * 3 + 1 ]; - data2[ i * 4 + 2 ] = data[ i * 3 + 2 ]; - data2[ i * 4 + 3 ] = 0xff; + for ( int y = 0; y < rc->height; y++ ) { + const byte *sourceRow = data + (size_t)y * sourceStride; + byte *destinationRow = data2 + (size_t)y * rc->width * 4; + for ( int x = 0; x < rc->width; x++ ) { + destinationRow[ x * 4 ] = sourceRow[ x * 3 ]; + destinationRow[ x * 4 + 1 ] = sourceRow[ x * 3 + 1 ]; + destinationRow[ x * 4 + 2 ] = sourceRow[ x * 3 + 2 ]; + destinationRow[ x * 4 + 3 ] = 0xff; + } } + R_StaticFree( data ); + data = NULL; - R_WriteTGA( fileName, data2, rc->width, rc->height, true ); + byte *outputData = data2; + int capturedWidth = rc->width; + int capturedHeight = rc->height; + if ( resample ) { + outputData = R_ResampleCaptureToAspectRGBA( data2, rc->width, rc->height, + outputWidth, outputHeight ); + R_StaticFree( data2 ); + data2 = NULL; + capturedWidth = outputWidth; + capturedHeight = outputHeight; + } - R_StaticFree( data ); - R_StaticFree( data2 ); + R_WriteTGA( fileName, outputData, capturedWidth, capturedHeight, true ); + R_StaticFree( outputData ); } @@ -1911,6 +2039,8 @@ void idRenderSystemLocal::ResolveMSAA(idRenderTexture* msaaRenderTexture, idRend cmd->msaaRenderTexture = msaaRenderTexture; cmd->destRenderTexture = destRenderTexture; cmd->resolveDepth = resolveDepth; + cmd->frameNumber = tr.frameCount; + cmd->historyGeneration = R_TemporalPresentation_HistoryGeneration(); if ( R_ScenePackets_FrontEndCaptureRequired() ) { R_ScenePackets_AddRenderTargetOp(); } @@ -2138,6 +2268,157 @@ const glconfig_t &idRenderSystemLocal::GetGLConfig( void ) const { return glConfig; } +/* +==================== +idRenderSystemLocal::GetGpuFrameTiming + +The query objects stay backend-private. This POD snapshot is the sole +cross-module timing surface and is safe to consume from engine/game code. +==================== +*/ +void idRenderSystemLocal::GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) const { + R_RendererMetrics_GetGpuFrameTiming( timing ); +} + +void idRenderSystemLocal::ResetGpuFrameTiming( const char *reason ) { + R_RendererMetrics_ResetGpuFrameTiming( reason ); + // Session/map/backend discontinuities invalidate both delayed timing samples + // and native-resolution image history. The controller maintains independent + // generations so rejecting one never aliases an unrelated sample stream. + R_TemporalPresentation_InvalidateHistory( reason ); +} + +/* +==================== +idRenderSystemLocal::GetPresentationState + +Publishes the scene extent selected once at BeginFrame. Game render targets +consume this POD while the final composite and UI continue to use the native +output extent. Calls made before the first BeginFrame receive the active video +mode as a safe native-resolution fallback. +==================== +*/ +void idRenderSystemLocal::GetPresentationState( renderPresentationState_t &state ) const { + const temporalPresentationFrameState_t &frame = + R_TemporalPresentation_GetFrameState(); + const int fallbackWidth = Max( 0, glConfig.vidWidth ); + const int fallbackHeight = Max( 0, glConfig.vidHeight ); + + memset( &state, 0, sizeof( state ) ); + state.frameNumber = frame.frameNumber; + state.outputWidth = frame.nativeWidth > 0 ? frame.nativeWidth : fallbackWidth; + state.outputHeight = frame.nativeHeight > 0 ? frame.nativeHeight : fallbackHeight; + state.sceneWidth = frame.sceneWidth > 0 ? frame.sceneWidth : state.outputWidth; + state.sceneHeight = frame.sceneHeight > 0 ? frame.sceneHeight : state.outputHeight; + state.effectiveScalePercent = frame.effectiveScalePercent > 0 + ? frame.effectiveScalePercent : 100; + state.historyGeneration = R_TemporalPresentation_HistoryGeneration(); + state.dynamicResolutionRequested = frame.nativeWidth > 0 + ? frame.dynamicResolutionRequested + : R_TemporalPresentation_DynamicResolutionRequested(); + state.dynamicResolutionActive = frame.dynamicResolutionActive; + state.temporalAARequested = frame.nativeWidth > 0 + ? frame.temporalAARequested + : R_TemporalPresentation_TemporalAARequested(); + state.captureFrozen = frame.captureFrozen; + state.captureForcedNative = frame.captureForcedNative; +} + +/* +==================== +idRenderSystemLocal::ResolveTemporalPresentation + +Queues a backend-neutral temporal resolve after the game has completed its +scene-sized post chain. The immutable command snapshot is the only temporal +state consumed by an asynchronous backend. Runtime execution still observes +tr.takingScreenshot so a save-preview readback that starts after enqueue can +reject both history reads and history writes. +==================== +*/ +bool idRenderSystemLocal::ResolveTemporalPresentation( + idRenderTexture *sceneColorTarget, + idRenderTexture *sceneDepthTarget, + idRenderTexture *historyReadTarget, + idRenderTexture *historyWriteTarget ) { + if ( sceneColorTarget == NULL || sceneDepthTarget == NULL + || tr.primaryView == NULL ) { + return false; + } + + renderPresentationState_t presentation; + GetPresentationState( presentation ); + const temporalPresentationFrameState_t &temporalFrame = + R_TemporalPresentation_GetFrameState(); + const unsigned int historyGeneration = + R_TemporalPresentation_HistoryGeneration(); + const viewDef_t *primaryView = tr.primaryView; + const bool screenSpaceRequested = AdvancedScreenSpaceCore_Requested( + temporalFrame.advancedScreenSpace ); + const bool captureFrame = tr.takingScreenshot + || presentation.captureFrozen || presentation.captureForcedNative + || primaryView->temporalCaptureFrame; + if ( ( !presentation.temporalAARequested && !screenSpaceRequested ) + || presentation.frameNumber != tr.frameCount + || presentation.outputWidth <= 0 || presentation.outputHeight <= 0 + || presentation.sceneWidth <= 0 || presentation.sceneHeight <= 0 + || !primaryView->temporalPrepared + || ( presentation.temporalAARequested && !captureFrame + && primaryView->temporalHistoryGeneration != historyGeneration ) ) { + return false; + } + + if ( sceneColorTarget->GetWidth() != presentation.sceneWidth + || sceneColorTarget->GetHeight() != presentation.sceneHeight + || sceneDepthTarget->GetWidth() != presentation.sceneWidth + || sceneDepthTarget->GetHeight() != presentation.sceneHeight + || ( historyWriteTarget != NULL + && ( historyWriteTarget->GetWidth() != presentation.outputWidth + || historyWriteTarget->GetHeight() != presentation.outputHeight + || historyWriteTarget == sceneColorTarget + || historyWriteTarget == sceneDepthTarget ) ) ) { + return false; + } + if ( historyReadTarget != NULL + && ( historyReadTarget == historyWriteTarget + || historyReadTarget == sceneColorTarget + || historyReadTarget == sceneDepthTarget + || historyReadTarget->GetWidth() != presentation.outputWidth + || historyReadTarget->GetHeight() != presentation.outputHeight ) ) { + return false; + } + + // Full-screen post submissions are batched in guiModel. Seal them before + // appending the resolve so it remains the last scene operation before UI. + if ( guiModel != NULL ) { + guiModel->EmitFullScreen(); + guiModel->Clear(); + } + + resolveTemporalPresentationCommand_t *cmd = + reinterpret_cast( + R_GetCommandBuffer( sizeof( *cmd ) ) ); + cmd->commandId = RC_RESOLVE_TEMPORAL_PRESENTATION; + cmd->sceneColorTarget = sceneColorTarget; + cmd->sceneDepthTarget = sceneDepthTarget; + cmd->historyReadTarget = historyReadTarget; + cmd->historyWriteTarget = historyWriteTarget; + cmd->viewDef = primaryView; + cmd->presentation = presentation; + cmd->historyGeneration = historyGeneration; + cmd->historyValid = !captureFrame && historyReadTarget != NULL + && historyWriteTarget != NULL + && primaryView->temporalHistoryValid; + cmd->captureFrame = captureFrame; + cmd->feedback = temporalFrame.temporalFeedback; + cmd->reactiveScale = temporalFrame.temporalReactiveScale; + cmd->debugMode = temporalFrame.temporalDebugMode; + cmd->advancedScreenSpace = temporalFrame.advancedScreenSpace; + if ( R_ScenePackets_FrontEndCaptureRequired() ) { + R_ScenePackets_AddRenderTargetOp(); + } + return true; +} + /* =============== idRenderSystemLocal::SetUnderwaterView diff --git a/src/renderer/RenderSystem.h b/src/renderer/RenderSystem.h index adcaa182..5ee863fc 100644 --- a/src/renderer/RenderSystem.h +++ b/src/renderer/RenderSystem.h @@ -341,6 +341,50 @@ typedef enum } ESpecialEffectType; // RAVEN END +// Backend-neutral, delayed GPU duration for one complete submitted renderer +// frame. The sample is intentionally POD because idRenderSystem may cross the +// renderer-module boundary. A valid result always belongs to the reported +// timing generation; resets invalidate the previous generation immediately. +typedef enum { + RENDER_GPU_TIMING_BACKEND_NONE = 0, + RENDER_GPU_TIMING_BACKEND_OPENGL, + RENDER_GPU_TIMING_BACKEND_VULKAN +} renderGpuTimingBackend_t; + +typedef struct renderGpuFrameTiming_s { + bool supported; + bool valid; + renderGpuTimingBackend_t backend; + int frameNumber; + unsigned int generation; + unsigned int latencyFrames; + unsigned long long elapsedMicroseconds; + unsigned long long resolvedSamples; + unsigned long long unavailableSamples; + unsigned long long droppedSamples; + unsigned long long resetCount; +} renderGpuFrameTiming_t; + +// Frame-latched presentation dimensions shared with the game modules. The +// 3D scene and its post-process chain render at sceneWidth/sceneHeight; final +// composition and all game/UI drawing remain at outputWidth/outputHeight. +// Keep this structure POD because idRenderSystem crosses both renderer- and +// game-module boundaries. +typedef struct renderPresentationState_s { + int frameNumber; + int outputWidth; + int outputHeight; + int sceneWidth; + int sceneHeight; + int effectiveScalePercent; + unsigned int historyGeneration; + bool dynamicResolutionRequested; + bool dynamicResolutionActive; + bool temporalAARequested; + bool captureFrozen; + bool captureForcedNative; +} renderPresentationState_t; + class idRenderWorld; @@ -636,6 +680,35 @@ class idRenderSystem { // fogDistance is how far light travels through this liquid before it is fully absorbed, in // world units - the knob that separates clear water from lava you cannot see a foot into. virtual bool SetUnderwaterView( float amount, const idVec3 &tint, float fogDistance ) = 0; + + // Capture the coherent current crop, then center-crop and resample it on the CPU. + // Kept at the end of the interface so existing render-system vtable slots remain stable. + virtual void CaptureRenderToFile( const char *fileName, bool fixAlpha, int outputWidth, int outputHeight ) = 0; + + // Returns the newest resolved whole-frame GPU duration. Resolution is + // delayed and never waits for the current frame; valid=false is an expected + // warm-up, reset, disabled, or query-not-ready state. + virtual void GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) const = 0; + + // Invalidates delayed samples at a non-renderer session discontinuity. The + // renderer owns the generation change and backend query retirement. + virtual void ResetGpuFrameTiming( const char *reason ) = 0; + + // Returns the immutable scene/output extent selected at BeginFrame. Before + // the first frame, output dimensions fall back to the active video mode. + virtual void GetPresentationState( renderPresentationState_t &state ) const = 0; + + // Queues the backend-neutral temporal presentation resolve. Scene colour + // and depth remain scene-sized; history targets are native output-sized. + // historyReadTarget may be NULL when seeding or invalidating history, and + // both history targets may be NULL to request a jitter-recentered spatial + // present. Returns false without queuing only when neither safe route can be + // represented, so the game can retain its existing SMAA/spatial path. + virtual bool ResolveTemporalPresentation( + idRenderTexture *sceneColorTarget, + idRenderTexture *sceneDepthTarget, + idRenderTexture *historyReadTarget, + idRenderTexture *historyWriteTarget ) = 0; }; extern idRenderSystem * renderSystem; diff --git a/src/renderer/RenderSystem_init.cpp b/src/renderer/RenderSystem_init.cpp index c8f1b8e7..fdb994a1 100644 --- a/src/renderer/RenderSystem_init.cpp +++ b/src/renderer/RenderSystem_init.cpp @@ -39,9 +39,18 @@ If you have questions concerning this license or the applicable additional terms #include "RendererBenchmarks.h" #include "RendererMetrics.h" #include "RendererUpload.h" +#include "RendererContracts.h" #include "RenderGraph.h" #include "RenderGraphResources.h" #include "MaterialResourceTable.h" +#include "ClassicGuiDomain.h" +#include "ClassicCinematicPostDomain.h" +#include "ClassicSpecialFrameDomain.h" +#include "ClassicWorldAmbientDomain.h" +#include "ClassicInteractionDomain.h" +#include "ClassicFogBlendDomain.h" +#include "ClassicSubviewDomain.h" +#include "ClassicDeformDomain.h" #include "GeometryResources.h" #include "ScenePackets.h" #include "ModernClusteredLighting.h" @@ -219,6 +228,12 @@ idCVar r_useLightPortalFlow( "r_useLightPortalFlow", "1", CVAR_RENDERER | CVAR_B idCVar r_multiSamples( "r_multiSamples", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "MSAA sample count: 0 = off, 2/4/8/16 = supported quality steps", 0, 16, idCmdSystem::ArgCompletion_String ); idCVar r_postAA( "r_postAA", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "post AA mode: 0 = off, 1 = SMAA 1x medium, 2 = SMAA 1x high, 3 = SMAA 1x ultra, 4 = SMAA 1x colour-edge prototype", 0, 4, idCmdSystem::ArgCompletion_Integer<0,4> ); idCVar r_postAAStatePoisonTest( "r_postAAStatePoisonTest", "0", CVAR_RENDERER | CVAR_BOOL, "intentionally dirty GL texture/client state before SMAA post-AA draws for validation" ); +idCVar r_pbrMaterials( "r_pbrMaterials", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "allow explicitly PBR-authored materials on supported modern renderer paths" ); +idCVar r_pbrGeneratedLegacyFallback( "r_pbrGeneratedLegacyFallback", "1", CVAR_RENDERER | CVAR_BOOL, "allow development-only classic fallback generation for PBR-only materials" ); +idCVar r_pbrDebug( "r_pbrDebug", "0", CVAR_RENDERER | CVAR_INTEGER, "PBR debug view: 0=off, 1=albedo, 2=normal, 3=metallic, 4=roughness, 5=AO, 6=emissive, 7=state marker (green=PBR, magenta=contract mismatch)", 0, 7, idCmdSystem::ArgCompletion_Integer<0,7> ); +idCVar r_pbrIBL( "r_pbrIBL", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "enable the analytic environment contribution for explicitly PBR-authored materials" ); +idCVar r_pbrIBLIntensity( "r_pbrIBLIntensity", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "analytic PBR environment intensity", 0.0f, 4.0f ); +idCVar r_pbrInferFromLegacyMaterials( "r_pbrInferFromLegacyMaterials", "0", CVAR_RENDERER | CVAR_BOOL, "research-only legacy material reinterpretation; never used for stock rendering by default" ); idCVar r_bloom( "r_bloom", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "enable bloom post-process" ); idCVar r_bloomThreshold( "r_bloomThreshold", "0.45", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "bloom bright-pass threshold in scene-referred units", 0.0f, 16.0f ); idCVar r_bloomSoftKnee( "r_bloomSoftKnee", "0.15", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "relative bloom soft-threshold knee", 0.0f, 1.0f ); @@ -335,11 +350,11 @@ idCVar r_shadowMapConservativeCasters( "r_shadowMapConservativeCasters", "1", CV idCVar r_shadowMapProjectedCSM( "r_shadowMapProjectedCSM", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "allow ordinary projected lights to use cascades when r_shadowMapCSM is enabled" ); idCVar r_shadowMapDepthCompare( "r_shadowMapDepthCompare", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "use hardware comparison sampling for projected depth shadow maps when supported; PCSS-lite (r_shadowMapFilterMode 2) implies the manual path" ); idCVar r_shadowMapTexelBiasScale( "r_shadowMapTexelBiasScale", "0.45", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "texel-aware receiver bias scale for projected and point shadow maps", 0.0f, 8.0f ); -idCVar r_shadowMapNormalOffsetScale( "r_shadowMapNormalOffsetScale", "1.0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "normal-offset shadow bias in shadow texels: pushes the receiver sample point along the geometric normal on sloped surfaces, fixing self-shadow acne without detaching contact shadows", 0.0f, 8.0f ); -idCVar r_shadowMapCasterCulling( "r_shadowMapCasterCulling", "2", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "shadow caster face culling: 0 = two-sided, 1 = store light-facing faces, 2 = store back faces (fewer acne artifacts, slight detachment on thin geometry); material twoSided/backSided is always honored", 0, 2, idCmdSystem::ArgCompletion_Integer<0,2> ); -idCVar r_shadowMapReceiverPlaneBias( "r_shadowMapReceiverPlaneBias", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "add derivative receiver-plane depth bias for wider projected shadow filters" ); -idCVar r_shadowMapFilterTaps( "r_shadowMapFilterTaps", "13", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "projected-light PCF tap budget: 1, 5, 9, or 13", 1, 13, idCmdSystem::ArgCompletion_Integer<1,13> ); -idCVar r_shadowMapPointFilterTaps( "r_shadowMapPointFilterTaps", "13", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "point-light PCF tap budget: 1, 5, 9, or 13", 1, 13, idCmdSystem::ArgCompletion_Integer<1,13> ); +idCVar r_shadowMapNormalOffsetScale( "r_shadowMapNormalOffsetScale", "1.0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "normal-offset shadow bias in shadow texels; reduces slope acne but excessive values can move contact edges (point-light use is bounded by r_shadowMapPointMaxWorldBias)", 0.0f, 8.0f ); +idCVar r_shadowMapCasterCulling( "r_shadowMapCasterCulling", "2", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "shadow caster face culling: 0 = always two-sided, 1 = force material-oriented near faces, 2 = automatic (near faces for sealed hulls outside the light, two-sided for open/uncertain/enclosing geometry)", 0, 2, idCmdSystem::ArgCompletion_Integer<0,2> ); +idCVar r_shadowMapReceiverPlaneBias( "r_shadowMapReceiverPlaneBias", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "enable the experimental derivative receiver-plane depth-bias approximation for projected shadow filters" ); +idCVar r_shadowMapFilterTaps( "r_shadowMapFilterTaps", "9", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "projected-light PCF tap budget: 1, 5, 9, or 13", 1, 13, idCmdSystem::ArgCompletion_Integer<1,13> ); +idCVar r_shadowMapPointFilterTaps( "r_shadowMapPointFilterTaps", "9", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "point-light PCF tap budget: 1, 5, 9, or 13", 1, 13, idCmdSystem::ArgCompletion_Integer<1,13> ); idCVar r_shadowMapFilterMode( "r_shadowMapFilterMode", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "projected-light shadow filter mode: 0 = fixed PCF, 1 = stable rotated Poisson, 2 = PCSS-lite when raw depth is available", 0, 2, idCmdSystem::ArgCompletion_Integer<0,2> ); idCVar r_shadowMapPointFilterMode( "r_shadowMapPointFilterMode", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "point-light shadow filter mode: 0 = fixed PCF, 1 = stable rotated Poisson", 0, 1, idCmdSystem::ArgCompletion_Integer<0,1> ); idCVar r_shadowMapDistantFilterScale( "r_shadowMapDistantFilterScale", "0.35", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "scale projected PCF and PCSS radii for parallel/global sky lights whose shadow texels cover more world space", 0.0f, 1.0f ); @@ -379,6 +394,7 @@ idCVar r_useDeferredTangents( "r_useDeferredTangents", "1", CVAR_RENDERER | CVAR idCVar r_useCachedDynamicModels( "r_useCachedDynamicModels", "1", CVAR_RENDERER | CVAR_BOOL, "cache snapshots of dynamic models" ); idCVar r_useRepeatedStateReuse( "r_useRepeatedStateReuse", "1", CVAR_RENDERER | CVAR_BOOL, "keep model-space dynamic snapshots across transform-only entity updates so repeated-state presentation frames skip CPU re-skinning" ); idCVar r_useNewSkinning( "r_useNewSkinning", "1", CVAR_RENDERER | CVAR_BOOL, "use retail-style SIMD MD5 skinning data" ); +idCVar r_gpuSkinning( "r_gpuSkinning", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "use exact four-weight GPU deformation for eligible animated surfaces" ); idCVar r_useFastSkinning( "r_useFastSkinning", "0", CVAR_RENDERER | CVAR_BOOL, "approximate MD5 tangent skinning with the dominant joint only" ); idCVar r_deriveBiTangents( "r_deriveBiTangents", "0", CVAR_RENDERER | CVAR_BOOL, "derive bitangents from skinned normals and tangents" ); idCVar r_forceConvertMD5R( "r_forceConvertMD5R", "0", CVAR_RENDERER | CVAR_BOOL, "prefer source md5/proc assets over any future prebuilt MD5R companions" ); @@ -452,26 +468,39 @@ idCVar r_actualRenderer( "r_actualRenderer", "UNINITIALIZED", CVAR_RENDERER | CV idCVar r_glTier( "r_glTier", "auto", CVAR_RENDERER | CVAR_ARCHIVE, "OpenGL renderer tier: auto, legacy, gl33, gl41, gl43, gl45, gl46", r_glTierArgs, idCmdSystem::ArgCompletion_String ); idCVar r_vkValidation( "r_vkValidation", "0", CVAR_RENDERER | CVAR_BOOL, "enable Vulkan validation layers for the Vulkan renderer module and rendererVkProbe" ); idCVar r_vkDevice( "r_vkDevice", "-1", CVAR_RENDERER | CVAR_INTEGER, "Vulkan physical-device index override, -1 = automatic selection", -1, 15 ); +idCVar r_vkShadowFallbackTest( "r_vkShadowFallbackTest", "0", CVAR_RENDERER | CVAR_BOOL, "diagnostic: make Vulkan shadow maps and stencil ownership unavailable to exercise unshadowed receiver fallback" ); idCVar r_glDebugContext( "r_glDebugContext", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "request a debug OpenGL context when the platform backend supports it" ); idCVar r_glDebugOutput( "r_glDebugOutput", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "report OpenGL driver debug messages when a debug context is active" ); idCVar r_glDebugSynchronous( "r_glDebugSynchronous", "0", CVAR_RENDERER | CVAR_BOOL, "deliver OpenGL debug callbacks synchronously (diagnostic; may reduce performance)" ); idCVar r_rendererMetrics( "r_rendererMetrics", "0", CVAR_RENDERER | CVAR_INTEGER, "renderer metrics: 0 = off, 1 = periodic summary, 2 = per-frame/pass detail", 0, 2, idCmdSystem::ArgCompletion_Integer<0,2> ); -idCVar r_rendererGpuTimers( "r_rendererGpuTimers", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "sample GL timer queries when r_rendererMetrics is enabled and supported" ); +idCVar r_rendererGpuTimers( "r_rendererGpuTimers", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "sample delayed whole-frame GPU timestamps on supported backends; OpenGL pass detail additionally requires r_rendererMetrics" ); idCVar r_rendererBenchmarkPreset( "r_rendererBenchmarkPreset", "baseline", CVAR_RENDERER | CVAR_ARCHIVE, "renderer benchmark budget preset: low, baseline, modern, high-end", r_rendererBenchmarkPresetArgs, idCmdSystem::ArgCompletion_String ); idCVar r_rendererPerfThresholdP95( "r_rendererPerfThresholdP95", "0", CVAR_RENDERER | CVAR_INTEGER, "custom renderer benchmark P95 frame-time budget in milliseconds (0 = preset default)", 0, 1000, idCmdSystem::ArgCompletion_Integer<0,1000> ); idCVar r_rendererPerfThresholdP99( "r_rendererPerfThresholdP99", "0", CVAR_RENDERER | CVAR_INTEGER, "custom renderer benchmark P99 frame-time budget in milliseconds (0 = preset default)", 0, 1000, idCmdSystem::ArgCompletion_Integer<0,1000> ); idCVar r_rendererAdaptiveClusterGrid( "r_rendererAdaptiveClusterGrid", "0", CVAR_RENDERER | CVAR_BOOL, "use benchmark preset cluster-grid dimensions for the modern clustered-light experiment" ); -idCVar r_rendererDynamicResolution( "r_rendererDynamicResolution", "0", CVAR_RENDERER | CVAR_BOOL, "allow benchmark presets to recommend dynamic screen-percentage experiments; disabled by default" ); +idCVar r_rendererDynamicResolution( "r_rendererDynamicResolution", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "automatically scale the 3D scene from delayed GPU timing; disabled by default" ); idCVar r_rendererUploadMegs( "r_rendererUploadMegs", "16", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "dynamic renderer upload stream size in megabytes per frame buffer", 1, 128, idCmdSystem::ArgCompletion_Integer<1,128> ); idCVar r_rendererUploadFrameBuffers( "r_rendererUploadFrameBuffers", "4", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "dynamic renderer upload stream frame-buffer rotation depth", 3, 8, idCmdSystem::ArgCompletion_Integer<3,8> ); idCVar r_rendererUploadPersistent( "r_rendererUploadPersistent", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "allow persistent-mapped dynamic renderer uploads when supported" ); idCVar r_rendererUploadBufferPool( "r_rendererUploadBufferPool", "1", CVAR_RENDERER | CVAR_BOOL, "recycle static GL buffer names instead of gen/data/delete churn for per-frame regenerated geometry" ); +idCVar r_rendererModernQuality( "r_rendererModernQuality", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "master permit for experimental Milestone-F PBR, probe, decal, volumetric, SSR, and SSGI domains; 0 rolls every domain back to classic ownership" ); +idCVar r_rendererReflectionProbes( "r_rendererReflectionProbes", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "enable authored reflection probes for eligible modern PBR views; unsupported or incomplete probe sets use analytic/classic fallback" ); +idCVar r_rendererClusteredDecals( "r_rendererClusteredDecals", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "transfer complete eligible decal subsets to bounded clustered forward rendering; incomplete views remain entirely classic" ); idCVar r_rendererModernExecutor( "r_rendererModernExecutor", "0", CVAR_RENDERER | CVAR_BOOL, "prepare the opt-in modern GL executor frame contract while legacy ARB2 still executes" ); idCVar r_rendererModernSubmit( "r_rendererModernSubmit", "0", CVAR_RENDERER | CVAR_BOOL, "execute opt-in modern GL draw submission before legacy ARB2 fallback; diagnostic until visible pass replacement lands" ); idCVar r_rendererGpuValidation( "r_rendererGpuValidation", "0", CVAR_RENDERER | CVAR_BOOL, "compare GL 4.3 GPU-driven compute results against CPU reference data on sampled frames" ); idCVar r_rendererGpuValidationReadbackDelay( "r_rendererGpuValidationReadbackDelay", "2", CVAR_RENDERER | CVAR_INTEGER, "frames to defer opt-in GL 4.3 GPU validation readbacks before polling without a sync wait", 1, 8, idCmdSystem::ArgCompletion_Integer<1,8> ); idCVar r_rendererBindless( "r_rendererBindless", "0", CVAR_RENDERER | CVAR_BOOL, "enable experimental GL 4.5 bindless texture diagnostics without changing visible output" ); idCVar r_rendererModernVisible( "r_rendererModernVisible", "0", CVAR_RENDERER | CVAR_BOOL, "execute the opt-in modern hybrid visible-frame composition when all required pass owners are modern-safe" ); +idCVar r_rendererSharedGui( "r_rendererSharedGui", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute eligible fixed-function 2D GUI views from the backend-neutral ordered material-stage stream; any unsupported view uses the complete classic path" ); +idCVar r_rendererSharedInWorldGui( "r_rendererSharedInWorldGui", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute eligible GUI geometry emitted onto 3D surfaces from a backend-neutral ordered material-stage stream; any unsupported view keeps every in-world GUI surface on the classic path" ); +idCVar r_rendererSharedCinematicPost( "r_rendererSharedCinematicPost", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute eligible root cinematic playback and complete authored post-process ranges through a sealed shared transaction; post/video ranges nested in a shared special-view tree publish or roll back with its root; unsupported ranges use the complete classic path" ); +idCVar r_rendererSharedSpecialFrame( "r_rendererSharedSpecialFrame", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute eligible render-demo playback and Raven special-effect controller frames through a sealed shared transaction; unsupported views and commands use the complete classic path" ); +idCVar r_rendererSharedWorldAmbient( "r_rendererSharedWorldAmbient", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute every eligible classic world ambient/material surface in a complete 3D view from the backend-neutral ordered material-stage stream; any unsupported view uses the complete classic path" ); +idCVar r_rendererSharedWorldInteraction( "r_rendererSharedWorldInteraction", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute every eligible unshadowed fixed-classic interaction light and receiver in a complete 3D view from a backend-neutral sealed stream; any unsupported view uses the complete classic path" ); +idCVar r_rendererSharedWorldFogBlend( "r_rendererSharedWorldFogBlend", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute the complete classic fog/blend light phase in an eligible 3D view from a backend-neutral sealed stream; any unsupported view uses the complete classic path" ); +idCVar r_rendererSharedSubview( "r_rendererSharedSubview", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "execute eligible direct mirror and capture-backed remote-camera, mirror, reflection, refraction, and x-ray subview transactions from backend-neutral sealed records; nested child trees commit atomically at their outermost special view, while 2D/cubemap color and depth captures retain exact type, aspect, and face; unsupported subviews use the complete classic path" ); +idCVar r_rendererSharedDeform( "r_rendererSharedDeform", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "enable optional, default-off backend-neutral material-deform ownership; incomplete or unsupported work retains the complete classic path" ); idCVar r_rendererModernLightingParity( "r_rendererModernLightingParity", "0", CVAR_RENDERER | CVAR_INTEGER, "diagnostic override forcing modern lighting-ownership parity contracts proven for bring-up capture: 1 = interaction, 2 = fog/blend, 4 = light grid, 8 = shadow receivers; 0 keeps every unproven domain on the ARB2 bridge", 0, 15, idCmdSystem::ArgCompletion_Integer<0,15> ); idCVar r_rendererModernAutoPromote( "r_rendererModernAutoPromote", "0", CVAR_RENDERER | CVAR_BOOL, "allow r_glTier auto and r_renderer best to request the guarded modern visible path after promotion evidence and sign-off; off keeps ARB2 default" ); idCVar r_rendererPromotionEvidence( "r_rendererPromotionEvidence", "", CVAR_RENDERER, "Phase 8 validation evidence token required with r_rendererModernAutoPromote before automatic modern visible promotion" ); @@ -570,8 +599,9 @@ idCVar r_shadowMapBias( "r_shadowMapBias", "0.00016", CVAR_RENDERER | CVAR_ARCHI idCVar r_shadowMapNormalBias( "r_shadowMapNormalBias", "0.00075", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "geometric normal bias added on sloped projected-light receivers", 0.0f, 0.05f ); idCVar r_shadowMapPointBias( "r_shadowMapPointBias", "0.00010", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "constant receiver depth bias for point-light shadow maps", 0.0f, 0.05f ); idCVar r_shadowMapPointNormalBias( "r_shadowMapPointNormalBias", "0.0010", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "geometric normal bias added on sloped point-light receivers", 0.0f, 0.05f ); -idCVar r_shadowMapFilterRadius( "r_shadowMapFilterRadius", "2.0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "projected-light PCF radius in texels for the simple shadow-map path", 0.0f, 8.0f ); -idCVar r_shadowMapPointFilterRadius( "r_shadowMapPointFilterRadius", "2.5", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "point-light PCF radius in texels for the simple shadow-map path", 0.0f, 8.0f ); +idCVar r_shadowMapPointMaxWorldBias( "r_shadowMapPointMaxWorldBias", "4.0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "maximum combined point-shadow receiver depth bias and normal offset at the padded far envelope, in world units; 0 disables the cap", 0.0f, 64.0f ); +idCVar r_shadowMapFilterRadius( "r_shadowMapFilterRadius", "0.75", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "projected-light PCF radius in texels for the simple shadow-map path", 0.0f, 8.0f ); +idCVar r_shadowMapPointFilterRadius( "r_shadowMapPointFilterRadius", "1.0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "point-light PCF radius in texels for the simple shadow-map path", 0.0f, 8.0f ); idCVar r_shadowMapProjectionPad( "r_shadowMapProjectionPad", "0.15", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "normalized padding applied around projected-light shadow-map coverage", 0.0f, 1.0f ); idCVar r_shadowMapCascadeCount( "r_shadowMapCascadeCount", "4", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "number of projected-light cascades when r_shadowMapCSM is enabled", 1, 4, idCmdSystem::ArgCompletion_Integer<1,4> ); idCVar r_shadowMapCascadeDistance( "r_shadowMapCascadeDistance", "1536", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "camera-space distance covered by the cropped projected-light cascades", 64.0f, 8192.0f ); @@ -585,8 +615,9 @@ idCVar r_shadowMapDebugMode( "r_shadowMapDebugMode", "0", CVAR_RENDERER | CVAR_I 0, SHADOWMAP_DEBUGMODE_COUNT - 1, idCmdSystem::ArgCompletion_Integer<0, SHADOWMAP_DEBUGMODE_COUNT - 1> ); idCVar r_shadowMapCascadeStabilize( "r_shadowMapCascadeStabilize", "1", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "snap projected-light cascade bounds to texels to reduce shimmering" ); idCVar r_shadowMapPointFarScale( "r_shadowMapPointFarScale", "1.25", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "padding multiplier applied to point-light shadow-map range", 1.0f, 4.0f ); -idCVar r_shadowMapPolygonFactor( "r_shadowMapPolygonFactor", "0.75", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "slope-scale depth bias used when rendering shadow-map casters", 0.0f, 16.0f ); +idCVar r_shadowMapPolygonFactor( "r_shadowMapPolygonFactor", "0.25", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "slope-scale depth bias used when rendering shadow-map casters", 0.0f, 16.0f ); idCVar r_shadowMapPolygonOffset( "r_shadowMapPolygonOffset", "0.5", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, "constant depth bias used when rendering shadow-map casters", 0.0f, 64.0f ); +static idCVar r_shadowMapContactQualityMigrated( "r_shadowMapContactQualityMigrated", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "one-time migration flag for legacy broad shadow filtering defaults" ); idCVar r_frontBuffer( "r_frontBuffer", "0", CVAR_RENDERER | CVAR_BOOL, "draw to front buffer for debugging" ); idCVar r_skipSubviews( "r_skipSubviews", "0", CVAR_RENDERER | CVAR_INTEGER, "1 = don't render any gui elements on surfaces" ); idCVar r_skipParticles( "r_skipParticles", "0", CVAR_RENDERER | CVAR_INTEGER, "1 = skip all particle systems", 0, 1, idCmdSystem::ArgCompletion_Integer<0,1> ); @@ -758,6 +789,7 @@ static void R_RendererDefaultSafetySelfTest_f( const idCmdArgs &args ) { static void R_RendererBenchmarkCapture_f( const idCmdArgs &args ) { (void)args; RendererBenchmarks_PrintLatestCapture(); + RendererBenchmarks_PrintTimingMarker(); } static void R_RendererUploadSelfTest_f( const idCmdArgs &args ) { @@ -807,6 +839,109 @@ static void R_RendererMaterialResourceTableSelfTest_f( const idCmdArgs &args ) { } } +static void R_RendererClassicGuiDomainSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( RendererClassicGuiDomain_RunSelfTest() ) { + common->Printf( "RendererClassicGuiDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicGuiDomain self-test failed" ); + } +} + +static void R_RendererClassicCinematicPostDomainSelfTest_f( + const idCmdArgs &args ) { + (void)args; + if ( RendererClassicCinematicPostDomain_RunSelfTest() ) { + common->Printf( "RendererClassicCinematicPostDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicCinematicPostDomain self-test failed" ); + } +} + +static void R_RendererClassicSpecialFrameDomainSelfTest_f( + const idCmdArgs &args ) { + (void)args; + if ( RendererClassicSpecialFrameDomain_RunSelfTest() ) { + common->Printf( "RendererClassicSpecialFrameDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicSpecialFrameDomain self-test failed" ); + } +} + +static void R_RendererClassicWorldAmbientDomainSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( RendererClassicWorldAmbientDomain_RunSelfTest() ) { + common->Printf( "RendererClassicWorldAmbientDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicWorldAmbientDomain self-test failed" ); + } +} + +static void R_RendererClassicInteractionDomainSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( RendererClassicInteractionDomain_RunSelfTest() ) { + common->Printf( "RendererClassicInteractionDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicInteractionDomain self-test failed" ); + } +} + +static void R_RendererClassicFogBlendDomainSelfTest_f( + const idCmdArgs &args ) { + (void)args; + if ( RendererClassicFogBlendDomain_RunSelfTest() ) { + common->Printf( "RendererClassicFogBlendDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicFogBlendDomain self-test failed" ); + } +} + +static void R_RendererClassicSubviewDomainSelfTest_f( + const idCmdArgs &args ) { + (void)args; + if ( RendererClassicSubviewDomain_RunSelfTest() ) { + common->Printf( "RendererClassicSubviewDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicSubviewDomain self-test failed" ); + } +} + +static void R_RendererClassicDeformDomainSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( RendererClassicDeformDomain_RunSelfTest() ) { + common->Printf( "RendererClassicDeformDomain self-test passed\n" ); + } else { + common->Warning( "RendererClassicDeformDomain self-test failed" ); + } +} + +static void R_RendererGpuSkinningSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( R_GpuSkinning_RunSelfTest() ) { + common->Printf( "RendererGpuSkinning self-test passed\n" ); + } else { + common->Warning( "RendererGpuSkinning self-test failed" ); + } +} + +static void R_RendererContractsSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( RendererContracts_RunSelfTest() ) { + common->Printf( "RendererContracts self-test passed\n" ); + } else { + common->Warning( "RendererContracts self-test failed" ); + } +} + +static void R_RendererPBRMaterialSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( !R_PBRMaterialParserSelfTest() ) { + common->Warning( "Renderer PBR material parser self-test failed" ); + return; + } + common->Printf( "RendererPBRMaterial self-test passed\n" ); +} + static void R_RendererMaterialResourceTableDump_f( const idCmdArgs &args ) { (void)args; R_MaterialResourceTable_DumpLatest(); @@ -854,6 +989,13 @@ static void R_RendererGBufferSelfTest_f( const idCmdArgs &args ) { } } +static void R_RendererPBRVisibleSelfTest_f( const idCmdArgs &args ) { + (void)args; + if ( !RendererPBRVisible_RunSelfTest() ) { + common->Warning( "Renderer PBR visible-path self-test failed" ); + } +} + static void R_RendererClusterGridSelfTest_f( const idCmdArgs &args ) { (void)args; if ( !RendererClusterGrid_RunSelfTest() ) { @@ -1822,6 +1964,7 @@ void R_InitOpenGL( void ) { R_GLDebugOutput_FlushMessages(); R_RendererUpload_Init( glConfig.backendCaps ); + R_GpuSkinning_ContractInit( glConfig.backendCaps ); // allocate the vertex array range or vertex objects R_RecordRendererStartupPhase( RENDERER_STARTUP_PHASE_VERTEX_CACHE_INIT ); @@ -2297,6 +2440,7 @@ void R_ReadTiledPixels( int width, int height, byte *buffer, renderView_t *ref = int oldWidth = glConfig.vidWidth; int oldHeight = glConfig.vidHeight; + const bool oldUseScissor = r_useScissor.GetBool(); tr.tiledViewport[0] = width; tr.tiledViewport[1] = height; @@ -2362,7 +2506,7 @@ void R_ReadTiledPixels( int width, int height, byte *buffer, renderView_t *ref = } } - r_useScissor.SetBool( true ); + r_useScissor.SetBool( oldUseScissor ); tr.viewportOffset[0] = 0; tr.viewportOffset[1] = 0; @@ -3663,15 +3807,37 @@ void GfxInfo_f( const idCmdArgs &args ) { common->Printf( "Renderer caps: %s\n", capsSummary ); } RendererModule_PrintGfxInfo(); + R_GpuSkinning_PrintGfxInfo(); RendererCompatibilityGates_PrintGfxInfo(); RendererTierContract_PrintGfxInfo(); RendererBootstrap_PrintGfxInfo(); RendererBenchmarks_PrintGfxInfo(); + renderGpuFrameTiming_t gpuFrameTiming; + R_RendererMetrics_GetGpuFrameTiming( gpuFrameTiming ); + const char *gpuTimingBackend = "none"; + if ( gpuFrameTiming.backend == RENDER_GPU_TIMING_BACKEND_OPENGL ) { + gpuTimingBackend = "opengl"; + } else if ( gpuFrameTiming.backend == RENDER_GPU_TIMING_BACKEND_VULKAN ) { + gpuTimingBackend = "vulkan"; + } common->Printf( - "Renderer GPU timers: %s, cvar=%d, timerQuery=%d\n", + "Renderer GPU timers: %s, cvar=%d, glPassTimerQuery=%d\n", R_RendererMetrics_GpuTimersAvailable() ? "available" : "unavailable", r_rendererGpuTimers.GetBool() ? 1 : 0, glConfig.backendCaps.hasTimerQuery ? 1 : 0 ); + common->Printf( + "Renderer whole-frame GPU timing: backend=%s supported=%d valid=%d frame=%d generation=%u elapsed=%lluus latency=%u resolved=%llu unavailable=%llu dropped=%llu resets=%llu nonblocking=1\n", + gpuTimingBackend, + gpuFrameTiming.supported ? 1 : 0, + gpuFrameTiming.valid ? 1 : 0, + gpuFrameTiming.frameNumber, + gpuFrameTiming.generation, + gpuFrameTiming.elapsedMicroseconds, + gpuFrameTiming.latencyFrames, + gpuFrameTiming.resolvedSamples, + gpuFrameTiming.unavailableSamples, + gpuFrameTiming.droppedSamples, + gpuFrameTiming.resetCount ); common->Printf( "Renderer scene packets: legacy bridge V2, maxScenes=%d, maxPasses=%d, maxDrawPackets=%d, maxMaterialRecords=%d, maxGeometryRecords=%d, maxInstanceRecords=%d\n", SCENE_PACKET_MAX_SCENES, @@ -3680,6 +3846,24 @@ void GfxInfo_f( const idCmdArgs &args ) { SCENE_PACKET_MAX_MATERIAL_RECORDS, SCENE_PACKET_MAX_GEOMETRY_RECORDS, SCENE_PACKET_MAX_INSTANCE_RECORDS ); + { + const classicDeformDomainStats_t &deform = + R_ClassicDeformDomain_Stats(); + common->Printf( + "Renderer shared material deform: requested=%d frameToken=%llu records=%d none=%d notApplicable=%d completed=%d empty=%d skipped=%d failed=%d unsupported=%d invalid=%d hash=%016llx\n", + r_rendererSharedDeform.GetBool() ? 1 : 0, + static_cast( deform.frameToken ), + deform.records, + deform.none, + deform.notApplicable, + deform.completed, + deform.empty, + deform.skipped, + deform.failed, + deform.unsupported, + deform.invalid, + static_cast( deform.hash ) ); + } R_GeometryResources_PrintGfxInfo(); common->Printf( "Renderer graph: resource-backed packet graph, maxPasses=%d, maxResources=%d, maxResourceAccesses=%d\n", @@ -3688,6 +3872,538 @@ void GfxInfo_f( const idCmdArgs &args ) { RENDER_GRAPH_MAX_RESOURCE_ACCESSES ); R_RenderGraphResources_PrintGfxInfo(); R_MaterialResourceTable_PrintGfxInfo(); + { + const classicGuiDomainStats_t &guiDomain = R_ClassicGuiDomain_Stats(); + common->Printf( + "Renderer shared classic GUI: rootRequested=%d inWorldRequested=%d prepared=%d valid=%d views=%d(root=%d world=%d) ready=%d fallback=%d surfaces=%d/%d noop=%d deform=%d/%d/%d passes=%d draw=%d inactive=%d activeNoop=%d hash=%016llx status=%s GL=%d/%d VK=%d/%d\n", + r_rendererSharedGui.GetBool() ? 1 : 0, + r_rendererSharedInWorldGui.GetBool() ? 1 : 0, + guiDomain.prepared ? 1 : 0, + guiDomain.frameValid ? 1 : 0, + guiDomain.guiViews, + guiDomain.rootViews, + guiDomain.inWorldViews, + guiDomain.readyViews, + guiDomain.fallbackViews, + guiDomain.drawableSurfaces, + guiDomain.sourceSurfaces, + guiDomain.noopSurfaces, + guiDomain.materialDeformSurfaces, + guiDomain.completedDeformSurfaces, + guiDomain.emptyDeformSurfaces, + guiDomain.evaluatedPasses, + guiDomain.drawablePasses, + guiDomain.inactivePasses, + guiDomain.activeNoopPasses, + static_cast( guiDomain.hash ), + guiDomain.status, + guiDomain.backend[CLASSIC_GUI_DOMAIN_BACKEND_GL].ownedViews, + guiDomain.backend[CLASSIC_GUI_DOMAIN_BACKEND_GL].fallbackViews, + guiDomain.backend[CLASSIC_GUI_DOMAIN_BACKEND_VULKAN].ownedViews, + guiDomain.backend[CLASSIC_GUI_DOMAIN_BACKEND_VULKAN].fallbackViews ); + } + { + const classicCinematicPostDomainStats_t &cinematicPost = + R_ClassicCinematicPostDomain_Stats(); + common->Printf( + "Renderer shared cinematic/post: requested=%d prepared=%d valid=%d overflow=%d scenes=%d views=%d(root=%d post=%d nested=%d/%d nestedCinematic=%d) ready=%d fallback=%d cinematicStages=%d currentRender=%d currentDepth=%d hash=%016llx status=%s GL=%d/%d/%d/%d VK=%d/%d/%d/%d\n", + r_rendererSharedCinematicPost.GetBool() ? 1 : 0, + cinematicPost.prepared ? 1 : 0, + cinematicPost.frameValid ? 1 : 0, + cinematicPost.overflow ? 1 : 0, + cinematicPost.sourceScenes, + cinematicPost.views, + cinematicPost.rootCinematicViews, + cinematicPost.authoredPostViews, + cinematicPost.nestedSpecialViews, + cinematicPost.nestedSpecialTransactions, + cinematicPost.nestedCinematicStages, + cinematicPost.readyViews, + cinematicPost.fallbackViews, + cinematicPost.cinematicStages, + cinematicPost.currentRenderStages, + cinematicPost.currentDepthStages, + static_cast( cinematicPost.hash ), + cinematicPost.status, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_GL].ownedViews, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_GL].fallbackViews, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_GL].coverageMismatches, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_GL].duplicateReports, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN].ownedViews, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN].fallbackViews, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN].coverageMismatches, + cinematicPost.backend[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN].duplicateReports ); + } + { + const classicSpecialFrameDomainStats_t &specialFrame = + R_ClassicSpecialFrameDomain_Stats(); + common->Printf( + "Renderer shared special frame: requested=%d prepared=%d valid=%d overflow=%d scenes=%d views=%d(demo=%d raven=%d) ready=%d fallback=%d effectsMask=0x%x hash=%016llx status=%s GL=%d/%d VK=%d/%d\n", + r_rendererSharedSpecialFrame.GetBool() ? 1 : 0, + specialFrame.prepared ? 1 : 0, + specialFrame.frameValid ? 1 : 0, + specialFrame.overflow ? 1 : 0, + specialFrame.sourceScenes, + specialFrame.views, + specialFrame.renderDemoViews, + specialFrame.ravenEffectsViews, + specialFrame.readyViews, + specialFrame.fallbackViews, + specialFrame.specialEffectsMask, + static_cast( specialFrame.hash ), + specialFrame.status, + specialFrame.backend[CLASSIC_SPECIAL_FRAME_BACKEND_GL].ownedViews, + specialFrame.backend[CLASSIC_SPECIAL_FRAME_BACKEND_GL].fallbackViews, + specialFrame.backend[CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN].ownedViews, + specialFrame.backend[CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN].fallbackViews ); + } + { + const classicWorldAmbientDomainStats_t &worldAmbient = + R_ClassicWorldAmbientDomain_Stats(); + common->Printf( + "Renderer shared world ambient: requested=%d prepared=%d valid=%d views=%d ready=%d fallback=%d surfaces=%d/%d noop=%d deform=%d/%d/%d passes=%d draw=%d pre=%d post=%d inactive=%d activeNoop=%d hash=%016llx status=%s GL=%d/%d VK=%d/%d\n", + r_rendererSharedWorldAmbient.GetBool() ? 1 : 0, + worldAmbient.prepared ? 1 : 0, + worldAmbient.frameValid ? 1 : 0, + worldAmbient.worldViews, + worldAmbient.readyViews, + worldAmbient.fallbackViews, + worldAmbient.drawableSurfaces, + worldAmbient.sourceSurfaces, + worldAmbient.noopSurfaces, + worldAmbient.materialDeformSurfaces, + worldAmbient.completedDeformSurfaces, + worldAmbient.emptyDeformSurfaces, + worldAmbient.evaluatedPasses, + worldAmbient.drawablePasses, + worldAmbient.phaseDrawablePasses[CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG], + worldAmbient.phaseDrawablePasses[CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG], + worldAmbient.inactivePasses, + worldAmbient.activeNoopPasses, + static_cast( worldAmbient.hash ), + worldAmbient.status, + worldAmbient.backend[CLASSIC_WORLD_AMBIENT_BACKEND_GL].ownedViews, + worldAmbient.backend[CLASSIC_WORLD_AMBIENT_BACKEND_GL].fallbackViews, + worldAmbient.backend[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN].ownedViews, + worldAmbient.backend[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN].fallbackViews ); + for ( int viewIndex = 0; + viewIndex < R_ClassicWorldAmbientDomain_NumViews(); ++viewIndex ) { + const classicWorldAmbientDomainView_t *view = + R_ClassicWorldAmbientDomain_ViewByIndex( viewIndex ); + if ( view == NULL ) { + continue; + } + const char *sourceClass = "none"; + if ( view->failure + == CLASSIC_WORLD_AMBIENT_FAILURE_SOURCE_SURFACE_FALLBACK + && view->failureDetail >= 0 + && view->failureDetail + < CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_COUNT ) { + sourceClass = ClassicWorldAmbientDomainSourceSurface_Name( + static_cast( + view->failureDetail ) ); + } + common->Printf( + "Renderer shared world ambient view[%d]: scene=%d ready=%d failure=%s detail=%d sourceClass=%s surface=%d stage=%d worldPass=%s eval=%d hash=%016llx GL=%d/%s/%d/%d+%d/%d VK=%d/%s/%d/%d+%d/%d\n", + viewIndex, + view->scenePacketIndex, + view->ready ? 1 : 0, + ClassicWorldAmbientDomainFailure_Name( view->failure ), + view->failureDetail, + sourceClass, + view->failureSourceSurfaceIndex, + view->failureSourceStageIndex, + MaterialResourceWorldPassFailure_Name( + view->worldPassFailure ), + view->evaluationStatus, + static_cast( view->hash ), + view->backendOutcome[CLASSIC_WORLD_AMBIENT_BACKEND_GL], + ClassicWorldAmbientDomainFailure_Name( + view->backendFailure[CLASSIC_WORLD_AMBIENT_BACKEND_GL] ), + view->backendFailureDetail[CLASSIC_WORLD_AMBIENT_BACKEND_GL], + view->backendDrawnPasses[CLASSIC_WORLD_AMBIENT_BACKEND_GL] + [CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG], + view->backendDrawnPasses[CLASSIC_WORLD_AMBIENT_BACKEND_GL] + [CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG], + view->backendNoopPasses[CLASSIC_WORLD_AMBIENT_BACKEND_GL], + view->backendOutcome[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN], + ClassicWorldAmbientDomainFailure_Name( + view->backendFailure[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN] ), + view->backendFailureDetail[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN], + view->backendDrawnPasses[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN] + [CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG], + view->backendDrawnPasses[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN] + [CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG], + view->backendNoopPasses[CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN] ); + } + } + { + const classicInteractionDomainStats_t &interaction = + R_ClassicInteractionDomain_Stats(); + common->Printf( + "Renderer shared interaction: requested=%d prepared=%d valid=%d views=%d ready=%d fallback=%d lights=%d surfaces=%d primitives=%d draw=%d noop=%d shadow=%d/%d/%d+%d volumes=%d+%d maps=%d+%d modes=%d+%d csm=%d stages=%d/%d+%d/%d receivers=%d/%d/%d deform=%d/%d/%d hash=%016llx status=%s GL=%d/%d/%d VK=%d/%d/%d\n", + r_rendererSharedWorldInteraction.GetBool() ? 1 : 0, + interaction.prepared ? 1 : 0, + interaction.frameValid ? 1 : 0, + interaction.interactionViews, + interaction.readyViews, + interaction.fallbackViews, + interaction.lights, + interaction.surfaces, + interaction.primitives, + interaction.drawablePrimitives, + interaction.noopPrimitives, + interaction.shadowLights, + interaction.shadowCasters, + interaction.drawableShadowCasters, + interaction.noopShadowCasters, + interaction.logicalVolumeDraws, + interaction.preloadVolumeDraws, + interaction.shadowMapPasses, + interaction.hybridShadowPasses, + interaction.projectedShadowMapPasses, + interaction.pointShadowMapPasses, + interaction.csmShadowMapPasses, + interaction.activeLightStages, + interaction.inactiveLightStages, + interaction.activeSurfaceStages, + interaction.inactiveSurfaceStages, + interaction.receiverSurfaces[CLASSIC_INTERACTION_RECEIVER_LOCAL], + interaction.receiverSurfaces[CLASSIC_INTERACTION_RECEIVER_GLOBAL], + interaction.receiverSurfaces[CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT], + interaction.materialDeformReceivers, + interaction.completedDeformCasters, + interaction.emptyDeformCasters, + static_cast( interaction.hash ), + interaction.status, + interaction.backend[CLASSIC_INTERACTION_BACKEND_GL].ownedViews, + interaction.backend[CLASSIC_INTERACTION_BACKEND_GL].fallbackViews, + interaction.backend[CLASSIC_INTERACTION_BACKEND_GL].coverageMismatches, + interaction.backend[CLASSIC_INTERACTION_BACKEND_VULKAN].ownedViews, + interaction.backend[CLASSIC_INTERACTION_BACKEND_VULKAN].fallbackViews, + interaction.backend[CLASSIC_INTERACTION_BACKEND_VULKAN].coverageMismatches ); + for ( int viewIndex = 0; + viewIndex < R_ClassicInteractionDomain_NumViews(); ++viewIndex ) { + const classicInteractionDomainView_t *view = + R_ClassicInteractionDomain_ViewByIndex( viewIndex ); + if ( view == NULL ) { + continue; + } + common->Printf( + "Renderer shared interaction view[%d]: scene=%d pass=%d shadowPass=%d/%d mode=%s ready=%d failure=%s detail=%d drawPacket=%d light=%d receiver=%d stage=%d lights=%d surfaces=%d primitives=%d/%d noop=%d shadow=%d/%d/%d+%d volumes=%d+%d maps=%d+%d modes=%d+%d csm=%d hash=%016llx GL=%d/%s/%d/%d+%d/%d+%d+%d+%d/%d+%d VK=%d/%s/%d/%d+%d/%d+%d+%d+%d/%d+%d\n", + viewIndex, + view->scenePacketIndex, + view->interactionPassPacketIndex, + view->stencilShadowPassPacketIndex, + view->shadowMapPassPacketIndex, + ClassicInteractionDomainShadowMode_Name( view->shadowMode ), + view->ready ? 1 : 0, + ClassicInteractionDomainFailure_Name( view->failure ), + view->failureDetail, + view->failureDrawPacketIndex, + view->failureLightOrdinal, + view->failureReceiverOrdinal, + view->failureStageIndex, + view->lightCount, + view->surfaceCount, + view->primitiveCount, + view->drawablePrimitiveCount, + view->noopPrimitiveCount, + view->shadowLightCount, + view->shadowCasterCount, + view->drawableShadowCasterCount, + view->noopShadowCasterCount, + view->logicalVolumeDrawCount, + view->preloadVolumeDrawCount, + view->shadowMapPassCount, + view->hybridShadowPassCount, + view->projectedShadowMapPassCount, + view->pointShadowMapPassCount, + view->csmShadowMapPassCount, + static_cast( view->hash ), + view->backendOutcome[CLASSIC_INTERACTION_BACKEND_GL], + ClassicInteractionDomainFailure_Name( + view->backendFailure[CLASSIC_INTERACTION_BACKEND_GL] ), + view->backendFailureDetail[CLASSIC_INTERACTION_BACKEND_GL], + view->backendDrawnPrimitives[CLASSIC_INTERACTION_BACKEND_GL], + view->backendNoopPrimitives[CLASSIC_INTERACTION_BACKEND_GL], + view->backendShadowCasters[CLASSIC_INTERACTION_BACKEND_GL], + view->backendNoopShadowCasters[CLASSIC_INTERACTION_BACKEND_GL], + view->backendLogicalVolumeDraws[CLASSIC_INTERACTION_BACKEND_GL], + view->backendPreloadVolumeDraws[CLASSIC_INTERACTION_BACKEND_GL], + view->backendShadowMapPasses[CLASSIC_INTERACTION_BACKEND_GL], + view->backendHybridPasses[CLASSIC_INTERACTION_BACKEND_GL], + view->backendOutcome[CLASSIC_INTERACTION_BACKEND_VULKAN], + ClassicInteractionDomainFailure_Name( + view->backendFailure[CLASSIC_INTERACTION_BACKEND_VULKAN] ), + view->backendFailureDetail[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendDrawnPrimitives[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendNoopPrimitives[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendShadowCasters[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendNoopShadowCasters[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendLogicalVolumeDraws[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendPreloadVolumeDraws[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendShadowMapPasses[CLASSIC_INTERACTION_BACKEND_VULKAN], + view->backendHybridPasses[CLASSIC_INTERACTION_BACKEND_VULKAN] ); + for ( int lightIndex = 0; lightIndex < view->lightCount; + ++lightIndex ) { + const classicInteractionDomainLight_t *light = + R_ClassicInteractionDomain_ViewLight( *view, lightIndex ); + if ( light == NULL ) { + continue; + } + for ( int receiverIndex = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiverIndex <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; + ++receiverIndex ) { + const classicInteractionDomainReceiver_t receiver = + static_cast( + receiverIndex ); + const classicInteractionDomainShadowMapPass_t *mapPass = + R_ClassicInteractionDomain_LightShadowMapPass( + *light, receiver ); + if ( mapPass == NULL ) { + continue; + } + common->Printf( + "Renderer shared interaction map view[%d] light=%d receiver=%s mode=%s class=%s cascades=%d alias=%d plan=%016llx generation=%u casters=%d+%d features=%d+%d+%d+%d hash=%016llx\n", + viewIndex, lightIndex, + ClassicInteractionDomainReceiver_Name( receiver ), + ClassicInteractionDomainShadowMode_Name( + mapPass->mode ), + R_ShadowMapLightClassName( mapPass->lightClass ), + mapPass->lightClass == SHADOWMAP_LIGHT_POINT + ? 6 : mapPass->projected.state.cascadeCount, + mapPass->resourceAlias ? 1 : 0, + static_cast( + mapPass->resourcePlanId ), + mapPass->resourceGeneration, + mapPass->mappedCasterCount, + mapPass->supplementCasterCount, + mapPass->hasStaticCasters ? 1 : 0, + mapPass->hasDynamicCasters ? 1 : 0, + mapPass->hasAlphaCasters ? 1 : 0, + mapPass->hasTranslucentCasters ? 1 : 0, + static_cast( mapPass->hash ) ); + } + } + } + } + { + const classicFogBlendDomainStats_t &fogBlend = + R_ClassicFogBlendDomain_Stats(); + common->Printf( + "classicFogBlendDomain requested=%d prepared=%d frameValid=%d overflow=%d status=%s views=%d ready=%d fallback=%d lights=%d fog=%d blend=%d noopLights=%d surfaces=%d global=%d local=%d deformReceivers=%d stages=%d active=%d inactive=%d noopStages=%d primitives=%d fogReceivers=%d fogCaps=%d blendDraws=%d noop=%d textures=%d hash=%016llx\n", + r_rendererSharedWorldFogBlend.GetBool() ? 1 : 0, + fogBlend.prepared ? 1 : 0, + fogBlend.frameValid ? 1 : 0, + fogBlend.overflow ? 1 : 0, + fogBlend.status, + fogBlend.fogBlendViews, + fogBlend.readyViews, + fogBlend.fallbackViews, + fogBlend.lights, + fogBlend.fogLights, + fogBlend.blendLights, + fogBlend.noopLights, + fogBlend.surfaces, + fogBlend.receiverSurfaces[ + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL ], + fogBlend.receiverSurfaces[ + CLASSIC_FOG_BLEND_RECEIVER_LOCAL ], + fogBlend.materialDeformReceivers, + fogBlend.lightStages, + fogBlend.activeLightStages, + fogBlend.inactiveLightStages, + fogBlend.noopLightStages, + fogBlend.primitives, + fogBlend.fogReceiverPrimitives, + fogBlend.fogFrustumPrimitives, + fogBlend.blendPrimitives, + fogBlend.noopPrimitives, + fogBlend.textures, + static_cast( fogBlend.hash ) ); + for ( int backendIndex = 0; + backendIndex < CLASSIC_FOG_BLEND_BACKEND_COUNT; + ++backendIndex ) { + const classicFogBlendDomainBackend_t backend = + static_cast( backendIndex ); + const classicFogBlendDomainBackendCoverage_t &coverage = + fogBlend.backend[ backendIndex ]; + common->Printf( + "classicFogBlendDomain backend=%s ownedViews=%d fallbackViews=%d ownedFogReceivers=%d ownedFogCaps=%d ownedBlend=%d ownedNoops=%d ownedNoopStages=%d ownedNoopLights=%d mismatches=%d duplicate=%d untracked=%d\n", + ClassicFogBlendDomainBackend_Name( backend ), + coverage.ownedViews, + coverage.fallbackViews, + coverage.ownedFogReceiverPrimitives, + coverage.ownedFogFrustumPrimitives, + coverage.ownedBlendPrimitives, + coverage.ownedNoopPrimitives, + coverage.ownedNoopLightStages, + coverage.ownedNoopLights, + coverage.coverageMismatches, + coverage.duplicateReports, + coverage.untrackedFallbacks ); + } + for ( int viewIndex = 0; + viewIndex < R_ClassicFogBlendDomain_NumViews(); ++viewIndex ) { + const classicFogBlendDomainView_t *view = + R_ClassicFogBlendDomain_ViewByIndex( viewIndex ); + if ( view == NULL ) { + continue; + } + common->Printf( + "classicFogBlendDomain view[%d] scene=%d pass=%d ready=%d failure=%s detail=%d drawPacket=%d light=%d receiver=%d stage=%d lights=%d/%d+%d surfaces=%d stages=%d/%d+%d primitives=%d drawable=%d fogReceivers=%d fogCaps=%d blendDraws=%d noop=%d noopStages=%d noopLights=%d hash=%016llx GL=%d/%s/%d Vulkan=%d/%s/%d\n", + viewIndex, + view->scenePacketIndex, + view->fogBlendPassPacketIndex, + view->ready ? 1 : 0, + ClassicFogBlendDomainFailure_Name( view->failure ), + view->failureDetail, + view->failureDrawPacketIndex, + view->failureLightOrdinal, + view->failureReceiverOrdinal, + view->failureStageIndex, + view->lightCount, + view->fogLightCount, + view->blendLightCount, + view->surfaceCount, + view->lightStageCount, + view->activeLightStageCount, + view->inactiveLightStageCount, + view->primitiveCount, + view->drawablePrimitiveCount, + view->fogReceiverPrimitiveCount, + view->fogFrustumPrimitiveCount, + view->blendPrimitiveCount, + view->noopPrimitiveCount, + view->noopLightStageCount, + view->noopLightCount, + static_cast( view->hash ), + view->backendOutcome[ CLASSIC_FOG_BLEND_BACKEND_GL ], + ClassicFogBlendDomainFailure_Name( + view->backendFailure[ CLASSIC_FOG_BLEND_BACKEND_GL ] ), + view->backendFailureDetail[ CLASSIC_FOG_BLEND_BACKEND_GL ], + view->backendOutcome[ CLASSIC_FOG_BLEND_BACKEND_VULKAN ], + ClassicFogBlendDomainFailure_Name( + view->backendFailure[ + CLASSIC_FOG_BLEND_BACKEND_VULKAN ] ), + view->backendFailureDetail[ + CLASSIC_FOG_BLEND_BACKEND_VULKAN ] ); + } + } + { + const classicSubviewDomainStats_t &subview = + R_ClassicSubviewDomain_Stats(); + common->Printf( + "classicSubviewDomain requested=%d prepared=%d frameValid=%d overflow=%d status=%s scenes=%d subviews=%d captures=%d ready=%d fallback=%d nested=%d nestedTransactions=%d maxNestingDepth=%d directMirror=%d remote=%d mirror=%d reflection=%d refraction=%d xray=%d colorCubemap=%d depth2D=%d depthCubemap=%d hash=%016llx\n", + r_rendererSharedSubview.GetBool() ? 1 : 0, + subview.prepared ? 1 : 0, + subview.frameValid ? 1 : 0, + subview.overflow ? 1 : 0, + subview.status, + subview.sourceScenes, + subview.subviewScenes, + subview.capturePackets, + subview.readyViews, + subview.fallbackViews, + subview.nestedViews, + subview.nestedTransactions, + subview.maxNestingDepth, + subview.directMirrorViews, + subview.remoteCameraViews, + subview.mirrorViews, + subview.reflectionViews, + subview.refractionViews, + subview.xrayViews, + subview.colorCubemapCaptures, + subview.depth2DCaptures, + subview.depthCubemapCaptures, + static_cast( subview.hash ) ); + for ( int backendIndex = 0; + backendIndex < CLASSIC_SUBVIEW_DOMAIN_BACKEND_COUNT; + ++backendIndex ) { + const classicSubviewDomainBackend_t backend = + static_cast( backendIndex ); + const classicSubviewDomainBackendCoverage_t &coverage = + subview.backend[ backendIndex ]; + common->Printf( + "classicSubviewDomain backend=%s ownedViews=%d fallbackViews=%d nestedOwned=%d nestedTransactions=%d nestedFallback=%d nestedFallbackTransactions=%d directMirror=%d remote=%d mirror=%d reflection=%d refraction=%d xray=%d colorCubemap=%d depth2D=%d depthCubemap=%d mismatches=%d duplicate=%d untracked=%d\n", + ClassicSubviewDomainBackend_Name( backend ), + coverage.ownedViews, + coverage.fallbackViews, + coverage.ownedNestedViews, + coverage.ownedNestedTransactions, + coverage.fallbackNestedViews, + coverage.fallbackNestedTransactions, + coverage.ownedDirectMirrorViews, + coverage.ownedRemoteCameraViews, + coverage.ownedMirrorViews, + coverage.ownedReflectionViews, + coverage.ownedRefractionViews, + coverage.ownedXrayViews, + coverage.ownedColorCubemapCaptures, + coverage.ownedDepth2DCaptures, + coverage.ownedDepthCubemapCaptures, + coverage.coverageMismatches, + coverage.duplicateReports, + coverage.untrackedFallbacks ); + } + for ( int viewIndex = 0; + viewIndex < R_ClassicSubviewDomain_NumViews(); ++viewIndex ) { + const classicSubviewDomainView_t *view = + R_ClassicSubviewDomain_ViewByIndex( viewIndex ); + if ( view == NULL ) { + continue; + } + common->Printf( + "classicSubviewDomain view[%d] scene=%d parent=%d parentView=%d root=%d depth=%d subtree=%d capture=%d kind=%s captureType=%s ready=%d failure=%s detail=%d captureImage=%s rect=%d,%d %dx%d cube=%d depth=%d hash=%016llx GL=%d/%s/%d Vulkan=%d/%s/%d\n", + viewIndex, + view->scenePacketIndex, + view->parentScenePacketIndex, + view->parentViewIndex, + view->rootViewIndex, + view->nestingDepth, + view->subtreeViewCount, + view->capturePacketIndex, + ClassicSubviewDomainKind_Name( view->kind ), + ClassicSubviewDomainCaptureType_Name( view->captureType ), + view->ready ? 1 : 0, + ClassicSubviewDomainFailure_Name( view->failure ), + view->failureDetail, + view->captureImage != NULL ? view->captureImage->GetName() : "", + view->captureX, + view->captureY, + view->captureWidth, + view->captureHeight, + view->captureCubeFace, + view->captureCopyDepth ? 1 : 0, + static_cast( view->hash ), + view->backendOutcome[ CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ], + ClassicSubviewDomainFailure_Name( + view->backendFailure[ CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ] ), + view->backendFailureDetail[ CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ], + view->backendOutcome[ CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ], + ClassicSubviewDomainFailure_Name( + view->backendFailure[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ] ), + view->backendFailureDetail[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ] ); + } + } + common->Printf( + "Modern quality: master=%d pbr=%d probes=%d clusteredDecals=%d froxel=%d ssr=%d ssgi=%d (master=0 is complete Milestone-F rollback)\n" + "PBR materials: parser=1 modernLighting=1 effective=%d generatedLegacyFallback=%d inferLegacy=%d debug=%d\n", + r_rendererModernQuality.GetBool() ? 1 : 0, + r_pbrMaterials.GetBool() ? 1 : 0, + r_rendererReflectionProbes.GetBool() ? 1 : 0, + r_rendererClusteredDecals.GetBool() ? 1 : 0, + r_rendererFroxelVolumetrics.GetBool() ? 1 : 0, + r_rendererSSR.GetBool() ? 1 : 0, + r_rendererSSGI.GetBool() ? 1 : 0, + ( r_rendererModernQuality.GetBool() && r_pbrMaterials.GetBool() ) ? 1 : 0, + r_pbrGeneratedLegacyFallback.GetBool() ? 1 : 0, + r_pbrInferFromLegacyMaterials.GetBool() ? 1 : 0, + r_pbrDebug.GetInteger() ); R_ModernGLExecutor_PrintGfxInfo(); R_ModernClusteredLighting_PrintGfxInfo(); R_ModernShadowPlanner_PrintGfxInfo(); @@ -3825,9 +4541,17 @@ static void R_PerformFullVidRestart( bool forceWindow ) { globalImages->PurgeAllImages(); R_ShutdownFrameData(); R_RendererMetrics_ShutdownGpuTimers(); + R_ClassicGuiDomain_ResetFrame(); + R_ClassicCinematicPostDomain_ResetFrame(); + R_ClassicSpecialFrameDomain_ResetFrame(); + R_ClassicWorldAmbientDomain_ResetFrame(); + R_ClassicInteractionDomain_ResetFrame(); + R_ClassicFogBlendDomain_ResetFrame(); + R_ClassicSubviewDomain_ResetFrame(); R_MaterialResourceTable_Shutdown(); R_RenderGraphResources_Shutdown(); R_ModernGLExecutor_Shutdown(); + R_GpuSkinning_ContractShutdown(); R_RendererUpload_Shutdown(); RendererBootstrap_Shutdown(); R_PurgeFramebufferCopyFBOs(); @@ -3866,6 +4590,7 @@ void R_VidRestart_f( const idCmdArgs &args ) { if ( !glConfig.isInitialized ) { return; } + R_RendererMetrics_ResetGpuFrameTiming( "vid_restart" ); bool full = true; bool forceWindow = false; @@ -4046,6 +4771,41 @@ void R_InitCvars( void ) { // update latched cvars here } +/* +================== +R_MigrateLegacyShadowMapContactQuality + +Renderer cvars are registered by Init before the archived config and autoexec +files run. Perform value-based migrations at device startup after those files +and the initial StartupVariable pass, or an existing profile would be marked +migrated while the compiled defaults were still visible. Common's later +AddStartupCommands replay intentionally leaves explicit command-line +set +values authoritative over the migrated archive. +================== +*/ +static void R_MigrateLegacyShadowMapContactQuality( void ) { + if ( !r_shadowMapContactQualityMigrated.GetBool() ) { + // Preserve customized profiles. Only replace the complete legacy tuple, + // whose wide filters and caster slope offset blurred contact shadows and + // amplified the old far-shell detachment. + const bool legacyShadowQualityDefaults = + idMath::Fabs( r_shadowMapFilterRadius.GetFloat() - 2.0f ) < 0.0001f && + idMath::Fabs( r_shadowMapPointFilterRadius.GetFloat() - 2.5f ) < 0.0001f && + r_shadowMapFilterTaps.GetInteger() == 13 && + r_shadowMapPointFilterTaps.GetInteger() == 13 && + idMath::Fabs( r_shadowMapPolygonFactor.GetFloat() - 0.75f ) < 0.0001f; + if ( legacyShadowQualityDefaults ) { + common->Printf( "Migrating legacy shadow-map defaults: restoring contact detail and balanced filtering\n" ); + r_shadowMapFilterRadius.SetFloat( 0.75f ); + r_shadowMapPointFilterRadius.SetFloat( 1.0f ); + r_shadowMapFilterTaps.SetInteger( 9 ); + r_shadowMapPointFilterTaps.SetInteger( 9 ); + r_shadowMapPolygonFactor.SetFloat( 0.25f ); + } + r_shadowMapContactQualityMigrated.SetBool( true ); + } +} + /* ================= R_InitCommands @@ -4073,12 +4833,24 @@ void R_InitCommands( void ) { cmdSystem->AddCommand( "rendererDefaultSafetySelfTest", R_RendererDefaultSafetySelfTest_f, CMD_FL_RENDERER, "run renderer conservative-default safety self tests" ); cmdSystem->AddCommand( "rendererBenchmarkCapture", R_RendererBenchmarkCapture_f, CMD_FL_RENDERER, "print the latest renderer benchmark capture summary" ); cmdSystem->AddCommand( "rendererUploadSelfTest", R_RendererUploadSelfTest_f, CMD_FL_RENDERER, "run renderer upload stream self tests" ); + cmdSystem->AddCommand( "rendererGpuSkinningSelfTest", R_RendererGpuSkinningSelfTest_f, CMD_FL_RENDERER, "run renderer GPU skinning contract self tests" ); + cmdSystem->AddCommand( "rendererContractsSelfTest", R_RendererContractsSelfTest_f, CMD_FL_RENDERER, "run renderer layout and buffer contract self tests" ); cmdSystem->AddCommand( "rendererGpuTimerSelfTest", R_RendererGpuTimerSelfTest_f, CMD_FL_RENDERER, "run renderer GPU timer query self tests" ); + cmdSystem->AddCommand( "rendererTemporalPresentationStatus", R_TemporalPresentation_PrintStatus_f, CMD_FL_RENDERER, "report dynamic-resolution and temporal-history state" ); cmdSystem->AddCommand( "rendererScenePacketSelfTest", R_RendererScenePacketSelfTest_f, CMD_FL_RENDERER, "run renderer front-end scene-packet self tests" ); cmdSystem->AddCommand( "rendererRenderGraphSelfTest", R_RendererRenderGraphSelfTest_f, CMD_FL_RENDERER, "run renderer resource-graph self tests" ); cmdSystem->AddCommand( "rendererRenderGraphResourceSelfTest", R_RendererRenderGraphResourceSelfTest_f, CMD_FL_RENDERER, "run renderer graph resource owner self tests" ); cmdSystem->AddCommand( "rendererRenderGraphResourceDump", R_RendererRenderGraphResourceDump_f, CMD_FL_RENDERER, "dump the latest renderer graph resource handles" ); cmdSystem->AddCommand( "rendererMaterialResourceTableSelfTest", R_RendererMaterialResourceTableSelfTest_f, CMD_FL_RENDERER, "run renderer material resource-table self tests" ); + cmdSystem->AddCommand( "rendererClassicGuiDomainSelfTest", R_RendererClassicGuiDomainSelfTest_f, CMD_FL_RENDERER, "run backend-neutral classic GUI whole-view contract self tests" ); + cmdSystem->AddCommand( "rendererClassicCinematicPostDomainSelfTest", R_RendererClassicCinematicPostDomainSelfTest_f, CMD_FL_RENDERER, "run sealed classic cinematic and authored post transaction self tests" ); + cmdSystem->AddCommand( "rendererClassicSpecialFrameDomainSelfTest", R_RendererClassicSpecialFrameDomainSelfTest_f, CMD_FL_RENDERER, "run sealed render-demo and Raven special-frame transaction self tests" ); + cmdSystem->AddCommand( "rendererClassicWorldAmbientDomainSelfTest", R_RendererClassicWorldAmbientDomainSelfTest_f, CMD_FL_RENDERER, "run backend-neutral classic world ambient whole-view contract self tests" ); + cmdSystem->AddCommand( "rendererClassicInteractionDomainSelfTest", R_RendererClassicInteractionDomainSelfTest_f, CMD_FL_RENDERER, "run backend-neutral classic interaction whole-view contract self tests" ); + cmdSystem->AddCommand( "rendererClassicFogBlendDomainSelfTest", R_RendererClassicFogBlendDomainSelfTest_f, CMD_FL_RENDERER, "run backend-neutral classic fog/blend whole-view contract self tests" ); + cmdSystem->AddCommand( "rendererClassicSubviewDomainSelfTest", R_RendererClassicSubviewDomainSelfTest_f, CMD_FL_RENDERER, "run backend-neutral capture-backed classic subview transaction self tests" ); + cmdSystem->AddCommand( "rendererClassicDeformDomainSelfTest", R_RendererClassicDeformDomainSelfTest_f, CMD_FL_RENDERER, "run sealed classic material-deform contract self tests" ); + cmdSystem->AddCommand( "rendererPBRMaterialSelfTest", R_RendererPBRMaterialSelfTest_f, CMD_FL_RENDERER, "run PBR material parser and classic fallback self tests" ); cmdSystem->AddCommand( "rendererMaterialResourceTableDump", R_RendererMaterialResourceTableDump_f, CMD_FL_RENDERER, "dump the latest renderer material resource table" ); cmdSystem->AddCommand( "rendererGeometryResourceSelfTest", R_RendererGeometryResourceSelfTest_f, CMD_FL_RENDERER, "run renderer geometry and instance packet self tests" ); cmdSystem->AddCommand( "rendererGLStateCacheSelfTest", R_RendererGLStateCacheSelfTest_f, CMD_FL_RENDERER, "run renderer GL state-cache self tests" ); @@ -4086,6 +4858,7 @@ void R_InitCommands( void ) { cmdSystem->AddCommand( "rendererGpuDrivenSelfTest", R_RendererGpuDrivenSelfTest_f, CMD_FL_RENDERER, "run renderer GL43 GPU-driven compute and indirect self tests" ); cmdSystem->AddCommand( "rendererVisiblePathSelfTest", R_RendererVisiblePathSelfTest_f, CMD_FL_RENDERER, "run renderer visible modern depth-path self tests" ); cmdSystem->AddCommand( "rendererGBufferSelfTest", R_RendererGBufferSelfTest_f, CMD_FL_RENDERER, "run renderer modern opaque G-buffer self tests" ); + cmdSystem->AddCommand( "rendererPBRVisibleSelfTest", R_RendererPBRVisibleSelfTest_f, CMD_FL_RENDERER, "run guarded modern PBR G-buffer/deferred/forward self tests" ); cmdSystem->AddCommand( "rendererClusterGridSelfTest", R_RendererClusterGridSelfTest_f, CMD_FL_RENDERER, "run renderer clustered light-grid self tests" ); cmdSystem->AddCommand( "rendererLightImageAtlasSelfTest", R_RendererLightImageAtlasSelfTest_f, CMD_FL_RENDERER, "run renderer modern light falloff/projection atlas self tests" ); cmdSystem->AddCommand( "rendererShadowPlannerSelfTest", R_RendererShadowPlannerSelfTest_f, CMD_FL_RENDERER, "run renderer modern shadow-planner self tests" ); @@ -4193,6 +4966,7 @@ idRenderSystemLocal::Init void idRenderSystemLocal::Init( void ) { common->Printf( "------- Initializing renderSystem --------\n" ); + R_RendererMetrics_ResetGpuFrameTiming( "renderer init" ); // route imagetools static-allocation counters into tr.pc before any // image or model loading can call R_StaticAlloc @@ -4259,6 +5033,7 @@ idRenderSystemLocal::Shutdown */ void idRenderSystemLocal::Shutdown( void ) { common->Printf( "idRenderSystem::Shutdown()\n" ); + R_RendererMetrics_ResetGpuFrameTiming( "renderer shutdown" ); R_DoneFreeType( ); @@ -4312,6 +5087,7 @@ idRenderSystemLocal::BeginLevelLoad ======================== */ void idRenderSystemLocal::BeginLevelLoad( void ) { + R_RendererMetrics_ResetGpuFrameTiming( "begin level load" ); // SetUnderwaterView is the only writer of this state, and it is driven by the game // each frame. A map change started while the player was submerged would otherwise // leave the effect live across the whole loading screen and into the first frames @@ -4349,6 +5125,10 @@ void idRenderSystemLocal::EndLevelLoad( void ) { if ( r_forceLoadImages.GetBool() ) { RB_ShowImages(); } + + // Loading-screen work belongs to the old timing epoch. Start gameplay with + // an empty delayed ring so budget capture cannot mix the two workloads. + R_RendererMetrics_ResetGpuFrameTiming( "end level load" ); } /* @@ -4357,6 +5137,8 @@ idRenderSystemLocal::InitOpenGL ======================== */ void idRenderSystemLocal::InitOpenGL( void ) { + R_MigrateLegacyShadowMapContactQuality(); + // if the device isn't started, start it now if ( !glConfig.isInitialized ) { #ifdef OPENQ4_RENDERER_VK_MODULE @@ -4366,6 +5148,15 @@ void idRenderSystemLocal::InitOpenGL( void ) { if ( !VK_InitRenderDevice() ) { common->FatalError( "Vulkan renderer device initialization failed" ); } + // The Vulkan module does not run the OpenGL tier/bootstrap tail that + // normally initializes shared scene resources. The classic-GUI domain + // only needs the backend-neutral scene-packet/material contract, so make + // that one conservative feature explicit without advertising any GL + // modern-visible capability. + renderFeatureSet_t vkSharedFeatures = glConfig.renderFeatures; + vkSharedFeatures.scenePackets = true; + R_MaterialResourceTable_Init( glConfig.backendCaps, vkSharedFeatures ); + R_GpuSkinning_ContractInit( glConfig.backendCaps ); // mirror the R_InitOpenGL tail the seam skips: the front-end needs // the vertex cache, a resolved back end, frame data, and gamma // tables before the first BeginFrame @@ -4411,9 +5202,18 @@ void idRenderSystemLocal::ShutdownOpenGL( void ) { // free the context and close the window R_ShutdownFrameData(); + R_GpuSkinning_ContractShutdown(); + R_ClassicGuiDomain_ResetFrame(); + R_ClassicCinematicPostDomain_ResetFrame(); + R_ClassicSpecialFrameDomain_ResetFrame(); + R_ClassicWorldAmbientDomain_ResetFrame(); + R_ClassicInteractionDomain_ResetFrame(); + R_ClassicFogBlendDomain_ResetFrame(); + R_ClassicSubviewDomain_ResetFrame(); #ifdef OPENQ4_RENDERER_VK_MODULE // Vulkan backend seam (Phase C): the GL executor/upload/graph/debug // subsystems never initialized; device + window teardown is GLimp_Shutdown + R_MaterialResourceTable_Shutdown(); GLimp_Shutdown(); #else R_RendererMetrics_ShutdownGpuTimers(); diff --git a/src/renderer/RenderWorld.cpp b/src/renderer/RenderWorld.cpp index b5ad712a..d4124413 100644 --- a/src/renderer/RenderWorld.cpp +++ b/src/renderer/RenderWorld.cpp @@ -391,6 +391,12 @@ void idRenderWorldLocal::UpdateEntityDef( qhandle_t entityHandle, const renderEn while ( entityHandle >= entityDefs.Num() ) { entityDefs.Append( NULL ); } + // Growing the def list past the interaction table dimension would make the + // ldef*width+edef index used to address interactionTable run out of bounds; + // dump and rebuild the table exactly as AddEntityDef does on growth. + if ( interactionTable && entityDefs.Num() > interactionTableWidth ) { + ResizeInteractionTable(); + } // Track before the early-out paths below. Those return only when nothing that // matters changed, which includes the flag, so tracking here is correct for @@ -460,6 +466,22 @@ void idRenderWorldLocal::UpdateEntityDef( qhandle_t entityHandle, const renderEn } if ( keepDynamicModel ) { tr.pc.c_entitySnapshotsReused++; + + // A bytewise-identical update whose joint contents also hashed equal + // derives nothing new: interactions, area references, and the model + // matrix would rebuild to exactly what they already are, so keep them. + // lastModifiedFrameNum must still advance: R_ShadowMapCasterIsDynamic + // has one frame of hysteresis, so letting the counter stall would + // oscillate tick-rate-animated entities between the static and dynamic + // caster sets at render rates above the game tick and churn the cached + // static shadow tiles through the caster signature hash. + if ( re->callbackData == NULL + && !session->writeDemo + && memcmp( re, &def->parms, sizeof( *re ) ) == 0 ) { + tr.pc.c_entityUpdatesElided++; + def->lastModifiedFrameNum = tr.frameCount; + return; + } } } @@ -542,6 +564,10 @@ void idRenderWorldLocal::FreeEntityDef( qhandle_t entityHandle ) { def->demoRemoteRenderView = NULL; } + // a later idRenderEntityLocal allocation recycling this address must not + // revive this def's memoized draw-surf areas + R_InvalidateDrawSurfAreaMemoForEntity( def ); + delete def; entityDefs[ entityHandle ] = NULL; } @@ -621,6 +647,11 @@ void idRenderWorldLocal::UpdateLightDef( qhandle_t lightHandle, const renderLigh while ( lightHandle >= lightDefs.Num() ) { lightDefs.Append( NULL ); } + // Keep the interaction table sized to the light-def list, as AddLightDef + // does: the ldef*width+edef index would otherwise address out of bounds. + if ( interactionTable && lightDefs.Num() > interactionTableHeight ) { + ResizeInteractionTable(); + } bool justUpdate = false; idRenderLightLocal *light = lightDefs[lightHandle]; @@ -1075,8 +1106,8 @@ exitPortal_t idRenderWorldLocal::GetPortal( int areaNum, int portalNum ) { portal_t *portal; exitPortal_t ret; - if ( areaNum > numPortalAreas ) { - common->Error( "idRenderWorld::GetPortal: areaNum > numAreas" ); + if ( areaNum < 0 || areaNum >= numPortalAreas ) { + common->Error( "idRenderWorld::GetPortal: areaNum out of range" ); } area = &portalAreas[areaNum]; @@ -1525,6 +1556,12 @@ bool idRenderWorldLocal::ModelTrace( modelTrace_t &trace, qhandle_t entityHandle shader = R_RemapShaderBySkin( surf->shader, def->parms.customSkin, def->parms.customShader ); + // R_RemapShaderBySkin returns NULL for a deform surface under a + // customShader (and for a NULL surface shader); such a surface is never + // a collision surface, so skip it as the second loop already does. + if ( !shader ) { + continue; + } if ( shader->GetSurfaceFlags() & SURF_COLLISION ) { collisionSurface = true; break; @@ -1637,13 +1674,11 @@ bool idRenderWorldLocal::Trace( modelTrace_t &trace, const idVec3 &start, const #if 1 /* _D3XP addition. could use a cleaner approach */ if ( skipPlayer ) { - idStr name = model->Name(); - const char *exclude; + const char *name = model->Name(); int k; for ( k = 0; playerModelExcludeList[k]; k++ ) { - exclude = playerModelExcludeList[k]; - if ( name == exclude ) { + if ( idStr::Cmp( name, playerModelExcludeList[k] ) == 0 ) { break; } } @@ -1667,6 +1702,13 @@ bool idRenderWorldLocal::Trace( modelTrace_t &trace, const idVec3 &start, const continue; } + // The model matrix and the local-space trace endpoints depend only on + // this entity's (post-callback) origin/axis, so build them once per + // model rather than once per surface. + R_AxisToModelMatrix( def->parms.axis, def->parms.origin, modelMatrix ); + R_GlobalPointToLocal( modelMatrix, start, localStart ); + R_GlobalPointToLocal( modelMatrix, end, localEnd ); + // check all model surfaces const int surfaceCount = model->NumSurfaces(); for ( j = 0; j < surfaceCount; j++ ) { @@ -1681,13 +1723,11 @@ bool idRenderWorldLocal::Trace( modelTrace_t &trace, const idVec3 &start, const #if 1 /* _D3XP addition. could use a cleaner approach */ if ( skipPlayer ) { - idStr name = shader->GetName(); - const char *exclude; + const char *name = shader->GetName(); int k; for ( k = 0; playerMaterialExcludeList[k]; k++ ) { - exclude = playerMaterialExcludeList[k]; - if ( name == exclude ) { + if ( idStr::Cmp( name, playerMaterialExcludeList[k] ) == 0 ) { break; } } @@ -1709,11 +1749,7 @@ bool idRenderWorldLocal::Trace( modelTrace_t &trace, const idVec3 &start, const numSurfaces++; - // transform the points into local space - R_AxisToModelMatrix( def->parms.axis, def->parms.origin, modelMatrix ); - R_GlobalPointToLocal( modelMatrix, start, localStart ); - R_GlobalPointToLocal( modelMatrix, end, localEnd ); - + // modelMatrix / localStart / localEnd are computed once per model above localTrace = R_LocalTrace( localStart, localEnd, radius, surf->geometry ); if ( localTrace.fraction < trace.fraction ) { diff --git a/src/renderer/RenderWorld_lightgrid.cpp b/src/renderer/RenderWorld_lightgrid.cpp index c91a9151..975e7084 100644 --- a/src/renderer/RenderWorld_lightgrid.cpp +++ b/src/renderer/RenderWorld_lightgrid.cpp @@ -2738,18 +2738,13 @@ void idRenderWorldLocal::SetupLightGrid() { return; } - int totalValidGridPoints = 0; - for ( int i = 0; i < numPortalAreas; i++ ) { - portalAreas[i].lightGrid.SetupGrid( portalAreas[i].globalBounds, this, LIGHTGRID_DEFAULT_SIZE, i, numPortalAreas, -1, false ); - totalValidGridPoints += portalAreas[i].lightGrid.CountValidGridPoints(); - } - - if ( totalValidGridPoints > 0 ) { - common->DPrintf( "No lightgrid assets found for %s. Generated a runtime probe layout with %i valid points for baking/debugging.\n", mapName.c_str(), totalValidGridPoints ); - } - + // Stock Quake 4 maps do not contain openQ4 light-grid assets. Building a + // probe layout here performs thousands of collision/PVS tests even though + // the runtime cannot consume the layout without baked atlas images. Bake + // entry points build the requested layout explicitly, so leave stock maps + // empty and pay this cost only when the developer actually starts a bake. lightGridAvailabilityFrame = -1; - common->DPrintf( "LightGrid setup for %s generated bake/debug layout in %.3fs\n", mapName.c_str(), ( Sys_Milliseconds() - setupStart ) * 0.001f ); + common->DPrintf( "LightGrid setup for %s found no baked assets in %.3fs\n", mapName.c_str(), ( Sys_Milliseconds() - setupStart ) * 0.001f ); } /* diff --git a/src/renderer/RenderWorld_load.cpp b/src/renderer/RenderWorld_load.cpp index ac983eb0..6d9574f7 100644 --- a/src/renderer/RenderWorld_load.cpp +++ b/src/renderer/RenderWorld_load.cpp @@ -444,6 +444,930 @@ static void R_RenderWorld_FinalizeLoadedWorld( idRenderWorldLocal &world ) { world.ClearPortalStates(); } +namespace { + // This is intentionally a different format from BFG's trusted bproc stream. + // The BFG serializer was used only as a field inventory (models, portals and + // nodes); all bounds, validation and transactional publication below are + // openQ4-specific. + static const unsigned int RENDER_WORLD_CACHE_MAGIC = 0x5734514fU; // "OQ4W" + static const int RENDER_WORLD_CACHE_VERSION = 2; + static const unsigned int RENDER_WORLD_CACHE_PARSER_VERSION = 2u; + static const unsigned int RENDER_WORLD_CACHE_FLAGS = 0u; + static const unsigned int RENDER_WORLD_SHADOW_CACHE_MAGIC = 0x4834514fU; // "OQ4H" + static const int RENDER_WORLD_SHADOW_CACHE_VERSION = 1; + static const int RENDER_WORLD_CACHE_MODEL_STATIC = 1; + static const int RENDER_WORLD_CACHE_MODEL_SHADOW = 2; + static const int RENDER_WORLD_CACHE_MAX_MAP_NAME = 4096; + static const int RENDER_WORLD_CACHE_MAX_IMAGE_NAME = 4096; + static const int RENDER_WORLD_CACHE_MAX_MODELS = 65536; + static const int RENDER_WORLD_CACHE_MAX_MODEL_PAYLOAD = 128 * 1024 * 1024; + static const uint64_t RENDER_WORLD_CACHE_MAX_MODEL_MEMORY = 384ULL * 1024ULL * 1024ULL; + static const int RENDER_WORLD_CACHE_MAX_SHADOW_VERTS = 1 << 20; + static const int RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES = 1 << 24; + static const int RENDER_WORLD_CACHE_MAX_AREAS = 4096; + static const int RENDER_WORLD_CACHE_MAX_PORTALS = 16384; + static const int RENDER_WORLD_CACHE_MAX_WINDING_POINTS = 4096; + static const int RENDER_WORLD_CACHE_MAX_TOTAL_WINDING_POINTS = 1 << 20; + static const int RENDER_WORLD_CACHE_MAX_NODES = 1 << 20; + static const int RENDER_WORLD_CACHE_MAX_NODE_DEPTH = 1024; + static const int RENDER_WORLD_CACHE_MAX_PAYLOAD_BYTES = 512 * 1024 * 1024; + static const float RENDER_WORLD_CACHE_MAX_COORDINATE = 1000000000.0f; + static const char *const RENDER_WORLD_CACHE_CLASSIC_SETTINGS = "proc4-static-cpu-v2"; + static const char *const RENDER_WORLD_CACHE_CLASSIC_SEMANTIC = "classic-proc"; + static const char *const RENDER_WORLD_CACHE_MD5R_SEMANTIC = "md5rproc"; + + static unsigned int R_RenderWorldCacheFloatBits( float value ) { + unsigned int bits = 0; + static_assert( sizeof( bits ) == sizeof( value ), "cache setting float width" ); + memcpy( &bits, &value, sizeof( bits ) ); + return bits; + } + + static idStr R_RenderWorldCacheBuildSettingsKey() { + // FinishSurfaces consumes these merge/slop settings, so they are part of + // the cached CPU representation rather than merely runtime preferences. + return va( + "%s-m%d-s%d-v%08x-t%08x-n%08x", + RENDER_WORLD_CACHE_CLASSIC_SETTINGS, + cvarSystem->GetCVarBool( "r_mergeModelSurfaces" ) ? 1 : 0, + cvarSystem->GetCVarBool( "r_useSilRemap" ) ? 1 : 0, + R_RenderWorldCacheFloatBits( cvarSystem->GetCVarFloat( "r_slopVertex" ) ), + R_RenderWorldCacheFloatBits( cvarSystem->GetCVarFloat( "r_slopTexCoord" ) ), + R_RenderWorldCacheFloatBits( cvarSystem->GetCVarFloat( "r_slopNormal" ) ) ); + } + + struct renderWorldCachePortalStage_t { + int areas[2]; + idList points; + idStr imageName; + float cullNear; + float cullFar; + + renderWorldCachePortalStage_t() : cullNear( 0.0f ), cullFar( 0.0f ) { + areas[0] = areas[1] = -1; + } + }; + + class idRenderWorldCacheModelOwner { + public: + ~idRenderWorldCacheModelOwner() { + for ( int i = 0; i < models.Num(); ++i ) { + delete models[i]; + } + } + + void Release() { + models.Clear(); + } + + idList models; + }; + + class idRenderWorldCacheMemoryFileOwner { + public: + idRenderWorldCacheMemoryFileOwner() { + file = fileSystem != NULL ? fileSystem->GetNewFileMemory() : NULL; + } + + ~idRenderWorldCacheMemoryFileOwner() { + if ( file != NULL ) { + fileSystem->CloseFile( file ); + } + } + + idFile *Get() const { + return file; + } + + private: + idRenderWorldCacheMemoryFileOwner( const idRenderWorldCacheMemoryFileOwner & ); + idRenderWorldCacheMemoryFileOwner &operator=( const idRenderWorldCacheMemoryFileOwner & ); + + idFile *file; + }; + + static int R_RenderWorldCacheBoundedStringLength( const char *value, int maximumLength ) { + if ( value == NULL || maximumLength < 0 ) { + return -1; + } + for ( int length = 0; length <= maximumLength; ++length ) { + if ( value[length] == '\0' ) { + return length; + } + } + return -1; + } + + static bool R_RenderWorldCacheStringIsSafe( const char *value, int maximumLength, bool allowEmpty ) { + const int length = R_RenderWorldCacheBoundedStringLength( value, maximumLength ); + if ( length < 0 || ( !allowEmpty && length == 0 ) ) { + return false; + } + for ( int i = 0; i < length; ++i ) { + const unsigned char c = static_cast( value[i] ); + if ( c < 32 || c == 127 ) { + return false; + } + } + return true; + } + + static bool R_RenderWorldCacheCoordinateIsValid( float value ) { + return R_RenderModelCacheFloatIsFinite( value ) + && idMath::Fabs( value ) <= RENDER_WORLD_CACHE_MAX_COORDINATE; + } + + static bool R_RenderWorldCacheVec3IsValid( const idVec3 &value ) { + return R_RenderWorldCacheCoordinateIsValid( value.x ) + && R_RenderWorldCacheCoordinateIsValid( value.y ) + && R_RenderWorldCacheCoordinateIsValid( value.z ); + } + + static bool R_RenderWorldCacheBoundsIsValid( const idBounds &bounds ) { + if ( bounds.IsCleared() ) { + return false; + } + return R_RenderWorldCacheVec3IsValid( bounds[0] ) + && R_RenderWorldCacheVec3IsValid( bounds[1] ) + && bounds[0].x <= bounds[1].x + && bounds[0].y <= bounds[1].y + && bounds[0].z <= bounds[1].z; + } + + /* + ================ + R_RenderWorldCacheWriteShadowModel + + Classic proc shadowModel blocks are deliberately not ordinary static-model + payloads: their sole surface owns shadow vertices and indexes, but no draw + vertices. Keep this narrow source representation local to the render-world + cache instead of broadening the standalone model codec. + ================ + */ + static bool R_RenderWorldCacheWriteShadowModel( idFile &file, const idRenderModel &model ) { + if ( model.IsDefaultModel() || model.IsDynamicModel() != DM_STATIC + || model.IsReloadable() || model.IsStaticWorldModel() + || model.NumSurfaces() != 1 || model.NumBaseSurfaces() != 1 + || !R_RenderWorldCacheStringIsSafe( model.Name(), RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) ) { + return false; + } + + const modelSurface_t *surface = model.Surface( 0 ); + if ( surface == NULL || surface->shader != tr.defaultMaterial || surface->geometry == NULL ) { + return false; + } + const srfTriangles_t *tri = surface->geometry; + if ( tri->verts != NULL || tri->shadowVertexes == NULL || tri->indexes == NULL + || tri->numVerts <= 0 || tri->numVerts > RENDER_WORLD_CACHE_MAX_SHADOW_VERTS + || tri->numIndexes <= 0 || tri->numIndexes > RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES + || ( tri->numIndexes % 3 ) != 0 + || tri->numShadowIndexesNoCaps < 0 || ( tri->numShadowIndexesNoCaps % 3 ) != 0 + || tri->numShadowIndexesNoFrontCaps < tri->numShadowIndexesNoCaps + || ( tri->numShadowIndexesNoFrontCaps % 3 ) != 0 + || tri->numShadowIndexesNoFrontCaps > tri->numIndexes + || tri->shadowCapPlaneBits < 0 || tri->shadowCapPlaneBits > 127 + || tri->deformedSurface || !R_RenderWorldCacheBoundsIsValid( tri->bounds ) + || tri->silIndexes != NULL || tri->numMirroredVerts != 0 || tri->mirroredVerts != NULL + || tri->numDupVerts != 0 || tri->dupVerts != NULL + || tri->numSilEdges != 0 || tri->silEdges != NULL + || tri->facePlanes != NULL || tri->dominantTris != NULL + || R_TriHasPrimBatchMesh( tri ) ) { + return false; + } +#if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) + if ( tri->silTraceVerts != NULL || tri->numSkinToModelTransforms != 0 + || tri->skinToModelTransforms != NULL || tri->skinToModelTransformsAlloc != NULL ) { + return false; + } +#endif + + idBounds computedBounds; + computedBounds.Clear(); + for ( int i = 0; i < tri->numVerts; ++i ) { + const idVec4 &vertex = tri->shadowVertexes[i].xyz; + if ( vertex.w != 1.0f || !R_RenderWorldCacheVec3IsValid( vertex.ToVec3() ) ) { + return false; + } + computedBounds.AddPoint( vertex.ToVec3() ); + } + if ( !computedBounds.Compare( tri->bounds ) ) { + return false; + } + for ( int i = 0; i < tri->numIndexes; ++i ) { + if ( tri->indexes[i] < 0 || tri->indexes[i] >= tri->numVerts ) { + return false; + } + } + + idRenderModelCacheWriter writer( file ); + if ( !writer.WriteUnsignedInt( RENDER_WORLD_SHADOW_CACHE_MAGIC ) + || !writer.WriteInt( RENDER_WORLD_SHADOW_CACHE_VERSION ) + || !writer.WriteString( model.Name(), RENDER_WORLD_CACHE_MAX_MAP_NAME ) + || !writer.WriteInt( tri->numVerts ) + || !writer.WriteInt( tri->numShadowIndexesNoCaps ) + || !writer.WriteInt( tri->numShadowIndexesNoFrontCaps ) + || !writer.WriteInt( tri->numIndexes ) + || !writer.WriteInt( tri->shadowCapPlaneBits ) ) { + return false; + } + for ( int i = 0; i < tri->numVerts; ++i ) { + if ( !writer.WriteVec3( tri->shadowVertexes[i].xyz.ToVec3() ) ) { + return false; + } + } + for ( int i = 0; i < tri->numIndexes; ++i ) { + if ( !writer.WriteInt( tri->indexes[i] ) ) { + return false; + } + } + return writer.IsValid(); + } + + static idRenderModelStatic *R_RenderWorldCacheReadShadowModel( idFile &file ) { + idRenderModelCacheReader reader( file ); + unsigned int magic = 0; + int version = 0; + idStr name; + int numVerts = 0; + int numIndexes = 0; + int numIndexesNoCaps = 0; + int numIndexesNoFrontCaps = 0; + int shadowCapPlaneBits = 0; + if ( !reader.ReadUnsignedInt( magic ) || magic != RENDER_WORLD_SHADOW_CACHE_MAGIC + || !reader.ReadInt( version ) || version != RENDER_WORLD_SHADOW_CACHE_VERSION + || !reader.ReadString( name, RENDER_WORLD_CACHE_MAX_MAP_NAME ) + || !R_RenderWorldCacheStringIsSafe( name.c_str(), RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) + || !reader.ReadCount( numVerts, RENDER_WORLD_CACHE_MAX_SHADOW_VERTS, sizeof( idVec4 ) ) + || numVerts <= 0 + || !reader.ReadCount( numIndexesNoCaps, RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES ) + || !reader.ReadCount( numIndexesNoFrontCaps, RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES ) + || !reader.ReadCount( numIndexes, RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES, sizeof( glIndex_t ) ) + || numIndexes <= 0 || ( numIndexes % 3 ) != 0 + || numIndexesNoCaps > numIndexesNoFrontCaps || numIndexesNoFrontCaps > numIndexes + || ( numIndexesNoCaps % 3 ) != 0 || ( numIndexesNoFrontCaps % 3 ) != 0 + || !reader.ReadInt( shadowCapPlaneBits ) || shadowCapPlaneBits < 0 || shadowCapPlaneBits > 127 ) { + return NULL; + } + + idList vertices; + idList indexes; + vertices.SetNum( numVerts ); + indexes.SetNum( numIndexes ); + idBounds bounds; + bounds.Clear(); + for ( int i = 0; i < numVerts; ++i ) { + if ( !reader.ReadVec3( vertices[i] ) || !R_RenderWorldCacheVec3IsValid( vertices[i] ) ) { + return NULL; + } + bounds.AddPoint( vertices[i] ); + } + if ( !R_RenderWorldCacheBoundsIsValid( bounds ) ) { + return NULL; + } + for ( int i = 0; i < numIndexes; ++i ) { + if ( !reader.ReadInt( indexes[i] ) || indexes[i] < 0 || indexes[i] >= numVerts ) { + return NULL; + } + } + if ( !reader.IsValid() || tr.defaultMaterial == NULL ) { + return NULL; + } + + idRenderModelStatic *model = new idRenderModelStatic; + if ( model == NULL ) { + return NULL; + } + model->InitEmpty( name.c_str() ); + if ( model->IsStaticWorldModel() ) { + delete model; + return NULL; + } + srfTriangles_t *tri = R_AllocStaticTriSurf(); + tri->numVerts = numVerts; + tri->numIndexes = numIndexes; + tri->numShadowIndexesNoCaps = numIndexesNoCaps; + tri->numShadowIndexesNoFrontCaps = numIndexesNoFrontCaps; + tri->shadowCapPlaneBits = shadowCapPlaneBits; + tri->bounds = bounds; + R_AllocStaticTriSurfShadowVerts( tri, numVerts ); + for ( int i = 0; i < numVerts; ++i ) { + tri->shadowVertexes[i].xyz.Set( vertices[i].x, vertices[i].y, vertices[i].z, 1.0f ); + } + R_AllocStaticTriSurfIndexes( tri, numIndexes ); + memcpy( tri->indexes, indexes.Ptr(), numIndexes * sizeof( indexes[0] ) ); + + modelSurface_t surface; + surface.id = 0; + surface.shader = tr.defaultMaterial; + surface.geometry = tri; + surface.mOriginalSurfaceName = NULL; + model->AddSurface( surface ); + return model; + } + + static bool R_RenderWorldCachePlaneIsValid( const idPlane &plane ) { + for ( int i = 0; i < 4; ++i ) { + if ( !R_RenderWorldCacheCoordinateIsValid( plane[i] ) ) { + return false; + } + } + const double normalLengthSquared = + static_cast( plane[0] ) * plane[0] + + static_cast( plane[1] ) * plane[1] + + static_cast( plane[2] ) * plane[2]; + return normalLengthSquared >= 0.25 && normalLengthSquared <= 4.0; + } + + static bool R_RenderWorldCacheWindingIsValid( const idVec3 *points, int numPoints ) { + if ( points == NULL || numPoints < 3 || numPoints > RENDER_WORLD_CACHE_MAX_WINDING_POINTS ) { + return false; + } + for ( int i = 0; i < numPoints; ++i ) { + if ( !R_RenderWorldCacheVec3IsValid( points[i] ) ) { + return false; + } + } + + const double edgeX = static_cast( points[1].x ) - points[0].x; + const double edgeY = static_cast( points[1].y ) - points[0].y; + const double edgeZ = static_cast( points[1].z ) - points[0].z; + for ( int i = 2; i < numPoints; ++i ) { + const double otherX = static_cast( points[i].x ) - points[0].x; + const double otherY = static_cast( points[i].y ) - points[0].y; + const double otherZ = static_cast( points[i].z ) - points[0].z; + const double crossX = edgeY * otherZ - edgeZ * otherY; + const double crossY = edgeZ * otherX - edgeX * otherZ; + const double crossZ = edgeX * otherY - edgeY * otherX; + const double areaSquared = crossX * crossX + crossY * crossY + crossZ * crossZ; + if ( areaSquared > 1.0e-12 ) { + return true; + } + } + return false; + } + + static bool R_RenderWorldCachePortalRangeIsValid( float cullNear, float cullFar ) { + return R_RenderModelCacheFloatIsFinite( cullNear ) + && R_RenderModelCacheFloatIsFinite( cullFar ) + && cullNear >= 0.0f && cullNear <= RENDER_WORLD_CACHE_MAX_COORDINATE + && cullFar >= cullNear && cullFar <= RENDER_WORLD_CACHE_MAX_COORDINATE; + } + + static bool R_RenderWorldCacheValidateNodeGraph( const areaNode_t *nodes, int numNodes, int numAreas ) { + if ( nodes == NULL || numNodes <= 0 || numNodes > RENDER_WORLD_CACHE_MAX_NODES + || numAreas <= 0 || numAreas > RENDER_WORLD_CACHE_MAX_AREAS ) { + return false; + } + + for ( int i = 0; i < numNodes; ++i ) { + if ( !R_RenderWorldCachePlaneIsValid( nodes[i].plane ) ) { + return false; + } + for ( int side = 0; side < 2; ++side ) { + const int child = nodes[i].children[side]; + if ( child > 0 ) { + if ( child >= numNodes ) { + return false; + } + } else if ( child < -numAreas ) { + return false; + } + } + } + + idList colors; + idList nodeStack; + idList edgeStack; + colors.SetNum( numNodes ); + memset( colors.Ptr(), 0, numNodes * sizeof( colors[0] ) ); + nodeStack.Append( 0 ); + edgeStack.Append( 0 ); + + while ( nodeStack.Num() > 0 ) { + const int stackIndex = nodeStack.Num() - 1; + const int nodeIndex = nodeStack[stackIndex]; + if ( colors[nodeIndex] == 0 ) { + colors[nodeIndex] = 1; + } + + if ( edgeStack[stackIndex] >= 2 ) { + colors[nodeIndex] = 2; + nodeStack.SetNum( stackIndex ); + edgeStack.SetNum( stackIndex ); + continue; + } + + const int side = edgeStack[stackIndex]++; + const int child = nodes[nodeIndex].children[side]; + if ( child <= 0 ) { + continue; + } + if ( colors[child] == 1 ) { + return false; + } + if ( colors[child] == 0 ) { + if ( nodeStack.Num() >= RENDER_WORLD_CACHE_MAX_NODE_DEPTH ) { + return false; + } + nodeStack.Append( child ); + edgeStack.Append( 0 ); + } + } + + // Hidden, unreachable nodes are never part of the finalized BSP and are + // rejected instead of being retained as attacker-controlled baggage. + for ( int i = 0; i < numNodes; ++i ) { + if ( colors[i] != 2 ) { + return false; + } + } + return true; + } + + static bool R_RenderWorldCacheValidateModels( const idList &models, int numAreas ) { + if ( models.Num() < numAreas || models.Num() > RENDER_WORLD_CACHE_MAX_MODELS ) { + return false; + } + + idHashIndex nameHash( 1024, models.Num() > 1 ? models.Num() : 1 ); + for ( int i = 0; i < models.Num(); ++i ) { + const idRenderModel *model = models[i]; + const idRenderModelStatic *staticModel = dynamic_cast( model ); + if ( staticModel == NULL || staticModel->LevelLoadCachePayloadType() != RENDER_MODEL_CACHE_STATIC + || dynamic_cast( model ) != NULL + || dynamic_cast( model ) != NULL + || model->IsDefaultModel() + || !R_RenderWorldCacheStringIsSafe( model->Name(), RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) ) { + return false; + } + + const int key = nameHash.GenerateKey( model->Name(), false ); + for ( int previous = nameHash.First( key ); previous != -1; previous = nameHash.Next( previous ) ) { + if ( idStr::Icmp( models[previous]->Name(), model->Name() ) == 0 ) { + return false; + } + } + nameHash.Add( key, i ); + } + + for ( int area = 0; area < numAreas; ++area ) { + const idStr areaModelName = va( "_area%i", area ); + const int key = nameHash.GenerateKey( areaModelName.c_str(), false ); + int foundIndex = -1; + for ( int index = nameHash.First( key ); index != -1; index = nameHash.Next( index ) ) { + if ( idStr::Icmp( models[index]->Name(), areaModelName.c_str() ) == 0 ) { + foundIndex = index; + break; + } + } + if ( foundIndex < 0 || !models[foundIndex]->IsStaticWorldModel() ) { + return false; + } + } + return true; + } + + static bool R_RenderWorldCacheValidateLivePortals( const idRenderWorldLocal &world ) { + if ( world.numPortalAreas <= 0 || world.numPortalAreas > RENDER_WORLD_CACHE_MAX_AREAS + || world.portalAreas == NULL || world.areaScreenRect == NULL + || world.numInterAreaPortals < 0 || world.numInterAreaPortals > RENDER_WORLD_CACHE_MAX_PORTALS + || ( world.numInterAreaPortals > 0 && world.doublePortals == NULL ) ) { + return false; + } + + int totalWindingPoints = 0; + for ( int i = 0; i < world.numInterAreaPortals; ++i ) { + const doublePortal_t &doublePortal = world.doublePortals[i]; + const portal_t *p0 = doublePortal.portals[0]; + const portal_t *p1 = doublePortal.portals[1]; + if ( p0 == NULL || p1 == NULL || p0 == p1 || p0->doublePortal != &doublePortal + || p1->doublePortal != &doublePortal || p0->w == NULL || p1->w == NULL + || p0->intoArea < 0 || p0->intoArea >= world.numPortalAreas + || p1->intoArea < 0 || p1->intoArea >= world.numPortalAreas + || p0->intoArea == p1->intoArea || p0->image != p1->image + || p0->cullNear != p1->cullNear || p0->cullFar != p1->cullFar + || !R_RenderWorldCachePortalRangeIsValid( p0->cullNear, p0->cullFar ) ) { + return false; + } + + const int numPoints = p0->w->GetNumPoints(); + if ( numPoints < 3 || numPoints > RENDER_WORLD_CACHE_MAX_WINDING_POINTS + || p1->w->GetNumPoints() != numPoints + || numPoints > RENDER_WORLD_CACHE_MAX_TOTAL_WINDING_POINTS - totalWindingPoints ) { + return false; + } + idList points; + points.SetNum( numPoints ); + for ( int point = 0; point < numPoints; ++point ) { + points[point] = ( *p0->w )[point].ToVec3(); + const idVec3 reversePoint = ( *p1->w )[numPoints - point - 1].ToVec3(); + if ( points[point].x != reversePoint.x || points[point].y != reversePoint.y || points[point].z != reversePoint.z ) { + return false; + } + } + if ( !R_RenderWorldCacheWindingIsValid( points.Ptr(), numPoints ) ) { + return false; + } + if ( p0->image != NULL + && !R_RenderWorldCacheStringIsSafe( p0->image->GetName(), RENDER_WORLD_CACHE_MAX_IMAGE_NAME, false ) ) { + return false; + } + totalWindingPoints += numPoints; + } + + idList seenPortalSides; + seenPortalSides.SetNum( world.numInterAreaPortals * 2 ); + if ( seenPortalSides.Num() > 0 ) { + memset( seenPortalSides.Ptr(), 0, seenPortalSides.Num() * sizeof( seenPortalSides[0] ) ); + } + int traversedPortalSides = 0; + const uintptr_t doublePortalBase = reinterpret_cast( world.doublePortals ); + const size_t doublePortalBytes = static_cast( world.numInterAreaPortals ) * sizeof( world.doublePortals[0] ); + for ( int area = 0; area < world.numPortalAreas; ++area ) { + for ( const portal_t *portal = world.portalAreas[area].portals; portal != NULL; portal = portal->next ) { + if ( traversedPortalSides >= world.numInterAreaPortals * 2 || portal->doublePortal == NULL ) { + return false; + } + const uintptr_t portalAddress = reinterpret_cast( portal->doublePortal ); + if ( portalAddress < doublePortalBase ) { + return false; + } + const uintptr_t byteOffset = portalAddress - doublePortalBase; + if ( byteOffset >= doublePortalBytes || ( byteOffset % sizeof( doublePortal_t ) ) != 0 ) { + return false; + } + const int portalIndex = static_cast( byteOffset / sizeof( doublePortal_t ) ); + const doublePortal_t &doublePortal = world.doublePortals[portalIndex]; + int side = -1; + if ( doublePortal.portals[0] == portal ) { + side = 0; + } else if ( doublePortal.portals[1] == portal ) { + side = 1; + } + const int oppositeSide = 1 - side; + if ( side < 0 || doublePortal.portals[oppositeSide] == NULL + || doublePortal.portals[oppositeSide]->intoArea != area + || seenPortalSides[portalIndex * 2 + side] != 0 ) { + return false; + } + seenPortalSides[portalIndex * 2 + side] = 1; + ++traversedPortalSides; + } + } + return traversedPortalSides == world.numInterAreaPortals * 2; + } +} + +/* +================ +idRenderWorldLocal::WriteLevelLoadCachePayload +================ +*/ +bool idRenderWorldLocal::WriteLevelLoadCachePayload( idFile &file ) const { + // Packed MD5R proc worlds contain cross-model shared buffers. The ordinary + // model codec deliberately owns its decoded buffers, so caching such a world + // here would silently change that representation. Fail before writing and + // let InitFromMap retain the MD5RProc / classic-proc source path. + if ( md5rProcData != NULL && md5rProcData->HasPackedWorldData() ) { + common->DPrintf( "Render-world cache skipped for '%s': packed MD5R world\n", mapName.c_str() ); + return false; + } + if ( mapName.Length() <= 0 || mapName == "" + || mapTimeStamp == FILE_NOT_FOUND_TIMESTAMP + || !R_RenderWorldCacheStringIsSafe( mapName.c_str(), RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) ) { + common->DPrintf( "Render-world cache skipped: invalid map identity\n" ); + return false; + } + if ( !R_RenderWorldCacheValidateModels( localModels, numPortalAreas ) ) { + common->DPrintf( "Render-world cache skipped for '%s': model validation failed\n", mapName.c_str() ); + return false; + } + if ( !R_RenderWorldCacheValidateLivePortals( *this ) ) { + common->DPrintf( "Render-world cache skipped for '%s': portal validation failed\n", mapName.c_str() ); + return false; + } + if ( !R_RenderWorldCacheValidateNodeGraph( areaNodes, numAreaNodes, numPortalAreas ) ) { + common->DPrintf( "Render-world cache skipped for '%s': BSP validation failed\n", mapName.c_str() ); + return false; + } + + idRenderWorldCacheMemoryFileOwner payloadOwner; + idFile *payload = payloadOwner.Get(); + if ( payload == NULL ) { + return false; + } + idRenderModelCacheWriter writer( *payload ); + if ( !writer.WriteUnsignedInt( RENDER_WORLD_CACHE_MAGIC ) + || !writer.WriteInt( RENDER_WORLD_CACHE_VERSION ) + || !writer.WriteUnsignedInt( RENDER_WORLD_CACHE_FLAGS ) + || !writer.WriteString( mapName.c_str(), RENDER_WORLD_CACHE_MAX_MAP_NAME ) + || !writer.WriteUnsigned64( static_cast( static_cast( mapTimeStamp ) ) ) + || !writer.WriteUnsignedInt( mapFileCRC ) + || !writer.WriteInt( localModels.Num() ) ) { + return false; + } + + for ( int i = 0; i < localModels.Num(); ++i ) { + const idRenderModel *model = localModels[i]; + const idRenderModelStatic *staticModel = dynamic_cast( model ); + if ( staticModel == NULL || staticModel->LevelLoadCachePayloadType() != RENDER_MODEL_CACHE_STATIC ) { + return false; + } + const bool shadowModel = R_RenderWorld_IsShadowModel( *model ); + idRenderWorldCacheMemoryFileOwner modelPayloadOwner; + idFile *modelPayload = modelPayloadOwner.Get(); + if ( modelPayload == NULL + || ( shadowModel + ? !R_RenderWorldCacheWriteShadowModel( *modelPayload, *model ) + : !staticModel->WriteLevelLoadCachePayload( *modelPayload ) ) ) { + common->DPrintf( "Render-world cache skipped for '%s': model payload %d ('%s') failed\n", + mapName.c_str(), i, model->Name() ); + return false; + } + const int modelPayloadLength = modelPayload->Length(); + const char *modelPayloadData = modelPayload->GetDataPtr(); + if ( modelPayloadLength <= 0 || modelPayloadLength > RENDER_WORLD_CACHE_MAX_MODEL_PAYLOAD + || modelPayloadData == NULL + || !writer.WriteInt( shadowModel ? RENDER_WORLD_CACHE_MODEL_SHADOW : RENDER_WORLD_CACHE_MODEL_STATIC ) + || !writer.WriteInt( modelPayloadLength ) + || !writer.WriteBytes( modelPayloadData, modelPayloadLength ) ) { + return false; + } + } + + if ( !writer.WriteInt( numPortalAreas ) || !writer.WriteInt( numInterAreaPortals ) ) { + return false; + } + for ( int i = 0; i < numInterAreaPortals; ++i ) { + const portal_t *p0 = doublePortals[i].portals[0]; + const portal_t *p1 = doublePortals[i].portals[1]; + const int numPoints = p0->w->GetNumPoints(); + if ( !writer.WriteInt( p1->intoArea ) || !writer.WriteInt( p0->intoArea ) + || !writer.WriteInt( numPoints ) ) { + return false; + } + for ( int point = 0; point < numPoints; ++point ) { + if ( !writer.WriteVec3( ( *p0->w )[point].ToVec3() ) ) { + return false; + } + } + const bool hasImage = p0->image != NULL; + if ( !writer.WriteBool( hasImage ) + || ( hasImage && !writer.WriteString( p0->image->GetName(), RENDER_WORLD_CACHE_MAX_IMAGE_NAME ) ) + || !writer.WriteFloat( p0->cullNear ) || !writer.WriteFloat( p0->cullFar ) ) { + return false; + } + } + + if ( !writer.WriteInt( numAreaNodes ) ) { + return false; + } + for ( int i = 0; i < numAreaNodes; ++i ) { + if ( !writer.WriteVec4( areaNodes[i].plane.ToVec4() ) + || !writer.WriteInt( areaNodes[i].children[0] ) + || !writer.WriteInt( areaNodes[i].children[1] ) ) { + return false; + } + } + + const int payloadLength = payload->Length(); + const char *payloadData = payload->GetDataPtr(); + if ( !writer.IsValid() || payloadLength <= 0 || payloadLength > RENDER_WORLD_CACHE_MAX_PAYLOAD_BYTES ) { + return false; + } + return payloadData != NULL && file.Write( payloadData, payloadLength ) == payloadLength; +} + +/* +================ +idRenderWorldLocal::ReadLevelLoadCachePayload +================ +*/ +bool idRenderWorldLocal::ReadLevelLoadCachePayload( idFile &file, const char *expectedMapName, ID_TIME_T sourceTimestamp ) { + const int expectedMapNameLength = R_RenderWorldCacheBoundedStringLength( expectedMapName, RENDER_WORLD_CACHE_MAX_MAP_NAME ); + const int payloadStart = file.Tell(); + const int payloadEnd = file.Length(); + if ( expectedMapNameLength <= 0 + || !R_RenderWorldCacheStringIsSafe( expectedMapName, RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) + || sourceTimestamp == FILE_NOT_FOUND_TIMESTAMP + || payloadStart < 0 || payloadEnd < payloadStart + || payloadEnd - payloadStart <= 0 || payloadEnd - payloadStart > RENDER_WORLD_CACHE_MAX_PAYLOAD_BYTES ) { + return false; + } + + idRenderModelCacheReader reader( file ); + unsigned int magic = 0; + int version = 0; + unsigned int flags = 0; + idStr stagedMapName; + uint64_t encodedTimestamp = 0; + unsigned int stagedMapCRC = 0; + int numModels = 0; + if ( !reader.ReadUnsignedInt( magic ) || magic != RENDER_WORLD_CACHE_MAGIC + || !reader.ReadInt( version ) || version != RENDER_WORLD_CACHE_VERSION + || !reader.ReadUnsignedInt( flags ) || flags != RENDER_WORLD_CACHE_FLAGS + || !reader.ReadString( stagedMapName, RENDER_WORLD_CACHE_MAX_MAP_NAME ) + || !R_RenderWorldCacheStringIsSafe( stagedMapName.c_str(), RENDER_WORLD_CACHE_MAX_MAP_NAME, false ) + || stagedMapName == "" || idStr::Icmp( stagedMapName.c_str(), expectedMapName ) != 0 + || !reader.ReadUnsigned64( encodedTimestamp ) + || encodedTimestamp != static_cast( static_cast( sourceTimestamp ) ) + || !reader.ReadUnsignedInt( stagedMapCRC ) + || !reader.ReadCount( numModels, RENDER_WORLD_CACHE_MAX_MODELS, sizeof( idRenderModel * ) ) ) { + return false; + } + + idRenderWorldCacheModelOwner modelOwner; + uint64_t aggregateModelMemory = 0; + for ( int i = 0; i < numModels; ++i ) { + int encodedModelType = 0; + int modelPayloadLength = 0; + if ( !reader.ReadInt( encodedModelType ) + || ( encodedModelType != RENDER_WORLD_CACHE_MODEL_STATIC + && encodedModelType != RENDER_WORLD_CACHE_MODEL_SHADOW ) + || !reader.ReadCount( modelPayloadLength, RENDER_WORLD_CACHE_MAX_MODEL_PAYLOAD, sizeof( byte ) ) + || modelPayloadLength <= 0 ) { + return false; + } + + idRenderWorldCacheMemoryFileOwner modelPayloadOwner; + idFile *modelPayload = modelPayloadOwner.Get(); + if ( modelPayload == NULL ) { + return false; + } + byte copyBuffer[16 * 1024]; + int remainingPayloadBytes = modelPayloadLength; + while ( remainingPayloadBytes > 0 ) { + const int copyBytes = remainingPayloadBytes < static_cast( sizeof( copyBuffer ) ) + ? remainingPayloadBytes + : static_cast( sizeof( copyBuffer ) ); + if ( !reader.ReadBytes( copyBuffer, copyBytes ) + || modelPayload->Write( copyBuffer, copyBytes ) != copyBytes ) { + return false; + } + remainingPayloadBytes -= copyBytes; + } + modelPayload->MakeReadOnly(); + idRenderModelStatic *model = encodedModelType == RENDER_WORLD_CACHE_MODEL_SHADOW + ? R_RenderWorldCacheReadShadowModel( *modelPayload ) + : new idRenderModelStatic; + if ( model == NULL ) { + return false; + } + if ( ( encodedModelType == RENDER_WORLD_CACHE_MODEL_STATIC + && !model->ReadLevelLoadCachePayload( *modelPayload ) ) + || modelPayload->Tell() != modelPayload->Length() + || model->LevelLoadCachePayloadType() != RENDER_MODEL_CACHE_STATIC ) { + delete model; + return false; + } + + const int modelMemory = model->Memory(); + if ( modelMemory <= 0 || static_cast( modelMemory ) > RENDER_WORLD_CACHE_MAX_MODEL_MEMORY - aggregateModelMemory ) { + delete model; + return false; + } + aggregateModelMemory += static_cast( modelMemory ); + modelOwner.models.Append( model ); + } + + int stagedNumAreas = 0; + int stagedNumPortals = 0; + if ( !reader.ReadCount( stagedNumAreas, RENDER_WORLD_CACHE_MAX_AREAS, sizeof( portalArea_t ) ) + || stagedNumAreas <= 0 + || !reader.ReadCount( stagedNumPortals, RENDER_WORLD_CACHE_MAX_PORTALS, sizeof( renderWorldCachePortalStage_t ) ) ) { + return false; + } + + idList stagedPortals; + stagedPortals.SetNum( stagedNumPortals ); + int totalWindingPoints = 0; + for ( int i = 0; i < stagedNumPortals; ++i ) { + renderWorldCachePortalStage_t &portal = stagedPortals[i]; + int numPoints = 0; + bool hasImage = false; + if ( !reader.ReadInt( portal.areas[0] ) || !reader.ReadInt( portal.areas[1] ) + || portal.areas[0] < 0 || portal.areas[0] >= stagedNumAreas + || portal.areas[1] < 0 || portal.areas[1] >= stagedNumAreas + || portal.areas[0] == portal.areas[1] + || !reader.ReadCount( numPoints, RENDER_WORLD_CACHE_MAX_WINDING_POINTS, sizeof( idVec3 ) ) + || numPoints < 3 || numPoints > RENDER_WORLD_CACHE_MAX_TOTAL_WINDING_POINTS - totalWindingPoints ) { + return false; + } + portal.points.SetNum( numPoints ); + for ( int point = 0; point < numPoints; ++point ) { + if ( !reader.ReadVec3( portal.points[point] ) || !R_RenderWorldCacheVec3IsValid( portal.points[point] ) ) { + return false; + } + } + if ( !R_RenderWorldCacheWindingIsValid( portal.points.Ptr(), numPoints ) + || !reader.ReadBool( hasImage ) + || ( hasImage && ( !reader.ReadString( portal.imageName, RENDER_WORLD_CACHE_MAX_IMAGE_NAME ) + || !R_RenderWorldCacheStringIsSafe( portal.imageName.c_str(), RENDER_WORLD_CACHE_MAX_IMAGE_NAME, false ) ) ) + || !reader.ReadFloat( portal.cullNear ) || !reader.ReadFloat( portal.cullFar ) + || !R_RenderWorldCachePortalRangeIsValid( portal.cullNear, portal.cullFar ) ) { + return false; + } + if ( !hasImage ) { + portal.imageName.Clear(); + } + totalWindingPoints += numPoints; + } + + int stagedNumNodes = 0; + if ( !reader.ReadCount( stagedNumNodes, RENDER_WORLD_CACHE_MAX_NODES, sizeof( areaNode_t ) ) + || stagedNumNodes <= 0 ) { + return false; + } + idList stagedNodes; + stagedNodes.SetNum( stagedNumNodes ); + for ( int i = 0; i < stagedNumNodes; ++i ) { + idVec4 plane; + if ( !reader.ReadVec4( plane ) || !reader.ReadInt( stagedNodes[i].children[0] ) + || !reader.ReadInt( stagedNodes[i].children[1] ) ) { + return false; + } + stagedNodes[i].plane.ToVec4() = plane; + stagedNodes[i].commonChildrenArea = CHILDREN_HAVE_MULTIPLE_AREAS; + } + + if ( !reader.IsValid() || file.Tell() != payloadEnd + || !R_RenderWorldCacheValidateModels( modelOwner.models, stagedNumAreas ) + || !R_RenderWorldCacheValidateNodeGraph( stagedNodes.Ptr(), stagedNumNodes, stagedNumAreas ) ) { + return false; + } + + // Publication begins only after every byte and every cross-reference has + // passed validation. No failure return exists below this boundary. + FreeWorld(); + mapName = stagedMapName; + mapTimeStamp = sourceTimestamp; + mapFileCRC = stagedMapCRC; + + numPortalAreas = stagedNumAreas; + portalAreas = static_cast( R_ClearedStaticAlloc( numPortalAreas * sizeof( portalAreas[0] ) ) ); + areaScreenRect = static_cast( R_ClearedStaticAlloc( numPortalAreas * sizeof( areaScreenRect[0] ) ) ); + SetupAreaRefs(); + + numInterAreaPortals = stagedNumPortals; + if ( numInterAreaPortals > 0 ) { + doublePortals = static_cast( R_ClearedStaticAlloc( numInterAreaPortals * sizeof( doublePortals[0] ) ) ); + } + for ( int i = 0; i < numInterAreaPortals; ++i ) { + const renderWorldCachePortalStage_t &sourcePortal = stagedPortals[i]; + idWinding *winding = new idWinding( sourcePortal.points.Num() ); + winding->SetNumPoints( sourcePortal.points.Num() ); + for ( int point = 0; point < sourcePortal.points.Num(); ++point ) { + ( *winding )[point].ToVec3() = sourcePortal.points[point]; + ( *winding )[point][3] = 0.0f; + ( *winding )[point][4] = 0.0f; + } + idImage *portalImage = NULL; + if ( sourcePortal.imageName.Length() > 0 ) { + portalImage = globalImages->ImageFromFile( + sourcePortal.imageName.c_str(), TF_DEFAULT, TR_REPEAT, TD_DEFAULT ); + } + + portal_t *p0 = static_cast( R_ClearedStaticAlloc( sizeof( *p0 ) ) ); + p0->intoArea = sourcePortal.areas[1]; + p0->doublePortal = &doublePortals[i]; + p0->w = winding; + p0->w->GetPlane( p0->plane ); + p0->image = portalImage; + p0->cullNear = sourcePortal.cullNear; + p0->cullFar = sourcePortal.cullFar; + p0->next = portalAreas[sourcePortal.areas[0]].portals; + portalAreas[sourcePortal.areas[0]].portals = p0; + doublePortals[i].portals[0] = p0; + + portal_t *p1 = static_cast( R_ClearedStaticAlloc( sizeof( *p1 ) ) ); + p1->intoArea = sourcePortal.areas[0]; + p1->doublePortal = &doublePortals[i]; + p1->w = winding->Reverse(); + p1->w->GetPlane( p1->plane ); + p1->image = portalImage; + p1->cullNear = sourcePortal.cullNear; + p1->cullFar = sourcePortal.cullFar; + p1->next = portalAreas[sourcePortal.areas[1]].portals; + portalAreas[sourcePortal.areas[1]].portals = p1; + doublePortals[i].portals[1] = p1; + } + + numAreaNodes = stagedNumNodes; + areaNodes = static_cast( R_ClearedStaticAlloc( numAreaNodes * sizeof( areaNodes[0] ) ) ); + for ( int i = 0; i < numAreaNodes; ++i ) { + areaNodes[i] = stagedNodes[i]; + } + + for ( int i = 0; i < modelOwner.models.Num(); ++i ) { + renderModelManager->AddModel( modelOwner.models[i] ); + localModels.Append( modelOwner.models[i] ); + } + modelOwner.Release(); + + R_RenderWorld_FinalizeLoadedWorld( *this ); + return true; +} + /* ================ R_RenderWorld_ParseSupportedMD5RProc @@ -577,6 +1501,11 @@ idRenderWorldLocal::FreeWorld void idRenderWorldLocal::FreeWorld() { int i; + // drop every memoized draw-surf area before the defs and portal areas the + // entries point at are freed (also keeps FreeEntityDef's memo scan trivial + // during mass teardown) + R_ClearDrawSurfAreaMemo(); + // this will free all the lightDefs and entityDefs FreeDefs(); @@ -781,6 +1710,22 @@ idRenderModel *idRenderWorldLocal::ParseShadowModel( Lexer *src ) { tri->numIndexes = src->ParseInt(); tri->shadowCapPlaneBits = src->ParseInt(); + // FinishSurfaces is intentionally skipped for shadow models, so nothing + // downstream range-checks these file-provided counts before the stencil draw + // fetches indexes. Validate them here with the same predicates the binary + // shadow-cache reader applies. Zero counts are allowed (an empty shadow model + // parses harmlessly); only malformed data is rejected. + if ( tri->numVerts < 0 || tri->numVerts > RENDER_WORLD_CACHE_MAX_SHADOW_VERTS + || tri->numIndexes < 0 || tri->numIndexes > RENDER_WORLD_CACHE_MAX_SHADOW_INDEXES + || ( tri->numIndexes % 3 ) != 0 + || tri->numShadowIndexesNoCaps < 0 || tri->numShadowIndexesNoFrontCaps < 0 + || ( tri->numShadowIndexesNoCaps % 3 ) != 0 || ( tri->numShadowIndexesNoFrontCaps % 3 ) != 0 + || tri->numShadowIndexesNoCaps > tri->numShadowIndexesNoFrontCaps + || tri->numShadowIndexesNoFrontCaps > tri->numIndexes ) { + src->Error( "R_ParseShadowModel: bad shadow model counts" ); + return NULL; + } + R_AllocStaticTriSurfShadowVerts( tri, tri->numVerts ); tri->bounds.Clear(); for ( j = 0 ; j < tri->numVerts ; j++ ) { @@ -876,6 +1821,19 @@ void idRenderWorldLocal::ParseInterAreaPortals( Lexer *src ) { a1 = src->ParseInt(); a2 = src->ParseInt(); + // a1/a2 index portalAreas directly below; reject file-provided area + // numbers out of range rather than corrupting adjacent memory. numPoints + // must be at least a triangle (GetPlane reads three points) and is capped + // to the same bound the binary cache reader enforces. + if ( a1 < 0 || a1 >= numPortalAreas || a2 < 0 || a2 >= numPortalAreas ) { + src->Error( "R_ParseInterAreaPortals: portal %i references area out of range (%i, %i of %i)", i, a1, a2, numPortalAreas ); + return; + } + if ( numPoints < 3 || numPoints > RENDER_WORLD_CACHE_MAX_WINDING_POINTS ) { + src->Error( "R_ParseInterAreaPortals: portal %i has bad point count %i", i, numPoints ); + return; + } + w = new idWinding( numPoints ); w->SetNumPoints( numPoints ); for ( j = 0 ; j < numPoints ; j++ ) { @@ -958,6 +1916,16 @@ void idRenderWorldLocal::ParseNodes( Lexer *src ) { } src->ExpectTokenString( "}" ); + + // CommonChildrenArea_r (run at finalize) recurses this graph over file- + // provided child indices with no bounds or cycle guard, so validate it here + // with the same bounds/area-range/cycle/depth checks the binary cache path + // enforces. Nodes are written after portals, so numPortalAreas is already + // set for any multi-area map that carries nodes. + if ( numAreaNodes > 0 + && !R_RenderWorldCacheValidateNodeGraph( areaNodes, numAreaNodes, numPortalAreas ) ) { + src->Error( "R_ParseNodes: invalid area node graph" ); + } } /* @@ -1125,6 +2093,12 @@ void idRenderWorldLocal::FreeDefs() { generateAllInteractionsCalled = false; + // Every def is about to be freed one by one; clearing the draw-surf area memo + // up front keeps each FreeEntityDef's per-entity invalidation trivial instead + // of scanning a still-populated memo thousands of times (e.g. same-map restart + // via InitFromMap's retain path, which frees defs without FreeWorld's clear). + R_ClearDrawSurfAreaMemo(); + if ( interactionTable ) { R_StaticFree( interactionTable ); interactionTable = NULL; @@ -1215,9 +2189,13 @@ Retail prefers a compiled MD5RProc companion when binary reads are enabled, then falls back to the text MD5RProc file before using the classic .proc world. ================= */ -static bool R_RenderWorld_HasMD5RProcCompanion( const char *mapName, idStr &md5rProcFilename, ID_TIME_T *timeStamp = NULL ) { +static bool R_RenderWorld_HasMD5RProcCompanion( const char *mapName, idStr &md5rProcFilename, + ID_TIME_T *timeStamp = NULL, idStr *resolvedFilename = NULL ) { if ( r_forceConvertMD5R.GetBool() ) { md5rProcFilename.Clear(); + if ( resolvedFilename != NULL ) { + resolvedFilename->Clear(); + } if ( timeStamp != NULL ) { *timeStamp = FILE_NOT_FOUND_TIMESTAMP; } @@ -1227,7 +2205,7 @@ static bool R_RenderWorld_HasMD5RProcCompanion( const char *mapName, idStr &md5r md5rProcFilename = mapName; md5rProcFilename.SetFileExtension( MD5R_PROC_FILE_EXT ); - const ID_TIME_T md5rProcTimeStamp = R_RenderWorld_ReadBinaryAwareTimestamp( md5rProcFilename ); + const ID_TIME_T md5rProcTimeStamp = R_RenderWorld_ReadBinaryAwareTimestamp( md5rProcFilename, resolvedFilename ); if ( timeStamp != NULL ) { *timeStamp = md5rProcTimeStamp; } @@ -1251,7 +2229,9 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { Lexer * src; idToken token; idStr filename; + idStr procSourceFilename; idStr md5rProcFilename; + idStr md5rProcSourceFilename; ID_TIME_T procTimeStamp; ID_TIME_T md5rProcTimeStamp = FILE_NOT_FOUND_TIMESTAMP; idRenderModel * lastModel; @@ -1269,11 +2249,12 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { // load it filename = name; filename.SetFileExtension( PROC_FILE_EXT ); - procTimeStamp = R_RenderWorld_ReadBinaryAwareTimestamp( filename ); + procTimeStamp = R_RenderWorld_ReadBinaryAwareTimestamp( filename, &procSourceFilename ); R_DisableUnavailableMD5RCVar( r_convertProcToMD5R, "the MD5R proc-world runtime" ); - const bool hasMD5RProcCompanion = R_RenderWorld_HasMD5RProcCompanion( name, md5rProcFilename, &md5rProcTimeStamp ); + const bool hasMD5RProcCompanion = R_RenderWorld_HasMD5RProcCompanion( + name, md5rProcFilename, &md5rProcTimeStamp, &md5rProcSourceFilename ); if ( hasMD5RProcCompanion ) { common->DPrintf( "Found MD5RProc companion '%s' for map '%s'; openQ4 will prefer it before the classic proc world '%s'.\n", @@ -1294,6 +2275,14 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { AddWorldModelEntities(); SetupLightGrid(); ClearPortalStates(); + fileSystem->RecordLevelLoadResource( + LEVEL_LOAD_RESOURCE_WORLD, + hasMD5RProcCompanion + ? ( md5rProcSourceFilename.Length() > 0 ? md5rProcSourceFilename.c_str() : md5rProcFilename.c_str() ) + : ( procSourceFilename.Length() > 0 ? procSourceFilename.c_str() : filename.c_str() ), + hasMD5RProcCompanion ? RENDER_WORLD_CACHE_MD5R_SEMANTIC : RENDER_WORLD_CACHE_CLASSIC_SEMANTIC, + 0u, + 3u ); return true; } common->Printf( "idRenderWorldLocal::InitFromMap: timestamp has changed, reloading.\n" ); @@ -1318,6 +2307,12 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { } R_RenderWorld_FinalizeLoadedWorld( *this ); + fileSystem->RecordLevelLoadResource( + LEVEL_LOAD_RESOURCE_WORLD, + md5rProcSourceFilename.Length() > 0 ? md5rProcSourceFilename.c_str() : md5rProcFilename.c_str(), + RENDER_WORLD_CACHE_MD5R_SEMANTIC, + 0u, + 3u ); return true; } @@ -1329,6 +2324,42 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { md5rProcFilename.c_str() ); } + const char *procCacheSource = procSourceFilename.Length() > 0 + ? procSourceFilename.c_str() : filename.c_str(); + const idStr cacheSettingsKey = R_RenderWorldCacheBuildSettingsKey(); + // Proc-to-MD5R conversion selects a shared packed representation that this + // world codec intentionally refuses to flatten. In that mode source parsing + // remains authoritative and no unusable cache is probed or generated. + if ( procTimeStamp != FILE_NOT_FOUND_TIMESTAMP && !r_convertProcToMD5R.GetBool() ) { + idFile *cacheFile = fileSystem->OpenGeneratedCacheRead( + GENERATED_CACHE_RENDER_WORLD, + procCacheSource, + RENDER_WORLD_CACHE_PARSER_VERSION, + cacheSettingsKey.c_str() ); + if ( cacheFile != NULL ) { + const bool loadedFromCache = ReadLevelLoadCachePayload( *cacheFile, name, procTimeStamp ); + fileSystem->CloseFile( cacheFile ); + if ( loadedFromCache ) { + fileSystem->RecordLevelLoadResource( + LEVEL_LOAD_RESOURCE_WORLD, + procCacheSource, + RENDER_WORLD_CACHE_CLASSIC_SEMANTIC, + 0u, + 3u ); + if ( session->writeDemo ) { + WriteLoadMap(); + } + common->DPrintf( "Loaded generated render-world cache for '%s'\n", procCacheSource ); + return true; + } + fileSystem->DiscardGeneratedCache( + GENERATED_CACHE_RENDER_WORLD, + procCacheSource, + RENDER_WORLD_CACHE_PARSER_VERSION, + cacheSettingsKey.c_str() ); + } + } + src = LexerFactory::MakeLexer( filename.c_str(), LEXFL_NOSTRINGCONCAT | LEXFL_NODOLLARPRECOMPILE, @@ -1423,6 +2454,29 @@ bool idRenderWorldLocal::InitFromMap( const char *name ) { } R_RenderWorld_FinalizeLoadedWorld( *this ); + fileSystem->RecordLevelLoadResource( + LEVEL_LOAD_RESOURCE_WORLD, + procCacheSource, + RENDER_WORLD_CACHE_CLASSIC_SEMANTIC, + 0u, + 3u ); + + if ( !r_convertProcToMD5R.GetBool() ) { + idRenderWorldCacheMemoryFileOwner cachePayloadOwner; + idFile *cachePayload = cachePayloadOwner.Get(); + if ( cachePayload != NULL && WriteLevelLoadCachePayload( *cachePayload ) && cachePayload->Length() > 0 ) { + const char *cachePayloadData = cachePayload->GetDataPtr(); + if ( cachePayloadData != NULL ) { + fileSystem->WriteGeneratedCache( + GENERATED_CACHE_RENDER_WORLD, + procCacheSource, + RENDER_WORLD_CACHE_PARSER_VERSION, + cacheSettingsKey.c_str(), + cachePayloadData, + static_cast( cachePayload->Length() ) ); + } + } + } // done! return true; diff --git a/src/renderer/RenderWorld_local.h b/src/renderer/RenderWorld_local.h index ced2202c..969e2d35 100644 --- a/src/renderer/RenderWorld_local.h +++ b/src/renderer/RenderWorld_local.h @@ -296,6 +296,8 @@ class idRenderWorldLocal : public idRenderWorld { void ConvertProcToMD5R(); void ClearPortalStates(); virtual bool InitFromMap( const char *mapName ); + bool WriteLevelLoadCachePayload( idFile &file ) const; + bool ReadLevelLoadCachePayload( idFile &file, const char *expectedMapName, ID_TIME_T sourceTimestamp ); bool WriteMD5R( bool compressed ); void SetupLightGrid(); void LoadLightGridImages( bool forceReloadLoaded = false ); diff --git a/src/renderer/RendererBenchmarks.cpp b/src/renderer/RendererBenchmarks.cpp index 70dc2cb2..16378cbb 100644 --- a/src/renderer/RendererBenchmarks.cpp +++ b/src/renderer/RendererBenchmarks.cpp @@ -30,6 +30,15 @@ typedef struct rendererBenchmarkPercentiles_s { int maxMsec; } rendererBenchmarkPercentiles_t; +typedef struct rendererBenchmarkValuePercentiles_s { + int count; + unsigned long long average; + unsigned long long p50; + unsigned long long p95; + unsigned long long p99; + unsigned long long maximum; +} rendererBenchmarkValuePercentiles_t; + static const rendererBenchmarkPreset_t rg_benchmarkPresets[] = { { "low", 75, true, 4, 3, 8, 32, 16, 512, 2, 0, 33, 50 }, { "baseline", 100, false, 6, 4, 12, 64, 32, 1024, 1, 1, 20, 28 }, @@ -103,9 +112,9 @@ static int RendererBenchmarks_PercentileIndex( int sampleCount, int percentile ) return RendererBenchmarks_ClampBudgetInt( rank - 1, 0, sampleCount - 1 ); } -static void RendererBenchmarks_SortFrameTimes( int *values, int sampleCount ) { +static void RendererBenchmarks_SortValues( unsigned long long *values, int sampleCount ) { for ( int i = 1; i < sampleCount; ++i ) { - const int key = values[i]; + const unsigned long long key = values[i]; int j = i - 1; while ( j >= 0 && values[j] > key ) { values[j + 1] = values[j]; @@ -115,6 +124,32 @@ static void RendererBenchmarks_SortFrameTimes( int *values, int sampleCount ) { } } +static rendererBenchmarkValuePercentiles_t RendererBenchmarks_CalculateValuePercentiles( + const unsigned long long *sourceValues, int sampleCount ) { + rendererBenchmarkValuePercentiles_t result; + memset( &result, 0, sizeof( result ) ); + if ( sourceValues == NULL || sampleCount <= 0 ) { + return result; + } + + unsigned long long values[RENDERER_BENCHMARK_HISTORY]; + const int count = Min( sampleCount, RENDERER_BENCHMARK_HISTORY ); + long double total = 0.0L; + for ( int i = 0; i < count; ++i ) { + values[i] = sourceValues[i]; + total += static_cast( values[i] ); + } + RendererBenchmarks_SortValues( values, count ); + + result.count = count; + result.average = static_cast( total / count + 0.5L ); + result.p50 = values[RendererBenchmarks_PercentileIndex( count, 50 )]; + result.p95 = values[RendererBenchmarks_PercentileIndex( count, 95 )]; + result.p99 = values[RendererBenchmarks_PercentileIndex( count, 99 )]; + result.maximum = values[count - 1]; + return result; +} + static rendererBenchmarkPercentiles_t RendererBenchmarks_CalculatePercentiles( const rendererBenchmarkFrameSample_t *samples, int sampleCount ) { rendererBenchmarkPercentiles_t result; memset( &result, 0, sizeof( result ) ); @@ -122,33 +157,49 @@ static rendererBenchmarkPercentiles_t RendererBenchmarks_CalculatePercentiles( c return result; } - int frameTimes[RENDERER_BENCHMARK_HISTORY]; - int total = 0; + unsigned long long frameTimes[RENDERER_BENCHMARK_HISTORY]; const int count = Min( sampleCount, RENDERER_BENCHMARK_HISTORY ); for ( int i = 0; i < count; ++i ) { - frameTimes[i] = Max( 0, samples[i].frameMsec ); - total += frameTimes[i]; + frameTimes[i] = static_cast( Max( 0, samples[i].frameMsec ) ); } - RendererBenchmarks_SortFrameTimes( frameTimes, count ); - - result.averageMsec = ( total + count / 2 ) / count; - result.p50Msec = frameTimes[RendererBenchmarks_PercentileIndex( count, 50 )]; - result.p95Msec = frameTimes[RendererBenchmarks_PercentileIndex( count, 95 )]; - result.p99Msec = frameTimes[RendererBenchmarks_PercentileIndex( count, 99 )]; - result.maxMsec = frameTimes[count - 1]; + const rendererBenchmarkValuePercentiles_t values = + RendererBenchmarks_CalculateValuePercentiles( frameTimes, count ); + result.averageMsec = static_cast( values.average ); + result.p50Msec = static_cast( values.p50 ); + result.p95Msec = static_cast( values.p95 ); + result.p99Msec = static_cast( values.p99 ); + result.maxMsec = static_cast( values.maximum ); return result; } -static rendererBenchmarkPercentiles_t RendererBenchmarks_CurrentPercentiles( void ) { - rendererBenchmarkFrameSample_t orderedSamples[RENDERER_BENCHMARK_HISTORY]; - const int count = rg_benchmarkSampleCount; +static int RendererBenchmarks_CopyOrderedSamples( + rendererBenchmarkFrameSample_t *orderedSamples, int capacity ) { + if ( orderedSamples == NULL || capacity <= 0 ) { + return 0; + } + const int count = Min( rg_benchmarkSampleCount, capacity ); for ( int i = 0; i < count; ++i ) { - const int sampleIndex = ( rg_benchmarkSampleCursor - count + i + RENDERER_BENCHMARK_HISTORY ) % RENDERER_BENCHMARK_HISTORY; + const int sampleIndex = ( rg_benchmarkSampleCursor - count + i + + RENDERER_BENCHMARK_HISTORY ) % RENDERER_BENCHMARK_HISTORY; orderedSamples[i] = rg_benchmarkSamples[sampleIndex]; } + return count; +} + +static rendererBenchmarkPercentiles_t RendererBenchmarks_CurrentPercentiles( void ) { + rendererBenchmarkFrameSample_t orderedSamples[RENDERER_BENCHMARK_HISTORY]; + const int count = RendererBenchmarks_CopyOrderedSamples( + orderedSamples, RENDERER_BENCHMARK_HISTORY ); return RendererBenchmarks_CalculatePercentiles( orderedSamples, count ); } +void RendererBenchmarks_ResetHistory( void ) { + memset( rg_benchmarkSamples, 0, sizeof( rg_benchmarkSamples ) ); + memset( &rg_benchmarkLatestSample, 0, sizeof( rg_benchmarkLatestSample ) ); + rg_benchmarkSampleCursor = 0; + rg_benchmarkSampleCount = 0; +} + void RendererBenchmarks_RecordFrame( const rendererBenchmarkFrameSample_t &sample ) { rg_benchmarkLatestSample = sample; rg_benchmarkSamples[rg_benchmarkSampleCursor] = sample; @@ -247,6 +298,107 @@ void RendererBenchmarks_PrintLatestCapture( void ) { RendererBenchmarks_AdaptiveClusterGridEnabled() ? 1 : 0 ); } +static const char *RendererBenchmarks_GpuBackendName( renderGpuTimingBackend_t backend ) { + switch ( backend ) { + case RENDER_GPU_TIMING_BACKEND_OPENGL: + return "opengl"; + case RENDER_GPU_TIMING_BACKEND_VULKAN: + return "vulkan"; + default: + return "none"; + } +} + +static idStr RendererBenchmarks_NormalizeMapName( idStr mapName ) { + mapName.BackSlashesToSlashes(); + mapName.StripFileExtension(); + if ( mapName.Icmpn( "maps/", 5 ) == 0 ) { + mapName = mapName.Mid( 5, mapName.Length() - 5 ); + } + mapName.ToLower(); + if ( mapName.Length() == 0 ) { + mapName = "unknown"; + } + return mapName; +} + +static idStr RendererBenchmarks_CurrentMapName( void ) { + idStr mapName; + if ( tr.primaryWorld != NULL && tr.primaryWorld->mapName.Length() > 0 + && tr.primaryWorld->mapName != "" ) { + mapName = tr.primaryWorld->mapName; + } else { + mapName = cvarSystem->GetCVarString( "si_map" ); + } + return RendererBenchmarks_NormalizeMapName( mapName ); +} + +static int RendererBenchmarks_CollectGpuValues( + const rendererBenchmarkFrameSample_t *samples, int sampleCount, + unsigned int generation, unsigned long long *values, int capacity ) { + if ( samples == NULL || values == NULL || sampleCount <= 0 || capacity <= 0 ) { + return 0; + } + int valueCount = 0; + int acceptedFrames[RENDERER_BENCHMARK_HISTORY]; + for ( int i = 0; i < sampleCount && valueCount < capacity; ++i ) { + const rendererBenchmarkFrameSample_t &sample = samples[i]; + if ( !sample.gpuFrameTimingValid || sample.gpuFrameMicroseconds == 0 + || sample.gpuFrameGeneration != generation || sample.gpuFrameNumber < 0 ) { + continue; + } + bool duplicate = false; + for ( int existing = 0; existing < valueCount; ++existing ) { + if ( acceptedFrames[existing] == sample.gpuFrameNumber ) { + duplicate = true; + break; + } + } + if ( duplicate ) { + continue; + } + acceptedFrames[valueCount] = sample.gpuFrameNumber; + values[valueCount++] = sample.gpuFrameMicroseconds; + } + return valueCount; +} + +void RendererBenchmarks_PrintTimingMarker( void ) { + rendererBenchmarkFrameSample_t orderedSamples[RENDERER_BENCHMARK_HISTORY]; + const int orderedCount = RendererBenchmarks_CopyOrderedSamples( + orderedSamples, RENDERER_BENCHMARK_HISTORY ); + unsigned long long cpuValues[RENDERER_BENCHMARK_HISTORY]; + int cpuCount = 0; + for ( int i = 0; i < orderedCount; ++i ) { + if ( orderedSamples[i].cpuFrameMicroseconds > 0 ) { + cpuValues[cpuCount++] = orderedSamples[i].cpuFrameMicroseconds; + } + } + + renderGpuFrameTiming_t timing; + R_RendererMetrics_GetGpuFrameTiming( timing ); + unsigned long long gpuValues[RENDERER_BENCHMARK_HISTORY]; + const int gpuCount = timing.supported + ? RendererBenchmarks_CollectGpuValues( orderedSamples, orderedCount, + timing.generation, gpuValues, RENDERER_BENCHMARK_HISTORY ) + : 0; + const rendererBenchmarkValuePercentiles_t cpu = + RendererBenchmarks_CalculateValuePercentiles( cpuValues, cpuCount ); + const rendererBenchmarkValuePercentiles_t gpu = + RendererBenchmarks_CalculateValuePercentiles( gpuValues, gpuCount ); + const rendererBenchmarkBudget_t &budget = RendererBenchmarks_CurrentBudget(); + const idStr mapName = RendererBenchmarks_CurrentMapName(); + const long long gpuP50 = gpu.count > 0 ? static_cast( gpu.p50 ) : -1; + const long long gpuP95 = gpu.count > 0 ? static_cast( gpu.p95 ) : -1; + const long long gpuP99 = gpu.count > 0 ? static_cast( gpu.p99 ) : -1; + + common->Printf( + "OPENQ4_FRAME_TIMING_V1 map=%s backend=%s profile=%s cpuSamples=%d cpuP50Us=%llu cpuP95Us=%llu cpuP99Us=%llu gpuAvailable=%d gpuSamples=%d gpuP50Us=%lld gpuP95Us=%lld gpuP99Us=%lld\n", + mapName.c_str(), RendererBenchmarks_GpuBackendName( timing.backend ), + budget.presetName, cpu.count, cpu.p50, cpu.p95, cpu.p99, + gpu.count > 0 ? 1 : 0, gpu.count, gpuP50, gpuP95, gpuP99 ); +} + void RendererBenchmarks_PrintGfxInfo( void ) { const rendererBenchmarkBudget_t &budget = RendererBenchmarks_CurrentBudget(); const rendererBenchmarkPercentiles_t percentiles = RendererBenchmarks_CurrentPercentiles(); @@ -306,6 +458,34 @@ bool RendererBenchmark_RunSelfTest( void ) { ok = false; } + rendererBenchmarkFrameSample_t delayedSamples[4]; + memset( delayedSamples, 0, sizeof( delayedSamples ) ); + for ( int i = 0; i < 3; ++i ) { + delayedSamples[i].gpuFrameTimingValid = true; + delayedSamples[i].gpuFrameGeneration = 7; + delayedSamples[i].gpuFrameNumber = 100; + delayedSamples[i].gpuFrameMicroseconds = 5000; + } + delayedSamples[3].gpuFrameTimingValid = true; + delayedSamples[3].gpuFrameGeneration = 7; + delayedSamples[3].gpuFrameNumber = 101; + delayedSamples[3].gpuFrameMicroseconds = 7000; + unsigned long long delayedValues[4]; + const int delayedCount = RendererBenchmarks_CollectGpuValues( + delayedSamples, 4, 7, delayedValues, 4 ); + if ( delayedCount != 2 || delayedValues[0] != 5000 || delayedValues[1] != 7000 ) { + common->Printf( "RendererBenchmark self-test failed: delayed GPU sample dedup mismatch (count=%d)\n", + delayedCount ); + ok = false; + } + const idStr normalizedMap = RendererBenchmarks_NormalizeMapName( + idStr( "MAPS\\GAME\\STORAGE1.MAP" ) ); + if ( normalizedMap != "game/storage1" ) { + common->Printf( "RendererBenchmark self-test failed: map normalization produced '%s'\n", + normalizedMap.c_str() ); + ok = false; + } + const int presetCount = RendererBenchmarks_NumPresets(); for ( int i = 0; i < presetCount; ++i ) { if ( rg_benchmarkPresets[i].clusterTilesX < 1 || rg_benchmarkPresets[i].clusterTilesX > 8 || diff --git a/src/renderer/RendererBenchmarks.h b/src/renderer/RendererBenchmarks.h index 8656f8bc..ef79d096 100644 --- a/src/renderer/RendererBenchmarks.h +++ b/src/renderer/RendererBenchmarks.h @@ -31,7 +31,13 @@ typedef struct rendererBenchmarkFrameSample_s { int submitMsec; int backEndMsec; int presentMsec; + unsigned long long cpuFrameMicroseconds; int gpuMsec; + bool gpuFrameTimingValid; + renderGpuTimingBackend_t gpuFrameBackend; + int gpuFrameNumber; + unsigned int gpuFrameGeneration; + unsigned long long gpuFrameMicroseconds; int gpuPassMsec[RENDERER_GPU_TIMER_COUNT]; int gpuPassSamples[RENDERER_GPU_TIMER_COUNT]; int uploadBytes; @@ -63,8 +69,10 @@ typedef struct rendererBenchmarkFrameSample_s { const rendererBenchmarkBudget_t &RendererBenchmarks_CurrentBudget( void ); bool RendererBenchmarks_AdaptiveClusterGridEnabled( void ); +void RendererBenchmarks_ResetHistory( void ); void RendererBenchmarks_RecordFrame( const rendererBenchmarkFrameSample_t &sample ); void RendererBenchmarks_PrintLatestCapture( void ); +void RendererBenchmarks_PrintTimingMarker( void ); void RendererBenchmarks_PrintGfxInfo( void ); bool RendererBenchmark_RunSelfTest( void ); diff --git a/src/renderer/RendererBootstrap.cpp b/src/renderer/RendererBootstrap.cpp index 2494a01b..87a0a86c 100644 --- a/src/renderer/RendererBootstrap.cpp +++ b/src/renderer/RendererBootstrap.cpp @@ -44,6 +44,23 @@ static bool RendererBootstrap_BuildDefaultSafetyReport( idStr &report, idStr &is { "r_rendererModernSubmit", &r_rendererModernSubmit, 0 }, { "r_rendererModernAutoPromote", &r_rendererModernAutoPromote, 0 }, { "r_rendererModernVisible", &r_rendererModernVisible, 0 }, + { "r_rendererModernQuality", &r_rendererModernQuality, 1 }, + { "r_pbrMaterials", &r_pbrMaterials, 0 }, + { "r_rendererReflectionProbes", &r_rendererReflectionProbes, 0 }, + { "r_rendererClusteredDecals", &r_rendererClusteredDecals, 0 }, + { "r_rendererFroxelVolumetrics", &r_rendererFroxelVolumetrics, 0 }, + { "r_rendererSSR", &r_rendererSSR, 0 }, + { "r_rendererSSGI", &r_rendererSSGI, 0 }, + { "r_rendererSharedGui", &r_rendererSharedGui, 0 }, + { "r_rendererSharedInWorldGui", &r_rendererSharedInWorldGui, 0 }, + { "r_rendererSharedCinematicPost", &r_rendererSharedCinematicPost, 0 }, + { "r_rendererSharedSpecialFrame", &r_rendererSharedSpecialFrame, 0 }, + { "r_rendererSharedWorldAmbient", &r_rendererSharedWorldAmbient, 0 }, + { "r_rendererSharedWorldInteraction", &r_rendererSharedWorldInteraction, 0 }, + { "r_rendererSharedWorldFogBlend", &r_rendererSharedWorldFogBlend, 0 }, + { "r_rendererSharedSubview", &r_rendererSharedSubview, 0 }, + { "r_rendererSharedDeform", &r_rendererSharedDeform, 0 }, + { "r_vkShadowFallbackTest", &r_vkShadowFallbackTest, 0 }, { "r_rendererModernLightingParity", &r_rendererModernLightingParity, 0 }, { "r_rendererModernVisibleDepth", &r_rendererModernVisibleDepth, 0 }, { "r_rendererModernDepthDebug", &r_rendererModernDepthDebug, 0 }, @@ -86,7 +103,7 @@ static bool RendererBootstrap_BuildDefaultSafetyReport( idStr &report, idStr &is } report = va( - "renderer=%s glTier=%s executor=%d submit=%d autoPromote=%d promotionEvidence=%d visible=%d lightingParity=%d visibleDepth=%d depthDebug=%d opaque=%d gbufferDebug=%d deferred=%d deferredDebug=%d forwardPlus=%d clusterDebug=%d gpuValidation=%d bindless=%d shaderReload=%d rollback=%s issues=%s", + "renderer=%s glTier=%s executor=%d submit=%d autoPromote=%d promotionEvidence=%d visible=%d modernQuality=%d pbr=%d probes=%d clusteredDecals=%d froxel=%d ssr=%d ssgi=%d sharedGui=%d sharedInWorldGui=%d sharedCinematicPost=%d sharedSpecialFrame=%d sharedWorldAmbient=%d sharedWorldInteraction=%d sharedWorldFogBlend=%d sharedSubview=%d sharedDeform=%d vkShadowFallbackTest=%d lightingParity=%d visibleDepth=%d depthDebug=%d opaque=%d gbufferDebug=%d deferred=%d deferredDebug=%d forwardPlus=%d clusterDebug=%d gpuValidation=%d bindless=%d shaderReload=%d rollback=%s issues=%s", r_renderer.GetString(), r_glTier.GetString(), r_rendererModernExecutor.GetInteger(), @@ -94,6 +111,23 @@ static bool RendererBootstrap_BuildDefaultSafetyReport( idStr &report, idStr &is r_rendererModernAutoPromote.GetInteger(), r_rendererPromotionEvidence.GetString()[0] != '\0' ? 1 : 0, r_rendererModernVisible.GetInteger(), + r_rendererModernQuality.GetInteger(), + r_pbrMaterials.GetInteger(), + r_rendererReflectionProbes.GetInteger(), + r_rendererClusteredDecals.GetInteger(), + r_rendererFroxelVolumetrics.GetInteger(), + r_rendererSSR.GetInteger(), + r_rendererSSGI.GetInteger(), + r_rendererSharedGui.GetInteger(), + r_rendererSharedInWorldGui.GetInteger(), + r_rendererSharedCinematicPost.GetInteger(), + r_rendererSharedSpecialFrame.GetInteger(), + r_rendererSharedWorldAmbient.GetInteger(), + r_rendererSharedWorldInteraction.GetInteger(), + r_rendererSharedWorldFogBlend.GetInteger(), + r_rendererSharedSubview.GetInteger(), + r_rendererSharedDeform.GetInteger(), + r_vkShadowFallbackTest.GetInteger(), r_rendererModernLightingParity.GetInteger(), r_rendererModernVisibleDepth.GetInteger(), r_rendererModernDepthDebug.GetInteger(), diff --git a/src/renderer/RendererContracts.cpp b/src/renderer/RendererContracts.cpp new file mode 100644 index 00000000..9080204a --- /dev/null +++ b/src/renderer/RendererContracts.cpp @@ -0,0 +1,720 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "RendererContracts.h" + +#include +#include + +static bool RendererContracts_RegisterRefValid( const rendererRegisterRef_t &reference ) { + switch ( reference.source ) { + case RENDERER_REGISTER_UNUSED: + return true; + case RENDERER_REGISTER_INDEX: + return reference.index >= 0; + case RENDERER_REGISTER_CONSTANT: + return std::isfinite( reference.constantValue ); + default: + return false; + } +} + +rendererRegisterRef_t RendererContracts_UnusedRegister( void ) { + rendererRegisterRef_t result; + std::memset( &result, 0, sizeof( result ) ); + result.source = RENDERER_REGISTER_UNUSED; + result.index = -1; + return result; +} + +rendererRegisterRef_t RendererContracts_Register( std::int32_t index ) { + rendererRegisterRef_t result; + std::memset( &result, 0, sizeof( result ) ); + result.source = RENDERER_REGISTER_INDEX; + result.index = index; + return result; +} + +rendererRegisterRef_t RendererContracts_Constant( float value ) { + rendererRegisterRef_t result; + std::memset( &result, 0, sizeof( result ) ); + result.source = RENDERER_REGISTER_CONSTANT; + result.index = -1; + result.constantValue = value; + return result; +} + +rendererMaterialPass_t RendererContracts_DefaultMaterialPass( void ) { + rendererMaterialPass_t pass; + std::memset( &pass, 0, sizeof( pass ) ); + pass.sourceStageIndex = -1; + pass.kind = RENDERER_MATERIAL_PASS_SURFACE; + pass.textureSemantic = RENDERER_TEXTURE_NONE; + pass.condition = RendererContracts_Constant( 1.0f ); + for ( int i = 0; i < 4; ++i ) { + pass.color[ i ] = RendererContracts_Constant( 1.0f ); + } + pass.textureMatrix[ 0 ] = RendererContracts_Constant( 1.0f ); + pass.textureMatrix[ 1 ] = RendererContracts_Constant( 0.0f ); + pass.textureMatrix[ 2 ] = RendererContracts_Constant( 0.0f ); + pass.textureMatrix[ 3 ] = RendererContracts_Constant( 0.0f ); + pass.textureMatrix[ 4 ] = RendererContracts_Constant( 1.0f ); + pass.textureMatrix[ 5 ] = RendererContracts_Constant( 0.0f ); + pass.blend.enabled = false; + pass.blend.sourceColor = RENDERER_BLEND_ONE; + pass.blend.destinationColor = RENDERER_BLEND_ZERO; + pass.blend.colorOperation = RENDERER_BLEND_OP_ADD; + pass.blend.sourceAlpha = RENDERER_BLEND_ONE; + pass.blend.destinationAlpha = RENDERER_BLEND_ZERO; + pass.blend.alphaOperation = RENDERER_BLEND_OP_ADD; + pass.depth.testEnabled = true; + pass.depth.writeEnabled = true; + pass.depth.compareOperation = RENDERER_COMPARE_LESS_OR_EQUAL; + pass.cull = RENDERER_CULL_BACK; + pass.colorWriteMask = RENDERER_COLOR_WRITE_RGBA; + pass.alphaTestCompareOperation = RENDERER_COMPARE_GREATER; + pass.alphaTest = RendererContracts_Constant( 0.5f ); + pass.texgen = RENDERER_TEXGEN_EXPLICIT; + pass.vertexColor = RENDERER_VERTEX_COLOR_IGNORE; + pass.polygonOffsetFactor = RendererContracts_Constant( 0.0f ); + pass.polygonOffsetUnits = RendererContracts_Constant( 0.0f ); + pass.programFamily = RENDERER_PROGRAM_FIXED; + return pass; +} + +void RendererContracts_ResetMaterialPassList( rendererMaterialPassList_t &list ) { + std::memset( &list, 0, sizeof( list ) ); +} + +bool RendererContracts_ValidateMaterialPass( const rendererMaterialPass_t &pass ) { + if ( pass.sourceStageIndex < 0 + || pass.kind < RENDERER_MATERIAL_PASS_SURFACE + || pass.kind > RENDERER_MATERIAL_PASS_POST_PROCESS + || pass.textureSemantic < RENDERER_TEXTURE_NONE + || pass.textureSemantic > RENDERER_TEXTURE_CUSTOM + || pass.blend.sourceColor < RENDERER_BLEND_ZERO + || pass.blend.sourceColor > RENDERER_BLEND_SRC_ALPHA_SATURATE + || pass.blend.destinationColor < RENDERER_BLEND_ZERO + || pass.blend.destinationColor > RENDERER_BLEND_SRC_ALPHA_SATURATE + || pass.blend.sourceAlpha < RENDERER_BLEND_ZERO + || pass.blend.sourceAlpha > RENDERER_BLEND_SRC_ALPHA_SATURATE + || pass.blend.destinationAlpha < RENDERER_BLEND_ZERO + || pass.blend.destinationAlpha > RENDERER_BLEND_SRC_ALPHA_SATURATE + || pass.blend.colorOperation < RENDERER_BLEND_OP_ADD + || pass.blend.colorOperation > RENDERER_BLEND_OP_MAX + || pass.blend.alphaOperation < RENDERER_BLEND_OP_ADD + || pass.blend.alphaOperation > RENDERER_BLEND_OP_MAX + || pass.depth.compareOperation < RENDERER_COMPARE_NEVER + || pass.depth.compareOperation > RENDERER_COMPARE_ALWAYS + || pass.alphaTestCompareOperation < RENDERER_COMPARE_NEVER + || pass.alphaTestCompareOperation > RENDERER_COMPARE_ALWAYS + || pass.cull < RENDERER_CULL_NONE || pass.cull > RENDERER_CULL_BACK + || ( pass.colorWriteMask & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 + || pass.texgen < RENDERER_TEXGEN_EXPLICIT || pass.texgen > RENDERER_TEXGEN_GLASS_WARP + || pass.vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || pass.vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || pass.programFamily < RENDERER_PROGRAM_FIXED + || pass.programFamily > RENDERER_PROGRAM_CUSTOM ) { + return false; + } + if ( !RendererContracts_RegisterRefValid( pass.condition ) + || !RendererContracts_RegisterRefValid( pass.alphaTest ) + || !RendererContracts_RegisterRefValid( pass.polygonOffsetFactor ) + || !RendererContracts_RegisterRefValid( pass.polygonOffsetUnits ) ) { + return false; + } + for ( int i = 0; i < 4; ++i ) { + if ( !RendererContracts_RegisterRefValid( pass.color[ i ] ) ) { + return false; + } + } + for ( int i = 0; i < 6; ++i ) { + if ( !RendererContracts_RegisterRefValid( pass.textureMatrix[ i ] ) ) { + return false; + } + } + return true; +} + +bool RendererContracts_AppendMaterialPass( rendererMaterialPassList_t &list, + const rendererMaterialPass_t &pass ) { + if ( !RendererContracts_ValidateMaterialPass( pass ) ) { + return false; + } + if ( list.count >= RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) { + list.overflowed = true; + return false; + } + list.passes[ list.count ] = pass; + list.passes[ list.count ].order = list.count; + list.count++; + return true; +} + +static_assert( sizeof( rendererEvaluatedMaterialPass_t ) < sizeof( rendererMaterialPass_t ), + "evaluated material passes must remain smaller than register-reference records" ); + +void RendererContracts_ResetEvaluatedMaterialPassList( + rendererEvaluatedMaterialPassList_t &list ) { + std::memset( &list, 0, sizeof( list ) ); +} + +static bool RendererContracts_RegisterRefHasNonFiniteConstant( + const rendererRegisterRef_t &reference ) { + return reference.source == RENDERER_REGISTER_CONSTANT + && !std::isfinite( reference.constantValue ); +} + +static bool RendererContracts_MaterialPassHasNonFiniteConstant( + const rendererMaterialPass_t &pass ) { + if ( RendererContracts_RegisterRefHasNonFiniteConstant( pass.condition ) + || RendererContracts_RegisterRefHasNonFiniteConstant( pass.alphaTest ) + || RendererContracts_RegisterRefHasNonFiniteConstant( pass.polygonOffsetFactor ) + || RendererContracts_RegisterRefHasNonFiniteConstant( pass.polygonOffsetUnits ) ) { + return true; + } + for ( int i = 0; i < 4; ++i ) { + if ( RendererContracts_RegisterRefHasNonFiniteConstant( pass.color[ i ] ) ) { + return true; + } + } + for ( int i = 0; i < 6; ++i ) { + if ( RendererContracts_RegisterRefHasNonFiniteConstant( pass.textureMatrix[ i ] ) ) { + return true; + } + } + return false; +} + +static rendererMaterialPassEvaluationStatus_t RendererContracts_ResolveRegisterRef( + float &destination, const rendererRegisterRef_t &reference, + const float *registers, std::uint32_t registerCount, float unusedValue ) { + switch ( reference.source ) { + case RENDERER_REGISTER_UNUSED: + destination = unusedValue; + return RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; + case RENDERER_REGISTER_CONSTANT: + if ( !std::isfinite( reference.constantValue ) ) { + return RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE; + } + destination = reference.constantValue; + return RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; + case RENDERER_REGISTER_INDEX: + if ( reference.index < 0 ) { + return RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS; + } + if ( registers == NULL || registerCount == 0 ) { + return RENDERER_MATERIAL_PASS_EVALUATION_REGISTERS_UNAVAILABLE; + } + if ( static_cast( reference.index ) >= registerCount ) { + return RENDERER_MATERIAL_PASS_EVALUATION_REGISTER_OUT_OF_RANGE; + } + destination = registers[ reference.index ]; + if ( !std::isfinite( destination ) ) { + return RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE; + } + return RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; + default: + return RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS; + } +} + +static rendererMaterialPassEvaluationStatus_t RendererContracts_EvaluateMaterialPass( + rendererEvaluatedMaterialPass_t &destination, + const rendererMaterialPass_t &source, + const float *registers, std::uint32_t registerCount ) { + if ( RendererContracts_MaterialPassHasNonFiniteConstant( source ) ) { + return RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE; + } + if ( !RendererContracts_ValidateMaterialPass( source ) ) { + return RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS; + } + + rendererEvaluatedMaterialPass_t evaluated; + std::memset( &evaluated, 0, sizeof( evaluated ) ); + evaluated.order = source.order; + evaluated.sourceStageIndex = source.sourceStageIndex; + evaluated.kind = source.kind; + evaluated.textureSemantic = source.textureSemantic; + evaluated.textureResourceId = source.textureResourceId; + evaluated.blend = source.blend; + evaluated.depth = source.depth; + evaluated.cull = source.cull; + evaluated.colorWriteMask = source.colorWriteMask; + evaluated.alphaTestEnabled = source.alphaTestEnabled; + evaluated.alphaTestCompareOperation = source.alphaTestCompareOperation; + evaluated.texgen = source.texgen; + evaluated.vertexColor = source.vertexColor; + evaluated.polygonOffsetEnabled = source.polygonOffsetEnabled; + evaluated.programFamily = source.programFamily; + evaluated.programKey = source.programKey; + + rendererMaterialPassEvaluationStatus_t status = RendererContracts_ResolveRegisterRef( + evaluated.condition, source.condition, registers, registerCount, 1.0f ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + for ( int i = 0; i < 4; ++i ) { + status = RendererContracts_ResolveRegisterRef( + evaluated.color[ i ], source.color[ i ], registers, registerCount, 1.0f ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + } + static const float unusedTextureMatrix[ 6 ] = { 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }; + for ( int i = 0; i < 6; ++i ) { + status = RendererContracts_ResolveRegisterRef( + evaluated.textureMatrix[ i ], source.textureMatrix[ i ], registers, + registerCount, unusedTextureMatrix[ i ] ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + } + status = RendererContracts_ResolveRegisterRef( + evaluated.alphaTest, source.alphaTest, registers, registerCount, 0.5f ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + status = RendererContracts_ResolveRegisterRef( + evaluated.polygonOffsetFactor, source.polygonOffsetFactor, + registers, registerCount, 0.0f ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + status = RendererContracts_ResolveRegisterRef( + evaluated.polygonOffsetUnits, source.polygonOffsetUnits, + registers, registerCount, 0.0f ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + return status; + } + + evaluated.active = evaluated.condition != 0.0f; + if ( !evaluated.active ) { + evaluated.disposition = RENDERER_MATERIAL_PASS_INACTIVE_CONDITION; + } else if ( evaluated.blend.enabled + && evaluated.blend.sourceColor == RENDERER_BLEND_ZERO + && evaluated.blend.destinationColor == RENDERER_BLEND_ONE ) { + evaluated.disposition = RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND; + } else if ( evaluated.blend.enabled + && evaluated.blend.sourceColor == RENDERER_BLEND_ONE + && evaluated.blend.destinationColor == RENDERER_BLEND_ONE + && evaluated.color[ 0 ] <= 0.0f + && evaluated.color[ 1 ] <= 0.0f + && evaluated.color[ 2 ] <= 0.0f ) { + evaluated.disposition = RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE; + } else if ( evaluated.blend.enabled + && evaluated.blend.sourceColor == RENDERER_BLEND_SRC_ALPHA + && evaluated.blend.destinationColor == RENDERER_BLEND_ONE_MINUS_SRC_ALPHA + && evaluated.color[ 3 ] <= 0.0f ) { + evaluated.disposition = RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA; + } else { + evaluated.disposition = RENDERER_MATERIAL_PASS_DRAW; + } + destination = evaluated; + return RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; +} + +rendererMaterialPassEvaluationStatus_t RendererContracts_EvaluateMaterialPassList( + rendererEvaluatedMaterialPassList_t &destination, + const rendererMaterialPassList_t &source, + const float *registers, std::uint32_t registerCount ) { + RendererContracts_ResetEvaluatedMaterialPassList( destination ); + if ( source.overflowed || source.count > RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) { + return RENDERER_MATERIAL_PASS_EVALUATION_SOURCE_OVERFLOW; + } + if ( registers == NULL && registerCount != 0 ) { + return RENDERER_MATERIAL_PASS_EVALUATION_REGISTERS_UNAVAILABLE; + } + + for ( std::uint32_t i = 0; i < source.count; ++i ) { + const rendererMaterialPassEvaluationStatus_t status = + RendererContracts_EvaluateMaterialPass( + destination.passes[ i ], source.passes[ i ], registers, registerCount ); + if ( status != RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS ) { + RendererContracts_ResetEvaluatedMaterialPassList( destination ); + return status; + } + destination.count++; + if ( destination.passes[ i ].active ) { + destination.activeCount++; + } + } + return RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; +} + +rendererClipSpaceConvention_t RendererContracts_GLClipSpace( void ) { + rendererClipSpaceConvention_t convention; + convention.depthRange = RENDERER_CLIP_DEPTH_NEGATIVE_ONE_TO_ONE; + convention.framebufferOrigin = RENDERER_FRAMEBUFFER_ORIGIN_LOWER_LEFT; + convention.viewportYAxis = RENDERER_VIEWPORT_Y_POSITIVE; + convention.frontFace = RENDERER_FRONT_FACE_COUNTER_CLOCKWISE; + return convention; +} + +rendererClipSpaceConvention_t RendererContracts_VulkanClipSpace( void ) { + rendererClipSpaceConvention_t convention; + convention.depthRange = RENDERER_CLIP_DEPTH_ZERO_TO_ONE; + convention.framebufferOrigin = RENDERER_FRAMEBUFFER_ORIGIN_UPPER_LEFT; + convention.viewportYAxis = RENDERER_VIEWPORT_Y_NEGATIVE; + convention.frontFace = RENDERER_FRONT_FACE_COUNTER_CLOCKWISE; + return convention; +} + +static bool RendererContracts_ClipConventionValid( const rendererClipSpaceConvention_t &convention ) { + return convention.depthRange >= RENDERER_CLIP_DEPTH_NEGATIVE_ONE_TO_ONE + && convention.depthRange <= RENDERER_CLIP_DEPTH_ZERO_TO_ONE + && convention.framebufferOrigin >= RENDERER_FRAMEBUFFER_ORIGIN_LOWER_LEFT + && convention.framebufferOrigin <= RENDERER_FRAMEBUFFER_ORIGIN_UPPER_LEFT + && convention.viewportYAxis >= RENDERER_VIEWPORT_Y_POSITIVE + && convention.viewportYAxis <= RENDERER_VIEWPORT_Y_NEGATIVE + && convention.frontFace >= RENDERER_FRONT_FACE_COUNTER_CLOCKWISE + && convention.frontFace <= RENDERER_FRONT_FACE_CLOCKWISE; +} + +bool RendererContracts_ConvertClipMatrix( float destination[ 16 ], + const float source[ 16 ], const rendererClipSpaceConvention_t &sourceConvention, + const rendererClipSpaceConvention_t &destinationConvention ) { + if ( destination == NULL || source == NULL + || !RendererContracts_ClipConventionValid( sourceConvention ) + || !RendererContracts_ClipConventionValid( destinationConvention ) ) { + return false; + } + float canonical[ 16 ]; + std::memcpy( canonical, source, sizeof( canonical ) ); + std::memcpy( destination, canonical, sizeof( canonical ) ); + if ( sourceConvention.depthRange == destinationConvention.depthRange ) { + return true; + } + for ( int column = 0; column < 4; ++column ) { + const int z = column * 4 + 2; + const int w = column * 4 + 3; + if ( sourceConvention.depthRange == RENDERER_CLIP_DEPTH_NEGATIVE_ONE_TO_ONE ) { + destination[ z ] = 0.5f * ( canonical[ z ] + canonical[ w ] ); + } else { + destination[ z ] = 2.0f * canonical[ z ] - canonical[ w ]; + } + } + return true; +} + +bool RendererContracts_BuildViewport( rendererBackendViewport_t &destination, + const rendererCanonicalViewport_t &source, float framebufferHeight, + const rendererClipSpaceConvention_t &destinationConvention ) { + if ( !RendererContracts_ClipConventionValid( destinationConvention ) + || !std::isfinite( source.x ) || !std::isfinite( source.y ) + || !std::isfinite( source.width ) || !std::isfinite( source.height ) + || !std::isfinite( source.minDepth ) || !std::isfinite( source.maxDepth ) + || !std::isfinite( framebufferHeight ) || framebufferHeight <= 0.0f + || source.width <= 0.0f || source.height <= 0.0f + || source.minDepth < 0.0f || source.maxDepth > 1.0f + || source.minDepth > source.maxDepth ) { + return false; + } + destination.x = source.x; + destination.width = source.width; + destination.minDepth = source.minDepth; + destination.maxDepth = source.maxDepth; + if ( destinationConvention.framebufferOrigin == RENDERER_FRAMEBUFFER_ORIGIN_UPPER_LEFT ) { + if ( destinationConvention.viewportYAxis == RENDERER_VIEWPORT_Y_NEGATIVE ) { + destination.y = framebufferHeight - source.y; + destination.height = -source.height; + } else { + destination.y = framebufferHeight - source.y - source.height; + destination.height = source.height; + } + } else if ( destinationConvention.viewportYAxis == RENDERER_VIEWPORT_Y_NEGATIVE ) { + destination.y = source.y + source.height; + destination.height = -source.height; + } else { + destination.y = source.y; + destination.height = source.height; + } + return true; +} + +static const rendererVertexLayoutDesc_t rendererLegacyDrawVertLayout = { + 1, + 6, + { + { 0, 64, RENDERER_VERTEX_RATE_PER_VERTEX }, + { 0, 0, RENDERER_VERTEX_RATE_PER_VERTEX }, + { 0, 0, RENDERER_VERTEX_RATE_PER_VERTEX } + }, + { + { RENDERER_VERTEX_SEMANTIC_POSITION, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 0 }, + { RENDERER_VERTEX_SEMANTIC_COLOR0, RENDERER_VERTEX_FORMAT_UNORM8X4, 0, 12 }, + { RENDERER_VERTEX_SEMANTIC_NORMAL, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 16 }, + { RENDERER_VERTEX_SEMANTIC_TANGENT0, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 32 }, + { RENDERER_VERTEX_SEMANTIC_TANGENT1, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 44 }, + { RENDERER_VERTEX_SEMANTIC_TEXCOORD0, RENDERER_VERTEX_FORMAT_FLOAT32X2, 0, 56 } + } +}; + +static const rendererVertexLayoutDesc_t rendererSkinnedDrawVertLayout = { + 2, + 8, + { + { 0, 64, RENDERER_VERTEX_RATE_PER_VERTEX }, + { 1, 32, RENDERER_VERTEX_RATE_PER_VERTEX }, + { 0, 0, RENDERER_VERTEX_RATE_PER_VERTEX } + }, + { + { RENDERER_VERTEX_SEMANTIC_POSITION, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 0 }, + { RENDERER_VERTEX_SEMANTIC_COLOR0, RENDERER_VERTEX_FORMAT_UNORM8X4, 0, 12 }, + { RENDERER_VERTEX_SEMANTIC_NORMAL, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 16 }, + { RENDERER_VERTEX_SEMANTIC_TANGENT0, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 32 }, + { RENDERER_VERTEX_SEMANTIC_TANGENT1, RENDERER_VERTEX_FORMAT_FLOAT32X3, 0, 44 }, + { RENDERER_VERTEX_SEMANTIC_TEXCOORD0, RENDERER_VERTEX_FORMAT_FLOAT32X2, 0, 56 }, + { RENDERER_VERTEX_SEMANTIC_JOINT_INDICES, RENDERER_VERTEX_FORMAT_UINT32X4, 1, 0 }, + { RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS, RENDERER_VERTEX_FORMAT_FLOAT32X4, 1, 16 } + } +}; + +const rendererVertexLayoutDesc_t &RendererContracts_LegacyDrawVertLayout( void ) { + return rendererLegacyDrawVertLayout; +} + +const rendererVertexLayoutDesc_t &RendererContracts_SkinnedDrawVertLayout( void ) { + return rendererSkinnedDrawVertLayout; +} + +std::uint32_t RendererContracts_VertexFormatSize( rendererVertexFormat_t format ) { + switch ( format ) { + case RENDERER_VERTEX_FORMAT_FLOAT32X2: + return 8; + case RENDERER_VERTEX_FORMAT_FLOAT32X3: + return 12; + case RENDERER_VERTEX_FORMAT_FLOAT32X4: + case RENDERER_VERTEX_FORMAT_UINT32X4: + return 16; + case RENDERER_VERTEX_FORMAT_UNORM8X4: + return 4; + default: + return 0; + } +} + +static const rendererVertexBindingDesc_t *RendererContracts_FindVertexBinding( + const rendererVertexLayoutDesc_t &layout, std::uint32_t binding ) { + for ( std::uint32_t i = 0; i < layout.bindingCount; ++i ) { + if ( layout.bindings[ i ].binding == binding ) { + return &layout.bindings[ i ]; + } + } + return NULL; +} + +bool RendererContracts_ValidateVertexLayout( const rendererVertexLayoutDesc_t &layout ) { + if ( layout.bindingCount == 0 || layout.bindingCount > RENDERER_CONTRACT_MAX_VERTEX_BINDINGS + || layout.attributeCount == 0 || layout.attributeCount > RENDERER_CONTRACT_MAX_VERTEX_ATTRIBUTES ) { + return false; + } + for ( std::uint32_t i = 0; i < layout.bindingCount; ++i ) { + const rendererVertexBindingDesc_t &binding = layout.bindings[ i ]; + if ( binding.stride == 0 || binding.inputRate < RENDERER_VERTEX_RATE_PER_VERTEX + || binding.inputRate > RENDERER_VERTEX_RATE_PER_INSTANCE ) { + return false; + } + for ( std::uint32_t j = i + 1; j < layout.bindingCount; ++j ) { + if ( binding.binding == layout.bindings[ j ].binding ) { + return false; + } + } + } + for ( std::uint32_t i = 0; i < layout.attributeCount; ++i ) { + const rendererVertexAttributeDesc_t &attribute = layout.attributes[ i ]; + if ( attribute.semantic < RENDERER_VERTEX_SEMANTIC_POSITION + || attribute.semantic > RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS ) { + return false; + } + const rendererVertexBindingDesc_t *binding = + RendererContracts_FindVertexBinding( layout, attribute.binding ); + const std::uint32_t formatSize = RendererContracts_VertexFormatSize( attribute.format ); + if ( binding == NULL || formatSize == 0 || attribute.offset > binding->stride + || formatSize > binding->stride - attribute.offset ) { + return false; + } + for ( std::uint32_t j = i + 1; j < layout.attributeCount; ++j ) { + const rendererVertexAttributeDesc_t &other = layout.attributes[ j ]; + if ( attribute.semantic == other.semantic ) { + return false; + } + if ( attribute.binding != other.binding ) { + continue; + } + const std::uint32_t otherSize = RendererContracts_VertexFormatSize( other.format ); + if ( otherSize != 0 && attribute.offset < other.offset + otherSize + && other.offset < attribute.offset + formatSize ) { + return false; + } + } + } + return true; +} + +const rendererVertexAttributeDesc_t *RendererContracts_FindVertexAttribute( + const rendererVertexLayoutDesc_t &layout, rendererVertexSemantic_t semantic ) { + if ( layout.attributeCount > RENDERER_CONTRACT_MAX_VERTEX_ATTRIBUTES ) { + return NULL; + } + for ( std::uint32_t i = 0; i < layout.attributeCount; ++i ) { + if ( layout.attributes[ i ].semantic == semantic ) { + return &layout.attributes[ i ]; + } + } + return NULL; +} + +static const std::uint64_t RENDERER_BUFFER_SLOT_MASK = 0x0000000000ffffffULL; +static const std::uint64_t RENDERER_BUFFER_GENERATION_MASK = 0x0000ffffff000000ULL; +static const std::uint64_t RENDERER_BUFFER_KIND_MASK = 0x00ff000000000000ULL; +static const std::uint64_t RENDERER_BUFFER_LIFETIME_MASK = 0x0f00000000000000ULL; +static const std::uint64_t RENDERER_BUFFER_RESERVED_MASK = 0xf000000000000000ULL; + +static_assert( sizeof( rendererBufferHandle_t ) == sizeof( std::uint64_t ), + "renderer buffer handles must remain opaque 64-bit values" ); + +rendererBufferHandle_t RendererContracts_InvalidBufferHandle( void ) { + rendererBufferHandle_t result; + result.value = 0; + return result; +} + +rendererBufferHandle_t RendererContracts_MakeBufferHandle( rendererBufferKind_t kind, + rendererBufferLifetime_t lifetime, std::uint32_t slot, std::uint32_t generation ) { + rendererBufferHandle_t result = RendererContracts_InvalidBufferHandle(); + if ( kind < RENDERER_BUFFER_KIND_VERTEX || kind > RENDERER_BUFFER_KIND_JOINT_PALETTE + || lifetime < RENDERER_BUFFER_LIFETIME_STATIC || lifetime > RENDERER_BUFFER_LIFETIME_FRAME + || slot >= 0x00ffffffu || generation == 0 || generation > 0x00ffffffu ) { + return result; + } + result.value = static_cast( slot + 1 ) + | ( static_cast( generation ) << 24 ) + | ( static_cast( kind ) << 48 ) + | ( static_cast( lifetime ) << 56 ); + return result; +} + +bool RendererContracts_DecodeBufferHandle( rendererBufferHandle_t handle, + rendererBufferKind_t &kind, rendererBufferLifetime_t &lifetime, + std::uint32_t &slot, std::uint32_t &generation ) { + if ( handle.value == 0 || ( handle.value & RENDERER_BUFFER_RESERVED_MASK ) != 0 ) { + return false; + } + const std::uint32_t encodedSlot = static_cast( handle.value & RENDERER_BUFFER_SLOT_MASK ); + generation = static_cast( ( handle.value & RENDERER_BUFFER_GENERATION_MASK ) >> 24 ); + kind = static_cast( ( handle.value & RENDERER_BUFFER_KIND_MASK ) >> 48 ); + lifetime = static_cast( ( handle.value & RENDERER_BUFFER_LIFETIME_MASK ) >> 56 ); + if ( encodedSlot == 0 || generation == 0 + || kind < RENDERER_BUFFER_KIND_VERTEX || kind > RENDERER_BUFFER_KIND_JOINT_PALETTE + || lifetime < RENDERER_BUFFER_LIFETIME_STATIC || lifetime > RENDERER_BUFFER_LIFETIME_FRAME ) { + return false; + } + slot = encodedSlot - 1; + return true; +} + +rendererBufferSliceValidation_t RendererContracts_ValidateBufferSlice( + const rendererBufferSlice_t &slice, const rendererBufferRecord_t &record, + rendererBufferKind_t expectedKind, std::uint64_t currentFrameSerial ) { + rendererBufferKind_t sliceKind; + rendererBufferLifetime_t sliceLifetime; + std::uint32_t sliceSlot; + std::uint32_t sliceGeneration; + rendererBufferKind_t recordKind; + rendererBufferLifetime_t recordLifetime; + std::uint32_t recordSlot; + std::uint32_t recordGeneration; + if ( !RendererContracts_DecodeBufferHandle( slice.handle, sliceKind, sliceLifetime, + sliceSlot, sliceGeneration ) + || !RendererContracts_DecodeBufferHandle( record.handle, recordKind, recordLifetime, + recordSlot, recordGeneration ) ) { + return RENDERER_BUFFER_SLICE_INVALID_HANDLE; + } + if ( sliceKind != expectedKind || recordKind != expectedKind ) { + return RENDERER_BUFFER_SLICE_WRONG_KIND; + } + if ( sliceSlot != recordSlot ) { + return RENDERER_BUFFER_SLICE_WRONG_SLOT; + } + if ( sliceGeneration != recordGeneration ) { + return RENDERER_BUFFER_SLICE_STALE_GENERATION; + } + if ( sliceLifetime != recordLifetime ) { + return RENDERER_BUFFER_SLICE_WRONG_LIFETIME; + } + if ( recordLifetime == RENDERER_BUFFER_LIFETIME_FRAME + && record.frameSerial != currentFrameSerial ) { + return RENDERER_BUFFER_SLICE_EXPIRED; + } + if ( slice.sizeBytes == 0 ) { + return RENDERER_BUFFER_SLICE_EMPTY; + } + if ( slice.offsetBytes > record.capacityBytes + || slice.sizeBytes > record.capacityBytes - slice.offsetBytes ) { + return RENDERER_BUFFER_SLICE_RANGE_OVERFLOW; + } + return RENDERER_BUFFER_SLICE_VALID; +} + +bool RendererContracts_RunSelfTest( void ) { + rendererMaterialPassList_t passes; + RendererContracts_ResetMaterialPassList( passes ); + rendererMaterialPass_t first = RendererContracts_DefaultMaterialPass(); + first.sourceStageIndex = 2; + first.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + first.condition = RendererContracts_Register( 4 ); + rendererMaterialPass_t second = first; + second.sourceStageIndex = 5; + second.condition = RendererContracts_Register( 9 ); + if ( !RendererContracts_AppendMaterialPass( passes, first ) + || !RendererContracts_AppendMaterialPass( passes, second ) + || passes.count != 2 || passes.passes[ 0 ].order != 0 + || passes.passes[ 1 ].order != 1 + || passes.passes[ 0 ].textureSemantic != passes.passes[ 1 ].textureSemantic + || passes.passes[ 0 ].condition.index != 4 + || passes.passes[ 1 ].condition.index != 9 ) { + return false; + } + + float identity[ 16 ]; + std::memset( identity, 0, sizeof( identity ) ); + identity[ 0 ] = identity[ 5 ] = identity[ 10 ] = identity[ 15 ] = 1.0f; + float converted[ 16 ]; + if ( !RendererContracts_ConvertClipMatrix( converted, identity, + RendererContracts_GLClipSpace(), RendererContracts_VulkanClipSpace() ) + || converted[ 10 ] != 0.5f || converted[ 14 ] != 0.5f + || !RendererContracts_ConvertClipMatrix( converted, converted, + RendererContracts_VulkanClipSpace(), RendererContracts_GLClipSpace() ) + || std::memcmp( converted, identity, sizeof( identity ) ) != 0 ) { + return false; + } + + if ( !RendererContracts_ValidateVertexLayout( RendererContracts_LegacyDrawVertLayout() ) + || !RendererContracts_ValidateVertexLayout( RendererContracts_SkinnedDrawVertLayout() ) ) { + return false; + } + const rendererVertexAttributeDesc_t *joints = RendererContracts_FindVertexAttribute( + RendererContracts_SkinnedDrawVertLayout(), RENDERER_VERTEX_SEMANTIC_JOINT_INDICES ); + const rendererVertexAttributeDesc_t *weights = RendererContracts_FindVertexAttribute( + RendererContracts_SkinnedDrawVertLayout(), RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS ); + if ( joints == NULL || weights == NULL || joints->binding != 1 || joints->offset != 0 + || joints->format != RENDERER_VERTEX_FORMAT_UINT32X4 + || weights->binding != 1 || weights->offset != 16 + || weights->format != RENDERER_VERTEX_FORMAT_FLOAT32X4 ) { + return false; + } + + const rendererBufferHandle_t handle = RendererContracts_MakeBufferHandle( + RENDERER_BUFFER_KIND_VERTEX, RENDERER_BUFFER_LIFETIME_FRAME, 3, 11 ); + rendererBufferRecord_t record = { handle, 1024, 17 }; + rendererBufferSlice_t slice = { handle, 64, 128 }; + if ( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 17 ) != RENDERER_BUFFER_SLICE_VALID ) { + return false; + } + slice.handle = RendererContracts_MakeBufferHandle( + RENDERER_BUFFER_KIND_VERTEX, RENDERER_BUFFER_LIFETIME_FRAME, 3, 12 ); + return RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 17 ) == RENDERER_BUFFER_SLICE_STALE_GENERATION; +} diff --git a/src/renderer/RendererContracts.h b/src/renderer/RendererContracts.h new file mode 100644 index 00000000..266ad196 --- /dev/null +++ b/src/renderer/RendererContracts.h @@ -0,0 +1,432 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __RENDERER_CONTRACTS_H__ +#define __RENDERER_CONTRACTS_H__ + +#include +#include + +/* +=============================================================================== + + Backend-neutral renderer contracts. + + This header deliberately contains no idlib, OpenGL, or Vulkan types. The + front end can therefore describe material work, clip-space conversion, + vertex input, and buffer slices once while each backend retains ownership of + its native objects. + +=============================================================================== +*/ + +const std::uint32_t RENDERER_CONTRACT_MAX_MATERIAL_PASSES = 256; +const std::uint32_t RENDERER_CONTRACT_MAX_VERTEX_BINDINGS = 3; +const std::uint32_t RENDERER_CONTRACT_MAX_VERTEX_ATTRIBUTES = 10; + +enum rendererRegisterSource_t : int { + RENDERER_REGISTER_UNUSED = 0, + RENDERER_REGISTER_INDEX, + RENDERER_REGISTER_CONSTANT +}; + +typedef struct rendererRegisterRef_s { + rendererRegisterSource_t source; + std::int32_t index; + float constantValue; +} rendererRegisterRef_t; + +rendererRegisterRef_t RendererContracts_UnusedRegister( void ); +rendererRegisterRef_t RendererContracts_Register( std::int32_t index ); +rendererRegisterRef_t RendererContracts_Constant( float value ); + +enum rendererMaterialPassKind_t : int { + RENDERER_MATERIAL_PASS_SURFACE = 0, + RENDERER_MATERIAL_PASS_DEPTH, + RENDERER_MATERIAL_PASS_INTERACTION, + RENDERER_MATERIAL_PASS_SHADOW, + RENDERER_MATERIAL_PASS_GUI, + RENDERER_MATERIAL_PASS_POST_PROCESS +}; + +enum rendererMaterialTextureSemantic_t : int { + RENDERER_TEXTURE_NONE = 0, + RENDERER_TEXTURE_DIFFUSE, + RENDERER_TEXTURE_NORMAL, + RENDERER_TEXTURE_SPECULAR, + RENDERER_TEXTURE_HEIGHT, + RENDERER_TEXTURE_EMISSIVE, + RENDERER_TEXTURE_LIGHT, + RENDERER_TEXTURE_FALLOFF, + RENDERER_TEXTURE_CURRENT_RENDER, + RENDERER_TEXTURE_CURRENT_DEPTH, + RENDERER_TEXTURE_CUBE, + RENDERER_TEXTURE_CUSTOM +}; + +enum rendererBlendFactor_t : int { + RENDERER_BLEND_ZERO = 0, + RENDERER_BLEND_ONE, + RENDERER_BLEND_SRC_COLOR, + RENDERER_BLEND_ONE_MINUS_SRC_COLOR, + RENDERER_BLEND_DST_COLOR, + RENDERER_BLEND_ONE_MINUS_DST_COLOR, + RENDERER_BLEND_SRC_ALPHA, + RENDERER_BLEND_ONE_MINUS_SRC_ALPHA, + RENDERER_BLEND_DST_ALPHA, + RENDERER_BLEND_ONE_MINUS_DST_ALPHA, + RENDERER_BLEND_SRC_ALPHA_SATURATE +}; + +enum rendererBlendOp_t : int { + RENDERER_BLEND_OP_ADD = 0, + RENDERER_BLEND_OP_SUBTRACT, + RENDERER_BLEND_OP_REVERSE_SUBTRACT, + RENDERER_BLEND_OP_MIN, + RENDERER_BLEND_OP_MAX +}; + +typedef struct rendererBlendState_s { + bool enabled; + rendererBlendFactor_t sourceColor; + rendererBlendFactor_t destinationColor; + rendererBlendOp_t colorOperation; + rendererBlendFactor_t sourceAlpha; + rendererBlendFactor_t destinationAlpha; + rendererBlendOp_t alphaOperation; +} rendererBlendState_t; + +enum rendererCompareOp_t : int { + RENDERER_COMPARE_NEVER = 0, + RENDERER_COMPARE_LESS, + RENDERER_COMPARE_EQUAL, + RENDERER_COMPARE_LESS_OR_EQUAL, + RENDERER_COMPARE_GREATER, + RENDERER_COMPARE_NOT_EQUAL, + RENDERER_COMPARE_GREATER_OR_EQUAL, + RENDERER_COMPARE_ALWAYS +}; + +typedef struct rendererDepthState_s { + bool testEnabled; + bool writeEnabled; + rendererCompareOp_t compareOperation; +} rendererDepthState_t; + +enum rendererCullMode_t : int { + RENDERER_CULL_NONE = 0, + RENDERER_CULL_FRONT, + RENDERER_CULL_BACK +}; + +enum rendererColorWriteMask_t : int { + RENDERER_COLOR_WRITE_RED = 1u << 0, + RENDERER_COLOR_WRITE_GREEN = 1u << 1, + RENDERER_COLOR_WRITE_BLUE = 1u << 2, + RENDERER_COLOR_WRITE_ALPHA = 1u << 3, + RENDERER_COLOR_WRITE_RGBA = RENDERER_COLOR_WRITE_RED + | RENDERER_COLOR_WRITE_GREEN + | RENDERER_COLOR_WRITE_BLUE + | RENDERER_COLOR_WRITE_ALPHA +}; + +enum rendererTexGen_t : int { + RENDERER_TEXGEN_EXPLICIT = 0, + RENDERER_TEXGEN_SCREEN, + RENDERER_TEXGEN_SKYBOX_CUBE, + RENDERER_TEXGEN_WOBBLESKY_CUBE, + RENDERER_TEXGEN_DIFFUSE_CUBE, + RENDERER_TEXGEN_REFLECT_CUBE, + RENDERER_TEXGEN_GLASS_WARP +}; + +enum rendererVertexColorMode_t : int { + RENDERER_VERTEX_COLOR_IGNORE = 0, + RENDERER_VERTEX_COLOR_MODULATE, + RENDERER_VERTEX_COLOR_INVERSE_MODULATE +}; + +enum rendererMaterialProgramFamily_t : int { + RENDERER_PROGRAM_FIXED = 0, + RENDERER_PROGRAM_DEPTH, + RENDERER_PROGRAM_AMBIENT, + RENDERER_PROGRAM_INTERACTION, + RENDERER_PROGRAM_SHADOW, + RENDERER_PROGRAM_GUI, + RENDERER_PROGRAM_HEAT_HAZE, + RENDERER_PROGRAM_MONOCHROME, + RENDERER_PROGRAM_REFRACTIVE, + RENDERER_PROGRAM_CUSTOM +}; + +enum rendererMaterialPassDisposition_t : int { + RENDERER_MATERIAL_PASS_DRAW = 0, + RENDERER_MATERIAL_PASS_INACTIVE_CONDITION, + RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND, + RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE, + RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA +}; + +typedef struct rendererMaterialPass_s { + std::uint32_t order; + std::int32_t sourceStageIndex; + rendererMaterialPassKind_t kind; + rendererMaterialTextureSemantic_t textureSemantic; + std::uint64_t textureResourceId; + rendererRegisterRef_t condition; + rendererRegisterRef_t color[ 4 ]; + rendererRegisterRef_t textureMatrix[ 6 ]; + rendererBlendState_t blend; + rendererDepthState_t depth; + rendererCullMode_t cull; + std::uint32_t colorWriteMask; + bool alphaTestEnabled; + rendererCompareOp_t alphaTestCompareOperation; + rendererRegisterRef_t alphaTest; + rendererTexGen_t texgen; + rendererVertexColorMode_t vertexColor; + bool polygonOffsetEnabled; + rendererRegisterRef_t polygonOffsetFactor; + rendererRegisterRef_t polygonOffsetUnits; + rendererMaterialProgramFamily_t programFamily; + std::uint32_t programKey; +} rendererMaterialPass_t; + +typedef struct rendererMaterialPassList_s { + std::uint32_t count; + bool overflowed; + rendererMaterialPass_t passes[ RENDERER_CONTRACT_MAX_MATERIAL_PASSES ]; +} rendererMaterialPassList_t; + +rendererMaterialPass_t RendererContracts_DefaultMaterialPass( void ); +void RendererContracts_ResetMaterialPassList( rendererMaterialPassList_t &list ); +bool RendererContracts_ValidateMaterialPass( const rendererMaterialPass_t &pass ); +bool RendererContracts_AppendMaterialPass( rendererMaterialPassList_t &list, + const rendererMaterialPass_t &pass ); + +/* + * Per-draw material state after register references have been resolved. This + * deliberately retains the source identity and immutable render state from the + * authored pass while replacing register references with compact scalar data. + * Inactive and recognized no-op passes remain in the ordered list so backends + * cannot disagree about which source stage occupied a given position. The + * active flag reflects only the authored condition; disposition determines + * whether an active pass can affect the framebuffer. + */ +typedef struct rendererEvaluatedMaterialPass_s { + std::uint32_t order; + std::int32_t sourceStageIndex; + rendererMaterialPassKind_t kind; + rendererMaterialTextureSemantic_t textureSemantic; + std::uint64_t textureResourceId; + float condition; + bool active; + rendererMaterialPassDisposition_t disposition; + float color[ 4 ]; + float textureMatrix[ 6 ]; + rendererBlendState_t blend; + rendererDepthState_t depth; + rendererCullMode_t cull; + std::uint32_t colorWriteMask; + bool alphaTestEnabled; + rendererCompareOp_t alphaTestCompareOperation; + float alphaTest; + rendererTexGen_t texgen; + rendererVertexColorMode_t vertexColor; + bool polygonOffsetEnabled; + float polygonOffsetFactor; + float polygonOffsetUnits; + rendererMaterialProgramFamily_t programFamily; + std::uint32_t programKey; +} rendererEvaluatedMaterialPass_t; + +typedef struct rendererEvaluatedMaterialPassList_s { + std::uint32_t count; + std::uint32_t activeCount; + rendererEvaluatedMaterialPass_t passes[ RENDERER_CONTRACT_MAX_MATERIAL_PASSES ]; +} rendererEvaluatedMaterialPassList_t; + +enum rendererMaterialPassEvaluationStatus_t : int { + RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS = 0, + RENDERER_MATERIAL_PASS_EVALUATION_SOURCE_OVERFLOW, + RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS, + RENDERER_MATERIAL_PASS_EVALUATION_REGISTERS_UNAVAILABLE, + RENDERER_MATERIAL_PASS_EVALUATION_REGISTER_OUT_OF_RANGE, + RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE +}; + +void RendererContracts_ResetEvaluatedMaterialPassList( + rendererEvaluatedMaterialPassList_t &list ); + +/* + * Resolves one ordered material list against a bounded per-draw register array. + * An exactly zero condition produces a valid inactive pass, while recognized + * classic blend/color no-ops receive an explicit disposition. Any malformed + * pass, unavailable/out-of-range register, non-finite value, or overflow + * rejects the complete list and resets destination, so consumers never see + * partial state. Constant-only and empty lists may be evaluated with no + * register array. + */ +rendererMaterialPassEvaluationStatus_t RendererContracts_EvaluateMaterialPassList( + rendererEvaluatedMaterialPassList_t &destination, + const rendererMaterialPassList_t &source, + const float *registers, std::uint32_t registerCount ); + +enum rendererClipDepthRange_t : int { + RENDERER_CLIP_DEPTH_NEGATIVE_ONE_TO_ONE = 0, + RENDERER_CLIP_DEPTH_ZERO_TO_ONE +}; + +enum rendererFramebufferOrigin_t : int { + RENDERER_FRAMEBUFFER_ORIGIN_LOWER_LEFT = 0, + RENDERER_FRAMEBUFFER_ORIGIN_UPPER_LEFT +}; + +enum rendererViewportYAxis_t : int { + RENDERER_VIEWPORT_Y_POSITIVE = 0, + RENDERER_VIEWPORT_Y_NEGATIVE +}; + +enum rendererFrontFace_t : int { + RENDERER_FRONT_FACE_COUNTER_CLOCKWISE = 0, + RENDERER_FRONT_FACE_CLOCKWISE +}; + +typedef struct rendererClipSpaceConvention_s { + rendererClipDepthRange_t depthRange; + rendererFramebufferOrigin_t framebufferOrigin; + rendererViewportYAxis_t viewportYAxis; + rendererFrontFace_t frontFace; +} rendererClipSpaceConvention_t; + +typedef struct rendererCanonicalViewport_s { + float x; + float y; + float width; + float height; + float minDepth; + float maxDepth; +} rendererCanonicalViewport_t; + +typedef struct rendererBackendViewport_s { + float x; + float y; + float width; + float height; + float minDepth; + float maxDepth; +} rendererBackendViewport_t; + +rendererClipSpaceConvention_t RendererContracts_GLClipSpace( void ); +rendererClipSpaceConvention_t RendererContracts_VulkanClipSpace( void ); +bool RendererContracts_ConvertClipMatrix( float destination[ 16 ], + const float source[ 16 ], const rendererClipSpaceConvention_t &sourceConvention, + const rendererClipSpaceConvention_t &destinationConvention ); +bool RendererContracts_BuildViewport( rendererBackendViewport_t &destination, + const rendererCanonicalViewport_t &source, float framebufferHeight, + const rendererClipSpaceConvention_t &destinationConvention ); + +enum rendererVertexSemantic_t : int { + RENDERER_VERTEX_SEMANTIC_POSITION = 0, + RENDERER_VERTEX_SEMANTIC_COLOR0, + RENDERER_VERTEX_SEMANTIC_NORMAL, + RENDERER_VERTEX_SEMANTIC_TANGENT0, + RENDERER_VERTEX_SEMANTIC_TANGENT1, + RENDERER_VERTEX_SEMANTIC_TEXCOORD0, + RENDERER_VERTEX_SEMANTIC_JOINT_INDICES, + RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS +}; + +enum rendererVertexFormat_t : int { + RENDERER_VERTEX_FORMAT_FLOAT32X2 = 0, + RENDERER_VERTEX_FORMAT_FLOAT32X3, + RENDERER_VERTEX_FORMAT_FLOAT32X4, + RENDERER_VERTEX_FORMAT_UNORM8X4, + RENDERER_VERTEX_FORMAT_UINT32X4 +}; + +enum rendererVertexInputRate_t : int { + RENDERER_VERTEX_RATE_PER_VERTEX = 0, + RENDERER_VERTEX_RATE_PER_INSTANCE +}; + +typedef struct rendererVertexBindingDesc_s { + std::uint32_t binding; + std::uint32_t stride; + rendererVertexInputRate_t inputRate; +} rendererVertexBindingDesc_t; + +typedef struct rendererVertexAttributeDesc_s { + rendererVertexSemantic_t semantic; + rendererVertexFormat_t format; + std::uint32_t binding; + std::uint32_t offset; +} rendererVertexAttributeDesc_t; + +typedef struct rendererVertexLayoutDesc_s { + std::uint32_t bindingCount; + std::uint32_t attributeCount; + rendererVertexBindingDesc_t bindings[ RENDERER_CONTRACT_MAX_VERTEX_BINDINGS ]; + rendererVertexAttributeDesc_t attributes[ RENDERER_CONTRACT_MAX_VERTEX_ATTRIBUTES ]; +} rendererVertexLayoutDesc_t; + +const rendererVertexLayoutDesc_t &RendererContracts_LegacyDrawVertLayout( void ); +const rendererVertexLayoutDesc_t &RendererContracts_SkinnedDrawVertLayout( void ); +std::uint32_t RendererContracts_VertexFormatSize( rendererVertexFormat_t format ); +bool RendererContracts_ValidateVertexLayout( const rendererVertexLayoutDesc_t &layout ); +const rendererVertexAttributeDesc_t *RendererContracts_FindVertexAttribute( + const rendererVertexLayoutDesc_t &layout, rendererVertexSemantic_t semantic ); + +enum rendererBufferKind_t : int { + RENDERER_BUFFER_KIND_VERTEX = 1, + RENDERER_BUFFER_KIND_INDEX, + RENDERER_BUFFER_KIND_UNIFORM, + RENDERER_BUFFER_KIND_STORAGE, + RENDERER_BUFFER_KIND_JOINT_PALETTE +}; + +enum rendererBufferLifetime_t : int { + RENDERER_BUFFER_LIFETIME_STATIC = 1, + RENDERER_BUFFER_LIFETIME_FRAME +}; + +typedef struct rendererBufferHandle_s { + std::uint64_t value; +} rendererBufferHandle_t; + +typedef struct rendererBufferRecord_s { + rendererBufferHandle_t handle; + std::uint64_t capacityBytes; + std::uint64_t frameSerial; +} rendererBufferRecord_t; + +typedef struct rendererBufferSlice_s { + rendererBufferHandle_t handle; + std::uint64_t offsetBytes; + std::uint64_t sizeBytes; +} rendererBufferSlice_t; + +enum rendererBufferSliceValidation_t : int { + RENDERER_BUFFER_SLICE_VALID = 0, + RENDERER_BUFFER_SLICE_INVALID_HANDLE, + RENDERER_BUFFER_SLICE_WRONG_SLOT, + RENDERER_BUFFER_SLICE_STALE_GENERATION, + RENDERER_BUFFER_SLICE_WRONG_KIND, + RENDERER_BUFFER_SLICE_WRONG_LIFETIME, + RENDERER_BUFFER_SLICE_EXPIRED, + RENDERER_BUFFER_SLICE_EMPTY, + RENDERER_BUFFER_SLICE_RANGE_OVERFLOW +}; + +rendererBufferHandle_t RendererContracts_InvalidBufferHandle( void ); +rendererBufferHandle_t RendererContracts_MakeBufferHandle( rendererBufferKind_t kind, + rendererBufferLifetime_t lifetime, std::uint32_t slot, std::uint32_t generation ); +bool RendererContracts_DecodeBufferHandle( rendererBufferHandle_t handle, + rendererBufferKind_t &kind, rendererBufferLifetime_t &lifetime, + std::uint32_t &slot, std::uint32_t &generation ); +rendererBufferSliceValidation_t RendererContracts_ValidateBufferSlice( + const rendererBufferSlice_t &slice, const rendererBufferRecord_t &record, + rendererBufferKind_t expectedKind, std::uint64_t currentFrameSerial ); +bool RendererContracts_RunSelfTest( void ); + +#endif /* !__RENDERER_CONTRACTS_H__ */ diff --git a/src/renderer/RendererMetrics.cpp b/src/renderer/RendererMetrics.cpp index b366e997..efb91e9c 100644 --- a/src/renderer/RendererMetrics.cpp +++ b/src/renderer/RendererMetrics.cpp @@ -5,6 +5,11 @@ #include "RendererBenchmarks.h" #include "RendererMetrics.h" #include "RendererUpload.h" +#include "GpuFrameTimingCore.h" +#include +#ifdef OPENQ4_RENDERER_VK_MODULE +#include "Vulkan/VulkanGpuFrameTiming.h" +#endif typedef struct rendererMetricsFrame_s { int frameCount; @@ -16,6 +21,7 @@ typedef struct rendererMetricsFrame_s { int submitMsec; int backEndMsec; int presentMsec; + unsigned long long cpuFrameMicroseconds; int gpuMsec; int draw3d; int draw2d; @@ -351,6 +357,11 @@ typedef struct rendererGpuTimerFrame_s { rendererGpuTimerQuery_t queries[128]; int numQueries; int frameCount; + GLuint fullFrameBeginQuery; + GLuint fullFrameEndQuery; + unsigned int fullFrameGeneration; + bool fullFramePending; + bool fullFrameOpen; } rendererGpuTimerFrame_t; typedef struct rendererGpuTimerLatest_s { @@ -685,11 +696,107 @@ static rendererModernVisibilityLatest_t rg_modernVisibilityLatest; static rendererLowOverheadLatest_t rg_lowOverheadLatest; static rendererClusteredLightingStats_t rg_clusteredLightingLatest; static rendererGLStateCacheLatest_t rg_glStateCacheLatest; +static gpuFrameTimingCoreState_t rg_gpuFrameTiming; +static bool rg_gpuFrameTimingInitialized = false; static int rg_gpuTimerFrameCursor = 0; static int rg_gpuTimerBackendFrameCount = 0; static bool rg_gpuTimerQueryActive = false; static rendererGpuTimerQuery_t *rg_gpuTimerActiveQuery = NULL; static bool rg_gpuTimerOverflowThisFrame = false; +static bool rg_gpuFullFrameEnabledLastFrame = false; +static bool rg_gpuFullFrameEnableStateKnown = false; +static int rg_gpuFullFrameContextGeneration = -1; +typedef std::chrono::steady_clock rendererCpuFrameClock_t; +static rendererCpuFrameClock_t::time_point rg_cpuFrameBeginTime; +static bool rg_cpuFrameBeginValid = false; +static bool rg_rendererMetricsCaptureEnabledLastFrame = false; + +static void R_RendererMetrics_EnsureGpuFrameTimingInitialized( void ) { + if ( rg_gpuFrameTimingInitialized ) { + return; + } + GpuFrameTimingCore_Initialize( rg_gpuFrameTiming ); + rg_gpuFrameTimingInitialized = true; +} + +void R_RendererMetrics_MarkCpuFrameBegin( void ) { + rg_cpuFrameBeginTime = rendererCpuFrameClock_t::now(); + rg_cpuFrameBeginValid = true; +} + +static unsigned long long R_RendererMetrics_ConsumeCpuFrameMicroseconds( void ) { + if ( !rg_cpuFrameBeginValid ) { + return 0; + } + const rendererCpuFrameClock_t::time_point beginTime = rg_cpuFrameBeginTime; + rg_cpuFrameBeginValid = false; + const long long elapsedMicroseconds = std::chrono::duration_cast( + rendererCpuFrameClock_t::now() - beginTime ).count(); + if ( elapsedMicroseconds <= 0 ) { + return 0; + } + return static_cast( elapsedMicroseconds ); +} + +void R_RendererMetrics_GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + memset( &timing, 0, sizeof( timing ) ); + timing.supported = rg_gpuFrameTiming.supported; + timing.valid = rg_gpuFrameTiming.valid; + timing.backend = static_cast( rg_gpuFrameTiming.backend ); + timing.frameNumber = rg_gpuFrameTiming.frameNumber; + timing.generation = rg_gpuFrameTiming.generation; + timing.latencyFrames = rg_gpuFrameTiming.latencyFrames; + timing.elapsedMicroseconds = rg_gpuFrameTiming.elapsedMicroseconds; + timing.resolvedSamples = rg_gpuFrameTiming.resolvedSamples; + timing.unavailableSamples = rg_gpuFrameTiming.unavailableSamples; + timing.droppedSamples = rg_gpuFrameTiming.droppedSamples; + timing.resetCount = rg_gpuFrameTiming.resetCount; +} + +void R_RendererMetrics_ResetGpuFrameTiming( const char *reason ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + GpuFrameTimingCore_Reset( rg_gpuFrameTiming ); + RendererBenchmarks_ResetHistory(); + if ( reason != NULL && reason[ 0 ] != '\0' ) { + common->DPrintf( "GPU frame timing reset: generation=%u reason=%s\n", + rg_gpuFrameTiming.generation, reason ); + } +} + +void R_RendererMetrics_SetGpuFrameTimingBackend( renderGpuTimingBackend_t backend, bool supported ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + const bool changed = rg_gpuFrameTiming.backend != static_cast( backend ) + || rg_gpuFrameTiming.supported != supported; + GpuFrameTimingCore_SetBackend( rg_gpuFrameTiming, static_cast( backend ), supported ); + if ( changed ) { + RendererBenchmarks_ResetHistory(); + } +} + +unsigned int R_RendererMetrics_GpuFrameTimingGeneration( void ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + return rg_gpuFrameTiming.generation; +} + +void R_RendererMetrics_RecordGpuFrameTimingResolved( renderGpuTimingBackend_t backend, + int frameNumber, unsigned int generation, unsigned long long elapsedMicroseconds, + int currentFrameNumber ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + GpuFrameTimingCore_RecordResolved( rg_gpuFrameTiming, static_cast( backend ), + frameNumber, generation, elapsedMicroseconds, + GpuFrameTimingCore_FrameLatency( frameNumber, currentFrameNumber ) ); +} + +void R_RendererMetrics_RecordGpuFrameTimingUnavailable( void ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + rg_gpuFrameTiming.unavailableSamples++; +} + +void R_RendererMetrics_RecordGpuFrameTimingDropped( void ) { + R_RendererMetrics_EnsureGpuFrameTimingInitialized(); + rg_gpuFrameTiming.droppedSamples++; +} static const char *R_RendererMetrics_ModernExecutorModeName( rendererModernExecutorMetricsMode_t mode ) { switch ( mode ) { @@ -716,11 +823,21 @@ static const char *R_RendererMetrics_ScenePacketSourceName( const rendererMetric return "none"; } +static bool R_RendererMetrics_GpuPassTimersAvailable( void ) { + return glConfig.backendCaps.hasTimerQuery && + glGenQueries != NULL && + glDeleteQueries != NULL && + glBeginQuery != NULL && + glEndQuery != NULL && + glGetQueryObjectiv != NULL && + glGetQueryObjectuiv != NULL; +} + static bool R_RendererMetrics_GpuTimersEnabled( void ) { if ( r_rendererMetrics.GetInteger() <= 0 || !r_rendererGpuTimers.GetBool() ) { return false; } - return R_RendererMetrics_GpuTimersAvailable(); + return R_RendererMetrics_GpuPassTimersAvailable(); } static int R_RendererMetrics_TotalGpuMsec( const rendererMetricsFrame_t &metrics ) { @@ -746,6 +863,7 @@ static const char *R_RendererMetrics_FormatGpuMsec( const rendererMetricsFrame_t return buffer; } +#ifndef OPENQ4_RENDERER_VK_MODULE static void R_RendererMetrics_PollGpuTimerFrame( rendererGpuTimerFrame_t &frame ) { if ( frame.numQueries <= 0 ) { return; @@ -796,6 +914,74 @@ static void R_RendererMetrics_PollGpuTimerFrame( rendererGpuTimerFrame_t &frame frame.numQueries = 0; } +static bool R_RendererMetrics_GlFullFrameTimingAvailable( void ) { + return glConfig.backendCaps.hasTimerQuery && + glGenQueries != NULL && + glDeleteQueries != NULL && + glQueryCounter != NULL && + glGetQueryObjectiv != NULL && + glGetQueryObjectui64v != NULL; +} + +static bool R_RendererMetrics_PollGlFullFrameTiming( rendererGpuTimerFrame_t &frame ) { + if ( !frame.fullFramePending ) { + return true; + } + + GLint beginAvailable = 0; + GLint endAvailable = 0; + glGetQueryObjectiv( frame.fullFrameBeginQuery, GL_QUERY_RESULT_AVAILABLE, &beginAvailable ); + glGetQueryObjectiv( frame.fullFrameEndQuery, GL_QUERY_RESULT_AVAILABLE, &endAvailable ); + if ( !beginAvailable || !endAvailable ) { + R_RendererMetrics_RecordGpuFrameTimingUnavailable(); + return false; + } + + GLuint64 beginTimestamp = 0; + GLuint64 endTimestamp = 0; + glGetQueryObjectui64v( frame.fullFrameBeginQuery, GL_QUERY_RESULT, &beginTimestamp ); + glGetQueryObjectui64v( frame.fullFrameEndQuery, GL_QUERY_RESULT, &endTimestamp ); + frame.fullFramePending = false; + + const unsigned int currentGeneration = R_RendererMetrics_GpuFrameTimingGeneration(); + if ( frame.fullFrameGeneration != currentGeneration || endTimestamp < beginTimestamp ) { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + return true; + } + + const unsigned long long elapsedMicroseconds = + GpuFrameTimingCore_TicksToMicroseconds( endTimestamp - beginTimestamp, 1.0 ); + R_RendererMetrics_RecordGpuFrameTimingResolved( RENDER_GPU_TIMING_BACKEND_OPENGL, + frame.frameCount, frame.fullFrameGeneration, elapsedMicroseconds, tr.frameCount ); + return true; +} + +static bool R_RendererMetrics_BeginGlFullFrameTiming( rendererGpuTimerFrame_t &frame ) { + if ( frame.fullFramePending ) { + // Preserve the unresolved older sample. The current frame is deliberately + // unsampled instead of waiting or overwriting live query objects. + R_RendererMetrics_RecordGpuFrameTimingDropped(); + return false; + } + if ( frame.fullFrameBeginQuery == 0 ) { + glGenQueries( 1, &frame.fullFrameBeginQuery ); + } + if ( frame.fullFrameEndQuery == 0 ) { + glGenQueries( 1, &frame.fullFrameEndQuery ); + } + if ( frame.fullFrameBeginQuery == 0 || frame.fullFrameEndQuery == 0 ) { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + return false; + } + + frame.frameCount = tr.frameCount; + frame.fullFrameGeneration = R_RendererMetrics_GpuFrameTimingGeneration(); + frame.fullFrameOpen = true; + glQueryCounter( frame.fullFrameBeginQuery, GL_TIMESTAMP ); + return true; +} +#endif + void R_RendererMetrics_BeginFrame( int frameCount ) { // The memset must stay ahead of the gate so Add*/Record* writes from // disabled frames cannot leak into the first enabled frame; EndFrame is the @@ -803,7 +989,12 @@ void R_RendererMetrics_BeginFrame( int frameCount ) { // rebuild below is dead work while metrics are off. memset( &rg_rendererMetrics, 0, sizeof( rg_rendererMetrics ) ); rg_rendererMetrics.frameCount = frameCount; - if ( r_rendererMetrics.GetInteger() <= 0 ) { + const bool captureEnabled = r_rendererMetrics.GetInteger() > 0; + if ( captureEnabled && !rg_rendererMetricsCaptureEnabledLastFrame ) { + R_RendererMetrics_ResetGpuFrameTiming( "renderer metrics capture window started" ); + } + rg_rendererMetricsCaptureEnabledLastFrame = captureEnabled; + if ( !captureEnabled ) { return; } rg_rendererMetrics.gpuTimersValid = rg_gpuTimerLatest.valid; @@ -1667,7 +1858,20 @@ static void R_RendererMetrics_RecordBenchmarkCapture( void ) { sample.submitMsec = rg_rendererMetrics.submitMsec; sample.backEndMsec = rg_rendererMetrics.backEndMsec; sample.presentMsec = rg_rendererMetrics.presentMsec; - sample.gpuMsec = Max( 0, R_RendererMetrics_TotalGpuMsec( rg_rendererMetrics ) ); + sample.cpuFrameMicroseconds = rg_rendererMetrics.cpuFrameMicroseconds; + renderGpuFrameTiming_t gpuFrameTiming; + R_RendererMetrics_GetGpuFrameTiming( gpuFrameTiming ); + sample.gpuFrameTimingValid = gpuFrameTiming.valid && gpuFrameTiming.elapsedMicroseconds > 0; + sample.gpuFrameBackend = gpuFrameTiming.backend; + sample.gpuFrameNumber = gpuFrameTiming.frameNumber; + sample.gpuFrameGeneration = gpuFrameTiming.generation; + sample.gpuFrameMicroseconds = sample.gpuFrameTimingValid ? gpuFrameTiming.elapsedMicroseconds : 0; + const unsigned long long gpuFrameMsec = sample.gpuFrameMicroseconds / 1000ULL + + ( sample.gpuFrameMicroseconds % 1000ULL >= 500ULL ? 1ULL : 0ULL ); + sample.gpuMsec = !sample.gpuFrameTimingValid ? 0 + : gpuFrameMsec > static_cast( ( std::numeric_limits::max )() ) + ? ( std::numeric_limits::max )() + : static_cast( gpuFrameMsec ); for ( int i = 0; i < RENDERER_GPU_TIMER_COUNT; ++i ) { sample.gpuPassMsec[i] = rg_rendererMetrics.gpuTimerMsec[i]; sample.gpuPassSamples[i] = rg_rendererMetrics.gpuTimerSamples[i]; @@ -1707,6 +1911,7 @@ void R_RendererMetrics_EndFrame( int frontEndMsec, int backEndMsec, int viewCoun } rg_rendererMetrics.frontEndMsec = frontEndMsec; + rg_rendererMetrics.cpuFrameMicroseconds = R_RendererMetrics_ConsumeCpuFrameMicroseconds(); rg_rendererMetrics.backEndMsec = backEndMsec; rg_rendererMetrics.views = viewCount; rg_rendererMetrics.visibleEntities = visibleEntities; @@ -2348,11 +2553,54 @@ void R_RendererMetrics_EndFrame( int frontEndMsec, int backEndMsec, int viewCoun } void R_RendererMetrics_BeginGpuBackendFrame( void ) { +#ifdef OPENQ4_RENDERER_VK_MODULE + // Vulkan brackets its true command-buffer lifetime in vk_GuiExecutor; + // the shared command-dispatch hook is intentionally not used there. + return; +#else rg_gpuTimerQueryActive = false; rg_gpuTimerActiveQuery = NULL; rg_gpuTimerOverflowThisFrame = false; - if ( !R_RendererMetrics_GpuTimersEnabled() ) { + const bool fullFrameSupported = R_RendererMetrics_GlFullFrameTimingAvailable(); + R_RendererMetrics_SetGpuFrameTimingBackend( RENDER_GPU_TIMING_BACKEND_OPENGL, + fullFrameSupported ); + + if ( rg_gpuFullFrameContextGeneration != tr.glContextGeneration ) { + if ( rg_gpuFullFrameContextGeneration >= 0 ) { + R_RendererMetrics_ResetGpuFrameTiming( "OpenGL context generation changed" ); + } + rg_gpuFullFrameContextGeneration = tr.glContextGeneration; + for ( int frameIndex = 0; frameIndex < static_cast( sizeof( rg_gpuTimerFrames ) / sizeof( rg_gpuTimerFrames[ 0 ] ) ); ++frameIndex ) { + rendererGpuTimerFrame_t &staleFrame = rg_gpuTimerFrames[ frameIndex ]; + // Names from a destroyed GL context must never be deleted or reused in + // the replacement context; they may alias unrelated new objects. + staleFrame.fullFrameBeginQuery = 0; + staleFrame.fullFrameEndQuery = 0; + staleFrame.fullFramePending = false; + staleFrame.fullFrameOpen = false; + staleFrame.numQueries = 0; + for ( int queryIndex = 0; queryIndex < static_cast( sizeof( staleFrame.queries ) / sizeof( staleFrame.queries[ 0 ] ) ); ++queryIndex ) { + staleFrame.queries[ queryIndex ].id = 0; + } + } + } + + // Dynamic resolution is itself a consumer of the backend-neutral whole-frame + // timing stream. Requiring the separate diagnostics CVar as well would make + // `r_rendererDynamicResolution 1` silently inert on a clean configuration. + const bool fullFrameEnabled = fullFrameSupported + && ( r_rendererGpuTimers.GetBool() + || r_rendererDynamicResolution.GetBool() ); + if ( rg_gpuFullFrameEnableStateKnown && fullFrameEnabled != rg_gpuFullFrameEnabledLastFrame ) { + R_RendererMetrics_ResetGpuFrameTiming( fullFrameEnabled + ? "OpenGL whole-frame timing enabled" : "OpenGL whole-frame timing disabled" ); + } + rg_gpuFullFrameEnableStateKnown = true; + rg_gpuFullFrameEnabledLastFrame = fullFrameEnabled; + + const bool passTimersEnabled = R_RendererMetrics_GpuTimersEnabled(); + if ( !fullFrameEnabled && !passTimersEnabled ) { return; } @@ -2360,18 +2608,37 @@ void R_RendererMetrics_BeginGpuBackendFrame( void ) { rg_gpuTimerBackendFrameCount++; rendererGpuTimerFrame_t &frame = rg_gpuTimerFrames[rg_gpuTimerFrameCursor]; - R_RendererMetrics_PollGpuTimerFrame( frame ); - frame.numQueries = 0; - frame.frameCount = tr.frameCount; + if ( fullFrameEnabled ) { + if ( R_RendererMetrics_PollGlFullFrameTiming( frame ) ) { + R_RendererMetrics_BeginGlFullFrameTiming( frame ); + } else { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + } + } + if ( passTimersEnabled ) { + R_RendererMetrics_PollGpuTimerFrame( frame ); + frame.numQueries = 0; + } +#endif } void R_RendererMetrics_EndGpuBackendFrame( void ) { +#ifdef OPENQ4_RENDERER_VK_MODULE + return; +#else if ( rg_gpuTimerQueryActive ) { R_RendererMetrics_EndGpuTimer(); } + rendererGpuTimerFrame_t &frame = rg_gpuTimerFrames[rg_gpuTimerFrameCursor]; + if ( frame.fullFrameOpen ) { + glQueryCounter( frame.fullFrameEndQuery, GL_TIMESTAMP ); + frame.fullFrameOpen = false; + frame.fullFramePending = true; + } if ( rg_gpuTimerOverflowThisFrame ) { rg_gpuTimerLatest.droppedQueries++; } +#endif } void R_RendererMetrics_BeginGpuTimer( rendererGpuTimerSlot_t slot ) { @@ -2427,6 +2694,11 @@ void R_RendererMetrics_ResumeGpuTimer( rendererGpuTimerSlot_t slot, bool resume } void R_RendererMetrics_ShutdownGpuTimers( void ) { +#ifdef OPENQ4_RENDERER_VK_MODULE + VK_GpuFrameTiming_Shutdown(); + R_RendererMetrics_SetGpuFrameTimingBackend( RENDER_GPU_TIMING_BACKEND_NONE, false ); + return; +#else if ( rg_gpuTimerQueryActive ) { R_RendererMetrics_EndGpuTimer(); } @@ -2439,60 +2711,68 @@ void R_RendererMetrics_ShutdownGpuTimers( void ) { frame.queries[queryIndex].id = 0; } } + if ( frame.fullFrameBeginQuery != 0 ) { + glDeleteQueries( 1, &frame.fullFrameBeginQuery ); + frame.fullFrameBeginQuery = 0; + } + if ( frame.fullFrameEndQuery != 0 ) { + glDeleteQueries( 1, &frame.fullFrameEndQuery ); + frame.fullFrameEndQuery = 0; + } frame.numQueries = 0; + frame.fullFramePending = false; + frame.fullFrameOpen = false; } memset( &rg_gpuTimerLatest, 0, sizeof( rg_gpuTimerLatest ) ); rg_gpuTimerFrameCursor = 0; rg_gpuTimerBackendFrameCount = 0; + rg_gpuFullFrameEnabledLastFrame = false; + rg_gpuFullFrameEnableStateKnown = false; + rg_gpuFullFrameContextGeneration = -1; + R_RendererMetrics_SetGpuFrameTimingBackend( RENDER_GPU_TIMING_BACKEND_NONE, false ); +#endif } bool R_RendererMetrics_GpuTimersAvailable( void ) { - return glConfig.backendCaps.hasTimerQuery && - glGenQueries != NULL && - glDeleteQueries != NULL && - glBeginQuery != NULL && - glEndQuery != NULL && - glGetQueryObjectiv != NULL && - glGetQueryObjectuiv != NULL; +#ifdef OPENQ4_RENDERER_VK_MODULE + return VK_GpuFrameTiming_Available(); +#else + return R_RendererMetrics_GlFullFrameTimingAvailable(); +#endif } bool RendererGpuTimer_RunSelfTest( void ) { +#ifdef OPENQ4_RENDERER_VK_MODULE + return VK_GpuFrameTiming_RunSelfTest(); +#else if ( !R_RendererMetrics_GpuTimersAvailable() ) { common->Printf( "RendererGpuTimer self-test skipped: GL timer queries are unavailable\n" ); return true; } - GLuint query = 0; - glGenQueries( 1, &query ); - if ( query == 0 ) { - common->Printf( "RendererGpuTimer self-test failed: glGenQueries returned 0\n" ); + GLuint queries[ 2 ] = { 0, 0 }; + glGenQueries( 2, queries ); + if ( queries[ 0 ] == 0 || queries[ 1 ] == 0 ) { + if ( queries[ 0 ] != 0 ) { + glDeleteQueries( 1, &queries[ 0 ] ); + } + if ( queries[ 1 ] != 0 ) { + glDeleteQueries( 1, &queries[ 1 ] ); + } + common->Printf( "RendererGpuTimer self-test failed: timestamp query allocation returned 0\n" ); return false; } - glBeginQuery( GL_TIME_ELAPSED, query ); + glQueryCounter( queries[ 0 ], GL_TIMESTAMP ); glClear( GL_COLOR_BUFFER_BIT ); - glEndQuery( GL_TIME_ELAPSED ); - glFinish(); - - GLint available = 0; - glGetQueryObjectiv( query, GL_QUERY_RESULT_AVAILABLE, &available ); - if ( !available ) { - glDeleteQueries( 1, &query ); - common->Printf( "RendererGpuTimer self-test failed: query result unavailable after glFinish\n" ); - return false; - } - - GLuint64 elapsedNsec = 0; - if ( glGetQueryObjectui64v != NULL ) { - glGetQueryObjectui64v( query, GL_QUERY_RESULT, &elapsedNsec ); - } else { - GLuint elapsedNsec32 = 0; - glGetQueryObjectuiv( query, GL_QUERY_RESULT, &elapsedNsec32 ); - elapsedNsec = elapsedNsec32; - } - glDeleteQueries( 1, &query ); + glQueryCounter( queries[ 1 ], GL_TIMESTAMP ); - common->Printf( "RendererGpuTimer self-test passed (%u usec)\n", static_cast( elapsedNsec / 1000ULL ) ); + GLint endAvailable = 0; + glGetQueryObjectiv( queries[ 1 ], GL_QUERY_RESULT_AVAILABLE, &endAvailable ); + glDeleteQueries( 2, queries ); + common->Printf( "RendererGpuTimer self-test passed (timestamp pair queued, immediateReady=%d, waits=0)\n", + endAvailable ? 1 : 0 ); return true; +#endif } diff --git a/src/renderer/RendererMetrics.h b/src/renderer/RendererMetrics.h index a66989ab..2b8aa5f9 100644 --- a/src/renderer/RendererMetrics.h +++ b/src/renderer/RendererMetrics.h @@ -32,6 +32,7 @@ enum rendererModernExecutorMetricsMode_t { }; void R_RendererMetrics_BeginFrame( int frameCount ); +void R_RendererMetrics_MarkCpuFrameBegin( void ); void R_RendererMetrics_RecordSubmitMsec( int submitMsec ); void R_RendererMetrics_AddVisibilityMsec( int msec ); void R_RendererMetrics_AddPacketBuildMsec( int msec ); @@ -65,4 +66,17 @@ void R_RendererMetrics_ShutdownGpuTimers( void ); bool R_RendererMetrics_GpuTimersAvailable( void ); bool RendererGpuTimer_RunSelfTest( void ); +// Delayed whole-frame timing shared by every renderer backend. Backends only +// publish results after a nonblocking availability check; consumers receive a +// coherent POD snapshot and never touch GL/Vulkan query objects. +void R_RendererMetrics_GetGpuFrameTiming( renderGpuFrameTiming_t &timing ); +void R_RendererMetrics_ResetGpuFrameTiming( const char *reason ); +void R_RendererMetrics_SetGpuFrameTimingBackend( renderGpuTimingBackend_t backend, bool supported ); +unsigned int R_RendererMetrics_GpuFrameTimingGeneration( void ); +void R_RendererMetrics_RecordGpuFrameTimingResolved( renderGpuTimingBackend_t backend, + int frameNumber, unsigned int generation, unsigned long long elapsedMicroseconds, + int currentFrameNumber ); +void R_RendererMetrics_RecordGpuFrameTimingUnavailable( void ); +void R_RendererMetrics_RecordGpuFrameTimingDropped( void ); + #endif /* !__RENDERER_METRICS_H__ */ diff --git a/src/renderer/RendererUpload.cpp b/src/renderer/RendererUpload.cpp index ff243521..4189ed0f 100644 --- a/src/renderer/RendererUpload.cpp +++ b/src/renderer/RendererUpload.cpp @@ -479,6 +479,11 @@ void idUploadManager::BeginFrame( int frameCount ) { frameBuffer_t &frame = frameBuffers[currentFrameBuffer]; if ( path != UPLOAD_PATH_PERSISTENT ) { + // The modern executor may have rebound GL_ARRAY_BUFFER through its state + // cache since the legacy vertex-cache shadow was last updated. Force the + // real buffer bind before orphaning this frame slot so the orphan cannot + // accidentally apply to the most recently submitted geometry VBO. + idVertexCache::InvalidateBufferBindings(); idVertexCache::BindArrayBuffer( frame.vbo ); glBufferDataARB( GL_ARRAY_BUFFER_ARB, (GLsizeiptrARB)stats.ringSizeBytes, NULL, GL_STREAM_DRAW_ARB ); R_GLStateCache_InvalidateBufferBinding( GL_ARRAY_BUFFER, "renderer upload frame orphan" ); @@ -520,9 +525,19 @@ bool idUploadManager::AllocFrameTemp( void *data, int bytes, int alignment, rend return false; } - idVertexCache::BindArrayBuffer( frame.vbo ); + // A persistent+coherent mapping is written through the pointer - no GL call + // touches GL_ARRAY_BUFFER on that branch, so the real binding and both + // binding shadows stay untouched. The map-range/subdata branches DO target + // GL_ARRAY_BUFFER and can be interleaved with modern submissions that bind + // it outside idVertexCache's redundant-bind shadow, so for those the + // ownership transfer stays explicit before writing a range in the stream. + const bool persistentWrite = path == UPLOAD_PATH_PERSISTENT && frame.mapped != NULL; + if ( !persistentWrite ) { + idVertexCache::InvalidateBufferBindings(); + idVertexCache::BindArrayBuffer( frame.vbo ); + } - if ( path == UPLOAD_PATH_PERSISTENT && frame.mapped != NULL ) { + if ( persistentWrite ) { SIMDProcessor->Memcpy( frame.mapped + offset, data, bytes ); stats.framePersistentWrites++; } else if ( path == UPLOAD_PATH_MAP_RANGE && glMapBufferRange != NULL ) { @@ -555,7 +570,10 @@ bool idUploadManager::AllocFrameTemp( void *data, int bytes, int alignment, rend stats.frameRingUsedBytes = ring.Used(); stats.frameRingHighWaterBytes = ring.HighWater(); R_RendererMetrics_AddUploadBytes( bytes ); - R_GLStateCache_InvalidateBufferBinding( GL_ARRAY_BUFFER, "renderer upload frame stream" ); + if ( !persistentWrite ) { + // the legacy bind above changed GL_ARRAY_BUFFER behind the modern cache + R_GLStateCache_InvalidateBufferBinding( GL_ARRAY_BUFFER, "renderer upload frame stream" ); + } return true; } @@ -627,6 +645,7 @@ bool idUploadManager::CreateFrameBuffers( uploadPath_t requestedPath ) { for ( int i = 0; i < frameBufferCount; ++i ) { glGenBuffersARB( 1, &frameBuffers[i].vbo ); + idVertexCache::InvalidateBufferBindings(); idVertexCache::BindArrayBuffer( frameBuffers[i].vbo ); if ( path == UPLOAD_PATH_PERSISTENT ) { diff --git a/src/renderer/ScenePackets.cpp b/src/renderer/ScenePackets.cpp index a9fae7f0..d97bf7e8 100644 --- a/src/renderer/ScenePackets.cpp +++ b/src/renderer/ScenePackets.cpp @@ -7,9 +7,152 @@ static idScenePacketFrame rg_frontEndScenePacketFrame; static bool rg_frontEndScenePacketFrameOpen = false; +// The front end emits RC_COPY_RENDER immediately after a capture-backed +// subview's RC_DRAW_VIEW. Preserve that producer while assembling packet data; +// tr.viewDef has already been restored to the parent when CaptureRenderToImage +// queues its copy command. +static const viewDef_t *rg_frontEndLastDrawView = NULL; + +typedef struct scenePacketTemporalInstanceHistory_s { + unsigned long long viewIdentity; + unsigned int historyGeneration; + int frameNumber; + int entityIndex; + float modelMatrix[16]; +} scenePacketTemporalInstanceHistory_t; + +static idList + rg_scenePacketTemporalInstanceHistory; +static idList + rg_scenePacketTemporalNextInstanceHistory; + +// Hash acceleration for the (viewIdentity, entityIndex) history lookups. +// Each idHashIndex always mirrors the list its pointer is paired with; the +// pointers swap in lockstep with the list Swap() in Commit. These are only +// touched from the runtime Find/Store/Commit paths, never from +// idScenePacketFrame::Clear(), preserving the static-constructor constraint +// documented above the lookup-slot table. +static idHashIndex rg_scenePacketTemporalHistoryLookupA; +static idHashIndex rg_scenePacketTemporalHistoryLookupB; +static idHashIndex *rg_scenePacketTemporalHistoryLookup = + &rg_scenePacketTemporalHistoryLookupA; +static idHashIndex *rg_scenePacketTemporalNextHistoryLookup = + &rg_scenePacketTemporalHistoryLookupB; + +static int R_ScenePackets_TemporalHistoryLookupKey( + unsigned long long viewIdentity, int entityIndex ) { + return static_cast( + static_cast( viewIdentity ) + ^ static_cast( viewIdentity >> 32 ) + ^ ( static_cast( entityIndex ) * 0x9e3779b9u ) ); +} + +static bool R_ScenePackets_FindTemporalInstanceHistory( + unsigned long long viewIdentity, int entityIndex, + float previousModelMatrix[16], int &ageFrames ) { + ageFrames = 0; + if ( viewIdentity == 0 || entityIndex < 0 ) { + return false; + } + const unsigned int historyGeneration = + R_TemporalPresentation_HistoryGeneration(); + const int lookupKey = + R_ScenePackets_TemporalHistoryLookupKey( viewIdentity, entityIndex ); + for ( int i = rg_scenePacketTemporalHistoryLookup->First( lookupKey ); + i >= 0; i = rg_scenePacketTemporalHistoryLookup->Next( i ) ) { + const scenePacketTemporalInstanceHistory_t &entry = + rg_scenePacketTemporalInstanceHistory[i]; + if ( entry.viewIdentity != viewIdentity + || entry.entityIndex != entityIndex + || entry.historyGeneration != historyGeneration ) { + continue; + } + const unsigned int age = static_cast( tr.frameCount ) + - static_cast( entry.frameNumber ); + if ( age == 0u || age > 2u ) { + return false; + } + memcpy( previousModelMatrix, entry.modelMatrix, + sizeof( entry.modelMatrix ) ); + ageFrames = static_cast( age ); + return true; + } + return false; +} + +static void R_ScenePackets_StoreTemporalInstanceHistory( + const instanceRecord_t &record ) { + if ( record.entityIndex < 0 || !record.hasModelMatrix + || record.temporalViewIdentity == 0 + || record.temporalCaptureFrame ) { + return; + } + const int lookupKey = R_ScenePackets_TemporalHistoryLookupKey( + record.temporalViewIdentity, record.entityIndex ); + for ( int i = rg_scenePacketTemporalNextHistoryLookup->First( lookupKey ); + i >= 0; i = rg_scenePacketTemporalNextHistoryLookup->Next( i ) ) { + const scenePacketTemporalInstanceHistory_t &existing = + rg_scenePacketTemporalNextInstanceHistory[i]; + if ( existing.viewIdentity == record.temporalViewIdentity + && existing.entityIndex == record.entityIndex ) { + return; + } + } + scenePacketTemporalInstanceHistory_t &entry = + rg_scenePacketTemporalNextInstanceHistory.Alloc(); + entry.viewIdentity = record.temporalViewIdentity; + entry.historyGeneration = record.temporalHistoryGeneration; + entry.frameNumber = tr.frameCount; + entry.entityIndex = record.entityIndex; + memcpy( entry.modelMatrix, record.modelMatrix, sizeof( entry.modelMatrix ) ); + rg_scenePacketTemporalNextHistoryLookup->Add( lookupKey, + rg_scenePacketTemporalNextInstanceHistory.Num() - 1 ); +} + +static void R_ScenePackets_CommitTemporalInstanceHistory( + const idScenePacketFrame &frame ) { + // SetNum( 0, false ) keeps the allocations (bounded at + // SCENE_PACKET_MAX_INSTANCE_RECORDS entries) so steady-state capture + // frames reuse the buffers instead of freeing and regrowing them. + rg_scenePacketTemporalNextInstanceHistory.SetNum( 0, false ); + rg_scenePacketTemporalNextHistoryLookup->Clear(); + for ( int i = 0; i < frame.NumInstanceRecords(); ++i ) { + R_ScenePackets_StoreTemporalInstanceHistory( frame.InstanceRecord( i ) ); + } + rg_scenePacketTemporalInstanceHistory.Swap( + rg_scenePacketTemporalNextInstanceHistory ); + idSwap( rg_scenePacketTemporalHistoryLookup, + rg_scenePacketTemporalNextHistoryLookup ); + rg_scenePacketTemporalNextInstanceHistory.SetNum( 0, false ); + rg_scenePacketTemporalNextHistoryLookup->Clear(); +} + +// A negative view id alone is not a render demo: portal-sky cameras use the +// same convention. The session's active demo reader and its exact render world +// make playback provenance explicit before command buffers are submitted. +static bool R_ScenePackets_IsRenderDemoPlaybackView( const viewDef_t *viewDef ) { + return viewDef != NULL && viewDef->viewEntitys != NULL + && viewDef->renderWorld != NULL && !viewDef->isSubview + && session != NULL && session->readDemo != NULL + && session->rw == viewDef->renderWorld; +} + +// A command-stream reconstruction has no controller payload. Reproduce the +// exact admission mask at the point the backend consumes the command so a +// backend-derived packet retains the same special-frame provenance as the +// front-end packet. +static int R_ScenePackets_ActiveSpecialEffectsMask( void ) { + int activeMask = tr.specialEffectsEnabled; + if ( r_forceSpecialEffects.GetInteger() > 0 ) { + activeMask = r_forceSpecialEffects.GetInteger(); + } + return activeMask & ( SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL ); +} static bool R_ScenePackets_ModernPipelineRequested( void ) { const bool modernVisibleRequested = r_rendererModernVisible.GetBool() || RendererBootstrap_ShouldAutoPromoteModernVisible(); + const bool advancedLightingLeafRequested = r_rendererModernQuality.GetBool() + && ( r_rendererReflectionProbes.GetBool() || r_rendererClusteredDecals.GetBool() ); const bool shadowMapSidecarRequested = r_useShadowMap.GetBool() && r_shadows.GetBool() @@ -20,6 +163,16 @@ static bool R_ScenePackets_ModernPipelineRequested( void ) { || r_rendererGpuValidation.GetBool() || r_rendererBindless.GetBool() || modernVisibleRequested + || advancedLightingLeafRequested + || r_rendererSharedGui.GetBool() + || r_rendererSharedInWorldGui.GetBool() + || r_rendererSharedCinematicPost.GetBool() + || r_rendererSharedSpecialFrame.GetBool() + || r_rendererSharedWorldAmbient.GetBool() + || r_rendererSharedWorldInteraction.GetBool() + || r_rendererSharedWorldFogBlend.GetBool() + || r_rendererSharedSubview.GetBool() + || r_rendererSharedDeform.GetBool() || r_rendererModernVisibleDepth.GetBool() || r_rendererModernDepthDebug.GetInteger() > 0 || r_rendererModernOpaque.GetBool() @@ -46,11 +199,18 @@ class rendererSelfTestSkipPostProcessRestore_t { }; bool R_ScenePackets_FrontEndCaptureRequired( void ) { - return r_rendererMetrics.GetInteger() >= 2; + // The shared subview transaction has to retain the exact RC_DRAW_VIEW -> + // RC_COPY_RENDER edge on the front end. Backend reconstruction cannot infer + // it when an image capture occurs after tr.viewDef returns to the parent. + return r_rendererSharedSubview.GetBool() + || R_TemporalPresentation_TemporalAARequested() + || r_rendererMetrics.GetInteger() >= 2; } bool R_ScenePackets_SidePipelineRequired( void ) { - return R_ScenePackets_ModernPipelineRequested() || r_rendererMetrics.GetInteger() >= 2; + return R_ScenePackets_ModernPipelineRequested() + || R_TemporalPresentation_TemporalAARequested() + || r_rendererMetrics.GetInteger() >= 2; } // Side tables accelerating the FindOrAdd* record dedup scans. They are keyed @@ -74,6 +234,36 @@ static int R_ScenePackets_PointerLookupKey( const void *pointer ) { return static_cast( ( address >> 4 ) ^ ( address >> 14 ) ); } +static classicDeformRole_t R_ScenePackets_ClassicDeformRoleForPass( + renderPassCategory_t category ) { + switch ( category ) { + case RENDER_PASS_ARB2_INTERACTION: + return CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER; + case RENDER_PASS_FOG_BLEND: + return CLASSIC_DEFORM_ROLE_FOG_RECEIVER; + case RENDER_PASS_STENCIL_SHADOW: + return CLASSIC_DEFORM_ROLE_SHADOW_VOLUME; + default: + // Ambient, depth, shadow-map caster, and forward/post classifications + // originate from the finalized view draw rather than receiver chains. + return CLASSIC_DEFORM_ROLE_FINALIZED_DRAW; + } +} + +static int R_ScenePackets_GeometryLookupKey( const srfTriangles_t *geometry, + const classicDeformRecord_t &classicDeform ) { + const std::uint64_t semantic = classicDeform.semanticHash; + const std::uint64_t frame = classicDeform.frameToken; + const unsigned int mixed = + static_cast( R_ScenePackets_PointerLookupKey( geometry ) ) + ^ static_cast( semantic ) + ^ static_cast( semantic >> 32 ) + ^ static_cast( frame ) + ^ static_cast( frame >> 32 ) + ^ ( static_cast( classicDeform.role ) * 0x9e3779b9u ); + return static_cast( mixed ); +} + static void R_ScenePackets_ClaimLookupSlot( const idScenePacketFrame *frame ) { int slot = -1; for ( int i = 0; i < SCENE_PACKET_LOOKUP_SLOTS; ++i ) { @@ -139,6 +329,7 @@ void idScenePacketFrame::Clear( void ) { memset( materialRecords, 0, sizeof( materialRecords ) ); memset( geometryRecords, 0, sizeof( geometryRecords ) ); memset( instanceRecords, 0, sizeof( instanceRecords ) ); + memset( subviewCaptures, 0, sizeof( subviewCaptures ) ); #endif memset( &stats, 0, sizeof( stats ) ); activeScene = -1; @@ -269,12 +460,32 @@ static const idImage *R_ScenePackets_FirstAmbientImage( const idMaterial *materi return firstAmbientImage; } -static bool R_ScenePackets_DrawSurfUsesSkinning( const drawSurf_t *drawSurf ) { +static geometrySkinningMode_t R_ScenePackets_DrawSurfSkinningMode( + const drawSurf_t *drawSurf, gpuSkinningSurface_t *surface = NULL ) { + if ( surface != NULL ) { + memset( surface, 0, sizeof( *surface ) ); + } + if ( drawSurf == NULL || drawSurf->geo == NULL ) { + return GEOMETRY_SKINNING_NONE; + } + gpuSkinningSurface_t candidate; + if ( R_GpuSkinning_GetSurface( drawSurf->geo, candidate ) ) { + if ( surface != NULL ) { + *surface = candidate; + } + return GEOMETRY_SKINNING_GPU_PALETTE; + } + // A source-side exactness or eligibility rejection means the ordinary + // deformed idDrawVert stream is authoritative for this snapshot. + if ( R_GpuSkinning_IsCandidate( drawSurf->geo ) ) { + return GEOMETRY_SKINNING_CPU; + } #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) - return drawSurf != NULL && drawSurf->geo != NULL && drawSurf->geo->numSkinToModelTransforms > 0; -#else - return false; + if ( drawSurf->geo->numSkinToModelTransforms > 0 ) { + return GEOMETRY_SKINNING_GPU_PALETTE; + } #endif + return GEOMETRY_SKINNING_NONE; } static bool R_ScenePackets_MaterialUsesRemoteRender( const idMaterial *material ) { @@ -291,7 +502,35 @@ static bool R_ScenePackets_MaterialUsesRemoteRender( const idMaterial *material return false; } -static scenePacketCategory_t R_ScenePackets_CategoryForDrawSurf( const viewDef_t *viewDef, const drawSurf_t *drawSurf, renderPassCategory_t passCategory ) { +bool R_ScenePackets_TemporalRigidMotionEligible( + const drawSurf_t *drawSurf ) { + if ( drawSurf == NULL || drawSurf->geo == NULL + || drawSurf->material == NULL || drawSurf->space == NULL + || drawSurf->space->entityDef == NULL ) { + return false; + } + + const srfTriangles_t *geometry = drawSurf->geo; + const idMaterial *material = drawSurf->material; + const idRenderEntityLocal *entity = drawSurf->space->entityDef; + if ( !material->IsDrawn() || !material->HasAmbient() + || material->IsPortalSky() || material->SuppressInSubview() + || material->Coverage() != MC_OPAQUE + || material->GetSort() >= SS_POST_PROCESS ) { + return false; + } + if ( geometry->numIndexes <= 0 || R_TriHasPrimBatchMesh( geometry ) + || geometry->deformedSurface || drawSurf->space->weaponDepthHack + || drawSurf->space->modelDepthHack != 0.0f ) { + return false; + } + return entity->index >= 0 && entity->parms.callback == NULL + && entity->parms.hModel != NULL + && entity->parms.hModel->IsDynamicModel() == DM_STATIC + && entity->dynamicModel == NULL; +} + +static scenePacketCategory_t R_ScenePackets_CategoryForDrawSurf( const viewDef_t *viewDef, const drawSurf_t *drawSurf, renderPassCategory_t passCategory, const materialResourceRecord_t *materialRecord = NULL ) { if ( passCategory == RENDER_PASS_SPECIAL_EFFECTS ) { return SCENE_PACKET_CATEGORY_SPECIAL_EFFECTS; } @@ -305,7 +544,14 @@ static scenePacketCategory_t R_ScenePackets_CategoryForDrawSurf( const viewDef_t return SCENE_PACKET_CATEGORY_POST_PROCESS; } if ( passCategory == RENDER_PASS_GUI ) { - return SCENE_PACKET_CATEGORY_GUI; + // A GUI pass in a 3D scene is the provenance-tagged output of + // R_RenderGuiSurf, not a root-2D command list. Keep it in the world + // domain so material compilation retains depth-test semantics. + return viewDef != NULL && viewDef->viewEntitys != NULL + ? SCENE_PACKET_CATEGORY_WORLD : SCENE_PACKET_CATEGORY_GUI; + } + if ( R_ScenePackets_IsRenderDemoPlaybackView( viewDef ) ) { + return SCENE_PACKET_CATEGORY_RENDER_DEMO; } if ( viewDef != NULL && viewDef->isSubview ) { return SCENE_PACKET_CATEGORY_SUBVIEW; @@ -322,10 +568,16 @@ static scenePacketCategory_t R_ScenePackets_CategoryForDrawSurf( const viewDef_t if ( drawSurf->space != NULL && drawSurf->space->entityDef != NULL && drawSurf->space->entityDef->parms.remoteRenderView != NULL ) { return SCENE_PACKET_CATEGORY_REMOTE_CAMERA; } - if ( R_ScenePackets_MaterialUsesRemoteRender( drawSurf->material ) ) { + const bool usesRemoteRender = materialRecord != NULL + ? materialRecord->usesRemoteRender + : R_ScenePackets_MaterialUsesRemoteRender( drawSurf->material ); + if ( usesRemoteRender ) { return SCENE_PACKET_CATEGORY_REMOTE_CAMERA; } - if ( R_ScenePackets_MaterialClassForDrawSurf( drawSurf ) == RENDER_MATERIAL_SUBVIEW ) { + const rendererMaterialClass_t materialClass = materialRecord != NULL + ? static_cast( materialRecord->permutation.materialClass ) + : R_ScenePackets_MaterialClassForDrawSurf( drawSurf ); + if ( materialClass == RENDER_MATERIAL_SUBVIEW ) { return SCENE_PACKET_CATEGORY_SUBVIEW; } return SCENE_PACKET_CATEGORY_WORLD; @@ -341,12 +593,16 @@ static scenePacketCategory_t R_ScenePackets_CategoryForCommandPass( renderPassCa case RENDER_PASS_AUTHORED_POST: return SCENE_PACKET_CATEGORY_POST_PROCESS; case RENDER_PASS_GUI: - return SCENE_PACKET_CATEGORY_GUI; + return viewDef != NULL && viewDef->viewEntitys != NULL + ? SCENE_PACKET_CATEGORY_WORLD : SCENE_PACKET_CATEGORY_GUI; case RENDER_PASS_PRESENT: return SCENE_PACKET_CATEGORY_PRESENT; default: break; } + if ( R_ScenePackets_IsRenderDemoPlaybackView( viewDef ) ) { + return SCENE_PACKET_CATEGORY_RENDER_DEMO; + } if ( viewDef != NULL && viewDef->isSubview ) { return SCENE_PACKET_CATEGORY_SUBVIEW; } @@ -501,7 +757,31 @@ int idScenePacketFrame::FindOrAddMaterialRecord( const drawSurf_t *drawSurf ) { } record.normalImage = R_ScenePackets_FirstStageImage( material, SL_BUMP ); record.specularImage = R_ScenePackets_FirstStageImage( material, SL_SPECULAR ); + if ( material->HasPBR() ) { + const pbrMaterialInfo_t &pbr = material->GetPBRInfo(); + record.hasPBR = true; + record.pbrWorkflow = static_cast( pbr.workflow ); + record.pbrNormalFormat = static_cast( pbr.normalFormat ); + record.pbrAlbedoImage = pbr.albedo.present ? pbr.albedo.image : NULL; + record.pbrNormalImage = pbr.normal.present ? pbr.normal.image : NULL; + record.pbrORMImage = pbr.orm.present ? pbr.orm.image : NULL; + record.pbrMetallicImage = pbr.metallic.present ? pbr.metallic.image : NULL; + record.pbrRoughnessImage = pbr.roughness.present ? pbr.roughness.image : NULL; + record.pbrAOImage = pbr.ao.present ? pbr.ao.image : NULL; + record.pbrEmissiveImage = pbr.emissive.present ? pbr.emissive.image : NULL; + record.pbrHasAuthoredClassicFallback = pbr.hasAuthoredClassicFallback; + record.pbrHasExplicitLegacyFallback = pbr.hasExplicitLegacyFallback; + record.pbrUsesGeneratedLegacyFallback = pbr.usesGeneratedLegacyFallback; + record.pbrUsesApproximateLegacyFallback = pbr.usesApproximateLegacyFallback; + record.pbrLegacyFallbackMissing = pbr.legacyFallbackMissing; + record.pbrMetallicRegister = pbr.metallicRegister; + record.pbrRoughnessRegister = pbr.roughnessRegister; + record.pbrAORegister = pbr.aoRegister; + record.pbrNormalScaleRegister = pbr.normalScaleRegister; + memcpy( record.pbrEmissiveColorRegisters, pbr.emissiveColorRegisters, sizeof( record.pbrEmissiveColorRegisters ) ); + } record.resourceTableIndex = material->Index(); + record.usesRemoteRender = R_ScenePackets_MaterialUsesRemoteRender( material ); record.permutation.materialClass = R_ScenePackets_MaterialClassForDrawSurf( drawSurf ); record.permutation.lightingMode = ( material->IsDrawn() ? 1u : 0u ) | @@ -514,10 +794,12 @@ int idScenePacketFrame::FindOrAddMaterialRecord( const drawSurf_t *drawSurf ) { ( material->TestMaterialFlag( MF_NOSELFSHADOW ) ? 2u : 0u ) | ( material->TestMaterialFlag( MF_FORCESHADOWS ) ? 4u : 0u ); record.permutation.alphaMode = material->Coverage(); - record.permutation.skinningMode = R_ScenePackets_DrawSurfUsesSkinning( drawSurf ) ? 1u : 0u; - record.permutation.deformMode = - ( drawSurf != NULL && drawSurf->geo != NULL && drawSurf->geo->deformedSurface ) ? 2u : - ( material->Deform() != DFRM_NONE ? 1u : 0u ); + record.permutation.skinningMode = + R_ScenePackets_DrawSurfSkinningMode( drawSurf ) != GEOMETRY_SKINNING_NONE ? 1u : 0u; + // srfTriangles_t::deformedSurface is generated-model topology ownership, + // not evidence of material deformation. Per-draw execution provenance is + // sealed separately in classicDeformRecord_t. + record.permutation.deformMode = material->Deform() != DFRM_NONE ? 1u : 0u; record.permutation.lightGridMode = ( material->ReceivesLighting() ? 1u : 0u ) | ( material->HasAmbient() ? 2u : 0u ); @@ -527,23 +809,29 @@ int idScenePacketFrame::FindOrAddMaterialRecord( const drawSurf_t *drawSurf ) { return recordIndex; } -int idScenePacketFrame::FindOrAddGeometryRecord( const drawSurf_t *drawSurf ) { +int idScenePacketFrame::FindOrAddGeometryRecord( const drawSurf_t *drawSurf, + const classicDeformRecord_t &classicDeform ) { if ( drawSurf == NULL || drawSurf->geo == NULL ) { return -1; } const srfTriangles_t *geo = drawSurf->geo; + const int lookupKey = R_ScenePackets_GeometryLookupKey( geo, classicDeform ); const int lookupSlot = R_ScenePackets_PreparedLookupSlot( this ); if ( lookupSlot >= 0 ) { const idHashIndex &lookup = rg_scenePacketGeometryLookup[lookupSlot]; - for ( int i = lookup.First( R_ScenePackets_PointerLookupKey( geo ) ); i >= 0; i = lookup.Next( i ) ) { - if ( geometryRecords[i].legacyGeometry == geo ) { + for ( int i = lookup.First( lookupKey ); i >= 0; i = lookup.Next( i ) ) { + if ( geometryRecords[i].legacyGeometry == geo + && R_ClassicDeformDomain_SameProvenance( + geometryRecords[i].classicDeform, classicDeform ) ) { return i; } } } else { for ( int i = 0; i < stats.geometryRecords; ++i ) { - if ( geometryRecords[i].legacyGeometry == geo ) { + if ( geometryRecords[i].legacyGeometry == geo + && R_ClassicDeformDomain_SameProvenance( + geometryRecords[i].classicDeform, classicDeform ) ) { return i; } } @@ -556,7 +844,7 @@ int idScenePacketFrame::FindOrAddGeometryRecord( const drawSurf_t *drawSurf ) { const int recordIndex = stats.geometryRecords++; if ( lookupSlot >= 0 ) { - rg_scenePacketGeometryLookup[lookupSlot].Add( R_ScenePackets_PointerLookupKey( geo ), recordIndex ); + rg_scenePacketGeometryLookup[lookupSlot].Add( lookupKey, recordIndex ); } geometryResourceRecord_t &record = geometryRecords[recordIndex]; memset( &record, 0, sizeof( record ) ); @@ -569,14 +857,29 @@ int idScenePacketFrame::FindOrAddGeometryRecord( const drawSurf_t *drawSurf ) { record.firstIndex = 0; record.vertexStride = sizeof( idDrawVert ); record.indexType = GL_INDEX_TYPE; - record.skinningMode = R_ScenePackets_DrawSurfUsesSkinning( drawSurf ) ? GEOMETRY_SKINNING_GPU_PALETTE : GEOMETRY_SKINNING_NONE; - record.deformMode = geo->deformedSurface ? GEOMETRY_DEFORM_SURFACE : - ( drawSurf->material != NULL && drawSurf->material->Deform() != DFRM_NONE ? GEOMETRY_DEFORM_MATERIAL : GEOMETRY_DEFORM_NONE ); + record.classicDeform = classicDeform; + record.hasClassicDeformRecord = + R_ClassicDeformDomain_ValidateRecordForFrame( classicDeform, + classicDeform.frameToken ); + record.skinningMode = R_ScenePackets_DrawSurfSkinningMode( + drawSurf, &record.gpuSkinningSurface ); + record.hasGpuSkinningContract = record.skinningMode == GEOMETRY_SKINNING_GPU_PALETTE + && record.gpuSkinningSurface.fallbackReason == GPU_SKINNING_FALLBACK_NONE + && record.gpuSkinningSurface.bindPoseVerts != NULL; + // Generated/dynamic topology remains an explicit surface-ownership class; + // it is never evidence that a material deform executed. Material deformation + // is classified only from the sealed per-draw record. + record.deformMode = geo->deformedSurface + ? GEOMETRY_DEFORM_SURFACE + : ( classicDeform.kind != CLASSIC_DEFORM_KIND_NONE + ? GEOMETRY_DEFORM_MATERIAL : GEOMETRY_DEFORM_NONE ); record.uploadLifetime = R_ScenePackets_UploadLifetimeForCache( geo->ambientCache, geo->indexCache, geo ); record.fallbackReason = GEOMETRY_RESOURCE_FALLBACK_NONE; - record.skinningPaletteOffset = 0; + record.skinningPaletteOffset = record.hasGpuSkinningContract + ? static_cast( record.gpuSkinningSurface.palette.buffer.offsetBytes ) : 0; #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) - record.skinningPaletteCount = geo->numSkinToModelTransforms; + record.skinningPaletteCount = record.hasGpuSkinningContract + ? record.gpuSkinningSurface.palette.numJoints : geo->numSkinToModelTransforms; record.hasPrimBatchMesh = geo->primBatchMesh != NULL; #else record.skinningPaletteCount = 0; @@ -618,7 +921,8 @@ int idScenePacketFrame::FindOrAddGeometryRecord( const drawSurf_t *drawSurf ) { GEOMETRY_RESOURCE_FALLBACK_UNSUPPORTED_DEFORM, GEOMETRY_RESOURCE_FALLBACK_FLAG_UNSUPPORTED_DEFORM ); } - if ( record.skinningMode == GEOMETRY_SKINNING_GPU_PALETTE ) { + if ( record.skinningMode == GEOMETRY_SKINNING_GPU_PALETTE + && !record.hasGpuSkinningContract ) { R_ScenePackets_AddGeometryFallback( record, GEOMETRY_RESOURCE_FALLBACK_UNSUPPORTED_GPU_SKINNING, @@ -683,16 +987,32 @@ int idScenePacketFrame::FindOrAddInstanceRecord( const drawSurf_t *drawSurf, sce record.shaderRegisterCount = ( shaderRegisters != NULL && drawSurf->material != NULL ) ? drawSurf->material->GetNumRegisters() : 0; record.skinningPaletteOffset = 0; record.visibilityFlags = R_ScenePackets_InstanceVisibilityFlags( drawSurf, packetCategory, activeScene >= 0 && scenes[activeScene].legacyBridge ); + const viewDef_t *instanceView = activeScene >= 0 + ? scenes[activeScene].viewDef : NULL; + record.temporalViewIdentity = instanceView != NULL + ? ( instanceView->temporalViewIdentity != 0 + ? instanceView->temporalViewIdentity + : R_TemporalPresentation_ViewIdentity( instanceView ) ) : 0; + record.temporalHistoryGeneration = + R_TemporalPresentation_HistoryGeneration(); + record.temporalHistoryAgeFrames = 0; memcpy( record.modelMatrix, space->modelMatrix, sizeof( record.modelMatrix ) ); - memcpy( record.previousModelMatrix, space->modelMatrix, sizeof( record.previousModelMatrix ) ); + record.hasPreviousModelMatrix = R_ScenePackets_FindTemporalInstanceHistory( + record.temporalViewIdentity, record.entityIndex, + record.previousModelMatrix, record.temporalHistoryAgeFrames ); + if ( !record.hasPreviousModelMatrix ) { + memcpy( record.previousModelMatrix, space->modelMatrix, + sizeof( record.previousModelMatrix ) ); + } memcpy( record.modelViewMatrix, space->modelViewMatrix, sizeof( record.modelViewMatrix ) ); record.modelDepthHack = space->modelDepthHack; record.hasModelMatrix = true; - record.hasPreviousModelMatrix = true; record.hasShaderRegisters = shaderRegisters != NULL; record.weaponDepthHack = space->weaponDepthHack; record.negativeScale = R_ScenePackets_MatrixHasNegativeScale( space->modelMatrix ); record.legacyBridge = activeScene >= 0 && scenes[activeScene].legacyBridge; + record.temporalCaptureFrame = instanceView != NULL + && instanceView->temporalCaptureFrame; if ( shaderRegisters != NULL ) { record.entityColor[0] = shaderRegisters[EXP_REG_PARM0]; record.entityColor[1] = shaderRegisters[EXP_REG_PARM1]; @@ -723,11 +1043,25 @@ bool idScenePacketFrame::AddScene( const viewDef_t *viewDef, bool legacyBridge ) scenePacket_t &scene = scenes[stats.scenePackets++]; memset( &scene, 0, sizeof( scene ) ); scene.viewDef = viewDef; - scene.packetCategory = viewDef != NULL && viewDef->isSubview ? SCENE_PACKET_CATEGORY_SUBVIEW : - ( viewDef != NULL && viewDef->renderView.viewID < 0 ? SCENE_PACKET_CATEGORY_RENDER_DEMO : SCENE_PACKET_CATEGORY_WORLD ); + scene.renderDemoPlayback = R_ScenePackets_IsRenderDemoPlaybackView( viewDef ); + scene.packetCategory = scene.renderDemoPlayback ? SCENE_PACKET_CATEGORY_RENDER_DEMO : + ( viewDef != NULL && viewDef->isSubview ? SCENE_PACKET_CATEGORY_SUBVIEW : + ( viewDef != NULL && viewDef->renderView.viewID < 0 ? SCENE_PACKET_CATEGORY_RENDER_DEMO : SCENE_PACKET_CATEGORY_WORLD ) ); scene.firstPassPacket = stats.passPackets; scene.firstDrawPacket = stats.drawPackets; scene.legacyBridge = legacyBridge; + if ( viewDef != NULL ) { + scene.temporalViewIdentity = viewDef->temporalViewIdentity != 0 + ? viewDef->temporalViewIdentity + : R_TemporalPresentation_ViewIdentity( viewDef ); + scene.temporalHistoryGeneration = viewDef->temporalHistoryGeneration; + scene.temporalJitterIndex = viewDef->temporalJitterIndex; + scene.temporalHistoryResetReason = + viewDef->temporalHistoryResetReason; + scene.temporalHistoryValid = viewDef->temporalHistoryValid; + scene.temporalJitterEnabled = viewDef->temporalJitterEnabled; + scene.temporalCaptureFrame = viewDef->temporalCaptureFrame; + } activeScene = stats.scenePackets - 1; activePass = -1; activePassLastSortKey = 0; @@ -764,7 +1098,9 @@ bool idScenePacketFrame::AddPass( renderPassCategory_t category, bool enabled, b return true; } -bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, renderPassCategory_t category, int drawIndex ) { +bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, + renderPassCategory_t category, int drawIndex, + classicDeformRole_t deformRole ) { if ( activePass < 0 ) { if ( !AddPass( category, true ) ) { return false; @@ -779,8 +1115,19 @@ bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, renderPassCa drawPacket_t &packet = drawPackets[stats.drawPackets++]; memset( &packet, 0, sizeof( packet ) ); const int materialRecordIndex = FindOrAddMaterialRecord( drawSurf ); - const scenePacketCategory_t packetCategory = R_ScenePackets_CategoryForDrawSurf( activeScene >= 0 ? scenes[activeScene].viewDef : NULL, drawSurf, category ); - const int geometryRecordIndex = FindOrAddGeometryRecord( drawSurf ); + const materialResourceRecord_t *cachedMaterialRecord = + materialRecordIndex >= 0 ? &materialRecords[materialRecordIndex] : NULL; + const scenePacketCategory_t packetCategory = R_ScenePackets_CategoryForDrawSurf( activeScene >= 0 ? scenes[activeScene].viewDef : NULL, drawSurf, category, cachedMaterialRecord ); + classicDeformRecord_t classicDeform; + const std::uint64_t deformFrameToken = + R_ClassicDeformDomain_CurrentFrameToken(); + if ( deformRole == CLASSIC_DEFORM_ROLE_UNKNOWN ) { + deformRole = R_ScenePackets_ClassicDeformRoleForPass( category ); + } + R_ClassicDeformDomain_SnapshotDrawSurf( drawSurf, + deformRole, deformFrameToken, classicDeform ); + const int geometryRecordIndex = FindOrAddGeometryRecord( + drawSurf, classicDeform ); const int instanceRecordIndex = FindOrAddInstanceRecord( drawSurf, packetCategory ); packet.legacyDrawSurf = drawSurf; packet.viewDef = activeScene >= 0 ? scenes[activeScene].viewDef : NULL; @@ -801,6 +1148,55 @@ bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, renderPassCa packet.vertexOffset = 0; packet.instanceOffset = 0; packet.instanceCount = drawSurf ? 1 : 0; + temporalMotionInput_t temporalInput = {}; + temporalInput.hasEntity = packet.instanceRecord != NULL + && packet.instanceRecord->entityIndex >= 0; + temporalInput.hasPreviousTransform = packet.instanceRecord != NULL + && packet.instanceRecord->hasPreviousModelMatrix; + temporalInput.skinned = packet.geometryRecord != NULL + && packet.geometryRecord->skinningMode != GEOMETRY_SKINNING_NONE; + // Previous joint palettes and previous material-deformed vertices are not + // silently approximated as rigid motion. Until a backend consumes those + // explicit streams, the shared policy marks the surfaces reactive. + temporalInput.hasPreviousSkinningPalette = false; + temporalInput.particle = drawSurf != NULL + && ( drawSurf->dsFlags & DSF_BSE_EFFECT ) != 0; + temporalInput.deform = packet.geometryRecord != NULL + && packet.geometryRecord->deformMode != GEOMETRY_DEFORM_NONE; + temporalInput.hasPreviousDeformedVertices = false; + temporalInput.subview = cachedMaterialRecord != NULL + ? ( cachedMaterialRecord->usesRemoteRender + || static_cast( + cachedMaterialRecord->permutation.materialClass ) + == RENDER_MATERIAL_SUBVIEW ) + : ( drawSurf != NULL + && ( R_ScenePackets_MaterialUsesRemoteRender( drawSurf->material ) + || R_ScenePackets_MaterialClassForDrawSurf( drawSurf ) + == RENDER_MATERIAL_SUBVIEW ) ); + temporalInput.inWorldGui = drawSurf != NULL + && ( drawSurf->dsFlags & DSF_IN_WORLD_GUI ) != 0; + temporalInput.viewModel = drawSurf != NULL && drawSurf->space != NULL + && drawSurf->space->weaponDepthHack; + temporalInput.translucent = drawSurf != NULL && drawSurf->material != NULL + && drawSurf->material->Coverage() != MC_OPAQUE; + packet.temporalMotion = TemporalHistoryCore_ClassifyMotion( temporalInput ); + packet.temporalExactRigidEligible = + packet.temporalMotion.domain == TEMPORAL_MOTION_DOMAIN_RIGID + && R_ScenePackets_TemporalRigidMotionEligible( drawSurf ); + if ( activeScene >= 0 && scenes[activeScene].viewDef != NULL + && scenes[activeScene].viewDef->isSubview ) { + packet.temporalMotion.flags |= + TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY; + } + packet.classicDeformRecord = packet.geometryRecord != NULL + ? &packet.geometryRecord->classicDeform : NULL; + packet.hasClassicDeformRecord = packet.classicDeformRecord != NULL + && packet.geometryRecord->hasClassicDeformRecord + && R_ClassicDeformDomain_ValidateRecordForFrame( + *packet.classicDeformRecord, deformFrameToken ) + && R_ClassicDeformDomain_SameProvenance( + *packet.classicDeformRecord, classicDeform ); + R_ClassicDeformDomain_RecordPacket( classicDeform ); if ( drawSurf != NULL ) { packet.scissorX1 = drawSurf->scissorRect.x1; packet.scissorY1 = drawSurf->scissorRect.y1; @@ -821,6 +1217,22 @@ bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, renderPassCa passes[activePass].drawPacketCount++; scenes[activeScene].drawPacketCount++; CountCategory( packetCategory ); + if ( packet.temporalMotion.domain >= TEMPORAL_MOTION_DOMAIN_STATIC_WORLD + && packet.temporalMotion.domain < TEMPORAL_MOTION_DOMAIN_COUNT ) { + stats.temporalMotionDomainPackets[packet.temporalMotion.domain]++; + } + if ( ( packet.temporalMotion.flags + & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) != 0 ) { + stats.temporalReactivePackets++; + } + if ( ( packet.temporalMotion.flags + & TEMPORAL_MOTION_OWNERSHIP_HAS_PREVIOUS_TRANSFORM ) != 0 ) { + stats.temporalPreviousTransformPackets++; + } + if ( ( packet.temporalMotion.flags + & TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY ) != 0 ) { + stats.temporalSeparateHistoryPackets++; + } if ( drawSurf != NULL && drawSurf->material != NULL ) { stats.drawPacketsWithMaterial++; } @@ -845,6 +1257,126 @@ bool idScenePacketFrame::AddDrawPacket( const drawSurf_t *drawSurf, renderPassCa if ( packet.hasAmbientCache ) { stats.drawPacketsWithAmbientCache++; } + if ( packet.hasClassicDeformRecord ) { + stats.drawPacketsWithClassicDeformRecord++; + } + if ( classicDeform.kind != CLASSIC_DEFORM_KIND_NONE ) { + stats.materialDeformDrawPackets++; + switch ( classicDeform.role ) { + case CLASSIC_DEFORM_ROLE_FINALIZED_DRAW: + stats.deformFinalizedDrawPackets++; + break; + case CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER: + stats.deformInteractionReceiverPackets++; + break; + case CLASSIC_DEFORM_ROLE_FOG_RECEIVER: + stats.deformFogReceiverPackets++; + break; + case CLASSIC_DEFORM_ROLE_SHADOW_VOLUME: + stats.deformShadowVolumePackets++; + break; + default: + stats.deformOtherRolePackets++; + break; + } + switch ( classicDeform.outcome ) { + case CLASSIC_DEFORM_OUTCOME_COMPLETED: + stats.deformCompletedPackets++; + break; + case CLASSIC_DEFORM_OUTCOME_EMPTY: + stats.deformEmptyPackets++; + break; + case CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE: + stats.deformNotApplicablePackets++; + break; + case CLASSIC_DEFORM_OUTCOME_SKIPPED: + stats.deformSkippedPackets++; + break; + case CLASSIC_DEFORM_OUTCOME_FAILED: + stats.deformFailedPackets++; + break; + case CLASSIC_DEFORM_OUTCOME_UNSUPPORTED: + stats.deformUnsupportedPackets++; + break; + default: + break; + } + if ( ( packet.geometryRecord != NULL + && ( packet.geometryRecord->fallbackFlags + & GEOMETRY_RESOURCE_FALLBACK_FLAG_UNSUPPORTED_DEFORM ) != 0 ) + || R_ClassicDeformDomain_IsFailClosed( classicDeform ) ) { + stats.deformFallbackPackets++; + } + } + return true; +} + +void idScenePacketFrame::SetLastSceneSpecialEffectsMask( + int specialEffectsMask ) { + const int sceneIndex = stats.scenePackets - 1; + if ( sceneIndex < 0 || sceneIndex >= stats.scenePackets ) { + return; + } + scenes[ sceneIndex ].specialEffectsMask = specialEffectsMask + & ( SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL ); +} + +bool idScenePacketFrame::AddInteractionDrawPacket( const drawSurf_t *drawSurf, + int drawIndex, const viewLight_t *viewLight, int lightOrdinal, + sceneInteractionReceiverClass_t receiverClass, int receiverOrdinal ) { + if ( !AddDrawPacket( drawSurf, RENDER_PASS_ARB2_INTERACTION, drawIndex ) ) { + return false; + } + drawPacket_t &packet = drawPackets[stats.drawPackets - 1]; + packet.interactionLight = viewLight; + packet.interactionLightOrdinal = lightOrdinal; + packet.interactionReceiverClass = receiverClass; + packet.interactionReceiverOrdinal = receiverOrdinal; + packet.interactionSourceOrdinal = drawIndex; + return true; +} + +bool idScenePacketFrame::AddFogBlendDrawPacket( const drawSurf_t *drawSurf, + int drawIndex, const viewLight_t *viewLight, int lightOrdinal, + sceneFogBlendReceiverClass_t receiverClass, int receiverOrdinal ) { + if ( receiverClass <= SCENE_FOG_BLEND_RECEIVER_NONE + || receiverClass >= SCENE_FOG_BLEND_RECEIVER_COUNT + || !AddDrawPacket( drawSurf, RENDER_PASS_FOG_BLEND, drawIndex ) ) { + return false; + } + drawPacket_t &packet = drawPackets[stats.drawPackets - 1]; + packet.fogBlendLight = viewLight; + packet.fogBlendLightOrdinal = lightOrdinal; + packet.fogBlendReceiverClass = receiverClass; + packet.fogBlendReceiverOrdinal = receiverOrdinal; + packet.fogBlendSourceOrdinal = drawIndex; + return true; +} + +bool idScenePacketFrame::AddShadowDrawPacket( const drawSurf_t *drawSurf, + renderPassCategory_t category, int drawIndex, + const viewLight_t *viewLight, int lightOrdinal, + sceneShadowCasterClass_t casterClass, int chainOrdinal ) { + if ( ( category != RENDER_PASS_STENCIL_SHADOW + && category != RENDER_PASS_SHADOW_MAP ) + || casterClass <= SCENE_SHADOW_CASTER_NONE + || casterClass >= SCENE_SHADOW_CASTER_COUNT ) { + return false; + } + const bool stencilGeometry = category == RENDER_PASS_STENCIL_SHADOW + || casterClass == SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL + || casterClass == SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL; + if ( !AddDrawPacket( drawSurf, category, drawIndex, + stencilGeometry ? CLASSIC_DEFORM_ROLE_SHADOW_VOLUME + : CLASSIC_DEFORM_ROLE_FINALIZED_DRAW ) ) { + return false; + } + drawPacket_t &packet = drawPackets[stats.drawPackets - 1]; + packet.shadowLight = viewLight; + packet.shadowLightOrdinal = lightOrdinal; + packet.shadowCasterClass = casterClass; + packet.shadowChainOrdinal = chainOrdinal; + packet.shadowSourceOrdinal = drawIndex; return true; } @@ -860,6 +1392,40 @@ void idScenePacketFrame::AddCommandPacket( scenePacketCategory_t category ) { CountCategory( category ); } +bool idScenePacketFrame::AddSubviewCapture( const viewDef_t *viewDef, + idImage *image, int x, int y, int width, int height, int cubeFace, + bool copyDepth ) { + if ( viewDef == NULL || image == NULL || width <= 0 || height <= 0 ) { + return false; + } + if ( stats.subviewCaptures >= SCENE_PACKET_MAX_SUBVIEW_CAPTURES ) { + SetOverflow( SCENE_PACKET_OVERFLOW_SUBVIEW_CAPTURES ); + return false; + } + int scenePacketIndex = -1; + for ( int i = stats.scenePackets - 1; i >= 0; --i ) { + if ( scenes[ i ].viewDef == viewDef ) { + scenePacketIndex = i; + break; + } + } + if ( scenePacketIndex < 0 ) { + return false; + } + sceneSubviewCapture_t &capture = subviewCaptures[ stats.subviewCaptures++ ]; + memset( &capture, 0, sizeof( capture ) ); + capture.viewDef = viewDef; + capture.image = image; + capture.viewScenePacketIndex = scenePacketIndex; + capture.x = x; + capture.y = y; + capture.width = width; + capture.height = height; + capture.cubeFace = cubeFace; + capture.copyDepth = copyDepth; + return true; +} + void idScenePacketFrame::AddLegacyDrawView( void ) { stats.legacyDrawViews++; } @@ -927,6 +1493,14 @@ const instanceRecord_t &idScenePacketFrame::InstanceRecord( int index ) const { return instanceRecords[index]; } +int idScenePacketFrame::NumSubviewCaptures( void ) const { + return stats.subviewCaptures; +} + +const sceneSubviewCapture_t &idScenePacketFrame::SubviewCapture( int index ) const { + return subviewCaptures[index]; +} + const scenePacketFrameStats_t &idScenePacketFrame::Stats( void ) const { return stats; } @@ -955,6 +1529,91 @@ bool idScenePacketFrame::ValidateSortKeys( void ) const { return stats.sortKeyValidationFailures == 0; } +static bool R_ScenePackets_TemporalVisiblePass( + renderPassCategory_t category ) { + switch ( category ) { + case RENDER_PASS_ARB2_INTERACTION: + case RENDER_PASS_LIGHT_GRID: + case RENDER_PASS_AMBIENT: + case RENDER_PASS_DEFERRED_RESOLVE: + case RENDER_PASS_FORWARD_PLUS: + case RENDER_PASS_FOG_BLEND: + case RENDER_PASS_GUI: + return true; + default: + // Depth, shadow, and fullscreen post packets are not independently + // visible surfaces in the color image consumed by temporal resolve. + return false; + } +} + +bool idScenePacketFrame::BuildTemporalViewMotionPolicy( + const viewDef_t *viewDef, unsigned int backendExactMotionDomainMask, + temporalViewMotionPolicy_t &policy ) const { + policy = TemporalHistoryCore_BeginViewMotionPolicy(); + if ( viewDef == NULL ) { + return false; + } + + const int viewWidth = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; + const int viewHeight = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; + if ( viewWidth <= 0 || viewHeight <= 0 ) { + return false; + } + const float inverseWidth = 1.0f / static_cast( viewWidth ); + const float inverseHeight = 1.0f / static_cast( viewHeight ); + bool foundScene = false; + for ( int sceneIndex = 0; sceneIndex < stats.scenePackets; ++sceneIndex ) { + const scenePacket_t &scene = scenes[sceneIndex]; + if ( scene.viewDef != viewDef ) { + continue; + } + foundScene = true; + for ( int drawIndex = 0; drawIndex < scene.drawPacketCount; ++drawIndex ) { + const int packetIndex = scene.firstDrawPacket + drawIndex; + if ( packetIndex < 0 || packetIndex >= stats.drawPackets ) { + continue; + } + const drawPacket_t &packet = drawPackets[packetIndex]; + if ( !R_ScenePackets_TemporalVisiblePass( packet.passCategory ) + || !packet.hasGeometry ) { + continue; + } + unsigned int packetExactMotionDomainMask = + backendExactMotionDomainMask; + if ( packet.temporalMotion.domain == TEMPORAL_MOTION_DOMAIN_RIGID + && !packet.temporalExactRigidEligible ) { + packetExactMotionDomainMask &= + ~TemporalHistoryCore_MotionDomainBit( + TEMPORAL_MOTION_DOMAIN_RIGID ); + } + TemporalHistoryCore_AddMotionOwnership( policy, + packet.temporalMotion, packetExactMotionDomainMask, + static_cast( packet.scissorX1 ) * inverseWidth, + static_cast( packet.scissorY1 ) * inverseHeight, + static_cast( packet.scissorX2 + 1 ) * inverseWidth, + static_cast( packet.scissorY2 + 1 ) * inverseHeight ); + } + } + + if ( foundScene && stats.overflow ) { + // Packet clipping makes a partial policy unsafe. Reject history over the + // full view for every object-motion domain rather than leaving clipped + // particles, characters, or view models with unowned stale history. + unsigned int overflowDomains = 0u; + for ( int domain = TEMPORAL_MOTION_DOMAIN_RIGID; + domain < TEMPORAL_MOTION_DOMAIN_COUNT; ++domain ) { + overflowDomains |= TemporalHistoryCore_MotionDomainBit( + static_cast( domain ) ); + } + policy.presentDomainMask |= overflowDomains; + policy.reactiveDomainMask |= overflowDomains; + TemporalHistoryCore_AddReactiveRegion( policy, overflowDomains, + 0.0f, 0.0f, 1.0f, 1.0f ); + } + return foundScene; +} + const char *RenderPassCategory_Name( renderPassCategory_t category ) { switch ( category ) { case RENDER_PASS_DEPTH: @@ -1038,6 +1697,8 @@ const char *ScenePacketOverflowCause_Name( scenePacketOverflowCause_t cause ) { return "geometryRecords"; case SCENE_PACKET_OVERFLOW_INSTANCE_RECORDS: return "instanceRecords"; + case SCENE_PACKET_OVERFLOW_SUBVIEW_CAPTURES: + return "subviewCaptures"; default: return "unknown"; } @@ -1089,6 +1750,9 @@ static void R_ScenePackets_EnsureFrontEndFrame( void ) { if ( !rg_frontEndScenePacketFrameOpen ) { rg_frontEndScenePacketFrame.Clear(); rg_frontEndScenePacketFrameOpen = true; + rg_frontEndLastDrawView = NULL; + R_ClassicDeformDomain_BeginPacketFrame( + R_ClassicDeformDomain_CurrentFrameToken() ); } rg_frontEndScenePacketFrame.MarkFrontEndDerived(); } @@ -1097,6 +1761,9 @@ void R_ScenePackets_BeginFrame( void ) { rg_frontEndScenePacketFrame.Clear(); rg_frontEndScenePacketFrame.MarkFrontEndDerived(); rg_frontEndScenePacketFrameOpen = true; + rg_frontEndLastDrawView = NULL; + R_ClassicDeformDomain_BeginPacketFrame( + R_ClassicDeformDomain_CurrentFrameToken() ); } void R_ScenePackets_EndFrame( void ) { @@ -1105,8 +1772,11 @@ void R_ScenePackets_EndFrame( void ) { // Clear() from the constructor or a prior EndFrame, nothing to do. return; } + R_ScenePackets_CommitTemporalInstanceHistory( + rg_frontEndScenePacketFrame ); rg_frontEndScenePacketFrame.Clear(); rg_frontEndScenePacketFrameOpen = false; + rg_frontEndLastDrawView = NULL; } typedef bool (*scenePacketDrawSurfFilter_t)( const viewDef_t *viewDef, const drawSurf_t *drawSurf ); @@ -1265,7 +1935,12 @@ static bool R_ScenePackets_DrawSurfAmbientEligible( const viewDef_t *viewDef, co return false; } const idMaterial *material = drawSurf->material; - if ( !material->HasAmbient() || material->IsPortalSky() || material->SuppressInSubview() ) { + // Clustered PBR lighting uses the ambient packet as the one stable surface + // owner for the complete light list. A valid PBR material is not required + // to author a legacy ambient stage, so retain it here even when HasAmbient() + // is false. The modern draw plan still fail-closes on PBR readiness and the + // classic renderer remains authoritative whenever that admission fails. + if ( ( !material->HasAmbient() && !material->HasPBR() ) || material->IsPortalSky() || material->SuppressInSubview() ) { return false; } if ( r_skipDecals.GetBool() && R_ScenePackets_DrawSurfIsDecalMaterialPass( drawSurf ) ) { @@ -1323,19 +1998,36 @@ static bool R_ScenePackets_DrawSurfGUIEligible( const viewDef_t *viewDef, const return material->HasAmbient() && !material->IsPortalSky(); } -static bool R_ScenePackets_DrawSurfInteractionEligible( const drawSurf_t *drawSurf ) { - if ( !R_ScenePackets_DrawSurfHasMaterialGeometry( drawSurf ) ) { +static bool R_ScenePackets_DrawSurfInWorldGUIEligible( const viewDef_t *viewDef, + const drawSurf_t *drawSurf ) { + return drawSurf != NULL && ( drawSurf->dsFlags & DSF_IN_WORLD_GUI ) != 0 + && R_ScenePackets_DrawSurfGUIEligible( viewDef, drawSurf ); +} + +// Preserve an empty GUI pass only when the front end actually emitted a +// provenance-tagged surface. That lets the ownership domain diagnose a +// malformed or otherwise ineligible tagged surface and fall back as a whole, +// without adding a synthetic GUI pass to every ordinary world view. +static bool R_ScenePackets_HasInWorldGUIDrawSurfs( const viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->drawSurfs == NULL || viewDef->numDrawSurfs <= 0 ) { return false; } - const idMaterial *material = drawSurf->material; - return material->ReceivesLighting() && !material->IsPortalSky(); + + for ( int i = 0; i < viewDef->numDrawSurfs; ++i ) { + const drawSurf_t *drawSurf = viewDef->drawSurfs[i]; + if ( drawSurf != NULL && ( drawSurf->dsFlags & DSF_IN_WORLD_GUI ) != 0 ) { + return true; + } + } + return false; } -static bool R_ScenePackets_DrawSurfFogBlendEligible( const drawSurf_t *drawSurf ) { +static bool R_ScenePackets_DrawSurfInteractionEligible( const drawSurf_t *drawSurf ) { if ( !R_ScenePackets_DrawSurfHasMaterialGeometry( drawSurf ) ) { return false; } - return drawSurf->material->ReceivesFog() && !drawSurf->material->SuppressInSubview(); + const idMaterial *material = drawSurf->material; + return material->ReceivesLighting() && !material->IsPortalSky(); } static bool R_ScenePackets_DrawSurfShadowEligible( const drawSurf_t *drawSurf ) { @@ -1380,35 +2072,65 @@ static int R_ScenePackets_CountDrawSurfChain( const drawSurf_t *drawSurf, bool ( return count; } -static bool R_ScenePackets_AppendDrawSurfChain( idScenePacketFrame &packetFrame, const drawSurf_t *drawSurf, renderPassCategory_t category, bool ( *filter )( const drawSurf_t *drawSurf ), int &drawIndex ) { - for ( const drawSurf_t *cursor = drawSurf; cursor != NULL; cursor = cursor->nextOnLight ) { +static bool R_ScenePackets_AppendInteractionChain( idScenePacketFrame &packetFrame, + const viewLight_t *viewLight, int lightOrdinal, + const drawSurf_t *drawSurf, sceneInteractionReceiverClass_t receiverClass, + bool ( *filter )( const drawSurf_t *drawSurf ), int &drawIndex ) { + int receiverOrdinal = 0; + for ( const drawSurf_t *cursor = drawSurf; cursor != NULL; + cursor = cursor->nextOnLight, ++receiverOrdinal ) { if ( filter != NULL && !filter( cursor ) ) { continue; } - if ( !packetFrame.AddDrawPacket( cursor, category, drawIndex++ ) ) { - packetFrame.AddClippedDrawPackets( R_ScenePackets_CountDrawSurfChain( cursor->nextOnLight, filter ) ); + if ( !packetFrame.AddInteractionDrawPacket( cursor, drawIndex++, viewLight, + lightOrdinal, receiverClass, receiverOrdinal ) ) { + packetFrame.AddClippedDrawPackets( + R_ScenePackets_CountDrawSurfChain( cursor->nextOnLight, filter ) ); return false; } } return true; } -static bool R_ScenePackets_AppendDrawSurfChainLazyPass( idScenePacketFrame &packetFrame, const drawSurf_t *drawSurf, renderPassCategory_t category, bool ( *filter )( const drawSurf_t *drawSurf ), int &drawIndex, bool &passAdded ) { - for ( const drawSurf_t *cursor = drawSurf; cursor != NULL; cursor = cursor->nextOnLight ) { +static bool R_ScenePackets_AppendFogBlendChain( + idScenePacketFrame &packetFrame, const viewLight_t *viewLight, + int lightOrdinal, const drawSurf_t *drawSurf, + sceneFogBlendReceiverClass_t receiverClass, int &drawIndex ) { + int receiverOrdinal = 0; + for ( const drawSurf_t *cursor = drawSurf; cursor != NULL; + cursor = cursor->nextOnLight, ++receiverOrdinal ) { + if ( !packetFrame.AddFogBlendDrawPacket( cursor, drawIndex++, viewLight, + lightOrdinal, receiverClass, receiverOrdinal ) ) { + packetFrame.AddClippedDrawPackets( + R_ScenePackets_CountDrawSurfChain( cursor->nextOnLight, NULL ) ); + return false; + } + } + return true; +} + +static bool R_ScenePackets_AppendShadowChainLazyPass( + idScenePacketFrame &packetFrame, const viewLight_t *viewLight, + int lightOrdinal, const drawSurf_t *drawSurf, + renderPassCategory_t category, sceneShadowCasterClass_t casterClass, + bool ( *filter )( const drawSurf_t *drawSurf ), int &drawIndex, + bool &passAdded ) { + int chainOrdinal = 0; + for ( const drawSurf_t *cursor = drawSurf; cursor != NULL; + cursor = cursor->nextOnLight, ++chainOrdinal ) { if ( filter != NULL && !filter( cursor ) ) { continue; } - // Pass existence drives executor ownership and legacy-skip decisions, - // so the pass is only opened once a surf actually passes the filter; - // AddDrawPacket would otherwise append into the previously open pass. if ( !passAdded ) { if ( !packetFrame.AddPass( category, true ) ) { return false; } passAdded = true; } - if ( !packetFrame.AddDrawPacket( cursor, category, drawIndex++ ) ) { - packetFrame.AddClippedDrawPackets( R_ScenePackets_CountDrawSurfChain( cursor->nextOnLight, filter ) ); + if ( !packetFrame.AddShadowDrawPacket( cursor, category, drawIndex++, + viewLight, lightOrdinal, casterClass, chainOrdinal ) ) { + packetFrame.AddClippedDrawPackets( + R_ScenePackets_CountDrawSurfChain( cursor->nextOnLight, filter ) ); return false; } } @@ -1456,26 +2178,28 @@ static bool R_ScenePackets_ViewLightCanCastShadows( const viewLight_t *vLight ) if ( !r_shadows.GetBool() || !vLight->lightShader->LightCastsShadows() ) { return false; } - if ( vLight->lightDef != NULL && ( vLight->lightDef->parms.noShadows || vLight->lightDef->parms.noDynamicShadows ) ) { + if ( vLight->lightDef != NULL && vLight->lightDef->parms.noShadows ) { return false; } return true; } -static bool R_ScenePackets_ViewLightIsFogOrBlend( const viewLight_t *vLight ) { - if ( vLight == NULL || vLight->lightShader == NULL ) { - return false; - } - if ( !vLight->lightShader->IsFogLight() && !vLight->lightShader->IsBlendLight() ) { +static bool R_ScenePackets_ViewLightCanUseShadowMaps( const viewLight_t *vLight ) { + if ( !R_ScenePackets_ViewLightCanCastShadows( vLight ) ) { return false; } - if ( r_skipFogLights.GetBool() ) { - return false; - } - if ( vLight->lightShader->IsBlendLight() && r_skipBlendLights.GetBool() ) { + // noDynamicShadows still permits dmap's optimized static/prelight stencil + // volumes. It only excludes the dynamic/map caster path. + return vLight->lightDef == NULL + || !vLight->lightDef->parms.noDynamicShadows; +} + +static bool R_ScenePackets_ViewLightIsFogOrBlend( const viewLight_t *vLight ) { + if ( vLight == NULL || vLight->lightShader == NULL ) { return false; } - return true; + return vLight->lightShader->IsFogLight() + || vLight->lightShader->IsBlendLight(); } static void R_ScenePackets_AddInteractionPass( idScenePacketFrame &packetFrame, const viewDef_t *viewDef ) { @@ -1484,17 +2208,29 @@ static void R_ScenePackets_AddInteractionPass( idScenePacketFrame &packetFrame, } int drawIndex = 0; - for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { + int lightOrdinal = 0; + for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; + vLight = vLight->next, ++lightOrdinal ) { if ( !R_ScenePackets_ViewLightHasMaterialInteractions( vLight ) ) { continue; } - if ( !R_ScenePackets_AppendDrawSurfChain( packetFrame, vLight->localInteractions, RENDER_PASS_ARB2_INTERACTION, R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { + if ( !R_ScenePackets_AppendInteractionChain( packetFrame, vLight, + lightOrdinal, vLight->localInteractions, + SCENE_INTERACTION_RECEIVER_LOCAL, + R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChain( packetFrame, vLight->globalInteractions, RENDER_PASS_ARB2_INTERACTION, R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { + if ( !R_ScenePackets_AppendInteractionChain( packetFrame, vLight, + lightOrdinal, vLight->globalInteractions, + SCENE_INTERACTION_RECEIVER_GLOBAL, + R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { return; } - if ( !r_skipTranslucent.GetBool() && !R_ScenePackets_AppendDrawSurfChain( packetFrame, vLight->translucentInteractions, RENDER_PASS_ARB2_INTERACTION, R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { + if ( !r_skipTranslucent.GetBool() + && !R_ScenePackets_AppendInteractionChain( packetFrame, vLight, + lightOrdinal, vLight->translucentInteractions, + SCENE_INTERACTION_RECEIVER_TRANSLUCENT, + R_ScenePackets_DrawSurfInteractionEligible, drawIndex ) ) { return; } } @@ -1507,26 +2243,57 @@ static void R_ScenePackets_AddShadowMapPass( idScenePacketFrame &packetFrame, co bool passAdded = false; int drawIndex = 0; - for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { - if ( !R_ScenePackets_ViewLightCanCastShadows( vLight ) ) { + int lightOrdinal = 0; + for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; + vLight = vLight->next, ++lightOrdinal ) { + if ( !R_ScenePackets_ViewLightHasMaterialInteractions( vLight ) + || !R_ScenePackets_ViewLightCanUseShadowMaps( vLight ) ) { continue; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->globalShadowMapCasters, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->globalShadowMapCasters, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_MAP_GLOBAL_STATIC, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->localShadowMapCasters, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->localShadowMapCasters, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_MAP_LOCAL_STATIC, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->globalTranslucentShadowMapCasters, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->globalShadowMapDynamicCasters, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_MAP_GLOBAL_DYNAMIC, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->localTranslucentShadowMapCasters, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->localShadowMapDynamicCasters, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_MAP_LOCAL_DYNAMIC, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->globalShadows, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->globalTranslucentShadowMapCasters, + RENDER_PASS_SHADOW_MAP, + SCENE_SHADOW_CASTER_MAP_GLOBAL_TRANSLUCENT, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->localShadows, RENDER_PASS_SHADOW_MAP, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->localTranslucentShadowMapCasters, + RENDER_PASS_SHADOW_MAP, + SCENE_SHADOW_CASTER_MAP_LOCAL_TRANSLUCENT, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) + || !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->globalShadowMapStencilSupplements, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) + || !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->localShadowMapStencilSupplements, + RENDER_PASS_SHADOW_MAP, SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } } @@ -1539,14 +2306,23 @@ static void R_ScenePackets_AddStencilShadowPass( idScenePacketFrame &packetFrame bool passAdded = false; int drawIndex = 0; - for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { - if ( !R_ScenePackets_ViewLightCanCastShadows( vLight ) ) { + int lightOrdinal = 0; + for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; + vLight = vLight->next, ++lightOrdinal ) { + if ( !R_ScenePackets_ViewLightHasMaterialInteractions( vLight ) + || !R_ScenePackets_ViewLightCanCastShadows( vLight ) ) { continue; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->globalShadows, RENDER_PASS_STENCIL_SHADOW, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->globalShadows, RENDER_PASS_STENCIL_SHADOW, + SCENE_SHADOW_CASTER_STENCIL_GLOBAL, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChainLazyPass( packetFrame, vLight->localShadows, RENDER_PASS_STENCIL_SHADOW, R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { + if ( !R_ScenePackets_AppendShadowChainLazyPass( packetFrame, vLight, + lightOrdinal, vLight->localShadows, RENDER_PASS_STENCIL_SHADOW, + SCENE_SHADOW_CASTER_STENCIL_LOCAL, + R_ScenePackets_DrawSurfShadowEligible, drawIndex, passAdded ) ) { return; } } @@ -1558,14 +2334,20 @@ static void R_ScenePackets_AddFogBlendPass( idScenePacketFrame &packetFrame, con } int drawIndex = 0; - for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { + int lightOrdinal = 0; + for ( const viewLight_t *vLight = viewDef->viewLights; vLight != NULL; + vLight = vLight->next, ++lightOrdinal ) { if ( !R_ScenePackets_ViewLightIsFogOrBlend( vLight ) ) { continue; } - if ( !R_ScenePackets_AppendDrawSurfChain( packetFrame, vLight->globalInteractions, RENDER_PASS_FOG_BLEND, R_ScenePackets_DrawSurfFogBlendEligible, drawIndex ) ) { + if ( !R_ScenePackets_AppendFogBlendChain( packetFrame, vLight, + lightOrdinal, vLight->globalInteractions, + SCENE_FOG_BLEND_RECEIVER_GLOBAL, drawIndex ) ) { return; } - if ( !R_ScenePackets_AppendDrawSurfChain( packetFrame, vLight->localInteractions, RENDER_PASS_FOG_BLEND, R_ScenePackets_DrawSurfFogBlendEligible, drawIndex ) ) { + if ( !R_ScenePackets_AppendFogBlendChain( packetFrame, vLight, + lightOrdinal, vLight->localInteractions, + SCENE_FOG_BLEND_RECEIVER_LOCAL, drawIndex ) ) { return; } } @@ -1668,6 +2450,10 @@ static void R_ScenePackets_AddDrawView( idScenePacketFrame &packetFrame, const v R_ScenePackets_AddStencilShadowPass( packetFrame, viewDef ); R_ScenePackets_AddInteractionPass( packetFrame, viewDef ); R_ScenePackets_AddFilteredDrawSurfPass( packetFrame, viewDef, RENDER_PASS_AMBIENT, R_ScenePackets_DrawSurfAmbientEligible ); + if ( r_rendererSharedInWorldGui.GetBool() + && R_ScenePackets_HasInWorldGUIDrawSurfs( viewDef ) ) { + R_ScenePackets_AddFilteredDrawSurfPass( packetFrame, viewDef, RENDER_PASS_GUI, R_ScenePackets_DrawSurfInWorldGUIEligible ); + } R_ScenePackets_AddFilteredDrawSurfPass( packetFrame, viewDef, RENDER_PASS_LIGHT_GRID, R_ScenePackets_DrawSurfLightGridEligible ); R_ScenePackets_AddFogBlendPass( packetFrame, viewDef ); R_ScenePackets_AddRootPostProcessPasses( packetFrame, viewDef ); @@ -1691,31 +2477,46 @@ static void R_ScenePackets_AddCommandPass( idScenePacketFrame &packetFrame, rend void R_ScenePackets_AddRenderView( const viewDef_t *viewDef ) { R_ScenePackets_EnsureFrontEndFrame(); R_ScenePackets_AddDrawView( rg_frontEndScenePacketFrame, viewDef, false ); + rg_frontEndLastDrawView = viewDef; } -void R_ScenePackets_AddSpecialEffects( const viewDef_t *viewDef ) { +void R_ScenePackets_AddSpecialEffects( const viewDef_t *viewDef, + int specialEffectsMask ) { R_ScenePackets_EnsureFrontEndFrame(); R_ScenePackets_AddCommandPass( rg_frontEndScenePacketFrame, RENDER_PASS_SPECIAL_EFFECTS, viewDef, false ); + rg_frontEndScenePacketFrame.SetLastSceneSpecialEffectsMask( + specialEffectsMask ); + rg_frontEndLastDrawView = NULL; } void R_ScenePackets_AddRenderTargetOp( void ) { R_ScenePackets_EnsureFrontEndFrame(); R_ScenePackets_AddCommandPass( rg_frontEndScenePacketFrame, RENDER_PASS_AUTHORED_POST, NULL, false ); + rg_frontEndLastDrawView = NULL; } -void R_ScenePackets_AddCopyRender( void ) { +void R_ScenePackets_AddCopyRender( idImage *image, int x, int y, + int width, int height, int cubeFace, bool copyDepth ) { R_ScenePackets_EnsureFrontEndFrame(); R_ScenePackets_AddCommandPass( rg_frontEndScenePacketFrame, RENDER_PASS_AUTHORED_POST, NULL, false ); + if ( rg_frontEndLastDrawView != NULL && rg_frontEndLastDrawView->isSubview ) { + rg_frontEndScenePacketFrame.AddSubviewCapture( rg_frontEndLastDrawView, + image, x, y, width, height, cubeFace, copyDepth ); + } + // A capture can belong to exactly one immediately preceding subview draw. + rg_frontEndLastDrawView = NULL; } void R_ScenePackets_AddPresent( void ) { R_ScenePackets_EnsureFrontEndFrame(); R_ScenePackets_AddCommandPass( rg_frontEndScenePacketFrame, RENDER_PASS_PRESENT, NULL, false ); + rg_frontEndLastDrawView = NULL; } void R_ScenePackets_AddCommandOnly( void ) { R_ScenePackets_EnsureFrontEndFrame(); rg_frontEndScenePacketFrame.AddCommandPacket(); + rg_frontEndLastDrawView = NULL; } const idScenePacketFrame &R_ScenePackets_FrontEndFrame( void ) { @@ -1729,17 +2530,31 @@ bool R_ScenePackets_FrontEndFrameAvailable( void ) { && ( stats.scenePackets > 0 || stats.passPackets > 0 || stats.drawPackets > 0 || stats.commandPackets > 0 ); } +bool R_ScenePackets_BuildTemporalViewMotionPolicy( const viewDef_t *viewDef, + unsigned int backendExactMotionDomainMask, + temporalViewMotionPolicy_t &policy ) { + policy = TemporalHistoryCore_BeginViewMotionPolicy(); + return R_ScenePackets_FrontEndFrameAvailable() + && rg_frontEndScenePacketFrame.BuildTemporalViewMotionPolicy( viewDef, + backendExactMotionDomainMask, policy ); +} + void R_ScenePackets_BuildLegacyCommandStream( const emptyCommand_t *cmds, idScenePacketFrame &packetFrame ) { packetFrame.Clear(); packetFrame.MarkBackendDerived(); + const viewDef_t *lastDrawView = NULL; for ( const emptyCommand_t *cmd = cmds; cmd != NULL; cmd = reinterpret_cast( cmd->next ) ) { switch ( cmd->commandId ) { case RC_DRAW_VIEW: - R_ScenePackets_AddDrawView( packetFrame, reinterpret_cast( cmd )->viewDef, true ); + lastDrawView = reinterpret_cast( cmd )->viewDef; + R_ScenePackets_AddDrawView( packetFrame, lastDrawView, true ); break; case RC_DRAW_SPECIAL_EFFECTS: R_ScenePackets_AddCommandPass( packetFrame, RENDER_PASS_SPECIAL_EFFECTS, reinterpret_cast( cmd )->viewDef, true ); + packetFrame.SetLastSceneSpecialEffectsMask( + R_ScenePackets_ActiveSpecialEffectsMask() ); + lastDrawView = NULL; break; case RC_SET_RENDERTEXTURE: case RC_RESOLVE_MSAA: @@ -1748,17 +2563,29 @@ void R_ScenePackets_BuildLegacyCommandStream( const emptyCommand_t *cmds, idScen case RC_SET_POSTPROCESS_SOURCE_COLOR_SPACE: case RC_SET_POSTPROCESS_SMAA_QUALITY: R_ScenePackets_AddCommandPass( packetFrame, RENDER_PASS_AUTHORED_POST, NULL, true ); + lastDrawView = NULL; break; case RC_COPY_RENDER: R_ScenePackets_AddCommandPass( packetFrame, RENDER_PASS_AUTHORED_POST, NULL, true ); + if ( lastDrawView != NULL && lastDrawView->isSubview ) { + const copyRenderCommand_t *copy = + reinterpret_cast( cmd ); + packetFrame.AddSubviewCapture( lastDrawView, copy->image, + copy->x, copy->y, copy->imageWidth, copy->imageHeight, + copy->cubeFace, copy->copyDepth ); + } + lastDrawView = NULL; break; case RC_SET_BUFFER: packetFrame.AddCommandPacket(); + lastDrawView = NULL; break; case RC_SWAP_BUFFERS: R_ScenePackets_AddCommandPass( packetFrame, RENDER_PASS_PRESENT, NULL, true ); + lastDrawView = NULL; break; default: + lastDrawView = NULL; break; } } @@ -1771,7 +2598,7 @@ void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ) { const scenePacketFrameStats_t &stats = packetFrame.Stats(); common->Printf( - "scenePackets source=%s scenes=%d passes=%d draws=%d clipped=%d cmds=%d drawViews=%d materials=%d geometryRecords=%d instanceRecords=%d withMaterial=%d resources=%d geometryRecordRefs=%d instanceRefs=%d geometry=%d regs=%d indexCache=%d ambientCache=%d categories(world=%d subview=%d remote=%d fx=%d viewmodel=%d demo=%d gui=%d post=%d present=%d command=%d) sortFailures=%d overflow=%d cause=%s\n", + "scenePackets source=%s scenes=%d passes=%d draws=%d clipped=%d cmds=%d drawViews=%d subviewCaptures=%d materials=%d geometryRecords=%d instanceRecords=%d withMaterial=%d resources=%d geometryRecordRefs=%d instanceRefs=%d geometry=%d regs=%d indexCache=%d ambientCache=%d deformRecords=%d deform(material=%d finalized=%d interaction=%d fog=%d shadowVolume=%d other=%d completed=%d empty=%d notApplicable=%d skipped=%d failed=%d unsupported=%d fallback=%d) categories(world=%d subview=%d remote=%d fx=%d viewmodel=%d demo=%d gui=%d post=%d present=%d command=%d) sortFailures=%d overflow=%d cause=%s\n", stats.frontEndDerived ? "frontend" : ( stats.backendDerived ? "backend" : "unknown" ), stats.scenePackets, stats.passPackets, @@ -1779,6 +2606,7 @@ void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ) { stats.clippedDrawPackets, stats.commandPackets, stats.legacyDrawViews, + stats.subviewCaptures, stats.materialRecords, stats.geometryRecords, stats.instanceRecords, @@ -1790,6 +2618,20 @@ void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ) { stats.drawPacketsWithShaderRegisters, stats.drawPacketsWithIndexCache, stats.drawPacketsWithAmbientCache, + stats.drawPacketsWithClassicDeformRecord, + stats.materialDeformDrawPackets, + stats.deformFinalizedDrawPackets, + stats.deformInteractionReceiverPackets, + stats.deformFogReceiverPackets, + stats.deformShadowVolumePackets, + stats.deformOtherRolePackets, + stats.deformCompletedPackets, + stats.deformEmptyPackets, + stats.deformNotApplicablePackets, + stats.deformSkippedPackets, + stats.deformFailedPackets, + stats.deformUnsupportedPackets, + stats.deformFallbackPackets, stats.worldPackets, stats.subviewPackets, stats.remoteCameraPackets, @@ -1818,20 +2660,22 @@ void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ) { for ( int i = 0; i < materialRecordLogCount; ++i ) { const materialResourceRecord_t &record = packetFrame.MaterialRecord( i ); common->Printf( - "scenePackets material[%d]=%s class=%s diffuse=%s normal=%s specular=%s table=%d\n", + "scenePackets material[%d]=%s class=%s diffuse=%s normal=%s specular=%s pbr=%d workflow=%d table=%d\n", i, record.material ? record.material->GetName() : "", RendererMaterialClass_Name( static_cast( record.permutation.materialClass ) ), record.diffuseImage ? record.diffuseImage->GetName() : "", record.normalImage ? record.normalImage->GetName() : "", record.specularImage ? record.specularImage->GetName() : "", + record.hasPBR ? 1 : 0, + record.pbrWorkflow, record.resourceTableIndex ); } const int geometryRecordLogCount = Min( packetFrame.NumGeometryRecords(), 8 ); for ( int i = 0; i < geometryRecordLogCount; ++i ) { const geometryResourceRecord_t &record = packetFrame.GeometryRecord( i ); common->Printf( - "scenePackets geometry[%d]=verts:%d indexes:%d vbo:%d@%d ibo:%d@%d lifetime=%d skin=%d deform=%d fallback=%s flags=0x%x bounds=%s\n", + "scenePackets geometry[%d]=verts:%d indexes:%d vbo:%d@%d ibo:%d@%d lifetime=%d skin=%d deform=%d contract=%s/%s/%s fresh=%d hash=%llu fallback=%s flags=0x%x bounds=%s\n", i, record.vertexCount, record.indexCount, @@ -1842,6 +2686,11 @@ void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ) { record.uploadLifetime, record.skinningMode, record.deformMode, + ClassicDeformRole_Name( record.classicDeform.role ), + ClassicDeformKind_Name( record.classicDeform.kind ), + ClassicDeformOutcome_Name( record.classicDeform.outcome ), + record.hasClassicDeformRecord ? 1 : 0, + static_cast( record.classicDeform.semanticHash ), GeometryResourceFallbackReason_Name( record.fallbackReason ), record.fallbackFlags, record.hasBounds ? "yes" : "no" ); @@ -1981,7 +2830,7 @@ bool RendererScenePacket_RunSelfTest( void ) { R_ScenePackets_BeginFrame(); R_ScenePackets_AddRenderView( &worldView ); - R_ScenePackets_AddSpecialEffects( &worldView ); + R_ScenePackets_AddSpecialEffects( &worldView, SPECIAL_EFFECT_BLUR ); R_ScenePackets_AddPresent(); const idScenePacketFrame &frontEndPacketFrame = R_ScenePackets_FrontEndFrame(); const scenePacketFrameStats_t &frontEndStats = frontEndPacketFrame.Stats(); @@ -2017,6 +2866,13 @@ bool RendererScenePacket_RunSelfTest( void ) { R_ScenePackets_EndFrame(); return false; } + if ( frontEndPacketFrame.NumScenes() < 2 + || frontEndPacketFrame.Scene( 1 ).specialEffectsMask + != SPECIAL_EFFECT_BLUR ) { + common->Printf( "RendererScenePacket self-test failed: frontend special-effect mask capture mismatch\n" ); + R_ScenePackets_EndFrame(); + return false; + } R_ScenePackets_EndFrame(); idScenePacketFrame postPacketFrame; @@ -2045,6 +2901,73 @@ bool RendererScenePacket_RunSelfTest( void ) { return false; } - common->Printf( "RendererScenePacket self-test passed (backend, frontend, post commands)\n" ); + // Exercise the actual material-record capture path with opt-in PBR metadata. + // The ambient stage makes this a drawable synthetic surface while the absent + // bump+diffuse interaction deliberately proves the missing-fallback bit. + static const char pbrPacketDeclaration[] = + "material _pbr_scene_packet_selftest {\n" + " {\n" + " map _white\n" + " }\n" + " pbr {\n" + " workflow metallicRoughness\n" + " albedoMap _white\n" + " metallic 0.35\n" + " roughness 0.65\n" + " ao 0.85\n" + " autoLegacyFallback 0\n" + " }\n" + "}\n"; + idDecl *pbrPacketDecl = declManager->AllocateDecl( DECL_MATERIAL ); + if ( pbrPacketDecl == NULL ) { + common->Printf( "RendererScenePacket self-test failed: PBR declaration allocation\n" ); + return false; + } + idMaterial *pbrPacketMaterial = static_cast( pbrPacketDecl ); + bool pbrPacketValid = pbrPacketMaterial->Parse( + pbrPacketDeclaration, + idLib::SizeToInt( sizeof( pbrPacketDeclaration ) - 1, "RendererScenePacket PBR self-test" ) ); + if ( pbrPacketValid ) { + drawSurf_t pbrDrawSurf; + memset( &pbrDrawSurf, 0, sizeof( pbrDrawSurf ) ); + pbrDrawSurf.material = pbrPacketMaterial; + pbrDrawSurf.sort = pbrPacketMaterial->GetSort(); + { + idScenePacketFrame pbrPacketFrame; + pbrPacketValid = pbrPacketFrame.AddDrawPacket( &pbrDrawSurf, RENDER_PASS_AMBIENT, 0 ); + const drawPacket_t *pbrDrawPacket = pbrPacketValid && pbrPacketFrame.NumDrawPackets() == 1 + ? &pbrPacketFrame.DrawPacket( 0 ) + : NULL; + const materialResourceRecord_t *pbrRecord = pbrDrawPacket != NULL && pbrDrawPacket->materialRecordIndex >= 0 + ? &pbrPacketFrame.MaterialRecord( pbrDrawPacket->materialRecordIndex ) + : NULL; + const pbrMaterialInfo_t &pbr = pbrPacketMaterial->GetPBRInfo(); + const int registerCount = pbrPacketMaterial->GetNumRegisters(); + auto validRegister = [&]( int index ) -> bool { return index >= 0 && index < registerCount; }; + pbrPacketValid = pbrRecord != NULL + && pbrDrawPacket->materialRecord == pbrRecord + && pbrRecord->material == pbrPacketMaterial + && pbrRecord->hasPBR + && pbrRecord->pbrWorkflow == static_cast( pbr.workflow ) + && pbrRecord->pbrAlbedoImage == pbr.albedo.image + && pbrRecord->pbrLegacyFallbackMissing + && pbrRecord->pbrLegacyFallbackMissing == pbr.legacyFallbackMissing + && pbrRecord->pbrMetallicRegister == pbr.metallicRegister + && pbrRecord->pbrRoughnessRegister == pbr.roughnessRegister + && pbrRecord->pbrAORegister == pbr.aoRegister + && pbrRecord->pbrNormalScaleRegister == pbr.normalScaleRegister + && validRegister( pbrRecord->pbrMetallicRegister ) + && validRegister( pbrRecord->pbrRoughnessRegister ) + && validRegister( pbrRecord->pbrAORegister ) + && validRegister( pbrRecord->pbrNormalScaleRegister ); + } + } + DeclManager_FreeAllocatedDecl( pbrPacketDecl ); + if ( !pbrPacketValid ) { + common->Printf( "RendererScenePacket self-test failed: PBR packet metadata propagation\n" ); + return false; + } + + common->Printf( "RendererScenePacket self-test passed (backend, frontend, post commands, PBR metadata)\n" ); return true; } diff --git a/src/renderer/ScenePackets.h b/src/renderer/ScenePackets.h index 67fb8ba6..a18d402f 100644 --- a/src/renderer/ScenePackets.h +++ b/src/renderer/ScenePackets.h @@ -4,6 +4,10 @@ #ifndef __SCENE_PACKETS_H__ #define __SCENE_PACKETS_H__ +#include "GpuSkinning.h" +#include "ClassicDeformDomain.h" +#include "TemporalHistoryCore.h" + /* =============================================================================== @@ -43,6 +47,9 @@ const int SCENE_PACKET_MAX_DRAWS = 4096; const int SCENE_PACKET_MAX_MATERIAL_RECORDS = 1024; const int SCENE_PACKET_MAX_GEOMETRY_RECORDS = 1024; const int SCENE_PACKET_MAX_INSTANCE_RECORDS = 1024; +// Captures are command-stream edges rather than drawable passes. A subview +// publishes at most one color capture, so bound this table with scenes. +const int SCENE_PACKET_MAX_SUBVIEW_CAPTURES = SCENE_PACKET_MAX_SCENES; enum scenePacketCategory_t { SCENE_PACKET_CATEGORY_UNKNOWN = 0, @@ -65,7 +72,8 @@ enum scenePacketOverflowCause_t { SCENE_PACKET_OVERFLOW_DRAWS, SCENE_PACKET_OVERFLOW_MATERIALS, SCENE_PACKET_OVERFLOW_GEOMETRY_RECORDS, - SCENE_PACKET_OVERFLOW_INSTANCE_RECORDS + SCENE_PACKET_OVERFLOW_INSTANCE_RECORDS, + SCENE_PACKET_OVERFLOW_SUBVIEW_CAPTURES }; enum rendererMaterialClass_t { @@ -97,7 +105,30 @@ typedef struct materialResourceRecord_s { const idImage *diffuseImage; const idImage *normalImage; const idImage *specularImage; + const idImage *pbrAlbedoImage; + const idImage *pbrNormalImage; + const idImage *pbrORMImage; + const idImage *pbrMetallicImage; + const idImage *pbrRoughnessImage; + const idImage *pbrAOImage; + const idImage *pbrEmissiveImage; + bool hasPBR; + int pbrWorkflow; + int pbrNormalFormat; + bool pbrHasAuthoredClassicFallback; + bool pbrHasExplicitLegacyFallback; + bool pbrUsesGeneratedLegacyFallback; + bool pbrUsesApproximateLegacyFallback; + bool pbrLegacyFallbackMissing; + int pbrMetallicRegister; + int pbrRoughnessRegister; + int pbrAORegister; + int pbrNormalScaleRegister; + int pbrEmissiveColorRegisters[3]; int resourceTableIndex; + // per-material DI_REMOTE_RENDER stage presence, cached once at record + // creation so AddDrawPacket does not re-walk stages per draw packet + bool usesRemoteRender; rendererPermutationKey_t permutation; } materialResourceRecord_t; @@ -157,16 +188,20 @@ typedef struct geometryResourceRecord_s { int skinningMode; int deformMode; int uploadLifetime; + classicDeformRecord_t classicDeform; geometryResourceFallbackReason_t fallbackReason; unsigned int fallbackFlags; int skinningPaletteOffset; int skinningPaletteCount; + gpuSkinningSurface_t gpuSkinningSurface; const glIndex_t *legacyIndexData; bool hasAmbientVertexBuffer; bool hasIndexBuffer; bool hasClientIndexData; bool hasPrimBatchMesh; + bool hasGpuSkinningContract; bool hasBounds; + bool hasClassicDeformRecord; } geometryResourceRecord_t; enum instanceVisibilityFlags_t { @@ -189,6 +224,9 @@ typedef struct instanceRecord_s { int shaderRegisterCount; int skinningPaletteOffset; int visibilityFlags; + unsigned long long temporalViewIdentity; + unsigned int temporalHistoryGeneration; + int temporalHistoryAgeFrames; float modelMatrix[16]; float previousModelMatrix[16]; float modelViewMatrix[16]; @@ -200,12 +238,50 @@ typedef struct instanceRecord_s { bool weaponDepthHack; bool negativeScale; bool legacyBridge; + bool temporalCaptureFrame; } instanceRecord_t; typedef struct drawPacketSortKey_s { unsigned long long value; } drawPacketSortKey_t; +enum sceneInteractionReceiverClass_t { + SCENE_INTERACTION_RECEIVER_NONE = 0, + SCENE_INTERACTION_RECEIVER_LOCAL, + SCENE_INTERACTION_RECEIVER_GLOBAL, + SCENE_INTERACTION_RECEIVER_TRANSLUCENT +}; + +// Fog/blend receiver identity is independent of the generic pass category. +// The classic phase walks each relevant viewLight in source order and then its +// authoritative GLOBAL -> LOCAL chains. Capturing that identity lets shared +// owners prove exact coverage without re-evaluating the receiver material. +enum sceneFogBlendReceiverClass_t { + SCENE_FOG_BLEND_RECEIVER_NONE = 0, + SCENE_FOG_BLEND_RECEIVER_GLOBAL, + SCENE_FOG_BLEND_RECEIVER_LOCAL, + SCENE_FOG_BLEND_RECEIVER_COUNT +}; + +// Shadow packet identity is deliberately separate from the render-pass +// category. One light can contribute the same surface to several ownership +// chains, and a flat RENDER_PASS_* list cannot prove which receiver-visible +// shadow contribution a packet represents. +enum sceneShadowCasterClass_t { + SCENE_SHADOW_CASTER_NONE = 0, + SCENE_SHADOW_CASTER_STENCIL_GLOBAL, + SCENE_SHADOW_CASTER_STENCIL_LOCAL, + SCENE_SHADOW_CASTER_MAP_GLOBAL_STATIC, + SCENE_SHADOW_CASTER_MAP_LOCAL_STATIC, + SCENE_SHADOW_CASTER_MAP_GLOBAL_DYNAMIC, + SCENE_SHADOW_CASTER_MAP_LOCAL_DYNAMIC, + SCENE_SHADOW_CASTER_MAP_GLOBAL_TRANSLUCENT, + SCENE_SHADOW_CASTER_MAP_LOCAL_TRANSLUCENT, + SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL, + SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL, + SCENE_SHADOW_CASTER_COUNT +}; + typedef struct drawPacket_s { const drawSurf_t *legacyDrawSurf; const viewDef_t *viewDef; @@ -226,14 +302,36 @@ typedef struct drawPacket_s { int vertexOffset; int instanceOffset; int instanceCount; + temporalMotionOwnership_t temporalMotion; + const viewLight_t *interactionLight; + int interactionLightOrdinal; + int interactionReceiverOrdinal; + int interactionSourceOrdinal; + sceneInteractionReceiverClass_t interactionReceiverClass; + const viewLight_t *fogBlendLight; + int fogBlendLightOrdinal; + int fogBlendReceiverOrdinal; + int fogBlendSourceOrdinal; + sceneFogBlendReceiverClass_t fogBlendReceiverClass; + const viewLight_t *shadowLight; + int shadowLightOrdinal; + int shadowChainOrdinal; + int shadowSourceOrdinal; + sceneShadowCasterClass_t shadowCasterClass; int scissorX1; int scissorY1; int scissorX2; int scissorY2; + // The immutable deform contract is owned by geometryRecord. Keeping only an + // alias here avoids duplicating the comparatively large sealed record across + // every draw slot in the fixed packet arena. + const classicDeformRecord_t *classicDeformRecord; bool hasGeometry; bool hasShaderRegisters; bool hasIndexCache; bool hasAmbientCache; + bool hasClassicDeformRecord; + bool temporalExactRigidEligible; } drawPacket_t; typedef struct passPacket_s { @@ -253,8 +351,36 @@ typedef struct scenePacket_s { int firstDrawPacket; int drawPacketCount; bool legacyBridge; + // A real render-demo frame is identified by the live session stream, not + // by a negative view id (which is also used by portal-sky cameras). + bool renderDemoPlayback; + // RC_DRAW_SPECIAL_EFFECTS is command-only. Preserve the admitted controller + // mask with its exact view association for special-frame ownership. + int specialEffectsMask; + unsigned long long temporalViewIdentity; + unsigned int temporalHistoryGeneration; + int temporalJitterIndex; + temporalHistoryResetReason_t temporalHistoryResetReason; + bool temporalHistoryValid; + bool temporalJitterEnabled; + bool temporalCaptureFrame; } scenePacket_t; +// CaptureRenderToImage immediately follows the RC_DRAW_VIEW which produced a +// capture-backed subview. Retaining that relationship prevents a shared owner +// from treating an unrelated feedback copy as its parent-facing image. +typedef struct sceneSubviewCapture_s { + const viewDef_t *viewDef; + idImage *image; + int viewScenePacketIndex; + int x; + int y; + int width; + int height; + int cubeFace; + bool copyDepth; +} sceneSubviewCapture_t; + typedef struct scenePacketFrameStats_s { int scenePackets; int passPackets; @@ -273,6 +399,20 @@ typedef struct scenePacketFrameStats_s { int drawPacketsWithShaderRegisters; int drawPacketsWithIndexCache; int drawPacketsWithAmbientCache; + int drawPacketsWithClassicDeformRecord; + int materialDeformDrawPackets; + int deformFinalizedDrawPackets; + int deformInteractionReceiverPackets; + int deformFogReceiverPackets; + int deformShadowVolumePackets; + int deformOtherRolePackets; + int deformCompletedPackets; + int deformEmptyPackets; + int deformNotApplicablePackets; + int deformSkippedPackets; + int deformFailedPackets; + int deformUnsupportedPackets; + int deformFallbackPackets; int worldPackets; int subviewPackets; int remoteCameraPackets; @@ -283,6 +423,11 @@ typedef struct scenePacketFrameStats_s { int postProcessPackets; int presentPackets; int commandOnlyPackets; + int temporalMotionDomainPackets[TEMPORAL_MOTION_DOMAIN_COUNT]; + int temporalReactivePackets; + int temporalPreviousTransformPackets; + int temporalSeparateHistoryPackets; + int subviewCaptures; int sortKeyValidationFailures; bool frontEndDerived; bool backendDerived; @@ -298,9 +443,24 @@ class idScenePacketFrame { bool AddScene( const viewDef_t *viewDef, bool legacyBridge ); bool AddPass( renderPassCategory_t category, bool enabled, bool commandOnly = false ); - bool AddDrawPacket( const drawSurf_t *drawSurf, renderPassCategory_t category, int drawIndex ); + void SetLastSceneSpecialEffectsMask( int specialEffectsMask ); + bool AddDrawPacket( const drawSurf_t *drawSurf, renderPassCategory_t category, + int drawIndex, + classicDeformRole_t deformRole = CLASSIC_DEFORM_ROLE_UNKNOWN ); + bool AddInteractionDrawPacket( const drawSurf_t *drawSurf, int drawIndex, + const viewLight_t *viewLight, int lightOrdinal, + sceneInteractionReceiverClass_t receiverClass, int receiverOrdinal ); + bool AddFogBlendDrawPacket( const drawSurf_t *drawSurf, int drawIndex, + const viewLight_t *viewLight, int lightOrdinal, + sceneFogBlendReceiverClass_t receiverClass, int receiverOrdinal ); + bool AddShadowDrawPacket( const drawSurf_t *drawSurf, + renderPassCategory_t category, int drawIndex, + const viewLight_t *viewLight, int lightOrdinal, + sceneShadowCasterClass_t casterClass, int chainOrdinal ); void FinishScene( void ); void AddCommandPacket( scenePacketCategory_t category = SCENE_PACKET_CATEGORY_COMMAND ); + bool AddSubviewCapture( const viewDef_t *viewDef, idImage *image, + int x, int y, int width, int height, int cubeFace, bool copyDepth ); void AddLegacyDrawView( void ); void AddClippedDrawPackets( int count ); void MarkFrontEndDerived( void ); @@ -318,12 +478,18 @@ class idScenePacketFrame { const materialResourceRecord_t &MaterialRecord( int index ) const; const geometryResourceRecord_t &GeometryRecord( int index ) const; const instanceRecord_t &InstanceRecord( int index ) const; + int NumSubviewCaptures( void ) const; + const sceneSubviewCapture_t &SubviewCapture( int index ) const; const scenePacketFrameStats_t &Stats( void ) const; bool ValidateSortKeys( void ) const; + bool BuildTemporalViewMotionPolicy( const viewDef_t *viewDef, + unsigned int backendExactMotionDomainMask, + temporalViewMotionPolicy_t &policy ) const; private: int FindOrAddMaterialRecord( const drawSurf_t *drawSurf ); - int FindOrAddGeometryRecord( const drawSurf_t *drawSurf ); + int FindOrAddGeometryRecord( const drawSurf_t *drawSurf, + const classicDeformRecord_t &classicDeform ); int FindOrAddInstanceRecord( const drawSurf_t *drawSurf, scenePacketCategory_t packetCategory ); void SetOverflow( scenePacketOverflowCause_t cause ); void CountCategory( scenePacketCategory_t category ); @@ -334,6 +500,7 @@ class idScenePacketFrame { materialResourceRecord_t materialRecords[SCENE_PACKET_MAX_MATERIAL_RECORDS]; geometryResourceRecord_t geometryRecords[SCENE_PACKET_MAX_GEOMETRY_RECORDS]; instanceRecord_t instanceRecords[SCENE_PACKET_MAX_INSTANCE_RECORDS]; + sceneSubviewCapture_t subviewCaptures[SCENE_PACKET_MAX_SUBVIEW_CAPTURES]; scenePacketFrameStats_t stats; int activeScene; int activePass; @@ -351,13 +518,19 @@ void R_ScenePackets_EndFrame( void ); bool R_ScenePackets_FrontEndCaptureRequired( void ); bool R_ScenePackets_SidePipelineRequired( void ); void R_ScenePackets_AddRenderView( const viewDef_t *viewDef ); -void R_ScenePackets_AddSpecialEffects( const viewDef_t *viewDef ); +void R_ScenePackets_AddSpecialEffects( const viewDef_t *viewDef, + int specialEffectsMask ); void R_ScenePackets_AddRenderTargetOp( void ); -void R_ScenePackets_AddCopyRender( void ); +void R_ScenePackets_AddCopyRender( idImage *image, int x, int y, + int width, int height, int cubeFace, bool copyDepth ); void R_ScenePackets_AddPresent( void ); void R_ScenePackets_AddCommandOnly( void ); const idScenePacketFrame &R_ScenePackets_FrontEndFrame( void ); bool R_ScenePackets_FrontEndFrameAvailable( void ); +bool R_ScenePackets_BuildTemporalViewMotionPolicy( const viewDef_t *viewDef, + unsigned int backendExactMotionDomainMask, + temporalViewMotionPolicy_t &policy ); +bool R_ScenePackets_TemporalRigidMotionEligible( const drawSurf_t *drawSurf ); void R_ScenePackets_BuildLegacyCommandStream( const emptyCommand_t *cmds, idScenePacketFrame &packetFrame ); void R_ScenePackets_LogIfVerbose( const idScenePacketFrame &packetFrame ); bool RendererScenePacket_RunSelfTest( void ); diff --git a/src/renderer/ShadowMapArb2Parity.h b/src/renderer/ShadowMapArb2Parity.h index ac6ff875..6d6562c9 100644 --- a/src/renderer/ShadowMapArb2Parity.h +++ b/src/renderer/ShadowMapArb2Parity.h @@ -4,6 +4,8 @@ #ifndef __SHADOWMAP_ARB2_PARITY_H__ #define __SHADOWMAP_ARB2_PARITY_H__ +#include + #include "ShadowMapProjected.h" typedef struct shadowMapArb2ParityState_s { @@ -46,6 +48,10 @@ typedef struct shadowMapArb2CacheEstimate_s { unsigned int cacheMissPassMask; unsigned int freshUpdatePassMask; unsigned int budgetFallbackPassMask; + // Canonical cache-key state is separate from requested receiver ownership: + // a LOCAL receiver pass can intentionally share a GLOBAL-keyed map. + unsigned int cacheKeyHitMask; + unsigned int plannedCacheUpdateKeyMask; int projectedCacheSlotsUsed; int projectedCacheSlotsTotal; int pointCacheSlotsUsed; @@ -57,11 +63,13 @@ typedef struct shadowMapArb2CacheEstimate_s { // cascade) with the same math the ARB2 receiver upload uses, so a consumer // can sample the atlas texture directly. Atlas cells hold STATIC content // only: lights with dynamic casters get those composed over scratch per -// frame, which the atlas cell does not contain. +// frame, which the atlas cell does not contain. storageGeneration identifies +// the exact physical atlas allocation behind the exported UVs. typedef struct shadowMapArb2AtlasSlot_s { bool valid; int lightIndex; int signature; + std::uint64_t storageGeneration; int cellX; int cellY; int cellSpan; @@ -76,10 +84,30 @@ typedef struct shadowMapArb2AtlasSlot_s { float cascadeAtlasRect[SHADOWMAP_PROJECTED_MAX_CASCADES][4]; } shadowMapArb2AtlasSlot_t; +// Provenance for the one point cube which the classic GL executor most +// recently left selected. Unlike the projected atlas, point caches own +// separate cube images and the modern shader exposes only one samplerCube; +// a descriptor may consume it only when this exact light/signature record is +// still valid. This deliberately fails closed for every other point light. +typedef struct shadowMapArb2PointCube_s { + bool valid; + int lightIndex; + int signature; + int size; + bool highPrecision; + int lastUpdatedFrame; +} shadowMapArb2PointCube_t; + bool RB_ShadowMapBuildArb2ParityState( const viewLight_t *vLight, const viewDef_t *viewDef, int shadowMapSize, shadowMapArb2ParityState_t &state ); bool RB_ShadowMapEstimateArb2CacheOwnership( const viewLight_t *vLight, const viewDef_t *viewDef, shadowMapArb2CacheEstimate_t &estimate ); -bool RB_ShadowMapProjectedAtlasSlotForLight( int lightDefIndex, shadowMapArb2AtlasSlot_t &slot ); -void RB_ShadowMapProjectedAtlasSlotMarkUsed( int lightDefIndex ); +bool RB_ShadowMapProjectedAtlasSlotForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2AtlasSlot_t &slot ); +bool RB_ShadowMapProjectedAtlasSlotMarkUsed( int lightDefIndex, + int signature, std::uint64_t storageGeneration, + int cellX, int cellY, int cellSpan ); +bool RB_ShadowMapPointCubeForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2PointCube_t &cube ); +void RB_ShadowMapPointCubeMarkUsed( int lightDefIndex ); bool RB_ShadowMapArb2ReceiverFallbackSelfTest( void ); #endif /* !__SHADOWMAP_ARB2_PARITY_H__ */ diff --git a/src/renderer/ShadowMapClassification.cpp b/src/renderer/ShadowMapClassification.cpp index 46d2d3fc..35569776 100644 --- a/src/renderer/ShadowMapClassification.cpp +++ b/src/renderer/ShadowMapClassification.cpp @@ -4,6 +4,8 @@ #include "tr_local.h" #include "ShadowMapClassification.h" +#include + static shadowMapLightClass_t R_ShadowMapLightClassForViewLight( const viewLight_t *vLight ) { if ( vLight == NULL ) { return SHADOWMAP_LIGHT_PROJECTED; @@ -118,6 +120,162 @@ shadowMapProjectedFilterSettings_t R_ShadowMapProjectedFilterSettings( const vie return settings; } +float R_ShadowMapPointFarDistance( const viewLight_t *vLight ) { + if ( vLight == NULL ) { + return 1.0f; + } + + idVec3 adjustedRadius = vLight->lightRadius; + if ( vLight->lightDef != NULL ) { + const renderLight_t &parms = vLight->lightDef->parms; + for ( int component = 0; component < 3; ++component ) { + adjustedRadius[ component ] = parms.lightRadius[ component ] + + idMath::Fabs( parms.lightCenter[ component ] ); + } + } + + const float farDistance = adjustedRadius.Length() + * r_shadowMapPointFarScale.GetFloat(); + return std::isfinite( static_cast( farDistance ) ) + ? Max( farDistance, 1.0f ) : 1.0f; +} + +static float R_ShadowMapNonNegativeFinite( float value ) { + return std::isfinite( static_cast( value ) ) && value > 0.0f + ? value : 0.0f; +} + +shadowMapPointReceiverSettings_t R_ClampShadowMapPointReceiverSettings( + float farDistance, int faceSize, float constantBias, float normalBias, + float texelBiasScale, float normalOffsetScale, float maxWorldBias ) { + shadowMapPointReceiverSettings_t settings; + settings.constantBias = R_ShadowMapNonNegativeFinite( constantBias ); + settings.normalBias = R_ShadowMapNonNegativeFinite( normalBias ); + settings.texelBiasScale = R_ShadowMapNonNegativeFinite( texelBiasScale ); + settings.normalOffsetScale = + R_ShadowMapNonNegativeFinite( normalOffsetScale ); + settings.worldBiasScale = 1.0f; + + const double safeFar = + std::isfinite( static_cast( farDistance ) ) + && farDistance > 0.0f + ? static_cast( farDistance ) : 1.0; + const double safeFace = static_cast( Max( 1, faceSize ) ); + const double cap = static_cast( + R_ShadowMapNonNegativeFinite( maxWorldBias ) ); + const double scalarDepthBias = + static_cast( settings.constantBias ) + + static_cast( settings.normalBias ); + const double texelDepthBias = + ( static_cast( settings.texelBiasScale ) / safeFace ) + * ( 1.0 + static_cast( + SHADOWMAP_POINT_RECEIVER_MAX_SLOPE ) ); + const double depthWorldBias = safeFar + * Max( scalarDepthBias, texelDepthBias ); + const double normalOffsetWorldBias = safeFar + * ( 2.0 * static_cast( settings.normalOffsetScale ) + / safeFace ); + const double requestedWorldBias = + depthWorldBias + normalOffsetWorldBias; + + if ( cap > 0.0 && requestedWorldBias > cap ) { + settings.worldBiasScale = static_cast( + cap / requestedWorldBias ); + settings.constantBias *= settings.worldBiasScale; + settings.normalBias *= settings.worldBiasScale; + settings.texelBiasScale *= settings.worldBiasScale; + settings.normalOffsetScale *= settings.worldBiasScale; + } + + return settings; +} + +shadowMapPointReceiverSettings_t R_ShadowMapPointReceiverSettings( + float farDistance, int faceSize ) { + return R_ClampShadowMapPointReceiverSettings( + farDistance, faceSize, + r_shadowMapPointBias.GetFloat(), + r_shadowMapPointNormalBias.GetFloat(), + r_shadowMapTexelBiasScale.GetFloat(), + r_shadowMapNormalOffsetScale.GetFloat(), + r_shadowMapPointMaxWorldBias.GetFloat() ); +} + +shadowMapPointReceiverSettings_t R_ShadowMapPointStorageAdjustedReceiverSettings( + const shadowMapPointReceiverSettings_t &baseSettings, + float farDistance, int faceSize, bool depthCompare, + bool highPrecision ) { + if ( depthCompare ) { + return baseSettings; + } + + // Manual color depth needs a quantization-aware constant floor. Re-run the + // common clamp after adding it: on an enormous light one fp16 step alone can + // exceed the configured world-space budget, and the cap deliberately wins + // that otherwise unsatisfiable tradeoff. Keep this outside the GL backend so + // modern receivers sampling the same color cube use identical coefficients. + const float storageStep = highPrecision + ? 1.0f / 2048.0f : 1.0f / 65025.0f; + return R_ClampShadowMapPointReceiverSettings( + farDistance, faceSize, + Max( baseSettings.constantBias, storageStep * 1.5f ), + baseSettings.normalBias, + baseSettings.texelBiasScale, + baseSettings.normalOffsetScale, + r_shadowMapPointMaxWorldBias.GetFloat() ); +} + +bool R_ShadowMapCasterTransformNeedsTwoSided( const float modelMatrix[ 16 ] ) { + if ( modelMatrix == NULL ) { + return true; + } + + // Evaluate the affine linear transform in double precision so large, but + // finite, float scales do not overflow before the validity check. + const double determinant = + static_cast( modelMatrix[ 0 ] ) * + ( static_cast( modelMatrix[ 5 ] ) * static_cast( modelMatrix[ 10 ] ) + - static_cast( modelMatrix[ 9 ] ) * static_cast( modelMatrix[ 6 ] ) ) + - static_cast( modelMatrix[ 4 ] ) * + ( static_cast( modelMatrix[ 1 ] ) * static_cast( modelMatrix[ 10 ] ) + - static_cast( modelMatrix[ 9 ] ) * static_cast( modelMatrix[ 2 ] ) ) + + static_cast( modelMatrix[ 8 ] ) * + ( static_cast( modelMatrix[ 1 ] ) * static_cast( modelMatrix[ 6 ] ) + - static_cast( modelMatrix[ 5 ] ) * static_cast( modelMatrix[ 2 ] ) ); + // A singular transform has no reliable winding either. It can arise from + // malformed or transient entity state; AUTO must fail conservatively rather + // than culling one side of the collapsed caster. + return !std::isfinite( determinant ) || determinant <= 0.0; +} + +bool R_ShadowMapLightOriginInsideCasterBounds( const viewLight_t *vLight, + const float modelMatrix[ 16 ], const float boundsMin[ 3 ], + const float boundsMax[ 3 ] ) { + if ( vLight == NULL || modelMatrix == NULL + || boundsMin == NULL || boundsMax == NULL ) { + return true; + } + + idVec3 localLightOrigin; + R_GlobalPointToLocal( modelMatrix, vLight->globalLightOrigin, + localLightOrigin ); + for ( int component = 0; component < 3; ++component ) { + const float local = localLightOrigin[ component ]; + const float minimum = boundsMin[ component ]; + const float maximum = boundsMax[ component ]; + if ( !std::isfinite( static_cast( local ) ) + || !std::isfinite( static_cast( minimum ) ) + || !std::isfinite( static_cast( maximum ) ) + || minimum > maximum ) { + return true; + } + if ( local < minimum || local > maximum ) { + return false; + } + } + return true; +} + const char *R_ShadowMapLightClassName( shadowMapLightClass_t lightClass ) { switch ( lightClass ) { case SHADOWMAP_LIGHT_POINT: diff --git a/src/renderer/ShadowMapClassification.h b/src/renderer/ShadowMapClassification.h index f75d77f2..f518e464 100644 --- a/src/renderer/ShadowMapClassification.h +++ b/src/renderer/ShadowMapClassification.h @@ -45,8 +45,41 @@ typedef struct shadowMapProjectedFilterSettings_s { float effectiveFilterRadius; } shadowMapProjectedFilterSettings_t; +// Point receivers express depth bias in normalized radial depth and normal +// offset in cube texels. On very large authored lights both therefore grow +// into a large world-space displacement. Keep their common CPU policy in one +// place so classic/shared GL and Vulkan upload identical effective values. +static const float SHADOWMAP_POINT_RECEIVER_MAX_SLOPE = 4.0f; + +typedef struct shadowMapPointReceiverSettings_s { + float constantBias; + float normalBias; + float texelBiasScale; + float normalOffsetScale; + float worldBiasScale; +} shadowMapPointReceiverSettings_t; + shadowMapLightClassification_t R_ClassifyShadowMapLight( const viewLight_t *vLight ); shadowMapProjectedFilterSettings_t R_ShadowMapProjectedFilterSettings( const viewLight_t *vLight ); +float R_ShadowMapPointFarDistance( const viewLight_t *vLight ); +shadowMapPointReceiverSettings_t R_ClampShadowMapPointReceiverSettings( + float farDistance, int faceSize, float constantBias, float normalBias, + float texelBiasScale, float normalOffsetScale, float maxWorldBias ); +shadowMapPointReceiverSettings_t R_ShadowMapPointReceiverSettings( + float farDistance, int faceSize ); +shadowMapPointReceiverSettings_t R_ShadowMapPointStorageAdjustedReceiverSettings( + const shadowMapPointReceiverSettings_t &baseSettings, + float farDistance, int faceSize, bool depthCompare, bool highPrecision ); +// Mirrored or invalid model transforms make authored front-face winding +// unreliable. Invalid/unknown inputs return true so AUTO culling can fall +// back conservatively to two-sided depth. +bool R_ShadowMapCasterTransformNeedsTwoSided( const float modelMatrix[ 16 ] ); +// A sealed hull is only safe for one-sided near-shell rendering when its +// light is outside the caster. Invalid/unknown inputs return true so AUTO +// culling fails conservatively to two-sided depth. +bool R_ShadowMapLightOriginInsideCasterBounds( const viewLight_t *vLight, + const float modelMatrix[ 16 ], const float boundsMin[ 3 ], + const float boundsMax[ 3 ] ); const char *R_ShadowMapLightClassName( shadowMapLightClass_t lightClass ); #endif /* !__SHADOWMAP_CLASSIFICATION_H__ */ diff --git a/src/renderer/TemporalHistoryCore.h b/src/renderer/TemporalHistoryCore.h new file mode 100644 index 00000000..f9f5b848 --- /dev/null +++ b/src/renderer/TemporalHistoryCore.h @@ -0,0 +1,457 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __TEMPORAL_HISTORY_CORE_H__ +#define __TEMPORAL_HISTORY_CORE_H__ + +#include +#include + +// Dependency-free temporal-history policy shared by the renderer front end, +// both graphics backends, and the native regression test. The core owns only +// deterministic identities and decisions; image allocation and synchronization +// remain backend responsibilities. + +typedef enum temporalHistoryResetReason_e { + TEMPORAL_HISTORY_RESET_NONE = 0, + TEMPORAL_HISTORY_RESET_FIRST_SAMPLE, + TEMPORAL_HISTORY_RESET_GENERATION, + TEMPORAL_HISTORY_RESET_VIEW_IDENTITY, + TEMPORAL_HISTORY_RESET_OUTPUT_EXTENT, + TEMPORAL_HISTORY_RESET_SCENE_EXTENT, + TEMPORAL_HISTORY_RESET_TIME_DISCONTINUITY, + TEMPORAL_HISTORY_RESET_CAMERA_TRANSLATION, + TEMPORAL_HISTORY_RESET_CAMERA_ROTATION, + TEMPORAL_HISTORY_RESET_PROJECTION, + TEMPORAL_HISTORY_RESET_EXPLICIT_CUT, + TEMPORAL_HISTORY_RESET_CAPTURE, + TEMPORAL_HISTORY_RESET_RESOURCE_LOSS +} temporalHistoryResetReason_t; + +typedef enum temporalMotionDomain_e { + TEMPORAL_MOTION_DOMAIN_STATIC_WORLD = 0, + TEMPORAL_MOTION_DOMAIN_RIGID, + TEMPORAL_MOTION_DOMAIN_SKINNED, + TEMPORAL_MOTION_DOMAIN_PARTICLE, + TEMPORAL_MOTION_DOMAIN_DEFORM, + TEMPORAL_MOTION_DOMAIN_SUBVIEW, + TEMPORAL_MOTION_DOMAIN_IN_WORLD_GUI, + TEMPORAL_MOTION_DOMAIN_VIEW_MODEL, + TEMPORAL_MOTION_DOMAIN_COUNT +} temporalMotionDomain_t; + +typedef enum temporalMotionSource_e { + TEMPORAL_MOTION_SOURCE_ZERO = 0, + TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH, + TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM, + TEMPORAL_MOTION_SOURCE_SKINNED_PALETTE, + TEMPORAL_MOTION_SOURCE_DEFORM_VERTEX, + TEMPORAL_MOTION_SOURCE_INDEPENDENT_VIEW +} temporalMotionSource_t; + +enum temporalMotionOwnershipFlags_t { + TEMPORAL_MOTION_OWNERSHIP_NONE = 0, + TEMPORAL_MOTION_OWNERSHIP_HAS_PREVIOUS_TRANSFORM = 1 << 0, + TEMPORAL_MOTION_OWNERSHIP_REACTIVE = 1 << 1, + TEMPORAL_MOTION_OWNERSHIP_DISOCCLUSION_TEST = 1 << 2, + TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY = 1 << 3, + TEMPORAL_MOTION_OWNERSHIP_DEPTH_HACK = 1 << 4 +}; + +typedef struct temporalJitterSample_s { + float x; + float y; +} temporalJitterSample_t; + +typedef struct temporalCameraState_s { + bool valid; + bool projectionValid; + std::uint64_t viewIdentity; + std::uint32_t historyGeneration; + int frameNumber; + int renderTimeMsec; + int outputWidth; + int outputHeight; + int sceneWidth; + int sceneHeight; + float viewOrigin[3]; + float viewAxis[9]; + float fovX; + float fovY; + float projectInfo[4]; + float depthProjection[2]; + float jitterPixels[2]; +} temporalCameraState_t; + +typedef struct temporalCameraPolicy_s { + int maximumFrameGap; + int maximumTimeGapMsec; + float maximumTranslation; + float minimumAxisDot; + float maximumFovDelta; +} temporalCameraPolicy_t; + +typedef struct temporalMotionInput_s { + bool hasEntity; + bool hasPreviousTransform; + bool skinned; + bool hasPreviousSkinningPalette; + bool particle; + bool deform; + bool hasPreviousDeformedVertices; + bool subview; + bool inWorldGui; + bool viewModel; + bool translucent; +} temporalMotionInput_t; + +typedef struct temporalMotionOwnership_s { + temporalMotionDomain_t domain; + temporalMotionSource_t source; + unsigned int flags; +} temporalMotionOwnership_t; + +// Two conservative screen-space regions fit in the Vulkan temporal uniform +// slice while avoiding the pathological full-screen rejection caused by a +// single union around unrelated moving objects. Coordinates are normalized +// renderer scissor coordinates (bottom-left origin, inclusive-exclusive). +const int TEMPORAL_MAX_REACTIVE_REGIONS = 2; + +typedef struct temporalReactiveRegion_s { + float x1; + float y1; + float x2; + float y2; + unsigned int domainMask; +} temporalReactiveRegion_t; + +typedef struct temporalViewMotionPolicy_s { + unsigned int presentDomainMask; + unsigned int reactiveDomainMask; + unsigned int exactMotionDomainMask; + temporalReactiveRegion_t reactiveRegions[TEMPORAL_MAX_REACTIVE_REGIONS]; + int reactiveRegionCount; + bool reactiveRegionsMerged; +} temporalViewMotionPolicy_t; + +inline unsigned int TemporalHistoryCore_MotionDomainBit( + temporalMotionDomain_t domain ) { + return domain >= TEMPORAL_MOTION_DOMAIN_STATIC_WORLD + && domain < TEMPORAL_MOTION_DOMAIN_COUNT + ? 1u << static_cast( domain ) : 0u; +} + +inline temporalViewMotionPolicy_t TemporalHistoryCore_BeginViewMotionPolicy( + void ) { + temporalViewMotionPolicy_t policy = {}; + return policy; +} + +inline float TemporalHistoryCore_ClampUnit( float value ) { + return value < 0.0f ? 0.0f : ( value > 1.0f ? 1.0f : value ); +} + +inline float TemporalHistoryCore_ReactiveRegionArea( + const temporalReactiveRegion_t ®ion ) { + const float width = region.x2 > region.x1 ? region.x2 - region.x1 : 0.0f; + const float height = region.y2 > region.y1 ? region.y2 - region.y1 : 0.0f; + return width * height; +} + +inline temporalReactiveRegion_t TemporalHistoryCore_UnionReactiveRegions( + const temporalReactiveRegion_t &a, + const temporalReactiveRegion_t &b ) { + temporalReactiveRegion_t result; + result.x1 = a.x1 < b.x1 ? a.x1 : b.x1; + result.y1 = a.y1 < b.y1 ? a.y1 : b.y1; + result.x2 = a.x2 > b.x2 ? a.x2 : b.x2; + result.y2 = a.y2 > b.y2 ? a.y2 : b.y2; + result.domainMask = a.domainMask | b.domainMask; + return result; +} + +inline bool TemporalHistoryCore_ReactiveRegionsTouch( + const temporalReactiveRegion_t &a, + const temporalReactiveRegion_t &b ) { + return a.x1 <= b.x2 && b.x1 <= a.x2 + && a.y1 <= b.y2 && b.y1 <= a.y2; +} + +inline void TemporalHistoryCore_AddReactiveRegion( + temporalViewMotionPolicy_t &policy, unsigned int domainMask, + float x1, float y1, float x2, float y2 ) { + temporalReactiveRegion_t incoming; + incoming.x1 = TemporalHistoryCore_ClampUnit( x1 ); + incoming.y1 = TemporalHistoryCore_ClampUnit( y1 ); + incoming.x2 = TemporalHistoryCore_ClampUnit( x2 ); + incoming.y2 = TemporalHistoryCore_ClampUnit( y2 ); + incoming.domainMask = domainMask; + if ( incoming.x2 <= incoming.x1 || incoming.y2 <= incoming.y1 ) { + // Missing or degenerate packet scissors must fail safe. A full-screen + // region is expensive for one frame, but it cannot retain stale history. + incoming.x1 = 0.0f; + incoming.y1 = 0.0f; + incoming.x2 = 1.0f; + incoming.y2 = 1.0f; + } + + for ( int i = 0; i < policy.reactiveRegionCount; ++i ) { + if ( !TemporalHistoryCore_ReactiveRegionsTouch( + policy.reactiveRegions[i], incoming ) ) { + continue; + } + policy.reactiveRegions[i] = TemporalHistoryCore_UnionReactiveRegions( + policy.reactiveRegions[i], incoming ); + // The expanded region may now connect the two retained islands. + if ( policy.reactiveRegionCount == 2 + && TemporalHistoryCore_ReactiveRegionsTouch( + policy.reactiveRegions[0], policy.reactiveRegions[1] ) ) { + policy.reactiveRegions[0] = TemporalHistoryCore_UnionReactiveRegions( + policy.reactiveRegions[0], policy.reactiveRegions[1] ); + policy.reactiveRegionCount = 1; + policy.reactiveRegionsMerged = true; + } + return; + } + + if ( policy.reactiveRegionCount < TEMPORAL_MAX_REACTIVE_REGIONS ) { + policy.reactiveRegions[policy.reactiveRegionCount++] = incoming; + return; + } + + // Retain two conservative islands. Of the three possible pairings, merge + // the pair with the least additional covered area and keep the third island. + const temporalReactiveRegion_t merge0 = + TemporalHistoryCore_UnionReactiveRegions( policy.reactiveRegions[0], incoming ); + const temporalReactiveRegion_t merge1 = + TemporalHistoryCore_UnionReactiveRegions( policy.reactiveRegions[1], incoming ); + const temporalReactiveRegion_t mergeExisting = + TemporalHistoryCore_UnionReactiveRegions( + policy.reactiveRegions[0], policy.reactiveRegions[1] ); + const float cost0 = TemporalHistoryCore_ReactiveRegionArea( merge0 ) + - TemporalHistoryCore_ReactiveRegionArea( policy.reactiveRegions[0] ) + - TemporalHistoryCore_ReactiveRegionArea( incoming ); + const float cost1 = TemporalHistoryCore_ReactiveRegionArea( merge1 ) + - TemporalHistoryCore_ReactiveRegionArea( policy.reactiveRegions[1] ) + - TemporalHistoryCore_ReactiveRegionArea( incoming ); + const float costExisting = TemporalHistoryCore_ReactiveRegionArea( mergeExisting ) + - TemporalHistoryCore_ReactiveRegionArea( policy.reactiveRegions[0] ) + - TemporalHistoryCore_ReactiveRegionArea( policy.reactiveRegions[1] ); + if ( costExisting <= cost0 && costExisting <= cost1 ) { + policy.reactiveRegions[0] = mergeExisting; + policy.reactiveRegions[1] = incoming; + } else if ( cost0 <= cost1 ) { + policy.reactiveRegions[0] = merge0; + } else { + policy.reactiveRegions[1] = merge1; + } + policy.reactiveRegionsMerged = true; +} + +inline bool TemporalHistoryCore_MotionSourceHasPreviousGeometry( + temporalMotionSource_t source ) { + return source == TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM + || source == TEMPORAL_MOTION_SOURCE_SKINNED_PALETTE + || source == TEMPORAL_MOTION_SOURCE_DEFORM_VERTEX; +} + +inline void TemporalHistoryCore_AddMotionOwnership( + temporalViewMotionPolicy_t &policy, + const temporalMotionOwnership_t &ownership, + unsigned int backendExactMotionDomainMask, + float x1, float y1, float x2, float y2 ) { + const unsigned int domainBit = + TemporalHistoryCore_MotionDomainBit( ownership.domain ); + if ( domainBit == 0u ) { + return; + } + policy.presentDomainMask |= domainBit; + const bool exactMotion = ( backendExactMotionDomainMask & domainBit ) != 0u + && TemporalHistoryCore_MotionSourceHasPreviousGeometry( ownership.source ); + if ( exactMotion ) { + policy.exactMotionDomainMask |= domainBit; + } + const bool explicitlyReactive = ( ownership.flags + & ( TEMPORAL_MOTION_OWNERSHIP_REACTIVE + | TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY ) ) != 0u; + const bool unsupportedObjectMotion = + ownership.domain != TEMPORAL_MOTION_DOMAIN_STATIC_WORLD && !exactMotion; + if ( !explicitlyReactive && !unsupportedObjectMotion ) { + return; + } + policy.reactiveDomainMask |= domainBit; + TemporalHistoryCore_AddReactiveRegion( policy, domainBit, + x1, y1, x2, y2 ); +} + +inline std::uint64_t TemporalHistoryCore_MixIdentity( + std::uint64_t hash, std::uint64_t value ) { + // FNV-1a over caller-normalized fields. This is an in-process identity, + // not a security boundary or serialized asset hash. + for ( int byteIndex = 0; byteIndex < 8; ++byteIndex ) { + hash ^= ( value >> ( byteIndex * 8 ) ) & UINT64_C( 0xff ); + hash *= UINT64_C( 1099511628211 ); + } + return hash; +} + +inline std::uint64_t TemporalHistoryCore_BeginIdentity( void ) { + return UINT64_C( 14695981039346656037 ); +} + +inline float TemporalHistoryCore_RadicalInverse( + std::uint32_t index, std::uint32_t base ) { + const float inverseBase = 1.0f / static_cast( base ); + float fraction = inverseBase; + float result = 0.0f; + while ( index != 0u ) { + result += static_cast( index % base ) * fraction; + index /= base; + fraction *= inverseBase; + } + return result; +} + +inline temporalJitterSample_t TemporalHistoryCore_Jitter( + std::uint32_t frameIndex ) { + // An eight-sample Halton(2,3) cycle is deterministic across GL/Vulkan and + // bounded to a half-pixel footprint. Index zero intentionally starts at + // Halton sample one rather than the unhelpful (0,0) sequence origin. + const std::uint32_t sequenceIndex = ( frameIndex & 7u ) + 1u; + temporalJitterSample_t sample; + sample.x = TemporalHistoryCore_RadicalInverse( sequenceIndex, 2u ) - 0.5f; + sample.y = TemporalHistoryCore_RadicalInverse( sequenceIndex, 3u ) - 0.5f; + return sample; +} + +inline float TemporalHistoryCore_Dot3( const float *a, const float *b ) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +inline float TemporalHistoryCore_DistanceSquared3( + const float *a, const float *b ) { + const float x = a[0] - b[0]; + const float y = a[1] - b[1]; + const float z = a[2] - b[2]; + return x * x + y * y + z * z; +} + +inline temporalCameraPolicy_t TemporalHistoryCore_DefaultCameraPolicy( void ) { + temporalCameraPolicy_t policy; + policy.maximumFrameGap = 2; + policy.maximumTimeGapMsec = 100; + policy.maximumTranslation = 512.0f; + // About 20 degrees. Normal continuous camera motion remains reprojectable; + // teleports and hard authored cuts reject history immediately. + policy.minimumAxisDot = 0.93969262f; + policy.maximumFovDelta = 0.25f; + return policy; +} + +inline temporalHistoryResetReason_t TemporalHistoryCore_ValidateCameraHistory( + const temporalCameraState_t ¤t, + const temporalCameraState_t &previous, + const temporalCameraPolicy_t &policy ) { + if ( !previous.valid ) { + return TEMPORAL_HISTORY_RESET_FIRST_SAMPLE; + } + if ( current.historyGeneration != previous.historyGeneration ) { + return TEMPORAL_HISTORY_RESET_GENERATION; + } + if ( current.viewIdentity == 0 || current.viewIdentity != previous.viewIdentity ) { + return TEMPORAL_HISTORY_RESET_VIEW_IDENTITY; + } + if ( current.outputWidth != previous.outputWidth + || current.outputHeight != previous.outputHeight ) { + return TEMPORAL_HISTORY_RESET_OUTPUT_EXTENT; + } + // History images live at the native output extent. A dynamic-resolution + // change only alters the current input sampling footprint, so rejecting the + // prior native history here would defeat temporal upscaling exactly when the + // controller changes scale. + const std::uint32_t frameDelta = static_cast( current.frameNumber ) + - static_cast( previous.frameNumber ); + if ( frameDelta == 0u || frameDelta > static_cast( policy.maximumFrameGap ) + || current.renderTimeMsec < previous.renderTimeMsec + || current.renderTimeMsec - previous.renderTimeMsec > policy.maximumTimeGapMsec ) { + return TEMPORAL_HISTORY_RESET_TIME_DISCONTINUITY; + } + if ( TemporalHistoryCore_DistanceSquared3( current.viewOrigin, + previous.viewOrigin ) > policy.maximumTranslation * policy.maximumTranslation ) { + return TEMPORAL_HISTORY_RESET_CAMERA_TRANSLATION; + } + for ( int axisIndex = 0; axisIndex < 3; ++axisIndex ) { + if ( TemporalHistoryCore_Dot3( current.viewAxis + axisIndex * 3, + previous.viewAxis + axisIndex * 3 ) < policy.minimumAxisDot ) { + return TEMPORAL_HISTORY_RESET_CAMERA_ROTATION; + } + } + if ( std::fabs( current.fovX - previous.fovX ) > policy.maximumFovDelta + || std::fabs( current.fovY - previous.fovY ) > policy.maximumFovDelta ) { + return TEMPORAL_HISTORY_RESET_PROJECTION; + } + return TEMPORAL_HISTORY_RESET_NONE; +} + +inline temporalMotionOwnership_t TemporalHistoryCore_ClassifyMotion( + const temporalMotionInput_t &input ) { + temporalMotionOwnership_t ownership; + ownership.domain = TEMPORAL_MOTION_DOMAIN_STATIC_WORLD; + ownership.source = TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + ownership.flags = TEMPORAL_MOTION_OWNERSHIP_DISOCCLUSION_TEST; + + if ( input.subview ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_SUBVIEW; + ownership.source = TEMPORAL_MOTION_SOURCE_INDEPENDENT_VIEW; + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY + | TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } else if ( input.inWorldGui ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_IN_WORLD_GUI; + ownership.source = input.hasPreviousTransform + ? TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM + : TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } else if ( input.viewModel ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_VIEW_MODEL; + ownership.source = input.hasPreviousTransform + ? TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM + : TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE + | TEMPORAL_MOTION_OWNERSHIP_DEPTH_HACK; + } else if ( input.particle ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_PARTICLE; + ownership.source = TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } else if ( input.deform ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_DEFORM; + ownership.source = input.hasPreviousDeformedVertices + ? TEMPORAL_MOTION_SOURCE_DEFORM_VERTEX + : TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + if ( !input.hasPreviousDeformedVertices ) { + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } + } else if ( input.skinned ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_SKINNED; + ownership.source = input.hasPreviousSkinningPalette + ? TEMPORAL_MOTION_SOURCE_SKINNED_PALETTE + : TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + if ( !input.hasPreviousSkinningPalette ) { + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } + } else if ( input.hasEntity ) { + ownership.domain = TEMPORAL_MOTION_DOMAIN_RIGID; + ownership.source = input.hasPreviousTransform + ? TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM + : TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH; + } + + if ( input.hasPreviousTransform ) { + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_HAS_PREVIOUS_TRANSFORM; + } + if ( input.translucent ) { + ownership.flags |= TEMPORAL_MOTION_OWNERSHIP_REACTIVE; + } + return ownership; +} + +#endif /* !__TEMPORAL_HISTORY_CORE_H__ */ diff --git a/src/renderer/TemporalPresentation.cpp b/src/renderer/TemporalPresentation.cpp new file mode 100644 index 00000000..572ed4c7 --- /dev/null +++ b/src/renderer/TemporalPresentation.cpp @@ -0,0 +1,790 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "tr_local.h" +#include "RendererMetrics.h" +#include "TemporalPresentation.h" + +idCVar r_dynamicResolutionMinScale( "r_dynamicResolutionMinScale", "50", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "minimum automatic 3D scene scale percentage", 10, 100 ); +idCVar r_dynamicResolutionMaxScale( "r_dynamicResolutionMaxScale", "100", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "maximum automatic 3D scene scale percentage", 10, 100 ); +idCVar r_dynamicResolutionTargetMsec( "r_dynamicResolutionTargetMsec", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "GPU frame-time target in milliseconds; 0 derives it from the presentation cap and display", 0.0f, 1000.0f ); +idCVar r_dynamicResolutionTargetUtilization( "r_dynamicResolutionTargetUtilization", "90", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "percentage of the presentation interval available to renderer GPU work", 50, 100 ); +idCVar r_dynamicResolutionDropStep( "r_dynamicResolutionDropStep", "5", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "maximum percentage-point reduction per newly retired over-budget sample", 1, 50 ); +idCVar r_dynamicResolutionRaiseStep( "r_dynamicResolutionRaiseStep", "2", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "percentage-point increase after a complete under-budget streak", 1, 25 ); +idCVar r_dynamicResolutionRaiseFrames( "r_dynamicResolutionRaiseFrames", "30", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "new retired under-budget samples required before increasing scene scale", 1, 1000 ); +idCVar r_dynamicResolutionRaiseThreshold( "r_dynamicResolutionRaiseThreshold", "85", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "target-budget percentage below which a sample contributes to scale recovery", 1, 99 ); +idCVar r_dynamicResolutionAlignment( "r_dynamicResolutionAlignment", "8", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "scene-target dimension alignment in pixels", 1, 256 ); +idCVar r_dynamicResolutionMaxSampleAge( "r_dynamicResolutionMaxSampleAge", "32", + CVAR_RENDERER | CVAR_INTEGER, + "maximum delayed GPU-timing sample age accepted by the controller", 1, 1000 ); +idCVar r_dynamicResolutionMissingSampleReset( "r_dynamicResolutionMissingSampleReset", "120", + CVAR_RENDERER | CVAR_INTEGER, + "frames without a usable retired timing sample before returning to the safe scale ceiling", 1, 10000 ); +idCVar r_dynamicResolutionCaptureNative( "r_dynamicResolutionCaptureNative", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, + "render screenshot/save-preview 3D scenes at 100%; default keeps the active scale and freezes feedback" ); +idCVar r_dynamicResolutionDebug( "r_dynamicResolutionDebug", "0", + CVAR_RENDERER | CVAR_INTEGER, + "print automatic resolution decisions: 0 = off, 1 = scale/reset, 2 = every frame", 0, 2 ); + +idCVar r_temporalAA( "r_temporalAA", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, + "enable experimental temporal anti-aliasing/upscaling with automatic SMAA rollback" ); +idCVar r_temporalAAFeedback( "r_temporalAAFeedback", "0.90", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "maximum accepted temporal-history contribution", 0.0f, 0.98f ); +idCVar r_temporalAAReactiveScale( "r_temporalAAReactiveScale", "1.0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "reactive-mask rejection strength for particles, translucency, deforms, and in-world GUI", 0.0f, 2.0f ); +idCVar r_temporalAADebug( "r_temporalAADebug", "0", + CVAR_RENDERER | CVAR_INTEGER, + "temporal presentation diagnostic view: 0 = final, 1 = velocity, 2 = reactive, 3 = history weight", 0, 3 ); + +idCVar r_rendererFroxelVolumetrics( "r_rendererFroxelVolumetrics", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, + "enable bounded view-aligned froxel volumetric integration in the native scene presentation tail" ); +idCVar r_froxelVolumetricDensity( "r_froxelVolumetricDensity", "0.00012", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "base extinction density for optional froxel volumetrics", 0.00001f, 0.01f ); +idCVar r_froxelVolumetricMaxDistance( "r_froxelVolumetricMaxDistance", "2048", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "maximum world-space distance integrated by optional froxel volumetrics", 64.0f, 8192.0f ); +idCVar r_froxelVolumetricSlices( "r_froxelVolumetricSlices", "12", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "bounded view-depth slices integrated by optional froxel volumetrics", 4, + ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES ); +idCVar r_rendererSSR( "r_rendererSSR", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, + "enable bounded depth-derived screen-space reflections in the native scene presentation tail" ); +idCVar r_screenReflectionIntensity( "r_screenReflectionIntensity", "0.35", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "maximum contribution of optional screen-space reflections", 0.0f, 1.0f ); +idCVar r_screenReflectionMaxDistance( "r_screenReflectionMaxDistance", "512", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "maximum view-space ray distance for optional screen-space reflections", 32.0f, 2048.0f ); +idCVar r_screenReflectionSteps( "r_screenReflectionSteps", "10", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, + "bounded ray-march steps for optional screen-space reflections", 4, + ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS ); +idCVar r_rendererSSGI( "r_rendererSSGI", "0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, + "enable bounded eight-tap screen-space diffuse GI in the native scene presentation tail" ); +idCVar r_screenGIIntensity( "r_screenGIIntensity", "0.25", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT, + "maximum contribution of optional screen-space diffuse GI", 0.0f, 1.0f ); + +static const int TEMPORAL_SCALE_HISTORY_COUNT = 128; + +typedef struct temporalScaleHistoryEntry_s { + bool valid; + bool eligibleForFeedback; + int frameNumber; + unsigned int generation; + int scalePercent; +} temporalScaleHistoryEntry_t; + +static temporalResolutionState_t rg_temporalResolutionState; +static bool rg_temporalPresentationInitialized = false; +static bool rg_dynamicResolutionEnabledLastFrame = false; +static bool rg_temporalAAEnabledLastFrame = false; +static advancedScreenSpaceConfig_t rg_advancedScreenSpaceLast = {}; +static int rg_videoRestartCount = -1; +static temporalPresentationFrameState_t rg_temporalFrameState; +static temporalScaleHistoryEntry_t rg_temporalScaleHistory[TEMPORAL_SCALE_HISTORY_COUNT]; +static int rg_temporalScaleHistoryCursor = 0; +static unsigned int rg_temporalHistoryGeneration = 1; +static idStr rg_temporalHistoryResetReason = "renderer initialization"; + +static advancedScreenSpaceConfig_t R_TemporalPresentation_BuildAdvancedScreenSpaceConfig( void ) { + return AdvancedScreenSpaceCore_Build( + r_rendererModernQuality.GetBool(), + r_rendererFroxelVolumetrics.GetBool(), + r_rendererSSR.GetBool(), + r_rendererSSGI.GetBool(), + r_froxelVolumetricDensity.GetFloat(), + r_froxelVolumetricMaxDistance.GetFloat(), + r_froxelVolumetricSlices.GetInteger(), + r_screenReflectionIntensity.GetFloat(), + r_screenReflectionMaxDistance.GetFloat(), + r_screenReflectionSteps.GetInteger(), + r_screenGIIntensity.GetFloat() ); +} + +static bool R_TemporalPresentation_AdvancedScreenSpaceEqual( + const advancedScreenSpaceConfig_t &lhs, + const advancedScreenSpaceConfig_t &rhs ) { + return lhs.effectMask == rhs.effectMask + && lhs.froxelDensity == rhs.froxelDensity + && lhs.froxelMaxDistance == rhs.froxelMaxDistance + && lhs.froxelSlices == rhs.froxelSlices + && lhs.ssrIntensity == rhs.ssrIntensity + && lhs.ssrMaxDistance == rhs.ssrMaxDistance + && lhs.ssrSteps == rhs.ssrSteps + && lhs.ssgiIntensity == rhs.ssgiIntensity; +} +static int rg_lastCaptureMarkedFrame = -1; + +static const int TEMPORAL_VIEW_HISTORY_SLOTS = 32; + +typedef struct temporalViewHistorySlot_s { + bool valid; + int lastUsedFrame; + temporalCameraState_t camera; +} temporalViewHistorySlot_t; + +static temporalViewHistorySlot_t + rg_temporalViewHistory[TEMPORAL_VIEW_HISTORY_SLOTS]; + +static void R_TemporalPresentation_ClearViewHistory( void ) { + memset( rg_temporalViewHistory, 0, sizeof( rg_temporalViewHistory ) ); +} + +static void R_TemporalPresentation_ClearScaleHistory( void ) { + memset( rg_temporalScaleHistory, 0, sizeof( rg_temporalScaleHistory ) ); + rg_temporalScaleHistoryCursor = 0; +} + +static int R_TemporalPresentation_FrameAge( int currentFrame, int sampleFrame ) { + const unsigned int age = static_cast( currentFrame ) + - static_cast( sampleFrame ); + if ( age >= 0x80000000u || age > 0x7fffffffu ) { + return -1; + } + return static_cast( age ); +} + +static int R_TemporalPresentation_FindSampleScale( int frameNumber, + unsigned int generation ) { + for ( int i = 0; i < TEMPORAL_SCALE_HISTORY_COUNT; i++ ) { + const temporalScaleHistoryEntry_t &entry = rg_temporalScaleHistory[i]; + if ( entry.valid && entry.eligibleForFeedback + && entry.frameNumber == frameNumber + && entry.generation == generation ) { + return entry.scalePercent; + } + } + return 0; +} + +static void R_TemporalPresentation_RecordFrameScale( int frameNumber, + unsigned int generation, int scalePercent, bool eligibleForFeedback ) { + temporalScaleHistoryEntry_t &entry = + rg_temporalScaleHistory[rg_temporalScaleHistoryCursor]; + entry.valid = true; + entry.eligibleForFeedback = eligibleForFeedback; + entry.frameNumber = frameNumber; + entry.generation = generation; + entry.scalePercent = scalePercent; + rg_temporalScaleHistoryCursor = + ( rg_temporalScaleHistoryCursor + 1 ) % TEMPORAL_SCALE_HISTORY_COUNT; +} + +static unsigned long long R_TemporalPresentation_TargetMicroseconds( void ) { + const float explicitMsec = r_dynamicResolutionTargetMsec.GetFloat(); + if ( explicitMsec > 0.0f ) { + return static_cast( idMath::ClampFloat( + 0.1f, 1000.0f, explicitMsec ) * 1000.0f + 0.5f ); + } + + int presentationFps = 0; + if ( r_swapInterval.GetInteger() != 0 && glConfig.displayFrequency > 0 ) { + presentationFps = glConfig.displayFrequency; + } + if ( cvarSystem != NULL ) { + const int maxFps = Max( 0, cvarSystem->GetCVarInteger( "com_maxfps" ) ); + if ( maxFps > 0 ) { + presentationFps = presentationFps > 0 + ? Min( presentationFps, maxFps ) : maxFps; + } + } + if ( presentationFps <= 0 ) { + presentationFps = glConfig.displayFrequency > 0 + ? glConfig.displayFrequency : 60; + } + + const unsigned long long interval = + UINT64_C( 1000000 ) / static_cast( Max( 1, presentationFps ) ); + return TemporalResolutionCore_PercentOf( interval, + idMath::ClampInt( 50, 100, r_dynamicResolutionTargetUtilization.GetInteger() ) ); +} + +const char *R_TemporalPresentation_DecisionName( temporalResolutionDecision_t decision ) { + switch ( decision ) { + case TEMPORAL_RESOLUTION_DECISION_DISABLED: return "disabled"; + case TEMPORAL_RESOLUTION_DECISION_FORCED_NATIVE: return "forced-native"; + case TEMPORAL_RESOLUTION_DECISION_FROZEN: return "frozen"; + case TEMPORAL_RESOLUTION_DECISION_WAITING_FOR_SAMPLE: return "waiting"; + case TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET: return "reset"; + case TEMPORAL_RESOLUTION_DECISION_HOLD: return "hold"; + case TEMPORAL_RESOLUTION_DECISION_DROP: return "drop"; + case TEMPORAL_RESOLUTION_DECISION_RAISE: return "raise"; + default: return "unknown"; + } +} + +void R_TemporalPresentation_InvalidateHistory( const char *reason ) { + rg_temporalHistoryGeneration++; + if ( rg_temporalHistoryGeneration == 0 ) { + rg_temporalHistoryGeneration = 1; + } + rg_temporalHistoryResetReason = + ( reason != NULL && reason[0] != '\0' ) ? reason : "unspecified"; + R_TemporalPresentation_ClearViewHistory(); +} + +unsigned int R_TemporalPresentation_HistoryGeneration( void ) { + return rg_temporalHistoryGeneration; +} + +const char *R_TemporalPresentation_LastHistoryResetReason( void ) { + return rg_temporalHistoryResetReason.c_str(); +} + +void R_TemporalPresentation_MarkCurrentFrameCapture( const char *reason ) { + for ( int i = 0; i < TEMPORAL_SCALE_HISTORY_COUNT; ++i ) { + temporalScaleHistoryEntry_t &entry = rg_temporalScaleHistory[i]; + if ( entry.valid && entry.frameNumber == tr.frameCount ) { + entry.eligibleForFeedback = false; + } + } + rg_temporalFrameState.captureFrozen = true; + if ( rg_lastCaptureMarkedFrame == tr.frameCount ) { + return; + } + rg_lastCaptureMarkedFrame = tr.frameCount; + // A synchronous capture can flush a temporal-resolve command that the game + // queued before it knew a readback was coming. The backend rejects both + // history reads and writes while tr.takingScreenshot is live; advance the + // image-history generation as well so the game's ping-pong owner cannot use + // the untouched destination as a valid sample on the following frame. + R_TemporalPresentation_InvalidateHistory( + ( reason != NULL && reason[0] != '\0' ) ? reason : "late capture" ); + if ( r_dynamicResolutionDebug.GetInteger() >= 1 ) { + common->Printf( + "Temporal resolution: frame=%d captureScope=%s scale=%d%% feedbackEligible=0\n", + tr.frameCount, + ( reason != NULL && reason[0] != '\0' ) ? reason : "late-capture", + rg_temporalFrameState.effectiveScalePercent ); + } +} + +const char *R_TemporalPresentation_HistoryResetReasonName( + temporalHistoryResetReason_t reason ) { + switch ( reason ) { + case TEMPORAL_HISTORY_RESET_NONE: return "none"; + case TEMPORAL_HISTORY_RESET_FIRST_SAMPLE: return "first-sample"; + case TEMPORAL_HISTORY_RESET_GENERATION: return "generation"; + case TEMPORAL_HISTORY_RESET_VIEW_IDENTITY: return "view-identity"; + case TEMPORAL_HISTORY_RESET_OUTPUT_EXTENT: return "output-extent"; + case TEMPORAL_HISTORY_RESET_SCENE_EXTENT: return "scene-extent"; + case TEMPORAL_HISTORY_RESET_TIME_DISCONTINUITY: return "time-discontinuity"; + case TEMPORAL_HISTORY_RESET_CAMERA_TRANSLATION: return "camera-translation"; + case TEMPORAL_HISTORY_RESET_CAMERA_ROTATION: return "camera-rotation"; + case TEMPORAL_HISTORY_RESET_PROJECTION: return "projection"; + case TEMPORAL_HISTORY_RESET_EXPLICIT_CUT: return "explicit-cut"; + case TEMPORAL_HISTORY_RESET_CAPTURE: return "capture"; + case TEMPORAL_HISTORY_RESET_RESOURCE_LOSS: return "resource-loss"; + default: return "unknown"; + } +} + +static unsigned long long R_TemporalPresentation_MixStringIdentity( + unsigned long long identity, const char *text ) { + if ( text == NULL ) { + return TemporalHistoryCore_MixIdentity( identity, 0 ); + } + unsigned long long value = TemporalHistoryCore_BeginIdentity(); + for ( const unsigned char *cursor = + reinterpret_cast( text ); *cursor != 0; ++cursor ) { + value ^= *cursor; + value *= UINT64_C( 1099511628211 ); + } + return TemporalHistoryCore_MixIdentity( identity, value ); +} + +unsigned long long R_TemporalPresentation_ViewIdentity( + const viewDef_t *viewDef ) { + if ( viewDef == NULL ) { + return 0; + } + + unsigned long long identity = TemporalHistoryCore_BeginIdentity(); + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( reinterpret_cast( + viewDef->renderWorld ) ) ); + identity = R_TemporalPresentation_MixStringIdentity( identity, + viewDef->renderWorld != NULL + ? viewDef->renderWorld->mapName.c_str() : NULL ); + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( viewDef->renderView.viewID ) ); + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( viewDef->renderFlags ) ); + identity = TemporalHistoryCore_MixIdentity( identity, + ( viewDef->isSubview ? 1u : 0u ) + | ( viewDef->isMirror ? 2u : 0u ) + | ( viewDef->isXraySubview ? 4u : 0u ) ); + + const viewDef_t *cursor = viewDef; + int depth = 0; + while ( cursor != NULL && depth < 16 ) { + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( cursor->renderView.viewID ) ); + const drawSurf_t *surface = cursor->subviewSurface; + if ( surface != NULL ) { + const int entityIndex = surface->space != NULL + && surface->space->entityDef != NULL + ? surface->space->entityDef->index : -1; + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( entityIndex ) ); + identity = TemporalHistoryCore_MixIdentity( identity, + surface->geo != NULL + ? static_cast( surface->geo->myID ) : 0u ); + identity = R_TemporalPresentation_MixStringIdentity( identity, + surface->material != NULL ? surface->material->GetName() : NULL ); + } + cursor = cursor->superView; + depth++; + } + identity = TemporalHistoryCore_MixIdentity( identity, + static_cast( depth ) ); + return identity != 0 ? identity : 1; +} + +static temporalViewHistorySlot_t *R_TemporalPresentation_FindViewHistory( + unsigned long long identity, bool allocate ) { + temporalViewHistorySlot_t *oldest = &rg_temporalViewHistory[0]; + for ( int i = 0; i < TEMPORAL_VIEW_HISTORY_SLOTS; ++i ) { + temporalViewHistorySlot_t &slot = rg_temporalViewHistory[i]; + if ( slot.valid && slot.camera.viewIdentity == identity ) { + return &slot; + } + if ( !slot.valid ) { + oldest = &slot; + break; + } + if ( slot.lastUsedFrame < oldest->lastUsedFrame ) { + oldest = &slot; + } + } + if ( !allocate ) { + return NULL; + } + memset( oldest, 0, sizeof( *oldest ) ); + return oldest; +} + +static void R_TemporalPresentation_BuildCameraState( + const viewDef_t *viewDef, temporalCameraState_t &camera ) { + memset( &camera, 0, sizeof( camera ) ); + camera.valid = true; + camera.viewIdentity = viewDef->temporalViewIdentity; + camera.historyGeneration = rg_temporalHistoryGeneration; + camera.frameNumber = tr.frameCount; + camera.renderTimeMsec = viewDef->renderView.time; + + const int viewWidth = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; + const int viewHeight = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; + const bool usesPresentationExtent = !viewDef->isSubview + || ( viewDef->superView != NULL + && viewDef->viewport.Equals( viewDef->superView->viewport ) ); + if ( usesPresentationExtent && rg_temporalPresentationInitialized ) { + camera.outputWidth = rg_temporalFrameState.nativeWidth; + camera.outputHeight = rg_temporalFrameState.nativeHeight; + camera.sceneWidth = rg_temporalFrameState.sceneWidth; + camera.sceneHeight = rg_temporalFrameState.sceneHeight; + } else { + camera.outputWidth = viewWidth; + camera.outputHeight = viewHeight; + camera.sceneWidth = viewWidth; + camera.sceneHeight = viewHeight; + } + memcpy( camera.viewOrigin, viewDef->renderView.vieworg.ToFloatPtr(), + sizeof( camera.viewOrigin ) ); + for ( int axisIndex = 0; axisIndex < 3; ++axisIndex ) { + memcpy( camera.viewAxis + axisIndex * 3, + viewDef->renderView.viewaxis[axisIndex].ToFloatPtr(), + 3 * sizeof( float ) ); + } + camera.fovX = viewDef->renderView.fov_x; + camera.fovY = viewDef->renderView.fov_y; +} + +void R_TemporalPresentation_PrepareView( viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->temporalPrepared ) { + return; + } + viewDef->temporalPrepared = true; + viewDef->temporalViewIdentity = R_TemporalPresentation_ViewIdentity( viewDef ); + viewDef->temporalHistoryGeneration = rg_temporalHistoryGeneration; + viewDef->temporalJitterIndex = tr.frameCount & 7; + viewDef->temporalJitterPixels.Zero(); + viewDef->temporalPreviousViewOrigin.Zero(); + viewDef->temporalPreviousViewAxis.Identity(); + viewDef->temporalPreviousProjectInfo.Zero(); + viewDef->temporalPreviousJitterPixels.Zero(); + viewDef->temporalHistoryResetReason = TEMPORAL_HISTORY_RESET_FIRST_SAMPLE; + viewDef->temporalJitterEnabled = false; + viewDef->temporalHistoryValid = false; + viewDef->temporalCaptureFrame = tr.takingScreenshot; + viewDef->temporalPreviousProjectionValid = false; + + if ( !R_TemporalPresentation_TemporalAARequested() + || viewDef->renderWorld == NULL || viewDef->isEditor + || viewDef->isSubview + || ( viewDef->renderFlags & RF_PORTAL_SKY ) != 0 ) { + // Capture-backed portals, mirrors, and remote views do not yet own an + // independent image-history target. Keep their projection stable and + // let the parent temporal pass treat the sampled surface as reactive. + return; + } + if ( tr.takingScreenshot ) { + // A known screenshot/save-preview frame is rendered without temporal + // projection jitter because the backend deliberately bypasses history. + viewDef->temporalHistoryResetReason = TEMPORAL_HISTORY_RESET_CAPTURE; + return; + } + + const temporalJitterSample_t jitter = TemporalHistoryCore_Jitter( + static_cast( viewDef->temporalJitterIndex ) ); + const int projectionWidth = viewDef->viewport.x2 + - viewDef->viewport.x1 + 1; + const int projectionHeight = viewDef->viewport.y2 + - viewDef->viewport.y1 + 1; + const int sceneWidth = rg_temporalFrameState.sceneWidth > 0 + ? rg_temporalFrameState.sceneWidth : projectionWidth; + const int sceneHeight = rg_temporalFrameState.sceneHeight > 0 + ? rg_temporalFrameState.sceneHeight : projectionHeight; + // R_SetupProjection divides these values by its current front-end viewport. + // Scale the sample so the final offset remains one render-target subpixel + // whether the target is selected by a front-end crop or by a backend FBO. + viewDef->temporalJitterPixels.Set( + jitter.x * static_cast( projectionWidth ) + / static_cast( Max( 1, sceneWidth ) ), + jitter.y * static_cast( projectionHeight ) + / static_cast( Max( 1, sceneHeight ) ) ); + viewDef->temporalJitterEnabled = true; + + temporalCameraState_t current; + R_TemporalPresentation_BuildCameraState( viewDef, current ); + temporalViewHistorySlot_t *slot = R_TemporalPresentation_FindViewHistory( + viewDef->temporalViewIdentity, true ); + const temporalCameraState_t previous = slot->camera; + if ( previous.valid && previous.projectionValid ) { + viewDef->temporalPreviousViewOrigin.Set( + previous.viewOrigin[0], previous.viewOrigin[1], + previous.viewOrigin[2] ); + for ( int axisIndex = 0; axisIndex < 3; ++axisIndex ) { + viewDef->temporalPreviousViewAxis[axisIndex].Set( + previous.viewAxis[axisIndex * 3 + 0], + previous.viewAxis[axisIndex * 3 + 1], + previous.viewAxis[axisIndex * 3 + 2] ); + } + viewDef->temporalPreviousProjectInfo.Set( + previous.projectInfo[0], previous.projectInfo[1], + previous.projectInfo[2], previous.projectInfo[3] ); + viewDef->temporalPreviousJitterPixels.Set( + previous.jitterPixels[0], previous.jitterPixels[1] ); + viewDef->temporalPreviousProjectionValid = true; + } + viewDef->temporalHistoryResetReason = + TemporalHistoryCore_ValidateCameraHistory( current, previous, + TemporalHistoryCore_DefaultCameraPolicy() ); + if ( viewDef->renderView.forceUpdate ) { + // Camera animations publish authored cuts through the existing render-view + // force-update bit. This catches visually discontinuous cuts that happen to + // remain inside the geometric translation/rotation/FOV thresholds. + viewDef->temporalHistoryResetReason = TEMPORAL_HISTORY_RESET_EXPLICIT_CUT; + } + viewDef->temporalHistoryValid = + viewDef->temporalHistoryResetReason == TEMPORAL_HISTORY_RESET_NONE + && viewDef->temporalPreviousProjectionValid; + + const bool rootCut = !viewDef->isSubview + && ( viewDef->temporalHistoryResetReason + == TEMPORAL_HISTORY_RESET_TIME_DISCONTINUITY + || viewDef->temporalHistoryResetReason + == TEMPORAL_HISTORY_RESET_CAMERA_TRANSLATION + || viewDef->temporalHistoryResetReason + == TEMPORAL_HISTORY_RESET_CAMERA_ROTATION + || viewDef->temporalHistoryResetReason + == TEMPORAL_HISTORY_RESET_PROJECTION + || viewDef->temporalHistoryResetReason + == TEMPORAL_HISTORY_RESET_EXPLICIT_CUT ); + if ( rootCut ) { + const char *reason = R_TemporalPresentation_HistoryResetReasonName( + viewDef->temporalHistoryResetReason ); + R_TemporalPresentation_InvalidateHistory( reason ); + viewDef->temporalHistoryGeneration = rg_temporalHistoryGeneration; + current.historyGeneration = rg_temporalHistoryGeneration; + viewDef->temporalHistoryValid = false; + slot = R_TemporalPresentation_FindViewHistory( + viewDef->temporalViewIdentity, true ); + } + + slot->valid = true; + slot->lastUsedFrame = tr.frameCount; + slot->camera = current; + slot->camera.valid = false; + slot->camera.projectionValid = false; +} + +void R_TemporalPresentation_FinalizeViewProjection( viewDef_t *viewDef ) { + if ( viewDef == NULL || !viewDef->temporalPrepared + || !viewDef->temporalJitterEnabled + || viewDef->temporalCaptureFrame ) { + return; + } + temporalViewHistorySlot_t *slot = R_TemporalPresentation_FindViewHistory( + viewDef->temporalViewIdentity, false ); + if ( slot == NULL || slot->camera.historyGeneration + != viewDef->temporalHistoryGeneration ) { + return; + } + slot->camera.projectInfo[0] = viewDef->projectionMatrix[0]; + slot->camera.projectInfo[1] = viewDef->projectionMatrix[5]; + slot->camera.projectInfo[2] = viewDef->projectionMatrix[8]; + slot->camera.projectInfo[3] = viewDef->projectionMatrix[9]; + slot->camera.depthProjection[0] = viewDef->projectionMatrix[10]; + slot->camera.depthProjection[1] = viewDef->projectionMatrix[14]; + slot->camera.jitterPixels[0] = viewDef->temporalJitterPixels.x; + slot->camera.jitterPixels[1] = viewDef->temporalJitterPixels.y; + slot->camera.valid = true; + slot->camera.projectionValid = true; +} + +void R_TemporalPresentation_BeginFrame( int nativeWidth, int nativeHeight, + bool captureFrame ) { + const advancedScreenSpaceConfig_t advancedScreenSpace = + R_TemporalPresentation_BuildAdvancedScreenSpaceConfig(); + if ( !rg_temporalPresentationInitialized ) { + TemporalResolutionCore_Initialize( rg_temporalResolutionState ); + R_TemporalPresentation_ClearScaleHistory(); + memset( &rg_temporalFrameState, 0, sizeof( rg_temporalFrameState ) ); + rg_temporalPresentationInitialized = true; + rg_dynamicResolutionEnabledLastFrame = r_rendererDynamicResolution.GetBool(); + rg_temporalAAEnabledLastFrame = r_temporalAA.GetBool(); + rg_advancedScreenSpaceLast = advancedScreenSpace; + rg_videoRestartCount = tr.videoRestartCount; + } + + const bool dynamicResolutionRequested = r_rendererDynamicResolution.GetBool(); + const bool temporalAARequested = r_temporalAA.GetBool(); + if ( dynamicResolutionRequested != rg_dynamicResolutionEnabledLastFrame ) { + R_RendererMetrics_ResetGpuFrameTiming( dynamicResolutionRequested + ? "dynamic resolution enabled" : "dynamic resolution disabled" ); + R_TemporalPresentation_ClearScaleHistory(); + TemporalResolutionCore_Initialize( rg_temporalResolutionState ); + R_TemporalPresentation_InvalidateHistory( dynamicResolutionRequested + ? "dynamic resolution enabled" : "dynamic resolution disabled" ); + rg_dynamicResolutionEnabledLastFrame = dynamicResolutionRequested; + } + if ( temporalAARequested != rg_temporalAAEnabledLastFrame ) { + R_TemporalPresentation_InvalidateHistory( temporalAARequested + ? "temporal AA enabled" : "temporal AA disabled" ); + rg_temporalAAEnabledLastFrame = temporalAARequested; + } + if ( !R_TemporalPresentation_AdvancedScreenSpaceEqual( + advancedScreenSpace, rg_advancedScreenSpaceLast ) ) { + R_TemporalPresentation_InvalidateHistory( + "advanced screen-space configuration changed" ); + rg_advancedScreenSpaceLast = advancedScreenSpace; + } + if ( tr.videoRestartCount != rg_videoRestartCount ) { + R_TemporalPresentation_ClearScaleHistory(); + TemporalResolutionCore_Initialize( rg_temporalResolutionState ); + R_TemporalPresentation_InvalidateHistory( "video restart" ); + rg_videoRestartCount = tr.videoRestartCount; + } + + renderGpuFrameTiming_t gpuTiming; + R_RendererMetrics_GetGpuFrameTiming( gpuTiming ); + + const int manualScalePercent = idMath::ClampInt( + 10, 200, r_screenFraction.GetInteger() ); + int maximumScalePercent = idMath::ClampInt( + 10, 100, r_dynamicResolutionMaxScale.GetInteger() ); + if ( manualScalePercent < 100 ) { + maximumScalePercent = Min( maximumScalePercent, manualScalePercent ); + } + const int minimumScalePercent = Min( maximumScalePercent, + idMath::ClampInt( 10, 100, r_dynamicResolutionMinScale.GetInteger() ) ); + + temporalResolutionConfig_t config = {}; + config.enabled = dynamicResolutionRequested; + config.minimumScalePercent = minimumScalePercent; + config.maximumScalePercent = maximumScalePercent; + config.dropStepPercent = r_dynamicResolutionDropStep.GetInteger(); + config.raiseStepPercent = r_dynamicResolutionRaiseStep.GetInteger(); + config.raiseFrames = r_dynamicResolutionRaiseFrames.GetInteger(); + config.raiseThresholdPercent = r_dynamicResolutionRaiseThreshold.GetInteger(); + config.missingSampleResetFrames = r_dynamicResolutionMissingSampleReset.GetInteger(); + config.maximumSampleAgeFrames = r_dynamicResolutionMaxSampleAge.GetInteger(); + config.targetMicroseconds = R_TemporalPresentation_TargetMicroseconds(); + config.dimensionAlignment = r_dynamicResolutionAlignment.GetInteger(); + + temporalResolutionSample_t sample = {}; + sample.supported = gpuTiming.supported; + sample.valid = gpuTiming.valid; + sample.backend = static_cast( gpuTiming.backend ); + sample.frameNumber = gpuTiming.frameNumber; + sample.generation = gpuTiming.generation; + sample.elapsedMicroseconds = gpuTiming.elapsedMicroseconds; + sample.scalePercent = gpuTiming.valid + ? R_TemporalPresentation_FindSampleScale( + gpuTiming.frameNumber, gpuTiming.generation ) : 0; + sample.ageFrames = gpuTiming.valid + ? R_TemporalPresentation_FrameAge( tr.frameCount, gpuTiming.frameNumber ) : 0; + + const bool forceNative = captureFrame && r_dynamicResolutionCaptureNative.GetBool(); + temporalResolutionOutput_t output = TemporalResolutionCore_Update( + rg_temporalResolutionState, config, sample, + nativeWidth, nativeHeight, captureFrame, forceNative ); + if ( !dynamicResolutionRequested ) { + output.scalePercent = manualScalePercent; + output.width = Max( 1, static_cast( + ( static_cast( Max( 0, nativeWidth ) ) * manualScalePercent + 50 ) / 100 ) ); + output.height = Max( 1, static_cast( + ( static_cast( Max( 0, nativeHeight ) ) * manualScalePercent + 50 ) / 100 ) ); + } + + const int previousScale = rg_temporalFrameState.effectiveScalePercent; + memset( &rg_temporalFrameState, 0, sizeof( rg_temporalFrameState ) ); + rg_temporalFrameState.dynamicResolutionRequested = dynamicResolutionRequested; + rg_temporalFrameState.dynamicResolutionActive = dynamicResolutionRequested + && gpuTiming.supported; + rg_temporalFrameState.temporalAARequested = temporalAARequested; + rg_temporalFrameState.captureFrozen = captureFrame && !forceNative; + rg_temporalFrameState.captureForcedNative = forceNative; + rg_temporalFrameState.timingSupported = gpuTiming.supported; + rg_temporalFrameState.timingValid = gpuTiming.valid; + rg_temporalFrameState.frameNumber = tr.frameCount; + rg_temporalFrameState.nativeWidth = nativeWidth; + rg_temporalFrameState.nativeHeight = nativeHeight; + rg_temporalFrameState.sceneWidth = output.width; + rg_temporalFrameState.sceneHeight = output.height; + rg_temporalFrameState.manualScalePercent = manualScalePercent; + rg_temporalFrameState.effectiveScalePercent = output.scalePercent; + rg_temporalFrameState.sampledScalePercent = sample.scalePercent; + rg_temporalFrameState.timingFrameNumber = gpuTiming.frameNumber; + rg_temporalFrameState.timingAgeFrames = sample.ageFrames; + rg_temporalFrameState.timingBackend = static_cast( gpuTiming.backend ); + rg_temporalFrameState.timingGeneration = gpuTiming.generation; + rg_temporalFrameState.timingMicroseconds = gpuTiming.elapsedMicroseconds; + rg_temporalFrameState.targetMicroseconds = config.targetMicroseconds; + rg_temporalFrameState.temporalFeedback = idMath::ClampFloat( + 0.0f, 0.98f, r_temporalAAFeedback.GetFloat() ); + rg_temporalFrameState.temporalReactiveScale = idMath::ClampFloat( + 0.0f, 2.0f, r_temporalAAReactiveScale.GetFloat() ); + rg_temporalFrameState.temporalDebugMode = idMath::ClampInt( + 0, 3, r_temporalAADebug.GetInteger() ); + rg_temporalFrameState.advancedScreenSpace = advancedScreenSpace; + rg_temporalFrameState.decision = output.decision; + + R_TemporalPresentation_RecordFrameScale( tr.frameCount, + gpuTiming.generation, output.scalePercent, !captureFrame ); + + const int debug = r_dynamicResolutionDebug.GetInteger(); + if ( debug >= 2 || ( debug >= 1 + && ( previousScale != output.scalePercent + || output.decision == TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET ) ) ) { + common->Printf( + "Temporal resolution: frame=%d decision=%s scale=%d%% scene=%dx%d output=%dx%d gpu=%lluus target=%lluus sampleFrame=%d sampleScale=%d%% age=%d generation=%u capture=%d\n", + rg_temporalFrameState.frameNumber, + R_TemporalPresentation_DecisionName( output.decision ), + output.scalePercent, output.width, output.height, + nativeWidth, nativeHeight, + gpuTiming.elapsedMicroseconds, config.targetMicroseconds, + gpuTiming.frameNumber, sample.scalePercent, sample.ageFrames, + gpuTiming.generation, captureFrame ? 1 : 0 ); + } +} + +const temporalPresentationFrameState_t &R_TemporalPresentation_GetFrameState( void ) { + return rg_temporalFrameState; +} + +int R_TemporalPresentation_EffectiveScreenFraction( void ) { + if ( !rg_temporalPresentationInitialized + || rg_temporalFrameState.effectiveScalePercent <= 0 ) { + return idMath::ClampInt( 10, 200, r_screenFraction.GetInteger() ); + } + return rg_temporalFrameState.effectiveScalePercent; +} + +bool R_TemporalPresentation_DynamicResolutionRequested( void ) { + return r_rendererDynamicResolution.GetBool(); +} + +bool R_TemporalPresentation_TemporalAARequested( void ) { + return r_temporalAA.GetBool(); +} + +bool R_TemporalPresentation_ScreenSpaceEffectsRequested( void ) { + return AdvancedScreenSpaceCore_Requested( + R_TemporalPresentation_BuildAdvancedScreenSpaceConfig() ); +} + +void R_TemporalPresentation_PrintStatus_f( const idCmdArgs &args ) { + (void)args; + const temporalPresentationFrameState_t &state = + R_TemporalPresentation_GetFrameState(); + common->Printf( + "Temporal presentation: dynamicRequested=%d dynamicActive=%d taaRequested=%d screenEffects=0x%x froxel={density=%.6f distance=%.0f slices=%d} ssr={intensity=%.2f distance=%.0f steps=%d} ssgi={intensity=%.2f taps=%d} frame=%d decision=%s manualScale=%d%% effectiveScale=%d%% scene=%dx%d output=%dx%d target=%lluus feedback=%.3f reactiveScale=%.3f debug=%d timingSupported=%d timingValid=%d timingBackend=%d timingFrame=%d timingScale=%d%% timingAge=%d timingGeneration=%u timing=%lluus captureFrozen=%d captureForcedNative=%d historyGeneration=%u historyReset=%s processed=%llu rejected=%llu drops=%llu raises=%llu resets=%llu\n", + state.dynamicResolutionRequested ? 1 : 0, + state.dynamicResolutionActive ? 1 : 0, + state.temporalAARequested ? 1 : 0, + state.advancedScreenSpace.effectMask, + state.advancedScreenSpace.froxelDensity, + state.advancedScreenSpace.froxelMaxDistance, + state.advancedScreenSpace.froxelSlices, + state.advancedScreenSpace.ssrIntensity, + state.advancedScreenSpace.ssrMaxDistance, + state.advancedScreenSpace.ssrSteps, + state.advancedScreenSpace.ssgiIntensity, + ADVANCED_SCREEN_SPACE_SSGI_TAPS, + state.frameNumber, + R_TemporalPresentation_DecisionName( state.decision ), + state.manualScalePercent, + state.effectiveScalePercent, + state.sceneWidth, state.sceneHeight, + state.nativeWidth, state.nativeHeight, + state.targetMicroseconds, + state.temporalFeedback, + state.temporalReactiveScale, + state.temporalDebugMode, + state.timingSupported ? 1 : 0, + state.timingValid ? 1 : 0, + state.timingBackend, + state.timingFrameNumber, + state.sampledScalePercent, + state.timingAgeFrames, + state.timingGeneration, + state.timingMicroseconds, + state.captureFrozen ? 1 : 0, + state.captureForcedNative ? 1 : 0, + rg_temporalHistoryGeneration, + rg_temporalHistoryResetReason.c_str(), + rg_temporalResolutionState.processedSamples, + rg_temporalResolutionState.rejectedSamples, + rg_temporalResolutionState.droppedScaleChanges, + rg_temporalResolutionState.raisedScaleChanges, + rg_temporalResolutionState.discontinuityResets ); +} diff --git a/src/renderer/TemporalPresentation.h b/src/renderer/TemporalPresentation.h new file mode 100644 index 00000000..f6ff30dc --- /dev/null +++ b/src/renderer/TemporalPresentation.h @@ -0,0 +1,81 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __TEMPORAL_PRESENTATION_H__ +#define __TEMPORAL_PRESENTATION_H__ + +#include "AdvancedScreenSpaceCore.h" +#include "TemporalPresentationCore.h" +#include "TemporalHistoryCore.h" + +class idCmdArgs; + +typedef struct temporalPresentationFrameState_s { + bool dynamicResolutionRequested; + bool dynamicResolutionActive; + bool temporalAARequested; + bool captureFrozen; + bool captureForcedNative; + bool timingSupported; + bool timingValid; + int frameNumber; + int nativeWidth; + int nativeHeight; + int sceneWidth; + int sceneHeight; + int manualScalePercent; + int effectiveScalePercent; + int sampledScalePercent; + int timingFrameNumber; + int timingAgeFrames; + int timingBackend; + unsigned int timingGeneration; + unsigned long long timingMicroseconds; + unsigned long long targetMicroseconds; + float temporalFeedback; + float temporalReactiveScale; + int temporalDebugMode; + advancedScreenSpaceConfig_t advancedScreenSpace; + temporalResolutionDecision_t decision; +} temporalPresentationFrameState_t; + +// Called once by the renderer front end after its frame and timing generation +// have advanced. The selected percentage remains immutable until the following +// BeginFrame, including synchronous save-preview command flushes. +void R_TemporalPresentation_BeginFrame( int nativeWidth, int nativeHeight, + bool captureFrame ); + +const temporalPresentationFrameState_t &R_TemporalPresentation_GetFrameState( void ); +int R_TemporalPresentation_EffectiveScreenFraction( void ); +bool R_TemporalPresentation_DynamicResolutionRequested( void ); +bool R_TemporalPresentation_TemporalAARequested( void ); +bool R_TemporalPresentation_ScreenSpaceEffectsRequested( void ); +const char *R_TemporalPresentation_DecisionName( temporalResolutionDecision_t decision ); + +// Temporal image histories use a generation separate from delayed GPU timing. +// Backends may invalidate it for a camera cut, view-identity change, resource +// loss, or another discontinuity without disturbing the timing controller. +unsigned int R_TemporalPresentation_HistoryGeneration( void ); +void R_TemporalPresentation_InvalidateHistory( const char *reason ); +const char *R_TemporalPresentation_LastHistoryResetReason( void ); + +// A save-preview readback can begin after BeginFrame and after the game has +// queued its scene. Mark the already-latched frame ineligible for delayed GPU +// feedback without changing its scale or timing generation, and advance the +// separate image-history generation so an untouched ping-pong destination +// cannot be sampled on the following gameplay frame. +void R_TemporalPresentation_MarkCurrentFrameCapture( const char *reason ); + +// Seals a deterministic per-view identity, camera-cut decision, and sub-pixel +// jitter before the projection matrix is built. Repeated calls for the same +// frame-allocated view are harmless. +void R_TemporalPresentation_PrepareView( struct viewDef_s *viewDef ); +void R_TemporalPresentation_FinalizeViewProjection( struct viewDef_s *viewDef ); +unsigned long long R_TemporalPresentation_ViewIdentity( + const struct viewDef_s *viewDef ); +const char *R_TemporalPresentation_HistoryResetReasonName( + temporalHistoryResetReason_t reason ); + +void R_TemporalPresentation_PrintStatus_f( const idCmdArgs &args ); + +#endif /* !__TEMPORAL_PRESENTATION_H__ */ diff --git a/src/renderer/TemporalPresentationCore.h b/src/renderer/TemporalPresentationCore.h new file mode 100644 index 00000000..f892dee6 --- /dev/null +++ b/src/renderer/TemporalPresentationCore.h @@ -0,0 +1,352 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __TEMPORAL_PRESENTATION_CORE_H__ +#define __TEMPORAL_PRESENTATION_CORE_H__ + +#include + +// Device-independent dynamic-resolution policy. The renderer feeds this only +// retired whole-frame GPU samples; no function in this contract can wait on a +// graphics API query or fence. Keeping the policy free of idlib, GL, and +// Vulkan types lets the native test exercise every transition deterministically. + +typedef enum temporalResolutionDecision_e { + TEMPORAL_RESOLUTION_DECISION_DISABLED = 0, + TEMPORAL_RESOLUTION_DECISION_FORCED_NATIVE, + TEMPORAL_RESOLUTION_DECISION_FROZEN, + TEMPORAL_RESOLUTION_DECISION_WAITING_FOR_SAMPLE, + TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET, + TEMPORAL_RESOLUTION_DECISION_HOLD, + TEMPORAL_RESOLUTION_DECISION_DROP, + TEMPORAL_RESOLUTION_DECISION_RAISE +} temporalResolutionDecision_t; + +typedef struct temporalResolutionConfig_s { + bool enabled; + int minimumScalePercent; + int maximumScalePercent; + int dropStepPercent; + int raiseStepPercent; + int raiseFrames; + int raiseThresholdPercent; + int missingSampleResetFrames; + int maximumSampleAgeFrames; + std::uint64_t targetMicroseconds; + int dimensionAlignment; +} temporalResolutionConfig_t; + +typedef struct temporalResolutionSample_s { + bool supported; + bool valid; + int backend; + int frameNumber; + std::uint32_t generation; + std::uint64_t elapsedMicroseconds; + int scalePercent; + int ageFrames; +} temporalResolutionSample_t; + +typedef struct temporalResolutionState_s { + bool initialized; + bool sampleIdentityValid; + int backend; + int lastSampleFrame; + std::uint32_t generation; + std::uint64_t configSignature; + int nativeWidth; + int nativeHeight; + int currentScalePercent; + int belowBudgetFrames; + int missingSampleFrames; + std::uint64_t processedSamples; + std::uint64_t rejectedSamples; + std::uint64_t droppedScaleChanges; + std::uint64_t raisedScaleChanges; + std::uint64_t discontinuityResets; +} temporalResolutionState_t; + +typedef struct temporalResolutionOutput_s { + temporalResolutionDecision_t decision; + bool sampleConsumed; + int scalePercent; + int width; + int height; +} temporalResolutionOutput_t; + +inline int TemporalResolutionCore_ClampInt( int minimum, int maximum, int value ) { + return value < minimum ? minimum : ( value > maximum ? maximum : value ); +} + +inline void TemporalResolutionCore_Initialize( temporalResolutionState_t &state ) { + state.initialized = false; + state.sampleIdentityValid = false; + state.backend = 0; + state.lastSampleFrame = -1; + state.generation = 0; + state.configSignature = 0; + state.nativeWidth = 0; + state.nativeHeight = 0; + state.currentScalePercent = 100; + state.belowBudgetFrames = 0; + state.missingSampleFrames = 0; + state.processedSamples = 0; + state.rejectedSamples = 0; + state.droppedScaleChanges = 0; + state.raisedScaleChanges = 0; + state.discontinuityResets = 0; +} + +inline std::uint64_t TemporalResolutionCore_MixSignature( + std::uint64_t signature, std::uint64_t value ) { + // A small deterministic combiner is sufficient here: this is change + // detection, not an externally supplied hash table key. + return signature ^ ( value + UINT64_C( 0x9e3779b97f4a7c15 ) + + ( signature << 6 ) + ( signature >> 2 ) ); +} + +inline std::uint64_t TemporalResolutionCore_ConfigSignature( + const temporalResolutionConfig_t &config ) { + std::uint64_t signature = UINT64_C( 0xcbf29ce484222325 ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.minimumScalePercent ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.maximumScalePercent ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.dropStepPercent ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.raiseStepPercent ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.raiseFrames ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.raiseThresholdPercent ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.missingSampleResetFrames ) ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.maximumSampleAgeFrames ) ); + signature = TemporalResolutionCore_MixSignature( signature, + config.targetMicroseconds ); + signature = TemporalResolutionCore_MixSignature( signature, + static_cast( config.dimensionAlignment ) ); + return signature; +} + +inline bool TemporalResolutionCore_SampleIsNewer( int candidate, int previous ) { + if ( previous < 0 ) { + return true; + } + const std::uint32_t delta = static_cast( candidate ) + - static_cast( previous ); + return delta != 0u && delta < UINT32_C( 0x80000000 ); +} + +inline std::uint64_t TemporalResolutionCore_PercentOf( + std::uint64_t value, int percent ) { + percent = TemporalResolutionCore_ClampInt( 0, 100, percent ); + // Splitting quotient and remainder avoids overflowing value * percent. + return ( value / 100u ) * static_cast( percent ) + + ( ( value % 100u ) * static_cast( percent ) ) / 100u; +} + +inline int TemporalResolutionCore_AlignedDimension( int nativeDimension, + int scalePercent, int alignment ) { + if ( nativeDimension <= 0 ) { + return 0; + } + // The controller's ceiling is the real output extent. Alignment is only a + // constraint on reduced scene targets; applying it at 100% would quietly + // turn common odd-sized windows (for example 1366 wide) into an upscale. + if ( scalePercent >= 100 ) { + return nativeDimension; + } + alignment = alignment < 1 ? 1 : alignment; + const std::int64_t scaledNumerator = + static_cast( nativeDimension ) * scalePercent; + int scaled = static_cast( ( scaledNumerator + 50 ) / 100 ); + scaled = scaled < 1 ? 1 : ( scaled > nativeDimension ? nativeDimension : scaled ); + if ( scaled >= alignment ) { + scaled -= scaled % alignment; + } + return scaled < 1 ? 1 : scaled; +} + +inline temporalResolutionOutput_t TemporalResolutionCore_Update( + temporalResolutionState_t &state, + const temporalResolutionConfig_t &requestedConfig, + const temporalResolutionSample_t &sample, + int nativeWidth, int nativeHeight, bool freezeScale, bool forceNative ) { + temporalResolutionConfig_t config = requestedConfig; + config.minimumScalePercent = TemporalResolutionCore_ClampInt( + 10, 100, config.minimumScalePercent ); + config.maximumScalePercent = TemporalResolutionCore_ClampInt( + config.minimumScalePercent, 100, config.maximumScalePercent ); + config.dropStepPercent = TemporalResolutionCore_ClampInt( + 1, 50, config.dropStepPercent ); + config.raiseStepPercent = TemporalResolutionCore_ClampInt( + 1, 25, config.raiseStepPercent ); + config.raiseFrames = TemporalResolutionCore_ClampInt( 1, 1000, config.raiseFrames ); + config.raiseThresholdPercent = TemporalResolutionCore_ClampInt( + 1, 99, config.raiseThresholdPercent ); + config.missingSampleResetFrames = TemporalResolutionCore_ClampInt( + 1, 1000000, config.missingSampleResetFrames ); + config.maximumSampleAgeFrames = TemporalResolutionCore_ClampInt( + 1, 1000000, config.maximumSampleAgeFrames ); + config.dimensionAlignment = TemporalResolutionCore_ClampInt( + 1, 256, config.dimensionAlignment ); + + temporalResolutionOutput_t output; + output.decision = TEMPORAL_RESOLUTION_DECISION_WAITING_FOR_SAMPLE; + output.sampleConsumed = false; + output.scalePercent = 100; + output.width = nativeWidth > 0 ? nativeWidth : 0; + output.height = nativeHeight > 0 ? nativeHeight : 0; + + if ( !config.enabled || config.targetMicroseconds == 0 ) { + state.initialized = false; + state.sampleIdentityValid = false; + state.currentScalePercent = config.maximumScalePercent; + state.belowBudgetFrames = 0; + state.missingSampleFrames = 0; + output.decision = TEMPORAL_RESOLUTION_DECISION_DISABLED; + output.scalePercent = state.currentScalePercent; + output.width = TemporalResolutionCore_AlignedDimension( + nativeWidth, output.scalePercent, config.dimensionAlignment ); + output.height = TemporalResolutionCore_AlignedDimension( + nativeHeight, output.scalePercent, config.dimensionAlignment ); + return output; + } + + if ( forceNative ) { + output.decision = TEMPORAL_RESOLUTION_DECISION_FORCED_NATIVE; + output.scalePercent = 100; + return output; + } + + if ( freezeScale && state.initialized ) { + output.decision = TEMPORAL_RESOLUTION_DECISION_FROZEN; + output.scalePercent = TemporalResolutionCore_ClampInt( + config.minimumScalePercent, config.maximumScalePercent, + state.currentScalePercent ); + output.width = TemporalResolutionCore_AlignedDimension( + nativeWidth, output.scalePercent, config.dimensionAlignment ); + output.height = TemporalResolutionCore_AlignedDimension( + nativeHeight, output.scalePercent, config.dimensionAlignment ); + return output; + } + + const std::uint64_t configSignature = + TemporalResolutionCore_ConfigSignature( config ); + const bool configurationChanged = state.initialized + && state.configSignature != configSignature; + const bool dimensionsChanged = state.initialized + && ( state.nativeWidth != nativeWidth || state.nativeHeight != nativeHeight ); + const bool identityChanged = state.sampleIdentityValid && sample.supported + && ( state.backend != sample.backend || state.generation != sample.generation ); + + if ( !state.initialized || configurationChanged || dimensionsChanged + || identityChanged ) { + state.initialized = true; + state.sampleIdentityValid = sample.supported; + state.backend = sample.supported ? sample.backend : 0; + state.generation = sample.supported ? sample.generation : 0; + state.lastSampleFrame = sample.supported && sample.valid + ? sample.frameNumber : -1; + state.configSignature = configSignature; + state.nativeWidth = nativeWidth; + state.nativeHeight = nativeHeight; + state.currentScalePercent = config.maximumScalePercent; + state.belowBudgetFrames = 0; + state.missingSampleFrames = 0; + state.discontinuityResets++; + output.decision = TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET; + } else if ( !state.sampleIdentityValid && sample.supported ) { + state.sampleIdentityValid = true; + state.backend = sample.backend; + state.generation = sample.generation; + state.lastSampleFrame = sample.valid ? sample.frameNumber : -1; + state.currentScalePercent = config.maximumScalePercent; + state.belowBudgetFrames = 0; + state.missingSampleFrames = 0; + state.discontinuityResets++; + output.decision = TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET; + } else if ( !sample.supported || !sample.valid + || sample.elapsedMicroseconds == 0 + || sample.scalePercent < config.minimumScalePercent + || sample.scalePercent > config.maximumScalePercent + || sample.ageFrames < 0 + || sample.ageFrames > config.maximumSampleAgeFrames + || !TemporalResolutionCore_SampleIsNewer( + sample.frameNumber, state.lastSampleFrame ) ) { + if ( sample.valid && state.sampleIdentityValid + && sample.frameNumber != state.lastSampleFrame ) { + state.rejectedSamples++; + } + state.missingSampleFrames++; + if ( state.missingSampleFrames >= config.missingSampleResetFrames ) { + state.currentScalePercent = config.maximumScalePercent; + state.belowBudgetFrames = 0; + state.missingSampleFrames = 0; + state.discontinuityResets++; + output.decision = TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET; + } else { + output.decision = TEMPORAL_RESOLUTION_DECISION_WAITING_FOR_SAMPLE; + } + } else { + state.lastSampleFrame = sample.frameNumber; + state.missingSampleFrames = 0; + state.processedSamples++; + output.sampleConsumed = true; + + const int previousScale = state.currentScalePercent; + if ( sample.elapsedMicroseconds > config.targetMicroseconds ) { + const int sampledScaleTarget = TemporalResolutionCore_ClampInt( + config.minimumScalePercent, config.maximumScalePercent, + sample.scalePercent - config.dropStepPercent ); + state.currentScalePercent = state.currentScalePercent < sampledScaleTarget + ? state.currentScalePercent : sampledScaleTarget; + state.belowBudgetFrames = 0; + if ( state.currentScalePercent < previousScale ) { + state.droppedScaleChanges++; + output.decision = TEMPORAL_RESOLUTION_DECISION_DROP; + } else { + output.decision = TEMPORAL_RESOLUTION_DECISION_HOLD; + } + } else { + const std::uint64_t raiseThreshold = TemporalResolutionCore_PercentOf( + config.targetMicroseconds, config.raiseThresholdPercent ); + if ( sample.elapsedMicroseconds < raiseThreshold + && sample.scalePercent == state.currentScalePercent ) { + state.belowBudgetFrames++; + if ( state.belowBudgetFrames >= config.raiseFrames ) { + state.belowBudgetFrames = 0; + state.currentScalePercent = TemporalResolutionCore_ClampInt( + config.minimumScalePercent, config.maximumScalePercent, + state.currentScalePercent + config.raiseStepPercent ); + if ( state.currentScalePercent > previousScale ) { + state.raisedScaleChanges++; + output.decision = TEMPORAL_RESOLUTION_DECISION_RAISE; + } else { + output.decision = TEMPORAL_RESOLUTION_DECISION_HOLD; + } + } else { + output.decision = TEMPORAL_RESOLUTION_DECISION_HOLD; + } + } else { + state.belowBudgetFrames = 0; + output.decision = TEMPORAL_RESOLUTION_DECISION_HOLD; + } + } + } + + state.currentScalePercent = TemporalResolutionCore_ClampInt( + config.minimumScalePercent, config.maximumScalePercent, + state.currentScalePercent ); + output.scalePercent = state.currentScalePercent; + output.width = TemporalResolutionCore_AlignedDimension( + nativeWidth, output.scalePercent, config.dimensionAlignment ); + output.height = TemporalResolutionCore_AlignedDimension( + nativeHeight, output.scalePercent, config.dimensionAlignment ); + return output; +} + +#endif /* !__TEMPORAL_PRESENTATION_CORE_H__ */ diff --git a/src/renderer/Vulkan/VulkanDevice.cpp b/src/renderer/Vulkan/VulkanDevice.cpp index 1c761eb4..b077e85e 100644 --- a/src/renderer/Vulkan/VulkanDevice.cpp +++ b/src/renderer/Vulkan/VulkanDevice.cpp @@ -40,6 +40,7 @@ #include "vk_mem_alloc.h" #include "VulkanDevice.h" +#include "../RendererMetrics.h" vkDeviceContext_t vkCtx; @@ -445,10 +446,16 @@ static bool VK_Device_CreateSwapchain( void ) { return false; } - // surface format: prefer BGRA8/UNORM sRGB-less for parity with the GL - // default framebuffer, fall back to the first reported format + // The legacy renderer already produces display-coded SDR values. Use a + // non-sRGB UNORM attachment with the standard nonlinear presentation colour + // space so attachment writes do not encode those values a second time. uint32_t formatCount = 0; - vkGetPhysicalDeviceSurfaceFormatsKHR( vkCtx.physicalDevice, vkCtx.surface, &formatCount, NULL ); + VkResult formatResult = vkGetPhysicalDeviceSurfaceFormatsKHR( + vkCtx.physicalDevice, vkCtx.surface, &formatCount, NULL ); + if ( formatResult != VK_SUCCESS ) { + common->Warning( "Vulkan: surface-format count query failed (%d)", (int)formatResult ); + return false; + } if ( formatCount == 0 ) { common->Warning( "Vulkan: surface reports no formats" ); return false; @@ -457,15 +464,39 @@ static bool VK_Device_CreateSwapchain( void ) { formatCount = 64; } VkSurfaceFormatKHR formats[ 64 ]; - vkGetPhysicalDeviceSurfaceFormatsKHR( vkCtx.physicalDevice, vkCtx.surface, &formatCount, formats ); - VkSurfaceFormatKHR chosen = formats[ 0 ]; + formatResult = vkGetPhysicalDeviceSurfaceFormatsKHR( + vkCtx.physicalDevice, vkCtx.surface, &formatCount, formats ); + if ( formatResult != VK_SUCCESS && formatResult != VK_INCOMPLETE ) { + common->Warning( "Vulkan: surface-format query failed (%d)", (int)formatResult ); + return false; + } + VkSurfaceFormatKHR chosen; + memset( &chosen, 0, sizeof( chosen ) ); + chosen.format = VK_FORMAT_UNDEFINED; + bool compatibleSurfaceFormat = false; for ( uint32_t i = 0; i < formatCount; i++ ) { if ( ( formats[ i ].format == VK_FORMAT_B8G8R8A8_UNORM || formats[ i ].format == VK_FORMAT_R8G8B8A8_UNORM ) && formats[ i ].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ) { chosen = formats[ i ]; + compatibleSurfaceFormat = true; break; } } + if ( !compatibleSurfaceFormat ) { + for ( uint32_t i = 0; i < formatCount; i++ ) { + if ( formats[ i ].format == VK_FORMAT_UNDEFINED + && formats[ i ].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ) { + chosen.format = VK_FORMAT_B8G8R8A8_UNORM; + chosen.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + compatibleSurfaceFormat = true; + break; + } + } + } + if ( !compatibleSurfaceFormat ) { + common->Warning( "Vulkan: surface has no compatible legacy SDR UNORM + SRGB_NONLINEAR format" ); + return false; + } // present mode from r_swapInterval: 0 = IMMEDIATE (or MAILBOX when // IMMEDIATE is absent), else FIFO (always available) @@ -565,11 +596,26 @@ static bool VK_Device_CreateSwapchain( void ) { vkCtx.swapchainTransferSrc = transferSrcSupported; uint32_t count = 0; - vkGetSwapchainImagesKHR( vkCtx.device, vkCtx.swapchain, &count, NULL ); + if ( vkGetSwapchainImagesKHR( vkCtx.device, vkCtx.swapchain, &count, NULL ) != VK_SUCCESS || count == 0 ) { + common->Warning( "Vulkan: swapchain image count query failed" ); + VK_Device_DestroySwapchainObjects(); + return false; + } if ( count > 8 ) { - count = 8; + // swapchainImages/Views/renderFinishedSemaphores are fixed at 8 and are + // indexed by the image index vkAcquireNextImageKHR returns, which can be + // any value below the actual image count. An implementation that creates + // more than requested would index them out of bounds, so fail closed + // rather than clamp the count while the driver still owns more images. + common->Warning( "Vulkan: swapchain created %u images, exceeding the supported maximum of 8", count ); + VK_Device_DestroySwapchainObjects(); + return false; + } + if ( vkGetSwapchainImagesKHR( vkCtx.device, vkCtx.swapchain, &count, vkCtx.swapchainImages ) != VK_SUCCESS ) { + common->Warning( "Vulkan: swapchain image retrieval failed" ); + VK_Device_DestroySwapchainObjects(); + return false; } - vkGetSwapchainImagesKHR( vkCtx.device, vkCtx.swapchain, &count, vkCtx.swapchainImages ); vkCtx.swapchainImageCount = count; for ( uint32_t i = 0; i < count; i++ ) { @@ -605,8 +651,9 @@ static bool VK_Device_CreateSwapchain( void ) { return false; } - common->Printf( "Vulkan: created swapchain %ux%u format=%d images=%u presentMode=%d\n", - extent.width, extent.height, (int)chosen.format, count, (int)presentMode ); + common->Printf( "Vulkan: created swapchain %ux%u format=%d colorSpace=%d images=%u presentMode=%d\n", + extent.width, extent.height, (int)chosen.format, (int)chosen.colorSpace, + count, (int)presentMode ); if ( !vkCtx.swapchainTransferSrc ) { common->Warning( "Vulkan: swapchain does not support transfer-source captures; screenshots and backbuffer feedback are unavailable" ); } @@ -623,6 +670,7 @@ bool VK_Device_RecreateSwapchain( void ) { return false; } vkDeviceWaitIdle( vkCtx.device ); + R_RendererMetrics_ResetGpuFrameTiming( "Vulkan swapchain recreation" ); return VK_Device_CreateSwapchain(); } @@ -748,6 +796,7 @@ bool VK_Device_Init( const renderWindowServices_s *windowServices ) { const int forcedDevice = r_vkDevice.GetInteger(); int chosenDevice = -1; uint32_t chosenQueueFamily = 0; + uint32_t chosenTimestampValidBits = 0; for ( uint32_t d = 0; d < deviceCount; d++ ) { if ( forcedDevice >= 0 && (int)d != forcedDevice ) { @@ -769,6 +818,7 @@ bool VK_Device_Init( const renderWindowServices_s *windowServices ) { if ( presentable ) { chosenDevice = (int)d; chosenQueueFamily = f; + chosenTimestampValidBits = families[ f ].timestampValidBits; break; } } @@ -784,9 +834,11 @@ bool VK_Device_Init( const renderWindowServices_s *windowServices ) { } vkCtx.physicalDevice = devices[ chosenDevice ]; vkCtx.graphicsQueueFamily = chosenQueueFamily; + vkCtx.graphicsTimestampValidBits = chosenTimestampValidBits; vkGetPhysicalDeviceProperties( vkCtx.physicalDevice, &vkCtx.deviceProperties ); - common->Printf( "Vulkan: device %d '%s' (queue family %u)\n", - chosenDevice, vkCtx.deviceProperties.deviceName, chosenQueueFamily ); + common->Printf( "Vulkan: device %d '%s' (queue family %u, timestampValidBits=%u, timestampPeriod=%.6fns)\n", + chosenDevice, vkCtx.deviceProperties.deviceName, chosenQueueFamily, + chosenTimestampValidBits, vkCtx.deviceProperties.limits.timestampPeriod ); // Hard API floor. The back end calls ~135 core-1.3 entry points // (vkCmdBeginRendering, vkCmdPipelineBarrier2, vkQueueSubmit2 and the @@ -1035,6 +1087,24 @@ void VK_Device_Shutdown( void ) { if ( vkCtx.device != VK_NULL_HANDLE ) { vkDeviceWaitIdle( vkCtx.device ); } + if ( vkCtx.device != VK_NULL_HANDLE && vkCtx.allocator != NULL ) { + // an open batch is abandoned, never submitted: the images it records + // into are torn down below, and the command buffer dies with the pool + for ( int i = 0; i < vkCtx.numUploadBatchPending; i++ ) { + vmaDestroyBuffer( vkCtx.allocator, vkCtx.uploadBatchPendingBuffers[ i ], + vkCtx.uploadBatchPendingAllocations[ i ] ); + } + vkCtx.numUploadBatchPending = 0; + vkCtx.uploadBatchPendingBytes = 0; + vkCtx.uploadBatchOpen = false; + // the wait-idle above already retired any submitted batch + for ( int i = 0; i < vkCtx.numUploadBatchInFlight; i++ ) { + vmaDestroyBuffer( vkCtx.allocator, vkCtx.uploadBatchInFlightBuffers[ i ], + vkCtx.uploadBatchInFlightAllocations[ i ] ); + } + vkCtx.numUploadBatchInFlight = 0; + vkCtx.uploadBatchInFlight = false; + } if ( vkCtx.device != VK_NULL_HANDLE ) { // release the D-layer consumers first (images, executor, buffers) { @@ -1102,6 +1172,9 @@ void VK_Device_PresentClearFrame( const float clearColor[ 4 ] ) { vkCtx.recordingSlot = slot; vkWaitForFences( vkCtx.device, 1, &vkCtx.frameFences[ slot ], VK_TRUE, UINT64_MAX ); + // deferred destroys must never run while a submitted upload batch could + // still reference their images + VK_Device_WaitUploadBatch(); VK_Device_FlushDeferredDestroys( slot ); uint32_t imageIndex = 0; @@ -1219,6 +1292,9 @@ void VK_Device_PresentClearFrame( const float clearColor[ 4 ] ) { si.signalSemaphoreInfoCount = 1; si.pSignalSemaphoreInfos = &signalInfo; + // the upload batch must precede any frame submission in queue order so + // this frame's fence covers it + VK_Device_FlushUploadBatch(); vkQueueSubmit2( vkCtx.graphicsQueue, 1, &si, vkCtx.frameFences[ slot ] ); VkPresentInfoKHR pi; @@ -1238,24 +1314,33 @@ void VK_Device_PresentClearFrame( const float clearColor[ 4 ] ) { /* ==================== -VK_Device_ImmediateSubmit +VK_Device_WaitUploadBatch / VK_Device_FlushUploadBatch / VK_Device_BatchedUpload ==================== */ -bool VK_Device_ImmediateSubmit( vkImmediateRecord_t record, void *user ) { - if ( vkCtx.device == VK_NULL_HANDLE || vkCtx.uploadCommandBuffer == VK_NULL_HANDLE ) { - return false; - } - vkResetCommandBuffer( vkCtx.uploadCommandBuffer, 0 ); - VkCommandBufferBeginInfo cbbi; - memset( &cbbi, 0, sizeof( cbbi ) ); - cbbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - cbbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer( vkCtx.uploadCommandBuffer, &cbbi ); +// staging bytes a single upload batch may own before it is force-flushed +static const VkDeviceSize VK_UPLOAD_BATCH_BYTE_BUDGET = 64u << 20; - record( vkCtx.uploadCommandBuffer, user ); +void VK_Device_WaitUploadBatch( void ) { + if ( !vkCtx.uploadBatchInFlight ) { + return; + } + vkWaitForFences( vkCtx.device, 1, &vkCtx.uploadFence, VK_TRUE, UINT64_MAX ); + vkResetFences( vkCtx.device, 1, &vkCtx.uploadFence ); + for ( int i = 0; i < vkCtx.numUploadBatchInFlight; i++ ) { + vmaDestroyBuffer( vkCtx.allocator, vkCtx.uploadBatchInFlightBuffers[ i ], + vkCtx.uploadBatchInFlightAllocations[ i ] ); + } + vkCtx.numUploadBatchInFlight = 0; + vkCtx.uploadBatchInFlight = false; +} +void VK_Device_FlushUploadBatch( void ) { + if ( !vkCtx.uploadBatchOpen ) { + return; + } vkEndCommandBuffer( vkCtx.uploadCommandBuffer ); + vkCtx.uploadBatchOpen = false; VkSubmitInfo si; memset( &si, 0, sizeof( si ) ); @@ -1263,10 +1348,59 @@ bool VK_Device_ImmediateSubmit( vkImmediateRecord_t record, void *user ) { si.commandBufferCount = 1; si.pCommandBuffers = &vkCtx.uploadCommandBuffer; if ( vkQueueSubmit( vkCtx.graphicsQueue, 1, &si, vkCtx.uploadFence ) != VK_SUCCESS ) { + common->Warning( "Vulkan: upload batch submit failed (%d staged regions dropped)", + vkCtx.numUploadBatchPending ); + for ( int i = 0; i < vkCtx.numUploadBatchPending; i++ ) { + vmaDestroyBuffer( vkCtx.allocator, vkCtx.uploadBatchPendingBuffers[ i ], + vkCtx.uploadBatchPendingAllocations[ i ] ); + } + vkCtx.numUploadBatchPending = 0; + vkCtx.uploadBatchPendingBytes = 0; + return; + } + // opening a batch waits out the previous one first, so the in-flight list + // is always empty here + for ( int i = 0; i < vkCtx.numUploadBatchPending; i++ ) { + vkCtx.uploadBatchInFlightBuffers[ i ] = vkCtx.uploadBatchPendingBuffers[ i ]; + vkCtx.uploadBatchInFlightAllocations[ i ] = vkCtx.uploadBatchPendingAllocations[ i ]; + } + vkCtx.numUploadBatchInFlight = vkCtx.numUploadBatchPending; + vkCtx.numUploadBatchPending = 0; + vkCtx.uploadBatchPendingBytes = 0; + vkCtx.uploadBatchInFlight = true; +} + +bool VK_Device_BatchedUpload( vkImmediateRecord_t record, void *user, + VkBuffer staging, VmaAllocation stagingAllocation, VkDeviceSize stagingBytes ) { + if ( vkCtx.device == VK_NULL_HANDLE || vkCtx.uploadCommandBuffer == VK_NULL_HANDLE ) { return false; } - vkWaitForFences( vkCtx.device, 1, &vkCtx.uploadFence, VK_TRUE, UINT64_MAX ); - vkResetFences( vkCtx.device, 1, &vkCtx.uploadFence ); + + if ( !vkCtx.uploadBatchOpen ) { + VK_Device_WaitUploadBatch(); + vkResetCommandBuffer( vkCtx.uploadCommandBuffer, 0 ); + VkCommandBufferBeginInfo cbbi; + memset( &cbbi, 0, sizeof( cbbi ) ); + cbbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + cbbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if ( vkBeginCommandBuffer( vkCtx.uploadCommandBuffer, &cbbi ) != VK_SUCCESS ) { + return false; + } + vkCtx.uploadBatchOpen = true; + } + + record( vkCtx.uploadCommandBuffer, user ); + + if ( staging != VK_NULL_HANDLE ) { + vkCtx.uploadBatchPendingBuffers[ vkCtx.numUploadBatchPending ] = staging; + vkCtx.uploadBatchPendingAllocations[ vkCtx.numUploadBatchPending ] = stagingAllocation; + vkCtx.numUploadBatchPending++; + vkCtx.uploadBatchPendingBytes += stagingBytes; + } + if ( vkCtx.numUploadBatchPending >= VK_MAX_UPLOAD_BATCH_STAGING + || vkCtx.uploadBatchPendingBytes >= VK_UPLOAD_BATCH_BYTE_BUDGET ) { + VK_Device_FlushUploadBatch(); + } return true; } @@ -1283,7 +1417,12 @@ void VK_Device_DeferDestroy( VkImage image, VkImageView view, VkBuffer buffer, V } const int slot = vkCtx.recordingSlot; if ( vkCtx.numDeferredDestroys[ slot ] >= VK_MAX_DEFERRED_DESTROYS ) { - // queue full: block for safety rather than leak or free early + // queue full: block for safety rather than leak or free early. An open + // unsubmitted upload batch must be submitted first: destroying a + // resource its recorded commands reference would invalidate the + // command buffer, and wait-idle only retires submitted work. + VK_Device_FlushUploadBatch(); + VK_Device_WaitUploadBatch(); vkDeviceWaitIdle( vkCtx.device ); for ( int i = 0; i < VK_FRAMES_IN_FLIGHT; i++ ) { VK_Device_FlushDeferredDestroys( i ); diff --git a/src/renderer/Vulkan/VulkanDevice.h b/src/renderer/Vulkan/VulkanDevice.h index 15551035..a3689341 100644 --- a/src/renderer/Vulkan/VulkanDevice.h +++ b/src/renderer/Vulkan/VulkanDevice.h @@ -51,6 +51,9 @@ typedef struct vkDeferredDestroy_s { static const int VK_MAX_DEFERRED_DESTROYS = 512; +// staging buffers a single upload batch may own before it is force-flushed +static const int VK_MAX_UPLOAD_BATCH_STAGING = 1024; + typedef struct vkDeviceContext_s { bool initialized; @@ -75,6 +78,7 @@ typedef struct vkDeviceContext_s { bool packed565Supported; VkDevice device; uint32_t graphicsQueueFamily; // also the present family (required) + uint32_t graphicsTimestampValidBits; VkQueue graphicsQueue; VkSwapchainKHR swapchain; @@ -121,11 +125,21 @@ typedef struct vkDeviceContext_s { VkImageView depthViews[ VK_FRAMES_IN_FLIGHT ]; VmaAllocation depthAllocations[ VK_FRAMES_IN_FLIGHT ]; - // synchronous upload path: its own command buffer + fence, submitted and - // waited immediately (image/vertex data reaches the GPU before the frame - // that samples it is submitted) + // batched upload path: uploads record into uploadCommandBuffer; the batch + // is submitted before every frame/clear-frame submission (so consuming + // frames execute after their uploads), when the staging budget fills, or + // with a CPU wait at wait-idle teardown points VkCommandBuffer uploadCommandBuffer; VkFence uploadFence; + bool uploadBatchOpen; // commands recorded, not yet submitted + bool uploadBatchInFlight; // submitted, uploadFence not yet waited + int numUploadBatchPending; + VkDeviceSize uploadBatchPendingBytes; + VkBuffer uploadBatchPendingBuffers[ VK_MAX_UPLOAD_BATCH_STAGING ]; + VmaAllocation uploadBatchPendingAllocations[ VK_MAX_UPLOAD_BATCH_STAGING ]; + int numUploadBatchInFlight; + VkBuffer uploadBatchInFlightBuffers[ VK_MAX_UPLOAD_BATCH_STAGING ]; + VmaAllocation uploadBatchInFlightAllocations[ VK_MAX_UPLOAD_BATCH_STAGING ]; vkDeferredDestroy_t deferredDestroys[ VK_FRAMES_IN_FLIGHT ][ VK_MAX_DEFERRED_DESTROYS ]; int numDeferredDestroys[ VK_FRAMES_IN_FLIGHT ]; @@ -149,11 +163,20 @@ bool VK_Device_RecreateSwapchain( void ); // presents; handles OUT_OF_DATE/SUBOPTIMAL by recreating and retrying once void VK_Device_PresentClearFrame( const float clearColor[ 4 ] ); -// records commands into the dedicated upload command buffer, submits, and -// blocks on the upload fence; safe mid-frame (the frame's own command -// buffer is still recording, so the upload strictly precedes its submit) typedef void ( *vkImmediateRecord_t )( VkCommandBuffer cmd, void *user ); -bool VK_Device_ImmediateSubmit( vkImmediateRecord_t record, void *user ); +// records upload commands into the shared upload command buffer, opening a +// batch if none is open (recycling any in-flight batch first). On true the +// batch takes ownership of the staging buffer/allocation; the commands reach +// the GPU before the next frame or clear-frame submission. Returns false +// without recording when no device/upload command buffer exists. +bool VK_Device_BatchedUpload( vkImmediateRecord_t record, void *user, + VkBuffer staging, VmaAllocation stagingAllocation, VkDeviceSize stagingBytes ); +// submits the open upload batch (if any) without a CPU wait; must run before +// every queue submission so consuming work executes after its uploads +void VK_Device_FlushUploadBatch( void ); +// waits out the last submitted batch and releases its staging buffers; must +// run before deferred destroys are flushed and before wait-idle teardown +void VK_Device_WaitUploadBatch( void ); // queues GPU objects for destruction once the current frame slot's fence // has cycled; any handle may be VK_NULL_HANDLE diff --git a/src/renderer/Vulkan/VulkanGpuFrameTiming.cpp b/src/renderer/Vulkan/VulkanGpuFrameTiming.cpp new file mode 100644 index 00000000..f76eb51c --- /dev/null +++ b/src/renderer/Vulkan/VulkanGpuFrameTiming.cpp @@ -0,0 +1,197 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifdef OPENQ4_RENDERER_VK_MODULE + +#include "../../idlib/precompiled.h" +#pragma hdrstop + +#include "../tr_local.h" +#include "../GpuFrameTimingCore.h" +#include "../RendererMetrics.h" +#include "VulkanGpuFrameTiming.h" + +typedef struct vkGpuFrameTimingSlot_s { + bool open; + bool pending; + int frameNumber; + unsigned int generation; +} vkGpuFrameTimingSlot_t; + +static VkQueryPool vkGpuTimingQueryPool = VK_NULL_HANDLE; +static VkDevice vkGpuTimingOwnerDevice = VK_NULL_HANDLE; +static vkGpuFrameTimingSlot_t vkGpuTimingSlots[ VK_FRAMES_IN_FLIGHT ]; +static bool vkGpuTimingEnableStateKnown = false; +static bool vkGpuTimingEnabledLastFrame = false; + +bool VK_GpuFrameTiming_Available( void ) { + return vkCtx.initialized + && vkCtx.device != VK_NULL_HANDLE + && vkCtx.graphicsTimestampValidBits > 0 + && vkCtx.deviceProperties.limits.timestampPeriod > 0.0f + && vkCreateQueryPool != NULL + && vkDestroyQueryPool != NULL + && vkGetQueryPoolResults != NULL + && vkCmdResetQueryPool != NULL + && vkCmdWriteTimestamp != NULL; +} + +static bool VK_GpuFrameTiming_EnsureQueryPool( void ) { + if ( vkGpuTimingQueryPool != VK_NULL_HANDLE && vkGpuTimingOwnerDevice == vkCtx.device ) { + return true; + } + if ( vkGpuTimingOwnerDevice != VK_NULL_HANDLE && vkGpuTimingOwnerDevice != vkCtx.device ) { + // The old device is already gone. Its non-dispatchable query-pool handle + // must not be presented to the replacement device. + vkGpuTimingQueryPool = VK_NULL_HANDLE; + memset( vkGpuTimingSlots, 0, sizeof( vkGpuTimingSlots ) ); + R_RendererMetrics_ResetGpuFrameTiming( "Vulkan device generation changed" ); + } + + VkQueryPoolCreateInfo createInfo; + memset( &createInfo, 0, sizeof( createInfo ) ); + createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + createInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + createInfo.queryCount = VK_FRAMES_IN_FLIGHT * 2; + if ( vkCreateQueryPool( vkCtx.device, &createInfo, NULL, &vkGpuTimingQueryPool ) != VK_SUCCESS ) { + vkGpuTimingQueryPool = VK_NULL_HANDLE; + vkGpuTimingOwnerDevice = VK_NULL_HANDLE; + return false; + } + vkGpuTimingOwnerDevice = vkCtx.device; + return true; +} + +static void VK_GpuFrameTiming_ResolveRetiredSlot( int frameSlot, int currentFrameNumber ) { + vkGpuFrameTimingSlot_t &slot = vkGpuTimingSlots[ frameSlot ]; + if ( !slot.pending ) { + return; + } + + std::uint64_t timestamps[ 2 ] = { 0, 0 }; + const VkResult result = vkGetQueryPoolResults( vkCtx.device, vkGpuTimingQueryPool, + static_cast( frameSlot * 2 ), 2, sizeof( timestamps ), timestamps, + sizeof( timestamps[ 0 ] ), VK_QUERY_RESULT_64_BIT ); + // Deliberately no VK_QUERY_RESULT_WAIT_BIT: this function is reached only + // after the normal frame-slot fence has retired, but readiness is still + // treated as fallible and never escalated into an extra wait. + if ( result == VK_NOT_READY ) { + R_RendererMetrics_RecordGpuFrameTimingUnavailable(); + R_RendererMetrics_RecordGpuFrameTimingDropped(); + slot.pending = false; + return; + } + if ( result != VK_SUCCESS ) { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + slot.pending = false; + return; + } + + const std::uint64_t ticks = GpuFrameTimingCore_TimestampDelta( + timestamps[ 0 ], timestamps[ 1 ], vkCtx.graphicsTimestampValidBits ); + const std::uint64_t elapsedMicroseconds = GpuFrameTimingCore_TicksToMicroseconds( + ticks, static_cast( vkCtx.deviceProperties.limits.timestampPeriod ) ); + R_RendererMetrics_RecordGpuFrameTimingResolved( RENDER_GPU_TIMING_BACKEND_VULKAN, + slot.frameNumber, slot.generation, elapsedMicroseconds, currentFrameNumber ); + slot.pending = false; +} + +void VK_GpuFrameTiming_BeginFrame( VkCommandBuffer commandBuffer, int frameSlot, + int frameNumber ) { + const bool supported = VK_GpuFrameTiming_Available(); + R_RendererMetrics_SetGpuFrameTimingBackend( RENDER_GPU_TIMING_BACKEND_VULKAN, supported ); + const bool enabled = supported + && ( r_rendererGpuTimers.GetBool() + || r_rendererDynamicResolution.GetBool() ); + if ( vkGpuTimingEnableStateKnown && enabled != vkGpuTimingEnabledLastFrame ) { + R_RendererMetrics_ResetGpuFrameTiming( enabled + ? "Vulkan whole-frame timing enabled" : "Vulkan whole-frame timing disabled" ); + } + vkGpuTimingEnableStateKnown = true; + vkGpuTimingEnabledLastFrame = enabled; + + if ( !enabled || commandBuffer == VK_NULL_HANDLE || frameSlot < 0 + || frameSlot >= VK_FRAMES_IN_FLIGHT ) { + return; + } + if ( !VK_GpuFrameTiming_EnsureQueryPool() ) { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + return; + } + + VK_GpuFrameTiming_ResolveRetiredSlot( frameSlot, frameNumber ); + vkGpuFrameTimingSlot_t &slot = vkGpuTimingSlots[ frameSlot ]; + const std::uint32_t firstQuery = static_cast( frameSlot * 2 ); + vkCmdResetQueryPool( commandBuffer, vkGpuTimingQueryPool, firstQuery, 2 ); + vkCmdWriteTimestamp( commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + vkGpuTimingQueryPool, firstQuery ); + slot.open = true; + slot.pending = false; + slot.frameNumber = frameNumber; + slot.generation = R_RendererMetrics_GpuFrameTimingGeneration(); +} + +void VK_GpuFrameTiming_EndFrame( VkCommandBuffer commandBuffer, int frameSlot ) { + if ( vkGpuTimingQueryPool == VK_NULL_HANDLE || commandBuffer == VK_NULL_HANDLE + || frameSlot < 0 || frameSlot >= VK_FRAMES_IN_FLIGHT ) { + return; + } + vkGpuFrameTimingSlot_t &slot = vkGpuTimingSlots[ frameSlot ]; + if ( !slot.open ) { + return; + } + vkCmdWriteTimestamp( commandBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + vkGpuTimingQueryPool, static_cast( frameSlot * 2 + 1 ) ); + slot.open = false; + slot.pending = true; +} + +void VK_GpuFrameTiming_SubmitFailed( int frameSlot ) { + if ( frameSlot < 0 || frameSlot >= VK_FRAMES_IN_FLIGHT ) { + return; + } + vkGpuFrameTimingSlot_t &slot = vkGpuTimingSlots[ frameSlot ]; + if ( slot.open || slot.pending ) { + R_RendererMetrics_RecordGpuFrameTimingDropped(); + } + slot.open = false; + slot.pending = false; +} + +void VK_GpuFrameTiming_Shutdown( void ) { + if ( vkGpuTimingQueryPool != VK_NULL_HANDLE && vkGpuTimingOwnerDevice != VK_NULL_HANDLE ) { + // Shutdown/vid_restart already accepts device retirement. Keeping the wait + // here prevents destroying a pool referenced by an in-flight command + // buffer; normal frame sampling never calls a device/queue wait. + vkDeviceWaitIdle( vkGpuTimingOwnerDevice ); + vkDestroyQueryPool( vkGpuTimingOwnerDevice, vkGpuTimingQueryPool, NULL ); + } + vkGpuTimingQueryPool = VK_NULL_HANDLE; + vkGpuTimingOwnerDevice = VK_NULL_HANDLE; + memset( vkGpuTimingSlots, 0, sizeof( vkGpuTimingSlots ) ); + vkGpuTimingEnableStateKnown = false; + vkGpuTimingEnabledLastFrame = false; +} + +bool VK_GpuFrameTiming_RunSelfTest( void ) { + if ( !VK_GpuFrameTiming_Available() ) { + common->Printf( "RendererGpuTimer self-test skipped: Vulkan timestamps are unavailable\n" ); + return true; + } + VkQueryPoolCreateInfo createInfo; + memset( &createInfo, 0, sizeof( createInfo ) ); + createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + createInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + createInfo.queryCount = 2; + VkQueryPool testPool = VK_NULL_HANDLE; + if ( vkCreateQueryPool( vkCtx.device, &createInfo, NULL, &testPool ) != VK_SUCCESS ) { + common->Printf( "RendererGpuTimer self-test failed: Vulkan query-pool creation failed\n" ); + return false; + } + vkDestroyQueryPool( vkCtx.device, testPool, NULL ); + common->Printf( "RendererGpuTimer self-test passed (Vulkan timestampPeriod=%.6fns validBits=%u waits=0)\n", + vkCtx.deviceProperties.limits.timestampPeriod, vkCtx.graphicsTimestampValidBits ); + return true; +} + +#endif /* OPENQ4_RENDERER_VK_MODULE */ diff --git a/src/renderer/Vulkan/VulkanGpuFrameTiming.h b/src/renderer/Vulkan/VulkanGpuFrameTiming.h new file mode 100644 index 00000000..a472adee --- /dev/null +++ b/src/renderer/Vulkan/VulkanGpuFrameTiming.h @@ -0,0 +1,17 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#ifndef __VULKAN_GPU_FRAME_TIMING_H__ +#define __VULKAN_GPU_FRAME_TIMING_H__ + +#include "VulkanDevice.h" + +bool VK_GpuFrameTiming_Available( void ); +void VK_GpuFrameTiming_BeginFrame( VkCommandBuffer commandBuffer, int frameSlot, + int frameNumber ); +void VK_GpuFrameTiming_EndFrame( VkCommandBuffer commandBuffer, int frameSlot ); +void VK_GpuFrameTiming_SubmitFailed( int frameSlot ); +void VK_GpuFrameTiming_Shutdown( void ); +bool VK_GpuFrameTiming_RunSelfTest( void ); + +#endif /* !__VULKAN_GPU_FRAME_TIMING_H__ */ diff --git a/src/renderer/Vulkan/shaders/gpu_skinning.comp b/src/renderer/Vulkan/shaders/gpu_skinning.comp new file mode 100644 index 00000000..5611dccc --- /dev/null +++ b/src/renderer/Vulkan/shaders/gpu_skinning.comp @@ -0,0 +1,108 @@ +#version 450 + +// Backend-neutral GPU skinning contract: +// - idDrawVert is sixteen 32-bit words (64 bytes) +// - gpuSkinningVertex_t is four uint joint indices followed by four floats +// - each joint is three row-major vec4 rows (twelve floats) +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(std430, set = 0, binding = 0) readonly buffer SourceWords { + uint words[]; +} sourceData; + +layout(std430, set = 0, binding = 1) readonly buffer SkinWords { + uint words[]; +} skinData; + +layout(std430, set = 0, binding = 2) readonly buffer JointWords { + uint words[]; +} jointData; + +layout(std430, set = 0, binding = 3) writeonly buffer OutputWords { + uint words[]; +} outputData; + +layout(push_constant) uniform SkinningPushConstants { + uint sourceWordOffset; + uint skinWordOffset; + uint jointWordOffset; + uint outputWordOffset; + uint vertexCount; + uint jointCount; +} skinning; + +vec4 LoadJointRow( uint jointIndex, uint row ) { + uint base = skinning.jointWordOffset + jointIndex * 12u + row * 4u; + return vec4( + uintBitsToFloat( jointData.words[ base + 0u ] ), + uintBitsToFloat( jointData.words[ base + 1u ] ), + uintBitsToFloat( jointData.words[ base + 2u ] ), + uintBitsToFloat( jointData.words[ base + 3u ] ) ); +} + +vec3 TransformBasis( vec4 row0, vec4 row1, vec4 row2, vec3 value, float w ) { + vec4 source = vec4( value, w ); + return vec3( dot( row0, source ), dot( row1, source ), dot( row2, source ) ); +} + +void StoreVec3( uint base, uint member, vec3 value ) { + outputData.words[ base + member + 0u ] = floatBitsToUint( value.x ); + outputData.words[ base + member + 1u ] = floatBitsToUint( value.y ); + outputData.words[ base + member + 2u ] = floatBitsToUint( value.z ); +} + +void main() { + uint vertex = gl_GlobalInvocationID.x; + if ( vertex >= skinning.vertexCount ) { + return; + } + + uint sourceBase = skinning.sourceWordOffset + vertex * 16u; + uint outputBase = skinning.outputWordOffset + vertex * 16u; + for ( uint word = 0u; word < 16u; ++word ) { + outputData.words[ outputBase + word ] = sourceData.words[ sourceBase + word ]; + } + + uint skinBase = skinning.skinWordOffset + vertex * 8u; + vec4 row0 = vec4( 0.0 ); + vec4 row1 = vec4( 0.0 ); + vec4 row2 = vec4( 0.0 ); + for ( uint influence = 0u; influence < 4u; ++influence ) { + float weight = uintBitsToFloat( skinData.words[ skinBase + 4u + influence ] ); + if ( weight == 0.0 ) { + continue; + } + uint jointIndex = skinData.words[ skinBase + influence ]; + // The CPU contract rejects this before dispatch. Retain a shader-side + // range guard so malformed data can never index outside the palette; + // the already-copied bind pose is the fail-closed output. + if ( jointIndex >= skinning.jointCount ) { + return; + } + row0 += LoadJointRow( jointIndex, 0u ) * weight; + row1 += LoadJointRow( jointIndex, 1u ) * weight; + row2 += LoadJointRow( jointIndex, 2u ) * weight; + } + + vec3 position = vec3( + uintBitsToFloat( sourceData.words[ sourceBase + 0u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 1u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 2u ] ) ); + vec3 normal = vec3( + uintBitsToFloat( sourceData.words[ sourceBase + 4u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 5u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 6u ] ) ); + vec3 tangent0 = vec3( + uintBitsToFloat( sourceData.words[ sourceBase + 8u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 9u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 10u ] ) ); + vec3 tangent1 = vec3( + uintBitsToFloat( sourceData.words[ sourceBase + 11u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 12u ] ), + uintBitsToFloat( sourceData.words[ sourceBase + 13u ] ) ); + + StoreVec3( outputBase, 0u, TransformBasis( row0, row1, row2, position, 1.0 ) ); + StoreVec3( outputBase, 4u, TransformBasis( row0, row1, row2, normal, 0.0 ) ); + StoreVec3( outputBase, 8u, TransformBasis( row0, row1, row2, tangent0, 0.0 ) ); + StoreVec3( outputBase, 11u, TransformBasis( row0, row1, row2, tangent1, 0.0 ) ); +} diff --git a/src/renderer/Vulkan/shaders/gui.frag b/src/renderer/Vulkan/shaders/gui.frag index 955d3445..fe7e2dcb 100644 --- a/src/renderer/Vulkan/shaders/gui.frag +++ b/src/renderer/Vulkan/shaders/gui.frag @@ -26,7 +26,9 @@ void main() { vec4 color = texture(texSampler, fragTexCoord) * fragColor; if (pc.params.y > 0.5) { if (pc.params.y > 1.5) { - if (abs(color.a - pc.params.z) > (0.5 / 255.0)) { + // Match fixed-function GL_EQUAL exactly. A tolerance admits + // linearly filtered values that the classic alpha test rejects. + if (color.a != pc.params.z) { discard; } } else if (color.a <= pc.params.z) { diff --git a/src/renderer/Vulkan/shaders/gui_shaders_spv.h b/src/renderer/Vulkan/shaders/gui_shaders_spv.h index 235db02a..4212146c 100644 --- a/src/renderer/Vulkan/shaders/gui_shaders_spv.h +++ b/src/renderer/Vulkan/shaders/gui_shaders_spv.h @@ -4,6 +4,645 @@ #ifndef __VK_GUI_SHADERS_SPV_H__ #define __VK_GUI_SHADERS_SPV_H__ +alignas(4) static const unsigned char vk_gpu_skinning_comp_spv[] = { + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x8e, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x06, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x10, 0x00, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x11, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x4c, 0x6f, 0x61, 0x64, 0x4a, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x6f, 0x77, + 0x28, 0x75, 0x31, 0x3b, 0x75, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x6a, 0x6f, 0x69, 0x6e, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x42, 0x61, 0x73, 0x69, 0x73, 0x28, 0x76, + 0x66, 0x34, 0x3b, 0x76, 0x66, 0x34, 0x3b, 0x76, 0x66, 0x34, 0x3b, 0x76, 0x66, 0x33, 0x3b, 0x66, + 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x31, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x32, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x56, 0x65, 0x63, + 0x33, 0x28, 0x75, 0x31, 0x3b, 0x75, 0x31, 0x3b, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x62, 0x61, 0x73, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, 0x62, 0x61, 0x73, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x22, 0x00, 0x00, 0x00, 0x53, 0x6b, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, + 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x57, 0x6f, 0x72, 0x64, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x73, 0x6b, 0x69, 0x6e, + 0x57, 0x6f, 0x72, 0x64, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6a, 0x6f, 0x69, 0x6e, 0x74, 0x57, 0x6f, 0x72, + 0x64, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x06, 0x00, 0x08, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x57, 0x6f, 0x72, 0x64, 0x4f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x22, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x6a, 0x6f, 0x69, 0x6e, + 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x73, 0x6b, 0x69, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x4a, 0x6f, 0x69, 0x6e, 0x74, 0x57, 0x6f, 0x72, 0x64, 0x73, 0x00, 0x00, + 0x06, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x64, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x35, 0x00, 0x00, 0x00, 0x6a, 0x6f, 0x69, 0x6e, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x57, 0x6f, 0x72, 0x64, 0x73, 0x00, 0x06, 0x00, 0x05, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x69, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x44, 0x61, + 0x74, 0x61, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x82, 0x00, 0x00, 0x00, 0x76, 0x65, 0x72, 0x74, + 0x65, 0x78, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x85, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x47, + 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x92, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x42, 0x61, 0x73, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, 0x61, 0x73, 0x65, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x6f, 0x72, 0x64, 0x73, 0x00, + 0x06, 0x00, 0x05, 0x00, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x64, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xae, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x44, 0x61, 0x74, 0x61, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xb8, 0x00, 0x00, 0x00, + 0x73, 0x6b, 0x69, 0x6e, 0x42, 0x61, 0x73, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xbf, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x30, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xc2, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x31, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xc3, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x77, 0x32, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x66, 0x6c, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xce, 0x00, 0x00, 0x00, 0x53, 0x6b, 0x69, 0x6e, 0x57, 0x6f, 0x72, 0x64, + 0x73, 0x00, 0x00, 0x00, 0x06, 0x00, 0x05, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x77, 0x6f, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0x73, 0x6b, 0x69, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xdd, 0x00, 0x00, 0x00, 0x6a, 0x6f, 0x69, 0x6e, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xed, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xf3, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x16, 0x01, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x29, 0x01, 0x00, 0x00, 0x74, 0x61, 0x6e, 0x67, + 0x65, 0x6e, 0x74, 0x30, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x74, 0x61, 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x31, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x50, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x52, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x54, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x56, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x58, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x5a, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x5c, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x5d, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x5f, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x65, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x67, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x69, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x6b, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x6c, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x6e, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x70, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x72, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x74, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x76, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x7a, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x7b, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x7d, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x7f, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x81, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x83, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x85, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x89, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x8a, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x03, 0x00, 0x33, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x03, 0x00, 0x35, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x66, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x85, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xab, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0xac, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xae, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xae, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0xce, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x04, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x05, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x21, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x12, 0x00, 0x00, 0x00, 0x21, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x08, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x03, 0x00, 0x33, 0x00, 0x00, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x34, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x34, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, 0x66, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x03, 0x00, 0x67, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x83, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x86, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, 0xab, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x03, 0x00, 0xac, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0xad, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0xad, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x25, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x07, 0x00, + 0x09, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x03, 0x00, 0xcd, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x03, 0x00, 0xce, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xcf, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x06, 0x00, 0x83, 0x00, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x86, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0xae, 0x00, 0x05, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x8e, 0x00, 0x00, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0xfd, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x90, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x84, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, + 0x96, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x92, 0x00, 0x00, 0x00, + 0x98, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9e, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x99, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa1, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, 0xa3, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa6, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x05, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0xa7, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xa7, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0xa8, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xaf, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb0, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb1, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0xb1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, + 0xb2, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb4, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa4, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb5, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb7, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0xb9, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, + 0xbd, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc2, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0xc1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc5, 0x00, 0x00, 0x00, + 0xf6, 0x00, 0x04, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xb0, 0x00, 0x05, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, + 0xc7, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, + 0xd3, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xd8, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x05, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0xd9, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xdb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xd9, 0x00, 0x00, 0x00, + 0xda, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xda, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xdd, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe3, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0xe5, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xae, 0x00, 0x05, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xe7, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xe8, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe9, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xed, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x09, 0x00, 0x00, 0x00, + 0xee, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, + 0xef, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0xbf, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0xf1, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, + 0xdd, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x09, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, + 0xf5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, + 0xf6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc2, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfb, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x09, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xfb, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xc8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x09, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x0a, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0c, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x0c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, + 0x0d, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x12, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, + 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, + 0x0f, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x15, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, + 0x17, 0x01, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x19, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, + 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, + 0x1d, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, + 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x26, 0x01, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x27, 0x01, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x28, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2d, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x2e, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2f, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x31, 0x01, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x31, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, + 0x32, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, + 0x33, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, + 0x35, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, + 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, + 0x34, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x29, 0x01, 0x00, 0x00, + 0x3b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, + 0x3d, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x40, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, + 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, + 0xae, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4c, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x4d, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x4e, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x50, 0x01, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x53, 0x01, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x52, 0x01, 0x00, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, + 0xc3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x54, 0x01, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x56, 0x01, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x58, 0x01, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x59, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, + 0x54, 0x01, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5a, 0x01, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x5a, 0x01, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5f, 0x01, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, + 0xc3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x65, 0x01, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x67, 0x01, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x68, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x69, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x01, 0x00, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x69, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6e, 0x01, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x70, 0x01, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, + 0xc3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x72, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x74, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x76, 0x01, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x77, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, + 0x72, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7a, 0x01, 0x00, 0x00, + 0xbc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x7d, 0x01, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7f, 0x01, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, + 0xc3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x81, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x83, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x85, 0x01, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x86, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, + 0x81, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x3e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x84, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x2e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4e, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x50, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x12, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x56, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x08, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x09, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, + 0x56, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x53, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x08, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x5e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x08, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x63, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x12, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x6f, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x71, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x71, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x12, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x79, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x12, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x81, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0x38, 0x00, 0x01, 0x00, +}; +static const unsigned int vk_gpu_skinning_comp_spv_size = 10148u; + alignas(4) static const unsigned char vk_gui_vert_spv[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, @@ -193,12 +832,12 @@ alignas(4) static const unsigned char vk_gui_vert_spv[] = { static const unsigned int vk_gui_vert_spv_size = 2944u; alignas(4) static const unsigned char vk_gui_frag_spv[] = { - 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x58, 0x00, 0x00, 0x00, + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, @@ -216,7 +855,7 @@ alignas(4) static const unsigned char vk_gui_frag_spv[] = { 0x74, 0x65, 0x78, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x00, 0x06, 0x00, 0x05, 0x00, 0x19, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x56, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x11, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -230,7 +869,7 @@ alignas(4) static const unsigned char vk_gui_frag_spv[] = { 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x19, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x19, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x56, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x53, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, @@ -261,10 +900,9 @@ alignas(4) static const unsigned char vk_gui_frag_spv[] = { 0x2e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x00, 0x00, 0x81, 0x80, 0x00, 0x3b, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x20, 0x00, 0x04, 0x00, 0x55, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x55, 0x00, 0x00, 0x00, - 0x56, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x20, 0x00, 0x04, 0x00, 0x52, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x52, 0x00, 0x00, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, @@ -278,61 +916,58 @@ alignas(4) static const unsigned char vk_gui_frag_spv[] = { 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x26, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0x31, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x38, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x39, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x3c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x3f, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x42, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x43, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x2d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x45, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, - 0x24, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x49, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4a, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4d, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0xbe, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x4b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4b, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, - 0x49, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x51, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x52, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x53, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x56, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, - 0x38, 0x00, 0x01, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x37, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x36, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x37, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x39, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x3f, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x42, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x43, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x46, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x47, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x49, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x4b, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x24, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x4a, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x48, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x48, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x4e, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x4f, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x50, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x53, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, }; -static const unsigned int vk_gui_frag_spv_size = 2196u; +static const unsigned int vk_gui_frag_spv_size = 2136u; alignas(4) static const unsigned char vk_screen_vert_spv[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x7a, 0x00, 0x00, 0x00, @@ -13707,14 +14342,14 @@ alignas(4) static const unsigned char vk_interaction_vert_spv[] = { static const unsigned int vk_interaction_vert_spv_size = 5772u; alignas(4) static const unsigned char vk_interaction_frag_spv[] = { - 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0f, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, - 0xe5, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, + 0x27, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, + 0x4a, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x53, 0x61, 0x66, 0x65, @@ -13724,489 +14359,856 @@ alignas(4) static const unsigned char vk_interaction_frag_spv[] = { 0x74, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x53, 0x77, 0x65, 0x65, 0x70, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5a, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x56, 0x69, 0x65, 0x77, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x51, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, - 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x06, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, - 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, - 0x78, 0x54, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x43, - 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x43, 0x6f, 0x6c, 0x6f, - 0x72, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0x66, 0x6c, 0x61, 0x74, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, - 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x54, 0x6f, 0x42, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x47, 0x00, 0x00, 0x00, 0x62, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x59, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x76, 0x42, 0x75, 0x6d, - 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x5d, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, - 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x76, 0x44, 0x69, 0x66, - 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x60, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x61, 0x00, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5a, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x45, 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x42, + 0x52, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, + 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x16, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, + 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, 0x6f, 0x72, 0x6d, 0x54, 0x65, 0x78, 0x43, 0x6f, + 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x06, 0x00, 0x19, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, + 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x56, 0x69, 0x65, 0x77, 0x4f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x00, 0x06, 0x00, 0x08, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x27, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x27, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x51, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x53, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x27, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, + 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x06, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, + 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, + 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, + 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x27, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, + 0x75, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x66, 0x6c, 0x61, 0x74, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x6f, 0x42, 0x61, 0x6e, 0x64, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x50, 0x00, 0x00, 0x00, 0x62, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x60, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, + 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x6f, 0x72, 0x6d, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x05, 0x00, 0x74, 0x00, 0x00, 0x00, 0x6d, 0x65, 0x74, 0x61, + 0x6c, 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x78, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x76, 0x70, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6f, 0x00, 0x00, 0x00, - 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x7a, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x76, 0x70, 0x00, 0x06, 0x00, 0x04, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x72, 0x6f, 0x75, 0x67, 0x68, 0x6e, 0x65, 0x73, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x98, 0x00, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x72, 0x00, 0x05, 0x00, 0x05, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x76, 0x56, 0x69, 0x65, 0x77, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x91, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x95, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, - 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xaf, 0x00, 0x00, 0x00, - 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, - 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x08, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0x9f, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x44, 0x69, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa6, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa8, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xad, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x76, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xb2, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xb7, 0x00, 0x00, 0x00, 0x76, 0x64, 0x6f, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xbc, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x53, 0x71, 0x75, 0x61, 0x72, 0x65, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x64, 0x65, 0x6e, 0x6f, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xce, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xde, 0x00, 0x00, 0x00, + 0x67, 0x65, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf9, 0x00, 0x00, 0x00, + 0x66, 0x72, 0x65, 0x73, 0x6e, 0x65, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x11, 0x01, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x1b, 0x01, 0x00, 0x00, 0x72, 0x61, 0x64, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, + 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x08, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x54, 0x65, 0x78, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xc7, 0x00, 0x00, 0x00, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x25, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, - 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, + 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x27, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x65, 0x78, 0x43, - 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd2, 0x00, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xdd, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xdf, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xe4, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0xe5, 0x00, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, - 0x6c, 0x65, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xe6, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xe9, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x44, 0x6f, 0x74, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xee, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x54, 0x65, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xef, 0x00, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf6, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf7, 0x00, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x04, 0x01, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x76, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, - 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x61, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x03, 0x00, 0x64, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xaf, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe5, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xef, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xef, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, 0x17, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x11, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x1f, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x25, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, - 0x29, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, - 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, - 0x57, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x58, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, - 0x63, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, - 0x64, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x65, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x65, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x67, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x68, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x19, 0x00, 0x09, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, - 0x71, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x90, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x9d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, - 0xa2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0xaf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0xbf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0xc9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0xd2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, - 0xd7, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xd8, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xe0, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0xe5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x72, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, - 0xfc, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, - 0x04, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x0a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x58, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x58, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x58, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x58, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x90, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x57, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, - 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0x29, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x6b, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, - 0x74, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, - 0x75, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x76, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x7d, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, - 0x57, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x66, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x57, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x57, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, - 0x87, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, - 0x8a, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x57, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, - 0x8d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x60, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0x6e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6e, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, - 0x57, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, - 0x93, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x91, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, - 0x96, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, - 0x91, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x99, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x9a, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, - 0x9b, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, - 0x9c, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x9f, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, - 0x9f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x95, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0xa2, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa4, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x29, 0x00, 0x00, 0x00, - 0xa5, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xa5, 0x00, 0x00, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0xaa, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, - 0xab, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0xac, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xa8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xae, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0xa8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb5, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb6, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb7, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0xb7, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x00, 0x00, 0x00, - 0xb8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, - 0xb4, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, - 0xba, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb9, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, - 0xbd, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0xc0, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc1, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, - 0xbd, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xc4, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xc5, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xc6, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb9, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, - 0xc8, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0xca, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xcb, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, - 0xc8, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xce, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xcf, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xd0, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb9, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, - 0xd3, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, - 0xd4, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0xd5, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0xd8, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, - 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, - 0xdb, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0xe0, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, - 0xdf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x71, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, - 0x57, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, - 0x57, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, - 0xf2, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, - 0xf3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xf5, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xee, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x71, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x57, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0xd8, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, - 0xff, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, - 0xee, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf6, 0x00, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, - 0xd1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, - 0xf6, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, - 0x05, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x08, 0x01, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x09, 0x01, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, - 0x0c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0f, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, - 0x0f, 0x01, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x10, 0x01, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x27, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x29, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x25, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x25, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x35, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2f, 0x00, 0x00, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x25, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x3f, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x25, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x3b, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x49, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4c, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x47, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x4d, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x25, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, - 0x51, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, - 0x52, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, - 0x4d, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x76, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x42, 0x01, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, + 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x44, 0x01, 0x00, 0x00, + 0x76, 0x42, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x46, 0x01, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, + 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, 0x47, 0x01, 0x00, 0x00, + 0x76, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x49, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, + 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x62, 0x01, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x63, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x82, 0x01, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x95, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x96, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x98, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9a, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xa1, 0x01, 0x00, 0x00, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xac, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xbb, 0x01, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xd3, 0x01, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xde, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xe3, 0x01, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, + 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xe8, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x44, 0x6f, 0x74, 0x00, 0x05, 0x00, 0x06, 0x00, 0xed, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0xee, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, + 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf5, 0x01, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x27, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xe0, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x29, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x29, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x64, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x64, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x04, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x98, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9e, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x1c, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x1c, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x1f, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x25, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x25, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x27, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x44, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x47, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x4a, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x53, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x5d, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x5d, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe4, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xee, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xee, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x21, 0x00, 0x07, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, 0x17, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x11, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x2a, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x2e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, + 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, + 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x03, 0x00, 0x62, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0xcd, 0xcc, 0x0c, 0x40, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, + 0x6b, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x7b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x89, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x91, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x92, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x97, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x97, 0x00, 0x00, 0x00, + 0x98, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x97, 0x00, 0x00, 0x00, + 0x9e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0x17, 0xb7, 0xd1, 0x38, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0xdb, 0x0f, 0x49, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf3, 0x00, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3d, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf4, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x83, 0xf9, 0xa2, 0x3e, + 0x3b, 0x00, 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x97, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f, + 0x20, 0x00, 0x04, 0x00, 0x52, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x52, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x07, 0x00, 0x26, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x2a, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x04, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x97, 0x00, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x63, 0x00, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x2a, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x42, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x46, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x49, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x5c, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x79, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x98, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x9a, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xa1, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xac, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xb7, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xbb, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xc0, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xd3, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xdc, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xde, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xe3, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xe5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xe8, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xf5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x45, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x42, 0x01, 0x00, 0x00, + 0x45, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, + 0x47, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x46, 0x01, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x49, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0x4c, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, + 0x4d, 0x01, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x51, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, + 0x51, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x50, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x51, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x59, 0x01, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x5a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, + 0x5d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, + 0x42, 0x01, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, + 0x5e, 0x01, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5c, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x64, 0x01, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x64, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x66, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, + 0x5c, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, + 0x56, 0x01, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6c, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6d, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x62, 0x01, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x42, 0x01, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x46, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x49, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x5b, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x62, 0x00, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, + 0x7d, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, + 0x7e, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x81, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x81, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x80, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x88, 0x01, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x89, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, + 0x8b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, + 0x8d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8e, 0x01, 0x00, 0x00, + 0x8f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x91, 0x01, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x90, 0x01, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x95, 0x01, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x46, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x98, 0x01, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x9a, 0x01, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x39, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x9b, 0x01, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, + 0x98, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9c, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x26, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, + 0x9c, 0x01, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x53, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x81, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xa2, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, + 0xa6, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa8, 0x01, 0x00, 0x00, + 0xa3, 0x01, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xa8, 0x01, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xab, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa1, 0x01, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xad, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0xad, 0x01, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xaf, 0x01, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x92, 0x00, 0x00, 0x00, + 0xb3, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x26, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb2, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb6, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb8, 0x01, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb7, 0x01, 0x00, 0x00, + 0xb8, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0xb9, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb2, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xb2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xac, 0x01, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, + 0xbd, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, + 0xc1, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x26, 0x00, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, + 0xc5, 0x01, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, + 0x26, 0x00, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, + 0xc7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc9, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, 0xc9, 0x01, 0x00, 0x00, + 0xc8, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0xcb, 0x01, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x26, 0x00, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, + 0xcd, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, + 0x26, 0x00, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xcb, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, + 0xcf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, + 0xd0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0xd4, 0x01, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, + 0x57, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0xd4, 0x01, 0x00, 0x00, + 0xd5, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, + 0xd6, 0x01, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x01, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, + 0xd9, 0x01, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xda, 0x01, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdb, 0x01, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xd3, 0x01, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdd, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, + 0xdd, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdf, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0x1f, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe1, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xde, 0x01, 0x00, 0x00, + 0xe1, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, + 0x11, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xd3, 0x01, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xe6, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe5, 0x01, 0x00, 0x00, + 0xe6, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe3, 0x01, 0x00, 0x00, + 0xe7, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, + 0xe3, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, + 0xa1, 0x01, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, + 0xe9, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xec, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe8, 0x01, 0x00, 0x00, + 0xec, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, + 0xee, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, + 0xe8, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, + 0xf0, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, + 0xf2, 0x01, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, + 0x85, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xed, 0x01, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, + 0x57, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, + 0xf7, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, + 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x01, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, + 0xfc, 0x01, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfd, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfe, 0x01, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, + 0x04, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, + 0x3d, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, + 0x05, 0x02, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x26, 0x00, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x53, 0x01, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x34, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x36, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x35, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x38, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x2e, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x4b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x4e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x50, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x59, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5a, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, + 0x5c, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x19, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x62, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x6a, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x6d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, + 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x6e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x74, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x81, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x8a, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0x96, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x90, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x96, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x9b, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x90, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, + 0x9e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9f, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x9f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9d, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0xa6, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa2, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, + 0xaa, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa8, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xad, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb3, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb4, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb5, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xb5, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x00, 0x00, 0x00, + 0xb6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xbb, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb7, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, + 0xbe, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, + 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, + 0xc6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, + 0xc8, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xca, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xcf, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd2, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, + 0xd4, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xce, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, + 0xd9, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdb, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, + 0xe2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, + 0xe3, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe6, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe8, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe9, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xea, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, + 0xed, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, + 0xef, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0xe7, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xde, 0x00, 0x00, 0x00, + 0xf1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, + 0x74, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0xf6, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0xf4, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfb, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x05, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, + 0xf9, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x07, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0a, 0x01, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, + 0xf9, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x14, 0x01, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x15, 0x01, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, + 0x17, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, + 0x18, 0x01, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x1a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, + 0x1c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, + 0x1f, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, + 0x20, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x22, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, + 0x22, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, + 0x23, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x62, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, + 0x25, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x26, 0x00, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, + 0x27, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, + 0x28, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x2a, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x05, 0x00, 0x26, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, + 0x2a, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, + 0x2b, 0x01, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, + 0x24, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x2f, 0x01, 0x00, 0x00, + 0x30, 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, + 0x34, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, + 0x37, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, + 0x1b, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3b, 0x01, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3c, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, }; -static const unsigned int vk_interaction_frag_spv_size = 7960u; +static const unsigned int vk_interaction_frag_spv_size = 13836u; alignas(4) static const unsigned char vk_interaction_shadow_vert_spv[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x2c, 0x01, 0x00, 0x00, @@ -14825,16 +15827,16 @@ alignas(4) static const unsigned char vk_interaction_shadow_vert_spv[] = { static const unsigned int vk_interaction_shadow_vert_spv_size = 9784u; alignas(4) static const unsigned char vk_interaction_shadow_frag_spv[] = { - 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0xff, 0x05, 0x00, 0x00, + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x14, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x15, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0xd4, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, - 0x56, 0x05, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, 0x9f, 0x05, 0x00, 0x00, - 0xae, 0x05, 0x00, 0x00, 0xb8, 0x05, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0xf2, 0x05, 0x00, 0x00, - 0xf8, 0x05, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0xb7, 0x01, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, + 0x51, 0x06, 0x00, 0x00, 0x56, 0x06, 0x00, 0x00, 0x59, 0x06, 0x00, 0x00, 0x5c, 0x06, 0x00, 0x00, + 0xed, 0x06, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x53, 0x61, 0x66, 0x65, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, @@ -14844,2420 +15846,2771 @@ alignas(4) static const unsigned char vk_interaction_shadow_frag_spv[] = { 0x53, 0x77, 0x65, 0x65, 0x70, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5a, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x61, - 0x73, 0x63, 0x61, 0x64, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x28, 0x00, 0x05, 0x00, 0x09, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x28, 0x76, 0x66, 0x34, 0x3b, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x42, 0x79, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x08, 0x00, 0x24, 0x00, 0x00, 0x00, 0x41, 0x74, 0x6c, 0x61, 0x73, 0x52, 0x65, 0x63, - 0x74, 0x42, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0x23, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x48, 0x61, 0x73, 0x68, - 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x27, 0x00, 0x00, 0x00, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x4f, 0x66, 0x66, 0x73, - 0x65, 0x74, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x47, - 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x37, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x69, 0x61, 0x73, 0x28, 0x69, - 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x36, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, - 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, - 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x69, - 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x52, 0x61, 0x77, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x28, 0x76, - 0x66, 0x32, 0x3b, 0x00, 0x05, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, - 0x05, 0x00, 0x0d, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x50, 0x43, 0x53, 0x53, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x28, 0x76, 0x66, 0x32, 0x3b, - 0x66, 0x31, 0x3b, 0x69, 0x31, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, - 0x32, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x44, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x45, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0x46, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x47, 0x00, 0x00, 0x00, - 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x4d, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x48, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x4d, 0x61, 0x78, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x49, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x53, 0x74, 0x65, - 0x70, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, 0x50, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x28, - 0x76, 0x66, 0x34, 0x3b, 0x76, 0x66, 0x34, 0x3b, 0x69, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x4d, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x61, 0x74, 0x6c, 0x61, 0x73, 0x52, 0x65, 0x63, - 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, + 0x05, 0x00, 0x0b, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x45, 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x42, 0x52, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, + 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x54, 0x65, + 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x6f, 0x72, 0x6d, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x19, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, + 0x6f, 0x77, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x28, 0x00, + 0x05, 0x00, 0x09, 0x00, 0x26, 0x00, 0x00, 0x00, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x43, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x28, 0x76, 0x66, 0x34, 0x3b, 0x69, 0x31, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x24, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x25, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, - 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x28, 0x66, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x53, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x09, 0x00, 0x57, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x43, 0x61, - 0x73, 0x63, 0x61, 0x64, 0x65, 0x42, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x56, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, + 0x2a, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x42, + 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x41, 0x74, 0x6c, 0x61, + 0x73, 0x52, 0x65, 0x63, 0x74, 0x42, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, + 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, + 0x48, 0x61, 0x73, 0x68, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x00, + 0x37, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x35, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x44, 0x65, + 0x70, 0x74, 0x68, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x28, 0x69, 0x31, 0x3b, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x69, + 0x61, 0x73, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x0a, 0x00, 0x44, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, + 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x28, 0x76, 0x66, 0x32, 0x3b, + 0x66, 0x31, 0x3b, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x42, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x43, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, - 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x28, 0x00, 0x05, 0x00, 0x08, 0x00, 0x5d, 0x00, 0x00, 0x00, - 0x67, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x47, 0x72, 0x61, 0x64, - 0x69, 0x65, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x56, 0x69, 0x65, 0x77, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x08, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x51, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, - 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x06, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, - 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, - 0x78, 0x54, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x43, - 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x43, 0x6f, 0x6c, 0x6f, - 0x72, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0x66, 0x6c, 0x61, 0x74, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, - 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x85, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x54, 0x6f, 0x42, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x91, 0x00, 0x00, 0x00, 0x62, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, - 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x30, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x31, 0x00, 0x00, - 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x32, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x33, 0x00, 0x00, - 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x61, 0x74, 0x6c, 0x61, - 0x73, 0x52, 0x65, 0x63, 0x74, 0x73, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, 0x00, - 0x06, 0x00, 0x08, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, - 0x61, 0x64, 0x65, 0x42, 0x69, 0x61, 0x73, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, - 0x6c, 0x44, 0x65, 0x70, 0x74, 0x68, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x4f, 0x66, - 0x66, 0x73, 0x65, 0x74, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x44, 0x65, 0x70, 0x74, - 0x68, 0x52, 0x6f, 0x77, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x62, 0x69, 0x61, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, - 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, - 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x70, 0x63, 0x73, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xc2, 0x00, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x30, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, - 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x31, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xd1, 0x00, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x32, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, - 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x33, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x06, 0x01, 0x00, 0x00, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x17, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x1b, 0x01, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x1e, 0x01, 0x00, 0x00, - 0x63, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x36, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x38, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x43, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x3f, 0x01, 0x00, 0x00, - 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x43, 0x6f, 0x73, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x43, 0x01, 0x00, 0x00, 0x73, 0x69, 0x6e, 0x54, 0x68, 0x65, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x73, 0x6c, 0x6f, 0x70, - 0x65, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x50, 0x01, 0x00, 0x00, - 0x73, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x59, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x5c, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x60, 0x01, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x62, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x65, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, - 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x72, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x82, 0x01, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x84, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x90, 0x01, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x44, 0x65, - 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x06, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x52, 0x61, 0x77, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0x62, 0x61, 0x73, 0x65, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x44, - 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd1, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd5, 0x01, 0x00, 0x00, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x54, 0x61, 0x70, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xdb, 0x01, 0x00, 0x00, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x44, 0x65, 0x70, 0x74, 0x68, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x62, 0x6c, 0x6f, 0x63, - 0x6b, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0xdd, 0x01, 0x00, 0x00, 0x64, 0x30, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xde, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xeb, 0x01, 0x00, 0x00, - 0x6f, 0x31, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xee, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xef, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf6, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf9, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xfe, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xff, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x01, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x04, 0x02, 0x00, 0x00, 0x6f, 0x34, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x06, 0x02, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x07, 0x02, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x09, 0x02, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x0c, 0x02, 0x00, 0x00, - 0x64, 0x31, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x15, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x17, 0x02, 0x00, 0x00, 0x64, 0x32, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x20, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x22, 0x02, 0x00, 0x00, 0x64, 0x33, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x2b, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x2d, 0x02, 0x00, 0x00, 0x64, 0x34, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x36, 0x02, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x66, 0x02, 0x00, 0x00, - 0x61, 0x76, 0x65, 0x72, 0x61, 0x67, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x6a, 0x02, 0x00, 0x00, 0x70, 0x65, 0x6e, 0x75, 0x6d, 0x62, 0x72, 0x61, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x72, 0x02, 0x00, 0x00, 0x6d, 0x61, 0x78, 0x52, - 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x82, 0x02, 0x00, 0x00, - 0x77, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x90, 0x02, 0x00, 0x00, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x58, 0x59, 0x00, 0x05, 0x00, 0x04, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x99, 0x02, 0x00, 0x00, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xb6, 0x02, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x55, 0x76, 0x00, 0x05, 0x00, 0x03, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe5, 0x02, 0x00, 0x00, - 0x72, 0x65, 0x63, 0x74, 0x4d, 0x69, 0x6e, 0x00, 0x05, 0x00, 0x04, 0x00, 0xeb, 0x02, 0x00, 0x00, - 0x72, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x78, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf1, 0x02, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x52, 0x61, 0x77, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x44, 0x65, 0x70, + 0x74, 0x68, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x00, 0x05, 0x00, 0x03, 0x00, 0x47, 0x00, 0x00, 0x00, + 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x53, 0x00, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x50, 0x43, 0x53, 0x53, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x28, + 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x69, 0x31, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, + 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x6d, 0x66, 0x32, 0x32, 0x3b, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x4e, 0x00, 0x00, 0x00, + 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x4d, 0x69, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, 0x63, 0x6c, 0x61, 0x6d, + 0x70, 0x4d, 0x61, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x51, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x53, 0x74, 0x65, 0x70, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xfc, 0x02, 0x00, 0x00, 0x67, 0x75, 0x61, 0x72, 0x64, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x02, 0x03, 0x00, 0x00, 0x67, 0x75, 0x61, 0x72, 0x64, 0x42, 0x61, 0x6e, - 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x09, 0x03, 0x00, 0x00, 0x63, 0x6c, 0x61, 0x6d, - 0x70, 0x4d, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x4d, 0x61, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x18, 0x03, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x19, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1f, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x21, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x23, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2a, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2e, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x32, 0x03, 0x00, 0x00, 0x74, 0x61, 0x70, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x36, 0x03, 0x00, 0x00, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x37, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x39, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x45, 0x03, 0x00, 0x00, 0x6f, 0x31, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x49, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x4a, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x4c, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x4f, 0x03, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x53, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x54, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x56, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x59, 0x03, 0x00, 0x00, - 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x60, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x63, 0x03, 0x00, 0x00, 0x6f, 0x34, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x67, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x68, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x75, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x76, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x78, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x85, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x86, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x88, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x95, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x96, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x98, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa5, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa6, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0xb6, 0x03, 0x00, 0x00, 0x6f, 0x35, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xba, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xbb, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xbd, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x6f, 0x36, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc4, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc5, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc7, 0x03, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xca, 0x03, 0x00, 0x00, - 0x6f, 0x37, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xce, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x6f, 0x38, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xd8, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xd9, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xdb, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xe6, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xe7, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xe9, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf6, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf7, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xf9, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x06, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x07, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x09, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x16, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x17, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x19, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x28, 0x04, 0x00, 0x00, 0x6f, 0x39, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x2c, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x2d, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x2f, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x32, 0x04, 0x00, 0x00, 0x6f, 0x31, 0x30, 0x00, 0x05, 0x00, 0x04, 0x00, 0x36, 0x04, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x37, 0x04, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x39, 0x04, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x3c, 0x04, 0x00, 0x00, - 0x6f, 0x31, 0x31, 0x00, 0x05, 0x00, 0x04, 0x00, 0x40, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x41, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x43, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x46, 0x04, 0x00, 0x00, 0x6f, 0x31, 0x32, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x4a, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x4d, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x58, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x59, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x68, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x69, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x6b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x78, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x79, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x88, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x89, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x8b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x95, 0x04, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x69, 0x6f, 0x72, - 0x53, 0x70, 0x6c, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xc2, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xc5, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xc8, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xc9, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xca, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xcf, 0x04, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x76, 0x56, 0x69, 0x65, - 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd1, 0x04, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xd4, 0x04, 0x00, 0x00, - 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xd5, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0xd8, 0x04, 0x00, 0x00, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x69, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xdb, 0x04, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x42, 0x6c, 0x65, 0x6e, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xe8, 0x04, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, - 0x69, 0x6f, 0x75, 0x73, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xf1, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xf4, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0xf7, 0x04, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x70, 0x6c, 0x69, 0x74, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf8, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xfb, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x62, 0x6c, 0x65, 0x6e, - 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x05, 0x00, 0x00, - 0x62, 0x6c, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x10, 0x05, 0x00, 0x00, 0x62, 0x6c, 0x65, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x21, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x51, 0x05, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x53, 0x05, 0x00, 0x00, 0x76, 0x42, 0x75, 0x6d, - 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x55, 0x05, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, - 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, 0x56, 0x05, 0x00, 0x00, 0x76, 0x44, 0x69, 0x66, - 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x58, 0x05, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x59, 0x05, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x5c, 0x05, 0x00, 0x00, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x73, 0x68, 0x43, - 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x76, 0x70, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x5e, 0x05, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x65, 0x05, 0x00, 0x00, - 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x66, 0x05, 0x00, 0x00, - 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6b, 0x05, 0x00, 0x00, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x6d, 0x05, 0x00, 0x00, - 0x76, 0x56, 0x69, 0x65, 0x77, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x6e, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x83, 0x05, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x87, 0x05, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, - 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, 0x93, 0x05, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x9f, 0x05, 0x00, 0x00, - 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa4, 0x05, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa9, 0x05, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xac, 0x05, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, - 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x08, 0x00, 0xae, 0x05, 0x00, 0x00, - 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x54, 0x65, 0x78, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xb6, 0x05, 0x00, 0x00, - 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, - 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0xb8, 0x05, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, - 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x65, 0x78, 0x43, - 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc3, 0x05, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, 0xc4, 0x05, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xcd, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xcf, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xd3, 0x05, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, - 0x6c, 0x65, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xd5, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xd8, 0x05, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x44, 0x6f, 0x74, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xdd, 0x05, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x54, 0x65, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xde, 0x05, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xe5, 0x05, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xe6, 0x05, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x0a, 0x00, 0x59, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, + 0x61, 0x64, 0x6f, 0x77, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x28, 0x76, 0x66, 0x34, 0x3b, + 0x76, 0x66, 0x34, 0x3b, 0x69, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x61, 0x74, 0x6c, 0x61, 0x73, 0x52, 0x65, 0x63, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x58, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x61, 0x73, 0x63, + 0x61, 0x64, 0x65, 0x28, 0x66, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, 0x5c, 0x00, 0x00, 0x00, + 0x76, 0x69, 0x65, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x43, 0x61, 0x73, 0x63, 0x61, 0x64, + 0x65, 0x42, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x28, 0x69, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x28, 0x00, 0x05, 0x00, 0x08, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x53, 0x68, 0x61, + 0x64, 0x6f, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x56, 0x69, 0x65, 0x77, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x51, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, + 0x6c, 0x6f, 0x66, 0x66, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x73, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, + 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, + 0x78, 0x53, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x66, 0x6c, 0x61, 0x74, + 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x82, 0x00, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x54, 0x6f, 0x42, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9a, 0x00, 0x00, 0x00, + 0x62, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xb8, 0x00, 0x00, 0x00, 0x6f, 0x72, 0x6d, 0x00, 0x05, 0x00, 0x05, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xf2, 0x05, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xf8, 0x05, 0x00, 0x00, 0x76, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, - 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x6a, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x03, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x40, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xc0, 0x01, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x90, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x90, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x0e, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x53, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x56, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x59, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x04, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x5c, 0x05, 0x00, 0x00, + 0xbe, 0x00, 0x00, 0x00, 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x09, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6d, 0x76, 0x70, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x6e, 0x65, 0x73, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xe2, 0x00, 0x00, 0x00, + 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x72, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x76, 0x56, 0x69, 0x65, 0x77, 0x56, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xec, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, + 0x44, 0x69, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x6c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x76, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0x76, 0x64, 0x6f, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x06, 0x01, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x53, 0x71, 0x75, 0x61, 0x72, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x18, 0x01, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x20, 0x01, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x28, 0x01, 0x00, 0x00, 0x67, 0x65, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x79, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x66, 0x72, 0x65, 0x73, 0x6e, 0x65, 0x6c, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, + 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, 0x65, 0x01, 0x00, 0x00, 0x72, 0x61, 0x64, 0x69, + 0x61, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x66, 0x01, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x69, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, + 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x87, 0x01, 0x00, 0x00, 0x76, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, + 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, + 0x77, 0x30, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x92, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x31, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, + 0x77, 0x32, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x92, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x6f, 0x77, 0x33, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x61, 0x74, 0x6c, 0x61, 0x73, 0x52, 0x65, 0x63, + 0x74, 0x73, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x92, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x73, 0x70, 0x6c, 0x69, 0x74, 0x44, 0x65, 0x70, 0x74, 0x68, 0x73, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x42, + 0x69, 0x61, 0x73, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x44, 0x65, 0x70, + 0x74, 0x68, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, + 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x52, 0x6f, 0x77, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x92, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x62, 0x69, 0x61, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x53, 0x69, 0x7a, + 0x65, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x92, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x92, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x70, 0x63, 0x73, 0x73, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xa9, 0x01, 0x00, 0x00, + 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x30, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xb0, 0x01, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, + 0x6f, 0x6f, 0x72, 0x64, 0x31, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xb7, 0x01, 0x00, 0x00, + 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x32, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xba, 0x01, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, + 0x6f, 0x6f, 0x72, 0x64, 0x33, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xec, 0x01, 0x00, 0x00, + 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xfd, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x11, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x13, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x18, 0x02, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x43, 0x6f, 0x73, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, + 0x64, 0x6f, 0x77, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x43, 0x6f, 0x73, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x1e, 0x02, 0x00, 0x00, 0x73, 0x69, 0x6e, 0x54, 0x68, 0x65, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x25, 0x02, 0x00, 0x00, 0x73, 0x6c, 0x6f, 0x70, 0x65, 0x42, 0x69, 0x61, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x73, 0x63, 0x61, 0x6c, + 0x61, 0x72, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x33, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x3a, 0x02, 0x00, 0x00, + 0x74, 0x65, 0x78, 0x65, 0x6c, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x3c, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x3f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x45, 0x02, 0x00, 0x00, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x50, 0x6c, 0x61, 0x6e, + 0x65, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x4c, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x5c, 0x02, 0x00, 0x00, + 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x6a, 0x02, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, + 0x6d, 0x70, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x74, 0x02, 0x00, 0x00, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x75, 0x02, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x61, + 0x77, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x86, 0x02, 0x00, 0x00, + 0x62, 0x61, 0x73, 0x65, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa8, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xac, 0x02, 0x00, 0x00, 0x73, 0x65, 0x61, 0x72, + 0x63, 0x68, 0x54, 0x61, 0x70, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xb4, 0x02, 0x00, 0x00, + 0x64, 0x30, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xb5, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xc2, 0x02, 0x00, 0x00, 0x6f, 0x31, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xc8, 0x02, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xce, 0x02, 0x00, 0x00, 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xd4, 0x02, 0x00, 0x00, + 0x6f, 0x34, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xda, 0x02, 0x00, 0x00, 0x6f, 0x35, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xe0, 0x02, 0x00, 0x00, 0x6f, 0x36, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xe6, 0x02, 0x00, 0x00, 0x6f, 0x37, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xec, 0x02, 0x00, 0x00, + 0x6f, 0x38, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xf2, 0x02, 0x00, 0x00, 0x64, 0x31, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xfb, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x64, 0x32, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x06, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x08, 0x03, 0x00, 0x00, 0x64, 0x33, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x11, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x13, 0x03, 0x00, 0x00, + 0x64, 0x34, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x64, 0x35, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x27, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x29, 0x03, 0x00, 0x00, 0x64, 0x36, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x32, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x34, 0x03, 0x00, 0x00, 0x64, 0x37, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x3f, 0x03, 0x00, 0x00, + 0x64, 0x38, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x48, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x61, 0x76, 0x65, 0x72, + 0x61, 0x67, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xa3, 0x03, 0x00, 0x00, 0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x70, 0x65, 0x6e, 0x75, 0x6d, 0x62, 0x72, 0x61, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xad, 0x03, 0x00, 0x00, 0x6d, 0x61, 0x78, 0x52, + 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xbd, 0x03, 0x00, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x58, 0x59, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xd4, 0x03, 0x00, 0x00, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xf1, 0x03, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x55, 0x76, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x04, 0x00, 0x00, + 0x72, 0x65, 0x63, 0x74, 0x4d, 0x69, 0x6e, 0x00, 0x05, 0x00, 0x04, 0x00, 0x26, 0x04, 0x00, 0x00, + 0x72, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x78, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2c, 0x04, 0x00, 0x00, + 0x74, 0x65, 0x78, 0x65, 0x6c, 0x53, 0x74, 0x65, 0x70, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x37, 0x04, 0x00, 0x00, 0x67, 0x75, 0x61, 0x72, 0x64, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x67, 0x75, 0x61, 0x72, 0x64, 0x42, 0x61, 0x6e, + 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x44, 0x04, 0x00, 0x00, 0x63, 0x6c, 0x61, 0x6d, + 0x70, 0x4d, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x48, 0x04, 0x00, 0x00, + 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x4d, 0x61, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x54, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x56, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x59, 0x04, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x61, + 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5a, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5c, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5e, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x60, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x62, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x64, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x66, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6d, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6f, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x71, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x75, 0x04, 0x00, 0x00, + 0x74, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, 0x79, 0x04, 0x00, 0x00, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7a, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7c, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x88, 0x04, 0x00, 0x00, 0x6f, 0x31, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x8b, 0x04, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x8e, 0x04, 0x00, 0x00, 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x91, 0x04, 0x00, 0x00, + 0x6f, 0x34, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9c, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9f, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xac, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xad, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xaf, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xbc, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xbd, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xbf, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcc, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcd, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xdc, 0x04, 0x00, 0x00, 0x6f, 0x35, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xdf, 0x04, 0x00, 0x00, 0x6f, 0x36, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xe2, 0x04, 0x00, 0x00, 0x6f, 0x37, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xe5, 0x04, 0x00, 0x00, + 0x6f, 0x38, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf1, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf3, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x01, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x03, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x10, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x11, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x13, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x21, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x23, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x32, 0x05, 0x00, 0x00, 0x6f, 0x39, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x38, 0x05, 0x00, 0x00, 0x6f, 0x31, 0x30, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x3e, 0x05, 0x00, 0x00, 0x6f, 0x31, 0x31, 0x00, 0x05, 0x00, 0x03, 0x00, 0x44, 0x05, 0x00, 0x00, + 0x6f, 0x31, 0x32, 0x00, 0x05, 0x00, 0x04, 0x00, 0x52, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x53, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x55, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x62, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x63, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x65, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x72, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x73, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x75, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x82, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x83, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x85, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x8f, 0x05, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x69, 0x6f, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xbc, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xbf, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc2, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc3, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc4, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xc9, 0x05, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xca, 0x05, 0x00, 0x00, + 0x76, 0x56, 0x69, 0x65, 0x77, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xcb, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xce, 0x05, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcf, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xd2, 0x05, 0x00, 0x00, 0x6c, 0x61, 0x73, 0x74, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x69, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xd5, 0x05, 0x00, 0x00, 0x63, 0x61, 0x73, 0x63, 0x61, 0x64, 0x65, 0x42, + 0x6c, 0x65, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xe2, 0x05, 0x00, 0x00, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xeb, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xee, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xf1, 0x05, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, + 0x70, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf2, 0x05, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf5, 0x05, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf8, 0x05, 0x00, 0x00, + 0x62, 0x6c, 0x65, 0x6e, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xff, 0x05, 0x00, 0x00, 0x62, 0x6c, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x72, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x0a, 0x06, 0x00, 0x00, 0x62, 0x6c, 0x65, 0x6e, 0x64, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1b, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x51, 0x06, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x54, 0x06, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, + 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x56, 0x06, 0x00, 0x00, 0x76, 0x42, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, + 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x58, 0x06, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, + 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x59, 0x06, 0x00, 0x00, 0x76, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, + 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x5b, 0x06, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x5c, 0x06, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, + 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x63, 0x06, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x64, 0x06, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x69, 0x06, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x6a, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x7f, 0x06, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x88, 0x06, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x04, 0x00, 0x98, 0x06, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9c, 0x06, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9d, 0x06, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9f, 0x06, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa1, 0x06, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xa8, 0x06, 0x00, 0x00, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xb3, 0x06, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xbe, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc2, 0x06, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdd, 0x06, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe6, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe8, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xec, 0x06, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xed, 0x06, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, + 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xee, 0x06, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xf1, 0x06, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x44, 0x6f, 0x74, 0x00, 0x05, 0x00, 0x06, 0x00, 0xf6, 0x06, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0xf7, 0x06, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, + 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xfe, 0x06, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xe0, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xae, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xae, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x04, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x5c, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x66, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x66, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x6d, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9f, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xac, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xac, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xae, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xb6, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xb6, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xb8, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc4, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xc4, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xd4, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xde, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xde, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe6, 0x05, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe6, 0x05, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xf2, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xf8, 0x05, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, - 0x14, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, - 0x1f, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, - 0x26, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x21, 0x00, 0x06, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x21, 0x00, 0x06, 0x00, 0x39, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, - 0x3f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x21, 0x00, 0x09, 0x00, - 0x43, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x06, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x52, 0x00, 0x00, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x59, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x07, 0x00, 0x17, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, - 0x1e, 0x00, 0x11, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x6a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, - 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x70, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x73, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x6e, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x6e, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, - 0x6e, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x6e, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x10, 0x00, 0xa7, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, - 0xa3, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0xa7, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xa8, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xdc, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, - 0x39, 0xd6, 0x4f, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, - 0x4c, 0x77, 0x9c, 0x42, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, - 0x42, 0xe0, 0x16, 0x42, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, - 0xf3, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x8c, 0xee, 0x2a, 0x47, 0x2b, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0xbd, 0x37, 0x86, 0x35, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x44, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0xdb, 0x0f, 0xc9, 0x40, 0x20, 0x00, 0x04, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3e, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, 0x2b, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, - 0x8d, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1b, 0x00, 0x03, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x8f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x8f, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, - 0x9b, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc2, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe2, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe8, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x66, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x66, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x69, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x6f, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x6f, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x71, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x87, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x8e, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x8f, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x60, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x92, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xa0, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xa9, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xb7, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xba, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x1a, 0x02, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x6a, 0x02, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x6a, 0x02, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x75, 0x02, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x75, 0x02, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xca, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x51, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x56, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x59, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x5c, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x64, 0x06, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x64, 0x06, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xed, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xf7, 0x06, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0xf7, 0x06, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x06, 0x00, 0x40, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x46, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x4a, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0a, 0x00, 0x4b, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x06, 0x00, 0x55, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x5b, 0x00, 0x00, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x62, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x65, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x65, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, + 0x1e, 0x00, 0x11, 0x00, 0x73, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x74, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x74, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x77, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x77, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xab, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, 0xac, 0x00, 0x00, 0x00, + 0xab, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xcd, 0xcc, 0x0c, 0x40, 0x2c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, + 0xb5, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xc3, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0xdc, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x17, 0xb7, 0xd1, 0x38, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0xdb, 0x0f, 0x49, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3d, + 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x3d, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x49, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x83, 0xf9, 0xa2, 0x3e, 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, + 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x69, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, + 0x6f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x77, 0x00, 0x00, 0x00, + 0x8c, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x8e, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x8f, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x8d, 0x01, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x91, 0x01, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x93, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x93, 0x01, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x9e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x9f, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0xa9, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0xb7, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x68, 0x01, 0x00, 0x00, + 0xba, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd8, 0x01, 0x00, 0x00, 0x39, 0xd6, 0x4f, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd9, 0x01, 0x00, 0x00, 0x4c, 0x77, 0x9c, 0x42, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xda, 0x01, 0x00, 0x00, 0x42, 0xe0, 0x16, 0x42, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdb, 0x01, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0x8c, 0xee, 0x2a, 0x47, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, + 0xea, 0x01, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0xbd, 0x37, 0x86, 0x35, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x44, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xdb, 0x0f, 0xc9, 0x40, 0x20, 0x00, 0x04, 0x00, + 0x19, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x19, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3e, 0x2b, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, + 0x67, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1b, 0x00, 0x03, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x9d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x9d, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x2c, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, - 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0xec, 0x01, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, - 0xec, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x05, 0x02, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, 0x17, 0xb7, 0xd1, 0x38, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0xac, 0xc5, 0x27, 0x37, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x80, 0x47, 0x17, 0x00, 0x04, 0x00, - 0xa0, 0x02, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, - 0x8a, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x40, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, - 0x42, 0x05, 0xa7, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, - 0xac, 0xc5, 0xcf, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, - 0x46, 0x03, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x50, 0x03, 0x00, 0x00, 0xad, 0x13, 0x57, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x51, 0x03, 0x00, 0x00, 0x1c, 0xb1, 0x96, 0xbd, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x52, 0x03, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x6b, 0x27, 0x32, 0xbf, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, 0xdc, 0x0d, 0xea, 0x3e, 0x2c, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, 0xac, 0x39, 0x50, 0xbe, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, 0x3e, 0xe7, 0x1e, 0x3f, - 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, - 0x65, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaf, 0x03, 0x00, 0x00, - 0x00, 0x00, 0xa0, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb7, 0x03, 0x00, 0x00, - 0xea, 0x5b, 0x76, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, - 0xa0, 0xa9, 0x47, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, - 0xb7, 0x03, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc1, 0x03, 0x00, 0x00, 0xf1, 0x65, 0xf2, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc2, 0x03, 0x00, 0x00, 0xf8, 0xc5, 0xf5, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xc3, 0x03, 0x00, 0x00, 0xc1, 0x03, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x12, 0xfb, 0x04, 0x3f, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, 0x8e, 0x5b, 0x44, 0x3f, 0x2c, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, 0x7d, 0xe9, 0x3d, 0x3e, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, 0xc6, 0xa3, 0x64, 0xbf, - 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, - 0xd6, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x10, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, - 0x39, 0x8e, 0xe3, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, - 0xff, 0xe6, 0x01, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, - 0x41, 0xf1, 0x83, 0x3d, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, - 0x29, 0x04, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x33, 0x04, 0x00, 0x00, 0xc8, 0x7b, 0x65, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x34, 0x04, 0x00, 0x00, 0xb2, 0x2d, 0xd3, 0x3e, 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x35, 0x04, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x52, 0xd5, 0xa4, 0xbe, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, 0xdb, 0xbf, 0x6e, 0xbf, 0x2c, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, 0x9c, 0xa3, 0x4a, 0xbf, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x32, 0x03, 0x19, 0xbf, - 0x2c, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, - 0x48, 0x04, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x04, 0x00, 0x00, - 0xd9, 0x89, 0x9d, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x3e, 0x01, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x05, 0x00, 0x00, - 0x0a, 0xd7, 0xa3, 0x3c, 0x20, 0x00, 0x04, 0x00, 0x52, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x52, 0x05, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x52, 0x05, 0x00, 0x00, 0x56, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x52, 0x05, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, 0x5b, 0x05, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x5b, 0x05, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0x5d, 0x05, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x5c, 0x05, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x5d, 0x05, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, - 0x20, 0x00, 0x04, 0x00, 0x9a, 0x05, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x9f, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0xac, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0xb6, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xb8, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0xc4, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x6c, 0x05, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0xde, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x9d, 0x01, 0x00, 0x00, 0xe6, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0xf1, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0xf1, 0x05, 0x00, 0x00, 0xf2, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x6c, 0x05, 0x00, 0x00, 0xf8, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x03, 0x00, 0x68, 0x02, 0x00, 0x00, 0x67, 0x02, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x69, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x69, 0x02, 0x00, 0x00, 0x6a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0xad, 0x00, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x42, 0x05, 0xa7, 0xbe, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, 0xac, 0xc5, 0xcf, 0xbe, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, 0xad, 0x13, 0x57, 0xbf, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x1c, 0xb1, 0x96, 0xbd, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, + 0xcb, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, + 0x6b, 0x27, 0x32, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, + 0xdc, 0x0d, 0xea, 0x3e, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, + 0xd0, 0x02, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd6, 0x02, 0x00, 0x00, 0xac, 0x39, 0x50, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd7, 0x02, 0x00, 0x00, 0x3e, 0xe7, 0x1e, 0x3f, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xd8, 0x02, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, 0xea, 0x5b, 0x76, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdd, 0x02, 0x00, 0x00, 0xa0, 0xa9, 0x47, 0xbe, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, 0xdd, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0xf1, 0x65, 0xf2, 0x3e, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0xf8, 0xc5, 0xf5, 0xbe, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe4, 0x02, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, + 0xe3, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, + 0x12, 0xfb, 0x04, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, + 0x8e, 0x5b, 0x44, 0x3f, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, + 0xe8, 0x02, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xee, 0x02, 0x00, 0x00, 0x7d, 0xe9, 0x3d, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xef, 0x02, 0x00, 0x00, 0xc6, 0xa3, 0x64, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xf0, 0x02, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc1, 0x03, 0x00, 0x00, 0xac, 0xc5, 0x27, 0x37, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x47, 0x17, 0x00, 0x04, 0x00, + 0xdb, 0x03, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, + 0xc5, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x40, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x05, 0x00, 0x00, + 0x39, 0x8e, 0xe3, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x05, 0x00, 0x00, + 0xff, 0xe6, 0x01, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, + 0x41, 0xf1, 0x83, 0x3d, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x05, 0x00, 0x00, + 0x34, 0x05, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3a, 0x05, 0x00, 0x00, 0xc8, 0x7b, 0x65, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3b, 0x05, 0x00, 0x00, 0xb2, 0x2d, 0xd3, 0x3e, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x3c, 0x05, 0x00, 0x00, 0x3a, 0x05, 0x00, 0x00, 0x3b, 0x05, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, 0x52, 0xd5, 0xa4, 0xbe, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x41, 0x05, 0x00, 0x00, 0xdb, 0xbf, 0x6e, 0xbf, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, 0x41, 0x05, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x46, 0x05, 0x00, 0x00, 0x9c, 0xa3, 0x4a, 0xbf, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x05, 0x00, 0x00, 0x32, 0x03, 0x19, 0xbf, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, 0x46, 0x05, 0x00, 0x00, + 0x47, 0x05, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x05, 0x00, 0x00, + 0xd9, 0x89, 0x9d, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x02, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x06, 0x00, 0x00, + 0x0a, 0xd7, 0xa3, 0x3c, 0x20, 0x00, 0x04, 0x00, 0x50, 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x50, 0x06, 0x00, 0x00, 0x51, 0x06, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, 0x52, 0x06, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x55, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x55, 0x06, 0x00, 0x00, 0x56, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x55, 0x06, 0x00, 0x00, 0x59, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x55, 0x06, 0x00, 0x00, 0x5c, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, 0x64, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, + 0x3b, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xed, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xad, 0x00, 0x00, 0x00, 0xf7, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x55, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x65, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x87, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xa4, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa9, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xcd, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xcf, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xd3, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xd5, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xdd, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xe5, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x5d, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x27, 0x05, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0x27, 0x05, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x29, 0x05, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, - 0xad, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x2b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x29, 0x05, 0x00, 0x00, 0x2a, 0x05, 0x00, 0x00, 0x2b, 0x05, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x2a, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0x2c, 0x05, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2d, 0x05, 0x00, 0x00, 0x2c, 0x05, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2e, 0x05, 0x00, 0x00, 0x2d, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2f, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x2e, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x30, 0x05, 0x00, 0x00, - 0xc2, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x31, 0x05, 0x00, 0x00, 0x30, 0x05, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x32, 0x05, 0x00, 0x00, 0x31, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x33, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x32, 0x05, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x05, 0x00, 0x00, 0x2f, 0x05, 0x00, 0x00, - 0x33, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, - 0xc9, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x36, 0x05, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x37, 0x05, 0x00, 0x00, 0x36, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x38, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x37, 0x05, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x39, 0x05, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3a, 0x05, 0x00, 0x00, - 0x39, 0x05, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x05, 0x00, 0x00, - 0x3a, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3b, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3d, 0x05, 0x00, 0x00, 0x38, 0x05, 0x00, 0x00, 0x3c, 0x05, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3e, 0x05, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x05, 0x00, 0x00, - 0x3e, 0x05, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, - 0x3f, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x05, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, - 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x05, 0x00, 0x00, 0x43, 0x05, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x45, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x44, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x46, 0x05, 0x00, 0x00, 0x41, 0x05, 0x00, 0x00, 0x45, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x47, 0x05, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, 0x47, 0x05, 0x00, 0x00, - 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x05, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x49, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0x4b, 0x05, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, 0x4b, 0x05, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4d, 0x05, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4e, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x4d, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x05, 0x00, 0x00, - 0x4a, 0x05, 0x00, 0x00, 0x4e, 0x05, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x50, 0x05, 0x00, 0x00, 0x34, 0x05, 0x00, 0x00, 0x3d, 0x05, 0x00, 0x00, 0x46, 0x05, 0x00, 0x00, - 0x4f, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0x2b, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2b, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x54, 0x05, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x51, 0x05, 0x00, 0x00, 0x54, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, 0x56, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x55, 0x05, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x5a, 0x05, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x05, 0x00, 0x00, - 0x5a, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x60, 0x05, 0x00, 0x00, - 0x5e, 0x05, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0x60, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x62, 0x05, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x64, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x62, 0x05, 0x00, 0x00, 0x63, 0x05, 0x00, 0x00, 0x64, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x63, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x67, 0x05, 0x00, 0x00, - 0x66, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, - 0x51, 0x05, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x69, 0x05, 0x00, 0x00, - 0x67, 0x05, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x6a, 0x05, 0x00, 0x00, 0x69, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x65, 0x05, 0x00, 0x00, 0x6a, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x6f, 0x05, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x05, 0x00, 0x00, - 0x6f, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x71, 0x05, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x72, 0x05, 0x00, 0x00, - 0x65, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, - 0x5e, 0x05, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x74, 0x05, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x72, 0x05, 0x00, 0x00, 0x74, 0x05, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, - 0xb7, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x77, 0x05, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x78, 0x05, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x77, 0x05, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x79, 0x05, 0x00, 0x00, 0x71, 0x05, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x79, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7a, 0x05, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7b, 0x05, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, 0x7b, 0x05, 0x00, 0x00, 0x7a, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x51, 0x05, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7d, 0x05, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x55, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x7d, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x55, 0x05, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x81, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x82, 0x05, 0x00, 0x00, 0x81, 0x05, 0x00, 0x00, 0x80, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x58, 0x05, 0x00, 0x00, 0x82, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x64, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x64, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x9c, 0x01, 0x00, 0x00, 0x84, 0x05, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x86, 0x05, 0x00, 0x00, 0x84, 0x05, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x83, 0x05, 0x00, 0x00, 0x86, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x88, 0x05, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x05, 0x00, 0x00, 0x88, 0x05, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8a, 0x05, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x05, 0x00, 0x00, - 0x8a, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8c, 0x05, 0x00, 0x00, - 0x83, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8d, 0x05, 0x00, 0x00, 0x8c, 0x05, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x8e, 0x05, 0x00, 0x00, 0x89, 0x05, 0x00, 0x00, 0x8b, 0x05, 0x00, 0x00, 0x8d, 0x05, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0x8e, 0x05, 0x00, 0x00, - 0x8f, 0x05, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x87, 0x05, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x5f, 0x05, 0x00, 0x00, 0x94, 0x05, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x95, 0x05, 0x00, 0x00, - 0x94, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x96, 0x05, 0x00, 0x00, - 0x95, 0x05, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x99, 0x05, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x96, 0x05, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, - 0x9e, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x98, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x9a, 0x05, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x9c, 0x05, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, - 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9d, 0x05, 0x00, 0x00, 0x9c, 0x05, 0x00, 0x00, - 0x9c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x97, 0x05, 0x00, 0x00, 0x9d, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x99, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9e, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xa1, 0x05, 0x00, 0x00, 0x9f, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xa0, 0x05, 0x00, 0x00, 0xa1, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xa2, 0x05, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x97, 0x05, 0x00, 0x00, 0xa2, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x99, 0x05, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x99, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xa3, 0x05, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x93, 0x05, 0x00, 0x00, - 0xa3, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa5, 0x05, 0x00, 0x00, - 0x93, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa6, 0x05, 0x00, 0x00, - 0x87, 0x05, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa7, 0x05, 0x00, 0x00, - 0xa5, 0x05, 0x00, 0x00, 0xa6, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa8, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xa7, 0x05, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa4, 0x05, 0x00, 0x00, 0xa8, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaa, 0x05, 0x00, 0x00, 0xa4, 0x05, 0x00, 0x00, - 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xab, 0x05, 0x00, 0x00, 0xaa, 0x05, 0x00, 0x00, - 0xaa, 0x05, 0x00, 0x00, 0xaa, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x05, 0x00, 0x00, - 0xab, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, 0xad, 0x05, 0x00, 0x00, - 0xac, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xaf, 0x05, 0x00, 0x00, - 0xae, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb0, 0x05, 0x00, 0x00, - 0xaf, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xb1, 0x05, 0x00, 0x00, 0xb0, 0x05, 0x00, 0x00, 0xaf, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xb2, 0x05, 0x00, 0x00, 0xad, 0x05, 0x00, 0x00, - 0xb1, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb3, 0x05, 0x00, 0x00, - 0xb2, 0x05, 0x00, 0x00, 0xb2, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, - 0xa9, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb5, 0x05, 0x00, 0x00, - 0xb4, 0x05, 0x00, 0x00, 0xb3, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x05, 0x00, 0x00, - 0xb5, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, 0xb7, 0x05, 0x00, 0x00, - 0xb6, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xb9, 0x05, 0x00, 0x00, - 0xb8, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x05, 0x00, 0x00, - 0xb9, 0x05, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xbb, 0x05, 0x00, 0x00, 0xba, 0x05, 0x00, 0x00, 0xb9, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xbc, 0x05, 0x00, 0x00, 0xb7, 0x05, 0x00, 0x00, - 0xbb, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbd, 0x05, 0x00, 0x00, - 0xbc, 0x05, 0x00, 0x00, 0xbc, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbe, 0x05, 0x00, 0x00, - 0xa9, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbf, 0x05, 0x00, 0x00, - 0xbe, 0x05, 0x00, 0x00, 0xbd, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x05, 0x00, 0x00, - 0xbf, 0x05, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc1, 0x05, 0x00, 0x00, - 0xa9, 0x05, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc2, 0x05, 0x00, 0x00, - 0xc1, 0x05, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x05, 0x00, 0x00, - 0xc2, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, 0xc5, 0x05, 0x00, 0x00, - 0xc4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xc6, 0x05, 0x00, 0x00, - 0x55, 0x05, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xc7, 0x05, 0x00, 0x00, - 0xc5, 0x05, 0x00, 0x00, 0xc6, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xc8, 0x05, 0x00, 0x00, 0xc7, 0x05, 0x00, 0x00, 0xc7, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0xc9, 0x05, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xcb, 0x05, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, 0xc8, 0x05, 0x00, 0x00, 0xcb, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc3, 0x05, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xcd, 0x05, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0xd0, 0x05, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd1, 0x05, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xcf, 0x05, 0x00, 0x00, 0xd1, 0x05, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xd2, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0xcd, 0x05, 0x00, 0x00, 0xcf, 0x05, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc3, 0x05, 0x00, 0x00, 0xd2, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xd6, 0x05, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd5, 0x05, 0x00, 0x00, 0xd6, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xd7, 0x05, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xd5, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd3, 0x05, 0x00, 0x00, 0xd7, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xd9, 0x05, 0x00, 0x00, 0xd3, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xda, 0x05, 0x00, 0x00, 0x87, 0x05, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xdb, 0x05, 0x00, 0x00, 0xd9, 0x05, 0x00, 0x00, 0xda, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xdb, 0x05, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd8, 0x05, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, - 0xdf, 0x05, 0x00, 0x00, 0xde, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xe0, 0x05, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xe1, 0x05, 0x00, 0x00, 0xe0, 0x05, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xe2, 0x05, 0x00, 0x00, 0xdf, 0x05, 0x00, 0x00, 0xe1, 0x05, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x05, 0x00, 0x00, 0xe2, 0x05, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe4, 0x05, 0x00, 0x00, - 0xe3, 0x05, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x05, 0x00, 0x00, - 0xe4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, 0xe7, 0x05, 0x00, 0x00, - 0xe6, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe8, 0x05, 0x00, 0x00, - 0x58, 0x05, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe9, 0x05, 0x00, 0x00, - 0xe7, 0x05, 0x00, 0x00, 0xe8, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xea, 0x05, 0x00, 0x00, 0xe9, 0x05, 0x00, 0x00, 0xe9, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0xeb, 0x05, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xec, 0x05, 0x00, 0x00, 0xeb, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0xec, 0x05, 0x00, 0x00, 0xec, 0x05, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x54, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x58, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x5b, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x63, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x69, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x6a, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x7f, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x88, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x98, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x9c, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x9d, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x9f, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xa1, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xa8, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xb3, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xb7, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xbe, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xc2, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xdd, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xe6, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xe8, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xec, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xee, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xf6, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x66, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x21, 0x06, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x06, 0x00, 0x00, 0x21, 0x06, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x23, 0x06, 0x00, 0x00, 0x22, 0x06, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x25, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x23, 0x06, 0x00, 0x00, 0x24, 0x06, 0x00, 0x00, 0x25, 0x06, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x24, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, + 0x26, 0x06, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x27, 0x06, 0x00, 0x00, 0x26, 0x06, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x28, 0x06, 0x00, 0x00, 0x27, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x29, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x28, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, 0x2a, 0x06, 0x00, 0x00, + 0xa9, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2b, 0x06, 0x00, 0x00, 0x2a, 0x06, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2c, 0x06, 0x00, 0x00, 0x2b, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2d, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2c, 0x06, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x06, 0x00, 0x00, 0x29, 0x06, 0x00, 0x00, + 0x2d, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, 0x2f, 0x06, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x30, 0x06, 0x00, 0x00, 0x2f, 0x06, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x31, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x32, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x31, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, 0x33, 0x06, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x06, 0x00, 0x00, + 0x33, 0x06, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x06, 0x00, 0x00, + 0x34, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x06, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x35, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x37, 0x06, 0x00, 0x00, 0x32, 0x06, 0x00, 0x00, 0x36, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, 0x38, 0x06, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x39, 0x06, 0x00, 0x00, + 0x38, 0x06, 0x00, 0x00, 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3a, 0x06, 0x00, 0x00, + 0x39, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x06, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3a, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x19, 0x02, 0x00, 0x00, 0x3c, 0x06, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x06, 0x00, 0x00, 0x3c, 0x06, 0x00, 0x00, + 0xd0, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x06, 0x00, 0x00, 0x3d, 0x06, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x3e, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x40, 0x06, 0x00, 0x00, 0x3b, 0x06, 0x00, 0x00, 0x3f, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x19, 0x02, 0x00, 0x00, 0x41, 0x06, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x06, 0x00, 0x00, 0x41, 0x06, 0x00, 0x00, + 0xcf, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x06, 0x00, 0x00, 0x42, 0x06, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x43, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x19, 0x02, 0x00, 0x00, + 0x45, 0x06, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x46, 0x06, 0x00, 0x00, 0x45, 0x06, 0x00, 0x00, 0xd0, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x47, 0x06, 0x00, 0x00, 0x46, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x48, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x47, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x06, 0x00, 0x00, + 0x44, 0x06, 0x00, 0x00, 0x48, 0x06, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x4a, 0x06, 0x00, 0x00, 0x2e, 0x06, 0x00, 0x00, 0x37, 0x06, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, + 0x49, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x66, 0x00, 0x00, 0x00, 0x4a, 0x06, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x25, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x25, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x4b, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4c, 0x06, 0x00, 0x00, 0x4b, 0x06, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x4d, 0x06, 0x00, 0x00, 0x4c, 0x06, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x4f, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x4d, 0x06, 0x00, 0x00, + 0x4e, 0x06, 0x00, 0x00, 0x4f, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4e, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x51, 0x06, 0x00, 0x00, 0x52, 0x06, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x4f, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x57, 0x06, 0x00, 0x00, 0x56, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x54, 0x06, 0x00, 0x00, + 0x57, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5a, 0x06, 0x00, 0x00, + 0x59, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x06, 0x00, 0x00, 0x5a, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5d, 0x06, 0x00, 0x00, 0x5c, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x5b, 0x06, 0x00, 0x00, 0x5d, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0xc6, 0x00, 0x00, 0x00, 0x5e, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5f, 0x06, 0x00, 0x00, + 0x5e, 0x06, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x60, 0x06, 0x00, 0x00, + 0x5f, 0x06, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x62, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x60, 0x06, 0x00, 0x00, 0x61, 0x06, 0x00, 0x00, + 0x62, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x61, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x65, 0x06, 0x00, 0x00, 0x64, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x66, 0x06, 0x00, 0x00, 0x54, 0x06, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x67, 0x06, 0x00, 0x00, 0x65, 0x06, 0x00, 0x00, 0x66, 0x06, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x06, 0x00, 0x00, 0x67, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x63, 0x06, 0x00, 0x00, 0x68, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6b, 0x06, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6a, 0x06, 0x00, 0x00, 0x6b, 0x06, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x6c, 0x06, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6a, 0x06, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x06, 0x00, 0x00, 0x6c, 0x06, 0x00, 0x00, + 0x6c, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6e, 0x06, 0x00, 0x00, 0x63, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0xc6, 0x00, 0x00, 0x00, 0x6f, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x70, 0x06, 0x00, 0x00, + 0x6f, 0x06, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x71, 0x06, 0x00, 0x00, + 0x6e, 0x06, 0x00, 0x00, 0x70, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, + 0x72, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x06, 0x00, 0x00, 0x72, 0x06, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x74, 0x06, 0x00, 0x00, 0x71, 0x06, 0x00, 0x00, + 0x73, 0x06, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x75, 0x06, 0x00, 0x00, + 0x6d, 0x06, 0x00, 0x00, 0x74, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x69, 0x06, 0x00, 0x00, + 0x75, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x76, 0x06, 0x00, 0x00, + 0x69, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x77, 0x06, 0x00, 0x00, + 0x54, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x78, 0x06, 0x00, 0x00, + 0x77, 0x06, 0x00, 0x00, 0x76, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x54, 0x06, 0x00, 0x00, + 0x78, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x79, 0x06, 0x00, 0x00, + 0x69, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7a, 0x06, 0x00, 0x00, + 0x58, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7b, 0x06, 0x00, 0x00, + 0x7a, 0x06, 0x00, 0x00, 0x79, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x06, 0x00, 0x00, + 0x7b, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7c, 0x06, 0x00, 0x00, + 0x69, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7d, 0x06, 0x00, 0x00, + 0x5b, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7e, 0x06, 0x00, 0x00, + 0x7d, 0x06, 0x00, 0x00, 0x7c, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5b, 0x06, 0x00, 0x00, + 0x7e, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x62, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x62, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, 0x80, 0x06, 0x00, 0x00, + 0x64, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x81, 0x06, 0x00, 0x00, + 0x54, 0x06, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x82, 0x06, 0x00, 0x00, + 0x80, 0x06, 0x00, 0x00, 0x81, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7f, 0x06, 0x00, 0x00, + 0x82, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x83, 0x06, 0x00, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x84, 0x06, 0x00, 0x00, 0x83, 0x06, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x85, 0x06, 0x00, 0x00, 0x84, 0x06, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x87, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x85, 0x06, 0x00, 0x00, 0x86, 0x06, 0x00, 0x00, 0x87, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x86, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x89, 0x06, 0x00, 0x00, + 0x7f, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8a, 0x06, 0x00, 0x00, + 0x89, 0x06, 0x00, 0x00, 0x89, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8c, 0x06, 0x00, 0x00, + 0x8a, 0x06, 0x00, 0x00, 0x8b, 0x06, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x8d, 0x06, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8e, 0x06, 0x00, 0x00, 0x8c, 0x06, 0x00, 0x00, + 0x8d, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x88, 0x06, 0x00, 0x00, 0x8e, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x8f, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x90, 0x06, 0x00, 0x00, 0x8f, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x91, 0x06, 0x00, 0x00, 0x88, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x92, 0x06, 0x00, 0x00, 0x91, 0x06, 0x00, 0x00, 0x91, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x93, 0x06, 0x00, 0x00, + 0x92, 0x06, 0x00, 0x00, 0x90, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x94, 0x06, 0x00, 0x00, 0x88, 0x06, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x95, 0x06, 0x00, 0x00, 0x93, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x94, 0x06, 0x00, 0x00, 0x95, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x96, 0x06, 0x00, 0x00, 0x88, 0x06, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x06, 0x00, 0x00, 0x93, 0x06, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x96, 0x06, 0x00, 0x00, 0x97, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x99, 0x06, 0x00, 0x00, 0x88, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x98, 0x06, 0x00, 0x00, 0x99, 0x06, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x9a, 0x06, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x98, 0x06, 0x00, 0x00, + 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9b, 0x06, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9c, 0x06, 0x00, 0x00, 0x9a, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x9e, 0x06, 0x00, 0x00, 0x58, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x9d, 0x06, 0x00, 0x00, 0x9e, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xa0, 0x06, 0x00, 0x00, 0x5b, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9f, 0x06, 0x00, 0x00, + 0xa0, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa1, 0x06, 0x00, 0x00, 0x9b, 0x06, 0x00, 0x00, + 0x39, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa2, 0x06, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x9c, 0x06, 0x00, 0x00, 0x9d, 0x06, 0x00, 0x00, 0x9f, 0x06, 0x00, 0x00, 0xa1, 0x06, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa3, 0x06, 0x00, 0x00, 0xa2, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x00, + 0xa2, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa5, 0x06, 0x00, 0x00, 0xa2, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xa6, 0x06, 0x00, 0x00, 0xa3, 0x06, 0x00, 0x00, 0xa4, 0x06, 0x00, 0x00, + 0xa5, 0x06, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x51, 0x06, 0x00, 0x00, + 0xa6, 0x06, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x87, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa9, 0x06, 0x00, 0x00, 0x7f, 0x06, 0x00, 0x00, + 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaa, 0x06, 0x00, 0x00, + 0xa9, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xab, 0x06, 0x00, 0x00, + 0x7f, 0x06, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xac, 0x06, 0x00, 0x00, 0xab, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xad, 0x06, 0x00, 0x00, 0x7f, 0x06, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xae, 0x06, 0x00, 0x00, 0xad, 0x06, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xaf, 0x06, 0x00, 0x00, 0xaa, 0x06, 0x00, 0x00, 0xac, 0x06, 0x00, 0x00, + 0xae, 0x06, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb0, 0x06, 0x00, 0x00, + 0xaf, 0x06, 0x00, 0x00, 0x8b, 0x06, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb1, 0x06, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb2, 0x06, 0x00, 0x00, 0xb0, 0x06, 0x00, 0x00, + 0xb1, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa8, 0x06, 0x00, 0x00, 0xb2, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xb4, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xd3, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb5, 0x06, 0x00, 0x00, 0xb4, 0x06, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xb6, 0x06, 0x00, 0x00, 0xb5, 0x06, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xb9, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xb6, 0x06, 0x00, 0x00, + 0xb8, 0x06, 0x00, 0x00, 0xbd, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb8, 0x06, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xba, 0x06, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xdb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xbb, 0x06, 0x00, 0x00, + 0xba, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbc, 0x06, 0x00, 0x00, + 0xbb, 0x06, 0x00, 0x00, 0xbb, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb7, 0x06, 0x00, 0x00, 0xbc, 0x06, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xb9, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xbd, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbf, 0x06, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xbe, 0x06, 0x00, 0x00, 0xbf, 0x06, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xbe, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb7, 0x06, 0x00, 0x00, 0xc0, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xb9, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb9, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xc1, 0x06, 0x00, 0x00, 0xb7, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb3, 0x06, 0x00, 0x00, 0xc1, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xc3, 0x06, 0x00, 0x00, 0xb3, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xc4, 0x06, 0x00, 0x00, 0xa8, 0x06, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc5, 0x06, 0x00, 0x00, 0xc3, 0x06, 0x00, 0x00, 0xc4, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc6, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xc5, 0x06, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc2, 0x06, 0x00, 0x00, + 0xc6, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x06, 0x00, 0x00, + 0xc2, 0x06, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc9, 0x06, 0x00, 0x00, + 0xc8, 0x06, 0x00, 0x00, 0xc8, 0x06, 0x00, 0x00, 0xc8, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc7, 0x06, 0x00, 0x00, 0xc9, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, + 0xca, 0x06, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xcb, 0x06, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xcc, 0x06, 0x00, 0x00, 0xcb, 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xcd, 0x06, 0x00, 0x00, 0xcc, 0x06, 0x00, 0x00, 0xcb, 0x06, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0xce, 0x06, 0x00, 0x00, + 0xca, 0x06, 0x00, 0x00, 0xcd, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xcf, 0x06, 0x00, 0x00, 0xce, 0x06, 0x00, 0x00, 0xce, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd0, 0x06, 0x00, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd1, 0x06, 0x00, 0x00, 0xd0, 0x06, 0x00, 0x00, 0xcf, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc7, 0x06, 0x00, 0x00, 0xd1, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, + 0xd2, 0x06, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xd3, 0x06, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd4, 0x06, 0x00, 0x00, 0xd3, 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xd5, 0x06, 0x00, 0x00, 0xd4, 0x06, 0x00, 0x00, 0xd3, 0x06, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0xd6, 0x06, 0x00, 0x00, + 0xd2, 0x06, 0x00, 0x00, 0xd5, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd7, 0x06, 0x00, 0x00, 0xd6, 0x06, 0x00, 0x00, 0xd6, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd8, 0x06, 0x00, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd9, 0x06, 0x00, 0x00, 0xd8, 0x06, 0x00, 0x00, 0xd7, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc7, 0x06, 0x00, 0x00, 0xd9, 0x06, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xda, 0x06, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdb, 0x06, 0x00, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdc, 0x06, 0x00, 0x00, 0xdb, 0x06, 0x00, 0x00, 0xda, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc7, 0x06, 0x00, 0x00, 0xdc, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, + 0xde, 0x06, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xdf, 0x06, 0x00, 0x00, 0x58, 0x06, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xe0, 0x06, 0x00, 0x00, 0xde, 0x06, 0x00, 0x00, 0xdf, 0x06, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe1, 0x06, 0x00, 0x00, 0xe0, 0x06, 0x00, 0x00, 0xe0, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x79, 0x01, 0x00, 0x00, 0xe2, 0x06, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xe3, 0x06, 0x00, 0x00, 0xe2, 0x06, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe4, 0x06, 0x00, 0x00, 0xe3, 0x06, 0x00, 0x00, + 0xe3, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe5, 0x06, 0x00, 0x00, 0xe1, 0x06, 0x00, 0x00, + 0xe4, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x06, 0x00, 0x00, 0xe5, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x06, 0x00, 0x00, 0xdd, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe6, 0x06, 0x00, 0x00, 0xe7, 0x06, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x19, 0x02, 0x00, 0x00, 0xe9, 0x06, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xea, 0x06, 0x00, 0x00, 0xe9, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe8, 0x06, 0x00, 0x00, 0xea, 0x06, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xeb, 0x06, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0xe6, 0x06, 0x00, 0x00, + 0xe8, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x06, 0x00, 0x00, 0xeb, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xef, 0x06, 0x00, 0x00, 0xed, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xee, 0x06, 0x00, 0x00, 0xef, 0x06, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf0, 0x06, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xee, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xec, 0x06, 0x00, 0x00, 0xf0, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf2, 0x06, 0x00, 0x00, 0xec, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf3, 0x06, 0x00, 0x00, 0xa8, 0x06, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf4, 0x06, 0x00, 0x00, 0xf2, 0x06, 0x00, 0x00, 0xf3, 0x06, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf5, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xf4, 0x06, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x06, 0x00, 0x00, 0xf5, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0xf8, 0x06, 0x00, 0x00, 0xf7, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf9, 0x06, 0x00, 0x00, 0xf1, 0x06, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xfa, 0x06, 0x00, 0x00, 0xf9, 0x06, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0xfb, 0x06, 0x00, 0x00, 0xf8, 0x06, 0x00, 0x00, + 0xfa, 0x06, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfc, 0x06, 0x00, 0x00, + 0xfb, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfd, 0x06, 0x00, 0x00, 0xfc, 0x06, 0x00, 0x00, 0x8b, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf6, 0x06, 0x00, 0x00, 0xfd, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, + 0xff, 0x06, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x00, 0x00, 0x5b, 0x06, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x01, 0x07, 0x00, 0x00, 0xff, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x79, 0x01, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x04, 0x07, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x05, 0x07, 0x00, 0x00, 0x04, 0x07, 0x00, 0x00, + 0x04, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x07, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, + 0x05, 0x07, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, + 0xf6, 0x06, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x07, 0x00, 0x00, + 0x06, 0x07, 0x00, 0x00, 0x07, 0x07, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfe, 0x06, 0x00, 0x00, + 0x08, 0x07, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x07, 0x00, 0x00, + 0xdd, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0a, 0x07, 0x00, 0x00, + 0xfe, 0x06, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0b, 0x07, 0x00, 0x00, + 0x09, 0x07, 0x00, 0x00, 0x0a, 0x07, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0c, 0x07, 0x00, 0x00, 0xc7, 0x06, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0d, 0x07, 0x00, 0x00, 0x0b, 0x07, 0x00, 0x00, 0x0c, 0x07, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0e, 0x07, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0f, 0x07, 0x00, 0x00, 0x0d, 0x07, 0x00, 0x00, 0x0e, 0x07, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x07, 0x00, 0x00, 0x0f, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x07, 0x00, 0x00, + 0x0f, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x12, 0x07, 0x00, 0x00, 0x0f, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x13, 0x07, 0x00, 0x00, 0x10, 0x07, 0x00, 0x00, 0x11, 0x07, 0x00, 0x00, + 0x12, 0x07, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x51, 0x06, 0x00, 0x00, + 0x13, 0x07, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, + 0x6a, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, + 0x6d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7b, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x80, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x86, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x87, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, + 0x89, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x87, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, + 0x75, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x95, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9f, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x9a, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, + 0xa5, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xa7, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, + 0xae, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, + 0xaf, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb2, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb4, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, + 0xb3, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xaa, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc8, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc9, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xc9, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xbe, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0xc6, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, + 0xce, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0xcd, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0x9b, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x00, 0x00, 0x00, + 0xd1, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xd7, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xd9, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xde, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xda, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xe0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, + 0xe2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe3, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0xe3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xda, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xda, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe9, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe7, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xed, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xee, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xef, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf0, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf1, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xec, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf3, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf4, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf5, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xf5, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0xf6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, + 0xe7, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfb, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x07, 0x01, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x09, 0x01, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x06, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0c, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0d, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0a, 0x01, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0f, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x11, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x13, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, + 0x14, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x17, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, + 0x16, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, + 0x1b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, + 0x1c, 0x01, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1f, 0x01, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x18, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x21, 0x01, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x22, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, + 0x24, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, + 0x25, 0x01, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x20, 0x01, 0x00, 0x00, + 0x27, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, + 0x20, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2d, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x34, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, + 0x33, 0x01, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x37, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3a, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x28, 0x01, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, + 0x40, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, + 0x41, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0x45, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, + 0x49, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, + 0x46, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x4c, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x43, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4e, 0x01, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4f, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x50, 0x01, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, + 0x54, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, + 0xf7, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, + 0x55, 0x01, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x58, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, + 0x16, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x58, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x5e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, + 0x5d, 0x01, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x61, 0x01, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, + 0x6a, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x6d, 0x01, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x06, 0x00, 0x20, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, + 0x72, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x75, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, 0xea, 0x05, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xef, 0x05, 0x00, 0x00, 0xdd, 0x05, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf0, 0x05, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, - 0xef, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe5, 0x05, 0x00, 0x00, 0xf0, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf3, 0x05, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0xe5, 0x05, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf5, 0x05, 0x00, 0x00, 0xf3, 0x05, 0x00, 0x00, - 0xf4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf6, 0x05, 0x00, 0x00, - 0xa9, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf7, 0x05, 0x00, 0x00, - 0xf5, 0x05, 0x00, 0x00, 0xf6, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xf9, 0x05, 0x00, 0x00, 0xf8, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xfa, 0x05, 0x00, 0x00, 0xf7, 0x05, 0x00, 0x00, 0xf9, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xfb, 0x05, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfc, 0x05, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, - 0xfa, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xfe, 0x05, 0x00, 0x00, 0xfb, 0x05, 0x00, 0x00, 0xfc, 0x05, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x05, 0x00, 0x00, 0xfe, 0x05, 0x00, 0x00, - 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, - 0x62, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x67, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x67, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, - 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, - 0x71, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x76, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x74, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, - 0x76, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x75, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x77, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x76, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, - 0x7c, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x80, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, - 0x6d, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x89, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, - 0x8a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x85, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8d, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8e, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8f, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, - 0x8d, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x85, 0x00, 0x00, 0x00, - 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, - 0x94, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x91, 0x00, 0x00, 0x00, - 0x96, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, - 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, - 0x6c, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, - 0x9c, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x9e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, - 0x98, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x9e, 0x00, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0xaa, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xac, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xae, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0xaf, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xb2, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0xb5, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xba, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0xbd, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xc0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xbe, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, - 0xc0, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xc3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xc6, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xc7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, - 0xc9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xca, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xc8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, - 0xcc, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xd0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xce, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, - 0xd0, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xd2, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xd5, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x25, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xb3, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, - 0xb6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xdb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xd9, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xda, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0xdd, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xde, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdb, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xaa, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, - 0xb0, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xe2, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0xe4, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe3, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xaa, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, - 0xcd, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xea, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0xeb, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xec, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xea, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0xb1, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xef, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xef, 0x00, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x27, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x29, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x31, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x04, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x03, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x08, 0x01, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0a, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, - 0x0b, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, - 0x0c, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x0e, 0x01, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x0e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, - 0x10, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x13, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, - 0x0f, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x16, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x17, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, - 0x19, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x06, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x01, 0x00, 0x00, - 0x1d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, - 0x06, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x1e, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x21, 0x01, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x22, 0x01, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, - 0x26, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, - 0x25, 0x01, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x29, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, - 0x2c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, - 0x1e, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, - 0x2d, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x30, 0x01, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x31, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, - 0x50, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, - 0x32, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x33, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x35, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x36, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x38, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x37, 0x01, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x01, 0x00, 0x00, - 0x37, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x38, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x36, 0x01, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x3a, 0x01, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x36, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x40, 0x01, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x3d, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x44, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x45, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x46, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0x48, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, - 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, - 0x4c, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, - 0x51, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, - 0x56, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x5b, 0x01, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x01, 0x00, 0x00, - 0x5b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x59, 0x01, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x5f, 0x01, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x50, 0x01, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, - 0x63, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x62, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x66, 0x01, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x65, 0x01, 0x00, 0x00, - 0x66, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, - 0x69, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x60, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, - 0x87, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, - 0x6d, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, - 0x6e, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x71, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, - 0x71, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x70, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x72, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x74, 0x01, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, - 0x75, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, - 0x77, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0x71, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x71, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x7b, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x84, 0x01, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x86, 0x01, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x82, 0x01, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, - 0x87, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, - 0x88, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, - 0x89, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x8c, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, - 0x8c, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x8b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x8e, 0x01, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, - 0x94, 0x01, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x59, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, - 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x98, 0x01, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x9c, 0x01, 0x00, 0x00, - 0x9f, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xa0, 0x01, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xa1, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x9a, 0x01, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, - 0xa9, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xa6, 0x01, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x42, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x9c, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xac, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, + 0x7a, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, + 0x7b, 0x01, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, + 0x77, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7e, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x7f, 0x01, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x65, 0x01, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x81, 0x01, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, + 0x85, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x86, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x97, 0x01, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x98, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x99, 0x01, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x9a, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x25, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x9d, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xa2, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x22, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, + 0xa5, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa8, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xa6, 0x01, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, + 0xa8, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xaa, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xad, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xaf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xad, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xae, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xb1, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xaf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, + 0xb3, 0x01, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb6, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xb4, 0x01, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, + 0xb6, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb5, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xb8, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb6, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xbb, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0xb3, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, + 0x9e, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xc1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x01, 0x00, 0x00, + 0xc2, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc1, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0xaa, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, + 0xd3, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xc7, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x01, 0x00, 0x00, + 0xc9, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, 0xc9, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xca, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0xaa, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, + 0xdb, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xcd, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xce, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x01, 0x00, 0x00, + 0xd0, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xcf, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xd4, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xd4, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x32, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6a, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xb2, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, - 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, - 0xb4, 0x01, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, - 0xb5, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xb6, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xb9, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, - 0xad, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb8, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xb8, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, - 0xb5, 0x01, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, - 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xbd, 0x01, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xbe, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xbf, 0x01, 0x00, 0x00, - 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, - 0xb8, 0x01, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xc6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xc4, 0x01, 0x00, 0x00, - 0xc5, 0x01, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc5, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x6c, 0x01, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc8, 0x01, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xc9, 0x01, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0xc6, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, - 0xc9, 0x01, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xcc, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xca, 0x01, 0x00, 0x00, 0xcb, 0x01, 0x00, 0x00, - 0xcc, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xcb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xcd, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd1, 0x01, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xd3, 0x01, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd4, 0x01, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xd4, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, - 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, - 0xd7, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, - 0xd9, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd5, 0x01, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xdc, 0x01, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xdf, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xde, 0x01, 0x00, 0x00, - 0xdf, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x01, 0x00, 0x00, - 0xe0, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe1, 0x01, 0x00, 0x00, - 0xdd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, - 0xcf, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, - 0xe1, 0x01, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xe5, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xe3, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, - 0xe5, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0xe5, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xee, 0x01, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xf0, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xef, 0x01, 0x00, 0x00, - 0xf0, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, - 0x45, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x01, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0xee, 0x01, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xeb, 0x01, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf6, 0x01, 0x00, 0x00, - 0xf5, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf7, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xf9, 0x01, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, - 0xf7, 0x01, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf4, 0x01, 0x00, 0x00, - 0xfb, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x01, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x03, 0x02, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, - 0x01, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x06, 0x02, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x07, 0x02, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0a, 0x02, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x09, 0x02, 0x00, 0x00, - 0x0a, 0x02, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x0d, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x0d, 0x02, 0x00, 0x00, - 0x10, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, - 0x47, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, - 0x48, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x14, 0x02, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, - 0x13, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x15, 0x02, 0x00, 0x00, 0x14, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x15, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, - 0x1a, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, - 0x18, 0x02, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x1d, 0x02, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x1e, 0x02, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x1f, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, - 0x1d, 0x02, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x20, 0x02, 0x00, 0x00, - 0x1f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, - 0x41, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x17, 0x02, 0x00, 0x00, - 0x21, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, - 0xfc, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, - 0xd5, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, - 0x24, 0x02, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x27, 0x02, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x27, 0x02, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x2b, 0x02, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2c, 0x02, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x22, 0x02, 0x00, 0x00, 0x2c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x2e, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x2f, 0x02, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x30, 0x02, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x31, 0x02, 0x00, 0x00, 0x2f, 0x02, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x36, 0x02, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x3a, 0x02, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x3b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, - 0x0c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, - 0xdb, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, - 0x3e, 0x02, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, - 0x3f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, - 0xdc, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x02, 0x00, 0x00, - 0x40, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, - 0x41, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x3c, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x3c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, - 0x17, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, - 0xcf, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x44, 0x02, 0x00, 0x00, - 0x42, 0x02, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x46, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x44, 0x02, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, - 0x46, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x45, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x48, 0x02, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x49, 0x02, 0x00, 0x00, 0x48, 0x02, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x49, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4a, 0x02, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x4a, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x46, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x46, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x4e, 0x02, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x50, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x4e, 0x02, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, - 0x22, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, - 0xdb, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, - 0x52, 0x02, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, - 0x53, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, - 0xdc, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, - 0x54, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, - 0x55, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x50, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x50, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, - 0x2d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, - 0xcf, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, - 0x56, 0x02, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x5a, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x58, 0x02, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, - 0x5a, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x59, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x5a, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x63, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x61, 0x02, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x62, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x02, 0x00, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x64, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x63, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x02, 0x00, 0x00, - 0xdb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, - 0xdc, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, - 0x67, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x66, 0x02, 0x00, 0x00, - 0x69, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, - 0x45, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, - 0x66, 0x02, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, - 0x6b, 0x02, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x6e, 0x02, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x70, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, - 0x6f, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, - 0x6d, 0x02, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6a, 0x02, 0x00, 0x00, - 0x71, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x02, 0x00, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x73, 0x02, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x72, 0x02, 0x00, 0x00, - 0x76, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, - 0x6a, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7f, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x7c, 0x02, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x7f, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x82, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x90, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x99, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xb6, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xe5, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xeb, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xf1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xfc, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x02, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x09, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x0d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x18, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x19, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x1b, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x1d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x1f, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x23, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x2a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x2c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x2e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x32, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x37, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x39, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x3b, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x45, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x49, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x4a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x4c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x4f, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x53, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x54, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x56, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x59, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x5d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x5e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x60, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x63, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x67, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x68, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x6a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x75, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x76, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x78, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x85, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x86, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x88, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x95, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x96, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x98, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xa5, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xa6, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xa8, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xb6, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xba, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xbb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xbd, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xc4, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xc5, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xc7, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xca, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xce, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xcf, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xd1, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xd4, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xd8, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xd9, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xdb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xe6, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xe7, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xe9, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xf6, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xf7, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xf9, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x06, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x07, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x09, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x16, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x17, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x19, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x28, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x2c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x2d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x2f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x32, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x36, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x37, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x39, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x3c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x40, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x41, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x43, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x46, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x4a, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x4b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x4d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x58, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x59, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x5b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x68, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x69, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x6b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x78, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x79, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x7b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x88, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x89, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x8b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x83, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x82, 0x02, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x85, 0x02, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x87, 0x02, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, - 0xa6, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, - 0x8b, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x8d, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x83, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x91, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x92, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, - 0x82, 0x02, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, - 0x93, 0x02, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x95, 0x02, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x90, 0x02, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x97, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x9a, 0x02, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x9b, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x9c, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x9c, 0x02, 0x00, 0x00, - 0x9d, 0x02, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x99, 0x02, 0x00, 0x00, - 0x9e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, - 0x99, 0x02, 0x00, 0x00, 0x9c, 0x00, 0x04, 0x00, 0xa0, 0x02, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, - 0x9f, 0x02, 0x00, 0x00, 0x9a, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, - 0xa1, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, - 0xa2, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xa3, 0x02, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xa4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xa6, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x04, 0x00, 0xa0, 0x02, 0x00, 0x00, - 0xa7, 0x02, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0x9a, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xa8, 0x02, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa5, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xa5, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xa9, 0x02, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, - 0xa4, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, - 0xa9, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xac, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xaa, 0x02, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xab, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xad, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xae, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0xa0, 0x02, 0x00, 0x00, 0xb0, 0x02, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, - 0xaf, 0x02, 0x00, 0x00, 0x9a, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, - 0xb0, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xac, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xac, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, - 0xa9, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xb4, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xb2, 0x02, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xb3, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x83, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xb4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, - 0x90, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, - 0xb7, 0x02, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xb9, 0x02, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xb6, 0x02, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, - 0xbd, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xc0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xbf, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xc1, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xc0, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc0, 0x02, 0x00, 0x00, - 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, - 0xb4, 0x02, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xc7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xc5, 0x02, 0x00, 0x00, - 0xc6, 0x02, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc6, 0x02, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, - 0xc8, 0x02, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, - 0xc9, 0x02, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xc7, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xc7, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xcb, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, - 0xc6, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, - 0xcb, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xce, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xcc, 0x02, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0xce, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xcd, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xce, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xce, 0x02, 0x00, 0x00, - 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, - 0xc7, 0x02, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xd4, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xd2, 0x02, 0x00, 0x00, - 0xd3, 0x02, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd3, 0x02, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x83, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd4, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, - 0xd8, 0x02, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xda, 0x02, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xdc, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, - 0xdb, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdb, 0x02, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x83, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdc, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, - 0xdf, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xe3, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xe4, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, - 0xe2, 0x02, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xde, 0x02, 0x00, 0x00, - 0xe4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, - 0xe6, 0x02, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, - 0xe8, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, - 0xe7, 0x02, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe5, 0x02, 0x00, 0x00, - 0xea, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xed, 0x02, 0x00, 0x00, - 0xec, 0x02, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, - 0xee, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0xed, 0x02, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xeb, 0x02, 0x00, 0x00, - 0xf0, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xf3, 0x02, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xf4, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, - 0xf5, 0x02, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf8, 0x02, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, - 0xf7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0xf8, 0x02, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xfb, 0x02, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xf1, 0x02, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xfd, 0x02, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, - 0xff, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xfc, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0xdc, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, - 0x4f, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00, - 0x04, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x05, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, - 0xfc, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, - 0x06, 0x03, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, - 0x08, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, - 0xe5, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, - 0x02, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, - 0x0a, 0x03, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x0c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, - 0xeb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, - 0x02, 0x03, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, - 0x0e, 0x03, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x10, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, - 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, - 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x09, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x16, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x14, 0x03, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x16, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x1a, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x19, 0x03, 0x00, 0x00, - 0x1a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x1f, 0x03, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x22, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, - 0x22, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, - 0xf1, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x23, 0x03, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x0a, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, - 0x19, 0x03, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x18, 0x03, 0x00, 0x00, - 0x25, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x03, 0x00, 0x00, - 0x18, 0x03, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, - 0x26, 0x03, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x29, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x27, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, - 0x29, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x28, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x2a, 0x03, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2d, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2c, 0x03, 0x00, 0x00, - 0x2d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2f, 0x03, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2e, 0x03, 0x00, 0x00, 0x2f, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x2a, 0x03, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x30, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x29, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x32, 0x03, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x37, 0x03, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x3a, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x39, 0x03, 0x00, 0x00, - 0x3a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x37, 0x03, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x3e, 0x03, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, - 0x83, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x42, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x40, 0x03, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x41, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x43, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x43, 0x03, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x42, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x49, 0x03, 0x00, 0x00, - 0x48, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4a, 0x03, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, - 0x4a, 0x03, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x45, 0x03, 0x00, 0x00, - 0x4e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x53, 0x03, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x54, 0x03, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x56, 0x03, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x58, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x53, 0x03, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, - 0x56, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x58, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x5e, 0x03, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x61, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x60, 0x03, 0x00, 0x00, - 0x61, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x59, 0x03, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x67, 0x03, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x69, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x68, 0x03, 0x00, 0x00, - 0x69, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x67, 0x03, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x63, 0x03, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x6d, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x6e, 0x03, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x6f, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x70, 0x03, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x75, 0x03, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x76, 0x03, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x79, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x03, 0x00, 0x00, - 0x79, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7b, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, 0x7b, 0x03, 0x00, 0x00, - 0x7a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x7e, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x7e, 0x03, 0x00, 0x00, - 0x7f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, - 0x7d, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x82, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x83, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, - 0x82, 0x03, 0x00, 0x00, 0x83, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x85, 0x03, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x86, 0x03, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x88, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, - 0x86, 0x03, 0x00, 0x00, 0x88, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8b, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8c, 0x03, 0x00, 0x00, 0x8b, 0x03, 0x00, 0x00, 0x8a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x8c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x8d, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x8e, 0x03, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x8f, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x90, 0x03, 0x00, 0x00, 0x8e, 0x03, 0x00, 0x00, 0x8f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, 0x8d, 0x03, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x92, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x93, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x94, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, 0x92, 0x03, 0x00, 0x00, 0x93, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x95, 0x03, 0x00, 0x00, 0x94, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x96, 0x03, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x99, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x98, 0x03, 0x00, 0x00, - 0x99, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x95, 0x03, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, - 0x9a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, - 0x9f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, - 0x9d, 0x03, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xa2, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xa3, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xa4, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, - 0xa2, 0x03, 0x00, 0x00, 0xa3, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa5, 0x03, 0x00, 0x00, - 0xa4, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x03, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xa8, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, - 0xa6, 0x03, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xab, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xac, 0x03, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x03, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xad, 0x03, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0xad, 0x03, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, - 0xaf, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xb0, 0x03, 0x00, 0x00, 0xb1, 0x03, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb3, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb4, 0x03, 0x00, 0x00, 0xb3, 0x03, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xb4, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb2, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xba, 0x03, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xbc, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbb, 0x03, 0x00, 0x00, - 0xbc, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbd, 0x03, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0xba, 0x03, 0x00, 0x00, 0xbb, 0x03, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb6, 0x03, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x03, 0x00, 0x00, - 0xc3, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc5, 0x03, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc7, 0x03, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xc9, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, - 0xc5, 0x03, 0x00, 0x00, 0xc7, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, - 0xc9, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xce, 0x03, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x03, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd2, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd1, 0x03, 0x00, 0x00, 0xd2, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xd3, 0x03, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, - 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xca, 0x03, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd9, 0x03, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xdc, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x03, 0x00, 0x00, - 0xdc, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xd9, 0x03, 0x00, 0x00, 0xdb, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xd4, 0x03, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xde, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0xde, 0x03, 0x00, 0x00, - 0xe1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe3, 0x03, 0x00, 0x00, - 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, - 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0xe3, 0x03, 0x00, 0x00, - 0xe4, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe7, 0x03, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xe9, 0x03, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xeb, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, - 0xe9, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xec, 0x03, 0x00, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xed, 0x03, 0x00, 0x00, - 0xec, 0x03, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, - 0xed, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, - 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, - 0xef, 0x03, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0xf2, 0x03, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xf2, 0x03, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xf6, 0x03, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xf8, 0x03, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf7, 0x03, 0x00, 0x00, - 0xf8, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfa, 0x03, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf9, 0x03, 0x00, 0x00, 0xfa, 0x03, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfb, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0xf6, 0x03, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, 0xf9, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xfd, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xfb, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0xfd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0xca, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, - 0x01, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, - 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, - 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, - 0x04, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x06, 0x04, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x07, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x09, 0x04, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x07, 0x04, 0x00, 0x00, - 0x09, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x04, 0x00, 0x00, - 0x0c, 0x04, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x0d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, - 0xd4, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, - 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, - 0x0f, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x12, 0x04, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x14, 0x04, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x15, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x12, 0x04, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x14, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x16, 0x04, 0x00, 0x00, 0x15, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x18, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x17, 0x04, 0x00, 0x00, - 0x18, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, - 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x19, 0x04, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x16, 0x04, 0x00, 0x00, 0x17, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x70, 0x00, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, - 0x1e, 0x04, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, - 0x1f, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x23, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x21, 0x04, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, - 0x23, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x22, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x26, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x23, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x2d, 0x04, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x30, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2f, 0x04, 0x00, 0x00, - 0x30, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, - 0x30, 0x00, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x28, 0x04, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x04, 0x00, 0x00, 0x35, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x38, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x37, 0x04, 0x00, 0x00, - 0x38, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x39, 0x04, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, - 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x36, 0x04, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x32, 0x04, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x40, 0x04, 0x00, 0x00, - 0x3f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, - 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x41, 0x04, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x43, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, - 0x41, 0x04, 0x00, 0x00, 0x43, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x04, 0x00, 0x00, - 0x45, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4a, 0x04, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x4d, 0x04, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x4f, 0x04, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, - 0x4d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x46, 0x04, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x50, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, - 0x52, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, - 0x50, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x55, 0x04, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x56, 0x04, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x57, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, - 0x55, 0x04, 0x00, 0x00, 0x56, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x04, 0x00, 0x00, - 0x57, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5a, 0x04, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x04, 0x00, 0x00, 0x5a, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x58, 0x04, 0x00, 0x00, - 0x59, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x5e, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x5f, 0x04, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x5f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x60, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x61, 0x04, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x62, 0x04, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x63, 0x04, 0x00, 0x00, 0x61, 0x04, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x64, 0x04, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x63, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x65, 0x04, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x66, 0x04, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x64, 0x04, 0x00, 0x00, 0x65, 0x04, 0x00, 0x00, 0x66, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x68, 0x04, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x69, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x6c, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x04, 0x00, 0x00, - 0x6c, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x68, 0x04, 0x00, 0x00, 0x69, 0x04, 0x00, 0x00, 0x6b, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6f, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, - 0x6d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0x6f, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x70, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x72, 0x04, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x73, 0x04, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, - 0x72, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x74, 0x04, 0x00, 0x00, - 0x70, 0x04, 0x00, 0x00, 0x73, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x75, 0x04, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x76, 0x04, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x77, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x74, 0x04, 0x00, 0x00, - 0x75, 0x04, 0x00, 0x00, 0x76, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x04, 0x00, 0x00, - 0x77, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x04, 0x00, 0x00, - 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x7a, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x7c, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, - 0x79, 0x04, 0x00, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7e, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7f, 0x04, 0x00, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x7d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x36, 0x03, 0x00, 0x00, 0x7f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x80, 0x04, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x81, 0x04, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x82, 0x04, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x2a, 0x00, 0x00, 0x00, - 0x83, 0x04, 0x00, 0x00, 0x81, 0x04, 0x00, 0x00, 0x82, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x84, 0x04, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x83, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x85, 0x04, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x86, 0x04, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x87, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x84, 0x04, 0x00, 0x00, 0x85, 0x04, 0x00, 0x00, 0x86, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x88, 0x04, 0x00, 0x00, 0x87, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8a, 0x04, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x89, 0x04, 0x00, 0x00, 0x8a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x8c, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8b, 0x04, 0x00, 0x00, - 0x8c, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x88, 0x04, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, 0x8b, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8e, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, 0x8e, 0x04, 0x00, 0x00, - 0x8d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x03, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x92, 0x04, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, - 0x91, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x92, 0x04, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x52, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x55, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x95, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x96, 0x04, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x82, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x97, 0x04, 0x00, 0x00, 0x96, 0x04, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x95, 0x04, 0x00, 0x00, 0x97, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0x98, 0x04, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x99, 0x04, 0x00, 0x00, 0x98, 0x04, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x9a, 0x04, 0x00, 0x00, 0x99, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0x9c, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x9a, 0x04, 0x00, 0x00, - 0x9b, 0x04, 0x00, 0x00, 0x9c, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9b, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x9f, 0x04, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x9e, 0x04, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa0, 0x04, 0x00, 0x00, 0x9f, 0x04, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, - 0xa1, 0x04, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, 0xa0, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x9c, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9c, 0x04, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xa2, 0x04, 0x00, 0x00, 0x99, 0x04, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, - 0xa1, 0x04, 0x00, 0x00, 0x9b, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa4, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xa2, 0x04, 0x00, 0x00, 0xa3, 0x04, 0x00, 0x00, - 0xa4, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xb6, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa4, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xa6, 0x04, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, 0xa6, 0x04, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, - 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xa8, 0x04, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xaa, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xa8, 0x04, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0xaa, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xa9, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, - 0x53, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0xac, 0x04, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xad, 0x04, 0x00, 0x00, 0xac, 0x04, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xae, 0x04, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, 0xad, 0x04, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xaa, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xaa, 0x04, 0x00, 0x00, - 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xaf, 0x04, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, - 0xa4, 0x04, 0x00, 0x00, 0xae, 0x04, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xb1, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xaf, 0x04, 0x00, 0x00, - 0xb0, 0x04, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb0, 0x04, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xb0, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, - 0xb3, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb4, 0x04, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, - 0xcd, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, 0xb5, 0x04, 0x00, 0x00, - 0xb4, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xb5, 0x04, 0x00, 0x00, 0xb6, 0x04, 0x00, 0x00, 0xb7, 0x04, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xb6, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb8, 0x04, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xb9, 0x04, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x04, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, - 0xb8, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, 0xb8, 0x04, 0x00, 0x00, - 0xba, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb7, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xb7, 0x04, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x73, 0x00, 0x00, 0x00, 0xbc, 0x04, 0x00, 0x00, - 0xb4, 0x04, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, 0xb6, 0x04, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xbe, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xbc, 0x04, 0x00, 0x00, 0xbd, 0x04, 0x00, 0x00, 0xbe, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xbd, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xbe, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x19, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xc2, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xc5, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, - 0xc8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, - 0xc9, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, - 0xca, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0xc3, 0x04, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc2, 0x04, 0x00, 0x00, - 0xc3, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xc4, 0x04, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0xc2, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0xc6, 0x04, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc5, 0x04, 0x00, 0x00, - 0xc6, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xc7, 0x04, 0x00, 0x00, - 0x24, 0x00, 0x00, 0x00, 0xc5, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc8, 0x04, 0x00, 0x00, - 0xc4, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc9, 0x04, 0x00, 0x00, 0xc7, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xca, 0x04, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xcc, 0x04, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, - 0xc9, 0x04, 0x00, 0x00, 0xca, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xcc, 0x04, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5b, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd1, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd4, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xd5, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xd8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdb, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xf1, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xf8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0xfb, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x04, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0xd1, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x04, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xd6, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd5, 0x04, 0x00, 0x00, 0xd6, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xd7, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0xd5, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd4, 0x04, 0x00, 0x00, 0xd7, 0x04, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, - 0xd9, 0x04, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x82, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, - 0xda, 0x04, 0x00, 0x00, 0xd9, 0x04, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd8, 0x04, 0x00, 0x00, 0xda, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, - 0xdc, 0x04, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdd, 0x04, 0x00, 0x00, 0xdc, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x04, 0x00, 0x00, 0xdd, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xde, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xdf, 0x04, 0x00, 0x00, 0xd8, 0x04, 0x00, 0x00, 0xad, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xe0, 0x04, 0x00, 0x00, 0xde, 0x04, 0x00, 0x00, 0xdf, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe1, 0x04, 0x00, 0x00, 0xdb, 0x04, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xe2, 0x04, 0x00, 0x00, 0xe1, 0x04, 0x00, 0x00, - 0x5e, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0xe3, 0x04, 0x00, 0x00, - 0xe0, 0x04, 0x00, 0x00, 0xe2, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xe5, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xe3, 0x04, 0x00, 0x00, 0xe4, 0x04, 0x00, 0x00, - 0xe5, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe4, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe6, 0x04, 0x00, 0x00, 0xd4, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0xe6, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe5, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x13, 0x00, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0xaa, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0xea, 0x04, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xed, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xea, 0x04, 0x00, 0x00, 0xec, 0x04, 0x00, 0x00, 0xee, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xec, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xeb, 0x04, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x02, 0x00, 0xed, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xee, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xef, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, - 0x82, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, 0xef, 0x04, 0x00, 0x00, - 0xb0, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, - 0xa9, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xf3, 0x04, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x04, 0x00, 0x00, - 0xf3, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf4, 0x04, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf5, 0x04, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0xf1, 0x04, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xeb, 0x04, 0x00, 0x00, - 0xf5, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xed, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xed, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf6, 0x04, 0x00, 0x00, - 0xeb, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe8, 0x04, 0x00, 0x00, 0xf6, 0x04, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xf9, 0x04, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x9e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xfa, 0x04, 0x00, 0x00, - 0xf9, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf8, 0x04, 0x00, 0x00, 0xfa, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xfb, 0x04, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0xf8, 0x04, 0x00, 0x00, - 0xfb, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf7, 0x04, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0xe8, 0x04, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x05, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, - 0x00, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, - 0xdb, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, - 0x01, 0x05, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x04, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x03, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, - 0x07, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x05, 0x05, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0a, 0x05, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0b, 0x05, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, - 0x0a, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x0d, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x0b, 0x05, 0x00, 0x00, 0x0c, 0x05, 0x00, 0x00, 0x0d, 0x05, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x0c, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0e, 0x05, 0x00, 0x00, 0xd4, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x0e, 0x05, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x0d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x11, 0x05, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x12, 0x05, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x13, 0x05, 0x00, 0x00, 0x11, 0x05, 0x00, 0x00, 0x12, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, - 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x10, 0x05, 0x00, 0x00, 0x16, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x17, 0x05, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x73, 0x00, 0x00, 0x00, 0x19, 0x05, 0x00, 0x00, 0x17, 0x05, 0x00, 0x00, 0x18, 0x05, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x1b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x19, 0x05, 0x00, 0x00, 0x1a, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x1a, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x00, - 0xd4, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x1c, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x1b, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x05, 0x00, 0x00, - 0xd4, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1f, 0x05, 0x00, 0x00, - 0xcf, 0x04, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x00, - 0x1f, 0x05, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x05, 0x00, 0x00, - 0x20, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x05, 0x00, 0x00, - 0x57, 0x00, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x23, 0x05, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x24, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x1e, 0x05, 0x00, 0x00, - 0x22, 0x05, 0x00, 0x00, 0x23, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x24, 0x05, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xe7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xe5, 0x01, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xe6, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xea, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xe7, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xf0, 0x01, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xf3, 0x01, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf6, 0x01, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf8, 0x01, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xf8, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, + 0xf5, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfb, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, + 0xf9, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfd, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0xfd, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0xfe, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xec, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0xec, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x01, 0x02, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x05, 0x02, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0d, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, + 0x0b, 0x02, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, + 0x0c, 0x02, 0x00, 0x00, 0x0d, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x0e, 0x02, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x11, 0x02, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x14, 0x02, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x13, 0x02, 0x00, 0x00, + 0x14, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x15, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x18, 0x02, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x21, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x1f, 0x00, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x02, 0x00, 0x00, + 0x24, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, + 0x1e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, + 0x18, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, + 0x26, 0x02, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x29, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x25, 0x02, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2c, 0x02, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x02, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, + 0x2f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x2c, 0x02, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x34, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x33, 0x02, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x37, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x37, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x79, 0x01, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x3f, 0x02, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x41, 0x02, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x42, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x02, 0x00, 0x00, + 0x41, 0x02, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3a, 0x02, 0x00, 0x00, + 0x44, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x45, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x46, 0x02, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x48, 0x02, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x49, 0x02, 0x00, 0x00, 0x48, 0x02, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x4b, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x49, 0x02, 0x00, 0x00, + 0x4a, 0x02, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4a, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4e, 0x02, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x46, 0x02, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x50, 0x02, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x51, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, + 0x4e, 0x02, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x45, 0x02, 0x00, 0x00, + 0x52, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x4b, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x4b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, + 0x2a, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x57, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x59, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, + 0x58, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x59, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, + 0x60, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x63, 0x02, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x64, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x66, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x64, 0x02, 0x00, 0x00, + 0x65, 0x02, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x65, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x68, 0x02, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x6a, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, + 0x6c, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x70, 0x02, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x59, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x72, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x66, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x74, 0x02, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, + 0x7b, 0x02, 0x00, 0x00, 0xa9, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, + 0x7c, 0x02, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x7d, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x81, 0x02, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, + 0x57, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, + 0x81, 0x02, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, + 0x82, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x83, 0x02, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x22, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xce, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xda, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa3, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xad, 0x03, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x86, 0x02, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x89, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, + 0x8b, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, + 0x8c, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x90, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x8f, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x8f, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, + 0x8c, 0x02, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, + 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x96, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x94, 0x02, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x95, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x96, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x96, 0x02, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, + 0x8f, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x9d, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x9b, 0x02, 0x00, 0x00, + 0x9c, 0x02, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9c, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x46, 0x02, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9f, 0x02, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xa0, 0x02, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x9d, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9d, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, + 0xa0, 0x02, 0x00, 0x00, 0x9c, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa3, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xa1, 0x02, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, + 0xa3, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xa4, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xa9, 0x02, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa8, 0x02, 0x00, 0x00, 0xa9, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xaa, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, + 0xae, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb0, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, + 0xb0, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xac, 0x02, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xb6, 0x02, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb5, 0x02, 0x00, 0x00, + 0xb6, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x02, 0x00, 0x00, + 0xb7, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, + 0xb4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, + 0xb8, 0x02, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xbc, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xba, 0x02, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, + 0xbc, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xbb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xbc, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xbc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc2, 0x02, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc8, 0x02, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xce, 0x02, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xd5, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0xd5, 0x02, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd4, 0x02, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xda, 0x02, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0xe4, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe0, 0x02, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe6, 0x02, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0xed, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0xed, 0x02, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xec, 0x02, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, + 0xf6, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf8, 0x02, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf8, 0x02, 0x00, 0x00, + 0xf9, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfb, 0x02, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0xfb, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x02, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, + 0xfe, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x03, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x04, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x05, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, + 0x03, 0x03, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x06, 0x03, 0x00, 0x00, + 0x05, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfd, 0x02, 0x00, 0x00, + 0x07, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, + 0xce, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, + 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, + 0x0a, 0x03, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x0d, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x0d, 0x03, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x11, 0x03, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x12, 0x03, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x08, 0x03, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x14, 0x03, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x15, 0x03, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x16, 0x03, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x17, 0x03, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x16, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x19, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1a, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x19, 0x03, 0x00, 0x00, 0x1a, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x13, 0x03, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0xda, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, + 0x22, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x26, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, + 0x25, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x27, 0x03, 0x00, 0x00, 0x26, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0x27, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2a, 0x03, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, + 0x2c, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, + 0x2a, 0x03, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x2f, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x30, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x31, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, + 0x2f, 0x03, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x32, 0x03, 0x00, 0x00, + 0x31, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x29, 0x03, 0x00, 0x00, + 0x33, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, + 0xe6, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, + 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, + 0x36, 0x03, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x39, 0x03, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3a, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x39, 0x03, 0x00, 0x00, 0x3a, 0x03, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x3d, 0x03, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3e, 0x03, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x34, 0x03, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x40, 0x03, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x41, 0x03, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x42, 0x03, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x43, 0x03, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x43, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x48, 0x03, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x3f, 0x03, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x4c, 0x03, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x4d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, + 0xf2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, + 0x50, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x51, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x03, 0x00, 0x00, + 0x52, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0x53, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x4e, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x4e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, + 0xfd, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x56, 0x03, 0x00, 0x00, + 0x54, 0x03, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x58, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x56, 0x03, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, + 0x58, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x57, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x58, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x58, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x62, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x60, 0x03, 0x00, 0x00, 0x61, 0x03, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x61, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, + 0x08, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, + 0x64, 0x03, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x65, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x67, 0x03, 0x00, 0x00, + 0x66, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0x67, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x62, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x62, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, + 0x13, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, + 0x68, 0x03, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x6c, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, + 0x6c, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x6c, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x76, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x74, 0x03, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x75, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, + 0x1e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, + 0x78, 0x03, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x79, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7b, 0x03, 0x00, 0x00, + 0x7a, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0x7b, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x76, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x76, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, + 0x29, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x7e, 0x03, 0x00, 0x00, + 0x7c, 0x03, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7e, 0x03, 0x00, 0x00, 0x7f, 0x03, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x83, 0x03, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x83, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x80, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x80, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x88, 0x03, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x8a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x88, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x8a, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x89, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8b, 0x03, 0x00, 0x00, + 0x34, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x03, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x03, 0x00, 0x00, + 0x8c, 0x03, 0x00, 0x00, 0x8b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, + 0x8d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8e, 0x03, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x03, 0x00, 0x00, + 0x8e, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0x8f, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x8a, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x8a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x90, 0x03, 0x00, 0x00, + 0x3f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x92, 0x03, 0x00, 0x00, + 0x90, 0x03, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x94, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x92, 0x03, 0x00, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x94, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x93, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x95, 0x03, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0x95, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x99, 0x03, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x99, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x94, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x94, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x9b, 0x03, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x9c, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x67, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x9d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa2, 0x03, 0x00, 0x00, + 0xa0, 0x03, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9f, 0x03, 0x00, 0x00, + 0xa2, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, + 0x9f, 0x03, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0xa4, 0x03, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa7, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa3, 0x03, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0xa3, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa8, 0x03, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xaf, 0x03, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, + 0xaf, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb1, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xad, 0x03, 0x00, 0x00, 0xb1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb3, 0x03, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xb4, 0x03, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb5, 0x03, 0x00, 0x00, + 0xb4, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, + 0xb3, 0x03, 0x00, 0x00, 0xb5, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb7, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, + 0xb6, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, + 0x86, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, + 0xad, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xb7, 0x03, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, + 0xb9, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xba, 0x03, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x21, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x21, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5a, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x56, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x59, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x5a, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x64, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x66, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x6d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x7a, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x88, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x8b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x8e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x91, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x9c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x9f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xac, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xad, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xaf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xbc, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xbd, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xbf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xcc, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcd, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xdc, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xdf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe2, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe5, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xf3, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x11, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x23, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x32, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x38, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x44, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x52, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x55, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x62, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x63, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x65, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x72, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x82, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x90, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, + 0xbe, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbd, 0x03, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, + 0xc1, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xc3, 0x03, 0x00, 0x00, + 0xc2, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, + 0xbd, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, + 0xc4, 0x03, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xc7, 0x03, 0x00, 0x00, 0xc3, 0x03, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xc9, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xc7, 0x03, 0x00, 0x00, + 0xc8, 0x03, 0x00, 0x00, 0xc9, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc8, 0x03, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xc9, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, + 0xcc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, + 0xcf, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x03, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd2, 0x03, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, + 0xd2, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x03, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, + 0xd5, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd9, 0x03, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd4, 0x03, 0x00, 0x00, 0xd9, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x9c, 0x00, 0x04, 0x00, + 0xdb, 0x03, 0x00, 0x00, 0xdc, 0x03, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0x9a, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, 0xdc, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xde, 0x03, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xde, 0x03, 0x00, 0x00, + 0xdf, 0x03, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdf, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, + 0x9d, 0x00, 0x04, 0x00, 0xdb, 0x03, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, + 0x9a, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xe3, 0x03, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe0, 0x03, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, + 0xc9, 0x03, 0x00, 0x00, 0xe3, 0x03, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xe7, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xe5, 0x03, 0x00, 0x00, + 0xe6, 0x03, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe6, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe9, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0xdb, 0x03, 0x00, 0x00, + 0xeb, 0x03, 0x00, 0x00, 0xe9, 0x03, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, 0x9a, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xec, 0x03, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xe7, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe7, 0x03, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xed, 0x03, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, + 0xec, 0x03, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xef, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xed, 0x03, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, + 0xef, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xee, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xef, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, + 0xf3, 0x03, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0xf5, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf6, 0x03, 0x00, 0x00, + 0xf1, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf7, 0x03, 0x00, 0x00, 0xf6, 0x03, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xf8, 0x03, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0xf9, 0x03, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xfb, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xf9, 0x03, 0x00, 0x00, + 0xfa, 0x03, 0x00, 0x00, 0xfb, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfa, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfd, 0x03, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, + 0xfd, 0x03, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xfb, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xfb, 0x03, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xff, 0x03, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, + 0xfa, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, + 0xff, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x01, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x03, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x02, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x02, 0x04, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, + 0xfb, 0x03, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x07, 0x04, 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x09, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x07, 0x04, 0x00, 0x00, + 0x08, 0x04, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x08, 0x04, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, + 0x0b, 0x04, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x09, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x09, 0x04, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x0d, 0x04, 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, + 0x08, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x0d, 0x04, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x0e, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x11, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x12, 0x04, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x14, 0x04, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0xa6, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x15, 0x04, 0x00, 0x00, 0x12, 0x04, 0x00, 0x00, + 0x14, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x17, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x15, 0x04, 0x00, 0x00, 0x16, 0x04, 0x00, 0x00, 0x17, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x16, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x17, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x1a, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x1b, 0x04, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, + 0x1c, 0x04, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2e, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x19, 0x04, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x23, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x24, 0x04, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x20, 0x04, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x29, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x2a, 0x04, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x26, 0x04, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x79, 0x01, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, + 0x2e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x32, 0x04, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x33, 0x04, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, + 0x31, 0x04, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x35, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, + 0x35, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x46, 0x02, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x39, 0x04, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3b, 0x04, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x37, 0x04, 0x00, 0x00, + 0x3c, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x3f, 0x04, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x40, 0x04, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x13, 0x00, 0x00, 0x00, 0x41, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x43, 0x04, 0x00, 0x00, 0x41, 0x04, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x43, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x44, 0x04, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x48, 0x04, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4d, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, + 0x4c, 0x04, 0x00, 0x00, 0x4d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x04, 0x00, 0x00, + 0x4e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x50, 0x04, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x50, 0x04, 0x00, 0x00, + 0x51, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x19, 0x04, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x54, 0x04, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x56, 0x04, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x33, 0x00, 0x00, 0x00, + 0x58, 0x04, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, 0x56, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x53, 0x04, 0x00, 0x00, 0x58, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5a, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5d, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x04, 0x00, 0x00, + 0x5d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x5f, 0x04, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x5f, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x61, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x60, 0x04, 0x00, 0x00, 0x61, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x63, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x62, 0x04, 0x00, 0x00, 0x63, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x65, 0x04, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x64, 0x04, 0x00, 0x00, + 0x65, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x66, 0x04, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, + 0x39, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x04, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x5a, 0x04, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, + 0x62, 0x04, 0x00, 0x00, 0x64, 0x04, 0x00, 0x00, 0x66, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x59, 0x04, 0x00, 0x00, 0x68, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x69, 0x04, 0x00, 0x00, 0x59, 0x04, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x6a, 0x04, 0x00, 0x00, 0x69, 0x04, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x6c, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x6b, 0x04, 0x00, 0x00, 0x6c, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6b, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6d, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x70, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6f, 0x04, 0x00, 0x00, 0x70, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x72, 0x04, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x71, 0x04, 0x00, 0x00, + 0x72, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x04, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x6d, 0x04, 0x00, 0x00, 0x6f, 0x04, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x73, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6c, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x76, 0x04, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x04, 0x00, 0x00, 0x59, 0x04, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, 0x76, 0x04, 0x00, 0x00, + 0x77, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x75, 0x04, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7a, 0x04, 0x00, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7d, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x7c, 0x04, 0x00, 0x00, 0x7d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x7f, 0x04, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7e, 0x04, 0x00, 0x00, + 0x7f, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x7a, 0x04, 0x00, 0x00, 0x7c, 0x04, 0x00, 0x00, 0x7e, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0x81, 0x04, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x82, 0x04, 0x00, 0x00, + 0x81, 0x04, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x83, 0x04, 0x00, 0x00, + 0x82, 0x04, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x85, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x83, 0x04, 0x00, 0x00, 0x84, 0x04, 0x00, 0x00, + 0x85, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x84, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x86, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x86, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x85, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x8a, 0x04, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x88, 0x04, 0x00, 0x00, 0x8a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x8c, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x8d, 0x04, 0x00, 0x00, 0x8c, 0x04, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8b, 0x04, 0x00, 0x00, 0x8d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8e, 0x04, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x92, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x93, 0x04, 0x00, 0x00, 0x92, 0x04, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x91, 0x04, 0x00, 0x00, 0x93, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x94, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, 0x88, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x96, 0x04, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x97, 0x04, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, 0x96, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x98, 0x04, 0x00, 0x00, 0x94, 0x04, 0x00, 0x00, + 0x97, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x99, 0x04, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x9a, 0x04, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x9b, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x98, 0x04, 0x00, 0x00, 0x99, 0x04, 0x00, 0x00, + 0x9a, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9c, 0x04, 0x00, 0x00, 0x9b, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xa0, 0x04, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x9f, 0x04, 0x00, 0x00, 0xa0, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa1, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x9c, 0x04, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, + 0x9f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa2, 0x04, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa3, 0x04, 0x00, 0x00, + 0xa2, 0x04, 0x00, 0x00, 0xa1, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, + 0xa3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xa5, 0x04, 0x00, 0x00, + 0x8b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xa6, 0x04, 0x00, 0x00, + 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, + 0xa5, 0x04, 0x00, 0x00, 0xa6, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xa8, 0x04, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xaa, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xa8, 0x04, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0xaa, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xac, 0x04, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xae, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xad, 0x04, 0x00, 0x00, + 0xae, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xaf, 0x04, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0xac, 0x04, 0x00, 0x00, 0xad, 0x04, 0x00, 0x00, 0xaf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb2, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, 0xb2, 0x04, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xb4, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xb5, 0x04, 0x00, 0x00, 0x8e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xb6, 0x04, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xb7, 0x04, 0x00, 0x00, 0xb5, 0x04, 0x00, 0x00, 0xb6, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb8, 0x04, 0x00, 0x00, 0xb4, 0x04, 0x00, 0x00, + 0xb7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xba, 0x04, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xb8, 0x04, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, + 0xba, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbc, 0x04, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xbd, 0x04, 0x00, 0x00, 0xbe, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xc0, 0x04, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xbf, 0x04, 0x00, 0x00, 0xc0, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc1, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xbc, 0x04, 0x00, 0x00, 0xbd, 0x04, 0x00, 0x00, + 0xbf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc2, 0x04, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x04, 0x00, 0x00, + 0xc2, 0x04, 0x00, 0x00, 0xc1, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, + 0xc3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc4, 0x04, 0x00, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc5, 0x04, 0x00, 0x00, + 0x91, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc6, 0x04, 0x00, 0x00, + 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc7, 0x04, 0x00, 0x00, + 0xc5, 0x04, 0x00, 0x00, 0xc6, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xc8, 0x04, 0x00, 0x00, 0xc4, 0x04, 0x00, 0x00, 0xc7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xc9, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xca, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xc8, 0x04, 0x00, 0x00, 0xc9, 0x04, 0x00, 0x00, 0xca, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xcc, 0x04, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xce, 0x04, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcd, 0x04, 0x00, 0x00, + 0xce, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x04, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd1, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0xcc, 0x04, 0x00, 0x00, 0xcd, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0xd1, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xd4, 0x04, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd5, 0x04, 0x00, 0x00, + 0xd4, 0x04, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xd6, 0x04, 0x00, 0x00, + 0xd5, 0x04, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xd8, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xd6, 0x04, 0x00, 0x00, 0xd7, 0x04, 0x00, 0x00, + 0xd8, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd9, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xda, 0x04, 0x00, 0x00, 0xd9, 0x04, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xda, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd8, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0xdd, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, + 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xde, 0x04, 0x00, 0x00, 0xdd, 0x04, 0x00, 0x00, + 0xde, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdc, 0x04, 0x00, 0x00, 0xde, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0xe0, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, + 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe1, 0x04, 0x00, 0x00, 0xe0, 0x04, 0x00, 0x00, + 0xe4, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdf, 0x04, 0x00, 0x00, 0xe1, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0xe3, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, + 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe4, 0x04, 0x00, 0x00, 0xe3, 0x04, 0x00, 0x00, + 0xea, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe2, 0x04, 0x00, 0x00, 0xe4, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0xe6, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, + 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe7, 0x04, 0x00, 0x00, 0xe6, 0x04, 0x00, 0x00, + 0xf0, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe5, 0x04, 0x00, 0x00, 0xe7, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe8, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, 0xdc, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xea, 0x04, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, + 0xea, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xec, 0x04, 0x00, 0x00, + 0xe8, 0x04, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xed, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xee, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xef, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xec, 0x04, 0x00, 0x00, + 0xed, 0x04, 0x00, 0x00, 0xee, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf0, 0x04, 0x00, 0x00, + 0xef, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, + 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x04, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf3, 0x04, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf5, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, + 0xf1, 0x04, 0x00, 0x00, 0xf3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf6, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf7, 0x04, 0x00, 0x00, 0xf6, 0x04, 0x00, 0x00, 0xf5, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x79, 0x04, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xf8, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xf9, 0x04, 0x00, 0x00, 0xdf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xfa, 0x04, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xfb, 0x04, 0x00, 0x00, 0xf9, 0x04, 0x00, 0x00, 0xfa, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0xf8, 0x04, 0x00, 0x00, 0xfb, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x00, 0x05, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x01, 0x05, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x04, 0x05, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x03, 0x05, 0x00, 0x00, + 0x04, 0x05, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x01, 0x05, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, + 0x05, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, 0xe2, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0a, 0x05, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0b, 0x05, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, + 0x0a, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0c, 0x05, 0x00, 0x00, + 0x08, 0x05, 0x00, 0x00, 0x0b, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x0d, 0x05, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x0e, 0x05, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x0f, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0c, 0x05, 0x00, 0x00, + 0x0d, 0x05, 0x00, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x10, 0x05, 0x00, 0x00, + 0x0f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x05, 0x00, 0x00, + 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x11, 0x05, 0x00, 0x00, 0x12, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x13, 0x05, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, + 0x11, 0x05, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x16, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x17, 0x05, 0x00, 0x00, 0x16, 0x05, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x17, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x18, 0x05, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x19, 0x05, 0x00, 0x00, 0xe5, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x1a, 0x05, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x1b, 0x05, 0x00, 0x00, 0x19, 0x05, 0x00, 0x00, 0x1a, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x00, 0x18, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1d, 0x05, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1e, 0x05, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1f, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x00, 0x1d, 0x05, 0x00, 0x00, 0x1e, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x20, 0x05, 0x00, 0x00, 0x1f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x22, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x21, 0x05, 0x00, 0x00, 0x22, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x24, 0x05, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x23, 0x05, 0x00, 0x00, + 0x24, 0x05, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x05, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x23, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x05, 0x00, 0x00, 0x26, 0x05, 0x00, 0x00, + 0x25, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x27, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x78, 0x01, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x29, 0x05, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0x2b, 0x05, 0x00, 0x00, 0x29, 0x05, 0x00, 0x00, 0x2a, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x2d, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x2b, 0x05, 0x00, 0x00, + 0x2c, 0x05, 0x00, 0x00, 0x2d, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2c, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x05, 0x00, 0x00, 0x2e, 0x05, 0x00, 0x00, + 0x2f, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x30, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x2d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x33, 0x05, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x37, 0x05, 0x00, 0x00, + 0x33, 0x05, 0x00, 0x00, 0x36, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x32, 0x05, 0x00, 0x00, + 0x37, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x39, 0x05, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x3d, 0x05, 0x00, 0x00, + 0x39, 0x05, 0x00, 0x00, 0x3c, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x38, 0x05, 0x00, 0x00, + 0x3d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x3f, 0x05, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x43, 0x05, 0x00, 0x00, + 0x3f, 0x05, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3e, 0x05, 0x00, 0x00, + 0x43, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x33, 0x00, 0x00, 0x00, 0x45, 0x05, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x49, 0x05, 0x00, 0x00, + 0x45, 0x05, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x05, 0x00, 0x00, + 0x49, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4b, 0x05, 0x00, 0x00, + 0x32, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, + 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x4d, 0x05, 0x00, 0x00, + 0x4b, 0x05, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x4e, 0x05, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x4d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x4f, 0x05, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x4e, 0x05, 0x00, 0x00, 0x4f, 0x05, 0x00, 0x00, 0x50, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x52, 0x05, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x54, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x53, 0x05, 0x00, 0x00, + 0x54, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x56, 0x05, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x55, 0x05, 0x00, 0x00, 0x56, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x52, 0x05, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, 0x55, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x5a, 0x05, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x5b, 0x05, 0x00, 0x00, 0x38, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x5d, 0x05, 0x00, 0x00, 0x5b, 0x05, 0x00, 0x00, 0x5c, 0x05, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0x5a, 0x05, 0x00, 0x00, + 0x5d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x5f, 0x05, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x60, 0x05, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0x5f, 0x05, 0x00, 0x00, + 0x60, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x62, 0x05, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x63, 0x05, 0x00, 0x00, 0x64, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x65, 0x05, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x67, 0x05, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x62, 0x05, 0x00, 0x00, 0x63, 0x05, 0x00, 0x00, + 0x65, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x69, 0x05, 0x00, 0x00, + 0x68, 0x05, 0x00, 0x00, 0x67, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, + 0x69, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6a, 0x05, 0x00, 0x00, + 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, + 0x3e, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6c, 0x05, 0x00, 0x00, + 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, + 0x6b, 0x05, 0x00, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x6e, 0x05, 0x00, 0x00, 0x6a, 0x05, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x6f, 0x05, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x71, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x6e, 0x05, 0x00, 0x00, 0x6f, 0x05, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x72, 0x05, 0x00, 0x00, 0x71, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x74, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x73, 0x05, 0x00, 0x00, + 0x74, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x75, 0x05, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x05, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x72, 0x05, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x79, 0x05, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, 0x77, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, 0x79, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x7a, 0x05, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x7b, 0x05, 0x00, 0x00, 0x44, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x7d, 0x05, 0x00, 0x00, 0x7b, 0x05, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x7a, 0x05, 0x00, 0x00, + 0x7d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x81, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, + 0x80, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x05, 0x00, 0x00, 0x81, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x05, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x83, 0x05, 0x00, 0x00, 0x84, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x86, 0x05, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x85, 0x05, 0x00, 0x00, 0x86, 0x05, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x87, 0x05, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x82, 0x05, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, + 0x85, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x88, 0x05, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x05, 0x00, 0x00, + 0x88, 0x05, 0x00, 0x00, 0x87, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x79, 0x04, 0x00, 0x00, + 0x89, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x05, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x05, 0x00, 0x00, + 0x8a, 0x05, 0x00, 0x00, 0x8b, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x8c, 0x05, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x5c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x82, 0x00, 0x05, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8f, 0x05, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, + 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x94, 0x05, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x96, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x94, 0x05, 0x00, 0x00, 0x95, 0x05, 0x00, 0x00, 0x96, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x95, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, + 0x5c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, 0x99, 0x05, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9a, 0x05, 0x00, 0x00, 0x99, 0x05, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, 0x9a, 0x05, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x96, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x96, 0x05, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x9c, 0x05, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, + 0x5e, 0x00, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, 0x95, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x9e, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x9c, 0x05, 0x00, 0x00, + 0x9d, 0x05, 0x00, 0x00, 0x9e, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9d, 0x05, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x9e, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x9e, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, + 0xb3, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xa1, 0x05, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, + 0xd3, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xa2, 0x05, 0x00, 0x00, + 0xa1, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa4, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xa2, 0x05, 0x00, 0x00, 0xa3, 0x05, 0x00, 0x00, 0xa4, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa5, 0x05, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x79, 0x00, 0x00, 0x00, + 0xa6, 0x05, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa7, 0x05, 0x00, 0x00, 0xa6, 0x05, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xa8, 0x05, 0x00, 0x00, 0xa5, 0x05, 0x00, 0x00, + 0xa7, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa4, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa4, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xa9, 0x05, 0x00, 0x00, + 0xa1, 0x05, 0x00, 0x00, 0x9e, 0x05, 0x00, 0x00, 0xa8, 0x05, 0x00, 0x00, 0xa3, 0x05, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xab, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xa9, 0x05, 0x00, 0x00, 0xaa, 0x05, 0x00, 0x00, 0xab, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xaa, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xab, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xad, 0x05, 0x00, 0x00, + 0x8f, 0x05, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, + 0xad, 0x05, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xaf, 0x05, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb1, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xaf, 0x05, 0x00, 0x00, 0xb0, 0x05, 0x00, 0x00, + 0xb1, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb0, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb2, 0x05, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xb3, 0x05, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0xb3, 0x05, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xb5, 0x05, 0x00, 0x00, + 0xb2, 0x05, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xb1, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xb6, 0x05, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0xab, 0x05, 0x00, 0x00, 0xb5, 0x05, 0x00, 0x00, + 0xb0, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xb8, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xb6, 0x05, 0x00, 0x00, 0xb7, 0x05, 0x00, 0x00, 0xb8, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb7, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb8, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x9f, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x5f, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x61, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x22, 0x00, 0x00, 0x00, 0xbc, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x22, 0x00, 0x00, 0x00, 0xbf, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x21, 0x00, 0x00, 0x00, 0xc2, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x21, 0x00, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x22, 0x00, 0x00, 0x00, 0xc4, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xbd, 0x05, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xbc, 0x05, 0x00, 0x00, 0xbd, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xbe, 0x05, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xbc, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xbf, 0x05, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xc1, 0x05, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xbf, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xc2, 0x05, 0x00, 0x00, 0xbe, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc3, 0x05, 0x00, 0x00, + 0xc1, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xc5, 0x05, 0x00, 0x00, + 0x5f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x05, 0x00, 0x00, 0xc5, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc6, 0x05, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, + 0xc2, 0x05, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, 0xc4, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xc6, 0x05, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcb, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xcf, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xd2, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd5, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe2, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe5, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, 0xeb, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, 0xf2, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0xf5, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf8, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xff, 0x05, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0a, 0x06, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x1b, 0x06, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, + 0xca, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x05, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xcd, 0x05, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0xcb, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc9, 0x05, 0x00, 0x00, 0xcd, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x05, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd1, 0x05, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0xcf, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xce, 0x05, 0x00, 0x00, 0xd1, 0x05, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xd3, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x82, 0x00, 0x05, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0xd3, 0x05, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd2, 0x05, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x79, 0x00, 0x00, 0x00, 0xd6, 0x05, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd7, 0x05, 0x00, 0x00, + 0xd6, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd5, 0x05, 0x00, 0x00, 0xd7, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd9, 0x05, 0x00, 0x00, 0xd2, 0x05, 0x00, 0x00, + 0xad, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xda, 0x05, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, + 0xd9, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdb, 0x05, 0x00, 0x00, + 0xd5, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, + 0xdb, 0x05, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, + 0xdd, 0x05, 0x00, 0x00, 0xda, 0x05, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xdf, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xdd, 0x05, 0x00, 0x00, + 0xde, 0x05, 0x00, 0x00, 0xdf, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xde, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe0, 0x05, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xe0, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdf, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xe3, 0x05, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, + 0xaa, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xe4, 0x05, 0x00, 0x00, 0xe3, 0x05, 0x00, 0x00, + 0x9e, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xe7, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xe4, 0x05, 0x00, 0x00, 0xe6, 0x05, 0x00, 0x00, 0xe8, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xe6, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe5, 0x05, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe7, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xe8, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xe9, 0x05, 0x00, 0x00, + 0xc9, 0x05, 0x00, 0x00, 0x82, 0x00, 0x05, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xea, 0x05, 0x00, 0x00, + 0xe9, 0x05, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, + 0xec, 0x05, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0xec, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xeb, 0x05, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xee, 0x05, 0x00, 0x00, + 0xea, 0x05, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xef, 0x05, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0xeb, 0x05, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe5, 0x05, 0x00, 0x00, 0xef, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe7, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xe7, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf0, 0x05, 0x00, 0x00, 0xe5, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe2, 0x05, 0x00, 0x00, + 0xf0, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, 0xf3, 0x05, 0x00, 0x00, + 0x94, 0x01, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xf4, 0x05, 0x00, 0x00, 0xf3, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x05, 0x00, 0x00, + 0xf4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xf6, 0x05, 0x00, 0x00, + 0xc9, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf5, 0x05, 0x00, 0x00, 0xf6, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf7, 0x05, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0xf2, 0x05, 0x00, 0x00, 0xf5, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x05, 0x00, 0x00, + 0xf7, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf9, 0x05, 0x00, 0x00, + 0xf1, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, + 0xe2, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfb, 0x05, 0x00, 0x00, + 0xf9, 0x05, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfc, 0x05, 0x00, 0x00, 0xd5, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfd, 0x05, 0x00, 0x00, 0xfb, 0x05, 0x00, 0x00, 0xfc, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfe, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf8, 0x05, 0x00, 0x00, + 0xfe, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0xf1, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, + 0xf8, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x00, + 0x00, 0x06, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xff, 0x05, 0x00, 0x00, + 0x02, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, + 0xca, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x06, 0x00, 0x00, + 0xff, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x05, 0x06, 0x00, 0x00, + 0x03, 0x06, 0x00, 0x00, 0x04, 0x06, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x07, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x05, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, + 0x07, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x06, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x08, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x07, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0b, 0x06, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0c, 0x06, 0x00, 0x00, 0xff, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0d, 0x06, 0x00, 0x00, 0x0b, 0x06, 0x00, 0x00, 0x0c, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x06, 0x00, 0x00, 0xf8, 0x05, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0f, 0x06, 0x00, 0x00, 0x0d, 0x06, 0x00, 0x00, + 0x0e, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0f, 0x06, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0a, 0x06, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x06, 0x00, 0x00, 0x0a, 0x06, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x13, 0x06, 0x00, 0x00, 0x11, 0x06, 0x00, 0x00, + 0x12, 0x06, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x15, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x13, 0x06, 0x00, 0x00, 0x14, 0x06, 0x00, 0x00, 0x15, 0x06, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x14, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x16, 0x06, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x16, 0x06, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x15, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x18, 0x06, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x19, 0x06, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x1a, 0x06, 0x00, 0x00, 0x19, 0x06, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1b, 0x06, 0x00, 0x00, 0x1a, 0x06, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1c, 0x06, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x1b, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1d, 0x06, 0x00, 0x00, 0x0a, 0x06, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1e, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x18, 0x06, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0x1d, 0x06, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x1e, 0x06, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, }; -static const unsigned int vk_interaction_shadow_frag_spv_size = 38884u; +static const unsigned int vk_interaction_shadow_frag_spv_size = 44504u; alignas(4) static const unsigned char vk_interaction_shadow_point_vert_spv[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x0a, 0x01, 0x00, 0x00, @@ -17771,15 +19124,15 @@ alignas(4) static const unsigned char vk_interaction_shadow_point_vert_spv[] = { static const unsigned int vk_interaction_shadow_point_vert_spv_size = 8108u; alignas(4) static const unsigned char vk_interaction_shadow_point_frag_spv[] = { - 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x7b, 0x03, 0x00, 0x00, + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x58, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x11, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, - 0xc9, 0x02, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, - 0x27, 0x03, 0x00, 0x00, 0x31, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, - 0x74, 0x03, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, + 0x3a, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0x93, 0x03, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, + 0x30, 0x04, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x53, 0x61, 0x66, 0x65, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, @@ -17789,1415 +19142,1709 @@ alignas(4) static const unsigned char vk_interaction_shadow_point_frag_spv[] = { 0x53, 0x77, 0x65, 0x65, 0x70, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5a, 0x00, 0x00, - 0x05, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0x53, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x68, - 0x61, 0x64, 0x6f, 0x77, 0x48, 0x61, 0x73, 0x68, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x09, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x53, 0x68, - 0x61, 0x64, 0x6f, 0x77, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x76, - 0x66, 0x33, 0x3b, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, - 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x42, 0x69, - 0x61, 0x73, 0x28, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x24, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6d, - 0x70, 0x61, 0x72, 0x65, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, - 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x26, 0x00, 0x00, 0x00, 0x53, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x28, 0x00, - 0x05, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4c, 0x69, 0x67, - 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x56, 0x69, 0x65, - 0x77, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x06, 0x00, 0x08, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x54, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x51, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, - 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x33, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x06, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, - 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, - 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, - 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x66, 0x6c, 0x61, 0x74, 0x44, 0x69, 0x66, 0x66, - 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x35, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x50, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x6f, 0x42, 0x61, - 0x6e, 0x64, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x62, 0x61, 0x6e, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, - 0x6f, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x06, 0x00, 0x06, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x6f, 0x77, 0x30, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x06, 0x00, 0x78, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6d, 0x6f, 0x64, 0x65, - 0x6c, 0x52, 0x6f, 0x77, 0x31, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x6f, 0x77, 0x32, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x78, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x46, 0x61, 0x72, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, - 0x78, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x62, 0x69, 0x61, 0x73, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x73, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x78, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x07, 0x00, 0x78, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, - 0x6c, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x84, 0x00, 0x00, 0x00, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x89, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x8d, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x00, - 0x63, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x43, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xaa, 0x00, 0x00, 0x00, - 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x43, 0x6f, 0x73, 0x00, - 0x05, 0x00, 0x05, 0x00, 0xae, 0x00, 0x00, 0x00, 0x73, 0x69, 0x6e, 0x54, 0x68, 0x65, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x73, 0x6c, 0x6f, 0x70, - 0x65, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xbb, 0x00, 0x00, 0x00, - 0x73, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xc4, 0x00, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x44, - 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xde, 0x00, 0x00, 0x00, - 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x64, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x06, 0x00, 0xed, 0x00, 0x00, 0x00, - 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x61, 0x77, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x66, 0x61, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x04, 0x01, 0x00, 0x00, 0x76, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, - 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x12, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x15, 0x01, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x61, 0x64, 0x69, 0x75, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x23, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x25, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x29, 0x01, 0x00, 0x00, 0x75, 0x70, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x34, 0x01, 0x00, 0x00, 0x74, 0x61, 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x38, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x62, 0x69, 0x74, 0x61, 0x6e, 0x67, 0x65, 0x6e, - 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x74, 0x61, 0x70, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x43, 0x01, 0x00, 0x00, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x44, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x46, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x50, 0x01, 0x00, 0x00, 0x6f, 0x31, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x54, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x55, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x58, 0x01, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5c, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5d, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x60, 0x01, 0x00, 0x00, - 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x64, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x65, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x68, 0x01, 0x00, 0x00, 0x6f, 0x34, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x80, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x94, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x95, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa9, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xbc, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xbe, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xbf, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x6f, 0x35, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xd1, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xd2, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0xd5, 0x01, 0x00, 0x00, 0x6f, 0x36, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd9, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xda, 0x01, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xdd, 0x01, 0x00, 0x00, - 0x6f, 0x37, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe1, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe2, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x6f, 0x38, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xe9, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xea, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xfd, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x11, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x12, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x24, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x26, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x27, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x39, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x6f, 0x39, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x4f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x50, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, - 0x53, 0x02, 0x00, 0x00, 0x6f, 0x31, 0x30, 0x00, 0x05, 0x00, 0x04, 0x00, 0x57, 0x02, 0x00, 0x00, + 0x05, 0x00, 0x0b, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x45, 0x76, 0x61, 0x6c, 0x75, 0x61, 0x74, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x42, 0x52, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, + 0x32, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x54, 0x65, + 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x6f, 0x72, 0x6d, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x19, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x53, 0x74, 0x61, 0x62, + 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x48, 0x61, 0x73, 0x68, 0x28, 0x76, 0x66, 0x33, + 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x23, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, + 0x6f, 0x77, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, + 0x65, 0x72, 0x42, 0x69, 0x61, 0x73, 0x28, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x68, 0x61, 0x64, 0x6f, + 0x77, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x66, 0x31, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x29, 0x00, 0x00, 0x00, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x46, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x28, 0x00, 0x05, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x56, 0x69, 0x65, 0x77, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x51, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, + 0x6c, 0x6f, 0x66, 0x66, 0x53, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, + 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, + 0x78, 0x53, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x53, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x54, 0x00, 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x66, 0x6c, 0x61, 0x74, + 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x57, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x54, 0x6f, 0x42, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x62, 0x61, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x61, 0x6c, 0x62, 0x65, 0x64, 0x6f, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x77, 0x00, 0x00, 0x00, + 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x81, 0x00, 0x00, 0x00, 0x6f, 0x72, 0x6d, 0x00, 0x05, 0x00, 0x05, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x87, 0x00, 0x00, 0x00, 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x09, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6d, 0x76, 0x70, 0x00, 0x06, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x61, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x94, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x6e, 0x65, 0x73, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xab, 0x00, 0x00, 0x00, + 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xac, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x44, 0x69, 0x72, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x76, 0x56, 0x69, 0x65, 0x77, 0x56, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, + 0x44, 0x69, 0x72, 0x00, 0x05, 0x00, 0x04, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x6c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x76, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xca, 0x00, 0x00, 0x00, 0x76, 0x64, 0x6f, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x61, 0x6c, 0x70, 0x68, + 0x61, 0x53, 0x71, 0x75, 0x61, 0x72, 0x65, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xd7, 0x00, 0x00, 0x00, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xe1, 0x00, 0x00, 0x00, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x67, 0x65, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x79, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x05, 0x01, 0x00, 0x00, 0x66, 0x30, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x66, 0x72, 0x65, 0x73, 0x6e, 0x65, 0x6c, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x16, 0x01, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x24, 0x01, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, + 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x72, 0x61, 0x64, 0x69, + 0x61, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x2f, 0x01, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x32, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, + 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x38, 0x01, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, + 0x3a, 0x01, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x50, 0x01, 0x00, 0x00, 0x76, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, + 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x6f, 0x77, + 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x61, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x6f, 0x77, 0x31, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x6f, 0x77, + 0x32, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x61, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x46, 0x61, 0x72, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x00, 0x61, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x62, 0x69, 0x61, 0x73, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x61, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x63, 0x01, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x73, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x77, 0x01, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x7a, 0x01, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x87, 0x01, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x43, 0x6f, 0x73, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x89, 0x01, 0x00, 0x00, 0x76, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x4c, 0x69, 0x67, 0x68, 0x74, + 0x43, 0x6f, 0x73, 0x00, 0x05, 0x00, 0x05, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x73, 0x69, 0x6e, 0x54, + 0x68, 0x65, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x94, 0x01, 0x00, 0x00, + 0x73, 0x6c, 0x6f, 0x70, 0x65, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x99, 0x01, 0x00, 0x00, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42, 0x69, 0x61, 0x73, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x74, 0x65, 0x78, 0x65, 0x6c, 0x42, 0x69, 0x61, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xae, 0x01, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, + 0x61, 0x72, 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0xbb, 0x01, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, + 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xc6, 0x01, 0x00, 0x00, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xca, 0x01, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x52, 0x61, 0x77, 0x4d, 0x61, 0x70, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x66, 0x61, 0x72, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x76, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x68, + 0x61, 0x64, 0x6f, 0x77, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xee, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x61, + 0x64, 0x69, 0x75, 0x73, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x01, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x05, 0x02, 0x00, 0x00, + 0x75, 0x70, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x10, 0x02, 0x00, 0x00, 0x74, 0x61, 0x6e, 0x67, + 0x65, 0x6e, 0x74, 0x00, 0x05, 0x00, 0x04, 0x00, 0x14, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x16, 0x02, 0x00, 0x00, 0x62, 0x69, 0x74, 0x61, + 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x74, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x22, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2e, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x6f, 0x31, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x37, 0x02, 0x00, 0x00, 0x6f, 0x32, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x6f, 0x33, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x43, 0x02, 0x00, 0x00, 0x6f, 0x34, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x56, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x58, 0x02, 0x00, 0x00, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x5b, 0x02, 0x00, 0x00, - 0x6f, 0x31, 0x31, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x60, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x63, 0x02, 0x00, 0x00, 0x6f, 0x31, 0x32, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x67, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x68, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x78, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x90, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa2, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa4, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xb9, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xba, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, - 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xc6, 0x02, 0x00, 0x00, - 0x76, 0x42, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x06, 0x00, 0xc8, 0x02, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, - 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, 0xc9, 0x02, 0x00, 0x00, - 0x76, 0x44, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x07, 0x00, 0xcc, 0x02, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, - 0x72, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, - 0x75, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x76, 0x70, 0x00, - 0x06, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x03, 0x00, 0xd1, 0x02, 0x00, 0x00, 0x70, 0x63, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xd8, 0x02, 0x00, 0x00, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xdc, 0x02, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, - 0xe1, 0x02, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0xe2, 0x02, 0x00, 0x00, 0x76, 0x56, 0x69, 0x65, 0x77, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, - 0x05, 0x00, 0x04, 0x00, 0xe3, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0xf9, 0x02, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x6c, 0x69, 0x67, 0x68, 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, - 0x17, 0x03, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x18, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x6e, 0x64, 0x6f, 0x74, - 0x6c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x21, 0x03, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x24, 0x03, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, - 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, 0x66, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x08, 0x00, - 0x27, 0x03, 0x00, 0x00, 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, 0x6c, 0x6c, 0x6f, 0x66, - 0x66, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x2f, 0x03, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x31, 0x03, 0x00, 0x00, - 0x76, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, - 0x3c, 0x03, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x3d, 0x03, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x48, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x4a, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, - 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, - 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x04, 0x00, 0x50, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x53, 0x03, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, - 0x44, 0x6f, 0x74, 0x00, 0x05, 0x00, 0x06, 0x00, 0x58, 0x03, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, - 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x72, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, - 0x59, 0x03, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, - 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x60, 0x03, 0x00, 0x00, - 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, - 0x61, 0x03, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x4d, 0x61, 0x70, 0x00, - 0x05, 0x00, 0x05, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x74, 0x03, 0x00, 0x00, 0x76, 0x56, 0x65, 0x72, - 0x74, 0x65, 0x78, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xa0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0xe0, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x35, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x59, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6b, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6d, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6e, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x80, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x82, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x83, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x95, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x97, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x98, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xa5, 0x02, 0x00, 0x00, + 0x6f, 0x35, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xab, 0x02, 0x00, 0x00, 0x6f, 0x36, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xb1, 0x02, 0x00, 0x00, 0x6f, 0x37, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xb7, 0x02, 0x00, 0x00, 0x6f, 0x38, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xca, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcc, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xcd, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xdf, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe1, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe2, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf4, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf6, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xf7, 0x02, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x09, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0b, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0c, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x1b, 0x03, 0x00, 0x00, + 0x6f, 0x39, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, 0x6f, 0x31, 0x30, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x27, 0x03, 0x00, 0x00, 0x6f, 0x31, 0x31, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x2d, 0x03, 0x00, 0x00, 0x6f, 0x31, 0x32, 0x00, 0x05, 0x00, 0x04, 0x00, 0x40, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x42, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x43, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x55, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x57, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x58, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6a, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6c, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6d, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7f, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x81, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x82, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x96, 0x03, 0x00, 0x00, 0x62, 0x75, 0x6d, 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x98, 0x03, 0x00, 0x00, 0x76, 0x42, 0x75, 0x6d, + 0x70, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x9a, 0x03, 0x00, 0x00, 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, + 0x6f, 0x72, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, 0x9b, 0x03, 0x00, 0x00, 0x76, 0x44, 0x69, 0x66, + 0x66, 0x75, 0x73, 0x65, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x9e, 0x03, 0x00, 0x00, 0x76, 0x53, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x78, + 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa5, 0x03, 0x00, 0x00, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0x62, 0x75, 0x6d, 0x70, 0x4d, 0x61, 0x70, 0x00, 0x05, 0x00, 0x04, 0x00, 0xab, 0x03, 0x00, 0x00, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xac, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xc2, 0x03, 0x00, 0x00, + 0x62, 0x75, 0x6d, 0x70, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xcb, 0x03, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdb, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdf, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe2, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xe4, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x72, + 0x6d, 0x61, 0x6c, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf6, 0x03, 0x00, 0x00, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x44, 0x69, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x01, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x05, 0x04, 0x00, 0x00, + 0x6e, 0x64, 0x6f, 0x74, 0x6c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x0a, 0x04, 0x00, 0x00, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x04, 0x00, 0x00, + 0x64, 0x69, 0x66, 0x66, 0x75, 0x73, 0x65, 0x00, 0x05, 0x00, 0x04, 0x00, 0x29, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2b, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2f, 0x04, 0x00, 0x00, + 0x68, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x30, 0x04, 0x00, 0x00, 0x76, 0x48, 0x61, 0x6c, 0x66, 0x41, 0x6e, 0x67, 0x6c, 0x65, 0x56, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x31, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x34, 0x04, 0x00, 0x00, + 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x44, 0x6f, 0x74, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x39, 0x04, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, 0x54, 0x65, 0x72, 0x6d, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x3a, 0x04, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, + 0x75, 0x6c, 0x61, 0x72, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x61, 0x70, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x41, 0x04, 0x00, 0x00, 0x73, 0x70, 0x65, 0x63, 0x75, 0x6c, 0x61, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0x78, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x78, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xaa, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xde, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xde, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xed, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xed, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0x04, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xc6, 0x02, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xc9, 0x02, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, - 0xcc, 0x02, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, 0xcf, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xcf, 0x02, 0x00, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x3a, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x04, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x48, 0x00, 0x05, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0xcf, 0x02, 0x00, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xdc, 0x02, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xdc, 0x02, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0xe2, 0x02, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x17, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x24, 0x03, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x24, 0x03, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x27, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x2f, 0x03, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x2f, 0x03, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x31, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x59, 0x03, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x59, 0x03, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x61, 0x03, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x61, 0x03, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x04, 0x00, 0x74, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x19, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x21, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0xab, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x32, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x38, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x38, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x04, 0x00, 0x50, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x03, 0x00, 0x61, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x61, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x61, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xbb, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xbb, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xca, 0x01, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xca, 0x01, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x98, 0x03, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9b, 0x03, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x9e, 0x03, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x30, 0x04, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x3a, 0x04, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x3a, 0x04, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x21, 0x00, 0x07, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x25, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, 0x17, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x11, 0x00, 0x33, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, - 0x38, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x38, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x38, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x38, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x38, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, - 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x39, 0xd6, 0x4f, 0x41, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x4c, 0x77, 0x9c, 0x42, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x42, 0xe0, 0x16, 0x42, 0x2c, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, - 0x6f, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, - 0x8c, 0xee, 0x2a, 0x47, 0x1e, 0x00, 0x09, 0x00, 0x78, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x79, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x42, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x8b, 0x00, 0x00, 0x00, 0xdb, 0x0f, 0xc9, 0x40, 0x20, 0x00, 0x04, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0xaa, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xac, 0x00, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, - 0xbc, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, - 0xd5, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x77, 0xcc, 0x2b, 0x32, 0x17, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x11, 0x00, 0x3a, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x3d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x3d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x41, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x02, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + 0xec, 0x51, 0x38, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x0a, 0xd7, 0x23, 0x3e, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, + 0x74, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x03, 0x00, 0x75, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, + 0xcd, 0xcc, 0x0c, 0x40, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x76, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, + 0x8a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x8b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x8c, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0xab, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0xb1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0x17, 0xb7, 0xd1, 0x38, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe3, 0x00, 0x00, 0x00, 0xdb, 0x0f, 0x49, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x06, 0x01, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3d, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x07, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x83, 0xf9, 0xa2, 0x3e, + 0x3b, 0x00, 0x04, 0x00, 0x76, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x31, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x31, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x76, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x31, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x42, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x39, 0xd6, 0x4f, 0x41, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x4c, 0x77, 0x9c, 0x42, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x42, 0xe0, 0x16, 0x42, + 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, + 0x57, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5c, 0x01, 0x00, 0x00, 0x8c, 0xee, 0x2a, 0x47, 0x1e, 0x00, 0x09, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x3d, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, + 0x6a, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x70, 0x01, 0x00, 0x00, 0x00, 0x00, 0x14, 0x42, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x75, 0x01, 0x00, 0x00, 0xdb, 0x0f, 0xc9, 0x40, 0x20, 0x00, 0x04, 0x00, 0x88, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x88, 0x01, 0x00, 0x00, + 0x89, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8b, 0x01, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0xb2, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xb8, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, - 0xdc, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xdd, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdd, 0x00, 0x00, 0x00, - 0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xea, 0x00, 0x00, 0x00, + 0xb9, 0x01, 0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xba, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xba, 0x01, 0x00, 0x00, + 0xbb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xc7, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, - 0xeb, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xec, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xec, 0x00, 0x00, 0x00, - 0xed, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, - 0xf9, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, - 0x04, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2d, 0x01, 0x00, 0x00, 0xa4, 0x70, 0x7d, 0x3f, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x2f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x31, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x51, 0x01, 0x00, 0x00, 0x42, 0x05, 0xa7, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x52, 0x01, 0x00, 0x00, 0xac, 0xc5, 0xcf, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x53, 0x01, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0xad, 0x13, 0x57, 0xbf, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x1c, 0xb1, 0x96, 0xbd, 0x2c, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x6b, 0x27, 0x32, 0xbf, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x01, 0x00, 0x00, 0xdc, 0x0d, 0xea, 0x3e, - 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, - 0x62, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, - 0xac, 0x39, 0x50, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, - 0x3e, 0xe7, 0x1e, 0x3f, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, - 0x69, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc6, 0x01, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xce, 0x01, 0x00, 0x00, 0xea, 0x5b, 0x76, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xcf, 0x01, 0x00, 0x00, 0xa0, 0xa9, 0x47, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xd0, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0xf1, 0x65, 0xf2, 0x3e, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0xf8, 0xc5, 0xf5, 0xbe, 0x2c, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0x12, 0xfb, 0x04, 0x3f, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x8e, 0x5b, 0x44, 0x3f, - 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, - 0xdf, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, - 0x7d, 0xe9, 0x3d, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, - 0xc6, 0xa3, 0x64, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, - 0xe6, 0x01, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x43, 0x02, 0x00, 0x00, 0x00, 0x00, 0x10, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x48, 0x02, 0x00, 0x00, 0x39, 0x8e, 0xe3, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4c, 0x02, 0x00, 0x00, 0xff, 0xe6, 0x01, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x4d, 0x02, 0x00, 0x00, 0x41, 0xf1, 0x83, 0x3d, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x4e, 0x02, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, 0xc8, 0x7b, 0x65, 0x3f, 0x2b, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0xb2, 0x2d, 0xd3, 0x3e, 0x2c, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x52, 0xd5, 0xa4, 0xbe, - 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, 0xdb, 0xbf, 0x6e, 0xbf, - 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, - 0x5d, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x02, 0x00, 0x00, - 0x9c, 0xa3, 0x4a, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, - 0x32, 0x03, 0x19, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, - 0x64, 0x02, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc0, 0x02, 0x00, 0x00, 0xd9, 0x89, 0x9d, 0x3d, 0x20, 0x00, 0x04, 0x00, 0xc5, 0x02, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc5, 0x02, 0x00, 0x00, - 0xc6, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc5, 0x02, 0x00, 0x00, - 0xc9, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc5, 0x02, 0x00, 0x00, - 0xcc, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x04, 0x00, 0xce, 0x02, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x07, 0x00, 0xcf, 0x02, 0x00, 0x00, - 0xce, 0x02, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xd0, 0x02, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0xcf, 0x02, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xd0, 0x02, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xd2, 0x02, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0xd9, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, 0xda, 0x02, 0x00, 0x00, - 0xd9, 0x02, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xda, 0x02, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xf8, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x12, 0x03, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x26, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x26, 0x03, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x2f, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x26, 0x03, 0x00, 0x00, 0x31, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x43, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x61, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x36, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x03, 0x01, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0xc8, 0x01, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xc9, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xc9, 0x01, 0x00, 0x00, + 0xca, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0xd6, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, + 0xe0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x09, 0x02, 0x00, 0x00, 0xa4, 0x70, 0x7d, 0x3f, 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0b, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x0d, 0x02, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x2c, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x33, 0x02, 0x00, 0x00, 0x42, 0x05, 0xa7, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x34, 0x02, 0x00, 0x00, 0xac, 0xc5, 0xcf, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x35, 0x02, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0xad, 0x13, 0x57, 0xbf, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x1c, 0xb1, 0x96, 0xbd, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, 0x6b, 0x27, 0x32, 0xbf, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0xdc, 0x0d, 0xea, 0x3e, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x41, 0x02, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, + 0x40, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, + 0xac, 0x39, 0x50, 0xbe, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x3e, 0xe7, 0x1e, 0x3f, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, + 0x45, 0x02, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa7, 0x02, 0x00, 0x00, 0xea, 0x5b, 0x76, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa8, 0x02, 0x00, 0x00, 0xa0, 0xa9, 0x47, 0xbe, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xa9, 0x02, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, 0xf1, 0x65, 0xf2, 0x3e, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, 0xf8, 0xc5, 0xf5, 0xbe, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x12, 0xfb, 0x04, 0x3f, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, 0x8e, 0x5b, 0x44, 0x3f, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, + 0xb4, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, + 0x7d, 0xe9, 0x3d, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, + 0xc6, 0xa3, 0x64, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, + 0xb9, 0x02, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x13, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x18, 0x03, 0x00, 0x00, 0x39, 0x8e, 0xe3, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1d, 0x03, 0x00, 0x00, 0xff, 0xe6, 0x01, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1e, 0x03, 0x00, 0x00, 0x41, 0xf1, 0x83, 0x3d, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x1f, 0x03, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0xc8, 0x7b, 0x65, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, 0xb2, 0x2d, 0xd3, 0x3e, 0x2c, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, 0x52, 0xd5, 0xa4, 0xbe, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x03, 0x00, 0x00, 0xdb, 0xbf, 0x6e, 0xbf, + 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, + 0x2a, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x03, 0x00, 0x00, + 0x9c, 0xa3, 0x4a, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, + 0x32, 0x03, 0x19, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x31, 0x03, 0x00, 0x00, + 0x2f, 0x03, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x88, 0x03, 0x00, 0x00, 0xd9, 0x89, 0x9d, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8e, 0x03, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f, 0x20, 0x00, 0x04, 0x00, 0x92, 0x03, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x92, 0x03, 0x00, 0x00, + 0x93, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x07, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x94, 0x03, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x97, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x97, 0x03, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x97, 0x03, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x97, 0x03, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x76, 0x00, 0x00, 0x00, 0xa6, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0xc1, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x3b, 0x00, 0x04, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x76, 0x00, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x18, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0xf8, 0x02, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0xc1, 0x03, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xdb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf6, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xfa, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x41, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x8c, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8d, 0x03, 0x00, 0x00, 0x8c, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x8f, 0x03, 0x00, 0x00, 0x8d, 0x03, 0x00, 0x00, 0x8e, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x91, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x8f, 0x03, 0x00, 0x00, + 0x90, 0x03, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x90, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x93, 0x03, 0x00, 0x00, 0x94, 0x03, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x91, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x99, 0x03, 0x00, 0x00, 0x98, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x96, 0x03, 0x00, 0x00, + 0x99, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, + 0x9b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, + 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, + 0xa0, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0xa2, 0x03, 0x00, 0x00, + 0xa1, 0x03, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xa4, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xa2, 0x03, 0x00, 0x00, 0xa3, 0x03, 0x00, 0x00, + 0xa4, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x75, 0x00, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, 0xa6, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x39, 0x00, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa5, 0x03, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xad, 0x03, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xac, 0x03, 0x00, 0x00, 0xad, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xac, 0x03, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, 0xaf, 0x03, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, + 0xae, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0xb1, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, + 0xb1, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x03, 0x00, 0x00, + 0xb0, 0x03, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0xb4, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb5, 0x03, 0x00, 0x00, 0xb4, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, 0xb3, 0x03, 0x00, 0x00, + 0xb5, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb7, 0x03, 0x00, 0x00, + 0xaf, 0x03, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xab, 0x03, 0x00, 0x00, + 0xb7, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, + 0xab, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, + 0x96, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xba, 0x03, 0x00, 0x00, + 0xb9, 0x03, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x96, 0x03, 0x00, 0x00, + 0xba, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbb, 0x03, 0x00, 0x00, + 0xab, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbc, 0x03, 0x00, 0x00, + 0x9a, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, + 0xbc, 0x03, 0x00, 0x00, 0xbb, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9a, 0x03, 0x00, 0x00, + 0xbd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, + 0xab, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, + 0x9d, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, + 0xbf, 0x03, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9d, 0x03, 0x00, 0x00, + 0xc0, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa4, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa4, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, 0xc3, 0x03, 0x00, 0x00, + 0xa6, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, + 0x96, 0x03, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, + 0xc3, 0x03, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc2, 0x03, 0x00, 0x00, + 0xc5, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc7, 0x03, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0xc7, 0x03, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xca, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xc8, 0x03, 0x00, 0x00, 0xc9, 0x03, 0x00, 0x00, 0xca, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xc9, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, + 0xc2, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, + 0xcc, 0x03, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, + 0xcd, 0x03, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd0, 0x03, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, + 0xd0, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x03, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0xd2, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd3, 0x03, 0x00, 0x00, 0xd2, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd4, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xd5, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, + 0xd5, 0x03, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xd7, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd7, 0x03, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xd9, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0xd6, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd9, 0x03, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdc, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x03, 0x00, 0x00, 0xdc, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xdb, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x03, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xe3, 0x03, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe2, 0x03, 0x00, 0x00, + 0xe3, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe4, 0x03, 0x00, 0x00, 0xde, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0xdf, 0x03, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0xe5, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe8, 0x03, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, + 0x39, 0x00, 0x00, 0x00, 0xe9, 0x03, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0xe8, 0x03, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x93, 0x03, 0x00, 0x00, + 0xe9, 0x03, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xca, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xec, 0x03, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, + 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xed, 0x03, 0x00, 0x00, + 0xec, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, + 0xc2, 0x03, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xef, 0x03, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xf0, 0x03, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, 0xed, 0x03, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, + 0xf1, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, + 0xf2, 0x03, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf4, 0x03, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, + 0xf4, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xeb, 0x03, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf8, 0x03, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xf9, 0x03, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xf9, 0x03, 0x00, 0x00, + 0xfb, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfb, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xfd, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, + 0xfd, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, + 0xfe, 0x03, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfa, 0x03, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x01, 0x04, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xfa, 0x03, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0xfa, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf6, 0x03, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x06, 0x04, 0x00, 0x00, 0xf6, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x07, 0x04, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x08, 0x04, 0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x07, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x08, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x05, 0x04, 0x00, 0x00, + 0x09, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, + 0x05, 0x04, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, + 0x0b, 0x04, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x0d, 0x04, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x0e, 0x04, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0f, 0x04, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, + 0x0d, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x12, 0x04, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x13, 0x04, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x14, 0x04, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x12, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0x14, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x15, 0x04, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x16, 0x04, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x17, 0x04, 0x00, 0x00, 0x16, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x18, 0x04, 0x00, 0x00, 0x17, 0x04, 0x00, 0x00, 0x16, 0x04, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x15, 0x04, 0x00, 0x00, 0x18, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1a, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1b, 0x04, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1c, 0x04, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1d, 0x04, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1e, 0x04, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1f, 0x04, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x21, 0x04, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x22, 0x04, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x23, 0x04, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x42, 0x01, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, + 0x26, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, + 0x27, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x20, 0x04, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x29, 0x04, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x88, 0x01, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, + 0x2b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x20, 0x04, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x31, 0x04, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x35, 0x04, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, 0x35, 0x04, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x34, 0x04, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x75, 0x00, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, + 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, + 0x3d, 0x04, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, + 0x3e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x40, 0x04, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x39, 0x04, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x42, 0x04, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x43, 0x04, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, 0x43, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x42, 0x01, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, + 0x48, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, + 0x49, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, + 0x39, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, + 0x4a, 0x04, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x41, 0x04, 0x00, 0x00, + 0x4c, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4d, 0x04, 0x00, 0x00, + 0x20, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, + 0x41, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, + 0x4d, 0x04, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x50, 0x04, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x51, 0x04, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x50, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, + 0x53, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x56, 0x04, 0x00, 0x00, 0x53, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, 0x54, 0x04, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, + 0x56, 0x04, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x57, 0x04, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x36, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x43, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x46, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x46, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x47, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x49, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x48, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x41, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x56, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4b, 0x00, 0x00, 0x00, + 0x56, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, + 0x4b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x57, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x66, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x65, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x63, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x6b, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, + 0x6d, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, + 0x6e, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x6f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x75, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x7b, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x7c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x73, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x75, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x81, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x91, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x92, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x87, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x95, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, + 0x97, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x96, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x94, 0x00, 0x00, 0x00, + 0x9a, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xa0, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, + 0xa7, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xa8, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0xa8, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, + 0xab, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xac, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa1, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xa3, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xa3, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9b, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb2, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb4, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb0, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb6, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb7, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb9, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xba, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb5, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xbc, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xbd, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xbe, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xbe, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0xbf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, + 0xb0, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0xc1, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc4, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, + 0xc7, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xca, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd2, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xcf, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd4, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd5, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd6, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xd3, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd8, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd9, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xda, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, + 0xdc, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, + 0xdd, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd7, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, + 0xe4, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, + 0xd7, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, + 0xe5, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe8, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe1, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xea, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0xed, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, + 0xee, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe9, 0x00, 0x00, 0x00, + 0xf0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0xbb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, + 0xbb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, + 0xe9, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf6, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0xfd, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x03, 0x01, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, + 0x09, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x0a, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x05, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, + 0xca, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x13, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x12, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, + 0x0f, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x15, 0x01, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0c, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x17, 0x01, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x18, 0x01, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x19, 0x01, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, + 0x1d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, + 0x1e, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x21, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, + 0xdf, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, + 0x21, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x16, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x27, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, + 0x26, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x2a, 0x01, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x2b, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x24, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x75, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x06, 0x00, 0x39, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, + 0x33, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x36, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x75, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x06, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x3b, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x3e, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x42, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x41, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x39, 0x00, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, + 0x43, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, + 0x44, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, + 0x40, 0x01, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x47, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x48, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x2e, 0x01, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x49, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x4a, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x4b, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, + 0x4e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, + 0x50, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, + 0x4f, 0x01, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x52, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5d, 0x01, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x5d, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x24, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x6e, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x73, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x77, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x7a, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x65, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x66, 0x01, 0x00, 0x00, + 0x9f, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x69, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x67, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x68, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x6c, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x69, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x6f, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x73, 0x01, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x73, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, + 0x74, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x01, 0x00, 0x00, + 0x76, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, + 0x6e, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x77, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7b, 0x01, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x81, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x84, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x87, 0x01, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x90, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x1f, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, + 0x8d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x98, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x1c, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x94, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x8e, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9b, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x9c, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, + 0x9e, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, + 0x9b, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x99, 0x01, 0x00, 0x00, + 0xa0, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, + 0xa5, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa1, 0x01, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa8, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xa9, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xab, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xa8, 0x01, 0x00, 0x00, + 0xaa, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xab, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, + 0x2a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, + 0xaf, 0x01, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xae, 0x01, 0x00, 0x00, + 0xb1, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xb5, 0x01, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xb6, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xb9, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, + 0xbb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, + 0xae, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, + 0xbd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc0, 0x01, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x07, 0x00, 0x39, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, + 0xc0, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x59, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, + 0xc2, 0x01, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xc4, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xcb, 0x01, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xcc, 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x39, 0x00, 0x00, 0x00, + 0xcd, 0x01, 0x00, 0x00, 0xcb, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, + 0xa9, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xd2, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x14, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x2c, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x53, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x14, 0x00, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x43, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x58, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xc8, 0x02, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xcd, 0x02, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x02, 0x00, 0x00, - 0xcd, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, - 0xd1, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0xd5, 0x02, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xd7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xd5, 0x02, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xd6, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0xdd, 0x02, 0x00, 0x00, - 0xdc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, - 0xc4, 0x02, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, - 0xdd, 0x02, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xe0, 0x02, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xd8, 0x02, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xe4, 0x02, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe3, 0x02, 0x00, 0x00, - 0xe4, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xe6, 0x02, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, - 0xd8, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, - 0xd1, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, - 0xf9, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xec, 0x02, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xed, 0x02, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, 0xed, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe1, 0x02, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc8, 0x02, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x02, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0xd7, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd7, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0xda, 0x02, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, - 0x32, 0x00, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xf9, 0x02, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0xd6, 0x01, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd8, 0x01, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd5, 0x01, 0x00, 0x00, + 0xd8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, + 0xd5, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, + 0xd9, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xdb, 0x01, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xdd, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xdb, 0x01, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, + 0xdd, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdc, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x55, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe1, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0xe1, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, + 0xd5, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, + 0xe2, 0x01, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xdf, 0x01, 0x00, 0x00, + 0xe4, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, + 0xe5, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe7, 0x01, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xe8, 0x01, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xeb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xe9, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xea, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x55, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xeb, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, + 0xe0, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xee, 0x01, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0xee, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xed, 0x01, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0x64, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf3, 0x01, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x01, 0x00, 0x00, + 0xf3, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, + 0xf1, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, + 0xf4, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xf6, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xf8, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xf6, 0x01, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, + 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x41, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, + 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, + 0xf9, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, + 0xfa, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xf8, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xfc, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, + 0xf7, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xfc, 0x01, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xfd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x03, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x06, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x07, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x0d, 0x02, 0x00, 0x00, + 0x0e, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, + 0xa9, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, + 0x0b, 0x02, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x05, 0x02, 0x00, 0x00, + 0x0f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, + 0x05, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x14, 0x02, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x14, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x10, 0x02, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, + 0x17, 0x02, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x16, 0x02, 0x00, 0x00, + 0x19, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x20, 0x02, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x23, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x22, 0x02, 0x00, 0x00, + 0x23, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x25, 0x02, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, + 0x55, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x27, 0x02, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x28, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2a, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x2a, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x29, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x2f, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2e, 0x02, 0x00, 0x00, + 0x2f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x2d, 0x02, 0x00, 0x00, + 0x30, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x32, 0x02, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x36, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, + 0x38, 0x02, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x37, 0x02, 0x00, 0x00, + 0x3c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, + 0x3e, 0x02, 0x00, 0x00, 0x41, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3d, 0x02, 0x00, 0x00, + 0x42, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, 0x44, 0x02, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, 0x48, 0x02, 0x00, 0x00, + 0x44, 0x02, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x02, 0x00, 0x00, + 0x48, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x49, 0x02, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4a, 0x02, 0x00, 0x00, + 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, + 0x31, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4c, 0x02, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x4d, 0x02, 0x00, 0x00, 0x4a, 0x02, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x4e, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, 0x4e, 0x02, 0x00, 0x00, + 0x50, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, + 0x4d, 0x02, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x53, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x54, 0x02, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0x49, 0x02, 0x00, 0x00, 0x54, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x56, 0x02, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x56, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x58, 0x02, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5a, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x59, 0x02, 0x00, 0x00, 0x5a, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x5b, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, + 0x5b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, + 0x60, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, + 0x5f, 0x02, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x63, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x64, 0x02, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, 0x64, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x67, 0x02, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, + 0x66, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, + 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, + 0x67, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x6a, 0x02, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0x6a, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x6c, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6d, 0x02, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x02, 0x00, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x73, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, + 0x76, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, + 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, + 0x3d, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7a, 0x02, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x7b, 0x02, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, + 0x7d, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7f, 0x02, 0x00, 0x00, + 0x73, 0x02, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x80, 0x02, 0x00, 0x00, + 0x7f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x81, 0x02, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x02, 0x00, 0x00, + 0x81, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x83, 0x02, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x82, 0x02, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x86, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x87, 0x02, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x88, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x89, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x8a, 0x02, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x43, 0x02, 0x00, 0x00, + 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, + 0x8e, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, + 0x8d, 0x02, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x91, 0x02, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, + 0x93, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x95, 0x02, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x95, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x97, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x98, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, + 0x98, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x02, 0x00, 0x00, + 0x9b, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x9c, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x9f, 0x02, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xa0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, + 0xa2, 0x02, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xa3, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xaa, 0x02, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xa9, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa5, 0x02, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xac, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xb0, 0x02, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xab, 0x02, 0x00, 0x00, 0xb0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xb6, 0x02, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb1, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x20, 0x00, 0x00, 0x00, + 0xb8, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xbc, 0x02, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb7, 0x02, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xbd, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xbe, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xbf, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, + 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, + 0xc3, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, + 0xc2, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xc6, 0x02, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, + 0xc8, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xca, 0x02, 0x00, 0x00, 0xc9, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0xca, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcc, 0x02, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xce, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xcd, 0x02, 0x00, 0x00, 0xce, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, + 0xcd, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, + 0xd0, 0x02, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0xd1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, + 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, + 0xab, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd5, 0x02, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xd6, 0x02, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0xd5, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xda, 0x02, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, + 0xd9, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, + 0xd6, 0x02, 0x00, 0x00, 0xda, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdc, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xdd, 0x02, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xdd, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xdf, 0x02, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xdf, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe1, 0x02, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe2, 0x02, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe4, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, + 0xe4, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, + 0xe9, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, + 0xe8, 0x02, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xec, 0x02, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xed, 0x02, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0xed, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0xec, 0x02, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, + 0xef, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, + 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, + 0xf0, 0x02, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf3, 0x02, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf4, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf5, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf6, 0x02, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf8, 0x02, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf7, 0x02, 0x00, 0x00, + 0xf8, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xf6, 0x02, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0xfe, 0x02, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, - 0xf9, 0x02, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x03, 0x03, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x04, 0x03, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00, - 0x05, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0xd2, 0x02, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, - 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, - 0x0b, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, - 0x0c, 0x03, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x10, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, - 0x16, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0f, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x12, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, - 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, - 0x14, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, - 0x10, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x16, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x19, 0x03, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x18, 0x03, 0x00, 0x00, 0x19, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1a, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x0e, 0x03, 0x00, 0x00, 0x1a, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x10, 0x03, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x10, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1b, 0x03, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x09, 0x03, 0x00, 0x00, - 0x1b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, - 0x09, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, - 0xfd, 0x02, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, - 0x1d, 0x03, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x20, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, - 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x03, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, - 0x22, 0x03, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, - 0x23, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, - 0x24, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, - 0x27, 0x03, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, - 0x28, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x2a, 0x03, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, - 0x2a, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, - 0x2b, 0x03, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, - 0x2d, 0x03, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, - 0x2e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, - 0x2f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x32, 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, - 0x31, 0x03, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, - 0x32, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x52, 0x00, 0x06, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x34, 0x03, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, - 0x34, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, - 0x35, 0x03, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, - 0x37, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, - 0x38, 0x03, 0x00, 0x00, 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, - 0x26, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3a, 0x03, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, - 0x3a, 0x03, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, - 0x3b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, - 0x3d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, - 0xc8, 0x02, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, - 0x3e, 0x03, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x41, 0x03, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x43, 0x03, 0x00, 0x00, - 0x44, 0x03, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x48, 0x03, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0xa9, 0x00, 0x00, 0x00, - 0x4b, 0x03, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x4a, 0x03, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x4d, 0x03, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x50, 0x03, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x52, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x4e, 0x03, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x54, 0x03, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x55, 0x03, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x56, 0x03, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0x56, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x53, 0x03, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, - 0x5a, 0x03, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x5b, 0x03, 0x00, 0x00, 0x53, 0x03, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x5c, 0x03, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, - 0x5e, 0x03, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x03, 0x00, 0x00, - 0x5f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, - 0x61, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, - 0xcb, 0x02, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, - 0x62, 0x03, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x65, 0x03, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x43, 0x03, 0x00, 0x00, - 0x67, 0x03, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x32, 0x00, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, 0x67, 0x03, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, 0x58, 0x03, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, - 0x6b, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x60, 0x03, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, - 0x70, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, - 0x21, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, - 0x71, 0x03, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x75, 0x03, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x76, 0x03, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, - 0x76, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x32, 0x00, 0x00, 0x00, - 0x7a, 0x03, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, - 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x12, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x41, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x42, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x41, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x47, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, - 0x45, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, - 0x4b, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x4b, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, - 0x4c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x55, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, - 0x55, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x50, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x58, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x59, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x5a, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, - 0x58, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x50, 0x00, 0x00, 0x00, - 0x5b, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, - 0x5f, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, - 0x4e, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x00, 0x00, 0x00, - 0x61, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, - 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, - 0x35, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, - 0x67, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x69, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, - 0x63, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x69, 0x00, 0x00, 0x00, - 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x74, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x74, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x75, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x19, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x1d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x7f, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x80, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, - 0x1a, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x82, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x81, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, - 0x1b, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x88, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x89, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, - 0x8b, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x84, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8d, 0x00, 0x00, 0x00, - 0x8f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, - 0x84, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x90, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x93, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x94, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, - 0x98, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, - 0x97, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x9b, 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, - 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, - 0x9e, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, - 0x90, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, - 0x1a, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa2, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa3, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, - 0x50, 0x00, 0x05, 0x00, 0x17, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, - 0xa4, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xa5, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, - 0xab, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xa8, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xaf, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb0, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xb1, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0xb3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xae, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, - 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, - 0xb7, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0xbd, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, - 0xc2, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbb, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, - 0xbc, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc6, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc7, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xc8, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xca, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0xca, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xcc, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xcd, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xcb, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xce, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, - 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x25, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x04, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x39, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, - 0xd3, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, - 0xd5, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xd7, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0xd8, 0x00, 0x00, 0x00, 0xd7, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xd8, 0x00, 0x00, 0x00, - 0xd9, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd9, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, - 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, - 0xe0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xe4, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, - 0x32, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, - 0xe4, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xe6, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x59, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, - 0xe6, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xda, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, - 0xed, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x32, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, - 0xee, 0x00, 0x00, 0x00, 0xef, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xf1, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xe9, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xf2, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xf3, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0xf4, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, - 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xe1, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x57, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x67, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x08, 0x00, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, - 0x52, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, - 0xba, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x3e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, - 0xfd, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x05, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x06, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, - 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, - 0x07, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x02, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, - 0x0b, 0x01, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0x0d, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, - 0x0f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x0d, 0x01, 0x00, 0x00, - 0x0e, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0e, 0x01, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0x4e, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0f, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x12, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x11, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, - 0x3a, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, - 0x16, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x15, 0x01, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x19, 0x01, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, - 0x19, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, - 0x1d, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x1c, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x1c, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, - 0x19, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x22, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x20, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x21, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x23, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x25, 0x01, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, - 0x25, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x27, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x22, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2b, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x2c, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, - 0xb8, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, - 0x2d, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x31, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, - 0x2e, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0xa9, 0x00, 0x06, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, - 0x30, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x29, 0x01, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x38, 0x01, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x39, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x34, 0x01, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3b, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3c, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x3d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, - 0x3c, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, - 0x7b, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x40, 0x01, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x41, 0x01, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x42, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x45, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x44, 0x01, 0x00, 0x00, - 0x45, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x46, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x44, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x48, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x4e, 0x00, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0x4b, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4c, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, - 0x43, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x4e, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0x4d, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x54, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x55, 0x01, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, - 0x55, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x50, 0x01, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x5b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x5d, 0x01, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x5f, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x5c, 0x01, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x58, 0x01, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x64, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x66, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x65, 0x01, 0x00, 0x00, - 0x66, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x65, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x60, 0x01, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6c, 0x01, 0x00, 0x00, - 0x6b, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, - 0x6c, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x68, 0x01, 0x00, 0x00, - 0x6f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, - 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, - 0x50, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x73, 0x01, 0x00, 0x00, 0x72, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x74, 0x01, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, - 0x77, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, - 0x74, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7a, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x7b, 0x01, 0x00, 0x00, 0x79, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x7e, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x80, 0x01, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x82, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, - 0x82, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x86, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, - 0x87, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, - 0x86, 0x01, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x8a, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x8b, 0x01, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, - 0x8d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, - 0x8e, 0x01, 0x00, 0x00, 0x8f, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x91, 0x01, 0x00, 0x00, 0x85, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x92, 0x01, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x93, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x94, 0x01, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x96, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x95, 0x01, 0x00, 0x00, - 0x96, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, - 0x24, 0x00, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9d, 0x01, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, - 0x9d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, - 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, - 0x60, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa1, 0x01, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xa2, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0x9e, 0x01, 0x00, 0x00, 0xa2, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, - 0xa4, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, - 0x9a, 0x01, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa7, 0x01, 0x00, 0x00, - 0xa6, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa8, 0x01, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0xa7, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x01, 0x00, 0x00, - 0xa8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xaa, 0x01, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0xa9, 0x01, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xad, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xae, 0x01, 0x00, 0x00, 0xad, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x43, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xaf, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb0, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xb1, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, - 0xb5, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, - 0xb4, 0x01, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb8, 0x01, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, - 0xba, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbc, 0x01, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0xbc, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbe, 0x01, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, - 0xbf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, - 0xc2, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, - 0xc3, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, - 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, - 0x3e, 0x00, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, - 0xf7, 0x00, 0x03, 0x00, 0xc9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, - 0xc7, 0x01, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xc9, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, - 0xc8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xca, 0x01, 0x00, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcb, 0x01, 0x00, 0x00, - 0xca, 0x01, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xcb, 0x01, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0xc9, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd1, 0x01, 0x00, 0x00, - 0xd0, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd2, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0xd4, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, - 0xd1, 0x01, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcd, 0x01, 0x00, 0x00, - 0xd4, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd9, 0x01, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xda, 0x01, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x17, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, - 0xda, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd5, 0x01, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xe1, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xe2, 0x01, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, - 0xe4, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0xe1, 0x01, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xdd, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xe9, 0x01, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xeb, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xea, 0x01, 0x00, 0x00, - 0xeb, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xe5, 0x01, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xed, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xee, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xef, 0x01, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0xef, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, - 0xf3, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, - 0xf2, 0x01, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xf6, 0x01, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, - 0xf8, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfa, 0x01, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0xfa, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfc, 0x01, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xfd, 0x01, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, - 0xfd, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, - 0x00, 0x02, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x01, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, - 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, - 0xd5, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x05, 0x02, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x06, 0x02, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, - 0x09, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, - 0x06, 0x02, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x0c, 0x02, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x0d, 0x02, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x0d, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x11, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x12, 0x02, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x14, 0x02, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, - 0x14, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, - 0x19, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, - 0x18, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x1c, 0x02, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x1d, 0x02, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, - 0x1f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, - 0x20, 0x02, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x23, 0x02, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x24, 0x02, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x25, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x26, 0x02, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x28, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x27, 0x02, 0x00, 0x00, - 0x28, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, - 0x24, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x2c, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x02, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, - 0x2f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, - 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, - 0xe5, 0x01, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x33, 0x02, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x34, 0x02, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, - 0x36, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, - 0x2c, 0x02, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x39, 0x02, 0x00, 0x00, - 0x38, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3b, 0x02, 0x00, 0x00, - 0x3a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x3b, 0x02, 0x00, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x3f, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x40, 0x02, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x3a, 0x00, 0x00, 0x00, - 0x41, 0x02, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, 0x41, 0x02, 0x00, 0x00, - 0xbc, 0x00, 0x05, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x44, 0x02, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, - 0x43, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x46, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfa, 0x00, 0x04, 0x00, 0x44, 0x02, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, - 0xf8, 0x00, 0x02, 0x00, 0x45, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x47, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x49, 0x02, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0x48, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, - 0x49, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x46, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x4f, 0x02, 0x00, 0x00, 0x4e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x51, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x50, 0x02, 0x00, 0x00, - 0x51, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0x4f, 0x02, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x4b, 0x02, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x57, 0x02, 0x00, 0x00, - 0x56, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x58, 0x02, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, 0x5a, 0x02, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, - 0x57, 0x02, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x53, 0x02, 0x00, 0x00, - 0x5a, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x5e, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x60, 0x02, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, - 0x60, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x67, 0x02, 0x00, 0x00, 0x66, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x68, 0x02, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x17, 0x00, 0x00, 0x00, - 0x6a, 0x02, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x67, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x63, 0x02, 0x00, 0x00, 0x6a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x02, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, - 0x6e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, - 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, - 0x4b, 0x02, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x72, 0x02, 0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x73, 0x02, 0x00, 0x00, 0x70, 0x02, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, 0x73, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x74, 0x02, 0x00, 0x00, - 0x75, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, - 0x6b, 0x02, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x02, 0x00, 0x00, - 0x77, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7a, 0x02, 0x00, 0x00, - 0x79, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, - 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x7a, 0x02, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7e, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x7f, 0x02, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x7f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x80, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x81, 0x02, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x82, 0x02, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, 0x81, 0x02, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, - 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, - 0x86, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, - 0x85, 0x02, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x89, 0x02, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, - 0x8b, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x8d, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x90, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, - 0x90, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, - 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, - 0x93, 0x02, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x94, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, - 0x11, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, - 0x34, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, - 0x5b, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x98, 0x02, 0x00, 0x00, 0x97, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x99, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0x98, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x02, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, - 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0x9a, 0x02, 0x00, 0x00, - 0x9c, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, - 0x99, 0x02, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x9f, 0x02, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xa0, 0x02, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xa2, 0x02, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0xa4, 0x02, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xa5, 0x02, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xa7, 0x02, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa9, 0x02, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, - 0xa7, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0xa9, 0x02, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, - 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, - 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, - 0x39, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, - 0xac, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, - 0xab, 0x02, 0x00, 0x00, 0xad, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xaf, 0x02, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xb0, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0xb0, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, - 0x07, 0x00, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, - 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, - 0xb2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, - 0x3e, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, - 0xb3, 0x02, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb6, 0x02, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb7, 0x02, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, - 0xb8, 0x02, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, - 0xb9, 0x02, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, - 0xbb, 0x02, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xba, 0x02, 0x00, 0x00, - 0xbb, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, - 0x24, 0x00, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, - 0x3e, 0x00, 0x03, 0x00, 0x43, 0x01, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, - 0x06, 0x00, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, - 0xfe, 0x00, 0x02, 0x00, 0xc1, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, + 0xff, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, + 0xb7, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x03, 0x03, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x04, 0x03, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, + 0x06, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, + 0xfc, 0x02, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x09, 0x03, 0x00, 0x00, + 0x08, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0b, 0x03, 0x00, 0x00, + 0x0a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x0b, 0x03, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0f, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x10, 0x03, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x41, 0x00, 0x00, 0x00, + 0x11, 0x03, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x45, 0x00, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, + 0x13, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x16, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x14, 0x03, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x16, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x15, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x17, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x19, 0x03, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x19, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x16, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x26, 0x03, 0x00, 0x00, 0x22, 0x03, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, 0x26, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x27, 0x03, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, 0x91, 0x00, 0x05, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, 0x31, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, + 0x36, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, + 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, + 0x1b, 0x03, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x3a, 0x03, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x03, 0x00, 0x00, 0x38, 0x03, 0x00, 0x00, 0x3a, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, 0x3c, 0x03, 0x00, 0x00, + 0x3d, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3f, 0x03, 0x00, 0x00, + 0x33, 0x03, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x40, 0x03, 0x00, 0x00, + 0x3f, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x42, 0x03, 0x00, 0x00, + 0x41, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, + 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x43, 0x03, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x42, 0x03, 0x00, 0x00, 0x43, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x46, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x47, 0x03, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, 0x45, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x49, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x4a, 0x03, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, + 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, + 0x4e, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, + 0x4d, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x51, 0x03, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x53, 0x03, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, + 0x53, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x55, 0x03, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x56, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x55, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x57, 0x03, 0x00, 0x00, 0x56, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x58, 0x03, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, + 0x58, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5b, 0x03, 0x00, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, + 0x5b, 0x03, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x5c, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, + 0x10, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, + 0x27, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x60, 0x03, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x61, 0x03, 0x00, 0x00, 0x5e, 0x03, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x03, 0x00, 0x00, 0x63, 0x03, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, + 0x64, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, + 0x61, 0x03, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x67, 0x03, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x68, 0x03, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x67, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6d, 0x03, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6f, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, + 0x6f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, + 0x74, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, + 0x73, 0x03, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x77, 0x03, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x78, 0x03, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7b, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, + 0x7a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, + 0x1a, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, + 0x7b, 0x03, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x7e, 0x03, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x7f, 0x03, 0x00, 0x00, 0x7e, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x81, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x83, 0x03, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x03, 0x00, 0x00, + 0x83, 0x03, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0x88, 0x03, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x89, 0x03, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, }; -static const unsigned int vk_interaction_shadow_point_frag_spv_size = 22796u; +static const unsigned int vk_interaction_shadow_point_frag_spv_size = 27500u; alignas(4) static const unsigned char vk_shadow_caster_vert_spv[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x47, 0x00, 0x00, 0x00, diff --git a/src/renderer/Vulkan/shaders/interaction.frag b/src/renderer/Vulkan/shaders/interaction.frag index c07a4516..caf25775 100644 --- a/src/renderer/Vulkan/shaders/interaction.frag +++ b/src/renderer/Vulkan/shaders/interaction.frag @@ -77,10 +77,46 @@ vec3 ApplyFlatDiffuseSweep(vec3 diffuse, float localZ) { return mix(diffuse, vec3(1.0), inter.flatDiffuseParams.x * band); } +vec3 EvaluatePackedPBR(vec3 localNormal, vec2 albedoTexCoord, + vec2 ormTexCoord, float shadowFactor) { + vec3 albedo = pow(max(texture(diffuseMap, albedoTexCoord).rgb, vec3(0.0)), vec3(2.2)); + vec3 orm = texture(specularMap, ormTexCoord).rgb; + float metallic = clamp(orm.b * pc.d.y, 0.0, 1.0); + float roughness = clamp(orm.g * pc.d.z, 0.045, 1.0); + vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); + vec3 viewDir = SafeNormalize(vViewVector); + vec3 halfDir = SafeNormalize(lightDir + viewDir); + float ndotl = max(dot(localNormal, lightDir), 0.0); + float ndotv = max(dot(localNormal, viewDir), 0.0); + float ndoth = max(dot(localNormal, halfDir), 0.0); + float vdoth = max(dot(viewDir, halfDir), 0.0); + float alpha = roughness * roughness; + float alphaSquared = alpha * alpha; + float denom = max(ndoth * ndoth * (alphaSquared - 1.0) + 1.0, 1.0e-4); + float distribution = alphaSquared / (3.14159265 * denom * denom); + float k = (roughness + 1.0) * (roughness + 1.0) * 0.125; + float geometry = (ndotl / max(ndotl * (1.0 - k) + k, 1.0e-4)) + * (ndotv / max(ndotv * (1.0 - k) + k, 1.0e-4)); + vec3 f0 = mix(vec3(0.04), albedo, metallic); + vec3 fresnel = f0 + (vec3(1.0) - f0) * pow(1.0 - vdoth, 5.0); + vec3 specular = distribution * geometry * fresnel + / max(4.0 * ndotl * ndotv, 1.0e-4); + vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic) + * albedo * (1.0 / 3.14159265); + vec3 radiance = textureProj(lightFalloffMap, vLightFalloffTexCoord).rgb + * textureProj(lightProjectionMap, vLightProjectionTexCoord).rgb + * inter.diffuseColor.rgb * shadowFactor; + return (diffuse + specular) * radiance * ndotl * vVertexColor; +} + void main() { vec2 bumpTexCoord = vBumpTexCoord; vec2 diffuseTexCoord = vDiffuseTexCoord; vec2 specularTexCoord = vSpecularTexCoord; + if (pc.d.x > 1.5) { + outColor = vec4(0.0, 1.0, 0.0, 0.0); + return; + } if (pc.c.z > 0.5) { float height = texture(bumpMap, bumpTexCoord).r; vec2 offset = SafeNormalize(vViewVector).xy * (height * pc.c.x + pc.c.y); @@ -90,6 +126,13 @@ void main() { } vec4 bumpSample = texture(bumpMap, bumpTexCoord); + if (pc.d.x > 0.5) { + vec3 localNormal = bumpSample.rgb * 2.0 - 1.0; + localNormal.xy *= pc.d.w; + outColor = vec4(EvaluatePackedPBR(SafeNormalize(localNormal), + diffuseTexCoord, specularTexCoord, 1.0), 0.0); + return; + } vec3 localNormal = vec3(bumpSample.a, bumpSample.g, bumpSample.b) * 2.0 - 1.0; vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); diff --git a/src/renderer/Vulkan/shaders/interaction_shadow.frag b/src/renderer/Vulkan/shaders/interaction_shadow.frag index 8dfd635c..0bbc7188 100644 --- a/src/renderer/Vulkan/shaders/interaction_shadow.frag +++ b/src/renderer/Vulkan/shaders/interaction_shadow.frag @@ -104,6 +104,38 @@ vec3 ApplyFlatDiffuseSweep(vec3 diffuse, float localZ) { return mix(diffuse, vec3(1.0), inter.flatDiffuseParams.x * band); } +vec3 EvaluatePackedPBR(vec3 localNormal, vec2 albedoTexCoord, + vec2 ormTexCoord, float shadowFactor) { + vec3 albedo = pow(max(texture(diffuseMap, albedoTexCoord).rgb, vec3(0.0)), vec3(2.2)); + vec3 orm = texture(specularMap, ormTexCoord).rgb; + float metallic = clamp(orm.b * pc.d.y, 0.0, 1.0); + float roughness = clamp(orm.g * pc.d.z, 0.045, 1.0); + vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); + vec3 viewDir = SafeNormalize(vViewVector); + vec3 halfDir = SafeNormalize(lightDir + viewDir); + float ndotl = max(dot(localNormal, lightDir), 0.0); + float ndotv = max(dot(localNormal, viewDir), 0.0); + float ndoth = max(dot(localNormal, halfDir), 0.0); + float vdoth = max(dot(viewDir, halfDir), 0.0); + float alpha = roughness * roughness; + float alphaSquared = alpha * alpha; + float denom = max(ndoth * ndoth * (alphaSquared - 1.0) + 1.0, 1.0e-4); + float distribution = alphaSquared / (3.14159265 * denom * denom); + float k = (roughness + 1.0) * (roughness + 1.0) * 0.125; + float geometry = (ndotl / max(ndotl * (1.0 - k) + k, 1.0e-4)) + * (ndotv / max(ndotv * (1.0 - k) + k, 1.0e-4)); + vec3 f0 = mix(vec3(0.04), albedo, metallic); + vec3 fresnel = f0 + (vec3(1.0) - f0) * pow(1.0 - vdoth, 5.0); + vec3 specular = distribution * geometry * fresnel + / max(4.0 * ndotl * ndotv, 1.0e-4); + vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic) + * albedo * (1.0 / 3.14159265); + vec3 radiance = textureProj(lightFalloffMap, vLightFalloffTexCoord).rgb + * textureProj(lightProjectionMap, vLightProjectionTexCoord).rgb + * inter.diffuseColor.rgb * shadowFactor; + return (diffuse + specular) * radiance * ndotl * vVertexColor; +} + int ShadowCascadeCount() { return clamp(int(shadow.biasParams.w + 0.5), 1, 4); } @@ -143,17 +175,16 @@ float StableShadowHash(vec3 value) { * 43758.5453); } -vec2 RotateShadowOffset(vec2 offset, vec2 uv, float depth) { +mat2 ShadowOffsetRotation(vec2 uv, float depth) { if (shadow.filterParams.z < 0.5) { - return offset; + return mat2(1.0, 0.0, 0.0, 1.0); } float angle = StableShadowHash(vec3( floor(uv / max(shadow.texelSize.x, 1.0e-6)), floor(depth * 1024.0))) * 6.2831853; float s = sin(angle); float c = cos(angle); - return vec2(c * offset.x - s * offset.y, - s * offset.x + c * offset.y); + return mat2(c, s, -s, c); } float ShadowDepthGradient(int cascadeIndex) { @@ -192,7 +223,7 @@ float RawShadowDepth(vec2 uv) { } float ProjectedPCSSRadius(vec2 uv, float depth, int cascadeIndex, - vec2 clampMin, vec2 clampMax, vec2 texelStep) { + vec2 clampMin, vec2 clampMax, vec2 texelStep, mat2 rotation) { float baseRadius = shadow.filterParams.x; if (shadow.filterParams.z < 1.5 || shadow.filterParams.w > 0.5 || shadow.pcssParams.x <= 0.0 @@ -209,10 +240,14 @@ float ProjectedPCSSRadius(vec2 uv, float depth, int cascadeIndex, blockerDepth += d0; blockerCount += 1.0; } - vec2 o1 = RotateShadowOffset(vec2(-0.5, -0.5), uv, depth); - vec2 o2 = RotateShadowOffset(vec2(0.5, -0.5), uv, depth); - vec2 o3 = RotateShadowOffset(vec2(-0.5, 0.5), uv, depth); - vec2 o4 = RotateShadowOffset(vec2(0.5, 0.5), uv, depth); + vec2 o1 = rotation * vec2(-0.326212, -0.405805); + vec2 o2 = rotation * vec2(-0.840144, -0.073580); + vec2 o3 = rotation * vec2(-0.695914, 0.457137); + vec2 o4 = rotation * vec2(-0.203345, 0.620716); + vec2 o5 = rotation * vec2(0.962340, -0.194983); + vec2 o6 = rotation * vec2(0.473434, -0.480026); + vec2 o7 = rotation * vec2(0.519456, 0.767022); + vec2 o8 = rotation * vec2(0.185461, -0.893124); float d1 = RawShadowDepth(clamp(uv + o1 * searchTap, clampMin, clampMax)); float d2 = RawShadowDepth(clamp(uv + o2 * searchTap, @@ -221,16 +256,29 @@ float ProjectedPCSSRadius(vec2 uv, float depth, int cascadeIndex, clampMin, clampMax)); float d4 = RawShadowDepth(clamp(uv + o4 * searchTap, clampMin, clampMax)); + float d5 = RawShadowDepth(clamp(uv + o5 * searchTap, + clampMin, clampMax)); + float d6 = RawShadowDepth(clamp(uv + o6 * searchTap, + clampMin, clampMax)); + float d7 = RawShadowDepth(clamp(uv + o7 * searchTap, + clampMin, clampMax)); + float d8 = RawShadowDepth(clamp(uv + o8 * searchTap, + clampMin, clampMax)); if (d1 < compareDepth) { blockerDepth += d1; blockerCount += 1.0; } if (d2 < compareDepth) { blockerDepth += d2; blockerCount += 1.0; } if (d3 < compareDepth) { blockerDepth += d3; blockerCount += 1.0; } if (d4 < compareDepth) { blockerDepth += d4; blockerCount += 1.0; } + if (d5 < compareDepth) { blockerDepth += d5; blockerCount += 1.0; } + if (d6 < compareDepth) { blockerDepth += d6; blockerCount += 1.0; } + if (d7 < compareDepth) { blockerDepth += d7; blockerCount += 1.0; } + if (d8 < compareDepth) { blockerDepth += d8; blockerCount += 1.0; } if (blockerCount <= 0.0) { - return baseRadius; + return 0.0; } float averageBlocker = blockerDepth / blockerCount; - float penumbra = (depth - averageBlocker) + float separation = max(compareDepth - averageBlocker, 0.0); + float penumbra = separation / max(averageBlocker, 1.0e-4); float maxRadius = max(baseRadius, shadow.pcssParams.y); return clamp(max(baseRadius, penumbra * shadow.pcssParams.x), @@ -275,9 +323,12 @@ float SampleShadowCascade(vec4 shadowCoord, vec4 atlasRect, vec2 clampMax = rectMax - guardBand; clampMin = min(clampMin, clampMax); uv = clamp(uv, clampMin, clampMax); + // Blocker search and PCF use the same stable kernel orientation. Compute + // the hash/trigonometry once so PCSS has one bounded rotation per receiver. + mat2 rotation = ShadowOffsetRotation(uv, depth); float filterRadius = ProjectedPCSSRadius(uv, depth, cascadeIndex, - clampMin, clampMax, texelStep); + clampMin, clampMax, texelStep, rotation); if (filterRadius <= 0.0) { return SampleShadowCompare(uv, depth, cascadeIndex); } @@ -287,10 +338,10 @@ float SampleShadowCascade(vec4 shadowCoord, vec4 atlasRect, if (shadow.filterParams.y <= 1.0) { return result; } - vec2 o1 = RotateShadowOffset(vec2(-0.326212, -0.405805), uv, depth); - vec2 o2 = RotateShadowOffset(vec2(-0.840144, -0.073580), uv, depth); - vec2 o3 = RotateShadowOffset(vec2(-0.695914, 0.457137), uv, depth); - vec2 o4 = RotateShadowOffset(vec2(-0.203345, 0.620716), uv, depth); + vec2 o1 = rotation * vec2(-0.326212, -0.405805); + vec2 o2 = rotation * vec2(-0.840144, -0.073580); + vec2 o3 = rotation * vec2(-0.695914, 0.457137); + vec2 o4 = rotation * vec2(-0.203345, 0.620716); result += SampleShadowCompare(clamp(uv + o1 * tap, clampMin, clampMax), depth, cascadeIndex); result += SampleShadowCompare(clamp(uv + o2 * tap, @@ -302,10 +353,10 @@ float SampleShadowCascade(vec4 shadowCoord, vec4 atlasRect, if (shadow.filterParams.y <= 5.0) { return result * (1.0 / 5.0); } - vec2 o5 = RotateShadowOffset(vec2(0.962340, -0.194983), uv, depth); - vec2 o6 = RotateShadowOffset(vec2(0.473434, -0.480026), uv, depth); - vec2 o7 = RotateShadowOffset(vec2(0.519456, 0.767022), uv, depth); - vec2 o8 = RotateShadowOffset(vec2(0.185461, -0.893124), uv, depth); + vec2 o5 = rotation * vec2(0.962340, -0.194983); + vec2 o6 = rotation * vec2(0.473434, -0.480026); + vec2 o7 = rotation * vec2(0.519456, 0.767022); + vec2 o8 = rotation * vec2(0.185461, -0.893124); result += SampleShadowCompare(clamp(uv + o5 * tap, clampMin, clampMax), depth, cascadeIndex); result += SampleShadowCompare(clamp(uv + o6 * tap, @@ -317,10 +368,10 @@ float SampleShadowCascade(vec4 shadowCoord, vec4 atlasRect, if (shadow.filterParams.y <= 9.0) { return result * (1.0 / 9.0); } - vec2 o9 = RotateShadowOffset(vec2(0.507431, 0.064425), uv, depth); - vec2 o10 = RotateShadowOffset(vec2(0.896420, 0.412458), uv, depth); - vec2 o11 = RotateShadowOffset(vec2(-0.321940, -0.932615), uv, depth); - vec2 o12 = RotateShadowOffset(vec2(-0.791559, -0.597705), uv, depth); + vec2 o9 = rotation * vec2(0.507431, 0.064425); + vec2 o10 = rotation * vec2(0.896420, 0.412458); + vec2 o11 = rotation * vec2(-0.321940, -0.932615); + vec2 o12 = rotation * vec2(-0.791559, -0.597705); result += SampleShadowCompare(clamp(uv + o9 * tap, clampMin, clampMax), depth, cascadeIndex); result += SampleShadowCompare(clamp(uv + o10 * tap, @@ -387,6 +438,11 @@ void main() { abs(dFdx(vShadowCoord3.z)) + abs(dFdy(vShadowCoord3.z))); } + if (pc.d.x > 1.5) { + outColor = vec4(0.0, 1.0, 0.0, 0.0); + return; + } + vec2 bumpTexCoord = vBumpTexCoord; vec2 diffuseTexCoord = vDiffuseTexCoord; vec2 specularTexCoord = vSpecularTexCoord; @@ -399,6 +455,13 @@ void main() { } vec4 bumpSample = texture(bumpMap, bumpTexCoord); + if (pc.d.x > 0.5) { + vec3 localNormal = bumpSample.rgb * 2.0 - 1.0; + localNormal.xy *= pc.d.w; + outColor = vec4(EvaluatePackedPBR(SafeNormalize(localNormal), + diffuseTexCoord, specularTexCoord, SampleShadowFactor()), 0.0); + return; + } vec3 localNormal = vec3(bumpSample.a, bumpSample.g, bumpSample.b) * 2.0 - 1.0; vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); diff --git a/src/renderer/Vulkan/shaders/interaction_shadow_point.frag b/src/renderer/Vulkan/shaders/interaction_shadow_point.frag index 2ab3383d..1ff2e648 100644 --- a/src/renderer/Vulkan/shaders/interaction_shadow_point.frag +++ b/src/renderer/Vulkan/shaders/interaction_shadow_point.frag @@ -88,20 +88,51 @@ vec3 ApplyFlatDiffuseSweep(vec3 diffuse, float localZ) { return mix(diffuse, vec3(1.0), inter.flatDiffuseParams.x * band); } +vec3 EvaluatePackedPBR(vec3 localNormal, vec2 albedoTexCoord, + vec2 ormTexCoord, float shadowFactor) { + vec3 albedo = pow(max(texture(diffuseMap, albedoTexCoord).rgb, vec3(0.0)), vec3(2.2)); + vec3 orm = texture(specularMap, ormTexCoord).rgb; + float metallic = clamp(orm.b * pc.d.y, 0.0, 1.0); + float roughness = clamp(orm.g * pc.d.z, 0.045, 1.0); + vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); + vec3 viewDir = SafeNormalize(vViewVector); + vec3 halfDir = SafeNormalize(lightDir + viewDir); + float ndotl = max(dot(localNormal, lightDir), 0.0); + float ndotv = max(dot(localNormal, viewDir), 0.0); + float ndoth = max(dot(localNormal, halfDir), 0.0); + float vdoth = max(dot(viewDir, halfDir), 0.0); + float alpha = roughness * roughness; + float alphaSquared = alpha * alpha; + float denom = max(ndoth * ndoth * (alphaSquared - 1.0) + 1.0, 1.0e-4); + float distribution = alphaSquared / (3.14159265 * denom * denom); + float k = (roughness + 1.0) * (roughness + 1.0) * 0.125; + float geometry = (ndotl / max(ndotl * (1.0 - k) + k, 1.0e-4)) + * (ndotv / max(ndotv * (1.0 - k) + k, 1.0e-4)); + vec3 f0 = mix(vec3(0.04), albedo, metallic); + vec3 fresnel = f0 + (vec3(1.0) - f0) * pow(1.0 - vdoth, 5.0); + vec3 specular = distribution * geometry * fresnel + / max(4.0 * ndotl * ndotv, 1.0e-4); + vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic) + * albedo * (1.0 / 3.14159265); + vec3 radiance = textureProj(lightFalloffMap, vLightFalloffTexCoord).rgb + * textureProj(lightProjectionMap, vLightProjectionTexCoord).rgb + * inter.diffuseColor.rgb * shadowFactor; + return (diffuse + specular) * radiance * ndotl * vVertexColor; +} + float StableShadowHash(vec3 value) { return fract(sin(dot(value, vec3(12.9898, 78.233, 37.719))) * 43758.5453); } -vec2 RotateShadowOffset(vec2 offset, vec3 direction) { +mat2 ShadowOffsetRotation(vec3 direction) { if (shadow.filterParams.z < 0.5) { - return offset; + return mat2(1.0, 0.0, 0.0, 1.0); } float angle = StableShadowHash(floor(direction * 37.0)) * 6.2831853; float s = sin(angle); float c = cos(angle); - return vec2(c * offset.x - s * offset.y, - s * offset.x + c * offset.y); + return mat2(c, s, -s, c); } float ShadowReceiverBias() { @@ -155,10 +186,11 @@ float SampleShadowFactor() { if (shadow.filterParams.y <= 1.0) { return result; } - vec2 o1 = RotateShadowOffset(vec2(-0.326212, -0.405805), direction); - vec2 o2 = RotateShadowOffset(vec2(-0.840144, -0.073580), direction); - vec2 o3 = RotateShadowOffset(vec2(-0.695914, 0.457137), direction); - vec2 o4 = RotateShadowOffset(vec2(-0.203345, 0.620716), direction); + mat2 rotation = ShadowOffsetRotation(direction); + vec2 o1 = rotation * vec2(-0.326212, -0.405805); + vec2 o2 = rotation * vec2(-0.840144, -0.073580); + vec2 o3 = rotation * vec2(-0.695914, 0.457137); + vec2 o4 = rotation * vec2(-0.203345, 0.620716); result += SamplePointShadowCompare(SafeNormalize(direction + (tangent * o1.x + bitangent * o1.y) * tap), depth); result += SamplePointShadowCompare(SafeNormalize(direction @@ -170,10 +202,10 @@ float SampleShadowFactor() { if (shadow.filterParams.y <= 5.0) { return result * (1.0 / 5.0); } - vec2 o5 = RotateShadowOffset(vec2(0.962340, -0.194983), direction); - vec2 o6 = RotateShadowOffset(vec2(0.473434, -0.480026), direction); - vec2 o7 = RotateShadowOffset(vec2(0.519456, 0.767022), direction); - vec2 o8 = RotateShadowOffset(vec2(0.185461, -0.893124), direction); + vec2 o5 = rotation * vec2(0.962340, -0.194983); + vec2 o6 = rotation * vec2(0.473434, -0.480026); + vec2 o7 = rotation * vec2(0.519456, 0.767022); + vec2 o8 = rotation * vec2(0.185461, -0.893124); result += SamplePointShadowCompare(SafeNormalize(direction + (tangent * o5.x + bitangent * o5.y) * tap), depth); result += SamplePointShadowCompare(SafeNormalize(direction @@ -185,10 +217,10 @@ float SampleShadowFactor() { if (shadow.filterParams.y <= 9.0) { return result * (1.0 / 9.0); } - vec2 o9 = RotateShadowOffset(vec2(0.507431, 0.064425), direction); - vec2 o10 = RotateShadowOffset(vec2(0.896420, 0.412458), direction); - vec2 o11 = RotateShadowOffset(vec2(-0.321940, -0.932615), direction); - vec2 o12 = RotateShadowOffset(vec2(-0.791559, -0.597705), direction); + vec2 o9 = rotation * vec2(0.507431, 0.064425); + vec2 o10 = rotation * vec2(0.896420, 0.412458); + vec2 o11 = rotation * vec2(-0.321940, -0.932615); + vec2 o12 = rotation * vec2(-0.791559, -0.597705); result += SamplePointShadowCompare(SafeNormalize(direction + (tangent * o9.x + bitangent * o9.y) * tap), depth); result += SamplePointShadowCompare(SafeNormalize(direction @@ -201,6 +233,11 @@ float SampleShadowFactor() { } void main() { + if (pc.d.x > 1.5) { + outColor = vec4(0.0, 1.0, 0.0, 0.0); + return; + } + vec2 bumpTexCoord = vBumpTexCoord; vec2 diffuseTexCoord = vDiffuseTexCoord; vec2 specularTexCoord = vSpecularTexCoord; @@ -213,6 +250,13 @@ void main() { } vec4 bumpSample = texture(bumpMap, bumpTexCoord); + if (pc.d.x > 0.5) { + vec3 localNormal = bumpSample.rgb * 2.0 - 1.0; + localNormal.xy *= pc.d.w; + outColor = vec4(EvaluatePackedPBR(SafeNormalize(localNormal), + diffuseTexCoord, specularTexCoord, SampleShadowFactor()), 0.0); + return; + } vec3 localNormal = vec3(bumpSample.a, bumpSample.g, bumpSample.b) * 2.0 - 1.0; vec3 lightDir = (pc.a.z > 0.5) ? pc.b.xyz : SafeNormalize(vLightVector); diff --git a/src/renderer/Vulkan/shaders/temporal_resolve.frag b/src/renderer/Vulkan/shaders/temporal_resolve.frag new file mode 100644 index 00000000..3aad5bff --- /dev/null +++ b/src/renderer/Vulkan/shaders/temporal_resolve.frag @@ -0,0 +1,415 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D currentScene; +layout(set = 1, binding = 0) uniform sampler2D sceneDepth; +layout(set = 2, binding = 0) uniform sampler2D historyScene; + +layout(std140, set = 6, binding = 0) uniform TemporalResolveBlock { + // xy = inverse scene extent; zw = native output extent. + vec4 sceneOutputExtent; + // xy = inverse projection scale; zw = current projection offsets. + vec4 currentReconstruct; + // xy = previous projection scale; zw = previous jittered offsets. + vec4 previousProject; + // xy = current depth projection; z = feedback; w = reactive scale. + vec4 depthFeedback; + // The unused w components carry the bounded screen-effect packet: + // mask, froxel density/distance/slices, SSR intensity/distance/steps, + // and SSGI intensity. xyz retain their temporal camera contract. + vec4 currentViewOrigin; + vec4 currentViewAxis0; + vec4 currentViewAxis1; + vec4 currentViewAxis2; + vec4 previousViewOrigin; + vec4 previousViewAxis0; + vec4 previousViewAxis1; + vec4 previousViewAxis2; + // xy = current jitter in native normalized coordinates; + // z = use history; w = debug mode. + vec4 temporalParams; + // x = use object velocity (currently zero on Vulkan); + // y = camera reprojection valid; z = depth valid; + // w = recenter the current jitter for capture/spatial fallback. + vec4 motionParams; + // Conservative current-frame regions whose packet motion domains lack an + // exact Vulkan velocity stream. Invalid entries have non-positive extent. + vec4 reactiveRect0; + vec4 reactiveRect1; +} temporal; + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +const float kDepthEpsilon = 0.00001; +vec2 TextureToCameraUV(vec2 uv) { + return vec2(uv.x, 1.0 - uv.y); +} + +vec2 CameraToTextureUV(vec2 uv) { + return vec2(uv.x, 1.0 - uv.y); +} + +float ViewZFromDepth(float depth) { + float denominator = depth * 2.0 - 1.0 + temporal.depthFeedback.x; + if (abs(denominator) < kDepthEpsilon) { + denominator = denominator < 0.0 ? -kDepthEpsilon : kDepthEpsilon; + } + return -temporal.depthFeedback.y / denominator; +} + +vec3 CurrentViewToWorldDirection(vec3 viewPosition) { + return temporal.currentViewAxis0.xyz * (-viewPosition.z) + + temporal.currentViewAxis1.xyz * (-viewPosition.x) + + temporal.currentViewAxis2.xyz * viewPosition.y; +} + +vec3 WorldToPreviousViewDirection(vec3 worldDirection) { + return vec3( + -dot(worldDirection, temporal.previousViewAxis1.xyz), + dot(worldDirection, temporal.previousViewAxis2.xyz), + -dot(worldDirection, temporal.previousViewAxis0.xyz)); +} + +vec2 ProjectPreviousView(vec3 previousViewPosition) { + float w = -previousViewPosition.z; + if (abs(w) < kDepthEpsilon) { + return vec2(-1000.0); + } + vec2 clipXY = temporal.previousProject.xy * previousViewPosition.xy + + temporal.previousProject.zw * previousViewPosition.z; + return clipXY / w * 0.5 + 0.5; +} + +vec2 CameraPreviousUV(vec2 cameraUV, float depth) { + vec2 ndc = cameraUV * 2.0 - 1.0; + if (depth >= 0.99999) { + vec3 ray = vec3( + -(ndc.x + temporal.currentReconstruct.z) + * temporal.currentReconstruct.x, + -(ndc.y + temporal.currentReconstruct.w) + * temporal.currentReconstruct.y, + -1.0); + return ProjectPreviousView(WorldToPreviousViewDirection( + CurrentViewToWorldDirection(ray))); + } + float viewZ = ViewZFromDepth(depth); + vec3 viewPosition = vec3( + -viewZ * (ndc.x + temporal.currentReconstruct.z) + * temporal.currentReconstruct.x, + -viewZ * (ndc.y + temporal.currentReconstruct.w) + * temporal.currentReconstruct.y, + viewZ); + vec3 worldPosition = temporal.currentViewOrigin.xyz + + CurrentViewToWorldDirection(viewPosition); + vec3 delta = worldPosition - temporal.previousViewOrigin.xyz; + return ProjectPreviousView(vec3( + -dot(delta, temporal.previousViewAxis1.xyz), + dot(delta, temporal.previousViewAxis2.xyz), + -dot(delta, temporal.previousViewAxis0.xyz))); +} + +float MaxComponent(vec3 value) { + return max(value.x, max(value.y, value.z)); +} + +bool InsideReactiveRect(vec2 cameraUV, vec4 rect) { + return rect.z > rect.x && rect.w > rect.y + && all(greaterThanEqual(cameraUV, rect.xy)) + && all(lessThan(cameraUV, rect.zw)); +} + +bool ScreenEffectEnabled(float bitValue) { + return mod(floor(temporal.currentViewOrigin.w / bitValue), 2.0) > 0.5; +} + +float SceneDepthAt(vec2 cameraUV) { + return texture(sceneDepth, CameraToTextureUV(clamp(cameraUV, + vec2(0.0), vec2(1.0)))).r; +} + +vec3 SceneColorAt(vec2 cameraUV) { + return texture(currentScene, CameraToTextureUV(clamp(cameraUV, + vec2(0.0), vec2(1.0)))).rgb; +} + +vec3 CurrentViewPosition(vec2 cameraUV, float depth) { + vec2 ndc = cameraUV * 2.0 - 1.0; + float viewZ = ViewZFromDepth(depth); + return vec3( + -viewZ * (ndc.x + temporal.currentReconstruct.z) + * temporal.currentReconstruct.x, + -viewZ * (ndc.y + temporal.currentReconstruct.w) + * temporal.currentReconstruct.y, + viewZ); +} + +vec2 ProjectCurrentView(vec3 viewPosition) { + float w = -viewPosition.z; + if (w <= kDepthEpsilon) { + return vec2(-1000.0); + } + vec2 clipXY = vec2( + viewPosition.x / temporal.currentReconstruct.x, + viewPosition.y / temporal.currentReconstruct.y) + + temporal.currentReconstruct.zw * viewPosition.z; + return clipXY / w * 0.5 + 0.5; +} + +vec3 DepthNormal(vec2 cameraUV, vec3 centerPosition) { + vec2 dx = vec2(temporal.sceneOutputExtent.x, 0.0); + vec2 dy = vec2(0.0, temporal.sceneOutputExtent.y); + vec2 leftUV = clamp(cameraUV - dx, vec2(0.0), vec2(1.0)); + vec2 rightUV = clamp(cameraUV + dx, vec2(0.0), vec2(1.0)); + vec2 downUV = clamp(cameraUV - dy, vec2(0.0), vec2(1.0)); + vec2 upUV = clamp(cameraUV + dy, vec2(0.0), vec2(1.0)); + vec3 tangentX = CurrentViewPosition(rightUV, SceneDepthAt(rightUV)) + - CurrentViewPosition(leftUV, SceneDepthAt(leftUV)); + vec3 tangentY = CurrentViewPosition(upUV, SceneDepthAt(upUV)) + - CurrentViewPosition(downUV, SceneDepthAt(downUV)); + vec3 normalCross = cross(tangentX, tangentY); + float normalLengthSquared = dot(normalCross, normalCross); + vec3 normal = normalLengthSquared > 1.0e-12 + ? normalCross * inversesqrt(normalLengthSquared) + : vec3(0.0, 0.0, 1.0); + if (dot(normal, -centerPosition) < 0.0) { + normal = -normal; + } + return normal; +} + +float Luminance(vec3 color) { + return dot(color, vec3(0.2126, 0.7152, 0.0722)); +} + +vec3 ApplyScreenSpaceGI(vec3 baseColor, vec2 cameraUV, float depth, + vec3 position, vec3 normal) { + if (!ScreenEffectEnabled(4.0) || temporal.motionParams.z < 0.5 + || depth >= 0.99999) { + return baseColor; + } + vec3 indirect = vec3(0.0); + float totalWeight = 0.0; + for (int i = 0; i < 8; ++i) { + float fi = float(i); + float angle = fi * 2.39996323; + vec2 offset = vec2(cos(angle), sin(angle)) * (2.0 + fi * 1.5) + * temporal.sceneOutputExtent.xy; + vec2 sampleUV = clamp(cameraUV + offset, vec2(0.0), vec2(1.0)); + float sampleDepth = SceneDepthAt(sampleUV); + if (sampleDepth >= 0.99999) { + continue; + } + vec3 samplePosition = CurrentViewPosition(sampleUV, sampleDepth); + vec3 delta = samplePosition - position; + float deltaLength = length(delta); + if (deltaLength <= 0.0001) { + continue; + } + float distanceWeight = 1.0 - smoothstep(0.0, + max(24.0, abs(position.z) * 0.18), deltaLength); + float facing = max(dot(normal, delta / deltaLength), 0.0); + float weight = distanceWeight * (0.20 + 0.80 * facing); + indirect += SceneColorAt(sampleUV) * weight; + totalWeight += weight; + } + indirect /= max(totalWeight, 0.0001); + float receive = 0.08 + 0.12 + * (1.0 - clamp(Luminance(baseColor), 0.0, 1.0)); + return baseColor + indirect * temporal.previousViewAxis2.w * receive; +} + +vec3 ApplyScreenSpaceReflection(vec3 baseColor, vec2 cameraUV, float depth, + vec3 position, vec3 normal) { + if (!ScreenEffectEnabled(2.0) || temporal.motionParams.z < 0.5 + || depth >= 0.99999) { + return baseColor; + } + vec3 incident = normalize(position); + vec3 rayDirection = normalize(reflect(incident, normal)); + float stepCount = clamp(floor(temporal.previousViewAxis1.w + 0.5), + 4.0, 16.0); + float stepLength = temporal.previousViewAxis0.w / stepCount; + vec3 rayPosition = position + normal + * max(1.0, abs(position.z) * 0.002); + vec3 hitColor = baseColor; + float hitWeight = 0.0; + for (int i = 0; i < 16; ++i) { + if (float(i) >= stepCount) { + break; + } + rayPosition += rayDirection * stepLength; + if (rayPosition.z >= -0.5) { + break; + } + vec2 rayUV = ProjectCurrentView(rayPosition); + if (rayUV.x <= 0.0 || rayUV.y <= 0.0 + || rayUV.x >= 1.0 || rayUV.y >= 1.0) { + break; + } + float rayDepth = SceneDepthAt(rayUV); + if (rayDepth >= 0.99999) { + continue; + } + float sceneZ = ViewZFromDepth(rayDepth); + float thickness = max(4.0, abs(rayPosition.z) * 0.02); + float crossing = sceneZ - rayPosition.z; + if (i > 0 && crossing >= 0.0 && crossing < thickness) { + vec2 edge = smoothstep(vec2(0.0), vec2(0.08), rayUV) + * smoothstep(vec2(0.0), vec2(0.08), vec2(1.0) - rayUV); + hitColor = SceneColorAt(rayUV); + hitWeight = edge.x * edge.y; + break; + } + } + float facing = clamp(dot(-incident, normal), 0.0, 1.0); + float fresnel = 0.04 + 0.96 * pow(1.0 - facing, 5.0); + return mix(baseColor, hitColor, + hitWeight * fresnel * temporal.previousViewOrigin.w); +} + +vec3 ApplyFroxelVolumetrics(vec3 baseColor, vec2 cameraUV, float depth) { + if (!ScreenEffectEnabled(1.0)) { + return baseColor; + } + vec2 ndc = cameraUV * 2.0 - 1.0; + vec3 viewRay = normalize(vec3( + -(ndc.x + temporal.currentReconstruct.z) + * temporal.currentReconstruct.x, + -(ndc.y + temporal.currentReconstruct.w) + * temporal.currentReconstruct.y, + -1.0)); + vec3 worldRay = normalize(CurrentViewToWorldDirection(viewRay)); + float travel = temporal.currentViewAxis1.w; + if (temporal.motionParams.z > 0.5 && depth < 0.99999) { + travel = min(travel, length(CurrentViewPosition(cameraUV, depth))); + } + float sliceCount = clamp(floor(temporal.currentViewAxis2.w + 0.5), + 4.0, 16.0); + float sliceLength = travel / sliceCount; + float transmittance = 1.0; + vec3 scattering = vec3(0.0); + vec3 sunDirection = normalize(vec3(0.35, 0.25, 0.90)); + for (int i = 0; i < 16; ++i) { + if (float(i) >= sliceCount) { + break; + } + float midpoint = (float(i) + 0.5) * sliceLength; + float worldHeight = temporal.currentViewOrigin.z + + worldRay.z * midpoint; + float heightDensity = exp(-clamp((worldHeight + - temporal.currentViewOrigin.z) * 0.0008, -1.5, 2.0)); + float sliceTransmittance = exp(-temporal.currentViewAxis0.w + * heightDensity * sliceLength); + float phase = 0.55 + 0.45 + * pow(max(dot(worldRay, sunDirection), 0.0), 2.0); + vec3 fogColor = vec3(0.17, 0.21, 0.26) + + vec3(0.08, 0.07, 0.04) * max(worldRay.z, 0.0); + scattering += transmittance * (1.0 - sliceTransmittance) + * fogColor * phase; + transmittance *= sliceTransmittance; + } + return baseColor * transmittance + scattering; +} + +void main() { + vec2 outputCameraUV = TextureToCameraUV(fragUV); + vec2 sceneCameraUV = clamp(outputCameraUV + - temporal.temporalParams.xy * temporal.motionParams.w, + vec2(0.0), vec2(1.0)); + vec2 sceneTextureUV = CameraToTextureUV(sceneCameraUV); + vec4 current = texture(currentScene, sceneTextureUV); + float centerDepth = texture(sceneDepth, sceneTextureUV).r; + if (temporal.currentViewOrigin.w > 0.5) { + vec3 centerPosition = centerDepth < 0.99999 + ? CurrentViewPosition(sceneCameraUV, centerDepth) + : vec3(0.0, 0.0, -temporal.currentViewAxis1.w); + vec3 centerNormal = centerDepth < 0.99999 + ? DepthNormal(sceneCameraUV, centerPosition) + : vec3(0.0, 0.0, 1.0); + current.rgb = ApplyScreenSpaceGI(current.rgb, sceneCameraUV, + centerDepth, centerPosition, centerNormal); + current.rgb = ApplyScreenSpaceReflection(current.rgb, sceneCameraUV, + centerDepth, centerPosition, centerNormal); + current.rgb = ApplyFroxelVolumetrics(current.rgb, sceneCameraUV, + centerDepth); + } + vec3 neighborhoodMin = current.rgb; + vec3 neighborhoodMax = current.rgb; + float depthMin = centerDepth; + float depthMax = centerDepth; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + vec2 sampleCameraUV = clamp(sceneCameraUV + + vec2(float(x), float(y)) * temporal.sceneOutputExtent.xy, + vec2(0.0), vec2(1.0)); + vec2 sampleTextureUV = CameraToTextureUV(sampleCameraUV); + vec3 sampleColor = texture(currentScene, sampleTextureUV).rgb; + neighborhoodMin = min(neighborhoodMin, sampleColor); + neighborhoodMax = max(neighborhoodMax, sampleColor); + float sampleDepth = texture(sceneDepth, sampleTextureUV).r; + depthMin = min(depthMin, sampleDepth); + depthMax = max(depthMax, sampleDepth); + } + } + + // Vulkan currently has no dedicated rigid/skinned velocity target. Keep + // the object-valid branch in the shared contract disabled and rely on + // camera reprojection plus conservative rejection for moving geometry. + bool objectValid = false; + bool cameraValid = temporal.motionParams.y > 0.5 + && temporal.motionParams.z > 0.5; + vec2 previousCameraUV = cameraValid + ? CameraPreviousUV(sceneCameraUV, centerDepth) + : outputCameraUV; + bool inside = previousCameraUV.x >= 0.0 + && previousCameraUV.y >= 0.0 + && previousCameraUV.x <= 1.0 + && previousCameraUV.y <= 1.0; + bool historyUsable = temporal.temporalParams.z > 0.5 + && inside && (objectValid || cameraValid); + vec3 historyRaw = historyUsable + ? texture(historyScene, CameraToTextureUV(previousCameraUV)).rgb + : current.rgb; + vec3 historyClamped = clamp(historyRaw, neighborhoodMin, neighborhoodMax); + float colorDelta = MaxComponent(abs(current.rgb - historyRaw)); + float clampDelta = MaxComponent(abs(historyRaw - historyClamped)); + vec2 velocityPixels = (outputCameraUV - previousCameraUV) + * temporal.sceneOutputExtent.zw; + float motionReactive = smoothstep(12.0, 96.0, + length(velocityPixels)) * 0.35; + float depthReactive = temporal.motionParams.z > 0.5 + ? smoothstep(0.001, 0.02, depthMax - depthMin) * 0.20 + : 0.0; + float unsupportedNear = (temporal.motionParams.z > 0.5 && !objectValid) + ? (1.0 - smoothstep(0.82, 0.98, centerDepth)) * 0.25 + : 0.0; + float packetReactive = (InsideReactiveRect(outputCameraUV, + temporal.reactiveRect0) || InsideReactiveRect(outputCameraUV, + temporal.reactiveRect1)) ? 1.0 : 0.0; + float reactive = clamp(max( + max(colorDelta * 2.5, clampDelta * 5.0) + * temporal.depthFeedback.w, + max(motionReactive, max(depthReactive, unsupportedNear))), + 0.0, 1.0); + reactive = max(reactive, packetReactive); + if (!historyUsable) { + reactive = 1.0; + } + float historyWeight = historyUsable + ? temporal.depthFeedback.z * (1.0 - reactive) + : 0.0; + vec3 resolved = mix(current.rgb, historyClamped, historyWeight); + if (temporal.temporalParams.w > 0.5 + && temporal.temporalParams.w < 1.5) { + float magnitude = clamp(length(velocityPixels) / 32.0, 0.0, 1.0); + vec2 direction = clamp(velocityPixels / 32.0, + vec2(-1.0), vec2(1.0)); + resolved = vec3(direction * 0.5 + 0.5, 1.0) * magnitude; + } else if (temporal.temporalParams.w > 1.5 + && temporal.temporalParams.w < 2.5) { + resolved = vec3(reactive, reactive * 0.25, 0.0); + } else if (temporal.temporalParams.w > 2.5) { + resolved = vec3(historyWeight); + } + outColor = vec4(resolved, current.a); +} diff --git a/src/renderer/Vulkan/shaders/temporal_resolve.vert b/src/renderer/Vulkan/shaders/temporal_resolve.vert new file mode 100644 index 00000000..5f14c4a8 --- /dev/null +++ b/src/renderer/Vulkan/shaders/temporal_resolve.vert @@ -0,0 +1,20 @@ +#version 450 + +// Full-screen triangle for the native-output temporal presentation pass. +// This pass deliberately uses a positive Vulkan viewport: fragUV therefore +// follows image-memory coordinates (0,0 is the top-left texel) even though +// the rest of the renderer preserves idTech's bottom-left view convention +// with negative-height viewports. + +layout(location = 0) out vec2 fragUV; + +void main() { + const vec2 positions[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + vec2 position = positions[gl_VertexIndex]; + gl_Position = vec4(position, 0.0, 1.0); + fragUV = position * 0.5 + 0.5; +} diff --git a/src/renderer/Vulkan/shaders/temporal_resolve_spv.h b/src/renderer/Vulkan/shaders/temporal_resolve_spv.h new file mode 100644 index 00000000..d29d3365 --- /dev/null +++ b/src/renderer/Vulkan/shaders/temporal_resolve_spv.h @@ -0,0 +1,2441 @@ +// GENERATED by tools/build/spirv_to_header.py — do not edit by hand. +// Regenerate after editing src/renderer/Vulkan/shaders/*; +// tools/tests/vk_shader_header_pin.py guards drift. +#ifndef __VK_TEMPORAL_RESOLVE_SPV_H__ +#define __VK_TEMPORAL_RESOLVE_SPV_H__ + +alignas(4) static const unsigned char vk_temporal_resolve_vert_spv[] = { + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x56, 0x65, 0x72, 0x74, 0x65, + 0x78, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x50, 0x65, 0x72, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x67, 0x6c, 0x5f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x50, 0x6f, 0x69, 0x6e, 0x74, + 0x53, 0x69, 0x7a, 0x65, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x67, 0x6c, 0x5f, 0x43, 0x6c, 0x69, 0x70, 0x44, 0x69, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x00, 0x06, 0x00, 0x07, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x67, 0x6c, 0x5f, 0x43, 0x75, 0x6c, 0x6c, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x66, 0x72, 0x61, 0x67, 0x55, 0x56, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x47, 0x00, 0x03, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x16, 0x00, 0x03, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xbf, 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x2c, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x04, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x06, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x3f, 0x20, 0x00, 0x04, 0x00, 0x28, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x17, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x13, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x09, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x29, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, +}; +static const unsigned int vk_temporal_resolve_vert_spv_size = 1328u; + +alignas(4) static const unsigned char vk_temporal_resolve_frag_spv[] = { + 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x08, 0x00, 0x20, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x06, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x73, 0x74, 0x64, 0x2e, 0x34, 0x35, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x07, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, 0x10, 0x00, 0x03, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, + 0xc2, 0x01, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x54, 0x65, 0x78, 0x74, + 0x75, 0x72, 0x65, 0x54, 0x6f, 0x43, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x28, 0x76, 0x66, + 0x32, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x43, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x54, 0x6f, + 0x54, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x55, 0x56, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x75, 0x76, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x13, 0x00, 0x00, 0x00, 0x56, 0x69, 0x65, 0x77, 0x5a, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x70, + 0x74, 0x68, 0x28, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x54, 0x6f, 0x57, 0x6f, 0x72, + 0x6c, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x76, 0x66, 0x33, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x18, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, + 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, + 0x1c, 0x00, 0x00, 0x00, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x54, 0x6f, 0x50, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x1b, 0x00, 0x00, 0x00, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, + 0x05, 0x00, 0x09, 0x00, 0x20, 0x00, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x50, + 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, 0x28, 0x76, 0x66, 0x33, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x25, 0x00, 0x00, 0x00, 0x43, 0x61, 0x6d, 0x65, + 0x72, 0x61, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x55, 0x56, 0x28, 0x76, 0x66, 0x32, + 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x24, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x49, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, + 0x63, 0x74, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, 0x34, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x30, 0x00, 0x00, 0x00, 0x72, 0x65, 0x63, 0x74, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x35, 0x00, 0x00, 0x00, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x45, 0x66, + 0x66, 0x65, 0x63, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x28, 0x66, 0x31, 0x3b, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x34, 0x00, 0x00, 0x00, 0x62, 0x69, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x39, 0x00, 0x00, 0x00, 0x53, 0x63, 0x65, 0x6e, + 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, 0x41, 0x74, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x07, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x53, 0x63, 0x65, 0x6e, + 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x41, 0x74, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x09, 0x00, 0x42, 0x00, 0x00, 0x00, 0x43, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x28, + 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x00, 0x05, 0x00, 0x05, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x41, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x08, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x44, 0x65, 0x70, 0x74, + 0x68, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x28, 0x76, 0x66, 0x32, 0x3b, 0x76, 0x66, 0x33, 0x3b, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x48, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, + 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x49, 0x00, 0x00, 0x00, + 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x4c, 0x75, 0x6d, 0x69, 0x6e, 0x61, 0x6e, 0x63, + 0x65, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0c, 0x00, 0x55, 0x00, 0x00, 0x00, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x53, 0x70, 0x61, 0x63, 0x65, + 0x47, 0x49, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, 0x31, 0x3b, 0x76, 0x66, + 0x33, 0x3b, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x62, 0x61, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x51, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x52, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x53, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x54, 0x00, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x00, 0x00, 0x05, 0x00, 0x0e, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x41, 0x70, 0x70, 0x6c, + 0x79, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x66, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, + 0x31, 0x3b, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, 0x33, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x62, 0x61, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x58, 0x00, 0x00, 0x00, 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x59, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, + 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x5b, 0x00, 0x00, 0x00, + 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x00, 0x05, 0x00, 0x0b, 0x00, 0x62, 0x00, 0x00, 0x00, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x46, 0x72, 0x6f, 0x78, 0x65, 0x6c, 0x56, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x28, 0x76, 0x66, 0x33, 0x3b, 0x76, 0x66, 0x32, 0x3b, 0x66, + 0x31, 0x3b, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x62, 0x61, 0x73, 0x65, + 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x61, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x78, 0x00, 0x00, 0x00, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x00, + 0x05, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x54, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x63, 0x65, 0x6e, + 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x46, 0x65, 0x65, + 0x64, 0x62, 0x61, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x41, + 0x78, 0x69, 0x73, 0x30, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x41, + 0x78, 0x69, 0x73, 0x31, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x56, 0x69, 0x65, 0x77, 0x41, + 0x78, 0x69, 0x73, 0x32, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, + 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, + 0x41, 0x78, 0x69, 0x73, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, + 0x41, 0x78, 0x69, 0x73, 0x31, 0x00, 0x00, 0x00, 0x06, 0x00, 0x08, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x56, 0x69, 0x65, 0x77, + 0x41, 0x78, 0x69, 0x73, 0x32, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x07, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x72, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x63, 0x74, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x52, 0x65, 0x63, 0x74, 0x31, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd8, 0x00, 0x00, 0x00, + 0x63, 0x6c, 0x69, 0x70, 0x58, 0x59, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0x6e, 0x64, 0x63, 0x00, 0x05, 0x00, 0x03, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x72, 0x61, 0x79, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x12, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x15, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x17, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x5a, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, 0x50, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x37, 0x01, 0x00, 0x00, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x40, 0x01, 0x00, 0x00, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x59, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x93, 0x01, 0x00, 0x00, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x44, 0x65, 0x70, + 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x99, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa3, 0x01, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, + 0xa9, 0x01, 0x00, 0x00, 0x6e, 0x64, 0x63, 0x00, 0x05, 0x00, 0x04, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x76, 0x69, 0x65, 0x77, 0x5a, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xaf, 0x01, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xcc, 0x01, 0x00, 0x00, + 0x77, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x63, 0x6c, 0x69, 0x70, + 0x58, 0x59, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xf1, 0x01, 0x00, 0x00, 0x64, 0x78, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xf6, 0x01, 0x00, 0x00, 0x64, 0x79, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xfa, 0x01, 0x00, 0x00, 0x6c, 0x65, 0x66, 0x74, 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xff, 0x01, 0x00, 0x00, 0x72, 0x69, 0x67, 0x68, 0x74, 0x55, 0x56, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x04, 0x02, 0x00, 0x00, 0x64, 0x6f, 0x77, 0x6e, 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x09, 0x02, 0x00, 0x00, 0x75, 0x70, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x0e, 0x02, 0x00, 0x00, 0x74, 0x61, 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x58, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x12, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x14, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x16, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x19, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x1e, 0x02, 0x00, 0x00, 0x74, 0x61, 0x6e, 0x67, 0x65, 0x6e, 0x74, 0x59, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x22, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x24, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x26, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x29, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x43, 0x72, 0x6f, 0x73, 0x73, 0x00, 0x05, 0x00, 0x07, 0x00, 0x32, 0x02, 0x00, 0x00, + 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x53, 0x71, 0x75, 0x61, + 0x72, 0x65, 0x64, 0x00, 0x05, 0x00, 0x04, 0x00, 0x36, 0x02, 0x00, 0x00, 0x6e, 0x6f, 0x72, 0x6d, + 0x61, 0x6c, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x59, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x69, 0x6e, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x6d, 0x02, 0x00, 0x00, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x05, 0x00, 0x03, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x77, 0x02, 0x00, 0x00, + 0x66, 0x69, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x61, 0x6e, 0x67, 0x6c, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x6f, 0x66, 0x66, 0x73, + 0x65, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x92, 0x02, 0x00, 0x00, + 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x93, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x9b, 0x02, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9c, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x9e, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x64, 0x65, 0x6c, 0x74, + 0x61, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xa5, 0x02, 0x00, 0x00, 0x64, 0x65, 0x6c, 0x74, + 0x61, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x00, 0x05, 0x00, 0x06, 0x00, 0xae, 0x02, 0x00, 0x00, + 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xb9, 0x02, 0x00, 0x00, 0x66, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc1, 0x02, 0x00, 0x00, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xc9, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xda, 0x02, 0x00, 0x00, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xdd, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0xee, 0x02, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xff, 0x02, 0x00, 0x00, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x02, 0x03, 0x00, 0x00, 0x72, 0x61, 0x79, 0x44, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x07, 0x03, 0x00, 0x00, 0x73, 0x74, 0x65, 0x70, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x73, 0x74, 0x65, 0x70, 0x4c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x13, 0x03, 0x00, 0x00, 0x72, 0x61, 0x79, 0x50, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x05, 0x00, 0x05, 0x00, 0x1e, 0x03, 0x00, 0x00, + 0x68, 0x69, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x20, 0x03, 0x00, 0x00, 0x68, 0x69, 0x74, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x3d, 0x03, 0x00, 0x00, 0x72, 0x61, 0x79, 0x55, 0x56, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x3e, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x5c, 0x03, 0x00, 0x00, 0x72, 0x61, 0x79, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x65, 0x03, 0x00, 0x00, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x5a, 0x00, 0x00, + 0x05, 0x00, 0x04, 0x00, 0x66, 0x03, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x69, 0x03, 0x00, 0x00, 0x74, 0x68, 0x69, 0x63, 0x6b, 0x6e, 0x65, 0x73, + 0x73, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x70, 0x03, 0x00, 0x00, 0x63, 0x72, 0x6f, 0x73, + 0x73, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x65, 0x64, 0x67, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x88, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x66, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x99, 0x03, 0x00, 0x00, + 0x66, 0x72, 0x65, 0x73, 0x6e, 0x65, 0x6c, 0x00, 0x05, 0x00, 0x04, 0x00, 0xae, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xb5, 0x03, 0x00, 0x00, + 0x6e, 0x64, 0x63, 0x00, 0x05, 0x00, 0x04, 0x00, 0xba, 0x03, 0x00, 0x00, 0x76, 0x69, 0x65, 0x77, + 0x52, 0x61, 0x79, 0x00, 0x05, 0x00, 0x05, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x52, 0x61, 0x79, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd0, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xd4, 0x03, 0x00, 0x00, + 0x74, 0x72, 0x61, 0x76, 0x65, 0x6c, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe0, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xe2, 0x03, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0x73, 0x6c, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xed, 0x03, 0x00, 0x00, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x00, + 0x05, 0x00, 0x06, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf2, 0x03, 0x00, 0x00, + 0x73, 0x63, 0x61, 0x74, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xf3, 0x03, 0x00, 0x00, 0x73, 0x75, 0x6e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x07, 0x04, 0x00, 0x00, 0x6d, 0x69, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x0d, 0x04, 0x00, 0x00, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x05, 0x00, 0x06, 0x00, 0x15, 0x04, 0x00, 0x00, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x44, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x79, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x07, 0x00, 0x20, 0x04, 0x00, 0x00, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x29, 0x04, 0x00, 0x00, 0x70, 0x68, 0x61, 0x73, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x33, 0x04, 0x00, 0x00, 0x66, 0x6f, 0x67, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x55, 0x04, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x61, + 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x57, 0x04, 0x00, 0x00, + 0x66, 0x72, 0x61, 0x67, 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x58, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x5b, 0x04, 0x00, 0x00, + 0x73, 0x63, 0x65, 0x6e, 0x65, 0x43, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x06, 0x00, 0x66, 0x04, 0x00, 0x00, 0x73, 0x63, 0x65, 0x6e, 0x65, 0x54, 0x65, 0x78, + 0x74, 0x75, 0x72, 0x65, 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x67, 0x04, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x00, 0x05, 0x00, 0x05, 0x00, 0x6e, 0x04, 0x00, 0x00, + 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x44, 0x65, 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x78, 0x04, 0x00, 0x00, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x80, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x89, 0x04, 0x00, 0x00, 0x63, 0x65, 0x6e, 0x74, + 0x65, 0x72, 0x4e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x8f, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x91, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x96, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x99, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x9b, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x9d, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x9f, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xa8, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xab, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xad, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xaf, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xb1, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xba, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xbd, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xbf, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xc8, 0x04, 0x00, 0x00, 0x6e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x68, 0x6f, 0x6f, 0x64, + 0x4d, 0x69, 0x6e, 0x00, 0x05, 0x00, 0x06, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x6e, 0x65, 0x69, 0x67, + 0x68, 0x62, 0x6f, 0x72, 0x68, 0x6f, 0x6f, 0x64, 0x4d, 0x61, 0x78, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xce, 0x04, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x4d, 0x69, 0x6e, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x4d, 0x61, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x03, 0x00, 0xdb, 0x04, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xe3, 0x04, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x43, 0x61, 0x6d, 0x65, 0x72, 0x61, + 0x55, 0x56, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x54, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x55, 0x56, 0x00, 0x05, 0x00, 0x04, 0x00, + 0xf1, 0x04, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0xf4, 0x04, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xff, 0x04, 0x00, 0x00, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x44, 0x65, + 0x70, 0x74, 0x68, 0x00, 0x05, 0x00, 0x05, 0x00, 0x0f, 0x05, 0x00, 0x00, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x00, 0x05, 0x00, 0x05, 0x00, 0x11, 0x05, 0x00, 0x00, + 0x63, 0x61, 0x6d, 0x65, 0x72, 0x61, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x00, 0x05, 0x00, 0x07, 0x00, + 0x1b, 0x05, 0x00, 0x00, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x43, 0x61, 0x6d, 0x65, + 0x72, 0x61, 0x55, 0x56, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x20, 0x05, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x22, 0x05, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x28, 0x05, 0x00, 0x00, + 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x3e, 0x05, 0x00, 0x00, + 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, + 0x61, 0x77, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x4f, 0x05, 0x00, 0x00, 0x68, 0x69, 0x73, 0x74, + 0x6f, 0x72, 0x79, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x51, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x5a, 0x05, 0x00, 0x00, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x43, 0x6c, 0x61, 0x6d, 0x70, + 0x65, 0x64, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0x65, 0x05, 0x00, 0x00, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x67, 0x05, 0x00, 0x00, + 0x63, 0x6c, 0x61, 0x6d, 0x70, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, + 0x6c, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x6e, 0x05, 0x00, 0x00, 0x76, 0x65, 0x6c, 0x6f, 0x63, 0x69, 0x74, 0x79, 0x50, 0x69, 0x78, 0x65, + 0x6c, 0x73, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x76, 0x05, 0x00, 0x00, 0x6d, 0x6f, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, + 0x7e, 0x05, 0x00, 0x00, 0x64, 0x65, 0x70, 0x74, 0x68, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0x8d, 0x05, 0x00, 0x00, 0x75, 0x6e, 0x73, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x4e, 0x65, 0x61, 0x72, 0x00, 0x05, 0x00, 0x06, 0x00, + 0xa0, 0x05, 0x00, 0x00, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa2, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xa4, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xac, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x04, 0x00, 0xae, 0x05, 0x00, 0x00, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xb4, 0x05, 0x00, 0x00, 0x72, 0x65, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x06, 0x00, 0xcc, 0x05, 0x00, 0x00, + 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x05, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xea, 0x05, 0x00, 0x00, 0x6d, 0x61, 0x67, 0x6e, + 0x69, 0x74, 0x75, 0x64, 0x65, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0xf0, 0x05, 0x00, 0x00, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, + 0x18, 0x06, 0x00, 0x00, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x03, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x90, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x48, 0x00, 0x05, 0x00, 0x7d, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x9f, 0x01, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x9f, 0x01, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x57, 0x04, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x4f, 0x05, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x4f, 0x05, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x47, 0x00, 0x04, 0x00, + 0x18, 0x06, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x11, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x04, 0x00, 0x27, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x33, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x37, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x21, 0x00, 0x04, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x21, 0x00, 0x05, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x21, 0x00, 0x05, 0x00, 0x47, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x21, 0x00, 0x08, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x21, 0x00, 0x06, 0x00, 0x5e, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x1e, 0x00, 0x12, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x7d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x15, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x82, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0xac, 0xc5, 0x27, 0x37, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, + 0xac, 0xc5, 0x27, 0xb7, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x99, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x64, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7a, 0xc4, 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xd5, 0x00, 0x00, 0x00, 0xd5, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xd9, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x00, 0x00, 0x58, 0xff, 0x7f, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x64, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x42, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x19, 0x00, 0x09, 0x00, 0x90, 0x01, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x03, 0x00, + 0x91, 0x01, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x91, 0x01, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, 0xcc, 0xbc, 0x8c, 0x2b, + 0x2c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x51, 0x02, 0x00, 0x00, 0xd0, 0xb3, 0x59, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x52, 0x02, 0x00, 0x00, 0x59, 0x17, 0x37, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x53, 0x02, 0x00, 0x00, 0x98, 0xdd, 0x93, 0x3d, 0x2c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x54, 0x02, 0x00, 0x00, 0x51, 0x02, 0x00, 0x00, 0x52, 0x02, 0x00, 0x00, 0x53, 0x02, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x6e, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x7c, 0x02, 0x00, 0x00, 0xff, 0x98, 0x19, 0x40, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x85, 0x02, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa9, 0x02, 0x00, 0x00, 0x17, 0xb7, 0xd1, 0x38, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xaf, 0x02, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb3, 0x02, 0x00, 0x00, 0xec, 0x51, 0x38, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc3, 0x02, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc4, 0x02, 0x00, 0x00, 0xcd, 0xcc, 0x4c, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdb, 0x02, 0x00, 0x00, 0x0a, 0xd7, 0xa3, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdc, 0x02, 0x00, 0x00, 0x8f, 0xc2, 0xf5, 0x3d, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x41, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x19, 0x03, 0x00, 0x00, 0x6f, 0x12, 0x03, 0x3b, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x28, 0x03, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x38, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6d, 0x03, 0x00, 0x00, 0x0a, 0xd7, 0xa3, 0x3c, 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x81, 0x03, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0x0a, 0xd7, 0x23, 0x3d, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9b, 0x03, 0x00, 0x00, 0x8f, 0xc2, 0x75, 0x3f, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9e, 0x03, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x40, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0x52, 0xa6, 0xb3, 0x3e, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, 0x3a, 0x52, 0x80, 0x3e, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf6, 0x03, 0x00, 0x00, 0x69, 0xfa, 0x66, 0x3f, 0x2c, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, 0xf4, 0x03, 0x00, 0x00, 0xf5, 0x03, 0x00, 0x00, + 0xf6, 0x03, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1a, 0x04, 0x00, 0x00, + 0x17, 0xb7, 0x51, 0x3a, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, + 0x00, 0x00, 0xc0, 0xbf, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, + 0xcd, 0xcc, 0x0c, 0x3f, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, + 0x66, 0x66, 0xe6, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x34, 0x04, 0x00, 0x00, + 0x7b, 0x14, 0x2e, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x35, 0x04, 0x00, 0x00, + 0x3d, 0x0a, 0x57, 0x3e, 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, + 0xb8, 0x1e, 0x85, 0x3e, 0x2c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, + 0x34, 0x04, 0x00, 0x00, 0x35, 0x04, 0x00, 0x00, 0x36, 0x04, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x29, 0x5c, 0x8f, 0x3d, 0x2c, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00, 0xdb, 0x02, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, + 0x9a, 0x03, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x56, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x56, 0x04, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x04, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x03, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x92, 0x01, 0x00, 0x00, 0x4f, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x77, 0x05, 0x00, 0x00, 0x00, 0x00, 0x40, 0x41, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x42, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, 0x33, 0x33, 0xb3, 0x3e, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00, 0x6f, 0x12, 0x83, 0x3a, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, 0x85, 0xeb, 0x51, 0x3f, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, 0x48, 0xe1, 0x7a, 0x3f, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3e, + 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xa1, 0x05, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xab, 0x05, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x05, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, + 0x2b, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, + 0x2c, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0x20, 0x00, 0x04, 0x00, 0x17, 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x17, 0x06, 0x00, 0x00, 0x18, 0x06, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x58, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x66, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x8c, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x91, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x96, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x99, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x9b, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x9f, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xa8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xad, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xaf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xba, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xbd, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xbf, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xce, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x6e, 0x02, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x6e, 0x02, 0x00, 0x00, 0xdb, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xe3, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf1, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x0f, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x11, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1d, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x22, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x0e, 0x05, 0x00, 0x00, 0x3e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x51, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x5a, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x65, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x67, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x82, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x8d, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x94, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xa2, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xa4, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xac, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0xea, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf0, 0x05, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x59, 0x04, 0x00, 0x00, 0x57, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x58, 0x04, 0x00, 0x00, 0x59, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5a, 0x04, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x58, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x55, 0x04, 0x00, 0x00, 0x5a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x5f, 0x04, 0x00, 0x00, 0x5e, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x60, 0x04, 0x00, 0x00, 0x5f, 0x04, 0x00, 0x00, + 0x5f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x61, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00, + 0x61, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x63, 0x04, 0x00, 0x00, + 0x60, 0x04, 0x00, 0x00, 0x62, 0x04, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x64, 0x04, 0x00, 0x00, 0x5c, 0x04, 0x00, 0x00, 0x63, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x65, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x64, 0x04, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5b, 0x04, 0x00, 0x00, 0x65, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x68, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x67, 0x04, 0x00, 0x00, + 0x68, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x69, 0x04, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x67, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x66, 0x04, 0x00, 0x00, + 0x69, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, 0x6b, 0x04, 0x00, 0x00, + 0x9f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6c, 0x04, 0x00, 0x00, + 0x66, 0x04, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x6d, 0x04, 0x00, 0x00, + 0x6b, 0x04, 0x00, 0x00, 0x6c, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x6d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, 0x6f, 0x04, 0x00, 0x00, + 0x93, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x70, 0x04, 0x00, 0x00, + 0x66, 0x04, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, + 0x6f, 0x04, 0x00, 0x00, 0x70, 0x04, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x72, 0x04, 0x00, 0x00, 0x71, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6e, 0x04, 0x00, 0x00, 0x72, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x73, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x74, 0x04, 0x00, 0x00, 0x73, 0x04, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x75, 0x04, 0x00, 0x00, 0x74, 0x04, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x77, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x75, 0x04, 0x00, 0x00, 0x76, 0x04, 0x00, 0x00, 0x77, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x76, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x79, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x7a, 0x04, 0x00, 0x00, 0x79, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x7d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7a, 0x04, 0x00, 0x00, + 0x7c, 0x04, 0x00, 0x00, 0x83, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7c, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x7f, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x7f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x81, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x80, 0x04, 0x00, 0x00, 0x81, 0x04, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x82, 0x04, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x7e, 0x04, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x82, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x7d, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x83, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x84, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xa2, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x85, 0x04, 0x00, 0x00, + 0x84, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x86, 0x04, 0x00, 0x00, + 0x85, 0x04, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x87, 0x04, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x86, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x7b, 0x04, 0x00, 0x00, 0x87, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x7d, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x7d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x88, 0x04, 0x00, 0x00, 0x7b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x04, 0x00, 0x00, + 0x88, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x04, 0x00, 0x00, + 0x6e, 0x04, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x8b, 0x04, 0x00, 0x00, + 0x8a, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x8e, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x8b, 0x04, 0x00, 0x00, 0x8d, 0x04, 0x00, 0x00, + 0x94, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x8d, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x8f, 0x04, 0x00, 0x00, 0x90, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x92, 0x04, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x91, 0x04, 0x00, 0x00, + 0x92, 0x04, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x93, 0x04, 0x00, 0x00, + 0x4a, 0x00, 0x00, 0x00, 0x8f, 0x04, 0x00, 0x00, 0x91, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x8c, 0x04, 0x00, 0x00, 0x93, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x8e, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x94, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8c, 0x04, 0x00, 0x00, + 0x42, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x8e, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x8e, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, + 0x8c, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x89, 0x04, 0x00, 0x00, 0x95, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x97, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0x98, 0x04, 0x00, 0x00, 0x97, 0x04, 0x00, 0x00, + 0x97, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x96, 0x04, 0x00, 0x00, 0x98, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x9a, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x99, 0x04, 0x00, 0x00, 0x9a, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9c, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9b, 0x04, 0x00, 0x00, + 0x9c, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, + 0x78, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x9e, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xa0, 0x04, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9f, 0x04, 0x00, 0x00, 0xa0, 0x04, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa1, 0x04, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x96, 0x04, 0x00, 0x00, + 0x99, 0x04, 0x00, 0x00, 0x9b, 0x04, 0x00, 0x00, 0x9d, 0x04, 0x00, 0x00, 0x9f, 0x04, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xa2, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x65, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa3, 0x04, 0x00, 0x00, + 0xa1, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa2, 0x04, 0x00, 0x00, + 0xa3, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xa4, 0x04, 0x00, 0x00, + 0x6a, 0x04, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa5, 0x04, 0x00, 0x00, 0xa1, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa4, 0x04, 0x00, 0x00, 0xa5, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xa6, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, 0xa1, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa6, 0x04, 0x00, 0x00, 0xa7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xaa, 0x04, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, 0xa9, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xa8, 0x04, 0x00, 0x00, 0xaa, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xac, 0x04, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xab, 0x04, 0x00, 0x00, + 0xac, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xae, 0x04, 0x00, 0x00, + 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xad, 0x04, 0x00, 0x00, 0xae, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, 0x78, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xaf, 0x04, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xb2, 0x04, 0x00, 0x00, 0x89, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb1, 0x04, 0x00, 0x00, 0xb2, 0x04, 0x00, 0x00, 0x39, 0x00, 0x09, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xb3, 0x04, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0xa8, 0x04, 0x00, 0x00, 0xab, 0x04, 0x00, 0x00, + 0xad, 0x04, 0x00, 0x00, 0xaf, 0x04, 0x00, 0x00, 0xb1, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xb4, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb5, 0x04, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x04, 0x00, 0x00, 0xb5, 0x04, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xb6, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb7, 0x04, 0x00, 0x00, + 0xb3, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb6, 0x04, 0x00, 0x00, + 0xb7, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xb8, 0x04, 0x00, 0x00, + 0x6a, 0x04, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb9, 0x04, 0x00, 0x00, 0xb3, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb8, 0x04, 0x00, 0x00, 0xb9, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xbb, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xbc, 0x04, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, 0xbb, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xba, 0x04, 0x00, 0x00, + 0xbc, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xbe, 0x04, 0x00, 0x00, + 0x5b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xbd, 0x04, 0x00, 0x00, 0xbe, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xbf, 0x04, 0x00, 0x00, 0xc0, 0x04, 0x00, 0x00, 0x39, 0x00, 0x07, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xc1, 0x04, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0xba, 0x04, 0x00, 0x00, + 0xbd, 0x04, 0x00, 0x00, 0xbf, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xc2, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc3, 0x04, 0x00, 0x00, 0xc1, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xc2, 0x04, 0x00, 0x00, 0xc3, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xc4, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc5, 0x04, 0x00, 0x00, 0xc1, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc4, 0x04, 0x00, 0x00, 0xc5, 0x04, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xc6, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc7, 0x04, 0x00, 0x00, + 0xc1, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc6, 0x04, 0x00, 0x00, + 0xc7, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x77, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x77, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xc9, 0x04, 0x00, 0x00, + 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xca, 0x04, 0x00, 0x00, + 0xc9, 0x04, 0x00, 0x00, 0xc9, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc8, 0x04, 0x00, 0x00, 0xca, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xcc, 0x04, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xcd, 0x04, 0x00, 0x00, 0xcc, 0x04, 0x00, 0x00, + 0xcc, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xcb, 0x04, 0x00, 0x00, 0xcd, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xce, 0x04, 0x00, 0x00, 0xcf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x04, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd0, 0x04, 0x00, 0x00, + 0xd1, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd2, 0x04, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xd4, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd4, 0x04, 0x00, 0x00, + 0xf6, 0x00, 0x04, 0x00, 0xd6, 0x04, 0x00, 0x00, 0xd7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xd8, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd8, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xd9, 0x04, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, + 0xb3, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xda, 0x04, 0x00, 0x00, 0xd9, 0x04, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xda, 0x04, 0x00, 0x00, 0xd5, 0x04, 0x00, 0x00, + 0xd6, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd5, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xdb, 0x04, 0x00, 0x00, 0xd3, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xdc, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xdc, 0x04, 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, 0xde, 0x04, 0x00, 0x00, + 0xdf, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe0, 0x04, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xe0, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xe1, 0x04, 0x00, 0x00, 0xdb, 0x04, 0x00, 0x00, 0xb3, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xe2, 0x04, 0x00, 0x00, 0xe1, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xe2, 0x04, 0x00, 0x00, 0xdd, 0x04, 0x00, 0x00, 0xde, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xdd, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe4, 0x04, 0x00, 0x00, + 0x5b, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xe5, 0x04, 0x00, 0x00, + 0xdb, 0x04, 0x00, 0x00, 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x04, 0x00, 0x00, + 0xe5, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0xe7, 0x04, 0x00, 0x00, + 0xd2, 0x04, 0x00, 0x00, 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe8, 0x04, 0x00, 0x00, + 0xe7, 0x04, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, + 0xe6, 0x04, 0x00, 0x00, 0xe8, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0xea, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, 0xea, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xec, 0x04, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, 0xeb, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xed, 0x04, 0x00, 0x00, 0xe9, 0x04, 0x00, 0x00, 0xec, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xee, 0x04, 0x00, 0x00, 0xe4, 0x04, 0x00, 0x00, 0xed, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0xef, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xee, 0x04, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe3, 0x04, 0x00, 0x00, 0xef, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, 0xe3, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf1, 0x04, 0x00, 0x00, 0xf2, 0x04, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf3, 0x04, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0xf1, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf0, 0x04, 0x00, 0x00, 0xf3, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, + 0xf5, 0x04, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf6, 0x04, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xf7, 0x04, 0x00, 0x00, 0xf5, 0x04, 0x00, 0x00, 0xf6, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xf8, 0x04, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, 0xf7, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf4, 0x04, 0x00, 0x00, 0xf8, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xf9, 0x04, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xfa, 0x04, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xfb, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0xf9, 0x04, 0x00, 0x00, + 0xfa, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc8, 0x04, 0x00, 0x00, 0xfb, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, 0xf4, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xfc, 0x04, 0x00, 0x00, 0xfd, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xcb, 0x04, 0x00, 0x00, 0xfe, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, + 0x00, 0x05, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x01, 0x05, 0x00, 0x00, 0xf0, 0x04, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x02, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x01, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xff, 0x04, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00, 0xce, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0xff, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, + 0x04, 0x05, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xce, 0x04, 0x00, 0x00, + 0x06, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, + 0xd0, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, + 0xff, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd0, 0x04, 0x00, 0x00, 0x09, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xdf, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdf, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x0a, 0x05, 0x00, 0x00, 0xdb, 0x04, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x0b, 0x05, 0x00, 0x00, 0x0a, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xdb, 0x04, 0x00, 0x00, 0x0b, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xdc, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xde, 0x04, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xd7, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd7, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x0c, 0x05, 0x00, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x0d, 0x05, 0x00, 0x00, 0x0c, 0x05, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd2, 0x04, 0x00, 0x00, 0x0d, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xd4, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd6, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x0f, 0x05, 0x00, 0x00, 0x10, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x12, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, 0x12, 0x05, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x14, 0x05, 0x00, 0x00, 0x13, 0x05, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x16, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x14, 0x05, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x16, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x15, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x17, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x05, 0x00, 0x00, 0x17, 0x05, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x19, 0x05, 0x00, 0x00, 0x18, 0x05, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x16, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x16, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x1a, 0x05, 0x00, 0x00, + 0x14, 0x05, 0x00, 0x00, 0xd6, 0x04, 0x00, 0x00, 0x19, 0x05, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x11, 0x05, 0x00, 0x00, 0x1a, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x1c, 0x05, 0x00, 0x00, 0x11, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x1f, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x1c, 0x05, 0x00, 0x00, + 0x1e, 0x05, 0x00, 0x00, 0x25, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x1e, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x5b, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x20, 0x05, 0x00, 0x00, 0x21, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x23, 0x05, 0x00, 0x00, 0x6e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x22, 0x05, 0x00, 0x00, 0x23, 0x05, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x24, 0x05, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x00, 0x22, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1d, 0x05, 0x00, 0x00, 0x24, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x1f, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x25, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x26, 0x05, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x1d, 0x05, 0x00, 0x00, 0x26, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x1f, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x1f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x27, 0x05, 0x00, 0x00, 0x1d, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x05, 0x00, 0x00, + 0x27, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x29, 0x05, 0x00, 0x00, + 0x1b, 0x05, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2a, 0x05, 0x00, 0x00, 0x29, 0x05, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x2b, 0x05, 0x00, 0x00, 0x2a, 0x05, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x2d, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x2b, 0x05, 0x00, 0x00, + 0x2c, 0x05, 0x00, 0x00, 0x2d, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2c, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x2e, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x05, 0x00, 0x00, + 0x2e, 0x05, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x30, 0x05, 0x00, 0x00, + 0x2f, 0x05, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x2d, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x2d, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x31, 0x05, 0x00, 0x00, 0x2b, 0x05, 0x00, 0x00, 0x1f, 0x05, 0x00, 0x00, 0x30, 0x05, 0x00, 0x00, + 0x2c, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x33, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x31, 0x05, 0x00, 0x00, 0x32, 0x05, 0x00, 0x00, 0x33, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x32, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x34, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, 0x34, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x36, 0x05, 0x00, 0x00, 0x35, 0x05, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x33, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x33, 0x05, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x37, 0x05, 0x00, 0x00, 0x31, 0x05, 0x00, 0x00, + 0x2d, 0x05, 0x00, 0x00, 0x36, 0x05, 0x00, 0x00, 0x32, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x39, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x37, 0x05, 0x00, 0x00, + 0x38, 0x05, 0x00, 0x00, 0x39, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x38, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3a, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x05, 0x00, 0x00, + 0x3a, 0x05, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x3c, 0x05, 0x00, 0x00, + 0x3b, 0x05, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x39, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x39, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x3d, 0x05, 0x00, 0x00, 0x37, 0x05, 0x00, 0x00, 0x33, 0x05, 0x00, 0x00, 0x3c, 0x05, 0x00, 0x00, + 0x38, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x28, 0x05, 0x00, 0x00, 0x3d, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x3f, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x5d, 0x04, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x40, 0x05, 0x00, 0x00, 0x3f, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x41, 0x05, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, 0x28, 0x05, 0x00, 0x00, 0xa7, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x43, 0x05, 0x00, 0x00, 0x41, 0x05, 0x00, 0x00, 0x42, 0x05, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x45, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x43, 0x05, 0x00, 0x00, 0x44, 0x05, 0x00, 0x00, 0x45, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x44, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x46, 0x05, 0x00, 0x00, + 0x0f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x47, 0x05, 0x00, 0x00, + 0x11, 0x05, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, + 0x46, 0x05, 0x00, 0x00, 0x47, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x45, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x45, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x49, 0x05, 0x00, 0x00, 0x43, 0x05, 0x00, 0x00, 0x39, 0x05, 0x00, 0x00, 0x48, 0x05, 0x00, 0x00, + 0x44, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3e, 0x05, 0x00, 0x00, 0x49, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x4b, 0x05, 0x00, 0x00, 0x3e, 0x05, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x4e, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x4b, 0x05, 0x00, 0x00, 0x4d, 0x05, 0x00, 0x00, 0x56, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x4d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, 0x50, 0x05, 0x00, 0x00, + 0x4f, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x52, 0x05, 0x00, 0x00, + 0x1b, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x51, 0x05, 0x00, 0x00, 0x52, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x51, 0x05, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x54, 0x05, 0x00, 0x00, + 0x50, 0x05, 0x00, 0x00, 0x53, 0x05, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x55, 0x05, 0x00, 0x00, 0x54, 0x05, 0x00, 0x00, 0x54, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4c, 0x05, 0x00, 0x00, + 0x55, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x4e, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x56, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, + 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, + 0x57, 0x05, 0x00, 0x00, 0x57, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x4c, 0x05, 0x00, 0x00, 0x58, 0x05, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x4e, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4e, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x4c, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x59, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x5b, 0x05, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x5c, 0x05, 0x00, 0x00, 0xc8, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x5d, 0x05, 0x00, 0x00, 0xcb, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x5b, 0x05, 0x00, 0x00, 0x5c, 0x05, 0x00, 0x00, 0x5d, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x5a, 0x05, 0x00, 0x00, 0x5e, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x60, 0x05, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x61, 0x05, 0x00, 0x00, 0x60, 0x05, 0x00, 0x00, 0x60, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x62, 0x05, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x63, 0x05, 0x00, 0x00, 0x61, 0x05, 0x00, 0x00, 0x62, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x64, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x63, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x65, 0x05, 0x00, 0x00, 0x64, 0x05, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x65, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x66, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, 0x4a, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x69, 0x05, 0x00, 0x00, 0x5a, 0x05, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x6a, 0x05, 0x00, 0x00, 0x68, 0x05, 0x00, 0x00, + 0x69, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x6a, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6c, 0x05, 0x00, 0x00, 0x6b, 0x05, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6d, 0x05, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x6c, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x67, 0x05, 0x00, 0x00, 0x6d, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x6f, 0x05, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x70, 0x05, 0x00, 0x00, 0x1b, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x71, 0x05, 0x00, 0x00, 0x6f, 0x05, 0x00, 0x00, 0x70, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x72, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, 0x72, 0x05, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x74, 0x05, 0x00, 0x00, 0x73, 0x05, 0x00, 0x00, + 0x73, 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x71, 0x05, 0x00, 0x00, 0x74, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x75, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x79, 0x05, 0x00, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7a, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0x79, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7b, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x77, 0x05, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, + 0x7a, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7d, 0x05, 0x00, 0x00, + 0x7b, 0x05, 0x00, 0x00, 0x7c, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x76, 0x05, 0x00, 0x00, + 0x7d, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, 0x00, 0x7f, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x81, 0x05, 0x00, 0x00, 0x80, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x84, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x81, 0x05, 0x00, 0x00, 0x83, 0x05, 0x00, 0x00, 0x8b, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x83, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x86, 0x05, 0x00, 0x00, + 0xd0, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x87, 0x05, 0x00, 0x00, + 0xce, 0x04, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x88, 0x05, 0x00, 0x00, + 0x86, 0x05, 0x00, 0x00, 0x87, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x89, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00, + 0x6d, 0x03, 0x00, 0x00, 0x88, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8a, 0x05, 0x00, 0x00, 0x89, 0x05, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x82, 0x05, 0x00, 0x00, 0x8a, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x84, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x8b, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x82, 0x05, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x84, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x84, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x05, 0x00, 0x00, + 0x82, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x8c, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x8e, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x5f, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8f, 0x05, 0x00, 0x00, 0x8e, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x90, 0x05, 0x00, 0x00, 0x8f, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, 0x0f, 0x05, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, 0x91, 0x05, 0x00, 0x00, 0xa7, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, 0x90, 0x05, 0x00, 0x00, 0x92, 0x05, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x96, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x93, 0x05, 0x00, 0x00, 0x95, 0x05, 0x00, 0x00, 0x9e, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x95, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x99, 0x05, 0x00, 0x00, + 0x6e, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9a, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x97, 0x05, 0x00, 0x00, 0x98, 0x05, 0x00, 0x00, + 0x99, 0x05, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x9a, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9d, 0x05, 0x00, 0x00, 0x9b, 0x05, 0x00, 0x00, 0x9c, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x94, 0x05, 0x00, 0x00, 0x9d, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x96, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x9e, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x94, 0x05, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x96, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x96, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9f, 0x05, 0x00, 0x00, + 0x94, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x8d, 0x05, 0x00, 0x00, 0x9f, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa3, 0x05, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa2, 0x05, 0x00, 0x00, 0xa3, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x99, 0x00, 0x00, 0x00, 0xa5, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xa1, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xa6, 0x05, 0x00, 0x00, 0xa5, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa4, 0x05, 0x00, 0x00, 0xa6, 0x05, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xa7, 0x05, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0xa2, 0x05, 0x00, 0x00, + 0xa4, 0x05, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xa8, 0x05, 0x00, 0x00, + 0xa7, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xa8, 0x05, 0x00, 0x00, 0xa9, 0x05, 0x00, 0x00, 0xaa, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xa9, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xad, 0x05, 0x00, 0x00, 0x55, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xac, 0x05, 0x00, 0x00, + 0xad, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0xaf, 0x05, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xab, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xb0, 0x05, 0x00, 0x00, 0xaf, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xae, 0x05, 0x00, 0x00, + 0xb0, 0x05, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xb1, 0x05, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0xac, 0x05, 0x00, 0x00, 0xae, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xaa, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xaa, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xb2, 0x05, 0x00, 0x00, 0xa7, 0x05, 0x00, 0x00, 0x96, 0x05, 0x00, 0x00, + 0xb1, 0x05, 0x00, 0x00, 0xa9, 0x05, 0x00, 0x00, 0xa9, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb3, 0x05, 0x00, 0x00, 0xb2, 0x05, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa0, 0x05, 0x00, 0x00, 0xb3, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb5, 0x05, 0x00, 0x00, 0x5f, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb7, 0x05, 0x00, 0x00, 0xb5, 0x05, 0x00, 0x00, 0xb6, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x05, 0x00, 0x00, 0x67, 0x05, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb9, 0x05, 0x00, 0x00, 0xb8, 0x05, 0x00, 0x00, + 0x9e, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xba, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xb7, 0x05, 0x00, 0x00, 0xb9, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xbb, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xbc, 0x05, 0x00, 0x00, 0xbb, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xbd, 0x05, 0x00, 0x00, 0xba, 0x05, 0x00, 0x00, 0xbc, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbe, 0x05, 0x00, 0x00, 0x76, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbf, 0x05, 0x00, 0x00, 0x7e, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, 0x8d, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc1, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xbf, 0x05, 0x00, 0x00, 0xc0, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc2, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbe, 0x05, 0x00, 0x00, + 0xc1, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbd, 0x05, 0x00, 0x00, 0xc2, 0x05, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xc3, 0x05, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x05, 0x00, 0x00, 0xc4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc5, 0x05, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc6, 0x05, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc7, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0xc5, 0x05, 0x00, 0x00, 0xc6, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0xc7, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xc8, 0x05, 0x00, 0x00, + 0x3e, 0x05, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xc9, 0x05, 0x00, 0x00, + 0xc8, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xcb, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xc9, 0x05, 0x00, 0x00, 0xca, 0x05, 0x00, 0x00, 0xcb, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xca, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xcb, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xcb, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xcd, 0x05, 0x00, 0x00, + 0x3e, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xd0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xcd, 0x05, 0x00, 0x00, 0xcf, 0x05, 0x00, 0x00, 0xd6, 0x05, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xcf, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xd1, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd2, 0x05, 0x00, 0x00, 0xd1, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd3, 0x05, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0xd3, 0x05, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd5, 0x05, 0x00, 0x00, + 0xd2, 0x05, 0x00, 0x00, 0xd4, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xce, 0x05, 0x00, 0x00, + 0xd5, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xd0, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xd6, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xce, 0x05, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xd0, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xd0, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd7, 0x05, 0x00, 0x00, 0xce, 0x05, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xcc, 0x05, 0x00, 0x00, 0xd7, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xd9, 0x05, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xda, 0x05, 0x00, 0x00, 0xd9, 0x05, 0x00, 0x00, 0xd9, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xdb, 0x05, 0x00, 0x00, 0x5a, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xdd, 0x05, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, + 0xdc, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xde, 0x05, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0xda, 0x05, 0x00, 0x00, 0xdb, 0x05, 0x00, 0x00, + 0xdd, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x05, 0x00, 0x00, 0xde, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xdf, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x5d, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe0, 0x05, 0x00, 0x00, 0xdf, 0x05, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xe1, 0x05, 0x00, 0x00, 0xe0, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xe3, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xe1, 0x05, 0x00, 0x00, + 0xe2, 0x05, 0x00, 0x00, 0xe3, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe2, 0x05, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xe4, 0x05, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x5d, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe5, 0x05, 0x00, 0x00, 0xe4, 0x05, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xe6, 0x05, 0x00, 0x00, 0xe5, 0x05, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xe3, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe3, 0x05, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xe7, 0x05, 0x00, 0x00, 0xe1, 0x05, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x00, + 0xe6, 0x05, 0x00, 0x00, 0xe2, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xe9, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xe7, 0x05, 0x00, 0x00, 0xe8, 0x05, 0x00, 0x00, + 0xff, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe8, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xeb, 0x05, 0x00, 0x00, 0x6e, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xec, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0xeb, 0x05, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, + 0xec, 0x05, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xef, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xee, 0x05, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xea, 0x05, 0x00, 0x00, + 0xef, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf1, 0x05, 0x00, 0x00, + 0x6e, 0x05, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf2, 0x05, 0x00, 0x00, + 0xed, 0x05, 0x00, 0x00, 0xed, 0x05, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf3, 0x05, 0x00, 0x00, 0xf1, 0x05, 0x00, 0x00, 0xf2, 0x05, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf5, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xf3, 0x05, 0x00, 0x00, 0xf4, 0x05, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf0, 0x05, 0x00, 0x00, 0xf5, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf6, 0x05, 0x00, 0x00, 0xf0, 0x05, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf7, 0x05, 0x00, 0x00, 0xf6, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf8, 0x05, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf9, 0x05, 0x00, 0x00, 0xf7, 0x05, 0x00, 0x00, + 0xf8, 0x05, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, + 0xf9, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfb, 0x05, 0x00, 0x00, 0xf9, 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xfc, 0x05, 0x00, 0x00, 0xfa, 0x05, 0x00, 0x00, 0xfb, 0x05, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, + 0xea, 0x05, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0xfe, 0x05, 0x00, 0x00, + 0xfc, 0x05, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x05, 0x00, 0x00, + 0xfe, 0x05, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0xe9, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xff, 0x05, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x02, 0x06, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 0x85, 0x02, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x02, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x04, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x03, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x05, 0x06, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x05, 0x06, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x07, 0x06, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0xb6, 0x05, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x04, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x04, 0x06, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x02, 0x06, 0x00, 0x00, + 0xff, 0x05, 0x00, 0x00, 0x07, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x0a, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x08, 0x06, 0x00, 0x00, + 0x09, 0x06, 0x00, 0x00, 0x0f, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x09, 0x06, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x06, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0c, 0x06, 0x00, 0x00, 0xb4, 0x05, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0d, 0x06, 0x00, 0x00, 0x0c, 0x06, 0x00, 0x00, + 0x9c, 0x05, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x0e, 0x06, 0x00, 0x00, + 0x0b, 0x06, 0x00, 0x00, 0x0d, 0x06, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xd8, 0x05, 0x00, 0x00, 0x0e, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x0a, 0x06, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x0f, 0x06, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x10, 0x06, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5d, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x06, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, + 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x12, 0x06, 0x00, 0x00, 0x11, 0x06, 0x00, 0x00, + 0xb6, 0x05, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x14, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x12, 0x06, 0x00, 0x00, 0x13, 0x06, 0x00, 0x00, 0x14, 0x06, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x13, 0x06, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x15, 0x06, 0x00, 0x00, 0xcc, 0x05, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x16, 0x06, 0x00, 0x00, 0x15, 0x06, 0x00, 0x00, 0x15, 0x06, 0x00, 0x00, 0x15, 0x06, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x16, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x14, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x14, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x0a, 0x06, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x0a, 0x06, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xe9, 0x05, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xe9, 0x05, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x19, 0x06, 0x00, 0x00, 0xd8, 0x05, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x1a, 0x06, 0x00, 0x00, 0x6a, 0x04, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x06, 0x00, 0x00, 0x1a, 0x06, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0x19, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x06, 0x00, 0x00, + 0x19, 0x06, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x1e, 0x06, 0x00, 0x00, 0x19, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x07, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x1f, 0x06, 0x00, 0x00, 0x1c, 0x06, 0x00, 0x00, 0x1d, 0x06, 0x00, 0x00, + 0x1e, 0x06, 0x00, 0x00, 0x1b, 0x06, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x18, 0x06, 0x00, 0x00, + 0x1f, 0x06, 0x00, 0x00, 0xfd, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x67, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x6a, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00, + 0x6c, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x73, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x71, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x75, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x12, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x7b, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x86, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00, + 0x87, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x8b, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x89, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x00, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x8a, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x90, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x00, 0x00, + 0x8f, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, + 0x90, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x8b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x8b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x93, 0x00, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x95, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x1a, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x9b, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x9c, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x9e, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x9e, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0x9c, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, + 0xa3, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, + 0xa4, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa7, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa8, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xa9, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0xa1, 0x00, 0x00, 0x00, 0xa9, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0xac, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xab, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, + 0xac, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, + 0xad, 0x00, 0x00, 0x00, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb0, 0x00, 0x00, 0x00, 0xaf, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xb1, 0x00, 0x00, 0x00, 0xae, 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xb2, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xb2, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xb9, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xba, 0x00, 0x00, 0x00, 0xb5, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0xba, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x99, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, + 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0xbc, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xc5, 0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xc6, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc7, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0xc7, 0x00, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xc9, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, + 0xc8, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xc9, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x21, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xd8, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xcd, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0xcd, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0xce, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xcc, 0x00, 0x00, 0x00, 0xcf, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd0, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd1, 0x00, 0x00, 0x00, + 0x88, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xd2, 0x00, 0x00, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xd3, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xd6, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0xda, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xda, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, 0xdb, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xdd, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xde, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, + 0xdc, 0x00, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0xe0, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xd9, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, 0xe1, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xe3, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe5, 0x00, 0x00, 0x00, 0xe2, 0x00, 0x00, 0x00, 0xe4, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x00, + 0xe5, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd8, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xcc, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0xe8, 0x00, 0x00, 0x00, + 0xe8, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, + 0xe7, 0x00, 0x00, 0x00, 0xe9, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xec, 0x00, 0x00, 0x00, 0xea, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xed, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xee, 0x00, 0x00, 0x00, 0xec, 0x00, 0x00, 0x00, + 0xed, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xee, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x26, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x37, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3c, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, + 0x23, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, + 0xf2, 0x00, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xf4, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0xf4, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xf6, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xf8, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xf9, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, + 0xf1, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xfd, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xff, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xfd, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, + 0x06, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x09, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x0b, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0e, 0x01, 0x00, 0x00, 0x0d, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0f, 0x01, 0x00, 0x00, 0x0c, 0x01, 0x00, 0x00, 0x0e, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, + 0x10, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x12, 0x01, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x15, 0x01, 0x00, 0x00, 0x14, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x15, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x17, 0x01, 0x00, 0x00, 0x16, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x18, 0x01, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x17, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x18, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfa, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x01, 0x00, 0x00, 0x1c, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x1b, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x1d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x1f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0x21, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x24, 0x01, 0x00, 0x00, 0x23, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x25, 0x01, 0x00, 0x00, 0x22, 0x01, 0x00, 0x00, 0x24, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x25, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x28, 0x01, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x29, 0x01, 0x00, 0x00, 0x26, 0x01, 0x00, 0x00, 0x28, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x2a, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0xf1, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x2f, 0x01, 0x00, 0x00, 0x2e, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x30, 0x01, 0x00, 0x00, 0x2d, 0x01, 0x00, 0x00, 0x2f, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x2b, 0x01, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x33, 0x01, 0x00, 0x00, 0x32, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x34, 0x01, 0x00, 0x00, 0x31, 0x01, 0x00, 0x00, 0x33, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x35, 0x01, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, 0x29, 0x01, 0x00, 0x00, 0x34, 0x01, 0x00, 0x00, + 0x35, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x01, 0x00, 0x00, 0x36, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0x39, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x38, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, + 0x39, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3b, 0x01, 0x00, 0x00, + 0x3a, 0x01, 0x00, 0x00, 0x3a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x1e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3c, 0x01, 0x00, 0x00, 0x3d, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x3c, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3f, 0x01, 0x00, 0x00, + 0x3b, 0x01, 0x00, 0x00, 0x3e, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x37, 0x01, 0x00, 0x00, + 0x3f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, + 0x37, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x44, 0x01, 0x00, 0x00, 0x43, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x45, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x44, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x46, 0x01, 0x00, 0x00, 0x41, 0x01, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x40, 0x01, 0x00, 0x00, 0x46, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x47, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x48, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, 0x49, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, 0x47, 0x01, 0x00, 0x00, 0x4a, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, 0x4b, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x4d, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, 0x4e, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xbd, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, + 0x4e, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, + 0x4f, 0x01, 0x00, 0x00, 0x4f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x51, 0x01, 0x00, 0x00, + 0x4d, 0x01, 0x00, 0x00, 0x50, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x52, 0x01, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0x53, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x53, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, 0x52, 0x01, 0x00, 0x00, 0x55, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x56, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x58, 0x01, 0x00, 0x00, 0x4c, 0x01, 0x00, 0x00, + 0x51, 0x01, 0x00, 0x00, 0x57, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x01, 0x00, 0x00, + 0x58, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x5a, 0x01, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x59, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x5a, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x5d, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x5f, 0x01, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, + 0x5f, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x61, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x63, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, + 0x62, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x5e, 0x01, 0x00, 0x00, 0x63, 0x01, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x64, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x67, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, + 0x67, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6a, 0x01, 0x00, 0x00, 0x69, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x6b, 0x01, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x6a, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x6b, 0x01, 0x00, 0x00, + 0x6c, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6c, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x6e, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, + 0x6e, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x71, 0x01, 0x00, 0x00, 0x70, 0x01, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x72, 0x01, 0x00, 0x00, 0x6f, 0x01, 0x00, 0x00, 0x71, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x6d, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x6d, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x6b, 0x01, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x72, 0x01, 0x00, 0x00, 0x6c, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x75, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x73, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, + 0x75, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x74, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, 0x77, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x7a, 0x01, 0x00, 0x00, 0x76, 0x01, 0x00, 0x00, 0x78, 0x01, 0x00, 0x00, 0x9b, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x7b, 0x01, 0x00, 0x00, 0x7a, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x75, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x75, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x73, 0x01, 0x00, 0x00, 0x6d, 0x01, 0x00, 0x00, + 0x7b, 0x01, 0x00, 0x00, 0x74, 0x01, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x7e, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, + 0x7e, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7d, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x79, 0x01, 0x00, 0x00, + 0x82, 0x01, 0x00, 0x00, 0x7f, 0x01, 0x00, 0x00, 0x81, 0x01, 0x00, 0x00, 0x9b, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x83, 0x01, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x7e, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7e, 0x01, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x84, 0x01, 0x00, 0x00, 0x7c, 0x01, 0x00, 0x00, 0x75, 0x01, 0x00, 0x00, + 0x83, 0x01, 0x00, 0x00, 0x7d, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x84, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x36, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x87, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x88, 0x01, 0x00, 0x00, + 0x87, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, + 0x88, 0x01, 0x00, 0x00, 0x89, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x8b, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x8a, 0x01, 0x00, 0x00, + 0x8d, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x00, 0x8b, 0x01, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x8c, 0x01, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x8d, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x91, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x93, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x95, 0x01, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x95, 0x01, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x99, 0x01, 0x00, 0x00, 0x98, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x9a, 0x01, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x99, 0x01, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, 0x94, 0x01, 0x00, 0x00, 0x9a, 0x01, 0x00, 0x00, + 0x51, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x9b, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xa3, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x91, 0x01, 0x00, 0x00, + 0xa0, 0x01, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xa1, 0x01, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xa2, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xa1, 0x01, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa3, 0x01, 0x00, 0x00, + 0xa2, 0x01, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0xa3, 0x01, 0x00, 0x00, 0x57, 0x00, 0x05, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xa5, 0x01, 0x00, 0x00, 0xa0, 0x01, 0x00, 0x00, 0xa4, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x08, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa6, 0x01, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa5, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0xa6, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x43, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xaf, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xab, 0x01, 0x00, 0x00, 0xaa, 0x01, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xad, 0x01, 0x00, 0x00, + 0xab, 0x01, 0x00, 0x00, 0xac, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa9, 0x01, 0x00, 0x00, + 0xad, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xaf, 0x01, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0xaf, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xae, 0x01, 0x00, 0x00, 0xb1, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, 0xb2, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xb4, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, + 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, + 0xb4, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, 0xb6, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, 0xb5, 0x01, 0x00, 0x00, 0xb7, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0xb3, 0x01, 0x00, 0x00, + 0xb8, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0xba, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, 0xb9, 0x01, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, 0xbd, 0x01, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xbf, 0x01, 0x00, 0x00, 0xa9, 0x01, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, + 0xbf, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, 0xc1, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc3, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xc2, 0x01, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0xbe, 0x01, 0x00, 0x00, + 0xc3, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, 0xc5, 0x01, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc7, 0x01, 0x00, 0x00, 0xc4, 0x01, 0x00, 0x00, 0xc6, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xae, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0xc9, 0x01, 0x00, 0x00, 0xbc, 0x01, 0x00, 0x00, + 0xc7, 0x01, 0x00, 0x00, 0xc8, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xc9, 0x01, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x46, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, 0xcd, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0xce, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xcc, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xd3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xd1, 0x01, 0x00, 0x00, 0xd2, 0x01, 0x00, 0x00, 0xd3, 0x01, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xd2, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xd6, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xd3, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd7, 0x01, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xd8, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xd9, 0x01, 0x00, 0x00, 0xd8, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xda, 0x01, 0x00, 0x00, 0xd7, 0x01, 0x00, 0x00, + 0xd9, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdc, 0x01, 0x00, 0x00, 0xdb, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xdd, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0xdd, 0x01, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, + 0xde, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0xda, 0x01, 0x00, 0x00, 0xdf, 0x01, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x99, 0x00, 0x00, 0x00, + 0xe1, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0xe1, 0x01, 0x00, 0x00, 0x4f, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, 0xe2, 0x01, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xe4, 0x01, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, 0xe4, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe3, 0x01, 0x00, 0x00, 0xe5, 0x01, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, + 0xe6, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd5, 0x01, 0x00, 0x00, 0xe7, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xe8, 0x01, 0x00, 0x00, 0xd5, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, 0xcc, 0x01, 0x00, 0x00, + 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0xe9, 0x01, 0x00, 0x00, + 0xe9, 0x01, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, + 0xe8, 0x01, 0x00, 0x00, 0xea, 0x01, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xec, 0x01, 0x00, 0x00, 0xeb, 0x01, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xed, 0x01, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xee, 0x01, 0x00, 0x00, 0xec, 0x01, 0x00, 0x00, + 0xed, 0x01, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xee, 0x01, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x47, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x4b, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xf6, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xfa, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x0e, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0f, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x14, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x16, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x1b, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1e, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1f, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x24, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x26, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x2b, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0xf3, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x01, 0x00, 0x00, 0xf5, 0x01, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0xf7, 0x01, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, + 0xf7, 0x01, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xf9, 0x01, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf6, 0x01, 0x00, 0x00, + 0xf9, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfb, 0x01, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, + 0xf1, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, + 0xfb, 0x01, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xfe, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0xfd, 0x01, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xfa, 0x01, 0x00, 0x00, + 0xfe, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, + 0xf1, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x03, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xff, 0x01, 0x00, 0x00, + 0x03, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x05, 0x02, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, + 0xf6, 0x01, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, + 0x05, 0x02, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x07, 0x02, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, + 0x08, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0a, 0x02, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, + 0xf6, 0x01, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, + 0x0a, 0x02, 0x00, 0x00, 0x0b, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0d, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x0c, 0x02, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x09, 0x02, 0x00, 0x00, + 0x0d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0f, 0x02, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x0f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x12, 0x02, 0x00, 0x00, 0x13, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x14, 0x02, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x15, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x12, 0x02, 0x00, 0x00, + 0x14, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, + 0xfa, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x16, 0x02, 0x00, 0x00, 0x17, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x16, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0xfa, 0x01, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x19, 0x02, 0x00, 0x00, 0x1a, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1b, 0x02, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x19, 0x02, 0x00, 0x00, + 0x1b, 0x02, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x1d, 0x02, 0x00, 0x00, + 0x15, 0x02, 0x00, 0x00, 0x1c, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0e, 0x02, 0x00, 0x00, + 0x1d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, + 0x09, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1f, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x1f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, + 0x09, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x22, 0x02, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x24, 0x02, 0x00, 0x00, 0x21, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x25, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x22, 0x02, 0x00, 0x00, + 0x24, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, + 0x04, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x26, 0x02, 0x00, 0x00, 0x27, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, + 0x26, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, + 0x04, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x29, 0x02, 0x00, 0x00, 0x2a, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2b, 0x02, 0x00, 0x00, 0x28, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x2c, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, + 0x2b, 0x02, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x2d, 0x02, 0x00, 0x00, + 0x25, 0x02, 0x00, 0x00, 0x2c, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x02, 0x00, 0x00, + 0x2d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x2f, 0x02, 0x00, 0x00, + 0x0e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, + 0x1e, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x2f, 0x02, 0x00, 0x00, 0x30, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x31, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, 0x2e, 0x02, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x33, 0x02, 0x00, 0x00, 0x34, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x32, 0x02, 0x00, 0x00, 0x35, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x32, 0x02, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x39, 0x02, 0x00, 0x00, 0x37, 0x02, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0x39, 0x02, 0x00, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x41, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x3b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, + 0x2e, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, + 0x32, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x3e, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x3f, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x3c, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x41, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x3a, 0x02, 0x00, 0x00, 0x42, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x3c, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x3c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x43, 0x02, 0x00, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x43, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x44, 0x02, 0x00, 0x00, + 0x36, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x45, 0x02, 0x00, 0x00, + 0x49, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, + 0x45, 0x02, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, + 0x44, 0x02, 0x00, 0x00, 0x46, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x48, 0x02, 0x00, 0x00, 0x47, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x4a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x48, 0x02, 0x00, 0x00, + 0x49, 0x02, 0x00, 0x00, 0x4a, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x49, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0x4b, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x36, 0x02, 0x00, 0x00, 0x4c, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x4a, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4a, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x4d, 0x02, 0x00, 0x00, 0x36, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, + 0x4d, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4e, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x55, 0x02, 0x00, 0x00, 0x50, 0x02, 0x00, 0x00, + 0x54, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x55, 0x02, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, + 0x36, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x4f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x53, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x56, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x59, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x6d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x6e, 0x02, 0x00, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x77, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x7a, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x7e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x8d, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x92, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x93, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x9b, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x9c, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x9e, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, + 0xa1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xa5, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xae, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xb9, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xc1, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x00, + 0xc9, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xda, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, + 0xdd, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x59, 0x02, 0x00, 0x00, + 0x58, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x5a, 0x02, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x59, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x5b, 0x02, 0x00, 0x00, 0x5a, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x5c, 0x02, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x5e, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x5c, 0x02, 0x00, 0x00, 0x5d, 0x02, 0x00, 0x00, + 0x5e, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5d, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0x60, 0x02, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x02, 0x00, 0x00, + 0x60, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, + 0x61, 0x02, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x5e, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x5e, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x63, 0x02, 0x00, 0x00, 0x5b, 0x02, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x62, 0x02, 0x00, 0x00, + 0x5d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, 0x02, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, + 0x64, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x66, 0x02, 0x00, 0x00, 0x63, 0x02, 0x00, 0x00, 0x65, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x68, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x66, 0x02, 0x00, 0x00, + 0x67, 0x02, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x67, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x69, 0x02, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0x69, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x68, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x6c, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6d, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x6f, 0x02, 0x00, 0x00, + 0xf2, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x70, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x70, 0x02, 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, 0x72, 0x02, 0x00, 0x00, 0x73, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x74, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x74, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x75, 0x02, 0x00, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0xb1, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x76, 0x02, 0x00, 0x00, + 0x75, 0x02, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x76, 0x02, 0x00, 0x00, + 0x71, 0x02, 0x00, 0x00, 0x72, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x71, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, + 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, 0x78, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x77, 0x02, 0x00, 0x00, 0x79, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x7b, 0x02, 0x00, 0x00, 0x7c, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x7d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x7f, 0x02, 0x00, 0x00, 0x7a, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, + 0x7f, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x81, 0x02, 0x00, 0x00, + 0x7a, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x81, 0x02, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x82, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, 0x77, 0x02, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x84, 0x02, 0x00, 0x00, + 0x85, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x86, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x88, 0x02, 0x00, 0x00, 0x83, 0x02, 0x00, 0x00, 0x87, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x99, 0x00, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, 0x89, 0x02, 0x00, 0x00, + 0x4f, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, 0x8a, 0x02, 0x00, 0x00, + 0x8a, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x88, 0x02, 0x00, 0x00, 0x8b, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x8c, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, 0x7e, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, 0x8e, 0x02, 0x00, 0x00, 0x8f, 0x02, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0x90, 0x02, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x91, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x93, 0x02, 0x00, 0x00, 0x94, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x95, 0x02, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x93, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x92, 0x02, 0x00, 0x00, 0x95, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x96, 0x02, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x97, 0x02, 0x00, 0x00, 0x96, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x99, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x97, 0x02, 0x00, 0x00, + 0x98, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x98, 0x02, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x73, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x99, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0x8d, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9c, 0x02, 0x00, 0x00, 0x9d, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, 0x92, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x9e, 0x02, 0x00, 0x00, 0x9f, 0x02, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xa0, 0x02, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x9c, 0x02, 0x00, 0x00, 0x9e, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x9b, 0x02, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, 0x9b, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0xa2, 0x02, 0x00, 0x00, 0xa3, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xa1, 0x02, 0x00, 0x00, 0xa4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xa6, 0x02, 0x00, 0x00, 0xa1, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0xa6, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xa5, 0x02, 0x00, 0x00, 0xa7, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, 0xa5, 0x02, 0x00, 0x00, + 0xbc, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xaa, 0x02, 0x00, 0x00, 0xa8, 0x02, 0x00, 0x00, + 0xa9, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xac, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xaa, 0x02, 0x00, 0x00, 0xab, 0x02, 0x00, 0x00, 0xac, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xab, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x73, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xac, 0x02, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xb0, 0x02, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb1, 0x02, 0x00, 0x00, 0xb0, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xb2, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0xb1, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb4, 0x02, 0x00, 0x00, + 0xb2, 0x02, 0x00, 0x00, 0xb3, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xb5, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xaf, 0x02, 0x00, 0x00, + 0xb4, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb6, 0x02, 0x00, 0x00, + 0xa5, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xb5, 0x02, 0x00, 0x00, + 0xb6, 0x02, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xb8, 0x02, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xae, 0x02, 0x00, 0x00, + 0xb8, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, + 0xa1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, + 0xa5, 0x02, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, + 0xbc, 0x02, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, 0xbc, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xbe, 0x02, 0x00, 0x00, 0xbb, 0x02, 0x00, 0x00, 0xbd, 0x02, 0x00, 0x00, + 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0xba, 0x02, 0x00, 0x00, + 0xbe, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xbf, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xb9, 0x02, 0x00, 0x00, 0xc0, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc2, 0x02, 0x00, 0x00, 0xae, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, 0xb9, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xc6, 0x02, 0x00, 0x00, 0xc4, 0x02, 0x00, 0x00, 0xc5, 0x02, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0xc3, 0x02, 0x00, 0x00, + 0xc6, 0x02, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x02, 0x00, 0x00, + 0xc2, 0x02, 0x00, 0x00, 0xc7, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc1, 0x02, 0x00, 0x00, + 0xc8, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, + 0x8d, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xc9, 0x02, 0x00, 0x00, 0xca, 0x02, 0x00, 0x00, + 0x39, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0xcb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, + 0xc9, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, + 0xc1, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, + 0xcb, 0x02, 0x00, 0x00, 0xcc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xce, 0x02, 0x00, 0x00, 0x6b, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xcf, 0x02, 0x00, 0x00, 0xce, 0x02, 0x00, 0x00, 0xcd, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0xcf, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd0, 0x02, 0x00, 0x00, 0xc1, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd1, 0x02, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd2, 0x02, 0x00, 0x00, 0xd1, 0x02, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6d, 0x02, 0x00, 0x00, 0xd2, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x73, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x73, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xd3, 0x02, 0x00, 0x00, 0x6f, 0x02, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x80, 0x00, 0x00, 0x00, + 0xd4, 0x02, 0x00, 0x00, 0xd3, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x6f, 0x02, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x70, 0x02, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x72, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd5, 0x02, 0x00, 0x00, 0x6d, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd6, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xd5, 0x02, 0x00, 0x00, + 0xa9, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, + 0xd6, 0x02, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, 0xd6, 0x02, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0xd7, 0x02, 0x00, 0x00, 0xd8, 0x02, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x6b, 0x02, 0x00, 0x00, 0xd9, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xdd, 0x02, 0x00, 0x00, 0xde, 0x02, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xdf, 0x02, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x00, 0xdd, 0x02, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0xdf, 0x02, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe1, 0x02, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0xe0, 0x02, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0xdc, 0x02, 0x00, 0x00, + 0xe1, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe3, 0x02, 0x00, 0x00, + 0xdb, 0x02, 0x00, 0x00, 0xe2, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xda, 0x02, 0x00, 0x00, + 0xe3, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xe4, 0x02, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, + 0x6b, 0x02, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, 0xe6, 0x02, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, 0xe5, 0x02, 0x00, 0x00, 0xe7, 0x02, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe9, 0x02, 0x00, 0x00, 0xda, 0x02, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0xe8, 0x02, 0x00, 0x00, + 0xe9, 0x02, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0xeb, 0x02, 0x00, 0x00, + 0xe4, 0x02, 0x00, 0x00, 0xea, 0x02, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xeb, 0x02, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x57, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x5b, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x6e, 0x02, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x88, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x93, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x99, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xee, 0x02, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xef, 0x02, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0xee, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0xef, 0x02, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xf1, 0x02, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xf3, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xf1, 0x02, 0x00, 0x00, + 0xf2, 0x02, 0x00, 0x00, 0xf3, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xf2, 0x02, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0x5f, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf5, 0x02, 0x00, 0x00, 0xf4, 0x02, 0x00, 0x00, 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xf6, 0x02, 0x00, 0x00, 0xf5, 0x02, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xf3, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xf3, 0x02, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf0, 0x02, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, + 0xf6, 0x02, 0x00, 0x00, 0xf2, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xf8, 0x02, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xf9, 0x02, 0x00, 0x00, 0xf8, 0x02, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xfa, 0x02, 0x00, 0x00, 0xf7, 0x02, 0x00, 0x00, 0xf9, 0x02, 0x00, 0x00, + 0xf7, 0x00, 0x03, 0x00, 0xfc, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, + 0xfa, 0x02, 0x00, 0x00, 0xfb, 0x02, 0x00, 0x00, 0xfc, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xfb, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00, + 0x57, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0xfd, 0x02, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0xfc, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x5a, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xff, 0x02, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x03, 0x03, 0x00, 0x00, 0xff, 0x02, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x04, 0x03, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x05, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, + 0x04, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x05, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x08, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, 0x09, 0x03, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0a, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x0b, 0x03, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x07, 0x03, 0x00, 0x00, 0x0d, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x0f, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xc3, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x0f, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x03, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, + 0x88, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, + 0x11, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x12, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x5b, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x16, 0x03, 0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, + 0x16, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x17, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x1a, 0x03, 0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x19, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x1a, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x1c, 0x03, 0x00, 0x00, 0x15, 0x03, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, 0x14, 0x03, 0x00, 0x00, + 0x1c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x13, 0x03, 0x00, 0x00, 0x1d, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x1f, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x20, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, + 0xf2, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x22, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x22, 0x03, 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, 0x24, 0x03, 0x00, 0x00, 0x25, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x26, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x26, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x27, 0x03, 0x00, 0x00, + 0x21, 0x03, 0x00, 0x00, 0xb1, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x29, 0x03, 0x00, 0x00, + 0x27, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x29, 0x03, 0x00, 0x00, + 0x23, 0x03, 0x00, 0x00, 0x24, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x23, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x2a, 0x03, 0x00, 0x00, 0x21, 0x03, 0x00, 0x00, + 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, 0x2a, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2c, 0x03, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, + 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x2b, 0x03, 0x00, 0x00, + 0x2c, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x2f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x2d, 0x03, 0x00, 0x00, 0x2e, 0x03, 0x00, 0x00, 0x2f, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x2e, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x24, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x2f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x31, 0x03, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x32, 0x03, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x33, 0x03, 0x00, 0x00, 0x31, 0x03, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x34, 0x03, 0x00, 0x00, 0x33, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x13, 0x03, 0x00, 0x00, 0x35, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, 0x36, 0x03, 0x00, 0x00, + 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x39, 0x03, 0x00, 0x00, 0x37, 0x03, 0x00, 0x00, + 0x38, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x39, 0x03, 0x00, 0x00, 0x3a, 0x03, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x3a, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x24, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x3f, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3e, 0x03, 0x00, 0x00, + 0x3f, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x40, 0x03, 0x00, 0x00, + 0x45, 0x00, 0x00, 0x00, 0x3e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x40, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, + 0x3d, 0x03, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x42, 0x03, 0x00, 0x00, 0x41, 0x03, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x43, 0x03, 0x00, 0x00, 0x42, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x44, 0x03, 0x00, 0x00, 0x43, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x46, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x44, 0x03, 0x00, 0x00, + 0x45, 0x03, 0x00, 0x00, 0x46, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x45, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x47, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, + 0x47, 0x03, 0x00, 0x00, 0xbc, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, + 0x48, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x46, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x46, 0x03, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x4a, 0x03, 0x00, 0x00, 0x43, 0x03, 0x00, 0x00, 0x3b, 0x03, 0x00, 0x00, 0x49, 0x03, 0x00, 0x00, + 0x45, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x4b, 0x03, 0x00, 0x00, + 0x4a, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x4b, 0x03, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x4c, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x4e, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x4e, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0x4d, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x4d, 0x03, 0x00, 0x00, + 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x4a, 0x03, 0x00, 0x00, + 0x46, 0x03, 0x00, 0x00, 0x50, 0x03, 0x00, 0x00, 0x4c, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x52, 0x03, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x54, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x52, 0x03, 0x00, 0x00, + 0x53, 0x03, 0x00, 0x00, 0x54, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x53, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x55, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x56, 0x03, 0x00, 0x00, + 0x55, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, + 0x56, 0x03, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x54, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x54, 0x03, 0x00, 0x00, 0xf5, 0x00, 0x07, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x58, 0x03, 0x00, 0x00, 0x51, 0x03, 0x00, 0x00, 0x4d, 0x03, 0x00, 0x00, 0x57, 0x03, 0x00, 0x00, + 0x53, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0x58, 0x03, 0x00, 0x00, 0x59, 0x03, 0x00, 0x00, 0x5a, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x59, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x24, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0x5a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x5e, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5d, 0x03, 0x00, 0x00, + 0x5e, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5f, 0x03, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x5d, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x5c, 0x03, 0x00, 0x00, + 0x5f, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x60, 0x03, 0x00, 0x00, + 0x5c, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x61, 0x03, 0x00, 0x00, + 0x60, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x63, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x61, 0x03, 0x00, 0x00, 0x62, 0x03, 0x00, 0x00, + 0x63, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x62, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0x25, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x63, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x67, 0x03, 0x00, 0x00, 0x5c, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x66, 0x03, 0x00, 0x00, 0x67, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x68, 0x03, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x66, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x65, 0x03, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x6a, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6b, 0x03, 0x00, 0x00, 0x6a, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x6c, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x6b, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x6e, 0x03, 0x00, 0x00, + 0x6c, 0x03, 0x00, 0x00, 0x6d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x6f, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, + 0x6e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x69, 0x03, 0x00, 0x00, 0x6f, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x71, 0x03, 0x00, 0x00, 0x65, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x72, 0x03, 0x00, 0x00, 0x13, 0x03, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, + 0x72, 0x03, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x74, 0x03, 0x00, 0x00, + 0x71, 0x03, 0x00, 0x00, 0x73, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x70, 0x03, 0x00, 0x00, + 0x74, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x75, 0x03, 0x00, 0x00, + 0x21, 0x03, 0x00, 0x00, 0xad, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, + 0x75, 0x03, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x77, 0x03, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x78, 0x03, 0x00, 0x00, 0x77, 0x03, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x79, 0x03, 0x00, 0x00, 0x76, 0x03, 0x00, 0x00, 0x78, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, 0x70, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x7b, 0x03, 0x00, 0x00, 0x69, 0x03, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, 0x7a, 0x03, 0x00, 0x00, + 0x7b, 0x03, 0x00, 0x00, 0xa7, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x7d, 0x03, 0x00, 0x00, + 0x79, 0x03, 0x00, 0x00, 0x7c, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x7f, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x7d, 0x03, 0x00, 0x00, 0x7e, 0x03, 0x00, 0x00, + 0x7f, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x7e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x83, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x96, 0x01, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x82, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x07, 0x00, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x96, 0x01, 0x00, 0x00, 0x81, 0x03, 0x00, 0x00, 0x85, 0x03, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, 0x83, 0x03, 0x00, 0x00, + 0x86, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x80, 0x03, 0x00, 0x00, 0x87, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x3d, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x88, 0x03, 0x00, 0x00, 0x89, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x8a, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x88, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x8a, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x8b, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8c, 0x03, 0x00, 0x00, 0x8b, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x8d, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, + 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8e, 0x03, 0x00, 0x00, + 0x8d, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x8f, 0x03, 0x00, 0x00, + 0x8c, 0x03, 0x00, 0x00, 0x8e, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x20, 0x03, 0x00, 0x00, + 0x8f, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x24, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x7f, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x25, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x25, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x91, 0x03, 0x00, 0x00, + 0x21, 0x03, 0x00, 0x00, 0x80, 0x00, 0x05, 0x00, 0x80, 0x00, 0x00, 0x00, 0x92, 0x03, 0x00, 0x00, + 0x91, 0x03, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x21, 0x03, 0x00, 0x00, + 0x92, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, 0x22, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, + 0x24, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x94, 0x03, 0x00, 0x00, + 0xff, 0x02, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x95, 0x03, 0x00, 0x00, + 0x94, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, + 0x5b, 0x00, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, + 0x95, 0x03, 0x00, 0x00, 0x96, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x98, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x97, 0x03, 0x00, 0x00, + 0x8d, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x93, 0x03, 0x00, 0x00, + 0x98, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, + 0x93, 0x03, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x9c, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x9f, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x9d, 0x03, 0x00, 0x00, + 0x9e, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, + 0x9b, 0x03, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa1, 0x03, 0x00, 0x00, 0x9a, 0x03, 0x00, 0x00, 0xa0, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x99, 0x03, 0x00, 0x00, 0xa1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xa2, 0x03, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xa3, 0x03, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa4, 0x03, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa5, 0x03, 0x00, 0x00, 0x99, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xa6, 0x03, 0x00, 0x00, 0xa4, 0x03, 0x00, 0x00, 0xa5, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0xa7, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, + 0xa7, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, + 0xa6, 0x03, 0x00, 0x00, 0xa8, 0x03, 0x00, 0x00, 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xaa, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, 0xa9, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2e, 0x00, 0x00, 0x00, 0xa2, 0x03, 0x00, 0x00, 0xa3, 0x03, 0x00, 0x00, 0xaa, 0x03, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xab, 0x03, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00, 0x36, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x37, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x61, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x63, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xb5, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xba, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x08, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xed, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x6e, 0x02, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x07, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x0d, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x15, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x04, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xae, 0x03, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0xaf, 0x03, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0xae, 0x03, 0x00, 0x00, 0xa8, 0x00, 0x04, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xb0, 0x03, 0x00, 0x00, 0xaf, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0xb2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xb0, 0x03, 0x00, 0x00, + 0xb1, 0x03, 0x00, 0x00, 0xb2, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb1, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0xb3, 0x03, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, + 0xfe, 0x00, 0x02, 0x00, 0xb3, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xb2, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb7, 0x03, 0x00, 0x00, 0xb6, 0x03, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x50, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, 0x00, + 0xb9, 0x03, 0x00, 0x00, 0xb7, 0x03, 0x00, 0x00, 0xb8, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xb5, 0x03, 0x00, 0x00, 0xb9, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xbb, 0x03, 0x00, 0x00, 0xb5, 0x03, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xbc, 0x03, 0x00, 0x00, 0xbb, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0xbd, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, + 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, + 0xbd, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, + 0xbc, 0x03, 0x00, 0x00, 0xbe, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc0, 0x03, 0x00, 0x00, 0xbf, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xc1, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc2, 0x03, 0x00, 0x00, 0xc1, 0x03, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc3, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, + 0xc2, 0x03, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, + 0xb5, 0x03, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xc5, 0x03, 0x00, 0x00, 0xc4, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0xc6, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc7, 0x03, 0x00, 0x00, 0xc6, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0xc5, 0x03, 0x00, 0x00, + 0xc7, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xc9, 0x03, 0x00, 0x00, + 0xc8, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xca, 0x03, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, 0xca, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, 0xc9, 0x03, 0x00, 0x00, 0xcb, 0x03, 0x00, 0x00, + 0x50, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, 0xc3, 0x03, 0x00, 0x00, + 0xcc, 0x03, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xce, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0xcd, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xba, 0x03, 0x00, 0x00, 0xce, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0xba, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xd0, 0x03, 0x00, 0x00, 0xd1, 0x03, 0x00, 0x00, 0x39, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0xd2, 0x03, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0xd0, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x15, 0x00, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, + 0xd2, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xcf, 0x03, 0x00, 0x00, 0xd3, 0x03, 0x00, 0x00, + 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, + 0xa2, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xd6, 0x03, 0x00, 0x00, 0xd5, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xd4, 0x03, 0x00, 0x00, + 0xd6, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x5f, 0x02, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xd7, 0x03, 0x00, 0x00, 0xba, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xd9, 0x03, 0x00, 0x00, 0xd8, 0x03, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, + 0xb8, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xdb, 0x03, 0x00, 0x00, 0xda, 0x03, 0x00, 0x00, + 0xf7, 0x00, 0x00, 0x00, 0xa7, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, 0xdc, 0x03, 0x00, 0x00, + 0xd9, 0x03, 0x00, 0x00, 0xdb, 0x03, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, 0xde, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0xdc, 0x03, 0x00, 0x00, 0xdd, 0x03, 0x00, 0x00, + 0xde, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xdd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x07, 0x00, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0xe1, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xe3, 0x03, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xe2, 0x03, 0x00, 0x00, + 0xe3, 0x03, 0x00, 0x00, 0x39, 0x00, 0x06, 0x00, 0x15, 0x00, 0x00, 0x00, 0xe4, 0x03, 0x00, 0x00, + 0x42, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xe2, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, + 0xe4, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0xdf, 0x03, 0x00, 0x00, 0xe5, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xd4, 0x03, 0x00, 0x00, 0xe6, 0x03, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xde, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xde, 0x03, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, + 0x82, 0x00, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xab, 0x00, 0x00, 0x00, + 0x08, 0x01, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0xe9, 0x03, 0x00, 0x00, + 0xe8, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, + 0xe9, 0x03, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0xeb, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xea, 0x03, 0x00, 0x00, + 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0xec, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x2b, 0x00, 0x00, 0x00, 0xeb, 0x03, 0x00, 0x00, 0x58, 0x02, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xe7, 0x03, 0x00, 0x00, 0xec, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, 0x88, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0xee, 0x03, 0x00, 0x00, 0xef, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xed, 0x03, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0xf1, 0x03, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x03, 0x00, 0x00, + 0x6c, 0x02, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf3, 0x03, 0x00, 0x00, 0xf7, 0x03, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0xf8, 0x03, 0x00, 0x00, 0xf2, 0x01, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xf9, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xf9, 0x03, 0x00, 0x00, 0xf6, 0x00, 0x04, 0x00, + 0xfb, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf9, 0x00, 0x02, 0x00, + 0xfd, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfd, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x80, 0x00, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0xb1, 0x00, 0x05, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, 0x28, 0x03, 0x00, 0x00, + 0xfa, 0x00, 0x04, 0x00, 0xff, 0x03, 0x00, 0x00, 0xfa, 0x03, 0x00, 0x00, 0xfb, 0x03, 0x00, 0x00, + 0xf8, 0x00, 0x02, 0x00, 0xfa, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x01, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x02, 0x04, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, 0xbe, 0x00, 0x05, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x03, 0x04, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xf7, 0x00, 0x03, 0x00, + 0x05, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x04, 0x00, 0x03, 0x04, 0x00, 0x00, + 0x04, 0x04, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x04, 0x04, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xfb, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0x05, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, + 0x6f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0a, 0x04, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, + 0xeb, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, + 0xed, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, + 0x0a, 0x04, 0x00, 0x00, 0x0b, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x07, 0x04, 0x00, 0x00, + 0x0c, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x00, 0x00, 0x0e, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x12, 0x04, 0x00, 0x00, 0x07, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x11, 0x04, 0x00, 0x00, + 0x12, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x14, 0x04, 0x00, 0x00, + 0x0f, 0x04, 0x00, 0x00, 0x13, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x0d, 0x04, 0x00, 0x00, + 0x14, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x16, 0x04, 0x00, 0x00, + 0x0d, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, 0x17, 0x04, 0x00, 0x00, + 0x7f, 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x18, 0x04, 0x00, 0x00, 0x17, 0x04, 0x00, 0x00, 0x83, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, 0x16, 0x04, 0x00, 0x00, 0x18, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x19, 0x04, 0x00, 0x00, + 0x1a, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1d, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x00, 0x00, 0x1c, 0x04, 0x00, 0x00, + 0x7a, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, + 0x1d, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x1e, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, + 0x15, 0x04, 0x00, 0x00, 0x1f, 0x04, 0x00, 0x00, 0x41, 0x00, 0x06, 0x00, 0x82, 0x00, 0x00, 0x00, + 0x21, 0x04, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x08, 0x01, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, 0x21, 0x04, 0x00, 0x00, + 0x7f, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, 0x22, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x24, 0x04, 0x00, 0x00, 0x15, 0x04, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x25, 0x04, 0x00, 0x00, 0x23, 0x04, 0x00, 0x00, + 0x24, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, + 0xed, 0x03, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, + 0x25, 0x04, 0x00, 0x00, 0x26, 0x04, 0x00, 0x00, 0x0c, 0x00, 0x06, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x28, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x27, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x20, 0x04, 0x00, 0x00, 0x28, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, 0xf3, 0x03, 0x00, 0x00, 0x94, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x2d, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2f, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x2e, 0x04, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x07, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x2f, 0x04, 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x31, 0x04, 0x00, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x2a, 0x04, 0x00, 0x00, 0x31, 0x04, 0x00, 0x00, + 0x3e, 0x00, 0x03, 0x00, 0x29, 0x04, 0x00, 0x00, 0x32, 0x04, 0x00, 0x00, 0x41, 0x00, 0x05, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, 0xcf, 0x03, 0x00, 0x00, 0x9d, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x3a, 0x04, 0x00, 0x00, + 0x0c, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x3b, 0x04, 0x00, 0x00, 0x8d, 0x00, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00, 0x3c, 0x04, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, 0x37, 0x04, 0x00, 0x00, + 0x3d, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0x33, 0x04, 0x00, 0x00, 0x3e, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x3f, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x40, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, + 0x83, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x41, 0x04, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x40, 0x04, 0x00, 0x00, 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, + 0x3f, 0x04, 0x00, 0x00, 0x41, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x43, 0x04, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x44, 0x04, 0x00, 0x00, 0x43, 0x04, 0x00, 0x00, 0x42, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, 0x29, 0x04, 0x00, 0x00, 0x8e, 0x00, 0x05, 0x00, + 0x15, 0x00, 0x00, 0x00, 0x46, 0x04, 0x00, 0x00, 0x44, 0x04, 0x00, 0x00, 0x45, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, 0xf2, 0x03, 0x00, 0x00, + 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, 0x47, 0x04, 0x00, 0x00, + 0x46, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf2, 0x03, 0x00, 0x00, 0x48, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x49, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x85, 0x00, 0x05, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, 0x4a, 0x04, 0x00, 0x00, + 0x49, 0x04, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf1, 0x03, 0x00, 0x00, 0x4b, 0x04, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, + 0x80, 0x00, 0x05, 0x00, 0x80, 0x00, 0x00, 0x00, 0x4d, 0x04, 0x00, 0x00, 0x4c, 0x04, 0x00, 0x00, + 0xfe, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x03, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x4d, 0x04, 0x00, 0x00, + 0xf9, 0x00, 0x02, 0x00, 0xf9, 0x03, 0x00, 0x00, 0xf8, 0x00, 0x02, 0x00, 0xfb, 0x03, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0xf1, 0x03, 0x00, 0x00, + 0x8e, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x50, 0x04, 0x00, 0x00, 0x4e, 0x04, 0x00, 0x00, + 0x4f, 0x04, 0x00, 0x00, 0x3d, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, + 0xf2, 0x03, 0x00, 0x00, 0x81, 0x00, 0x05, 0x00, 0x15, 0x00, 0x00, 0x00, 0x52, 0x04, 0x00, 0x00, + 0x50, 0x04, 0x00, 0x00, 0x51, 0x04, 0x00, 0x00, 0xfe, 0x00, 0x02, 0x00, 0x52, 0x04, 0x00, 0x00, + 0x38, 0x00, 0x01, 0x00, +}; +static const unsigned int vk_temporal_resolve_frag_spv_size = 37476u; + +#endif diff --git a/src/renderer/Vulkan/vk_Backend.cpp b/src/renderer/Vulkan/vk_Backend.cpp index 4973c034..1f5aadd4 100644 --- a/src/renderer/Vulkan/vk_Backend.cpp +++ b/src/renderer/Vulkan/vk_Backend.cpp @@ -33,6 +33,15 @@ #include "../ModernGLExecutor.h" #include "../ModernLightImageAtlas.h" #include "../RenderGraphResources.h" +#include "../RendererMetrics.h" +#include "../MaterialResourceTable.h" +#include "../ClassicGuiDomain.h" +#include "../ClassicCinematicPostDomain.h" +#include "../ClassicSpecialFrameDomain.h" +#include "../ClassicWorldAmbientDomain.h" +#include "../ClassicInteractionDomain.h" +#include "../ClassicFogBlendDomain.h" +#include "../ClassicSubviewDomain.h" #include "VulkanDevice.h" #include "vk_Image.h" @@ -44,6 +53,55 @@ bool R_GetInitialWindowSize( bool fullScreen, int *width, int *height ); static const renderWindowServices_t *vkBackendServices = NULL; static float vkClearColor[ 4 ] = { 0.0f, 0.0f, 0.0f, 1.0f }; +// The Vulkan backend does not otherwise build the GL render graph. Keep one +// backend-owned packet frame for the shared classic-GUI domain when front-end +// capture was not requested for metrics. +static idScenePacketFrame vkClassicDomainPacketFrame; + +static const classicSubviewDomainView_t *VK_ClassicSubview_Preflight( + const viewDef_t *viewDef ) { + if ( !r_rendererSharedSubview.GetBool() || viewDef == NULL + || !viewDef->isSubview ) { + return NULL; + } + const classicSubviewDomainView_t *view = + R_ClassicSubviewDomain_FindView( viewDef ); + if ( view == NULL ) { + R_ClassicSubviewDomain_RecordBackendFallback( viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY, 0 ); + return NULL; + } + if ( view->backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + // A rejected nested member returns its entire sealed transaction to the + // established walker, including every later parent command. + return NULL; + } + const bool nestedDynamicReady = + R_ClassicCinematicPostDomain_SubviewTransactionReady( viewDef, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ); + if ( !view->ready || !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) + || ( R_ClassicSubviewDomain_IsCaptureBacked( *view ) + && ( view->captureImage == NULL || !view->captureImage->IsLoaded() ) ) + || !R_ClassicSubviewDomain_ReadyForBackend( *view, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ) ) { + R_ClassicSubviewDomain_RecordBackendFallback( viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + !view->ready ? view->failure + : ( !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH + : ( !nestedDynamicReady + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE + : ( !R_ClassicSubviewDomain_ReadyForBackend( *view, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ) + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE + : CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED ) ) ), + !view->ready ? view->failureDetail : 1 ); + return NULL; + } + return view; +} // vk_GuiExecutor.cpp void VK_GuiExecutor_SetClearColor( const float color[ 4 ] ); @@ -65,6 +123,25 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, int cubeFace, bool copyDepth ); bool VK_Exec_ResolveRenderTargets( idRenderTexture *sourceRenderTexture, idRenderTexture *destinationRenderTexture, bool resolveDepth ); +bool VK_GuiExecutor_ResolveTemporalPresentation( + const resolveTemporalPresentationCommand_t &command ); + +static void VK_DrawSharedDirectSubview( const classicSubviewDomainView_t &view ) { + // Direct SS_SUBVIEW mirrors compose into the parent target and therefore + // have no capture command. Keep the mature 3D executor, but publish only + // after its complete sealed child-view coverage has returned. + VK_GuiExecutor_Draw3DView( view.viewDef ); + if ( view.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + // A nested cinematic/post range rejected while the mature child-view + // executor was active. Its rollback already covered this complete tree. + return; + } + if ( !R_ClassicSubviewDomain_RecordDirectOwned( view.viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN ) ) { + common->Warning( "Vulkan: shared direct subview coverage rejected after committed view" ); + } +} /* ==================== @@ -227,6 +304,7 @@ VK_ShutdownRenderDevice ==================== */ void VK_ShutdownRenderDevice( void ) { + R_RendererMetrics_ShutdownGpuTimers(); VK_Device_Shutdown(); if ( vkBackendServices != NULL ) { vkBackendServices->BeginWindowTeardown(); @@ -368,13 +446,77 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { return; } + R_ClassicGuiDomain_ResetFrame(); + R_ClassicCinematicPostDomain_ResetFrame(); + R_ClassicSpecialFrameDomain_ResetFrame(); + R_ClassicWorldAmbientDomain_ResetFrame(); + R_ClassicInteractionDomain_ResetFrame(); + R_ClassicFogBlendDomain_ResetFrame(); + R_ClassicSubviewDomain_ResetFrame(); + if ( ( r_rendererModernQuality.GetBool() && r_pbrMaterials.GetBool() ) + || r_rendererSharedGui.GetBool() + || r_rendererSharedInWorldGui.GetBool() + || r_rendererSharedCinematicPost.GetBool() + || r_rendererSharedSpecialFrame.GetBool() + || r_rendererSharedWorldAmbient.GetBool() + || r_rendererSharedWorldInteraction.GetBool() + || r_rendererSharedWorldFogBlend.GetBool() + || r_rendererSharedSubview.GetBool() ) { + const idScenePacketFrame *scenePackets = NULL; + if ( R_ScenePackets_FrontEndFrameAvailable() ) { + scenePackets = &R_ScenePackets_FrontEndFrame(); + } else { + R_ScenePackets_BuildLegacyCommandStream( cmds, vkClassicDomainPacketFrame ); + scenePackets = &vkClassicDomainPacketFrame; + } + R_MaterialResourceTable_PrepareFrame( *scenePackets ); + if ( r_rendererSharedGui.GetBool() + || r_rendererSharedInWorldGui.GetBool() ) { + R_ClassicGuiDomain_PrepareFrame( *scenePackets ); + } + // Special-view topology must be sealed before cinematic/post ranges can + // join one of its atomic root transactions. + if ( r_rendererSharedSubview.GetBool() ) { + R_ClassicSubviewDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedCinematicPost.GetBool() ) { + R_ClassicCinematicPostDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedSpecialFrame.GetBool() ) { + R_ClassicSpecialFrameDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldAmbient.GetBool() ) { + R_ClassicWorldAmbientDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldInteraction.GetBool() ) { + R_ClassicInteractionDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldFogBlend.GetBool() ) { + R_ClassicFogBlendDomain_PrepareFrame( *scenePackets ); + } + } + backEnd.renderTexture = NULL; backEnd.feedbackRenderTexture = NULL; backEnd.postProcessTexelSize = tr.postProcessTexelSize; backEnd.postProcessSourceColorSpace = tr.postProcessSourceColorSpace; backEnd.postProcessSMAAQuality = tr.postProcessSMAAQuality; + const classicSubviewDomainView_t *pendingSharedSubview = NULL; for ( ; cmds != NULL; cmds = (const emptyCommand_t *)cmds->next ) { + if ( pendingSharedSubview != NULL + && cmds->commandId != RC_COPY_RENDER ) { + if ( pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, + static_cast( cmds->commandId ) ); + } + pendingSharedSubview = NULL; + } switch ( cmds->commandId ) { case RC_NOP: break; @@ -402,9 +544,17 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { case RC_DRAW_VIEW: { const drawSurfsCommand_t *cmd = (const drawSurfsCommand_t *)cmds; if ( cmd->viewDef != NULL ) { + const classicSubviewDomainView_t *sharedSubview = + VK_ClassicSubview_Preflight( cmd->viewDef ); + pendingSharedSubview = sharedSubview != NULL + && R_ClassicSubviewDomain_IsCaptureBacked( *sharedSubview ) + ? sharedSubview : NULL; if ( cmd->viewDef->viewEntitys == NULL ) { // 2D view (GUI/console/cinematics) VK_GuiExecutor_Draw2DView( cmd->viewDef ); + } else if ( sharedSubview != NULL + && R_ClassicSubviewDomain_IsDirect( *sharedSubview ) ) { + VK_DrawSharedDirectSubview( *sharedSubview ); } else { // world view: depth prepass + ambient walks (Phase E) VK_GuiExecutor_Draw3DView( cmd->viewDef ); @@ -450,6 +600,20 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { } break; } + case RC_RESOLVE_TEMPORAL_PRESENTATION: { + resolveTemporalPresentationCommand_t executionCommand = + *reinterpret_cast( cmds ); + // Save previews can enter their capture scope after the front end + // queued this command. Never let that late flush read or advance + // gameplay history. + if ( tr.takingScreenshot ) { + executionCommand.captureFrame = true; + executionCommand.historyValid = false; + } + (void)VK_GuiExecutor_ResolveTemporalPresentation( + executionCommand ); + break; + } case RC_SET_POSTPROCESS_SOURCE_SIZE: backEnd.postProcessTexelSize = ((const setPostProcessSourceSizeCommand_t *)cmds)->texelSize; @@ -464,10 +628,56 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { break; case RC_COPY_RENDER: { const copyRenderCommand_t *cmd = (const copyRenderCommand_t *)cmds; + const bool sharedCaptureMatches = pendingSharedSubview != NULL + && pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + && !r_skipCopyTexture.GetBool() + && R_ClassicSubviewDomain_CaptureMatches( *pendingSharedSubview, + cmd->image, cmd->x, cmd->y, cmd->imageWidth, + cmd->imageHeight, cmd->cubeFace, cmd->copyDepth ); + bool copied = false; if ( !r_skipCopyTexture.GetBool() ) { - (void)VK_Exec_CopyRender( cmd->image, cmd->x, cmd->y, + if ( sharedCaptureMatches ) { + copied = VK_Exec_CopyRender( + pendingSharedSubview->captureImage, + pendingSharedSubview->captureX, + pendingSharedSubview->captureY, + pendingSharedSubview->captureWidth, + pendingSharedSubview->captureHeight, + pendingSharedSubview->captureCubeFace, + pendingSharedSubview->captureCopyDepth ); + } else { + copied = VK_Exec_CopyRender( cmd->image, cmd->x, cmd->y, cmd->imageWidth, cmd->imageHeight, cmd->cubeFace, cmd->copyDepth ); + } + } + if ( pendingSharedSubview != NULL + && pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + if ( sharedCaptureMatches && copied ) { + R_ClassicSubviewDomain_RecordOwned( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + pendingSharedSubview->captureImage, + pendingSharedSubview->captureX, + pendingSharedSubview->captureY, + pendingSharedSubview->captureWidth, + pendingSharedSubview->captureHeight, + pendingSharedSubview->captureCubeFace, + pendingSharedSubview->captureCopyDepth ); + } else { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + r_skipCopyTexture.GetBool() || !copied + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED + : CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, + r_skipCopyTexture.GetBool() ? 2 : 3 ); + } } + pendingSharedSubview = NULL; break; } case RC_SWAP_BUFFERS: @@ -485,6 +695,18 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { break; } } + if ( pendingSharedSubview != NULL ) { + if ( pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, 4 ); + } + } + R_ClassicSpecialFrameDomain_FinalizeBackendFrame( + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ); } @@ -924,13 +1146,29 @@ bool RB_ShadowMapEstimateArb2CacheOwnership( const viewLight_t *vLight, const vi return false; } -bool RB_ShadowMapProjectedAtlasSlotForLight( int lightDefIndex, shadowMapArb2AtlasSlot_t &slot ) { - (void)lightDefIndex; +bool RB_ShadowMapProjectedAtlasSlotForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2AtlasSlot_t &slot ) { + (void)vLight; (void)viewDef; memset( &slot, 0, sizeof( slot ) ); return false; } -void RB_ShadowMapProjectedAtlasSlotMarkUsed( int lightDefIndex ) { +bool RB_ShadowMapProjectedAtlasSlotMarkUsed( int lightDefIndex, + int signature, std::uint64_t storageGeneration, + int cellX, int cellY, int cellSpan ) { + (void)lightDefIndex; (void)signature; (void)cellX; (void)cellY; + (void)storageGeneration; (void)cellSpan; + return false; +} + +bool RB_ShadowMapPointCubeForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2PointCube_t &cube ) { + (void)vLight; (void)viewDef; + memset( &cube, 0, sizeof( cube ) ); + return false; +} + +void RB_ShadowMapPointCubeMarkUsed( int lightDefIndex ) { (void)lightDefIndex; } @@ -1337,6 +1575,7 @@ void RB_ShutdownDebugTools( void ) { VK_GL_SELFTEST_STUB( RendererDeferredResolve_RunSelfTest ) VK_GL_SELFTEST_STUB( RendererForwardPlus_RunSelfTest ) VK_GL_SELFTEST_STUB( RendererGBuffer_RunSelfTest ) +VK_GL_SELFTEST_STUB( RendererPBRVisible_RunSelfTest ) VK_GL_SELFTEST_STUB( RendererGLStateCache_RunSelfTest ) VK_GL_SELFTEST_STUB( RendererGpuDriven_RunSelfTest ) VK_GL_SELFTEST_STUB( RendererLightImageAtlas_RunSelfTest ) diff --git a/src/renderer/Vulkan/vk_GuiExecutor.cpp b/src/renderer/Vulkan/vk_GuiExecutor.cpp index a5c167f9..909a3c5f 100644 --- a/src/renderer/Vulkan/vk_GuiExecutor.cpp +++ b/src/renderer/Vulkan/vk_GuiExecutor.cpp @@ -43,24 +43,42 @@ #define UINT_MAX 0xffffffffu #endif #include +#include #include "volk.h" #include "vk_mem_alloc.h" #include "VulkanDevice.h" +#include "VulkanGpuFrameTiming.h" +#include "../ClassicGuiDomain.h" +#include "../ClassicCinematicPostDomain.h" +#include "../ClassicSpecialFrameDomain.h" +#include "../ClassicInteractionDomain.h" +#include "../ClassicWorldAmbientDomain.h" +#include "../ClassicFogBlendDomain.h" +#include "../GpuSkinning.h" +#include "../MaterialResourceTable.h" +#include "../RendererContracts.h" +#include "../RendererMetrics.h" +#include "../ScenePackets.h" #include "vk_Image.h" // vk_ShadowMap.h: VK_ShadowMap_Shutdown + the point cube pool size the // descriptor pool budgets for (Phase F2a/F2b) #include "vk_ShadowMap.h" #include "shaders/gui_shaders_spv.h" +#include "shaders/temporal_resolve_spv.h" extern idCVar r_skipDynamicTextures; // vk_Interactions.cpp: the Phase F1 per-light interaction pass, inserted // between the depth fill and the ambient walks void VK_Interactions_DrawLights( const viewDef_t *viewDef ); +bool VK_ClassicInteraction_Preflight( const viewDef_t *viewDef ); +void VK_ClassicInteraction_DrawOwnedView( const viewDef_t *viewDef ); // vk_Interactions.cpp: the Phase G2 fog/blend light pass, inserted between // the two ambient walks (RB_STD_DrawView's fog point) void VK_Fog_DrawAllLights( const viewDef_t *viewDef ); +bool VK_ClassicFogBlend_Preflight( const viewDef_t *viewDef ); +void VK_ClassicFogBlend_DrawOwnedView( const viewDef_t *viewDef ); // frame-scope split (Phase F2a): the swapchain rendering scope can be // suspended for the shadow atlas caster pass and resumed with loadOp LOAD @@ -68,6 +86,14 @@ bool VK_Exec_BeginMainRendering( bool clearColorDepth ); void VK_Exec_EndMainRendering( void ); bool VK_GuiExecutor_EndFrameAndPresent( void ); static bool VK_GuiExecutor_SubmitFrame( bool present ); +VkDescriptorSet VK_Exec_InteractionUniformSet( void ); +int VK_Exec_InteractionUniformAlloc( const void *data, int bytes ); +static bool VK_TemporalPresentation_DrawPendingSceneSpatial( + const viewDef_t *viewDef, idImage *sceneImage, vkImageEntry_t *sceneEntry, + idImage *depthImage, vkImageEntry_t *depthEntry ); +static bool VK_TemporalPresentation_ResolveTargets( + const resolveTemporalPresentationCommand_t &sourceCommand, + bool trackExternalHistory, bool *historyAdvanced ); // no module-owned vertex-cache GPU state exists: the engine cache runs // CPU-backed under Vulkan and the executor streams into its own rings @@ -83,6 +109,8 @@ Executor state // (the vertex cache is CPU-backed); sized for q4dm2-scale 3D views static const int VK_VERTEX_RING_BYTES = 32 * 1024 * 1024; static const int VK_INDEX_RING_BYTES = 8 * 1024 * 1024; +static const int VK_GPU_SKINNING_WORKGROUP_SIZE = 64; +static const int VK_GPU_SKINNING_MEMO_SIZE = 2048; // power of two static const int VK_MAX_GUI_PIPELINES = 64; static const int VK_MAX_SCREEN_PIPELINES = 64; static const int VK_MAX_CUBE_PIPELINES = 32; @@ -90,6 +118,7 @@ static const int VK_MAX_ENV_PIPELINES = 32; static const int VK_MAX_PROGRAM_PIPELINES = 128; static const int VK_MAX_BLEND_LIGHT_PIPELINES = 32; static const int VK_MAX_SPECIAL_PIPELINES = 64; +static const int VK_MAX_TEMPORAL_RESOLVE_PIPELINES = 8; static const int VK_MAX_DESCRIPTOR_SETS = 4096; // rvspecial_depth.fs linearizes the Raven controller's depth texture with // this historical near-plane convention. The Vulkan path samples raw depth, @@ -112,6 +141,7 @@ typedef struct vkRing_s { unsigned char * mapped; int capacity; int cursor; + bool overflowWarned; // latched per frame so one oversized scene cannot emit a warning per failed surface allocation } vkRing_t; typedef struct vkPipelineTarget_s { @@ -172,6 +202,32 @@ typedef struct vkBumpyEnvironmentBlock_s { float modelRow2[ 4 ]; } vkBumpyEnvironmentBlock_t; +// std140-compatible block consumed by temporal_resolve.frag. Keep this under +// the established 256-byte dynamic-uniform slice so the temporal pass can +// reuse the interaction ring without adding another allocator or descriptor +// ownership surface. +typedef struct vkTemporalResolveBlock_s { + float sceneOutputExtent[ 4 ]; + float currentReconstruct[ 4 ]; + float previousProject[ 4 ]; + float depthFeedback[ 4 ]; + float currentViewOrigin[ 4 ]; + float currentViewAxis0[ 4 ]; + float currentViewAxis1[ 4 ]; + float currentViewAxis2[ 4 ]; + float previousViewOrigin[ 4 ]; + float previousViewAxis0[ 4 ]; + float previousViewAxis1[ 4 ]; + float previousViewAxis2[ 4 ]; + float temporalParams[ 4 ]; + float motionParams[ 4 ]; + float reactiveRect0[ 4 ]; + float reactiveRect1[ 4 ]; +} vkTemporalResolveBlock_t; + +static_assert( sizeof( vkTemporalResolveBlock_t ) == 16 * 16, + "Vulkan temporal resolve block must fill one 256-byte uniform slice" ); + typedef struct vkDescriptorCacheEntry_s { VkDescriptorSet set; unsigned int generation; @@ -181,7 +237,7 @@ typedef struct vkDescriptorCacheEntry_s { // fill and the two ambient walks visit the same tris, and re-uploading // triples ring traffic. Direct-mapped on the geometry pointers; a // collision just re-uploads. -static const int VK_TRI_MEMO_SIZE = 1024; // power of two +static const int VK_TRI_MEMO_SIZE = 4096; // power of two typedef struct vkVertUpload_s { const void * vertKey; int vertexOffset; @@ -191,6 +247,37 @@ typedef struct vkIdxUpload_s { int indexOffset; } vkIdxUpload_t; +// GPU-skinned output is retained separately from the ordinary direct-mapped +// upload memo. Stencil volumes deliberately use the CPU shadow cache and must +// not evict a compute result that later ambient/interaction passes still need. +typedef struct vkGpuSkinningMemo_s { + const void * ambientKey; + int vertexOffset; +} vkGpuSkinningMemo_t; + +typedef struct vkGpuSkinningPushConstants_s { + uint32_t sourceWordOffset; + uint32_t skinWordOffset; + uint32_t jointWordOffset; + uint32_t outputWordOffset; + uint32_t vertexCount; + uint32_t jointCount; +} vkGpuSkinningPushConstants_t; + +typedef struct vkGpuSkinningDispatch_s { + int memoIndex; + int sourceOffset; + int skinOffset; + int jointOffset; + int outputOffset; + int sourceBytes; + int skinBytes; + int jointBytes; + int outputBytes; + uint32_t vertexCount; + uint32_t jointCount; +} vkGpuSkinningDispatch_t; + typedef struct vkGuiExecutor_s { bool initialized; @@ -233,13 +320,19 @@ typedef struct vkGuiExecutor_s { VkShaderModule fogFragModule; VkShaderModule blendLightVertModule; VkShaderModule blendLightFragModule; + VkShaderModule gpuSkinningModule; + VkShaderModule temporalResolveVertModule; + VkShaderModule temporalResolveFragModule; VkDescriptorSetLayout setLayout; VkDescriptorSetLayout uboSetLayout; // one dynamic uniform buffer (interaction block ring) // shadow receiver set: binding 0 = atlas + compare sampler (fragment), // binding 1 = dynamic uniform buffer (per-space shadow block ring slice) VkDescriptorSetLayout shadowSetLayout; + VkDescriptorSetLayout gpuSkinningSetLayout; VkDescriptorPool descriptorPool; VkPipelineLayout pipelineLayout; + VkPipelineLayout gpuSkinningPipelineLayout; + VkPipeline gpuSkinningPipeline; // interactions: 6 single-combined-sampler sets (0=specTable, 1=bump, // 2=falloff, 3=lightProjection, 4=diffuse, 5=specular) + set 6 dynamic UBO VkPipelineLayout interactionPipelineLayout; @@ -254,6 +347,7 @@ typedef struct vkGuiExecutor_s { int numSpecialPipelines; vkGuiPipeline_t pipelines[ VK_MAX_GUI_PIPELINES ]; int numPipelines; + int lastPipelineMRU; // index of the last exact-key VK_GuiExecutor_GetPipeline result vkGuiPipeline_t screenPipelines[ VK_MAX_SCREEN_PIPELINES ]; int numScreenPipelines; vkCubePipeline_t cubePipelines[ VK_MAX_CUBE_PIPELINES ]; @@ -264,6 +358,8 @@ typedef struct vkGuiExecutor_s { int numProgramPipelines; vkGuiPipeline_t blendLightPipelines[ VK_MAX_BLEND_LIGHT_PIPELINES ]; // per light-stage blend bits int numBlendLightPipelines; + vkGuiPipeline_t temporalResolvePipelines[ VK_MAX_TEMPORAL_RESOLVE_PIPELINES ]; + int numTemporalResolvePipelines; VkFormat pipelineTargetFormat; // swapchain format the pipelines were built for vkRing_t vertexRings[ VK_FRAMES_IN_FLIGHT ]; @@ -271,7 +367,9 @@ typedef struct vkGuiExecutor_s { vkRing_t uniformRings[ VK_FRAMES_IN_FLIGHT ]; VkDescriptorSet uniformRingSets[ VK_FRAMES_IN_FLIGHT ]; VkDescriptorSet shadowSets[ VK_FRAMES_IN_FLIGHT ]; + VkDescriptorSet gpuSkinningSets[ VK_FRAMES_IN_FLIGHT ]; bool shadowSetsHaveAtlas; // compare + raw bindings written with a live atlas view + bool gpuSkinningAvailable; vkDescriptorCacheEntry_t descriptorCache[ 4096 ]; // parallel to the image table VkDescriptorSet retiredSets[ VK_FRAMES_IN_FLIGHT ][ VK_MAX_RETIRED_SETS ]; @@ -295,6 +393,43 @@ typedef struct vkGuiExecutor_s { VkImageView activeDepthAttachmentView; VkExtent2D activeExtent; vkPipelineTarget_t activePipelineTarget; + // Direct 3D presentation is isolated from the native swapchain so dynamic + // resolution never filters the HUD/menu views that follow it. Game-owned + // feedback render targets use the same coordinate mapper but keep their own + // allocation and final-post ownership. + idImage * temporalSceneColorImage; + idImage * temporalSceneDepthImage; + idRenderTexture * temporalSceneRenderTexture; + int temporalSceneWidth; + int temporalSceneHeight; + int temporalNativeWidth; + int temporalNativeHeight; + VkFormat temporalSwapchainFormat; + int temporalSceneFrame; + const viewDef_t * temporalSceneRoot; + bool temporalScenePendingComposite; + bool temporalSceneAllocationWarned; + idImage * temporalDirectHistoryImages[ 2 ]; + idRenderTexture * temporalDirectHistoryRenderTextures[ 2 ]; + unsigned long long temporalDirectHistoryStorageGenerations[ 2 ]; + int temporalDirectHistoryWidth; + int temporalDirectHistoryHeight; + int temporalDirectHistoryReadIndex; + int temporalDirectHistoryFrame; + unsigned int temporalDirectHistoryGeneration; + unsigned long long temporalDirectHistoryViewIdentity; + bool temporalDirectHistoryValid; + bool temporalDirectHistoryAllocationWarned; + idRenderTexture * temporalDepthStampTarget; + unsigned long long temporalDepthStampStorageGeneration; + int temporalDepthStampFrame; + unsigned int temporalDepthStampHistoryGeneration; + bool temporalDepthStampValid; + unsigned int temporalHistoryGenerationSeen; + int temporalCaptureInvalidatedFrame; + int temporalSceneCompositeInvalidatedFrame; + idRenderTexture * temporalHistoryTargets[ 2 ]; + unsigned long long temporalHistoryStorageGenerations[ 2 ]; const viewDef_t * pendingSpecialEffectsView; int pendingSpecialEffectsMask; idRenderTexture * pendingSpecialEffectsSource; @@ -302,9 +437,53 @@ typedef struct vkGuiExecutor_s { vkVertUpload_t vertMemo[ VK_TRI_MEMO_SIZE ]; vkIdxUpload_t idxMemo[ VK_TRI_MEMO_SIZE ]; + vkGpuSkinningMemo_t gpuSkinningMemo[ VK_GPU_SKINNING_MEMO_SIZE ]; + uint64 gpuSkinningDispatches; + uint64 gpuSkinningVertices; + uint64 gpuSkinningRingFallbacks; + uint64 gpuSkinningMemoFallbacks; + uint64 gpuSkinningValidationFallbacks; } vkGuiExecutor_t; static vkGuiExecutor_t vkExec; +static bool vkGpuSkinningBackendRequested = true; + +typedef struct vkSceneScaleState_s { + bool active; + bool backendOwned; + bool projectionRecentered; + const viewDef_t *rootView; + int nativeWidth; + int nativeHeight; + int targetWidth; + int targetHeight; + float nativeProjectionOffsetX; + float nativeProjectionOffsetY; + idScreenRect nativeViewport; + typedef struct rectRestore_s { + idScreenRect *rect; + idScreenRect original; + } rectRestore_t; + idList rectRestores; + idHashIndex rectRestoreHash; +} vkSceneScaleState_t; + +// Shared-domain preflight is allowed to populate the ordinary geometry rings, +// but it has not taken framebuffer ownership yet. Snapshot the bounded upload +// state so any later rejection can return the established classic walker to +// the exact cursor/memo state it would have seen without the speculative walk. +typedef struct vkSharedGeometryCheckpoint_s { + bool active; + int frameSlot; + int vertexCursor; + int indexCursor; + int boundVertexOffset; + vkVertUpload_t vertMemo[ VK_TRI_MEMO_SIZE ]; + vkIdxUpload_t idxMemo[ VK_TRI_MEMO_SIZE ]; + vkGpuSkinningMemo_t gpuSkinningMemo[ VK_GPU_SKINNING_MEMO_SIZE ]; +} vkSharedGeometryCheckpoint_t; + +static vkSharedGeometryCheckpoint_t vkSharedGeometryCheckpoint; static vkPipelineTarget_t VK_Exec_SwapchainPipelineTarget( void ) { vkPipelineTarget_t target; @@ -343,12 +522,28 @@ world passes. Column-major float[16]: row2 = elements 2,6,10,14. ==================== */ void VK_FixupClipSpaceZ( float dst[ 16 ], const float src[ 16 ] ) { - if ( dst != src ) { - memcpy( dst, src, 16 * sizeof( float ) ); - } - for ( int col = 0; col < 4; col++ ) { - dst[ col * 4 + 2 ] = 0.5f * ( src[ col * 4 + 2 ] + src[ col * 4 + 3 ] ); + RendererContracts_ConvertClipMatrix( dst, src, + RendererContracts_GLClipSpace(), RendererContracts_VulkanClipSpace() ); +} + +static bool VK_BuildCanonicalViewport( VkViewport &destination, float x, float y, + float width, float height, float framebufferHeight, + float minDepth = 0.0f, float maxDepth = 1.0f ) { + const rendererCanonicalViewport_t source = { + x, y, width, height, minDepth, maxDepth + }; + rendererBackendViewport_t backend; + if ( !RendererContracts_BuildViewport( backend, source, framebufferHeight, + RendererContracts_VulkanClipSpace() ) ) { + return false; } + destination.x = backend.x; + destination.y = backend.y; + destination.width = backend.width; + destination.height = backend.height; + destination.minDepth = backend.minDepth; + destination.maxDepth = backend.maxDepth; + return true; } /* @@ -383,7 +578,10 @@ static int VK_Ring_Alloc( vkRing_t &ring, const void *data, size_t bytes, int al if ( data == NULL || ring.mapped == NULL || bytes == 0 || ring.capacity <= 0 || ring.cursor < 0 || ring.cursor > ring.capacity || alignment <= 0 || ( alignment & ( alignment - 1 ) ) != 0 ) { - common->Warning( "Vulkan: invalid frame geometry ring allocation" ); + if ( !ring.overflowWarned ) { + common->Warning( "Vulkan: invalid frame geometry ring allocation" ); + ring.overflowWarned = true; + } return -1; } @@ -391,16 +589,42 @@ static int VK_Ring_Alloc( vkRing_t &ring, const void *data, size_t bytes, int al const size_t alignmentMask = static_cast( alignment - 1 ); const size_t offset = ( static_cast( ring.cursor ) + alignmentMask ) & ~alignmentMask; if ( offset > capacity || bytes > capacity - offset ) { - common->Warning( "Vulkan: frame geometry ring overflow (%zu + %zu > %zu)", offset, bytes, capacity ); + if ( !ring.overflowWarned ) { + common->Warning( "Vulkan: frame geometry ring overflow (%zu + %zu > %zu)", offset, bytes, capacity ); + ring.overflowWarned = true; + } return -1; } memcpy( ring.mapped + offset, data, bytes ); - const VkResult flushResult = vmaFlushAllocation( - vkCtx.allocator, ring.allocation, (VkDeviceSize)offset, - (VkDeviceSize)bytes ); - if ( flushResult != VK_SUCCESS ) { - common->Warning( "Vulkan: frame geometry ring flush failed (%d)", - (int)flushResult ); + // Host writes are flushed once per ring in VK_GuiExecutor_SubmitFrame, before + // the queue submit that consumes them, rather than once per allocation. + ring.cursor = static_cast( offset + bytes ); + return static_cast( offset ); +} + +// Reserve device-written output without spending host bandwidth initializing +// it. The same bounds and power-of-two alignment contract as VK_Ring_Alloc is +// retained; the frame-slot fence protects the range until the next reuse. +static int VK_Ring_Reserve( vkRing_t &ring, size_t bytes, int alignment ) { + if ( ring.mapped == NULL || bytes == 0 || ring.capacity <= 0 + || ring.cursor < 0 || ring.cursor > ring.capacity || alignment <= 0 + || ( alignment & ( alignment - 1 ) ) != 0 ) { + if ( !ring.overflowWarned ) { + common->Warning( "Vulkan: invalid frame geometry ring reservation" ); + ring.overflowWarned = true; + } + return -1; + } + + const size_t capacity = static_cast( ring.capacity ); + const size_t alignmentMask = static_cast( alignment - 1 ); + const size_t offset = ( static_cast( ring.cursor ) + alignmentMask ) & ~alignmentMask; + if ( offset > capacity || bytes > capacity - offset ) { + if ( !ring.overflowWarned ) { + common->Warning( "Vulkan: frame geometry ring reservation overflow (%zu + %zu > %zu)", + offset, bytes, capacity ); + ring.overflowWarned = true; + } return -1; } ring.cursor = static_cast( offset + bytes ); @@ -617,10 +841,19 @@ static VkPipeline VK_GuiExecutor_GetPipeline( int stateBits, bool separateColor | GLS_COLORMASK | GLS_ALPHAMASK ); const vkPipelineTarget_t target = VK_Exec_CurrentPipelineTarget(); + if ( vkExec.lastPipelineMRU >= 0 && vkExec.lastPipelineMRU < vkExec.numPipelines ) { + const vkGuiPipeline_t &mru = vkExec.pipelines[ vkExec.lastPipelineMRU ]; + if ( mru.stateBits == pipelineBits && mru.separateColor == separateColor + && VK_Exec_PipelineTargetsMatch( mru.target, target ) ) { + return mru.pipeline; + } + } + for ( int i = 0; i < vkExec.numPipelines; i++ ) { if ( vkExec.pipelines[ i ].stateBits == pipelineBits && vkExec.pipelines[ i ].separateColor == separateColor && VK_Exec_PipelineTargetsMatch( vkExec.pipelines[ i ].target, target ) ) { + vkExec.lastPipelineMRU = i; return vkExec.pipelines[ i ].pipeline; } } @@ -670,9 +903,52 @@ static VkPipeline VK_GuiExecutor_GetPipeline( int stateBits, bool separateColor vkExec.pipelines[ vkExec.numPipelines ].target = target; vkExec.pipelines[ vkExec.numPipelines ].pipeline = pipeline; vkExec.numPipelines++; + vkExec.lastPipelineMRU = vkExec.numPipelines - 1; return pipeline; } +/* +==================== +VK_GuiExecutor_GetPipelineStrict + +The legacy material walker historically tolerates GUI-pipeline allocation or +cache exhaustion by returning the first cached pipeline. Whole-view shared +ownership cannot: accepting a different blend/write-mask pipeline would make +the backend claim a view whose framebuffer result is known to be wrong. Ask +the normal cache to populate the exact key, then prove that the returned +object is backed by that key before exposing it to the transactional path. +==================== +*/ +static VkPipeline VK_GuiExecutor_GetPipelineStrict( int stateBits ) { + const int pipelineBits = stateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS + | GLS_COLORMASK | GLS_ALPHAMASK ); + const vkPipelineTarget_t target = VK_Exec_CurrentPipelineTarget(); + for ( int i = 0; i < vkExec.numPipelines; ++i ) { + const vkGuiPipeline_t &entry = vkExec.pipelines[ i ]; + if ( entry.stateBits == pipelineBits && !entry.separateColor + && VK_Exec_PipelineTargetsMatch( entry.target, target ) ) { + return entry.pipeline; + } + } + if ( vkExec.numPipelines >= VK_MAX_GUI_PIPELINES ) { + return VK_NULL_HANDLE; + } + + const VkPipeline candidate = VK_GuiExecutor_GetPipeline( stateBits, false ); + if ( candidate == VK_NULL_HANDLE ) { + return VK_NULL_HANDLE; + } + for ( int i = 0; i < vkExec.numPipelines; ++i ) { + const vkGuiPipeline_t &entry = vkExec.pipelines[ i ]; + if ( entry.pipeline == candidate && entry.stateBits == pipelineBits + && !entry.separateColor + && VK_Exec_PipelineTargetsMatch( entry.target, target ) ) { + return candidate; + } + } + return VK_NULL_HANDLE; +} + static VkPipeline VK_GuiExecutor_GetScreenPipeline( int stateBits, bool separateColor = false ) { const int pipelineBits = stateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS @@ -737,6 +1013,41 @@ static VkPipeline VK_GuiExecutor_GetScreenPipeline( int stateBits, return pipeline; } +static VkPipeline VK_TemporalPresentation_GetResolvePipeline( void ) { + const int pipelineBits = GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO; + const vkPipelineTarget_t target = VK_Exec_CurrentPipelineTarget(); + for ( int i = 0; i < vkExec.numTemporalResolvePipelines; ++i ) { + const vkGuiPipeline_t &entry = vkExec.temporalResolvePipelines[i]; + if ( VK_Exec_PipelineTargetsMatch( entry.target, target ) ) { + return entry.pipeline; + } + } + if ( vkExec.temporalResolveVertModule == VK_NULL_HANDLE + || vkExec.temporalResolveFragModule == VK_NULL_HANDLE + || vkExec.numTemporalResolvePipelines + >= VK_MAX_TEMPORAL_RESOLVE_PIPELINES ) { + return VK_NULL_HANDLE; + } + + VkPipelineVertexInputStateCreateInfo vertexInput; + memset( &vertexInput, 0, sizeof( vertexInput ) ); + vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + const VkPipeline pipeline = VK_Exec_CreatePipeline( + vkExec.temporalResolveVertModule, vkExec.temporalResolveFragModule, + &vertexInput, pipelineBits, vkExec.interactionPipelineLayout, + false, false, target ); + if ( pipeline == VK_NULL_HANDLE ) { + return VK_NULL_HANDLE; + } + vkGuiPipeline_t &entry = vkExec.temporalResolvePipelines[ + vkExec.numTemporalResolvePipelines++ ]; + entry.stateBits = pipelineBits; + entry.separateColor = false; + entry.target = target; + entry.pipeline = pipeline; + return pipeline; +} + // cube-texgen pipelines (TG_SKYBOX_CUBE / TG_WOBBLESKY_CUBE / TG_DIFFUSE_CUBE): // position from the idDrawVert stream plus a vec3 direction attribute — the // front-end texgen's tightly packed stream on binding 1 for the skies, or @@ -878,37 +1189,85 @@ static VkPipeline VK_GuiExecutor_GetEnvironmentPipeline( int stateBits, bool bum // full idDrawVert vertex input shared by the interaction pipelines: // xyz@0, color ubyte4@12, normal@16, tangent0@32, tangent1@44, st@56 // (64-byte stride, one binding) +static bool VK_Exec_TranslateInteractionVertexAttribute( + const rendererVertexAttributeDesc_t &source, std::uint32_t &location, + VkFormat &format ) { + switch ( source.semantic ) { + case RENDERER_VERTEX_SEMANTIC_POSITION: + location = 0; + break; + case RENDERER_VERTEX_SEMANTIC_COLOR0: + location = 1; + break; + case RENDERER_VERTEX_SEMANTIC_NORMAL: + location = 2; + break; + case RENDERER_VERTEX_SEMANTIC_TANGENT0: + location = 3; + break; + case RENDERER_VERTEX_SEMANTIC_TANGENT1: + location = 4; + break; + case RENDERER_VERTEX_SEMANTIC_TEXCOORD0: + location = 5; + break; + default: + return false; + } + switch ( source.format ) { + case RENDERER_VERTEX_FORMAT_FLOAT32X2: + format = VK_FORMAT_R32G32_SFLOAT; + return true; + case RENDERER_VERTEX_FORMAT_FLOAT32X3: + format = VK_FORMAT_R32G32B32_SFLOAT; + return true; + case RENDERER_VERTEX_FORMAT_FLOAT32X4: + format = VK_FORMAT_R32G32B32A32_SFLOAT; + return true; + case RENDERER_VERTEX_FORMAT_UNORM8X4: + format = VK_FORMAT_R8G8B8A8_UNORM; + return true; + case RENDERER_VERTEX_FORMAT_UINT32X4: + format = VK_FORMAT_R32G32B32A32_UINT; + return true; + default: + return false; + } +} + static void VK_Exec_InteractionVertexInput( VkVertexInputBindingDescription &binding, VkVertexInputAttributeDescription attrs[ 6 ], VkPipelineVertexInputStateCreateInfo &vertexInput ) { memset( &binding, 0, sizeof( binding ) ); - binding.stride = sizeof( idDrawVert ); - binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - memset( attrs, 0, 6 * sizeof( attrs[ 0 ] ) ); - attrs[ 0 ].location = 0; - attrs[ 0 ].format = VK_FORMAT_R32G32B32_SFLOAT; - attrs[ 0 ].offset = (uint32_t)offsetof( idDrawVert, xyz ); - attrs[ 1 ].location = 1; - attrs[ 1 ].format = VK_FORMAT_R8G8B8A8_UNORM; - attrs[ 1 ].offset = (uint32_t)offsetof( idDrawVert, color ); - attrs[ 2 ].location = 2; - attrs[ 2 ].format = VK_FORMAT_R32G32B32_SFLOAT; - attrs[ 2 ].offset = (uint32_t)offsetof( idDrawVert, normal ); - attrs[ 3 ].location = 3; - attrs[ 3 ].format = VK_FORMAT_R32G32B32_SFLOAT; - attrs[ 3 ].offset = (uint32_t)offsetof( idDrawVert, tangents ); - attrs[ 4 ].location = 4; - attrs[ 4 ].format = VK_FORMAT_R32G32B32_SFLOAT; - attrs[ 4 ].offset = (uint32_t)( offsetof( idDrawVert, tangents ) + sizeof( idVec3 ) ); - attrs[ 5 ].location = 5; - attrs[ 5 ].format = VK_FORMAT_R32G32_SFLOAT; - attrs[ 5 ].offset = (uint32_t)offsetof( idDrawVert, st ); - memset( &vertexInput, 0, sizeof( vertexInput ) ); vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + const rendererVertexLayoutDesc_t &layout = RendererContracts_LegacyDrawVertLayout(); + if ( !RendererContracts_ValidateVertexLayout( layout ) + || layout.bindingCount != 1 || layout.attributeCount != 6 ) { + common->Warning( "Vulkan: shared idDrawVert layout contract is invalid" ); + return; + } + binding.binding = layout.bindings[ 0 ].binding; + binding.stride = layout.bindings[ 0 ].stride; + binding.inputRate = layout.bindings[ 0 ].inputRate == RENDERER_VERTEX_RATE_PER_INSTANCE + ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX; + for ( std::uint32_t i = 0; i < layout.attributeCount; ++i ) { + const rendererVertexAttributeDesc_t &source = layout.attributes[ i ]; + std::uint32_t location = 0; + VkFormat format = VK_FORMAT_UNDEFINED; + if ( !VK_Exec_TranslateInteractionVertexAttribute( source, location, format ) ) { + common->Warning( "Vulkan: shared idDrawVert attribute cannot be translated" ); + return; + } + attrs[ i ].location = location; + attrs[ i ].binding = source.binding; + attrs[ i ].format = format; + attrs[ i ].offset = source.offset; + } vertexInput.vertexBindingDescriptionCount = 1; vertexInput.pVertexBindingDescriptions = &binding; - vertexInput.vertexAttributeDescriptionCount = 6; + vertexInput.vertexAttributeDescriptionCount = layout.attributeCount; vertexInput.pVertexAttributeDescriptions = attrs; } @@ -1493,7 +1852,10 @@ VkPipeline VK_Exec_BlendLightPipeline( int stateBits ) { } if ( vkExec.numBlendLightPipelines >= VK_MAX_BLEND_LIGHT_PIPELINES ) { common->Warning( "Vulkan: blend-light pipeline cache exhausted" ); - return vkExec.blendLightPipelines[ 0 ].pipeline; + // The requested blend/color-mask key has no exact pipeline. Returning + // an arbitrary cached variant would corrupt the sealed light-stage + // contract; shared preflight must reject the complete view instead. + return VK_NULL_HANDLE; } VkVertexInputBindingDescription binding; @@ -1607,6 +1969,176 @@ static VkDescriptorSet VK_GuiExecutor_GetResidentImageDescriptor( idImage *image return VK_GuiExecutor_GetImageDescriptor( image->GetDeviceHandle() ); } +/* +==================== +VK_GpuSkinning_DestroyResources +==================== +*/ +static void VK_GpuSkinning_DestroyResources( void ) { + vkExec.gpuSkinningAvailable = false; + if ( vkCtx.device == VK_NULL_HANDLE ) { + vkExec.gpuSkinningPipeline = VK_NULL_HANDLE; + vkExec.gpuSkinningPipelineLayout = VK_NULL_HANDLE; + vkExec.gpuSkinningSetLayout = VK_NULL_HANDLE; + vkExec.gpuSkinningModule = VK_NULL_HANDLE; + memset( vkExec.gpuSkinningSets, 0, sizeof( vkExec.gpuSkinningSets ) ); + return; + } + if ( vkExec.gpuSkinningPipeline != VK_NULL_HANDLE ) { + vkDestroyPipeline( vkCtx.device, vkExec.gpuSkinningPipeline, NULL ); + vkExec.gpuSkinningPipeline = VK_NULL_HANDLE; + } + if ( vkExec.gpuSkinningPipelineLayout != VK_NULL_HANDLE ) { + vkDestroyPipelineLayout( vkCtx.device, vkExec.gpuSkinningPipelineLayout, NULL ); + vkExec.gpuSkinningPipelineLayout = VK_NULL_HANDLE; + } + if ( vkExec.gpuSkinningSetLayout != VK_NULL_HANDLE ) { + vkDestroyDescriptorSetLayout( vkCtx.device, vkExec.gpuSkinningSetLayout, NULL ); + vkExec.gpuSkinningSetLayout = VK_NULL_HANDLE; + } + if ( vkExec.gpuSkinningModule != VK_NULL_HANDLE ) { + vkDestroyShaderModule( vkCtx.device, vkExec.gpuSkinningModule, NULL ); + vkExec.gpuSkinningModule = VK_NULL_HANDLE; + } + memset( vkExec.gpuSkinningSets, 0, sizeof( vkExec.gpuSkinningSets ) ); +} + +/* +==================== +VK_GpuSkinning_InitResources + +GPU skinning is optional. Any unsupported limit or object-creation failure +leaves the existing CPU deformation/cache path authoritative without taking +down the Vulkan renderer. +==================== +*/ +static bool VK_GpuSkinning_InitResources( void ) { + VK_GpuSkinning_DestroyResources(); + + const VkPhysicalDeviceLimits &limits = vkCtx.deviceProperties.limits; + if ( sizeof( idDrawVert ) != 64 + || offsetof( idDrawVert, xyz ) != 0 + || offsetof( idDrawVert, color ) != 12 + || offsetof( idDrawVert, normal ) != 16 + || offsetof( idDrawVert, color2 ) != 28 + || offsetof( idDrawVert, tangents ) != 32 + || offsetof( idDrawVert, st ) != 56 + || sizeof( gpuSkinningVertex_t ) != 32 + || offsetof( gpuSkinningVertex_t, jointIndices ) != 0 + || offsetof( gpuSkinningVertex_t, jointWeights ) != 16 + || limits.maxStorageBufferRange < static_cast( VK_VERTEX_RING_BYTES ) + || limits.maxPushConstantsSize < sizeof( vkGpuSkinningPushConstants_t ) + || limits.maxComputeWorkGroupInvocations < VK_GPU_SKINNING_WORKGROUP_SIZE + || limits.maxComputeWorkGroupSize[ 0 ] < VK_GPU_SKINNING_WORKGROUP_SIZE ) { + common->Warning( "Vulkan: GPU skinning unavailable because device/ABI limits are insufficient" ); + return false; + } + + VkShaderModuleCreateInfo smci; + memset( &smci, 0, sizeof( smci ) ); + smci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + smci.codeSize = vk_gpu_skinning_comp_spv_size; + smci.pCode = reinterpret_cast( vk_gpu_skinning_comp_spv ); + if ( vkCreateShaderModule( vkCtx.device, &smci, NULL, + &vkExec.gpuSkinningModule ) != VK_SUCCESS ) { + common->Warning( "Vulkan: GPU skinning compute shader module creation failed" ); + VK_GpuSkinning_DestroyResources(); + return false; + } + + VkDescriptorSetLayoutBinding bindings[ 4 ]; + memset( bindings, 0, sizeof( bindings ) ); + for ( uint32_t binding = 0; binding < 4; ++binding ) { + bindings[ binding ].binding = binding; + bindings[ binding ].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + bindings[ binding ].descriptorCount = 1; + bindings[ binding ].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + } + VkDescriptorSetLayoutCreateInfo dslci; + memset( &dslci, 0, sizeof( dslci ) ); + dslci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + dslci.bindingCount = 4; + dslci.pBindings = bindings; + if ( vkCreateDescriptorSetLayout( vkCtx.device, &dslci, NULL, + &vkExec.gpuSkinningSetLayout ) != VK_SUCCESS ) { + common->Warning( "Vulkan: GPU skinning descriptor layout creation failed" ); + VK_GpuSkinning_DestroyResources(); + return false; + } + + VkPushConstantRange pushRange; + memset( &pushRange, 0, sizeof( pushRange ) ); + pushRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + pushRange.size = sizeof( vkGpuSkinningPushConstants_t ); + VkPipelineLayoutCreateInfo plci; + memset( &plci, 0, sizeof( plci ) ); + plci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plci.setLayoutCount = 1; + plci.pSetLayouts = &vkExec.gpuSkinningSetLayout; + plci.pushConstantRangeCount = 1; + plci.pPushConstantRanges = &pushRange; + if ( vkCreatePipelineLayout( vkCtx.device, &plci, NULL, + &vkExec.gpuSkinningPipelineLayout ) != VK_SUCCESS ) { + common->Warning( "Vulkan: GPU skinning pipeline layout creation failed" ); + VK_GpuSkinning_DestroyResources(); + return false; + } + + VkPipelineShaderStageCreateInfo stage; + memset( &stage, 0, sizeof( stage ) ); + stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + stage.module = vkExec.gpuSkinningModule; + stage.pName = "main"; + VkComputePipelineCreateInfo cpci; + memset( &cpci, 0, sizeof( cpci ) ); + cpci.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + cpci.stage = stage; + cpci.layout = vkExec.gpuSkinningPipelineLayout; + if ( vkCreateComputePipelines( vkCtx.device, VK_NULL_HANDLE, 1, &cpci, NULL, + &vkExec.gpuSkinningPipeline ) != VK_SUCCESS ) { + common->Warning( "Vulkan: GPU skinning compute pipeline creation failed" ); + VK_GpuSkinning_DestroyResources(); + return false; + } + + for ( int slot = 0; slot < VK_FRAMES_IN_FLIGHT; ++slot ) { + VkDescriptorSetAllocateInfo dsai; + memset( &dsai, 0, sizeof( dsai ) ); + dsai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsai.descriptorPool = vkExec.descriptorPool; + dsai.descriptorSetCount = 1; + dsai.pSetLayouts = &vkExec.gpuSkinningSetLayout; + if ( vkAllocateDescriptorSets( vkCtx.device, &dsai, + &vkExec.gpuSkinningSets[ slot ] ) != VK_SUCCESS ) { + common->Warning( "Vulkan: GPU skinning frame descriptor allocation failed" ); + VK_GpuSkinning_DestroyResources(); + return false; + } + + VkDescriptorBufferInfo bufferInfos[ 4 ]; + VkWriteDescriptorSet writes[ 4 ]; + memset( bufferInfos, 0, sizeof( bufferInfos ) ); + memset( writes, 0, sizeof( writes ) ); + for ( uint32_t binding = 0; binding < 4; ++binding ) { + bufferInfos[ binding ].buffer = vkExec.vertexRings[ slot ].buffer; + bufferInfos[ binding ].range = static_cast( + vkExec.vertexRings[ slot ].capacity ); + writes[ binding ].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[ binding ].dstSet = vkExec.gpuSkinningSets[ slot ]; + writes[ binding ].dstBinding = binding; + writes[ binding ].descriptorCount = 1; + writes[ binding ].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[ binding ].pBufferInfo = &bufferInfos[ binding ]; + } + vkUpdateDescriptorSets( vkCtx.device, 4, writes, 0, NULL ); + } + + vkExec.gpuSkinningAvailable = true; + common->Printf( "Vulkan: GPU skinning compute initialized\n" ); + return true; +} + /* ==================== Init / Shutdown @@ -1676,6 +2208,20 @@ static bool VK_GuiExecutor_Init( void ) { common->Warning( "Vulkan: screen fragment shader module creation failed" ); return false; } + smci.codeSize = vk_temporal_resolve_vert_spv_size; + smci.pCode = (const uint32_t *)vk_temporal_resolve_vert_spv; + if ( vkCreateShaderModule( vkCtx.device, &smci, NULL, + &vkExec.temporalResolveVertModule ) != VK_SUCCESS ) { + common->Warning( "Vulkan: temporal resolve vertex shader module creation failed" ); + return false; + } + smci.codeSize = vk_temporal_resolve_frag_spv_size; + smci.pCode = (const uint32_t *)vk_temporal_resolve_frag_spv; + if ( vkCreateShaderModule( vkCtx.device, &smci, NULL, + &vkExec.temporalResolveFragModule ) != VK_SUCCESS ) { + common->Warning( "Vulkan: temporal resolve fragment shader module creation failed" ); + return false; + } smci.codeSize = vk_sky_vert_spv_size; smci.pCode = (const uint32_t *)vk_sky_vert_spv; if ( vkCreateShaderModule( vkCtx.device, &smci, NULL, &vkExec.skyVertModule ) != VK_SUCCESS ) { @@ -2038,17 +2584,19 @@ static bool VK_GuiExecutor_Init( void ) { const int shadowSetBudget = ( 1 + VK_SHADOW_MAX_POINT_CUBES + VK_SHADOW_MAX_CACHE_SLOTS ) * VK_FRAMES_IN_FLIGHT; const int retiredSetBudget = VK_MAX_RETIRED_SETS * VK_FRAMES_IN_FLIGHT; - VkDescriptorPoolSize poolSizes[ 2 ]; + VkDescriptorPoolSize poolSizes[ 3 ]; poolSizes[ 0 ].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; poolSizes[ 0 ].descriptorCount = VK_MAX_DESCRIPTOR_SETS + 2 * shadowSetBudget + retiredSetBudget; poolSizes[ 1 ].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; poolSizes[ 1 ].descriptorCount = VK_FRAMES_IN_FLIGHT + shadowSetBudget; + poolSizes[ 2 ].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + poolSizes[ 2 ].descriptorCount = 4 * VK_FRAMES_IN_FLIGHT; VkDescriptorPoolCreateInfo dpci; memset( &dpci, 0, sizeof( dpci ) ); dpci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; - dpci.maxSets = VK_MAX_DESCRIPTOR_SETS + VK_FRAMES_IN_FLIGHT + shadowSetBudget + retiredSetBudget; - dpci.poolSizeCount = 2; + dpci.maxSets = VK_MAX_DESCRIPTOR_SETS + 2 * VK_FRAMES_IN_FLIGHT + shadowSetBudget + retiredSetBudget; + dpci.poolSizeCount = 3; dpci.pPoolSizes = poolSizes; if ( vkCreateDescriptorPool( vkCtx.device, &dpci, NULL, &vkExec.descriptorPool ) != VK_SUCCESS ) { common->Warning( "Vulkan: descriptor pool creation failed" ); @@ -2114,7 +2662,8 @@ static bool VK_GuiExecutor_Init( void ) { } for ( int i = 0; i < VK_FRAMES_IN_FLIGHT; i++ ) { - if ( !VK_Ring_Create( vkExec.vertexRings[ i ], VK_VERTEX_RING_BYTES, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT ) + if ( !VK_Ring_Create( vkExec.vertexRings[ i ], VK_VERTEX_RING_BYTES, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT ) || !VK_Ring_Create( vkExec.indexRings[ i ], VK_INDEX_RING_BYTES, VK_BUFFER_USAGE_INDEX_BUFFER_BIT ) || !VK_Ring_Create( vkExec.uniformRings[ i ], VK_UNIFORM_RING_BYTES, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT ) ) { return false; @@ -2158,11 +2707,26 @@ static bool VK_GuiExecutor_Init( void ) { vkUpdateDescriptorSets( vkCtx.device, 1, &write, 0, NULL ); } + // Optional: failures leave the authoritative CPU deformation/cache path in + // place and do not prevent the Vulkan renderer from starting. + if ( !VK_GpuSkinning_InitResources() ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + } + vkExec.pipelineTargetFormat = vkCtx.swapchainFormat; vkExec.clearColor[ 0 ] = 0.0f; vkExec.clearColor[ 1 ] = 0.0f; vkExec.clearColor[ 2 ] = 0.0f; vkExec.clearColor[ 3 ] = 1.0f; + vkExec.temporalSceneFrame = -1; + vkExec.temporalDirectHistoryFrame = -1; + vkExec.temporalDepthStampFrame = -1; + vkExec.temporalDirectHistoryGeneration = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalCaptureInvalidatedFrame = -1; + vkExec.temporalSceneCompositeInvalidatedFrame = -1; + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); vkExec.initialized = true; initGuard.Commit(); common->Printf( "Vulkan: GUI executor initialized\n" ); @@ -2170,11 +2734,31 @@ static bool VK_GuiExecutor_Init( void ) { } void VK_GuiExecutor_Shutdown( void ) { + for ( int i = 0; i < 2; ++i ) { + if ( vkExec.temporalDirectHistoryRenderTextures[i] != NULL ) { + delete vkExec.temporalDirectHistoryRenderTextures[i]; + vkExec.temporalDirectHistoryRenderTextures[i] = NULL; + } + vkExec.temporalDirectHistoryImages[i] = NULL; + } + if ( vkExec.temporalSceneRenderTexture != NULL ) { + if ( backEnd.renderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.renderTexture = NULL; + } + if ( backEnd.feedbackRenderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.feedbackRenderTexture = NULL; + } + delete vkExec.temporalSceneRenderTexture; + vkExec.temporalSceneRenderTexture = NULL; + } + vkExec.temporalSceneColorImage = NULL; + vkExec.temporalSceneDepthImage = NULL; if ( vkCtx.device == VK_NULL_HANDLE ) { memset( &vkExec, 0, sizeof( vkExec ) ); return; } VK_ShadowMap_Shutdown(); + VK_GpuSkinning_DestroyResources(); for ( int i = 0; i < vkExec.numPipelines; i++ ) { if ( vkExec.pipelines[ i ].pipeline != VK_NULL_HANDLE ) { vkDestroyPipeline( vkCtx.device, vkExec.pipelines[ i ].pipeline, NULL ); @@ -2216,6 +2800,12 @@ void VK_GuiExecutor_Shutdown( void ) { vkDestroyPipeline( vkCtx.device, vkExec.blendLightPipelines[ i ].pipeline, NULL ); } } + for ( int i = 0; i < vkExec.numTemporalResolvePipelines; ++i ) { + if ( vkExec.temporalResolvePipelines[i].pipeline != VK_NULL_HANDLE ) { + vkDestroyPipeline( vkCtx.device, + vkExec.temporalResolvePipelines[i].pipeline, NULL ); + } + } if ( vkExec.pipelineLayout != VK_NULL_HANDLE ) { vkDestroyPipelineLayout( vkCtx.device, vkExec.pipelineLayout, NULL ); } @@ -2252,6 +2842,14 @@ void VK_GuiExecutor_Shutdown( void ) { if ( vkExec.screenFragModule != VK_NULL_HANDLE ) { vkDestroyShaderModule( vkCtx.device, vkExec.screenFragModule, NULL ); } + if ( vkExec.temporalResolveVertModule != VK_NULL_HANDLE ) { + vkDestroyShaderModule( vkCtx.device, + vkExec.temporalResolveVertModule, NULL ); + } + if ( vkExec.temporalResolveFragModule != VK_NULL_HANDLE ) { + vkDestroyShaderModule( vkCtx.device, + vkExec.temporalResolveFragModule, NULL ); + } if ( vkExec.skyVertModule != VK_NULL_HANDLE ) { vkDestroyShaderModule( vkCtx.device, vkExec.skyVertModule, NULL ); } @@ -2415,6 +3013,21 @@ static bool VK_GuiExecutor_BeginFrame( void ) { return false; } } + const bool temporalOutputKnown = vkExec.temporalNativeWidth > 0 + && vkExec.temporalNativeHeight > 0 + && vkExec.temporalSwapchainFormat != VK_FORMAT_UNDEFINED; + if ( temporalOutputKnown + && ( vkExec.temporalNativeWidth != (int)vkCtx.swapchainExtent.width + || vkExec.temporalNativeHeight != (int)vkCtx.swapchainExtent.height + || vkExec.temporalSwapchainFormat != vkCtx.swapchainFormat ) ) { + R_TemporalPresentation_InvalidateHistory( + "Vulkan swapchain extent/format changed" ); + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + } + vkExec.temporalNativeWidth = (int)vkCtx.swapchainExtent.width; + vkExec.temporalNativeHeight = (int)vkCtx.swapchainExtent.height; + vkExec.temporalSwapchainFormat = vkCtx.swapchainFormat; // swapchain format changes (rare) invalidate the pipeline set if ( vkExec.pipelineTargetFormat != vkCtx.swapchainFormat ) { vkDeviceWaitIdle( vkCtx.device ); @@ -2456,6 +3069,11 @@ static bool VK_GuiExecutor_BeginFrame( void ) { vkDestroyPipeline( vkCtx.device, vkExec.blendLightPipelines[ i ].pipeline, NULL ); } vkExec.numBlendLightPipelines = 0; + for ( int i = 0; i < vkExec.numTemporalResolvePipelines; ++i ) { + vkDestroyPipeline( vkCtx.device, + vkExec.temporalResolvePipelines[i].pipeline, NULL ); + } + vkExec.numTemporalResolvePipelines = 0; vkExec.pipelineTargetFormat = vkCtx.swapchainFormat; } @@ -2463,7 +3081,18 @@ static bool VK_GuiExecutor_BeginFrame( void ) { vkCtx.frameSlot = ( vkCtx.frameSlot + 1 ) % VK_FRAMES_IN_FLIGHT; vkCtx.recordingSlot = slot; - vkWaitForFences( vkCtx.device, 1, &vkCtx.frameFences[ slot ], VK_TRUE, UINT64_MAX ); + const VkResult frameFenceResult = vkWaitForFences( vkCtx.device, 1, + &vkCtx.frameFences[ slot ], VK_TRUE, UINT64_MAX ); + if ( frameFenceResult != VK_SUCCESS ) { + common->Warning( "Vulkan: frame-slot %d fence wait failed (%d)", slot, + static_cast( frameFenceResult ) ); + return false; + } + // retire the last upload batch (and, per fence submission-order scope, + // every earlier one) before deferred destroys can release images those + // batches referenced; near-free, since the batch was submitted before the + // previous frame's submit + VK_Device_WaitUploadBatch(); VK_Device_FlushDeferredDestroys( slot ); if ( vkExec.numRetiredSets[ slot ] > 0 ) { vkFreeDescriptorSets( vkCtx.device, vkExec.descriptorPool, @@ -2485,15 +3114,30 @@ static bool VK_GuiExecutor_BeginFrame( void ) { return false; } - vkResetFences( vkCtx.device, 1, &vkCtx.frameFences[ slot ] ); - VkCommandBuffer cmd = vkCtx.commandBuffers[ slot ]; - vkResetCommandBuffer( cmd, 0 ); VkCommandBufferBeginInfo cbbi; memset( &cbbi, 0, sizeof( cbbi ) ); cbbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; cbbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - vkBeginCommandBuffer( cmd, &cbbi ); + // Reset/begin the command buffer BEFORE resetting the slot fence, and bail + // on failure (device-lost between the fence wait and here): recording the + // barriers below into a command buffer that was never begun is undefined + // behavior, and leaving the fence signaled on the failure path keeps the + // slot's "fence signaled == slot idle" invariant so the next frame does not + // deadlock. The acquired swapchain image is abandoned only on this already + // terminal device-lost path. + if ( vkResetCommandBuffer( cmd, 0 ) != VK_SUCCESS + || vkBeginCommandBuffer( cmd, &cbbi ) != VK_SUCCESS ) { + common->Warning( "Vulkan: frame-slot %d command buffer begin failed", slot ); + return false; + } + if ( vkResetFences( vkCtx.device, 1, &vkCtx.frameFences[ slot ] ) != VK_SUCCESS ) { + common->Warning( "Vulkan: frame-slot %d fence reset failed", slot ); + return false; + } + // The slot fence above is the sole retirement wait. Timestamp results are + // polled here without VK_QUERY_RESULT_WAIT_BIT before this slot is reset. + VK_GpuFrameTiming_BeginFrame( cmd, slot, tr.frameCount ); VkImageMemoryBarrier2 toAttachment[ 2 ]; memset( toAttachment, 0, sizeof( toAttachment ) ); @@ -2540,8 +3184,13 @@ static bool VK_GuiExecutor_BeginFrame( void ) { vkExec.vertexRings[ slot ].cursor = 0; vkExec.indexRings[ slot ].cursor = 0; vkExec.uniformRings[ slot ].cursor = 0; + vkExec.vertexRings[ slot ].overflowWarned = false; + vkExec.indexRings[ slot ].overflowWarned = false; + vkExec.uniformRings[ slot ].overflowWarned = false; memset( vkExec.vertMemo, 0, sizeof( vkExec.vertMemo ) ); memset( vkExec.idxMemo, 0, sizeof( vkExec.idxMemo ) ); + memset( vkExec.gpuSkinningMemo, 0, sizeof( vkExec.gpuSkinningMemo ) ); + vkSharedGeometryCheckpoint.active = false; return true; } @@ -2940,6 +3589,24 @@ bool VK_Exec_ActiveTargetHasStencil( void ) { && vkExec.activePipelineTarget.stencilFormat != VK_FORMAT_UNDEFINED; } +// Shared interaction ownership is intentionally limited to the primary +// swapchain scope. Keep the executor's private attachment and pending-effect +// state behind one fail-closed query so the interaction consumer cannot +// mistake an offscreen render texture for the main framebuffer. +bool VK_Exec_SharedInteractionTargetReady( void ) { + return vkExec.frameOpen && vkExec.mainScopeOpen + && vkExec.cmd != VK_NULL_HANDLE + && vkExec.activeRenderTexture == NULL + && vkExec.activeColorEntry == NULL + && vkExec.activeDepthEntry == NULL + && vkExec.pendingSpecialEffectsView == NULL + && vkExec.pendingSpecialEffectsMask == 0 + && vkExec.pendingSpecialEffectsSource == NULL + && !vkExec.pendingSpecialEffectsNeedsResolve + && VK_Exec_PipelineTargetsMatch( vkExec.activePipelineTarget, + VK_Exec_SwapchainPipelineTarget() ); +} + void VK_Exec_ClearRenderTarget( bool clearColor, bool clearDepth, float depthValue, const float colorValue[ 4 ] ) { if ( !VK_GuiExecutor_BeginFrame() ) { @@ -2978,82 +3645,1024 @@ void VK_Exec_ClearRenderTarget( bool clearColor, bool clearDepth, float depthVal vkCmdClearAttachments( vkExec.cmd, (uint32_t)attachmentCount, attachments, 1, &rect ); } -static bool VK_Exec_PrepareCopyDestination( idImage *image, int width, int height, - vkImageEntry_t *&entry ) { - entry = NULL; - if ( image == NULL || width <= 0 || height <= 0 ) { +static void VK_TemporalPresentation_ClearScaleState( vkSceneScaleState_t &state ) { + state.active = false; + state.backendOwned = false; + state.projectionRecentered = false; + state.rootView = NULL; + state.nativeWidth = 0; + state.nativeHeight = 0; + state.targetWidth = 0; + state.targetHeight = 0; + state.nativeProjectionOffsetX = 0.0f; + state.nativeProjectionOffsetY = 0.0f; + state.nativeViewport.Clear(); + state.rectRestores.Clear(); + state.rectRestores.SetGranularity( 256 ); + state.rectRestoreHash.Clear( 4096, 4096 ); +} + +static bool VK_TemporalPresentation_IsMainSceneView( const viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->viewEntitys == NULL + || ( viewDef->renderFlags & RF_PORTAL_SKY ) != 0 + || viewDef->isSubview || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->isXraySubview + || viewDef->renderView.viewID < 0 ) { return false; } - if ( image->GetUploadWidth() != width || image->GetUploadHeight() != height ) { - image->Resize( width, height ); - } - entry = VK_Image_GetEntry( image->GetDeviceHandle() ); - return entry != NULL && ( entry->usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT ) != 0; + return viewDef->renderWorld == NULL + || viewDef->renderWorld->mapName.Length() > 0; } -bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, - int cubeFace, bool copyDepth ) { - if ( !VK_GuiExecutor_BeginFrame() || image == NULL || width <= 0 || height <= 0 ) { +static bool VK_TemporalPresentation_ViewMatchesRoot( const viewDef_t *viewDef, + const viewDef_t *rootView ) { + return viewDef != NULL && rootView != NULL + && viewDef->viewport.Equals( rootView->viewport ) + && viewDef->renderView.width == rootView->renderView.width + && viewDef->renderView.height == rootView->renderView.height; +} + +static const viewDef_t *VK_TemporalPresentation_RootForView( + const viewDef_t *viewDef ) { + if ( VK_TemporalPresentation_IsMainSceneView( viewDef ) ) { + return viewDef; + } + if ( viewDef == NULL || viewDef->viewEntitys == NULL ) { + return NULL; + } + + if ( ( viewDef->renderFlags & RF_PORTAL_SKY ) != 0 ) { + const viewDef_t *rootView = tr.primaryView; + return rootView != viewDef + && VK_TemporalPresentation_IsMainSceneView( rootView ) + && VK_TemporalPresentation_ViewMatchesRoot( viewDef, rootView ) + ? rootView : NULL; + } + + // SS_SUBVIEW contributors render directly into their ancestor's viewport. + // Capture-backed mirror/remote/refraction views use different footprints and + // remain isolated on the target selected by their authored crop/copy stream. + const viewDef_t *candidate = viewDef; + while ( candidate != NULL && candidate->isSubview ) { + const viewDef_t *superView = candidate->superView; + if ( candidate->isXraySubview || superView == NULL + || candidate->subviewSurface == NULL + || candidate->subviewSurface->material == NULL + || candidate->subviewSurface->material->GetSort() != SS_SUBVIEW + || !VK_TemporalPresentation_ViewMatchesRoot( candidate, superView ) ) { + return NULL; + } + candidate = superView; + } + return VK_TemporalPresentation_IsMainSceneView( candidate ) + && VK_TemporalPresentation_ViewMatchesRoot( viewDef, candidate ) + ? candidate : NULL; +} + +static bool VK_TemporalPresentation_RootCoversNativeOutput( + const viewDef_t *rootView ) { + if ( rootView == NULL ) { return false; } + const int width = rootView->viewport.x2 - rootView->viewport.x1 + 1; + const int height = rootView->viewport.y2 - rootView->viewport.y1 + 1; + return rootView->viewport.x1 == 0 && rootView->viewport.y1 == 0 + && width == (int)vkCtx.swapchainExtent.width + && height == (int)vkCtx.swapchainExtent.height; +} - VkImage sourceImage = VK_NULL_HANDLE; - VkFormat sourceFormat = VK_FORMAT_UNDEFINED; - VkImageAspectFlags copyAspect = copyDepth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; - vkImageEntry_t *sourceEntry = NULL; - if ( copyDepth ) { - sourceEntry = vkExec.activeRenderTexture != NULL ? vkExec.activeDepthEntry : NULL; - sourceImage = sourceEntry != NULL ? sourceEntry->image : vkCtx.depthImages[ vkExec.frameSlot ]; - sourceFormat = sourceEntry != NULL ? sourceEntry->format : vkCtx.depthFormat; - } else { - sourceEntry = vkExec.activeRenderTexture != NULL ? vkExec.activeColorEntry : NULL; - sourceImage = sourceEntry != NULL ? sourceEntry->image : vkCtx.swapchainImages[ vkExec.swapImageIndex ]; - sourceFormat = sourceEntry != NULL ? sourceEntry->format : vkCtx.swapchainFormat; +static bool VK_TemporalPresentation_BackendSceneRequested( + const viewDef_t *rootView, int &targetWidth, int &targetHeight ) { + targetWidth = 0; + targetHeight = 0; + if ( !VK_TemporalPresentation_RootCoversNativeOutput( rootView ) ) { + return false; } - if ( sourceImage == VK_NULL_HANDLE - || ( sourceEntry == NULL && !copyDepth && !vkCtx.swapchainTransferSrc ) - || ( sourceEntry != NULL && ( sourceEntry->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ) == 0 ) - || ( sourceEntry != NULL && sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT ) ) { + const temporalPresentationFrameState_t &presentation = + R_TemporalPresentation_GetFrameState(); + if ( presentation.frameNumber != backEnd.frameCount + || presentation.nativeWidth != (int)vkCtx.swapchainExtent.width + || presentation.nativeHeight != (int)vkCtx.swapchainExtent.height + || presentation.sceneWidth <= 0 || presentation.sceneHeight <= 0 ) { return false; } - - const int sourceWidth = VK_Exec_ActiveFramebufferWidth(); - const int sourceHeight = VK_Exec_ActiveFramebufferHeight(); - if ( x < 0 ) { - width += x; - x = 0; + const bool sceneScaled = presentation.sceneWidth != presentation.nativeWidth + || presentation.sceneHeight != presentation.nativeHeight; + if ( sceneScaled && !presentation.dynamicResolutionRequested + && presentation.effectiveScalePercent < 100 + && r_resolutionScaleMode.GetInteger() == 0 + && !R_TemporalPresentation_TemporalAARequested() + && !R_TemporalPresentation_ScreenSpaceEffectsRequested() ) { + return false; } - if ( y < 0 ) { - height += y; - y = 0; + const bool knownCapture = presentation.captureFrozen + || presentation.captureForcedNative + || ( rootView != NULL && rootView->temporalCaptureFrame ); + const bool temporalScene = presentation.temporalAARequested && !knownCapture; + const bool screenSpaceScene = AdvancedScreenSpaceCore_Requested( + presentation.advancedScreenSpace ); + if ( !sceneScaled && !temporalScene && !screenSpaceScene ) { + return false; } - if ( x + width > sourceWidth ) { - width = sourceWidth - x; + // The spatial presentation shader is also the projection-jitter safety + // gate for a backend-owned scene target. If its swapchain pipeline cannot + // be created, leave the view native so a later fixed-function scale cannot + // expose subpixel jitter. + if ( vkExec.temporalResolveVertModule == VK_NULL_HANDLE + || vkExec.temporalResolveFragModule == VK_NULL_HANDLE + || VK_TemporalPresentation_GetResolvePipeline() == VK_NULL_HANDLE ) { + return false; } - if ( y + height > sourceHeight ) { - height = sourceHeight - y; + targetWidth = presentation.sceneWidth; + targetHeight = presentation.sceneHeight; + return true; +} + +static int VK_TemporalPresentation_ScaleRectStart( int value, + int sourceExtent, int targetExtent ) { + return static_cast( ( static_cast( value ) * targetExtent ) + / sourceExtent ); +} + +static int VK_TemporalPresentation_ScaleRectEnd( int value, + int sourceExtent, int targetExtent ) { + return static_cast( ( ( static_cast( value + 1 ) * targetExtent ) + + sourceExtent - 1 ) / sourceExtent ) - 1; +} + +static void VK_TemporalPresentation_ScaleLocalRect( idScreenRect &rect, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + if ( rect.IsEmpty() || sourceWidth <= 0 || sourceHeight <= 0 + || targetWidth <= 0 || targetHeight <= 0 ) { + return; } - if ( width <= 0 || height <= 0 ) { - return false; + const int x1 = idMath::ClampInt( 0, targetWidth - 1, + VK_TemporalPresentation_ScaleRectStart( rect.x1, sourceWidth, targetWidth ) ); + const int y1 = idMath::ClampInt( 0, targetHeight - 1, + VK_TemporalPresentation_ScaleRectStart( rect.y1, sourceHeight, targetHeight ) ); + const int x2 = idMath::ClampInt( 0, targetWidth - 1, + VK_TemporalPresentation_ScaleRectEnd( rect.x2, sourceWidth, targetWidth ) ); + const int y2 = idMath::ClampInt( 0, targetHeight - 1, + VK_TemporalPresentation_ScaleRectEnd( rect.y2, sourceHeight, targetHeight ) ); + if ( x2 < x1 || y2 < y1 ) { + rect.Clear(); + return; } + rect.x1 = static_cast( x1 ); + rect.y1 = static_cast( y1 ); + rect.x2 = static_cast( x2 ); + rect.y2 = static_cast( y2 ); +} - // Validate aliasing before a resize/re-back can retire the destination. - vkImageEntry_t *destination = VK_Image_GetEntry( image->GetDeviceHandle() ); - if ( destination != NULL && destination->image == sourceImage ) { +static bool VK_TemporalPresentation_RecordScaledRect( + vkSceneScaleState_t &state, idScreenRect *rect ) { + if ( rect == NULL ) { return false; } - if ( copyDepth ) { - if ( !VK_Image_MakeDepthCopyTarget( image, width, height, sourceFormat ) ) { + const int key = static_cast( reinterpret_cast( rect ) >> 4 ); + for ( int i = state.rectRestoreHash.First( key ); i != -1; + i = state.rectRestoreHash.Next( i ) ) { + if ( state.rectRestores[i].rect == rect ) { return false; } - destination = VK_Image_GetEntry( image->GetDeviceHandle() ); + } + vkSceneScaleState_t::rectRestore_t &restore = state.rectRestores.Alloc(); + restore.rect = rect; + restore.original = *rect; + state.rectRestoreHash.Add( key, state.rectRestores.Num() - 1 ); + return true; +} + +static void VK_TemporalPresentation_ScaleTrackedRect( + vkSceneScaleState_t &state, idScreenRect *rect, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + if ( !VK_TemporalPresentation_RecordScaledRect( state, rect ) ) { + return; + } + VK_TemporalPresentation_ScaleLocalRect( *rect, sourceWidth, sourceHeight, + targetWidth, targetHeight ); +} + +static void VK_TemporalPresentation_ScaleDrawSurf( + vkSceneScaleState_t &state, const drawSurf_t *surf, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + if ( surf != NULL ) { + VK_TemporalPresentation_ScaleTrackedRect( state, + &const_cast( surf )->scissorRect, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + } +} + +static void VK_TemporalPresentation_ScaleDrawSurfChain( + vkSceneScaleState_t &state, const drawSurf_t *surf, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + for ( const drawSurf_t *chainSurf = surf; chainSurf != NULL; + chainSurf = chainSurf->nextOnLight ) { + VK_TemporalPresentation_ScaleDrawSurf( state, chainSurf, sourceWidth, + sourceHeight, targetWidth, targetHeight ); + } +} + +static void VK_TemporalPresentation_ScaleLight( + vkSceneScaleState_t &state, const viewLight_t *vLight, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + if ( vLight == NULL ) { + return; + } + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->globalShadows, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->localInteractions, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->localShadows, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->globalInteractions, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->localShadowMapCasters, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->globalShadowMapCasters, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, + vLight->localTranslucentShadowMapCasters, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, + vLight->globalTranslucentShadowMapCasters, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleDrawSurfChain( state, vLight->translucentInteractions, + sourceWidth, sourceHeight, targetWidth, targetHeight ); +} + +static bool VK_TemporalPresentation_BeginScale( vkSceneScaleState_t &state, + const viewDef_t *viewDef, const viewDef_t *rootView, + int targetWidth, int targetHeight, bool backendOwned ) { + VK_TemporalPresentation_ClearScaleState( state ); + if ( viewDef == NULL || rootView == NULL || targetWidth <= 0 || targetHeight <= 0 + || targetWidth > 32767 || targetHeight > 32767 ) { + return false; + } + viewDef_t *mutableView = const_cast( viewDef ); + const int sourceWidth = mutableView->viewport.x2 - mutableView->viewport.x1 + 1; + const int sourceHeight = mutableView->viewport.y2 - mutableView->viewport.y1 + 1; + if ( sourceWidth <= 0 || sourceHeight <= 0 ) { + return false; + } + + state.active = true; + state.backendOwned = backendOwned; + state.rootView = rootView; + state.nativeWidth = sourceWidth; + state.nativeHeight = sourceHeight; + state.targetWidth = targetWidth; + state.targetHeight = targetHeight; + state.nativeViewport = mutableView->viewport; + // Native-scale TAA still needs backend ownership so the jittered 3D scene + // reaches a history consumer before native UI begins. It needs no coordinate + // mutation, but EndView must retain the same transaction/composite seam. + if ( sourceWidth == targetWidth && sourceHeight == targetHeight ) { + return true; + } + VK_TemporalPresentation_ScaleTrackedRect( state, &mutableView->scissor, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + mutableView->viewport.x1 = 0; + mutableView->viewport.y1 = 0; + mutableView->viewport.x2 = static_cast( targetWidth - 1 ); + mutableView->viewport.y2 = static_cast( targetHeight - 1 ); + + for ( int i = 0; i < mutableView->numDrawSurfs; i++ ) { + VK_TemporalPresentation_ScaleDrawSurf( state, mutableView->drawSurfs[i], + sourceWidth, sourceHeight, targetWidth, targetHeight ); + } + for ( viewLight_t *vLight = mutableView->viewLights; vLight != NULL; + vLight = vLight->next ) { + VK_TemporalPresentation_ScaleTrackedRect( state, &vLight->scissorRect, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + VK_TemporalPresentation_ScaleLight( state, vLight, sourceWidth, + sourceHeight, targetWidth, targetHeight ); + } + for ( viewEntity_t *vEntity = mutableView->viewEntitys; vEntity != NULL; + vEntity = vEntity->next ) { + VK_TemporalPresentation_ScaleTrackedRect( state, &vEntity->scissorRect, + sourceWidth, sourceHeight, targetWidth, targetHeight ); + } + return true; +} + +static void VK_TemporalPresentation_RestoreScale( vkSceneScaleState_t &state, + const viewDef_t *viewDef ) { + if ( !state.active || viewDef == NULL ) { + return; + } + viewDef_t *mutableView = const_cast( viewDef ); + mutableView->viewport = state.nativeViewport; + for ( int i = state.rectRestores.Num() - 1; i >= 0; i-- ) { + if ( state.rectRestores[i].rect != NULL ) { + *state.rectRestores[i].rect = state.rectRestores[i].original; + } + } + state.rectRestores.Clear(); + state.rectRestoreHash.Clear(); + state.active = false; +} + +static void VK_TemporalPresentation_RecenterDirectProjection( + vkSceneScaleState_t &state, const viewDef_t *viewDef ) { + if ( viewDef == NULL || !viewDef->temporalJitterEnabled + || state.projectionRecentered ) { + return; + } + const int width = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; + const int height = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; + if ( width <= 0 || height <= 0 ) { + return; + } + viewDef_t *mutableView = const_cast( viewDef ); + state.nativeProjectionOffsetX = mutableView->projectionMatrix[8]; + state.nativeProjectionOffsetY = mutableView->projectionMatrix[9]; + mutableView->projectionMatrix[8] -= 2.0f + * mutableView->temporalJitterPixels.x / static_cast( width ); + mutableView->projectionMatrix[9] -= 2.0f + * mutableView->temporalJitterPixels.y / static_cast( height ); + state.projectionRecentered = true; + + // No temporal consumer owns this direct view. Retire image/camera history so + // a later successful offscreen frame is seeded instead of blending across + // the spatial-only gap. A late capture already advanced this generation. + if ( viewDef->temporalHistoryGeneration + == R_TemporalPresentation_HistoryGeneration() ) { + R_TemporalPresentation_InvalidateHistory( + "Vulkan temporal scene ownership unavailable" ); + } +} + +static void VK_TemporalPresentation_RestoreDirectProjection( + vkSceneScaleState_t &state, const viewDef_t *viewDef ) { + if ( !state.projectionRecentered || viewDef == NULL ) { + return; + } + viewDef_t *mutableView = const_cast( viewDef ); + mutableView->projectionMatrix[8] = state.nativeProjectionOffsetX; + mutableView->projectionMatrix[9] = state.nativeProjectionOffsetY; + state.projectionRecentered = false; +} + +static void VK_TemporalPresentation_ObserveHistoryAndCapture( void ) { + const unsigned int historyGeneration = + R_TemporalPresentation_HistoryGeneration(); + if ( vkExec.temporalHistoryGenerationSeen != historyGeneration ) { + // Both backend-owned and game-owned Vulkan histories retire at the shared + // generation seam. Game ownership validates its immutable command later; + // direct roots also drop their local ping-pong immediately. + vkExec.temporalHistoryGenerationSeen = historyGeneration; + vkExec.temporalDirectHistoryGeneration = historyGeneration; + vkExec.temporalDirectHistoryValid = false; + vkExec.temporalDirectHistoryFrame = -1; + vkExec.temporalDepthStampValid = false; + } + if ( tr.takingScreenshot + && vkExec.temporalCaptureInvalidatedFrame != backEnd.frameCount ) { + R_TemporalPresentation_MarkCurrentFrameCapture( + "Vulkan execution-time capture" ); + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalDirectHistoryGeneration = + vkExec.temporalHistoryGenerationSeen; + vkExec.temporalDirectHistoryValid = false; + vkExec.temporalDirectHistoryFrame = -1; + vkExec.temporalDepthStampValid = false; + vkExec.temporalCaptureInvalidatedFrame = backEnd.frameCount; + } +} + +static void VK_TemporalPresentation_StampDepth( + idRenderTexture *target, bool success ) { + vkExec.temporalDepthStampTarget = target; + vkExec.temporalDepthStampFrame = backEnd.frameCount; + vkExec.temporalDepthStampHistoryGeneration = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalDepthStampStorageGeneration = 0; + vkExec.temporalDepthStampValid = false; + if ( !success || target == NULL || target->GetDepthImage() == NULL ) { + return; + } + vkExec.temporalDepthStampStorageGeneration = + static_cast( + target->GetDepthImage()->GetStorageGeneration() ); + vkExec.temporalDepthStampValid = true; +} + +static bool VK_TemporalPresentation_DepthStampMatches( + idRenderTexture *target, idImage *depthImage, + unsigned int historyGeneration ) { + return vkExec.temporalDepthStampValid && target != NULL + && depthImage != NULL && vkExec.temporalDepthStampTarget == target + && vkExec.temporalDepthStampFrame == backEnd.frameCount + && vkExec.temporalDepthStampHistoryGeneration == historyGeneration + && vkExec.temporalDepthStampStorageGeneration + == static_cast( + depthImage->GetStorageGeneration() ); +} + +static bool VK_TemporalPresentation_BlitSupported( VkFormat sourceFormat, + VkFilter &filter ) { + VkFormatProperties sourceProperties; + VkFormatProperties destinationProperties; + memset( &sourceProperties, 0, sizeof( sourceProperties ) ); + memset( &destinationProperties, 0, sizeof( destinationProperties ) ); + vkGetPhysicalDeviceFormatProperties( vkCtx.physicalDevice, + sourceFormat, &sourceProperties ); + vkGetPhysicalDeviceFormatProperties( vkCtx.physicalDevice, + vkCtx.swapchainFormat, &destinationProperties ); + if ( ( sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT ) == 0 + || ( destinationProperties.optimalTilingFeatures + & VK_FORMAT_FEATURE_BLIT_DST_BIT ) == 0 ) { + return false; + } + filter = ( sourceProperties.optimalTilingFeatures + & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ) != 0 + ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; + return true; +} + +// Depth formats have no mandatory blit support, so probe before using a single +// flipped vkCmdBlitImage in place of the per-row depth copy. Source and +// destination share the depth format (the copyDepth path requires it). +static bool VK_Exec_DepthBlitSupported( VkFormat depthFormat ) { + VkFormatProperties properties; + memset( &properties, 0, sizeof( properties ) ); + vkGetPhysicalDeviceFormatProperties( vkCtx.physicalDevice, depthFormat, &properties ); + return ( properties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT ) != 0 + && ( properties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT ) != 0; +} + +static bool VK_TemporalPresentation_EnsureSceneTarget( int width, int height ) { + const int maxDimension = Min( 32767, + static_cast( vkCtx.deviceProperties.limits.maxImageDimension2D ) ); + if ( width <= 0 || height <= 0 || width > maxDimension + || height > maxDimension ) { + if ( !vkExec.temporalSceneAllocationWarned ) { + vkExec.temporalSceneAllocationWarned = true; + common->Warning( + "Vulkan: temporal scene target %dx%d exceeds device/composite support; using native rendering", + width, height ); + } + return false; + } + + const bool sizeChanged = vkExec.temporalSceneWidth != width + || vkExec.temporalSceneHeight != height; + if ( vkExec.temporalSceneRenderTexture == NULL ) { + idImageOpts colorOpts; + colorOpts.format = FMT_RGBA8; + colorOpts.colorFormat = CFM_DEFAULT; + colorOpts.numLevels = 1; + colorOpts.textureType = TT_2D; + colorOpts.isPersistant = true; + colorOpts.width = width; + colorOpts.height = height; + colorOpts.numMSAASamples = 0; + vkExec.temporalSceneColorImage = globalImages->ScratchImage( + "_vkTemporalSceneColor", &colorOpts, TF_LINEAR, TR_CLAMP, TD_DEFAULT ); + idImageOpts depthOpts = colorOpts; + depthOpts.format = FMT_DEPTH_STENCIL; + vkExec.temporalSceneDepthImage = globalImages->ScratchImage( + "_vkTemporalSceneDepth", &depthOpts, TF_NEAREST, TR_CLAMP, TD_DEPTH ); + if ( vkExec.temporalSceneColorImage != NULL + && vkExec.temporalSceneDepthImage != NULL ) { + vkExec.temporalSceneRenderTexture = tr.CreateRenderTexture( + vkExec.temporalSceneColorImage, vkExec.temporalSceneDepthImage ); + } + if ( vkExec.temporalSceneRenderTexture != NULL ) { + vkExec.temporalSceneRenderTexture->SetDebugLabel( + "Vulkan temporal presentation scene" ); + } + } else if ( sizeChanged ) { + (void)tr.ResizeRenderTexture( vkExec.temporalSceneRenderTexture, + width, height ); + } + + if ( vkExec.temporalSceneRenderTexture == NULL + || vkExec.temporalSceneRenderTexture->GetWidth() != width + || vkExec.temporalSceneRenderTexture->GetHeight() != height ) { + if ( !vkExec.temporalSceneAllocationWarned ) { + vkExec.temporalSceneAllocationWarned = true; + common->Warning( + "Vulkan: temporal scene target allocation failed; using native rendering" ); + } + return false; + } + + if ( sizeChanged ) { + vkExec.temporalSceneWidth = width; + vkExec.temporalSceneHeight = height; + vkExec.temporalSceneFrame = -1; + vkExec.temporalSceneRoot = NULL; + vkExec.temporalScenePendingComposite = false; + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + } + vkExec.temporalSceneAllocationWarned = false; + return true; +} + +static void VK_TemporalPresentation_ResetDirectHistory( + const char *reason, bool invalidateSharedGeneration ) { + const unsigned int generation = + R_TemporalPresentation_HistoryGeneration(); + if ( invalidateSharedGeneration && vkExec.temporalDirectHistoryValid + && vkExec.temporalDirectHistoryGeneration == generation ) { + R_TemporalPresentation_InvalidateHistory( reason ); + } + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalDirectHistoryGeneration = + vkExec.temporalHistoryGenerationSeen; + vkExec.temporalDirectHistoryReadIndex = 0; + vkExec.temporalDirectHistoryFrame = -1; + vkExec.temporalDirectHistoryViewIdentity = 0; + vkExec.temporalDirectHistoryValid = false; +} + +static bool VK_TemporalPresentation_EnsureDirectHistoryTargets( + int width, int height ) { + const int maxDimension = Min( 32767, + static_cast( vkCtx.deviceProperties.limits.maxImageDimension2D ) ); + if ( width <= 0 || height <= 0 || width > maxDimension + || height > maxDimension ) { + return false; + } + + const bool hadExtent = vkExec.temporalDirectHistoryWidth > 0 + && vkExec.temporalDirectHistoryHeight > 0; + const bool extentChanged = vkExec.temporalDirectHistoryWidth != width + || vkExec.temporalDirectHistoryHeight != height; + bool resourceChanged = hadExtent && extentChanged; + bool ready = true; + + idImageOpts opts; + opts.format = FMT_RGBA16F; + opts.colorFormat = CFM_DEFAULT; + opts.numLevels = 1; + opts.textureType = TT_2D; + opts.isPersistant = true; + opts.width = width; + opts.height = height; + opts.numMSAASamples = 0; + + for ( int i = 0; i < 2; ++i ) { + idRenderTexture *&target = + vkExec.temporalDirectHistoryRenderTextures[i]; + idImage *&image = vkExec.temporalDirectHistoryImages[i]; + if ( target != NULL && extentChanged ) { + resourceChanged = true; + (void)tr.ResizeRenderTexture( target, width, height ); + } + if ( target == NULL ) { + if ( image == NULL ) { + image = globalImages->ScratchImage( i == 0 + ? "_vkTemporalDirectHistory0" + : "_vkTemporalDirectHistory1", + &opts, TF_LINEAR, TR_CLAMP, TD_DEFAULT ); + } else if ( image->GetUploadWidth() != width + || image->GetUploadHeight() != height ) { + image->Resize( width, height ); + } + if ( image != NULL ) { + target = tr.CreateRenderTexture( image, NULL ); + } + if ( target != NULL ) { + target->SetDebugLabel( i == 0 + ? "Vulkan direct temporal history 0 (native)" + : "Vulkan direct temporal history 1 (native)" ); + } + } + + if ( target == NULL || image == NULL + || target->GetWidth() != width + || target->GetHeight() != height ) { + ready = false; + continue; + } + const unsigned long long storageGeneration = + static_cast( image->GetStorageGeneration() ); + if ( vkExec.temporalDirectHistoryStorageGenerations[i] != 0 + && vkExec.temporalDirectHistoryStorageGenerations[i] + != storageGeneration ) { + resourceChanged = true; + } + vkExec.temporalDirectHistoryStorageGenerations[i] = storageGeneration; + } + + if ( !ready ) { + VK_TemporalPresentation_ResetDirectHistory( + "Vulkan direct temporal history resource loss", true ); + if ( !vkExec.temporalDirectHistoryAllocationWarned ) { + vkExec.temporalDirectHistoryAllocationWarned = true; + common->Warning( + "Vulkan: native temporal history allocation failed; using spatial presentation" ); + } + return false; + } + if ( resourceChanged ) { + VK_TemporalPresentation_ResetDirectHistory( + "Vulkan direct temporal history resources changed", true ); + } + vkExec.temporalDirectHistoryWidth = width; + vkExec.temporalDirectHistoryHeight = height; + vkExec.temporalDirectHistoryAllocationWarned = false; + return true; +} + +static bool VK_TemporalPresentation_ResolvePendingSceneTemporal( + const viewDef_t *viewDef ) { + const temporalPresentationFrameState_t &presentation = + R_TemporalPresentation_GetFrameState(); + const bool captureFrame = tr.takingScreenshot + || presentation.captureFrozen || presentation.captureForcedNative + || ( viewDef != NULL && viewDef->temporalCaptureFrame ); + if ( viewDef == NULL || !presentation.temporalAARequested || captureFrame ) { + return false; + } + const int outputWidth = static_cast( vkCtx.swapchainExtent.width ); + const int outputHeight = static_cast( vkCtx.swapchainExtent.height ); + if ( outputWidth <= 0 || outputHeight <= 0 + || !VK_TemporalPresentation_EnsureDirectHistoryTargets( + outputWidth, outputHeight ) ) { + return false; + } + + const unsigned int generation = + R_TemporalPresentation_HistoryGeneration(); + const bool continuity = vkExec.temporalDirectHistoryValid + && vkExec.temporalDirectHistoryGeneration == generation + && vkExec.temporalDirectHistoryViewIdentity + == viewDef->temporalViewIdentity + && vkExec.temporalDirectHistoryFrame == backEnd.frameCount - 1 + && viewDef->temporalPrepared && viewDef->temporalHistoryValid + && viewDef->temporalPreviousProjectionValid + && viewDef->temporalHistoryGeneration == generation; + const int readIndex = vkExec.temporalDirectHistoryReadIndex; + const int writeIndex = readIndex ^ 1; + + resolveTemporalPresentationCommand_t command; + memset( &command, 0, sizeof( command ) ); + command.sceneColorTarget = vkExec.temporalSceneRenderTexture; + command.sceneDepthTarget = vkExec.temporalSceneRenderTexture; + command.historyReadTarget = continuity + ? vkExec.temporalDirectHistoryRenderTextures[readIndex] : NULL; + command.historyWriteTarget = + vkExec.temporalDirectHistoryRenderTextures[writeIndex]; + command.viewDef = viewDef; + command.presentation.frameNumber = backEnd.frameCount; + command.presentation.outputWidth = outputWidth; + command.presentation.outputHeight = outputHeight; + command.presentation.sceneWidth = vkExec.temporalSceneWidth; + command.presentation.sceneHeight = vkExec.temporalSceneHeight; + command.presentation.effectiveScalePercent = + presentation.effectiveScalePercent; + command.presentation.historyGeneration = generation; + command.presentation.dynamicResolutionRequested = + presentation.dynamicResolutionRequested; + command.presentation.dynamicResolutionActive = + presentation.dynamicResolutionActive; + command.presentation.temporalAARequested = true; + command.historyGeneration = generation; + command.historyValid = continuity; + command.feedback = presentation.temporalFeedback; + command.reactiveScale = presentation.temporalReactiveScale; + command.debugMode = presentation.temporalDebugMode; + command.advancedScreenSpace = presentation.advancedScreenSpace; + + bool historyAdvanced = false; + const bool presented = VK_TemporalPresentation_ResolveTargets( + command, false, &historyAdvanced ); + if ( !presented ) { + vkExec.temporalDirectHistoryValid = false; + vkExec.temporalDirectHistoryFrame = -1; + return false; + } + if ( historyAdvanced ) { + vkExec.temporalDirectHistoryReadIndex = writeIndex; + vkExec.temporalDirectHistoryGeneration = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalDirectHistoryViewIdentity = + viewDef->temporalViewIdentity; + vkExec.temporalDirectHistoryFrame = backEnd.frameCount; + vkExec.temporalDirectHistoryValid = + vkExec.temporalDirectHistoryGeneration == generation; + } else if ( vkExec.temporalDirectHistoryGeneration + != R_TemporalPresentation_HistoryGeneration() ) { + vkExec.temporalDirectHistoryGeneration = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalDirectHistoryValid = false; + vkExec.temporalDirectHistoryFrame = -1; + } + return true; +} + +static bool VK_TemporalPresentation_CompositePendingScene( void ) { + if ( !vkExec.temporalScenePendingComposite ) { + return true; + } + if ( !vkExec.frameOpen || vkExec.temporalSceneRenderTexture == NULL ) { + vkExec.temporalScenePendingComposite = false; + vkExec.temporalSceneRoot = NULL; + R_TemporalPresentation_InvalidateHistory( + "Vulkan temporal scene composite unavailable" ); + return false; + } + + vkImageEntry_t *sceneColor = NULL; + vkImageEntry_t *sceneDepth = NULL; + if ( !VK_Exec_RenderTextureEntries( vkExec.temporalSceneRenderTexture, + sceneColor, sceneDepth ) || sceneColor == NULL + || sceneColor->samples != VK_SAMPLE_COUNT_1_BIT + || ( sceneColor->usage & VK_IMAGE_USAGE_SAMPLED_BIT ) == 0 ) { + return false; + } + if ( vkExec.activeRenderTexture != vkExec.temporalSceneRenderTexture + && !VK_Exec_SetRenderTarget( vkExec.temporalSceneRenderTexture ) ) { + return false; + } + if ( !VK_Exec_SetRenderTarget( NULL ) ) { + return false; + } + const viewDef_t *sceneRoot = vkExec.temporalSceneRoot; + const bool depthReady = sceneDepth != NULL + && ( sceneDepth->usage & VK_IMAGE_USAGE_SAMPLED_BIT ) != 0; + // Let the resolver validate the exact depth-success stamp. A missing or + // stale depth image is a spatial/no-write result that must advance the + // shared invalidation generation rather than silently bypassing ownership. + const bool temporalPresented = + VK_TemporalPresentation_ResolvePendingSceneTemporal( sceneRoot ); + const bool presented = temporalPresented + || VK_TemporalPresentation_DrawPendingSceneSpatial( sceneRoot, + vkExec.temporalSceneColorImage, sceneColor, + depthReady ? vkExec.temporalSceneDepthImage : NULL, + depthReady ? sceneDepth : NULL ); + if ( presented ) { + vkExec.temporalScenePendingComposite = false; + vkExec.temporalSceneRoot = NULL; + if ( backEnd.renderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.renderTexture = NULL; + } + if ( backEnd.feedbackRenderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.feedbackRenderTexture = NULL; + } + return true; + } + + // Shader creation is capability-gated before the low-resolution target is + // selected. A failure here is therefore an execution-time resource fault; + // preserve a visible frame with the transfer fallback and retire history. + if ( vkExec.temporalSceneCompositeInvalidatedFrame + != backEnd.frameCount ) { + R_TemporalPresentation_InvalidateHistory( + "Vulkan temporal spatial composite execution failed" ); + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + vkExec.temporalSceneCompositeInvalidatedFrame = backEnd.frameCount; + } + if ( sceneRoot != NULL && sceneRoot->temporalJitterEnabled ) { + // VkImageBlit cannot express the fractional projection recenter required + // by a jittered scene. Fail closed instead of exposing an unstable frame; + // abandon this scene so a later frame cannot retry stale command-arena + // view state after the source frame has already been submitted. + vkExec.temporalScenePendingComposite = false; + vkExec.temporalSceneRoot = NULL; + if ( backEnd.renderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.renderTexture = NULL; + } + if ( backEnd.feedbackRenderTexture + == vkExec.temporalSceneRenderTexture ) { + backEnd.feedbackRenderTexture = NULL; + } + return false; + } + if ( ( sceneColor->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ) == 0 ) { + return false; + } + + VkFilter filter = VK_FILTER_NEAREST; + if ( !VK_TemporalPresentation_BlitSupported( sceneColor->format, filter ) ) { + return false; + } + VK_Exec_EndMainRendering(); + VK_Exec_TransitionImage( sceneColor, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ); + VK_Exec_TransitionSwapchain( VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ); + + VkImageBlit region; + memset( ®ion, 0, sizeof( region ) ); + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.srcSubresource.layerCount = 1; + region.srcOffsets[1].x = vkExec.temporalSceneWidth; + region.srcOffsets[1].y = vkExec.temporalSceneHeight; + region.srcOffsets[1].z = 1; + region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.dstSubresource.layerCount = 1; + region.dstOffsets[1].x = static_cast( vkCtx.swapchainExtent.width ); + region.dstOffsets[1].y = static_cast( vkCtx.swapchainExtent.height ); + region.dstOffsets[1].z = 1; + vkCmdBlitImage( vkExec.cmd, sceneColor->image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkCtx.swapchainImages[vkExec.swapImageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, filter ); + + VK_Exec_TransitionImage( sceneColor, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + VK_Exec_TransitionSwapchain( VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ); + const bool resumed = VK_Exec_BeginMainRendering( false ); + vkExec.temporalScenePendingComposite = false; + vkExec.temporalSceneRoot = NULL; + if ( backEnd.renderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.renderTexture = NULL; + } + if ( backEnd.feedbackRenderTexture == vkExec.temporalSceneRenderTexture ) { + backEnd.feedbackRenderTexture = NULL; + } + return resumed; +} + +static bool VK_TemporalPresentation_BeginView( const viewDef_t *viewDef, + vkSceneScaleState_t &scaleState ) { + VK_TemporalPresentation_ClearScaleState( scaleState ); + VK_TemporalPresentation_ObserveHistoryAndCapture(); + const viewDef_t *rootView = + VK_TemporalPresentation_RootForView( viewDef ); + if ( rootView == NULL ) { + return false; + } + + // feedbackRenderTexture is the explicit game-side opt-in: map its native + // root view to the actual attachment extent, but do not take allocation or + // final-post ownership away from the game render graph. + if ( backEnd.renderTexture != NULL + && backEnd.renderTexture == backEnd.feedbackRenderTexture + && vkExec.activeRenderTexture == backEnd.renderTexture ) { + const temporalPresentationFrameState_t &presentation = + R_TemporalPresentation_GetFrameState(); + if ( presentation.frameNumber != backEnd.frameCount + || !VK_TemporalPresentation_RootCoversNativeOutput( rootView ) + || presentation.nativeWidth != (int)vkCtx.swapchainExtent.width + || presentation.nativeHeight != (int)vkCtx.swapchainExtent.height + || presentation.sceneWidth != (int)vkExec.activeExtent.width + || presentation.sceneHeight != (int)vkExec.activeExtent.height ) { + return false; + } + return VK_TemporalPresentation_BeginScale( scaleState, viewDef, rootView, + (int)vkExec.activeExtent.width, (int)vkExec.activeExtent.height, false ); + } + if ( backEnd.renderTexture != NULL || vkExec.activeRenderTexture != NULL ) { + return false; + } + + int targetWidth = 0; + int targetHeight = 0; + if ( !VK_TemporalPresentation_BackendSceneRequested( rootView, + targetWidth, targetHeight ) + || !VK_TemporalPresentation_EnsureSceneTarget( + targetWidth, targetHeight ) ) { + return false; + } + if ( vkExec.temporalScenePendingComposite + && ( vkExec.temporalSceneFrame != backEnd.frameCount + || vkExec.temporalSceneRoot != rootView ) + && !VK_TemporalPresentation_CompositePendingScene() ) { + return false; + } + if ( !VK_Exec_SetRenderTarget( vkExec.temporalSceneRenderTexture ) ) { + return false; + } + if ( vkExec.temporalSceneFrame != backEnd.frameCount + || vkExec.temporalSceneRoot != rootView ) { + VK_Exec_ClearRenderTarget( true, true, 1.0f, vkExec.clearColor ); + vkExec.temporalSceneFrame = backEnd.frameCount; + vkExec.temporalSceneRoot = rootView; + } + vkExec.temporalScenePendingComposite = true; + if ( !VK_TemporalPresentation_BeginScale( scaleState, viewDef, rootView, + targetWidth, targetHeight, true ) ) { + (void)VK_Exec_SetRenderTarget( NULL ); + vkExec.temporalScenePendingComposite = false; + vkExec.temporalSceneRoot = NULL; + return false; + } + return true; +} + +static void VK_TemporalPresentation_EndView( const viewDef_t *viewDef, + vkSceneScaleState_t &scaleState, bool depthComplete ) { + VK_TemporalPresentation_RestoreDirectProjection( scaleState, viewDef ); + if ( !scaleState.active ) { + return; + } + const bool backendOwned = scaleState.backendOwned; + const bool rootView = viewDef == scaleState.rootView; + VK_TemporalPresentation_RestoreScale( scaleState, viewDef ); + if ( !backendOwned ) { + return; + } + if ( rootView ) { + // Backend-owned scene depth is single-sample, so a completed root view is + // the resolve seam. Stamp the exact frame, generation and storage owner; + // an aborted view must retire any earlier resident stamp before composite. + VK_TemporalPresentation_StampDepth( + vkExec.temporalSceneRenderTexture, depthComplete ); + (void)VK_TemporalPresentation_CompositePendingScene(); + } else { + (void)VK_Exec_SetRenderTarget( NULL ); + } +} + +static bool VK_Exec_PrepareCopyDestination( idImage *image, int width, int height, + vkImageEntry_t *&entry ) { + entry = NULL; + if ( image == NULL || width <= 0 || height <= 0 ) { + return false; + } + if ( image->GetUploadWidth() != width || image->GetUploadHeight() != height ) { + image->Resize( width, height ); + } + entry = VK_Image_GetEntry( image->GetDeviceHandle() ); + return entry != NULL && ( entry->usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT ) != 0; +} + +bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, + int cubeFace, bool copyDepth ) { + if ( !VK_GuiExecutor_BeginFrame() || image == NULL || width <= 0 || height <= 0 ) { + return false; + } + const idImageOpts &imageOpts = image->GetOpts(); + const bool targetIsCube = imageOpts.textureType == TT_CUBIC; + if ( ( imageOpts.textureType != TT_2D && !targetIsCube ) + || ( targetIsCube && ( cubeFace < 0 || cubeFace >= 6 ) ) + || ( !targetIsCube && cubeFace != 0 ) ) { + return false; + } + const bool targetIsDepth = imageOpts.format == FMT_DEPTH + || imageOpts.format == FMT_DEPTH_STENCIL; + if ( copyDepth != targetIsDepth ) { + return false; + } + + VkImage sourceImage = VK_NULL_HANDLE; + VkFormat sourceFormat = VK_FORMAT_UNDEFINED; + VkImageAspectFlags copyAspect = copyDepth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + vkImageEntry_t *sourceEntry = NULL; + if ( copyDepth ) { + sourceEntry = vkExec.activeRenderTexture != NULL ? vkExec.activeDepthEntry : NULL; + sourceImage = sourceEntry != NULL ? sourceEntry->image : vkCtx.depthImages[ vkExec.frameSlot ]; + sourceFormat = sourceEntry != NULL ? sourceEntry->format : vkCtx.depthFormat; + } else { + sourceEntry = vkExec.activeRenderTexture != NULL ? vkExec.activeColorEntry : NULL; + sourceImage = sourceEntry != NULL ? sourceEntry->image : vkCtx.swapchainImages[ vkExec.swapImageIndex ]; + sourceFormat = sourceEntry != NULL ? sourceEntry->format : vkCtx.swapchainFormat; + } + if ( sourceImage == VK_NULL_HANDLE + || ( sourceEntry == NULL && !copyDepth && !vkCtx.swapchainTransferSrc ) + || ( sourceEntry != NULL && ( sourceEntry->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ) == 0 ) + || ( sourceEntry != NULL && sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT ) ) { + return false; + } + + const int sourceWidth = VK_Exec_ActiveFramebufferWidth(); + const int sourceHeight = VK_Exec_ActiveFramebufferHeight(); + if ( x < 0 ) { + width += x; + x = 0; + } + if ( y < 0 ) { + height += y; + y = 0; + } + if ( x + width > sourceWidth ) { + width = sourceWidth - x; + } + if ( y + height > sourceHeight ) { + height = sourceHeight - y; + } + if ( width <= 0 || height <= 0 ) { + return false; + } + + // Validate aliasing before a resize/re-back can retire the destination. + vkImageEntry_t *destination = VK_Image_GetEntry( image->GetDeviceHandle() ); + if ( destination != NULL && destination->image == sourceImage ) { + return false; + } + if ( copyDepth ) { + if ( !VK_Image_MakeDepthCopyTarget( image, width, height, sourceFormat ) ) { + return false; + } + destination = VK_Image_GetEntry( image->GetDeviceHandle() ); } else if ( !VK_Exec_PrepareCopyDestination( image, width, height, destination ) ) { return false; } if ( destination == NULL || destination->image == sourceImage || destination->samples != VK_SAMPLE_COUNT_1_BIT || ( destination->usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT ) == 0 - || ( copyDepth && destination->format != sourceFormat ) ) { + || ( copyDepth && destination->format != sourceFormat ) + || destination->isCube != targetIsCube + || destination->numLayers != ( targetIsCube ? 6 : 1 ) ) { return false; } @@ -3074,38 +4683,64 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ); if ( copyDepth ) { - // vkCmdBlitImage is not universally supported for depth formats and a - // copy cannot use reversed coordinates. Emit one exact-format copy per - // row to preserve the GL bottom-left capture orientation. - VkImageCopy *rows = (VkImageCopy *)Mem_Alloc( height * sizeof( VkImageCopy ) ); - if ( rows == NULL ) { - VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); - if ( sourceEntry != NULL ) { - VK_Exec_TransitionImage( sourceEntry, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL ); - } else { - VK_Exec_TransitionSwapchainDepth( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL ); + if ( VK_Exec_DepthBlitSupported( sourceFormat ) ) { + // One flipped NEAREST blit reproduces the per-row copy's GL + // bottom-left orientation texel-exactly (identical format + NEAREST), + // without allocating a per-row region array. Same offset convention + // as the colour blit path below. + VkImageBlit depthBlit; + memset( &depthBlit, 0, sizeof( depthBlit ) ); + depthBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + depthBlit.srcSubresource.layerCount = 1; + depthBlit.srcOffsets[ 0 ].x = x; + depthBlit.srcOffsets[ 0 ].y = sourceHeight - y; + depthBlit.srcOffsets[ 1 ].x = x + width; + depthBlit.srcOffsets[ 1 ].y = sourceHeight - y - height; + depthBlit.srcOffsets[ 1 ].z = 1; + depthBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + depthBlit.dstSubresource.baseArrayLayer = targetIsCube ? (uint32_t)cubeFace : 0; + depthBlit.dstSubresource.layerCount = 1; + depthBlit.dstOffsets[ 1 ].x = width; + depthBlit.dstOffsets[ 1 ].y = height; + depthBlit.dstOffsets[ 1 ].z = 1; + vkCmdBlitImage( vkExec.cmd, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &depthBlit, VK_FILTER_NEAREST ); + } else { + // Depth blit unsupported on this device: emit one exact-format copy + // per row to preserve the GL bottom-left capture orientation. + VkImageCopy *rows = (VkImageCopy *)Mem_Alloc( height * sizeof( VkImageCopy ) ); + if ( rows == NULL ) { + VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + if ( sourceEntry != NULL ) { + VK_Exec_TransitionImage( sourceEntry, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL ); + } else { + VK_Exec_TransitionSwapchainDepth( VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL ); + } + VK_Exec_BeginMainRendering( false ); + return false; } - VK_Exec_BeginMainRendering( false ); - return false; + memset( rows, 0, height * sizeof( VkImageCopy ) ); + for ( int row = 0; row < height; row++ ) { + rows[ row ].srcSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + rows[ row ].srcSubresource.layerCount = 1; + rows[ row ].srcOffset.x = x; + rows[ row ].srcOffset.y = sourceHeight - 1 - ( y + row ); + rows[ row ].dstSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + rows[ row ].dstSubresource.baseArrayLayer = targetIsCube + ? (uint32_t)cubeFace : 0; + rows[ row ].dstSubresource.layerCount = 1; + rows[ row ].dstOffset.y = row; + rows[ row ].extent.width = (uint32_t)width; + rows[ row ].extent.height = 1; + rows[ row ].extent.depth = 1; + } + vkCmdCopyImage( vkExec.cmd, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + destination->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + (uint32_t)height, rows ); + Mem_Free( rows ); } - memset( rows, 0, height * sizeof( VkImageCopy ) ); - for ( int row = 0; row < height; row++ ) { - rows[ row ].srcSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - rows[ row ].srcSubresource.layerCount = 1; - rows[ row ].srcOffset.x = x; - rows[ row ].srcOffset.y = sourceHeight - 1 - ( y + row ); - rows[ row ].dstSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; - rows[ row ].dstSubresource.layerCount = 1; - rows[ row ].dstOffset.y = row; - rows[ row ].extent.width = (uint32_t)width; - rows[ row ].extent.height = 1; - rows[ row ].extent.depth = 1; - } - vkCmdCopyImage( vkExec.cmd, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - destination->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - (uint32_t)height, rows ); - Mem_Free( rows ); destination->everUploaded = true; VK_Exec_TransitionImage( destination, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); @@ -3129,8 +4764,7 @@ bool VK_Exec_CopyRender( idImage *image, int x, int y, int width, int height, region.srcOffsets[ 1 ].y = sourceHeight - y - height; region.srcOffsets[ 1 ].z = 1; region.dstSubresource.aspectMask = copyAspect; - region.dstSubresource.baseArrayLayer = destination->isCube - ? (uint32_t)Max( 0, Min( cubeFace, destination->numLayers - 1 ) ) : 0; + region.dstSubresource.baseArrayLayer = targetIsCube ? (uint32_t)cubeFace : 0; region.dstSubresource.layerCount = 1; region.dstOffsets[ 1 ].x = width; region.dstOffsets[ 1 ].y = height; @@ -3481,6 +5115,12 @@ static bool VK_Exec_ResolveDepthImage( vkImageEntry_t *sourceDepth, bool VK_Exec_ResolveRenderTargets( idRenderTexture *sourceRenderTexture, idRenderTexture *destinationRenderTexture, bool resolveDepth ) { + if ( resolveDepth ) { + // Retire a resident success stamp before any validation or copy work. A + // failed MSAA depth resolve must never make stale depth look current. + VK_TemporalPresentation_StampDepth( + destinationRenderTexture, false ); + } if ( !VK_GuiExecutor_BeginFrame() || sourceRenderTexture == NULL || destinationRenderTexture == NULL ) { return false; @@ -3560,6 +5200,8 @@ bool VK_Exec_ResolveRenderTargets( idRenderTexture *sourceRenderTexture, common->Warning( "Vulkan: requested depth resolve failed; color resolve completed" ); } } + VK_TemporalPresentation_StampDepth( + destinationRenderTexture, depthResolved ); } VK_Exec_TransitionActiveTargetToAttachments(); @@ -3572,6 +5214,11 @@ bool VK_GuiExecutor_ReadPixels( int x, int y, int width, int height, void *pixel || !vkCtx.swapchainTransferSrc ) { return false; } + VK_TemporalPresentation_ObserveHistoryAndCapture(); + if ( vkExec.temporalScenePendingComposite + && !VK_TemporalPresentation_CompositePendingScene() ) { + return false; + } if ( vkExec.activeRenderTexture != NULL && !VK_Exec_SetRenderTarget( NULL ) ) { return false; } @@ -3653,11 +5300,20 @@ bool VK_GuiExecutor_ReadPixels( int x, int y, int width, int height, void *pixel // the synchronous readback, then resume recording against this acquired // swapchain image for the actual frame. const bool resumeAfterReadback = tr.takingScreenshot; + R_RendererMetrics_ResetGpuFrameTiming( "Vulkan synchronous screenshot readback" ); if ( !VK_GuiExecutor_SubmitFrame( !resumeAfterReadback ) ) { vmaDestroyBuffer( vkCtx.allocator, readbackBuffer, readbackAllocation ); return false; } - vkWaitForFences( vkCtx.device, 1, &vkCtx.frameFences[ submittedSlot ], VK_TRUE, UINT64_MAX ); + const VkResult readbackFenceResult = vkWaitForFences( vkCtx.device, 1, + &vkCtx.frameFences[ submittedSlot ], VK_TRUE, UINT64_MAX ); + if ( readbackFenceResult != VK_SUCCESS ) { + VK_GpuFrameTiming_SubmitFailed( submittedSlot ); + common->Warning( "Vulkan: screenshot readback fence wait failed (%d)", + static_cast( readbackFenceResult ) ); + vmaDestroyBuffer( vkCtx.allocator, readbackBuffer, readbackAllocation ); + return false; + } vmaInvalidateAllocation( vkCtx.allocator, readbackAllocation, 0, VK_WHOLE_SIZE ); const byte *source = (const byte *)allocationInfo.pMappedData; @@ -3684,14 +5340,21 @@ bool VK_GuiExecutor_ReadPixels( int x, int y, int width, int height, void *pixel memset( &cbbi, 0, sizeof( cbbi ) ); cbbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; cbbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - if ( vkResetFences( vkCtx.device, 1, &vkCtx.frameFences[ submittedSlot ] ) != VK_SUCCESS - || vkResetCommandBuffer( vkExec.cmd, 0 ) != VK_SUCCESS - || vkBeginCommandBuffer( vkExec.cmd, &cbbi ) != VK_SUCCESS ) { + // Reset the command buffer and begin recording BEFORE resetting the slot + // fence: if the command-buffer reset/begin fails, the fence stays + // signaled so the next BeginFrame's fence wait on this slot cannot + // deadlock on a fence that will never be submitted. + if ( vkResetCommandBuffer( vkExec.cmd, 0 ) != VK_SUCCESS + || vkBeginCommandBuffer( vkExec.cmd, &cbbi ) != VK_SUCCESS + || vkResetFences( vkCtx.device, 1, &vkCtx.frameFences[ submittedSlot ] ) != VK_SUCCESS ) { common->Warning( "Vulkan: failed to resume rendering after screenshot readback" ); return false; } vkExec.frameOpen = true; vkExec.mainScopeOpen = false; + // The screenshot path explicitly retired this same slot; start a fresh + // timing epoch for the real frame recorded after the crop. + VK_GpuFrameTiming_BeginFrame( vkExec.cmd, submittedSlot, tr.frameCount ); VK_Exec_BeginMainRendering( true ); } return true; @@ -3705,6 +5368,11 @@ static bool VK_GuiExecutor_SubmitFrame( bool present ) { const uint32_t imageIndex = vkExec.swapImageIndex; VkCommandBuffer cmd = vkExec.cmd; + // submit any batched image uploads first: same-queue submission order makes + // them execute before this frame samples the images, and this frame's fence + // then covers them for deferred-destroy retirement + VK_Device_FlushUploadBatch(); + VK_Exec_EndMainRendering(); VK_Exec_TransitionActiveTargetToSampled(); @@ -3729,8 +5397,27 @@ static bool VK_GuiExecutor_SubmitFrame( bool present ) { vkCmdPipelineBarrier2( cmd, &dep ); } + VK_GpuFrameTiming_EndFrame( cmd, slot ); vkEndCommandBuffer( cmd ); + // Flush each ring's host-written prefix once, before the queue submit that + // consumes it, instead of once per allocation. vmaFlushAllocation rounds to + // nonCoherentAtomSize and clamps to the allocation, and is a no-op on the + // HOST_COHERENT memory VMA hands us on desktop. + { + vkRing_t *frameRings[ 3 ] = { + &vkExec.vertexRings[ slot ], &vkExec.indexRings[ slot ], &vkExec.uniformRings[ slot ] }; + for ( int r = 0; r < 3; r++ ) { + if ( frameRings[ r ]->mapped != NULL && frameRings[ r ]->cursor > 0 ) { + const VkResult flushResult = vmaFlushAllocation( vkCtx.allocator, + frameRings[ r ]->allocation, 0, (VkDeviceSize)frameRings[ r ]->cursor ); + if ( flushResult != VK_SUCCESS ) { + common->Warning( "Vulkan: frame ring flush failed (%d)", (int)flushResult ); + } + } + } + } + VkSemaphoreSubmitInfo waitInfo; memset( &waitInfo, 0, sizeof( waitInfo ) ); waitInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; @@ -3755,6 +5442,27 @@ static bool VK_GuiExecutor_SubmitFrame( bool present ) { si.signalSemaphoreInfoCount = present ? 1 : 0; si.pSignalSemaphoreInfos = present ? &signalInfo : NULL; if ( vkQueueSubmit2( vkCtx.graphicsQueue, 1, &si, vkCtx.frameFences[ slot ] ) != VK_SUCCESS ) { + VK_GpuFrameTiming_SubmitFailed( slot ); + // The slot fence was reset in BeginFrame; a failed submit never signals + // it, so a later vkWaitForFences on this slot (next BeginFrame, or a + // screenshot readback) would block forever. The failed submit enqueued + // nothing, so the reset fence is idle and can be replaced with a fresh + // signaled one. Swap through a temporary so a create failure leaves the + // old handle intact rather than a NULL handle. Also clear the frame-open + // state: the command buffer is already ended, so the next BeginFrame must + // start fresh rather than resume recording into it. + vkExec.acquireWaitPending = false; + vkExec.frameOpen = false; + VkFenceCreateInfo fci; + memset( &fci, 0, sizeof( fci ) ); + fci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + VkFence recoveredFence = VK_NULL_HANDLE; + if ( vkCreateFence( vkCtx.device, &fci, NULL, &recoveredFence ) == VK_SUCCESS ) { + vkDestroyFence( vkCtx.device, vkCtx.frameFences[ slot ], NULL ); + vkCtx.frameFences[ slot ] = recoveredFence; + } + common->Warning( "Vulkan: frame submit failed on slot %d; recovered frame state", slot ); return false; } vkExec.acquireWaitPending = false; @@ -3959,17 +5667,319 @@ static bool VK_Exec_FlattenPackedAmbientIndexes( const srfTriangles_t *tri, } #endif -// memoized ring upload + bind: the depth fill and both ambient walks visit -// the same tris; a hit re-binds without re-copying. Also serves the -// interaction pass, where the light-tris chains carry their own index -// subset over the shared ambient vertex cache (distinct idxKey, so memo -// entries never alias across the subsets). -bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_t *tri ) { +static int VK_GpuSkinning_FindMemo( const void *ambientKey, bool reserve, + bool &existing ) { + existing = false; + if ( ambientKey == NULL ) { + return -1; + } + const unsigned int first = static_cast( + ( reinterpret_cast( ambientKey ) >> 4 ) + & ( VK_GPU_SKINNING_MEMO_SIZE - 1 ) ); + for ( int probe = 0; probe < VK_GPU_SKINNING_MEMO_SIZE; ++probe ) { + const int index = static_cast( ( first + static_cast( probe ) ) + & ( VK_GPU_SKINNING_MEMO_SIZE - 1 ) ); + vkGpuSkinningMemo_t &memo = vkExec.gpuSkinningMemo[ index ]; + if ( memo.ambientKey == ambientKey ) { + existing = true; + return index; + } + if ( memo.ambientKey == NULL ) { + if ( reserve ) { + memo.ambientKey = ambientKey; + memo.vertexOffset = -1; + } + return index; + } + } + return -1; +} + +/* +==================== +VK_GpuSkinning_PrepareView + +The front end cannot dispatch while building dynamic models, so Vulkan keeps +its CPU ambient cache as a correctness fallback. Immediately before a 3D view +starts issuing caller state, gather each unique eligible surface, interrupt +the active dynamic-rendering scope once, and materialize ordinary idDrawVert +output in the frame vertex ring. The ambient-cache handle remains the draw +memo key, so depth, interactions, ambient passes, shadow maps, subviews, and +view models all consume the same result without learning a skinning ABI. +==================== +*/ +static bool VK_GpuSkinning_PrepareView( const viewDef_t *viewDef ) { + if ( viewDef == NULL || viewDef->drawSurfs == NULL || viewDef->numDrawSurfs <= 0 + || !r_gpuSkinning.GetBool() || !vkGpuSkinningBackendRequested + || !vkExec.gpuSkinningAvailable || !vkExec.frameOpen + || !vkExec.mainScopeOpen || vkExec.cmd == VK_NULL_HANDLE + || vkExec.frameSlot < 0 || vkExec.frameSlot >= VK_FRAMES_IN_FLIGHT ) { + return true; + } + + const int slot = vkExec.frameSlot; + vkRing_t &ring = vkExec.vertexRings[ slot ]; + const VkDeviceSize nonCoherentAtomSize = + vkCtx.deviceProperties.limits.nonCoherentAtomSize; + if ( nonCoherentAtomSize == 0 || nonCoherentAtomSize > static_cast( INT_MAX ) + || ( nonCoherentAtomSize & ( nonCoherentAtomSize - 1 ) ) != 0 ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + return true; + } + const int deviceWriteAlignment = Max( 64, static_cast( nonCoherentAtomSize ) ); + idTempArray dispatches( + static_cast( viewDef->numDrawSurfs ) ); + int dispatchCount = 0; + int firstHostWrite = INT_MAX; + int lastHostWrite = 0; + int firstOutput = INT_MAX; + int lastOutput = 0; + + drawSurf_t **drawSurfs = reinterpret_cast( viewDef->drawSurfs ); + for ( int surfNum = 0; surfNum < viewDef->numDrawSurfs; ++surfNum ) { + const drawSurf_t *drawSurf = drawSurfs[ surfNum ]; + const srfTriangles_t *tri = drawSurf != NULL ? drawSurf->geo : NULL; + if ( tri == NULL || tri->ambientCache == NULL || tri->numVerts <= 0 + || !R_GpuSkinning_IsCandidate( tri ) ) { + continue; + } + bool existing = false; + if ( VK_GpuSkinning_FindMemo( tri->ambientCache, false, existing ) < 0 ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + vkExec.gpuSkinningMemoFallbacks++; + continue; + } + if ( existing ) { + continue; + } + + gpuSkinningSurface_t surface; + if ( !R_GpuSkinning_GetSurface( tri, surface ) ) { + R_GpuSkinning_RecordFallback( surface.fallbackReason ); + vkExec.gpuSkinningValidationFallbacks++; + continue; + } + const gpuSkinningFallbackReason_t validation = + R_GpuSkinning_ValidateSurface( surface ); + if ( validation != GPU_SKINNING_FALLBACK_NONE + || surface.fallbackReason != GPU_SKINNING_FALLBACK_NONE + || surface.numVerts != tri->numVerts + || surface.palette.matrixStrideFloats != GPU_SKINNING_JOINT_FLOATS ) { + const gpuSkinningFallbackReason_t reason = + validation != GPU_SKINNING_FALLBACK_NONE ? validation + : ( surface.fallbackReason != GPU_SKINNING_FALLBACK_NONE + ? surface.fallbackReason + : ( surface.numVerts != tri->numVerts + ? GPU_SKINNING_FALLBACK_VERTEX_COUNT + : GPU_SKINNING_FALLBACK_STALE_PALETTE ) ); + R_GpuSkinning_RecordFallback( reason ); + vkExec.gpuSkinningValidationFallbacks++; + continue; + } + + const size_t sourceBytes = static_cast( surface.numVerts ) + * sizeof( idDrawVert ); + const size_t skinBytes = static_cast( surface.numVerts ) + * sizeof( gpuSkinningVertex_t ); + const size_t jointBytes = static_cast( surface.palette.numJoints ) + * GPU_SKINNING_JOINT_FLOATS * sizeof( float ); + if ( sourceBytes == 0 || sourceBytes > static_cast( INT_MAX ) + || skinBytes == 0 || skinBytes > static_cast( INT_MAX ) + || jointBytes == 0 || jointBytes > static_cast( INT_MAX ) ) { + R_GpuSkinning_RecordFallback( sourceBytes == 0 + || sourceBytes > static_cast( INT_MAX ) + || skinBytes == 0 || skinBytes > static_cast( INT_MAX ) + ? GPU_SKINNING_FALLBACK_VERTEX_COUNT + : GPU_SKINNING_FALLBACK_JOINT_COUNT ); + vkExec.gpuSkinningValidationFallbacks++; + continue; + } + const size_t deviceWriteMask = static_cast( deviceWriteAlignment - 1 ); + if ( sourceBytes > static_cast( INT_MAX ) - deviceWriteMask ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_VERTEX_COUNT ); + vkExec.gpuSkinningValidationFallbacks++; + continue; + } + const size_t outputReserveBytes = ( sourceBytes + deviceWriteMask ) + & ~deviceWriteMask; + const uint32_t groupCount = static_cast( + ( surface.numVerts + VK_GPU_SKINNING_WORKGROUP_SIZE - 1 ) + / VK_GPU_SKINNING_WORKGROUP_SIZE ); + if ( groupCount == 0 + || groupCount > vkCtx.deviceProperties.limits.maxComputeWorkGroupCount[ 0 ] ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_VERTEX_COUNT ); + vkExec.gpuSkinningValidationFallbacks++; + continue; + } + + const int savedCursor = ring.cursor; + const int sourceOffset = VK_Ring_Alloc( ring, surface.bindPoseVerts, + sourceBytes, 64 ); + const int skinOffset = sourceOffset >= 0 + ? VK_Ring_Alloc( ring, surface.skinVerts, skinBytes, 16 ) : -1; + const int jointOffset = skinOffset >= 0 + ? VK_Ring_Alloc( ring, surface.palette.matrices, jointBytes, 16 ) : -1; + const int outputOffset = jointOffset >= 0 + ? VK_Ring_Reserve( ring, outputReserveBytes, + deviceWriteAlignment ) : -1; + if ( sourceOffset < 0 || skinOffset < 0 || jointOffset < 0 || outputOffset < 0 ) { + ring.cursor = savedCursor; + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_PALETTE_ALLOCATION ); + vkExec.gpuSkinningRingFallbacks++; + continue; + } + bool insertedExisting = false; + const int memoIndex = VK_GpuSkinning_FindMemo( + tri->ambientCache, true, insertedExisting ); + if ( memoIndex < 0 || insertedExisting ) { + ring.cursor = savedCursor; + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + vkExec.gpuSkinningMemoFallbacks++; + continue; + } + + vkGpuSkinningDispatch_t &dispatch = dispatches[ dispatchCount++ ]; + dispatch.memoIndex = memoIndex; + dispatch.sourceOffset = sourceOffset; + dispatch.skinOffset = skinOffset; + dispatch.jointOffset = jointOffset; + dispatch.outputOffset = outputOffset; + dispatch.sourceBytes = static_cast( sourceBytes ); + dispatch.skinBytes = static_cast( skinBytes ); + dispatch.jointBytes = static_cast( jointBytes ); + dispatch.outputBytes = static_cast( sourceBytes ); + dispatch.vertexCount = static_cast( surface.numVerts ); + dispatch.jointCount = static_cast( surface.palette.numJoints ); + firstHostWrite = Min( firstHostWrite, sourceOffset ); + lastHostWrite = Max( lastHostWrite, jointOffset + dispatch.jointBytes ); + firstOutput = Min( firstOutput, outputOffset ); + lastOutput = Max( lastOutput, outputOffset + dispatch.outputBytes ); + } + + if ( dispatchCount == 0 ) { + return true; + } + + VK_Exec_EndMainRendering(); + + VkBufferMemoryBarrier2 hostToCompute; + memset( &hostToCompute, 0, sizeof( hostToCompute ) ); + hostToCompute.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; + hostToCompute.srcStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; + hostToCompute.srcAccessMask = VK_ACCESS_2_HOST_WRITE_BIT; + hostToCompute.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + hostToCompute.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT; + hostToCompute.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + hostToCompute.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + hostToCompute.buffer = ring.buffer; + hostToCompute.offset = static_cast( firstHostWrite ); + hostToCompute.size = static_cast( lastHostWrite - firstHostWrite ); + VkDependencyInfo dependency; + memset( &dependency, 0, sizeof( dependency ) ); + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.bufferMemoryBarrierCount = 1; + dependency.pBufferMemoryBarriers = &hostToCompute; + vkCmdPipelineBarrier2( vkExec.cmd, &dependency ); + + vkCmdBindPipeline( vkExec.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, + vkExec.gpuSkinningPipeline ); + vkCmdBindDescriptorSets( vkExec.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, + vkExec.gpuSkinningPipelineLayout, 0, 1, + &vkExec.gpuSkinningSets[ slot ], 0, NULL ); + for ( int dispatchIndex = 0; dispatchIndex < dispatchCount; ++dispatchIndex ) { + const vkGpuSkinningDispatch_t &dispatch = dispatches[ dispatchIndex ]; + vkGpuSkinningPushConstants_t push; + push.sourceWordOffset = static_cast( dispatch.sourceOffset / 4 ); + push.skinWordOffset = static_cast( dispatch.skinOffset / 4 ); + push.jointWordOffset = static_cast( dispatch.jointOffset / 4 ); + push.outputWordOffset = static_cast( dispatch.outputOffset / 4 ); + push.vertexCount = dispatch.vertexCount; + push.jointCount = dispatch.jointCount; + vkCmdPushConstants( vkExec.cmd, vkExec.gpuSkinningPipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof( push ), &push ); + vkCmdDispatch( vkExec.cmd, + ( dispatch.vertexCount + VK_GPU_SKINNING_WORKGROUP_SIZE - 1 ) + / VK_GPU_SKINNING_WORKGROUP_SIZE, 1, 1 ); + } + + VkBufferMemoryBarrier2 computeToVertex; + memset( &computeToVertex, 0, sizeof( computeToVertex ) ); + computeToVertex.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2; + computeToVertex.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + computeToVertex.srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT; + computeToVertex.dstStageMask = VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT; + computeToVertex.dstAccessMask = VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT; + computeToVertex.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + computeToVertex.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + computeToVertex.buffer = ring.buffer; + computeToVertex.offset = static_cast( firstOutput ); + computeToVertex.size = static_cast( lastOutput - firstOutput ); + dependency.pBufferMemoryBarriers = &computeToVertex; + vkCmdPipelineBarrier2( vkExec.cmd, &dependency ); + + if ( !VK_Exec_BeginMainRendering( false ) ) { + for ( int dispatchIndex = 0; dispatchIndex < dispatchCount; ++dispatchIndex ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + } + common->Warning( "Vulkan: GPU skinning could not resume main rendering; view skipped" ); + return false; + } + + for ( int dispatchIndex = 0; dispatchIndex < dispatchCount; ++dispatchIndex ) { + const vkGpuSkinningDispatch_t &dispatch = dispatches[ dispatchIndex ]; + vkExec.gpuSkinningMemo[ dispatch.memoIndex ].vertexOffset = dispatch.outputOffset; + vkExec.gpuSkinningDispatches++; + vkExec.gpuSkinningVertices += dispatch.vertexCount; + R_GpuSkinning_RecordBackendPrepared( + static_cast( dispatch.vertexCount ), + static_cast( dispatch.jointCount ), + static_cast( dispatch.jointBytes ) ); + } + + static bool loggedFirstDispatch = false; + if ( !loggedFirstDispatch ) { + loggedFirstDispatch = true; + common->Printf( "Vulkan: GPU skinning dispatched %d surfaces (%llu vertices)\n", + dispatchCount, static_cast( vkExec.gpuSkinningVertices ) ); + } + return true; +} + +// Memoized ring upload: the depth fill and both ambient walks visit the same +// tris, so a hit retains the existing offsets without re-copying. Binding is +// deliberately separate so speculative shared-domain preflight records no +// graphics state. The same upload path also serves the +// interaction pass, where the light-tris chains carry their own index +// subset over the shared ambient vertex cache (distinct idxKey, so memo +// entries never alias across the subsets). +static bool VK_Exec_CPUCacheValid( const vertCache_t *cache, + const bool indexBuffer, const size_t requiredBytes ) { + return cache != NULL && requiredBytes > 0 + && requiredBytes <= static_cast( INT_MAX ) + && cache->tag != TAG_FREE + && cache->indexBuffer == indexBuffer && cache->offset >= 0 + && cache->size >= static_cast( requiredBytes ) + && cache->vbo == 0 && cache->virtMem != NULL; +} + +static bool VK_Exec_UploadTriGeometry( int slot, const srfTriangles_t *tri, + int &vertexOffset, int &indexOffset ) { + vertexOffset = -1; + indexOffset = -1; if ( tri == NULL || tri->ambientCache == NULL || tri->numVerts <= 0 || tri->numIndexes <= 0 || slot < 0 || slot >= VK_FRAMES_IN_FLIGHT ) { return false; } + const size_t vertexBytes = static_cast( tri->numVerts ) + * sizeof( idDrawVert ); + const size_t exactIndexBytes = static_cast( tri->numIndexes ) * sizeof( glIndex_t ); + if ( !VK_Exec_CPUCacheValid( tri->ambientCache, false, vertexBytes ) + || ( tri->indexes == NULL && tri->indexCache != NULL + && !VK_Exec_CPUCacheValid( tri->indexCache, true, + exactIndexBytes ) ) ) { + return false; + } const void *vertKey = tri->ambientCache; const void *idxKey = tri->indexes != NULL @@ -3981,34 +5991,39 @@ bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_ // independent vert/index memos: light-tris chains carry their own index // subset over the SHARED ambient vertex array, so a combined memo would // re-upload the full vertex payload once per light per surface - int vertexOffset; { - const unsigned int memoIndex = (unsigned int)( ( ( (uintptr_t)vertKey ) >> 4 ) & ( VK_TRI_MEMO_SIZE - 1 ) ); - vkVertUpload_t &memo = vkExec.vertMemo[ memoIndex ]; - if ( memo.vertKey == vertKey && vertKey != NULL ) { - vertexOffset = memo.vertexOffset; + bool gpuMemoExisting = false; + const int gpuMemoIndex = VK_GpuSkinning_FindMemo( + vertKey, false, gpuMemoExisting ); + if ( gpuMemoExisting && gpuMemoIndex >= 0 + && vkExec.gpuSkinningMemo[ gpuMemoIndex ].vertexOffset >= 0 ) { + vertexOffset = vkExec.gpuSkinningMemo[ gpuMemoIndex ].vertexOffset; } else { - const idDrawVert *verts = (const idDrawVert *)vertexCache.Position( tri->ambientCache ); - vertexOffset = VK_Ring_Alloc( vkExec.vertexRings[ slot ], verts, - static_cast( tri->numVerts ) * sizeof( idDrawVert ), 64 ); - if ( vertexOffset < 0 ) { - return false; - } - if ( vertKey != NULL ) { - memo.vertKey = vertKey; - memo.vertexOffset = vertexOffset; + const unsigned int memoIndex = (unsigned int)( ( ( (uintptr_t)vertKey ) >> 4 ) & ( VK_TRI_MEMO_SIZE - 1 ) ); + vkVertUpload_t &memo = vkExec.vertMemo[ memoIndex ]; + if ( memo.vertKey == vertKey && vertKey != NULL ) { + vertexOffset = memo.vertexOffset; + } else { + const idDrawVert *verts = (const idDrawVert *)vertexCache.Position( tri->ambientCache ); + vertexOffset = VK_Ring_Alloc( vkExec.vertexRings[ slot ], verts, + static_cast( tri->numVerts ) * sizeof( idDrawVert ), 64 ); + if ( vertexOffset < 0 ) { + return false; + } + if ( vertKey != NULL ) { + memo.vertKey = vertKey; + memo.vertexOffset = vertexOffset; + } } } } - int indexOffset; { const unsigned int memoIndex = (unsigned int)( ( ( (uintptr_t)idxKey ) >> 4 ) & ( VK_TRI_MEMO_SIZE - 1 ) ); vkIdxUpload_t &memo = vkExec.idxMemo[ memoIndex ]; if ( memo.idxKey == idxKey && idxKey != NULL ) { indexOffset = memo.indexOffset; } else { - const size_t indexBytes = - static_cast( tri->numIndexes ) * sizeof( glIndex_t ); + const size_t indexBytes = exactIndexBytes; if ( indexBytes > static_cast( vkExec.indexRings[ slot ].capacity ) ) { return false; } @@ -4039,8 +6054,8 @@ bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_ if ( indexSource != NULL ) { indexOffset = VK_Ring_Alloc( vkExec.indexRings[ slot ], indexSource, indexBytes, 4 ); - } else if ( tri->indexCache != NULL - && tri->indexCache->size >= static_cast( indexBytes ) ) { + } else if ( VK_Exec_CPUCacheValid( tri->indexCache, true, + indexBytes ) ) { const glIndex_t *cachedIndexes = static_cast( vertexCache.Position( tri->indexCache ) ); indexOffset = VK_Ring_Alloc( vkExec.indexRings[ slot ], @@ -4058,9 +6073,25 @@ bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_ } } - VkDeviceSize vertexBindOffset = (VkDeviceSize)vertexOffset; - vkCmdBindVertexBuffers( cmd, 0, 1, &vkExec.vertexRings[ slot ].buffer, &vertexBindOffset ); - vkCmdBindIndexBuffer( cmd, vkExec.indexRings[ slot ].buffer, (VkDeviceSize)indexOffset, VK_INDEX_TYPE_UINT32 ); + return true; +} + +bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri ) { + int vertexOffset; + int indexOffset; + if ( cmd == VK_NULL_HANDLE + || !VK_Exec_UploadTriGeometry( slot, tri, + vertexOffset, indexOffset ) ) { + return false; + } + + const VkDeviceSize vertexBindOffset = + static_cast( vertexOffset ); + vkCmdBindVertexBuffers( cmd, 0, 1, + &vkExec.vertexRings[ slot ].buffer, &vertexBindOffset ); + vkCmdBindIndexBuffer( cmd, vkExec.indexRings[ slot ].buffer, + static_cast( indexOffset ), VK_INDEX_TYPE_UINT32 ); vkExec.boundVertexOffset = vertexOffset; // the cube-texgen path re-binds binding 0 alongside its dir stream return true; } @@ -4072,13 +6103,22 @@ bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_ // tables — cache handles and index arrays never alias across the two // families, and a direct-mapped collision just re-uploads. bool VK_Exec_BindShadowGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_t *tri ) { - if ( tri == NULL ) { + if ( tri == NULL || slot < 0 || slot >= VK_FRAMES_IN_FLIGHT ) { return false; } + // Explicit GPU_SKINNING_FALLBACK_STENCIL_VOLUME: silhouette extrusion, + // caps, and the trace-visible shadow topology remain CPU authoritative. + // Never consult the compute-output memo here; only tri->shadowCache is admissible. const void *vertKey = tri->shadowCache; const void *idxKey = tri->indexes; - if ( vertKey == NULL || idxKey == NULL || tri->numVerts <= 0 || tri->numIndexes <= 0 ) { + const size_t vertexBytes = static_cast( tri->numVerts ) * sizeof( shadowCache_t ); + const size_t indexBytes = static_cast( tri->numIndexes ) * sizeof( glIndex_t ); + if ( vertKey == NULL || idxKey == NULL || tri->numVerts <= 0 + || tri->numIndexes <= 0 + || !VK_Exec_CPUCacheValid( tri->shadowCache, false, + vertexBytes ) + || indexBytes == 0 || indexBytes > static_cast( INT_MAX ) ) { return false; } @@ -4091,7 +6131,7 @@ bool VK_Exec_BindShadowGeometry( VkCommandBuffer cmd, int slot, const srfTriangl } else { const shadowCache_t *verts = (const shadowCache_t *)vertexCache.Position( tri->shadowCache ); vertexOffset = VK_Ring_Alloc( vkExec.vertexRings[ slot ], verts, - static_cast( tri->numVerts ) * sizeof( shadowCache_t ), 16 ); + vertexBytes, 16 ); if ( vertexOffset < 0 ) { return false; } @@ -4107,7 +6147,7 @@ bool VK_Exec_BindShadowGeometry( VkCommandBuffer cmd, int slot, const srfTriangl indexOffset = memo.indexOffset; } else { indexOffset = VK_Ring_Alloc( vkExec.indexRings[ slot ], tri->indexes, - static_cast( tri->numIndexes ) * sizeof( glIndex_t ), 4 ); + indexBytes, 4 ); if ( indexOffset < 0 ) { return false; } @@ -4123,6 +6163,33 @@ bool VK_Exec_BindShadowGeometry( VkCommandBuffer cmd, int slot, const srfTriangl return true; } +// Shared stencil-interaction preflight retains the exact ring offsets produced +// by the ordinary CPU-authored shadow-cache bridge. The bind performed here is +// limited to ring upload/bind command recording; no framebuffer write occurs +// until the complete view has reconciled and selected shared ownership. +bool VK_Exec_PrepareShadowGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ) { + vertexOffset = -1; + indexOffset = -1; + if ( !VK_Exec_BindShadowGeometry( cmd, slot, tri ) || tri == NULL + || tri->indexes == NULL ) { + return false; + } + + vertexOffset = vkExec.boundVertexOffset; + const void *indexKey = static_cast( tri->indexes ); + const unsigned int memoIndex = static_cast( + ( reinterpret_cast( indexKey ) >> 4 ) + & ( VK_TRI_MEMO_SIZE - 1 ) ); + const vkIdxUpload_t &memo = vkExec.idxMemo[ memoIndex ]; + if ( memo.idxKey != indexKey || vertexOffset < 0 || memo.indexOffset < 0 ) { + vertexOffset = -1; + return false; + } + indexOffset = memo.indexOffset; + return true; +} + // Backend-generated geometry which has no idVertexCache handle cannot use the // pointer-keyed upload memo above. Packed MD5R shadow volumes are decoded and // skinned one primitive batch at a time, so stream those transient arrays @@ -4230,6 +6297,10 @@ int VK_Exec_ActiveFrameSlot( void ) { // (require2D) when its view is a cube — the interaction pipeline samples // every slot through sampler2D and a cube view would trip validation VkDescriptorSet VK_Exec_ImageDescriptor( unsigned int texnum, bool require2D ) { + if ( texnum >= sizeof( vkExec.descriptorCache ) + / sizeof( vkExec.descriptorCache[0] ) ) { + return VK_NULL_HANDLE; + } if ( require2D ) { const vkImageEntry_t *entry = VK_Image_GetEntry( texnum ); if ( entry == NULL || entry->isCube ) { @@ -4258,6 +6329,552 @@ int VK_Exec_InteractionUniformAlloc( const void *data, int bytes ) { : -1; } +static bool VK_TemporalPresentation_GetColorTarget( + idRenderTexture *renderTexture, idImage *&image, + vkImageEntry_t *&entry ) { + image = NULL; + entry = NULL; + vkImageEntry_t *depthEntry = NULL; + if ( !VK_Exec_RenderTextureEntries( renderTexture, entry, depthEntry ) + || entry == NULL || entry->samples != VK_SAMPLE_COUNT_1_BIT ) { + return false; + } + image = renderTexture->GetColorImage( 0 ); + return image != NULL + && ( entry->usage & VK_IMAGE_USAGE_SAMPLED_BIT ) != 0; +} + +static bool VK_TemporalPresentation_GetDepthTarget( + idRenderTexture *renderTexture, idImage *&image, + vkImageEntry_t *&entry ) { + image = NULL; + entry = NULL; + vkImageEntry_t *colorEntry = NULL; + if ( !VK_Exec_RenderTextureEntries( renderTexture, colorEntry, entry ) + || entry == NULL || entry->samples != VK_SAMPLE_COUNT_1_BIT ) { + return false; + } + image = renderTexture->GetDepthImage(); + return image != NULL + && ( entry->usage & VK_IMAGE_USAGE_SAMPLED_BIT ) != 0; +} + +static bool VK_TemporalPresentation_TrackHistoryTarget( + idRenderTexture *target ) { + if ( target == NULL || target->GetNumColorImages() != 1 + || target->GetColorImage( 0 ) == NULL ) { + return false; + } + idImage *image = target->GetColorImage( 0 ); + const unsigned long long generation = + static_cast( image->GetStorageGeneration() ); + for ( int i = 0; i < 2; ++i ) { + if ( vkExec.temporalHistoryTargets[i] == target ) { + if ( vkExec.temporalHistoryStorageGenerations[i] == generation ) { + return false; + } + vkExec.temporalHistoryStorageGenerations[i] = generation; + return true; + } + } + for ( int i = 0; i < 2; ++i ) { + if ( vkExec.temporalHistoryTargets[i] == NULL ) { + vkExec.temporalHistoryTargets[i] = target; + vkExec.temporalHistoryStorageGenerations[i] = generation; + return false; + } + } + // The public contract is a two-image ping-pong. Seeing a third owner means + // the native history allocation was replaced without a module restart. + vkExec.temporalHistoryTargets[0] = target; + vkExec.temporalHistoryStorageGenerations[0] = generation; + vkExec.temporalHistoryTargets[1] = NULL; + vkExec.temporalHistoryStorageGenerations[1] = 0; + return true; +} + +static void VK_TemporalPresentation_FillResolveBlock( + const resolveTemporalPresentationCommand_t &command, + int sceneWidth, int sceneHeight, bool useHistory, bool depthValid, + bool captureRecenter, int debugMode, + vkTemporalResolveBlock_t &block ) { + memset( &block, 0, sizeof( block ) ); + const viewDef_t *viewDef = command.viewDef; + const int outputWidth = Max( 1, (int)vkCtx.swapchainExtent.width ); + const int outputHeight = Max( 1, (int)vkCtx.swapchainExtent.height ); + block.sceneOutputExtent[0] = 1.0f / Max( 1, sceneWidth ); + block.sceneOutputExtent[1] = 1.0f / Max( 1, sceneHeight ); + block.sceneOutputExtent[2] = (float)outputWidth; + block.sceneOutputExtent[3] = (float)outputHeight; + block.depthFeedback[2] = idMath::ClampFloat( 0.0f, 0.98f, + command.feedback ); + block.depthFeedback[3] = idMath::ClampFloat( 0.0f, 2.0f, + command.reactiveScale ); + block.temporalParams[2] = useHistory ? 1.0f : 0.0f; + block.temporalParams[3] = (float)idMath::ClampInt( 0, 3, debugMode ); + block.motionParams[2] = depthValid ? 1.0f : 0.0f; + block.motionParams[3] = captureRecenter ? 1.0f : 0.0f; + for ( int component = 0; component < 4; ++component ) { + block.reactiveRect0[component] = -1.0f; + block.reactiveRect1[component] = -1.0f; + } + if ( useHistory ) { + // Packet ownership is a correctness input, not an optional quality hint. + // Until a matching policy is proven available, reject history over the + // complete output so a packet-frame miss cannot leave moving content + // consuming unowned stale samples. + block.reactiveRect0[0] = 0.0f; + block.reactiveRect0[1] = 0.0f; + block.reactiveRect0[2] = 1.0f; + block.reactiveRect0[3] = 1.0f; + temporalViewMotionPolicy_t motionPolicy; + const bool motionPolicyAvailable = viewDef != NULL + && R_ScenePackets_BuildTemporalViewMotionPolicy( + viewDef, 0u, motionPolicy ); + if ( motionPolicyAvailable ) { + for ( int component = 0; component < 4; ++component ) { + block.reactiveRect0[component] = -1.0f; + } + float *rects[TEMPORAL_MAX_REACTIVE_REGIONS] = { + block.reactiveRect0, block.reactiveRect1 + }; + const int regionCount = Min( motionPolicy.reactiveRegionCount, + TEMPORAL_MAX_REACTIVE_REGIONS ); + for ( int regionIndex = 0; regionIndex < regionCount; + ++regionIndex ) { + const temporalReactiveRegion_t ®ion = + motionPolicy.reactiveRegions[regionIndex]; + rects[regionIndex][0] = region.x1; + rects[regionIndex][1] = region.y1; + rects[regionIndex][2] = region.x2; + rects[regionIndex][3] = region.y2; + } + } + } + if ( viewDef == NULL ) { + return; + } + + const float projectionX = viewDef->projectionMatrix[0]; + const float projectionY = viewDef->projectionMatrix[5]; + if ( idMath::Fabs( projectionX ) > 0.00001f + && idMath::Fabs( projectionY ) > 0.00001f ) { + block.currentReconstruct[0] = 1.0f / projectionX; + block.currentReconstruct[1] = 1.0f / projectionY; + } + block.currentReconstruct[2] = viewDef->projectionMatrix[8]; + block.currentReconstruct[3] = viewDef->projectionMatrix[9]; + memcpy( block.previousProject, + viewDef->temporalPreviousProjectInfo.ToFloatPtr(), + sizeof( block.previousProject ) ); + block.depthFeedback[0] = viewDef->projectionMatrix[10]; + block.depthFeedback[1] = viewDef->projectionMatrix[14]; + + memcpy( block.currentViewOrigin, + viewDef->renderView.vieworg.ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.currentViewAxis0, + viewDef->renderView.viewaxis[0].ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.currentViewAxis1, + viewDef->renderView.viewaxis[1].ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.currentViewAxis2, + viewDef->renderView.viewaxis[2].ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.previousViewOrigin, + viewDef->temporalPreviousViewOrigin.ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.previousViewAxis0, + viewDef->temporalPreviousViewAxis[0].ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.previousViewAxis1, + viewDef->temporalPreviousViewAxis[1].ToFloatPtr(), 3 * sizeof( float ) ); + memcpy( block.previousViewAxis2, + viewDef->temporalPreviousViewAxis[2].ToFloatPtr(), 3 * sizeof( float ) ); + float screenEffects[8]; + AdvancedScreenSpaceCore_Pack( command.advancedScreenSpace, screenEffects ); + block.currentViewOrigin[3] = screenEffects[0]; + block.currentViewAxis0[3] = screenEffects[1]; + block.currentViewAxis1[3] = screenEffects[2]; + block.currentViewAxis2[3] = screenEffects[3]; + block.previousViewOrigin[3] = screenEffects[4]; + block.previousViewAxis0[3] = screenEffects[5]; + block.previousViewAxis1[3] = screenEffects[6]; + block.previousViewAxis2[3] = screenEffects[7]; + block.temporalParams[0] = viewDef->temporalJitterPixels.x / outputWidth; + block.temporalParams[1] = viewDef->temporalJitterPixels.y / outputHeight; + block.motionParams[1] = useHistory && depthValid + && viewDef->temporalPreviousProjectionValid + && idMath::Fabs( projectionX ) > 0.00001f + && idMath::Fabs( projectionY ) > 0.00001f ? 1.0f : 0.0f; +} + +static bool VK_TemporalPresentation_DrawResolve( + idRenderTexture *destination, + idImage *sceneImage, vkImageEntry_t *sceneEntry, + idImage *depthImage, vkImageEntry_t *depthEntry, + idImage *historyImage, vkImageEntry_t *historyEntry, + const vkTemporalResolveBlock_t &block ) { + if ( sceneImage == NULL || sceneEntry == NULL + || !VK_Exec_SetRenderTarget( NULL ) ) { + return false; + } + VK_Exec_TransitionImage( sceneEntry, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + if ( depthEntry != NULL ) { + VK_Exec_TransitionImage( depthEntry, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + } + if ( historyEntry != NULL ) { + VK_Exec_TransitionImage( historyEntry, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + } + + VkDescriptorSet sceneSet = VK_GuiExecutor_GetImageDescriptor( + sceneImage->GetDeviceHandle() ); + idImage *depthFallbackImage = globalImages->whiteImage; + idImage *historyFallbackImage = globalImages->blackImage; + VkDescriptorSet depthSet = depthImage != NULL && depthEntry != NULL + ? VK_GuiExecutor_GetImageDescriptor( depthImage->GetDeviceHandle() ) + : VK_GuiExecutor_GetResidentImageDescriptor( depthFallbackImage ); + VkDescriptorSet historySet = historyImage != NULL && historyEntry != NULL + ? VK_GuiExecutor_GetImageDescriptor( historyImage->GetDeviceHandle() ) + : VK_GuiExecutor_GetResidentImageDescriptor( historyFallbackImage ); + const int uniformOffset = VK_Exec_InteractionUniformAlloc( + &block, sizeof( block ) ); + if ( sceneSet == VK_NULL_HANDLE || depthSet == VK_NULL_HANDLE + || historySet == VK_NULL_HANDLE || uniformOffset < 0 + || !VK_Exec_SetRenderTarget( destination ) ) { + return false; + } + + const VkPipeline pipeline = + VK_TemporalPresentation_GetResolvePipeline(); + if ( pipeline == VK_NULL_HANDLE ) { + return false; + } + VkViewport viewport; + memset( &viewport, 0, sizeof( viewport ) ); + viewport.width = (float)vkExec.activeExtent.width; + viewport.height = (float)vkExec.activeExtent.height; + viewport.maxDepth = 1.0f; + VkRect2D scissor; + memset( &scissor, 0, sizeof( scissor ) ); + scissor.extent = vkExec.activeExtent; + vkCmdSetViewport( vkExec.cmd, 0, 1, &viewport ); + vkCmdSetScissor( vkExec.cmd, 0, 1, &scissor ); + vkCmdSetDepthTestEnable( vkExec.cmd, VK_FALSE ); + vkCmdSetDepthWriteEnable( vkExec.cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( vkExec.cmd, VK_COMPARE_OP_ALWAYS ); + vkCmdSetCullMode( vkExec.cmd, VK_CULL_MODE_NONE ); + vkCmdSetFrontFace( vkExec.cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + vkCmdSetDepthBiasEnable( vkExec.cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( vkExec.cmd, VK_FALSE ); + vkCmdBindPipeline( vkExec.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline ); + VkDescriptorSet imageSets[3] = { sceneSet, depthSet, historySet }; + vkCmdBindDescriptorSets( vkExec.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkExec.interactionPipelineLayout, 0, 3, imageSets, 0, NULL ); + const VkDescriptorSet uniformSet = VK_Exec_InteractionUniformSet(); + const uint32_t dynamicOffset = (uint32_t)uniformOffset; + vkCmdBindDescriptorSets( vkExec.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkExec.interactionPipelineLayout, 6, 1, &uniformSet, + 1, &dynamicOffset ); + vkCmdDraw( vkExec.cmd, 3, 1, 0, 0 ); + return true; +} + +static bool VK_TemporalPresentation_DrawPendingSceneSpatial( + const viewDef_t *viewDef, idImage *sceneImage, + vkImageEntry_t *sceneEntry, idImage *depthImage, + vkImageEntry_t *depthEntry ) { + if ( viewDef == NULL || sceneImage == NULL || sceneEntry == NULL ) { + return false; + } + resolveTemporalPresentationCommand_t command; + memset( &command, 0, sizeof( command ) ); + command.viewDef = viewDef; + command.reactiveScale = 1.0f; + command.advancedScreenSpace = + R_TemporalPresentation_GetFrameState().advancedScreenSpace; + vkTemporalResolveBlock_t block; + VK_TemporalPresentation_FillResolveBlock( command, + sceneEntry->width, sceneEntry->height, false, depthEntry != NULL, + true, 0, block ); + return VK_TemporalPresentation_DrawResolve( NULL, sceneImage, sceneEntry, + depthImage, depthEntry, NULL, NULL, block ); +} + +static bool VK_TemporalPresentation_DrawResolvedColorToSwap( + const resolveTemporalPresentationCommand_t &command, + idImage *resolvedImage, vkImageEntry_t *resolvedEntry, + int outputWidth, int outputHeight ) { + if ( resolvedImage == NULL || resolvedEntry == NULL + || resolvedEntry->width != outputWidth + || resolvedEntry->height != outputHeight ) { + return false; + } + resolveTemporalPresentationCommand_t presentCommand = command; + memset( &presentCommand.advancedScreenSpace, 0, + sizeof( presentCommand.advancedScreenSpace ) ); + vkTemporalResolveBlock_t block; + VK_TemporalPresentation_FillResolveBlock( presentCommand, + outputWidth, outputHeight, false, false, false, 0, block ); + return VK_TemporalPresentation_DrawResolve( NULL, + resolvedImage, resolvedEntry, NULL, NULL, NULL, NULL, block ); +} + +static bool VK_TemporalPresentation_BlitColorToSwap( + vkImageEntry_t *sourceEntry, int width, int height ) { + if ( sourceEntry == NULL || sourceEntry->samples != VK_SAMPLE_COUNT_1_BIT + || ( sourceEntry->usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT ) == 0 + || width != (int)vkCtx.swapchainExtent.width + || height != (int)vkCtx.swapchainExtent.height + || !VK_Exec_SetRenderTarget( NULL ) ) { + return false; + } + VkFilter filter = VK_FILTER_NEAREST; + if ( !VK_TemporalPresentation_BlitSupported( sourceEntry->format, filter ) ) { + return false; + } + VK_Exec_EndMainRendering(); + VK_Exec_TransitionImage( sourceEntry, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ); + VK_Exec_TransitionSwapchain( VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ); + VkImageBlit region; + memset( ®ion, 0, sizeof( region ) ); + region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.srcSubresource.layerCount = 1; + region.srcOffsets[1].x = width; + region.srcOffsets[1].y = height; + region.srcOffsets[1].z = 1; + region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.dstSubresource.layerCount = 1; + region.dstOffsets[1].x = width; + region.dstOffsets[1].y = height; + region.dstOffsets[1].z = 1; + vkCmdBlitImage( vkExec.cmd, sourceEntry->image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + vkCtx.swapchainImages[vkExec.swapImageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, filter ); + VK_Exec_TransitionImage( sourceEntry, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ); + VK_Exec_TransitionSwapchain( VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ); + return VK_Exec_BeginMainRendering( false ); +} + +static void VK_TemporalPresentation_InvalidateAcceptedNoWrite( + const resolveTemporalPresentationCommand_t &command, + const char *reason ) { + // Effect-only presentation deliberately has no history write target. That + // is a successful current-frame resolve, not a temporal discontinuity; do + // not churn the shared history generation once per frame while a leaf is on. + if ( !command.presentation.temporalAARequested + || command.captureFrame || tr.takingScreenshot + || command.historyGeneration + != R_TemporalPresentation_HistoryGeneration() ) { + return; + } + R_TemporalPresentation_InvalidateHistory( reason ); + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); +} + +static bool VK_TemporalPresentation_ResolveTargets( + const resolveTemporalPresentationCommand_t &sourceCommand, + bool trackExternalHistory, bool *historyAdvanced ) { + if ( historyAdvanced != NULL ) { + *historyAdvanced = false; + } + resolveTemporalPresentationCommand_t command = sourceCommand; + if ( tr.takingScreenshot ) { + command.captureFrame = true; + command.historyValid = false; + } + + idImage *sceneImage = NULL; + idImage *depthImage = NULL; + idImage *historyReadImage = NULL; + idImage *historyWriteImage = NULL; + vkImageEntry_t *sceneEntry = NULL; + vkImageEntry_t *depthEntry = NULL; + vkImageEntry_t *historyReadEntry = NULL; + vkImageEntry_t *historyWriteEntry = NULL; + if ( !VK_TemporalPresentation_GetColorTarget( command.sceneColorTarget, + sceneImage, sceneEntry ) ) { + VK_TemporalPresentation_InvalidateAcceptedNoWrite( command, + "Vulkan temporal scene color is unavailable" ); + return false; + } + const bool depthResident = VK_TemporalPresentation_GetDepthTarget( + command.sceneDepthTarget, depthImage, depthEntry ); + const bool depthReady = depthResident + && VK_TemporalPresentation_DepthStampMatches( + command.sceneDepthTarget, depthImage, + command.historyGeneration ); + const bool writeReady = !command.captureFrame + && VK_TemporalPresentation_GetColorTarget( command.historyWriteTarget, + historyWriteImage, historyWriteEntry ); + const bool readReady = !command.captureFrame + && command.historyReadTarget != NULL + && VK_TemporalPresentation_GetColorTarget( command.historyReadTarget, + historyReadImage, historyReadEntry ); + + bool historyResourceChanged = false; + if ( trackExternalHistory && !command.captureFrame && writeReady ) { + historyResourceChanged |= + VK_TemporalPresentation_TrackHistoryTarget( + command.historyWriteTarget ); + } + if ( trackExternalHistory && !command.captureFrame && readReady ) { + historyResourceChanged |= + VK_TemporalPresentation_TrackHistoryTarget( + command.historyReadTarget ); + } + if ( historyResourceChanged ) { + // A swapchain/output reset can retire these images before this queued + // command executes. Do not advance the shared generation twice for the + // same reset; a replacement under the still-current generation does own + // a new invalidation. + if ( command.historyGeneration + == R_TemporalPresentation_HistoryGeneration() ) { + R_TemporalPresentation_InvalidateHistory( + "Vulkan temporal history resource changed" ); + } + vkExec.temporalHistoryGenerationSeen = + R_TemporalPresentation_HistoryGeneration(); + command.historyValid = false; + } + + const int sceneWidth = sceneEntry->width; + const int sceneHeight = sceneEntry->height; + const int outputWidth = (int)vkCtx.swapchainExtent.width; + const int outputHeight = (int)vkCtx.swapchainExtent.height; + const bool presentationMatches = command.presentation.frameNumber + == backEnd.frameCount + && command.presentation.outputWidth == outputWidth + && command.presentation.outputHeight == outputHeight + && command.presentation.sceneWidth == sceneWidth + && command.presentation.sceneHeight == sceneHeight; + const bool writeMatches = writeReady + && historyWriteEntry->width == outputWidth + && historyWriteEntry->height == outputHeight; + const bool readMatches = readReady + && historyReadEntry->width == outputWidth + && historyReadEntry->height == outputHeight; + const bool generationMatches = command.historyGeneration + == R_TemporalPresentation_HistoryGeneration(); + const bool viewHistoryReady = command.viewDef != NULL + && command.viewDef->temporalPrepared + && command.viewDef->temporalHistoryValid + && command.viewDef->temporalPreviousProjectionValid + && command.viewDef->temporalHistoryGeneration + == command.historyGeneration; + const bool historyValid = !command.captureFrame && command.historyValid + && presentationMatches && generationMatches + && command.presentation.historyGeneration == command.historyGeneration + && viewHistoryReady + && depthReady && readMatches + && command.historyReadTarget != command.historyWriteTarget + && historyReadEntry->image != sceneEntry->image + && ( !writeReady || historyReadEntry->image != historyWriteEntry->image ); + const bool writeHistory = !command.captureFrame && presentationMatches + && generationMatches + && command.presentation.historyGeneration == command.historyGeneration + && depthReady && writeMatches + && historyWriteEntry->image != sceneEntry->image; + + if ( !writeHistory ) { + // Capture and every validated failure path go directly to the native + // output. No game history target is bound, sampled, or mutated. Recenter + // the current projection so a shader/resource fallback cannot expose its + // per-frame jitter at native output. + VK_TemporalPresentation_InvalidateAcceptedNoWrite( command, depthReady + ? "Vulkan temporal history write unavailable" + : "Vulkan temporal depth is not current" ); + vkTemporalResolveBlock_t spatialBlock; + VK_TemporalPresentation_FillResolveBlock( command, sceneWidth, + sceneHeight, false, depthReady, true, 0, spatialBlock ); + return VK_TemporalPresentation_DrawResolve( NULL, sceneImage, + sceneEntry, depthReady ? depthImage : NULL, + depthReady ? depthEntry : NULL, NULL, NULL, spatialBlock ); + } + + vkTemporalResolveBlock_t normalBlock; + VK_TemporalPresentation_FillResolveBlock( command, sceneWidth, + sceneHeight, historyValid, depthReady, false, 0, normalBlock ); + if ( !VK_TemporalPresentation_DrawResolve( + command.historyWriteTarget, sceneImage, sceneEntry, + depthReady ? depthImage : NULL, depthReady ? depthEntry : NULL, + historyValid ? historyReadImage : NULL, + historyValid ? historyReadEntry : NULL, normalBlock ) + || !VK_Exec_SetRenderTarget( NULL ) ) { + // Fail closed to a spatial present without sampling or further mutating + // history. The game will reject this generation on the next frame. + VK_TemporalPresentation_InvalidateAcceptedNoWrite( command, + "Vulkan temporal resolve execution failed" ); + vkTemporalResolveBlock_t spatialBlock; + VK_TemporalPresentation_FillResolveBlock( command, sceneWidth, + sceneHeight, false, depthReady, true, 0, spatialBlock ); + return VK_TemporalPresentation_DrawResolve( NULL, sceneImage, + sceneEntry, depthReady ? depthImage : NULL, + depthReady ? depthEntry : NULL, NULL, NULL, spatialBlock ); + } + + const int debugMode = idMath::ClampInt( 0, 3, command.debugMode ); + if ( debugMode != 0 ) { + vkTemporalResolveBlock_t debugBlock; + VK_TemporalPresentation_FillResolveBlock( command, sceneWidth, + sceneHeight, historyValid, depthReady, false, + debugMode, debugBlock ); + const bool presented = VK_TemporalPresentation_DrawResolve( + NULL, sceneImage, + sceneEntry, depthReady ? depthImage : NULL, + depthReady ? depthEntry : NULL, + historyValid ? historyReadImage : NULL, + historyValid ? historyReadEntry : NULL, debugBlock ); + if ( presented && historyAdvanced != NULL ) { + *historyAdvanced = true; + } + return presented; + } + const bool presented = VK_TemporalPresentation_DrawResolvedColorToSwap( + command, historyWriteImage, historyWriteEntry, + outputWidth, outputHeight ) + || VK_TemporalPresentation_BlitColorToSwap( + historyWriteEntry, outputWidth, outputHeight ); + if ( presented && historyAdvanced != NULL ) { + *historyAdvanced = true; + } + return presented; +} + +bool VK_GuiExecutor_ResolveTemporalPresentation( + const resolveTemporalPresentationCommand_t &sourceCommand ) { + if ( !VK_GuiExecutor_BeginFrame() ) { + VK_TemporalPresentation_InvalidateAcceptedNoWrite( sourceCommand, + "Vulkan temporal frame is unavailable" ); + return false; + } + if ( vkExec.temporalScenePendingComposite + && !VK_TemporalPresentation_CompositePendingScene() ) { + VK_TemporalPresentation_InvalidateAcceptedNoWrite( sourceCommand, + "Vulkan temporal scene composite blocked command execution" ); + return false; + } + VK_TemporalPresentation_ObserveHistoryAndCapture(); + return VK_TemporalPresentation_ResolveTargets( + sourceCommand, true, NULL ); +} + +int VK_Exec_InteractionUniformCheckpoint( void ) { + return vkExec.frameSlot >= 0 && vkExec.frameSlot < VK_FRAMES_IN_FLIGHT + ? vkExec.uniformRings[ vkExec.frameSlot ].cursor : -1; +} + +void VK_Exec_InteractionUniformRestore( int checkpoint ) { + if ( vkExec.frameSlot < 0 || vkExec.frameSlot >= VK_FRAMES_IN_FLIGHT + || checkpoint < 0 + || checkpoint > vkExec.uniformRings[ vkExec.frameSlot ].capacity ) { + return; + } + vkExec.uniformRings[ vkExec.frameSlot ].cursor = checkpoint; +} + // Shadow receiver blocks use a larger descriptor range for four localized // projected cascades. Point receivers share set 7 and use this allocator too, // keeping every set-7 dynamic descriptor valid through the full 512 bytes. @@ -5229,12 +7846,11 @@ static void VK_Exec_SetSpecialEffectsViewport( const viewDef_t *viewDef ) { viewDef->viewport.y2 - viewDef->viewport.y1 + 1; VkViewport viewport; - viewport.x = (float)viewDef->viewport.x1; - viewport.y = (float)( framebufferHeight - viewDef->viewport.y1 ); - viewport.width = (float)viewportWidth; - viewport.height = -(float)viewportHeight; - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; + if ( !VK_BuildCanonicalViewport( viewport, + (float)viewDef->viewport.x1, (float)viewDef->viewport.y1, + (float)viewportWidth, (float)viewportHeight, (float)framebufferHeight ) ) { + return; + } vkCmdSetViewport( vkExec.cmd, 0, 1, &viewport ); VkRect2D scissor; @@ -5612,6 +8228,8 @@ static void VK_Exec_DrawRVSpecialEffects( const viewDef_t *viewDef ) { return; } const int activeMask = vkExec.pendingSpecialEffectsMask; + const bool sharedSpecialFrame = r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRavenEffectsView( viewDef ) != NULL; vkExec.pendingSpecialEffectsView = NULL; vkExec.pendingSpecialEffectsMask = 0; vkExec.pendingSpecialEffectsSource = NULL; @@ -5625,6 +8243,16 @@ static void VK_Exec_DrawRVSpecialEffects( const viewDef_t *viewDef ) { if ( ( activeMask & SPECIAL_EFFECT_AL ) != 0 ) { drewAL = VK_Exec_DrawRVSpecialAL( viewDef ); } + if ( sharedSpecialFrame && drewBlur ) { + (void)R_ClassicSpecialFrameDomain_RecordOwned( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, SPECIAL_EFFECT_BLUR ); + } + if ( sharedSpecialFrame && drewAL ) { + (void)R_ClassicSpecialFrameDomain_RecordOwned( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, SPECIAL_EFFECT_AL ); + } static bool loggedBlur = false; static bool loggedAL = false; @@ -5666,7 +8294,18 @@ void VK_GuiExecutor_DrawResolvedSpecialEffects( backEnd.renderTexture = destinationRenderTexture; backEnd.feedbackRenderTexture = NULL; vkExec.pendingSpecialEffectsNeedsResolve = false; - VK_Exec_DrawRVSpecialEffects( vkExec.pendingSpecialEffectsView ); + const viewDef_t *effectsView = vkExec.pendingSpecialEffectsView; + vkSceneScaleState_t sceneScaleState; + VK_TemporalPresentation_ClearScaleState( sceneScaleState ); + const viewDef_t *rootView = + VK_TemporalPresentation_RootForView( effectsView ); + if ( rootView != NULL ) { + (void)VK_TemporalPresentation_BeginScale( sceneScaleState, + effectsView, rootView, + (int)vkExec.activeExtent.width, (int)vkExec.activeExtent.height, false ); + } + VK_Exec_DrawRVSpecialEffects( effectsView ); + VK_TemporalPresentation_RestoreScale( sceneScaleState, effectsView ); (void)VK_Exec_SetRenderTarget( savedRenderTexture ); backEnd.renderTexture = savedBackEndRenderTexture; @@ -5946,6 +8585,11 @@ static void VK_Exec_DrawProgramStage( const viewDef_t *viewDef, } } +// mirrors the GL backend's cross-frame cinematic upload dedupe (tr_render.cpp) +static const idCinematic * vkGuiLastCinematic = NULL; +static int vkGuiLastCinematicSerial = 0; +static unsigned long long vkGuiLastCinematicStorageGeneration = 0; + static void VK_Exec_DrawAmbientStages( const viewDef_t *viewDef, const drawSurf_t *drawSurf, const srfTriangles_t *tri, const float mvp[ 16 ], bool worldDepthState ) { VkCommandBuffer cmd = vkExec.cmd; @@ -6084,7 +8728,21 @@ static void VK_Exec_DrawAmbientStages( const viewDef_t *viewDef, const drawSurf_ cinData_t cin = pStage->texture.cinematic->ImageForTime( (int)( 1000 * ( viewDef->floatTime + viewDef->renderView.shaderParms[ 11 ] ) ) ); if ( cin.image != NULL ) { - globalImages->cinematicImage->UploadScratch( cin.image, cin.imageWidth, cin.imageHeight ); + idImage *cinImage = globalImages->cinematicImage; + const bool sameFrameResident = + cin.frameSerial != 0 + && pStage->texture.cinematic == vkGuiLastCinematic + && cin.frameSerial == vkGuiLastCinematicSerial + && cinImage->IsLoaded() + && cinImage->GetStorageGeneration() == vkGuiLastCinematicStorageGeneration + && cinImage->GetOpts().textureType == TT_2D + && cin.imageHeight != cin.imageWidth * 6; + if ( !sameFrameResident ) { + globalImages->cinematicImage->UploadScratch( cin.image, cin.imageWidth, cin.imageHeight ); + vkGuiLastCinematic = pStage->texture.cinematic; + vkGuiLastCinematicSerial = cin.frameSerial; + vkGuiLastCinematicStorageGeneration = cinImage->GetStorageGeneration(); + } stageImage = globalImages->cinematicImage; } else { stageImage = globalImages->blackImage; @@ -6353,39 +9011,1820 @@ static void VK_Exec_DrawAmbientStages( const viewDef_t *viewDef, const drawSurf_ } /* -==================== -VK_GuiExecutor_Draw2DView +=============================================================================== -The RB_STD_DrawShaderPasses contract for 2D views, on the swapchain. -==================== -*/ -void VK_GuiExecutor_Draw2DView( const viewDef_t *viewDef ) { - if ( viewDef == NULL ) { - return; - } - backEnd.currentRenderCopied = false; - backEnd.currentDepthCopied = false; - if ( viewDef->numDrawSurfs <= 0 ) { - return; - } - if ( !VK_GuiExecutor_BeginFrame() ) { - return; - } + Backend-neutral classic GUI whole-view consumer - backEnd.viewDef = (viewDef_t *)viewDef; + The preparation contract in ClassicGuiDomain owns material interpretation. + This adapter is deliberately limited to geometry upload from legacyDrawSurf; + all material state, register results, texture identity, transforms, and + scissors come from immutable domain records. Every fallible resource lookup + and upload completes before the first indexed draw, so a rejection can still + return to the untouched classic walker for the complete view. - VkCommandBuffer cmd = vkExec.cmd; - const int slot = vkExec.frameSlot; - const int fbHeight = VK_Exec_ActiveFramebufferHeight(); +=============================================================================== +*/ - // 2D semantics regardless of what an earlier 3D view left behind - vkCmdSetDepthTestEnable( cmd, VK_FALSE ); - vkCmdSetDepthWriteEnable( cmd, VK_FALSE ); - vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_ALWAYS ); - vkCmdSetCullMode( cmd, VK_CULL_MODE_NONE ); - vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); - vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); - vkCmdSetStencilTestEnable( cmd, VK_FALSE ); +enum vkClassicGuiRejectDetail_t { + VK_CLASSIC_GUI_REJECT_NONE = 0, + VK_CLASSIC_GUI_REJECT_VIEW_MUTATION, + VK_CLASSIC_GUI_REJECT_OFFSCREEN_TARGET, + VK_CLASSIC_GUI_REJECT_RENDER_SCOPE, + VK_CLASSIC_GUI_REJECT_VIEWPORT, + VK_CLASSIC_GUI_REJECT_DOMAIN_COUNTS, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD, + VK_CLASSIC_GUI_REJECT_GEOMETRY, + VK_CLASSIC_GUI_REJECT_PASS_RECORD, + VK_CLASSIC_GUI_REJECT_PASS_STATE, + VK_CLASSIC_GUI_REJECT_TEXTURE_BINDING, + VK_CLASSIC_GUI_REJECT_TEXTURE_RESIDENCY, + VK_CLASSIC_GUI_REJECT_DESCRIPTOR, + VK_CLASSIC_GUI_REJECT_PIPELINE, + VK_CLASSIC_GUI_REJECT_COVERAGE +}; + +typedef struct vkClassicGuiDrawPlan_s { + const classicGuiDomainDraw_t *draw; + const srfTriangles_t *tri; + int vertexOffset; + int indexOffset; + int firstPass; + int passCount; + VkRect2D scissor; + float mvp[ 16 ]; +} vkClassicGuiDrawPlan_t; + +typedef struct vkClassicGuiPassPlan_s { + const rendererEvaluatedMaterialPass_t *pass; + VkPipeline pipeline; + VkDescriptorSet descriptor; + int stateBits; + float alphaTestMode; + VkCompareOp depthCompare; + VkCullModeFlags cullMode; +} vkClassicGuiPassPlan_t; + +static vkClassicGuiDrawPlan_t vkClassicGuiDrawPlans[ CLASSIC_GUI_DOMAIN_MAX_DRAWS ]; +static vkClassicGuiPassPlan_t vkClassicGuiPassPlans[ CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES ]; + +static bool VK_ClassicGui_Fail( const viewDef_t *viewDef, + classicGuiDomainFailure_t failure, int detail ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, failure, detail ); + return false; +} + +static bool VK_ClassicGui_SourceNoopValid( + const classicGuiDomainDraw_t &draw ) { + switch ( draw.sourceSurface ) { + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NULL_SURFACE: + if ( draw.legacyDrawSurf != NULL ) { + return false; + } + break; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NO_AMBIENT: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_PORTAL_SKY: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: + if ( draw.legacyDrawSurf == NULL ) { + return false; + } + break; + default: + return false; + } + return !draw.packetBacked && draw.drawPacketIndex == -1 + && draw.firstEvaluatedPass == -1 && draw.evaluatedPassCount == 0 + && draw.activePassCount == 0 && draw.drawablePassCount == 0 + && draw.inactivePassCount == 0 && draw.activeNoopPassCount == 0 + && draw.noopPassCount == 0; +} + +static bool VK_ClassicGui_MapSourceBlend( rendererBlendFactor_t factor, int &bits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: bits = GLS_SRCBLEND_ZERO; return true; + case RENDERER_BLEND_ONE: bits = GLS_SRCBLEND_ONE; return true; + case RENDERER_BLEND_SRC_COLOR: bits = GLS_SRCBLEND_SRC_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: bits = GLS_SRCBLEND_ONE_MINUS_SRC_COLOR; return true; + case RENDERER_BLEND_DST_COLOR: bits = GLS_SRCBLEND_DST_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_DST_COLOR: bits = GLS_SRCBLEND_ONE_MINUS_DST_COLOR; return true; + case RENDERER_BLEND_SRC_ALPHA: bits = GLS_SRCBLEND_SRC_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: bits = GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA; return true; + case RENDERER_BLEND_DST_ALPHA: bits = GLS_SRCBLEND_DST_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: bits = GLS_SRCBLEND_ONE_MINUS_DST_ALPHA; return true; + case RENDERER_BLEND_SRC_ALPHA_SATURATE: bits = GLS_SRCBLEND_ALPHA_SATURATE; return true; + default: return false; + } +} + +static bool VK_ClassicGui_MapDestinationBlend( rendererBlendFactor_t factor, int &bits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: bits = GLS_DSTBLEND_ZERO; return true; + case RENDERER_BLEND_ONE: bits = GLS_DSTBLEND_ONE; return true; + case RENDERER_BLEND_SRC_COLOR: bits = GLS_DSTBLEND_SRC_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: bits = GLS_DSTBLEND_ONE_MINUS_SRC_COLOR; return true; + case RENDERER_BLEND_SRC_ALPHA: bits = GLS_DSTBLEND_SRC_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: bits = GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA; return true; + case RENDERER_BLEND_DST_ALPHA: bits = GLS_DSTBLEND_DST_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: bits = GLS_DSTBLEND_ONE_MINUS_DST_ALPHA; return true; + default: return false; + } +} + +static bool VK_ClassicGui_MapDepthCompare( rendererCompareOp_t operation, + VkCompareOp &compare ) { + switch ( operation ) { + case RENDERER_COMPARE_NEVER: compare = VK_COMPARE_OP_NEVER; return true; + case RENDERER_COMPARE_LESS: compare = VK_COMPARE_OP_LESS; return true; + case RENDERER_COMPARE_EQUAL: compare = VK_COMPARE_OP_EQUAL; return true; + case RENDERER_COMPARE_LESS_OR_EQUAL: compare = VK_COMPARE_OP_LESS_OR_EQUAL; return true; + case RENDERER_COMPARE_GREATER: compare = VK_COMPARE_OP_GREATER; return true; + case RENDERER_COMPARE_NOT_EQUAL: compare = VK_COMPARE_OP_NOT_EQUAL; return true; + case RENDERER_COMPARE_GREATER_OR_EQUAL: compare = VK_COMPARE_OP_GREATER_OR_EQUAL; return true; + case RENDERER_COMPARE_ALWAYS: compare = VK_COMPARE_OP_ALWAYS; return true; + default: return false; + } +} + +static bool VK_ClassicGui_MapAlphaTest( const rendererEvaluatedMaterialPass_t &pass, + float &mode ) { + if ( !pass.alphaTestEnabled + || pass.alphaTestCompareOperation == RENDERER_COMPARE_ALWAYS ) { + mode = 0.0f; + return true; + } + switch ( pass.alphaTestCompareOperation ) { + case RENDERER_COMPARE_LESS: mode = -1.0f; return true; + case RENDERER_COMPARE_EQUAL: mode = 2.0f; return true; + case RENDERER_COMPARE_GREATER: mode = 1.0f; return true; + default: + // gui.frag intentionally exposes only these three exact classic tests. + // Other neutral compare operations keep whole-view ownership classic. + return false; + } +} + +static bool VK_ClassicGui_MapCull( rendererCullMode_t cull, + VkCullModeFlags &mode ) { + switch ( cull ) { + case RENDERER_CULL_NONE: mode = VK_CULL_MODE_NONE; return true; + case RENDERER_CULL_FRONT: mode = VK_CULL_MODE_FRONT_BIT; return true; + case RENDERER_CULL_BACK: mode = VK_CULL_MODE_BACK_BIT; return true; + default: return false; + } +} + +static bool VK_ClassicGui_MapState( const rendererEvaluatedMaterialPass_t &pass, + bool inWorld, vkClassicGuiPassPlan_t &plan ) { + if ( pass.kind != ( inWorld ? RENDERER_MATERIAL_PASS_SURFACE + : RENDERER_MATERIAL_PASS_GUI ) + || ( inWorld + ? pass.programFamily != RENDERER_PROGRAM_FIXED + : ( pass.programFamily != RENDERER_PROGRAM_GUI + && pass.programFamily != RENDERER_PROGRAM_FIXED ) ) + || pass.programKey != 0 + || pass.texgen != RENDERER_TEXGEN_EXPLICIT + || pass.textureSemantic != RENDERER_TEXTURE_DIFFUSE + || pass.textureResourceId == 0 + || ( pass.colorWriteMask + & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 + || pass.vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || pass.vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || pass.blend.colorOperation != RENDERER_BLEND_OP_ADD + || pass.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || pass.blend.sourceAlpha != pass.blend.sourceColor + || pass.blend.destinationAlpha != pass.blend.destinationColor + || pass.depth.testEnabled != inWorld + || ( pass.depth.compareOperation != RENDERER_COMPARE_LESS_OR_EQUAL + && pass.depth.compareOperation != RENDERER_COMPARE_EQUAL + && pass.depth.compareOperation != RENDERER_COMPARE_ALWAYS ) ) { + return false; + } + const bool replacementBlend = pass.blend.sourceColor == RENDERER_BLEND_ONE + && pass.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( pass.blend.enabled == replacementBlend ) { + return false; + } + + int sourceBits = 0; + int destinationBits = 0; + if ( !VK_ClassicGui_MapSourceBlend( pass.blend.sourceColor, sourceBits ) + || !VK_ClassicGui_MapDestinationBlend( + pass.blend.destinationColor, destinationBits ) + || !VK_ClassicGui_MapDepthCompare( + pass.depth.compareOperation, plan.depthCompare ) + || !VK_ClassicGui_MapAlphaTest( pass, plan.alphaTestMode ) + || !VK_ClassicGui_MapCull( pass.cull, plan.cullMode ) ) { + return false; + } + + plan.stateBits = sourceBits | destinationBits; + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + plan.stateBits |= GLS_REDMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + plan.stateBits |= GLS_GREENMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + plan.stateBits |= GLS_BLUEMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + plan.stateBits |= GLS_ALPHAMASK; + } + + if ( !std::isfinite( pass.condition ) || !std::isfinite( pass.alphaTest ) + || !std::isfinite( pass.polygonOffsetFactor ) + || !std::isfinite( pass.polygonOffsetUnits ) ) { + return false; + } + for ( int i = 0; i < 4; ++i ) { + if ( !std::isfinite( pass.color[ i ] ) ) { + return false; + } + } + for ( int i = 0; i < 6; ++i ) { + if ( !std::isfinite( pass.textureMatrix[ i ] ) ) { + return false; + } + } + if ( static_cast( pass.textureMatrix[ 2 ] ) + < static_cast( INT_MIN ) + || static_cast( pass.textureMatrix[ 2 ] ) + > static_cast( INT_MAX ) + || static_cast( pass.textureMatrix[ 5 ] ) + < static_cast( INT_MIN ) + || static_cast( pass.textureMatrix[ 5 ] ) + > static_cast( INT_MAX ) ) { + return false; + } + return true; +} + +static bool VK_ClassicGui_BuildScissor( const classicGuiDomainView_t &view, + const classicGuiDomainDraw_t &draw, int framebufferHeight, + VkRect2D &scissor ) { + const int viewportWidth = view.viewportX2 - view.viewportX1 + 1; + const int viewportHeight = view.viewportY2 - view.viewportY1 + 1; + if ( viewportWidth <= 0 || viewportHeight <= 0 || framebufferHeight <= 0 ) { + return false; + } + + const bool drawScissorEmpty = draw.scissorX2 < draw.scissorX1 + || draw.scissorY2 < draw.scissorY1; + const bool useDrawScissor = r_useScissor.GetBool() && !drawScissorEmpty; + int requestedX1 = useDrawScissor ? draw.scissorX1 : view.scissorX1; + int requestedY1 = useDrawScissor ? draw.scissorY1 : view.scissorY1; + int requestedX2 = useDrawScissor ? draw.scissorX2 : view.scissorX2; + int requestedY2 = useDrawScissor ? draw.scissorY2 : view.scissorY2; + const bool requestedEmpty = requestedX2 < requestedX1 + || requestedY2 < requestedY1; + + int x0 = view.viewportX1; + int x1 = view.viewportX1 + viewportWidth; + int y0GL = view.viewportY1; + int y1GL = view.viewportY1 + viewportHeight; + if ( !requestedEmpty ) { + x0 = Max( x0, view.viewportX1 + requestedX1 ); + x1 = Min( x1, view.viewportX1 + requestedX2 + 1 ); + y0GL = Max( y0GL, view.viewportY1 + requestedY1 ); + y1GL = Min( y1GL, view.viewportY1 + requestedY2 + 1 ); + } + x0 = Min( view.viewportX1 + viewportWidth, Max( 0, x0 ) ); + x1 = Max( x0, x1 ); + y0GL = Min( view.viewportY1 + viewportHeight, + Max( view.viewportY1, y0GL ) ); + y1GL = Max( y0GL, y1GL ); + const int y0 = Max( 0, framebufferHeight - y1GL ); + const int y1 = Max( y0, Min( framebufferHeight, + framebufferHeight - y0GL ) ); + + scissor.offset.x = x0; + scissor.offset.y = y0; + scissor.extent.width = static_cast( x1 - x0 ); + scissor.extent.height = static_cast( y1 - y0 ); + return true; +} + +static bool VK_Exec_PrepareTriGeometryOffsets( int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ) { + return VK_Exec_UploadTriGeometry( slot, tri, + vertexOffset, indexOffset ); +} + +static bool VK_Exec_SharedGeometryCheckpoint( void ) { + vkSharedGeometryCheckpoint_t &checkpoint = vkSharedGeometryCheckpoint; + if ( checkpoint.active || !vkExec.frameOpen + || vkExec.frameSlot < 0 + || vkExec.frameSlot >= VK_FRAMES_IN_FLIGHT ) { + return false; + } + const vkRing_t &vertexRing = vkExec.vertexRings[ vkExec.frameSlot ]; + const vkRing_t &indexRing = vkExec.indexRings[ vkExec.frameSlot ]; + if ( vertexRing.cursor < 0 || vertexRing.cursor > vertexRing.capacity + || indexRing.cursor < 0 || indexRing.cursor > indexRing.capacity ) { + return false; + } + + checkpoint.frameSlot = vkExec.frameSlot; + checkpoint.vertexCursor = vertexRing.cursor; + checkpoint.indexCursor = indexRing.cursor; + checkpoint.boundVertexOffset = vkExec.boundVertexOffset; + memcpy( checkpoint.vertMemo, vkExec.vertMemo, + sizeof( checkpoint.vertMemo ) ); + memcpy( checkpoint.idxMemo, vkExec.idxMemo, + sizeof( checkpoint.idxMemo ) ); + memcpy( checkpoint.gpuSkinningMemo, vkExec.gpuSkinningMemo, + sizeof( checkpoint.gpuSkinningMemo ) ); + checkpoint.active = true; + return true; +} + +static void VK_Exec_SharedGeometryRestore( void ) { + vkSharedGeometryCheckpoint_t &checkpoint = vkSharedGeometryCheckpoint; + if ( !checkpoint.active ) { + return; + } + if ( checkpoint.frameSlot >= 0 + && checkpoint.frameSlot < VK_FRAMES_IN_FLIGHT ) { + vkExec.vertexRings[ checkpoint.frameSlot ].cursor = + checkpoint.vertexCursor; + vkExec.indexRings[ checkpoint.frameSlot ].cursor = + checkpoint.indexCursor; + vkExec.boundVertexOffset = checkpoint.boundVertexOffset; + memcpy( vkExec.vertMemo, checkpoint.vertMemo, + sizeof( checkpoint.vertMemo ) ); + memcpy( vkExec.idxMemo, checkpoint.idxMemo, + sizeof( checkpoint.idxMemo ) ); + memcpy( vkExec.gpuSkinningMemo, checkpoint.gpuSkinningMemo, + sizeof( checkpoint.gpuSkinningMemo ) ); + } + checkpoint.active = false; +} + +static void VK_Exec_SharedGeometryCommit( void ) { + // The complete shared view is now ready. Its retained offsets and memo + // entries become ordinary frame allocations consumed by the committed draw. + vkSharedGeometryCheckpoint.active = false; +} + +// Preserve the interaction/fog ABI while all shared classic domains use the +// same single-owner geometry transaction internally. +bool VK_Exec_SharedInteractionGeometryCheckpoint( void ) { + return VK_Exec_SharedGeometryCheckpoint(); +} + +void VK_Exec_SharedInteractionGeometryRestore( void ) { + VK_Exec_SharedGeometryRestore(); +} + +void VK_Exec_SharedInteractionGeometryCommit( void ) { + VK_Exec_SharedGeometryCommit(); +} + +// Shared classic domains preflight every geometry upload before they take +// framebuffer ownership. Keep the ring/memo implementation private while +// exposing the exact retained offsets to the Vulkan interaction consumer. +bool VK_Exec_PrepareTriGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ) { + (void)cmd; + return VK_Exec_PrepareTriGeometryOffsets( slot, tri, + vertexOffset, indexOffset ); +} + +void VK_Exec_BindPreparedTriGeometry( VkCommandBuffer cmd, int slot, + int vertexOffset, int indexOffset ) { + if ( cmd == VK_NULL_HANDLE || slot < 0 || slot >= VK_FRAMES_IN_FLIGHT + || vertexOffset < 0 || indexOffset < 0 ) { + return; + } + const VkBuffer vertexBuffer = vkExec.vertexRings[ slot ].buffer; + const VkDeviceSize bindOffset = static_cast( vertexOffset ); + vkCmdBindVertexBuffers( cmd, 0, 1, &vertexBuffer, &bindOffset ); + vkCmdBindIndexBuffer( cmd, vkExec.indexRings[ slot ].buffer, + static_cast( indexOffset ), VK_INDEX_TYPE_UINT32 ); +} + +static void VK_ClassicGui_BindPreparedGeometry( VkCommandBuffer cmd, int slot, + const vkClassicGuiDrawPlan_t &plan ) { + const VkBuffer vertexBuffer = vkExec.vertexRings[ slot ].buffer; + const VkDeviceSize vertexOffset = static_cast( plan.vertexOffset ); + vkCmdBindVertexBuffers( cmd, 0, 1, &vertexBuffer, &vertexOffset ); + vkCmdBindIndexBuffer( cmd, vkExec.indexRings[ slot ].buffer, + static_cast( plan.indexOffset ), VK_INDEX_TYPE_UINT32 ); +} + +static void VK_ClassicGui_SetPushTextureMatrix( + const rendererEvaluatedMaterialPass_t &pass, + vkGuiPushConstants_t &push ) { + float offsetS = pass.textureMatrix[ 2 ]; + float offsetT = pass.textureMatrix[ 5 ]; + // Match RB_GetShaderTextureMatrix exactly: only large translations wrap, + // retaining center-rotation/scale offsets in the ordinary range. + if ( offsetS < -40.0f || offsetS > 40.0f ) { + offsetS -= static_cast( offsetS ); + } + if ( offsetT < -40.0f || offsetT > 40.0f ) { + offsetT -= static_cast( offsetT ); + } + push.texMatrixS[ 0 ] = pass.textureMatrix[ 0 ]; + push.texMatrixS[ 1 ] = pass.textureMatrix[ 1 ]; + push.texMatrixS[ 2 ] = 0.0f; + push.texMatrixS[ 3 ] = offsetS; + push.texMatrixT[ 0 ] = pass.textureMatrix[ 3 ]; + push.texMatrixT[ 1 ] = pass.textureMatrix[ 4 ]; + push.texMatrixT[ 2 ] = 0.0f; + push.texMatrixT[ 3 ] = offsetT; + push.params[ 3 ] = + pass.textureMatrix[ 0 ] != 1.0f || pass.textureMatrix[ 1 ] != 0.0f + || offsetS != 0.0f || pass.textureMatrix[ 3 ] != 0.0f + || pass.textureMatrix[ 4 ] != 1.0f || offsetT != 0.0f + ? 1.0f : 0.0f; +} + +static bool VK_ClassicGui_DrawOwnedViewForScope( const viewDef_t *viewDef, + classicGuiDomainScope_t scope ) { + const bool inWorld = scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD; + const classicGuiDomainView_t *view = inWorld + ? R_ClassicGuiDomain_FindInWorldView( viewDef ) + : R_ClassicGuiDomain_FindRootView( viewDef ); + if ( view == NULL ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, -1 ); + } + if ( view->scope != scope || !view->ready ) { + return VK_ClassicGui_Fail( viewDef, + view->failure != CLASSIC_GUI_DOMAIN_FAILURE_NONE + ? view->failure : CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + // The same immutable view pointer may legitimately appear in more than one + // RC_DRAW_VIEW command. An earlier owned outcome must not suppress that + // later command's draw. A recorded fallback remains classic for every + // occurrence; duplicate ownership reporting after commit is diagnostic. + if ( view->backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_VULKAN ] + == CLASSIC_GUI_DOMAIN_BACKEND_FALLBACK ) { + return false; + } + + if ( view->viewDef != viewDef + || ( !inWorld && view->sourceSurfaceCount != viewDef->numDrawSurfs ) + || ( inWorld && view->sourceSurfaceCount > viewDef->numDrawSurfs ) + || view->sourceSurfaceCount < 0 + || view->sourceSurfaceCount > SCENE_PACKET_MAX_DRAWS + || view->drawableSurfaceCount < 0 + || view->drawableSurfaceCount > view->sourceSurfaceCount + || view->noopSurfaceCount + != view->sourceSurfaceCount - view->drawableSurfaceCount + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DOMAIN_COUNTS ); + } + if ( ( !inWorld && ( viewDef->viewEntitys != NULL + || viewDef->viewLights != NULL || viewDef->renderWorld != NULL ) ) + || ( inWorld && ( viewDef->viewEntitys == NULL + || viewDef->renderWorld == NULL ) ) + || viewDef->isSubview + || viewDef->isMirror || viewDef->isXraySubview || viewDef->isEditor + || viewDef->superView != NULL || viewDef->subviewSurface != NULL + || viewDef->numClipPlanes != 0 + || ( !inWorld && viewDef->renderFlags != 0 ) + || viewDef->numOutlineDrawSurfs != 0 + || viewDef->renderView.globalMaterial != NULL + || r_showOverDraw.GetInteger() != 0 + || r_singleTriangle.GetBool() ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_VIEW_MUTATION ); + } + if ( vkExec.activeRenderTexture != NULL || vkExec.activeColorEntry != NULL + || vkExec.activeDepthEntry != NULL || backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_OFFSCREEN_TARGET ); + } + if ( !vkExec.frameOpen || !vkExec.mainScopeOpen + || vkExec.cmd == VK_NULL_HANDLE + || vkExec.pendingSpecialEffectsView != NULL + || vkExec.pendingSpecialEffectsMask != 0 + || vkExec.pendingSpecialEffectsSource != NULL + || vkExec.pendingSpecialEffectsNeedsResolve + || !VK_Exec_PipelineTargetsMatch( vkExec.activePipelineTarget, + VK_Exec_SwapchainPipelineTarget() ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_GUI_REJECT_RENDER_SCOPE ); + } + + const int framebufferWidth = VK_Exec_ActiveFramebufferWidth(); + const int framebufferHeight = VK_Exec_ActiveFramebufferHeight(); + const int viewportWidth = view->viewportX2 - view->viewportX1 + 1; + const int viewportHeight = view->viewportY2 - view->viewportY1 + 1; + if ( view->viewportX1 != viewDef->viewport.x1 + || view->viewportY1 != viewDef->viewport.y1 + || view->viewportX2 != viewDef->viewport.x2 + || view->viewportY2 != viewDef->viewport.y2 + || view->scissorX1 != viewDef->scissor.x1 + || view->scissorY1 != viewDef->scissor.y1 + || view->scissorX2 != viewDef->scissor.x2 + || view->scissorY2 != viewDef->scissor.y2 + || memcmp( view->projectionMatrix, viewDef->projectionMatrix, + sizeof( view->projectionMatrix ) ) != 0 + || viewportWidth <= 0 || viewportHeight <= 0 + || view->viewportX1 < 0 || view->viewportY1 < 0 + || view->viewportX2 >= framebufferWidth + || view->viewportY2 >= framebufferHeight + || view->drawCount < 0 + || view->drawCount > CLASSIC_GUI_DOMAIN_MAX_DRAWS + || view->evaluatedPassCount < 0 + || view->evaluatedPassCount + > CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_VIEWPORT ); + } + VkViewport viewport; + if ( !VK_BuildCanonicalViewport( viewport, + static_cast( view->viewportX1 ), + static_cast( view->viewportY1 ), + static_cast( viewportWidth ), + static_cast( viewportHeight ), + static_cast( framebufferHeight ) ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_VIEWPORT ); + } + for ( int matrixIndex = 0; matrixIndex < 16; ++matrixIndex ) { + if ( !std::isfinite( view->projectionMatrix[ matrixIndex ] ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_VIEW_MUTATION ); + } + } + + int planDrawCount = 0; + int planPassCount = 0; + int drawablePasses = 0; + int activePasses = 0; + int noopPasses = 0; + int inactivePasses = 0; + int activeNoopPasses = 0; + int sourceNoopSurfaces = 0; + int previousSourceSurface = -1; + for ( int drawIndex = 0; drawIndex < view->drawCount; ++drawIndex ) { + const classicGuiDomainDraw_t *draw = + R_ClassicGuiDomain_ViewDraw( *view, drawIndex ); + if ( draw == NULL || draw->sourceSurfaceIndex <= previousSourceSurface + || draw->sourceSurfaceIndex < 0 + || draw->sourceSurfaceIndex >= viewDef->numDrawSurfs + || viewDef->drawSurfs[ draw->sourceSurfaceIndex ] + != draw->legacyDrawSurf + ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD ); + } + if ( inWorld && ( draw->legacyDrawSurf == NULL + || ( draw->legacyDrawSurf->dsFlags & DSF_IN_WORLD_GUI ) == 0 ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD ); + } + previousSourceSurface = draw->sourceSurfaceIndex; + if ( draw->sourceSurface != CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE ) { + if ( !VK_ClassicGui_SourceNoopValid( *draw ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD ); + } + sourceNoopSurfaces++; + continue; + } + if ( draw->legacyDrawSurf == NULL || !draw->packetBacked + || draw->firstIndex != 0 || draw->vertexOffset != 0 + || draw->evaluatedPassCount < 0 + || planPassCount > CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES + - draw->evaluatedPassCount ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD ); + } + + vkClassicGuiDrawPlan_t &drawPlan = vkClassicGuiDrawPlans[ planDrawCount++ ]; + memset( &drawPlan, 0, sizeof( drawPlan ) ); + drawPlan.draw = draw; + drawPlan.tri = draw->legacyDrawSurf->geo; + drawPlan.vertexOffset = -1; + drawPlan.indexOffset = -1; + drawPlan.firstPass = planPassCount; + drawPlan.passCount = draw->evaluatedPassCount; + const srfTriangles_t *tri = drawPlan.tri; + if ( tri == NULL || tri->ambientCache == NULL + || tri->numVerts <= 0 || tri->numIndexes <= 0 + || tri->numIndexes % 3 != 0 + || tri->numVerts != draw->vertexCount + || tri->numIndexes != draw->indexCount + || draw->vertexCount > INT_MAX + / static_cast( sizeof( idDrawVert ) ) + || draw->indexCount > INT_MAX + / static_cast( sizeof( glIndex_t ) ) + || !draw->hasAmbientCache + || draw->hasIndexCache != ( tri->indexCache != NULL ) + || ( tri->indexes == NULL && tri->indexCache == NULL ) + || draw->legacyDrawSurf->decalColorCache != NULL ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_GEOMETRY ); + } + const long long scissorWidth = static_cast( draw->scissorX2 ) + - draw->scissorX1 + 1; + const long long scissorHeight = static_cast( draw->scissorY2 ) + - draw->scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > INT_MAX || scissorHeight > INT_MAX ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_GEOMETRY ); + } + if ( !VK_ClassicGui_BuildScissor( *view, *draw, + framebufferHeight, drawPlan.scissor ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_VIEWPORT ); + } + float mvpGL[ 16 ]; + myGlMultMatrix( draw->modelViewMatrix, view->projectionMatrix, mvpGL ); + VK_FixupClipSpaceZ( drawPlan.mvp, mvpGL ); + for ( int matrixIndex = 0; matrixIndex < 16; ++matrixIndex ) { + if ( !std::isfinite( drawPlan.mvp[ matrixIndex ] ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DRAW_RECORD ); + } + } + + int drawDrawablePasses = 0; + int drawActivePasses = 0; + int drawInactivePasses = 0; + int drawActiveNoopPasses = 0; + for ( int passIndex = 0; passIndex < draw->evaluatedPassCount; + ++passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicGuiDomain_DrawPass( *draw, passIndex ); + const materialResourceTextureBinding_t *binding = + R_ClassicGuiDomain_DrawPassTexture( *draw, passIndex ); + if ( pass == NULL || binding == NULL + || pass->order != static_cast( passIndex ) + || pass->sourceStageIndex < 0 + || pass->sourceStageIndex != binding->stageIndex + || pass->textureResourceId != binding->textureResourceId + || binding->image == NULL || binding->textureResourceId == 0 + || pass->active != ( pass->condition != 0.0f ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_RECORD ); + } + + vkClassicGuiPassPlan_t &passPlan = + vkClassicGuiPassPlans[ planPassCount++ ]; + memset( &passPlan, 0, sizeof( passPlan ) ); + passPlan.pass = pass; + if ( !VK_ClassicGui_MapState( *pass, inWorld, passPlan ) ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_STATE ); + } + + switch ( pass->disposition ) { + case RENDERER_MATERIAL_PASS_DRAW: + if ( !pass->active ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_RECORD ); + } + drawDrawablePasses++; + drawActivePasses++; + drawablePasses++; + activePasses++; + break; + case RENDERER_MATERIAL_PASS_INACTIVE_CONDITION: + if ( pass->active ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_RECORD ); + } + drawInactivePasses++; + inactivePasses++; + noopPasses++; + break; + case RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND: + case RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE: + case RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA: + if ( !pass->active ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_RECORD ); + } + drawActiveNoopPasses++; + drawActivePasses++; + activeNoopPasses++; + activePasses++; + noopPasses++; + break; + default: + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PASS_RECORD ); + } + + // The opaque resource must resolve for every ordered pass, including + // inactive/no-op entries. Only framebuffer-affecting passes spend + // descriptor and pipeline capacity. + const materialResourceTextureBinding_t *resolved = + R_MaterialResourceTable_ResolveTextureResource( + pass->textureResourceId ); + if ( resolved != binding || resolved->image != binding->image + || resolved->textureHandle != binding->textureHandle ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_TEXTURE_BINDING ); + } + idImage *image = const_cast( binding->image ); + const idImageOpts &imageOptions = image->GetOpts(); + if ( !binding->loaded || binding->defaulted || binding->missing + || binding->filter < TF_LINEAR + || binding->filter > TF_DEFAULT + || binding->repeat < TR_REPEAT + || binding->repeat > TR_CLAMP_TO_ZERO_ALPHA + || !image->IsLoaded() || image->IsDefaulted() + || image->GetDeviceHandle() != binding->textureHandle + || binding->textureHandle == 0 + || binding->textureHandle + >= static_cast( + sizeof( vkExec.descriptorCache ) + / sizeof( vkExec.descriptorCache[ 0 ] ) ) + || imageOptions.textureType != TT_2D + || imageOptions.width <= 0 || imageOptions.height <= 0 ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_TEXTURE_RESIDENCY ); + } + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + continue; + } + + if ( image->GetFilter() != binding->filter + || image->GetRepeat() != binding->repeat ) { + image->SetSamplerState( binding->filter, binding->repeat ); + } + const vkImageEntry_t *imageEntry = + VK_Image_GetEntry( binding->textureHandle ); + if ( imageEntry == NULL || imageEntry->isCube + || imageEntry->view == VK_NULL_HANDLE + || imageEntry->sampler == VK_NULL_HANDLE + || imageEntry->layout + != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_TEXTURE_RESIDENCY ); + } + passPlan.descriptor = VK_GuiExecutor_GetImageDescriptor( + binding->textureHandle ); + if ( passPlan.descriptor == VK_NULL_HANDLE ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_DESCRIPTOR ); + } + passPlan.pipeline = + VK_GuiExecutor_GetPipelineStrict( passPlan.stateBits ); + if ( passPlan.pipeline == VK_NULL_HANDLE ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_PIPELINE ); + } + } + if ( drawActivePasses != draw->activePassCount + || drawDrawablePasses != draw->drawablePassCount + || drawInactivePasses != draw->inactivePassCount + || drawActiveNoopPasses != draw->activeNoopPassCount + || drawInactivePasses + drawActiveNoopPasses + != draw->noopPassCount ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_GUI_REJECT_COVERAGE ); + } + } + + if ( planDrawCount != view->drawableSurfaceCount + || sourceNoopSurfaces != view->noopSurfaceCount + || planPassCount != view->evaluatedPassCount + || activePasses != view->activePassCount + || drawablePasses != view->drawablePassCount + || inactivePasses != view->inactivePassCount + || activeNoopPasses != view->activeNoopPassCount + || noopPasses != view->noopPassCount ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_GUI_REJECT_DOMAIN_COUNTS ); + } + + // Geometry upload is the final preflight phase. Retain every offset without + // recording bind state, then commit the ring/memo transaction only after the + // complete view fits. A mid-list failure leaves the classic walker untouched. + VkCommandBuffer cmd = vkExec.cmd; + const int slot = vkExec.frameSlot; + if ( !VK_Exec_SharedGeometryCheckpoint() ) { + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_GUI_REJECT_GEOMETRY ); + } + for ( int drawIndex = 0; drawIndex < planDrawCount; ++drawIndex ) { + vkClassicGuiDrawPlan_t &drawPlan = vkClassicGuiDrawPlans[ drawIndex ]; + if ( !VK_Exec_PrepareTriGeometryOffsets( slot, drawPlan.tri, + drawPlan.vertexOffset, drawPlan.indexOffset ) ) { + VK_Exec_SharedGeometryRestore(); + return VK_ClassicGui_Fail( viewDef, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_GUI_REJECT_GEOMETRY ); + } + } + VK_Exec_SharedGeometryCommit(); + + // Commit: every command below is infallible command-buffer recording over + // prevalidated handles and offsets. Preserve source-surface/pass order. + vkCmdSetViewport( cmd, 0, 1, &viewport ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + int submittedPasses = 0; + int submittedNoops = 0; + for ( int drawIndex = 0; drawIndex < planDrawCount; ++drawIndex ) { + const vkClassicGuiDrawPlan_t &drawPlan = + vkClassicGuiDrawPlans[ drawIndex ]; + VK_ClassicGui_BindPreparedGeometry( cmd, slot, drawPlan ); + vkCmdSetScissor( cmd, 0, 1, &drawPlan.scissor ); + for ( int localPass = 0; localPass < drawPlan.passCount; ++localPass ) { + const vkClassicGuiPassPlan_t &passPlan = + vkClassicGuiPassPlans[ drawPlan.firstPass + localPass ]; + const rendererEvaluatedMaterialPass_t &pass = *passPlan.pass; + if ( pass.disposition != RENDERER_MATERIAL_PASS_DRAW ) { + submittedNoops++; + continue; + } + + vkCmdSetDepthTestEnable( cmd, + pass.depth.testEnabled ? VK_TRUE : VK_FALSE ); + vkCmdSetDepthWriteEnable( cmd, + pass.depth.writeEnabled ? VK_TRUE : VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, passPlan.depthCompare ); + vkCmdSetCullMode( cmd, passPlan.cullMode ); + vkCmdSetDepthBiasEnable( cmd, + pass.polygonOffsetEnabled ? VK_TRUE : VK_FALSE ); + if ( pass.polygonOffsetEnabled ) { + vkCmdSetDepthBias( cmd, pass.polygonOffsetUnits, 0.0f, + pass.polygonOffsetFactor ); + } + + vkGuiPushConstants_t push; + memset( &push, 0, sizeof( push ) ); + memcpy( push.mvp, drawPlan.mvp, sizeof( push.mvp ) ); + memcpy( push.stageColor, pass.color, sizeof( push.stageColor ) ); + VK_ClassicGui_SetPushTextureMatrix( pass, push ); + switch ( pass.vertexColor ) { + case RENDERER_VERTEX_COLOR_MODULATE: push.params[ 0 ] = 1.0f; break; + case RENDERER_VERTEX_COLOR_INVERSE_MODULATE: push.params[ 0 ] = 2.0f; break; + default: push.params[ 0 ] = 0.0f; break; + } + push.params[ 1 ] = passPlan.alphaTestMode; + push.params[ 2 ] = pass.alphaTest; + + vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + passPlan.pipeline ); + vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkExec.pipelineLayout, 0, 1, &passPlan.descriptor, 0, NULL ); + vkCmdPushConstants( cmd, vkExec.pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( push ), &push ); + vkCmdDrawIndexed( cmd, + static_cast( drawPlan.draw->indexCount ), + 1, static_cast( drawPlan.draw->firstIndex ), + drawPlan.draw->vertexOffset, 0 ); + submittedPasses++; + } + } + vkCmdSetDepthTestEnable( cmd, inWorld ? VK_TRUE : VK_FALSE ); + vkCmdSetDepthWriteEnable( cmd, inWorld ? VK_TRUE : VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, + inWorld ? VK_COMPARE_OP_EQUAL : VK_COMPARE_OP_ALWAYS ); + vkCmdSetCullMode( cmd, inWorld ? VK_CULL_MODE_FRONT_BIT + : VK_CULL_MODE_NONE ); + vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + + const bool coverageRecorded = R_ClassicGuiDomain_RecordOwned( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, + submittedPasses, submittedNoops ); + if ( !coverageRecorded ) { + // The framebuffer work is already committed. Never compound a + // bookkeeping fault by replaying the complete classic view over it. + common->Warning( "Vulkan: shared classic GUI coverage record rejected after committed view" ); + } + return true; +} + +static bool VK_ClassicGui_DrawOwnedView( const viewDef_t *viewDef ) { + return VK_ClassicGui_DrawOwnedViewForScope( viewDef, + CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D ); +} + +static bool VK_ClassicGui_DrawOwnedInWorldView( const viewDef_t *viewDef ) { + return VK_ClassicGui_DrawOwnedViewForScope( viewDef, + CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD ); +} + +/* +=============================================================================== + + Backend-neutral classic world ambient/material whole-view consumer + + Every descriptor, strict pipeline key, geometry upload, state mapping, and + coverage count is validated before commit. The two ordered authored ranges + then execute at the established pre-fog and post-fog points without reading + legacy material stages or shader registers. + +=============================================================================== +*/ + +enum vkClassicWorldAmbientRejectDetail_t { + VK_CLASSIC_WORLD_AMBIENT_REJECT_NONE = 0, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEW_MUTATION, + VK_CLASSIC_WORLD_AMBIENT_REJECT_OFFSCREEN_TARGET, + VK_CLASSIC_WORLD_AMBIENT_REJECT_RENDER_SCOPE, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEWPORT, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DOMAIN_COUNTS, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DRAW_RECORD, + VK_CLASSIC_WORLD_AMBIENT_REJECT_GEOMETRY, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_STATE, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_BINDING, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_RESIDENCY, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DESCRIPTOR, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PIPELINE, + VK_CLASSIC_WORLD_AMBIENT_REJECT_COVERAGE, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PHASE +}; + +typedef struct vkClassicWorldAmbientDrawPlan_s { + const classicWorldAmbientDomainDraw_t *draw; + const srfTriangles_t *tri; + int vertexOffset; + int indexOffset; + int firstPass; + int passCount; + VkRect2D scissor; + float mvp[ 16 ]; +} vkClassicWorldAmbientDrawPlan_t; + +typedef struct vkClassicWorldAmbientPassPlan_s { + const rendererEvaluatedMaterialPass_t *pass; + VkPipeline pipeline; + VkDescriptorSet descriptor; + int stateBits; + float alphaTestMode; + VkCompareOp depthCompare; + VkCullModeFlags cullMode; +} vkClassicWorldAmbientPassPlan_t; + +typedef struct vkClassicWorldAmbientSamplerUndo_s { + idImage *image; + textureFilter_t filter; + textureRepeat_t repeat; +} vkClassicWorldAmbientSamplerUndo_t; + +typedef struct vkClassicWorldAmbientPreparedView_s { + const classicWorldAmbientDomainView_t *view; + const viewDef_t *viewDef; + VkViewport viewport; + int drawCount; + int passCount; + int drawablePasses; + int noopPasses; + int submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int submittedNoops; + bool ready; + bool committed; + bool completedPhase[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int samplerUndoCount; + vkClassicWorldAmbientSamplerUndo_t samplerUndo[ + CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ]; + vkClassicWorldAmbientDrawPlan_t draws[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS ]; + vkClassicWorldAmbientPassPlan_t passes[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ]; +} vkClassicWorldAmbientPreparedView_t; + +static vkClassicWorldAmbientPreparedView_t vkClassicWorldAmbientPreparedView; + +static void VK_ClassicWorldAmbient_RestoreSamplerState( void ) { + vkClassicWorldAmbientPreparedView_t &prepared = + vkClassicWorldAmbientPreparedView; + for ( int undoIndex = prepared.samplerUndoCount - 1; + undoIndex >= 0; --undoIndex ) { + const vkClassicWorldAmbientSamplerUndo_t &undo = + prepared.samplerUndo[ undoIndex ]; + if ( undo.image != NULL + && ( undo.image->GetFilter() != undo.filter + || undo.image->GetRepeat() != undo.repeat ) ) { + undo.image->SetSamplerState( undo.filter, undo.repeat ); + } + } + prepared.samplerUndoCount = 0; +} + +static bool VK_ClassicWorldAmbient_SetPreflightSamplerState( idImage *image, + textureFilter_t filter, textureRepeat_t repeat ) { + if ( image == NULL ) { + return false; + } + vkClassicWorldAmbientPreparedView_t &prepared = + vkClassicWorldAmbientPreparedView; + if ( image->GetFilter() == filter && image->GetRepeat() == repeat ) { + return true; + } + + bool alreadyTracked = false; + for ( int undoIndex = 0; undoIndex < prepared.samplerUndoCount; + ++undoIndex ) { + if ( prepared.samplerUndo[ undoIndex ].image == image ) { + alreadyTracked = true; + break; + } + } + if ( !alreadyTracked ) { + if ( prepared.samplerUndoCount + >= CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ) { + return false; + } + vkClassicWorldAmbientSamplerUndo_t &undo = + prepared.samplerUndo[ prepared.samplerUndoCount++ ]; + undo.image = image; + undo.filter = image->GetFilter(); + undo.repeat = image->GetRepeat(); + } + + image->SetSamplerState( filter, repeat ); + return image->GetFilter() == filter && image->GetRepeat() == repeat; +} + +static bool VK_ClassicWorldAmbient_Fail( const viewDef_t *viewDef, + classicWorldAmbientDomainFailure_t failure, int detail ) { + VK_ClassicWorldAmbient_RestoreSamplerState(); + R_ClassicWorldAmbientDomain_RecordBackendFallback( viewDef, + CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN, failure, detail ); + return false; +} + +static bool VK_ClassicWorldAmbient_SourceNoopValid( + const classicWorldAmbientDomainDraw_t &draw ) { + switch ( draw.sourceSurface ) { + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NULL_SURFACE: + if ( draw.legacyDrawSurf != NULL ) { + return false; + } + break; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NO_AMBIENT: + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: + if ( draw.legacyDrawSurf == NULL ) { + return false; + } + break; + default: + return false; + } + return !draw.packetBacked && draw.drawPacketIndex == -1 + && draw.depthDrawPacketIndex == -1 && !draw.depthPrerequisite + && draw.firstEvaluatedPass == -1 && draw.evaluatedPassCount == 0 + && draw.activePassCount == 0 && draw.drawablePassCount == 0 + && draw.inactivePassCount == 0 && draw.activeNoopPassCount == 0 + && draw.noopPassCount == 0; +} + +static bool VK_ClassicWorldAmbient_MapState( + const rendererEvaluatedMaterialPass_t &pass, + vkClassicWorldAmbientPassPlan_t &plan ) { + if ( pass.kind != RENDERER_MATERIAL_PASS_SURFACE + || pass.programFamily != RENDERER_PROGRAM_FIXED + || pass.programKey != 0 + || pass.texgen != RENDERER_TEXGEN_EXPLICIT + || pass.textureSemantic != RENDERER_TEXTURE_DIFFUSE + || pass.textureResourceId == 0 + || ( pass.colorWriteMask + & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 + || pass.vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || pass.vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || pass.blend.colorOperation != RENDERER_BLEND_OP_ADD + || pass.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || pass.blend.sourceAlpha != pass.blend.sourceColor + || pass.blend.destinationAlpha != pass.blend.destinationColor + || !pass.depth.testEnabled + || ( pass.depth.compareOperation != RENDERER_COMPARE_LESS_OR_EQUAL + && pass.depth.compareOperation != RENDERER_COMPARE_EQUAL + && pass.depth.compareOperation != RENDERER_COMPARE_ALWAYS ) ) { + return false; + } + const bool replacementBlend = pass.blend.sourceColor == RENDERER_BLEND_ONE + && pass.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( pass.blend.enabled == replacementBlend ) { + return false; + } + int sourceBits = 0; + int destinationBits = 0; + if ( !VK_ClassicGui_MapSourceBlend( pass.blend.sourceColor, sourceBits ) + || !VK_ClassicGui_MapDestinationBlend( + pass.blend.destinationColor, destinationBits ) + || !VK_ClassicGui_MapDepthCompare( + pass.depth.compareOperation, plan.depthCompare ) + || !VK_ClassicGui_MapAlphaTest( pass, plan.alphaTestMode ) + || !VK_ClassicGui_MapCull( pass.cull, plan.cullMode ) ) { + return false; + } + plan.stateBits = sourceBits | destinationBits; + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + plan.stateBits |= GLS_REDMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + plan.stateBits |= GLS_GREENMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + plan.stateBits |= GLS_BLUEMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + plan.stateBits |= GLS_ALPHAMASK; + } + if ( !std::isfinite( pass.condition ) || !std::isfinite( pass.alphaTest ) + || !std::isfinite( pass.polygonOffsetFactor ) + || !std::isfinite( pass.polygonOffsetUnits ) ) { + return false; + } + for ( int i = 0; i < 4; ++i ) { + if ( !std::isfinite( pass.color[i] ) ) { + return false; + } + } + for ( int i = 0; i < 6; ++i ) { + if ( !std::isfinite( pass.textureMatrix[i] ) ) { + return false; + } + } + return static_cast( pass.textureMatrix[2] ) >= INT_MIN + && static_cast( pass.textureMatrix[2] ) <= INT_MAX + && static_cast( pass.textureMatrix[5] ) >= INT_MIN + && static_cast( pass.textureMatrix[5] ) <= INT_MAX; +} + +static bool VK_ClassicWorldAmbient_BuildScissor( + const classicWorldAmbientDomainView_t &view, + const classicWorldAmbientDomainDraw_t &draw, int framebufferHeight, + VkRect2D &scissor ) { + const int viewportWidth = view.viewportX2 - view.viewportX1 + 1; + const int viewportHeight = view.viewportY2 - view.viewportY1 + 1; + if ( viewportWidth <= 0 || viewportHeight <= 0 || framebufferHeight <= 0 ) { + return false; + } + const bool useDrawScissor = r_useScissor.GetBool(); + const int requestedX1 = useDrawScissor ? draw.scissorX1 : view.scissorX1; + const int requestedY1 = useDrawScissor ? draw.scissorY1 : view.scissorY1; + const int requestedX2 = useDrawScissor ? draw.scissorX2 : view.scissorX2; + const int requestedY2 = useDrawScissor ? draw.scissorY2 : view.scissorY2; + if ( requestedX2 < requestedX1 || requestedY2 < requestedY1 ) { + return false; + } + int x0 = Max( view.viewportX1, + view.viewportX1 + requestedX1 ); + int x1 = Min( view.viewportX1 + viewportWidth, + view.viewportX1 + requestedX2 + 1 ); + int y0GL = Max( view.viewportY1, + view.viewportY1 + requestedY1 ); + int y1GL = Min( view.viewportY1 + viewportHeight, + view.viewportY1 + requestedY2 + 1 ); + x0 = Max( 0, x0 ); + x1 = Min( VK_Exec_ActiveFramebufferWidth(), x1 ); + y0GL = Max( 0, y0GL ); + y1GL = Min( framebufferHeight, y1GL ); + if ( x1 <= x0 || y1GL <= y0GL ) { + return false; + } + const int y0 = framebufferHeight - y1GL; + const int y1 = framebufferHeight - y0GL; + scissor.offset.x = x0; + scissor.offset.y = y0; + scissor.extent.width = static_cast( x1 - x0 ); + scissor.extent.height = static_cast( y1 - y0 ); + return true; +} + +static bool VK_ClassicWorldAmbient_Preflight( const viewDef_t *viewDef ) { + std::memset( &vkClassicWorldAmbientPreparedView, 0, + sizeof( vkClassicWorldAmbientPreparedView ) ); + vkClassicWorldAmbientPreparedView_t &prepared = + vkClassicWorldAmbientPreparedView; + const classicWorldAmbientDomainView_t *view = + R_ClassicWorldAmbientDomain_FindView( viewDef ); + prepared.view = view; + prepared.viewDef = viewDef; + if ( view == NULL ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEW_MUTATION ); + } + if ( view->backendOutcome[ CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN ] + == CLASSIC_WORLD_AMBIENT_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + view->failure != CLASSIC_WORLD_AMBIENT_FAILURE_NONE + ? view->failure : CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + if ( viewDef == NULL || view->viewDef != viewDef + || viewDef->viewEntitys == NULL + || view->sourceSurfaceCount != viewDef->numDrawSurfs + || view->sourceSurfaceCount < 0 + || view->sourceSurfaceCount > SCENE_PACKET_MAX_DRAWS + || view->drawableSurfaceCount < 0 + || view->drawableSurfaceCount > view->sourceSurfaceCount + || view->noopSurfaceCount + != view->sourceSurfaceCount - view->drawableSurfaceCount + || view->drawCount < 0 + || view->drawCount > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS + || view->evaluatedPassCount < 0 + || view->evaluatedPassCount + > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DOMAIN_COUNTS ); + } + const int allowedRenderFlags = + RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderWorld == NULL || viewDef->viewLights != NULL + || viewDef->renderView.viewID < 0 + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 + || viewDef->renderView.globalMaterial != NULL + || r_showOverDraw.GetInteger() != 0 || r_singleTriangle.GetBool() + || r_skipAmbient.GetBool() || r_skipRender.GetBool() + || r_skipRenderContext.GetBool() + || r_portalsDistanceCull.GetBool() + || r_celShading.GetBool() || r_celShadingWorld.GetBool() + || r_forceAmbient.GetFloat() > 0.0f ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEW_MUTATION ); + } + if ( vkExec.activeRenderTexture != NULL || vkExec.activeColorEntry != NULL + || vkExec.activeDepthEntry != NULL || backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_OFFSCREEN_TARGET ); + } + if ( !vkExec.frameOpen || !vkExec.mainScopeOpen + || vkExec.cmd == VK_NULL_HANDLE + || vkExec.pendingSpecialEffectsView != NULL + || vkExec.pendingSpecialEffectsMask != 0 + || vkExec.pendingSpecialEffectsSource != NULL + || vkExec.pendingSpecialEffectsNeedsResolve + || !VK_Exec_PipelineTargetsMatch( vkExec.activePipelineTarget, + VK_Exec_SwapchainPipelineTarget() ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_WORLD_AMBIENT_REJECT_RENDER_SCOPE ); + } + + const int framebufferWidth = VK_Exec_ActiveFramebufferWidth(); + const int framebufferHeight = VK_Exec_ActiveFramebufferHeight(); + const int viewportWidth = view->viewportX2 - view->viewportX1 + 1; + const int viewportHeight = view->viewportY2 - view->viewportY1 + 1; + if ( view->viewportX1 != viewDef->viewport.x1 + || view->viewportY1 != viewDef->viewport.y1 + || view->viewportX2 != viewDef->viewport.x2 + || view->viewportY2 != viewDef->viewport.y2 + || view->scissorX1 != viewDef->scissor.x1 + || view->scissorY1 != viewDef->scissor.y1 + || view->scissorX2 != viewDef->scissor.x2 + || view->scissorY2 != viewDef->scissor.y2 + || std::memcmp( view->projectionMatrix, viewDef->projectionMatrix, + sizeof( view->projectionMatrix ) ) != 0 + || viewportWidth <= 0 || viewportHeight <= 0 + || view->viewportX1 < 0 || view->viewportY1 < 0 + || view->viewportX2 >= framebufferWidth + || view->viewportY2 >= framebufferHeight + || !VK_BuildCanonicalViewport( prepared.viewport, + static_cast( view->viewportX1 ), + static_cast( view->viewportY1 ), + static_cast( viewportWidth ), + static_cast( viewportHeight ), + static_cast( framebufferHeight ) ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEWPORT ); + } + for ( int matrixIndex = 0; matrixIndex < 16; ++matrixIndex ) { + if ( !std::isfinite( view->projectionMatrix[matrixIndex] ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEW_MUTATION ); + } + } + + int previousSourceSurface = -1; + int sourceNoopSurfaces = 0; + int drawablePasses = 0; + int activePasses = 0; + int inactivePasses = 0; + int activeNoopPasses = 0; + int noopPasses = 0; + int phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ] = { 0, 0 }; + int phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ] = { 0, 0 }; + for ( int drawIndex = 0; drawIndex < view->drawCount; ++drawIndex ) { + const classicWorldAmbientDomainDraw_t *draw = + R_ClassicWorldAmbientDomain_ViewDraw( *view, drawIndex ); + if ( draw == NULL || draw->sourceSurfaceIndex <= previousSourceSurface + || draw->sourceSurfaceIndex < 0 + || draw->sourceSurfaceIndex >= view->sourceSurfaceCount + || viewDef->drawSurfs[ draw->sourceSurfaceIndex ] + != draw->legacyDrawSurf ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DRAW_RECORD ); + } + previousSourceSurface = draw->sourceSurfaceIndex; + if ( draw->sourceSurface + != CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE ) { + if ( !VK_ClassicWorldAmbient_SourceNoopValid( *draw ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DRAW_RECORD ); + } + sourceNoopSurfaces++; + continue; + } + if ( draw->legacyDrawSurf == NULL || !draw->packetBacked + || draw->firstIndex != 0 || draw->vertexOffset != 0 + || draw->phase < CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG + || draw->phase >= CLASSIC_WORLD_AMBIENT_PHASE_COUNT + || draw->evaluatedPassCount < 0 + || prepared.passCount + > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES + - draw->evaluatedPassCount ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DRAW_RECORD ); + } + + vkClassicWorldAmbientDrawPlan_t &drawPlan = + prepared.draws[ prepared.drawCount++ ]; + std::memset( &drawPlan, 0, sizeof( drawPlan ) ); + drawPlan.draw = draw; + drawPlan.tri = draw->legacyDrawSurf->geo; + drawPlan.vertexOffset = -1; + drawPlan.indexOffset = -1; + drawPlan.firstPass = prepared.passCount; + drawPlan.passCount = draw->evaluatedPassCount; + const srfTriangles_t *tri = drawPlan.tri; + if ( tri == NULL || tri->ambientCache == NULL + || draw->tableGeneration != view->tableGeneration + || tri->numVerts <= 0 || tri->numIndexes <= 0 + || tri->numIndexes % 3 != 0 + || tri->numVerts != draw->vertexCount + || tri->numIndexes != draw->indexCount + || !draw->hasAmbientCache + || draw->hasIndexCache != ( tri->indexCache != NULL ) + || ( tri->indexes == NULL && tri->indexCache == NULL ) + || draw->legacyDrawSurf->decalColorCache != NULL ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_GEOMETRY ); + } + if ( !VK_ClassicWorldAmbient_BuildScissor( + *view, *draw, framebufferHeight, drawPlan.scissor ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_VIEWPORT ); + } + float mvpGL[ 16 ]; + myGlMultMatrix( draw->modelViewMatrix, view->projectionMatrix, mvpGL ); + VK_FixupClipSpaceZ( drawPlan.mvp, mvpGL ); + for ( int matrixIndex = 0; matrixIndex < 16; ++matrixIndex ) { + if ( !std::isfinite( drawPlan.mvp[matrixIndex] ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DRAW_RECORD ); + } + } + + int drawDrawablePasses = 0; + int drawActivePasses = 0; + int drawInactivePasses = 0; + int drawActiveNoopPasses = 0; + for ( int passIndex = 0; passIndex < draw->evaluatedPassCount; + ++passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicWorldAmbientDomain_DrawPass( *draw, passIndex ); + const materialResourceTextureBinding_t *binding = + R_ClassicWorldAmbientDomain_DrawPassTexture( *draw, passIndex ); + if ( pass == NULL || binding == NULL + || pass->order != static_cast( passIndex ) + || pass->sourceStageIndex < 0 + || pass->sourceStageIndex != binding->stageIndex + || pass->textureResourceId != binding->textureResourceId + || binding->image == NULL || binding->textureResourceId == 0 + || pass->active != ( pass->condition != 0.0f ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD ); + } + vkClassicWorldAmbientPassPlan_t &passPlan = + prepared.passes[ prepared.passCount++ ]; + std::memset( &passPlan, 0, sizeof( passPlan ) ); + passPlan.pass = pass; + if ( !VK_ClassicWorldAmbient_MapState( *pass, passPlan ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_STATE ); + } + switch ( pass->disposition ) { + case RENDERER_MATERIAL_PASS_DRAW: + if ( !pass->active ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD ); + } + drawDrawablePasses++; + drawActivePasses++; + drawablePasses++; + activePasses++; + phaseDrawablePasses[ draw->phase ]++; + break; + case RENDERER_MATERIAL_PASS_INACTIVE_CONDITION: + if ( pass->active ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD ); + } + drawInactivePasses++; + inactivePasses++; + noopPasses++; + phaseNoopPasses[ draw->phase ]++; + break; + case RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND: + case RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE: + case RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA: + if ( !pass->active ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD ); + } + drawActiveNoopPasses++; + drawActivePasses++; + activeNoopPasses++; + activePasses++; + noopPasses++; + phaseNoopPasses[ draw->phase ]++; + break; + default: + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PASS_RECORD ); + } + + const materialResourceTextureBinding_t *resolved = + R_MaterialResourceTable_ResolveTextureResource( + pass->textureResourceId ); + if ( resolved != binding || resolved->image != binding->image + || resolved->textureHandle != binding->textureHandle ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_BINDING ); + } + idImage *image = const_cast( binding->image ); + const idImageOpts &imageOptions = image->GetOpts(); + if ( !binding->loaded || binding->defaulted || binding->missing + || binding->filter < TF_LINEAR + || binding->filter > TF_DEFAULT + || binding->repeat < TR_REPEAT + || binding->repeat > TR_CLAMP_TO_ZERO_ALPHA + || !image->IsLoaded() || image->IsDefaulted() + || image->GetDeviceHandle() != binding->textureHandle + || binding->textureHandle == 0 + || imageOptions.textureType != TT_2D + || imageOptions.width <= 0 || imageOptions.height <= 0 ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_RESIDENCY ); + } + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + continue; + } + if ( !VK_ClassicWorldAmbient_SetPreflightSamplerState( + image, binding->filter, binding->repeat ) ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_RESIDENCY ); + } + const vkImageEntry_t *imageEntry = + VK_Image_GetEntry( binding->textureHandle ); + if ( imageEntry == NULL || imageEntry->isCube + || imageEntry->view == VK_NULL_HANDLE + || imageEntry->sampler == VK_NULL_HANDLE + || imageEntry->layout + != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_TEXTURE_RESIDENCY ); + } + passPlan.descriptor = VK_GuiExecutor_GetImageDescriptor( + binding->textureHandle ); + if ( passPlan.descriptor == VK_NULL_HANDLE ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DESCRIPTOR ); + } + passPlan.pipeline = + VK_GuiExecutor_GetPipelineStrict( passPlan.stateBits ); + if ( passPlan.pipeline == VK_NULL_HANDLE ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_PIPELINE ); + } + } + if ( drawActivePasses != draw->activePassCount + || drawDrawablePasses != draw->drawablePassCount + || drawInactivePasses != draw->inactivePassCount + || drawActiveNoopPasses != draw->activeNoopPassCount + || drawInactivePasses + drawActiveNoopPasses + != draw->noopPassCount ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_WORLD_AMBIENT_REJECT_COVERAGE ); + } + } + + if ( prepared.drawCount != view->drawableSurfaceCount + || sourceNoopSurfaces != view->noopSurfaceCount + || prepared.passCount != view->evaluatedPassCount + || activePasses != view->activePassCount + || drawablePasses != view->drawablePassCount + || inactivePasses != view->inactivePassCount + || activeNoopPasses != view->activeNoopPassCount + || noopPasses != view->noopPassCount + || phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + != view->phaseDrawablePassCount[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + || phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + != view->phaseDrawablePassCount[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + || phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + != view->phaseNoopPassCount[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + || phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + != view->phaseNoopPassCount[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_WORLD_AMBIENT_REJECT_DOMAIN_COUNTS ); + } + + // Final preflight stage: retain every exact geometry offset without recording + // bind state. Commit only once the complete upload set fits; otherwise the + // ring cursors, memos, bound offset, and sampler state all roll back. + if ( !VK_Exec_SharedGeometryCheckpoint() ) { + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_WORLD_AMBIENT_REJECT_GEOMETRY ); + } + for ( int drawIndex = 0; drawIndex < prepared.drawCount; ++drawIndex ) { + vkClassicWorldAmbientDrawPlan_t &drawPlan = prepared.draws[drawIndex]; + if ( !VK_Exec_PrepareTriGeometryOffsets( vkExec.frameSlot, drawPlan.tri, + drawPlan.vertexOffset, drawPlan.indexOffset ) ) { + VK_Exec_SharedGeometryRestore(); + return VK_ClassicWorldAmbient_Fail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_WORLD_AMBIENT_REJECT_GEOMETRY ); + } + } + VK_Exec_SharedGeometryCommit(); + // The desired per-stage descriptors are now sealed and geometry ownership + // cannot fall back, so retain the final sampler state exactly as before. + prepared.samplerUndoCount = 0; + prepared.drawablePasses = drawablePasses; + prepared.noopPasses = noopPasses; + prepared.ready = true; + return true; +} + +static void VK_ClassicWorldAmbient_DrawPhase( + classicWorldAmbientPhase_t phase ) { + vkClassicWorldAmbientPreparedView_t &prepared = + vkClassicWorldAmbientPreparedView; + if ( !prepared.ready || prepared.view == NULL || prepared.viewDef == NULL + || phase < CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG + || phase >= CLASSIC_WORLD_AMBIENT_PHASE_COUNT + || prepared.completedPhase[phase] ) { + return; + } + prepared.committed = true; + prepared.completedPhase[phase] = true; + VkCommandBuffer cmd = vkExec.cmd; + const int slot = vkExec.frameSlot; + vkCmdSetViewport( cmd, 0, 1, &prepared.viewport ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + for ( int drawIndex = 0; drawIndex < prepared.drawCount; ++drawIndex ) { + const vkClassicWorldAmbientDrawPlan_t &drawPlan = + prepared.draws[drawIndex]; + if ( drawPlan.draw->phase != phase ) { + continue; + } + const VkBuffer vertexBuffer = vkExec.vertexRings[slot].buffer; + const VkDeviceSize vertexOffset = + static_cast( drawPlan.vertexOffset ); + vkCmdBindVertexBuffers( cmd, 0, 1, &vertexBuffer, &vertexOffset ); + vkCmdBindIndexBuffer( cmd, vkExec.indexRings[slot].buffer, + static_cast( drawPlan.indexOffset ), + VK_INDEX_TYPE_UINT32 ); + vkCmdSetScissor( cmd, 0, 1, &drawPlan.scissor ); + for ( int localPass = 0; localPass < drawPlan.passCount; ++localPass ) { + const vkClassicWorldAmbientPassPlan_t &passPlan = + prepared.passes[ drawPlan.firstPass + localPass ]; + const rendererEvaluatedMaterialPass_t &pass = *passPlan.pass; + if ( pass.disposition != RENDERER_MATERIAL_PASS_DRAW ) { + prepared.submittedNoops++; + continue; + } + vkCmdSetDepthTestEnable( cmd, VK_TRUE ); + vkCmdSetDepthWriteEnable( cmd, + pass.depth.writeEnabled ? VK_TRUE : VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, passPlan.depthCompare ); + vkCmdSetCullMode( cmd, passPlan.cullMode ); + vkCmdSetDepthBiasEnable( cmd, + pass.polygonOffsetEnabled ? VK_TRUE : VK_FALSE ); + if ( pass.polygonOffsetEnabled ) { + vkCmdSetDepthBias( cmd, pass.polygonOffsetUnits, 0.0f, + pass.polygonOffsetFactor ); + } + vkGuiPushConstants_t push; + std::memset( &push, 0, sizeof( push ) ); + std::memcpy( push.mvp, drawPlan.mvp, sizeof( push.mvp ) ); + std::memcpy( push.stageColor, pass.color, + sizeof( push.stageColor ) ); + VK_ClassicGui_SetPushTextureMatrix( pass, push ); + switch ( pass.vertexColor ) { + case RENDERER_VERTEX_COLOR_MODULATE: push.params[0] = 1.0f; break; + case RENDERER_VERTEX_COLOR_INVERSE_MODULATE: push.params[0] = 2.0f; break; + default: push.params[0] = 0.0f; break; + } + push.params[1] = passPlan.alphaTestMode; + push.params[2] = pass.alphaTest; + vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + passPlan.pipeline ); + vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + vkExec.pipelineLayout, 0, 1, &passPlan.descriptor, 0, NULL ); + vkCmdPushConstants( cmd, vkExec.pipelineLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( push ), &push ); + vkCmdDrawIndexed( cmd, + static_cast( drawPlan.draw->indexCount ), 1, + static_cast( drawPlan.draw->firstIndex ), + drawPlan.draw->vertexOffset, 0 ); + prepared.submittedPasses[phase]++; + } + } + vkCmdSetDepthTestEnable( cmd, VK_TRUE ); + vkCmdSetDepthWriteEnable( cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_LESS_OR_EQUAL ); + vkCmdSetCullMode( cmd, VK_CULL_MODE_FRONT_BIT ); + vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + if ( phase == CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ) { + const bool coverageRecorded = R_ClassicWorldAmbientDomain_RecordOwned( + prepared.viewDef, CLASSIC_WORLD_AMBIENT_BACKEND_VULKAN, + prepared.submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ], + prepared.submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ], + prepared.submittedNoops ); + if ( !coverageRecorded ) { + common->Warning( "Vulkan: shared world ambient coverage rejected after committed view" ); + } + } +} + +enum vkClassicCinematicPostRejectDetail_t { + VK_CLASSIC_CINEMATIC_POST_REJECT_NOT_READY = 0, + VK_CLASSIC_CINEMATIC_POST_REJECT_SKIP_OR_EMPTY, + VK_CLASSIC_CINEMATIC_POST_REJECT_FEEDBACK, + VK_CLASSIC_CINEMATIC_POST_REJECT_BEGIN_FRAME, + VK_CLASSIC_CINEMATIC_POST_REJECT_GPU_SKINNING, + VK_CLASSIC_CINEMATIC_POST_REJECT_VIEWPORT, + VK_CLASSIC_CINEMATIC_POST_REJECT_NO_POST_WALK +}; + +static void VK_RecordCinematicPostFallbackIfPending( + const viewDef_t *viewDef, classicCinematicPostDomainScope_t scope, + classicCinematicPostDomainFailure_t failure, int detail ) { + if ( !r_rendererSharedCinematicPost.GetBool() ) { + return; + } + const classicCinematicPostDomainView_t *view = NULL; + if ( scope == CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC ) { + view = R_ClassicCinematicPostDomain_FindRootCinematicView( viewDef ); + } else if ( scope == CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST ) { + view = R_ClassicCinematicPostDomain_FindAuthoredPostView( viewDef ); + } else { + return; + } + if ( view == NULL + || view->backendOutcome[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN] + != CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED + || view->backendCompleted[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN] ) { + return; + } + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, failure, detail ); +} + +/* +==================== +VK_GuiExecutor_Draw2DView + +The RB_STD_DrawShaderPasses contract for 2D views, on the swapchain. +==================== +*/ +void VK_GuiExecutor_Draw2DView( const viewDef_t *viewDef ) { + if ( viewDef == NULL ) { + return; + } + if ( vkExec.temporalScenePendingComposite ) { + if ( !VK_GuiExecutor_BeginFrame() + || !VK_TemporalPresentation_CompositePendingScene() ) { + return; + } + } + const bool sharedCinematicRoot = r_rendererSharedCinematicPost.GetBool() + && R_ClassicCinematicPostDomain_FindRootCinematicView( viewDef ) != NULL; + backEnd.currentRenderCopied = false; + backEnd.currentDepthCopied = false; + if ( viewDef->numDrawSurfs <= 0 ) { + if ( r_rendererSharedGui.GetBool() ) { + if ( !VK_GuiExecutor_BeginFrame() ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_GUI_REJECT_RENDER_SCOPE ); + return; + } + backEnd.viewDef = const_cast( viewDef ); + (void)VK_ClassicGui_DrawOwnedView( viewDef ); + } + return; + } + if ( !VK_GuiExecutor_BeginFrame() ) { + if ( r_rendererSharedGui.GetBool() ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_GUI_REJECT_RENDER_SCOPE ); + } + if ( sharedCinematicRoot ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_CINEMATIC_POST_REJECT_NOT_READY ); + } + return; + } + VK_TemporalPresentation_ObserveHistoryAndCapture(); + + backEnd.viewDef = (viewDef_t *)viewDef; + if ( r_rendererSharedGui.GetBool() && !sharedCinematicRoot + && VK_ClassicGui_DrawOwnedView( viewDef ) ) { + return; + } + + VkCommandBuffer cmd = vkExec.cmd; + const int slot = vkExec.frameSlot; + const int fbHeight = VK_Exec_ActiveFramebufferHeight(); + + // 2D semantics regardless of what an earlier 3D view left behind + vkCmdSetDepthTestEnable( cmd, VK_FALSE ); + vkCmdSetDepthWriteEnable( cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_ALWAYS ); + vkCmdSetCullMode( cmd, VK_CULL_MODE_NONE ); + vkCmdSetFrontFace( cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); // GL bottom-left viewport -> Vulkan negative-height viewport const int vpX = viewDef->viewport.x1; @@ -6394,12 +10833,14 @@ void VK_GuiExecutor_Draw2DView( const viewDef_t *viewDef ) { const int vpH = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; VkViewport viewport; - viewport.x = (float)vpX; - viewport.y = (float)( fbHeight - vpYGL ); - viewport.width = (float)vpW; - viewport.height = -(float)vpH; - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; + if ( !VK_BuildCanonicalViewport( viewport, (float)vpX, (float)vpYGL, + (float)vpW, (float)vpH, (float)fbHeight ) ) { + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_VIEWPORT ); + return; + } vkCmdSetViewport( cmd, 0, 1, &viewport ); // the front-end ortho projection is the whole-view MVP @@ -6422,6 +10863,22 @@ void VK_GuiExecutor_Draw2DView( const viewDef_t *viewDef ) { VK_Exec_SetSurfScissor( cmd, viewDef, drawSurf, fbHeight ); VK_Exec_DrawAmbientStages( viewDef, drawSurf, tri, mvp, false ); } + if ( sharedCinematicRoot ) { + if ( !R_ClassicCinematicPostDomain_ReadyForBackend( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ) ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_NOT_READY ); + } else if ( !R_ClassicCinematicPostDomain_RecordOwned( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + viewDef->numDrawSurfs ) ) { + common->Warning( "Vulkan: shared cinematic root coverage rejected after committed view" ); + } + } } /* @@ -6439,12 +10896,32 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { if ( viewDef == NULL ) { return; } + const bool sharedRenderDemo = r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRenderDemoView( viewDef ) != NULL; + const bool sharedRenderDemoReady = sharedRenderDemo + && R_ClassicSpecialFrameDomain_ReadyForBackend( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN ); + if ( sharedRenderDemo && !sharedRenderDemoReady ) { + R_ClassicSpecialFrameDomain_RecordBackendFallback( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_NOT_READY, 0 ); + } backEnd.currentRenderCopied = false; backEnd.currentDepthCopied = false; if ( viewDef->numDrawSurfs <= 0 ) { + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_SKIP_OR_EMPTY ); return; } if ( !VK_GuiExecutor_BeginFrame() ) { + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_CINEMATIC_POST_REJECT_BEGIN_FRAME ); return; } @@ -6452,12 +10929,33 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { VkCommandBuffer cmd = vkExec.cmd; const int slot = vkExec.frameSlot; - const int fbHeight = VK_Exec_ActiveFramebufferHeight(); - const int fbWidth = VK_Exec_ActiveFramebufferWidth(); drawSurf_t **drawSurfs = (drawSurf_t **)viewDef->drawSurfs; const int numDrawSurfs = viewDef->numDrawSurfs; - + // This is deliberately before any viewport/depth/cull state owned by the + // caller. Compute ends and resumes dynamic rendering exactly once, then the + // ordinary 3D walk establishes all graphics state from scratch. + if ( !VK_GpuSkinning_PrepareView( viewDef ) ) { + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_GPU_SKINNING ); + return; + } + vkSceneScaleState_t sceneScaleState; + VK_TemporalPresentation_ClearScaleState( sceneScaleState ); + if ( !VK_TemporalPresentation_BeginView( viewDef, sceneScaleState ) ) { + VK_TemporalPresentation_RecenterDirectProjection( + sceneScaleState, viewDef ); + } + const int fbHeight = VK_Exec_ActiveFramebufferHeight(); + const int fbWidth = VK_Exec_ActiveFramebufferWidth(); + // Seal the complete shared ambient plan before the depth clear or any + // other framebuffer-affecting command. A failed preflight leaves the + // established Vulkan world walker completely untouched. + const bool sharedWorldAmbientOwned = + !sceneScaleState.active && r_rendererSharedWorldAmbient.GetBool() + && VK_ClassicWorldAmbient_Preflight( viewDef ); // GL bottom-left viewport -> Vulkan negative-height viewport const int vpX = viewDef->viewport.x1; const int vpYGL = viewDef->viewport.y1; @@ -6465,12 +10963,15 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { const int vpH = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; VkViewport viewport; - viewport.x = (float)vpX; - viewport.y = (float)( fbHeight - vpYGL ); - viewport.width = (float)vpW; - viewport.height = -(float)vpH; - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; + if ( !VK_BuildCanonicalViewport( viewport, (float)vpX, (float)vpYGL, + (float)vpW, (float)vpH, (float)fbHeight ) ) { + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_VIEWPORT ); + VK_TemporalPresentation_EndView( viewDef, sceneScaleState, false ); + return; + } vkCmdSetViewport( cmd, 0, 1, &viewport ); // every 3D view starts from clean depth/stencil, exactly like @@ -6522,6 +11023,12 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { float mvp[ 16 ]; VK_FixupClipSpaceZ( mvp, viewDef->projectionMatrix ); + // same-value pipeline/descriptor re-binds are semantic no-ops; the fill + // uses at most two pipelines and mostly the white-image descriptor, so + // skip re-recording identical binds (no other bind sites exist in this loop) + VkPipeline depthFillBoundPipeline = VK_NULL_HANDLE; + VkDescriptorSet depthFillBoundSet = VK_NULL_HANDLE; + // ---- pass 1: depth fill (RB_STD_FillDepthBuffer contract) ---- vkCmdSetDepthTestEnable( cmd, VK_TRUE ); vkCmdSetDepthWriteEnable( cmd, VK_TRUE ); @@ -6640,8 +11147,14 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { if ( pipeline == VK_NULL_HANDLE ) { continue; } - vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline ); - vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vkExec.pipelineLayout, 0, 1, &stageDescriptor, 0, NULL ); + if ( pipeline != depthFillBoundPipeline ) { + vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline ); + depthFillBoundPipeline = pipeline; + } + if ( stageDescriptor != depthFillBoundSet ) { + vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vkExec.pipelineLayout, 0, 1, &stageDescriptor, 0, NULL ); + depthFillBoundSet = stageDescriptor; + } vkCmdPushConstants( cmd, vkExec.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof( push ), &push ); vkCmdDrawIndexed( cmd, (uint32_t)tri->numIndexes, 1, 0, 0, 0 ); @@ -6667,8 +11180,14 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { VkDescriptorSet whiteDescriptor = VK_GuiExecutor_GetImageDescriptor( globalImages->whiteImage->GetDeviceHandle() ); VkPipeline pipeline = VK_GuiExecutor_GetPipeline( fillBlendBits ); if ( whiteDescriptor != VK_NULL_HANDLE && pipeline != VK_NULL_HANDLE ) { - vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline ); - vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vkExec.pipelineLayout, 0, 1, &whiteDescriptor, 0, NULL ); + if ( pipeline != depthFillBoundPipeline ) { + vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline ); + depthFillBoundPipeline = pipeline; + } + if ( whiteDescriptor != depthFillBoundSet ) { + vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, vkExec.pipelineLayout, 0, 1, &whiteDescriptor, 0, NULL ); + depthFillBoundSet = whiteDescriptor; + } vkCmdPushConstants( cmd, vkExec.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof( push ), &push ); vkCmdDrawIndexed( cmd, (uint32_t)tri->numIndexes, 1, 0, 0, 0 ); @@ -6685,11 +11204,28 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { // then the ambient walks. The pass tracks its own space/depth-range // state and exits at maxDepth 1.0 with depth bias off; restart the // walk baseline to match. - VK_Interactions_DrawLights( viewDef ); + // Seal all Vulkan resources now, after the established depth prerequisite + // has consumed its transient allocations but before the first interaction + // framebuffer write. A rejection leaves the complete classic light/shadow + // walker untouched. + const bool sharedWorldInteractionOwned = + !sceneScaleState.active && r_rendererSharedWorldInteraction.GetBool() + && VK_ClassicInteraction_Preflight( viewDef ); + if ( sharedWorldInteractionOwned ) { + VK_ClassicInteraction_DrawOwnedView( viewDef ); + } else { + VK_Interactions_DrawLights( viewDef ); + } currentSpace = NULL; weaponDepthRange = false; viewport.maxDepth = 1.0f; vkCmdSetViewport( cmd, 0, 1, &viewport ); + // R_RenderGuiSurf output sorts before the regular world ambient ranges. + // Commit its sealed subset at that exact boundary; on rejection the normal + // walker below retains every source drawSurf unchanged. + if ( !sceneScaleState.active && r_rendererSharedInWorldGui.GetBool() ) { + (void)VK_ClassicGui_DrawOwnedInWorldView( viewDef ); + } // ---- passes 2-4: ambient walks split at fog, then post-process ---- int processed = numDrawSurfs; @@ -6699,6 +11235,31 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { break; } } + const classicCinematicPostDomainView_t *sharedAuthoredPostView = + !sceneScaleState.active && r_rendererSharedCinematicPost.GetBool() + ? R_ClassicCinematicPostDomain_FindAuthoredPostView( viewDef ) + : NULL; + // A prior member may already have rolled back this complete special-view + // transaction. Preserve the classic walk for later members without + // recording the same propagated fallback again. + const bool sharedAuthoredPostCandidate = sharedAuthoredPostView != NULL + && sharedAuthoredPostView->backendOutcome[ + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN] + == CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED + && !sharedAuthoredPostView->backendCompleted[ + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN]; + const bool sharedAuthoredPostReady = sharedAuthoredPostCandidate + && R_ClassicCinematicPostDomain_ReadyForBackend( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN ); + bool sharedAuthoredPostWalkExecuted = false; + if ( sharedAuthoredPostCandidate && !sharedAuthoredPostReady ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_NOT_READY ); + } for ( int pass = 0; pass < 3; pass++ ) { // ---- fog and blend lights between the two walks (Phase G2) ---- @@ -6708,7 +11269,14 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { // space/depth-range state and exits at maxDepth 1.0 with depth // bias off; restart the walk baseline like the interaction pass. if ( pass == 1 ) { - VK_Fog_DrawAllLights( viewDef ); + const bool sharedFogBlendOwned = + !sceneScaleState.active && r_rendererSharedWorldFogBlend.GetBool() + && VK_ClassicFogBlend_Preflight( viewDef ); + if ( sharedFogBlendOwned ) { + VK_ClassicFogBlend_DrawOwnedView( viewDef ); + } else { + VK_Fog_DrawAllLights( viewDef ); + } // Fog changes the framebuffer after any earlier lazy capture. backEnd.currentRenderCopied = false; backEnd.currentDepthCopied = false; @@ -6718,8 +11286,22 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { vkCmdSetViewport( cmd, 0, 1, &viewport ); } if ( !r_skipAmbient.GetBool() ) { + if ( sharedWorldAmbientOwned && pass < 2 ) { + VK_ClassicWorldAmbient_DrawPhase( + pass == 0 + ? CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG + : CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ); + continue; + } if ( pass == 2 ) { if ( processed >= numDrawSurfs || r_skipPostProcess.GetBool() ) { + if ( sharedAuthoredPostCandidate ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_SKIP_OR_EMPTY ); + } break; } bool feedbackReady = true; @@ -6741,6 +11323,13 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { backEnd.currentRenderCopied = true; } if ( !feedbackReady ) { + if ( sharedAuthoredPostCandidate ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_FEEDBACK ); + } static bool warnedPostCapture = false; if ( !warnedPostCapture ) { warnedPostCapture = true; @@ -6763,6 +11352,10 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { const int surfEnd = pass == 2 ? numDrawSurfs : processed; for ( int surfNum = surfBegin; surfNum < surfEnd; surfNum++ ) { const drawSurf_t *drawSurf = drawSurfs[ surfNum ]; + if ( R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_VULKAN, drawSurf ) ) { + continue; + } const idMaterial *shader = drawSurf->material; if ( shader == NULL || !shader->HasAmbient() || shader->IsPortalSky() ) { continue; @@ -6841,8 +11434,30 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); } } + if ( pass == 2 ) { + sharedAuthoredPostWalkExecuted = true; + } } } + if ( sharedAuthoredPostCandidate && sharedAuthoredPostReady + && sharedAuthoredPostWalkExecuted + && !R_ClassicCinematicPostDomain_RecordOwned( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_VULKAN, + numDrawSurfs - processed ) ) { + common->Warning( "Vulkan: shared authored post coverage rejected after committed range" ); + } + VK_RecordCinematicPostFallbackIfPending( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_CINEMATIC_POST_REJECT_NO_POST_WALK ); + + if ( sharedRenderDemoReady + && !R_ClassicSpecialFrameDomain_RecordOwned( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_VULKAN, numDrawSurfs ) ) { + common->Warning( "Vulkan: shared render-demo coverage rejected after committed view" ); + } // RC_DRAW_SPECIAL_EFFECTS immediately precedes this view in the command // stream. Consume its Raven controller state only after the complete scene @@ -6866,6 +11481,7 @@ void VK_GuiExecutor_Draw3DView( const viewDef_t *viewDef ) { common->Printf( "Vulkan: first world view drew %d surfaces (rings: %d KB verts, %d KB indexes)\n", numDrawSurfs, vkExec.vertexRings[ slot ].cursor / 1024, vkExec.indexRings[ slot ].cursor / 1024 ); } + VK_TemporalPresentation_EndView( viewDef, sceneScaleState, true ); } /* @@ -6876,11 +11492,61 @@ Clear-only frames (no 2D view was drawn) still need a presentable image. ==================== */ bool VK_GuiExecutor_EnsureFrameOpen( void ) { - return VK_GuiExecutor_BeginFrame(); + if ( !VK_GuiExecutor_BeginFrame() ) { + return false; + } + return VK_TemporalPresentation_CompositePendingScene(); } bool VK_GuiExecutor_FrameIsOpen( void ) { return vkExec.frameOpen; } +/* +==================== +Backend-neutral GPU skinning seam + +Vulkan cannot record compute while dynamic models are being built on the +front end. Returning false from PrepareAmbientCache intentionally requests +the established CPU cache as the fail-closed fallback; Draw3D can later +replace its ring upload with compute output while a command buffer is active. +==================== +*/ +void R_BackendGpuSkinning_Init( const renderBackendCaps_t &caps ) { + (void)caps; + // Compute and storage buffers are Vulkan core capabilities. Exact device + // limits and shader/pipeline creation are validated by the lazy executor. + vkGpuSkinningBackendRequested = true; +} + +void R_BackendGpuSkinning_Shutdown( void ) { + vkGpuSkinningBackendRequested = false; +} + +bool R_BackendGpuSkinning_PrepareAmbientCache( srfTriangles_t *tri, + bool needsLighting ) { + (void)tri; + (void)needsLighting; + if ( vkExec.initialized + && ( !vkGpuSkinningBackendRequested || !vkExec.gpuSkinningAvailable ) ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_BACKEND_UNAVAILABLE ); + } + return false; +} + +void R_BackendGpuSkinning_PrintGfxInfo( void ) { + const char *status = vkExec.gpuSkinningAvailable + ? "available" + : ( vkExec.initialized ? "unavailable" : "pending executor initialization" ); + common->Printf( + "GPU skinning compute (Vulkan): %s, dispatches=%llu vertices=%llu " + "fallbacks(validation=%llu ring=%llu memo=%llu)\n", + status, + static_cast( vkExec.gpuSkinningDispatches ), + static_cast( vkExec.gpuSkinningVertices ), + static_cast( vkExec.gpuSkinningValidationFallbacks ), + static_cast( vkExec.gpuSkinningRingFallbacks ), + static_cast( vkExec.gpuSkinningMemoFallbacks ) ); +} + #endif /* OPENQ4_RENDERER_VK_MODULE */ diff --git a/src/renderer/Vulkan/vk_Image.cpp b/src/renderer/Vulkan/vk_Image.cpp index 6b002678..d14e9b57 100644 --- a/src/renderer/Vulkan/vk_Image.cpp +++ b/src/renderer/Vulkan/vk_Image.cpp @@ -581,6 +581,12 @@ bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, || depthFormat == VK_FORMAT_UNDEFINED ) { return false; } + const idImageOpts &imageOpts = image->GetOpts(); + const bool isCube = imageOpts.textureType == TT_CUBIC; + if ( imageOpts.textureType != TT_2D && !isCube ) { + return false; + } + const uint32_t numLayers = isCube ? 6u : 1u; if ( image->GetUploadWidth() != width || image->GetUploadHeight() != height ) { image->Resize( width, height ); @@ -608,6 +614,7 @@ bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, && entry->view != VK_NULL_HANDLE && entry->samples == VK_SAMPLE_COUNT_1_BIT && entry->aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT + && entry->isCube == isCube && entry->numLayers == (int)numLayers && ( entry->usage & requiredUsage ) == requiredUsage ) { return true; } @@ -616,12 +623,13 @@ bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, memset( &ici, 0, sizeof( ici ) ); ici.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; ici.imageType = VK_IMAGE_TYPE_2D; + ici.flags = isCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0; ici.format = depthFormat; ici.extent.width = (uint32_t)width; ici.extent.height = (uint32_t)height; ici.extent.depth = 1; ici.mipLevels = 1; - ici.arrayLayers = 1; + ici.arrayLayers = numLayers; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = requiredUsage; @@ -644,12 +652,12 @@ bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, memset( &ivci, 0, sizeof( ivci ) ); ivci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; ivci.image = newImage; - ivci.viewType = VK_IMAGE_VIEW_TYPE_2D; + ivci.viewType = isCube ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_2D; ivci.format = depthFormat; ivci.components = VK_SWIZZLE_IDENTITY; ivci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; ivci.subresourceRange.levelCount = 1; - ivci.subresourceRange.layerCount = 1; + ivci.subresourceRange.layerCount = numLayers; VkImageView newView = VK_NULL_HANDLE; if ( vkCreateImageView( vkCtx.device, &ivci, NULL, &newView ) != VK_SUCCESS ) { @@ -675,8 +683,8 @@ bool VK_Image_MakeDepthCopyTarget( idImage *image, int width, int height, entry->width = width; entry->height = height; entry->numMips = 1; - entry->numLayers = 1; - entry->isCube = false; + entry->numLayers = (int)numLayers; + entry->isCube = isCube; entry->everUploaded = false; entry->generation = vkImageGenerationCounter++; return true; @@ -777,7 +785,7 @@ void idImage::SubImageUpload( int mipLevel, int x, int y, int z, int width, int dataBytes = (size_t)rowTexels * height * info.bytesPerBlock; } - // transient staging buffer, freed via the deferred queue + // transient staging buffer; on success the upload batch takes ownership VkBufferCreateInfo bci; memset( &bci, 0, sizeof( bci ) ); bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; @@ -848,12 +856,15 @@ void idImage::SubImageUpload( int mipLevel, int x, int y, int z, int width, int ctx.bufferRowLengthTexels = rowLengthTexels; ctx.oldLayout = entry->layout; - if ( VK_Device_ImmediateSubmit( VK_Image_RecordUpload, &ctx ) ) { + if ( VK_Device_BatchedUpload( VK_Image_RecordUpload, &ctx, staging, stagingAlloc, + (VkDeviceSize)dataBytes ) ) { entry->everUploaded = true; entry->layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } else { + // nothing recorded: release the staging buffer through the normal + // deferred path, matching the old no-device fallback + VK_Device_DeferDestroy( VK_NULL_HANDLE, VK_NULL_HANDLE, staging, stagingAlloc ); } - - VK_Device_DeferDestroy( VK_NULL_HANDLE, VK_NULL_HANDLE, staging, stagingAlloc ); } /* diff --git a/src/renderer/Vulkan/vk_Interactions.cpp b/src/renderer/Vulkan/vk_Interactions.cpp index 980f227e..b582e91d 100644 --- a/src/renderer/Vulkan/vk_Interactions.cpp +++ b/src/renderer/Vulkan/vk_Interactions.cpp @@ -60,6 +60,11 @@ #include "../tr_local.h" #include "../Model_local.h" +#include "../ClassicInteractionDomain.h" +#include "../ClassicFogBlendDomain.h" +#include "../MaterialResourceTable.h" + +extern idCVar r_vkShadowFallbackTest; #undef snprintf #undef vsnprintf @@ -73,6 +78,7 @@ #define UINT_MAX 0xffffffffu #endif #include +#include #include "volk.h" #include "VulkanDevice.h" @@ -82,6 +88,15 @@ VkCommandBuffer VK_Exec_ActiveCmd( void ); int VK_Exec_ActiveFrameSlot( void ); bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_t *tri ); +bool VK_Exec_SharedInteractionGeometryCheckpoint( void ); +void VK_Exec_SharedInteractionGeometryRestore( void ); +void VK_Exec_SharedInteractionGeometryCommit( void ); +bool VK_Exec_PrepareTriGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ); +bool VK_Exec_PrepareShadowGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ); +void VK_Exec_BindPreparedTriGeometry( VkCommandBuffer cmd, int slot, + int vertexOffset, int indexOffset ); bool VK_Exec_BindShadowGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_t *tri ); bool VK_Exec_BindRawShadowGeometry( VkCommandBuffer cmd, int slot, const shadowCache_t *verts, int numVerts, @@ -102,10 +117,14 @@ VkDescriptorSet VK_Exec_ShadowDescriptorSet( void ); VkDescriptorSet VK_Exec_ImageDescriptor( unsigned int texnum, bool require2D ); VkDescriptorSet VK_Exec_InteractionUniformSet( void ); int VK_Exec_InteractionUniformAlloc( const void *data, int bytes ); +int VK_Exec_InteractionUniformCheckpoint( void ); +void VK_Exec_InteractionUniformRestore( int checkpoint ); int VK_Exec_ShadowUniformAlloc( const void *data, int bytes ); int VK_Exec_ActiveFramebufferWidth( void ); int VK_Exec_ActiveFramebufferHeight( void ); bool VK_Exec_ActiveTargetHasStencil( void ); +bool VK_Exec_SharedInteractionTargetReady( void ); +void VK_FixupClipSpaceZ( float dst[ 16 ], const float src[ 16 ] ); /* ==================== @@ -115,7 +134,8 @@ Per-draw GPU blocks // mirror of the shared 128B push block ({mat4; vec4 a,b,c,d}): // a = (vertexColorModulate, vertexColorAdd, ambientLight, unused), -// b = tangent-space ambient light direction (cube-quantized), c/d unused +// b = tangent-space ambient light direction (cube-quantized), c = parallax, +// d = native packed-PBR mode, metallic scalar, roughness scalar, normal scale typedef struct vkInteractionPush_s { float mvp[ 16 ]; float a[ 4 ]; @@ -215,6 +235,9 @@ typedef struct vkInterPass_s { float ambientDir[ 3 ]; int lightCount; int drawCount; + int nativePBRDrawCount; + VkDescriptorSet lastImageSets[ 6 ]; // sets 0-5 as last bound by VK_DrawSingleInteractionMode + bool imageSetsValid; // lastImageSets mirror live bindings on cmd // Phase F2a/F2b shadow-map receivers bool shadowPassPrepared; // shadow maps rendered for this view @@ -258,6 +281,1575 @@ typedef struct vkInterPass_s { static vkInterPass_t interPass; +/* +=============================================================================== + + Shared fixed-classic interaction consumer + + The backend-neutral domain has already evaluated light and material stages. + Vulkan only validates device resources, retains geometry/ring offsets, and + submits those sealed values. The established VK_Interactions_DrawLights + walker below remains the complete shadow/custom/failure rollback. + +=============================================================================== +*/ + +enum vkClassicInteractionRejectDetail_t { + VK_CLASSIC_INTERACTION_REJECT_VIEW = 1, + VK_CLASSIC_INTERACTION_REJECT_COUNTS, + VK_CLASSIC_INTERACTION_REJECT_OFFSCREEN_TARGET, + VK_CLASSIC_INTERACTION_REJECT_RENDER_SCOPE, + VK_CLASSIC_INTERACTION_REJECT_PIPELINE, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_MODE, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_GEOMETRY, + VK_CLASSIC_INTERACTION_REJECT_STATE, + VK_CLASSIC_INTERACTION_REJECT_SCISSOR, + VK_CLASSIC_INTERACTION_REJECT_GEOMETRY, + VK_CLASSIC_INTERACTION_REJECT_TEXTURE, + VK_CLASSIC_INTERACTION_REJECT_UNIFORM +}; + +typedef struct vkClassicInteractionDrawPlan_s { + const classicInteractionDomainPrimitive_t *primitive; + int vertexOffset; + int indexOffset; + VkRect2D scissor; + VkCullModeFlags cullMode; + VkCompareOp depthCompare; + VkDescriptorSet sets[ 8 ]; + vkInteractionBlock_t block; + vkShadowBlock_t projectedShadowBlock; + vkPointShadowBlock_t pointShadowBlock; + vkInteractionPush_t push; + int uniformOffset; + int shadowUniformOffset; + int mappedShadowMode; // 0 none, 1 projected, 2 point +} vkClassicInteractionDrawPlan_t; + +typedef struct vkClassicInteractionShadowPlan_s { + const classicInteractionDomainShadowCaster_t *caster; + int vertexOffset; + int indexOffset; + VkRect2D scissor; + vkInteractionPush_t push; +} vkClassicInteractionShadowPlan_t; + +typedef struct vkClassicInteractionLightPlan_s { + const classicInteractionDomainLight_t *light; + int firstDraw[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int drawCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int firstShadow[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int shadowCount[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + VkRect2D clearRect; +} vkClassicInteractionLightPlan_t; + +typedef struct vkClassicInteractionPreparedView_s { + const classicInteractionDomainView_t *view; + const viewDef_t *viewDef; + VkCommandBuffer cmd; + VkPipeline pipeline; + VkPipelineLayout layout; + VkPipeline shadowPipeline; + VkPipelineLayout shadowLayout; + VkPipeline projectedInteractionPipeline; + VkPipeline pointInteractionPipeline; + VkPipelineLayout mappedInteractionLayout; + VkDescriptorSet atlasSet; + VkViewport viewport; + int frameSlot; + int framebufferWidth; + int framebufferHeight; + int drawPlanCount; + int noopPrimitiveCount; + int lightPlanCount; + int shadowPlanCount; + int noopShadowCasterCount; + int logicalVolumeDrawCount; + int preloadVolumeDrawCount; + int submittedDraws; + int submittedShadowCasters; + int submittedLogicalVolumeDraws; + int submittedPreloadVolumeDraws; + int uniformCheckpoint; + VkPipeline boundPipeline; // last pipeline bound in the owned-view walk; skips redundant same-pipeline binds + bool ready; + bool committed; + vkClassicInteractionDrawPlan_t draws[ CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ]; + vkClassicInteractionShadowPlan_t shadows[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + vkClassicInteractionLightPlan_t lights[ + CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ]; +} vkClassicInteractionPreparedView_t; + +static vkClassicInteractionPreparedView_t vkClassicInteractionPrepared; + +static bool VK_ClassicInteraction_Fail( const viewDef_t *viewDef, + classicInteractionDomainFailure_t failure, int detail ) { + VK_ShadowMap_AbortClassicInteractionView( + vkClassicInteractionPrepared.view ); + if ( vkClassicInteractionPrepared.uniformCheckpoint >= 0 ) { + VK_Exec_InteractionUniformRestore( + vkClassicInteractionPrepared.uniformCheckpoint ); + } + // Safe before a checkpoint exists and mandatory after any speculative + // geometry upload: classic fallback must see its original ring and memos. + VK_Exec_SharedInteractionGeometryRestore(); + R_ClassicInteractionDomain_RecordBackendFallback( viewDef, + CLASSIC_INTERACTION_BACKEND_VULKAN, + failure == CLASSIC_INTERACTION_FAILURE_NONE + ? CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED : failure, + detail ); + memset( &vkClassicInteractionPrepared, 0, + sizeof( vkClassicInteractionPrepared ) ); + vkClassicInteractionPrepared.uniformCheckpoint = -1; + return false; +} + +static bool VK_ClassicInteraction_FloatsFinite( const float *values, int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !std::isfinite( values[i] ) ) { + return false; + } + } + return true; +} + +static bool VK_ClassicInteraction_VolumeMode( + classicInteractionDomainShadowMode_t mode ) { + return mode == CLASSIC_INTERACTION_SHADOW_STENCIL + || mode == CLASSIC_INTERACTION_SHADOW_HYBRID; +} + +static bool VK_ClassicInteraction_MapCull( rendererCullMode_t cull, + VkCullModeFlags &mode ) { + switch ( cull ) { + case RENDERER_CULL_NONE: + mode = VK_CULL_MODE_NONE; + return true; + case RENDERER_CULL_FRONT: + mode = VK_CULL_MODE_FRONT_BIT; + return true; + case RENDERER_CULL_BACK: + mode = VK_CULL_MODE_BACK_BIT; + return true; + default: + return false; + } +} + +static bool VK_ClassicInteraction_BuildScissorBounds( + const classicInteractionDomainView_t &view, + int requestedX1, int requestedY1, int requestedX2, int requestedY2, + int framebufferWidth, int framebufferHeight, VkRect2D &scissor ) { + const int viewportWidth = view.viewportX2 - view.viewportX1 + 1; + const int viewportHeight = view.viewportY2 - view.viewportY1 + 1; + if ( viewportWidth <= 0 || viewportHeight <= 0 + || framebufferWidth <= 0 || framebufferHeight <= 0 ) { + return false; + } + + if ( requestedX2 < requestedX1 || requestedY2 < requestedY1 ) { + return false; + } + + int x0 = Max( view.viewportX1, view.viewportX1 + requestedX1 ); + int x1 = Min( view.viewportX1 + viewportWidth, + view.viewportX1 + requestedX2 + 1 ); + int y0GL = Max( view.viewportY1, view.viewportY1 + requestedY1 ); + int y1GL = Min( view.viewportY1 + viewportHeight, + view.viewportY1 + requestedY2 + 1 ); + x0 = Max( 0, x0 ); + x1 = Min( framebufferWidth, x1 ); + y0GL = Max( 0, y0GL ); + y1GL = Min( framebufferHeight, y1GL ); + if ( x1 <= x0 || y1GL <= y0GL ) { + return false; + } + + scissor.offset.x = x0; + scissor.offset.y = framebufferHeight - y1GL; + scissor.extent.width = static_cast( x1 - x0 ); + scissor.extent.height = static_cast( y1GL - y0GL ); + return true; +} + +static bool VK_ClassicInteraction_BuildScissor( + const classicInteractionDomainView_t &view, + const classicInteractionDomainPrimitive_t &primitive, + int framebufferWidth, int framebufferHeight, VkRect2D &scissor ) { + return VK_ClassicInteraction_BuildScissorBounds( view, + view.useScissor ? primitive.scissorX1 : view.scissorX1, + view.useScissor ? primitive.scissorY1 : view.scissorY1, + view.useScissor ? primitive.scissorX2 : view.scissorX2, + view.useScissor ? primitive.scissorY2 : view.scissorY2, + framebufferWidth, framebufferHeight, scissor ); +} + +static bool VK_ClassicInteraction_BuildShadowScissor( + const classicInteractionDomainView_t &view, + const classicInteractionDomainShadowCaster_t &caster, + int framebufferWidth, int framebufferHeight, VkRect2D &scissor ) { + return VK_ClassicInteraction_BuildScissorBounds( view, + view.useScissor ? caster.scissorX1 : view.scissorX1, + view.useScissor ? caster.scissorY1 : view.scissorY1, + view.useScissor ? caster.scissorX2 : view.scissorX2, + view.useScissor ? caster.scissorY2 : view.scissorY2, + framebufferWidth, framebufferHeight, scissor ); +} + +static bool VK_ClassicInteraction_BuildLightClearRect( + const classicInteractionDomainView_t &view, + const classicInteractionDomainLight_t &light, + int framebufferWidth, int framebufferHeight, VkRect2D &scissor ) { + return VK_ClassicInteraction_BuildScissorBounds( view, + view.useScissor ? light.scissorX1 : view.scissorX1, + view.useScissor ? light.scissorY1 : view.scissorY1, + view.useScissor ? light.scissorX2 : view.scissorX2, + view.useScissor ? light.scissorY2 : view.scissorY2, + framebufferWidth, framebufferHeight, scissor ); +} + +static bool VK_ClassicInteraction_ResolveDescriptor( std::uint64_t resourceId, + VkDescriptorSet &descriptor ) { + descriptor = VK_NULL_HANDLE; + const classicInteractionDomainTexture_t *texture = + R_ClassicInteractionDomain_ResolveTexture( resourceId ); + if ( texture == NULL || texture->textureResourceId != resourceId + || texture->image == NULL || !texture->loaded + || texture->defaulted || texture->mutableImage + || texture->textureHandle == 0 + || !texture->image->IsLoaded() + || texture->image->IsDefaulted() + || texture->textureHandle + != const_cast( texture->image )->GetDeviceHandle() + || texture->filter != texture->image->GetFilter() + || texture->repeat != texture->image->GetRepeat() + || texture->storageGeneration + != texture->image->GetStorageGeneration() ) { + return false; + } + descriptor = VK_Exec_ImageDescriptor( texture->textureHandle, true ); + return descriptor != VK_NULL_HANDLE; +} + +static bool VK_ClassicInteraction_ValidatePrimitive( + const classicInteractionDomainPrimitive_t &primitive, + VkCullModeFlags &cullMode, VkCompareOp &depthCompare ) { + if ( primitive.disposition != CLASSIC_INTERACTION_PRIMITIVE_DRAW + || primitive.legacyDrawSurf == NULL + || primitive.legacyDrawSurf->geo == NULL + || primitive.legacyDrawSurf->geo->ambientCache == NULL + || primitive.vertexCount <= 0 || primitive.indexCount <= 0 + || primitive.firstIndex != 0 || primitive.vertexOffset != 0 + || primitive.legacyDrawSurf->geo->numVerts != primitive.vertexCount + || primitive.legacyDrawSurf->geo->numIndexes != primitive.indexCount + || ( primitive.legacyDrawSurf->geo->indexes == NULL + && primitive.legacyDrawSurf->geo->indexCache == NULL ) + || primitive.lightImageResourceId == 0 + || primitive.lightFalloffImageResourceId == 0 + || primitive.bumpImageResourceId == 0 + || primitive.diffuseImageResourceId == 0 + || primitive.specularImageResourceId == 0 + || primitive.vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || primitive.vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || !primitive.blend.enabled + || primitive.blend.sourceColor != RENDERER_BLEND_ONE + || primitive.blend.destinationColor != RENDERER_BLEND_ONE + || primitive.blend.colorOperation != RENDERER_BLEND_OP_ADD + || primitive.blend.sourceAlpha != RENDERER_BLEND_ONE + || primitive.blend.destinationAlpha != RENDERER_BLEND_ONE + || primitive.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || !std::isfinite( primitive.polygonOffsetFactor ) + || !std::isfinite( primitive.polygonOffsetUnits ) + || !VK_ClassicInteraction_FloatsFinite( primitive.diffuseColor, 4 ) + || !VK_ClassicInteraction_FloatsFinite( primitive.specularColor, 4 ) + || !VK_ClassicInteraction_FloatsFinite( primitive.flatDiffuseParams, 4 ) + || !VK_ClassicInteraction_FloatsFinite( primitive.localLightOrigin, 4 ) + || !VK_ClassicInteraction_FloatsFinite( primitive.localViewOrigin, 4 ) + || !VK_ClassicInteraction_FloatsFinite( &primitive.lightProjection[0][0], 16 ) + || !VK_ClassicInteraction_FloatsFinite( &primitive.bumpMatrix[0][0], 8 ) + || !VK_ClassicInteraction_FloatsFinite( &primitive.diffuseMatrix[0][0], 8 ) + || !VK_ClassicInteraction_FloatsFinite( &primitive.specularMatrix[0][0], 8 ) + || !VK_ClassicInteraction_FloatsFinite( primitive.modelViewMatrix, 16 ) + || !VK_ClassicInteraction_MapCull( primitive.cull, cullMode ) ) { + return false; + } + + switch ( primitive.depth ) { + case CLASSIC_INTERACTION_DEPTH_EQUAL: + depthCompare = VK_COMPARE_OP_EQUAL; + return true; + case CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL: + depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + return true; + default: + return false; + } +} + +static void VK_ClassicInteraction_BuildBlocks( + const classicInteractionDomainView_t &view, + const classicInteractionDomainPrimitive_t &primitive, + vkClassicInteractionDrawPlan_t &plan ) { + memset( &plan.block, 0, sizeof( plan.block ) ); + memcpy( plan.block.localLightOrigin, primitive.localLightOrigin, + sizeof( plan.block.localLightOrigin ) ); + memcpy( plan.block.localViewOrigin, primitive.localViewOrigin, + sizeof( plan.block.localViewOrigin ) ); + memcpy( plan.block.lightProjectionS, primitive.lightProjection[0], + sizeof( plan.block.lightProjectionS ) ); + memcpy( plan.block.lightProjectionT, primitive.lightProjection[1], + sizeof( plan.block.lightProjectionT ) ); + memcpy( plan.block.lightProjectionQ, primitive.lightProjection[2], + sizeof( plan.block.lightProjectionQ ) ); + memcpy( plan.block.lightFalloffS, primitive.lightProjection[3], + sizeof( plan.block.lightFalloffS ) ); + memcpy( plan.block.bumpMatrixS, primitive.bumpMatrix[0], + sizeof( plan.block.bumpMatrixS ) ); + memcpy( plan.block.bumpMatrixT, primitive.bumpMatrix[1], + sizeof( plan.block.bumpMatrixT ) ); + memcpy( plan.block.diffuseMatrixS, primitive.diffuseMatrix[0], + sizeof( plan.block.diffuseMatrixS ) ); + memcpy( plan.block.diffuseMatrixT, primitive.diffuseMatrix[1], + sizeof( plan.block.diffuseMatrixT ) ); + memcpy( plan.block.specularMatrixS, primitive.specularMatrix[0], + sizeof( plan.block.specularMatrixS ) ); + memcpy( plan.block.specularMatrixT, primitive.specularMatrix[1], + sizeof( plan.block.specularMatrixT ) ); + memcpy( plan.block.diffuseColor, primitive.diffuseColor, + sizeof( plan.block.diffuseColor ) ); + memcpy( plan.block.specularColor, primitive.specularColor, + sizeof( plan.block.specularColor ) ); + memcpy( plan.block.flatDiffuseParams, primitive.flatDiffuseParams, + sizeof( plan.block.flatDiffuseParams ) ); + + memset( &plan.push, 0, sizeof( plan.push ) ); + float mvpGL[16]; + myGlMultMatrix( primitive.modelViewMatrix, view.projectionMatrix, mvpGL ); + VK_FixupClipSpaceZ( plan.push.mvp, mvpGL ); + switch ( primitive.vertexColor ) { + case RENDERER_VERTEX_COLOR_MODULATE: + plan.push.a[0] = 1.0f; + plan.push.a[1] = 0.0f; + break; + case RENDERER_VERTEX_COLOR_INVERSE_MODULATE: + plan.push.a[0] = -1.0f; + plan.push.a[1] = 1.0f; + break; + default: + plan.push.a[0] = 0.0f; + plan.push.a[1] = 1.0f; + break; + } + plan.push.a[2] = primitive.ambientLight ? 1.0f : 0.0f; + plan.push.b[0] = 1.0f; + plan.push.b[1] = + ( (float)(byte)( 255 * tr.ambientLightVector[1] ) / 255.0f ) + * 2.0f - 1.0f; + plan.push.b[2] = + ( (float)(byte)( 255 * tr.ambientLightVector[2] ) / 255.0f ) + * 2.0f - 1.0f; +} + +static bool VK_ClassicInteraction_BuildMappedShadowBlock( + const classicInteractionDomainPrimitive_t &primitive, + const classicInteractionDomainShadowMapPass_t &mapPass, + const vkShadowLightState_t &physicalLight, + const vkShadowPassState_t &physicalPass, + vkClassicInteractionDrawPlan_t &plan, + VkDescriptorSet atlasSet ) { + if ( !VK_ClassicInteraction_FloatsFinite( primitive.modelMatrix, 16 ) + || !VK_ClassicInteraction_FloatsFinite( + primitive.modelViewMatrix, 16 ) ) { + return false; + } + + if ( mapPass.lightClass == SHADOWMAP_LIGHT_POINT ) { + if ( !physicalLight.pointLight || !mapPass.point.valid + || physicalPass.pointSet == VK_NULL_HANDLE ) { + return false; + } + plan.mappedShadowMode = 2; + plan.sets[ 7 ] = physicalPass.pointSet; + vkPointShadowBlock_t &block = plan.pointShadowBlock; + memset( &block, 0, sizeof( block ) ); + for ( int i = 0; i < 4; ++i ) { + block.modelRow0[ i ] = primitive.modelMatrix[ i * 4 + 0 ]; + block.modelRow1[ i ] = primitive.modelMatrix[ i * 4 + 1 ]; + block.modelRow2[ i ] = primitive.modelMatrix[ i * 4 + 2 ]; + } + memcpy( block.lightOriginFar, mapPass.point.lightOrigin, + 3 * sizeof( float ) ); + block.lightOriginFar[ 3 ] = mapPass.point.farDistance; + block.biasParams[ 0 ] = mapPass.point.constantBias; + block.biasParams[ 1 ] = mapPass.point.normalBias; + block.biasParams[ 2 ] = mapPass.point.texelBiasScale + / static_cast( Max( 1, mapPass.point.faceSize ) ); + block.biasParams[ 3 ] = 2.0f * mapPass.point.normalOffsetScale + / static_cast( Max( 1, mapPass.point.faceSize ) ); + block.filterParams[ 0 ] = mapPass.point.filterRadius; + block.filterParams[ 1 ] = static_cast( + mapPass.point.filterTaps ); + block.filterParams[ 2 ] = static_cast( + mapPass.point.filterMode ); + block.filterParams[ 3 ] = 2.0f + / static_cast( Max( 1, mapPass.point.faceSize ) ); + block.samplingParams[ 0 ] = mapPass.point.depthCompare + ? 1.0f : 0.0f; + return true; + } + + if ( physicalLight.pointLight || atlasSet == VK_NULL_HANDLE + || !mapPass.projected.state.valid ) { + return false; + } + plan.mappedShadowMode = 1; + plan.sets[ 7 ] = atlasSet; + vkShadowBlock_t &block = plan.projectedShadowBlock; + memset( &block, 0, sizeof( block ) ); + const shadowMapProjectedLightState_t &projected = + mapPass.projected.state; + const int cascadeCount = idMath::ClampInt( 1, + SHADOWMAP_PROJECTED_MAX_CASCADES, projected.cascadeCount ); + for ( int cascadeIndex = 0; cascadeIndex < cascadeCount; + ++cascadeIndex ) { + idPlane localPlane; + R_GlobalPlaneToLocal( primitive.modelMatrix, + projected.clipPlanes[ cascadeIndex ][ 0 ], localPlane ); + memcpy( block.shadowRow0[ cascadeIndex ], localPlane.ToFloatPtr(), + sizeof( block.shadowRow0[ 0 ] ) ); + R_GlobalPlaneToLocal( primitive.modelMatrix, + projected.clipPlanes[ cascadeIndex ][ 1 ], localPlane ); + memcpy( block.shadowRow1[ cascadeIndex ], localPlane.ToFloatPtr(), + sizeof( block.shadowRow1[ 0 ] ) ); + R_GlobalPlaneToLocal( primitive.modelMatrix, + projected.clipPlanes[ cascadeIndex ][ 2 ], localPlane ); + memcpy( block.shadowRow2[ cascadeIndex ], localPlane.ToFloatPtr(), + sizeof( block.shadowRow2[ 0 ] ) ); + R_GlobalPlaneToLocal( primitive.modelMatrix, + projected.clipPlanes[ cascadeIndex ][ 3 ], localPlane ); + memcpy( block.shadowRow3[ cascadeIndex ], localPlane.ToFloatPtr(), + sizeof( block.shadowRow3[ 0 ] ) ); + } + memcpy( block.atlasRects, physicalPass.atlasRects, + sizeof( block.atlasRects ) ); + memcpy( block.splitDepths, projected.splitDepths, + sizeof( block.splitDepths ) ); + memcpy( block.cascadeBiasScale, projected.biasScale, + sizeof( block.cascadeBiasScale ) ); + memcpy( block.texelDepthBias, projected.texelDepthBias, + sizeof( block.texelDepthBias ) ); + for ( int cascadeIndex = 0; + cascadeIndex < SHADOWMAP_PROJECTED_MAX_CASCADES; + ++cascadeIndex ) { + block.normalOffsetWorld[ cascadeIndex ] = + projected.worldTexelSize[ cascadeIndex ] + * mapPass.projected.normalOffsetScale; + } + block.viewDepthRow[ 0 ] = -primitive.modelViewMatrix[ 2 ]; + block.viewDepthRow[ 1 ] = -primitive.modelViewMatrix[ 6 ]; + block.viewDepthRow[ 2 ] = -primitive.modelViewMatrix[ 10 ]; + block.viewDepthRow[ 3 ] = -primitive.modelViewMatrix[ 14 ]; + block.biasParams[ 0 ] = mapPass.projected.constantBias; + block.biasParams[ 1 ] = mapPass.projected.normalBias; + block.biasParams[ 2 ] = mapPass.projected.cascadeBlend; + block.biasParams[ 3 ] = static_cast( cascadeCount ); + block.texelSize[ 0 ] = physicalLight.invAtlasSize[ 0 ]; + block.texelSize[ 1 ] = physicalLight.invAtlasSize[ 1 ]; + block.filterParams[ 0 ] = mapPass.projected.filter.filterRadius; + block.filterParams[ 1 ] = static_cast( + mapPass.projected.filter.filterTaps ); + block.filterParams[ 2 ] = static_cast( + mapPass.projected.filter.filterMode ); + block.filterParams[ 3 ] = mapPass.projected.depthCompare + && mapPass.projected.filter.filterMode != 2 ? 1.0f : 0.0f; + block.pcssParams[ 0 ] = mapPass.projected.filter.pcssLightRadius; + block.pcssParams[ 1 ] = mapPass.projected.filter.pcssMaxRadius; + block.pcssParams[ 2 ] = mapPass.projected.filter.effectiveFilterRadius; + block.pcssParams[ 3 ] = mapPass.projected.receiverPlaneBias + ? 1.0f : 0.0f; + return true; +} + +static bool VK_ClassicInteraction_ValidateShadowCaster( + const classicInteractionDomainView_t &view, + const classicInteractionDomainLight_t &light, + const classicInteractionDomainShadowCaster_t &caster ) { + const srfTriangles_t *tri = caster.legacyCasterGeometry; + if ( caster.disposition != CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + || ( caster.chain + != CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL + && caster.chain + != CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL + && caster.chain + != CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + && caster.chain + != CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ) + || caster.legacyDrawSurf == NULL + || caster.legacyViewLight != light.legacyViewLight + || caster.legacyDrawSurf->geo != tri || tri == NULL + || caster.legacyDrawSurf->space == NULL + || R_TriHasPrimBatchMesh( tri ) + || tri->shadowCache == NULL || tri->indexes == NULL + || caster.vertexCount <= 0 || caster.totalIndexCount <= 0 + || caster.selectedIndexCount <= 0 + || caster.selectedIndexCount > caster.totalIndexCount + || tri->numVerts != caster.vertexCount + || tri->numIndexes != caster.totalIndexCount + || caster.depthMin < 0.0f || caster.depthMin > 1.0f + || caster.depthMax < caster.depthMin || caster.depthMax > 1.0f + || caster.preload == caster.external + || !VK_ClassicInteraction_FloatsFinite( + caster.localLightOrigin, 4 ) + || !VK_ClassicInteraction_FloatsFinite( + caster.modelViewMatrix, 16 ) ) { + return false; + } + + switch ( caster.indexSelection ) { + case CLASSIC_INTERACTION_SHADOW_INDEX_FULL: + return caster.selectedIndexCount == tri->numIndexes; + case CLASSIC_INTERACTION_SHADOW_INDEX_NO_FRONT_CAPS: + return caster.selectedIndexCount == tri->numShadowIndexesNoFrontCaps; + case CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS: + return caster.selectedIndexCount == tri->numShadowIndexesNoCaps; + default: + return false; + } +} + +static bool VK_ClassicInteraction_ValidateNoopShadowCaster( + const classicInteractionDomainLight_t &light, + const classicInteractionDomainShadowCaster_t &caster ) { + const srfTriangles_t *tri = caster.legacyCasterGeometry; + if ( caster.disposition != CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY + || caster.legacyDrawSurf == NULL + || caster.legacyViewLight != light.legacyViewLight + || caster.legacyDrawSurf->geo != tri || tri == NULL + || caster.selectedIndexCount != 0 || caster.preload + || caster.totalIndexCount != tri->numIndexes ) { + return false; + } + switch ( caster.indexSelection ) { + case CLASSIC_INTERACTION_SHADOW_INDEX_FULL: + return tri->numIndexes == 0; + case CLASSIC_INTERACTION_SHADOW_INDEX_NO_FRONT_CAPS: + return tri->numShadowIndexesNoFrontCaps == 0; + case CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS: + return tri->numShadowIndexesNoCaps == 0; + default: + return false; + } +} + +static void VK_ClassicInteraction_BuildShadowPush( + const classicInteractionDomainView_t &view, + const classicInteractionDomainShadowCaster_t &caster, + vkClassicInteractionShadowPlan_t &plan ) { + memset( &plan.push, 0, sizeof( plan.push ) ); + float mvpGL[ 16 ]; + myGlMultMatrix( caster.modelViewMatrix, view.projectionMatrix, mvpGL ); + VK_FixupClipSpaceZ( plan.push.mvp, mvpGL ); + memcpy( plan.push.a, caster.localLightOrigin, sizeof( plan.push.a ) ); +} + +static void VK_ClassicInteraction_CountShadowRange( + const vkClassicInteractionPreparedView_t &prepared, + const vkClassicInteractionLightPlan_t &lightPlan, + classicInteractionDomainShadowChain_t chain, + int &logicalDraws, int &preloadDraws ) { + const int first = lightPlan.firstShadow[ chain ]; + const int count = lightPlan.shadowCount[ chain ]; + for ( int i = 0; i < count; ++i ) { + const classicInteractionDomainShadowCaster_t *caster = + prepared.shadows[ first + i ].caster; + if ( caster == NULL ) { + continue; + } + logicalDraws++; + if ( caster->preload ) { + preloadDraws++; + } + } +} + +static void VK_ClassicInteraction_CountPlannedShadowWork( + const vkClassicInteractionPreparedView_t &prepared, + const vkClassicInteractionLightPlan_t &lightPlan, + int &logicalDraws, int &preloadDraws ) { + logicalDraws = 0; + preloadDraws = 0; + const classicInteractionDomainLight_t &light = *lightPlan.light; + classicInteractionDomainShadowMode_t preparedMode = + CLASSIC_INTERACTION_SHADOW_NONE; + bool preparedIncludesLocal = false; + + const classicInteractionDomainShadowMode_t localMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_LOCAL ]; + if ( VK_ClassicInteraction_VolumeMode( localMode ) ) { + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + localMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL, + logicalDraws, preloadDraws ); + preparedMode = localMode; + preparedIncludesLocal = false; + } + + const classicInteractionDomainShadowMode_t globalMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ]; + if ( VK_ClassicInteraction_VolumeMode( globalMode ) ) { + const classicInteractionDomainShadowChain_t globalChain = + globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + const classicInteractionDomainShadowChain_t localChain = + globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + if ( preparedMode != globalMode ) { + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + globalChain, logicalDraws, preloadDraws ); + } + if ( preparedMode != globalMode || !preparedIncludesLocal ) { + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + localChain, logicalDraws, preloadDraws ); + } + preparedMode = globalMode; + preparedIncludesLocal = true; + } + + const classicInteractionDomainShadowMode_t translucentMode = + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ]; + if ( VK_ClassicInteraction_VolumeMode( translucentMode ) ) { + const classicInteractionDomainShadowChain_t globalChain = + translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + const classicInteractionDomainShadowChain_t localChain = + translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + if ( preparedMode != translucentMode ) { + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + globalChain, logicalDraws, preloadDraws ); + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + localChain, logicalDraws, preloadDraws ); + } else if ( !preparedIncludesLocal ) { + VK_ClassicInteraction_CountShadowRange( prepared, lightPlan, + localChain, logicalDraws, preloadDraws ); + } + } +} + +bool VK_ClassicInteraction_Preflight( const viewDef_t *viewDef ) { + memset( &vkClassicInteractionPrepared, 0, + sizeof( vkClassicInteractionPrepared ) ); + vkClassicInteractionPreparedView_t &prepared = + vkClassicInteractionPrepared; + prepared.uniformCheckpoint = -1; + const classicInteractionDomainView_t *view = + R_ClassicInteractionDomain_FindView( viewDef ); + prepared.view = view; + prepared.viewDef = viewDef; + if ( view == NULL ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_VIEW ); + } + if ( view->backendOutcome[ CLASSIC_INTERACTION_BACKEND_VULKAN ] + == CLASSIC_INTERACTION_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + return VK_ClassicInteraction_Fail( viewDef, + view->failure != CLASSIC_INTERACTION_FAILURE_NONE + ? view->failure + : CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + // The legacy debug path deliberately clamps every interaction and shadow + // submission to one triangle. The sealed shared stream represents the full + // authored work, so preserve exact debug semantics through atomic fallback. + if ( r_singleTriangle.GetBool() ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_STATE ); + } + if ( viewDef == NULL || view->viewDef != viewDef + || view->lightCount < 0 + || view->lightCount > CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS + || view->surfaceCount < 0 + || view->surfaceCount > CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES + || view->primitiveCount < 0 + || view->primitiveCount > CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES + || view->drawablePrimitiveCount < 0 + || view->noopPrimitiveCount < 0 + || view->drawablePrimitiveCount + view->noopPrimitiveCount + != view->primitiveCount + || view->shadowCasterCount < 0 + || view->shadowCasterCount + > CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS + || view->drawableShadowCasterCount < 0 + || view->noopShadowCasterCount < 0 + || view->drawableShadowCasterCount + view->noopShadowCasterCount + != view->shadowCasterCount + || view->logicalVolumeDrawCount < 0 + || view->preloadVolumeDrawCount < 0 + || view->preloadVolumeDrawCount > view->logicalVolumeDrawCount + || view->shadowLightCount < 0 + || view->shadowLightCount > view->lightCount ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + if ( view->shadowMode < CLASSIC_INTERACTION_SHADOW_NONE + || view->shadowMode >= CLASSIC_INTERACTION_SHADOW_MODE_COUNT ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_MODE ); + } + if ( view->shadowMode == CLASSIC_INTERACTION_SHADOW_NONE + && ( view->shadowCasterCount != 0 || view->shadowLightCount != 0 + || view->logicalVolumeDrawCount != 0 + || view->preloadVolumeDrawCount != 0 ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE ); + } + bool viewNeedsStencil = false; + for ( int lightIndex = 0; lightIndex < view->lightCount; ++lightIndex ) { + const classicInteractionDomainLight_t *light = + R_ClassicInteractionDomain_ViewLight( *view, lightIndex ); + if ( light == NULL ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + lightIndex ); + } + for ( int receiverIndex = 0; + receiverIndex < CLASSIC_INTERACTION_RECEIVER_COUNT; + ++receiverIndex ) { + viewNeedsStencil = viewNeedsStencil + || VK_ClassicInteraction_VolumeMode( + light->receiverShadowMode[ receiverIndex ] ); + } + } + if ( backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL + || !VK_Exec_SharedInteractionTargetReady() ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_OFFSCREEN_TARGET ); + } + + prepared.cmd = VK_Exec_ActiveCmd(); + prepared.frameSlot = VK_Exec_ActiveFrameSlot(); + prepared.framebufferWidth = VK_Exec_ActiveFramebufferWidth(); + prepared.framebufferHeight = VK_Exec_ActiveFramebufferHeight(); + prepared.pipeline = VK_Exec_InteractionPipeline(); + prepared.layout = VK_Exec_InteractionPipelineLayout(); + prepared.shadowPipeline = view->logicalVolumeDrawCount > 0 + ? VK_Exec_StencilShadowPipeline() : VK_NULL_HANDLE; + prepared.shadowLayout = view->logicalVolumeDrawCount > 0 + ? VK_Exec_BasePipelineLayout() : VK_NULL_HANDLE; + prepared.projectedInteractionPipeline = view->projectedShadowMapPassCount > 0 + ? VK_Exec_ShadowInteractionPipeline() : VK_NULL_HANDLE; + prepared.pointInteractionPipeline = view->pointShadowMapPassCount > 0 + ? VK_Exec_PointShadowInteractionPipeline() : VK_NULL_HANDLE; + prepared.mappedInteractionLayout = view->shadowMapPassCount > 0 + ? VK_Exec_ShadowInteractionPipelineLayout() : VK_NULL_HANDLE; + prepared.atlasSet = VK_NULL_HANDLE; + if ( prepared.cmd == VK_NULL_HANDLE || prepared.frameSlot < 0 + || prepared.framebufferWidth <= 0 + || prepared.framebufferHeight <= 0 ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_RENDER_SCOPE ); + } + if ( prepared.pipeline == VK_NULL_HANDLE + || prepared.layout == VK_NULL_HANDLE + || ( view->projectedShadowMapPassCount > 0 + && ( prepared.projectedInteractionPipeline == VK_NULL_HANDLE + || prepared.mappedInteractionLayout == VK_NULL_HANDLE ) ) + || ( view->pointShadowMapPassCount > 0 + && ( prepared.pointInteractionPipeline == VK_NULL_HANDLE + || prepared.mappedInteractionLayout == VK_NULL_HANDLE ) ) + || ( viewNeedsStencil + && ( !VK_Exec_ActiveTargetHasStencil() + || view->stencilReference != 128 ) ) + || ( view->logicalVolumeDrawCount > 0 + && ( prepared.shadowPipeline == VK_NULL_HANDLE + || prepared.shadowLayout == VK_NULL_HANDLE + ) ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + viewNeedsStencil + ? VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE + : VK_CLASSIC_INTERACTION_REJECT_PIPELINE ); + } + + const int viewportWidth = view->viewportX2 - view->viewportX1 + 1; + const int viewportHeight = view->viewportY2 - view->viewportY1 + 1; + if ( viewportWidth <= 0 || viewportHeight <= 0 + || view->viewportX1 < 0 || view->viewportY1 < 0 + || view->viewportX1 + viewportWidth > prepared.framebufferWidth + || view->viewportY1 + viewportHeight > prepared.framebufferHeight ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_VIEW ); + } + prepared.viewport.x = static_cast( view->viewportX1 ); + prepared.viewport.y = static_cast( + prepared.framebufferHeight - view->viewportY1 ); + prepared.viewport.width = static_cast( viewportWidth ); + prepared.viewport.height = -static_cast( viewportHeight ); + prepared.viewport.minDepth = 0.0f; + prepared.viewport.maxDepth = 1.0f; + + VkDescriptorSet specularTableSet = VK_NULL_HANDLE; + if ( globalImages == NULL || globalImages->specularTableImage == NULL + || !globalImages->specularTableImage->IsLoaded() + || globalImages->specularTableImage->IsDefaulted() ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_TEXTURE ); + } + specularTableSet = VK_Exec_ImageDescriptor( + globalImages->specularTableImage->GetDeviceHandle(), true ); + const VkDescriptorSet uniformSet = VK_Exec_InteractionUniformSet(); + if ( specularTableSet == VK_NULL_HANDLE || uniformSet == VK_NULL_HANDLE ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_TEXTURE ); + } + if ( !VK_Exec_SharedInteractionGeometryCheckpoint() ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_GEOMETRY ); + } + if ( !VK_ShadowMap_PreflightClassicInteractionView( view ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE ); + } + prepared.atlasSet = view->projectedShadowMapPassCount > 0 + ? VK_Exec_ShadowDescriptorSet() : VK_NULL_HANDLE; + if ( view->projectedShadowMapPassCount > 0 + && prepared.atlasSet == VK_NULL_HANDLE ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_TEXTURE ); + } + + for ( int primitiveIndex = 0; primitiveIndex < view->primitiveCount; + ++primitiveIndex ) { + const classicInteractionDomainPrimitive_t *primitive = + R_ClassicInteractionDomain_ViewPrimitive( *view, primitiveIndex ); + if ( primitive == NULL ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + primitiveIndex ); + } + if ( primitive->disposition != CLASSIC_INTERACTION_PRIMITIVE_DRAW ) { + if ( primitive->disposition <= CLASSIC_INTERACTION_PRIMITIVE_DRAW + || primitive->disposition >= CLASSIC_INTERACTION_PRIMITIVE_COUNT ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_STATE ); + } + prepared.noopPrimitiveCount++; + continue; + } + if ( prepared.drawPlanCount >= CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + + vkClassicInteractionDrawPlan_t &plan = + prepared.draws[ prepared.drawPlanCount ]; + memset( &plan, 0, sizeof( plan ) ); + plan.primitive = primitive; + plan.uniformOffset = -1; + plan.shadowUniformOffset = -1; + if ( !VK_ClassicInteraction_ValidatePrimitive( *primitive, + plan.cullMode, plan.depthCompare ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_STATE ); + } + if ( !VK_ClassicInteraction_BuildScissor( *view, *primitive, + prepared.framebufferWidth, prepared.framebufferHeight, + plan.scissor ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_SCISSOR ); + } + if ( !VK_Exec_PrepareTriGeometry( prepared.cmd, prepared.frameSlot, + primitive->legacyDrawSurf->geo, plan.vertexOffset, + plan.indexOffset ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_GEOMETRY ); + } + + plan.sets[0] = specularTableSet; + if ( !VK_ClassicInteraction_ResolveDescriptor( + primitive->bumpImageResourceId, plan.sets[1] ) + || !VK_ClassicInteraction_ResolveDescriptor( + primitive->lightFalloffImageResourceId, plan.sets[2] ) + || !VK_ClassicInteraction_ResolveDescriptor( + primitive->lightImageResourceId, plan.sets[3] ) + || !VK_ClassicInteraction_ResolveDescriptor( + primitive->diffuseImageResourceId, plan.sets[4] ) + || !VK_ClassicInteraction_ResolveDescriptor( + primitive->specularImageResourceId, plan.sets[5] ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_TEXTURE ); + } + plan.sets[6] = uniformSet; + VK_ClassicInteraction_BuildBlocks( *view, *primitive, plan ); + prepared.drawPlanCount++; + } + + if ( prepared.drawPlanCount != view->drawablePrimitiveCount + || prepared.noopPrimitiveCount != view->noopPrimitiveCount ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + + // Reconcile the already prepared primitive stream back into its sealed + // light/receiver ranges, then prepare every stencil or hybrid supplement + // volume before the first attachment clear or draw. Mapped resources and + // caster commands were reserved by the whole-view shadow transaction above. + int drawCursor = 0; + int shadowLightCount = 0; + for ( int lightIndex = 0; lightIndex < view->lightCount; ++lightIndex ) { + if ( prepared.lightPlanCount >= CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + const classicInteractionDomainLight_t *light = + R_ClassicInteractionDomain_ViewLight( *view, lightIndex ); + if ( light == NULL || light->legacyViewLight == NULL ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + lightIndex ); + } + vkClassicInteractionLightPlan_t &lightPlan = + prepared.lights[ prepared.lightPlanCount++ ]; + memset( &lightPlan, 0, sizeof( lightPlan ) ); + lightPlan.light = light; + + for ( int receiverIndex = 0; + receiverIndex < CLASSIC_INTERACTION_RECEIVER_COUNT; + ++receiverIndex ) { + const classicInteractionDomainShadowMode_t receiverMode = + light->receiverShadowMode[ receiverIndex ]; + if ( receiverMode < CLASSIC_INTERACTION_SHADOW_NONE + || receiverMode >= CLASSIC_INTERACTION_SHADOW_MODE_COUNT ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_MODE ); + } + lightPlan.firstDraw[ receiverIndex ] = drawCursor; + while ( drawCursor < prepared.drawPlanCount ) { + const classicInteractionDomainPrimitive_t *primitive = + prepared.draws[ drawCursor ].primitive; + if ( primitive == NULL + || primitive->legacyViewLight != light->legacyViewLight + || primitive->receiver != receiverIndex ) { + break; + } + drawCursor++; + } + lightPlan.drawCount[ receiverIndex ] = drawCursor + - lightPlan.firstDraw[ receiverIndex ]; + + const bool mappedReceiver = + receiverMode == CLASSIC_INTERACTION_SHADOW_PROJECTED + || receiverMode == CLASSIC_INTERACTION_SHADOW_POINT + || receiverMode == CLASSIC_INTERACTION_SHADOW_HYBRID; + if ( mappedReceiver ) { + const classicInteractionDomainReceiver_t mapReceiver = + receiverIndex == CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT + ? CLASSIC_INTERACTION_RECEIVER_GLOBAL + : static_cast( + receiverIndex ); + const classicInteractionDomainShadowMapPass_t *mapPass = + R_ClassicInteractionDomain_LightShadowMapPass( + *light, mapReceiver ); + const vkShadowLightState_t *physicalLight = + VK_ShadowMap_LightState( light->legacyViewLight ); + const vkShadowPassState_t *physicalPass = + VK_ShadowMap_PassState( physicalLight, + static_cast( mapReceiver ) ); + if ( mapPass == NULL || physicalLight == NULL + || physicalPass == NULL + || ( receiverMode == CLASSIC_INTERACTION_SHADOW_HYBRID + && mapPass->disposition + != CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID ) + || ( receiverMode == CLASSIC_INTERACTION_SHADOW_POINT + && mapPass->lightClass != SHADOWMAP_LIGHT_POINT ) + || ( receiverMode + == CLASSIC_INTERACTION_SHADOW_PROJECTED + && mapPass->lightClass == SHADOWMAP_LIGHT_POINT ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE ); + } + for ( int drawIndex = lightPlan.firstDraw[ receiverIndex ]; + drawIndex < lightPlan.firstDraw[ receiverIndex ] + + lightPlan.drawCount[ receiverIndex ]; + ++drawIndex ) { + vkClassicInteractionDrawPlan_t &drawPlan = + prepared.draws[ drawIndex ]; + if ( !VK_ClassicInteraction_BuildMappedShadowBlock( + *drawPlan.primitive, *mapPass, *physicalLight, + *physicalPass, drawPlan, prepared.atlasSet ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE ); + } + } + } + } + if ( drawCursor < prepared.drawPlanCount + && prepared.draws[ drawCursor ].primitive != NULL + && prepared.draws[ drawCursor ].primitive->legacyViewLight + == light->legacyViewLight ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + + int lightShadowCasterCount = 0; + int lightDrawableShadowCasters = 0; + int lightNoopShadowCasters = 0; + int lightLogicalVolumeDraws = 0; + int lightPreloadVolumeDraws = 0; + bool useChain[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + memset( useChain, 0, sizeof( useChain ) ); + const classicInteractionDomainShadowMode_t localMode = + light->receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_LOCAL ]; + const classicInteractionDomainShadowMode_t globalMode = + light->receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ]; + const classicInteractionDomainShadowMode_t translucentMode = + light->receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ]; + useChain[ CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL ] = + localMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || globalMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || translucentMode == CLASSIC_INTERACTION_SHADOW_STENCIL; + useChain[ CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL ] = + globalMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || translucentMode == CLASSIC_INTERACTION_SHADOW_STENCIL; + useChain[ CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL ] = + localMode == CLASSIC_INTERACTION_SHADOW_HYBRID + || globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + || translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID; + useChain[ CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ] = + globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + || translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID; + for ( int chainIndex = 0; + chainIndex < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; + ++chainIndex ) { + const classicInteractionDomainShadowChain_t chain = + static_cast( chainIndex ); + lightPlan.firstShadow[ chainIndex ] = prepared.shadowPlanCount; + const int chainCount = light->shadowCasterCount[ chainIndex ]; + if ( chainCount < 0 ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_MAP, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_MODE ); + } + for ( int casterIndex = 0; casterIndex < chainCount; + ++casterIndex ) { + const classicInteractionDomainShadowCaster_t *caster = + R_ClassicInteractionDomain_LightShadowCaster( + *light, chain, casterIndex ); + if ( caster == NULL || caster->chain != chain + || caster->legacyViewLight != light->legacyViewLight ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + casterIndex ); + } + lightShadowCasterCount++; + if ( caster->disposition + == CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY ) { + if ( useChain[ chainIndex ] + && !VK_ClassicInteraction_ValidateNoopShadowCaster( + *light, *caster ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_GEOMETRY ); + } + prepared.noopShadowCasterCount++; + lightNoopShadowCasters++; + continue; + } + lightDrawableShadowCasters++; + if ( !useChain[ chainIndex ] ) { + continue; + } + if ( prepared.shadowPlanCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS + || !VK_ClassicInteraction_ValidateShadowCaster( + *view, *light, *caster ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_GEOMETRY ); + } + vkClassicInteractionShadowPlan_t &shadowPlan = + prepared.shadows[ prepared.shadowPlanCount ]; + memset( &shadowPlan, 0, sizeof( shadowPlan ) ); + shadowPlan.caster = caster; + shadowPlan.vertexOffset = -1; + shadowPlan.indexOffset = -1; + if ( !VK_ClassicInteraction_BuildShadowScissor( *view, + *caster, prepared.framebufferWidth, + prepared.framebufferHeight, shadowPlan.scissor ) + || !VK_Exec_PrepareShadowGeometry( prepared.cmd, + prepared.frameSlot, caster->legacyCasterGeometry, + shadowPlan.vertexOffset, shadowPlan.indexOffset ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_GEOMETRY ); + } + VK_ClassicInteraction_BuildShadowPush( + *view, *caster, shadowPlan ); + prepared.shadowPlanCount++; + } + lightPlan.shadowCount[ chainIndex ] = prepared.shadowPlanCount + - lightPlan.firstShadow[ chainIndex ]; + } + VK_ClassicInteraction_CountPlannedShadowWork( prepared, lightPlan, + lightLogicalVolumeDraws, lightPreloadVolumeDraws ); + prepared.logicalVolumeDrawCount += lightLogicalVolumeDraws; + prepared.preloadVolumeDrawCount += lightPreloadVolumeDraws; + const bool lightNeedsStencil = + VK_ClassicInteraction_VolumeMode( localMode ) + || VK_ClassicInteraction_VolumeMode( globalMode ) + || VK_ClassicInteraction_VolumeMode( translucentMode ); + if ( lightShadowCasterCount != light->shadowCasterTotal + || lightDrawableShadowCasters != light->drawableShadowCasters + || lightNoopShadowCasters != light->noopShadowCasters + || lightLogicalVolumeDraws != light->logicalVolumeDraws + || lightPreloadVolumeDraws != light->preloadVolumeDraws + || lightNeedsStencil != light->clearStencil ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_INTERACTION_REJECT_SHADOW_STATE ); + } + if ( light->clearStencil ) { + if ( !VK_ClassicInteraction_BuildLightClearRect( *view, *light, + prepared.framebufferWidth, prepared.framebufferHeight, + lightPlan.clearRect ) ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_SCISSOR ); + } + } + if ( light->shadowCasterTotal > 0 ) { + shadowLightCount++; + } + } + if ( drawCursor != prepared.drawPlanCount + || prepared.lightPlanCount != view->lightCount + || prepared.noopShadowCasterCount != view->noopShadowCasterCount + || prepared.logicalVolumeDrawCount != view->logicalVolumeDrawCount + || prepared.preloadVolumeDrawCount != view->preloadVolumeDrawCount + || shadowLightCount != view->shadowLightCount ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_INTERACTION_REJECT_COUNTS ); + } + + // Allocate the complete uniform stream last. If the ring cannot hold the + // whole view, restore its cursor before selecting the classic rollback so + // the failed shared attempt cannot starve the established light path. + prepared.uniformCheckpoint = VK_Exec_InteractionUniformCheckpoint(); + if ( prepared.uniformCheckpoint < 0 ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_INTERACTION_REJECT_UNIFORM ); + } + for ( int drawIndex = 0; drawIndex < prepared.drawPlanCount; ++drawIndex ) { + vkClassicInteractionDrawPlan_t &plan = prepared.draws[drawIndex]; + plan.uniformOffset = VK_Exec_InteractionUniformAlloc( + &plan.block, sizeof( plan.block ) ); + if ( plan.uniformOffset < 0 ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_UNIFORM ); + } + if ( plan.mappedShadowMode == 1 ) { + plan.shadowUniformOffset = VK_Exec_ShadowUniformAlloc( + &plan.projectedShadowBlock, + sizeof( plan.projectedShadowBlock ) ); + } else if ( plan.mappedShadowMode == 2 ) { + plan.shadowUniformOffset = VK_Exec_ShadowUniformAlloc( + &plan.pointShadowBlock, sizeof( plan.pointShadowBlock ) ); + } + if ( plan.mappedShadowMode != 0 + && plan.shadowUniformOffset < 0 ) { + return VK_ClassicInteraction_Fail( viewDef, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_INTERACTION_REJECT_UNIFORM ); + } + } + + VK_Exec_SharedInteractionGeometryCommit(); + prepared.uniformCheckpoint = -1; + prepared.ready = true; + return true; +} + +static void VK_ClassicInteraction_ClearStencil( + vkClassicInteractionPreparedView_t &prepared, + const vkClassicInteractionLightPlan_t &lightPlan ) { + VkClearAttachment clearAttachment; + memset( &clearAttachment, 0, sizeof( clearAttachment ) ); + clearAttachment.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; + clearAttachment.clearValue.depthStencil.stencil = + static_cast( prepared.view->stencilReference ); + VkClearRect clearRect; + memset( &clearRect, 0, sizeof( clearRect ) ); + clearRect.rect = lightPlan.clearRect; + clearRect.layerCount = 1; + vkCmdClearAttachments( prepared.cmd, 1, &clearAttachment, 1, &clearRect ); +} + +static void VK_ClassicInteraction_SelectReceiverShadowMode( + vkClassicInteractionPreparedView_t &prepared, + classicInteractionDomainShadowMode_t mode ) { + const bool stencil = mode == CLASSIC_INTERACTION_SHADOW_STENCIL + || mode == CLASSIC_INTERACTION_SHADOW_HYBRID; + vkCmdSetStencilTestEnable( prepared.cmd, stencil ? VK_TRUE : VK_FALSE ); + if ( stencil ) { + vkCmdSetStencilOp( prepared.cmd, VK_STENCIL_FACE_FRONT_AND_BACK, + VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, + VK_COMPARE_OP_GREATER_OR_EQUAL ); + vkCmdSetStencilCompareMask( prepared.cmd, + VK_STENCIL_FACE_FRONT_AND_BACK, 255 ); + vkCmdSetStencilWriteMask( prepared.cmd, + VK_STENCIL_FACE_FRONT_AND_BACK, 255 ); + vkCmdSetStencilReference( prepared.cmd, + VK_STENCIL_FACE_FRONT_AND_BACK, + static_cast( prepared.view->stencilReference ) ); + } +} + +static void VK_ClassicInteraction_DrawReceiverRange( + vkClassicInteractionPreparedView_t &prepared, int firstDraw, + int drawCount, classicInteractionDomainShadowMode_t shadowMode ) { + VK_ClassicInteraction_SelectReceiverShadowMode( prepared, shadowMode ); + for ( int localDraw = 0; localDraw < drawCount; ++localDraw ) { + const vkClassicInteractionDrawPlan_t &plan = + prepared.draws[ firstDraw + localDraw ]; + const classicInteractionDomainPrimitive_t &primitive = *plan.primitive; + VkPipeline pipeline = prepared.pipeline; + VkPipelineLayout layout = prepared.layout; + if ( plan.mappedShadowMode == 1 ) { + pipeline = prepared.projectedInteractionPipeline; + layout = prepared.mappedInteractionLayout; + } else if ( plan.mappedShadowMode == 2 ) { + pipeline = prepared.pointInteractionPipeline; + layout = prepared.mappedInteractionLayout; + } + if ( pipeline != prepared.boundPipeline ) { + vkCmdBindPipeline( prepared.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipeline ); + prepared.boundPipeline = pipeline; + } + VK_Exec_BindPreparedTriGeometry( prepared.cmd, prepared.frameSlot, + plan.vertexOffset, plan.indexOffset ); + vkCmdSetScissor( prepared.cmd, 0, 1, &plan.scissor ); + vkCmdSetCullMode( prepared.cmd, plan.cullMode ); + vkCmdSetDepthCompareOp( prepared.cmd, plan.depthCompare ); + vkCmdSetDepthBiasEnable( prepared.cmd, + primitive.polygonOffsetEnabled ? VK_TRUE : VK_FALSE ); + if ( primitive.polygonOffsetEnabled ) { + vkCmdSetDepthBias( prepared.cmd, primitive.polygonOffsetUnits, + 0.0f, primitive.polygonOffsetFactor ); + } + uint32_t dynamicOffsets[ 2 ]; + dynamicOffsets[ 0 ] = static_cast( plan.uniformOffset ); + dynamicOffsets[ 1 ] = static_cast( + Max( 0, plan.shadowUniformOffset ) ); + const uint32_t setCount = plan.mappedShadowMode != 0 ? 8u : 7u; + const uint32_t dynamicOffsetCount = + plan.mappedShadowMode != 0 ? 2u : 1u; + vkCmdBindDescriptorSets( prepared.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + layout, 0, setCount, plan.sets, + dynamicOffsetCount, dynamicOffsets ); + vkCmdPushConstants( prepared.cmd, layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( plan.push ), &plan.push ); + vkCmdDrawIndexed( prepared.cmd, + static_cast( primitive.indexCount ), 1, 0, 0, 0 ); + prepared.submittedDraws++; + } +} + +static void VK_ClassicInteraction_DrawShadowRange( + vkClassicInteractionPreparedView_t &prepared, int firstShadow, + int shadowCount ) { + if ( shadowCount <= 0 ) { + return; + } + + VkCommandBuffer cmd = prepared.cmd; + if ( prepared.shadowPipeline != prepared.boundPipeline ) { + vkCmdBindPipeline( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + prepared.shadowPipeline ); + prepared.boundPipeline = prepared.shadowPipeline; + } + vkCmdSetDepthTestEnable( cmd, VK_TRUE ); + vkCmdSetDepthWriteEnable( cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_LESS_OR_EQUAL ); + vkCmdSetCullMode( cmd, VK_CULL_MODE_NONE ); + const bool shadowBias = prepared.view->shadowPolygonFactor != 0.0f + || prepared.view->shadowPolygonUnits != 0.0f; + vkCmdSetDepthBiasEnable( cmd, shadowBias ? VK_TRUE : VK_FALSE ); + if ( shadowBias ) { + vkCmdSetDepthBias( cmd, prepared.view->shadowPolygonUnits, 0.0f, + prepared.view->shadowPolygonFactor ); + } + vkCmdSetStencilTestEnable( cmd, VK_TRUE ); + vkCmdSetStencilCompareMask( cmd, VK_STENCIL_FACE_FRONT_AND_BACK, 255 ); + vkCmdSetStencilWriteMask( cmd, VK_STENCIL_FACE_FRONT_AND_BACK, 255 ); + vkCmdSetStencilReference( cmd, VK_STENCIL_FACE_FRONT_AND_BACK, + static_cast( prepared.view->stencilReference ) ); + + const VkStencilFaceFlags frontSidedFace = prepared.viewDef->isMirror + ? VK_STENCIL_FACE_FRONT_BIT : VK_STENCIL_FACE_BACK_BIT; + const VkStencilFaceFlags backSidedFace = prepared.viewDef->isMirror + ? VK_STENCIL_FACE_BACK_BIT : VK_STENCIL_FACE_FRONT_BIT; + const bool useDepthBounds = prepared.view->useDepthBounds + && vkCtx.depthBoundsSupported; + if ( useDepthBounds ) { + vkCmdSetDepthBoundsTestEnable( cmd, VK_TRUE ); + } + + for ( int localShadow = 0; localShadow < shadowCount; ++localShadow ) { + const vkClassicInteractionShadowPlan_t &plan = + prepared.shadows[ firstShadow + localShadow ]; + const classicInteractionDomainShadowCaster_t &caster = *plan.caster; + VK_Exec_BindPreparedTriGeometry( cmd, prepared.frameSlot, + plan.vertexOffset, plan.indexOffset ); + vkCmdSetScissor( cmd, 0, 1, &plan.scissor ); + if ( useDepthBounds ) { + vkCmdSetDepthBounds( cmd, caster.depthMin, caster.depthMax ); + } + vkCmdPushConstants( cmd, prepared.shadowLayout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( plan.push ), &plan.push ); + + if ( caster.preload ) { + vkCmdSetStencilOp( cmd, frontSidedFace, VK_STENCIL_OP_KEEP, + VK_STENCIL_OP_DECREMENT_AND_WRAP, + VK_STENCIL_OP_DECREMENT_AND_WRAP, VK_COMPARE_OP_ALWAYS ); + vkCmdSetStencilOp( cmd, backSidedFace, VK_STENCIL_OP_KEEP, + VK_STENCIL_OP_INCREMENT_AND_WRAP, + VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_COMPARE_OP_ALWAYS ); + vkCmdDrawIndexed( cmd, + static_cast( caster.selectedIndexCount ), 1, 0, 0, 0 ); + prepared.submittedPreloadVolumeDraws++; + backEnd.pc.c_shadowElements++; + backEnd.pc.c_shadowIndexes += caster.selectedIndexCount; + backEnd.pc.c_shadowVertexes += caster.vertexCount; + } + + vkCmdSetStencilOp( cmd, frontSidedFace, VK_STENCIL_OP_KEEP, + VK_STENCIL_OP_INCREMENT_AND_WRAP, VK_STENCIL_OP_KEEP, + VK_COMPARE_OP_ALWAYS ); + vkCmdSetStencilOp( cmd, backSidedFace, VK_STENCIL_OP_KEEP, + VK_STENCIL_OP_DECREMENT_AND_WRAP, VK_STENCIL_OP_KEEP, + VK_COMPARE_OP_ALWAYS ); + vkCmdDrawIndexed( cmd, + static_cast( caster.selectedIndexCount ), 1, 0, 0, 0 ); + prepared.submittedShadowCasters++; + prepared.submittedLogicalVolumeDraws++; + backEnd.pc.c_shadowElements++; + backEnd.pc.c_shadowIndexes += caster.selectedIndexCount; + backEnd.pc.c_shadowVertexes += caster.vertexCount; + } + + if ( shadowBias ) { + vkCmdSetDepthBiasEnable( cmd, VK_FALSE ); + } + if ( useDepthBounds ) { + vkCmdSetDepthBoundsTestEnable( cmd, VK_FALSE ); + vkCmdSetDepthBounds( cmd, 0.0f, 1.0f ); + } + vkCmdSetStencilOp( cmd, VK_STENCIL_FACE_FRONT_AND_BACK, + VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, + VK_COMPARE_OP_GREATER_OR_EQUAL ); +} + +void VK_ClassicInteraction_DrawOwnedView( const viewDef_t *viewDef ) { + vkClassicInteractionPreparedView_t &prepared = + vkClassicInteractionPrepared; + if ( !prepared.ready || prepared.committed || prepared.view == NULL + || prepared.viewDef == NULL || prepared.viewDef != viewDef + || prepared.cmd == VK_NULL_HANDLE + || prepared.pipeline == VK_NULL_HANDLE + || prepared.layout == VK_NULL_HANDLE ) { + return; + } + + VK_ShadowMap_CommitClassicInteractionView( prepared.view ); + prepared.committed = true; + prepared.boundPipeline = VK_NULL_HANDLE; + vkCmdSetViewport( prepared.cmd, 0, 1, &prepared.viewport ); + vkCmdSetDepthTestEnable( prepared.cmd, VK_TRUE ); + vkCmdSetDepthWriteEnable( prepared.cmd, VK_FALSE ); + vkCmdSetDepthBiasEnable( prepared.cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( prepared.cmd, VK_FALSE ); + vkCmdSetFrontFace( prepared.cmd, VK_FRONT_FACE_COUNTER_CLOCKWISE ); + + for ( int lightIndex = 0; lightIndex < prepared.lightPlanCount; + ++lightIndex ) { + const vkClassicInteractionLightPlan_t &lightPlan = + prepared.lights[ lightIndex ]; + const classicInteractionDomainLight_t &light = *lightPlan.light; + classicInteractionDomainShadowMode_t preparedVolumeMode = + CLASSIC_INTERACTION_SHADOW_NONE; + bool preparedVolumeIncludesLocal = false; + const classicInteractionDomainShadowMode_t localMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_LOCAL ]; + if ( localMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || localMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + VK_ClassicInteraction_ClearStencil( prepared, lightPlan ); + const classicInteractionDomainShadowChain_t chain = + localMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ chain ], + lightPlan.shadowCount[ chain ] ); + preparedVolumeMode = localMode; + preparedVolumeIncludesLocal = false; + } + VK_ClassicInteraction_DrawReceiverRange( prepared, + lightPlan.firstDraw[ CLASSIC_INTERACTION_RECEIVER_LOCAL ], + lightPlan.drawCount[ CLASSIC_INTERACTION_RECEIVER_LOCAL ], + localMode ); + + const classicInteractionDomainShadowMode_t globalMode = + light.receiverShadowMode[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ]; + if ( globalMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + const classicInteractionDomainShadowChain_t globalChain = + globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + const classicInteractionDomainShadowChain_t localChain = + globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + const bool modeChanged = preparedVolumeMode != globalMode; + if ( modeChanged ) { + VK_ClassicInteraction_ClearStencil( prepared, lightPlan ); + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ globalChain ], + lightPlan.shadowCount[ globalChain ] ); + } + if ( modeChanged || !preparedVolumeIncludesLocal ) { + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ localChain ], + lightPlan.shadowCount[ localChain ] ); + } + preparedVolumeMode = globalMode; + preparedVolumeIncludesLocal = true; + } + VK_ClassicInteraction_DrawReceiverRange( prepared, + lightPlan.firstDraw[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ], + lightPlan.drawCount[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ], + globalMode ); + + const classicInteractionDomainShadowMode_t translucentMode = + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ]; + if ( ( translucentMode == CLASSIC_INTERACTION_SHADOW_STENCIL + || translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) + && preparedVolumeMode != translucentMode ) { + const classicInteractionDomainShadowChain_t globalChain = + translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + const classicInteractionDomainShadowChain_t localChain = + translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + VK_ClassicInteraction_ClearStencil( prepared, lightPlan ); + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ globalChain ], + lightPlan.shadowCount[ globalChain ] ); + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ localChain ], + lightPlan.shadowCount[ localChain ] ); + preparedVolumeMode = translucentMode; + preparedVolumeIncludesLocal = true; + } else if ( ( translucentMode + == CLASSIC_INTERACTION_SHADOW_STENCIL + || translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) + && !preparedVolumeIncludesLocal ) { + const classicInteractionDomainShadowChain_t localChain = + translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + VK_ClassicInteraction_DrawShadowRange( prepared, + lightPlan.firstShadow[ localChain ], + lightPlan.shadowCount[ localChain ] ); + preparedVolumeIncludesLocal = true; + } + VK_ClassicInteraction_DrawReceiverRange( prepared, + lightPlan.firstDraw[ CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ], + lightPlan.drawCount[ CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ], + translucentMode ); + } + + vkCmdSetDepthBiasEnable( prepared.cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( prepared.cmd, VK_FALSE ); + const bool coverageAccepted = R_ClassicInteractionDomain_RecordOwned( + viewDef, CLASSIC_INTERACTION_BACKEND_VULKAN, + prepared.submittedDraws, prepared.noopPrimitiveCount, + prepared.view->drawableShadowCasterCount, + prepared.noopShadowCasterCount, + prepared.submittedLogicalVolumeDraws, + prepared.submittedPreloadVolumeDraws, + prepared.view->shadowMapPassCount, + prepared.view->hybridShadowPassCount ); + if ( !coverageAccepted ) { + common->Warning( "Vulkan: shared interaction backend coverage mismatch after commit" ); + } + + static bool loggedFirstOwnedView = false; + if ( !loggedFirstOwnedView && coverageAccepted + && prepared.submittedDraws > 0 ) { + loggedFirstOwnedView = true; + common->Printf( + "Vulkan: shared interaction owned %d draws, %d noops, %d shadow records, %d volume draws, %d preloads, %d map passes, and %d hybrid passes (hash=%016llx)\n", + prepared.submittedDraws, prepared.noopPrimitiveCount, + prepared.view->drawableShadowCasterCount, + prepared.submittedLogicalVolumeDraws, + prepared.submittedPreloadVolumeDraws, + prepared.view->shadowMapPassCount, + prepared.view->hybridShadowPassCount, + static_cast( prepared.view->hash ) ); + } +} + /* ==================== VK_ResolveCustomLightingTexture @@ -514,6 +2106,151 @@ static void VK_SetDrawInteraction( const shaderStage_t *surfaceStage, const floa } } +/* +==================== +VK_PackedPBRInteraction + +The native Vulkan interaction pipelines have six fixed 2D descriptor slots. +Packed metallic/roughness/occlusion fits without expanding that ABI: slot 1 +becomes the RGB tangent normal, slot 4 becomes albedo, and slot 5 becomes +ORM. Everything outside this deliberately narrow contract stays on the +retail-compatible classic interaction path. +==================== +*/ +typedef struct vkPackedPBRInteraction_s { + idImage * normalImage; + idImage * albedoImage; + idImage * ormImage; + float metallic; + float roughness; + float normalScale; +} vkPackedPBRInteraction_t; + +static float VK_PBRRegisterValue( const drawSurf_t *surf, int registerIndex, float fallback ) { + if ( surf == NULL || surf->material == NULL || surf->shaderRegisters == NULL + || registerIndex < 0 || registerIndex >= surf->material->GetNumRegisters() ) { + return fallback; + } + const float value = surf->shaderRegisters[ registerIndex ]; + return std::isfinite( value ) ? value : fallback; +} + +static bool VK_PBRImageReady( idImage *image, textureUsage_t expectedUsage ) { + return image != NULL && image->GetUsage() == expectedUsage + && image->IsLoaded() && !image->IsDefaulted() + && image->GetDeviceHandle() != 0; +} + +/* +==================== +VK_PBRHasSingleClassicInteractionTopology + +The classic decomposition flushes an interaction whenever it encounters a +second bump, diffuse, or specular stage. A packed-PBR draw evaluates the whole +BRDF, so it may only replace the canonical final submit when the material has +exactly one active classic bump -> diffuse -> specular sequence. Declared +duplicates are rejected even when their current condition is false: changing +registers must never change which decomposition draw owns the complete BRDF. +==================== +*/ +static bool VK_PBRHasSingleClassicInteractionTopology( const drawSurf_t *surf ) { + if ( surf == NULL || surf->material == NULL || surf->shaderRegisters == NULL ) { + return false; + } + + const idMaterial *material = surf->material; + const float *registers = surf->shaderRegisters; + const int registerCount = material->GetNumRegisters(); + int bumpStage = -1; + int diffuseStage = -1; + int specularStage = -1; + + for ( int stageIndex = 0; stageIndex < material->GetNumStages(); stageIndex++ ) { + const shaderStage_t *surfaceStage = material->GetStage( stageIndex ); + if ( surfaceStage == NULL ) { + return false; + } + + // A custom-lighting stage is another complete per-light owner. Keep + // mixed custom/classic materials entirely on their authored path. + if ( surfaceStage->newStage != NULL && surfaceStage->newStage->customLighting ) { + return false; + } + + int *ownerStage = NULL; + switch ( surfaceStage->lighting ) { + case SL_BUMP: + ownerStage = &bumpStage; + break; + case SL_DIFFUSE: + ownerStage = &diffuseStage; + break; + case SL_SPECULAR: + ownerStage = &specularStage; + break; + default: + continue; + } + + if ( *ownerStage >= 0 || surfaceStage->newStage != NULL + || surfaceStage->texture.image == NULL + || surfaceStage->conditionRegister < 0 + || surfaceStage->conditionRegister >= registerCount ) { + return false; + } + const float condition = registers[ surfaceStage->conditionRegister ]; + if ( !std::isfinite( condition ) || condition == 0.0f ) { + return false; + } + *ownerStage = stageIndex; + } + + return bumpStage >= 0 && diffuseStage > bumpStage && specularStage > diffuseStage; +} + +static bool VK_PackedPBRInteraction( const drawInteraction_t *din, + vkPackedPBRInteraction_t &out ) { + memset( &out, 0, sizeof( out ) ); + if ( !r_rendererModernQuality.GetBool() || !r_pbrMaterials.GetBool() || r_skipBump.GetBool() + || r_skipDiffuse.GetBool() || r_skipSpecular.GetBool() + || din == NULL || din->surf == NULL || din->surf->material == NULL + || din->ambientLight + || !VK_PBRHasSingleClassicInteractionTopology( din->surf ) ) { + return false; + } + + const idMaterial *material = din->surf->material; + if ( material->Coverage() != MC_OPAQUE || !material->HasPBR() ) { + return false; + } + const materialResourceTableRecord_t *resourceRecord = + R_MaterialResourceTable_FindRecordForMaterial( material ); + if ( resourceRecord == NULL + || !R_MaterialResourceTable_PBRModernPathEligible( *resourceRecord ) ) { + return false; + } + const pbrMaterialInfo_t &info = material->GetPBRInfo(); + if ( !info.enabled || info.workflow != PBR_WORKFLOW_METALLIC_ROUGHNESS + || info.normalFormat != PBR_NORMAL_TANGENT_XYZ + || !info.albedo.present || !info.normal.present || !info.orm.present + || !VK_PBRImageReady( info.albedo.image, TD_PBR_COLOR ) + || !VK_PBRImageReady( info.normal.image, TD_BUMP ) + || !VK_PBRImageReady( info.orm.image, TD_MATERIAL_DATA ) ) { + return false; + } + + out.normalImage = info.normal.image; + out.albedoImage = info.albedo.image; + out.ormImage = info.orm.image; + out.metallic = idMath::ClampFloat( 0.0f, 1.0f, + VK_PBRRegisterValue( din->surf, info.metallicRegister, 0.0f ) ); + out.roughness = idMath::ClampFloat( 0.045f, 1.0f, + VK_PBRRegisterValue( din->surf, info.roughnessRegister, 0.5f ) ); + out.normalScale = idMath::ClampFloat( 0.0f, 4.0f, + VK_PBRRegisterValue( din->surf, info.normalScaleRegister, 1.0f ) ); + return true; +} + /* ==================== VK_DrawSingleInteraction @@ -524,9 +2261,10 @@ set, pushes the 128B block, and draws the bound light-tris geometry. ==================== */ static void VK_DrawSingleInteractionMode( const drawInteraction_t *din, - bool parallax, - float parallaxScale, - float parallaxBias ) { + bool parallax, + float parallaxScale, + float parallaxBias, + bool allowNativePBR ) { if ( din->bumpImage == NULL || din->lightFalloffImage == NULL || din->lightImage == NULL || din->diffuseImage == NULL || din->specularImage == NULL ) { return; @@ -541,14 +2279,16 @@ static void VK_DrawSingleInteractionMode( const drawInteraction_t *din, } const int setCount = shadowDraw ? 8 : 7; const VkPipelineLayout layout = shadowDraw ? interPass.layoutShadowed : interPass.layout; + vkPackedPBRInteraction_t pbr; + const bool nativePBR = allowNativePBR && VK_PackedPBRInteraction( din, pbr ); VkDescriptorSet sets[ 8 ]; sets[ 0 ] = interPass.specTableSet; - sets[ 1 ] = VK_Exec_ImageDescriptor( din->bumpImage->GetDeviceHandle(), true ); + sets[ 1 ] = VK_Exec_ImageDescriptor( ( nativePBR ? pbr.normalImage : din->bumpImage )->GetDeviceHandle(), true ); sets[ 2 ] = VK_Exec_ImageDescriptor( din->lightFalloffImage->GetDeviceHandle(), true ); sets[ 3 ] = VK_Exec_ImageDescriptor( din->lightImage->GetDeviceHandle(), true ); - sets[ 4 ] = VK_Exec_ImageDescriptor( din->diffuseImage->GetDeviceHandle(), true ); - sets[ 5 ] = VK_Exec_ImageDescriptor( din->specularImage->GetDeviceHandle(), true ); + sets[ 4 ] = VK_Exec_ImageDescriptor( ( nativePBR ? pbr.albedoImage : din->diffuseImage )->GetDeviceHandle(), true ); + sets[ 5 ] = VK_Exec_ImageDescriptor( ( nativePBR ? pbr.ormImage : din->specularImage )->GetDeviceHandle(), true ); sets[ 6 ] = VK_Exec_InteractionUniformSet(); sets[ 7 ] = interPass.shadowSet; for ( int i = 0 ; i < setCount ; i++ ) { @@ -604,26 +2344,56 @@ static void VK_DrawSingleInteractionMode( const drawInteraction_t *din, push.b[ 2 ] = interPass.ambientDir[ 2 ]; push.c[ 0 ] = parallaxScale; push.c[ 1 ] = parallaxBias; - push.c[ 2 ] = parallax ? 1.0f : 0.0f; + push.c[ 2 ] = parallax && !nativePBR ? 1.0f : 0.0f; + push.d[ 0 ] = nativePBR ? ( r_pbrDebug.GetInteger() == 7 ? 2.0f : 1.0f ) : 0.0f; + push.d[ 1 ] = nativePBR ? pbr.metallic : 0.0f; + push.d[ 2 ] = nativePBR ? pbr.roughness : 0.0f; + push.d[ 3 ] = nativePBR ? pbr.normalScale : 1.0f; // dynamic offsets consume in set order: set 6 interaction slice, then // (shadowed only) set 7 binding 1 shadow slice uint32_t dynamicOffsets[ 2 ]; dynamicOffsets[ 0 ] = (uint32_t)uboOffset; dynamicOffsets[ 1 ] = (uint32_t)interPass.shadowSliceOffset; + // Sets 0-5 are pass/material-constant image sets. The interaction and + // shadow-interaction pipeline layouts are built from the same set layouts + // for sets 0..6 and the same push range (vk_GuiExecutor.cpp), so they are + // compatible for sets 0..5 and a suffix rebind with either layout leaves + // the untouched prefix defined. Sets 6..7 carry the per-draw dynamic + // offsets and are always rebound; nothing else in this pass binds + // descriptor sets on this command buffer, so the mirror below stays live. + int firstChangedSet = interPass.imageSetsValid ? 6 : 0; + if ( interPass.imageSetsValid ) { + for ( int i = 0 ; i < 6 ; i++ ) { + if ( sets[ i ] != interPass.lastImageSets[ i ] ) { + firstChangedSet = i; + break; + } + } + } + if ( firstChangedSet < 6 ) { + vkCmdBindDescriptorSets( interPass.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, + (uint32_t)firstChangedSet, (uint32_t)( 6 - firstChangedSet ), + sets + firstChangedSet, 0, NULL ); + } vkCmdBindDescriptorSets( interPass.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, - 0, (uint32_t)setCount, sets, shadowDraw ? 2 : 1, dynamicOffsets ); + 6, (uint32_t)( setCount - 6 ), sets + 6, shadowDraw ? 2 : 1, dynamicOffsets ); + memcpy( interPass.lastImageSets, sets, sizeof( interPass.lastImageSets ) ); + interPass.imageSetsValid = true; vkCmdPushConstants( interPass.cmd, layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof( push ), &push ); vkCmdDrawIndexed( interPass.cmd, (uint32_t)din->surf->geo->numIndexes, 1, 0, 0, 0 ); interPass.drawCount++; + if ( nativePBR ) { + interPass.nativePBRDrawCount++; + } if ( shadowDraw ) { interPass.shadowDrawCount++; } } -static void VK_DrawSingleInteraction( const drawInteraction_t *din ) { - VK_DrawSingleInteractionMode( din, false, 0.0f, 0.0f ); +static void VK_DrawSingleInteraction( const drawInteraction_t *din, bool allowNativePBR ) { + VK_DrawSingleInteractionMode( din, false, 0.0f, 0.0f, allowNativePBR ); } /* @@ -635,7 +2405,7 @@ missing diffuse/specular (and the r_skip* debug substitutions), flat normal map for skipped bump, and the skip-if-nothing-would-draw rule. ==================== */ -static void VK_SubmitInteraction( drawInteraction_t *din ) { +static void VK_SubmitInteraction( drawInteraction_t *din, bool allowNativePBR ) { if ( !din->bumpImage ) { return; } @@ -658,7 +2428,7 @@ static void VK_SubmitInteraction( drawInteraction_t *din ) { || ( ( din->specularColor[0] > 0 || din->specularColor[1] > 0 || din->specularColor[2] > 0 ) && din->specularImage != globalImages->blackImage ) ) { - VK_DrawSingleInteraction( din ); + VK_DrawSingleInteraction( din, allowNativePBR ); } } @@ -680,7 +2450,7 @@ static void VK_SubmitCustomLightingInteraction( drawInteraction_t *din, return; } - VK_DrawSingleInteractionMode( din, parallax, scaleBias[ 0 ], scaleBias[ 1 ] ); + VK_DrawSingleInteractionMode( din, parallax, scaleBias[ 0 ], scaleBias[ 1 ], false ); } /* @@ -799,10 +2569,8 @@ static int VK_Inter_WriteShadowSlice( const viewEntity_t *space ) { pointBlock.lightOriginFar[ 1 ] = state->pointLightOrigin[ 1 ]; pointBlock.lightOriginFar[ 2 ] = state->pointLightOrigin[ 2 ]; pointBlock.lightOriginFar[ 3 ] = state->pointFar; - // the depth-compare path uses r_shadowMapPointBias directly; the GL - // storage-step floor only applies to the packed-color fallback - pointBlock.biasParams[ 0 ] = r_shadowMapPointBias.GetFloat(); - pointBlock.biasParams[ 1 ] = r_shadowMapPointNormalBias.GetFloat(); + pointBlock.biasParams[ 0 ] = state->constantBias; + pointBlock.biasParams[ 1 ] = state->normalBias; pointBlock.biasParams[ 2 ] = state->texelDepthBias; pointBlock.biasParams[ 3 ] = state->normalOffsetWorld; pointBlock.filterParams[ 0 ] = @@ -1668,6 +3436,10 @@ static void VK_CreateSingleDrawInteractions( const drawSurf_t *surf ) { const int lightStageCount = lightShader->GetNumStages(); const int surfaceStageCount = surfaceShader->GetNumStages(); + // Only the final decomposition submit may own a complete packed-PBR BRDF. + // The topology check also makes every intermediate flush impossible for an + // admitted material; false admission leaves every classic draw untouched. + const bool packedPBROwnerEligible = VK_PBRHasSingleClassicInteractionTopology( surf ); for ( int lightStageNum = 0 ; lightStageNum < lightStageCount ; lightStageNum++ ) { const shaderStage_t *lightStage = lightShader->GetStage( lightStageNum ); @@ -1716,7 +3488,7 @@ static void VK_CreateSingleDrawInteractions( const drawSurf_t *surf ) { break; } // draw any previous interaction - VK_SubmitInteraction( &inter ); + VK_SubmitInteraction( &inter, false ); inter.diffuseImage = NULL; inter.specularImage = NULL; VK_SetDrawInteraction( surfaceStage, surfaceRegs, &inter.bumpImage, inter.bumpMatrix, NULL ); @@ -1728,7 +3500,7 @@ static void VK_CreateSingleDrawInteractions( const drawSurf_t *surf ) { break; } if ( inter.diffuseImage ) { - VK_SubmitInteraction( &inter ); + VK_SubmitInteraction( &inter, false ); } VK_SetDrawInteraction( surfaceStage, surfaceRegs, &inter.diffuseImage, inter.diffuseMatrix, inter.diffuseColor.ToFloatPtr() ); @@ -1747,7 +3519,7 @@ static void VK_CreateSingleDrawInteractions( const drawSurf_t *surf ) { break; } if ( inter.specularImage ) { - VK_SubmitInteraction( &inter ); + VK_SubmitInteraction( &inter, false ); } VK_SetDrawInteraction( surfaceStage, surfaceRegs, &inter.specularImage, inter.specularMatrix, inter.specularColor.ToFloatPtr() ); @@ -1762,7 +3534,7 @@ static void VK_CreateSingleDrawInteractions( const drawSurf_t *surf ) { } // draw the final interaction - VK_SubmitInteraction( &inter ); + VK_SubmitInteraction( &inter, packedPBROwnerEligible ); // Quake 4's two shipped customLighting guide families are ambient // material stages that execute once for every active light stage. @@ -1979,12 +3751,20 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { } } } + if ( r_vkShadowFallbackTest.GetBool() && + ( localShadowState != NULL || globalShadowState != NULL ) ) { + VK_ShadowMap_MarkStencilFallbackSticky( vLight ); + shadowState = NULL; + localShadowState = NULL; + globalShadowState = NULL; + } // Determine shadow need per retail ownership chain. A LOCAL receiver // only sees global casters; a GLOBAL receiver sees both. This matters // when a map resource failed or a target lacks stencil: receivers that - // genuinely need unavailable shadow data must fail closed instead of - // silently receiving an unshadowed light. + // genuinely need unavailable shadow data prefer the retained stencil + // path. If neither ownership path can be submitted, preserve the direct + // light unshadowed for this frame instead of visibly dropping the light. const int incompleteMapMask = vLight->shadowMapIncompleteMapMask | vLight->shadowMapPrelightMapMissingMask; @@ -2096,7 +3876,8 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { vLight->localShadows == NULL; const bool stencilResourcesReady = activeTargetHasStencil && - interPass.pipelineStencilShadow != VK_NULL_HANDLE; + interPass.pipelineStencilShadow != VK_NULL_HANDLE && + !r_vkShadowFallbackTest.GetBool(); const bool localStencilFallback = localReceiverNeedsStencil && !localEmptyFallback && @@ -2118,14 +3899,15 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { !globalEmptyFallback && !globalStencilFallback ); if ( missingRequiredShadow && unresolvedBeforeSubmit ) { - static bool warnedFailClosedShadow = false; - if ( !warnedFailClosedShadow ) { - warnedFailClosedShadow = true; - common->Warning( "Vulkan: required shadow resource unavailable; affected light receivers are skipped fail-closed" ); + static bool warnedUnshadowedFallback = false; + if ( !warnedUnshadowedFallback ) { + warnedUnshadowedFallback = true; + common->Warning( "Vulkan: required shadow resource unavailable; affected light receivers fall back unshadowed" ); } } bool globalOpaqueReceiverDrawn = false; + bool localReceiverDrawn = false; bool localReceiverDrewWithStencil = false; bool globalStencilPassComplete = false; if ( anyStencilFallback ) { @@ -2164,6 +3946,7 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { vkCmdSetStencilTestEnable( cmd, VK_TRUE ); VK_DrawInteractionChain( vLight->localInteractions ); localReceiverDrewWithStencil = true; + localReceiverDrawn = true; } } @@ -2175,6 +3958,7 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { localEmptyFallback ? NULL : shadowState, localEmptyFallback ? NULL : localShadowState ); VK_DrawInteractionChain( vLight->localInteractions ); + localReceiverDrawn = true; } if ( globalStencilFallback ) { @@ -2227,7 +4011,7 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { // ownership accumulated so far. Never consume a partial stencil // buffer: keep any already-complete receiver draw, then use a // valid ownership map, an independently proven empty fallback, or - // skip only the affected receiver chain fail-closed. + // preserve direct illumination without a shadow for this frame. vkCmdSetStencilTestEnable( cmd, VK_FALSE ); if ( !globalOpaqueReceiverDrawn && ( globalEmptyFallback || @@ -2248,7 +4032,7 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { static bool warnedIncompleteStencilSubmission = false; if ( !warnedIncompleteStencilSubmission ) { warnedIncompleteStencilSubmission = true; - common->Warning( "Vulkan: stencil shadow volume submission incomplete; affected light receivers are skipped fail-closed" ); + common->Warning( "Vulkan: stencil shadow volume submission incomplete; affected light receivers fall back unshadowed" ); } } } else { @@ -2263,6 +4047,7 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { localEmptyFallback ? NULL : shadowState, localEmptyFallback ? NULL : localShadowState ); VK_DrawInteractionChain( vLight->localInteractions ); + localReceiverDrawn = true; } if ( globalEmptyFallback || !globalOpaqueReceiverNeedsStencil ) { @@ -2274,6 +4059,23 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { } } + // Shadow allocation/admission and late stencil streaming failures are + // recoverable presentation failures. A transient loss of shadowing is + // preferable to dropping the light contribution for a complete frame. + if ( !localReceiverDrawn && vLight->localInteractions != NULL ) { + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_EQUAL ); + VK_Inter_SelectShadowMode( NULL, NULL ); + VK_DrawInteractionChain( vLight->localInteractions ); + } + if ( !globalOpaqueReceiverDrawn && vLight->globalInteractions != NULL ) { + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); + vkCmdSetDepthCompareOp( cmd, VK_COMPARE_OP_EQUAL ); + VK_Inter_SelectShadowMode( NULL, NULL ); + VK_DrawInteractionChain( vLight->globalInteractions ); + globalOpaqueReceiverDrawn = true; + } + if ( !r_skipTranslucent.GetBool() ) { const bool translucentUsesStencilFallback = translucentReceiverNeedsStencil && @@ -2282,10 +4084,15 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { const bool translucentUsesEmptyFallback = translucentReceiverNeedsFallback && globalEmptyFallback; + const bool translucentUsesUnshadowedFallback = + translucentReceiverNeedsStencil && + !translucentUsesStencilFallback && + !translucentUsesEmptyFallback; const bool drawTranslucentReceiver = !translucentReceiverNeedsStencil || translucentUsesStencilFallback || - translucentUsesEmptyFallback; + translucentUsesEmptyFallback || + translucentUsesUnshadowedFallback; // Translucent receivers keep the GLOBAL pass state, matching the // GL pair where the global opaque pass is the last mapped state. if ( translucentUsesStencilFallback ) { @@ -2295,6 +4102,9 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { translucentMapNeedsSupplement ? globalShadowState : NULL ); vkCmdSetStencilTestEnable( cmd, VK_TRUE ); + } else if ( translucentUsesUnshadowedFallback ) { + VK_Inter_SelectShadowMode( NULL, NULL ); + vkCmdSetStencilTestEnable( cmd, VK_FALSE ); } else if ( drawTranslucentReceiver && translucentReceiverNeedsShadow && globalShadowState != NULL ) { @@ -2331,6 +4141,16 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { interPass.drawCount, interPass.lightCount ); } + // Native Vulkan PBR is deliberately limited to opaque packed-ORM, + // tangent-space RGB-normal materials. Keep its admission observable + // without conflating those draws with the retail-compatible fallback path. + static bool loggedFirstNativePBRPass = false; + if ( !loggedFirstNativePBRPass && interPass.nativePBRDrawCount > 0 ) { + loggedFirstNativePBRPass = true; + common->Printf( "Vulkan: native packed PBR direct interactions active (%d draws)\n", + interPass.nativePBRDrawCount ); + } + // one-shot bring-up evidence that shadow-receiving interactions drew static bool loggedFirstShadowReceivers = false; if ( !loggedFirstShadowReceivers && interPass.shadowDrawCount > 0 ) { @@ -2362,6 +4182,1052 @@ void VK_Interactions_DrawLights( const viewDef_t *viewDef ) { } } +/* +=============================================================================== + + Shared fixed-classic fog/blend consumer + + The backend-neutral domain owns light/stage interpretation and publishes one + atomic, source-ordered phase. Vulkan retains every exact pipeline, + descriptor, uniform slice and geometry offset before the first attachment + write. A failed preflight rewinds both speculative rings and leaves the + established VK_Fog_DrawAllLights walker below completely untouched. + +=============================================================================== +*/ + +enum vkClassicFogBlendRejectDetail_t { + VK_CLASSIC_FOG_BLEND_REJECT_VIEW = 1, + VK_CLASSIC_FOG_BLEND_REJECT_COUNTS, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER, + VK_CLASSIC_FOG_BLEND_REJECT_OFFSCREEN_TARGET, + VK_CLASSIC_FOG_BLEND_REJECT_RENDER_SCOPE, + VK_CLASSIC_FOG_BLEND_REJECT_PIPELINE, + VK_CLASSIC_FOG_BLEND_REJECT_STATE, + VK_CLASSIC_FOG_BLEND_REJECT_SCISSOR, + VK_CLASSIC_FOG_BLEND_REJECT_GEOMETRY, + VK_CLASSIC_FOG_BLEND_REJECT_TEXTURE, + VK_CLASSIC_FOG_BLEND_REJECT_UNIFORM +}; + +typedef struct vkClassicFogBlendDrawPlan_s { + const classicFogBlendDomainPrimitive_t *primitive; + const classicFogBlendDomainLight_t *light; + const classicFogBlendDomainLightStage_t *stage; + VkPipeline pipeline; + VkDescriptorSet textureSets[ 2 ]; + VkRect2D scissor; + VkCullModeFlags cullMode; + VkCompareOp depthCompare; + int vertexOffset; + int indexOffset; + int uniformOffset; + vkInteractionPush_t push; + vkBlendLightBlock_t blendBlock; +} vkClassicFogBlendDrawPlan_t; + +typedef struct vkClassicFogBlendPreparedView_s { + const classicFogBlendDomainView_t *view; + const viewDef_t *viewDef; + VkCommandBuffer cmd; + VkPipelineLayout layout; + VkPipeline fogPipeline; + VkDescriptorSet uniformSet; + VkViewport viewport; + int frameSlot; + int framebufferWidth; + int framebufferHeight; + int drawPlanCount; + int noopPrimitiveCount; + int noopLightStageCount; + int noopLightCount; + int submittedFogReceivers; + int submittedFogFrustums; + int submittedBlendReceivers; + int uniformCheckpoint; + bool ready; + bool committed; + vkClassicFogBlendDrawPlan_t draws[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES ]; +} vkClassicFogBlendPreparedView_t; + +static vkClassicFogBlendPreparedView_t vkClassicFogBlendPrepared; + +static bool VK_ClassicFogBlend_Fail( const viewDef_t *viewDef, + classicFogBlendDomainFailure_t failure, int detail ) { + if ( vkClassicFogBlendPrepared.uniformCheckpoint >= 0 ) { + VK_Exec_InteractionUniformRestore( + vkClassicFogBlendPrepared.uniformCheckpoint ); + } + // Safe before the checkpoint exists and mandatory after any speculative + // upload. The classic walker may need the exact same ring capacity/memos. + VK_Exec_SharedInteractionGeometryRestore(); + R_ClassicFogBlendDomain_RecordBackendFallback( viewDef, + CLASSIC_FOG_BLEND_BACKEND_VULKAN, + failure == CLASSIC_FOG_BLEND_FAILURE_NONE + ? CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED : failure, + detail ); + memset( &vkClassicFogBlendPrepared, 0, + sizeof( vkClassicFogBlendPrepared ) ); + vkClassicFogBlendPrepared.uniformCheckpoint = -1; + return false; +} + +static bool VK_ClassicFogBlend_FloatsFinite( const float *values, + int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !std::isfinite( values[ i ] ) ) { + return false; + } + } + return true; +} + +static bool VK_ClassicFogBlend_MapSourceBlend( + rendererBlendFactor_t factor, int &bits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: bits = GLS_SRCBLEND_ZERO; return true; + case RENDERER_BLEND_ONE: bits = GLS_SRCBLEND_ONE; return true; + case RENDERER_BLEND_SRC_COLOR: bits = GLS_SRCBLEND_SRC_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: bits = GLS_SRCBLEND_ONE_MINUS_SRC_COLOR; return true; + case RENDERER_BLEND_DST_COLOR: bits = GLS_SRCBLEND_DST_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_DST_COLOR: bits = GLS_SRCBLEND_ONE_MINUS_DST_COLOR; return true; + case RENDERER_BLEND_SRC_ALPHA: bits = GLS_SRCBLEND_SRC_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: bits = GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA; return true; + case RENDERER_BLEND_DST_ALPHA: bits = GLS_SRCBLEND_DST_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: bits = GLS_SRCBLEND_ONE_MINUS_DST_ALPHA; return true; + case RENDERER_BLEND_SRC_ALPHA_SATURATE: bits = GLS_SRCBLEND_ALPHA_SATURATE; return true; + default: return false; + } +} + +static bool VK_ClassicFogBlend_MapDestinationBlend( + rendererBlendFactor_t factor, int &bits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: bits = GLS_DSTBLEND_ZERO; return true; + case RENDERER_BLEND_ONE: bits = GLS_DSTBLEND_ONE; return true; + case RENDERER_BLEND_SRC_COLOR: bits = GLS_DSTBLEND_SRC_COLOR; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: bits = GLS_DSTBLEND_ONE_MINUS_SRC_COLOR; return true; + case RENDERER_BLEND_SRC_ALPHA: bits = GLS_DSTBLEND_SRC_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: bits = GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA; return true; + case RENDERER_BLEND_DST_ALPHA: bits = GLS_DSTBLEND_DST_ALPHA; return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: bits = GLS_DSTBLEND_ONE_MINUS_DST_ALPHA; return true; + default: return false; + } +} + +static bool VK_ClassicFogBlend_MapCull( rendererCullMode_t cull, + VkCullModeFlags &mode ) { + switch ( cull ) { + case RENDERER_CULL_NONE: mode = VK_CULL_MODE_NONE; return true; + case RENDERER_CULL_FRONT: mode = VK_CULL_MODE_FRONT_BIT; return true; + case RENDERER_CULL_BACK: mode = VK_CULL_MODE_BACK_BIT; return true; + default: return false; + } +} + +static bool VK_ClassicFogBlend_MapStageState( + const classicFogBlendDomainLightStage_t &stage, + const classicFogBlendDomainPrimitive_t &primitive, + int &pipelineBits, VkCompareOp &depthCompare, + VkCullModeFlags &cullMode ) { + pipelineBits = 0; + if ( stage.blend.colorOperation != RENDERER_BLEND_OP_ADD + || stage.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || stage.blend.sourceAlpha != stage.blend.sourceColor + || stage.blend.destinationAlpha + != stage.blend.destinationColor + || !stage.depth.testEnabled || stage.depth.writeEnabled + || stage.depth.compareOperation != RENDERER_COMPARE_EQUAL + || !primitive.depth.testEnabled || primitive.depth.writeEnabled + || stage.alphaTestEnabled + || ( stage.colorWriteMask + & ~static_cast( + RENDERER_COLOR_WRITE_RGBA ) ) != 0 ) { + return false; + } + const bool replacementBlend = + stage.blend.sourceColor == RENDERER_BLEND_ONE + && stage.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( stage.blend.enabled == replacementBlend ) { + return false; + } + + int sourceBits = 0; + int destinationBits = 0; + if ( !VK_ClassicFogBlend_MapSourceBlend( + stage.blend.sourceColor, sourceBits ) + || !VK_ClassicFogBlend_MapDestinationBlend( + stage.blend.destinationColor, destinationBits ) ) { + return false; + } + pipelineBits = sourceBits | destinationBits; + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + pipelineBits |= GLS_REDMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + pipelineBits |= GLS_GREENMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + pipelineBits |= GLS_BLUEMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + pipelineBits |= GLS_ALPHAMASK; + } + + if ( primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER + || primitive.kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP ) { + if ( stage.blend.sourceColor != RENDERER_BLEND_SRC_ALPHA + || stage.blend.destinationColor + != RENDERER_BLEND_ONE_MINUS_SRC_ALPHA + || !stage.blend.enabled + || stage.colorWriteMask != RENDERER_COLOR_WRITE_RGBA + || stage.cull != RENDERER_CULL_FRONT ) { + return false; + } + const bool cap = primitive.kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP; + if ( primitive.depth.compareOperation + != ( cap ? RENDERER_COMPARE_LESS_OR_EQUAL + : RENDERER_COMPARE_EQUAL ) + || primitive.cull + != ( cap ? RENDERER_CULL_BACK + : RENDERER_CULL_FRONT ) ) { + return false; + } + depthCompare = cap + ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_EQUAL; + return VK_ClassicFogBlend_MapCull( primitive.cull, cullMode ); + } + + if ( primitive.kind != CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + return false; + } + if ( primitive.depth.compareOperation != RENDERER_COMPARE_EQUAL + || primitive.cull != stage.cull ) { + return false; + } + depthCompare = VK_COMPARE_OP_EQUAL; + return VK_ClassicFogBlend_MapCull( primitive.cull, cullMode ); +} + +static bool VK_ClassicFogBlend_BuildScissor( + const classicFogBlendDomainView_t &view, + const classicFogBlendDomainPrimitive_t &primitive, + int framebufferWidth, int framebufferHeight, VkRect2D &scissor ) { + const int viewportWidth = view.viewportX2 - view.viewportX1 + 1; + const int viewportHeight = view.viewportY2 - view.viewportY1 + 1; + const int requestedX1 = view.useScissor + ? primitive.scissorX1 : view.scissorX1; + const int requestedY1 = view.useScissor + ? primitive.scissorY1 : view.scissorY1; + const int requestedX2 = view.useScissor + ? primitive.scissorX2 : view.scissorX2; + const int requestedY2 = view.useScissor + ? primitive.scissorY2 : view.scissorY2; + if ( viewportWidth <= 0 || viewportHeight <= 0 + || framebufferWidth <= 0 || framebufferHeight <= 0 + || requestedX2 < requestedX1 + || requestedY2 < requestedY1 ) { + return false; + } + + int x0 = Max( view.viewportX1, view.viewportX1 + requestedX1 ); + int x1 = Min( view.viewportX1 + viewportWidth, + view.viewportX1 + requestedX2 + 1 ); + int y0GL = Max( view.viewportY1, view.viewportY1 + requestedY1 ); + int y1GL = Min( view.viewportY1 + viewportHeight, + view.viewportY1 + requestedY2 + 1 ); + x0 = Max( 0, x0 ); + x1 = Min( framebufferWidth, x1 ); + y0GL = Max( 0, y0GL ); + y1GL = Min( framebufferHeight, y1GL ); + if ( x1 <= x0 || y1GL <= y0GL ) { + return false; + } + scissor.offset.x = x0; + scissor.offset.y = framebufferHeight - y1GL; + scissor.extent.width = static_cast( x1 - x0 ); + scissor.extent.height = static_cast( y1GL - y0GL ); + return true; +} + +static bool VK_ClassicFogBlend_ResolveDescriptor( + std::uint64_t resourceId, VkDescriptorSet &descriptor ) { + descriptor = VK_NULL_HANDLE; + const classicFogBlendDomainTexture_t *texture = + R_ClassicFogBlendDomain_ResolveTexture( resourceId ); + if ( texture == NULL || texture->textureResourceId != resourceId + || texture->image == NULL || !texture->loaded + || texture->defaulted || texture->mutableImage + || texture->textureHandle == 0 + || !texture->image->IsLoaded() + || texture->image->IsDefaulted() + || texture->textureHandle + != const_cast( texture->image )->GetDeviceHandle() + || texture->filter != texture->image->GetFilter() + || texture->repeat != texture->image->GetRepeat() + || texture->storageGeneration + != texture->image->GetStorageGeneration() ) { + return false; + } + descriptor = VK_Exec_ImageDescriptor( texture->textureHandle, true ); + return descriptor != VK_NULL_HANDLE; +} + +static bool VK_ClassicFogBlend_ValidateGeometry( + const classicFogBlendDomainPrimitive_t &primitive ) { + const srfTriangles_t *tri = primitive.legacyGeometry; + if ( primitive.disposition != CLASSIC_FOG_BLEND_PRIMITIVE_DRAW + || tri == NULL || R_TriHasPrimBatchMesh( tri ) + || primitive.vertexCount <= 0 || primitive.indexCount <= 0 + || primitive.indexCount % 3 != 0 + || primitive.firstIndex != 0 || primitive.vertexOffset != 0 + || primitive.vertexCount + > INT_MAX / static_cast( sizeof( idDrawVert ) ) + || primitive.indexCount + > INT_MAX / static_cast( sizeof( glIndex_t ) ) + || tri->numVerts != primitive.vertexCount + || tri->numIndexes != primitive.indexCount + || tri->ambientCache == NULL + || ( tri->indexes == NULL && tri->indexCache == NULL ) + || !VK_ClassicFogBlend_FloatsFinite( + primitive.modelMatrix, 16 ) + || !VK_ClassicFogBlend_FloatsFinite( + primitive.modelViewMatrix, 16 ) + || !VK_ClassicFogBlend_FloatsFinite( + &primitive.localLightProject[ 0 ][ 0 ], 16 ) + || !VK_ClassicFogBlend_FloatsFinite( + &primitive.fogTexgen[ 0 ][ 0 ][ 0 ], 16 ) ) { + return false; + } + if ( primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP ) { + return primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM + && ( primitive.legacyDrawSurf == NULL + || primitive.legacyDrawSurf->geo == tri ); + } + return primitive.legacyDrawSurf != NULL + && primitive.legacyDrawSurf->geo == tri + && ( primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + || primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_LOCAL ); +} + +static void VK_ClassicFogBlend_BuildPayloads( + const classicFogBlendDomainView_t &view, + const classicFogBlendDomainPrimitive_t &primitive, + const classicFogBlendDomainLight_t &light, + const classicFogBlendDomainLightStage_t &stage, + vkClassicFogBlendDrawPlan_t &plan ) { + memset( &plan.push, 0, sizeof( plan.push ) ); + float mvpGL[ 16 ]; + myGlMultMatrix( primitive.modelViewMatrix, view.projectionMatrix, mvpGL ); + VK_FixupClipSpaceZ( plan.push.mvp, mvpGL ); + + memset( &plan.blendBlock, 0, sizeof( plan.blendBlock ) ); + if ( primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + for ( int component = 0; component < 4; ++component ) { + // The front end sealed RB_GetShaderTextureMatrix's wrapped 2x4 + // rows. Fold them into S/T exactly as + // RB_BakeTextureMatrixIntoTexgen does, with Q carrying translation. + plan.blendBlock.lightProjectS[ component ] = + stage.textureMatrix[ 0 ][ 0 ] + * primitive.localLightProject[ 0 ][ component ] + + stage.textureMatrix[ 0 ][ 1 ] + * primitive.localLightProject[ 1 ][ component ] + + stage.textureMatrix[ 0 ][ 3 ] + * primitive.localLightProject[ 2 ][ component ]; + plan.blendBlock.lightProjectT[ component ] = + stage.textureMatrix[ 1 ][ 0 ] + * primitive.localLightProject[ 0 ][ component ] + + stage.textureMatrix[ 1 ][ 1 ] + * primitive.localLightProject[ 1 ][ component ] + + stage.textureMatrix[ 1 ][ 3 ] + * primitive.localLightProject[ 2 ][ component ]; + } + memcpy( plan.blendBlock.lightProjectQ, + primitive.localLightProject[ 2 ], + sizeof( plan.blendBlock.lightProjectQ ) ); + memcpy( plan.blendBlock.lightFalloffS, + primitive.localLightProject[ 3 ], + sizeof( plan.blendBlock.lightFalloffS ) ); + memcpy( plan.blendBlock.color, stage.color, + sizeof( plan.blendBlock.color ) ); + } else { + // fog.vert consumes these exact localized planes from the shared push: + // texture 0 S, texture 1 T, texture 1 S, then fog RGBA. + memcpy( plan.push.a, primitive.fogTexgen[ 0 ][ 0 ], + sizeof( plan.push.a ) ); + memcpy( plan.push.b, primitive.fogTexgen[ 1 ][ 1 ], + sizeof( plan.push.b ) ); + memcpy( plan.push.c, primitive.fogTexgen[ 1 ][ 0 ], + sizeof( plan.push.c ) ); + memcpy( plan.push.d, light.fogColor, + sizeof( plan.push.d ) ); + } +} + +static bool VK_ClassicFogBlend_ValidateRanges( + const viewDef_t *viewDef, + const classicFogBlendDomainView_t &view ) { + int expectedSurface = view.firstSurface; + int expectedStage = view.firstLightStage; + int expectedPrimitive = view.firstPrimitive; + int fogLights = 0; + int blendLights = 0; + int noopLights = 0; + int activeStages = 0; + int inactiveStages = 0; + int noopStages = 0; + int drawablePrimitives = 0; + int noopPrimitives = 0; + int fogReceivers = 0; + int fogFrustums = 0; + int blendReceivers = 0; + int previousSourceOrdinal = -1; + + for ( int lightIndex = 0; lightIndex < view.lightCount; ++lightIndex ) { + const classicFogBlendDomainLight_t *light = + R_ClassicFogBlendDomain_ViewLight( view, lightIndex ); + if ( light == NULL || light->sourceOrdinal <= previousSourceOrdinal + || light->firstSurface != expectedSurface + || light->firstLightStage != expectedStage + || light->firstPrimitive != expectedPrimitive + || light->surfaceCount < 0 || light->lightStageCount < 0 + || light->primitiveCount < 0 + || light->activeLightStageCount < 0 + || light->inactiveLightStageCount < 0 + || light->activeLightStageCount + + light->inactiveLightStageCount + != light->lightStageCount + || light->drawablePrimitiveCount < 0 + || light->noopPrimitiveCount < 0 + || light->drawablePrimitiveCount + + light->noopPrimitiveCount != light->primitiveCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + previousSourceOrdinal = light->sourceOrdinal; + expectedSurface += light->surfaceCount; + expectedStage += light->lightStageCount; + expectedPrimitive += light->primitiveCount; + if ( light->kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + fogLights++; + } else if ( light->kind == CLASSIC_FOG_BLEND_LIGHT_BLEND ) { + blendLights++; + } else { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + if ( light->disposition != CLASSIC_FOG_BLEND_LIGHT_DRAW ) { + if ( light->disposition <= CLASSIC_FOG_BLEND_LIGHT_DRAW + || light->disposition + >= CLASSIC_FOG_BLEND_LIGHT_DISPOSITION_COUNT ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + noopLights++; + } + + int expectedStagePrimitive = light->firstPrimitive; + int previousSourceStage = -1; + for ( int stageIndex = 0; stageIndex < light->lightStageCount; + ++stageIndex ) { + const classicFogBlendDomainLightStage_t *stage = + R_ClassicFogBlendDomain_LightStage( *light, stageIndex ); + if ( stage == NULL + || stage->lightIndex != view.firstLight + lightIndex + || stage->sourceStageIndex <= previousSourceStage + || stage->firstPrimitive != expectedStagePrimitive + || stage->primitiveCount < 0 + || stage->drawablePrimitiveCount < 0 + || stage->noopPrimitiveCount < 0 + || stage->drawablePrimitiveCount + + stage->noopPrimitiveCount + != stage->primitiveCount + || !VK_ClassicFogBlend_FloatsFinite( + stage->color, 4 ) + || !VK_ClassicFogBlend_FloatsFinite( + &stage->textureMatrix[ 0 ][ 0 ], 8 ) + || !std::isfinite( stage->condition ) + || !std::isfinite( stage->alphaTestValue ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + previousSourceStage = stage->sourceStageIndex; + int previousReceiver = -1; + for ( int stagePrimitiveIndex = 0; + stagePrimitiveIndex < stage->primitiveCount; + ++stagePrimitiveIndex ) { + const int absolutePrimitive = stage->firstPrimitive + + stagePrimitiveIndex; + const classicFogBlendDomainPrimitive_t *primitive = + R_ClassicFogBlendDomain_ViewPrimitive( view, + absolutePrimitive - view.firstPrimitive ); + if ( primitive == NULL + || primitive->lightIndex + != view.firstLight + lightIndex + || primitive->lightStageIndex + != light->firstLightStage + stageIndex + || ( primitive->kind + != CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP + && primitive->surfaceIndex + != light->firstSurface + + stagePrimitiveIndex ) + || primitive->receiver < previousReceiver + || ( stage->disposition + == CLASSIC_FOG_BLEND_STAGE_DRAW + && primitive->disposition + != CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) + || ( stage->disposition + != CLASSIC_FOG_BLEND_STAGE_DRAW + && primitive->disposition + == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + previousReceiver = primitive->receiver; + if ( light->kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + const bool cap = primitive->kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP; + if ( ( !cap && primitive->kind + != CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER ) + || cap != ( primitive->receiver + == CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM ) + || ( cap && stagePrimitiveIndex + != stage->primitiveCount - 1 ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + } else if ( primitive->kind + != CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER + || primitive->receiver + == CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + } + expectedStagePrimitive += stage->primitiveCount; + if ( stage->disposition == CLASSIC_FOG_BLEND_STAGE_DRAW ) { + activeStages++; + } else { + if ( stage->disposition <= CLASSIC_FOG_BLEND_STAGE_DRAW + || stage->disposition + >= CLASSIC_FOG_BLEND_STAGE_DISPOSITION_COUNT ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + noopStages++; + if ( stage->disposition + == CLASSIC_FOG_BLEND_STAGE_NOOP_INACTIVE_CONDITION ) { + inactiveStages++; + } else { + activeStages++; + } + } + } + if ( expectedStagePrimitive + != light->firstPrimitive + light->primitiveCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + } + + for ( int primitiveIndex = 0; primitiveIndex < view.primitiveCount; + ++primitiveIndex ) { + const classicFogBlendDomainPrimitive_t *primitive = + R_ClassicFogBlendDomain_ViewPrimitive( view, primitiveIndex ); + if ( primitive == NULL + || primitive->lightIndex < view.firstLight + || primitive->lightIndex + >= view.firstLight + view.lightCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + const int localLightIndex = primitive->lightIndex - view.firstLight; + const classicFogBlendDomainLight_t *light = + R_ClassicFogBlendDomain_ViewLight( view, localLightIndex ); + if ( light == NULL + || primitive->lightStageIndex < light->firstLightStage + || primitive->lightStageIndex + >= light->firstLightStage + light->lightStageCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + const int localStageIndex = primitive->lightStageIndex + - light->firstLightStage; + const classicFogBlendDomainLightStage_t *stage = + R_ClassicFogBlendDomain_LightStage( *light, localStageIndex ); + if ( stage == NULL + || ( primitive->kind + != CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP + && ( primitive->surfaceIndex < view.firstSurface + || primitive->surfaceIndex + >= view.firstSurface + view.surfaceCount ) ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_ORDER ); + } + if ( primitive->disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + drawablePrimitives++; + } else { + if ( primitive->disposition <= CLASSIC_FOG_BLEND_PRIMITIVE_DRAW + || primitive->disposition + >= CLASSIC_FOG_BLEND_PRIMITIVE_DISPOSITION_COUNT ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + noopPrimitives++; + } + switch ( primitive->kind ) { + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER: + if ( primitive->disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + fogReceivers++; + } + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP: + if ( primitive->disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + fogFrustums++; + } + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER: + if ( primitive->disposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + blendReceivers++; + } + break; + default: + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + } + + if ( expectedSurface != view.firstSurface + view.surfaceCount + || expectedStage != view.firstLightStage + view.lightStageCount + || expectedPrimitive != view.firstPrimitive + view.primitiveCount + || fogLights != view.fogLightCount + || blendLights != view.blendLightCount + || noopLights != view.noopLightCount + || activeStages != view.activeLightStageCount + || inactiveStages != view.inactiveLightStageCount + || noopStages != view.noopLightStageCount + || drawablePrimitives != view.drawablePrimitiveCount + || noopPrimitives != view.noopPrimitiveCount + || fogReceivers != view.fogReceiverPrimitiveCount + || fogFrustums != view.fogFrustumPrimitiveCount + || blendReceivers != view.blendPrimitiveCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_COUNTS ); + } + return true; +} + +bool VK_ClassicFogBlend_Preflight( const viewDef_t *viewDef ) { + memset( &vkClassicFogBlendPrepared, 0, + sizeof( vkClassicFogBlendPrepared ) ); + vkClassicFogBlendPreparedView_t &prepared = vkClassicFogBlendPrepared; + prepared.uniformCheckpoint = -1; + const classicFogBlendDomainView_t *view = + R_ClassicFogBlendDomain_FindView( viewDef ); + prepared.view = view; + prepared.viewDef = viewDef; + if ( view == NULL ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_VIEW ); + } + if ( view->backendOutcome[ CLASSIC_FOG_BLEND_BACKEND_VULKAN ] + == CLASSIC_FOG_BLEND_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + return VK_ClassicFogBlend_Fail( viewDef, + view->failure != CLASSIC_FOG_BLEND_FAILURE_NONE + ? view->failure + : CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + if ( r_skipFogLights.GetBool() || r_showOverDraw.GetInteger() != 0 + || r_singleTriangle.GetBool() || r_skipRender.GetBool() + || r_skipRenderContext.GetBool() + || view->skipBlendLights != r_skipBlendLights.GetBool() + || view->useScissor != r_useScissor.GetBool() ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + if ( viewDef == NULL || view->viewDef != viewDef + || view->lightCount < 0 + || view->lightCount > CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHTS + || view->surfaceCount < 0 + || view->surfaceCount > CLASSIC_FOG_BLEND_DOMAIN_MAX_SURFACES + || view->lightStageCount < 0 + || view->lightStageCount + > CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES + || view->primitiveCount < 0 + || view->primitiveCount + > CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES + || view->drawablePrimitiveCount < 0 + || view->noopPrimitiveCount < 0 + || view->drawablePrimitiveCount + view->noopPrimitiveCount + != view->primitiveCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_COUNTS ); + } + if ( backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL + || !VK_Exec_SharedInteractionTargetReady() ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_OFFSCREEN_TARGET ); + } + + prepared.cmd = VK_Exec_ActiveCmd(); + prepared.frameSlot = VK_Exec_ActiveFrameSlot(); + prepared.framebufferWidth = VK_Exec_ActiveFramebufferWidth(); + prepared.framebufferHeight = VK_Exec_ActiveFramebufferHeight(); + prepared.layout = view->drawablePrimitiveCount > 0 + ? VK_Exec_FogBlendPipelineLayout() : VK_NULL_HANDLE; + prepared.fogPipeline = view->fogReceiverPrimitiveCount > 0 + || view->fogFrustumPrimitiveCount > 0 + ? VK_Exec_FogPipeline() : VK_NULL_HANDLE; + prepared.uniformSet = view->blendPrimitiveCount > 0 + ? VK_Exec_InteractionUniformSet() : VK_NULL_HANDLE; + if ( prepared.cmd == VK_NULL_HANDLE || prepared.frameSlot < 0 + || prepared.framebufferWidth <= 0 + || prepared.framebufferHeight <= 0 ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_RENDER_SCOPE ); + } + if ( ( view->drawablePrimitiveCount > 0 + && prepared.layout == VK_NULL_HANDLE ) + || ( ( view->fogReceiverPrimitiveCount > 0 + || view->fogFrustumPrimitiveCount > 0 ) + && prepared.fogPipeline == VK_NULL_HANDLE ) + || ( view->blendPrimitiveCount > 0 + && prepared.uniformSet == VK_NULL_HANDLE ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_PIPELINE ); + } + + const int viewportWidth = view->viewportX2 - view->viewportX1 + 1; + const int viewportHeight = view->viewportY2 - view->viewportY1 + 1; + if ( viewportWidth <= 0 || viewportHeight <= 0 + || view->viewportX1 < 0 || view->viewportY1 < 0 + || view->viewportX1 != viewDef->viewport.x1 + || view->viewportY1 != viewDef->viewport.y1 + || view->viewportX2 != viewDef->viewport.x2 + || view->viewportY2 != viewDef->viewport.y2 + || view->scissorX1 != viewDef->scissor.x1 + || view->scissorY1 != viewDef->scissor.y1 + || view->scissorX2 != viewDef->scissor.x2 + || view->scissorY2 != viewDef->scissor.y2 + || view->viewportX1 + viewportWidth + > prepared.framebufferWidth + || view->viewportY1 + viewportHeight + > prepared.framebufferHeight + || !VK_ClassicFogBlend_FloatsFinite( + view->projectionMatrix, 16 ) + || memcmp( view->projectionMatrix, viewDef->projectionMatrix, + sizeof( view->projectionMatrix ) ) != 0 ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_VIEW ); + } + prepared.viewport.x = static_cast( view->viewportX1 ); + prepared.viewport.y = static_cast( + prepared.framebufferHeight - view->viewportY1 ); + prepared.viewport.width = static_cast( viewportWidth ); + prepared.viewport.height = -static_cast( viewportHeight ); + prepared.viewport.minDepth = 0.0f; + prepared.viewport.maxDepth = 1.0f; + + if ( !VK_ClassicFogBlend_ValidateRanges( viewDef, *view ) ) { + return false; + } + prepared.noopPrimitiveCount = view->noopPrimitiveCount; + prepared.noopLightStageCount = view->noopLightStageCount; + prepared.noopLightCount = view->noopLightCount; + + if ( !VK_Exec_SharedInteractionGeometryCheckpoint() ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_GEOMETRY ); + } + + for ( int primitiveIndex = 0; primitiveIndex < view->primitiveCount; + ++primitiveIndex ) { + const classicFogBlendDomainPrimitive_t *primitive = + R_ClassicFogBlendDomain_ViewPrimitive( *view, primitiveIndex ); + if ( primitive == NULL ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + primitiveIndex ); + } + if ( primitive->disposition != CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + continue; + } + if ( prepared.drawPlanCount + >= CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_COUNTS ); + } + + const int localLightIndex = primitive->lightIndex - view->firstLight; + const classicFogBlendDomainLight_t *light = + R_ClassicFogBlendDomain_ViewLight( *view, localLightIndex ); + const int localStageIndex = light != NULL + ? primitive->lightStageIndex - light->firstLightStage : -1; + const classicFogBlendDomainLightStage_t *stage = light != NULL + ? R_ClassicFogBlendDomain_LightStage( + *light, localStageIndex ) : NULL; + if ( light == NULL || stage == NULL + || stage->disposition != CLASSIC_FOG_BLEND_STAGE_DRAW + || light->disposition != CLASSIC_FOG_BLEND_LIGHT_DRAW + || !VK_ClassicFogBlend_ValidateGeometry( *primitive ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_GEOMETRY ); + } + + vkClassicFogBlendDrawPlan_t &plan = + prepared.draws[ prepared.drawPlanCount ]; + memset( &plan, 0, sizeof( plan ) ); + plan.primitive = primitive; + plan.light = light; + plan.stage = stage; + plan.vertexOffset = -1; + plan.indexOffset = -1; + plan.uniformOffset = -1; + int pipelineBits = 0; + if ( !VK_ClassicFogBlend_MapStageState( *stage, *primitive, + pipelineBits, plan.depthCompare, plan.cullMode ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + plan.pipeline = primitive->kind + == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER + ? VK_Exec_BlendLightPipeline( pipelineBits ) + : prepared.fogPipeline; + if ( plan.pipeline == VK_NULL_HANDLE ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_PIPELINE ); + } + if ( !VK_ClassicFogBlend_BuildScissor( *view, *primitive, + prepared.framebufferWidth, prepared.framebufferHeight, + plan.scissor ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_SCISSOR ); + } + if ( !VK_Exec_PrepareTriGeometry( prepared.cmd, + prepared.frameSlot, primitive->legacyGeometry, + plan.vertexOffset, plan.indexOffset ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_GEOMETRY ); + } + + if ( primitive->kind + == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + if ( stage->projectionTextureResourceId == 0 + || stage->falloffTextureResourceId == 0 + || stage->falloffTextureResourceId + != light->falloffTextureResourceId + || !VK_ClassicFogBlend_ResolveDescriptor( + stage->projectionTextureResourceId, + plan.textureSets[ 0 ] ) + || !VK_ClassicFogBlend_ResolveDescriptor( + stage->falloffTextureResourceId, + plan.textureSets[ 1 ] ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_TEXTURE ); + } + } else { + if ( stage->fogTextureResourceId == 0 + || stage->fogEnterTextureResourceId == 0 + || stage->fogTextureResourceId + != light->fogTextureResourceId + || stage->fogEnterTextureResourceId + != light->fogEnterTextureResourceId + || !VK_ClassicFogBlend_ResolveDescriptor( + stage->fogTextureResourceId, + plan.textureSets[ 0 ] ) + || !VK_ClassicFogBlend_ResolveDescriptor( + stage->fogEnterTextureResourceId, + plan.textureSets[ 1 ] ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_TEXTURE ); + } + } + VK_ClassicFogBlend_BuildPayloads( + *view, *primitive, *light, *stage, plan ); + if ( !VK_ClassicFogBlend_FloatsFinite( + &plan.push.mvp[ 0 ], 32 ) + || ( primitive->kind + == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER + && !VK_ClassicFogBlend_FloatsFinite( + &plan.blendBlock.lightProjectS[ 0 ], 20 ) ) ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_STATE ); + } + prepared.drawPlanCount++; + } + + if ( prepared.drawPlanCount != view->drawablePrimitiveCount ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_COVERAGE_MISMATCH, + VK_CLASSIC_FOG_BLEND_REJECT_COUNTS ); + } + + // Reserve the complete per-primitive blend stream only after every other + // plan field is known. Failure restores this cursor and all geometry. + prepared.uniformCheckpoint = VK_Exec_InteractionUniformCheckpoint(); + if ( prepared.uniformCheckpoint < 0 ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + VK_CLASSIC_FOG_BLEND_REJECT_UNIFORM ); + } + for ( int drawIndex = 0; drawIndex < prepared.drawPlanCount; ++drawIndex ) { + vkClassicFogBlendDrawPlan_t &plan = prepared.draws[ drawIndex ]; + if ( plan.primitive->kind + != CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + continue; + } + plan.uniformOffset = VK_Exec_InteractionUniformAlloc( + &plan.blendBlock, sizeof( plan.blendBlock ) ); + if ( plan.uniformOffset < 0 ) { + return VK_ClassicFogBlend_Fail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, + VK_CLASSIC_FOG_BLEND_REJECT_UNIFORM ); + } + } + + VK_Exec_SharedInteractionGeometryCommit(); + // The retained cursor is the uniform transaction's commit. + prepared.uniformCheckpoint = -1; + prepared.ready = true; + return true; +} + +void VK_ClassicFogBlend_DrawOwnedView( const viewDef_t *viewDef ) { + vkClassicFogBlendPreparedView_t &prepared = vkClassicFogBlendPrepared; + if ( !prepared.ready || prepared.committed || prepared.view == NULL + || prepared.viewDef == NULL || prepared.viewDef != viewDef + || prepared.cmd == VK_NULL_HANDLE ) { + return; + } + prepared.committed = true; + if ( prepared.drawPlanCount > 0 ) { + vkCmdSetViewport( prepared.cmd, 0, 1, &prepared.viewport ); + vkCmdSetDepthTestEnable( prepared.cmd, VK_TRUE ); + vkCmdSetDepthWriteEnable( prepared.cmd, VK_FALSE ); + vkCmdSetDepthBiasEnable( prepared.cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( prepared.cmd, VK_FALSE ); + vkCmdSetFrontFace( prepared.cmd, + VK_FRONT_FACE_COUNTER_CLOCKWISE ); + } + + for ( int drawIndex = 0; drawIndex < prepared.drawPlanCount; ++drawIndex ) { + const vkClassicFogBlendDrawPlan_t &plan = prepared.draws[ drawIndex ]; + VK_Exec_BindPreparedTriGeometry( prepared.cmd, prepared.frameSlot, + plan.vertexOffset, plan.indexOffset ); + vkCmdSetScissor( prepared.cmd, 0, 1, &plan.scissor ); + vkCmdSetDepthCompareOp( prepared.cmd, plan.depthCompare ); + vkCmdSetCullMode( prepared.cmd, plan.cullMode ); + vkCmdBindPipeline( prepared.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + plan.pipeline ); + vkCmdBindDescriptorSets( prepared.cmd, + VK_PIPELINE_BIND_POINT_GRAPHICS, prepared.layout, 0, 2, + plan.textureSets, 0, NULL ); + if ( plan.primitive->kind + == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + const uint32_t dynamicOffset = + static_cast( plan.uniformOffset ); + vkCmdBindDescriptorSets( prepared.cmd, + VK_PIPELINE_BIND_POINT_GRAPHICS, prepared.layout, 2, 1, + &prepared.uniformSet, 1, &dynamicOffset ); + } + vkCmdPushConstants( prepared.cmd, prepared.layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( plan.push ), &plan.push ); + vkCmdDrawIndexed( prepared.cmd, + static_cast( plan.primitive->indexCount ), + 1, 0, 0, 0 ); + switch ( plan.primitive->kind ) { + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER: + prepared.submittedFogReceivers++; + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP: + prepared.submittedFogFrustums++; + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER: + prepared.submittedBlendReceivers++; + break; + default: + break; + } + } + + if ( prepared.drawPlanCount > 0 ) { + vkCmdSetDepthBiasEnable( prepared.cmd, VK_FALSE ); + vkCmdSetStencilTestEnable( prepared.cmd, VK_FALSE ); + vkCmdSetCullMode( prepared.cmd, VK_CULL_MODE_FRONT_BIT ); + } + const bool coverageAccepted = R_ClassicFogBlendDomain_RecordOwned( + viewDef, CLASSIC_FOG_BLEND_BACKEND_VULKAN, + prepared.submittedFogReceivers, + prepared.submittedFogFrustums, + prepared.submittedBlendReceivers, + prepared.noopPrimitiveCount, + prepared.noopLightStageCount, + prepared.noopLightCount ); + if ( !coverageAccepted ) { + common->Warning( + "Vulkan: shared fog/blend backend coverage mismatch after commit" ); + } + + static bool loggedFirstOwnedView = false; + if ( !loggedFirstOwnedView && coverageAccepted + && prepared.drawPlanCount > 0 ) { + loggedFirstOwnedView = true; + common->Printf( + "Vulkan: shared fog/blend owned %d fog receivers, %d caps, %d blend receivers, %d primitive noops, %d stage noops, and %d light noops (hash=%016llx)\n", + prepared.submittedFogReceivers, + prepared.submittedFogFrustums, + prepared.submittedBlendReceivers, + prepared.noopPrimitiveCount, + prepared.noopLightStageCount, + prepared.noopLightCount, + static_cast( prepared.view->hash ) ); + } +} + /* =============================================================================== @@ -2741,6 +5607,18 @@ static void VK_BlendLight( const drawSurf_t *drawSurfs, const drawSurf_t *drawSu } } +// Named rollback seams used by the shared transaction: preflight decides the +// whole view before either helper can issue established fog/blend work. +static void VK_Fog_DrawFogLight( const drawSurf_t *globalSurfs, + const drawSurf_t *localSurfs ) { + VK_FogPass( globalSurfs, localSurfs ); +} + +static void VK_Fog_DrawBlendLight( const drawSurf_t *globalSurfs, + const drawSurf_t *localSurfs ) { + VK_BlendLight( globalSurfs, localSurfs ); +} + /* ==================== VK_Fog_DrawAllLights @@ -2832,10 +5710,12 @@ void VK_Fog_DrawAllLights( const viewDef_t *viewDef ) { continue; } interPass.fogLightCount++; - VK_FogPass( vLight->globalInteractions, vLight->localInteractions ); + VK_Fog_DrawFogLight( + vLight->globalInteractions, vLight->localInteractions ); } else if ( vLight->lightShader->IsBlendLight() ) { interPass.blendLightCount++; - VK_BlendLight( vLight->globalInteractions, vLight->localInteractions ); + VK_Fog_DrawBlendLight( + vLight->globalInteractions, vLight->localInteractions ); } } diff --git a/src/renderer/Vulkan/vk_ShadowMap.cpp b/src/renderer/Vulkan/vk_ShadowMap.cpp index 462b1da4..71fcb439 100644 --- a/src/renderer/Vulkan/vk_ShadowMap.cpp +++ b/src/renderer/Vulkan/vk_ShadowMap.cpp @@ -65,6 +65,7 @@ #include "../tr_local.h" #include "../RenderWorld_local.h" #include "../ShadowMapProjected.h" +#include "../ClassicInteractionDomain.h" #undef snprintf #undef vsnprintf @@ -78,6 +79,7 @@ #define UINT_MAX 0xffffffffu #endif #include +#include #include "volk.h" #include "vk_mem_alloc.h" @@ -88,6 +90,10 @@ VkCommandBuffer VK_Exec_ActiveCmd( void ); int VK_Exec_ActiveFrameSlot( void ); bool VK_Exec_BindTriGeometry( VkCommandBuffer cmd, int slot, const srfTriangles_t *tri ); +bool VK_Exec_PrepareTriGeometry( VkCommandBuffer cmd, int slot, + const srfTriangles_t *tri, int &vertexOffset, int &indexOffset ); +void VK_Exec_BindPreparedTriGeometry( VkCommandBuffer cmd, int slot, + int vertexOffset, int indexOffset ); VkDescriptorSet VK_Exec_ImageDescriptor( unsigned int texnum, bool require2D ); VkPipeline VK_Exec_CasterPipeline( void ); VkPipeline VK_Exec_PointCasterPipeline( void ); @@ -211,6 +217,67 @@ typedef struct vkShadowMapState_s { static vkShadowMapState_t vkShadow; +// One sealed caster record may feed both LOCAL and GLOBAL ownership maps, so +// geometry/descriptors are retained once and referenced from both resource +// plans. The reference table is bounded at twice the domain caster arena. +static const int VK_CLASSIC_SHADOW_MAX_CASTER_REFS = + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS * 2; + +typedef struct vkClassicShadowAlphaPlan_s { + const classicInteractionDomainShadowAlphaStage_t *stage; + VkDescriptorSet imageSet; +} vkClassicShadowAlphaPlan_t; + +typedef struct vkClassicShadowCasterPlan_s { + const classicInteractionDomainShadowCaster_t *caster; + int vertexOffset; + int indexOffset; + int firstAlpha; + int alphaCount; + VkCullModeFlags cullMode; +} vkClassicShadowCasterPlan_t; + +typedef struct vkClassicShadowPassPlan_s { + const classicInteractionDomainLight_t *light; + const classicInteractionDomainShadowMapPass_t *pass; + const vkShadowLightState_t *physicalLight; + const vkShadowPassState_t *physicalPass; + vkShadowReceiverPass_t receiver; + int firstCasterRef; + int casterRefCount; + bool resourceOwner; +} vkClassicShadowPassPlan_t; + +typedef struct vkClassicShadowTransaction_s { + const classicInteractionDomainView_t *view; + VkCommandBuffer cmd; + int frameSlot; + VkPipelineLayout layout; + VkPipeline projectedCasterPipeline; + VkPipeline pointCasterPipeline; + VkDescriptorSet whiteSet; + int projectedCount; + int projectedFreshCount; + int pointFreshCount; + int pointHitCount; + int passPlanCount; + int casterPlanCount; + int casterRefCount; + int alphaPlanCount; + bool active; + bool ready; + bool ownsPreparedLights; + vkClassicShadowPassPlan_t passPlans[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES ]; + vkClassicShadowCasterPlan_t casterPlans[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + int casterRefs[ VK_CLASSIC_SHADOW_MAX_CASTER_REFS ]; + vkClassicShadowAlphaPlan_t alphaPlans[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES ]; +} vkClassicShadowTransaction_t; + +static vkClassicShadowTransaction_t vkClassicShadowTransaction; + static VkImageAspectFlags VK_ShadowMap_DepthAspectMask( void ) { return VK_IMAGE_ASPECT_DEPTH_BIT | ( vkCtx.shadowDepthHasStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0 ); @@ -309,9 +376,9 @@ static void VK_ShadowMap_InvalidatePassResource( vkShadowLightState_t &light, VK_ShadowMap_MarkStencilFallbackSticky( light.vLight ); } -void VK_ShadowMap_AbandonPreparedLights( void ) { +static void VK_ShadowMap_ReleasePreparedLights( const bool markSticky ) { for ( int i = 0 ; i < vkShadow.numLights ; i++ ) { - if ( vkShadow.lights[ i ].valid ) { + if ( markSticky && vkShadow.lights[ i ].valid ) { VK_ShadowMap_MarkStencilFallbackSticky( vkShadow.lights[ i ].vLight ); } @@ -345,6 +412,20 @@ void VK_ShadowMap_AbandonPreparedLights( void ) { } vkShadow.lights[ i ].valid = false; } + vkShadow.numLights = 0; + vkShadow.nextTileX = 0; + vkShadow.nextTileY = 0; + vkShadow.nextTileRowHeight = 0; + vkShadow.pointCubesUsed = 0; + vkShadow.freshUpdates = 0; + vkShadow.projectedCacheHits = 0; + vkShadow.pointCacheHits = 0; + vkShadow.projectedFreshUpdates = 0; + vkShadow.pointFreshUpdates = 0; +} + +void VK_ShadowMap_AbandonPreparedLights( void ) { + VK_ShadowMap_ReleasePreparedLights( true ); } /* @@ -717,20 +798,6 @@ Point-light math (ports of the excluded draw_arb2.cpp helpers) ==================== */ -// port of RB_PointShadowMapLightFar (draw_arb2.cpp:7358): the padded radial -// far envelope both the caster and the receiver normalize against -static float VK_ShadowMap_PointLightFar( const viewLight_t *vLight ) { - idVec3 adjustedRadius = vLight->lightRadius; - if ( vLight->lightDef != NULL ) { - const renderLight_t &parms = vLight->lightDef->parms; - adjustedRadius[0] = parms.lightRadius[0] + idMath::Fabs( parms.lightCenter[0] ); - adjustedRadius[1] = parms.lightRadius[1] + idMath::Fabs( parms.lightCenter[1] ); - adjustedRadius[2] = parms.lightRadius[2] + idMath::Fabs( parms.lightCenter[2] ); - } - - return Max( adjustedRadius.Length() * r_shadowMapPointFarScale.GetFloat(), 1.0f ); -} - // port of RB_PointShadowMapBuildViewAxis (draw_arb2.cpp:7303): the GL cube // face view axes. Combined with the GL viewport row mapping (positive-height // viewport: NDC y=-1 -> texel row 0) these produce the exact GL/Vulkan cube @@ -929,7 +996,7 @@ static int VK_ShadowMap_BuildPassSignatureForView( hash = VK_ShadowMap_HashFloat( hash, r_shadowMapPointFarScale.GetFloat() ); hash = VK_ShadowMap_HashFloat( hash, - VK_ShadowMap_PointLightFar( vLight ) ); + R_ShadowMapPointFarDistance( vLight ) ); if ( vLight != NULL && vLight->lightDef != NULL ) { for ( int i = 0 ; i < 3 ; i++ ) { hash = VK_ShadowMap_HashFloat( hash, @@ -1906,7 +1973,7 @@ int VK_ShadowMap_PrepareViewLights( const viewDef_t *viewDef, memset( &entry, 0, sizeof( entry ) ); entry.vLight = vLight; entry.pointLight = true; - entry.pointFar = VK_ShadowMap_PointLightFar( vLight ); + entry.pointFar = R_ShadowMapPointFarDistance( vLight ); for ( int i = 0 ; i < 3 ; i++ ) { entry.pointLightOrigin[ i ] = vLight->globalLightOrigin[ i ]; @@ -1974,15 +2041,20 @@ int VK_ShadowMap_PrepareViewLights( const viewDef_t *viewDef, VK_ShadowMap_RefreshLightValidity( entry ); if ( entry.valid ) { - // receiver bias scalars (RB_GLSLPointShadowMap_ - // CreateDrawInteractions): texel-aware depth bias plus the - // per-distance normal-offset texel factor (a cube face spans - // 2*distance across faceSize texels) + // Seal the same world-bounded receiver coefficients as GL after + // cache reuse has restored the physical cube's final far envelope + // and face size. const int faceSize = entry.tileSize; - entry.texelDepthBias = Max( 0.0f, r_shadowMapTexelBiasScale.GetFloat() ) - / (float)Max( 1, faceSize ); - entry.normalOffsetWorld = 2.0f * Max( 0.0f, r_shadowMapNormalOffsetScale.GetFloat() ) - / (float)Max( 1, faceSize ); + const shadowMapPointReceiverSettings_t receiverSettings = + R_ShadowMapPointReceiverSettings( + entry.pointFar, faceSize ); + entry.constantBias = receiverSettings.constantBias; + entry.normalBias = receiverSettings.normalBias; + entry.texelDepthBias = receiverSettings.texelBiasScale + / static_cast( Max( 1, faceSize ) ); + entry.normalOffsetWorld = + 2.0f * receiverSettings.normalOffsetScale + / static_cast( Max( 1, faceSize ) ); vkShadow.numLights++; prepared++; } @@ -2111,16 +2183,31 @@ Caster drawing */ // r_shadowMapCasterCulling in the executor's GL-parity winding convention -// (negative-height viewport, CCW front): mode 1 stores light-facing -// engine-front faces (cull FRONT), mode 2 (default) stores engine-back faces -// (cull BACK); material twoSided/backSided is always honored -static VkCullModeFlags VK_ShadowMap_CasterCullMode( const idMaterial *shader ) { +// (negative-height viewport, CCW front): mode 0 is always two-sided, mode 1 +// stores light-facing near-shell faces (cull FRONT), and mode 2 (default) is +// topology-aware: open/unknown hulls and hulls enclosing the light are +// two-sided, as are mirrored/invalid model transforms, while other perfect +// hulls use mode 1. The authored material orientation is honored whenever +// one-sided culling is active. +static VkCullModeFlags VK_ShadowMap_CasterCullMode( + const viewLight_t *vLight, const drawSurf_t *surf, + const srfTriangles_t *casterGeo ) { const int mode = idMath::ClampInt( 0, 2, r_shadowMapCasterCulling.GetInteger() ); + const idMaterial *shader = surf != NULL ? surf->material : NULL; const int materialCull = ( shader != NULL ) ? shader->GetCullType() : CT_FRONT_SIDED; - if ( mode == 0 || materialCull == CT_TWO_SIDED ) { + if ( mode == 0 || materialCull == CT_TWO_SIDED + || ( mode == 2 + && ( casterGeo == NULL || !casterGeo->perfectHull + || surf == NULL || surf->space == NULL + || R_ShadowMapCasterTransformNeedsTwoSided( + surf->space->modelMatrix ) + || R_ShadowMapLightOriginInsideCasterBounds( vLight, + surf->space->modelMatrix, + casterGeo->bounds[0].ToFloatPtr(), + casterGeo->bounds[1].ToFloatPtr() ) ) ) ) { return VK_CULL_MODE_NONE; } - bool cullFront = ( mode == 1 ); + bool cullFront = true; if ( materialCull == CT_BACK_SIDED ) { cullFront = !cullFront; } @@ -2329,8 +2416,10 @@ static void VK_ShadowMap_ReportUnsupportedCaster( vkCasterPassCtx_t &ctx, // set ctx.unsupportedCaster and emit NO conservative solid substitute. static bool VK_ShadowMap_DrawResolvedCaster( vkCasterPassCtx_t &ctx, vkCasterPush_t &push, - const drawSurf_t *surf, const srfTriangles_t *casterGeo ) { - const VkCullModeFlags cullMode = VK_ShadowMap_CasterCullMode( surf->material ); + const viewLight_t *vLight, const drawSurf_t *surf, + const srfTriangles_t *casterGeo ) { + const VkCullModeFlags cullMode = + VK_ShadowMap_CasterCullMode( vLight, surf, casterGeo ); if ( cullMode != ctx.boundCullMode ) { ctx.boundCullMode = cullMode; vkCmdSetCullMode( ctx.cmd, cullMode ); @@ -2503,7 +2592,8 @@ static int VK_ShadowMap_DrawCasterChain( vkCasterPassCtx_t &ctx, push.alphaS[ 2 ] = ctx.slopeFactor; push.alphaT[ 2 ] = ctx.constOffset; - if ( VK_ShadowMap_DrawResolvedCaster( ctx, push, surf, casterGeo ) ) { + if ( VK_ShadowMap_DrawResolvedCaster( ctx, push, + light.vLight, surf, casterGeo ) ) { drawnCasters++; } } @@ -2515,8 +2605,10 @@ static int VK_ShadowMap_DrawCasterChain( vkCasterPassCtx_t &ctx, // minus the per-face frustum cull — see the header divergence note). The push // mvp is the model -> face-view matrix; the shader projects analytically // through projRow and stores the radial view-space distance / far. -static int VK_ShadowMap_DrawPointCasterChain( vkCasterPassCtx_t &ctx, const float faceViewMatrix[ 16 ], - const float projRow[ 2 ], const float farClip, const drawSurf_t *surf ) { +static int VK_ShadowMap_DrawPointCasterChain( vkCasterPassCtx_t &ctx, + const viewLight_t *vLight, const float faceViewMatrix[ 16 ], + const float projRow[ 2 ], const float farClip, + const drawSurf_t *surf ) { int drawnCasters = 0; for ( ; surf != NULL ; surf = surf->nextOnLight ) { @@ -2543,7 +2635,8 @@ static int VK_ShadowMap_DrawPointCasterChain( vkCasterPassCtx_t &ctx, const floa push.alphaS[ 2 ] = ctx.slopeFactor; push.alphaT[ 2 ] = ctx.constOffset; - if ( VK_ShadowMap_DrawResolvedCaster( ctx, push, surf, casterGeo ) ) { + if ( VK_ShadowMap_DrawResolvedCaster( ctx, push, + vLight, surf, casterGeo ) ) { drawnCasters++; } } @@ -2598,6 +2691,772 @@ static bool VK_ShadowMap_PassCastersRepresentable( return true; } +/* +==================== +Shared fixed-classic transaction preflight + +The legacy scheduler remains the physical atlas/cube/cache allocator. The +shared corridor accepts its result only after reconciling every allocation to +the sealed semantic pass and retaining every fresh caster upload and alpha +descriptor. No live material stage or register is followed by Commit. +==================== +*/ + +static bool VK_ClassicShadow_FloatsFinite( const float *values, + const int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !std::isfinite( values[ i ] ) ) { + return false; + } + } + return true; +} + +static bool VK_ClassicShadow_CullMode( const rendererCullMode_t cull, + VkCullModeFlags &mode ) { + switch ( cull ) { + case RENDERER_CULL_NONE: + mode = VK_CULL_MODE_NONE; + return true; + case RENDERER_CULL_FRONT: + mode = VK_CULL_MODE_FRONT_BIT; + return true; + case RENDERER_CULL_BACK: + mode = VK_CULL_MODE_BACK_BIT; + return true; + default: + return false; + } +} + +static bool VK_ClassicShadow_ResolveDescriptor( + const std::uint64_t resourceId, VkDescriptorSet &descriptor ) { + descriptor = VK_NULL_HANDLE; + const classicInteractionDomainTexture_t *texture = + R_ClassicInteractionDomain_ResolveTexture( resourceId ); + if ( texture == NULL || texture->textureResourceId != resourceId + || texture->image == NULL || !texture->loaded + || texture->defaulted || texture->mutableImage + || texture->textureHandle == 0 + || !texture->image->IsLoaded() + || texture->image->IsDefaulted() + || texture->textureHandle + != const_cast( texture->image )->GetDeviceHandle() + || texture->filter != texture->image->GetFilter() + || texture->repeat != texture->image->GetRepeat() + || texture->storageGeneration + != texture->image->GetStorageGeneration() ) { + return false; + } + descriptor = VK_Exec_ImageDescriptor( texture->textureHandle, true ); + return descriptor != VK_NULL_HANDLE; +} + +static bool VK_ClassicShadow_ProjectedStateMatches( + const shadowMapProjectedLightState_t &sealed, + const shadowMapProjectedLightState_t &physical ) { + if ( sealed.valid != physical.valid + || sealed.cascadeCount != physical.cascadeCount + || sealed.atlasDiv != physical.atlasDiv + || sealed.tileSize != physical.tileSize + || sealed.requestedCascadeCount != physical.requestedCascadeCount + || sealed.fallbackCascade != physical.fallbackCascade + || sealed.fallbackReason != physical.fallbackReason + || sealed.cascadeFallback != physical.cascadeFallback ) { + return false; + } + return memcmp( sealed.clipPlanes, physical.clipPlanes, + sizeof( sealed.clipPlanes ) ) == 0 + && memcmp( sealed.splitDepths, physical.splitDepths, + sizeof( sealed.splitDepths ) ) == 0 + && memcmp( sealed.biasScale, physical.biasScale, + sizeof( sealed.biasScale ) ) == 0 + && memcmp( sealed.texelDepthBias, physical.texelDepthBias, + sizeof( sealed.texelDepthBias ) ) == 0 + && memcmp( sealed.worldTexelSize, physical.worldTexelSize, + sizeof( sealed.worldTexelSize ) ) == 0; +} + +static int VK_ClassicShadow_FindCasterPlan( + const classicInteractionDomainShadowCaster_t *caster ) { + for ( int i = 0; i < vkClassicShadowTransaction.casterPlanCount; ++i ) { + if ( vkClassicShadowTransaction.casterPlans[ i ].caster == caster ) { + return i; + } + } + return -1; +} + +static bool VK_ClassicShadow_PrepareCaster( + const classicInteractionDomainShadowCaster_t &caster, + int &casterPlanIndex ) { + vkClassicShadowTransaction_t &transaction = + vkClassicShadowTransaction; + casterPlanIndex = VK_ClassicShadow_FindCasterPlan( &caster ); + if ( casterPlanIndex >= 0 ) { + return true; + } + if ( transaction.casterPlanCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS + || caster.disposition + != CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + || caster.indexSelection + != CLASSIC_INTERACTION_SHADOW_INDEX_AMBIENT + || !caster.ambientGeometry || caster.preload || caster.external + || caster.legacyDrawSurf == NULL + || caster.legacyCasterGeometry == NULL + || caster.legacyDrawSurf->space == NULL + || R_TriHasPrimBatchMesh( caster.legacyCasterGeometry ) + || caster.vertexCount <= 0 || caster.selectedIndexCount <= 0 + || caster.totalIndexCount != caster.selectedIndexCount + || caster.legacyCasterGeometry->numVerts != caster.vertexCount + || caster.legacyCasterGeometry->numIndexes + != caster.selectedIndexCount + || caster.legacyCasterGeometry->ambientCache == NULL + || ( caster.legacyCasterGeometry->indexes == NULL + && caster.legacyCasterGeometry->indexCache == NULL ) + || !VK_ClassicShadow_FloatsFinite( caster.modelMatrix, 16 ) + || !VK_ClassicShadow_FloatsFinite( caster.boundsMin, 3 ) + || !VK_ClassicShadow_FloatsFinite( caster.boundsMax, 3 ) ) { + return false; + } + + vkClassicShadowCasterPlan_t &plan = + transaction.casterPlans[ transaction.casterPlanCount ]; + memset( &plan, 0, sizeof( plan ) ); + plan.caster = &caster; + plan.vertexOffset = -1; + plan.indexOffset = -1; + plan.firstAlpha = transaction.alphaPlanCount; + if ( !VK_ClassicShadow_CullMode( caster.cull, plan.cullMode ) + || !VK_Exec_PrepareTriGeometry( transaction.cmd, + transaction.frameSlot, caster.legacyCasterGeometry, + plan.vertexOffset, plan.indexOffset ) ) { + return false; + } + + for ( int alphaIndex = 0; alphaIndex < caster.alphaStageCount; + ++alphaIndex ) { + if ( transaction.alphaPlanCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES ) { + return false; + } + const classicInteractionDomainShadowAlphaStage_t *stage = + R_ClassicInteractionDomain_ShadowAlphaStage( caster, + alphaIndex ); + float alphaMode = 0.0f; + VkDescriptorSet imageSet = VK_NULL_HANDLE; + if ( stage == NULL || stage->textureResourceId == 0 + || stage->alphaScale <= 0.0f + || !VK_ShadowMap_AlphaTestModeValue( + stage->alphaTestMode, alphaMode ) + || !std::isfinite( stage->alphaTestValue ) + || !std::isfinite( stage->alphaScale ) + || !std::isfinite( stage->alphaHashMode ) + || !VK_ClassicShadow_FloatsFinite( + &stage->textureMatrix[ 0 ][ 0 ], 8 ) + || !VK_ClassicShadow_ResolveDescriptor( + stage->textureResourceId, imageSet ) ) { + return false; + } + vkClassicShadowAlphaPlan_t &alphaPlan = + transaction.alphaPlans[ transaction.alphaPlanCount++ ]; + alphaPlan.stage = stage; + alphaPlan.imageSet = imageSet; + plan.alphaCount++; + } + + casterPlanIndex = transaction.casterPlanCount++; + return true; +} + +static bool VK_ClassicShadow_AddCasterChain( + const classicInteractionDomainLight_t &light, + const classicInteractionDomainShadowChain_t chain, + vkClassicShadowPassPlan_t &passPlan, + int &mappedCasters, int &drawableCasters, int &noopCasters ) { + vkClassicShadowTransaction_t &transaction = + vkClassicShadowTransaction; + const int chainCount = light.shadowCasterCount[ chain ]; + if ( chainCount < 0 ) { + return false; + } + for ( int i = 0; i < chainCount; ++i ) { + const classicInteractionDomainShadowCaster_t *caster = + R_ClassicInteractionDomain_LightShadowCaster( light, chain, i ); + if ( caster == NULL || caster->chain != chain + || caster->legacyViewLight != light.legacyViewLight ) { + return false; + } + mappedCasters++; + if ( caster->disposition + == CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY ) { + if ( !R_ClassicInteractionDomain_ShadowCasterNoopValid( *caster ) ) { + return false; + } + noopCasters++; + continue; + } + int planIndex = -1; + if ( transaction.casterRefCount >= VK_CLASSIC_SHADOW_MAX_CASTER_REFS + || !VK_ClassicShadow_PrepareCaster( *caster, planIndex ) ) { + return false; + } + transaction.casterRefs[ transaction.casterRefCount++ ] = planIndex; + passPlan.casterRefCount++; + drawableCasters++; + } + return true; +} + +static bool VK_ClassicShadow_ValidatePhysicalPass( + const classicInteractionDomainView_t &view, + const classicInteractionDomainShadowMapPass_t &sealed, + const vkShadowLightState_t &light, + const vkShadowPassState_t &pass, + const vkShadowReceiverPass_t receiver ) { + const bool point = sealed.lightClass == SHADOWMAP_LIGHT_POINT; + if ( !pass.valid || light.pointLight != point + || pass.resourcePass < VK_SHADOW_RECEIVER_LOCAL + || pass.resourcePass >= VK_SHADOW_RECEIVER_PASS_COUNT + || sealed.resourceAlias + != ( pass.resourcePass != receiver ) + || sealed.resourceOwner + != static_cast( + pass.resourcePass ) ) { + return false; + } + if ( pass.cacheHit && !sealed.allowCacheReuse ) { + return false; + } + if ( pass.cacheUpdate && !sealed.allowCacheUpdate ) { + return false; + } + if ( !pass.cacheHit && !pass.cacheUpdate && !sealed.allowScratch ) { + return false; + } + // The physical cache signature is built from the live caster state. Reject + // any drift after the front end sealed the pass so a cache hit cannot use + // different culling/bias, and freshly rendered sealed contents cannot be + // published under a mismatched live signature. + if ( light.vLight == NULL + || sealed.casterSignature + != light.vLight->shadowMapCasterSignature + || sealed.hashedAlpha != r_shadowMapHashedAlpha.GetBool() + || sealed.stableAlphaHash + != r_shadowMapStableAlphaHash.GetBool() + || sealed.casterCullMode != idMath::ClampInt( 0, 2, + r_shadowMapCasterCulling.GetInteger() ) + || idMath::Fabs( sealed.polygonFactor + - r_shadowMapPolygonFactor.GetFloat() ) > 0.00001f + || idMath::Fabs( sealed.polygonOffset + - r_shadowMapPolygonOffset.GetFloat() ) > 0.00001f ) { + return false; + } + + if ( point ) { + if ( !sealed.point.valid || sealed.point.faceCount != 6 + || sealed.point.faceSize != light.tileSize + || sealed.point.farDistance != light.pointFar + || sealed.point.lightOrigin[ 0 ] != light.pointLightOrigin[ 0 ] + || sealed.point.lightOrigin[ 1 ] != light.pointLightOrigin[ 1 ] + || sealed.point.lightOrigin[ 2 ] != light.pointLightOrigin[ 2 ] + || pass.pointSet == VK_NULL_HANDLE ) { + return false; + } + if ( pass.cacheHit ) { + const vkPointShadowCacheEntry_t *cache = + pass.cacheEntry >= 0 + && pass.cacheEntry < VK_SHADOW_MAX_CACHE_SLOTS + ? &vkShadow.pointCache[ pass.cacheEntry ] : NULL; + return cache != NULL && cache->valid && cache->reserved + && cache->generation == tr.videoRestartCount + && cache->renderWorld == view.viewDef->renderWorld + && cache->signature == pass.cacheSignature + && cache->size == light.tileSize + && cache->cube.image != VK_NULL_HANDLE + && cache->cube.layout + == VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + } + const vkPointShadowCube_t *cube = NULL; + if ( pass.cacheUpdate && pass.cacheEntry >= 0 + && pass.cacheEntry < VK_SHADOW_MAX_CACHE_SLOTS + && vkShadow.pointCache[ pass.cacheEntry ].reserved ) { + cube = &vkShadow.pointCache[ pass.cacheEntry ].cube; + } else if ( pass.cubeIndex >= 0 + && pass.cubeIndex < VK_SHADOW_MAX_POINT_CUBES ) { + cube = &vkShadow.pointCubes[ pass.cubeIndex ]; + } + return cube != NULL && cube->image != VK_NULL_HANDLE; + } + + if ( !sealed.projected.state.valid + || !VK_ClassicShadow_ProjectedStateMatches( + sealed.projected.state, light.projectedState ) + || light.tileSize != sealed.projected.state.tileSize + || vkShadow.atlasImage == VK_NULL_HANDLE ) { + return false; + } + if ( pass.cacheHit ) { + const vkProjectedShadowCacheEntry_t *cache = + pass.cacheEntry >= 0 + && pass.cacheEntry < VK_SHADOW_MAX_CACHE_SLOTS + ? &vkShadow.projectedCache[ pass.cacheEntry ] : NULL; + return cache != NULL && cache->valid && cache->reserved + && cache->generation == tr.videoRestartCount + && cache->renderWorld == view.viewDef->renderWorld + && cache->signature == pass.cacheSignature + && cache->tileSize == light.tileSize + && cache->image != VK_NULL_HANDLE + && cache->layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + } + return !pass.cacheUpdate || ( pass.cacheEntry >= 0 + && pass.cacheEntry < VK_SHADOW_MAX_CACHE_SLOTS + && vkShadow.projectedCache[ pass.cacheEntry ].reserved + && vkShadow.projectedCache[ pass.cacheEntry ].image + != VK_NULL_HANDLE ); +} + +void VK_ShadowMap_AbortClassicInteractionView( + const classicInteractionDomainView_t *view ) { + if ( !vkClassicShadowTransaction.active ) { + return; + } + if ( view != NULL && vkClassicShadowTransaction.view != view ) { + return; + } + if ( vkClassicShadowTransaction.ownsPreparedLights ) { + VK_ShadowMap_ReleasePreparedLights( false ); + } + memset( &vkClassicShadowTransaction, 0, + sizeof( vkClassicShadowTransaction ) ); +} + +bool VK_ShadowMap_PreflightClassicInteractionView( + const classicInteractionDomainView_t *view ) { + VK_ShadowMap_AbortClassicInteractionView( NULL ); + memset( &vkClassicShadowTransaction, 0, + sizeof( vkClassicShadowTransaction ) ); + vkClassicShadowTransaction_t &transaction = + vkClassicShadowTransaction; + transaction.active = true; + transaction.view = view; + if ( view == NULL || !view->ready || view->viewDef == NULL + || view->shadowMapPassCount < 0 + || view->shadowMapPassCount + > CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + if ( view->shadowMapPassCount == 0 ) { + transaction.ready = true; + return true; + } + + transaction.cmd = VK_Exec_ActiveCmd(); + transaction.frameSlot = VK_Exec_ActiveFrameSlot(); + transaction.layout = VK_Exec_BasePipelineLayout(); + transaction.projectedCasterPipeline = VK_Exec_CasterPipeline(); + transaction.pointCasterPipeline = VK_Exec_PointCasterPipeline(); + if ( transaction.cmd == VK_NULL_HANDLE || transaction.frameSlot < 0 + || transaction.frameSlot >= VK_FRAMES_IN_FLIGHT + || transaction.layout == VK_NULL_HANDLE + || globalImages == NULL || globalImages->whiteImage == NULL + || !globalImages->whiteImage->IsLoaded() + || globalImages->whiteImage->IsDefaulted() ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + transaction.whiteSet = VK_Exec_ImageDescriptor( + globalImages->whiteImage->GetDeviceHandle(), true ); + if ( transaction.whiteSet == VK_NULL_HANDLE ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + transaction.ownsPreparedLights = true; + // Shared ownership may atomically hand the whole view back to the legacy + // executor, whose own preparation will re-evaluate the active target. Let + // optional maps obey the ordinary budget here; if even one sealed pass is + // denied, reconciliation below aborts every private reservation and no + // shared attachment write has occurred. Correctness-required maps still + // bypass the budget through their incomplete-stencil mask. + if ( VK_ShadowMap_PrepareViewLights( view->viewDef, true ) <= 0 ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + + int reconciledPasses = 0; + for ( int lightIndex = 0; lightIndex < view->lightCount; ++lightIndex ) { + const classicInteractionDomainLight_t *light = + R_ClassicInteractionDomain_ViewLight( *view, lightIndex ); + if ( light == NULL || light->legacyViewLight == NULL ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + const vkShadowLightState_t *physicalLight = + VK_ShadowMap_LightState( light->legacyViewLight ); + + for ( int receiverIndex = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiverIndex <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; + ++receiverIndex ) { + const classicInteractionDomainReceiver_t domainReceiver = + static_cast( receiverIndex ); + const classicInteractionDomainShadowMapPass_t *sealed = + R_ClassicInteractionDomain_LightShadowMapPass( + *light, domainReceiver ); + if ( sealed == NULL ) { + continue; + } + if ( transaction.passPlanCount + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES + || physicalLight == NULL + || sealed->legacyViewLight != light->legacyViewLight + || sealed->lightIndex != view->firstLight + lightIndex + || sealed->receiver != domainReceiver + || ( sealed->disposition + != CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED + && sealed->disposition + != CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID ) + || !sealed->mapRequired + || ( sealed->disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED + && !sealed->mapComplete ) + || ( sealed->disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID + && !sealed->hybridComplete ) + || sealed->hasTranslucentCasters ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + const vkShadowReceiverPass_t receiver = + static_cast( receiverIndex ); + const vkShadowPassState_t *physicalPass = + VK_ShadowMap_PassState( physicalLight, receiver ); + if ( physicalPass == NULL + || !VK_ClassicShadow_ValidatePhysicalPass( *view, + *sealed, *physicalLight, *physicalPass, receiver ) ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + + vkClassicShadowPassPlan_t &passPlan = + transaction.passPlans[ transaction.passPlanCount++ ]; + memset( &passPlan, 0, sizeof( passPlan ) ); + passPlan.light = light; + passPlan.pass = sealed; + passPlan.physicalLight = physicalLight; + passPlan.physicalPass = physicalPass; + passPlan.receiver = receiver; + passPlan.resourceOwner = physicalPass->resourcePass == receiver; + passPlan.firstCasterRef = transaction.casterRefCount; + + int mappedCasters = 0; + int drawableCasters = 0; + int noopCasters = 0; + const classicInteractionDomainShadowChain_t chains[] = { + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC + }; + const int chainCount = receiver == VK_SHADOW_RECEIVER_GLOBAL + ? 4 : 2; + for ( int chainIndex = 0; chainIndex < chainCount; + ++chainIndex ) { + if ( !VK_ClassicShadow_AddCasterChain( *light, + chains[ chainIndex ], passPlan, mappedCasters, + drawableCasters, noopCasters ) ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + } + if ( mappedCasters != sealed->mappedCasterCount + || drawableCasters != sealed->drawableMappedCasters + || noopCasters != sealed->noopMappedCasters ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + + if ( passPlan.resourceOwner ) { + if ( physicalLight->pointLight ) { + if ( physicalPass->cacheHit ) { + transaction.pointHitCount++; + } else { + transaction.pointFreshCount++; + } + } else { + transaction.projectedCount++; + if ( !physicalPass->cacheHit ) { + transaction.projectedFreshCount++; + } + } + } + reconciledPasses++; + } + } + int physicalPassCount = 0; + for ( int physicalLightIndex = 0; + physicalLightIndex < vkShadow.numLights; ++physicalLightIndex ) { + const vkShadowLightState_t &physical = + vkShadow.lights[ physicalLightIndex ]; + for ( int receiverIndex = 0; + receiverIndex < VK_SHADOW_RECEIVER_PASS_COUNT; + ++receiverIndex ) { + if ( !physical.passes[ receiverIndex ].valid ) { + continue; + } + bool found = false; + for ( int planIndex = 0; + planIndex < transaction.passPlanCount; ++planIndex ) { + const vkClassicShadowPassPlan_t &plan = + transaction.passPlans[ planIndex ]; + if ( plan.physicalLight == &physical + && plan.receiver == receiverIndex ) { + found = true; + break; + } + } + if ( !found ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + physicalPassCount++; + } + } + + if ( reconciledPasses != view->shadowMapPassCount + || transaction.passPlanCount != view->shadowMapPassCount + || physicalPassCount != transaction.passPlanCount + || transaction.projectedCount + transaction.pointFreshCount + + transaction.pointHitCount <= 0 + || ( transaction.projectedFreshCount > 0 + && transaction.projectedCasterPipeline == VK_NULL_HANDLE ) + || ( transaction.pointFreshCount > 0 + && transaction.pointCasterPipeline == VK_NULL_HANDLE ) ) { + VK_ShadowMap_AbortClassicInteractionView( view ); + return false; + } + + transaction.ready = true; + return true; +} + +static const vkClassicShadowPassPlan_t *VK_ClassicShadow_FindPassPlan( + const vkShadowLightState_t *light, + const vkShadowReceiverPass_t receiver ) { + for ( int i = 0; i < vkClassicShadowTransaction.passPlanCount; ++i ) { + const vkClassicShadowPassPlan_t &plan = + vkClassicShadowTransaction.passPlans[ i ]; + if ( plan.physicalLight == light && plan.receiver == receiver ) { + return &plan; + } + } + return NULL; +} + +static VkCullModeFlags VK_ClassicShadow_EffectiveCull( + const vkClassicShadowCasterPlan_t &casterPlan, + const classicInteractionDomainShadowMapPass_t &pass ) { + if ( pass.casterCullMode == 0 + || casterPlan.cullMode == VK_CULL_MODE_NONE + || ( pass.casterCullMode == 2 + && ( !casterPlan.caster->perfectHull + || R_ShadowMapLightOriginInsideCasterBounds( + pass.legacyViewLight, + casterPlan.caster->modelMatrix, + casterPlan.caster->boundsMin, + casterPlan.caster->boundsMax ) ) ) ) { + return VK_CULL_MODE_NONE; + } + // Both explicit mode 1 and AUTO mode 2 on a sealed perfect hull store the + // light-facing near shell. The material cull orientation may reverse it. + bool cullFront = true; + if ( casterPlan.cullMode == VK_CULL_MODE_BACK_BIT ) { + cullFront = !cullFront; + } + return cullFront ? VK_CULL_MODE_FRONT_BIT : VK_CULL_MODE_BACK_BIT; +} + +static void VK_ClassicShadow_DrawCaster( + vkCasterPassCtx_t &ctx, + const vkClassicShadowCasterPlan_t &casterPlan, + const classicInteractionDomainShadowMapPass_t &pass, + const vkCasterPush_t &basePush ) { + const classicInteractionDomainShadowCaster_t &caster = + *casterPlan.caster; + const VkCullModeFlags cullMode = + VK_ClassicShadow_EffectiveCull( casterPlan, pass ); + if ( cullMode != ctx.boundCullMode ) { + ctx.boundCullMode = cullMode; + vkCmdSetCullMode( ctx.cmd, cullMode ); + } + VK_Exec_BindPreparedTriGeometry( ctx.cmd, ctx.slot, + casterPlan.vertexOffset, casterPlan.indexOffset ); + + const int drawCount = casterPlan.alphaCount > 0 + ? casterPlan.alphaCount : 1; + for ( int alphaIndex = 0; alphaIndex < drawCount; ++alphaIndex ) { + vkCasterPush_t push = basePush; + VkDescriptorSet imageSet = ctx.whiteSet; + if ( casterPlan.alphaCount > 0 ) { + const vkClassicShadowAlphaPlan_t &alphaPlan = + vkClassicShadowTransaction.alphaPlans[ + casterPlan.firstAlpha + alphaIndex ]; + const classicInteractionDomainShadowAlphaStage_t &stage = + *alphaPlan.stage; + imageSet = alphaPlan.imageSet; + push.alphaS[ 0 ] = stage.textureMatrix[ 0 ][ 0 ]; + push.alphaS[ 1 ] = stage.textureMatrix[ 0 ][ 1 ]; + push.alphaS[ 3 ] = stage.textureMatrix[ 0 ][ 3 ]; + push.alphaT[ 0 ] = stage.textureMatrix[ 1 ][ 0 ]; + push.alphaT[ 1 ] = stage.textureMatrix[ 1 ][ 1 ]; + push.alphaT[ 3 ] = stage.textureMatrix[ 1 ][ 3 ]; + VK_ShadowMap_AlphaTestModeValue( stage.alphaTestMode, + push.params[ 0 ] ); + push.params[ 1 ] = stage.alphaTestValue; + push.params[ 2 ] = stage.alphaScale; + push.params[ 3 ] = stage.alphaHashMode; + } + if ( imageSet != ctx.boundImageSet ) { + ctx.boundImageSet = imageSet; + vkCmdBindDescriptorSets( ctx.cmd, + VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.layout, + 0, 1, &imageSet, 0, NULL ); + } + vkCmdPushConstants( ctx.cmd, ctx.layout, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + 0, sizeof( push ), &push ); + vkCmdDrawIndexed( ctx.cmd, + static_cast( caster.selectedIndexCount ), + 1, 0, 0, 0 ); + backEnd.pc.c_shadowElements++; + backEnd.pc.c_shadowIndexes += caster.selectedIndexCount; + backEnd.pc.c_shadowVertexes += caster.vertexCount; + } +} + +static bool VK_ClassicShadow_CasterOutsideCascade( + const vkClassicShadowCasterPlan_t &casterPlan, + const classicInteractionDomainShadowMapPass_t &pass, + const int cascadeIndex ) { + const classicInteractionDomainShadowCaster_t &caster = + *casterPlan.caster; + const shadowMapProjectedLightState_t &projected = pass.projected.state; + if ( projected.cascadeCount <= 1 ) { + return false; + } + const int safeCascade = idMath::ClampInt( 0, + SHADOWMAP_PROJECTED_MAX_CASCADES - 1, cascadeIndex ); + const idPlane *planes = projected.clipPlanes[ safeCascade ]; + int outsideMask = 0x0f; + for ( int cornerIndex = 0; cornerIndex < 8; ++cornerIndex ) { + const idVec3 local( + caster.boundsMin[ 0 ] + ( ( cornerIndex & 1 ) != 0 + ? caster.boundsMax[ 0 ] - caster.boundsMin[ 0 ] : 0.0f ), + caster.boundsMin[ 1 ] + ( ( cornerIndex & 2 ) != 0 + ? caster.boundsMax[ 1 ] - caster.boundsMin[ 1 ] : 0.0f ), + caster.boundsMin[ 2 ] + ( ( cornerIndex & 4 ) != 0 + ? caster.boundsMax[ 2 ] - caster.boundsMin[ 2 ] : 0.0f ) ); + idVec3 world; + R_LocalPointToGlobal( caster.modelMatrix, local, world ); + const float w = planes[ 3 ].Distance( world ); + if ( w <= 1.0e-5f ) { + return false; + } + int mask = 0; + if ( planes[ 0 ].Distance( world ) < -w ) { + mask |= 1; + } else if ( planes[ 0 ].Distance( world ) > w ) { + mask |= 2; + } + if ( planes[ 1 ].Distance( world ) < -w ) { + mask |= 4; + } else if ( planes[ 1 ].Distance( world ) > w ) { + mask |= 8; + } + outsideMask &= mask; + if ( outsideMask == 0 ) { + return false; + } + } + return outsideMask != 0; +} + +static int VK_ClassicShadow_DrawProjectedPass( + vkCasterPassCtx_t &ctx, + const vkClassicShadowPassPlan_t &passPlan, + const int cascadeIndex ) { + const classicInteractionDomainShadowMapPass_t &pass = *passPlan.pass; + const int safeCascade = idMath::ClampInt( 0, + SHADOWMAP_PROJECTED_MAX_CASCADES - 1, cascadeIndex ); + const idPlane *clipPlanes = pass.projected.state.clipPlanes[ + safeCascade ]; + float clipMatrix[ 16 ]; + R_ShadowMapClipPlanesToGLMatrix( clipPlanes, clipMatrix ); + int draws = 0; + for ( int i = 0; i < passPlan.casterRefCount; ++i ) { + const vkClassicShadowCasterPlan_t &casterPlan = + vkClassicShadowTransaction.casterPlans[ + vkClassicShadowTransaction.casterRefs[ + passPlan.firstCasterRef + i ] ]; + if ( VK_ClassicShadow_CasterOutsideCascade( + casterPlan, pass, safeCascade ) ) { + continue; + } + const classicInteractionDomainShadowCaster_t &caster = + *casterPlan.caster; + vkCasterPush_t push; + memset( &push, 0, sizeof( push ) ); + float mvpGL[ 16 ]; + myGlMultMatrix( caster.modelMatrix, clipMatrix, mvpGL ); + VK_FixupClipSpaceZ( push.mvp, mvpGL ); + idPlane localDepthPlane; + R_GlobalPlaneToLocal( caster.modelMatrix, clipPlanes[ 2 ], + localDepthPlane ); + memcpy( push.depthRow, localDepthPlane.ToFloatPtr(), + sizeof( push.depthRow ) ); + VK_ShadowMap_SetPushAlphaIdentity( push ); + push.alphaS[ 2 ] = pass.polygonFactor; + push.alphaT[ 2 ] = pass.polygonOffset + * ( 1.0f / 16777216.0f ); + VK_ClassicShadow_DrawCaster( ctx, casterPlan, pass, push ); + draws++; + } + return draws; +} + +static int VK_ClassicShadow_DrawPointPass( + vkCasterPassCtx_t &ctx, + const vkClassicShadowPassPlan_t &passPlan, + const float faceViewMatrix[ 16 ], const float projRow[ 2 ] ) { + const classicInteractionDomainShadowMapPass_t &pass = *passPlan.pass; + int draws = 0; + for ( int i = 0; i < passPlan.casterRefCount; ++i ) { + const vkClassicShadowCasterPlan_t &casterPlan = + vkClassicShadowTransaction.casterPlans[ + vkClassicShadowTransaction.casterRefs[ + passPlan.firstCasterRef + i ] ]; + const classicInteractionDomainShadowCaster_t &caster = + *casterPlan.caster; + vkCasterPush_t push; + memset( &push, 0, sizeof( push ) ); + myGlMultMatrix( caster.modelMatrix, faceViewMatrix, push.mvp ); + push.depthRow[ 0 ] = projRow[ 0 ]; + push.depthRow[ 1 ] = projRow[ 1 ]; + push.depthRow[ 2 ] = pass.point.farDistance; + VK_ShadowMap_SetPushAlphaIdentity( push ); + push.alphaS[ 2 ] = pass.polygonFactor; + push.alphaT[ 2 ] = pass.polygonOffset + * ( 1.0f / 16777216.0f ); + VK_ClassicShadow_DrawCaster( ctx, casterPlan, pass, push ); + draws++; + } + return draws; +} + /* ==================== VK_ShadowMap_RenderAtlas @@ -2763,6 +3622,9 @@ static void VK_ShadowMap_FinalizeCachePasses( } bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { + const bool classicCommit = vkClassicShadowTransaction.ready + && vkClassicShadowTransaction.view != NULL + && vkClassicShadowTransaction.view->viewDef == viewDef; if ( viewDef == NULL || vkShadow.numLights <= 0 || vkShadow.atlasImage == VK_NULL_HANDLE ) { return false; @@ -2773,12 +3635,21 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { // pass for the same light remains usable this frame. Exact hits require // neither caster representability nor a caster pipeline: those properties // were proven by the successful update that published the signature. - int projectedCount = 0; - int projectedFreshCount = 0; - int pointFreshCount = 0; - int pointHitCount = 0; - VkPipeline casterPipeline = VK_Exec_CasterPipeline(); - VkPipeline pointCasterPipeline = VK_Exec_PointCasterPipeline(); + int projectedCount = classicCommit + ? vkClassicShadowTransaction.projectedCount : 0; + int projectedFreshCount = classicCommit + ? vkClassicShadowTransaction.projectedFreshCount : 0; + int pointFreshCount = classicCommit + ? vkClassicShadowTransaction.pointFreshCount : 0; + int pointHitCount = classicCommit + ? vkClassicShadowTransaction.pointHitCount : 0; + VkPipeline casterPipeline = classicCommit + ? vkClassicShadowTransaction.projectedCasterPipeline + : VK_Exec_CasterPipeline(); + VkPipeline pointCasterPipeline = classicCommit + ? vkClassicShadowTransaction.pointCasterPipeline + : VK_Exec_PointCasterPipeline(); + if ( !classicCommit ) { for ( int i = 0 ; i < vkShadow.numLights ; i++ ) { vkShadowLightState_t &light = vkShadow.lights[ i ]; if ( !light.valid ) { @@ -2910,6 +3781,7 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { } } } + } if ( projectedCount + pointFreshCount + pointHitCount == 0 ) { return false; } @@ -2922,9 +3794,11 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { return true; } - VkCommandBuffer cmd = VK_Exec_ActiveCmd(); - VkDescriptorSet whiteSet = VK_NULL_HANDLE; - if ( projectedFreshCount + pointFreshCount > 0 + VkCommandBuffer cmd = classicCommit + ? vkClassicShadowTransaction.cmd : VK_Exec_ActiveCmd(); + VkDescriptorSet whiteSet = classicCommit + ? vkClassicShadowTransaction.whiteSet : VK_NULL_HANDLE; + if ( !classicCommit && projectedFreshCount + pointFreshCount > 0 && globalImages->whiteImage != NULL ) { whiteSet = VK_Exec_ImageDescriptor( globalImages->whiteImage->GetDeviceHandle(), true ); @@ -2941,8 +3815,10 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { vkCasterPassCtx_t ctx; memset( &ctx, 0, sizeof( ctx ) ); ctx.cmd = cmd; - ctx.slot = VK_Exec_ActiveFrameSlot(); - ctx.layout = VK_Exec_BasePipelineLayout(); + ctx.slot = classicCommit ? vkClassicShadowTransaction.frameSlot + : VK_Exec_ActiveFrameSlot(); + ctx.layout = classicCommit ? vkClassicShadowTransaction.layout + : VK_Exec_BasePipelineLayout(); ctx.whiteSet = whiteSet; ctx.boundCullMode = (VkCullModeFlags)~0u; ctx.slopeFactor = r_shadowMapPolygonFactor.GetFloat(); @@ -3016,7 +3892,6 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { if ( !light.valid || light.pointLight ) { continue; } - const viewLight_t *vLight = light.vLight; for ( int passIndex = 0 ; passIndex < VK_SHADOW_RECEIVER_PASS_COUNT ; passIndex++ ) { @@ -3029,15 +3904,14 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { != receiverPass ) { continue; } + const vkClassicShadowPassPlan_t *classicPass = + classicCommit ? VK_ClassicShadow_FindPassPlan( + &light, receiverPass ) : NULL; ctx.unsupportedCaster = false; int drawnCasters = 0; - const int cascadeCount = - idMath::ClampInt( 1, - SHADOWMAP_PROJECTED_MAX_CASCADES, - light.projectedState.cascadeCount ); - const int atlasDiv = idMath::ClampInt( 1, 2, - light.projectedState.atlasDiv ); + const int cascadeCount = idMath::ClampInt( 1, SHADOWMAP_PROJECTED_MAX_CASCADES, light.projectedState.cascadeCount ); + const int atlasDiv = idMath::ClampInt( 1, 2, light.projectedState.atlasDiv ); for ( int cascadeIndex = 0 ; cascadeIndex < cascadeCount ; cascadeIndex++ ) { @@ -3072,6 +3946,12 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { vkCmdSetScissor( cmd, 0, 1, &scissor ); + if ( classicCommit ) { + drawnCasters += + VK_ClassicShadow_DrawProjectedPass( + ctx, *classicPass, cascadeIndex ); + } else { + const viewLight_t *vLight = light.vLight; drawnCasters += VK_ShadowMap_DrawCasterChain( ctx, light, @@ -3095,10 +3975,11 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { cascadeIndex, vLight->localShadowMapDynamicCasters ); } + } } // A fully transparent represented perforated chain emits // zero draws by design; the clear map is still complete. - if ( ctx.unsupportedCaster ) { + if ( !classicCommit && ctx.unsupportedCaster ) { VK_ShadowMap_InvalidatePassResource( light, receiverPass ); } @@ -3361,14 +4242,29 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { if ( !light.valid || !light.pointLight ) { continue; } - const viewLight_t *vLight = light.vLight; - + const vkClassicShadowPassPlan_t *classicPointState = NULL; + if ( classicCommit ) { + classicPointState = VK_ClassicShadow_FindPassPlan( &light, + VK_SHADOW_RECEIVER_LOCAL ); + if ( classicPointState == NULL ) { + classicPointState = VK_ClassicShadow_FindPassPlan( &light, + VK_SHADOW_RECEIVER_GLOBAL ); + } + if ( classicPointState == NULL ) { + common->FatalError( + "Vulkan: sealed point shadow state was lost after preflight" ); + } + } // near/far + the analytic face projection in the shared // VK_FixupClipSpaceZ convention: z_clip = zA*z_eye + zB*w_eye, // w_clip = -z_eye (RB_PointShadowMapBuildProjectionMatrix). The // Match the GL depth-clamp branch when the optional feature was // enabled; unsupported devices retain the existing 4-unit cap. - const float farClip = light.pointFar; + // Shared commits derive projection from the sealed semantic point + // state. Depth-clamp support is the sole backend capability input. + const float farClip = classicCommit + ? classicPointState->pass->point.farDistance + : light.pointFar; const float nearClip = idMath::ClampFloat( 0.5f, vkCtx.depthClampSupported ? 16.0f : 4.0f, farClip * 0.01f ); const float projA = -( farClip + nearClip ) / ( farClip - nearClip ); @@ -3384,6 +4280,9 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { || pass.resourcePass != receiverPass ) { continue; } + const vkClassicShadowPassPlan_t *classicPass = + classicCommit ? VK_ClassicShadow_FindPassPlan( + &light, receiverPass ) : NULL; vkPointShadowCube_t *cube = NULL; if ( pass.cacheUpdate && pass.cacheEntry >= 0 && pass.cacheEntry @@ -3397,8 +4296,10 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { pass.cubeIndex ]; } if ( cube == NULL ) { - VK_ShadowMap_InvalidatePassResource( - light, receiverPass ); + if ( !classicCommit ) { + VK_ShadowMap_InvalidatePassResource( + light, receiverPass ); + } continue; } @@ -3421,9 +4322,15 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { ctx.unsupportedCaster = false; int drawnCasters = 0; const idVec3 pointOrigin( - light.pointLightOrigin[ 0 ], - light.pointLightOrigin[ 1 ], - light.pointLightOrigin[ 2 ] ); + classicPass != NULL + ? classicPass->pass->point.lightOrigin[ 0 ] + : light.pointLightOrigin[ 0 ], + classicPass != NULL + ? classicPass->pass->point.lightOrigin[ 1 ] + : light.pointLightOrigin[ 1 ], + classicPass != NULL + ? classicPass->pass->point.lightOrigin[ 2 ] + : light.pointLightOrigin[ 2 ] ); for ( int cubeFace = 0 ; cubeFace < 6 ; cubeFace++ ) { float faceViewMatrix[ 16 ]; VK_ShadowMap_PointFaceViewMatrix( @@ -3450,23 +4357,29 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { ri.pStencilAttachment = vkCtx.shadowDepthHasStencil ? &depth : NULL; vkCmdBeginRendering( cmd, &ri ); - drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, faceViewMatrix, + if ( classicCommit ) { + drawnCasters += VK_ClassicShadow_DrawPointPass( + ctx, *classicPass, faceViewMatrix, projRow ); + } else { + const viewLight_t *vLight = light.vLight; + drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, vLight, faceViewMatrix, projRow, farClip, vLight->globalShadowMapCasters ); - drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, faceViewMatrix, + drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, vLight, faceViewMatrix, projRow, farClip, vLight->globalShadowMapDynamicCasters ); if ( receiverPass == VK_SHADOW_RECEIVER_GLOBAL ) { - drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, faceViewMatrix, + drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, vLight, faceViewMatrix, projRow, farClip, vLight->localShadowMapCasters ); - drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, faceViewMatrix, + drawnCasters += VK_ShadowMap_DrawPointCasterChain( ctx, vLight, faceViewMatrix, projRow, farClip, vLight->localShadowMapDynamicCasters ); } + } vkCmdEndRendering( cmd ); } // Zero emitted draws can be a represented fully transparent // perforated chain; its cleared six faces are valid. - if ( ctx.unsupportedCaster ) { + if ( !classicCommit && ctx.unsupportedCaster ) { VK_ShadowMap_InvalidatePassResource( light, receiverPass ); } @@ -3533,4 +4446,31 @@ bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ) { return resumedMainRendering; } +void VK_ShadowMap_CommitClassicInteractionView( + const classicInteractionDomainView_t *view ) { + if ( view == NULL || !vkClassicShadowTransaction.active + || !vkClassicShadowTransaction.ready + || vkClassicShadowTransaction.view != view ) { + return; + } + if ( view->shadowMapPassCount == 0 ) { + vkClassicShadowTransaction.active = false; + vkClassicShadowTransaction.ready = false; + vkClassicShadowTransaction.view = NULL; + return; + } + + // Every command/resource/descriptor decision was retained by Preflight. + // RenderAtlas recognizes this transaction and replaces every legacy caster + // walk with the sealed caster/alpha plans above. + const bool committed = VK_ShadowMap_RenderAtlas( view->viewDef ); + if ( !committed ) { + common->FatalError( + "Vulkan: preflighted classic shadow transaction could not commit" ); + } + vkClassicShadowTransaction.active = false; + vkClassicShadowTransaction.ready = false; + vkClassicShadowTransaction.view = NULL; +} + #endif /* OPENQ4_RENDERER_VK_MODULE */ diff --git a/src/renderer/Vulkan/vk_ShadowMap.h b/src/renderer/Vulkan/vk_ShadowMap.h index 5d93747b..c0a7ecff 100644 --- a/src/renderer/Vulkan/vk_ShadowMap.h +++ b/src/renderer/Vulkan/vk_ShadowMap.h @@ -6,6 +6,8 @@ #include "../ShadowMapProjected.h" +typedef struct classicInteractionDomainView_s classicInteractionDomainView_t; + /* =============================================================================== @@ -71,6 +73,8 @@ typedef struct vkShadowLightState_s { shadowMapProjectedLightState_t projectedState; // full shared CSM state (projected lights only) float invAtlasSize[ 2 ]; + float constantBias; // point-light effective normalized receiver bias + float normalBias; // point-light effective normalized slope bias float texelDepthBias; // point-light receiver value float normalOffsetWorld; // point: per-distance texel factor vkShadowPassState_t passes[ VK_SHADOW_RECEIVER_PASS_COUNT ]; @@ -89,6 +93,18 @@ int VK_ShadowMap_PrepareViewLights( const viewDef_t *viewDef, // LOAD bool VK_ShadowMap_RenderAtlas( const viewDef_t *viewDef ); +// Shared fixed-classic ownership is a whole-view transaction. Preflight may +// allocate private images/descriptors and reserve cache entries, but it emits +// no attachment writes. Commit consumes only the sealed domain records and is +// structurally infallible; Abort releases every private reservation so the +// unchanged classic interaction walker can own the view instead. +bool VK_ShadowMap_PreflightClassicInteractionView( + const classicInteractionDomainView_t *view ); +void VK_ShadowMap_CommitClassicInteractionView( + const classicInteractionDomainView_t *view ); +void VK_ShadowMap_AbortClassicInteractionView( + const classicInteractionDomainView_t *view ); + // prepared state for a light, or NULL when retained stencil handles it const vkShadowLightState_t *VK_ShadowMap_LightState( const viewLight_t *vLight ); diff --git a/src/renderer/draw_arb2.cpp b/src/renderer/draw_arb2.cpp index 8111f7cb..cc88ded5 100644 --- a/src/renderer/draw_arb2.cpp +++ b/src/renderer/draw_arb2.cpp @@ -29,6 +29,8 @@ If you have questions concerning this license or the applicable additional terms +#include + #include "tr_local.h" #include "CelShading.h" #include "Model_local.h" @@ -37,6 +39,8 @@ If you have questions concerning this license or the applicable additional terms #include "ShadowMapArb2Parity.h" #include "ModernShadowPlanner.h" #include "ModernClusteredLighting.h" +#include "ClassicInteractionDomain.h" +#include "RendererMetrics.h" #include "cg_explicit.h" #include @@ -2302,6 +2306,7 @@ typedef struct { int cascadeCount; int lastUsedFrame; int lastUpdatedFrame; + std::uint64_t atlasStorageGeneration; projectedShadowMapState_t state; // tile placement inside the shared projected atlas: cell coordinates in // the atlas grid and the block span (1 for single lights, atlasDiv for @@ -2320,13 +2325,38 @@ typedef struct { int size; bool highPrecision; bool depthCompare; + // Receiver projection identity used when this cube was rendered. Budget + // stale-reuse may intentionally ignore the full cache signature, but it + // must never decode an old cube from a moved/rescaled point light with the + // current light's direction vectors or far plane. + float lightOrigin[3]; + float farDistance; int lastUsedFrame; int lastUpdatedFrame; idImage * colorImage; idImage * depthImage; idRenderTexture * renderTexture; + std::uint64_t colorStorageGeneration; + std::uint64_t depthStorageGeneration; } pointShadowMapCacheEntry_t; +// Shared-domain ownership prepares every mapped pass before the first write to +// the main framebuffer. The legacy scratch images are intentionally singular, +// so they cannot retain two non-cacheable (dynamic or perforated) maps until +// the later receiver commit. These transaction-local slots provide the same +// scratch semantics with a distinct physical image per live pass. Slots are +// reused across views and only allocate images which a view actually needs. +typedef struct { + idImage * depthImage; + idRenderTexture * renderTexture; +} sharedInteractionProjectedScratch_t; + +typedef struct { + idImage * colorImage; + idImage * depthImage; + idRenderTexture * renderTexture; +} sharedInteractionPointScratch_t; + typedef struct { bool valid; int lightIndex; @@ -2346,7 +2376,14 @@ typedef struct { static projectedShadowMapCacheEntry_t g_projectedShadowMapCache[SHADOWMAP_CACHE_MAX_SLOTS]; static pointShadowMapCacheEntry_t g_pointShadowMapCache[SHADOWMAP_CACHE_MAX_SLOTS]; +static sharedInteractionProjectedScratch_t + g_sharedInteractionProjectedScratch[SHADOWMAP_CACHE_MAX_SLOTS]; +static sharedInteractionPointScratch_t + g_sharedInteractionPointScratch[SHADOWMAP_CACHE_MAX_SLOTS]; static shadowMapLightHistory_t g_shadowMapLightHistory[SHADOWMAP_LIGHT_HISTORY_SLOTS]; +static const idRenderWorldLocal * g_shadowMapCacheRenderWorld = NULL; +static unsigned int g_shadowMapCacheMapFileCRC = 0; +static int g_shadowMapCacheMapNameHash = 0; static projectedShadowMapCacheEntry_t * g_activeProjectedShadowMapCache = NULL; static pointShadowMapCacheEntry_t * g_activePointShadowMapCache = NULL; static const int SHADOWMAP_GPU_TIMER_QUERY_SLOTS = 64; @@ -2363,6 +2400,13 @@ typedef enum { SHADOWMAP_GLOBAL_MAPPED_POINT } shadowMapGlobalMapped_t; static shadowMapGlobalMapped_t g_shadowMapGlobalPassMapped = SHADOWMAP_GLOBAL_MAPPED_NONE; +static bool g_shadowMapGlobalPassHybrid = false; +// A translucent-only GLOBAL ownership prepares the resource in the opaque +// light walk but cannot claim a mapped result until its ordered receiver draw +// actually succeeds. Keep that result pending so diagnostics match the final +// mapped-or-stencil presentation rather than the resource preparation alone. +static bool g_shadowMapDeferredGlobalReportPending = false; +static shadowMapPassResult_t g_shadowMapDeferredGlobalPassResult = SHADOWMAP_PASS_RESULT_MAPPED; // Importance-ordered update admissions (M4): when r_shadowMapMaxUpdatesPerView // constrains the frame, the budget goes to the most important stale lights @@ -2737,8 +2781,64 @@ static void RB_ShadowMapFillTextureBinding( rendererShadowTextureBinding_t &bind } } +// A point cube is not a shared atlas: the modern GL shader has one +// samplerCube and must see the exact cache image which the classic backend +// most recently selected. Allocation alone is not evidence that the image +// contains current, globally complete shadow data. +static bool RB_ShadowMapActivePointCacheContentReady( void ) { + pointShadowMapCacheEntry_t *entry = g_activePointShadowMapCache; + if ( entry == NULL || !entry->valid ) { + return false; + } + + int slotIndex = -1; + for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; ++i ) { + if ( entry == &g_pointShadowMapCache[i] ) { + slotIndex = i; + break; + } + } + const int slotLimit = idMath::ClampInt( 0, SHADOWMAP_CACHE_MAX_SLOTS, r_shadowMapPointCacheSize.GetInteger() ); + if ( slotIndex < 0 || slotIndex >= slotLimit + || entry->passKind != static_cast( SHADOWMAP_PASS_GLOBAL ) + || entry->lastUpdatedFrame < 0 || entry->lastUpdatedFrame > tr.frameCount + || entry->size != RB_ShadowMapPointSizeValue() + || entry->highPrecision != RB_PointShadowMapHighPrecisionEnabled() + || entry->depthCompare != RB_PointShadowMapDepthCompareEnabled() + || !( entry->farDistance > 0.0f && entry->farDistance < 1.0e30f ) + || FLOAT_IS_NAN( entry->lightOrigin[0] ) + || FLOAT_IS_NAN( entry->lightOrigin[1] ) + || FLOAT_IS_NAN( entry->lightOrigin[2] ) + || idMath::Fabs( entry->lightOrigin[0] ) >= 1.0e30f + || idMath::Fabs( entry->lightOrigin[1] ) >= 1.0e30f + || idMath::Fabs( entry->lightOrigin[2] ) >= 1.0e30f + || entry->colorImage == NULL || entry->depthImage == NULL || entry->renderTexture == NULL + || entry->colorImage != g_pointShadowMapColorImage + || entry->depthImage != g_pointShadowMapDepthImage + || entry->renderTexture != g_pointShadowMapRenderTexture + || entry->colorStorageGeneration == 0 + || entry->depthStorageGeneration == 0 + || entry->colorImage->GetStorageGeneration() != entry->colorStorageGeneration + || entry->depthImage->GetStorageGeneration() != entry->depthStorageGeneration + || entry->colorImage->IsDefaulted() || entry->depthImage->IsDefaulted() + || entry->colorImage->GetOpts().textureType != TT_CUBIC + || entry->depthImage->GetOpts().textureType != TT_CUBIC + || entry->colorImage->GetOpts().format + != ( entry->highPrecision ? FMT_RGBA16F : FMT_RGBA8 ) + || entry->depthImage->GetOpts().format != FMT_DEPTH + || entry->renderTexture->GetWidth() != entry->size + || entry->renderTexture->GetHeight() != entry->size ) { + return false; + } + + return entry->colorImage->IsLoaded() && entry->colorImage->GetDeviceHandle() != 0 + && entry->depthImage->IsLoaded() && entry->depthImage->GetDeviceHandle() != 0; +} + bool RB_ShadowMapTextureBindings( rendererShadowTextureBindings_t &bindings ) { memset( &bindings, 0, sizeof( bindings ) ); + bindings.pointAtlasLightIndex = -1; + bindings.pointAtlasContentFrame = -1; bindings.projectedDepthCompare = RB_ShadowMapDepthCompareEnabled(); bindings.pointDepthCompare = RB_PointShadowMapDepthCompareEnabled(); bindings.pointHighPrecision = RB_PointShadowMapHighPrecisionEnabled(); @@ -2769,7 +2869,11 @@ bool RB_ShadowMapTextureBindings( rendererShadowTextureBindings_t &bindings ) { g_shadowMapAtlasDepthImage != NULL && persistentWidth > 0 && persistentHeight > 0 ); bindings.projectedPersistentAtlasReady = bindings.projectedPersistentAtlas.ready; - idImage *pointAtlasImage = ( bindings.pointDepthCompare && g_pointShadowMapDepthImage != NULL ) ? g_pointShadowMapDepthImage : g_pointShadowMapColorImage; + // The modern GL programs use a regular samplerCube and decode the manual + // color-depth encoding. Keep that binding independent of the classic + // receiver's hardware-compare choice; sampling the depth cube as packed RG + // corrupts the decoded value when r_shadowMapPointDepthCompare is enabled. + idImage *pointAtlasImage = g_pointShadowMapColorImage; const int pointWidth = g_pointShadowMapRenderTexture != NULL ? g_pointShadowMapRenderTexture->GetWidth() : ( pointAtlasImage != NULL ? pointAtlasImage->GetUploadWidth() : 0 ); const int pointHeight = g_pointShadowMapRenderTexture != NULL ? g_pointShadowMapRenderTexture->GetHeight() : ( pointAtlasImage != NULL ? pointAtlasImage->GetUploadHeight() : 0 ); RB_ShadowMapFillTextureBinding( @@ -2778,8 +2882,13 @@ bool RB_ShadowMapTextureBindings( rendererShadowTextureBindings_t &bindings ) { GL_TEXTURE_CUBE_MAP, pointWidth, pointHeight, - pointAtlasImage != NULL && pointWidth > 0 && pointHeight > 0 ); + RB_ShadowMapActivePointCacheContentReady() && pointWidth > 0 && pointHeight > 0 ); bindings.pointAtlasReady = bindings.pointAtlas.ready; + if ( bindings.pointAtlasReady ) { + bindings.pointAtlasLightIndex = g_activePointShadowMapCache->lightIndex; + bindings.pointAtlasSignature = g_activePointShadowMapCache->signature; + bindings.pointAtlasContentFrame = g_activePointShadowMapCache->lastUpdatedFrame; + } const bool projectedMomentsEnabled = RB_ProjectedTranslucentShadowEnabled(); const bool pointMomentsEnabled = RB_PointTranslucentShadowEnabled(); @@ -2891,23 +3000,35 @@ bool RB_ShadowMapResourcesKnownGood( bool pointLight ) { // Shadow casters render in light space, where the mirrored-view logic in // GL_Cull does not apply. idTech4 winding makes the engine-front (visible) -// side of a front-sided material the GL_BACK face, so: -// r_shadowMapCasterCulling 0: two-sided (no culling) - full coverage, most acne -// r_shadowMapCasterCulling 1: store light-facing engine-front faces (cull GL_FRONT) -// r_shadowMapCasterCulling 2: store engine-back faces (cull GL_BACK, the -// long-standing default here) - hides acne on closed meshes at the cost -// of slight detachment on thin geometry -// Material twoSided/backSided cull types are always honored per surface; -// previously every caster was drawn one-sided regardless of material, which -// dropped foliage/grate/curtain casters from the map entirely. +// side of a front-sided material the GL_BACK face, so culling GL_FRONT stores +// the light-facing, near shell. The automatic mode only does that for sealed +// hulls. Open, non-manifold, or otherwise uncertain geometry is rendered +// two-sided: either authored winding can then contribute and props cannot +// silently disappear from the depth map. Storing the far shell is deliberately +// avoided because mesh thickness turns into contact-shadow detachment. +// r_shadowMapCasterCulling 0: always two-sided +// r_shadowMapCasterCulling 1: always store the light-facing near shell +// r_shadowMapCasterCulling 2: automatic (near shell for perfect hulls whose +// bounds do not contain the light and whose transform preserves winding, +// two-sided otherwise) +// The authored material orientation is honored whenever one-sided culling is +// active. Mode 0 and AUTO's conservative fallbacks intentionally override it. static int g_shadowMapCasterCullApplied = -1; // 0 = culling disabled, else the GLenum face -static void RB_ShadowMapApplyCasterCull( const idMaterial *shader ) { +static void RB_ShadowMapApplyCasterCull( const idMaterial *shader, + const srfTriangles_t *casterGeo = NULL, + const float *modelMatrix = NULL ) { const int mode = idMath::ClampInt( 0, 2, r_shadowMapCasterCulling.GetInteger() ); const int materialCull = ( shader != NULL ) ? shader->GetCullType() : CT_FRONT_SIDED; + const bool automaticTwoSided = mode == 2 + && ( casterGeo == NULL || !casterGeo->perfectHull + || R_ShadowMapCasterTransformNeedsTwoSided( modelMatrix ) + || R_ShadowMapLightOriginInsideCasterBounds( backEnd.vLight, + modelMatrix, casterGeo->bounds[0].ToFloatPtr(), + casterGeo->bounds[1].ToFloatPtr() ) ); int desired = 0; - if ( mode != 0 && materialCull != CT_TWO_SIDED ) { - GLenum cullFace = ( mode == 1 ) ? GL_FRONT : GL_BACK; + if ( mode != 0 && !automaticTwoSided && materialCull != CT_TWO_SIDED ) { + GLenum cullFace = GL_FRONT; if ( materialCull == CT_BACK_SIDED ) { cullFace = ( cullFace == GL_FRONT ) ? GL_BACK : GL_FRONT; } @@ -3116,6 +3237,8 @@ typedef struct shadowMapTimedPhase_s { idTimer cpuTimer; idTimer gpuSyncTimer; shadowMapGpuTimerQuery_t *gpuTimerQuery; + rendererGpuTimerSlot_t parentGpuTimerSlot; + bool parentGpuTimerPaused; bool active; } shadowMapTimedPhase_t; @@ -3124,13 +3247,34 @@ static void RB_ShadowMapBeginTimedPhase( shadowMapTimedPhase_t &timedPhase, cons timedPhase.cpuTimer.Clear(); timedPhase.gpuSyncTimer.Clear(); timedPhase.gpuTimerQuery = NULL; + timedPhase.parentGpuTimerSlot = RENDERER_GPU_TIMER_COUNT; + timedPhase.parentGpuTimerPaused = false; timedPhase.active = true; timedPhase.cpuTimer.Start(); if ( r_shadowMapGpuSyncTimings.GetBool() ) { glFinish(); timedPhase.gpuSyncTimer.Start(); } - timedPhase.gpuTimerQuery = RB_ShadowMapBeginGpuTimerQuery( vLight, passKind, phase ); + // GL_TIME_ELAPSED queries cannot be nested. Renderer metrics normally owns + // the outer DRAW3D query while shadow-map phase metrics need a finer-grained + // query, so split the parent interval around the shadow query. + if ( RB_ShadowMapGpuTimerQueriesAvailable() ) { + for ( int slot = 0; slot < RENDERER_GPU_TIMER_COUNT; slot++ ) { + const rendererGpuTimerSlot_t timerSlot = + static_cast( slot ); + if ( R_RendererMetrics_PauseGpuTimer( timerSlot ) ) { + timedPhase.parentGpuTimerSlot = timerSlot; + timedPhase.parentGpuTimerPaused = true; + break; + } + } + timedPhase.gpuTimerQuery = + RB_ShadowMapBeginGpuTimerQuery( vLight, passKind, phase ); + if ( timedPhase.gpuTimerQuery == NULL && timedPhase.parentGpuTimerPaused ) { + R_RendererMetrics_ResumeGpuTimer( timedPhase.parentGpuTimerSlot, true ); + timedPhase.parentGpuTimerPaused = false; + } + } } static float RB_ShadowMapEndTimedPhase( shadowMapTimedPhase_t &timedPhase ) { @@ -3139,6 +3283,10 @@ static float RB_ShadowMapEndTimedPhase( shadowMapTimedPhase_t &timedPhase ) { } RB_ShadowMapEndGpuTimerQuery( timedPhase.gpuTimerQuery ); + if ( timedPhase.parentGpuTimerPaused ) { + R_RendererMetrics_ResumeGpuTimer( timedPhase.parentGpuTimerSlot, true ); + timedPhase.parentGpuTimerPaused = false; + } if ( r_shadowMapGpuSyncTimings.GetBool() ) { glFinish(); timedPhase.gpuSyncTimer.Stop(); @@ -3254,13 +3402,16 @@ bool RB_ShadowMapBuildArb2ParityState( const viewLight_t *vLight, const viewDef_ return true; } -static void RB_ShadowMapSelectScratchResources( void ) { +static void RB_ShadowMapSelectProjectedScratchResources( void ) { g_activeProjectedShadowMapCache = NULL; - g_activePointShadowMapCache = NULL; g_shadowMapDepthImage = g_shadowMapScratchDepthImage; g_shadowMapRenderTexture = g_shadowMapScratchRenderTexture; g_shadowMapActiveSlotOriginX = 0; g_shadowMapActiveSlotOriginY = 0; +} + +static void RB_ShadowMapSelectPointScratchResources( void ) { + g_activePointShadowMapCache = NULL; g_pointShadowMapColorImage = g_pointShadowMapScratchColorImage; g_pointShadowMapDepthImage = g_pointShadowMapScratchDepthImage; g_pointShadowMapRenderTexture = g_pointShadowMapScratchRenderTexture; @@ -3280,9 +3431,19 @@ static int RB_ShadowMapHashFloat( int hash, const float value ) { return RB_ShadowMapHashInt( hash, idMath::Ftoi( value * 1024.0f ) ); } +static int RB_ShadowMapMapNameHash( const viewDef_t *viewDef ) { + return ( viewDef != NULL && viewDef->renderWorld != NULL ) + ? idStr::Hash( viewDef->renderWorld->mapName.c_str() ) : 0; +} + static int RB_ShadowMapBuildPassSignatureForView( const viewLight_t *vLight, const viewDef_t *viewDef, const shadowMapPassKind_t passKind, const bool pointLight ) { int hash = static_cast( 2166136261u ); const int cascadeCount = RB_ShadowMapCascadeCountForLight( vLight ); + const idRenderWorldLocal *renderWorld = + viewDef != NULL ? viewDef->renderWorld : NULL; + hash = RB_ShadowMapHashInt( hash, + renderWorld != NULL ? static_cast( renderWorld->mapFileCRC ) : 0 ); + hash = RB_ShadowMapHashInt( hash, RB_ShadowMapMapNameHash( viewDef ) ); hash = RB_ShadowMapHashInt( hash, RB_ShadowMapLightIndex( vLight ) ); hash = RB_ShadowMapHashInt( hash, static_cast( passKind ) ); hash = RB_ShadowMapHashInt( hash, static_cast( RB_ShadowMapLightClass( vLight ) ) ); @@ -3292,6 +3453,9 @@ static int RB_ShadowMapBuildPassSignatureForView( const viewLight_t *vLight, con hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapStaticCasterCount : 0 ); hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapDynamicCasterCount : 0 ); hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapCasterSignature : 0 ); + hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapIncompleteMapMask : 0 ); + hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapHybridIncompleteMask : 0 ); + hash = RB_ShadowMapHashInt( hash, vLight != NULL ? vLight->shadowMapPrelightMapMissingMask : 0 ); hash = RB_ShadowMapHashInt( hash, cascadeCount ); hash = RB_ShadowMapHashInt( hash, RB_ShadowMapHashedAlphaEnabled() ? 1 : 0 ); hash = RB_ShadowMapHashInt( hash, r_shadowMapStableAlphaHash.GetBool() ? 1 : 0 ); @@ -3303,6 +3467,13 @@ static int RB_ShadowMapBuildPassSignatureForView( const viewLight_t *vLight, con hash = RB_ShadowMapHashInt( hash, RB_PointShadowMapHighPrecisionEnabled() ? 1 : 0 ); hash = RB_ShadowMapHashInt( hash, RB_PointShadowMapDepthCompareEnabled() ? 1 : 0 ); hash = RB_ShadowMapHashFloat( hash, r_shadowMapPointFarScale.GetFloat() ); + hash = RB_ShadowMapHashFloat( hash, R_ShadowMapPointFarDistance( vLight ) ); + if ( vLight != NULL && vLight->lightDef != NULL ) { + for ( int i = 0; i < 3; ++i ) { + hash = RB_ShadowMapHashFloat( hash, + vLight->lightDef->parms.lightCenter[i] ); + } + } } else { hash = RB_ShadowMapHashInt( hash, RB_ShadowMapTileSizeForLight( vLight ) ); hash = RB_ShadowMapHashInt( hash, RB_ShadowMapAtlasDivForLight( vLight ) ); @@ -3401,6 +3572,49 @@ static void RB_ShadowMapInvalidateLightCaches( const int lightIndex ) { } } +// Cache keys are only meaningful inside one loaded render world. Light +// indices are reused between maps, and signature-agnostic budget/subview +// reuse must never expose a tile produced for an earlier world. +bool RB_ShadowMapPrepareCacheView( const viewDef_t *viewDef ) { + if ( viewDef == NULL ) { + return false; + } + const idRenderWorldLocal *renderWorld = viewDef->renderWorld; + const unsigned int mapFileCRC = + renderWorld != NULL ? renderWorld->mapFileCRC : 0; + const int mapNameHash = RB_ShadowMapMapNameHash( viewDef ); + if ( g_shadowMapCacheRenderWorld == renderWorld + && g_shadowMapCacheMapFileCRC == mapFileCRC + && g_shadowMapCacheMapNameHash == mapNameHash ) { + return false; + } + + for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { + g_projectedShadowMapCache[i].valid = false; + g_pointShadowMapCache[i].valid = false; + } + memset( g_shadowMapLightHistory, 0, sizeof( g_shadowMapLightHistory ) ); + g_activeProjectedShadowMapCache = NULL; + g_activePointShadowMapCache = NULL; + g_shadowMapDepthImage = NULL; + g_shadowMapRenderTexture = NULL; + g_shadowMapActiveSlotOriginX = 0; + g_shadowMapActiveSlotOriginY = 0; + g_pointShadowMapColorImage = NULL; + g_pointShadowMapDepthImage = NULL; + g_pointShadowMapRenderTexture = NULL; + memset( &g_projectedShadowMapState, 0, sizeof( g_projectedShadowMapState ) ); + g_projectedTranslucentShadowPassReady = false; + g_pointTranslucentShadowPassReady = false; + g_shadowMapGlobalPassMapped = SHADOWMAP_GLOBAL_MAPPED_NONE; + g_shadowMapGlobalPassHybrid = false; + g_shadowMapDeferredGlobalReportPending = false; + g_shadowMapCacheRenderWorld = renderWorld; + g_shadowMapCacheMapFileCRC = mapFileCRC; + g_shadowMapCacheMapNameHash = mapNameHash; + return true; +} + static bool RB_ShadowMapStaticCacheable( const viewLight_t *vLight, const shadowMapPassKind_t passKind, const bool pointLight, const bool haveTranslucentCasters ) { if ( !r_shadowMapStaticCache.GetBool() || vLight == NULL || RB_ShadowMapLightIndex( vLight ) < 0 ) { return false; @@ -3411,7 +3625,11 @@ static bool RB_ShadowMapStaticCacheable( const viewLight_t *vLight, const shadow // draws instead of a full static-scene re-render. Point lights keep the // old rule until the cube path gains composition (phase 5c). const bool dynamicsDefeatCache = pointLight && vLight->shadowMapDynamicCasterCount > 0; - if ( haveTranslucentCasters || dynamicsDefeatCache || vLight->shadowMapCasterCount <= 0 ) { + // Alpha-tested stages can animate conditions, texture matrices, and images + // without changing the coarse front-end caster signature. Render them live + // instead of reusing stale cutout depth. + if ( haveTranslucentCasters || vLight->shadowMapAlphaCasterCount > 0 + || dynamicsDefeatCache || vLight->shadowMapCasterCount <= 0 ) { const int lightIndex = RB_ShadowMapLightIndex( vLight ); shadowMapLightHistory_t *history = RB_ShadowMapFindLightHistory( lightIndex ); if ( history != NULL && vLight->shadowMapDynamicCasterCount > 0 ) { @@ -3441,7 +3659,9 @@ static bool RB_ShadowMapStaticCacheableReadOnly( const viewLight_t *vLight, cons if ( !r_shadowMapStaticCache.GetBool() || vLight == NULL || RB_ShadowMapLightIndex( vLight ) < 0 ) { return false; } - if ( haveTranslucentCasters || ( pointLight && vLight->shadowMapDynamicCasterCount > 0 ) || vLight->shadowMapCasterCount <= 0 ) { + if ( haveTranslucentCasters || vLight->shadowMapAlphaCasterCount > 0 + || ( pointLight && vLight->shadowMapDynamicCasterCount > 0 ) + || vLight->shadowMapCasterCount <= 0 ) { return false; } if ( !pointLight && RB_ShadowMapCascadeCountForLight( vLight ) > 1 && !r_shadowMapCacheCSM.GetBool() ) { @@ -3493,50 +3713,153 @@ static double RB_ShadowMapResidentBytes( void ) { return bytes; } +static bool RB_ShadowMapProjectedCacheEntryStorageValid( + const projectedShadowMapCacheEntry_t *entry ); +static bool RB_ShadowMapPointCacheEntryStorageValid( + const pointShadowMapCacheEntry_t *entry ); + static void RB_ShadowMapExpireCaches( void ) { const int residentFrames = Max( 1, r_shadowMapResidentFrames.GetInteger() ); + const int projectedLimit = RB_ShadowMapProjectedCacheSlotLimit(); + const int pointLimit = RB_ShadowMapPointCacheSlotLimit(); for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { - if ( g_projectedShadowMapCache[i].valid && tr.frameCount - g_projectedShadowMapCache[i].lastUsedFrame > residentFrames ) { + // Cache-size cvars are live. Entries outside a reduced limit must stop + // occupying atlas cells immediately or in-range allocation can evict + // forever without finding a block that the stale metadata still marks. + if ( g_projectedShadowMapCache[i].valid + && ( !RB_ShadowMapProjectedCacheEntryStorageValid( + &g_projectedShadowMapCache[i] ) + || i >= projectedLimit + || tr.frameCount - g_projectedShadowMapCache[i].lastUsedFrame + > residentFrames ) ) { g_projectedShadowMapCache[i].valid = false; g_shadowMapStats.cacheExpired++; } - if ( g_pointShadowMapCache[i].valid && tr.frameCount - g_pointShadowMapCache[i].lastUsedFrame > residentFrames ) { + if ( g_pointShadowMapCache[i].valid + && ( !RB_ShadowMapPointCacheEntryStorageValid( + &g_pointShadowMapCache[i] ) + || i >= pointLimit + || tr.frameCount - g_pointShadowMapCache[i].lastUsedFrame + > residentFrames ) ) { g_pointShadowMapCache[i].valid = false; g_shadowMapStats.cacheExpired++; } } } -// signature-agnostic lookups back the budget stale-reuse path: when the -// per-view update budget is exhausted, the light's last rendered map (and the -// receiver state it was rendered with) is reused as-is +// Persistent cache metadata is usable only while it still names the physical +// atlas allocation that received the render. Image reload/recreation can keep +// the idImage pointer stable while replacing its storage. +static bool RB_ShadowMapProjectedCacheEntryStorageValid( + const projectedShadowMapCacheEntry_t *entry ) { + return entry != NULL && entry->valid + && entry->atlasStorageGeneration != 0 + && g_shadowMapAtlasDepthImage != NULL + && g_shadowMapAtlasRenderTexture != NULL + && g_shadowMapAtlasDepthImage->GetStorageGeneration() + == entry->atlasStorageGeneration + && g_shadowMapAtlasDepthImage->IsLoaded() + && !g_shadowMapAtlasDepthImage->IsDefaulted() + && g_shadowMapAtlasDepthImage->GetDeviceHandle() != 0 + && g_shadowMapAtlasDepthImage->GetOpts().textureType == TT_2D + && g_shadowMapAtlasDepthImage->GetOpts().format == FMT_DEPTH + && g_shadowMapAtlasRenderTexture->GetWidth() > 0 + && g_shadowMapAtlasRenderTexture->GetHeight() > 0; +} + static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntryAnySignature( const int lightIndex, const shadowMapPassKind_t passKind ) { const int slotLimit = RB_ShadowMapProjectedCacheSlotLimit(); + projectedShadowMapCacheEntry_t *newest = NULL; for ( int i = 0; i < slotLimit; i++ ) { projectedShadowMapCacheEntry_t *entry = &g_projectedShadowMapCache[i]; - if ( entry->valid && entry->lightIndex == lightIndex && entry->passKind == static_cast( passKind ) ) { - return entry; + if ( RB_ShadowMapProjectedCacheEntryStorageValid( entry ) + && entry->lightIndex == lightIndex + && entry->passKind == static_cast( passKind ) + && ( newest == NULL + || entry->lastUpdatedFrame > newest->lastUpdatedFrame ) ) { + newest = entry; } } - return NULL; + return newest; +} + +static bool RB_ShadowMapPointCacheEntryStorageValid( + const pointShadowMapCacheEntry_t *entry ) { + return entry != NULL && entry->valid + && entry->size > 0 + && entry->colorImage != NULL && entry->depthImage != NULL + && entry->renderTexture != NULL + && entry->colorStorageGeneration != 0 + && entry->depthStorageGeneration != 0 + && entry->colorImage->GetStorageGeneration() + == entry->colorStorageGeneration + && entry->depthImage->GetStorageGeneration() + == entry->depthStorageGeneration + && entry->colorImage->IsLoaded() + && !entry->colorImage->IsDefaulted() + && entry->colorImage->GetDeviceHandle() != 0 + && entry->depthImage->IsLoaded() + && !entry->depthImage->IsDefaulted() + && entry->depthImage->GetDeviceHandle() != 0 + && entry->colorImage->GetOpts().textureType == TT_CUBIC + && entry->depthImage->GetOpts().textureType == TT_CUBIC + && entry->colorImage->GetOpts().format + == ( entry->highPrecision ? FMT_RGBA16F : FMT_RGBA8 ) + && entry->depthImage->GetOpts().format == FMT_DEPTH + && entry->renderTexture->GetWidth() == entry->size + && entry->renderTexture->GetHeight() == entry->size; +} + +// Signature-agnostic lookups back the budget stale-reuse path: when the +// per-view update budget is exhausted, the light's last rendered map (and the +// receiver state it was rendered with) is reused as-is. Point storage modes +// still have to match because stale reuse intentionally ignores their hash. +static bool RB_ShadowMapPointCacheEntryProjectionMatches( + const pointShadowMapCacheEntry_t *entry, const viewLight_t *vLight ) { + if ( entry == NULL || vLight == NULL ) { + return false; + } + // Exact float identity is intentional. These values are copied directly + // from the light when the cube is rendered; rejecting an uncertain stale + // reuse is cheaper and safer than decoding it with a different projection. + return entry->farDistance == R_ShadowMapPointFarDistance( vLight ) + && entry->lightOrigin[0] == vLight->globalLightOrigin[0] + && entry->lightOrigin[1] == vLight->globalLightOrigin[1] + && entry->lightOrigin[2] == vLight->globalLightOrigin[2]; } -static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntryAnySignature( const int lightIndex, const shadowMapPassKind_t passKind ) { +static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntryAnySignature( const viewLight_t *vLight, const shadowMapPassKind_t passKind ) { const int slotLimit = RB_ShadowMapPointCacheSlotLimit(); + const int lightIndex = RB_ShadowMapLightIndex( vLight ); + const int requiredSize = RB_ShadowMapPointSizeValue(); + const bool requiredHighPrecision = RB_PointShadowMapHighPrecisionEnabled(); + const bool requiredDepthCompare = RB_PointShadowMapDepthCompareEnabled(); + pointShadowMapCacheEntry_t *newest = NULL; for ( int i = 0; i < slotLimit; i++ ) { pointShadowMapCacheEntry_t *entry = &g_pointShadowMapCache[i]; - if ( entry->valid && entry->lightIndex == lightIndex && entry->passKind == static_cast( passKind ) ) { - return entry; + if ( RB_ShadowMapPointCacheEntryStorageValid( entry ) + && entry->lightIndex == lightIndex + && entry->passKind == static_cast( passKind ) + && entry->size == requiredSize + && entry->highPrecision == requiredHighPrecision + && entry->depthCompare == requiredDepthCompare + && RB_ShadowMapPointCacheEntryProjectionMatches( entry, vLight ) + && ( newest == NULL + || entry->lastUpdatedFrame > newest->lastUpdatedFrame ) ) { + newest = entry; } } - return NULL; + return newest; } static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntry( const int lightIndex, const shadowMapPassKind_t passKind, const int signature ) { const int slotLimit = RB_ShadowMapProjectedCacheSlotLimit(); for ( int i = 0; i < slotLimit; i++ ) { projectedShadowMapCacheEntry_t *entry = &g_projectedShadowMapCache[i]; - if ( entry->valid && entry->lightIndex == lightIndex && entry->passKind == static_cast( passKind ) && entry->signature == signature ) { + if ( RB_ShadowMapProjectedCacheEntryStorageValid( entry ) + && entry->lightIndex == lightIndex + && entry->passKind == static_cast( passKind ) + && entry->signature == signature ) { return entry; } } @@ -3547,7 +3870,10 @@ static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntry( const int li const int slotLimit = RB_ShadowMapPointCacheSlotLimit(); for ( int i = 0; i < slotLimit; i++ ) { pointShadowMapCacheEntry_t *entry = &g_pointShadowMapCache[i]; - if ( entry->valid && entry->lightIndex == lightIndex && entry->passKind == static_cast( passKind ) && entry->signature == signature ) { + if ( RB_ShadowMapPointCacheEntryStorageValid( entry ) + && entry->lightIndex == lightIndex + && entry->passKind == static_cast( passKind ) + && entry->signature == signature ) { return entry; } } @@ -3559,12 +3885,24 @@ static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntry( const int li // from the valid entries on every allocation. static const int SHADOWMAP_ATLAS_MAX_GRID = 8; +static int RB_ShadowMapAtlasSizeValue( void ) { + const int maxTextureSize = + glConfig.maxTextureSize > 0 ? glConfig.maxTextureSize : 4096; + return Min( idMath::ClampInt( 2048, 8192, + r_shadowMapAtlasSize.GetInteger() ), maxTextureSize ); +} + static int RB_ShadowMapAtlasGridDim( void ) { if ( g_shadowMapAtlasCellSize <= 0 ) { return 0; } - const int atlasSize = idMath::ClampInt( 2048, 8192, r_shadowMapAtlasSize.GetInteger() ); - return idMath::ClampInt( 1, SHADOWMAP_ATLAS_MAX_GRID, atlasSize / g_shadowMapAtlasCellSize ); + const int atlasSize = g_shadowMapAtlasRenderTexture != NULL + ? Min( g_shadowMapAtlasRenderTexture->GetWidth(), + g_shadowMapAtlasRenderTexture->GetHeight() ) + : RB_ShadowMapAtlasSizeValue(); + const int gridDim = atlasSize / g_shadowMapAtlasCellSize; + return gridDim > 0 + ? idMath::ClampInt( 1, SHADOWMAP_ATLAS_MAX_GRID, gridDim ) : 0; } static bool RB_ShadowMapAtlasFindFreeBlock( const int span, int &cellX, int &cellY ) { @@ -3576,7 +3914,7 @@ static bool RB_ShadowMapAtlasFindFreeBlock( const int span, int &cellX, int &cel memset( occupied, 0, sizeof( occupied ) ); for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { const projectedShadowMapCacheEntry_t &entry = g_projectedShadowMapCache[i]; - if ( !entry.valid ) { + if ( !RB_ShadowMapProjectedCacheEntryStorageValid( &entry ) ) { continue; } for ( int y = entry.atlasCellY; y < entry.atlasCellY + entry.atlasCellSpan && y < gridDim; y++ ) { @@ -3606,7 +3944,8 @@ static bool RB_ShadowMapAtlasFindFreeBlock( const int span, int &cellX, int &cel return false; } -static projectedShadowMapCacheEntry_t *RB_ShadowMapAllocProjectedCacheEntry( const int cellSpan ) { +static projectedShadowMapCacheEntry_t *RB_ShadowMapAllocProjectedCacheEntry( + const int cellSpan, const bool allowEviction = true ) { const int slotLimit = RB_ShadowMapProjectedCacheSlotLimit(); if ( slotLimit <= 0 || g_shadowMapAtlasCellSize <= 0 ) { return NULL; @@ -3637,6 +3976,9 @@ static projectedShadowMapCacheEntry_t *RB_ShadowMapAllocProjectedCacheEntry( con freeEntry->atlasCellSpan = cellSpan; return freeEntry; } + if ( !allowEviction ) { + return NULL; + } // no record or no free block: evict the least recently used entry and retry if ( oldest == NULL ) { return NULL; @@ -3647,7 +3989,8 @@ static projectedShadowMapCacheEntry_t *RB_ShadowMapAllocProjectedCacheEntry( con return NULL; } -static pointShadowMapCacheEntry_t *RB_ShadowMapAllocPointCacheEntry( void ) { +static pointShadowMapCacheEntry_t *RB_ShadowMapAllocPointCacheEntry( + const bool allowEviction = true ) { const int slotLimit = RB_ShadowMapPointCacheSlotLimit(); if ( slotLimit <= 0 ) { return NULL; @@ -3662,6 +4005,9 @@ static pointShadowMapCacheEntry_t *RB_ShadowMapAllocPointCacheEntry( void ) { oldest = entry; } } + if ( !allowEviction ) { + return NULL; + } oldest->valid = false; g_shadowMapStats.cacheEvictions++; return oldest; @@ -3743,7 +4089,11 @@ static shadowMapSchedule_t RB_ShadowMapSchedulePass( const viewLight_t *vLight, return schedule; } - RB_ShadowMapSelectScratchResources(); + if ( pointLight ) { + RB_ShadowMapSelectPointScratchResources(); + } else { + RB_ShadowMapSelectProjectedScratchResources(); + } schedule.cacheable = RB_ShadowMapStaticCacheable( vLight, passKind, pointLight, haveTranslucentCasters ); const int lightIndex = RB_ShadowMapLightIndex( vLight ); if ( schedule.cacheable ) { @@ -3795,7 +4145,7 @@ static shadowMapSchedule_t RB_ShadowMapSchedulePass( const viewLight_t *vLight, // casters keeps the full-content stencil fallback. if ( schedule.cacheable ) { if ( pointLight ) { - schedule.pointEntry = RB_ShadowMapFindPointCacheEntryAnySignature( lightIndex, passKind ); + schedule.pointEntry = RB_ShadowMapFindPointCacheEntryAnySignature( vLight, passKind ); if ( schedule.pointEntry != NULL ) { schedule.cacheHit = true; schedule.action = SHADOWMAP_SCHEDULE_REUSE; @@ -3867,11 +4217,19 @@ static void RB_ShadowMapCompleteCacheUpdate( const shadowMapSchedule_t &schedule entry->size = g_pointShadowMapRenderTexture != NULL ? g_pointShadowMapRenderTexture->GetWidth() : 0; entry->highPrecision = RB_PointShadowMapHighPrecisionEnabled(); entry->depthCompare = RB_PointShadowMapDepthCompareEnabled(); + entry->lightOrigin[0] = vLight->globalLightOrigin[0]; + entry->lightOrigin[1] = vLight->globalLightOrigin[1]; + entry->lightOrigin[2] = vLight->globalLightOrigin[2]; + entry->farDistance = R_ShadowMapPointFarDistance( vLight ); entry->lastUsedFrame = tr.frameCount; entry->lastUpdatedFrame = tr.frameCount; entry->colorImage = g_pointShadowMapColorImage; entry->depthImage = g_pointShadowMapDepthImage; entry->renderTexture = g_pointShadowMapRenderTexture; + entry->colorStorageGeneration = entry->colorImage != NULL + ? entry->colorImage->GetStorageGeneration() : 0; + entry->depthStorageGeneration = entry->depthImage != NULL + ? entry->depthImage->GetStorageGeneration() : 0; } else { projectedShadowMapCacheEntry_t *entry = schedule.projectedEntry; if ( entry == NULL ) { @@ -3891,6 +4249,8 @@ static void RB_ShadowMapCompleteCacheUpdate( const shadowMapSchedule_t &schedule entry->cascadeCount = g_projectedShadowMapState.cascadeCount; entry->lastUsedFrame = tr.frameCount; entry->lastUpdatedFrame = tr.frameCount; + entry->atlasStorageGeneration = g_shadowMapAtlasDepthImage != NULL + ? g_shadowMapAtlasDepthImage->GetStorageGeneration() : 0; entry->state = g_projectedShadowMapState; // atlas cell placement was written at allocation; entries hold no textures @@ -4563,6 +4923,10 @@ void RB_ShutdownShadowMapResources( void ) { for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { RB_ShadowMapDestroyRenderTexture( g_pointShadowMapCache[i].renderTexture ); + RB_ShadowMapDestroyRenderTexture( + g_sharedInteractionProjectedScratch[i].renderTexture ); + RB_ShadowMapDestroyRenderTexture( + g_sharedInteractionPointScratch[i].renderTexture ); } if ( glConfig.isInitialized && glDeleteQueries != NULL ) { @@ -4601,7 +4965,14 @@ void RB_ShutdownShadowMapResources( void ) { g_shadowMapDebugOverlayState.lightDefIndex = -1; memset( g_projectedShadowMapCache, 0, sizeof( g_projectedShadowMapCache ) ); memset( g_pointShadowMapCache, 0, sizeof( g_pointShadowMapCache ) ); + memset( g_sharedInteractionProjectedScratch, 0, + sizeof( g_sharedInteractionProjectedScratch ) ); + memset( g_sharedInteractionPointScratch, 0, + sizeof( g_sharedInteractionPointScratch ) ); memset( g_shadowMapLightHistory, 0, sizeof( g_shadowMapLightHistory ) ); + g_shadowMapCacheRenderWorld = NULL; + g_shadowMapCacheMapFileCRC = 0; + g_shadowMapCacheMapNameHash = 0; g_activeProjectedShadowMapCache = NULL; g_activePointShadowMapCache = NULL; memset( g_shadowMapGpuTimerQuerySlots, 0, sizeof( g_shadowMapGpuTimerQuerySlots ) ); @@ -5721,7 +6092,7 @@ static bool RB_ShadowMapEnsureResources( const viewLight_t *vLight ) { // every cached tile placement { const int atlasCellSize = idMath::ClampInt( 128, glConfig.maxTextureSize > 0 ? glConfig.maxTextureSize : 4096, r_shadowMapSize.GetInteger() ); - const int atlasSize = Min( idMath::ClampInt( 2048, 8192, r_shadowMapAtlasSize.GetInteger() ), glConfig.maxTextureSize > 0 ? glConfig.maxTextureSize : 4096 ); + const int atlasSize = RB_ShadowMapAtlasSizeValue(); if ( atlasCellSize != g_shadowMapAtlasCellSize ) { for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { g_projectedShadowMapCache[i].valid = false; @@ -5989,6 +6360,14 @@ static bool RB_PointShadowMapEnsureTranslucentResources( void ) { } // Keep shadow-map support reporting aligned with the interaction shadow admission rules. +static bool RB_ShadowMapTranslucentGlobalReceiverNeeded( + const viewLight_t *vLight ) { + return vLight != NULL + && vLight->translucentInteractions != NULL + && r_shadowMapTranslucentReceivers.GetBool() + && !r_skipTranslucent.GetBool(); +} + static shadowMapLightSupportReason_t RB_ShadowMapLightPolicySupportReason( const viewLight_t *vLight ) { if ( vLight == NULL ) { return SHADOWMAP_SUPPORT_NULL_LIGHT; @@ -6015,7 +6394,6 @@ static shadowMapLightSupportReason_t RB_ShadowMapLightPolicySupportReason( const } static shadowMapLightSupportReason_t RB_ShadowMapLightSupportReason( const viewLight_t *vLight ) { - RB_ShadowMapSelectScratchResources(); if ( !r_useShadowMap.GetBool() || !r_shadows.GetBool() ) { return !r_useShadowMap.GetBool() ? SHADOWMAP_SUPPORT_DISABLED : SHADOWMAP_SUPPORT_SHADOWS_DISABLED; } @@ -6027,7 +6405,9 @@ static shadowMapLightSupportReason_t RB_ShadowMapLightSupportReason( const viewL if ( glConfig.maxTextureUnits < 6 || glConfig.maxTextureImageUnits < 6 ) { return SHADOWMAP_SUPPORT_TEXTURE_LIMIT; } - if ( vLight->globalInteractions == NULL && vLight->localInteractions == NULL ) { + if ( vLight->globalInteractions == NULL + && vLight->localInteractions == NULL + && !RB_ShadowMapTranslucentGlobalReceiverNeeded( vLight ) ) { return SHADOWMAP_SUPPORT_NO_INTERACTIONS; } // parallel lights carry pointLight=true but render through the projected @@ -6056,7 +6436,9 @@ static bool RB_ShadowMapAnyResidentResources( void ) { return true; } for ( int i = 0; i < SHADOWMAP_CACHE_MAX_SLOTS; i++ ) { - if ( g_pointShadowMapCache[i].renderTexture != NULL ) { + if ( g_pointShadowMapCache[i].renderTexture != NULL + || g_sharedInteractionProjectedScratch[i].renderTexture != NULL + || g_sharedInteractionPointScratch[i].renderTexture != NULL ) { return true; } } @@ -6079,6 +6461,12 @@ static void RB_ShadowMapPurgeIdleResources( void ) { RB_ShadowMapPurgeImage( g_pointShadowMapCache[i].colorImage ); RB_ShadowMapPurgeImage( g_pointShadowMapCache[i].depthImage ); + RB_ShadowMapPurgeImage( + g_sharedInteractionProjectedScratch[i].depthImage ); + RB_ShadowMapPurgeImage( + g_sharedInteractionPointScratch[i].colorImage ); + RB_ShadowMapPurgeImage( + g_sharedInteractionPointScratch[i].depthImage ); g_pointShadowMapCache[i].colorImage = NULL; g_pointShadowMapCache[i].depthImage = NULL; @@ -6090,6 +6478,7 @@ static void RB_ShadowMapPurgeIdleResources( void ) { static void RB_ShadowMapStatsReset( void ) { memset( &g_shadowMapStats, 0, sizeof( g_shadowMapStats ) ); + RB_ShadowMapPrepareCacheView( backEnd.viewDef ); g_shadowMapStats.csmRequested = r_shadowMapCSM.GetBool(); g_shadowMapStats.projectedCsmRequested = r_shadowMapProjectedCSM.GetBool(); g_shadowMapStats.requestedCascadeCount = idMath::ClampInt( 1, SHADOWMAP_CLASSIFICATION_MAX_CASCADES, r_shadowMapCascadeCount.GetInteger() ); @@ -7143,7 +7532,8 @@ static int RB_ShadowMapDrawCasterChain( const drawSurf_t *surf, const int cascad RB_ShadowMapSetCasterDepthRow( surf, cascadeIndex ); } - RB_ShadowMapApplyCasterCull( surf->material ); + RB_ShadowMapApplyCasterCull( surf->material, casterGeo, + surf->space->modelMatrix ); idDrawVert *ac = (idDrawVert *)vertexCache.Position( ambientCache ); glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), RB_DrawVertAttributePointer( ac, offsetof( idDrawVert, xyz ) ) ); @@ -7370,18 +7760,6 @@ static void RB_PointShadowMapBuildProjectionMatrix( const float zNear, const flo matrix[14] = -( 2.0f * zFar * zNear ) / ( zFar - zNear ); } -static float RB_PointShadowMapLightFar( const viewLight_t *vLight ) { - idVec3 adjustedRadius = vLight->lightRadius; - if ( vLight->lightDef != NULL ) { - const renderLight_t &parms = vLight->lightDef->parms; - adjustedRadius[0] = parms.lightRadius[0] + idMath::Fabs( parms.lightCenter[0] ); - adjustedRadius[1] = parms.lightRadius[1] + idMath::Fabs( parms.lightCenter[1] ); - adjustedRadius[2] = parms.lightRadius[2] + idMath::Fabs( parms.lightCenter[2] ); - } - - return Max( adjustedRadius.Length() * r_shadowMapPointFarScale.GetFloat(), 1.0f ); -} - static void RB_PointShadowMapSetAlphaTexCoordIdentity( void ) { static const float rowS[4] = { 1.0f, 0.0f, 0.0f, 0.0f }; static const float rowT[4] = { 0.0f, 1.0f, 0.0f, 0.0f }; @@ -7623,7 +8001,8 @@ static int RB_PointShadowMapDrawCasterChain( const drawSurf_t *surf, const float glUniform4fvARB( g_pointShadowCasterProgram.modelMatrixRow2, 1, row2 ); } - RB_ShadowMapApplyCasterCull( surf->material ); + RB_ShadowMapApplyCasterCull( surf->material, casterGeo, + surf->space->modelMatrix ); idDrawVert *ac = (idDrawVert *)vertexCache.Position( ambientCache ); glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), RB_DrawVertAttributePointer( ac, offsetof( idDrawVert, xyz ) ) ); @@ -8013,7 +8392,7 @@ static bool RB_RenderPointShadowMap( const drawSurf_t *primaryCasters, const dra const GLboolean stencilWasEnabled = glIsEnabled( GL_STENCIL_TEST ); const int savedFaceCulling = backEnd.glState.faceCulling; - const float farClip = RB_PointShadowMapLightFar( backEnd.vLight ); + const float farClip = R_ShadowMapPointFarDistance( backEnd.vLight ); // with depth clamp, casters inside the near plane still rasterize (their // color-packed radial depth stays exact), so the near plane only shapes the // ordering-depth distribution; without it, shrink the clipped-away zone @@ -8306,7 +8685,7 @@ static bool RB_RenderPointTranslucentShadowMap( const drawSurf_t *primaryCasters const GLboolean depthWasEnabled = glIsEnabled( GL_DEPTH_TEST ); const GLboolean stencilWasEnabled = glIsEnabled( GL_STENCIL_TEST ); const int savedFaceCulling = backEnd.glState.faceCulling; - const float farClip = RB_PointShadowMapLightFar( backEnd.vLight ); + const float farClip = R_ShadowMapPointFarDistance( backEnd.vLight ); // with depth clamp, casters inside the near plane still rasterize (their // color-packed radial depth stays exact), so the near plane only shapes the // ordering-depth distribution; without it, shrink the clipped-away zone @@ -8434,6 +8813,79 @@ static bool RB_RenderPointTranslucentShadowMap( const drawSurf_t *primaryCasters return true; } +// Value cache for the GLSL interaction uniforms sent per interaction draw. +// localLightOrigin/localViewOrigin/lightProjection are per-(light, space) +// constants and the stage matrices are identity for most materials, so the +// receiver loops re-send byte-identical values for nearly every draw; a +// 16-byte compare is far cheaper than the driver call (same rationale as +// g_arb2EnvParamCache above). Slots are logical uniforms, not GL locations: +// only one of the three GLSL interaction programs is ever bound within a +// receiver pass and the cache is invalidated at every pass entry, so values +// can never leak between programs, across lights, or across relinks. +enum { + GLSL_INTERACTION_CACHE_LOCAL_LIGHT_ORIGIN = 0, + GLSL_INTERACTION_CACHE_LOCAL_VIEW_ORIGIN, + GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_S, + GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_T, + GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_Q, + GLSL_INTERACTION_CACHE_LIGHT_FALLOFF_S, + GLSL_INTERACTION_CACHE_BUMP_MATRIX_S, + GLSL_INTERACTION_CACHE_BUMP_MATRIX_T, + GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_S, + GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_T, + GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_S, + GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_T, + GLSL_INTERACTION_CACHE_DIFFUSE_COLOR, + GLSL_INTERACTION_CACHE_SPECULAR_COLOR, + GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_0, + GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_1, + GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_2, + GLSL_INTERACTION_CACHE_GLOBAL_LIGHT_ORIGIN, + GLSL_INTERACTION_CACHE_VERTEX_COLOR_PARAMS, + GLSL_INTERACTION_CACHE_SLOTS +}; +typedef struct { + float values[GLSL_INTERACTION_CACHE_SLOTS][4]; + unsigned int validMask; +} glslInteractionUniformCache_t; +static glslInteractionUniformCache_t g_glslInteractionUniformCache; + +ID_INLINE static void RB_GLSLInteractionUniformCacheInvalidate( void ) { + g_glslInteractionUniformCache.validMask = 0; +} + +ID_INLINE static void RB_GLSLInteractionUniform4fv( const GLint location, const int slot, const float *v ) { + if ( r_useRedundantStateFiltering.GetBool() ) { + const unsigned int bit = 1u << slot; + float *cached = g_glslInteractionUniformCache.values[slot]; + if ( ( g_glslInteractionUniformCache.validMask & bit ) + && cached[0] == v[0] && cached[1] == v[1] && cached[2] == v[2] && cached[3] == v[3] ) { + return; + } + cached[0] = v[0]; + cached[1] = v[1]; + cached[2] = v[2]; + cached[3] = v[3]; + g_glslInteractionUniformCache.validMask |= bit; + } + glUniform4fvARB( location, 1, v ); +} + +ID_INLINE static void RB_GLSLInteractionUniform2fv( const GLint location, const int slot, const float *v ) { + if ( r_useRedundantStateFiltering.GetBool() ) { + const unsigned int bit = 1u << slot; + float *cached = g_glslInteractionUniformCache.values[slot]; + if ( ( g_glslInteractionUniformCache.validMask & bit ) + && cached[0] == v[0] && cached[1] == v[1] ) { + return; + } + cached[0] = v[0]; + cached[1] = v[1]; + g_glslInteractionUniformCache.validMask |= bit; + } + glUniform2fvARB( location, 1, v ); +} + static bool RB_EnhancedMaterialShadingActive( void ) { return glConfig.GLSLProgramAvailable && r_enhancedMaterials.GetBool(); } @@ -8501,20 +8953,20 @@ static void RB_MaterialInteractionSetEnhancementUniforms( const GLint normalScal } static void RB_GLSLMaterial_DrawInteraction( const drawInteraction_t *din ) { - glUniform4fvARB( g_materialInteractionProgram.localLightOrigin, 1, din->localLightOrigin.ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.localViewOrigin, 1, din->localViewOrigin.ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.lightProjectionS, 1, din->lightProjection[0].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.lightProjectionT, 1, din->lightProjection[1].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.lightProjectionQ, 1, din->lightProjection[2].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.lightFalloffS, 1, din->lightProjection[3].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.bumpMatrixS, 1, din->bumpMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.bumpMatrixT, 1, din->bumpMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.diffuseMatrixS, 1, din->diffuseMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.diffuseMatrixT, 1, din->diffuseMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.specularMatrixS, 1, din->specularMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.specularMatrixT, 1, din->specularMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.diffuseColor, 1, din->diffuseColor.ToFloatPtr() ); - glUniform4fvARB( g_materialInteractionProgram.specularColor, 1, din->specularColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.localLightOrigin, GLSL_INTERACTION_CACHE_LOCAL_LIGHT_ORIGIN, din->localLightOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.localViewOrigin, GLSL_INTERACTION_CACHE_LOCAL_VIEW_ORIGIN, din->localViewOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.lightProjectionS, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_S, din->lightProjection[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.lightProjectionT, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_T, din->lightProjection[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.lightProjectionQ, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_Q, din->lightProjection[2].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.lightFalloffS, GLSL_INTERACTION_CACHE_LIGHT_FALLOFF_S, din->lightProjection[3].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.bumpMatrixS, GLSL_INTERACTION_CACHE_BUMP_MATRIX_S, din->bumpMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.bumpMatrixT, GLSL_INTERACTION_CACHE_BUMP_MATRIX_T, din->bumpMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.diffuseMatrixS, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_S, din->diffuseMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.diffuseMatrixT, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_T, din->diffuseMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.specularMatrixS, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_S, din->specularMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.specularMatrixT, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_T, din->specularMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.diffuseColor, GLSL_INTERACTION_CACHE_DIFFUSE_COLOR, din->diffuseColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_materialInteractionProgram.specularColor, GLSL_INTERACTION_CACHE_SPECULAR_COLOR, din->specularColor.ToFloatPtr() ); if ( g_materialInteractionProgram.flatDiffuseParams >= 0 ) { glUniform4fvARB( g_materialInteractionProgram.flatDiffuseParams, 1, din->flatDiffuseParams.ToFloatPtr() ); } @@ -8536,7 +8988,7 @@ static void RB_GLSLMaterial_DrawInteraction( const drawInteraction_t *din ) { break; } const float vertexColorParams[2] = { modulate, add }; - glUniform2fvARB( g_materialInteractionProgram.vertexColorParams, 1, vertexColorParams ); + RB_GLSLInteractionUniform2fv( g_materialInteractionProgram.vertexColorParams, GLSL_INTERACTION_CACHE_VERTEX_COLOR_PARAMS, vertexColorParams ); GL_SelectTextureNoClient( 0 ); din->bumpImage->Bind(); @@ -8625,20 +9077,20 @@ static void RB_GLSLShadowMap_DrawInteraction( const drawInteraction_t *din ) { glUniform4fvARB( g_shadowMapProgram.shadowRow[3], cascadeCount, shadowRow3 ); } - glUniform4fvARB( g_shadowMapProgram.localLightOrigin, 1, din->localLightOrigin.ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.localViewOrigin, 1, din->localViewOrigin.ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.lightProjectionS, 1, din->lightProjection[0].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.lightProjectionT, 1, din->lightProjection[1].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.lightProjectionQ, 1, din->lightProjection[2].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.lightFalloffS, 1, din->lightProjection[3].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.bumpMatrixS, 1, din->bumpMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.bumpMatrixT, 1, din->bumpMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.diffuseMatrixS, 1, din->diffuseMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.diffuseMatrixT, 1, din->diffuseMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.specularMatrixS, 1, din->specularMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.specularMatrixT, 1, din->specularMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.diffuseColor, 1, din->diffuseColor.ToFloatPtr() ); - glUniform4fvARB( g_shadowMapProgram.specularColor, 1, din->specularColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.localLightOrigin, GLSL_INTERACTION_CACHE_LOCAL_LIGHT_ORIGIN, din->localLightOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.localViewOrigin, GLSL_INTERACTION_CACHE_LOCAL_VIEW_ORIGIN, din->localViewOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.lightProjectionS, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_S, din->lightProjection[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.lightProjectionT, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_T, din->lightProjection[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.lightProjectionQ, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_Q, din->lightProjection[2].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.lightFalloffS, GLSL_INTERACTION_CACHE_LIGHT_FALLOFF_S, din->lightProjection[3].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.bumpMatrixS, GLSL_INTERACTION_CACHE_BUMP_MATRIX_S, din->bumpMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.bumpMatrixT, GLSL_INTERACTION_CACHE_BUMP_MATRIX_T, din->bumpMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.diffuseMatrixS, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_S, din->diffuseMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.diffuseMatrixT, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_T, din->diffuseMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.specularMatrixS, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_S, din->specularMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.specularMatrixT, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_T, din->specularMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.diffuseColor, GLSL_INTERACTION_CACHE_DIFFUSE_COLOR, din->diffuseColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_shadowMapProgram.specularColor, GLSL_INTERACTION_CACHE_SPECULAR_COLOR, din->specularColor.ToFloatPtr() ); if ( g_shadowMapProgram.flatDiffuseParams >= 0 ) { glUniform4fvARB( g_shadowMapProgram.flatDiffuseParams, 1, din->flatDiffuseParams.ToFloatPtr() ); } @@ -8659,7 +9111,7 @@ static void RB_GLSLShadowMap_DrawInteraction( const drawInteraction_t *din ) { break; } const float vertexColorParams[2] = { modulate, add }; - glUniform2fvARB( g_shadowMapProgram.vertexColorParams, 1, vertexColorParams ); + RB_GLSLInteractionUniform2fv( g_shadowMapProgram.vertexColorParams, GLSL_INTERACTION_CACHE_VERTEX_COLOR_PARAMS, vertexColorParams ); if ( g_shadowMapProgram.shadowReceiverDebugReason >= 0 ) { glUniform1fARB( g_shadowMapProgram.shadowReceiverDebugReason, g_shadowMapReceiverDebugReason ); } @@ -8703,25 +9155,28 @@ static void RB_GLSLPointShadowMap_DrawInteraction( const drawInteraction_t *din RB_ShadowMapModelMatrixRows( din->surf->space->modelMatrix, row0, row1, row2 ); - glUniform4fvARB( g_pointShadowMapProgram.localLightOrigin, 1, din->localLightOrigin.ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.localViewOrigin, 1, din->localViewOrigin.ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.lightProjectionS, 1, din->lightProjection[0].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.lightProjectionT, 1, din->lightProjection[1].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.lightProjectionQ, 1, din->lightProjection[2].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.lightFalloffS, 1, din->lightProjection[3].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.bumpMatrixS, 1, din->bumpMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.bumpMatrixT, 1, din->bumpMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.diffuseMatrixS, 1, din->diffuseMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.diffuseMatrixT, 1, din->diffuseMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.specularMatrixS, 1, din->specularMatrix[0].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.specularMatrixT, 1, din->specularMatrix[1].ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow0, 1, row0 ); - glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow1, 1, row1 ); - glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow2, 1, row2 ); - glUniform4fvARB( g_pointShadowMapProgram.globalLightOrigin, 1, globalLightOrigin ); - glUniform1fARB( g_pointShadowMapProgram.pointShadowFar, RB_PointShadowMapLightFar( backEnd.vLight ) ); - glUniform4fvARB( g_pointShadowMapProgram.diffuseColor, 1, din->diffuseColor.ToFloatPtr() ); - glUniform4fvARB( g_pointShadowMapProgram.specularColor, 1, din->specularColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.localLightOrigin, GLSL_INTERACTION_CACHE_LOCAL_LIGHT_ORIGIN, din->localLightOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.localViewOrigin, GLSL_INTERACTION_CACHE_LOCAL_VIEW_ORIGIN, din->localViewOrigin.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.lightProjectionS, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_S, din->lightProjection[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.lightProjectionT, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_T, din->lightProjection[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.lightProjectionQ, GLSL_INTERACTION_CACHE_LIGHT_PROJECTION_Q, din->lightProjection[2].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.lightFalloffS, GLSL_INTERACTION_CACHE_LIGHT_FALLOFF_S, din->lightProjection[3].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.bumpMatrixS, GLSL_INTERACTION_CACHE_BUMP_MATRIX_S, din->bumpMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.bumpMatrixT, GLSL_INTERACTION_CACHE_BUMP_MATRIX_T, din->bumpMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.diffuseMatrixS, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_S, din->diffuseMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.diffuseMatrixT, GLSL_INTERACTION_CACHE_DIFFUSE_MATRIX_T, din->diffuseMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.specularMatrixS, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_S, din->specularMatrix[0].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.specularMatrixT, GLSL_INTERACTION_CACHE_SPECULAR_MATRIX_T, din->specularMatrix[1].ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.modelMatrixRow0, GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_0, row0 ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.modelMatrixRow1, GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_1, row1 ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.modelMatrixRow2, GLSL_INTERACTION_CACHE_MODEL_MATRIX_ROW_2, row2 ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.globalLightOrigin, GLSL_INTERACTION_CACHE_GLOBAL_LIGHT_ORIGIN, globalLightOrigin ); + // pointShadowFar is uploaded once at program-bind time in + // RB_GLSLPointShadowMap_CreateDrawInteractions and is constant across the + // whole receiver chain (backEnd.vLight does not change), so the per-draw + // re-upload here (and its sqrt in R_ShadowMapPointFarDistance) was redundant. + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.diffuseColor, GLSL_INTERACTION_CACHE_DIFFUSE_COLOR, din->diffuseColor.ToFloatPtr() ); + RB_GLSLInteractionUniform4fv( g_pointShadowMapProgram.specularColor, GLSL_INTERACTION_CACHE_SPECULAR_COLOR, din->specularColor.ToFloatPtr() ); if ( g_pointShadowMapProgram.flatDiffuseParams >= 0 ) { glUniform4fvARB( g_pointShadowMapProgram.flatDiffuseParams, 1, din->flatDiffuseParams.ToFloatPtr() ); } @@ -8742,7 +9197,7 @@ static void RB_GLSLPointShadowMap_DrawInteraction( const drawInteraction_t *din break; } const float vertexColorParams[2] = { modulate, add }; - glUniform2fvARB( g_pointShadowMapProgram.vertexColorParams, 1, vertexColorParams ); + RB_GLSLInteractionUniform2fv( g_pointShadowMapProgram.vertexColorParams, GLSL_INTERACTION_CACHE_VERTEX_COLOR_PARAMS, vertexColorParams ); if ( g_pointShadowMapProgram.shadowReceiverDebugReason >= 0 ) { glUniform1fARB( g_pointShadowMapProgram.shadowReceiverDebugReason, g_pointShadowMapReceiverDebugReason ); } @@ -9267,16 +9722,43 @@ static unsigned int RB_ShadowMapArb2CachePassMask( const shadowMapPassKind_t pas return ( passKind == SHADOWMAP_PASS_LOCAL ) ? SHADOWMAP_ARB2_CACHE_PASS_LOCAL : SHADOWMAP_ARB2_CACHE_PASS_GLOBAL; } +static bool RB_ShadowMapHybridAvailableForPass( const viewLight_t *vLight, + const shadowMapPassKind_t passKind ); +static bool RB_ShadowMapMapOrHybridAvailableForPass( + const viewLight_t *vLight, const shadowMapPassKind_t passKind, + bool *hybridOut ); + +static const drawSurf_t *RB_ShadowMapGlobalReceiverPlanningChain( + const viewLight_t *vLight, bool &deferReceiverDraw ) { + deferReceiverDraw = false; + if ( vLight == NULL ) { + return NULL; + } + const bool opaqueEligible = RB_DrawSurfChainHasFilteredSurface( + vLight->globalInteractions, RB_SurfaceEligibleForShadowMapReceiver ); + const bool translucentEligible = + RB_ShadowMapTranslucentGlobalReceiverNeeded( vLight ) + && RB_DrawSurfChainHasFilteredSurface( vLight->translucentInteractions, + RB_SurfaceEligibleForShadowMapReceiver ); + if ( !opaqueEligible && translucentEligible ) { + deferReceiverDraw = true; + return vLight->translucentInteractions; + } + return vLight->globalInteractions; +} + static void RB_ShadowMapArb2CacheSlotCountsReadOnly( shadowMapArb2CacheEstimate_t &estimate ) { estimate.projectedCacheSlotsTotal = RB_ShadowMapProjectedCacheSlotLimit(); estimate.pointCacheSlotsTotal = RB_ShadowMapPointCacheSlotLimit(); for ( int i = 0; i < estimate.projectedCacheSlotsTotal; i++ ) { - if ( g_projectedShadowMapCache[i].valid ) { + if ( RB_ShadowMapProjectedCacheEntryStorageValid( + &g_projectedShadowMapCache[i] ) ) { estimate.projectedCacheSlotsUsed++; } } for ( int i = 0; i < estimate.pointCacheSlotsTotal; i++ ) { - if ( g_pointShadowMapCache[i].valid ) { + if ( RB_ShadowMapPointCacheEntryStorageValid( + &g_pointShadowMapCache[i] ) ) { estimate.pointCacheSlotsUsed++; } } @@ -9288,6 +9770,10 @@ static void RB_ShadowMapEstimateArb2CachePass( const viewLight_t *vLight, const } const unsigned int passMask = RB_ShadowMapArb2CachePassMask( passKind ); + const shadowMapPassKind_t cachePassKind = + RB_ShadowMapCachePassKind( vLight, passKind ); + const unsigned int cacheKeyMask = + RB_ShadowMapArb2CachePassMask( cachePassKind ); estimate.shadowPasses++; const drawSurf_t *translucentPrimaryCasters = vLight != NULL ? vLight->globalTranslucentShadowMapCasters : NULL; @@ -9316,16 +9802,34 @@ static void RB_ShadowMapEstimateArb2CachePass( const viewLight_t *vLight, const estimate.receiverFallbackPasses++; } - const bool cacheable = RB_ShadowMapStaticCacheableReadOnly( vLight, passKind, pointLight, haveTranslucentCasters ); + if ( !RB_ShadowMapMapOrHybridAvailableForPass( + vLight, passKind, NULL ) ) { + // Mirror the direct draw's fail-closed gate. An ownership that cannot be + // represented by either a complete map or an exact map+stencil hybrid + // takes full stencil before scheduling and consumes no map-update budget. + estimate.stencilOnlyPasses++; + return; + } + + const bool cacheable = RB_ShadowMapStaticCacheableReadOnly( vLight, cachePassKind, pointLight, haveTranslucentCasters ); if ( cacheable ) { estimate.cacheablePasses++; estimate.cacheablePassMask |= passMask; - const int signature = RB_ShadowMapBuildPassSignatureForView( vLight, viewDef, passKind, pointLight ); + const int signature = RB_ShadowMapBuildPassSignatureForView( vLight, viewDef, cachePassKind, pointLight ); const int lightIndex = RB_ShadowMapLightIndex( vLight ); const bool cacheHit = pointLight - ? RB_ShadowMapFindPointCacheEntry( lightIndex, passKind, signature ) != NULL - : RB_ShadowMapFindProjectedCacheEntry( lightIndex, passKind, signature ) != NULL; + ? RB_ShadowMapFindPointCacheEntry( lightIndex, cachePassKind, signature ) != NULL + : RB_ShadowMapFindProjectedCacheEntry( lightIndex, cachePassKind, signature ) != NULL; if ( cacheHit ) { + estimate.cacheHitPasses++; + estimate.cacheHitPassMask |= passMask; + estimate.cacheKeyHitMask |= cacheKeyMask; + return; + } + // LOCAL and GLOBAL receivers can share one canonical GLOBAL map when + // there are no local/noSelf caster chains. Model the later receiver as + // a same-frame reuse instead of charging a phantom second update. + if ( ( estimate.plannedCacheUpdateKeyMask & cacheKeyMask ) != 0 ) { estimate.cacheHitPasses++; estimate.cacheHitPassMask |= passMask; return; @@ -9343,6 +9847,9 @@ static void RB_ShadowMapEstimateArb2CachePass( const viewLight_t *vLight, const estimate.freshUpdatePasses++; estimate.freshUpdatePassMask |= passMask; + if ( cacheable ) { + estimate.plannedCacheUpdateKeyMask |= cacheKeyMask; + } } bool RB_ShadowMapEstimateArb2CacheOwnership( const viewLight_t *vLight, const viewDef_t *viewDef, shadowMapArb2CacheEstimate_t &estimate ) { @@ -9360,20 +9867,39 @@ bool RB_ShadowMapEstimateArb2CacheOwnership( const viewLight_t *vLight, const vi if ( RB_ShadowMapLightPolicySupportReason( vLight ) != SHADOWMAP_SUPPORT_OK ) { return false; } - if ( vLight->globalInteractions == NULL && vLight->localInteractions == NULL ) { + // Mirror the read-only capability half of RB_ShadowMapLightSupportReason. + // The estimator must not allocate resources, but an impossible light must + // not consume admission priority and starve a viable update. + if ( glConfig.maxTextureUnits < 6 || glConfig.maxTextureImageUnits < 6 ) { return false; } - if ( estimatePointLight && !r_shadowMapPointLights.GetBool() ) { + bool deferGlobalReceiverDraw = false; + const drawSurf_t *globalReceiverInteractions = + RB_ShadowMapGlobalReceiverPlanningChain( + vLight, deferGlobalReceiverDraw ); + if ( globalReceiverInteractions == NULL && vLight->localInteractions == NULL ) { return false; } + if ( estimatePointLight ) { + if ( !r_shadowMapPointLights.GetBool() || !glConfig.cubeMapAvailable ) { + return false; + } + } estimate.valid = true; if ( estimatePointLight ) { - RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_LOCAL, true, vLight->globalShadowMapCasters, NULL, NULL, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); - RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_GLOBAL, true, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, NULL, NULL, vLight->globalShadows, vLight->localShadows, vLight->globalInteractions ); + RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_LOCAL, true, vLight->globalShadowMapCasters, NULL, vLight->globalShadowMapDynamicCasters, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); + RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_GLOBAL, true, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, globalReceiverInteractions ); } else { - RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_LOCAL, false, vLight->globalShadowMapCasters, NULL, NULL, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); - RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_GLOBAL, false, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, NULL, NULL, vLight->globalShadows, vLight->localShadows, vLight->globalInteractions ); + RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_LOCAL, false, vLight->globalShadowMapCasters, NULL, vLight->globalShadowMapDynamicCasters, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); + RB_ShadowMapEstimateArb2CachePass( vLight, viewDef, estimate, SHADOWMAP_PASS_GLOBAL, false, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, globalReceiverInteractions ); + } + if ( deferGlobalReceiverDraw + && RB_DrawSurfChainHasFilteredSurface( vLight->globalInteractions, + RB_SurfaceNeedsShadowMapReceiverFallback ) ) { + // The deferred translucent chain owns map admission, but opaque GLOBAL + // fallback surfaces still consume the same full-stencil ownership. + estimate.receiverFallbackPasses++; } return estimate.shadowPasses > 0; } @@ -9414,23 +9940,16 @@ static void RB_ShadowMapBuildUpdateAdmissions( void ) { } // budgetFallbackPasses folds back in: the estimate simulates the // per-light budget, but admission needs the light's full desired - // update count. The estimate replays LOCAL/GLOBAL without the cache - // pass collapse, so for lights whose passes share one GLOBAL entry - // only the GLOBAL prediction is real - the raw sum would charge - // steady-state lights for updates they will never render. - const unsigned int desiredMask = estimate.freshUpdatePassMask | estimate.budgetFallbackPassMask; - const bool passesCollapse = vLight->localShadowMapCasters == NULL - && vLight->localShadowMapDynamicCasters == NULL - && vLight->localTranslucentShadowMapCasters == NULL; - const int cost = passesCollapse - ? ( ( desiredMask & SHADOWMAP_ARB2_CACHE_PASS_GLOBAL ) != 0 ? 1 : 0 ) - : estimate.freshUpdatePasses + estimate.budgetFallbackPasses; + // update count. The estimator already canonicalizes LOCAL/GLOBAL cache + // ownership and models a same-frame reuse, so this sum is the real cost. + const int cost = estimate.freshUpdatePasses + + estimate.budgetFallbackPasses; if ( cost <= 0 ) { continue; } int lastUpdatedFrame = -1; if ( estimate.pointLight ) { - const pointShadowMapCacheEntry_t *entry = RB_ShadowMapFindPointCacheEntryAnySignature( estimate.lightIndex, SHADOWMAP_PASS_GLOBAL ); + const pointShadowMapCacheEntry_t *entry = RB_ShadowMapFindPointCacheEntryAnySignature( vLight, SHADOWMAP_PASS_GLOBAL ); if ( entry != NULL ) { lastUpdatedFrame = entry->lastUpdatedFrame; } @@ -9492,7 +10011,9 @@ static projectedShadowMapCacheEntry_t *RB_ShadowMapNewestProjectedGlobalEntry( c projectedShadowMapCacheEntry_t *newest = NULL; for ( int i = 0; i < slotLimit; i++ ) { projectedShadowMapCacheEntry_t *entry = &g_projectedShadowMapCache[i]; - if ( entry->valid && entry->lightIndex == lightIndex && entry->passKind == static_cast( SHADOWMAP_PASS_GLOBAL ) + if ( RB_ShadowMapProjectedCacheEntryStorageValid( entry ) + && entry->lightIndex == lightIndex + && entry->passKind == static_cast( SHADOWMAP_PASS_GLOBAL ) && ( newest == NULL || entry->lastUpdatedFrame > newest->lastUpdatedFrame ) ) { newest = entry; } @@ -9503,26 +10024,49 @@ static projectedShadowMapCacheEntry_t *RB_ShadowMapNewestProjectedGlobalEntry( c // Exposes a cached projected light's persistent-atlas placement so the modern // path can reference real tiles instead of aliasing whatever per-light target // the last ARB2 shadow pass selected. Read-only: must not touch LRU state. -// Only the GLOBAL pass entry qualifies - modern shading is single-pass over -// all receivers, and a LOCAL-only entry (global casters only) would -// under-shadow. Rects are composed into persistent-atlas UV space with the -// same math as the ARB2 receiver upload. -bool RB_ShadowMapProjectedAtlasSlotForLight( const int lightDefIndex, shadowMapArb2AtlasSlot_t &slot ) { +// Only the exact GLOBAL signature qualifies - a light can retain an older +// sibling for budget reuse, and "newest" is not necessarily the signature +// this view estimated as current. Rects use the ARB2 receiver's atlas math. +bool RB_ShadowMapProjectedAtlasSlotForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2AtlasSlot_t &slot ) { memset( &slot, 0, sizeof( slot ) ); + const int lightDefIndex = RB_ShadowMapLightIndex( vLight ); slot.lightIndex = lightDefIndex; - if ( lightDefIndex < 0 || g_shadowMapAtlasDepthImage == NULL || g_shadowMapAtlasRenderTexture == NULL || g_shadowMapAtlasCellSize <= 0 ) { + if ( vLight == NULL || viewDef == NULL || viewDef->renderWorld == NULL + || lightDefIndex < 0 + || g_shadowMapCacheRenderWorld != viewDef->renderWorld + || g_shadowMapCacheMapFileCRC != viewDef->renderWorld->mapFileCRC + || g_shadowMapCacheMapNameHash != RB_ShadowMapMapNameHash( viewDef ) + || g_shadowMapAtlasDepthImage == NULL + || !g_shadowMapAtlasDepthImage->IsLoaded() + || g_shadowMapAtlasDepthImage->IsDefaulted() + || g_shadowMapAtlasDepthImage->GetDeviceHandle() == 0 + || g_shadowMapAtlasDepthImage->GetOpts().textureType != TT_2D + || g_shadowMapAtlasDepthImage->GetOpts().format != FMT_DEPTH + || g_shadowMapAtlasRenderTexture == NULL + || g_shadowMapAtlasCellSize <= 0 ) { return false; } - const projectedShadowMapCacheEntry_t *entry = RB_ShadowMapNewestProjectedGlobalEntry( lightDefIndex ); + const int signature = RB_ShadowMapBuildPassSignatureForView( + vLight, viewDef, SHADOWMAP_PASS_GLOBAL, false ); + const projectedShadowMapCacheEntry_t *entry = + RB_ShadowMapFindProjectedCacheEntry( + lightDefIndex, SHADOWMAP_PASS_GLOBAL, signature ); if ( entry == NULL || !entry->state.valid || entry->atlasCellSpan <= 0 ) { return false; } const int atlasWidth = g_shadowMapAtlasRenderTexture->GetWidth(); const int atlasHeight = g_shadowMapAtlasRenderTexture->GetHeight(); - if ( atlasWidth <= 0 || atlasHeight <= 0 ) { + if ( atlasWidth <= 0 || atlasHeight <= 0 + || entry->atlasCellX < 0 || entry->atlasCellY < 0 + || ( entry->atlasCellX + entry->atlasCellSpan ) + * g_shadowMapAtlasCellSize > atlasWidth + || ( entry->atlasCellY + entry->atlasCellSpan ) + * g_shadowMapAtlasCellSize > atlasHeight ) { return false; } slot.signature = entry->signature; + slot.storageGeneration = entry->atlasStorageGeneration; slot.cellX = entry->atlasCellX; slot.cellY = entry->atlasCellY; slot.cellSpan = entry->atlasCellSpan; @@ -9552,12 +10096,66 @@ bool RB_ShadowMapProjectedAtlasSlotForLight( const int lightDefIndex, shadowMapA // Pins an atlas entry against idle expiry while modern receivers are // presenting it. Callers must gate this on live modern consumption - // diagnostics-only planner runs must stay strictly read-only or they would -// distort ARB2's cache LRU behavior. Targets the same newest entry the slot -// export composed from. -void RB_ShadowMapProjectedAtlasSlotMarkUsed( const int lightDefIndex ) { - projectedShadowMapCacheEntry_t *entry = RB_ShadowMapNewestProjectedGlobalEntry( lightDefIndex ); - if ( entry != NULL ) { - entry->lastUsedFrame = tr.frameCount; +// distort ARB2's cache LRU behavior. Revalidate the exact planned cell at the +// consumption point so an intervening eviction cannot retarget its UVs. +bool RB_ShadowMapProjectedAtlasSlotMarkUsed( const int lightDefIndex, + const int signature, const std::uint64_t storageGeneration, + const int cellX, const int cellY, const int cellSpan ) { + projectedShadowMapCacheEntry_t *entry = + RB_ShadowMapFindProjectedCacheEntry( + lightDefIndex, SHADOWMAP_PASS_GLOBAL, signature ); + if ( entry == NULL + || entry->atlasStorageGeneration != storageGeneration + || entry->atlasCellX != cellX + || entry->atlasCellY != cellY + || entry->atlasCellSpan != cellSpan ) { + return false; + } + entry->lastUsedFrame = tr.frameCount; + return true; +} + +// Exports provenance for the one cube the classic GL path has actually left +// selected. The render-world scope and GLOBAL-pass signature must both match +// the descriptor being built; a valid cube belonging to another point light +// is intentionally unavailable because the modern shader has no cube array. +bool RB_ShadowMapPointCubeForLight( const viewLight_t *vLight, + const viewDef_t *viewDef, shadowMapArb2PointCube_t &cube ) { + memset( &cube, 0, sizeof( cube ) ); + cube.lightIndex = RB_ShadowMapLightIndex( vLight ); + if ( vLight == NULL || viewDef == NULL || viewDef->renderWorld == NULL + || cube.lightIndex < 0 + || g_shadowMapCacheRenderWorld != viewDef->renderWorld + || g_shadowMapCacheMapFileCRC != viewDef->renderWorld->mapFileCRC + || g_shadowMapCacheMapNameHash != RB_ShadowMapMapNameHash( viewDef ) + || !RB_ShadowMapActivePointCacheContentReady() ) { + return false; + } + + const pointShadowMapCacheEntry_t *entry = g_activePointShadowMapCache; + const int signature = RB_ShadowMapBuildPassSignatureForView( + vLight, viewDef, SHADOWMAP_PASS_GLOBAL, true ); + if ( entry->lightIndex != cube.lightIndex + || entry->passKind != static_cast( SHADOWMAP_PASS_GLOBAL ) + || entry->signature != signature + || entry->size != RB_ShadowMapPointSizeValue() + || entry->highPrecision != RB_PointShadowMapHighPrecisionEnabled() + || !RB_ShadowMapPointCacheEntryProjectionMatches( entry, vLight ) ) { + return false; + } + + cube.valid = true; + cube.signature = entry->signature; + cube.size = entry->size; + cube.highPrecision = entry->highPrecision; + cube.lastUpdatedFrame = entry->lastUpdatedFrame; + return true; +} + +void RB_ShadowMapPointCubeMarkUsed( const int lightDefIndex ) { + if ( RB_ShadowMapActivePointCacheContentReady() + && g_activePointShadowMapCache->lightIndex == lightDefIndex ) { + g_activePointShadowMapCache->lastUsedFrame = tr.frameCount; } } @@ -9839,6 +10437,7 @@ static bool RB_GLSLMaterial_CreateDrawInteractions( const drawSurf_t *surf, cons glDisable( GL_VERTEX_PROGRAM_ARB ); glDisable( GL_FRAGMENT_PROGRAM_ARB ); glUseProgramObjectARB( g_materialInteractionProgram.programObject ); + RB_GLSLInteractionUniformCacheInvalidate(); if ( g_materialInteractionProgram.bumpMap >= 0 ) { glUniform1iARB( g_materialInteractionProgram.bumpMap, 0 ); @@ -10005,8 +10604,8 @@ static void RB_DrawMaterialInteractions( const drawSurf_t *surf ) { // banding is the only visible difference. const bool celBanded = !enhanced && glConfig.GLSLProgramAvailable && RB_DrawSurfChainNeedsCelBanding( surf ); - if ( !RB_DrawSurfChainHasCustomGLSLLighting( surf ) - && ( enhanced || celBanded ) + if ( ( enhanced || celBanded ) + && !RB_DrawSurfChainHasCustomGLSLLighting( surf ) && RB_DrawSurfChainEligibleForStockGLSLInteractions( surf ) && RB_GLSLMaterial_CreateDrawInteractions( surf, !enhanced ) ) { return; @@ -10025,6 +10624,7 @@ static bool RB_GLSLShadowMap_CreateDrawInteractions( const drawSurf_t *surf ) { glDisable( GL_VERTEX_PROGRAM_ARB ); glDisable( GL_FRAGMENT_PROGRAM_ARB ); glUseProgramObjectARB( g_shadowMapProgram.programObject ); + RB_GLSLInteractionUniformCacheInvalidate(); const shadowMapProjectedFilterSettings_t filterSettings = R_ShadowMapProjectedFilterSettings( backEnd.vLight ); if ( g_shadowMapProgram.bumpMap >= 0 ) { @@ -10294,6 +10894,20 @@ static bool RB_GLSLPointShadowMap_CreateDrawInteractions( const drawSurf_t *surf glDisable( GL_VERTEX_PROGRAM_ARB ); glDisable( GL_FRAGMENT_PROGRAM_ARB ); glUseProgramObjectARB( g_pointShadowMapProgram.programObject ); + RB_GLSLInteractionUniformCacheInvalidate(); + const int pointFaceSize = g_pointShadowMapRenderTexture != NULL + ? Max( 1, g_pointShadowMapRenderTexture->GetWidth() ) + : RB_ShadowMapPointSizeValue(); + const float pointFarDistance = + R_ShadowMapPointFarDistance( backEnd.vLight ); + const shadowMapPointReceiverSettings_t baseReceiverSettings = + R_ShadowMapPointReceiverSettings( + pointFarDistance, pointFaceSize ); + const shadowMapPointReceiverSettings_t receiverSettings = + R_ShadowMapPointStorageAdjustedReceiverSettings( + baseReceiverSettings, pointFarDistance, pointFaceSize, + RB_PointShadowMapDepthCompareEnabled(), + RB_PointShadowMapHighPrecisionEnabled() ); if ( g_pointShadowMapProgram.bumpMap >= 0 ) { glUniform1iARB( g_pointShadowMapProgram.bumpMap, 0 ); @@ -10319,28 +10933,24 @@ static bool RB_GLSLPointShadowMap_CreateDrawInteractions( const drawSurf_t *surf } } if ( g_pointShadowMapProgram.shadowBias >= 0 ) { - float pointBias = r_shadowMapPointBias.GetFloat(); - if ( !RB_PointShadowMapDepthCompareEnabled() ) { - // the manual compare path reads radial depth from color storage; - // floor the bias by that storage's quantization step near the far - // envelope (fp16 mantissa step in [0.5,1) vs 16-bit fixed packing) - const float storageStep = RB_PointShadowMapHighPrecisionEnabled() ? ( 1.0f / 2048.0f ) : ( 1.0f / 65025.0f ); - pointBias = Max( pointBias, storageStep * 1.5f ); - } - glUniform1fARB( g_pointShadowMapProgram.shadowBias, pointBias ); + glUniform1fARB( g_pointShadowMapProgram.shadowBias, + receiverSettings.constantBias ); } if ( g_pointShadowMapProgram.shadowNormalBias >= 0 ) { - glUniform1fARB( g_pointShadowMapProgram.shadowNormalBias, r_shadowMapPointNormalBias.GetFloat() ); + glUniform1fARB( g_pointShadowMapProgram.shadowNormalBias, + receiverSettings.normalBias ); } if ( g_pointShadowMapProgram.pointShadowTexelDepthBias >= 0 ) { - const float texelDepthBias = Max( 0.0f, r_shadowMapTexelBiasScale.GetFloat() ) / Max( 1, g_pointShadowMapRenderTexture->GetWidth() ); - glUniform1fARB( g_pointShadowMapProgram.pointShadowTexelDepthBias, texelDepthBias ); + glUniform1fARB( + g_pointShadowMapProgram.pointShadowTexelDepthBias, + receiverSettings.texelBiasScale / pointFaceSize ); } if ( g_pointShadowMapProgram.pointShadowNormalOffsetWorld >= 0 ) { // per-distance texel factor: a cube face spans 2*distance across // width texels, so one texel at distance d is (2*d/width) world units - const float normalOffsetFactor = 2.0f * Max( 0.0f, r_shadowMapNormalOffsetScale.GetFloat() ) / Max( 1, g_pointShadowMapRenderTexture->GetWidth() ); - glUniform1fARB( g_pointShadowMapProgram.pointShadowNormalOffsetWorld, normalOffsetFactor ); + glUniform1fARB( + g_pointShadowMapProgram.pointShadowNormalOffsetWorld, + 2.0f * receiverSettings.normalOffsetScale / pointFaceSize ); } if ( g_pointShadowMapProgram.shadowFilterRadius >= 0 ) { glUniform1fARB( g_pointShadowMapProgram.shadowFilterRadius, r_shadowMapPointFilterRadius.GetFloat() ); @@ -10364,10 +10974,11 @@ static bool RB_GLSLPointShadowMap_CreateDrawInteractions( const drawSurf_t *surf glUniform4fvARB( g_pointShadowMapProgram.globalLightOrigin, 1, globalLightOrigin ); } if ( g_pointShadowMapProgram.pointShadowFar >= 0 ) { - glUniform1fARB( g_pointShadowMapProgram.pointShadowFar, RB_PointShadowMapLightFar( backEnd.vLight ) ); + glUniform1fARB( g_pointShadowMapProgram.pointShadowFar, + pointFarDistance ); } if ( g_pointShadowMapProgram.pointShadowTexelScale >= 0 ) { - const float texelScale = 2.0f / Max( 1, g_pointShadowMapRenderTexture->GetWidth() ); + const float texelScale = 2.0f / pointFaceSize; glUniform1fARB( g_pointShadowMapProgram.pointShadowTexelScale, texelScale ); } if ( g_pointShadowMapProgram.pointShadowDepthMode >= 0 ) { @@ -11008,7 +11619,7 @@ static void RB_ShadowMapTrackWrappedCustomGLSLReceivers( const viewLight_t *vLig } } -static void RB_ShadowMapDrawReceiverFallbacks( const viewLight_t *vLight, const shadowMapPassKind_t passKind, const drawSurf_t *primaryShadowSurfs, const drawSurf_t *secondaryShadowSurfs, const drawSurf_t *interactions ) { +static void RB_ShadowMapDrawReceiverFallbacks( const viewLight_t *vLight, const shadowMapPassKind_t passKind, const drawSurf_t *primaryShadowSurfs, const drawSurf_t *secondaryShadowSurfs, const drawSurf_t *interactions, const bool drawInteractions = true ) { int receiverFallbackReasons[SHADOWMAP_RECEIVER_FALLBACK_COUNT]; const int receiverFallbackSurfaces = RB_CountShadowMapReceiverFallbackSurfaces( interactions, receiverFallbackReasons ); if ( receiverFallbackSurfaces <= 0 ) { @@ -11044,13 +11655,106 @@ static void RB_ShadowMapDrawReceiverFallbacks( const viewLight_t *vLight, const RB_ShadowMapReceiverFallbackReasonName( SHADOWMAP_RECEIVER_FALLBACK_GENERATED_GEOMETRY ), receiverFallbackReasons[SHADOWMAP_RECEIVER_FALLBACK_GENERATED_GEOMETRY] ); } + if ( !drawInteractions ) { + return; + } shadowMapTimedPhase_t timedPhase; RB_ShadowMapBeginTimedPhase( timedPhase, vLight, passKind, SHADOWMAP_TIMING_RECEIVER_FALLBACK ); RB_ShadowMapStencilFallbackFiltered( primaryShadowSurfs, secondaryShadowSurfs, interactions, RB_SurfaceNeedsShadowMapReceiverFallback ); RB_ShadowMapEndTimedPhase( timedPhase ); } -static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t passKind, bool pointLight, const drawSurf_t *primaryCasters, const drawSurf_t *secondaryCasters, const drawSurf_t *tertiaryCasters, const drawSurf_t *quaternaryCasters, const drawSurf_t *primaryShadowSurfs, const drawSurf_t *secondaryShadowSurfs, const drawSurf_t *interactions ) { +static int RB_ShadowMapReceiverMaskForPass( + const shadowMapPassKind_t passKind ) { + return passKind == SHADOWMAP_PASS_LOCAL + ? SHADOWMAP_RECEIVER_MASK_LOCAL : SHADOWMAP_RECEIVER_MASK_GLOBAL; +} + +static bool RB_ShadowMapHybridAvailableForPass( const viewLight_t *vLight, + const shadowMapPassKind_t passKind ) { + if ( vLight == NULL ) { + return false; + } + const int receiverMask = RB_ShadowMapReceiverMaskForPass( passKind ); + const int incompleteMapMask = vLight->shadowMapIncompleteMapMask + | vLight->shadowMapPrelightMapMissingMask; + if ( ( incompleteMapMask & receiverMask ) == 0 ) { + return false; + } + // A combined prelight volume cannot supplement a partial map without also + // restamping mapped casters. Ordinary per-surface supplements can. + if ( ( vLight->shadowMapPrelightMapMissingMask & receiverMask ) != 0 + || ( vLight->shadowMapHybridIncompleteMask & receiverMask ) != 0 ) { + return false; + } + return vLight->globalShadowMapStencilSupplements != NULL + || ( passKind == SHADOWMAP_PASS_GLOBAL + && vLight->localShadowMapStencilSupplements != NULL ); +} + +static bool RB_ShadowMapMapOrHybridAvailableForPass( + const viewLight_t *vLight, const shadowMapPassKind_t passKind, + bool *hybridOut ) { + if ( hybridOut != NULL ) { + *hybridOut = false; + } + if ( vLight == NULL ) { + return false; + } + const int receiverMask = RB_ShadowMapReceiverMaskForPass( passKind ); + const bool mapIncomplete = ( ( vLight->shadowMapIncompleteMapMask + | vLight->shadowMapPrelightMapMissingMask ) & receiverMask ) != 0; + if ( !mapIncomplete ) { + return true; + } + const bool hybrid = RB_ShadowMapHybridAvailableForPass( + vLight, passKind ); + if ( hybridOut != NULL ) { + *hybridOut = hybrid; + } + return hybrid; +} + +static bool RB_ShadowMapPrepareMappedReceiverStencil( + const viewLight_t *vLight, const shadowMapPassKind_t passKind, + const bool hybrid ) { + if ( !hybrid ) { + glStencilFunc( GL_ALWAYS, 128, 255 ); + return true; + } + const drawSurf_t *globalSupplements = + vLight != NULL ? vLight->globalShadowMapStencilSupplements : NULL; + const drawSurf_t *localSupplements = + ( vLight != NULL && passKind == SHADOWMAP_PASS_GLOBAL ) + ? vLight->localShadowMapStencilSupplements : NULL; + if ( globalSupplements == NULL && localSupplements == NULL ) { + return false; + } + + backEnd.currentScissor = vLight->scissorRect; + if ( r_useScissor.GetBool() ) { + glScissor( backEnd.viewDef->viewport.x1 + backEnd.currentScissor.x1, + backEnd.viewDef->viewport.y1 + backEnd.currentScissor.y1, + backEnd.currentScissor.x2 + 1 - backEnd.currentScissor.x1, + backEnd.currentScissor.y2 + 1 - backEnd.currentScissor.y1 ); + } + glClear( GL_STENCIL_BUFFER_BIT ); + const bool useShadowVertexProgram = r_useShadowVertexProgram.GetBool() + && R_BindARBProgram( GL_VERTEX_PROGRAM_ARB, VPROG_STENCIL_SHADOW, + "shadow-map hybrid stencil vertex program", true ); + if ( useShadowVertexProgram ) { + glEnable( GL_VERTEX_PROGRAM_ARB ); + } + RB_StencilShadowPass( globalSupplements ); + RB_StencilShadowPass( localSupplements ); + if ( useShadowVertexProgram ) { + glDisable( GL_VERTEX_PROGRAM_ARB ); + } + glStencilFunc( GL_GEQUAL, 128, 255 ); + return true; +} + +static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t passKind, bool pointLight, const drawSurf_t *primaryCasters, const drawSurf_t *secondaryCasters, const drawSurf_t *tertiaryCasters, const drawSurf_t *quaternaryCasters, const drawSurf_t *primaryShadowSurfs, const drawSurf_t *secondaryShadowSurfs, const drawSurf_t *interactions, const bool deferReceiverDraw = false ) { if ( interactions == NULL ) { return; } @@ -11081,8 +11785,10 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t g_shadowMapStats.unshadowedGlobalPasses++; } RB_ShadowMapPassReport( vLight, passKind, pointLight, SHADOWMAP_PASS_RESULT_NO_SHADOW_SURFS, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); - glStencilFunc( GL_ALWAYS, 128, 255 ); - RB_DrawMaterialInteractions( interactions ); + if ( !deferReceiverDraw ) { + glStencilFunc( GL_ALWAYS, 128, 255 ); + RB_DrawMaterialInteractions( interactions ); + } return; } @@ -11098,16 +11804,46 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapPassReport( vLight, passKind, pointLight, passResult, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapMarkStencilFallbackSticky( vLight ); - RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + } return; } if ( !RB_DrawSurfChainHasFilteredSurface( interactions, RB_SurfaceEligibleForShadowMapReceiver ) ) { - RB_ShadowMapDrawReceiverFallbacks( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + RB_ShadowMapDrawReceiverFallbacks( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions, !deferReceiverDraw ); RB_ShadowMapPassReport( vLight, passKind, pointLight, SHADOWMAP_PASS_RESULT_RECEIVER_FALLBACK, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); return; } + bool hybrid = false; + const bool mapOrHybridAvailable = + RB_ShadowMapMapOrHybridAvailableForPass( + vLight, passKind, &hybrid ); + if ( passKind == SHADOWMAP_PASS_GLOBAL ) { + g_shadowMapGlobalPassHybrid = hybrid; + } + if ( !mapOrHybridAvailable ) { + // This ownership cannot be represented by a map alone, and its exact + // supplement set is unavailable. Preserve full-stencil behavior rather + // than silently accepting a partial mapped shadow. + if ( passKind == SHADOWMAP_PASS_LOCAL ) { + g_shadowMapStats.fallbackLocalPasses++; + } else { + g_shadowMapStats.fallbackGlobalPasses++; + } + RB_ShadowMapPassReport( vLight, passKind, pointLight, + SHADOWMAP_PASS_RESULT_STENCIL_ONLY, primaryCasters, + secondaryCasters, tertiaryCasters, quaternaryCasters, + primaryShadowSurfs, secondaryShadowSurfs, interactions ); + RB_ShadowMapMarkStencilFallbackSticky( vLight ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, + secondaryShadowSurfs, interactions ); + } + return; + } + shadowMapSchedule_t schedule = RB_ShadowMapSchedulePass( vLight, passKind, pointLight, haveTranslucentCasters ); // cached projected lights with dynamic casters compose: blit the cached @@ -11120,7 +11856,7 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t if ( composeDynamics ) { shadowMapTimedPhase_t composeTimer; RB_ShadowMapBeginTimedPhase( composeTimer, vLight, passKind, SHADOWMAP_TIMING_MAP_RENDER ); - RB_ShadowMapSelectScratchResources(); + RB_ShadowMapSelectProjectedScratchResources(); const bool composeOk = RB_RenderShadowMap( primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, SHADOWMAP_RENDER_COMPOSE_DYNAMIC, schedule.projectedEntry ); g_shadowMapStats.cpuRenderMilliseconds += RB_ShadowMapEndTimedPhase( composeTimer ); @@ -11134,20 +11870,37 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } RB_ShadowMapPassReport( vLight, passKind, pointLight, SHADOWMAP_PASS_RESULT_RENDER_FAIL, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapMarkStencilFallbackSticky( vLight ); - RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + } return; } } - shadowMapTimedPhase_t cacheReuseTimer; - RB_ShadowMapBeginTimedPhase( cacheReuseTimer, vLight, passKind, SHADOWMAP_TIMING_CACHE_REUSE ); - const bool maskOk = pointLight ? RB_GLSLPointShadowMap_CreateDrawInteractions( interactions ) : RB_GLSLShadowMap_CreateDrawInteractions( interactions ); - const float cacheReuseMilliseconds = RB_ShadowMapEndTimedPhase( cacheReuseTimer ); - g_shadowMapStats.cpuMaskMilliseconds += cacheReuseMilliseconds; if ( pointLight && schedule.pointEntry != NULL ) { schedule.pointEntry->lastUsedFrame = tr.frameCount; } else if ( schedule.projectedEntry != NULL ) { schedule.projectedEntry->lastUsedFrame = tr.frameCount; } + if ( deferReceiverDraw ) { + g_shadowMapGlobalPassMapped = pointLight + ? SHADOWMAP_GLOBAL_MAPPED_POINT + : SHADOWMAP_GLOBAL_MAPPED_PROJECTED; + g_shadowMapDeferredGlobalReportPending = true; + g_shadowMapDeferredGlobalPassResult = + SHADOWMAP_PASS_RESULT_CACHE_REUSE; + return; + } + shadowMapTimedPhase_t cacheReuseTimer; + RB_ShadowMapBeginTimedPhase( cacheReuseTimer, vLight, passKind, SHADOWMAP_TIMING_CACHE_REUSE ); + const bool receiverStencilReady = + RB_ShadowMapPrepareMappedReceiverStencil( + vLight, passKind, hybrid ); + const bool maskOk = receiverStencilReady + && ( pointLight + ? RB_GLSLPointShadowMap_CreateDrawInteractions( interactions ) + : RB_GLSLShadowMap_CreateDrawInteractions( interactions ) ); + const float cacheReuseMilliseconds = RB_ShadowMapEndTimedPhase( cacheReuseTimer ); + g_shadowMapStats.cpuMaskMilliseconds += cacheReuseMilliseconds; if ( maskOk ) { if ( passKind == SHADOWMAP_PASS_LOCAL ) { g_shadowMapStats.mappedLocalPasses++; @@ -11176,7 +11929,9 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } RB_ShadowMapPassReport( vLight, passKind, pointLight, SHADOWMAP_PASS_RESULT_MASK_FAIL, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapMarkStencilFallbackSticky( vLight ); - RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + } return; } @@ -11189,7 +11944,9 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } RB_ShadowMapPassReport( vLight, passKind, pointLight, SHADOWMAP_PASS_RESULT_SCHEDULED_SKIP, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapMarkStencilFallbackSticky( vLight ); - RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + } return; } @@ -11207,7 +11964,7 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t if ( composePlanned && renderOk ) { shadowMapTimedPhase_t composeTimer; RB_ShadowMapBeginTimedPhase( composeTimer, vLight, passKind, SHADOWMAP_TIMING_MAP_RENDER ); - RB_ShadowMapSelectScratchResources(); + RB_ShadowMapSelectProjectedScratchResources(); renderOk = RB_RenderShadowMap( primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, SHADOWMAP_RENDER_COMPOSE_DYNAMIC, schedule.projectedEntry ); g_shadowMapStats.cpuRenderMilliseconds += RB_ShadowMapEndTimedPhase( composeTimer ); @@ -11221,11 +11978,23 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } bool maskOk = false; if ( renderOk ) { - shadowMapTimedPhase_t maskTimer; - RB_ShadowMapBeginTimedPhase( maskTimer, vLight, passKind, SHADOWMAP_TIMING_MASK_PASS ); - maskOk = pointLight ? RB_GLSLPointShadowMap_CreateDrawInteractions( interactions ) : RB_GLSLShadowMap_CreateDrawInteractions( interactions ); - const float maskMilliseconds = RB_ShadowMapEndTimedPhase( maskTimer ); - g_shadowMapStats.cpuMaskMilliseconds += maskMilliseconds; + if ( deferReceiverDraw ) { + // Blended receivers stay in their ordered translucent phase. That + // phase prepares any stencil supplement and consumes this map once. + maskOk = true; + } else { + shadowMapTimedPhase_t maskTimer; + RB_ShadowMapBeginTimedPhase( maskTimer, vLight, passKind, SHADOWMAP_TIMING_MASK_PASS ); + const bool receiverStencilReady = + RB_ShadowMapPrepareMappedReceiverStencil( + vLight, passKind, hybrid ); + maskOk = receiverStencilReady + && ( pointLight + ? RB_GLSLPointShadowMap_CreateDrawInteractions( interactions ) + : RB_GLSLShadowMap_CreateDrawInteractions( interactions ) ); + const float maskMilliseconds = RB_ShadowMapEndTimedPhase( maskTimer ); + g_shadowMapStats.cpuMaskMilliseconds += maskMilliseconds; + } } shadowMapPassResult_t passResult = SHADOWMAP_PASS_RESULT_MAPPED; if ( !renderOk ) { @@ -11235,11 +12004,20 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } const bool mapped = ( passResult == SHADOWMAP_PASS_RESULT_MAPPED ); - RB_ShadowMapDebugOverlayCapture( vLight, passKind, pointLight, mapped, renderOk, - primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, - primaryShadowSurfs, secondaryShadowSurfs, interactions ); - if ( mapped ) { + if ( deferReceiverDraw ) { + g_shadowMapGlobalPassMapped = pointLight + ? SHADOWMAP_GLOBAL_MAPPED_POINT + : SHADOWMAP_GLOBAL_MAPPED_PROJECTED; + g_shadowMapDeferredGlobalReportPending = true; + g_shadowMapDeferredGlobalPassResult = + SHADOWMAP_PASS_RESULT_MAPPED; + return; + } + RB_ShadowMapDebugOverlayCapture( vLight, passKind, pointLight, true, + renderOk, primaryCasters, secondaryCasters, tertiaryCasters, + quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, + interactions ); if ( passKind == SHADOWMAP_PASS_LOCAL ) { g_shadowMapStats.mappedLocalPasses++; } else { @@ -11256,6 +12034,11 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t return; } + RB_ShadowMapDebugOverlayCapture( vLight, passKind, pointLight, false, + renderOk, primaryCasters, secondaryCasters, tertiaryCasters, + quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, + interactions ); + if ( passKind == SHADOWMAP_PASS_LOCAL ) { g_shadowMapStats.fallbackLocalPasses++; if ( passResult == SHADOWMAP_PASS_RESULT_RENDER_FAIL ) { @@ -11273,7 +12056,38 @@ static void RB_ShadowMapRunPass( const viewLight_t *vLight, shadowMapPassKind_t } RB_ShadowMapPassReport( vLight, passKind, pointLight, passResult, primaryCasters, secondaryCasters, tertiaryCasters, quaternaryCasters, primaryShadowSurfs, secondaryShadowSurfs, interactions ); RB_ShadowMapMarkStencilFallbackSticky( vLight ); - RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + if ( !deferReceiverDraw ) { + RB_ShadowMapStencilFallback( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions ); + } +} + +static void RB_ShadowMapCompleteDeferredGlobalReceiver( + const viewLight_t *vLight, const bool pointLight, + const bool maskOk ) { + if ( !g_shadowMapDeferredGlobalReportPending ) { + return; + } + + const shadowMapPassResult_t result = maskOk + ? g_shadowMapDeferredGlobalPassResult + : SHADOWMAP_PASS_RESULT_MASK_FAIL; + if ( maskOk ) { + g_shadowMapStats.mappedGlobalPasses++; + } else { + g_shadowMapStats.fallbackGlobalPasses++; + g_shadowMapStats.maskFailGlobalPasses++; + } + RB_ShadowMapDebugOverlayCapture( vLight, SHADOWMAP_PASS_GLOBAL, + pointLight, maskOk, true, vLight->globalShadowMapCasters, + vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, + vLight->localShadowMapDynamicCasters, vLight->globalShadows, + vLight->localShadows, vLight->translucentInteractions ); + RB_ShadowMapPassReport( vLight, SHADOWMAP_PASS_GLOBAL, pointLight, + result, vLight->globalShadowMapCasters, + vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, + vLight->localShadowMapDynamicCasters, vLight->globalShadows, + vLight->localShadows, vLight->translucentInteractions ); + g_shadowMapDeferredGlobalReportPending = false; } /* @@ -11653,11 +12467,3710 @@ static void RB_ARB2_RestoreBypassedInteractionState( void ) { RB_ARB2_RestoreInteractionState( true ); } -void RB_ARB2_DrawInteractions( void ) { - viewLight_t *vLight; +/* +=============================================================================== - if ( glConfig.disableARB2Interactions ) { - if ( r_interactionColorMode.IsModified() ) { + Backend-neutral classic interaction consumer + + ClassicInteractionDomain has already interpreted the complete light, + receiver, and material-stage stream. This adapter performs every fallible + OpenGL/resource/cache check for the complete view before it changes the + framebuffer. After commit it uses only sealed values plus the explicitly + retained geometry bridge; the classic interaction walker remains the atomic + rollback path when any part of preflight is rejected. + +=============================================================================== +*/ + +enum rbSharedWorldInteractionGLReject_t { + RB_SHARED_WORLD_INTERACTION_GL_REJECT_VIEW = 1, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_MUTATED_VIEW, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_CAPACITY, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PROGRAM, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_LIGHT_RANGE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SURFACE_RANGE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PRIMITIVE_RANGE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PRIMITIVE_STATE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_GEOMETRY, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_VERTEX_CACHE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_INDEX_CACHE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_TEXTURE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_INTRINSIC_TEXTURE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MODE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_RANGE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_STATE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_GEOMETRY, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_VERTEX_CACHE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_INDEX_CACHE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_PASS, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_CASTER, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_ALPHA, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_RESOURCE, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_TRANSACTION, + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE +}; + +typedef struct rbSharedWorldInteractionGLPreparedPrimitive_s { + const classicInteractionDomainPrimitive_t *primitive; + unsigned int vertexBuffer; + const void *vertexPointer; + unsigned int indexBuffer; + const void *indexPointer; + int vertexCount; + int indexCount; + bool referencedVertexes; + bool referencedIndexes; + bool indexCacheBacked; + idImage *lightImage; + idImage *lightFalloffImage; + idImage *bumpImage; + idImage *diffuseImage; + idImage *specularImage; + idImage *normalImage; + int stateBits; + int cullType; +} rbSharedWorldInteractionGLPreparedPrimitive_t; + +typedef struct rbSharedWorldInteractionGLPreparedShadowCaster_s { + const classicInteractionDomainShadowCaster_t *caster; + unsigned int vertexBuffer; + const void *vertexPointer; + unsigned int indexBuffer; + const void *indexPointer; + int vertexCount; + int indexCount; + bool indexCacheBacked; +} rbSharedWorldInteractionGLPreparedShadowCaster_t; + +typedef struct rbSharedWorldInteractionGLPreparedShadowAlphaStage_s { + const classicInteractionDomainShadowAlphaStage_t *stage; + idImage *image; +} rbSharedWorldInteractionGLPreparedShadowAlphaStage_t; + +typedef struct rbSharedWorldInteractionGLPreparedMapCaster_s { + const classicInteractionDomainShadowCaster_t *caster; + unsigned int vertexBuffer; + const void *vertexPointer; + unsigned int indexBuffer; + const void *indexPointer; + int vertexCount; + int indexCount; + int firstAlphaStage; + int alphaStageCount; + bool indexCacheBacked; +} rbSharedWorldInteractionGLPreparedMapCaster_t; + +typedef struct rbSharedWorldInteractionGLPreparedMapPass_s { + const classicInteractionDomainShadowMapPass_t *pass; + projectedShadowMapCacheEntry_t *projectedEntry; + pointShadowMapCacheEntry_t *pointEntry; + idImage *projectedDepthImage; + idImage *pointColorImage; + idImage *pointDepthImage; + idRenderTexture *renderTexture; + std::uint64_t sampleStorageGeneration; + unsigned int sampleTextureHandle; + int resourceWidth; + int resourceHeight; + int projectedOriginX; + int projectedOriginY; + int projectedSpanPixels; + int cacheLightIndex; + int cachePassKind; + int signature; + bool pointLight; + bool scratchResource; + bool cacheUpdated; + bool prepared; +} rbSharedWorldInteractionGLPreparedMapPass_t; + +typedef struct rbSharedWorldInteractionGLPreparedLight_s { + const classicInteractionDomainLight_t *light; + int firstPrimitive[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int primitiveCount[ CLASSIC_INTERACTION_RECEIVER_COUNT ]; + int firstShadowCaster[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int shadowCasterCount[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int firstMapCaster[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int mapCasterCount[ CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ]; + int shadowMapPass[ 2 ]; +} rbSharedWorldInteractionGLPreparedLight_t; + +typedef struct rbSharedWorldInteractionGLPreparedView_s { + const classicInteractionDomainView_t *view; + const viewDef_t *viewDef; + std::uint64_t hash; + int lightCount; + int surfaceCount; + int primitiveCount; + int drawablePrimitiveCount; + int noopPrimitiveCount; + int shadowCasterCount; + int drawableShadowCasterCount; + int noopShadowCasterCount; + int logicalVolumeDrawCount; + int preloadVolumeDrawCount; + int mapCasterCount; + int shadowAlphaStageCount; + int shadowMapPassCount; + int hybridShadowPassCount; + int projectedScratchCount; + int pointScratchCount; + bool twoSidedStencil; + bool mapTransactionPrepared; + bool mapGLStateCaptured; + bool mapBlendWasEnabled; + bool mapDepthWasEnabled; + bool mapScissorWasEnabled; + bool mapStencilWasEnabled; + bool mapCullWasEnabled; + bool mapDepthWriteWasEnabled; + bool mapColorWriteWasEnabled[ 4 ]; + int mapSavedFaceCulling; + float mapSavedClearColor[ 4 ]; + bool ready; + bool committed; + bool seenLights[ CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ]; + bool seenSurfaces[ CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES ]; + bool seenPrimitives[ CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ]; + bool seenShadowCasters[ CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + rbSharedWorldInteractionGLPreparedLight_t lights[ + CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS ]; + rbSharedWorldInteractionGLPreparedPrimitive_t primitives[ + CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES ]; + rbSharedWorldInteractionGLPreparedShadowCaster_t shadowCasters[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + rbSharedWorldInteractionGLPreparedMapCaster_t mapCasters[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS ]; + rbSharedWorldInteractionGLPreparedShadowAlphaStage_t shadowAlphaStages[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES ]; + rbSharedWorldInteractionGLPreparedMapPass_t shadowMapPasses[ + CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES ]; +} rbSharedWorldInteractionGLPreparedView_t; + +static rbSharedWorldInteractionGLPreparedView_t + rbSharedWorldInteractionGLPreparedView; + +static int RB_SharedWorldInteractionGLFailureDetail( + rbSharedWorldInteractionGLReject_t reason, int lightIndex = -1, + int surfaceIndex = -1, int primitiveIndex = -1 ) { + const int lightDetail = lightIndex >= 0 ? Min( lightIndex + 1, 255 ) : 0; + const int surfaceDetail = surfaceIndex >= 0 ? Min( surfaceIndex + 1, 4095 ) : 0; + const int primitiveDetail = primitiveIndex >= 0 ? Min( primitiveIndex + 1, 4095 ) : 0; + return static_cast( reason ) * 100000000 + + lightDetail * 1000000 + surfaceDetail * 4096 + primitiveDetail; +} + +static bool RB_SharedWorldInteractionGLFinite( float value ) { + return !FLOAT_IS_NAN( value ); +} + +static bool RB_SharedWorldInteractionGLFiniteArray( const float *values, + int count ) { + if ( values == NULL || count < 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !RB_SharedWorldInteractionGLFinite( values[i] ) ) { + return false; + } + } + return true; +} + +static bool RB_SharedWorldInteractionGLMapCull( rendererCullMode_t cull, + int &cullType ) { + switch ( cull ) { + case RENDERER_CULL_NONE: + cullType = CT_TWO_SIDED; + return true; + case RENDERER_CULL_FRONT: + cullType = CT_FRONT_SIDED; + return true; + case RENDERER_CULL_BACK: + cullType = CT_BACK_SIDED; + return true; + default: + return false; + } +} + +static bool RB_SharedWorldInteractionGLBlendValid( + const rendererBlendState_t &blend ) { + return blend.enabled + && blend.sourceColor == RENDERER_BLEND_ONE + && blend.destinationColor == RENDERER_BLEND_ONE + && blend.colorOperation == RENDERER_BLEND_OP_ADD + && blend.sourceAlpha == RENDERER_BLEND_ONE + && blend.destinationAlpha == RENDERER_BLEND_ONE + && blend.alphaOperation == RENDERER_BLEND_OP_ADD; +} + +static bool RB_SharedWorldInteractionGLIntrinsicImageValid( idImage *image, + textureType_t expectedType ) { + return image != NULL && image->IsLoaded() && !image->IsDefaulted() + && image->GetDeviceHandle() != 0 + && image->GetOpts().textureType == expectedType; +} + +static bool RB_SharedWorldInteractionGLValidateTexture( + std::uint64_t resourceId, + const classicInteractionDomainTexture_t *texture, idImage *&image ) { + image = NULL; + if ( resourceId == 0 ) { + return false; + } + if ( texture == NULL || texture->textureResourceId != resourceId + || texture->image == NULL || !texture->loaded + || texture->defaulted || texture->mutableImage + || texture->textureHandle == 0 ) { + return false; + } + idImage *resolved = const_cast( texture->image ); + const textureType_t textureType = resolved->GetOpts().textureType; + if ( !resolved->IsLoaded() || resolved->IsDefaulted() + || resolved->GetDeviceHandle() != texture->textureHandle + || resolved->GetStorageGeneration() != texture->storageGeneration + || resolved->GetFilter() != texture->filter + || resolved->GetRepeat() != texture->repeat + || ( textureType != TT_2D && textureType != TT_CUBIC ) ) { + return false; + } + image = resolved; + return true; +} + +static bool RB_SharedWorldInteractionGLCacheValid( const vertCache_t *cache, + bool indexBuffer, int requiredBytes ) { + if ( cache == NULL || requiredBytes <= 0 || cache->tag == TAG_FREE + || cache->indexBuffer != indexBuffer || cache->offset < 0 + || cache->size < requiredBytes ) { + return false; + } + if ( cache->vbo != 0 ) { + return glConfig.ARBVertexBufferObjectAvailable + && cache->virtMem == NULL; + } + return cache->virtMem != NULL; +} + +static const void *RB_SharedWorldInteractionGLCachePointer( + const vertCache_t &cache ) { + if ( cache.vbo != 0 ) { + return reinterpret_cast( + static_cast( cache.offset ) ); + } + return static_cast( cache.virtMem ) + cache.offset; +} + +static bool RB_SharedWorldInteractionGLPrimitiveValuesValid( + const classicInteractionDomainPrimitive_t &primitive ) { + if ( primitive.lightIndex < 0 || primitive.surfaceIndex < 0 + || primitive.receiver < CLASSIC_INTERACTION_RECEIVER_LOCAL + || primitive.receiver >= CLASSIC_INTERACTION_RECEIVER_COUNT + || primitive.disposition < CLASSIC_INTERACTION_PRIMITIVE_DRAW + || primitive.disposition >= CLASSIC_INTERACTION_PRIMITIVE_COUNT + || primitive.depth < CLASSIC_INTERACTION_DEPTH_EQUAL + || primitive.depth >= CLASSIC_INTERACTION_DEPTH_COUNT + || primitive.vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || primitive.vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || primitive.vertexCount <= 0 || primitive.firstIndex != 0 + || primitive.indexCount <= 0 || primitive.indexCount % 3 != 0 + || primitive.vertexOffset != 0 ) { + return false; + } + const long long scissorWidth = + static_cast( primitive.scissorX2 ) - primitive.scissorX1 + 1; + const long long scissorHeight = + static_cast( primitive.scissorY2 ) - primitive.scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > idMath::INT_MAX + || scissorHeight > idMath::INT_MAX + || !RB_SharedWorldInteractionGLBlendValid( primitive.blend ) + || !RB_SharedWorldInteractionGLFinite( + primitive.polygonOffsetFactor ) + || !RB_SharedWorldInteractionGLFinite( + primitive.polygonOffsetUnits ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.diffuseColor, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.specularColor, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.flatDiffuseParams, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.localLightOrigin, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.localViewOrigin, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + &primitive.lightProjection[0][0], 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( + &primitive.bumpMatrix[0][0], 8 ) + || !RB_SharedWorldInteractionGLFiniteArray( + &primitive.diffuseMatrix[0][0], 8 ) + || !RB_SharedWorldInteractionGLFiniteArray( + &primitive.specularMatrix[0][0], 8 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.modelMatrix, 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( + primitive.modelViewMatrix, 16 ) ) { + return false; + } + return ( primitive.receiver == CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ) + ? primitive.depth == CLASSIC_INTERACTION_DEPTH_LESS_OR_EQUAL + : primitive.depth == CLASSIC_INTERACTION_DEPTH_EQUAL; +} + +static bool RB_SharedWorldInteractionGLShadowCasterValuesValid( + const classicInteractionDomainShadowCaster_t &caster, + classicInteractionDomainShadowChain_t expectedChain ) { + if ( caster.legacyDrawSurf == NULL || caster.legacyViewLight == NULL + || caster.legacyCasterGeometry == NULL || caster.lightIndex < 0 + || caster.sourceOrdinal < 0 || caster.chainOrdinal < 0 + || caster.chain != expectedChain + || caster.disposition < CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + || caster.disposition + >= CLASSIC_INTERACTION_SHADOW_CASTER_DISPOSITION_COUNT + || caster.indexSelection < CLASSIC_INTERACTION_SHADOW_INDEX_FULL + || caster.indexSelection > CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS + || caster.alphaStageCount != 0 || caster.firstAlphaStage != -1 + || caster.vertexCount < 0 || caster.totalIndexCount < 0 + || caster.selectedIndexCount < 0 + || caster.selectedIndexCount > caster.totalIndexCount + || caster.selectedIndexCount % 3 != 0 + || caster.ambientGeometry || caster.dynamicCaster + || caster.translucentCaster ) { + return false; + } + const long long scissorWidth = static_cast( caster.scissorX2 ) + - caster.scissorX1 + 1; + const long long scissorHeight = static_cast( caster.scissorY2 ) + - caster.scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > idMath::INT_MAX + || scissorHeight > idMath::INT_MAX + || !RB_SharedWorldInteractionGLFinite( caster.depthMin ) + || !RB_SharedWorldInteractionGLFinite( caster.depthMax ) + || caster.depthMin < 0.0f || caster.depthMin > 1.0f + || caster.depthMax < caster.depthMin || caster.depthMax > 1.0f + || !RB_SharedWorldInteractionGLFiniteArray( + caster.localLightOrigin, 4 ) + || !RB_SharedWorldInteractionGLFiniteArray( + caster.modelMatrix, 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( + caster.modelViewMatrix, 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( caster.boundsMin, 3 ) + || !RB_SharedWorldInteractionGLFiniteArray( caster.boundsMax, 3 ) ) { + return false; + } + if ( caster.disposition == CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY ) { + return !caster.preload + && ( caster.vertexCount == 0 || caster.selectedIndexCount == 0 ); + } + return caster.vertexCount > 0 && caster.totalIndexCount > 0 + && caster.selectedIndexCount > 0 + && caster.preload != caster.external; +} + +static bool RB_SharedWorldInteractionGLIsMapCasterChain( + classicInteractionDomainShadowChain_t chain ) { + return chain >= CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC + && chain <= CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT; +} + +static bool RB_SharedWorldInteractionGLIsVolumeChain( + classicInteractionDomainShadowChain_t chain ) { + return chain == CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL + || chain == CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL + || chain == CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + || chain == CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL; +} + +static bool RB_SharedWorldInteractionGLMapCasterValuesValid( + const classicInteractionDomainShadowCaster_t &caster, + classicInteractionDomainShadowChain_t expectedChain ) { + if ( caster.legacyDrawSurf == NULL || caster.legacyViewLight == NULL + || caster.legacyCasterGeometry == NULL || caster.lightIndex < 0 + || caster.sourceOrdinal < 0 || caster.chainOrdinal < 0 + || caster.chain != expectedChain + || !RB_SharedWorldInteractionGLIsMapCasterChain( expectedChain ) + || caster.disposition < CLASSIC_INTERACTION_SHADOW_CASTER_DRAW + || caster.disposition + >= CLASSIC_INTERACTION_SHADOW_CASTER_DISPOSITION_COUNT + || caster.indexSelection + != CLASSIC_INTERACTION_SHADOW_INDEX_AMBIENT + || !caster.ambientGeometry || caster.external || caster.preload + || caster.vertexCount < 0 || caster.totalIndexCount < 0 + || caster.selectedIndexCount < 0 + || caster.selectedIndexCount % 3 != 0 + || ( caster.materialCoverage != MC_OPAQUE + && caster.materialCoverage != MC_PERFORATED ) + || caster.alphaStageCount < 0 + || ( caster.alphaStageCount == 0 + && caster.firstAlphaStage != -1 ) + || ( caster.alphaStageCount > 0 + && caster.firstAlphaStage < 0 ) + || caster.translucentCaster + || caster.dynamicCaster != ( + expectedChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC + || expectedChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ) ) { + return false; + } + if ( caster.cull < RENDERER_CULL_NONE + || caster.cull > RENDERER_CULL_BACK + || !RB_SharedWorldInteractionGLFiniteArray( + caster.modelMatrix, 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( + caster.modelViewMatrix, 16 ) + || !RB_SharedWorldInteractionGLFiniteArray( caster.boundsMin, 3 ) + || !RB_SharedWorldInteractionGLFiniteArray( caster.boundsMax, 3 ) ) { + return false; + } + if ( caster.disposition == CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY ) { + return R_ClassicInteractionDomain_ShadowCasterNoopValid( caster ); + } + // A perforated material with no active alpha-test stage follows the classic + // opaque-depth fallback; active zero-alpha stages are normalized above to a + // no-op, while drawable stages carry explicit sealed alpha records. + return caster.vertexCount > 0 && caster.selectedIndexCount > 0 + && caster.selectedIndexCount == caster.totalIndexCount; +} + +static bool RB_SharedWorldInteractionGLShadowAlphaStageValuesValid( + const classicInteractionDomainShadowAlphaStage_t &stage, + int expectedCasterIndex ) { + return stage.casterIndex == expectedCasterIndex && stage.stageIndex >= 0 + && ( stage.alphaTestMode == GL_LESS + || stage.alphaTestMode == GL_EQUAL + || stage.alphaTestMode == GL_GREATER ) + && RB_SharedWorldInteractionGLFinite( stage.alphaTestValue ) + && RB_SharedWorldInteractionGLFinite( stage.alphaScale ) + && stage.alphaScale > 0.0f + && RB_SharedWorldInteractionGLFinite( stage.alphaHashMode ) + && RB_SharedWorldInteractionGLFiniteArray( + &stage.textureMatrix[0][0], 8 ) + && stage.textureResourceId != 0; +} + +static bool RB_SharedWorldInteractionGLMapPassValuesValid( + const classicInteractionDomainLight_t &light, + const classicInteractionDomainShadowMapPass_t &pass, + classicInteractionDomainReceiver_t receiver, int domainLightIndex ) { + const bool pointLight = light.pointLight; + const bool hybrid = pass.disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID; + if ( pass.legacyViewLight != light.legacyViewLight + || pass.lightIndex != domainLightIndex + || pass.receiver != receiver + || pass.resourceOwner < CLASSIC_INTERACTION_RECEIVER_LOCAL + || pass.resourceOwner > CLASSIC_INTERACTION_RECEIVER_GLOBAL + || pass.disposition + < CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED + || pass.disposition + >= CLASSIC_INTERACTION_SHADOW_MAP_PASS_DISPOSITION_COUNT + || pass.mode != ( hybrid ? CLASSIC_INTERACTION_SHADOW_HYBRID + : ( pointLight ? CLASSIC_INTERACTION_SHADOW_POINT + : CLASSIC_INTERACTION_SHADOW_PROJECTED ) ) + || pass.receiverMask != ( receiver + == CLASSIC_INTERACTION_RECEIVER_LOCAL + ? SHADOWMAP_RECEIVER_MASK_LOCAL + : SHADOWMAP_RECEIVER_MASK_GLOBAL ) + || pass.mappedCasterCount <= 0 + || pass.supplementCasterCount < 0 + || pass.drawableMappedCasters < 0 + || pass.noopMappedCasters < 0 + || pass.drawableMappedCasters + pass.noopMappedCasters + != pass.mappedCasterCount + || pass.drawableSupplementCasters < 0 + || pass.noopSupplementCasters < 0 + || pass.drawableSupplementCasters + pass.noopSupplementCasters + != pass.supplementCasterCount + || pass.resourcePlanId == 0 || pass.resourceGeneration == 0 + || pass.hash == 0 + || !pass.mapRequired || !pass.hybridComplete + || ( hybrid && ( pass.mapComplete + || pass.supplementCasterCount <= 0 ) ) + || ( !hybrid && !pass.mapComplete ) + || ( !pass.hasStaticCasters && !pass.hasDynamicCasters ) + || pass.hasTranslucentCasters + || ( ( pass.hasDynamicCasters || pass.hasAlphaCasters ) + && pass.allowCacheReuse ) + || ( !pass.allowCacheReuse && !pass.allowScratch ) + || ( !pass.allowCacheReuse && !pass.allowCacheUpdate ) + || pass.casterSignature + != light.legacyViewLight->shadowMapCasterSignature + || pass.casterCullMode + != idMath::ClampInt( 0, 2, + r_shadowMapCasterCulling.GetInteger() ) + || idMath::Fabs( pass.polygonFactor + - RB_ShadowMapPolygonFactor() ) > 0.00001f + || idMath::Fabs( pass.polygonOffset + - RB_ShadowMapPolygonOffset() ) > 0.00001f + || pass.hashedAlpha != RB_ShadowMapHashedAlphaEnabled() + || pass.stableAlphaHash + != r_shadowMapStableAlphaHash.GetBool() ) { + return false; + } + if ( pointLight ) { + return pass.point.valid && pass.point.faceCount == 6 + && pass.point.faceSize == RB_ShadowMapPointSizeValue() + && pass.point.filterTaps >= 1 && pass.point.filterTaps <= 13 + && pass.point.filterMode >= 0 && pass.point.filterMode <= 1 + && pass.point.depthCompare + == RB_PointShadowMapDepthCompareEnabled() + && pass.point.highPrecision + == RB_PointShadowMapHighPrecisionEnabled() + && idMath::Fabs( pass.point.farDistance + - R_ShadowMapPointFarDistance( light.legacyViewLight ) ) + <= 0.0001f + && RB_SharedWorldInteractionGLFiniteArray( + pass.point.lightOrigin, 4 ); + } + const char *projectedReason = NULL; + return pass.projected.state.valid + && RB_ShadowMapValidateProjectedStateContract( + light.legacyViewLight, pass.projected.state, &projectedReason ) + && pass.projected.depthCompare == RB_ShadowMapDepthCompareEnabled(); +} + +static void RB_SharedWorldInteractionGLAbortMapTransaction( + const viewDef_t *viewDef ) { + const rbSharedWorldInteractionGLPreparedView_t &prepared = + rbSharedWorldInteractionGLPreparedView; + if ( backEnd.renderTexture != NULL ) { + backEnd.renderTexture->MakeCurrent(); + } else { + idRenderTexture::BindNull(); + glDrawBuffer( GL_BACK ); + glReadBuffer( GL_BACK ); + } + if ( viewDef != NULL ) { + glViewport( viewDef->viewport.x1, viewDef->viewport.y1, + viewDef->viewport.x2 + 1 - viewDef->viewport.x1, + viewDef->viewport.y2 + 1 - viewDef->viewport.y1 ); + glScissor( viewDef->viewport.x1, viewDef->viewport.y1, + viewDef->viewport.x2 + 1 - viewDef->viewport.x1, + viewDef->viewport.y2 + 1 - viewDef->viewport.y1 ); + glMatrixMode( GL_PROJECTION ); + glLoadMatrixf( viewDef->projectionMatrix ); + glMatrixMode( GL_MODELVIEW ); + } + glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); + glDepthMask( GL_TRUE ); + glUseProgramObjectARB( 0 ); + if ( prepared.mapGLStateCaptured ) { + prepared.mapBlendWasEnabled + ? glEnable( GL_BLEND ) : glDisable( GL_BLEND ); + prepared.mapDepthWasEnabled + ? glEnable( GL_DEPTH_TEST ) : glDisable( GL_DEPTH_TEST ); + prepared.mapScissorWasEnabled + ? glEnable( GL_SCISSOR_TEST ) : glDisable( GL_SCISSOR_TEST ); + prepared.mapStencilWasEnabled + ? glEnable( GL_STENCIL_TEST ) : glDisable( GL_STENCIL_TEST ); + prepared.mapCullWasEnabled + ? glEnable( GL_CULL_FACE ) : glDisable( GL_CULL_FACE ); + glDepthMask( prepared.mapDepthWriteWasEnabled ? GL_TRUE : GL_FALSE ); + glColorMask( prepared.mapColorWriteWasEnabled[0] ? GL_TRUE : GL_FALSE, + prepared.mapColorWriteWasEnabled[1] ? GL_TRUE : GL_FALSE, + prepared.mapColorWriteWasEnabled[2] ? GL_TRUE : GL_FALSE, + prepared.mapColorWriteWasEnabled[3] ? GL_TRUE : GL_FALSE ); + glClearColor( prepared.mapSavedClearColor[0], + prepared.mapSavedClearColor[1], prepared.mapSavedClearColor[2], + prepared.mapSavedClearColor[3] ); + } + backEnd.currentSpace = NULL; + backEnd.currentScissor.Clear(); + backEnd.glState.faceCulling = -1; + if ( prepared.mapGLStateCaptured && prepared.mapCullWasEnabled + && prepared.mapSavedFaceCulling >= CT_FRONT_SIDED + && prepared.mapSavedFaceCulling <= CT_TWO_SIDED ) { + GL_Cull( prepared.mapSavedFaceCulling ); + } + backEnd.glState.currenttmu = -1; + GL_ClearStateDelta(); + GL_SelectTexture( 0 ); +} + +static void RB_SharedWorldInteractionGLActivateMapResource( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + if ( preparedPass.pointLight ) { + g_activePointShadowMapCache = preparedPass.pointEntry; + g_pointShadowMapColorImage = preparedPass.pointColorImage; + g_pointShadowMapDepthImage = preparedPass.pointDepthImage; + g_pointShadowMapRenderTexture = preparedPass.renderTexture; + return; + } + g_activeProjectedShadowMapCache = preparedPass.projectedEntry; + g_shadowMapDepthImage = preparedPass.projectedDepthImage; + g_shadowMapRenderTexture = preparedPass.renderTexture; + g_shadowMapActiveSlotOriginX = preparedPass.projectedOriginX; + g_shadowMapActiveSlotOriginY = preparedPass.projectedOriginY; +} + +static bool RB_SharedWorldInteractionGLCaptureMapResource( + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + idImage *sampleImage = preparedPass.pointLight + ? ( pass.point.depthCompare ? preparedPass.pointDepthImage + : preparedPass.pointColorImage ) + : preparedPass.projectedDepthImage; + const textureType_t expectedType = preparedPass.pointLight + ? TT_CUBIC : TT_2D; + if ( sampleImage == NULL || !sampleImage->IsLoaded() + || sampleImage->IsDefaulted() + || sampleImage->GetDeviceHandle() == 0 + || sampleImage->GetOpts().textureType != expectedType + || preparedPass.renderTexture == NULL + || preparedPass.renderTexture->GetWidth() <= 0 + || preparedPass.renderTexture->GetHeight() <= 0 ) { + return false; + } + preparedPass.sampleTextureHandle = sampleImage->GetDeviceHandle(); + preparedPass.sampleStorageGeneration = + sampleImage->GetStorageGeneration(); + preparedPass.resourceWidth = preparedPass.renderTexture->GetWidth(); + preparedPass.resourceHeight = preparedPass.renderTexture->GetHeight(); + return true; +} + +static bool RB_SharedWorldInteractionGLAssignProjectedCacheResource( + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass, + projectedShadowMapCacheEntry_t *entry ) { + if ( entry == NULL || g_shadowMapAtlasDepthImage == NULL + || g_shadowMapAtlasRenderTexture == NULL ) { + return false; + } + preparedPass.projectedEntry = entry; + preparedPass.pointEntry = NULL; + preparedPass.pointLight = false; + preparedPass.scratchResource = false; + preparedPass.projectedDepthImage = g_shadowMapAtlasDepthImage; + preparedPass.pointColorImage = NULL; + preparedPass.pointDepthImage = NULL; + preparedPass.renderTexture = g_shadowMapAtlasRenderTexture; + preparedPass.projectedOriginX = entry->atlasCellX + * g_shadowMapAtlasCellSize; + preparedPass.projectedOriginY = entry->atlasCellY + * g_shadowMapAtlasCellSize; + preparedPass.projectedSpanPixels = preparedPass.pass->projected.state.tileSize + * preparedPass.pass->projected.state.atlasDiv; + return RB_SharedWorldInteractionGLCaptureMapResource( preparedPass ); +} + +static bool RB_SharedWorldInteractionGLAssignPointCacheResource( + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass, + pointShadowMapCacheEntry_t *entry ) { + if ( entry == NULL || entry->colorImage == NULL + || entry->depthImage == NULL || entry->renderTexture == NULL ) { + return false; + } + preparedPass.projectedEntry = NULL; + preparedPass.pointEntry = entry; + preparedPass.pointLight = true; + preparedPass.scratchResource = false; + preparedPass.projectedDepthImage = NULL; + preparedPass.pointColorImage = entry->colorImage; + preparedPass.pointDepthImage = entry->depthImage; + preparedPass.renderTexture = entry->renderTexture; + preparedPass.projectedOriginX = 0; + preparedPass.projectedOriginY = 0; + preparedPass.projectedSpanPixels = 0; + return RB_SharedWorldInteractionGLCaptureMapResource( preparedPass ); +} + +static bool RB_SharedWorldInteractionGLReserveProjectedScratch( + rbSharedWorldInteractionGLPreparedView_t &prepared, + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + if ( !preparedPass.pass->allowScratch + || prepared.projectedScratchCount >= SHADOWMAP_CACHE_MAX_SLOTS ) { + return false; + } + const int slotIndex = prepared.projectedScratchCount++; + sharedInteractionProjectedScratch_t &slot = + g_sharedInteractionProjectedScratch[slotIndex]; + const int size = preparedPass.pass->projected.state.tileSize + * preparedPass.pass->projected.state.atlasDiv; + if ( size <= 0 || size > glConfig.maxTextureSize ) { + return false; + } + idImageOpts opts; + opts.textureType = TT_2D; + opts.format = FMT_DEPTH; + opts.width = size; + opts.height = size; + opts.numLevels = 1; + opts.isPersistant = true; + slot.depthImage = globalImages->ScratchImage( + va( "_sharedInteractionShadowMapDepth%d", slotIndex ), &opts, + TF_NEAREST, TR_CLAMP, TD_DEPTH ); + if ( slot.depthImage == NULL ) { + return false; + } + if ( slot.renderTexture == NULL ) { + slot.renderTexture = tr.CreateRenderTexture( NULL, slot.depthImage ); + if ( slot.renderTexture != NULL ) { + slot.renderTexture->SetAllowIncomplete(); + } + } else if ( slot.renderTexture->GetWidth() != size + || slot.renderTexture->GetHeight() != size ) { + tr.ResizeRenderTexture( slot.renderTexture, size, size ); + } + preparedPass.projectedEntry = NULL; + preparedPass.pointEntry = NULL; + preparedPass.pointLight = false; + preparedPass.scratchResource = true; + preparedPass.projectedDepthImage = slot.depthImage; + preparedPass.pointColorImage = NULL; + preparedPass.pointDepthImage = NULL; + preparedPass.renderTexture = slot.renderTexture; + preparedPass.projectedOriginX = 0; + preparedPass.projectedOriginY = 0; + preparedPass.projectedSpanPixels = size; + return RB_SharedWorldInteractionGLCaptureMapResource( preparedPass ); +} + +static bool RB_SharedWorldInteractionGLReservePointScratch( + rbSharedWorldInteractionGLPreparedView_t &prepared, + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + if ( !preparedPass.pass->allowScratch + || prepared.pointScratchCount >= SHADOWMAP_CACHE_MAX_SLOTS ) { + return false; + } + const int slotIndex = prepared.pointScratchCount++; + sharedInteractionPointScratch_t &slot = + g_sharedInteractionPointScratch[slotIndex]; + const classicInteractionDomainShadowPointState_t &point = + preparedPass.pass->point; + if ( point.faceSize <= 0 || point.faceSize > glConfig.maxTextureSize ) { + return false; + } + idImageOpts colorOpts; + colorOpts.textureType = TT_CUBIC; + colorOpts.format = point.highPrecision ? FMT_RGBA16F : FMT_RGBA8; + colorOpts.width = point.faceSize; + colorOpts.height = point.faceSize; + colorOpts.numLevels = 1; + colorOpts.isPersistant = true; + slot.colorImage = globalImages->ScratchImage( + va( "_sharedInteractionPointShadowColor%d", slotIndex ), + &colorOpts, TF_NEAREST, TR_CLAMP, TD_DEFAULT ); + idImageOpts depthOpts; + depthOpts.textureType = TT_CUBIC; + depthOpts.format = FMT_DEPTH; + depthOpts.width = point.faceSize; + depthOpts.height = point.faceSize; + depthOpts.numLevels = 1; + depthOpts.isPersistant = true; + slot.depthImage = globalImages->ScratchImage( + va( "_sharedInteractionPointShadowDepth%d", slotIndex ), + &depthOpts, TF_NEAREST, TR_CLAMP, TD_DEPTH ); + if ( slot.colorImage == NULL || slot.depthImage == NULL ) { + return false; + } + if ( slot.renderTexture == NULL ) { + slot.renderTexture = tr.CreateRenderTexture( + slot.colorImage, slot.depthImage ); + if ( slot.renderTexture != NULL ) { + slot.renderTexture->SetAllowIncomplete(); + } + } else if ( slot.renderTexture->GetWidth() != point.faceSize + || slot.renderTexture->GetHeight() != point.faceSize ) { + tr.ResizeRenderTexture( slot.renderTexture, + point.faceSize, point.faceSize ); + } + preparedPass.projectedEntry = NULL; + preparedPass.pointEntry = NULL; + preparedPass.pointLight = true; + preparedPass.scratchResource = true; + preparedPass.projectedDepthImage = NULL; + preparedPass.pointColorImage = slot.colorImage; + preparedPass.pointDepthImage = slot.depthImage; + preparedPass.renderTexture = slot.renderTexture; + preparedPass.projectedOriginX = 0; + preparedPass.projectedOriginY = 0; + preparedPass.projectedSpanPixels = 0; + return RB_SharedWorldInteractionGLCaptureMapResource( preparedPass ); +} + +static bool RB_SharedWorldInteractionGLMapUpdateAdmitted( void ) { + const int updateBudget = r_shadowMapMaxUpdatesPerView.GetInteger(); + if ( updateBudget > 0 + && g_shadowMapStats.shadowMapUpdates >= updateBudget ) { + return false; + } + if ( updateBudget > 0 && g_shadowMapAdmissionsActive + && !RB_ShadowMapUpdateAdmitted( + RB_ShadowMapLightIndex( backEnd.vLight ) ) ) { + return false; + } + return true; +} + +static bool RB_SharedWorldInteractionGLEnsurePointCacheResource( + pointShadowMapCacheEntry_t *entry, + const classicInteractionDomainShadowMapPass_t &pass ) { + if ( entry == NULL || !pass.point.valid ) { + return false; + } + const int slotIndex = static_cast( entry - g_pointShadowMapCache ); + if ( slotIndex < 0 || slotIndex >= SHADOWMAP_CACHE_MAX_SLOTS ) { + return false; + } + idImageOpts colorOpts; + colorOpts.textureType = TT_CUBIC; + colorOpts.format = pass.point.highPrecision ? FMT_RGBA16F : FMT_RGBA8; + colorOpts.width = pass.point.faceSize; + colorOpts.height = pass.point.faceSize; + colorOpts.numLevels = 1; + colorOpts.isPersistant = true; + entry->colorImage = globalImages->ScratchImage( + va( "_pointShadowMapColorCache%d", slotIndex ), &colorOpts, + TF_NEAREST, TR_CLAMP, TD_DEFAULT ); + idImageOpts depthOpts; + depthOpts.textureType = TT_CUBIC; + depthOpts.format = FMT_DEPTH; + depthOpts.width = pass.point.faceSize; + depthOpts.height = pass.point.faceSize; + depthOpts.numLevels = 1; + depthOpts.isPersistant = true; + entry->depthImage = globalImages->ScratchImage( + va( "_pointShadowMapDepthCache%d", slotIndex ), &depthOpts, + TF_NEAREST, TR_CLAMP, TD_DEPTH ); + if ( entry->colorImage == NULL || entry->depthImage == NULL ) { + return false; + } + if ( entry->renderTexture == NULL ) { + entry->renderTexture = tr.CreateRenderTexture( + entry->colorImage, entry->depthImage ); + if ( entry->renderTexture != NULL ) { + entry->renderTexture->SetAllowIncomplete(); + } + } else if ( entry->renderTexture->GetWidth() != pass.point.faceSize + || entry->renderTexture->GetHeight() != pass.point.faceSize ) { + tr.ResizeRenderTexture( entry->renderTexture, + pass.point.faceSize, pass.point.faceSize ); + } + return entry->renderTexture != NULL; +} + +static bool RB_SharedWorldInteractionGLScheduleSealedMapPass( + const classicInteractionDomainShadowMapPass_t &pass, + const viewDef_t *viewDef, + int cacheLightIndex, shadowMapPassKind_t passKind, + shadowMapSchedule_t &schedule ) { + memset( &schedule, 0, sizeof( schedule ) ); + schedule.action = SHADOWMAP_SCHEDULE_UPDATE; + schedule.signature = RB_ShadowMapBuildPassSignatureForView( + pass.legacyViewLight, viewDef, passKind, pass.point.valid ); + if ( !pass.allowCacheReuse || cacheLightIndex < 0 ) { + return RB_SharedWorldInteractionGLMapUpdateAdmitted(); + } + + schedule.cacheable = true; + if ( pass.point.valid ) { + schedule.pointEntry = RB_ShadowMapFindPointCacheEntry( + cacheLightIndex, passKind, schedule.signature ); + if ( schedule.pointEntry != NULL ) { + schedule.cacheHit = true; + schedule.action = SHADOWMAP_SCHEDULE_REUSE; + schedule.budgetReuse = r_shadowMapMaxUpdatesPerView.GetInteger() > 0 + && g_shadowMapStats.shadowMapUpdates + >= r_shadowMapMaxUpdatesPerView.GetInteger(); + g_shadowMapStats.cacheHitPasses++; + g_shadowMapStats.cacheReusePasses++; + g_shadowMapStats.cacheBudgetReusePasses += + schedule.budgetReuse ? 1 : 0; + return true; + } + } else { + schedule.projectedEntry = RB_ShadowMapFindProjectedCacheEntry( + cacheLightIndex, passKind, schedule.signature ); + if ( schedule.projectedEntry != NULL ) { + schedule.cacheHit = true; + schedule.action = SHADOWMAP_SCHEDULE_REUSE; + schedule.budgetReuse = r_shadowMapMaxUpdatesPerView.GetInteger() > 0 + && g_shadowMapStats.shadowMapUpdates + >= r_shadowMapMaxUpdatesPerView.GetInteger(); + g_shadowMapStats.cacheHitPasses++; + g_shadowMapStats.cacheReusePasses++; + g_shadowMapStats.cacheBudgetReusePasses += + schedule.budgetReuse ? 1 : 0; + return true; + } + } + g_shadowMapStats.cacheMissPasses++; + if ( !RB_SharedWorldInteractionGLMapUpdateAdmitted() ) { + schedule.action = SHADOWMAP_SCHEDULE_FALLBACK; + return false; + } + if ( pass.point.valid ) { + schedule.pointEntry = RB_ShadowMapAllocPointCacheEntry( false ); + if ( schedule.pointEntry != NULL + && !RB_SharedWorldInteractionGLEnsurePointCacheResource( + schedule.pointEntry, pass ) ) { + schedule.pointEntry = NULL; + } + } else { + schedule.projectedEntry = RB_ShadowMapAllocProjectedCacheEntry( + pass.projected.state.atlasDiv, false ); + } + if ( pass.point.valid ? schedule.pointEntry == NULL + : schedule.projectedEntry == NULL ) { + // Cache capacity is an optimization limit. The sealed scratch plan can + // still own the pass without changing its rendered semantics. + schedule.cacheable = false; + } + return true; +} + +static void RB_SharedWorldInteractionGLCompleteSealedCacheUpdate( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass, + const shadowMapSchedule_t &schedule, bool renderOk ) { + if ( !schedule.cacheable ) { + return; + } + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + if ( preparedPass.pointLight ) { + pointShadowMapCacheEntry_t *entry = schedule.pointEntry; + if ( entry == NULL ) { + return; + } + if ( !renderOk ) { + entry->valid = false; + return; + } + entry->valid = true; + entry->lightIndex = preparedPass.cacheLightIndex; + entry->passKind = preparedPass.cachePassKind; + entry->lightClass = static_cast( pass.lightClass ); + entry->signature = schedule.signature; + entry->size = pass.point.faceSize; + entry->highPrecision = pass.point.highPrecision; + entry->depthCompare = pass.point.depthCompare; + entry->lightOrigin[0] = pass.point.lightOrigin[0]; + entry->lightOrigin[1] = pass.point.lightOrigin[1]; + entry->lightOrigin[2] = pass.point.lightOrigin[2]; + entry->farDistance = pass.point.farDistance; + entry->lastUsedFrame = tr.frameCount; + entry->lastUpdatedFrame = tr.frameCount; + entry->colorImage = preparedPass.pointColorImage; + entry->depthImage = preparedPass.pointDepthImage; + entry->renderTexture = preparedPass.renderTexture; + entry->colorStorageGeneration = entry->colorImage != NULL + ? entry->colorImage->GetStorageGeneration() : 0; + entry->depthStorageGeneration = entry->depthImage != NULL + ? entry->depthImage->GetStorageGeneration() : 0; + return; + } + projectedShadowMapCacheEntry_t *entry = schedule.projectedEntry; + if ( entry == NULL ) { + return; + } + if ( !renderOk ) { + entry->valid = false; + return; + } + entry->valid = true; + entry->lightIndex = preparedPass.cacheLightIndex; + entry->passKind = preparedPass.cachePassKind; + entry->lightClass = static_cast( pass.lightClass ); + entry->signature = schedule.signature; + entry->tileSize = pass.projected.state.tileSize; + entry->atlasDiv = pass.projected.state.atlasDiv; + entry->cascadeCount = pass.projected.state.cascadeCount; + entry->lastUsedFrame = tr.frameCount; + entry->lastUpdatedFrame = tr.frameCount; + entry->atlasStorageGeneration = preparedPass.projectedDepthImage != NULL + ? preparedPass.projectedDepthImage->GetStorageGeneration() : 0; + entry->state = pass.projected.state; +} + +static bool RB_SharedWorldInteractionGLProjectedEntryMatches( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + const projectedShadowMapCacheEntry_t *entry = preparedPass.projectedEntry; + if ( preparedPass.pointLight || preparedPass.projectedDepthImage == NULL + || !preparedPass.projectedDepthImage->IsLoaded() + || preparedPass.projectedDepthImage->IsDefaulted() + || preparedPass.projectedDepthImage->GetDeviceHandle() + != preparedPass.sampleTextureHandle + || preparedPass.projectedDepthImage->GetStorageGeneration() + != preparedPass.sampleStorageGeneration + || preparedPass.projectedDepthImage->GetOpts().textureType != TT_2D + || preparedPass.projectedDepthImage->GetOpts().format != FMT_DEPTH + || preparedPass.renderTexture == NULL + || preparedPass.renderTexture->GetWidth() + != preparedPass.resourceWidth + || preparedPass.renderTexture->GetHeight() + != preparedPass.resourceHeight + || preparedPass.projectedSpanPixels + != pass.projected.state.tileSize + * pass.projected.state.atlasDiv ) { + return false; + } + if ( preparedPass.scratchResource ) { + return entry == NULL && preparedPass.projectedOriginX == 0 + && preparedPass.projectedOriginY == 0 + && preparedPass.resourceWidth + == preparedPass.projectedSpanPixels + && preparedPass.resourceHeight + == preparedPass.projectedSpanPixels; + } + return entry != NULL && entry->valid + && entry->lightIndex == preparedPass.cacheLightIndex + && entry->passKind == preparedPass.cachePassKind + && entry->lightClass == static_cast( pass.lightClass ) + && entry->signature == preparedPass.signature + && entry->atlasStorageGeneration != 0 + && entry->atlasStorageGeneration + == preparedPass.sampleStorageGeneration + && entry->state.valid + && entry->tileSize == pass.projected.state.tileSize + && entry->atlasDiv == pass.projected.state.atlasDiv + && entry->cascadeCount == pass.projected.state.cascadeCount + && std::memcmp( &entry->state, &pass.projected.state, + sizeof( entry->state ) ) == 0 + && preparedPass.projectedDepthImage == g_shadowMapAtlasDepthImage + && preparedPass.renderTexture == g_shadowMapAtlasRenderTexture + && entry->atlasCellSpan == pass.projected.state.atlasDiv + && preparedPass.projectedOriginX + == entry->atlasCellX * g_shadowMapAtlasCellSize + && preparedPass.projectedOriginY + == entry->atlasCellY * g_shadowMapAtlasCellSize; +} + +static bool RB_SharedWorldInteractionGLPointEntryMatches( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + const pointShadowMapCacheEntry_t *entry = preparedPass.pointEntry; + idImage *sampleImage = pass.point.depthCompare + ? preparedPass.pointDepthImage : preparedPass.pointColorImage; + if ( !preparedPass.pointLight || sampleImage == NULL + || preparedPass.pointColorImage == NULL + || !preparedPass.pointColorImage->IsLoaded() + || preparedPass.pointColorImage->IsDefaulted() + || preparedPass.pointColorImage->GetDeviceHandle() == 0 + || preparedPass.pointColorImage->GetOpts().textureType != TT_CUBIC + || preparedPass.pointColorImage->GetOpts().format + != ( pass.point.highPrecision ? FMT_RGBA16F : FMT_RGBA8 ) + || preparedPass.pointDepthImage == NULL + || !preparedPass.pointDepthImage->IsLoaded() + || preparedPass.pointDepthImage->IsDefaulted() + || preparedPass.pointDepthImage->GetDeviceHandle() == 0 + || preparedPass.pointDepthImage->GetOpts().textureType != TT_CUBIC + || preparedPass.pointDepthImage->GetOpts().format != FMT_DEPTH + || !sampleImage->IsLoaded() || sampleImage->IsDefaulted() + || sampleImage->GetDeviceHandle() + != preparedPass.sampleTextureHandle + || sampleImage->GetStorageGeneration() + != preparedPass.sampleStorageGeneration + || sampleImage->GetOpts().textureType != TT_CUBIC + || preparedPass.renderTexture == NULL + || preparedPass.renderTexture->GetWidth() + != pass.point.faceSize + || preparedPass.renderTexture->GetHeight() + != pass.point.faceSize + || preparedPass.resourceWidth != pass.point.faceSize + || preparedPass.resourceHeight != pass.point.faceSize ) { + return false; + } + if ( preparedPass.scratchResource ) { + return entry == NULL && preparedPass.pointColorImage != NULL + && preparedPass.pointDepthImage != NULL; + } + return entry != NULL && entry->valid + && entry->lightIndex == preparedPass.cacheLightIndex + && entry->passKind == preparedPass.cachePassKind + && entry->lightClass == static_cast( pass.lightClass ) + && entry->signature == preparedPass.signature + && entry->size == pass.point.faceSize + && entry->highPrecision == pass.point.highPrecision + && entry->depthCompare == pass.point.depthCompare + && entry->lightOrigin[0] == pass.point.lightOrigin[0] + && entry->lightOrigin[1] == pass.point.lightOrigin[1] + && entry->lightOrigin[2] == pass.point.lightOrigin[2] + && entry->farDistance == pass.point.farDistance + && entry->colorImage == preparedPass.pointColorImage + && entry->depthImage == preparedPass.pointDepthImage + && entry->colorStorageGeneration != 0 + && entry->depthStorageGeneration != 0 + && entry->colorStorageGeneration + == preparedPass.pointColorImage->GetStorageGeneration() + && entry->depthStorageGeneration + == preparedPass.pointDepthImage->GetStorageGeneration() + && entry->renderTexture == preparedPass.renderTexture; +} + +static void RB_SharedWorldInteractionGLApplyMapCasterCull( + const classicInteractionDomainShadowMapPass_t &pass, + const classicInteractionDomainShadowCaster_t &caster ) { + int desired = 0; + const bool automaticTwoSided = pass.casterCullMode == 2 + && ( !caster.perfectHull + || R_ShadowMapLightOriginInsideCasterBounds( + pass.legacyViewLight, caster.modelMatrix, + caster.boundsMin, caster.boundsMax ) ); + if ( pass.casterCullMode != 0 && !automaticTwoSided && caster.cull != RENDERER_CULL_NONE ) { + GLenum face = GL_FRONT; + if ( caster.cull == RENDERER_CULL_BACK ) { + face = face == GL_FRONT ? GL_BACK : GL_FRONT; + } + desired = static_cast( face ); + } + if ( desired == 0 ) { + glDisable( GL_CULL_FACE ); + } else { + glEnable( GL_CULL_FACE ); + glCullFace( static_cast( desired ) ); + } +} + +static void RB_SharedWorldInteractionGLSubmitMapCaster( + const rbSharedWorldInteractionGLPreparedMapCaster_t &caster ) { + idVertexCache::BindArrayBuffer( caster.vertexBuffer ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( caster.vertexPointer, + DRAWVERT_XYZ_OFFSET ) ); + idVertexCache::BindIndexBuffer( caster.indexBuffer ); + backEnd.pc.c_drawElements++; + backEnd.pc.c_drawIndexes += caster.indexCount; + backEnd.pc.c_drawVertexes += caster.vertexCount; + if ( caster.indexCacheBacked ) { + backEnd.pc.c_vboIndexes += caster.indexCount; + } + glDrawElements( GL_TRIANGLES, caster.indexCount, GL_INDEX_TYPE, + caster.indexPointer ); +} + +static void RB_SharedWorldInteractionGLDrawProjectedMapCaster( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedMapCaster_t &preparedCaster, + const classicInteractionDomainShadowMapPass_t &pass, + int cascadeIndex ) { + const classicInteractionDomainShadowCaster_t &caster = + *preparedCaster.caster; + glLoadMatrixf( caster.modelMatrix ); + float row0[4]; + float row1[4]; + float row2[4]; + RB_ShadowMapModelMatrixRows( caster.modelMatrix, row0, row1, row2 ); + if ( g_shadowMapCasterProgram.modelMatrixRow0 >= 0 ) { + glUniform4fvARB( g_shadowMapCasterProgram.modelMatrixRow0, 1, row0 ); + glUniform4fvARB( g_shadowMapCasterProgram.modelMatrixRow1, 1, row1 ); + glUniform4fvARB( g_shadowMapCasterProgram.modelMatrixRow2, 1, row2 ); + } + if ( g_shadowMapCasterProgram.shadowDepthRow >= 0 ) { + idPlane localDepth; + R_GlobalPlaneToLocal( caster.modelMatrix, + pass.projected.state.clipPlanes[cascadeIndex][2], localDepth ); + glUniform4fvARB( g_shadowMapCasterProgram.shadowDepthRow, 1, + localDepth.ToFloatPtr() ); + } + RB_SharedWorldInteractionGLApplyMapCasterCull( pass, caster ); + if ( preparedCaster.alphaStageCount <= 0 ) { + RB_ShadowMapDisableCasterAlphaTest(); + RB_SharedWorldInteractionGLSubmitMapCaster( preparedCaster ); + return; + } + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + idVertexCache::BindArrayBuffer( preparedCaster.vertexBuffer ); + glTexCoordPointer( 2, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( preparedCaster.vertexPointer, + DRAWVERT_ST_OFFSET ) ); + if ( g_shadowMapCasterProgram.alphaTestEnabled >= 0 ) { + glUniform1fARB( g_shadowMapCasterProgram.alphaTestEnabled, 1.0f ); + } + if ( g_shadowMapCasterProgram.alphaHashEnabled >= 0 ) { + glUniform1fARB( g_shadowMapCasterProgram.alphaHashEnabled, + pass.hashedAlpha ? 1.0f : 0.0f ); + } + if ( g_shadowMapCasterProgram.alphaHashStable >= 0 ) { + glUniform1fARB( g_shadowMapCasterProgram.alphaHashStable, + pass.stableAlphaHash ? 1.0f : 0.0f ); + } + for ( int i = 0; i < preparedCaster.alphaStageCount; ++i ) { + const rbSharedWorldInteractionGLPreparedShadowAlphaStage_t &preparedStage = + prepared.shadowAlphaStages[preparedCaster.firstAlphaStage + i]; + const classicInteractionDomainShadowAlphaStage_t &stage = + *preparedStage.stage; + if ( g_shadowMapCasterProgram.alphaRef >= 0 ) { + glUniform1fARB( g_shadowMapCasterProgram.alphaRef, + stage.alphaTestValue ); + glUniform1fARB( g_shadowMapCasterProgram.alphaTestMode, + RB_ShadowMapAlphaTestModeValue( stage.alphaTestMode ) ); + glUniform1fARB( g_shadowMapCasterProgram.alphaScale, + stage.alphaScale ); + } + glUniform4fvARB( g_shadowMapCasterProgram.alphaTexCoordS, 1, + stage.textureMatrix[0] ); + glUniform4fvARB( g_shadowMapCasterProgram.alphaTexCoordT, 1, + stage.textureMatrix[1] ); + GL_SelectTexture( 0 ); + preparedStage.image->Bind(); + RB_SharedWorldInteractionGLSubmitMapCaster( preparedCaster ); + } + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + RB_ShadowMapDisableCasterAlphaTest(); +} + +static void RB_SharedWorldInteractionGLDrawPointMapCaster( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedMapCaster_t &preparedCaster, + const classicInteractionDomainShadowMapPass_t &pass, + const float lightModelViewMatrix[16] ) { + const classicInteractionDomainShadowCaster_t &caster = + *preparedCaster.caster; + float modelViewMatrix[16]; + myGlMultMatrix( caster.modelMatrix, lightModelViewMatrix, + modelViewMatrix ); + glLoadMatrixf( modelViewMatrix ); + float row0[4]; + float row1[4]; + float row2[4]; + RB_ShadowMapModelMatrixRows( caster.modelMatrix, row0, row1, row2 ); + glUniform4fvARB( g_pointShadowCasterProgram.modelMatrixRow0, 1, row0 ); + glUniform4fvARB( g_pointShadowCasterProgram.modelMatrixRow1, 1, row1 ); + glUniform4fvARB( g_pointShadowCasterProgram.modelMatrixRow2, 1, row2 ); + RB_SharedWorldInteractionGLApplyMapCasterCull( pass, caster ); + if ( preparedCaster.alphaStageCount <= 0 ) { + if ( g_pointShadowCasterProgram.alphaTestEnabled >= 0 ) { + glUniform1fARB( g_pointShadowCasterProgram.alphaTestEnabled, 0.0f ); + } + RB_SharedWorldInteractionGLSubmitMapCaster( preparedCaster ); + return; + } + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + idVertexCache::BindArrayBuffer( preparedCaster.vertexBuffer ); + glTexCoordPointer( 2, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( preparedCaster.vertexPointer, + DRAWVERT_ST_OFFSET ) ); + glUniform1fARB( g_pointShadowCasterProgram.alphaTestEnabled, 1.0f ); + glUniform1fARB( g_pointShadowCasterProgram.alphaHashEnabled, + pass.hashedAlpha ? 1.0f : 0.0f ); + glUniform1fARB( g_pointShadowCasterProgram.alphaHashStable, + pass.stableAlphaHash ? 1.0f : 0.0f ); + for ( int i = 0; i < preparedCaster.alphaStageCount; ++i ) { + const rbSharedWorldInteractionGLPreparedShadowAlphaStage_t &preparedStage = + prepared.shadowAlphaStages[preparedCaster.firstAlphaStage + i]; + const classicInteractionDomainShadowAlphaStage_t &stage = + *preparedStage.stage; + glUniform1fARB( g_pointShadowCasterProgram.alphaRef, + stage.alphaTestValue ); + glUniform1fARB( g_pointShadowCasterProgram.alphaTestMode, + RB_ShadowMapAlphaTestModeValue( stage.alphaTestMode ) ); + glUniform1fARB( g_pointShadowCasterProgram.alphaScale, + stage.alphaScale ); + glUniform4fvARB( g_pointShadowCasterProgram.alphaTexCoordS, 1, + stage.textureMatrix[0] ); + glUniform4fvARB( g_pointShadowCasterProgram.alphaTexCoordT, 1, + stage.textureMatrix[1] ); + GL_SelectTexture( 0 ); + preparedStage.image->Bind(); + RB_SharedWorldInteractionGLSubmitMapCaster( preparedCaster ); + } + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + glUniform1fARB( g_pointShadowCasterProgram.alphaTestEnabled, 0.0f ); +} + +static int RB_SharedWorldInteractionGLDrawMapCasterChain( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + classicInteractionDomainShadowChain_t chain, + const classicInteractionDomainShadowMapPass_t &pass, + int cascadeIndex, const float *pointLightModelView ) { + const int first = light.firstMapCaster[chain]; + const int count = light.mapCasterCount[chain]; + for ( int i = 0; i < count; ++i ) { + const rbSharedWorldInteractionGLPreparedMapCaster_t &caster = + prepared.mapCasters[first + i]; + if ( pointLightModelView != NULL ) { + RB_SharedWorldInteractionGLDrawPointMapCaster( + prepared, caster, pass, pointLightModelView ); + } else { + RB_SharedWorldInteractionGLDrawProjectedMapCaster( + prepared, caster, pass, cascadeIndex ); + } + } + return count; +} + +static bool RB_SharedWorldInteractionGLRenderProjectedMap( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + RB_SharedWorldInteractionGLActivateMapResource( preparedPass ); + if ( preparedPass.renderTexture == NULL + || preparedPass.projectedDepthImage == NULL ) { + return false; + } + g_projectedShadowMapState = pass.projected.state; + preparedPass.renderTexture->MakeCurrent(); + if ( preparedPass.renderTexture->IsKnownIncomplete() ) { + RB_SharedWorldInteractionGLAbortMapTransaction( prepared.viewDef ); + return false; + } + glUseProgramObjectARB( g_shadowMapCasterProgram.programObject ); + glDisable( GL_VERTEX_PROGRAM_ARB ); + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + const float oneDepthStep = 1.0f / 16777216.0f; + glUniform2fARB( g_shadowMapCasterProgram.casterDepthOffset, + pass.polygonFactor, pass.polygonOffset * oneDepthStep ); + glUniform1iARB( g_shadowMapCasterProgram.alphaMap, 0 ); + RB_ShadowMapDisableCasterAlphaTest(); + glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); + glEnable( GL_DEPTH_TEST ); + glDepthMask( GL_TRUE ); + glDepthFunc( GL_LEQUAL ); + glDisable( GL_BLEND ); + glDisable( GL_STENCIL_TEST ); + glEnable( GL_SCISSOR_TEST ); + int drawn = 0; + for ( int cascade = 0; cascade < pass.projected.state.cascadeCount; + ++cascade ) { + const int tileX = g_shadowMapActiveSlotOriginX + + ( pass.projected.state.atlasDiv > 1 + ? cascade % pass.projected.state.atlasDiv + * pass.projected.state.tileSize : 0 ); + const int tileY = g_shadowMapActiveSlotOriginY + + ( pass.projected.state.atlasDiv > 1 + ? cascade / pass.projected.state.atlasDiv + * pass.projected.state.tileSize : 0 ); + float clipMatrix[16]; + R_ShadowMapClipPlanesToGLMatrix( + pass.projected.state.clipPlanes[cascade], clipMatrix ); + glViewport( tileX, tileY, pass.projected.state.tileSize, + pass.projected.state.tileSize ); + glScissor( tileX, tileY, pass.projected.state.tileSize, + pass.projected.state.tileSize ); + glClear( GL_DEPTH_BUFFER_BIT ); + glMatrixMode( GL_PROJECTION ); + glLoadMatrixf( clipMatrix ); + glMatrixMode( GL_MODELVIEW ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC, + pass, cascade, NULL ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC, + pass, cascade, NULL ); + if ( pass.receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL ) { + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC, + pass, cascade, NULL ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC, + pass, cascade, NULL ); + } + } + glEnable( GL_CULL_FACE ); + glUseProgramObjectARB( 0 ); + g_shadowMapStats.shadowMapUpdates++; + g_shadowMapStats.projectedAtlasTilesRendered += + pass.projected.state.cascadeCount; + RB_SharedWorldInteractionGLAbortMapTransaction( prepared.viewDef ); + return drawn == pass.drawableMappedCasters + * pass.projected.state.cascadeCount; +} + +static bool RB_SharedWorldInteractionGLRenderPointMap( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + RB_SharedWorldInteractionGLActivateMapResource( preparedPass ); + if ( preparedPass.renderTexture == NULL + || preparedPass.pointColorImage == NULL + || preparedPass.pointDepthImage == NULL ) { + return false; + } + preparedPass.renderTexture->MakeCurrent( 0 ); + if ( preparedPass.renderTexture->IsKnownIncomplete() ) { + RB_SharedWorldInteractionGLAbortMapTransaction( prepared.viewDef ); + return false; + } + glUseProgramObjectARB( g_pointShadowCasterProgram.programObject ); + glDisable( GL_VERTEX_PROGRAM_ARB ); + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + glUniform4fvARB( g_pointShadowCasterProgram.globalLightOrigin, 1, + pass.point.lightOrigin ); + glUniform1fARB( g_pointShadowCasterProgram.pointShadowFar, + pass.point.farDistance ); + glUniform1fARB( g_pointShadowCasterProgram.pointShadowDepthMode, + pass.point.highPrecision ? 1.0f : 0.0f ); + const float oneDepthStep = 1.0f / 16777216.0f; + glUniform2fARB( g_pointShadowCasterProgram.casterDepthOffset, + pass.polygonFactor, pass.polygonOffset * oneDepthStep ); + glUniform1iARB( g_pointShadowCasterProgram.alphaMap, 0 ); + glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); + glEnable( GL_DEPTH_TEST ); + glDepthMask( GL_TRUE ); + glDepthFunc( GL_LEQUAL ); + glDisable( GL_BLEND ); + glDisable( GL_STENCIL_TEST ); + const bool depthClamp = RB_ShadowMapDepthClampAvailable(); + if ( depthClamp ) { + glEnable( GL_DEPTH_CLAMP_NV ); + } + glClearColor( 1.0f, 1.0f, 1.0f, 1.0f ); + const float nearClip = idMath::ClampFloat( 0.5f, + depthClamp ? 16.0f : 4.0f, + pass.point.farDistance * 0.01f ); + float projectionMatrix[16]; + RB_PointShadowMapBuildProjectionMatrix( nearClip, + pass.point.farDistance, projectionMatrix ); + glMatrixMode( GL_PROJECTION ); + glLoadMatrixf( projectionMatrix ); + glMatrixMode( GL_MODELVIEW ); + int drawn = 0; + const idVec3 sealedLightOrigin( pass.point.lightOrigin[0], + pass.point.lightOrigin[1], pass.point.lightOrigin[2] ); + for ( int face = 0; face < pass.point.faceCount; ++face ) { + float lightModelView[16]; + RB_PointShadowMapBuildModelViewMatrix( + sealedLightOrigin, face, lightModelView ); + preparedPass.renderTexture->MakeCurrent( face ); + glViewport( 0, 0, pass.point.faceSize, pass.point.faceSize ); + glScissor( 0, 0, pass.point.faceSize, pass.point.faceSize ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC, + pass, 0, lightModelView ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC, + pass, 0, lightModelView ); + if ( pass.receiver == CLASSIC_INTERACTION_RECEIVER_GLOBAL ) { + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC, + pass, 0, lightModelView ); + drawn += RB_SharedWorldInteractionGLDrawMapCasterChain( + prepared, light, + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC, + pass, 0, lightModelView ); + } + } + if ( depthClamp ) { + glDisable( GL_DEPTH_CLAMP_NV ); + } + glEnable( GL_CULL_FACE ); + glUseProgramObjectARB( 0 ); + g_shadowMapStats.shadowMapUpdates++; + g_shadowMapStats.pointFacesRendered += pass.point.faceCount; + g_shadowMapStats.pointHighPrecisionPasses += + pass.point.highPrecision ? 1 : 0; + g_shadowMapStats.pointDepthComparePasses += + pass.point.depthCompare ? 1 : 0; + RB_SharedWorldInteractionGLAbortMapTransaction( prepared.viewDef ); + return drawn == pass.drawableMappedCasters * pass.point.faceCount; +} + +static bool RB_SharedWorldInteractionGLAliasMappedPass( + const rbSharedWorldInteractionGLPreparedLight_t &preparedLight, + rbSharedWorldInteractionGLPreparedView_t &prepared, + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + if ( !pass.resourceAlias + || pass.resourceOwner < CLASSIC_INTERACTION_RECEIVER_LOCAL + || pass.resourceOwner > CLASSIC_INTERACTION_RECEIVER_GLOBAL ) { + return false; + } + const int ownerIndex = preparedLight.shadowMapPass[pass.resourceOwner]; + if ( ownerIndex < 0 || ownerIndex >= prepared.shadowMapPassCount + || &prepared.shadowMapPasses[ownerIndex] == &preparedPass ) { + return false; + } + const rbSharedWorldInteractionGLPreparedMapPass_t &owner = + prepared.shadowMapPasses[ownerIndex]; + if ( !owner.prepared || owner.pass == NULL + || owner.pass->legacyViewLight != pass.legacyViewLight + || owner.pointLight != pass.point.valid + || owner.pass->drawableMappedCasters + != pass.drawableMappedCasters + || owner.pass->noopMappedCasters != pass.noopMappedCasters + || ( owner.pointLight + ? std::memcmp( &owner.pass->point, &pass.point, + sizeof( pass.point ) ) != 0 + : std::memcmp( &owner.pass->projected, &pass.projected, + sizeof( pass.projected ) ) != 0 ) ) { + return false; + } + preparedPass.projectedEntry = owner.projectedEntry; + preparedPass.pointEntry = owner.pointEntry; + preparedPass.projectedDepthImage = owner.projectedDepthImage; + preparedPass.pointColorImage = owner.pointColorImage; + preparedPass.pointDepthImage = owner.pointDepthImage; + preparedPass.renderTexture = owner.renderTexture; + preparedPass.sampleStorageGeneration = owner.sampleStorageGeneration; + preparedPass.sampleTextureHandle = owner.sampleTextureHandle; + preparedPass.resourceWidth = owner.resourceWidth; + preparedPass.resourceHeight = owner.resourceHeight; + preparedPass.projectedOriginX = owner.projectedOriginX; + preparedPass.projectedOriginY = owner.projectedOriginY; + preparedPass.projectedSpanPixels = owner.projectedSpanPixels; + preparedPass.cacheLightIndex = owner.cacheLightIndex; + preparedPass.cachePassKind = owner.cachePassKind; + preparedPass.signature = owner.signature; + preparedPass.pointLight = owner.pointLight; + preparedPass.scratchResource = owner.scratchResource; + preparedPass.cacheUpdated = owner.cacheUpdated; + preparedPass.prepared = owner.pointLight + ? RB_SharedWorldInteractionGLPointEntryMatches( preparedPass ) + : RB_SharedWorldInteractionGLProjectedEntryMatches( preparedPass ); + return preparedPass.prepared; +} + +static bool RB_SharedWorldInteractionGLPrepareMappedPass( + rbSharedWorldInteractionGLPreparedView_t &prepared, + rbSharedWorldInteractionGLPreparedLight_t &preparedLight, + classicInteractionDomainReceiver_t receiver ) { + rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass = + prepared.shadowMapPasses[preparedLight.shadowMapPass[receiver]]; + const classicInteractionDomainLight_t &light = *preparedLight.light; + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + viewLight_t *viewLight = const_cast( light.legacyViewLight ); + const bool pointLight = light.pointLight; + const shadowMapPassKind_t requestedPassKind = receiver + == CLASSIC_INTERACTION_RECEIVER_LOCAL + ? SHADOWMAP_PASS_LOCAL : SHADOWMAP_PASS_GLOBAL; + if ( viewLight == NULL || pass.hasTranslucentCasters + || RB_ShadowMapLightSupportReason( viewLight ) + != SHADOWMAP_SUPPORT_OK ) { + return false; + } + + backEnd.vLight = viewLight; + const shadowMapPassKind_t passKind = + RB_ShadowMapCachePassKind( viewLight, requestedPassKind ); + preparedPass.pointLight = pointLight; + preparedPass.cacheLightIndex = RB_ShadowMapLightIndex( viewLight ); + preparedPass.cachePassKind = static_cast( passKind ); + if ( pass.resourceAlias ) { + return RB_SharedWorldInteractionGLAliasMappedPass( + preparedLight, prepared, preparedPass ); + } + if ( !pass.allowCacheUpdate ) { + return false; + } + + shadowMapSchedule_t schedule; + if ( !RB_SharedWorldInteractionGLScheduleSealedMapPass( pass, + prepared.viewDef, + preparedPass.cacheLightIndex, passKind, schedule ) ) { + return false; + } + if ( schedule.action == SHADOWMAP_SCHEDULE_REUSE ) { + preparedPass.signature = schedule.signature; + const bool assigned = pointLight + ? RB_SharedWorldInteractionGLAssignPointCacheResource( + preparedPass, schedule.pointEntry ) + : RB_SharedWorldInteractionGLAssignProjectedCacheResource( + preparedPass, schedule.projectedEntry ); + if ( !assigned ) { + return false; + } + preparedPass.prepared = pointLight + ? RB_SharedWorldInteractionGLPointEntryMatches( preparedPass ) + : RB_SharedWorldInteractionGLProjectedEntryMatches( + preparedPass ); + return preparedPass.prepared; + } + const bool useCacheResource = schedule.cacheable + && ( pointLight ? schedule.pointEntry != NULL + : schedule.projectedEntry != NULL ); + + preparedPass.signature = schedule.signature; + const bool resourceReady = useCacheResource + ? ( pointLight + ? RB_SharedWorldInteractionGLAssignPointCacheResource( + preparedPass, schedule.pointEntry ) + : RB_SharedWorldInteractionGLAssignProjectedCacheResource( + preparedPass, schedule.projectedEntry ) ) + : ( pointLight + ? RB_SharedWorldInteractionGLReservePointScratch( + prepared, preparedPass ) + : RB_SharedWorldInteractionGLReserveProjectedScratch( + prepared, preparedPass ) ); + if ( !resourceReady ) { + return false; + } + const bool renderOk = pointLight + ? RB_SharedWorldInteractionGLRenderPointMap( + prepared, preparedLight, preparedPass ) + : RB_SharedWorldInteractionGLRenderProjectedMap( + prepared, preparedLight, preparedPass ); + if ( useCacheResource ) { + RB_SharedWorldInteractionGLCompleteSealedCacheUpdate( + preparedPass, schedule, renderOk ); + } + if ( !renderOk + || !RB_SharedWorldInteractionGLCaptureMapResource( preparedPass ) ) { + return false; + } + preparedPass.cacheUpdated = useCacheResource; + preparedPass.prepared = pointLight + ? RB_SharedWorldInteractionGLPointEntryMatches( preparedPass ) + : RB_SharedWorldInteractionGLProjectedEntryMatches( preparedPass ); + return preparedPass.prepared; +} + +static bool RB_SharedWorldInteractionGLPreflightMapTransaction( + const viewDef_t *viewDef, + rbSharedWorldInteractionGLPreparedView_t &prepared, + int &failureDetail ) { + if ( !prepared.mapTransactionPrepared ) { + RB_ShadowMapStatsReset(); + RB_ShadowMapDebugOverlayReset(); + } + if ( prepared.shadowMapPassCount <= 0 ) { + prepared.mapTransactionPrepared = true; + return true; + } + if ( !prepared.mapGLStateCaptured ) { + prepared.mapGLStateCaptured = true; + prepared.mapBlendWasEnabled = glIsEnabled( GL_BLEND ) != 0; + prepared.mapDepthWasEnabled = glIsEnabled( GL_DEPTH_TEST ) != 0; + prepared.mapScissorWasEnabled = + glIsEnabled( GL_SCISSOR_TEST ) != 0; + prepared.mapStencilWasEnabled = + glIsEnabled( GL_STENCIL_TEST ) != 0; + prepared.mapCullWasEnabled = glIsEnabled( GL_CULL_FACE ) != 0; + GLboolean depthWrite = GL_TRUE; + GLboolean colorWrite[4] = { + GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE + }; + glGetBooleanv( GL_DEPTH_WRITEMASK, &depthWrite ); + glGetBooleanv( GL_COLOR_WRITEMASK, colorWrite ); + prepared.mapDepthWriteWasEnabled = depthWrite != 0; + for ( int component = 0; component < 4; ++component ) { + prepared.mapColorWriteWasEnabled[component] = + colorWrite[component] != 0; + } + prepared.mapSavedFaceCulling = backEnd.glState.faceCulling; + glGetFloatv( GL_COLOR_CLEAR_VALUE, prepared.mapSavedClearColor ); + } + RB_ShadowMapBuildUpdateAdmissions(); + for ( int lightIndex = 0; lightIndex < prepared.lightCount; + ++lightIndex ) { + rbSharedWorldInteractionGLPreparedLight_t &preparedLight = + prepared.lights[lightIndex]; + for ( int receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiver <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; ++receiver ) { + const int mapPassIndex = preparedLight.shadowMapPass[receiver]; + if ( mapPassIndex < 0 ) { + continue; + } + if ( mapPassIndex >= prepared.shadowMapPassCount + || !RB_SharedWorldInteractionGLPrepareMappedPass( + prepared, preparedLight, + static_cast( + receiver ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_TRANSACTION, + lightIndex ); + RB_SharedWorldInteractionGLAbortMapTransaction( viewDef ); + return false; + } + } + } + + // A later allocation may have evicted a pass prepared earlier in the same + // view. Revalidate every exact signature only after all reservations and + // renders have completed; no main-framebuffer mutation has happened yet. + for ( int passIndex = 0; passIndex < prepared.shadowMapPassCount; + ++passIndex ) { + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass = + prepared.shadowMapPasses[passIndex]; + if ( !preparedPass.prepared + || ( preparedPass.pointLight + ? !RB_SharedWorldInteractionGLPointEntryMatches( preparedPass ) + : !RB_SharedWorldInteractionGLProjectedEntryMatches( + preparedPass ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_RESOURCE ); + RB_SharedWorldInteractionGLAbortMapTransaction( viewDef ); + return false; + } + for ( int earlierIndex = 0; earlierIndex < passIndex; + ++earlierIndex ) { + const rbSharedWorldInteractionGLPreparedMapPass_t &earlier = + prepared.shadowMapPasses[earlierIndex]; + const bool physicalAlias = preparedPass.pointLight + ? earlier.pointLight + && preparedPass.sampleTextureHandle + == earlier.sampleTextureHandle + : !earlier.pointLight + && preparedPass.projectedDepthImage + == earlier.projectedDepthImage + && preparedPass.projectedOriginX + < earlier.projectedOriginX + + earlier.projectedSpanPixels + && earlier.projectedOriginX + < preparedPass.projectedOriginX + + preparedPass.projectedSpanPixels + && preparedPass.projectedOriginY + < earlier.projectedOriginY + + earlier.projectedSpanPixels + && earlier.projectedOriginY + < preparedPass.projectedOriginY + + preparedPass.projectedSpanPixels; + const auto exactOwnerAlias = []( + const rbSharedWorldInteractionGLPreparedMapPass_t &alias, + const rbSharedWorldInteractionGLPreparedMapPass_t &owner ) { + return alias.pass->resourceAlias + && alias.pass->legacyViewLight + == owner.pass->legacyViewLight + && alias.pass->lightIndex == owner.pass->lightIndex + && alias.pass->resourceOwner == owner.pass->receiver + && !owner.pass->resourceAlias; + }; + const bool permittedAlias = exactOwnerAlias( + preparedPass, earlier ) || exactOwnerAlias( + earlier, preparedPass ); + if ( physicalAlias && !permittedAlias ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_RESOURCE ); + RB_SharedWorldInteractionGLAbortMapTransaction( viewDef ); + return false; + } + } + } + RB_SharedWorldInteractionGLAbortMapTransaction( viewDef ); + prepared.mapTransactionPrepared = true; + return true; +} + +static bool RB_ARB2_SharedInteractionPreflight( const viewDef_t *viewDef, + const classicInteractionDomainView_t &view, int &failureDetail ) { + rbSharedWorldInteractionGLPreparedView_t &prepared = + rbSharedWorldInteractionGLPreparedView; + std::memset( &prepared, 0, sizeof( prepared ) ); + prepared.view = &view; + prepared.viewDef = viewDef; + prepared.hash = view.hash; + const auto resolveTexture = []( std::uint64_t resourceId, + idImage *&image ) -> bool { + return RB_SharedWorldInteractionGLValidateTexture( resourceId, + R_ClassicInteractionDomain_ResolveTexture( resourceId ), image ); + }; + + if ( viewDef == NULL || view.viewDef != viewDef || !view.ready + || viewDef->viewEntitys == NULL || viewDef->renderWorld == NULL + || view.lightCount < 0 + || view.lightCount > CLASSIC_INTERACTION_DOMAIN_MAX_LIGHTS + || view.surfaceCount < 0 + || view.surfaceCount > CLASSIC_INTERACTION_DOMAIN_MAX_SURFACES + || view.primitiveCount < 0 + || view.primitiveCount > CLASSIC_INTERACTION_DOMAIN_MAX_PRIMITIVES + || view.shadowCasterCount < 0 + || view.shadowCasterCount + > CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_CASTERS + || view.drawablePrimitiveCount < 0 + || view.drawablePrimitiveCount > view.primitiveCount + || view.noopPrimitiveCount + != view.primitiveCount - view.drawablePrimitiveCount + || view.drawableShadowCasterCount < 0 + || view.drawableShadowCasterCount > view.shadowCasterCount + || view.noopShadowCasterCount != view.shadowCasterCount + - view.drawableShadowCasterCount + || view.logicalVolumeDrawCount < 0 + || view.preloadVolumeDrawCount < 0 + || view.preloadVolumeDrawCount > view.logicalVolumeDrawCount + || view.shadowMapPassCount < 0 + || view.shadowMapPassCount + > CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES + || view.hybridShadowPassCount < 0 + || view.hybridShadowPassCount > view.shadowMapPassCount + || view.shadowMode < CLASSIC_INTERACTION_SHADOW_NONE + || view.shadowMode >= CLASSIC_INTERACTION_SHADOW_MODE_COUNT + || view.stencilReference != RB_ShadowMapSafeStencilClearValue() ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_VIEW ); + return false; + } + + const int allowedRenderFlags = RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 + || viewDef->renderView.globalMaterial != NULL + || backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL + || r_skipRender.GetBool() || r_skipRenderContext.GetBool() + || r_skipInteractions.GetBool() || r_skipBump.GetBool() + || r_skipDiffuse.GetBool() || r_skipSpecular.GetBool() + || r_skipTranslucent.GetBool() || r_singleTriangle.GetBool() + || r_showOverDraw.GetInteger() != 0 + || r_showShadows.GetInteger() != 0 + || RB_ShadowMapDebugMode() != 0 + || RB_ShadowMapReceiverDebugMode() + || ( view.shadowMapPassCount > 0 + && !r_useShadowMap.GetBool() ) + || ( view.shadowCasterCount > 0 && !r_shadows.GetBool() ) + || r_celShading.GetBool() || r_celShadingWorld.GetBool() + || RB_EnhancedMaterialShadingActive() + || RB_AppleGL21AutomaticInteractionPath() + || RB_UseSimpleInteractionShader() || r_testARBProgram.GetBool() + || r_interactionColorMode.IsModified() ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_MUTATED_VIEW ); + return false; + } + + if ( view.viewportX1 != viewDef->viewport.x1 + || view.viewportY1 != viewDef->viewport.y1 + || view.viewportX2 != viewDef->viewport.x2 + || view.viewportY2 != viewDef->viewport.y2 + || view.scissorX1 != viewDef->scissor.x1 + || view.scissorY1 != viewDef->scissor.y1 + || view.scissorX2 != viewDef->scissor.x2 + || view.scissorY2 != viewDef->scissor.y2 + || std::memcmp( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ) != 0 + || !RB_SharedWorldInteractionGLFiniteArray( + view.projectionMatrix, 16 ) + || !RB_SharedWorldInteractionGLFinite( view.lightScale ) + || !RB_SharedWorldInteractionGLFinite( view.overBright ) + || !RB_SharedWorldInteractionGLFinite( view.shadowPolygonFactor ) + || !RB_SharedWorldInteractionGLFinite( view.shadowPolygonUnits ) + || idMath::Fabs( view.lightScale - backEnd.lightScale ) > 0.00001f + || idMath::Fabs( view.overBright - backEnd.overBright ) > 0.00001f + || view.useScissor != r_useScissor.GetBool() + || view.useShadowVertexProgram + != r_useShadowVertexProgram.GetBool() + || view.preferTwoSidedStencil != r_useTwoSidedStencil.GetBool() + || view.useDepthBounds != r_useDepthBoundsTest.GetBool() + || view.stencilTranslucentShadows + != r_stencilTranslucentShadows.GetBool() + || idMath::Fabs( view.shadowPolygonFactor + - r_shadowPolygonFactor.GetFloat() ) > 0.00001f + || idMath::Fabs( view.shadowPolygonUnits + + r_shadowPolygonOffset.GetFloat() ) > 0.00001f ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_MUTATED_VIEW ); + return false; + } + + if ( !glConfig.isInitialized || glConfig.disableARB2Interactions + || !glConfig.ARBVertexProgramAvailable + || !glConfig.ARBFragmentProgramAvailable || globalImages == NULL + || glConfig.maxTextureUnits < 7 + || glConfig.maxTextureImageUnits < 7 + || glBindProgramARB == NULL || glProgramEnvParameter4fvARB == NULL + || glEnableVertexAttribArrayARB == NULL + || glDisableVertexAttribArrayARB == NULL + || glVertexAttribPointerARB == NULL + || ( view.useDepthBounds + && ( !glConfig.depthBoundsTestAvailable + || glDepthBoundsEXT == NULL ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_CAPACITY ); + return false; + } + const GLuint vertexProgram = + RB_CurrentInteractionProgramIdent( GL_VERTEX_PROGRAM_ARB ); + const GLuint fragmentProgram = + RB_CurrentInteractionProgramIdent( GL_FRAGMENT_PROGRAM_ARB ); + if ( vertexProgram != VPROG_INTERACTION + || fragmentProgram != FPROG_INTERACTION + || !R_IsARBProgramValid( GL_VERTEX_PROGRAM_ARB, vertexProgram ) + || !R_IsARBProgramValid( GL_FRAGMENT_PROGRAM_ARB, + fragmentProgram ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PROGRAM ); + return false; + } + if ( view.useShadowVertexProgram + && ( !tr.backEndRendererHasVertexPrograms + || !R_IsARBProgramValid( GL_VERTEX_PROGRAM_ARB, + VPROG_STENCIL_SHADOW ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PROGRAM ); + return false; + } + if ( !RB_SharedWorldInteractionGLIntrinsicImageValid( + globalImages->normalCubeMapImage, TT_CUBIC ) + || !RB_SharedWorldInteractionGLIntrinsicImageValid( + globalImages->ambientNormalMap, TT_CUBIC ) + || !RB_SharedWorldInteractionGLIntrinsicImageValid( + globalImages->specularTableImage, TT_2D ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_INTRINSIC_TEXTURE ); + return false; + } + + int surfaceCursor = 0; + int primitiveCursor = 0; + int drawablePrimitives = 0; + int noopPrimitives = 0; + int shadowCasterCursor = 0; + int drawableShadowCasters = 0; + int noopShadowCasters = 0; + int logicalVolumeDraws = 0; + int preloadVolumeDraws = 0; + int drawableMapCasters = 0; + int shadowAlphaStages = 0; + int shadowMapPasses = 0; + int hybridShadowPasses = 0; + int shadowLights = 0; + int receiverSurfaces[ CLASSIC_INTERACTION_RECEIVER_COUNT ] = { 0, 0, 0 }; + int receiverPrimitives[ CLASSIC_INTERACTION_RECEIVER_COUNT ] = { 0, 0, 0 }; + for ( int lightIndex = 0; lightIndex < view.lightCount; ++lightIndex ) { + const int domainLightIndex = view.firstLight + lightIndex; + const classicInteractionDomainLight_t *light = + R_ClassicInteractionDomain_ViewLight( view, lightIndex ); + if ( light == NULL || prepared.seenLights[lightIndex] + || light->legacyViewLight == NULL + || light->surfaceCount < 0 + || light->surfaceCount > view.surfaceCount - surfaceCursor + || light->primitiveCount < 0 + || light->primitiveCount > view.primitiveCount - primitiveCursor ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_LIGHT_RANGE, + lightIndex ); + return false; + } + prepared.seenLights[lightIndex] = true; + rbSharedWorldInteractionGLPreparedLight_t &preparedLight = + prepared.lights[lightIndex]; + std::memset( &preparedLight, 0, sizeof( preparedLight ) ); + preparedLight.light = light; + for ( int receiver = 0; + receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; ++receiver ) { + preparedLight.firstPrimitive[receiver] = -1; + } + for ( int chain = 0; + chain < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; ++chain ) { + preparedLight.firstShadowCaster[chain] = -1; + preparedLight.firstMapCaster[chain] = -1; + } + preparedLight.shadowMapPass[0] = -1; + preparedLight.shadowMapPass[1] = -1; + int lightDrawablePrimitives = 0; + int lightNoopPrimitives = 0; + int lightReceiverSurfaces[ CLASSIC_INTERACTION_RECEIVER_COUNT ] = { 0, 0, 0 }; + int lightReceiverPrimitives[ CLASSIC_INTERACTION_RECEIVER_COUNT ] = { 0, 0, 0 }; + for ( int lightSurfaceIndex = 0; + lightSurfaceIndex < light->surfaceCount; ++lightSurfaceIndex ) { + const classicInteractionDomainSurface_t *surface = + R_ClassicInteractionDomain_LightSurface( + *light, lightSurfaceIndex ); + const classicInteractionDomainSurface_t *viewSurface = + R_ClassicInteractionDomain_ViewSurface( view, surfaceCursor ); + if ( surface == NULL || surface != viewSurface + || prepared.seenSurfaces[surfaceCursor] + || surface->legacyDrawSurf == NULL + || surface->legacyViewLight != light->legacyViewLight + || surface->lightIndex != domainLightIndex + || surface->receiver < CLASSIC_INTERACTION_RECEIVER_LOCAL + || surface->receiver >= CLASSIC_INTERACTION_RECEIVER_COUNT + || surface->primitiveCount < 0 + || surface->primitiveCount + > view.primitiveCount - primitiveCursor + || surface->drawablePrimitiveCount < 0 + || surface->drawablePrimitiveCount > surface->primitiveCount + || surface->noopPrimitiveCount != surface->primitiveCount + - surface->drawablePrimitiveCount ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SURFACE_RANGE, + lightIndex, surfaceCursor ); + return false; + } + prepared.seenSurfaces[surfaceCursor] = true; + receiverSurfaces[surface->receiver]++; + lightReceiverSurfaces[surface->receiver]++; + + const drawSurf_t *surf = surface->legacyDrawSurf; + const srfTriangles_t *tri = surf->geo; + if ( tri == NULL || surf->space == NULL + || surface->firstIndex != 0 || surface->vertexOffset != 0 + || surface->vertexCount <= 0 || surface->indexCount <= 0 + || surface->indexCount % 3 != 0 + || tri->numVerts != surface->vertexCount + || tri->numIndexes != surface->indexCount + || surface->vertexCount > idMath::INT_MAX + / static_cast( sizeof( idDrawVert ) ) + || surface->indexCount > idMath::INT_MAX + / static_cast( sizeof( glIndex_t ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_GEOMETRY, + lightIndex, surfaceCursor ); + return false; + } + + if ( surface->drawablePrimitiveCount > 0 + && !RB_SharedWorldInteractionGLCacheValid( + tri->ambientCache, false, + surface->vertexCount + * static_cast( sizeof( idDrawVert ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_VERTEX_CACHE, + lightIndex, surfaceCursor ); + return false; + } + if ( surface->drawablePrimitiveCount > 0 ) { + R_TouchVertexCache( tri->ambientCache ); + } + if ( surface->drawablePrimitiveCount > 0 + && tri->indexCache != NULL + && !RB_SharedWorldInteractionGLCacheValid( + tri->indexCache, true, + surface->indexCount + * static_cast( sizeof( glIndex_t ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_INDEX_CACHE, + lightIndex, surfaceCursor ); + return false; + } + if ( surface->drawablePrimitiveCount > 0 + && tri->indexCache != NULL ) { + R_TouchVertexCache( tri->indexCache ); + } + if ( surface->drawablePrimitiveCount > 0 + && ( ( !r_useIndexBuffers.GetBool() + || tri->indexCache == NULL ) + && tri->indexes == NULL ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_INDEX_CACHE, + lightIndex, surfaceCursor ); + return false; + } + + int surfaceDrawablePrimitives = 0; + int surfaceNoopPrimitives = 0; + for ( int surfacePrimitiveIndex = 0; + surfacePrimitiveIndex < surface->primitiveCount; + ++surfacePrimitiveIndex ) { + const classicInteractionDomainPrimitive_t *primitive = + R_ClassicInteractionDomain_SurfacePrimitive( + *surface, surfacePrimitiveIndex ); + const classicInteractionDomainPrimitive_t *viewPrimitive = + R_ClassicInteractionDomain_ViewPrimitive( + view, primitiveCursor ); + const int domainSurfaceIndex = view.firstSurface + surfaceCursor; + if ( primitive == NULL || primitive != viewPrimitive + || prepared.seenPrimitives[primitiveCursor] + || primitive->legacyDrawSurf != surf + || primitive->legacyViewLight != light->legacyViewLight + || primitive->lightIndex != domainLightIndex + || primitive->surfaceIndex != domainSurfaceIndex + || primitive->receiver != surface->receiver + || primitive->vertexCount != surface->vertexCount + || primitive->firstIndex != surface->firstIndex + || primitive->indexCount != surface->indexCount + || primitive->vertexOffset != surface->vertexOffset + || primitive->scissorX1 != surface->scissorX1 + || primitive->scissorY1 != surface->scissorY1 + || primitive->scissorX2 != surface->scissorX2 + || primitive->scissorY2 != surface->scissorY2 + || std::memcmp( primitive->modelMatrix, + surf->space->modelMatrix, + sizeof( primitive->modelMatrix ) ) != 0 + || std::memcmp( primitive->modelViewMatrix, + surf->space->modelViewMatrix, + sizeof( primitive->modelViewMatrix ) ) != 0 + || !RB_SharedWorldInteractionGLPrimitiveValuesValid( + *primitive ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PRIMITIVE_STATE, + lightIndex, surfaceCursor, primitiveCursor ); + return false; + } + prepared.seenPrimitives[primitiveCursor] = true; + receiverPrimitives[primitive->receiver]++; + lightReceiverPrimitives[primitive->receiver]++; + if ( primitive->disposition != CLASSIC_INTERACTION_PRIMITIVE_DRAW ) { + surfaceNoopPrimitives++; + lightNoopPrimitives++; + noopPrimitives++; + primitiveCursor++; + continue; + } + + rbSharedWorldInteractionGLPreparedPrimitive_t &preparedPrimitive = + prepared.primitives[drawablePrimitives]; + std::memset( &preparedPrimitive, 0, + sizeof( preparedPrimitive ) ); + preparedPrimitive.primitive = primitive; + preparedPrimitive.vertexBuffer = tri->ambientCache->vbo; + preparedPrimitive.vertexPointer = + RB_SharedWorldInteractionGLCachePointer( + *tri->ambientCache ); + const bool useIndexCache = r_useIndexBuffers.GetBool() + && tri->indexCache != NULL; + preparedPrimitive.indexBuffer = useIndexCache + ? tri->indexCache->vbo : 0; + preparedPrimitive.indexPointer = useIndexCache + ? RB_SharedWorldInteractionGLCachePointer( + *tri->indexCache ) + : static_cast( tri->indexes ); + preparedPrimitive.vertexCount = surface->vertexCount; + preparedPrimitive.indexCount = surface->indexCount; + preparedPrimitive.referencedVertexes = tri->ambientSurface != NULL + && tri->verts == tri->ambientSurface->verts; + preparedPrimitive.referencedIndexes = tri->ambientSurface != NULL + && tri->indexes == tri->ambientSurface->indexes; + preparedPrimitive.indexCacheBacked = useIndexCache; + preparedPrimitive.stateBits = GLS_SRCBLEND_ONE + | GLS_DSTBLEND_ONE | GLS_DEPTHMASK + | ( primitive->depth == CLASSIC_INTERACTION_DEPTH_EQUAL + ? GLS_DEPTHFUNC_EQUAL : GLS_DEPTHFUNC_LESS ); + if ( !RB_SharedWorldInteractionGLMapCull( + primitive->cull, preparedPrimitive.cullType ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_PRIMITIVE_STATE, + lightIndex, surfaceCursor, primitiveCursor ); + return false; + } + if ( !resolveTexture( + primitive->lightImageResourceId, + preparedPrimitive.lightImage ) + || !resolveTexture( + primitive->lightFalloffImageResourceId, + preparedPrimitive.lightFalloffImage ) + || !resolveTexture( + primitive->bumpImageResourceId, + preparedPrimitive.bumpImage ) + || !resolveTexture( + primitive->diffuseImageResourceId, + preparedPrimitive.diffuseImage ) + || !resolveTexture( + primitive->specularImageResourceId, + preparedPrimitive.specularImage ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_TEXTURE, + lightIndex, surfaceCursor, primitiveCursor ); + return false; + } + preparedPrimitive.normalImage = primitive->ambientLight + ? globalImages->ambientNormalMap + : globalImages->normalCubeMapImage; + const int receiver = static_cast( primitive->receiver ); + if ( preparedLight.firstPrimitive[receiver] < 0 ) { + preparedLight.firstPrimitive[receiver] = drawablePrimitives; + } else if ( preparedLight.firstPrimitive[receiver] + + preparedLight.primitiveCount[receiver] + != drawablePrimitives ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, + lightIndex, surfaceCursor, primitiveCursor ); + return false; + } + preparedLight.primitiveCount[receiver]++; + surfaceDrawablePrimitives++; + lightDrawablePrimitives++; + drawablePrimitives++; + primitiveCursor++; + } + if ( surfaceDrawablePrimitives != surface->drawablePrimitiveCount + || surfaceNoopPrimitives != surface->noopPrimitiveCount ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, + lightIndex, surfaceCursor ); + return false; + } + surfaceCursor++; + } + if ( lightDrawablePrimitives != light->drawablePrimitiveCount + || lightNoopPrimitives != light->noopPrimitiveCount ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, + lightIndex ); + return false; + } + for ( int receiver = 0; receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; + ++receiver ) { + if ( lightReceiverSurfaces[receiver] + != light->receiverSurfaceCount[receiver] + || lightReceiverPrimitives[receiver] + != light->receiverPrimitiveCount[receiver] ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, + lightIndex ); + return false; + } + if ( light->receiverShadowMode[receiver] + < CLASSIC_INTERACTION_SHADOW_NONE + || light->receiverShadowMode[receiver] + >= CLASSIC_INTERACTION_SHADOW_MODE_COUNT ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MODE, + lightIndex ); + return false; + } + } + + int lightDrawableShadowCasters = 0; + int lightNoopShadowCasters = 0; + int lightLogicalVolumeDraws = 0; + int lightPreloadVolumeDraws = 0; + int lightShadowCasterTotal = 0; + int lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ] = {}; + int lightPreloadByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT ] = {}; + for ( int chain = 0; chain < CLASSIC_INTERACTION_SHADOW_CHAIN_COUNT; + ++chain ) { + const int chainCount = light->shadowCasterCount[chain]; + if ( chainCount < 0 + || chainCount > view.shadowCasterCount - shadowCasterCursor ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_RANGE, + lightIndex ); + return false; + } + lightShadowCasterTotal += chainCount; + if ( chainCount <= 0 ) { + continue; + } + const classicInteractionDomainShadowChain_t shadowChain = + static_cast( chain ); + const bool mapCasterChain = + RB_SharedWorldInteractionGLIsMapCasterChain( shadowChain ); + const bool volumeChain = + RB_SharedWorldInteractionGLIsVolumeChain( shadowChain ); + if ( !mapCasterChain && !volumeChain ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_RANGE, + lightIndex ); + return false; + } + if ( mapCasterChain ) { + preparedLight.firstMapCaster[chain] = drawableMapCasters; + } else { + preparedLight.firstShadowCaster[chain] = + drawableShadowCasters; + } + for ( int chainCasterIndex = 0; chainCasterIndex < chainCount; + ++chainCasterIndex ) { + const int viewCasterIndex = shadowCasterCursor; + const classicInteractionDomainShadowCaster_t *caster = + R_ClassicInteractionDomain_LightShadowCaster( + *light, shadowChain, chainCasterIndex ); + const classicInteractionDomainShadowCaster_t *viewCaster = + R_ClassicInteractionDomain_ViewShadowCaster( + view, shadowCasterCursor ); + if ( caster == NULL || caster != viewCaster + || prepared.seenShadowCasters[shadowCasterCursor] + || caster->legacyViewLight != light->legacyViewLight + || caster->lightIndex != domainLightIndex + || caster->legacyDrawSurf == NULL + || caster->legacyDrawSurf->space == NULL + || std::memcmp( caster->modelMatrix, + caster->legacyDrawSurf->space->modelMatrix, + sizeof( caster->modelMatrix ) ) != 0 + || std::memcmp( caster->modelViewMatrix, + caster->legacyDrawSurf->space->modelViewMatrix, + sizeof( caster->modelViewMatrix ) ) != 0 + || ( mapCasterChain + ? !RB_SharedWorldInteractionGLMapCasterValuesValid( + *caster, shadowChain ) + : !RB_SharedWorldInteractionGLShadowCasterValuesValid( + *caster, shadowChain ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_STATE, + lightIndex, -1, shadowCasterCursor ); + return false; + } + prepared.seenShadowCasters[shadowCasterCursor] = true; + shadowCasterCursor++; + if ( caster->disposition + != CLASSIC_INTERACTION_SHADOW_CASTER_DRAW ) { + lightNoopShadowCasters++; + noopShadowCasters++; + continue; + } + + const drawSurf_t *shadowSurf = caster->legacyDrawSurf; + const srfTriangles_t *tri = mapCasterChain + ? caster->legacyCasterGeometry : shadowSurf->geo; + if ( mapCasterChain ) { + const srfTriangles_t *sourceTri = shadowSurf->geo; + const srfTriangles_t *expectedTri = sourceTri != NULL + && sourceTri->ambientSurface != NULL + ? sourceTri->ambientSurface : sourceTri; + if ( tri == NULL || expectedTri != tri + || shadowSurf->space == NULL + || R_TriHasPrimBatchMesh( tri ) + || tri->numVerts != caster->vertexCount + || tri->numIndexes != caster->selectedIndexCount + || tri->numIndexes != caster->totalIndexCount + || caster->vertexCount > idMath::INT_MAX + / static_cast( sizeof( idDrawVert ) ) + || caster->selectedIndexCount > idMath::INT_MAX + / static_cast( sizeof( glIndex_t ) ) + || !RB_SharedWorldInteractionGLCacheValid( + tri->ambientCache, false, + caster->vertexCount + * static_cast( sizeof( idDrawVert ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_CASTER, + lightIndex, -1, viewCasterIndex ); + return false; + } + R_TouchVertexCache( tri->ambientCache ); + if ( tri->indexCache != NULL + && !RB_SharedWorldInteractionGLCacheValid( + tri->indexCache, true, + caster->selectedIndexCount + * static_cast( sizeof( glIndex_t ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_INDEX_CACHE, + lightIndex, -1, viewCasterIndex ); + return false; + } + if ( tri->indexCache != NULL ) { + R_TouchVertexCache( tri->indexCache ); + } + const bool useIndexCache = r_useIndexBuffers.GetBool() + && tri->indexCache != NULL; + if ( !useIndexCache && tri->indexes == NULL ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_INDEX_CACHE, + lightIndex, -1, viewCasterIndex ); + return false; + } + + rbSharedWorldInteractionGLPreparedMapCaster_t &preparedCaster = + prepared.mapCasters[drawableMapCasters]; + std::memset( &preparedCaster, 0, sizeof( preparedCaster ) ); + preparedCaster.caster = caster; + preparedCaster.vertexBuffer = tri->ambientCache->vbo; + preparedCaster.vertexPointer = + RB_SharedWorldInteractionGLCachePointer( *tri->ambientCache ); + preparedCaster.indexBuffer = useIndexCache + ? tri->indexCache->vbo : 0; + preparedCaster.indexPointer = useIndexCache + ? RB_SharedWorldInteractionGLCachePointer( *tri->indexCache ) + : static_cast( tri->indexes ); + preparedCaster.vertexCount = caster->vertexCount; + preparedCaster.indexCount = caster->selectedIndexCount; + preparedCaster.indexCacheBacked = useIndexCache; + preparedCaster.firstAlphaStage = shadowAlphaStages; + for ( int alphaIndex = 0; + alphaIndex < caster->alphaStageCount; ++alphaIndex ) { + const classicInteractionDomainShadowAlphaStage_t *alpha = + R_ClassicInteractionDomain_ShadowAlphaStage( + *caster, alphaIndex ); + idImage *alphaImage = NULL; + if ( alpha == NULL + || shadowAlphaStages + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_ALPHA_STAGES + || !RB_SharedWorldInteractionGLShadowAlphaStageValuesValid( + *alpha, view.firstShadowCaster + viewCasterIndex ) + || !resolveTexture( alpha->textureResourceId, + alphaImage ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_ALPHA, + lightIndex, -1, viewCasterIndex ); + return false; + } + prepared.shadowAlphaStages[shadowAlphaStages].stage = alpha; + prepared.shadowAlphaStages[shadowAlphaStages].image = alphaImage; + shadowAlphaStages++; + preparedCaster.alphaStageCount++; + } + preparedLight.mapCasterCount[chain]++; + drawableMapCasters++; + lightDrawableShadowCasters++; + lightDrawableByChain[chain]++; + drawableShadowCasters++; + continue; + } + + int sealedIndexCount = -1; + if ( caster->indexSelection + == CLASSIC_INTERACTION_SHADOW_INDEX_FULL ) { + sealedIndexCount = tri != NULL ? tri->numIndexes : -1; + } else if ( caster->indexSelection + == CLASSIC_INTERACTION_SHADOW_INDEX_NO_FRONT_CAPS ) { + sealedIndexCount = tri != NULL + ? tri->numShadowIndexesNoFrontCaps : -1; + } else if ( caster->indexSelection + == CLASSIC_INTERACTION_SHADOW_INDEX_NO_CAPS ) { + sealedIndexCount = tri != NULL + ? tri->numShadowIndexesNoCaps : -1; + } + if ( tri == NULL || shadowSurf->space == NULL + || caster->legacyCasterGeometry != tri + || R_TriHasPrimBatchMesh( tri ) + || tri->numVerts != caster->vertexCount + || tri->numIndexes != caster->totalIndexCount + || sealedIndexCount != caster->selectedIndexCount + || caster->vertexCount > idMath::INT_MAX + / static_cast( sizeof( shadowCache_t ) ) + || caster->selectedIndexCount > idMath::INT_MAX + / static_cast( sizeof( glIndex_t ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_GEOMETRY, + lightIndex, -1, shadowCasterCursor - 1 ); + return false; + } + // Vertex-program turbo volumes seal numVerts as the paired + // front/back stream already, while private projected volumes seal + // their explicit stream. In both cases numVerts is the exact cache + // record count consumed by the index buffer. + if ( !RB_SharedWorldInteractionGLCacheValid( tri->shadowCache, + false, caster->vertexCount + * static_cast( sizeof( shadowCache_t ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_VERTEX_CACHE, + lightIndex, -1, shadowCasterCursor - 1 ); + return false; + } + R_TouchVertexCache( tri->shadowCache ); + if ( tri->indexCache != NULL + && !RB_SharedWorldInteractionGLCacheValid( tri->indexCache, + true, caster->selectedIndexCount + * static_cast( sizeof( glIndex_t ) ) ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_INDEX_CACHE, + lightIndex, -1, shadowCasterCursor - 1 ); + return false; + } + if ( tri->indexCache != NULL ) { + R_TouchVertexCache( tri->indexCache ); + } + const bool useIndexCache = r_useIndexBuffers.GetBool() + && tri->indexCache != NULL; + if ( !useIndexCache && tri->indexes == NULL ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_INDEX_CACHE, + lightIndex, -1, shadowCasterCursor - 1 ); + return false; + } + + rbSharedWorldInteractionGLPreparedShadowCaster_t &preparedCaster = + prepared.shadowCasters[drawableShadowCasters]; + std::memset( &preparedCaster, 0, sizeof( preparedCaster ) ); + preparedCaster.caster = caster; + preparedCaster.vertexBuffer = tri->shadowCache->vbo; + preparedCaster.vertexPointer = + RB_SharedWorldInteractionGLCachePointer( *tri->shadowCache ); + preparedCaster.indexBuffer = useIndexCache + ? tri->indexCache->vbo : 0; + preparedCaster.indexPointer = useIndexCache + ? RB_SharedWorldInteractionGLCachePointer( *tri->indexCache ) + : static_cast( tri->indexes ); + preparedCaster.vertexCount = caster->vertexCount; + preparedCaster.indexCount = caster->selectedIndexCount; + preparedCaster.indexCacheBacked = useIndexCache; + preparedLight.shadowCasterCount[chain]++; + lightDrawableShadowCasters++; + lightDrawableByChain[chain]++; + drawableShadowCasters++; + if ( caster->preload ) { + lightPreloadByChain[chain]++; + } + } + } + + for ( int receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiver <= CLASSIC_INTERACTION_RECEIVER_GLOBAL; ++receiver ) { + const classicInteractionDomainShadowMapPass_t *mapPass = + R_ClassicInteractionDomain_LightShadowMapPass( + *light, + static_cast( receiver ) ); + const classicInteractionDomainShadowMode_t receiverMode = + light->receiverShadowMode[receiver]; + const bool mappedReceiver = + receiverMode == CLASSIC_INTERACTION_SHADOW_PROJECTED + || receiverMode == CLASSIC_INTERACTION_SHADOW_POINT + || receiverMode == CLASSIC_INTERACTION_SHADOW_HYBRID; + if ( mappedReceiver != ( mapPass != NULL ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_PASS, + lightIndex ); + return false; + } + if ( mapPass == NULL ) { + continue; + } + if ( shadowMapPasses + >= CLASSIC_INTERACTION_DOMAIN_MAX_SHADOW_MAP_PASSES + || !RB_SharedWorldInteractionGLMapPassValuesValid( + *light, *mapPass, + static_cast( receiver ), + domainLightIndex ) ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_SHADOW_MAP_PASS, + lightIndex ); + return false; + } + const bool globalReceiver = receiver + == CLASSIC_INTERACTION_RECEIVER_GLOBAL; + const bool expectedHasStatic = + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC ] + + ( globalReceiver ? light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC ] + : 0 ) > 0; + const bool expectedHasDynamic = + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC ] + + ( globalReceiver ? light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ] + : 0 ) > 0; + bool expectedHasAlpha = false; + for ( int chainIndex = + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC; + chainIndex + <= CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC; + ++chainIndex ) { + const classicInteractionDomainShadowChain_t mapChain = + static_cast( + chainIndex ); + if ( !globalReceiver + && ( mapChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC + || mapChain + == CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ) ) { + continue; + } + for ( int casterIndex = 0; + casterIndex < light->shadowCasterCount[mapChain]; + ++casterIndex ) { + const classicInteractionDomainShadowCaster_t *mapCaster = + R_ClassicInteractionDomain_LightShadowCaster( + *light, mapChain, casterIndex ); + expectedHasAlpha = expectedHasAlpha + || ( mapCaster != NULL + && mapCaster->alphaStageCount > 0 ); + } + } + const int expectedMapped = + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC ] + + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC ] + + ( globalReceiver + ? light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC ] + + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ] + : 0 ); + const int expectedDrawableMapped = + lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC ] + + lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC ] + + ( globalReceiver + ? lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC ] + + lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC ] + : 0 ); + const int expectedSupplement = + light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL ] + + ( globalReceiver + ? light->shadowCasterCount[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ] + : 0 ); + const int expectedDrawableSupplement = + lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL ] + + ( globalReceiver + ? lightDrawableByChain[ + CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL ] + : 0 ); + if ( mapPass->hasStaticCasters != expectedHasStatic + || mapPass->hasDynamicCasters != expectedHasDynamic + || mapPass->hasAlphaCasters != expectedHasAlpha + || mapPass->mappedCasterCount != expectedMapped + || mapPass->drawableMappedCasters + != expectedDrawableMapped + || mapPass->noopMappedCasters + != expectedMapped - expectedDrawableMapped + || mapPass->supplementCasterCount + != expectedSupplement + || mapPass->drawableSupplementCasters + != expectedDrawableSupplement + || mapPass->noopSupplementCasters + != expectedSupplement + - expectedDrawableSupplement ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, + lightIndex ); + return false; + } + preparedLight.shadowMapPass[receiver] = shadowMapPasses; + prepared.shadowMapPasses[shadowMapPasses].pass = mapPass; + if ( mapPass->disposition + == CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID ) { + hybridShadowPasses++; + } + shadowMapPasses++; + } + classicInteractionDomainShadowMode_t plannedVolumeMode = + CLASSIC_INTERACTION_SHADOW_NONE; + bool plannedVolumeIncludesLocal = false; + bool plannedVolumeSelected = false; + for ( int receiver = CLASSIC_INTERACTION_RECEIVER_LOCAL; + receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; ++receiver ) { + const classicInteractionDomainShadowMode_t mode = + light->receiverShadowMode[receiver]; + const bool hybrid = mode == CLASSIC_INTERACTION_SHADOW_HYBRID; + if ( mode != CLASSIC_INTERACTION_SHADOW_STENCIL && !hybrid ) { + continue; + } + plannedVolumeSelected = true; + const int globalChain = hybrid + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL; + const int localChain = hybrid + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL; + if ( plannedVolumeMode != mode ) { + lightLogicalVolumeDraws += lightDrawableByChain[globalChain]; + lightPreloadVolumeDraws += lightPreloadByChain[globalChain]; + plannedVolumeMode = mode; + plannedVolumeIncludesLocal = false; + } + if ( receiver != CLASSIC_INTERACTION_RECEIVER_LOCAL + && !plannedVolumeIncludesLocal ) { + lightLogicalVolumeDraws += lightDrawableByChain[localChain]; + lightPreloadVolumeDraws += lightPreloadByChain[localChain]; + plannedVolumeIncludesLocal = true; + } + } + logicalVolumeDraws += lightLogicalVolumeDraws; + preloadVolumeDraws += lightPreloadVolumeDraws; + if ( lightShadowCasterTotal != light->shadowCasterTotal + || lightDrawableShadowCasters != light->drawableShadowCasters + || lightNoopShadowCasters != light->noopShadowCasters + || lightLogicalVolumeDraws != light->logicalVolumeDraws + || lightPreloadVolumeDraws != light->preloadVolumeDraws + || light->clearStencil != plannedVolumeSelected ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE, lightIndex ); + return false; + } + if ( lightShadowCasterTotal > 0 ) { + shadowLights++; + } + } + + if ( surfaceCursor != view.surfaceCount + || primitiveCursor != view.primitiveCount + || drawablePrimitives != view.drawablePrimitiveCount + || noopPrimitives != view.noopPrimitiveCount + || shadowCasterCursor != view.shadowCasterCount + || drawableShadowCasters != view.drawableShadowCasterCount + || noopShadowCasters != view.noopShadowCasterCount + || logicalVolumeDraws != view.logicalVolumeDrawCount + || preloadVolumeDraws != view.preloadVolumeDrawCount + || shadowLights != view.shadowLightCount + || shadowMapPasses != view.shadowMapPassCount + || hybridShadowPasses != view.hybridShadowPassCount ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE ); + return false; + } + for ( int receiver = 0; receiver < CLASSIC_INTERACTION_RECEIVER_COUNT; + ++receiver ) { + if ( receiverSurfaces[receiver] != view.receiverSurfaceCount[receiver] + || receiverPrimitives[receiver] + != view.receiverPrimitiveCount[receiver] ) { + failureDetail = RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE ); + return false; + } + } + + prepared.lightCount = view.lightCount; + prepared.surfaceCount = view.surfaceCount; + prepared.primitiveCount = view.primitiveCount; + prepared.drawablePrimitiveCount = drawablePrimitives; + prepared.noopPrimitiveCount = noopPrimitives; + prepared.shadowCasterCount = view.shadowCasterCount; + prepared.drawableShadowCasterCount = drawableShadowCasters; + prepared.noopShadowCasterCount = noopShadowCasters; + prepared.logicalVolumeDrawCount = logicalVolumeDraws; + prepared.preloadVolumeDrawCount = preloadVolumeDraws; + prepared.mapCasterCount = drawableMapCasters; + prepared.shadowAlphaStageCount = shadowAlphaStages; + prepared.shadowMapPassCount = shadowMapPasses; + prepared.hybridShadowPassCount = hybridShadowPasses; + prepared.twoSidedStencil = view.preferTwoSidedStencil + && glStencilOpSeparate != NULL && tr.stencilIncr == GL_INCR_WRAP_EXT; + if ( !RB_SharedWorldInteractionGLPreflightMapTransaction( + viewDef, prepared, failureDetail ) ) { + return false; + } + prepared.ready = true; + return true; +} + +static void RB_SharedWorldInteractionGLLoadPrimitiveParameters( + const classicInteractionDomainPrimitive_t &primitive ) { + RB_ARB2_SetVertexEnvParm( PP_LIGHT_ORIGIN, + primitive.localLightOrigin ); + RB_ARB2_SetVertexEnvParm( PP_VIEW_ORIGIN, + primitive.localViewOrigin ); + RB_ARB2_SetVertexEnvParm( PP_LIGHT_PROJECT_S, + primitive.lightProjection[0] ); + RB_ARB2_SetVertexEnvParm( PP_LIGHT_PROJECT_T, + primitive.lightProjection[1] ); + RB_ARB2_SetVertexEnvParm( PP_LIGHT_PROJECT_Q, + primitive.lightProjection[2] ); + RB_ARB2_SetVertexEnvParm( PP_LIGHT_FALLOFF_S, + primitive.lightProjection[3] ); + RB_ARB2_SetVertexEnvParm( PP_BUMP_MATRIX_S, primitive.bumpMatrix[0] ); + RB_ARB2_SetVertexEnvParm( PP_BUMP_MATRIX_T, primitive.bumpMatrix[1] ); + RB_ARB2_SetVertexEnvParm( PP_DIFFUSE_MATRIX_S, + primitive.diffuseMatrix[0] ); + RB_ARB2_SetVertexEnvParm( PP_DIFFUSE_MATRIX_T, + primitive.diffuseMatrix[1] ); + RB_ARB2_SetVertexEnvParm( PP_SPECULAR_MATRIX_S, + primitive.specularMatrix[0] ); + RB_ARB2_SetVertexEnvParm( PP_SPECULAR_MATRIX_T, + primitive.specularMatrix[1] ); + + float modulate = 0.0f; + float add = 1.0f; + switch ( primitive.vertexColor ) { + case RENDERER_VERTEX_COLOR_MODULATE: + modulate = 1.0f; + add = 0.0f; + break; + case RENDERER_VERTEX_COLOR_INVERSE_MODULATE: + modulate = -1.0f; + add = 1.0f; + break; + default: + break; + } + static const float zero[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + if ( g_interactionVertexProgramColorMode == ICM_PACKED ) { + const float packed[4] = { modulate, add, 0.0f, 0.0f }; + RB_ARB2_SetVertexEnvParm( PP_COLOR_MODULATE, packed ); + RB_ARB2_SetVertexEnvParm( PP_COLOR_ADD, zero ); + } else { + const float modulateVector[4] = { + modulate, modulate, modulate, modulate }; + const float addVector[4] = { add, add, add, add }; + RB_ARB2_SetVertexEnvParm( PP_COLOR_MODULATE, modulateVector ); + RB_ARB2_SetVertexEnvParm( PP_COLOR_ADD, addVector ); + } + + const float specularColorX2[4] = { + primitive.specularColor[0] * 2.0f, + primitive.specularColor[1] * 2.0f, + primitive.specularColor[2] * 2.0f, + primitive.specularColor[3] * 2.0f + }; + RB_ARB2_SetFragmentEnvParm( 0, primitive.diffuseColor ); + RB_ARB2_SetFragmentEnvParm( 1, specularColorX2 ); +} + +static void RB_SharedWorldInteractionGLBeginInteractionState( void ) { + RB_ARB2_InvalidateEnvParamCache(); + glBindProgramARB( GL_VERTEX_PROGRAM_ARB, VPROG_INTERACTION ); + glBindProgramARB( GL_FRAGMENT_PROGRAM_ARB, FPROG_INTERACTION ); + glEnable( GL_VERTEX_PROGRAM_ARB ); + glEnable( GL_FRAGMENT_PROGRAM_ARB ); + glDisableVertexAttribArrayARB( 1 ); + glDisableVertexAttribArrayARB( 2 ); + glDisableVertexAttribArrayARB( 5 ); + glDisableVertexAttribArrayARB( 6 ); + glDisableVertexAttribArrayARB( 7 ); + glEnableVertexAttribArrayARB( 8 ); + glEnableVertexAttribArrayARB( 9 ); + glEnableVertexAttribArrayARB( 10 ); + glEnableVertexAttribArrayARB( 11 ); + glEnableClientState( GL_COLOR_ARRAY ); + GL_SelectTextureNoClient( 6 ); + globalImages->specularTableImage->Bind(); +} + +static void RB_SharedWorldInteractionGLDrawPrimitive( + const rbSharedWorldInteractionGLPreparedPrimitive_t &preparedPrimitive, + const classicInteractionDomainView_t &view, + const viewDef_t *viewDef ) { + const classicInteractionDomainPrimitive_t &primitive = + *preparedPrimitive.primitive; + backEnd.currentSpace = NULL; + glLoadMatrixf( primitive.modelViewMatrix ); + if ( view.useScissor ) { + backEnd.currentScissor.x1 = primitive.scissorX1; + backEnd.currentScissor.y1 = primitive.scissorY1; + backEnd.currentScissor.x2 = primitive.scissorX2; + backEnd.currentScissor.y2 = primitive.scissorY2; + glScissor( viewDef->viewport.x1 + primitive.scissorX1, + viewDef->viewport.y1 + primitive.scissorY1, + primitive.scissorX2 + 1 - primitive.scissorX1, + primitive.scissorY2 + 1 - primitive.scissorY1 ); + } + GL_State( preparedPrimitive.stateBits ); + GL_Cull( preparedPrimitive.cullType ); + idVertexCache::BindArrayBuffer( preparedPrimitive.vertexBuffer ); + const void *ac = preparedPrimitive.vertexPointer; + glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_COLOR_OFFSET ) ); + glVertexAttribPointerARB( 11, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_NORMAL_OFFSET ) ); + glVertexAttribPointerARB( 10, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_TANGENT1_OFFSET ) ); + glVertexAttribPointerARB( 9, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_TANGENT0_OFFSET ) ); + glVertexAttribPointerARB( 8, 2, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_ST_OFFSET ) ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_XYZ_OFFSET ) ); + + RB_SharedWorldInteractionGLLoadPrimitiveParameters( primitive ); + GL_SelectTextureNoClient( 0 ); + preparedPrimitive.normalImage->Bind(); + GL_SelectTextureNoClient( 1 ); + preparedPrimitive.bumpImage->Bind(); + GL_SelectTextureNoClient( 2 ); + preparedPrimitive.lightFalloffImage->Bind(); + GL_SelectTextureNoClient( 3 ); + preparedPrimitive.lightImage->Bind(); + GL_SelectTextureNoClient( 4 ); + preparedPrimitive.diffuseImage->Bind(); + GL_SelectTextureNoClient( 5 ); + preparedPrimitive.specularImage->Bind(); + + if ( primitive.polygonOffsetEnabled ) { + glPolygonOffset( primitive.polygonOffsetFactor, + primitive.polygonOffsetUnits ); + glEnable( GL_POLYGON_OFFSET_FILL ); + } + idVertexCache::BindIndexBuffer( preparedPrimitive.indexBuffer ); + backEnd.pc.c_drawElements++; + backEnd.pc.c_drawIndexes += preparedPrimitive.indexCount; + backEnd.pc.c_drawVertexes += preparedPrimitive.vertexCount; + if ( preparedPrimitive.referencedIndexes ) { + backEnd.pc.c_drawRefIndexes += preparedPrimitive.indexCount; + } + if ( preparedPrimitive.referencedVertexes ) { + backEnd.pc.c_drawRefVertexes += preparedPrimitive.vertexCount; + } + if ( preparedPrimitive.indexCacheBacked ) { + backEnd.pc.c_vboIndexes += preparedPrimitive.indexCount; + } + glDrawElements( GL_TRIANGLES, preparedPrimitive.indexCount, + GL_INDEX_TYPE, preparedPrimitive.indexPointer ); + if ( primitive.polygonOffsetEnabled ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + } +} + +static int RB_SharedWorldInteractionGLDrawPrimitiveRange( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + classicInteractionDomainReceiver_t receiver, + const classicInteractionDomainView_t &view, + const viewDef_t *viewDef ) { + const int first = light.firstPrimitive[receiver]; + const int count = light.primitiveCount[receiver]; + for ( int i = 0; i < count; ++i ) { + RB_SharedWorldInteractionGLDrawPrimitive( + prepared.primitives[first + i], view, viewDef ); + } + return count; +} + +static void RB_SharedWorldInteractionGLSelectMapPass( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + RB_SharedWorldInteractionGLActivateMapResource( preparedPass ); + if ( !preparedPass.pointLight ) { + g_projectedShadowMapState = pass.projected.state; + } +} + +static void RB_SharedWorldInteractionGLBeginMappedReceiver( + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass ) { + const classicInteractionDomainShadowMapPass_t &pass = *preparedPass.pass; + const bool pointLight = preparedPass.pointLight; + glDisable( GL_VERTEX_PROGRAM_ARB ); + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + glUseProgramObjectARB( pointLight + ? g_pointShadowMapProgram.programObject + : g_shadowMapProgram.programObject ); + const GLint bumpMap = pointLight ? g_pointShadowMapProgram.bumpMap + : g_shadowMapProgram.bumpMap; + const GLint falloffMap = pointLight + ? g_pointShadowMapProgram.lightFalloffMap + : g_shadowMapProgram.lightFalloffMap; + const GLint projectionMap = pointLight + ? g_pointShadowMapProgram.lightProjectionMap + : g_shadowMapProgram.lightProjectionMap; + const GLint diffuseMap = pointLight ? g_pointShadowMapProgram.diffuseMap + : g_shadowMapProgram.diffuseMap; + const GLint specularMap = pointLight + ? g_pointShadowMapProgram.specularMap + : g_shadowMapProgram.specularMap; + glUniform1iARB( bumpMap, 0 ); + glUniform1iARB( falloffMap, 1 ); + glUniform1iARB( projectionMap, 2 ); + glUniform1iARB( diffuseMap, 3 ); + glUniform1iARB( specularMap, 4 ); + // The shared domain intentionally rejects translucent moment casters, but + // both mapped receiver programs still declare their three non-comparison + // moment samplers. Keep them on a neutral unit distinct from the depth + // comparison sampler; leaving their default binding at unit zero makes the + // point program's sampler types conflict once unit five enables comparison. + for ( int i = 0; i < 3; ++i ) { + const GLint momentMap = pointLight + ? g_pointShadowMapProgram.translucentShadowMap[i] + : g_shadowMapProgram.translucentShadowMap[i]; + if ( momentMap >= 0 ) { + glUniform1iARB( momentMap, 6 ); + } + } + + if ( pointLight ) { + glUniform1iARB( g_pointShadowMapProgram.pointShadowMap, 5 ); + shadowMapPointReceiverSettings_t sealedReceiverSettings; + sealedReceiverSettings.constantBias = pass.point.constantBias; + sealedReceiverSettings.normalBias = pass.point.normalBias; + sealedReceiverSettings.texelBiasScale = pass.point.texelBiasScale; + sealedReceiverSettings.normalOffsetScale = + pass.point.normalOffsetScale; + sealedReceiverSettings.worldBiasScale = 1.0f; + const shadowMapPointReceiverSettings_t receiverSettings = + R_ShadowMapPointStorageAdjustedReceiverSettings( + sealedReceiverSettings, pass.point.farDistance, + pass.point.faceSize, pass.point.depthCompare, + pass.point.highPrecision ); + glUniform1fARB( g_pointShadowMapProgram.shadowBias, + receiverSettings.constantBias ); + glUniform1fARB( g_pointShadowMapProgram.shadowNormalBias, + receiverSettings.normalBias ); + glUniform1fARB( + g_pointShadowMapProgram.pointShadowTexelDepthBias, + receiverSettings.texelBiasScale + / Max( 1, pass.point.faceSize ) ); + glUniform1fARB( + g_pointShadowMapProgram.pointShadowNormalOffsetWorld, + 2.0f * receiverSettings.normalOffsetScale + / Max( 1, pass.point.faceSize ) ); + glUniform1fARB( g_pointShadowMapProgram.shadowFilterRadius, + pass.point.filterRadius ); + glUniform1fARB( g_pointShadowMapProgram.shadowFilterTaps, + static_cast( pass.point.filterTaps ) ); + glUniform1fARB( g_pointShadowMapProgram.shadowFilterMode, + static_cast( pass.point.filterMode ) ); + glUniform1fARB( g_pointShadowMapProgram.shadowDebugMode, 0.0f ); + glUniform1fARB( + g_pointShadowMapProgram.shadowReceiverDebugReason, 0.0f ); + glUniform4fvARB( g_pointShadowMapProgram.globalLightOrigin, 1, + pass.point.lightOrigin ); + glUniform1fARB( g_pointShadowMapProgram.pointShadowFar, + pass.point.farDistance ); + glUniform1fARB( g_pointShadowMapProgram.pointShadowTexelScale, + 2.0f / Max( 1, pass.point.faceSize ) ); + glUniform1fARB( g_pointShadowMapProgram.pointShadowDepthMode, + pass.point.highPrecision ? 1.0f : 0.0f ); + glUniform1fARB( g_pointShadowMapProgram.translucentShadowEnabled, + 0.0f ); + GL_SelectTextureNoClient( 5 ); + idImage *sample = pass.point.depthCompare + ? preparedPass.pointDepthImage + : preparedPass.pointColorImage; + sample->Bind(); + glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_MODE, + pass.point.depthCompare ? GL_COMPARE_R_TO_TEXTURE : GL_NONE ); + if ( pass.point.depthCompare ) { + glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_COMPARE_FUNC, + GL_LEQUAL ); + glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR ); + glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, + GL_LINEAR ); + } + } else { + glUniform1iARB( g_shadowMapProgram.shadowMap, 5 ); + const int atlasWidth = preparedPass.resourceWidth; + const int atlasHeight = preparedPass.resourceHeight; + const float texelSize[2] = { + 1.0f / Max( 1, atlasWidth ), + 1.0f / Max( 1, atlasHeight ) + }; + glUniform2fvARB( g_shadowMapProgram.shadowTexelSize, 1, + texelSize ); + glUniform1fARB( g_shadowMapProgram.shadowBias, + pass.projected.constantBias ); + glUniform1fARB( g_shadowMapProgram.shadowNormalBias, + pass.projected.normalBias ); + glUniform1fvARB( g_shadowMapProgram.shadowTexelDepthBias, + SHADOWMAP_MAX_CASCADES, + pass.projected.state.texelDepthBias ); + float normalOffsets[SHADOWMAP_MAX_CASCADES] = {}; + for ( int i = 0; i < SHADOWMAP_MAX_CASCADES; ++i ) { + normalOffsets[i] = pass.projected.state.worldTexelSize[i] + * pass.projected.normalOffsetScale; + } + glUniform1fvARB( g_shadowMapProgram.shadowNormalOffsetWorld, + SHADOWMAP_MAX_CASCADES, normalOffsets ); + glUniform1fARB( g_shadowMapProgram.shadowReceiverPlaneBias, + pass.projected.receiverPlaneBias ? 1.0f : 0.0f ); + glUniform1fARB( g_shadowMapProgram.shadowFilterRadius, + pass.projected.filter.filterRadius ); + glUniform1fARB( g_shadowMapProgram.shadowFilterTaps, + static_cast( pass.projected.filter.filterTaps ) ); + glUniform1fARB( g_shadowMapProgram.shadowFilterMode, + static_cast( pass.projected.filter.filterMode ) ); + glUniform1fARB( g_shadowMapProgram.shadowPCSSLightRadius, + pass.projected.filter.pcssLightRadius ); + glUniform1fARB( g_shadowMapProgram.shadowPCSSMaxRadius, + pass.projected.filter.pcssMaxRadius ); + const float slotU0 = preparedPass.projectedOriginX + / static_cast( atlasWidth ); + const float slotV0 = preparedPass.projectedOriginY + / static_cast( atlasHeight ); + const float slotSpanU = preparedPass.projectedSpanPixels + / static_cast( atlasWidth ); + const float slotSpanV = preparedPass.projectedSpanPixels + / static_cast( atlasHeight ); + float composedRects[SHADOWMAP_MAX_CASCADES][4] = {}; + for ( int i = 0; i < SHADOWMAP_MAX_CASCADES; ++i ) { + const idVec4 &rect = pass.projected.state.atlasRect[i]; + composedRects[i][0] = slotU0 + rect.x * slotSpanU; + composedRects[i][1] = slotV0 + rect.y * slotSpanV; + composedRects[i][2] = slotU0 + rect.z * slotSpanU; + composedRects[i][3] = slotV0 + rect.w * slotSpanV; + } + glUniform4fvARB( g_shadowMapProgram.shadowAtlasRect, + SHADOWMAP_MAX_CASCADES, composedRects[0] ); + glUniform1fvARB( g_shadowMapProgram.shadowSplitDepths, + SHADOWMAP_MAX_CASCADES, pass.projected.state.splitDepths ); + glUniform1fvARB( g_shadowMapProgram.shadowCascadeBiasScale, + SHADOWMAP_MAX_CASCADES, pass.projected.state.biasScale ); + glUniform1iARB( g_shadowMapProgram.shadowCascadeCount, + pass.projected.state.cascadeCount ); + glUniform1fARB( g_shadowMapProgram.shadowCascadeBlend, + pass.projected.cascadeBlend ); + glUniform1fARB( g_shadowMapProgram.shadowDebugMode, 0.0f ); + glUniform1fARB( g_shadowMapProgram.shadowReceiverDebugReason, 0.0f ); + glUniform1fARB( g_shadowMapProgram.translucentShadowEnabled, + 0.0f ); + GL_SelectTextureNoClient( 5 ); + preparedPass.projectedDepthImage->Bind(); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, + pass.projected.depthCompare + ? GL_COMPARE_R_TO_TEXTURE : GL_NONE ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + pass.projected.depthCompare ? GL_LINEAR : GL_NEAREST ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, + pass.projected.depthCompare ? GL_LINEAR : GL_NEAREST ); + } + + for ( int unit = 6; unit <= 8 && unit < glConfig.maxTextureUnits; + ++unit ) { + GL_SelectTextureNoClient( unit ); + globalImages->BindNull(); + } + const float neutralCel[4] = { 0.0f, 1.0f, 0.0f, 0.0f }; + if ( pointLight ) { + glUniform1fARB( g_pointShadowMapProgram.materialEnhanced, 0.0f ); + glUniform1fARB( g_pointShadowMapProgram.materialNormalScale, 1.0f ); + glUniform1fARB( g_pointShadowMapProgram.materialSpecularBoost, 1.0f ); + glUniform1fARB( g_pointShadowMapProgram.materialFresnel, 0.0f ); + glUniform4fvARB( g_pointShadowMapProgram.celParams, 1, neutralCel ); + } else { + glUniform1fARB( g_shadowMapProgram.materialEnhanced, 0.0f ); + glUniform1fARB( g_shadowMapProgram.materialNormalScale, 1.0f ); + glUniform1fARB( g_shadowMapProgram.materialSpecularBoost, 1.0f ); + glUniform1fARB( g_shadowMapProgram.materialFresnel, 0.0f ); + glUniform4fvARB( g_shadowMapProgram.celParams, 1, neutralCel ); + } + glStencilMask( 255 ); + glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP ); + glEnableVertexAttribArrayARB( 8 ); + glEnableVertexAttribArrayARB( 9 ); + glEnableVertexAttribArrayARB( 10 ); + glEnableVertexAttribArrayARB( 11 ); + glEnableClientState( GL_COLOR_ARRAY ); +} + +static void RB_SharedWorldInteractionGLDrawMappedPrimitive( + const rbSharedWorldInteractionGLPreparedPrimitive_t &preparedPrimitive, + const classicInteractionDomainShadowMapPass_t &pass, + const classicInteractionDomainView_t &view, + const viewDef_t *viewDef ) { + const classicInteractionDomainPrimitive_t &primitive = + *preparedPrimitive.primitive; + const bool pointLight = pass.point.valid; + glLoadMatrixf( primitive.modelViewMatrix ); + if ( view.useScissor ) { + glScissor( viewDef->viewport.x1 + primitive.scissorX1, + viewDef->viewport.y1 + primitive.scissorY1, + primitive.scissorX2 + 1 - primitive.scissorX1, + primitive.scissorY2 + 1 - primitive.scissorY1 ); + } + GL_State( preparedPrimitive.stateBits ); + GL_Cull( preparedPrimitive.cullType ); + idVertexCache::BindArrayBuffer( preparedPrimitive.vertexBuffer ); + const void *ac = preparedPrimitive.vertexPointer; + glColorPointer( 4, GL_UNSIGNED_BYTE, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_COLOR_OFFSET ) ); + glVertexAttribPointerARB( 11, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_NORMAL_OFFSET ) ); + glVertexAttribPointerARB( 10, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_TANGENT1_OFFSET ) ); + glVertexAttribPointerARB( 9, 3, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_TANGENT0_OFFSET ) ); + glVertexAttribPointerARB( 8, 2, GL_FLOAT, false, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_ST_OFFSET ) ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ac, DRAWVERT_XYZ_OFFSET ) ); + +#define RB_SHARED_MAP_UNIFORM4( member, value ) \ + glUniform4fvARB( pointLight ? g_pointShadowMapProgram.member \ + : g_shadowMapProgram.member, 1, value ) + RB_SHARED_MAP_UNIFORM4( localLightOrigin, primitive.localLightOrigin ); + RB_SHARED_MAP_UNIFORM4( localViewOrigin, primitive.localViewOrigin ); + RB_SHARED_MAP_UNIFORM4( lightProjectionS, primitive.lightProjection[0] ); + RB_SHARED_MAP_UNIFORM4( lightProjectionT, primitive.lightProjection[1] ); + RB_SHARED_MAP_UNIFORM4( lightProjectionQ, primitive.lightProjection[2] ); + RB_SHARED_MAP_UNIFORM4( lightFalloffS, primitive.lightProjection[3] ); + RB_SHARED_MAP_UNIFORM4( bumpMatrixS, primitive.bumpMatrix[0] ); + RB_SHARED_MAP_UNIFORM4( bumpMatrixT, primitive.bumpMatrix[1] ); + RB_SHARED_MAP_UNIFORM4( diffuseMatrixS, primitive.diffuseMatrix[0] ); + RB_SHARED_MAP_UNIFORM4( diffuseMatrixT, primitive.diffuseMatrix[1] ); + RB_SHARED_MAP_UNIFORM4( specularMatrixS, primitive.specularMatrix[0] ); + RB_SHARED_MAP_UNIFORM4( specularMatrixT, primitive.specularMatrix[1] ); + RB_SHARED_MAP_UNIFORM4( diffuseColor, primitive.diffuseColor ); + RB_SHARED_MAP_UNIFORM4( specularColor, primitive.specularColor ); + RB_SHARED_MAP_UNIFORM4( flatDiffuseParams, primitive.flatDiffuseParams ); +#undef RB_SHARED_MAP_UNIFORM4 + float modulate = 0.0f; + float add = 1.0f; + if ( primitive.vertexColor == RENDERER_VERTEX_COLOR_MODULATE ) { + modulate = 1.0f; + add = 0.0f; + } else if ( primitive.vertexColor + == RENDERER_VERTEX_COLOR_INVERSE_MODULATE ) { + modulate = -1.0f; + } + const float vertexColorParams[2] = { modulate, add }; + glUniform2fvARB( pointLight + ? g_pointShadowMapProgram.vertexColorParams + : g_shadowMapProgram.vertexColorParams, 1, vertexColorParams ); + if ( pointLight ) { + float row0[4]; + float row1[4]; + float row2[4]; + RB_ShadowMapModelMatrixRows( primitive.modelMatrix, + row0, row1, row2 ); + glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow0, 1, row0 ); + glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow1, 1, row1 ); + glUniform4fvARB( g_pointShadowMapProgram.modelMatrixRow2, 1, row2 ); + } else { + float shadowRows[4][SHADOWMAP_MAX_CASCADES * 4] = {}; + for ( int cascade = 0; + cascade < pass.projected.state.cascadeCount; ++cascade ) { + for ( int plane = 0; plane < 4; ++plane ) { + idPlane localPlane; + R_GlobalPlaneToLocal( primitive.modelMatrix, + pass.projected.state.clipPlanes[cascade][plane], + localPlane ); + std::memcpy( &shadowRows[plane][cascade * 4], + localPlane.ToFloatPtr(), sizeof( float ) * 4 ); + } + } + for ( int row = 0; row < 4; ++row ) { + glUniform4fvARB( g_shadowMapProgram.shadowRow[row], + pass.projected.state.cascadeCount, shadowRows[row] ); + } + } + GL_SelectTextureNoClient( 0 ); + preparedPrimitive.bumpImage->Bind(); + GL_SelectTextureNoClient( 1 ); + preparedPrimitive.lightFalloffImage->Bind(); + GL_SelectTextureNoClient( 2 ); + preparedPrimitive.lightImage->Bind(); + GL_SelectTextureNoClient( 3 ); + preparedPrimitive.diffuseImage->Bind(); + GL_SelectTextureNoClient( 4 ); + preparedPrimitive.specularImage->Bind(); + if ( primitive.polygonOffsetEnabled ) { + glPolygonOffset( primitive.polygonOffsetFactor, + primitive.polygonOffsetUnits ); + glEnable( GL_POLYGON_OFFSET_FILL ); + } + idVertexCache::BindIndexBuffer( preparedPrimitive.indexBuffer ); + backEnd.pc.c_drawElements++; + backEnd.pc.c_drawIndexes += preparedPrimitive.indexCount; + backEnd.pc.c_drawVertexes += preparedPrimitive.vertexCount; + if ( preparedPrimitive.referencedIndexes ) { + backEnd.pc.c_drawRefIndexes += preparedPrimitive.indexCount; + } + if ( preparedPrimitive.referencedVertexes ) { + backEnd.pc.c_drawRefVertexes += preparedPrimitive.vertexCount; + } + if ( preparedPrimitive.indexCacheBacked ) { + backEnd.pc.c_vboIndexes += preparedPrimitive.indexCount; + } + glDrawElements( GL_TRIANGLES, preparedPrimitive.indexCount, + GL_INDEX_TYPE, preparedPrimitive.indexPointer ); + if ( primitive.polygonOffsetEnabled ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + } +} + +static int RB_SharedWorldInteractionGLCommitMappedPass( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + classicInteractionDomainReceiver_t receiver, + const classicInteractionDomainView_t &view, + const viewDef_t *viewDef ) { + const int mapPassIndex = light.shadowMapPass[receiver == + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT + ? CLASSIC_INTERACTION_RECEIVER_GLOBAL : receiver]; + const rbSharedWorldInteractionGLPreparedMapPass_t &preparedPass = + prepared.shadowMapPasses[mapPassIndex]; + RB_SharedWorldInteractionGLSelectMapPass( preparedPass ); + RB_SharedWorldInteractionGLBeginMappedReceiver( preparedPass ); + const int first = light.firstPrimitive[receiver]; + const int count = light.primitiveCount[receiver]; + for ( int i = 0; i < count; ++i ) { + RB_SharedWorldInteractionGLDrawMappedPrimitive( + prepared.primitives[first + i], *preparedPass.pass, + view, viewDef ); + } + glDisableVertexAttribArrayARB( 8 ); + glDisableVertexAttribArrayARB( 9 ); + glDisableVertexAttribArrayARB( 10 ); + glDisableVertexAttribArrayARB( 11 ); + glDisableClientState( GL_COLOR_ARRAY ); + glUseProgramObjectARB( 0 ); + // Exact shared-domain cache hits must count as live presentation just like + // the established ARB2 path. Refresh residency only after the mapped pass + // has committed so a later whole-view preflight rejection cannot pin an + // entry which was never consumed. + if ( preparedPass.pointEntry != NULL ) { + preparedPass.pointEntry->lastUsedFrame = tr.frameCount; + } else if ( preparedPass.projectedEntry != NULL ) { + preparedPass.projectedEntry->lastUsedFrame = tr.frameCount; + } + return count; +} + +static void RB_SharedWorldInteractionGLBeginShadowState( + const classicInteractionDomainView_t &view ) { + glDisableVertexAttribArrayARB( 8 ); + glDisableVertexAttribArrayARB( 9 ); + glDisableVertexAttribArrayARB( 10 ); + glDisableVertexAttribArrayARB( 11 ); + glDisableClientState( GL_COLOR_ARRAY ); + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + GL_SelectTextureNoClient( 6 ); + globalImages->BindNull(); + backEnd.glState.currenttmu = -1; + GL_SelectTexture( 0 ); + globalImages->BindNull(); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + GL_State( GLS_DEPTHMASK | GLS_COLORMASK | GLS_ALPHAMASK + | GLS_DEPTHFUNC_LESS ); + if ( view.shadowPolygonFactor != 0.0f + || view.shadowPolygonUnits != 0.0f ) { + glPolygonOffset( view.shadowPolygonFactor, view.shadowPolygonUnits ); + glEnable( GL_POLYGON_OFFSET_FILL ); + } + glStencilMask( 255 ); + glStencilFunc( GL_ALWAYS, 1, 255 ); + if ( view.useDepthBounds ) { + glEnable( GL_DEPTH_BOUNDS_TEST_EXT ); + } + if ( view.useShadowVertexProgram ) { + glBindProgramARB( GL_VERTEX_PROGRAM_ARB, VPROG_STENCIL_SHADOW ); + glEnable( GL_VERTEX_PROGRAM_ARB ); + } else { + glDisable( GL_VERTEX_PROGRAM_ARB ); + } +} + +static void RB_SharedWorldInteractionGLSubmitShadowElements( + const rbSharedWorldInteractionGLPreparedShadowCaster_t &preparedCaster ) { + backEnd.pc.c_shadowElements++; + backEnd.pc.c_shadowIndexes += preparedCaster.indexCount; + backEnd.pc.c_shadowVertexes += preparedCaster.vertexCount; + if ( preparedCaster.indexCacheBacked ) { + backEnd.pc.c_vboIndexes += preparedCaster.indexCount; + } + glDrawElements( GL_TRIANGLES, preparedCaster.indexCount, GL_INDEX_TYPE, + preparedCaster.indexPointer ); +} + +static void RB_SharedWorldInteractionGLDrawShadowCaster( + const rbSharedWorldInteractionGLPreparedShadowCaster_t &preparedCaster, + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const classicInteractionDomainView_t &view, + const viewDef_t *viewDef ) { + const classicInteractionDomainShadowCaster_t &caster = + *preparedCaster.caster; + backEnd.currentSpace = NULL; + glLoadMatrixf( caster.modelViewMatrix ); + if ( view.useScissor ) { + backEnd.currentScissor.x1 = caster.scissorX1; + backEnd.currentScissor.y1 = caster.scissorY1; + backEnd.currentScissor.x2 = caster.scissorX2; + backEnd.currentScissor.y2 = caster.scissorY2; + glScissor( viewDef->viewport.x1 + caster.scissorX1, + viewDef->viewport.y1 + caster.scissorY1, + caster.scissorX2 + 1 - caster.scissorX1, + caster.scissorY2 + 1 - caster.scissorY1 ); + } + if ( view.useDepthBounds ) { + glDepthBoundsEXT( caster.depthMin, caster.depthMax ); + } + if ( view.useShadowVertexProgram ) { + glProgramEnvParameter4fvARB( GL_VERTEX_PROGRAM_ARB, PP_LIGHT_ORIGIN, + caster.localLightOrigin ); + } + idVertexCache::BindArrayBuffer( preparedCaster.vertexBuffer ); + glVertexPointer( 4, GL_FLOAT, sizeof( shadowCache_t ), + preparedCaster.vertexPointer ); + idVertexCache::BindIndexBuffer( preparedCaster.indexBuffer ); + + if ( prepared.twoSidedStencil ) { + const GLenum frontSidedFace = viewDef->isMirror ? GL_FRONT : GL_BACK; + const GLenum backSidedFace = viewDef->isMirror ? GL_BACK : GL_FRONT; + GL_Cull( CT_TWO_SIDED ); + if ( caster.preload ) { + glStencilOpSeparate( frontSidedFace, GL_KEEP, + tr.stencilDecr, tr.stencilDecr ); + glStencilOpSeparate( backSidedFace, GL_KEEP, + tr.stencilIncr, tr.stencilIncr ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); + } + glStencilOpSeparate( frontSidedFace, GL_KEEP, GL_KEEP, + tr.stencilIncr ); + glStencilOpSeparate( backSidedFace, GL_KEEP, GL_KEEP, + tr.stencilDecr ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); + return; + } + + if ( caster.preload ) { + glStencilOp( GL_KEEP, tr.stencilDecr, tr.stencilDecr ); + GL_Cull( CT_FRONT_SIDED ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); + glStencilOp( GL_KEEP, tr.stencilIncr, tr.stencilIncr ); + GL_Cull( CT_BACK_SIDED ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); + } + glStencilOp( GL_KEEP, GL_KEEP, tr.stencilIncr ); + GL_Cull( CT_FRONT_SIDED ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); + glStencilOp( GL_KEEP, GL_KEEP, tr.stencilDecr ); + GL_Cull( CT_BACK_SIDED ); + RB_SharedWorldInteractionGLSubmitShadowElements( preparedCaster ); +} + +static void RB_SharedWorldInteractionGLEndShadowState( + const classicInteractionDomainView_t &view ) { + GL_Cull( CT_FRONT_SIDED ); + if ( view.shadowPolygonFactor != 0.0f + || view.shadowPolygonUnits != 0.0f ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + } + if ( view.useDepthBounds ) { + glDisable( GL_DEPTH_BOUNDS_TEST_EXT ); + } + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP ); +} + +static void RB_SharedWorldInteractionGLDrawShadowRange( + const rbSharedWorldInteractionGLPreparedView_t &prepared, + const rbSharedWorldInteractionGLPreparedLight_t &light, + int chain, const classicInteractionDomainView_t &view, + const viewDef_t *viewDef, int &logicalVolumeDraws, + int &preloadVolumeDraws ) { + const int count = light.shadowCasterCount[chain]; + if ( count <= 0 ) { + return; + } + RB_SharedWorldInteractionGLBeginShadowState( view ); + const int first = light.firstShadowCaster[chain]; + for ( int i = 0; i < count; ++i ) { + const rbSharedWorldInteractionGLPreparedShadowCaster_t &caster = + prepared.shadowCasters[first + i]; + RB_SharedWorldInteractionGLDrawShadowCaster( + caster, prepared, view, viewDef ); + logicalVolumeDraws++; + preloadVolumeDraws += caster.caster->preload ? 1 : 0; + } + RB_SharedWorldInteractionGLEndShadowState( view ); +} + +static void RB_SharedWorldInteractionGLRestoreState( void ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + if ( glConfig.depthBoundsTestAvailable ) { + glDisable( GL_DEPTH_BOUNDS_TEST_EXT ); + } + const int maxUnit = Min( 8, Min( glConfig.maxTextureUnits, + glConfig.maxTextureImageUnits ) - 1 ); + for ( int unit = maxUnit; unit >= 1; --unit ) { + GL_SelectTextureNoClient( unit ); + globalImages->BindNull(); + } + backEnd.glState.currenttmu = -1; + GL_SelectTexture( 0 ); + glDisable( GL_VERTEX_PROGRAM_ARB ); + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + glUseProgramObjectARB( 0 ); + glDisableVertexAttribArrayARB( 8 ); + glDisableVertexAttribArrayARB( 9 ); + glDisableVertexAttribArrayARB( 10 ); + glDisableVertexAttribArrayARB( 11 ); + glDisableClientState( GL_COLOR_ARRAY ); + glEnableClientState( GL_VERTEX_ARRAY ); + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + glStencilFunc( GL_ALWAYS, 128, 255 ); + glStencilMask( 255 ); + glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP ); + GL_Cull( CT_FRONT_SIDED ); + backEnd.depthFunc = GLS_DEPTHFUNC_EQUAL; + backEnd.currentSpace = NULL; +} + +static bool RB_ARB2_DrawSharedInteractionView( const viewDef_t *viewDef, + const classicInteractionDomainView_t &view ) { + rbSharedWorldInteractionGLPreparedView_t &prepared = + rbSharedWorldInteractionGLPreparedView; + if ( !prepared.ready || prepared.view != &view + || prepared.viewDef != viewDef || prepared.hash != view.hash ) { + return false; + } + + // Ownership commits here. Every operation which can reject the view has + // completed, so no code below this point returns to the classic walker. + prepared.committed = true; + if ( !g_firstARB2InteractionHandoffBreadcrumb ) { + g_firstARB2InteractionHandoffBreadcrumb = true; + R_RecordRendererStartupPhase( + RENDERER_STARTUP_PHASE_FIRST_ARB2_INTERACTION_HANDOFF ); + } + RB_LogComment( "---------- RB_ARB2_DrawSharedInteractionView ----------\n" ); + RB_ARB2_InvalidateEnvParamCache(); + GL_SelectTexture( 0 ); + glEnableClientState( GL_VERTEX_ARRAY ); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + glClearStencil( view.stencilReference ); + glStencilMask( 255 ); + glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP ); + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + + int drawnPrimitives = 0; + const int submittedShadowCasters = prepared.drawableShadowCasterCount; + int logicalVolumeDraws = 0; + int preloadVolumeDraws = 0; + for ( int lightIndex = 0; lightIndex < prepared.lightCount; ++lightIndex ) { + const rbSharedWorldInteractionGLPreparedLight_t &preparedLight = + prepared.lights[lightIndex]; + const classicInteractionDomainLight_t &light = *preparedLight.light; + backEnd.vLight = const_cast( light.legacyViewLight ); + classicInteractionDomainShadowMode_t preparedVolumeMode = + CLASSIC_INTERACTION_SHADOW_NONE; + bool preparedVolumeIncludesLocal = false; + const auto clearLightStencil = [&]() { + backEnd.currentScissor.x1 = light.scissorX1; + backEnd.currentScissor.y1 = light.scissorY1; + backEnd.currentScissor.x2 = light.scissorX2; + backEnd.currentScissor.y2 = light.scissorY2; + if ( view.useScissor ) { + glScissor( viewDef->viewport.x1 + light.scissorX1, + viewDef->viewport.y1 + light.scissorY1, + light.scissorX2 + 1 - light.scissorX1, + light.scissorY2 + 1 - light.scissorY1 ); + } + glClear( GL_STENCIL_BUFFER_BIT ); + }; + const auto drawOrdinaryReceiver = [&]( + classicInteractionDomainReceiver_t receiver ) { + if ( preparedLight.primitiveCount[receiver] <= 0 ) { + return; + } + RB_SharedWorldInteractionGLBeginInteractionState(); + drawnPrimitives += RB_SharedWorldInteractionGLDrawPrimitiveRange( + prepared, preparedLight, receiver, view, viewDef ); + }; + const auto mappedMode = []( classicInteractionDomainShadowMode_t mode ) { + return mode == CLASSIC_INTERACTION_SHADOW_PROJECTED + || mode == CLASSIC_INTERACTION_SHADOW_POINT + || mode == CLASSIC_INTERACTION_SHADOW_HYBRID; + }; + const auto prepareVolumeState = [&] ( + classicInteractionDomainShadowMode_t mode, + bool includeLocal ) { + const bool hybrid = mode == CLASSIC_INTERACTION_SHADOW_HYBRID; + if ( mode != CLASSIC_INTERACTION_SHADOW_STENCIL && !hybrid ) { + return; + } + if ( preparedVolumeMode != mode ) { + clearLightStencil(); + RB_SharedWorldInteractionGLDrawShadowRange( prepared, + preparedLight, hybrid + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL, + view, viewDef, logicalVolumeDraws, + preloadVolumeDraws ); + preparedVolumeMode = mode; + preparedVolumeIncludesLocal = false; + } + if ( includeLocal && !preparedVolumeIncludesLocal ) { + RB_SharedWorldInteractionGLDrawShadowRange( prepared, + preparedLight, hybrid + ? CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL + : CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL, + view, viewDef, logicalVolumeDraws, + preloadVolumeDraws ); + preparedVolumeIncludesLocal = true; + } + }; + + const classicInteractionDomainShadowMode_t localMode = + light.receiverShadowMode[CLASSIC_INTERACTION_RECEIVER_LOCAL]; + if ( localMode == CLASSIC_INTERACTION_SHADOW_STENCIL ) { + prepareVolumeState( localMode, false ); + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + drawOrdinaryReceiver( CLASSIC_INTERACTION_RECEIVER_LOCAL ); + } else if ( mappedMode( localMode ) ) { + if ( localMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + prepareVolumeState( localMode, false ); + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + } else { + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + } + drawnPrimitives += RB_SharedWorldInteractionGLCommitMappedPass( + prepared, preparedLight, + CLASSIC_INTERACTION_RECEIVER_LOCAL, view, viewDef ); + } else { + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + drawOrdinaryReceiver( CLASSIC_INTERACTION_RECEIVER_LOCAL ); + } + + const classicInteractionDomainShadowMode_t globalMode = + light.receiverShadowMode[CLASSIC_INTERACTION_RECEIVER_GLOBAL]; + if ( globalMode == CLASSIC_INTERACTION_SHADOW_STENCIL ) { + prepareVolumeState( globalMode, true ); + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + drawOrdinaryReceiver( CLASSIC_INTERACTION_RECEIVER_GLOBAL ); + } else if ( mappedMode( globalMode ) ) { + if ( globalMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + prepareVolumeState( globalMode, true ); + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + } else { + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + } + drawnPrimitives += RB_SharedWorldInteractionGLCommitMappedPass( + prepared, preparedLight, + CLASSIC_INTERACTION_RECEIVER_GLOBAL, view, viewDef ); + } else { + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + drawOrdinaryReceiver( CLASSIC_INTERACTION_RECEIVER_GLOBAL ); + } + + const classicInteractionDomainShadowMode_t translucentMode = + light.receiverShadowMode[ + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT]; + if ( mappedMode( translucentMode ) ) { + if ( translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID ) { + prepareVolumeState( translucentMode, true ); + } + glStencilFunc( translucentMode == CLASSIC_INTERACTION_SHADOW_HYBRID + ? GL_GEQUAL : GL_ALWAYS, + view.stencilReference, 255 ); + drawnPrimitives += RB_SharedWorldInteractionGLCommitMappedPass( + prepared, preparedLight, + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT, view, viewDef ); + } else if ( translucentMode == CLASSIC_INTERACTION_SHADOW_STENCIL ) { + prepareVolumeState( translucentMode, true ); + glStencilFunc( GL_GEQUAL, view.stencilReference, 255 ); + drawOrdinaryReceiver( + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ); + } else { + glStencilFunc( GL_ALWAYS, view.stencilReference, 255 ); + drawOrdinaryReceiver( + CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT ); + } + } + + RB_SharedWorldInteractionGLRestoreState(); + RB_ShadowMapStatsReport(); + RB_ShadowMapDebugOverlayDraw(); + const bool coverageRecorded = R_ClassicInteractionDomain_RecordOwned( + viewDef, CLASSIC_INTERACTION_BACKEND_GL, drawnPrimitives, + prepared.noopPrimitiveCount, submittedShadowCasters, + prepared.noopShadowCasterCount, logicalVolumeDraws, + preloadVolumeDraws, prepared.shadowMapPassCount, + prepared.hybridShadowPassCount ); + if ( !coverageRecorded ) { + // The framebuffer is already committed. Report the contract violation, + // but never render the classic stream too. + common->Warning( "RB_ARB2_DrawSharedInteractionView: GL ownership coverage rejected after committed draw (lights=%d surfaces=%d primitives=%d drawn=%d noop=%d shadowCasters=%d shadowNoop=%d volumes=%d preloads=%d hash=%llu)", + prepared.lightCount, prepared.surfaceCount, prepared.primitiveCount, + drawnPrimitives, prepared.noopPrimitiveCount, + submittedShadowCasters, prepared.noopShadowCasterCount, + logicalVolumeDraws, preloadVolumeDraws, + static_cast( prepared.hash ) ); + } + return true; +} + +void RB_ARB2_DrawInteractions( void ) { + viewLight_t *vLight; + + if ( r_rendererSharedWorldInteraction.GetBool() ) { + std::memset( &rbSharedWorldInteractionGLPreparedView, 0, + sizeof( rbSharedWorldInteractionGLPreparedView ) ); + const classicInteractionDomainView_t *sharedView = + R_ClassicInteractionDomain_FindView( backEnd.viewDef ); + if ( sharedView == NULL ) { + R_ClassicInteractionDomain_RecordBackendFallback( backEnd.viewDef, + CLASSIC_INTERACTION_BACKEND_GL, + CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_VIEW ) ); + } else if ( sharedView->backendOutcome[CLASSIC_INTERACTION_BACKEND_GL] + != CLASSIC_INTERACTION_BACKEND_FALLBACK ) { + if ( !sharedView->ready ) { + R_ClassicInteractionDomain_RecordBackendFallback( + backEnd.viewDef, CLASSIC_INTERACTION_BACKEND_GL, + sharedView->failure != CLASSIC_INTERACTION_FAILURE_NONE + ? sharedView->failure + : CLASSIC_INTERACTION_FAILURE_BACKEND_NOT_READY, + sharedView->failureDetail ); + } else { + int sharedFailureDetail = 0; + if ( RB_ARB2_SharedInteractionPreflight( backEnd.viewDef, + *sharedView, sharedFailureDetail ) ) { + if ( RB_ARB2_DrawSharedInteractionView( + backEnd.viewDef, *sharedView ) ) { + return; + } + // The draw function can reject only before its commit point. + sharedFailureDetail = + RB_SharedWorldInteractionGLFailureDetail( + RB_SHARED_WORLD_INTERACTION_GL_REJECT_COVERAGE ); + } + R_ClassicInteractionDomain_RecordBackendFallback( + backEnd.viewDef, CLASSIC_INTERACTION_BACKEND_GL, + CLASSIC_INTERACTION_FAILURE_BACKEND_REJECTED, + sharedFailureDetail ); + } + } + } + + if ( glConfig.disableARB2Interactions ) { + if ( r_interactionColorMode.IsModified() ) { r_interactionColorMode.ClearModified(); } RB_ShadowMapStatsReset(); @@ -11743,6 +16256,12 @@ void RB_ARB2_DrawInteractions( void ) { glStencilFunc( GL_ALWAYS, 128, 255 ); g_shadowMapGlobalPassMapped = SHADOWMAP_GLOBAL_MAPPED_NONE; + g_shadowMapGlobalPassHybrid = false; + g_shadowMapDeferredGlobalReportPending = false; + bool deferGlobalReceiverDraw = false; + const drawSurf_t *globalReceiverInteractions = + RB_ShadowMapGlobalReceiverPlanningChain( + vLight, deferGlobalReceiverDraw ); if ( classification.pointLight ) { // Point-light shadow maps use the same ownership split as the retail @@ -11752,14 +16271,22 @@ void RB_ARB2_DrawInteractions( void ) { // legacy stencil shadow chains resolve back to the same ambient // surfaces and can double-submit overlapping collision/helper meshes. RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_LOCAL, true, vLight->globalShadowMapCasters, NULL, vLight->globalShadowMapDynamicCasters, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); - RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_GLOBAL, true, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, vLight->globalInteractions ); + RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_GLOBAL, true, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, globalReceiverInteractions, deferGlobalReceiverDraw ); } else { RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_LOCAL, false, vLight->globalShadowMapCasters, NULL, vLight->globalShadowMapDynamicCasters, NULL, vLight->globalShadows, NULL, vLight->localInteractions ); - RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_GLOBAL, false, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, vLight->globalInteractions ); + RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_GLOBAL, false, vLight->globalShadowMapCasters, vLight->localShadowMapCasters, vLight->globalShadowMapDynamicCasters, vLight->localShadowMapDynamicCasters, vLight->globalShadows, vLight->localShadows, globalReceiverInteractions, deferGlobalReceiverDraw ); + } + if ( deferGlobalReceiverDraw + && vLight->globalInteractions != NULL ) { + // The translucent chain drove GLOBAL map preparation because the + // opaque chain has no map-capable receiver. Draw that opaque fallback + // subset now, before the ordered translucent phase below. + RB_ShadowMapDrawReceiverFallbacks( vLight, + SHADOWMAP_PASS_GLOBAL, vLight->globalShadows, + vLight->localShadows, vLight->globalInteractions ); } if ( !r_skipTranslucent.GetBool() ) { - glStencilFunc( GL_ALWAYS, 128, 255 ); backEnd.depthFunc = GLS_DEPTHFUNC_LESS; if ( r_shadowMapTranslucentReceivers.GetBool() && g_shadowMapGlobalPassMapped != SHADOWMAP_GLOBAL_MAPPED_NONE @@ -11769,17 +16296,62 @@ void RB_ARB2_DrawInteractions( void ) { // still current - translucent receivers sample the same map // the opaque receivers used. Surfaces the receiver path // cannot draw (custom GLSL, generated geometry, prepare - // failures) keep their unshadowed contribution below. - g_shadowMapStats.translucentReceiverPasses++; - if ( g_shadowMapGlobalPassMapped == SHADOWMAP_GLOBAL_MAPPED_POINT ) { - RB_GLSLPointShadowMap_CreateDrawInteractions( vLight->translucentInteractions ); + // failures) take the filtered stencil fallback below. + shadowMapTimedPhase_t translucentMaskTimer; + RB_ShadowMapBeginTimedPhase( translucentMaskTimer, vLight, + SHADOWMAP_PASS_GLOBAL, SHADOWMAP_TIMING_MASK_PASS ); + const bool receiverStencilReady = + RB_ShadowMapPrepareMappedReceiverStencil( vLight, + SHADOWMAP_PASS_GLOBAL, + g_shadowMapGlobalPassHybrid ); + const bool translucentMaskOk = receiverStencilReady + && ( g_shadowMapGlobalPassMapped == SHADOWMAP_GLOBAL_MAPPED_POINT + ? RB_GLSLPointShadowMap_CreateDrawInteractions( + vLight->translucentInteractions ) + : RB_GLSLShadowMap_CreateDrawInteractions( + vLight->translucentInteractions ) ); + g_shadowMapStats.cpuMaskMilliseconds += + RB_ShadowMapEndTimedPhase( translucentMaskTimer ); + if ( translucentMaskOk ) { + g_shadowMapStats.translucentReceiverPasses++; + RB_ShadowMapCompleteDeferredGlobalReceiver( vLight, + classification.pointLight, true ); + RB_ShadowMapTrackWrappedCustomGLSLReceivers( + vLight, SHADOWMAP_PASS_GLOBAL, + vLight->translucentInteractions ); + RB_ShadowMapDrawReceiverFallbacks( vLight, + SHADOWMAP_PASS_GLOBAL, vLight->globalShadows, + vLight->localShadows, + vLight->translucentInteractions ); + if ( g_shadowMapMaskPrepareFailures > 0 ) { + RB_ShadowMapStencilFallbackFiltered( + vLight->globalShadows, + vLight->localShadows, + vLight->translucentInteractions, + RB_SurfaceShadowMapReceiverPrepareFailed ); + } } else { - RB_GLSLShadowMap_CreateDrawInteractions( vLight->translucentInteractions ); - } - RB_DrawMaterialInteractionsFiltered( vLight->translucentInteractions, RB_SurfaceNeedsShadowMapReceiverFallback ); - if ( g_shadowMapMaskPrepareFailures > 0 ) { - RB_DrawMaterialInteractionsFiltered( vLight->translucentInteractions, RB_SurfaceShadowMapReceiverPrepareFailed ); + RB_ShadowMapCompleteDeferredGlobalReceiver( vLight, + classification.pointLight, false ); + RB_ShadowMapMarkStencilFallbackSticky( vLight ); + RB_ShadowMapStencilFallback( vLight, + SHADOWMAP_PASS_GLOBAL, vLight->globalShadows, + vLight->localShadows, + vLight->translucentInteractions ); } + } else if ( vLight->translucentInteractions != NULL + && r_shadows.GetBool() + && ( r_shadowMapTranslucentReceivers.GetBool() + || r_stencilTranslucentShadows.GetBool() ) + && ( vLight->globalShadows != NULL + || vLight->localShadows != NULL ) ) { + // A deferred GLOBAL map that could not be prepared must not + // degrade translucent receivers to an unshadowed draw. Restamp + // the complete ownership and draw it once at translucent depth. + RB_ShadowMapStencilFallback( vLight, + SHADOWMAP_PASS_GLOBAL, vLight->globalShadows, + vLight->localShadows, + vLight->translucentInteractions ); } else { RB_DrawMaterialInteractions( vLight->translucentInteractions ); } @@ -12337,8 +16909,10 @@ int R_FindARBProgram( GLenum target, const char *program ) { stripped.StripFileExtension(); - // see if it is already loaded - for ( i = 0 ; progs[i].name[0] ; i++ ) { + // see if it is already loaded. Bound the scan by MAX_GLPROGS as the sibling + // RB_FindARBProgramRecord does: without it, a full table reads progs[MAX_GLPROGS] + // out of bounds before the cap check below can fire. + for ( i = 0 ; i < MAX_GLPROGS && progs[i].name[0] ; i++ ) { if ( progs[i].target != target ) { continue; } diff --git a/src/renderer/draw_common.cpp b/src/renderer/draw_common.cpp index e3ae3b53..c5b5ffd8 100644 --- a/src/renderer/draw_common.cpp +++ b/src/renderer/draw_common.cpp @@ -28,11 +28,20 @@ If you have questions concerning this license or the applicable additional terms +#include + #include "tr_local.h" #include "CelShading.h" +#include "ClassicGuiDomain.h" +#include "ClassicCinematicPostDomain.h" +#include "ClassicSpecialFrameDomain.h" +#include "ClassicWorldAmbientDomain.h" +#include "ClassicFogBlendDomain.h" +#include "ModernClusteredLighting.h" #include "ModernGLExecutor.h" #include "ModernGLShaderLibrary.h" #include "RendererMetrics.h" +#include "ScenePackets.h" #ifndef GL_FRAMEBUFFER_SRGB #define GL_FRAMEBUFFER_SRGB 0x8DB9 @@ -170,6 +179,10 @@ static bool RB_DrawSurfIsDecalMaterialPass( const drawSurf_t *surf ) { } static bool RB_DrawSurfIsPreFogMaterialPass( const drawSurf_t *surf ) { + if ( R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( backEnd.viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, surf ) ) { + return false; + } const idMaterial *material = surf != NULL ? surf->material : NULL; if ( material == NULL ) { return false; @@ -181,6 +194,10 @@ static bool RB_DrawSurfIsPreFogMaterialPass( const drawSurf_t *surf ) { } static bool RB_DrawSurfIsPostFogMaterialPass( const drawSurf_t *surf ) { + if ( R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( backEnd.viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, surf ) ) { + return false; + } const idMaterial *material = surf != NULL ? surf->material : NULL; if ( material == NULL ) { return false; @@ -989,17 +1006,149 @@ static GLhandleARB rbSceneDepthAwarePresentFragmentShader = 0; static int rbSceneDepthAwarePresentGeneration = -1; static GLint rbSceneDepthAwarePresentSceneLocation = -1; static GLint rbSceneDepthAwarePresentDepthLocation = -1; +static GLint rbSceneDepthAwarePresentUVOffsetLocation = -1; +static GLhandleARB rbTemporalResolveProgram = 0; +static GLhandleARB rbTemporalResolveVertexShader = 0; +static GLhandleARB rbTemporalResolveFragmentShader = 0; +static int rbTemporalResolveGeneration = -1; +enum rbTemporalResolveUniformIndex_t { + RB_TEMPORAL_UNIFORM_SCENE = 0, + RB_TEMPORAL_UNIFORM_DEPTH, + RB_TEMPORAL_UNIFORM_HISTORY, + RB_TEMPORAL_UNIFORM_VELOCITY, + RB_TEMPORAL_UNIFORM_INV_SCENE_SIZE, + RB_TEMPORAL_UNIFORM_OUTPUT_SIZE, + RB_TEMPORAL_UNIFORM_CURRENT_RECONSTRUCT, + RB_TEMPORAL_UNIFORM_PREVIOUS_PROJECT, + RB_TEMPORAL_UNIFORM_DEPTH_PROJECTION, + RB_TEMPORAL_UNIFORM_CURRENT_VIEW_ORIGIN, + RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS0, + RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS1, + RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS2, + RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_ORIGIN, + RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS0, + RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS1, + RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS2, + RB_TEMPORAL_UNIFORM_CURRENT_JITTER, + RB_TEMPORAL_UNIFORM_PARAMS, + RB_TEMPORAL_UNIFORM_MOTION_PARAMS, + RB_TEMPORAL_UNIFORM_REACTIVE_REGION0, + RB_TEMPORAL_UNIFORM_REACTIVE_REGION1, + RB_TEMPORAL_UNIFORM_SCREEN_EFFECTS0, + RB_TEMPORAL_UNIFORM_SCREEN_EFFECTS1, + RB_TEMPORAL_UNIFORM_PRESERVE_FAR_DEPTH, + RB_TEMPORAL_UNIFORM_COUNT +}; +static GLint rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_COUNT]; +static idImage *rbBackendTemporalHistoryImages[2] = { NULL, NULL }; +static idRenderTexture *rbBackendTemporalHistoryTargets[2] = { NULL, NULL }; +static int rbBackendTemporalHistoryWidth = 0; +static int rbBackendTemporalHistoryHeight = 0; +static int rbBackendTemporalHistoryReadIndex = 0; +static int rbBackendTemporalHistoryFrame = -1; +static int rbBackendTemporalHistoryContextGeneration = -1; +static unsigned int rbBackendTemporalHistoryGeneration = 0; +static unsigned long long rbBackendTemporalHistoryViewIdentity = 0; +static bool rbBackendTemporalHistoryValid = false; +static int rbTemporalResolveHistoryWriteFrame = -1; +static idRenderTexture *rbTemporalResolveHistoryWriteTarget = NULL; +static bool rbTemporalResolveNeedsReprime = false; +static unsigned int rbTemporalResolveRejectedGeneration = 0; +static const int RB_TEMPORAL_DEPTH_STAMP_COUNT = 16; +struct rbTemporalDepthStamp_t { + idRenderTexture *target; + const idImage *depthImage; + int width; + int height; + int frameNumber; + int contextGeneration; + unsigned int historyGeneration; + bool valid; +}; +static rbTemporalDepthStamp_t rbTemporalDepthStamps[RB_TEMPORAL_DEPTH_STAMP_COUNT]; +static int rbTemporalDepthStampReplacement = 0; static const int RB_SCREEN_FRACTION_MIN = 10; static const int RB_SCREEN_FRACTION_NATIVE = 100; static const int RB_SCREEN_FRACTION_MAX = 200; static int rbLastReportedScreenFractionRequest = 0; static int rbLastReportedScreenFractionEffective = 0; +static int rbSceneScalePresentedFrame = -1; static float rbHDRAdaptedExposure = 1.0f; static float rbHDRLastAverageLuminance = 1.0f; static float rbHDRLastTargetExposure = 1.0f; static float rbHDRLastAdaptationTime = -1.0f; static bool rbHDRExposureInitialized = false; +static void RB_ClearTemporalDepthStamps( void ) { + memset( rbTemporalDepthStamps, 0, sizeof( rbTemporalDepthStamps ) ); + rbTemporalDepthStampReplacement = 0; +} + +void RB_InvalidateTemporalDepthStamp( idRenderTexture *target ) { + if ( target == NULL ) { + return; + } + for ( int i = 0; i < RB_TEMPORAL_DEPTH_STAMP_COUNT; i++ ) { + if ( rbTemporalDepthStamps[i].target == target ) { + rbTemporalDepthStamps[i].valid = false; + } + } +} + +void RB_StampTemporalDepthResolved( idRenderTexture *target, + int frameNumber, unsigned int historyGeneration ) { + if ( target == NULL || target->GetDepthImage() == NULL + || target->GetDeviceHandle() == 0 ) { + return; + } + + int stampIndex = -1; + for ( int i = 0; i < RB_TEMPORAL_DEPTH_STAMP_COUNT; i++ ) { + if ( rbTemporalDepthStamps[i].target == target ) { + stampIndex = i; + break; + } + if ( stampIndex < 0 && rbTemporalDepthStamps[i].target == NULL ) { + stampIndex = i; + } + } + if ( stampIndex < 0 ) { + stampIndex = rbTemporalDepthStampReplacement; + rbTemporalDepthStampReplacement = + ( rbTemporalDepthStampReplacement + 1 ) % RB_TEMPORAL_DEPTH_STAMP_COUNT; + } + + rbTemporalDepthStamp_t &stamp = rbTemporalDepthStamps[stampIndex]; + stamp.target = target; + stamp.depthImage = target->GetDepthImage(); + stamp.width = target->GetWidth(); + stamp.height = target->GetHeight(); + stamp.frameNumber = frameNumber; + stamp.contextGeneration = tr.glContextGeneration; + stamp.historyGeneration = historyGeneration; + stamp.valid = frameNumber == backEnd.frameCount; +} + +static bool RB_TemporalDepthStampIsCurrent( const idRenderTexture *target, + int frameNumber, unsigned int historyGeneration ) { + if ( target == NULL || frameNumber != backEnd.frameCount ) { + return false; + } + for ( int i = 0; i < RB_TEMPORAL_DEPTH_STAMP_COUNT; i++ ) { + const rbTemporalDepthStamp_t &stamp = rbTemporalDepthStamps[i]; + if ( stamp.target == target && stamp.valid + && stamp.depthImage == target->GetDepthImage() + && stamp.width == target->GetWidth() + && stamp.height == target->GetHeight() + && stamp.frameNumber == frameNumber + && stamp.contextGeneration == tr.glContextGeneration + && stamp.historyGeneration == historyGeneration ) { + return true; + } + } + return false; +} + // double-buffered pixel-pack buffers so the auto-exposure luminance sample can // be read back one frame late instead of draining the GPU pipeline every frame static GLuint rbHDRExposureReadbackPBOs[2] = { 0, 0 }; @@ -1008,22 +1157,49 @@ static int rbHDRExposureReadbackIndex = 0; struct rbSceneScaleState_t { bool active; + bool projectionRecentered; int requestedPercent; int effectivePercent; int nativeWidth; int nativeHeight; int scaledWidth; int scaledHeight; + float nativeProjectionOffsetX; + float nativeProjectionOffsetY; idScreenRect nativeViewport; idScreenRect nativeScissor; + typedef struct rectRestore_s { + idScreenRect *rect; + idScreenRect original; + } rectRestore_t; + idList rectRestores; + idHashIndex rectRestoreHash; }; static void RB_ClearSceneScaleState( rbSceneScaleState_t &state ) { - memset( &state, 0, sizeof( state ) ); + state.active = false; + state.projectionRecentered = false; + state.requestedPercent = 0; + state.effectivePercent = 0; + state.nativeWidth = 0; + state.nativeHeight = 0; + state.scaledWidth = 0; + state.scaledHeight = 0; + state.nativeProjectionOffsetX = 0.0f; + state.nativeProjectionOffsetY = 0.0f; + state.nativeViewport.Clear(); + state.nativeScissor.Clear(); + // Capacity-preserving reset: SetNum(0,false) keeps the list storage and the + // no-arg Clear() memsets the hash in place, so a per-view state that is now + // retained (see RB_STD_DrawView) no longer frees and reallocates every view. + state.rectRestores.SetNum( 0, false ); + state.rectRestores.SetGranularity( 256 ); + state.rectRestoreHash.Clear(); } static int RB_RequestedScreenFraction( void ) { - return idMath::ClampInt( RB_SCREEN_FRACTION_MIN, RB_SCREEN_FRACTION_MAX, r_screenFraction.GetInteger() ); + return idMath::ClampInt( RB_SCREEN_FRACTION_MIN, RB_SCREEN_FRACTION_MAX, + R_TemporalPresentation_EffectiveScreenFraction() ); } static bool RB_ViewCoversBackBuffer( const viewDef_t *viewDef ) { @@ -1090,30 +1266,50 @@ static int RB_ScaledDimension( int nativeDimension, int scalePercent ) { return Max( 1, static_cast( scaled / 100 ) ); } -static bool RB_SupersampledSceneTargetRequested( const viewDef_t *viewDef ) { - if ( RB_RequestedScreenFraction() <= RB_SCREEN_FRACTION_NATIVE ) { +static bool RB_ScaledSceneTargetRequested( const viewDef_t *viewDef ) { + const int requestedPercent = RB_RequestedScreenFraction(); + if ( requestedPercent == RB_SCREEN_FRACTION_NATIVE ) { return false; } if ( !RB_IsMainScenePostProcessView( viewDef ) || !RB_ViewCoversBackBuffer( viewDef ) ) { return false; } - return RB_EffectiveScreenFractionForView( viewDef ) > RB_SCREEN_FRACTION_NATIVE; + if ( requestedPercent < RB_SCREEN_FRACTION_NATIVE + && !R_TemporalPresentation_DynamicResolutionRequested() + && r_resolutionScaleMode.GetInteger() == 0 + && !R_TemporalPresentation_TemporalAARequested() + && !R_TemporalPresentation_ScreenSpaceEffectsRequested() ) { + return false; + } + return RB_EffectiveScreenFractionForView( viewDef ) != RB_SCREEN_FRACTION_NATIVE; } -static bool RB_ComputeSupersampledSceneSize( const viewDef_t *viewDef, int &targetWidth, int &targetHeight, int *effectivePercent = NULL ) { - if ( !RB_SupersampledSceneTargetRequested( viewDef ) ) { +static bool RB_ComputeScaledSceneSize( const viewDef_t *viewDef, int &targetWidth, + int &targetHeight, int *effectivePercent = NULL ) { + if ( !RB_ScaledSceneTargetRequested( viewDef ) ) { return false; } const int nativeWidth = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; const int nativeHeight = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; const int scalePercent = RB_EffectiveScreenFractionForView( viewDef ); - targetWidth = RB_ScaledDimension( nativeWidth, scalePercent ); - targetHeight = RB_ScaledDimension( nativeHeight, scalePercent ); + const temporalPresentationFrameState_t &temporalState = + R_TemporalPresentation_GetFrameState(); + if ( R_TemporalPresentation_DynamicResolutionRequested() + && nativeWidth == temporalState.nativeWidth + && nativeHeight == temporalState.nativeHeight + && temporalState.sceneWidth > 0 && temporalState.sceneHeight > 0 ) { + targetWidth = temporalState.sceneWidth; + targetHeight = temporalState.sceneHeight; + } else { + targetWidth = RB_ScaledDimension( nativeWidth, scalePercent ); + targetHeight = RB_ScaledDimension( nativeHeight, scalePercent ); + } if ( effectivePercent != NULL ) { *effectivePercent = scalePercent; } - return targetWidth > nativeWidth && targetHeight > nativeHeight; + return targetWidth > 0 && targetHeight > 0 + && ( targetWidth != nativeWidth || targetHeight != nativeHeight ); } static int RB_ScaleRectStart( int value, int sourceExtent, int targetExtent ) { @@ -1145,71 +1341,84 @@ static void RB_ScaleLocalScreenRect( idScreenRect &rect, int sourceWidth, int so rect.y2 = static_cast( y2 ); } -typedef struct rbScaledSurfSet_s { - idList surfs; - idHashIndex hash; -} rbScaledSurfSet_t; - -static bool RB_MarkDrawSurfScaled( rbScaledSurfSet_t &scaledSurfs, const drawSurf_t *surf ) { - if ( surf == NULL ) { +static bool RB_RecordScaledRect( rbSceneScaleState_t &state, idScreenRect *rect ) { + if ( rect == NULL ) { return false; } - const int key = static_cast( reinterpret_cast( surf ) >> 4 ); - for ( int i = scaledSurfs.hash.First( key ); i != -1; i = scaledSurfs.hash.Next( i ) ) { - if ( scaledSurfs.surfs[i] == surf ) { + const int key = static_cast( reinterpret_cast( rect ) >> 4 ); + for ( int i = state.rectRestoreHash.First( key ); i != -1; + i = state.rectRestoreHash.Next( i ) ) { + if ( state.rectRestores[i].rect == rect ) { return false; } } - scaledSurfs.hash.Add( key, scaledSurfs.surfs.Append( surf ) ); + rbSceneScaleState_t::rectRestore_t &restore = state.rectRestores.Alloc(); + restore.rect = rect; + restore.original = *rect; + state.rectRestoreHash.Add( key, state.rectRestores.Num() - 1 ); return true; } -static void RB_ScaleDrawSurfScissor( rbScaledSurfSet_t &scaledSurfs, const drawSurf_t *surf, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { - if ( !RB_MarkDrawSurfScaled( scaledSurfs, surf ) ) { +static void RB_ScaleTrackedRect( rbSceneScaleState_t &state, idScreenRect *rect, + int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { + if ( !RB_RecordScaledRect( state, rect ) ) { + return; + } + RB_ScaleLocalScreenRect( *rect, sourceWidth, sourceHeight, + targetWidth, targetHeight ); +} + +static void RB_ScaleDrawSurfScissor( rbSceneScaleState_t &state, + const drawSurf_t *surf, int sourceWidth, int sourceHeight, + int targetWidth, int targetHeight ) { + if ( surf == NULL ) { return; } - RB_ScaleLocalScreenRect( const_cast( surf )->scissorRect, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleTrackedRect( state, + &const_cast( surf )->scissorRect, + sourceWidth, sourceHeight, targetWidth, targetHeight ); } -static void RB_ScaleDrawSurfChainScissors( rbScaledSurfSet_t &scaledSurfs, const drawSurf_t *surf, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { - for ( const drawSurf_t *chainSurf = surf; chainSurf != NULL; chainSurf = chainSurf->nextOnLight ) { - RB_ScaleDrawSurfScissor( scaledSurfs, chainSurf, sourceWidth, sourceHeight, targetWidth, targetHeight ); +static void RB_ScaleDrawSurfChainScissors( rbSceneScaleState_t &state, + const drawSurf_t *surf, int sourceWidth, int sourceHeight, + int targetWidth, int targetHeight ) { + for ( const drawSurf_t *chainSurf = surf; chainSurf != NULL; + chainSurf = chainSurf->nextOnLight ) { + RB_ScaleDrawSurfScissor( state, chainSurf, sourceWidth, sourceHeight, + targetWidth, targetHeight ); } } -static void RB_ScaleLightDrawSurfScissors( rbScaledSurfSet_t &scaledSurfs, const viewLight_t *vLight, int sourceWidth, int sourceHeight, int targetWidth, int targetHeight ) { +static void RB_ScaleLightDrawSurfScissors( rbSceneScaleState_t &state, + const viewLight_t *vLight, int sourceWidth, int sourceHeight, + int targetWidth, int targetHeight ) { if ( vLight == NULL ) { return; } - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->globalShadows, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->localInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->localShadows, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->globalInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->localShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->globalShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->localTranslucentShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->globalTranslucentShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); - RB_ScaleDrawSurfChainScissors( scaledSurfs, vLight->translucentInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->globalShadows, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->localInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->localShadows, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->globalInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->localShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->globalShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->localTranslucentShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->globalTranslucentShadowMapCasters, sourceWidth, sourceHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfChainScissors( state, vLight->translucentInteractions, sourceWidth, sourceHeight, targetWidth, targetHeight ); } -static void RB_BeginSceneSupersampling( rbSceneScaleState_t &state, const viewDef_t *sceneTargetView ) { +static void RB_BeginSceneScalingToExtent( rbSceneScaleState_t &state, + int targetWidth, int targetHeight, int effectivePercent ) { RB_ClearSceneScaleState( state ); - if ( backEnd.viewDef == NULL || sceneTargetView == NULL ) { + if ( backEnd.viewDef == NULL ) { return; } viewDef_t *viewDef = const_cast( backEnd.viewDef ); - int targetWidth = 0; - int targetHeight = 0; - int effectivePercent = RB_SCREEN_FRACTION_NATIVE; - if ( !RB_ComputeSupersampledSceneSize( sceneTargetView, targetWidth, targetHeight, &effectivePercent ) ) { - return; - } - const int nativeWidth = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; const int nativeHeight = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; - if ( nativeWidth <= 0 || nativeHeight <= 0 || targetWidth <= nativeWidth || targetHeight <= nativeHeight ) { + if ( nativeWidth <= 0 || nativeHeight <= 0 || targetWidth <= 0 || targetHeight <= 0 + || ( targetWidth == nativeWidth && targetHeight == nativeHeight ) ) { return; } @@ -1229,22 +1438,70 @@ static void RB_BeginSceneSupersampling( rbSceneScaleState_t &state, const viewDe viewDef->viewport.x2 = static_cast( targetWidth - 1 ); viewDef->viewport.y2 = static_cast( targetHeight - 1 ); - rbScaledSurfSet_t scaledSurfs; - scaledSurfs.surfs.SetGranularity( 256 ); - scaledSurfs.hash.Clear( 4096, 4096 ); for ( int i = 0; i < viewDef->numDrawSurfs; i++ ) { - RB_ScaleDrawSurfScissor( scaledSurfs, viewDef->drawSurfs[i], nativeWidth, nativeHeight, targetWidth, targetHeight ); + RB_ScaleDrawSurfScissor( state, viewDef->drawSurfs[i], nativeWidth, + nativeHeight, targetWidth, targetHeight ); } for ( viewLight_t *vLight = viewDef->viewLights; vLight != NULL; vLight = vLight->next ) { - RB_ScaleLocalScreenRect( vLight->scissorRect, nativeWidth, nativeHeight, targetWidth, targetHeight ); - RB_ScaleLightDrawSurfScissors( scaledSurfs, vLight, nativeWidth, nativeHeight, targetWidth, targetHeight ); + RB_ScaleTrackedRect( state, &vLight->scissorRect, nativeWidth, + nativeHeight, targetWidth, targetHeight ); + RB_ScaleLightDrawSurfScissors( state, vLight, nativeWidth, + nativeHeight, targetWidth, targetHeight ); } for ( viewEntity_t *vEntity = viewDef->viewEntitys; vEntity != NULL; vEntity = vEntity->next ) { - RB_ScaleLocalScreenRect( vEntity->scissorRect, nativeWidth, nativeHeight, targetWidth, targetHeight ); + RB_ScaleTrackedRect( state, &vEntity->scissorRect, nativeWidth, + nativeHeight, targetWidth, targetHeight ); + } +} + +static void RB_BeginSceneScaling( rbSceneScaleState_t &state, + const viewDef_t *sceneTargetView ) { + int targetWidth = 0; + int targetHeight = 0; + int effectivePercent = RB_SCREEN_FRACTION_NATIVE; + if ( !RB_ComputeScaledSceneSize( sceneTargetView, targetWidth, targetHeight, + &effectivePercent ) ) { + RB_ClearSceneScaleState( state ); + return; + } + RB_BeginSceneScalingToExtent( state, targetWidth, targetHeight, + effectivePercent ); +} + +static bool RB_FeedbackSceneTargetScalingExtent( const viewDef_t *viewDef, + int &targetWidth, int &targetHeight, int &effectivePercent ) { + if ( viewDef == NULL || viewDef->viewEntitys == NULL + || backEnd.renderTexture == NULL + || backEnd.renderTexture != backEnd.feedbackRenderTexture ) { + return false; } + const bool portalSky = ( viewDef->renderFlags & RF_PORTAL_SKY ) != 0; + if ( !portalSky && ( viewDef->isSubview || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->renderView.viewID < 0 + || viewDef->isXraySubview || viewDef->isEditor ) ) { + return false; + } + + const temporalPresentationFrameState_t &presentation = + R_TemporalPresentation_GetFrameState(); + const int nativeWidth = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; + const int nativeHeight = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; + if ( nativeWidth != presentation.nativeWidth + || nativeHeight != presentation.nativeHeight + || presentation.sceneWidth <= 0 || presentation.sceneHeight <= 0 + || ( presentation.sceneWidth == nativeWidth + && presentation.sceneHeight == nativeHeight ) + || backEnd.renderTexture->GetWidth() != presentation.sceneWidth + || backEnd.renderTexture->GetHeight() != presentation.sceneHeight ) { + return false; + } + targetWidth = presentation.sceneWidth; + targetHeight = presentation.sceneHeight; + effectivePercent = presentation.effectiveScalePercent; + return true; } -static void RB_RestoreSceneSupersampling( const rbSceneScaleState_t &state ) { +static void RB_RestoreSceneScaling( const rbSceneScaleState_t &state ) { if ( !state.active || backEnd.viewDef == NULL ) { return; } @@ -1252,6 +1509,53 @@ static void RB_RestoreSceneSupersampling( const rbSceneScaleState_t &state ) { viewDef_t *viewDef = const_cast( backEnd.viewDef ); viewDef->viewport = state.nativeViewport; viewDef->scissor = state.nativeScissor; + for ( int i = state.rectRestores.Num() - 1; i >= 0; i-- ) { + if ( state.rectRestores[i].rect != NULL ) { + *state.rectRestores[i].rect = state.rectRestores[i].original; + } + } +} + +static void RB_RecenterDirectTemporalProjection( rbSceneScaleState_t &state, + const viewDef_t *viewDef ) { + if ( viewDef == NULL || !viewDef->temporalJitterEnabled + || state.projectionRecentered ) { + return; + } + const int width = viewDef->viewport.x2 - viewDef->viewport.x1 + 1; + const int height = viewDef->viewport.y2 - viewDef->viewport.y1 + 1; + if ( width <= 0 || height <= 0 ) { + return; + } + + viewDef_t *mutableView = const_cast( viewDef ); + state.nativeProjectionOffsetX = mutableView->projectionMatrix[8]; + state.nativeProjectionOffsetY = mutableView->projectionMatrix[9]; + mutableView->projectionMatrix[8] -= 2.0f + * mutableView->temporalJitterPixels.x / static_cast( width ); + mutableView->projectionMatrix[9] -= 2.0f + * mutableView->temporalJitterPixels.y / static_cast( height ); + state.projectionRecentered = true; + + // No offscreen consumer exists for this direct view. Retire shared history + // so the next successful scene-target frame seeds instead of blending across + // a spatial-only gap. Late capture already advances the generation itself. + if ( viewDef->temporalHistoryGeneration + == R_TemporalPresentation_HistoryGeneration() ) { + R_TemporalPresentation_InvalidateHistory( + "OpenGL temporal scene target unavailable" ); + } +} + +static void RB_RestoreDirectTemporalProjection( rbSceneScaleState_t &state, + const viewDef_t *viewDef ) { + if ( !state.projectionRecentered || viewDef == NULL ) { + return; + } + viewDef_t *mutableView = const_cast( viewDef ); + mutableView->projectionMatrix[8] = state.nativeProjectionOffsetX; + mutableView->projectionMatrix[9] = state.nativeProjectionOffsetY; + state.projectionRecentered = false; } static bool RB_PostProcessBloomRequested( void ) { @@ -1310,15 +1614,20 @@ static bool RB_HDRAutoExposureEnabled( void ) { } static bool RB_ViewRequestsSceneRenderTarget( const viewDef_t *viewDef ) { - if ( r_skipPostProcess.GetBool() ) { - return false; - } if ( !RB_IsMainScenePostProcessView( viewDef ) ) { return false; } - const bool supersampledSceneRequested = RB_SupersampledSceneTargetRequested( viewDef ); - if ( !glConfig.GLSLProgramAvailable && !supersampledSceneRequested ) { + const bool scaledSceneRequested = RB_ScaledSceneTargetRequested( viewDef ); + const bool temporalRequested = R_TemporalPresentation_TemporalAARequested(); + const bool screenSpaceRequested = + R_TemporalPresentation_ScreenSpaceEffectsRequested(); + if ( r_skipPostProcess.GetBool() && !scaledSceneRequested + && !temporalRequested && !screenSpaceRequested ) { + return false; + } + if ( !glConfig.GLSLProgramAvailable && !scaledSceneRequested + && !temporalRequested && !screenSpaceRequested ) { return false; } @@ -1340,7 +1649,9 @@ static bool RB_ViewRequestsSceneRenderTarget( const viewDef_t *viewDef ) { || autoExposureRequested || hdrDebugRequested || modernVisibleSceneTargetRequested - || supersampledSceneRequested; + || temporalRequested + || screenSpaceRequested + || scaledSceneRequested; } static bool RB_IsInlineSubviewOfScenePostProcessView( const viewDef_t *viewDef ) { @@ -1607,15 +1918,20 @@ static bool RB_EnsureSceneRenderTexture( const viewDef_t *sceneTargetView ) { return false; } - int supersampledWidth = 0; - int supersampledHeight = 0; - const bool supersampledScene = RB_ComputeSupersampledSceneSize( sceneTargetView, supersampledWidth, supersampledHeight ); - const int targetWidth = supersampledScene ? supersampledWidth : Max( glConfig.vidWidth, sceneTargetView->viewport.x2 + 1 ); - const int targetHeight = supersampledScene ? supersampledHeight : Max( glConfig.vidHeight, sceneTargetView->viewport.y2 + 1 ); + int scaledWidth = 0; + int scaledHeight = 0; + const bool scaledScene = RB_ComputeScaledSceneSize( sceneTargetView, + scaledWidth, scaledHeight ); + const int targetWidth = scaledScene ? scaledWidth : Max( glConfig.vidWidth, sceneTargetView->viewport.x2 + 1 ); + const int targetHeight = scaledScene ? scaledHeight : Max( glConfig.vidHeight, sceneTargetView->viewport.y2 + 1 ); const int requestedSamples = Max( 0, r_multiSamples.GetInteger() ); - // Supersampling is already an antialiasing resolve. Keep the oversized - // scene target single-sample instead of stacking an MSAA FP16 FBO on top of it. - const int sceneSamples = ( !supersampledScene && requestedSamples > 1 && !R_ModernGLExecutor_ModernVisibleRequestedForPost() ) ? requestedSamples : 0; + // Any non-native scene target already requires a resolve. Keep it + // single-sample instead of stacking an MSAA FP16 FBO on top of the scale + // transition; temporal AA owns antialiasing when enabled. + const int sceneSamples = ( !scaledScene && requestedSamples > 1 + && !R_TemporalPresentation_TemporalAARequested() + && !R_TemporalPresentation_ScreenSpaceEffectsRequested() + && !R_ModernGLExecutor_ModernVisibleRequestedForPost() ) ? requestedSamples : 0; if ( targetWidth <= 0 || targetHeight <= 0 ) { return false; @@ -1652,6 +1968,8 @@ static bool RB_EnsureSceneRenderTexture( const viewDef_t *sceneTargetView ) { ( rbSceneRenderTextureSamples != sceneSamples ); if ( recreateRenderTexture ) { + rbBackendTemporalHistoryValid = false; + rbBackendTemporalHistoryFrame = -1; if ( rbSceneRenderTexture != NULL ) { tr.DestroyRenderTexture( rbSceneRenderTexture ); rbSceneRenderTexture = NULL; @@ -2609,6 +2927,14 @@ static rbMotionBlurViewState_t rbMotionBlurHistory; static bool rbMotionBlurHistoryValid = false; static idList rbMotionBlurEntityHistory; static idList rbMotionBlurNextEntityHistory; +// TAA owns a transform timeline independent of motion blur. Motion blur may be +// disabled or may reset its camera history while temporal reconstruction still +// needs rigid-object vectors on the following frame. +static idList rbTemporalEntityHistory; +static idList rbTemporalNextEntityHistory; +static unsigned int rbTemporalEntityHistoryGeneration = 0; +static unsigned long long rbTemporalEntityHistoryViewIdentity = 0; +static int rbTemporalEntityHistoryFrame = -1; static idImage *rbMotionVectorImage = NULL; static idRenderTexture *rbMotionVectorRenderTexture = NULL; static bool rbMotionVectorImageValid = false; @@ -2809,38 +3135,14 @@ static void RB_BindPostProcessRenderTexture( idRenderTexture *renderTexture, int static void RB_RestorePostProcessTarget( idRenderTexture *renderTexture, int viewportWidth, int viewportHeight ); static bool RB_MotionVectorSurfaceEligible( const drawSurf_t *surf ) { - if ( surf == NULL || surf->geo == NULL || surf->material == NULL || surf->space == NULL || surf->space->entityDef == NULL ) { - return false; - } - - const srfTriangles_t *tri = surf->geo; - const idMaterial *shader = surf->material; - const idRenderEntityLocal *entity = surf->space->entityDef; - if ( !shader->IsDrawn() || !shader->HasAmbient() || shader->IsPortalSky() || shader->SuppressInSubview() ) { - return false; - } - if ( shader->Coverage() != MC_OPAQUE || shader->GetSort() >= SS_POST_PROCESS ) { - return false; - } - if ( tri->numIndexes <= 0 || R_TriHasPrimBatchMesh( tri ) || tri->deformedSurface ) { - return false; - } - if ( surf->space->weaponDepthHack || surf->space->modelDepthHack != 0.0f ) { - return false; - } - if ( entity->index < 0 || entity->parms.callback != NULL || entity->parms.hModel == NULL ) { - return false; - } - if ( entity->parms.hModel->IsDynamicModel() != DM_STATIC || entity->dynamicModel != NULL ) { - return false; - } - return true; + return R_ScenePackets_TemporalRigidMotionEligible( surf ); } -static bool RB_FindMotionBlurEntityHistory( int entityIndex, float previousModelMatrix[16] ) { - for ( int i = 0; i < rbMotionBlurEntityHistory.Num(); i++ ) { - if ( rbMotionBlurEntityHistory[i].entityIndex == entityIndex ) { - memcpy( previousModelMatrix, rbMotionBlurEntityHistory[i].modelMatrix, sizeof( rbMotionBlurEntityHistory[i].modelMatrix ) ); +static bool RB_FindMotionBlurEntityHistory( const idList &history, + int entityIndex, float previousModelMatrix[16] ) { + for ( int i = 0; i < history.Num(); i++ ) { + if ( history[i].entityIndex == entityIndex ) { + memcpy( previousModelMatrix, history[i].modelMatrix, sizeof( history[i].modelMatrix ) ); return true; } } @@ -2904,7 +3206,9 @@ static bool RB_EnsureMotionVectorRenderTexture( int viewportWidth, int viewportH } static const rbMotionBlurViewState_t *rbMotionVectorPreviousState = NULL; +static const idList *rbMotionVectorEntityHistory = NULL; static bool rbMotionVectorDrewSurface = false; +static bool rbMotionVectorMissedSurface = false; static void RB_T_RenderMotionVectorSurface( const drawSurf_t *surf ) { if ( !RB_MotionVectorSurfaceEligible( surf ) || rbMotionVectorPreviousState == NULL ) { @@ -2912,12 +3216,15 @@ static void RB_T_RenderMotionVectorSurface( const drawSurf_t *surf ) { } float previousModelMatrix[16]; - if ( !RB_FindMotionBlurEntityHistory( surf->space->entityDef->index, previousModelMatrix ) ) { + if ( rbMotionVectorEntityHistory == NULL || !RB_FindMotionBlurEntityHistory( + *rbMotionVectorEntityHistory, surf->space->entityDef->index, previousModelMatrix ) ) { + rbMotionVectorMissedSurface = true; return; } const srfTriangles_t *tri = surf->geo; if ( !RB_EnsurePackedClassicDrawCaches( surf, false, true ) || tri->ambientCache == NULL ) { + rbMotionVectorMissedSurface = true; return; } @@ -2939,11 +3246,17 @@ static void RB_T_RenderMotionVectorSurface( const drawSurf_t *surf ) { rbMotionVectorDrewSurface = true; } -static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurfs, const rbMotionBlurViewState_t &previousState, int viewportWidth, int viewportHeight ) { - if ( !RB_MotionBlurObjectVectorsRequested() ) { +static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurfs, + const rbMotionBlurViewState_t &previousState, int viewportWidth, int viewportHeight, + idImage *depthImage, const idList &entityHistory, + bool temporalRequested, bool *allEligibleSurfacesDrawn = NULL ) { + if ( allEligibleSurfacesDrawn != NULL ) { + *allEligibleSurfacesDrawn = false; + } + if ( !temporalRequested && !RB_MotionBlurObjectVectorsRequested() ) { return false; } - if ( globalImages->currentDepthImage == NULL ) { + if ( depthImage == NULL ) { return false; } @@ -2959,7 +3272,9 @@ static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurf idRenderTexture *previousRenderTexture = backEnd.renderTexture; rbMotionVectorImageValid = false; rbMotionVectorDrewSurface = false; + rbMotionVectorMissedSurface = false; rbMotionVectorPreviousState = &previousState; + rbMotionVectorEntityHistory = &entityHistory; RB_BindPostProcessRenderTexture( rbMotionVectorRenderTexture, viewportWidth, viewportHeight ); GL_State( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO | GLS_DEPTHMASK | GLS_DEPTHFUNC_ALWAYS ); @@ -2973,7 +3288,7 @@ static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurf glDisable( GL_DEPTH_TEST ); glDisable( GL_STENCIL_TEST ); GL_SelectTexture( 0 ); - globalImages->currentDepthImage->Bind(); + depthImage->Bind(); glUseProgramObjectARB( (GLhandleARB)rbMotionVectorStage.glslProgramObject ); for ( int i = 0; i < rbMotionVectorStage.numShaderTextures; i++ ) { @@ -3008,11 +3323,20 @@ static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurf } if ( r_useScissor.GetBool() && !backEnd.currentScissor.Equals( surf->scissorRect ) ) { backEnd.currentScissor = surf->scissorRect; + const int nativeWidth = Max( 1, backEnd.viewDef->viewport.x2 - backEnd.viewDef->viewport.x1 + 1 ); + const int nativeHeight = Max( 1, backEnd.viewDef->viewport.y2 - backEnd.viewDef->viewport.y1 + 1 ); + const float scaleX = static_cast( viewportWidth ) / static_cast( nativeWidth ); + const float scaleY = static_cast( viewportHeight ) / static_cast( nativeHeight ); + const int scissorX = idMath::ClampInt( 0, viewportWidth - 1, + idMath::Ftoi( static_cast( backEnd.currentScissor.x1 ) * scaleX ) ); + const int scissorY = idMath::ClampInt( 0, viewportHeight - 1, + idMath::Ftoi( static_cast( backEnd.currentScissor.y1 ) * scaleY ) ); + const int scissorX2 = idMath::ClampInt( scissorX + 1, viewportWidth, + idMath::Ceil( static_cast( backEnd.currentScissor.x2 + 1 ) * scaleX ) ); + const int scissorY2 = idMath::ClampInt( scissorY + 1, viewportHeight, + idMath::Ceil( static_cast( backEnd.currentScissor.y2 + 1 ) * scaleY ) ); glScissor( - backEnd.currentScissor.x1, - backEnd.currentScissor.y1, - backEnd.currentScissor.x2 + 1 - backEnd.currentScissor.x1, - backEnd.currentScissor.y2 + 1 - backEnd.currentScissor.y1 ); + scissorX, scissorY, scissorX2 - scissorX, scissorY2 - scissorY ); } RB_T_RenderMotionVectorSurface( surf ); } @@ -3023,6 +3347,7 @@ static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurf globalImages->BindNull(); glEnableClientState( GL_TEXTURE_COORD_ARRAY ); rbMotionVectorPreviousState = NULL; + rbMotionVectorEntityHistory = NULL; RB_RestorePostProcessTarget( previousRenderTexture, viewportWidth, viewportHeight ); glMatrixMode( GL_PROJECTION ); @@ -3030,6 +3355,9 @@ static bool RB_RenderMotionVectorBuffer( drawSurf_t **drawSurfs, int numDrawSurf glMatrixMode( GL_MODELVIEW ); rbMotionVectorImageValid = rbMotionVectorDrewSurface; + if ( allEligibleSurfacesDrawn != NULL ) { + *allEligibleSurfacesDrawn = !rbMotionVectorMissedSurface; + } return rbMotionVectorImageValid; } @@ -3106,7 +3434,8 @@ static void RB_STD_MotionBlur( void ) { } rbMotionVectorImageValid = false; if ( objectVectorsRequested ) { - RB_RenderMotionVectorBuffer( drawSurfs, numDrawSurfs, previousState, viewportWidth, viewportHeight ); + RB_RenderMotionVectorBuffer( drawSurfs, numDrawSurfs, previousState, + viewportWidth, viewportHeight, depthImage, rbMotionBlurEntityHistory, false ); } RB_UpdateMotionBlurEntityHistory( drawSurfs, numDrawSurfs ); @@ -3946,6 +4275,381 @@ static void RB_STD_Bloom( void ) { } } +static void RB_FreeTemporalResolveProgram( void ) { + const bool currentContext = glConfig.isInitialized + && rbTemporalResolveGeneration == tr.glContextGeneration; + if ( rbTemporalResolveProgram != 0 && currentContext ) { + if ( rbTemporalResolveVertexShader != 0 ) { + glDetachObjectARB( rbTemporalResolveProgram, rbTemporalResolveVertexShader ); + } + if ( rbTemporalResolveFragmentShader != 0 ) { + glDetachObjectARB( rbTemporalResolveProgram, rbTemporalResolveFragmentShader ); + } + glDeleteObjectARB( rbTemporalResolveProgram ); + } + if ( rbTemporalResolveVertexShader != 0 && currentContext ) { + glDeleteObjectARB( rbTemporalResolveVertexShader ); + } + if ( rbTemporalResolveFragmentShader != 0 && currentContext ) { + glDeleteObjectARB( rbTemporalResolveFragmentShader ); + } + rbTemporalResolveProgram = 0; + rbTemporalResolveVertexShader = 0; + rbTemporalResolveFragmentShader = 0; + rbTemporalResolveGeneration = -1; + for ( int i = 0; i < RB_TEMPORAL_UNIFORM_COUNT; i++ ) { + rbTemporalResolveUniforms[i] = -1; + } +} + +static bool RB_EnsureTemporalResolveProgram( void ) { + if ( !glConfig.GLSLProgramAvailable ) { + return false; + } + if ( rbTemporalResolveProgram != 0 + && rbTemporalResolveGeneration == tr.glContextGeneration ) { + return true; + } + + RB_FreeTemporalResolveProgram(); + + static const char *vertexSource = + "void main() {\n" + "\tgl_Position = ftransform();\n" + "\tgl_TexCoord[0] = gl_MultiTexCoord0;\n" + "}\n"; + static const char *fragmentSource = + "uniform sampler2D Scene;\n" + "uniform sampler2D DepthBuffer;\n" + "uniform sampler2D History;\n" + "uniform sampler2D VelocityBuffer;\n" + "uniform vec2 InvSceneSize;\n" + "uniform vec2 OutputSize;\n" + "uniform vec4 CurrentReconstructInfo;\n" + "uniform vec4 PreviousProjectInfo;\n" + "uniform vec2 DepthProjection;\n" + "uniform vec4 CurrentViewOrigin;\n" + "uniform vec4 CurrentViewAxis0;\n" + "uniform vec4 CurrentViewAxis1;\n" + "uniform vec4 CurrentViewAxis2;\n" + "uniform vec4 PreviousViewOrigin;\n" + "uniform vec4 PreviousViewAxis0;\n" + "uniform vec4 PreviousViewAxis1;\n" + "uniform vec4 PreviousViewAxis2;\n" + "uniform vec2 CurrentJitter;\n" + "uniform vec4 TemporalParams;\n" + "uniform vec4 MotionParams;\n" + "uniform vec4 ReactiveRegion0;\n" + "uniform vec4 ReactiveRegion1;\n" + "uniform vec4 ScreenEffects0;\n" + "uniform vec4 ScreenEffects1;\n" + "uniform float PreserveFarDepth;\n" + "float ViewZFromDepth( float depth ) {\n" + "\tfloat denom = depth * 2.0 - 1.0 + DepthProjection.x;\n" + "\tif ( abs( denom ) < 0.00001 ) denom = denom < 0.0 ? -0.00001 : 0.00001;\n" + "\treturn -DepthProjection.y / denom;\n" + "}\n" + "vec3 CurrentViewToWorldDirection( vec3 viewPos ) {\n" + "\treturn CurrentViewAxis0.xyz * ( -viewPos.z )\n" + "\t\t+ CurrentViewAxis1.xyz * ( -viewPos.x )\n" + "\t\t+ CurrentViewAxis2.xyz * viewPos.y;\n" + "}\n" + "vec3 WorldToPreviousViewDirection( vec3 worldDirection ) {\n" + "\treturn vec3( -dot( worldDirection, PreviousViewAxis1.xyz ),\n" + "\t\tdot( worldDirection, PreviousViewAxis2.xyz ),\n" + "\t\t-dot( worldDirection, PreviousViewAxis0.xyz ) );\n" + "}\n" + "vec2 ProjectPreviousView( vec3 previousViewPosition ) {\n" + "\tfloat w = -previousViewPosition.z;\n" + "\tif ( abs( w ) < 0.00001 ) return vec2( -1000.0 );\n" + "\tvec2 clipXY = PreviousProjectInfo.xy * previousViewPosition.xy\n" + "\t\t+ PreviousProjectInfo.zw * previousViewPosition.z;\n" + "\treturn clipXY / w * 0.5 + 0.5;\n" + "}\n" + "vec2 CameraPreviousUV( vec2 uv, float depth ) {\n" + "\tvec2 ndc = uv * 2.0 - 1.0;\n" + "\tif ( depth >= 0.99999 ) {\n" + "\t\tvec3 ray = vec3( -( ndc.x + CurrentReconstructInfo.z ) * CurrentReconstructInfo.x,\n" + "\t\t\t-( ndc.y + CurrentReconstructInfo.w ) * CurrentReconstructInfo.y, -1.0 );\n" + "\t\treturn ProjectPreviousView( WorldToPreviousViewDirection( CurrentViewToWorldDirection( ray ) ) );\n" + "\t}\n" + "\tfloat viewZ = ViewZFromDepth( depth );\n" + "\tvec3 viewPosition = vec3( -viewZ * ( ndc.x + CurrentReconstructInfo.z ) * CurrentReconstructInfo.x,\n" + "\t\t-viewZ * ( ndc.y + CurrentReconstructInfo.w ) * CurrentReconstructInfo.y, viewZ );\n" + "\tvec3 worldPosition = CurrentViewOrigin.xyz + CurrentViewToWorldDirection( viewPosition );\n" + "\tvec3 delta = worldPosition - PreviousViewOrigin.xyz;\n" + "\treturn ProjectPreviousView( vec3( -dot( delta, PreviousViewAxis1.xyz ),\n" + "\t\tdot( delta, PreviousViewAxis2.xyz ), -dot( delta, PreviousViewAxis0.xyz ) ) );\n" + "}\n" + "float MaxComponent( vec3 value ) { return max( value.x, max( value.y, value.z ) ); }\n" + "bool InsideReactiveRegion( vec2 uv, vec4 region ) {\n" + "\treturn region.z > region.x && region.w > region.y\n" + "\t\t&& uv.x >= region.x && uv.y >= region.y\n" + "\t\t&& uv.x < region.z && uv.y < region.w;\n" + "}\n" + "bool ScreenEffectEnabled( float bitValue ) {\n" + "\treturn mod( floor( ScreenEffects0.x / bitValue ), 2.0 ) > 0.5;\n" + "}\n" + "vec3 CurrentViewPosition( vec2 uv, float depth ) {\n" + "\tvec2 ndc = uv * 2.0 - 1.0;\n" + "\tfloat viewZ = ViewZFromDepth( depth );\n" + "\treturn vec3( -viewZ * ( ndc.x + CurrentReconstructInfo.z ) * CurrentReconstructInfo.x,\n" + "\t\t-viewZ * ( ndc.y + CurrentReconstructInfo.w ) * CurrentReconstructInfo.y, viewZ );\n" + "}\n" + "vec2 ProjectCurrentView( vec3 viewPosition ) {\n" + "\tfloat w = -viewPosition.z;\n" + "\tif ( w <= 0.00001 ) return vec2( -1000.0 );\n" + "\tvec2 clipXY = vec2( viewPosition.x / CurrentReconstructInfo.x,\n" + "\t\tviewPosition.y / CurrentReconstructInfo.y )\n" + "\t\t+ CurrentReconstructInfo.zw * viewPosition.z;\n" + "\treturn clipXY / w * 0.5 + 0.5;\n" + "}\n" + "vec3 DepthNormal( vec2 uv, vec3 centerPosition ) {\n" + "\tvec2 dx = vec2( InvSceneSize.x, 0.0 );\n" + "\tvec2 dy = vec2( 0.0, InvSceneSize.y );\n" + "\tvec2 leftUV = clamp( uv - dx, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\tvec2 rightUV = clamp( uv + dx, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\tvec2 downUV = clamp( uv - dy, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\tvec2 upUV = clamp( uv + dy, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\tvec3 tangentX = CurrentViewPosition( rightUV, texture2D( DepthBuffer, rightUV ).r )\n" + "\t\t- CurrentViewPosition( leftUV, texture2D( DepthBuffer, leftUV ).r );\n" + "\tvec3 tangentY = CurrentViewPosition( upUV, texture2D( DepthBuffer, upUV ).r )\n" + "\t\t- CurrentViewPosition( downUV, texture2D( DepthBuffer, downUV ).r );\n" + "\tvec3 normalCross = cross( tangentX, tangentY );\n" + "\tfloat normalLengthSquared = dot( normalCross, normalCross );\n" + "\tvec3 normal = normalLengthSquared > 1.0e-12 ? normalCross * inversesqrt( normalLengthSquared ) : vec3( 0.0, 0.0, 1.0 );\n" + "\tif ( dot( normal, -centerPosition ) < 0.0 ) normal = -normal;\n" + "\treturn normal;\n" + "}\n" + "float Luminance( vec3 color ) { return dot( color, vec3( 0.2126, 0.7152, 0.0722 ) ); }\n" + "vec3 ApplyScreenSpaceGI( vec3 baseColor, vec2 uv, float depth, vec3 position, vec3 normal ) {\n" + "\tif ( !ScreenEffectEnabled( 4.0 ) || MotionParams.z < 0.5 || depth >= 0.99999 ) return baseColor;\n" + "\tvec3 indirect = vec3( 0.0 );\n" + "\tfloat totalWeight = 0.0;\n" + "\tfor ( int i = 0; i < 8; ++i ) {\n" + "\t\tfloat fi = float( i );\n" + "\t\tfloat angle = fi * 2.39996323;\n" + "\t\tvec2 offset = vec2( cos( angle ), sin( angle ) ) * ( 2.0 + fi * 1.5 ) * InvSceneSize;\n" + "\t\tvec2 sampleUV = clamp( uv + offset, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\t\tfloat sampleDepth = texture2D( DepthBuffer, sampleUV ).r;\n" + "\t\tif ( sampleDepth >= 0.99999 ) continue;\n" + "\t\tvec3 samplePosition = CurrentViewPosition( sampleUV, sampleDepth );\n" + "\t\tvec3 delta = samplePosition - position;\n" + "\t\tfloat deltaLength = length( delta );\n" + "\t\tif ( deltaLength <= 0.0001 ) continue;\n" + "\t\tfloat distanceWeight = 1.0 - smoothstep( 0.0, max( 24.0, abs( position.z ) * 0.18 ), deltaLength );\n" + "\t\tfloat facing = max( dot( normal, delta / deltaLength ), 0.0 );\n" + "\t\tfloat weight = distanceWeight * ( 0.20 + 0.80 * facing );\n" + "\t\tindirect += texture2D( Scene, sampleUV ).rgb * weight;\n" + "\t\ttotalWeight += weight;\n" + "\t}\n" + "\tindirect /= max( totalWeight, 0.0001 );\n" + "\tfloat receive = 0.08 + 0.12 * ( 1.0 - clamp( Luminance( baseColor ), 0.0, 1.0 ) );\n" + "\treturn baseColor + indirect * ScreenEffects1.w * receive;\n" + "}\n" + "vec3 ApplyScreenSpaceReflection( vec3 baseColor, vec2 uv, float depth, vec3 position, vec3 normal ) {\n" + "\tif ( !ScreenEffectEnabled( 2.0 ) || MotionParams.z < 0.5 || depth >= 0.99999 ) return baseColor;\n" + "\tvec3 incident = normalize( position );\n" + "\tvec3 rayDirection = normalize( reflect( incident, normal ) );\n" + "\tfloat stepCount = clamp( floor( ScreenEffects1.z + 0.5 ), 4.0, 16.0 );\n" + "\tfloat stepLength = ScreenEffects1.y / stepCount;\n" + "\tvec3 rayPosition = position + normal * max( 1.0, abs( position.z ) * 0.002 );\n" + "\tvec3 hitColor = baseColor;\n" + "\tfloat hitWeight = 0.0;\n" + "\tfor ( int i = 0; i < 16; ++i ) {\n" + "\t\tif ( float( i ) >= stepCount ) break;\n" + "\t\trayPosition += rayDirection * stepLength;\n" + "\t\tif ( rayPosition.z >= -0.5 ) break;\n" + "\t\tvec2 rayUV = ProjectCurrentView( rayPosition );\n" + "\t\tif ( rayUV.x <= 0.0 || rayUV.y <= 0.0 || rayUV.x >= 1.0 || rayUV.y >= 1.0 ) break;\n" + "\t\tfloat rayDepth = texture2D( DepthBuffer, rayUV ).r;\n" + "\t\tif ( rayDepth >= 0.99999 ) continue;\n" + "\t\tfloat sceneZ = ViewZFromDepth( rayDepth );\n" + "\t\tfloat thickness = max( 4.0, abs( rayPosition.z ) * 0.02 );\n" + "\t\tfloat crossing = sceneZ - rayPosition.z;\n" + "\t\tif ( i > 0 && crossing >= 0.0 && crossing < thickness ) {\n" + "\t\t\tvec2 edge = smoothstep( vec2( 0.0 ), vec2( 0.08 ), rayUV )\n" + "\t\t\t\t* smoothstep( vec2( 0.0 ), vec2( 0.08 ), vec2( 1.0 ) - rayUV );\n" + "\t\t\thitColor = texture2D( Scene, rayUV ).rgb;\n" + "\t\t\thitWeight = edge.x * edge.y;\n" + "\t\t\tbreak;\n" + "\t\t}\n" + "\t}\n" + "\tfloat facing = clamp( dot( -incident, normal ), 0.0, 1.0 );\n" + "\tfloat fresnel = 0.04 + 0.96 * pow( 1.0 - facing, 5.0 );\n" + "\treturn mix( baseColor, hitColor, hitWeight * fresnel * ScreenEffects1.x );\n" + "}\n" + "vec3 ApplyFroxelVolumetrics( vec3 baseColor, vec2 uv, float depth ) {\n" + "\tif ( !ScreenEffectEnabled( 1.0 ) ) return baseColor;\n" + "\tvec2 ndc = uv * 2.0 - 1.0;\n" + "\tvec3 viewRay = normalize( vec3( -( ndc.x + CurrentReconstructInfo.z ) * CurrentReconstructInfo.x,\n" + "\t\t-( ndc.y + CurrentReconstructInfo.w ) * CurrentReconstructInfo.y, -1.0 ) );\n" + "\tvec3 worldRay = normalize( CurrentViewToWorldDirection( viewRay ) );\n" + "\tfloat travel = ScreenEffects0.z;\n" + "\tif ( MotionParams.z > 0.5 && depth < 0.99999 ) travel = min( travel, length( CurrentViewPosition( uv, depth ) ) );\n" + "\tfloat sliceCount = clamp( floor( ScreenEffects0.w + 0.5 ), 4.0, 16.0 );\n" + "\tfloat sliceLength = travel / sliceCount;\n" + "\tfloat transmittance = 1.0;\n" + "\tvec3 scattering = vec3( 0.0 );\n" + "\tvec3 sunDirection = normalize( vec3( 0.35, 0.25, 0.90 ) );\n" + "\tfor ( int i = 0; i < 16; ++i ) {\n" + "\t\tif ( float( i ) >= sliceCount ) break;\n" + "\t\tfloat midpoint = ( float( i ) + 0.5 ) * sliceLength;\n" + "\t\tfloat worldHeight = CurrentViewOrigin.z + worldRay.z * midpoint;\n" + "\t\tfloat heightDensity = exp( -clamp( ( worldHeight - CurrentViewOrigin.z ) * 0.0008, -1.5, 2.0 ) );\n" + "\t\tfloat sliceTransmittance = exp( -ScreenEffects0.y * heightDensity * sliceLength );\n" + "\t\tfloat phase = 0.55 + 0.45 * pow( max( dot( worldRay, sunDirection ), 0.0 ), 2.0 );\n" + "\t\tvec3 fogColor = vec3( 0.17, 0.21, 0.26 ) + vec3( 0.08, 0.07, 0.04 ) * max( worldRay.z, 0.0 );\n" + "\t\tscattering += transmittance * ( 1.0 - sliceTransmittance ) * fogColor * phase;\n" + "\t\ttransmittance *= sliceTransmittance;\n" + "\t}\n" + "\treturn baseColor * transmittance + scattering;\n" + "}\n" + "void main() {\n" + "\tvec2 outputUV = gl_TexCoord[0].st;\n" + "\tvec2 sceneUV = clamp( outputUV - CurrentJitter * MotionParams.w, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\tvec4 current = texture2D( Scene, sceneUV );\n" + "\tfloat centerDepth = texture2D( DepthBuffer, sceneUV ).r;\n" + "\tif ( ScreenEffects0.x > 0.5 ) {\n" + "\t\tvec3 centerPosition = centerDepth < 0.99999 ? CurrentViewPosition( sceneUV, centerDepth ) : vec3( 0.0, 0.0, -ScreenEffects0.z );\n" + "\t\tvec3 centerNormal = centerDepth < 0.99999 ? DepthNormal( sceneUV, centerPosition ) : vec3( 0.0, 0.0, 1.0 );\n" + "\t\tcurrent.rgb = ApplyScreenSpaceGI( current.rgb, sceneUV, centerDepth, centerPosition, centerNormal );\n" + "\t\tcurrent.rgb = ApplyScreenSpaceReflection( current.rgb, sceneUV, centerDepth, centerPosition, centerNormal );\n" + "\t\tcurrent.rgb = ApplyFroxelVolumetrics( current.rgb, sceneUV, centerDepth );\n" + "\t}\n" + "\tvec3 neighborhoodMin = current.rgb;\n" + "\tvec3 neighborhoodMax = current.rgb;\n" + "\tfloat depthMin = centerDepth;\n" + "\tfloat depthMax = centerDepth;\n" + "\tfor ( int y = -1; y <= 1; ++y ) {\n" + "\t\tfor ( int x = -1; x <= 1; ++x ) {\n" + "\t\t\tvec2 sampleUV = clamp( sceneUV + vec2( float( x ), float( y ) ) * InvSceneSize, vec2( 0.0 ), vec2( 1.0 ) );\n" + "\t\t\tvec3 sampleColor = texture2D( Scene, sampleUV ).rgb;\n" + "\t\t\tneighborhoodMin = min( neighborhoodMin, sampleColor );\n" + "\t\t\tneighborhoodMax = max( neighborhoodMax, sampleColor );\n" + "\t\t\tfloat sampleDepth = texture2D( DepthBuffer, sampleUV ).r;\n" + "\t\t\tdepthMin = min( depthMin, sampleDepth );\n" + "\t\t\tdepthMax = max( depthMax, sampleDepth );\n" + "\t\t}\n" + "\t}\n" + "\tvec4 objectVelocity = texture2D( VelocityBuffer, sceneUV );\n" + "\tbool objectValid = MotionParams.x > 0.5 && objectVelocity.a > 0.5;\n" + "\tbool cameraValid = MotionParams.y > 0.5 && MotionParams.z > 0.5;\n" + "\tvec2 previousUV = cameraValid ? CameraPreviousUV( sceneUV, centerDepth ) : outputUV;\n" + "\tif ( objectValid ) previousUV = outputUV - objectVelocity.xy * InvSceneSize;\n" + "\tbool inside = previousUV.x >= 0.0 && previousUV.y >= 0.0 && previousUV.x <= 1.0 && previousUV.y <= 1.0;\n" + "\tbool historyUsable = TemporalParams.z > 0.5 && inside && ( objectValid || cameraValid );\n" + "\tvec3 historyRaw = historyUsable ? texture2D( History, previousUV ).rgb : current.rgb;\n" + "\tvec3 historyClamped = clamp( historyRaw, neighborhoodMin, neighborhoodMax );\n" + "\tfloat colorDelta = MaxComponent( abs( current.rgb - historyRaw ) );\n" + "\tfloat clampDelta = MaxComponent( abs( historyRaw - historyClamped ) );\n" + "\tvec2 velocityPixels = ( outputUV - previousUV ) * OutputSize;\n" + "\tfloat motionReactive = smoothstep( 12.0, 96.0, length( velocityPixels ) ) * 0.35;\n" + "\tfloat depthReactive = MotionParams.z > 0.5 ? smoothstep( 0.001, 0.02, depthMax - depthMin ) * 0.20 : 0.0;\n" + "\tfloat unsupportedNear = ( MotionParams.z > 0.5 && !objectValid )\n" + "\t\t? ( 1.0 - smoothstep( 0.82, 0.98, centerDepth ) ) * 0.25 : 0.0;\n" + "\tfloat packetReactive = ( InsideReactiveRegion( outputUV, ReactiveRegion0 )\n" + "\t\t|| InsideReactiveRegion( outputUV, ReactiveRegion1 ) ) ? 1.0 : 0.0;\n" + "\tfloat reactive = clamp( max( max( colorDelta * 2.5, clampDelta * 5.0 ) * TemporalParams.y,\n" + "\t\tmax( packetReactive, max( motionReactive, max( depthReactive, unsupportedNear ) ) ) ), 0.0, 1.0 );\n" + "\tif ( !historyUsable ) reactive = 1.0;\n" + "\tfloat historyWeight = historyUsable ? TemporalParams.x * ( 1.0 - reactive ) : 0.0;\n" + "\tvec3 resolved = mix( current.rgb, historyClamped, historyWeight );\n" + "\tif ( TemporalParams.w > 0.5 && TemporalParams.w < 1.5 ) {\n" + "\t\tfloat magnitude = clamp( length( velocityPixels ) / 32.0, 0.0, 1.0 );\n" + "\t\tvec2 direction = clamp( velocityPixels / 32.0, vec2( -1.0 ), vec2( 1.0 ) );\n" + "\t\tresolved = vec3( direction * 0.5 + 0.5, 1.0 ) * magnitude;\n" + "\t} else if ( TemporalParams.w > 1.5 && TemporalParams.w < 2.5 ) {\n" + "\t\tresolved = vec3( reactive, reactive * 0.25, 0.0 );\n" + "\t} else if ( TemporalParams.w > 2.5 ) {\n" + "\t\tresolved = vec3( historyWeight );\n" + "\t}\n" + "\tif ( PreserveFarDepth > 0.5 && centerDepth >= 0.99999 ) discard;\n" + "\tgl_FragColor = vec4( resolved, current.a );\n" + "}\n"; + + GLhandleARB vertexShader = glCreateShaderObjectARB( GL_VERTEX_SHADER_ARB ); + GLhandleARB fragmentShader = glCreateShaderObjectARB( GL_FRAGMENT_SHADER_ARB ); + if ( vertexShader == 0 || fragmentShader == 0 ) { + if ( vertexShader != 0 ) glDeleteObjectARB( vertexShader ); + if ( fragmentShader != 0 ) glDeleteObjectARB( fragmentShader ); + return false; + } + + const GLcharARB *vertexSourceARB = reinterpret_cast( vertexSource ); + const GLcharARB *fragmentSourceARB = reinterpret_cast( fragmentSource ); + glShaderSourceARB( vertexShader, 1, &vertexSourceARB, NULL ); + glShaderSourceARB( fragmentShader, 1, &fragmentSourceARB, NULL ); + glCompileShaderARB( vertexShader ); + glCompileShaderARB( fragmentShader ); + + GLint status = GL_FALSE; + glGetObjectParameterivARB( vertexShader, GL_OBJECT_COMPILE_STATUS_ARB, &status ); + if ( status == GL_FALSE ) { + RB_PrintGLSLInfoLog( vertexShader, "vertex shader compile", "builtin/temporal_resolve" ); + glDeleteObjectARB( vertexShader ); + glDeleteObjectARB( fragmentShader ); + return false; + } + glGetObjectParameterivARB( fragmentShader, GL_OBJECT_COMPILE_STATUS_ARB, &status ); + if ( status == GL_FALSE ) { + RB_PrintGLSLInfoLog( fragmentShader, "fragment shader compile", "builtin/temporal_resolve" ); + glDeleteObjectARB( vertexShader ); + glDeleteObjectARB( fragmentShader ); + return false; + } + + GLhandleARB programObject = glCreateProgramObjectARB(); + if ( programObject == 0 ) { + glDeleteObjectARB( vertexShader ); + glDeleteObjectARB( fragmentShader ); + return false; + } + glAttachObjectARB( programObject, vertexShader ); + glAttachObjectARB( programObject, fragmentShader ); + glLinkProgramARB( programObject ); + glGetObjectParameterivARB( programObject, GL_OBJECT_LINK_STATUS_ARB, &status ); + if ( status == GL_FALSE ) { + RB_PrintGLSLInfoLog( programObject, "program link", "builtin/temporal_resolve" ); + glDetachObjectARB( programObject, vertexShader ); + glDetachObjectARB( programObject, fragmentShader ); + glDeleteObjectARB( vertexShader ); + glDeleteObjectARB( fragmentShader ); + glDeleteObjectARB( programObject ); + return false; + } + + static const char *uniformNames[RB_TEMPORAL_UNIFORM_COUNT] = { + "Scene", "DepthBuffer", "History", "VelocityBuffer", "InvSceneSize", + "OutputSize", "CurrentReconstructInfo", "PreviousProjectInfo", "DepthProjection", + "CurrentViewOrigin", "CurrentViewAxis0", "CurrentViewAxis1", "CurrentViewAxis2", + "PreviousViewOrigin", "PreviousViewAxis0", "PreviousViewAxis1", "PreviousViewAxis2", + "CurrentJitter", "TemporalParams", "MotionParams", + "ReactiveRegion0", "ReactiveRegion1", "ScreenEffects0", + "ScreenEffects1", "PreserveFarDepth" + }; + for ( int i = 0; i < RB_TEMPORAL_UNIFORM_COUNT; i++ ) { + rbTemporalResolveUniforms[i] = glGetUniformLocationARB( programObject, uniformNames[i] ); + if ( rbTemporalResolveUniforms[i] < 0 ) { + common->Warning( "builtin temporal resolve is missing uniform '%s'", uniformNames[i] ); + glDetachObjectARB( programObject, vertexShader ); + glDetachObjectARB( programObject, fragmentShader ); + glDeleteObjectARB( vertexShader ); + glDeleteObjectARB( fragmentShader ); + glDeleteObjectARB( programObject ); + for ( int j = 0; j < RB_TEMPORAL_UNIFORM_COUNT; j++ ) rbTemporalResolveUniforms[j] = -1; + return false; + } + } + + rbTemporalResolveProgram = programObject; + rbTemporalResolveVertexShader = vertexShader; + rbTemporalResolveFragmentShader = fragmentShader; + rbTemporalResolveGeneration = tr.glContextGeneration; + common->Printf( "Loaded internal GLSL program 'builtin/temporal_resolve'\n" ); + return true; +} + static void RB_FreeSceneDepthAwarePresentProgram( void ) { if ( rbSceneDepthAwarePresentProgram != 0 && glConfig.isInitialized ) { if ( rbSceneDepthAwarePresentVertexShader != 0 ) { @@ -3968,6 +4672,7 @@ static void RB_FreeSceneDepthAwarePresentProgram( void ) { rbSceneDepthAwarePresentGeneration = -1; rbSceneDepthAwarePresentSceneLocation = -1; rbSceneDepthAwarePresentDepthLocation = -1; + rbSceneDepthAwarePresentUVOffsetLocation = -1; } static bool RB_EnsureSceneDepthAwarePresentProgram( void ) { @@ -3988,8 +4693,9 @@ static bool RB_EnsureSceneDepthAwarePresentProgram( void ) { static const char *fragmentSource = "uniform sampler2D Scene;\n" "uniform sampler2D DepthBuffer;\n" + "uniform vec2 UVOffset;\n" "void main() {\n" - " vec2 uv = gl_TexCoord[0].st;\n" + " vec2 uv = clamp( gl_TexCoord[0].st - UVOffset, vec2( 0.0 ), vec2( 1.0 ) );\n" " if ( texture2D( DepthBuffer, uv ).r >= 0.99999 ) {\n" " discard;\n" " }\n" @@ -4058,8 +4764,10 @@ static bool RB_EnsureSceneDepthAwarePresentProgram( void ) { rbSceneDepthAwarePresentGeneration = tr.videoRestartCount; rbSceneDepthAwarePresentSceneLocation = glGetUniformLocationARB( programObject, "Scene" ); rbSceneDepthAwarePresentDepthLocation = glGetUniformLocationARB( programObject, "DepthBuffer" ); + rbSceneDepthAwarePresentUVOffsetLocation = glGetUniformLocationARB( programObject, "UVOffset" ); - if ( rbSceneDepthAwarePresentSceneLocation < 0 || rbSceneDepthAwarePresentDepthLocation < 0 ) { + if ( rbSceneDepthAwarePresentSceneLocation < 0 || rbSceneDepthAwarePresentDepthLocation < 0 + || rbSceneDepthAwarePresentUVOffsetLocation < 0 ) { common->Warning( "scene depth-aware present shader is missing required sampler uniforms" ); RB_FreeSceneDepthAwarePresentProgram(); return false; @@ -4068,13 +4776,24 @@ static bool RB_EnsureSceneDepthAwarePresentProgram( void ) { return true; } +static bool RB_BindSceneScaleSharpenProgram( int sourceWidth, int sourceHeight, + int textureWidth, int textureHeight ); +static void RB_DrawFullscreenPostProcessQuadOffsetScaled( int viewportWidth, + int viewportHeight, int textureWidth, int textureHeight, + float offsetX, float offsetY ); + static void RB_PresentSceneRenderTargetToBackBuffer( const rbSceneScaleState_t &scaleState ) { if ( !RB_IsSceneRenderTexture( backEnd.renderTexture ) || backEnd.viewDef == NULL ) { return; } - const int sourceViewportWidth = backEnd.viewDef->viewport.x2 - backEnd.viewDef->viewport.x1 + 1; - const int sourceViewportHeight = backEnd.viewDef->viewport.y2 - backEnd.viewDef->viewport.y1 + 1; + // Scene coordinate mutations are restored before any temporal/reactive + // policy is built. Preserve the actual offscreen source extent explicitly + // for the legacy spatial presenter instead of reading the restored viewport. + const int sourceViewportWidth = scaleState.active ? scaleState.scaledWidth + : backEnd.viewDef->viewport.x2 - backEnd.viewDef->viewport.x1 + 1; + const int sourceViewportHeight = scaleState.active ? scaleState.scaledHeight + : backEnd.viewDef->viewport.y2 - backEnd.viewDef->viewport.y1 + 1; const idScreenRect targetViewport = scaleState.active ? scaleState.nativeViewport : backEnd.viewDef->viewport; const idScreenRect targetScissor = scaleState.active ? scaleState.nativeScissor : backEnd.viewDef->scissor; const int targetViewportWidth = targetViewport.x2 - targetViewport.x1 + 1; @@ -4164,6 +4883,10 @@ static void RB_PresentSceneRenderTargetToBackBuffer( const rbSceneScaleState_t & GL_TexEnv( GL_MODULATE ); RB_SetFramebufferSRGBEnabled( true ); + const bool scaleSharpenBound = !preserveFarDepth && scaleState.active + && scaleState.effectivePercent < RB_SCREEN_FRACTION_NATIVE + && RB_BindSceneScaleSharpenProgram( sourceViewportWidth, + sourceViewportHeight, textureWidth, textureHeight ); if ( preserveFarDepth ) { GL_SelectTexture( 1 ); presentDepthImage->Bind(); @@ -4173,10 +4896,27 @@ static void RB_PresentSceneRenderTargetToBackBuffer( const rbSceneScaleState_t & glUseProgramObjectARB( rbSceneDepthAwarePresentProgram ); glUniform1iARB( rbSceneDepthAwarePresentSceneLocation, 0 ); glUniform1iARB( rbSceneDepthAwarePresentDepthLocation, 1 ); + const GLfloat uvOffset[2] = { + R_TemporalPresentation_TemporalAARequested() + ? backEnd.viewDef->temporalJitterPixels.x / static_cast( Max( 1, targetViewportWidth ) ) : 0.0f, + R_TemporalPresentation_TemporalAARequested() + ? backEnd.viewDef->temporalJitterPixels.y / static_cast( Max( 1, targetViewportHeight ) ) : 0.0f + }; + glUniform2fvARB( rbSceneDepthAwarePresentUVOffsetLocation, 1, uvOffset ); + } + if ( !preserveFarDepth && R_TemporalPresentation_TemporalAARequested() + && backEnd.viewDef->temporalJitterEnabled ) { + RB_DrawFullscreenPostProcessQuadOffsetScaled( + sourceViewportWidth, sourceViewportHeight, textureWidth, textureHeight, + backEnd.viewDef->temporalJitterPixels.x / static_cast( Max( 1, targetViewportWidth ) ), + backEnd.viewDef->temporalJitterPixels.y / static_cast( Max( 1, targetViewportHeight ) ) ); + } else { + RB_DrawFullscreenPostProcessQuad( sourceViewportWidth, sourceViewportHeight, textureWidth, textureHeight ); } - RB_DrawFullscreenPostProcessQuad( sourceViewportWidth, sourceViewportHeight, textureWidth, textureHeight ); - if ( preserveFarDepth ) { + if ( preserveFarDepth || scaleSharpenBound ) { glUseProgramObjectARB( 0 ); + } + if ( preserveFarDepth ) { GL_SelectTexture( 1 ); globalImages->BindNull(); GL_SelectTexture( 0 ); @@ -4185,6 +4925,9 @@ static void RB_PresentSceneRenderTargetToBackBuffer( const rbSceneScaleState_t & globalImages->BindNull(); RB_EndFullscreenPostProcessPass(); + if ( scaleState.active ) { + rbSceneScalePresentedFrame = backEnd.frameCount; + } } enum rbResolutionScaleUniformIndex_t { @@ -4224,43 +4967,859 @@ static void RB_InitResolutionScaleStage( void ) { rbResolutionScaleStageInitialized = true; } -void RB_ApplyResolutionScaleToBackBuffer( void ) { - if ( r_skipPostProcess.GetBool() ) { - return; +static bool RB_BindSceneScaleSharpenProgram( int sourceWidth, int sourceHeight, + int textureWidth, int textureHeight ) { + if ( idMath::ClampInt( 0, 2, r_resolutionScaleMode.GetInteger() ) != 2 + || !glConfig.GLSLProgramAvailable || sourceWidth <= 0 + || sourceHeight <= 0 || textureWidth <= 0 || textureHeight <= 0 ) { + return false; } - const int scalePercent = RB_RequestedScreenFraction(); - if ( scalePercent >= RB_SCREEN_FRACTION_NATIVE ) { - return; + RB_InitResolutionScaleStage(); + if ( !R_ValidateGLSLProgram( &rbResolutionScaleStage ) ) { + return false; } - int mode = idMath::ClampInt( 0, 2, r_resolutionScaleMode.GetInteger() ); - if ( mode == 0 ) { - // Legacy path: BeginFrame crop mode without fullscreen upscale. - return; + glUseProgramObjectARB( (GLhandleARB)rbResolutionScaleStage.glslProgramObject ); + const int sceneLocation = rbResolutionScaleStage.shaderTextureLocations[0]; + if ( sceneLocation >= 0 ) { + glUniform1iARB( sceneLocation, 0 ); } - - const int viewportWidth = glConfig.vidWidth; - const int viewportHeight = glConfig.vidHeight; - if ( viewportWidth <= 0 || viewportHeight <= 0 ) { - return; + const GLfloat invTexSize[2] = { + 1.0f / static_cast( textureWidth ), + 1.0f / static_cast( textureHeight ) + }; + const GLfloat invLowResSize[2] = { + 1.0f / static_cast( sourceWidth ), + 1.0f / static_cast( sourceHeight ) + }; + const GLfloat sharpenAmount = idMath::ClampFloat( + 0.0f, 1.5f, r_resolutionScaleSharpness.GetFloat() ); + if ( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_INV_TEX_SIZE] >= 0 ) { + glUniform2fvARB( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_INV_TEX_SIZE], 1, invTexSize ); } - - const int sourceWidth = idMath::ClampInt( 1, viewportWidth, - idMath::Ftoi( static_cast( viewportWidth ) * ( static_cast( scalePercent ) * 0.01f ) + 0.5f ) ); - const int sourceHeight = idMath::ClampInt( 1, viewportHeight, - idMath::Ftoi( static_cast( viewportHeight ) * ( static_cast( scalePercent ) * 0.01f ) + 0.5f ) ); - if ( sourceWidth <= 0 || sourceHeight <= 0 ) { - return; + if ( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_INV_LOW_RES_SIZE] >= 0 ) { + glUniform2fvARB( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_INV_LOW_RES_SIZE], 1, invLowResSize ); } - - if ( !glConfig.GLSLProgramAvailable ) { - return; + if ( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_SHARPEN_AMOUNT] >= 0 ) { + glUniform1fARB( rbResolutionScaleStage.shaderParmLocations[RB_RES_SCALE_UNIFORM_SHARPEN_AMOUNT], sharpenAmount ); } + return true; +} - RB_InitResolutionScaleStage(); - if ( !R_ValidateGLSLProgram( &rbResolutionScaleStage ) ) { - return; +static idImage *RB_TemporalColorImage( const idRenderTexture *target ) { + if ( target == NULL || target->GetNumColorImages() <= 0 ) { + return NULL; + } + return target->GetColorImage( 0 ); +} + +static bool RB_TemporalColorTargetMatches( const idRenderTexture *target, + int width, int height ) { + idImage *image = RB_TemporalColorImage( target ); + return image != NULL && target->GetWidth() == width && target->GetHeight() == height + && image->GetOpts().width == width && image->GetOpts().height == height + && image->GetOpts().numMSAASamples <= 1; +} + +static void RB_DrawFullscreenPostProcessQuadOffsetUV( float offsetX, float offsetY ) { + const float minS = idMath::ClampFloat( 0.0f, 1.0f, -offsetX ); + const float minT = idMath::ClampFloat( 0.0f, 1.0f, -offsetY ); + const float maxS = idMath::ClampFloat( 0.0f, 1.0f, 1.0f - offsetX ); + const float maxT = idMath::ClampFloat( 0.0f, 1.0f, 1.0f - offsetY ); + glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + glBegin( GL_QUADS ); + glTexCoord2f( minS, minT ); glVertex2f( 0.0f, 0.0f ); + glTexCoord2f( minS, maxT ); glVertex2f( 0.0f, 1.0f ); + glTexCoord2f( maxS, maxT ); glVertex2f( 1.0f, 1.0f ); + glTexCoord2f( maxS, minT ); glVertex2f( 1.0f, 0.0f ); + glEnd(); +} + +static void RB_DrawFullscreenPostProcessQuadOffsetScaled( int viewportWidth, + int viewportHeight, int textureWidth, int textureHeight, + float offsetX, float offsetY ) { + const float maxS = static_cast( viewportWidth ) / static_cast( Max( 1, textureWidth ) ); + const float maxT = static_cast( viewportHeight ) / static_cast( Max( 1, textureHeight ) ); + const float minS = idMath::ClampFloat( 0.0f, maxS, -offsetX * maxS ); + const float minT = idMath::ClampFloat( 0.0f, maxT, -offsetY * maxT ); + const float endS = idMath::ClampFloat( 0.0f, maxS, maxS - offsetX * maxS ); + const float endT = idMath::ClampFloat( 0.0f, maxT, maxT - offsetY * maxT ); + glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + glBegin( GL_QUADS ); + glTexCoord2f( minS, minT ); glVertex2f( 0.0f, 0.0f ); + glTexCoord2f( minS, endT ); glVertex2f( 0.0f, 1.0f ); + glTexCoord2f( endS, endT ); glVertex2f( 1.0f, 1.0f ); + glTexCoord2f( endS, minT ); glVertex2f( 1.0f, 0.0f ); + glEnd(); +} + +static bool RB_BindTemporalDestination( idRenderTexture *target, int width, int height ) { + if ( width <= 0 || height <= 0 ) { + return false; + } + if ( target != NULL ) { + if ( !target->MakeCurrent() ) { + return false; + } + backEnd.renderTexture = target; + } else { + idRenderTexture::BindNull(); + backEnd.renderTexture = NULL; + glDrawBuffer( GL_BACK ); + glReadBuffer( GL_BACK ); + } + backEnd.feedbackRenderTexture = NULL; + glViewport( 0, 0, width, height ); + glScissor( 0, 0, width, height ); + backEnd.currentScissor.x1 = 0; + backEnd.currentScissor.y1 = 0; + backEnd.currentScissor.x2 = width - 1; + backEnd.currentScissor.y2 = height - 1; + return true; +} + +static bool RB_PresentTemporalSpatialFallback( idImage *sceneImage, + int outputWidth, int outputHeight, const idVec2 &jitterPixels, + idImage *depthImage = NULL, bool preserveFarDepth = false ) { + if ( sceneImage == NULL || !RB_BindTemporalDestination( NULL, outputWidth, outputHeight ) ) { + return false; + } + + const bool depthAware = preserveFarDepth && depthImage != NULL + && RB_EnsureSceneDepthAwarePresentProgram(); + RB_BeginFullscreenPostProcessPass( 0, 0, outputWidth, outputHeight ); + GL_SelectTexture( 0 ); + sceneImage->Bind(); + GL_TexEnv( GL_MODULATE ); + if ( depthAware ) { + GL_SelectTexture( 1 ); + depthImage->Bind(); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE ); + glTexParameteri( GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE ); + GL_SelectTexture( 0 ); + glUseProgramObjectARB( rbSceneDepthAwarePresentProgram ); + glUniform1iARB( rbSceneDepthAwarePresentSceneLocation, 0 ); + glUniform1iARB( rbSceneDepthAwarePresentDepthLocation, 1 ); + const GLfloat uvOffset[2] = { + jitterPixels.x / static_cast( Max( 1, outputWidth ) ), + jitterPixels.y / static_cast( Max( 1, outputHeight ) ) + }; + glUniform2fvARB( rbSceneDepthAwarePresentUVOffsetLocation, 1, uvOffset ); + } + RB_SetFramebufferSRGBEnabled( true ); + if ( depthAware ) { + RB_DrawFullscreenPostProcessQuadUnitUV(); + } else { + RB_DrawFullscreenPostProcessQuadOffsetUV( + jitterPixels.x / static_cast( Max( 1, outputWidth ) ), + jitterPixels.y / static_cast( Max( 1, outputHeight ) ) ); + } + RB_SetFramebufferSRGBEnabled( false ); + if ( depthAware ) { + glUseProgramObjectARB( 0 ); + GL_SelectTexture( 1 ); + globalImages->BindNull(); + GL_SelectTexture( 0 ); + } + globalImages->BindNull(); + RB_EndFullscreenPostProcessPass(); + backEnd.currentRenderCopied = false; + rbSceneScalePresentedFrame = backEnd.frameCount; + return true; +} + +static void RB_BuildTemporalWorldModelView( const idVec3 &origin, + const idMat3 &axis, float matrix[16] ) { + float viewerMatrix[16]; + memset( viewerMatrix, 0, sizeof( viewerMatrix ) ); + viewerMatrix[0] = axis[0][0]; + viewerMatrix[4] = axis[0][1]; + viewerMatrix[8] = axis[0][2]; + viewerMatrix[12] = -origin[0] * viewerMatrix[0] + - origin[1] * viewerMatrix[4] - origin[2] * viewerMatrix[8]; + viewerMatrix[1] = axis[1][0]; + viewerMatrix[5] = axis[1][1]; + viewerMatrix[9] = axis[1][2]; + viewerMatrix[13] = -origin[0] * viewerMatrix[1] + - origin[1] * viewerMatrix[5] - origin[2] * viewerMatrix[9]; + viewerMatrix[2] = axis[2][0]; + viewerMatrix[6] = axis[2][1]; + viewerMatrix[10] = axis[2][2]; + viewerMatrix[14] = -origin[0] * viewerMatrix[2] + - origin[1] * viewerMatrix[6] - origin[2] * viewerMatrix[10]; + viewerMatrix[15] = 1.0f; + static const float flipMatrix[16] = { + 0, 0, -1, 0, + -1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 1 + }; + myGlMultMatrix( viewerMatrix, flipMatrix, matrix ); +} + +static bool RB_BuildTemporalPreviousMotionState( const viewDef_t *viewDef, + int sceneWidth, int sceneHeight, rbMotionBlurViewState_t &previousState ) { + if ( viewDef == NULL || !viewDef->temporalPreviousProjectionValid ) { + return false; + } + + const viewDef_t *savedViewDef = backEnd.viewDef; + backEnd.viewDef = const_cast( viewDef ); + const bool built = RB_BuildMotionBlurViewState( previousState, sceneWidth, sceneHeight ); + backEnd.viewDef = const_cast( savedViewDef ); + if ( !built ) { + return false; + } + + previousState.viewOrigin = viewDef->temporalPreviousViewOrigin; + for ( int i = 0; i < 3; i++ ) { + previousState.viewAxis[i] = viewDef->temporalPreviousViewAxis[i]; + } + previousState.projectInfo[0] = viewDef->temporalPreviousProjectInfo.x; + previousState.projectInfo[1] = viewDef->temporalPreviousProjectInfo.y; + previousState.projectInfo[2] = viewDef->temporalPreviousProjectInfo.z; + previousState.projectInfo[3] = viewDef->temporalPreviousProjectInfo.w; + previousState.projectionMatrix[0] = previousState.projectInfo[0]; + previousState.projectionMatrix[5] = previousState.projectInfo[1]; + previousState.projectionMatrix[8] = previousState.projectInfo[2]; + previousState.projectionMatrix[9] = previousState.projectInfo[3]; + RB_BuildTemporalWorldModelView( previousState.viewOrigin, + viewDef->temporalPreviousViewAxis, previousState.worldModelViewMatrix ); + return true; +} + +static void RB_ClearTemporalEntityHistory( void ) { + rbTemporalEntityHistory.Clear(); + rbTemporalNextEntityHistory.Clear(); + rbTemporalEntityHistoryGeneration = 0; + rbTemporalEntityHistoryViewIdentity = 0; + rbTemporalEntityHistoryFrame = -1; +} + +static void RB_RejectTemporalHistoryWrite( + const resolveTemporalPresentationCommand_t &command ) { + RB_ClearTemporalEntityHistory(); + if ( command.captureFrame || tr.takingScreenshot ) { + return; + } + // The front end accepted this command and may already have advanced its + // ping-pong index. Force the next command in the same generation to seed a + // fresh destination instead of sampling the untouched image. + rbTemporalResolveNeedsReprime = true; + rbTemporalResolveRejectedGeneration = command.historyGeneration; +} + +static void RB_UpdateTemporalEntityHistory( const viewDef_t *viewDef, + unsigned int generation ) { + rbTemporalNextEntityHistory.Clear(); + if ( viewDef != NULL ) { + for ( int i = 0; i < viewDef->numDrawSurfs; i++ ) { + const drawSurf_t *surf = viewDef->drawSurfs[i]; + if ( !RB_MotionVectorSurfaceEligible( surf ) ) { + continue; + } + RB_StoreMotionBlurEntityHistory( rbTemporalNextEntityHistory, + surf->space->entityDef->index, surf->space->modelMatrix ); + } + } + rbTemporalEntityHistory.Swap( rbTemporalNextEntityHistory ); + rbTemporalNextEntityHistory.Clear(); + rbTemporalEntityHistoryGeneration = generation; + rbTemporalEntityHistoryViewIdentity = viewDef != NULL + ? viewDef->temporalViewIdentity : 0; + rbTemporalEntityHistoryFrame = backEnd.frameCount; +} + +static void RB_UploadTemporalVec3AsVec4( int uniformIndex, const idVec3 &value ) { + const GLfloat data[4] = { value.x, value.y, value.z, 0.0f }; + glUniform4fvARB( rbTemporalResolveUniforms[uniformIndex], 1, data ); +} + +static bool RB_DrawTemporalResolvePass( const resolveTemporalPresentationCommand_t &command, + idImage *sceneImage, idImage *depthImage, idImage *historyImage, + idImage *velocityImage, idRenderTexture *destination, + const temporalViewMotionPolicy_t *motionPolicy, + bool useHistory, bool useVelocity, bool captureRecenter, + bool preserveFarDepth, int debugMode ) { + const int sceneWidth = command.presentation.sceneWidth; + const int sceneHeight = command.presentation.sceneHeight; + const int outputWidth = command.presentation.outputWidth; + const int outputHeight = command.presentation.outputHeight; + const viewDef_t *viewDef = command.viewDef; + if ( viewDef == NULL || sceneImage == NULL || !RB_EnsureTemporalResolveProgram() + || glConfig.maxTextureImageUnits < 4 + || !RB_BindTemporalDestination( destination, outputWidth, outputHeight ) ) { + return false; + } + + const bool depthValid = depthImage != NULL; + const bool cameraValid = useHistory && depthValid + && viewDef->temporalPreviousProjectionValid; + RB_BeginFullscreenPostProcessPass( 0, 0, outputWidth, outputHeight ); + GL_SelectTexture( 0 ); sceneImage->Bind(); + GL_SelectTexture( 1 ); ( depthValid ? depthImage : globalImages->whiteImage )->Bind(); + GL_SelectTexture( 2 ); ( useHistory && historyImage != NULL ? historyImage : globalImages->blackImage )->Bind(); + GL_SelectTexture( 3 ); ( useVelocity && velocityImage != NULL ? velocityImage : globalImages->blackImage )->Bind(); + GL_SelectTexture( 0 ); + glUseProgramObjectARB( rbTemporalResolveProgram ); + for ( int i = 0; i < 4; i++ ) { + glUniform1iARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_SCENE + i], i ); + } + + const GLfloat invSceneSize[2] = { + 1.0f / static_cast( Max( 1, sceneWidth ) ), + 1.0f / static_cast( Max( 1, sceneHeight ) ) + }; + const GLfloat outputSize[2] = { + static_cast( outputWidth ), static_cast( outputHeight ) + }; + const GLfloat currentReconstruct[4] = { + 1.0f / viewDef->projectionMatrix[0], 1.0f / viewDef->projectionMatrix[5], + viewDef->projectionMatrix[8], viewDef->projectionMatrix[9] + }; + const GLfloat previousProject[4] = { + viewDef->temporalPreviousProjectInfo.x, viewDef->temporalPreviousProjectInfo.y, + viewDef->temporalPreviousProjectInfo.z, viewDef->temporalPreviousProjectInfo.w + }; + const GLfloat depthProjection[2] = { + viewDef->projectionMatrix[10], viewDef->projectionMatrix[14] + }; + const GLfloat jitter[2] = { + viewDef->temporalJitterPixels.x / static_cast( Max( 1, outputWidth ) ), + viewDef->temporalJitterPixels.y / static_cast( Max( 1, outputHeight ) ) + }; + const GLfloat temporalParams[4] = { + idMath::ClampFloat( 0.0f, 0.98f, command.feedback ), + idMath::ClampFloat( 0.0f, 2.0f, command.reactiveScale ), + useHistory ? 1.0f : 0.0f, + static_cast( idMath::ClampInt( 0, 3, debugMode ) ) + }; + const GLfloat motionParams[4] = { + useVelocity ? 1.0f : 0.0f, + cameraValid ? 1.0f : 0.0f, + depthValid ? 1.0f : 0.0f, + captureRecenter ? 1.0f : 0.0f + }; + GLfloat reactiveRegions[TEMPORAL_MAX_REACTIVE_REGIONS][4] = {}; + if ( motionPolicy != NULL ) { + const int regionCount = idMath::ClampInt( 0, + TEMPORAL_MAX_REACTIVE_REGIONS, motionPolicy->reactiveRegionCount ); + for ( int i = 0; i < regionCount; i++ ) { + const temporalReactiveRegion_t ®ion = motionPolicy->reactiveRegions[i]; + reactiveRegions[i][0] = idMath::ClampFloat( 0.0f, 1.0f, region.x1 ); + reactiveRegions[i][1] = idMath::ClampFloat( 0.0f, 1.0f, region.y1 ); + reactiveRegions[i][2] = idMath::ClampFloat( 0.0f, 1.0f, region.x2 ); + reactiveRegions[i][3] = idMath::ClampFloat( 0.0f, 1.0f, region.y2 ); + } + } + glUniform2fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_INV_SCENE_SIZE], 1, invSceneSize ); + glUniform2fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_OUTPUT_SIZE], 1, outputSize ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_CURRENT_RECONSTRUCT], 1, currentReconstruct ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_PREVIOUS_PROJECT], 1, previousProject ); + glUniform2fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_DEPTH_PROJECTION], 1, depthProjection ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_CURRENT_VIEW_ORIGIN, viewDef->renderView.vieworg ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS0, viewDef->renderView.viewaxis[0] ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS1, viewDef->renderView.viewaxis[1] ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_CURRENT_VIEW_AXIS2, viewDef->renderView.viewaxis[2] ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_ORIGIN, viewDef->temporalPreviousViewOrigin ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS0, viewDef->temporalPreviousViewAxis[0] ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS1, viewDef->temporalPreviousViewAxis[1] ); + RB_UploadTemporalVec3AsVec4( RB_TEMPORAL_UNIFORM_PREVIOUS_VIEW_AXIS2, viewDef->temporalPreviousViewAxis[2] ); + glUniform2fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_CURRENT_JITTER], 1, jitter ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_PARAMS], 1, temporalParams ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_MOTION_PARAMS], 1, motionParams ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_REACTIVE_REGION0], 1, reactiveRegions[0] ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_REACTIVE_REGION1], 1, reactiveRegions[1] ); + GLfloat screenEffects[8]; + AdvancedScreenSpaceCore_Pack( command.advancedScreenSpace, screenEffects ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_SCREEN_EFFECTS0], 1, + screenEffects ); + glUniform4fvARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_SCREEN_EFFECTS1], 1, + screenEffects + 4 ); + glUniform1fARB( rbTemporalResolveUniforms[RB_TEMPORAL_UNIFORM_PRESERVE_FAR_DEPTH], + preserveFarDepth && destination == NULL ? 1.0f : 0.0f ); + + RB_SetFramebufferSRGBEnabled( destination == NULL ); + RB_DrawFullscreenPostProcessQuadUnitUV(); + RB_SetFramebufferSRGBEnabled( false ); + glUseProgramObjectARB( 0 ); + for ( int unit = 3; unit >= 0; unit-- ) { + GL_SelectTexture( unit ); + globalImages->BindNull(); + } + GL_SelectTexture( 0 ); + RB_EndFullscreenPostProcessPass(); + backEnd.currentRenderCopied = false; + if ( destination == NULL ) { + rbSceneScalePresentedFrame = backEnd.frameCount; + } + return true; +} + +bool RB_ResolveTemporalPresentation( const resolveTemporalPresentationCommand_t &command ) { + rbTemporalResolveHistoryWriteFrame = -1; + rbTemporalResolveHistoryWriteTarget = NULL; + const int sceneWidth = command.presentation.sceneWidth; + const int sceneHeight = command.presentation.sceneHeight; + const int outputWidth = command.presentation.outputWidth; + const int outputHeight = command.presentation.outputHeight; + idImage *sceneImage = RB_TemporalColorImage( command.sceneColorTarget ); + const bool captureFrame = command.captureFrame || tr.takingScreenshot; + idVec2 spatialJitter( 0.0f, 0.0f ); + if ( command.viewDef != NULL ) { + spatialJitter = command.viewDef->temporalJitterPixels; + } + if ( command.viewDef == NULL || sceneWidth <= 0 || sceneHeight <= 0 + || outputWidth <= 0 || outputHeight <= 0 + || command.presentation.frameNumber != backEnd.frameCount + || outputWidth != glConfig.vidWidth || outputHeight != glConfig.vidHeight + || !RB_TemporalColorTargetMatches( command.sceneColorTarget, sceneWidth, sceneHeight ) ) { + RB_RejectTemporalHistoryWrite( command ); + return sceneImage != NULL + ? RB_PresentTemporalSpatialFallback( sceneImage, outputWidth, outputHeight, spatialJitter ) + : false; + } + idImage *depthImage = command.sceneDepthTarget != NULL + ? command.sceneDepthTarget->GetDepthImage() : NULL; + if ( depthImage != NULL && ( command.sceneDepthTarget->GetWidth() != sceneWidth + || command.sceneDepthTarget->GetHeight() != sceneHeight + || depthImage->GetOpts().width != sceneWidth + || depthImage->GetOpts().height != sceneHeight + || depthImage->GetOpts().numMSAASamples > 1 ) ) { + depthImage = NULL; + } + const bool preserveFarDepth = command.sceneColorTarget == rbSceneRenderTexture + && RB_ShouldPreserveSceneRenderTargetFarDepth( command.viewDef ); + + // A synchronous save preview may flush a command that was queued before the + // capture began. It must not observe or advance either image or transform + // history. Recenter the already-jittered scene during the spatial present. + if ( captureFrame ) { + RB_ClearTemporalEntityHistory(); + if ( RB_DrawTemporalResolvePass( command, sceneImage, depthImage, NULL, NULL, + NULL, NULL, false, false, true, preserveFarDepth, 0 ) ) { + return true; + } + return RB_PresentTemporalSpatialFallback( sceneImage, + outputWidth, outputHeight, spatialJitter, depthImage, preserveFarDepth ); + } + + // History is eligible only when this exact scene-depth target was populated + // successfully for the immutable frame/generation carried by the resolve. + // A missing stamp includes failed GL depth blits and direct resource loss. + if ( depthImage == NULL || !RB_TemporalDepthStampIsCurrent( + command.sceneDepthTarget, command.presentation.frameNumber, + command.historyGeneration ) ) { + RB_RejectTemporalHistoryWrite( command ); + return RB_PresentTemporalSpatialFallback( sceneImage, + outputWidth, outputHeight, spatialJitter, depthImage, preserveFarDepth ); + } + if ( !command.presentation.temporalAARequested ) { + RB_ClearTemporalEntityHistory(); + if ( RB_DrawTemporalResolvePass( command, sceneImage, depthImage, + NULL, NULL, NULL, NULL, false, false, false, + preserveFarDepth, 0 ) ) { + return true; + } + return RB_PresentTemporalSpatialFallback( sceneImage, + outputWidth, outputHeight, spatialJitter, + depthImage, preserveFarDepth ); + } + + bool canWriteHistory = RB_TemporalColorTargetMatches( + command.historyWriteTarget, outputWidth, outputHeight ) + && command.historyWriteTarget != command.sceneColorTarget; + idImage *historyWriteImage = canWriteHistory + ? RB_TemporalColorImage( command.historyWriteTarget ) : NULL; + if ( historyWriteImage == sceneImage ) { + canWriteHistory = false; + historyWriteImage = NULL; + } + bool useHistory = command.historyValid && command.viewDef->temporalHistoryValid + && command.viewDef->temporalPreviousProjectionValid + && command.historyGeneration == command.presentation.historyGeneration + && command.viewDef->temporalHistoryGeneration == command.historyGeneration + && RB_TemporalColorTargetMatches( command.historyReadTarget, outputWidth, outputHeight ) + && command.historyReadTarget != command.historyWriteTarget + && command.historyReadTarget != command.sceneColorTarget; + if ( rbTemporalResolveNeedsReprime + && rbTemporalResolveRejectedGeneration == command.historyGeneration ) { + useHistory = false; + } else if ( rbTemporalResolveNeedsReprime ) { + // A generation transition already provides the same discontinuity as the + // local rejected-write guard. + rbTemporalResolveNeedsReprime = false; + rbTemporalResolveRejectedGeneration = 0; + } + idImage *historyReadImage = useHistory + ? RB_TemporalColorImage( command.historyReadTarget ) : NULL; + if ( useHistory && ( historyReadImage == historyWriteImage + || historyReadImage == sceneImage || historyWriteImage == sceneImage ) ) { + useHistory = false; + historyReadImage = NULL; + } + + if ( !canWriteHistory || !RB_EnsureTemporalResolveProgram() ) { + RB_RejectTemporalHistoryWrite( command ); + if ( AdvancedScreenSpaceCore_Requested( command.advancedScreenSpace ) + && RB_DrawTemporalResolvePass( command, sceneImage, depthImage, + NULL, NULL, NULL, NULL, false, false, true, + preserveFarDepth, 0 ) ) { + return true; + } + return RB_PresentTemporalSpatialFallback( sceneImage, + outputWidth, outputHeight, spatialJitter, + depthImage, preserveFarDepth ); + } + + const bool transformHistoryMatches = useHistory + && rbTemporalEntityHistoryGeneration == command.historyGeneration + && rbTemporalEntityHistoryViewIdentity == command.viewDef->temporalViewIdentity + && rbTemporalEntityHistoryFrame == backEnd.frameCount - 1; + if ( !transformHistoryMatches ) { + rbTemporalEntityHistory.Clear(); + } + + bool velocityValid = false; + bool velocityComplete = false; + if ( useHistory && depthImage != NULL && transformHistoryMatches ) { + rbMotionBlurViewState_t previousState; + if ( RB_BuildTemporalPreviousMotionState( command.viewDef, + sceneWidth, sceneHeight, previousState ) ) { + const viewDef_t *savedViewDef = backEnd.viewDef; + backEnd.viewDef = const_cast( command.viewDef ); + velocityValid = RB_RenderMotionVectorBuffer( + command.viewDef->drawSurfs, command.viewDef->numDrawSurfs, + previousState, sceneWidth, sceneHeight, depthImage, + rbTemporalEntityHistory, true, &velocityComplete ); + backEnd.viewDef = const_cast( savedViewDef ); + } + } + RB_UpdateTemporalEntityHistory( command.viewDef, command.historyGeneration ); + temporalViewMotionPolicy_t motionPolicy = + TemporalHistoryCore_BeginViewMotionPolicy(); + const unsigned int exactMotionDomains = velocityComplete + ? TemporalHistoryCore_MotionDomainBit( TEMPORAL_MOTION_DOMAIN_RIGID ) + : 0u; + if ( !R_ScenePackets_BuildTemporalViewMotionPolicy( command.viewDef, + exactMotionDomains, motionPolicy ) && useHistory ) { + // Packet capture is mandatory while temporal AA is active. If that + // contract is unavailable, reject history conservatively over the view. + TemporalHistoryCore_AddReactiveRegion( motionPolicy, ~0u, + 0.0f, 0.0f, 1.0f, 1.0f ); + } + + if ( !RB_DrawTemporalResolvePass( command, sceneImage, depthImage, + historyReadImage, velocityValid ? rbMotionVectorImage : NULL, + command.historyWriteTarget, &motionPolicy, + useHistory, velocityValid, false, false, 0 ) ) { + RB_RejectTemporalHistoryWrite( command ); + return RB_PresentTemporalSpatialFallback( sceneImage, + outputWidth, outputHeight, spatialJitter, + depthImage, preserveFarDepth ); + } + rbTemporalResolveHistoryWriteFrame = backEnd.frameCount; + rbTemporalResolveHistoryWriteTarget = command.historyWriteTarget; + rbTemporalResolveNeedsReprime = false; + rbTemporalResolveRejectedGeneration = 0; + + const int debugMode = idMath::ClampInt( 0, 3, command.debugMode ); + if ( debugMode > 0 && RB_DrawTemporalResolvePass( command, sceneImage, depthImage, + historyReadImage, velocityValid ? rbMotionVectorImage : NULL, + NULL, &motionPolicy, + useHistory, velocityValid, false, preserveFarDepth, debugMode ) ) { + return true; + } + return RB_PresentTemporalSpatialFallback( historyWriteImage, + outputWidth, outputHeight, idVec2( 0.0f, 0.0f ), + depthImage, preserveFarDepth ); +} + +static void RB_ResetBackendTemporalHistory( bool destroyResources ) { + rbBackendTemporalHistoryReadIndex = 0; + rbBackendTemporalHistoryFrame = -1; + rbBackendTemporalHistoryGeneration = 0; + rbBackendTemporalHistoryViewIdentity = 0; + rbBackendTemporalHistoryValid = false; + if ( !destroyResources ) { + return; + } + for ( int i = 0; i < 2; i++ ) { + if ( rbBackendTemporalHistoryTargets[i] != NULL ) { + tr.DestroyRenderTexture( rbBackendTemporalHistoryTargets[i] ); + rbBackendTemporalHistoryTargets[i] = NULL; + } + rbBackendTemporalHistoryImages[i] = NULL; + } + rbBackendTemporalHistoryWidth = 0; + rbBackendTemporalHistoryHeight = 0; + rbBackendTemporalHistoryContextGeneration = -1; +} + +static bool RB_EnsureBackendTemporalHistory( int width, int height, + unsigned int generation, unsigned long long viewIdentity ) { + if ( globalImages == NULL || width <= 0 || height <= 0 ) { + RB_ResetBackendTemporalHistory( false ); + return false; + } + + const bool continuityChanged = rbBackendTemporalHistoryWidth != width + || rbBackendTemporalHistoryHeight != height + || rbBackendTemporalHistoryContextGeneration != tr.glContextGeneration + || rbBackendTemporalHistoryGeneration != generation + || rbBackendTemporalHistoryViewIdentity != viewIdentity; + if ( continuityChanged ) { + rbBackendTemporalHistoryReadIndex = 0; + rbBackendTemporalHistoryFrame = -1; + rbBackendTemporalHistoryValid = false; + } + + idImageOpts opts; + opts.textureType = TT_2D; + opts.format = FMT_RGBA16F; + opts.width = width; + opts.height = height; + opts.numLevels = 1; + opts.numMSAASamples = 0; + opts.isPersistant = true; + for ( int i = 0; i < 2; i++ ) { + if ( rbBackendTemporalHistoryImages[i] == NULL + || rbBackendTemporalHistoryWidth != width + || rbBackendTemporalHistoryHeight != height + || rbBackendTemporalHistoryContextGeneration != tr.glContextGeneration ) { + rbBackendTemporalHistoryImages[i] = globalImages->ScratchImage( + i == 0 ? "_backendTemporalHistory0" : "_backendTemporalHistory1", + &opts, TF_LINEAR, TR_CLAMP, TD_DEFAULT ); + } + if ( rbBackendTemporalHistoryImages[i] == NULL ) { + RB_ResetBackendTemporalHistory( false ); + return false; + } + if ( rbBackendTemporalHistoryTargets[i] == NULL ) { + rbBackendTemporalHistoryTargets[i] = tr.CreateRenderTexture( + rbBackendTemporalHistoryImages[i], NULL ); + if ( rbBackendTemporalHistoryTargets[i] != NULL ) { + rbBackendTemporalHistoryTargets[i]->SetDebugLabel( + i == 0 ? "backend temporal history 0" : "backend temporal history 1" ); + } + } else if ( rbBackendTemporalHistoryTargets[i]->GetWidth() != width + || rbBackendTemporalHistoryTargets[i]->GetHeight() != height ) { + if ( !tr.ResizeRenderTexture( rbBackendTemporalHistoryTargets[i], width, height ) ) { + RB_ResetBackendTemporalHistory( false ); + return false; + } + } + if ( rbBackendTemporalHistoryTargets[i] == NULL + || !rbBackendTemporalHistoryTargets[i]->EnsureDeviceHandle() ) { + RB_ResetBackendTemporalHistory( false ); + return false; + } + } + + rbBackendTemporalHistoryWidth = width; + rbBackendTemporalHistoryHeight = height; + rbBackendTemporalHistoryContextGeneration = tr.glContextGeneration; + rbBackendTemporalHistoryGeneration = generation; + rbBackendTemporalHistoryViewIdentity = viewIdentity; + return true; +} + +static bool RB_PresentBackendTemporalSpatialScene( void ) { + if ( !RB_IsSceneRenderTexture( backEnd.renderTexture ) + || backEnd.viewDef == NULL ) { + return false; + } + idImage *sceneImage = RB_TemporalColorImage( backEnd.renderTexture ); + idImage *depthImage = backEnd.renderTexture->GetDepthImage(); + const bool preserveFarDepth = + RB_ShouldPreserveSceneRenderTargetFarDepth( backEnd.viewDef ); + return RB_PresentTemporalSpatialFallback( sceneImage, + Max( 1, glConfig.vidWidth ), Max( 1, glConfig.vidHeight ), + backEnd.viewDef->temporalJitterPixels, depthImage, preserveFarDepth ); +} + +static bool RB_PresentBackendTemporalScene( void ) { + if ( !R_TemporalPresentation_TemporalAARequested() + && !R_TemporalPresentation_ScreenSpaceEffectsRequested() ) { + return false; + } + if ( !RB_IsSceneRenderTexture( backEnd.renderTexture ) + || backEnd.viewDef == NULL || backEnd.viewDef->viewEntitys == NULL ) { + return false; + } + + const temporalPresentationFrameState_t &frame = + R_TemporalPresentation_GetFrameState(); + const unsigned int generation = R_TemporalPresentation_HistoryGeneration(); + const bool captureFrame = tr.takingScreenshot + || backEnd.viewDef->temporalCaptureFrame + || frame.captureFrozen || frame.captureForcedNative; + const int outputWidth = frame.nativeWidth; + const int outputHeight = frame.nativeHeight; + const int sceneWidth = backEnd.renderTexture->GetWidth(); + const int sceneHeight = backEnd.renderTexture->GetHeight(); + if ( frame.frameNumber != backEnd.frameCount + || outputWidth != glConfig.vidWidth || outputHeight != glConfig.vidHeight + || sceneWidth != frame.sceneWidth || sceneHeight != frame.sceneHeight ) { + RB_ResetBackendTemporalHistory( false ); + RB_ClearTemporalEntityHistory(); + return RB_PresentBackendTemporalSpatialScene(); + } + if ( !captureFrame ) { + // This target is single-sample while temporal presentation owns it, and + // the root view has just completed all depth-producing world passes. + RB_StampTemporalDepthResolved( backEnd.renderTexture, + backEnd.frameCount, generation ); + } + if ( !frame.temporalAARequested ) { + resolveTemporalPresentationCommand_t command; + memset( &command, 0, sizeof( command ) ); + command.commandId = RC_RESOLVE_TEMPORAL_PRESENTATION; + command.sceneColorTarget = backEnd.renderTexture; + command.sceneDepthTarget = backEnd.renderTexture; + command.viewDef = backEnd.viewDef; + command.presentation.frameNumber = frame.frameNumber; + command.presentation.outputWidth = outputWidth; + command.presentation.outputHeight = outputHeight; + command.presentation.sceneWidth = sceneWidth; + command.presentation.sceneHeight = sceneHeight; + command.presentation.effectiveScalePercent = frame.effectiveScalePercent; + command.presentation.historyGeneration = generation; + command.advancedScreenSpace = frame.advancedScreenSpace; + idImage *sceneImage = RB_TemporalColorImage( backEnd.renderTexture ); + idImage *depthImage = backEnd.renderTexture->GetDepthImage(); + const bool preserveFarDepth = + RB_ShouldPreserveSceneRenderTargetFarDepth( backEnd.viewDef ); + if ( RB_DrawTemporalResolvePass( command, sceneImage, depthImage, + NULL, NULL, NULL, NULL, false, false, false, + preserveFarDepth, 0 ) ) { + return true; + } + return RB_PresentBackendTemporalSpatialScene(); + } + + bool historyContinuous = !captureFrame && rbBackendTemporalHistoryValid + && rbBackendTemporalHistoryGeneration == generation + && rbBackendTemporalHistoryViewIdentity == backEnd.viewDef->temporalViewIdentity + && rbBackendTemporalHistoryFrame == backEnd.frameCount - 1 + && backEnd.viewDef->temporalHistoryValid; + if ( !captureFrame && !RB_EnsureBackendTemporalHistory( outputWidth, + outputHeight, generation, backEnd.viewDef->temporalViewIdentity ) ) { + RB_ClearTemporalEntityHistory(); + return RB_PresentBackendTemporalSpatialScene(); + } + if ( !rbBackendTemporalHistoryValid ) { + historyContinuous = false; + } + + const int writeIndex = rbBackendTemporalHistoryReadIndex ^ 1; + resolveTemporalPresentationCommand_t command; + memset( &command, 0, sizeof( command ) ); + command.commandId = RC_RESOLVE_TEMPORAL_PRESENTATION; + command.sceneColorTarget = backEnd.renderTexture; + command.sceneDepthTarget = backEnd.renderTexture; + command.historyReadTarget = historyContinuous + ? rbBackendTemporalHistoryTargets[rbBackendTemporalHistoryReadIndex] : NULL; + command.historyWriteTarget = captureFrame + ? NULL : rbBackendTemporalHistoryTargets[writeIndex]; + command.viewDef = backEnd.viewDef; + command.presentation.frameNumber = frame.frameNumber; + command.presentation.outputWidth = outputWidth; + command.presentation.outputHeight = outputHeight; + command.presentation.sceneWidth = sceneWidth; + command.presentation.sceneHeight = sceneHeight; + command.presentation.effectiveScalePercent = frame.effectiveScalePercent; + command.presentation.historyGeneration = generation; + command.presentation.dynamicResolutionRequested = frame.dynamicResolutionRequested; + command.presentation.dynamicResolutionActive = frame.dynamicResolutionActive; + command.presentation.temporalAARequested = frame.temporalAARequested; + command.presentation.captureFrozen = frame.captureFrozen; + command.presentation.captureForcedNative = frame.captureForcedNative; + command.historyGeneration = generation; + command.historyValid = historyContinuous; + command.captureFrame = captureFrame; + command.feedback = frame.temporalFeedback; + command.reactiveScale = frame.temporalReactiveScale; + command.debugMode = frame.temporalDebugMode; + command.advancedScreenSpace = frame.advancedScreenSpace; + + bool presented = RB_ResolveTemporalPresentation( command ); + if ( captureFrame ) { + return presented; + } + const bool wroteHistory = presented + && rbTemporalResolveHistoryWriteFrame == backEnd.frameCount + && rbTemporalResolveHistoryWriteTarget == command.historyWriteTarget; + if ( wroteHistory ) { + rbBackendTemporalHistoryReadIndex = writeIndex; + rbBackendTemporalHistoryFrame = backEnd.frameCount; + rbBackendTemporalHistoryGeneration = generation; + rbBackendTemporalHistoryViewIdentity = backEnd.viewDef->temporalViewIdentity; + rbBackendTemporalHistoryValid = true; + } else { + rbBackendTemporalHistoryFrame = -1; + rbBackendTemporalHistoryValid = false; + } + if ( !presented ) { + presented = RB_PresentBackendTemporalSpatialScene(); + } + return presented; +} + +void RB_ApplyResolutionScaleToBackBuffer( void ) { + if ( r_skipPostProcess.GetBool() ) { + return; + } + const temporalPresentationFrameState_t &presentation = + R_TemporalPresentation_GetFrameState(); + if ( presentation.dynamicResolutionRequested + || presentation.temporalAARequested + || AdvancedScreenSpaceCore_Requested( + presentation.advancedScreenSpace ) ) { + // Temporal presentation owns scene scaling before native HUD/menu draws. + // A UI-only frame has no scene-present marker, so the legacy swap-tail + // filter must still stay out of the native backbuffer. + return; + } + if ( rbSceneScalePresentedFrame == backEnd.frameCount ) { + // The 3D scene was already scaled into the native back buffer before + // later 2D commands. Re-filtering here would scale the HUD/menu too. + return; + } + + const int scalePercent = RB_RequestedScreenFraction(); + if ( scalePercent >= RB_SCREEN_FRACTION_NATIVE ) { + return; + } + + int mode = idMath::ClampInt( 0, 2, r_resolutionScaleMode.GetInteger() ); + if ( mode == 0 ) { + // Legacy path: BeginFrame crop mode without fullscreen upscale. + return; + } + + const int viewportWidth = glConfig.vidWidth; + const int viewportHeight = glConfig.vidHeight; + if ( viewportWidth <= 0 || viewportHeight <= 0 ) { + return; + } + + const int sourceWidth = idMath::ClampInt( 1, viewportWidth, + idMath::Ftoi( static_cast( viewportWidth ) * ( static_cast( scalePercent ) * 0.01f ) + 0.5f ) ); + const int sourceHeight = idMath::ClampInt( 1, viewportHeight, + idMath::Ftoi( static_cast( viewportHeight ) * ( static_cast( scalePercent ) * 0.01f ) + 0.5f ) ); + if ( sourceWidth <= 0 || sourceHeight <= 0 ) { + return; + } + + if ( !glConfig.GLSLProgramAvailable ) { + return; + } + + RB_InitResolutionScaleStage(); + if ( !R_ValidateGLSLProgram( &rbResolutionScaleStage ) ) { + return; } idImage *sceneImage = globalImages->currentRenderImage; @@ -5412,6 +6971,23 @@ static float RB_SoftParticleVertexColorModeValue( stageVertexColor_t vertexColor } } +// The current-depth image is only ever sampled with depth comparison disabled, +// and texture parameters persist on the texture object across respecification, +// so set them once per storage generation instead of on every draw that samples +// it. Nothing sets GL_TEXTURE_COMPARE_MODE to anything but GL_NONE on this image. +static void RB_EnsureCurrentDepthSampledPlain( idImage *depthImage ) { + static const idImage *lastImage = NULL; + static uint64_t lastGeneration = 0; + const uint64_t generation = depthImage->GetStorageGeneration(); + if ( depthImage == lastImage && generation == lastGeneration ) { + return; + } + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE ); + glTexParameteri( GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE ); + lastImage = depthImage; + lastGeneration = generation; +} + static bool RB_TryDrawSoftParticleStage( const drawSurf_t *surf, const shaderStage_t *pStage, const float *regs, const srfTriangles_t *tri, idDrawVert *ac, int stage, const float color[4] ) { if ( !RB_SoftParticleStageEligible( surf, pStage ) ) { return false; @@ -5465,8 +7041,7 @@ static bool RB_TryDrawSoftParticleStage( const drawSurf_t *surf, const shaderSta GL_SelectTexture( 1 ); depthImage->Bind(); - glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE ); - glTexParameteri( GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE ); + RB_EnsureCurrentDepthSampledPlain( depthImage ); GL_SelectTexture( 0 ); if ( glConfig.ARBVertexProgramAvailable ) { @@ -5578,6 +7153,8 @@ static bool rbRVSpecialBlurPrepared = false; static bool rbRVSpecialALPrepared = false; static bool rbRVSpecialCaptureUsesDiffuseImage = false; static int rbRVSpecialActiveMask = 0; +static const viewDef_t *rbRVSpecialCommandView = NULL; +static int rbRVSpecialCommandFrame = -1; static void RB_InitRVSpecialStages( void ) { if ( rbRVSpecialStagesInitialized ) { @@ -5762,7 +7339,14 @@ static void RB_DestroyPostProcessRenderTexture( idRenderTexture *&renderTexture } void RB_ShutdownScenePostProcess( void ) { + RB_FreeTemporalResolveProgram(); RB_FreeSceneDepthAwarePresentProgram(); + RB_ResetBackendTemporalHistory( true ); + rbTemporalResolveHistoryWriteFrame = -1; + rbTemporalResolveHistoryWriteTarget = NULL; + rbTemporalResolveNeedsReprime = false; + rbTemporalResolveRejectedGeneration = 0; + RB_ClearTemporalDepthStamps(); RB_FreeGLSLProgram( &rbLightGridIndirectStage ); RB_FreeGLSLProgram( &rbPlayerRimlightStage ); @@ -5801,10 +7385,12 @@ void RB_ShutdownScenePostProcess( void ) { rbSceneRenderTargetPreserveDepthHeight = 0; RB_ResetMotionBlurHistory(); + RB_ClearTemporalEntityHistory(); RB_DestroyPostProcessRenderTexture( rbMotionVectorRenderTexture ); rbMotionVectorImage = NULL; rbMotionVectorPreviousState = NULL; rbMotionVectorDrewSurface = false; + rbMotionVectorMissedSurface = false; for ( int level = 0; level < RB_BLOOM_MAX_LEVELS; level++ ) { for ( int ping = 0; ping < 2; ping++ ) { @@ -6359,6 +7945,8 @@ void RB_DrawSpecialEffects( const void *data ) { const drawSurfsCommand_t *cmd = (const drawSurfsCommand_t *)data; backEnd.viewDef = cmd->viewDef; + rbRVSpecialCommandView = cmd->viewDef; + rbRVSpecialCommandFrame = backEnd.frameCount; rbRVSpecialBlurPrepared = false; rbRVSpecialALPrepared = false; rbRVSpecialActiveMask = tr.specialEffectsEnabled; @@ -6405,6 +7993,14 @@ static void RB_DisplaySpecialEffects( const viewEntity_t *viewEnts, bool prePass bool restoredView = false; if ( RB_PrepareRVSpecialBlurImage() ) { restoredView |= RB_CompositeRVSpecialBlur(); + if ( restoredView && r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRavenEffectsView( + rbRVSpecialCommandView ) != NULL ) { + (void)R_ClassicSpecialFrameDomain_RecordOwned( + rbRVSpecialCommandView, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, SPECIAL_EFFECT_BLUR ); + } } if ( restoredView ) { RB_RVSpecialRestoreDrawingView(); @@ -6414,6 +8010,7 @@ static void RB_DisplaySpecialEffects( const viewEntity_t *viewEnts, bool prePass } bool restoredView = false; + int drawnALLights = 0; if ( viewEnts != NULL && ( rbRVSpecialActiveMask & SPECIAL_EFFECT_AL ) != 0 && rbRVSpecialALPrepared && tr.primaryWorld != NULL ) { RB_InitRVSpecialStages(); @@ -6444,7 +8041,10 @@ static void RB_DisplaySpecialEffects( const viewEntity_t *viewEnts, bool prePass } lightColor.Normalize(); - RB_DrawRVSpecialALLight( light->globalLightOrigin, 300.0f, lightColor ); + if ( RB_DrawRVSpecialALLight( light->globalLightOrigin, 300.0f, + lightColor ) ) { + drawnALLights++; + } } glUseProgramObjectARB( 0 ); @@ -6459,6 +8059,14 @@ static void RB_DisplaySpecialEffects( const viewEntity_t *viewEnts, bool prePass if ( restoredView ) { RB_RVSpecialRestoreDrawingView(); } + if ( drawnALLights > 0 && r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRavenEffectsView( + rbRVSpecialCommandView ) != NULL ) { + (void)R_ClassicSpecialFrameDomain_RecordOwned( + rbRVSpecialCommandView, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, SPECIAL_EFFECT_AL ); + } } void RB_T_FillDepthBuffer( const drawSurf_t *surf ) { @@ -7119,6 +8727,1665 @@ void RB_SetProgramEnvironmentSpace( void ) { glProgramEnvParameter4fvARB( GL_VERTEX_PROGRAM_ARB, 8, parm ); } +/* +=============================================================================== + + Shared classic GUI OpenGL consumer + + The shared domain is view-atomic. Nothing below may issue a draw until every + drawable surface and every evaluated pass has been copied into the prepared + plan. The legacy draw-surface pointer is used only for vertex/index caches and + optional baked stage-color data; material stages and shader registers are not + consulted here. + +=============================================================================== +*/ + +enum rbSharedGuiGLReject_t { + RB_SHARED_GUI_GL_REJECT_VIEW = 1, + RB_SHARED_GUI_GL_REJECT_MUTATED_VIEW, + RB_SHARED_GUI_GL_REJECT_CAPACITY, + RB_SHARED_GUI_GL_REJECT_DRAW_RANGE, + RB_SHARED_GUI_GL_REJECT_GEOMETRY, + RB_SHARED_GUI_GL_REJECT_VERTEX_CACHE, + RB_SHARED_GUI_GL_REJECT_INDEX_CACHE, + RB_SHARED_GUI_GL_REJECT_PASS_RANGE, + RB_SHARED_GUI_GL_REJECT_PASS_STATE, + RB_SHARED_GUI_GL_REJECT_TEXTURE, + RB_SHARED_GUI_GL_REJECT_TEXTURE_CAPACITY, + RB_SHARED_GUI_GL_REJECT_COVERAGE +}; + +typedef struct rbSharedGuiGLPreparedPass_s { + const rendererEvaluatedMaterialPass_t *pass; + idImage *image; + textureFilter_t filter; + textureRepeat_t repeat; + int stateBits; + int cullType; + GLenum alphaFunction; +} rbSharedGuiGLPreparedPass_t; + +typedef struct rbSharedGuiGLPreparedDraw_s { + const classicGuiDomainDraw_t *draw; + const drawSurf_t *surf; + const srfTriangles_t *tri; + int firstPass; + int passCount; +} rbSharedGuiGLPreparedDraw_t; + +typedef struct rbSharedGuiGLPreparedView_s { + const classicGuiDomainView_t *view; + int drawCount; + int passCount; + int drawablePasses; + int noopPasses; + bool seenSourceSurfaces[ SCENE_PACKET_MAX_DRAWS ]; + rbSharedGuiGLPreparedDraw_t draws[ CLASSIC_GUI_DOMAIN_MAX_DRAWS ]; + rbSharedGuiGLPreparedPass_t passes[ CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES ]; +} rbSharedGuiGLPreparedView_t; + +// The renderer backend is single-threaded. Keeping this plan out of the stack +// also makes the fixed domain capacities explicit at the backend boundary. +static rbSharedGuiGLPreparedView_t rbSharedGuiGLPreparedView; + +static int RB_SharedGuiGLFailureDetail( rbSharedGuiGLReject_t reason, + int drawIndex = -1, int passIndex = -1 ) { + const int drawDetail = drawIndex >= 0 ? Min( drawIndex + 1, 9999 ) : 0; + const int passDetail = passIndex >= 0 ? Min( passIndex + 1, 255 ) : 0; + return static_cast( reason ) * 1000000 + drawDetail * 256 + passDetail; +} + +static bool RB_SharedGuiGLFloatValid( float value ) { + return value == value && value >= -FLT_MAX && value <= FLT_MAX; +} + +static bool RB_SharedGuiGLMatrixValid( const float *matrix, int count ) { + if ( matrix == NULL || count <= 0 ) { + return false; + } + for ( int i = 0; i < count; ++i ) { + if ( !RB_SharedGuiGLFloatValid( matrix[i] ) ) { + return false; + } + } + return true; +} + +static bool RB_SharedGuiGLCacheValid( const vertCache_t *cache, + bool indexBuffer, int requiredBytes ) { + if ( cache == NULL || requiredBytes <= 0 || cache->tag == TAG_FREE + || cache->indexBuffer != indexBuffer || cache->offset < 0 + || cache->size < requiredBytes ) { + return false; + } + if ( cache->vbo != 0 ) { + return glConfig.ARBVertexBufferObjectAvailable && cache->virtMem == NULL; + } + return cache->virtMem != NULL; +} + +static bool RB_SharedGuiGLMapSourceBlend( rendererBlendFactor_t factor, + int &stateBits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: + stateBits |= GLS_SRCBLEND_ZERO; + return true; + case RENDERER_BLEND_ONE: + stateBits |= GLS_SRCBLEND_ONE; + return true; + case RENDERER_BLEND_SRC_COLOR: + stateBits |= GLS_SRCBLEND_SRC_COLOR; + return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: + stateBits |= GLS_SRCBLEND_ONE_MINUS_SRC_COLOR; + return true; + case RENDERER_BLEND_DST_COLOR: + stateBits |= GLS_SRCBLEND_DST_COLOR; + return true; + case RENDERER_BLEND_ONE_MINUS_DST_COLOR: + stateBits |= GLS_SRCBLEND_ONE_MINUS_DST_COLOR; + return true; + case RENDERER_BLEND_SRC_ALPHA: + stateBits |= GLS_SRCBLEND_SRC_ALPHA; + return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: + stateBits |= GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA; + return true; + case RENDERER_BLEND_DST_ALPHA: + stateBits |= GLS_SRCBLEND_DST_ALPHA; + return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: + stateBits |= GLS_SRCBLEND_ONE_MINUS_DST_ALPHA; + return true; + case RENDERER_BLEND_SRC_ALPHA_SATURATE: + stateBits |= GLS_SRCBLEND_ALPHA_SATURATE; + return true; + default: + return false; + } +} + +static bool RB_SharedGuiGLMapDestinationBlend( rendererBlendFactor_t factor, + int &stateBits ) { + switch ( factor ) { + case RENDERER_BLEND_ZERO: + stateBits |= GLS_DSTBLEND_ZERO; + return true; + case RENDERER_BLEND_ONE: + stateBits |= GLS_DSTBLEND_ONE; + return true; + case RENDERER_BLEND_SRC_COLOR: + stateBits |= GLS_DSTBLEND_SRC_COLOR; + return true; + case RENDERER_BLEND_ONE_MINUS_SRC_COLOR: + stateBits |= GLS_DSTBLEND_ONE_MINUS_SRC_COLOR; + return true; + case RENDERER_BLEND_SRC_ALPHA: + stateBits |= GLS_DSTBLEND_SRC_ALPHA; + return true; + case RENDERER_BLEND_ONE_MINUS_SRC_ALPHA: + stateBits |= GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA; + return true; + case RENDERER_BLEND_DST_ALPHA: + stateBits |= GLS_DSTBLEND_DST_ALPHA; + return true; + case RENDERER_BLEND_ONE_MINUS_DST_ALPHA: + stateBits |= GLS_DSTBLEND_ONE_MINUS_DST_ALPHA; + return true; + default: + return false; + } +} + +static bool RB_SharedGuiGLMapDepth( const rendererDepthState_t &depth, + bool inWorld, int &stateBits ) { + // Root GUI disables depth as a view-domain rule. GUI emitted onto a 3D + // surface instead retains the classic ambient walk's depth contract. + if ( depth.testEnabled != inWorld ) { + return false; + } + if ( !depth.writeEnabled ) { + stateBits |= GLS_DEPTHMASK; + } + switch ( depth.compareOperation ) { + case RENDERER_COMPARE_LESS_OR_EQUAL: + stateBits |= GLS_DEPTHFUNC_LESS; + return true; + case RENDERER_COMPARE_EQUAL: + stateBits |= GLS_DEPTHFUNC_EQUAL; + return true; + case RENDERER_COMPARE_ALWAYS: + stateBits |= GLS_DEPTHFUNC_ALWAYS; + return true; + default: + return false; + } +} + +static bool RB_SharedGuiGLMapCull( rendererCullMode_t cull, int &cullType ) { + switch ( cull ) { + case RENDERER_CULL_NONE: + cullType = CT_TWO_SIDED; + return true; + case RENDERER_CULL_FRONT: + cullType = CT_FRONT_SIDED; + return true; + case RENDERER_CULL_BACK: + cullType = CT_BACK_SIDED; + return true; + default: + return false; + } +} + +static bool RB_SharedGuiGLMapAlphaCompare( rendererCompareOp_t compare, + GLenum &alphaFunction ) { + switch ( compare ) { + case RENDERER_COMPARE_NEVER: + alphaFunction = GL_NEVER; + return true; + case RENDERER_COMPARE_LESS: + alphaFunction = GL_LESS; + return true; + case RENDERER_COMPARE_EQUAL: + alphaFunction = GL_EQUAL; + return true; + case RENDERER_COMPARE_LESS_OR_EQUAL: + alphaFunction = GL_LEQUAL; + return true; + case RENDERER_COMPARE_GREATER: + alphaFunction = GL_GREATER; + return true; + case RENDERER_COMPARE_NOT_EQUAL: + alphaFunction = GL_NOTEQUAL; + return true; + case RENDERER_COMPARE_GREATER_OR_EQUAL: + alphaFunction = GL_GEQUAL; + return true; + case RENDERER_COMPARE_ALWAYS: + alphaFunction = GL_ALWAYS; + return true; + default: + return false; + } +} + +static bool RB_SharedGuiGLBuildState( const rendererEvaluatedMaterialPass_t &pass, + bool inWorld, int &stateBits, GLenum &alphaFunction, int &cullType ) { + stateBits = 0; + alphaFunction = GL_ALWAYS; + cullType = CT_FRONT_SIDED; + + if ( pass.kind != ( inWorld ? RENDERER_MATERIAL_PASS_SURFACE + : RENDERER_MATERIAL_PASS_GUI ) + || ( inWorld + ? pass.programFamily != RENDERER_PROGRAM_FIXED + : ( pass.programFamily != RENDERER_PROGRAM_GUI + && pass.programFamily != RENDERER_PROGRAM_FIXED ) ) + || pass.programKey != 0 || pass.texgen != RENDERER_TEXGEN_EXPLICIT + || pass.textureSemantic != RENDERER_TEXTURE_DIFFUSE + || pass.blend.colorOperation != RENDERER_BLEND_OP_ADD + || pass.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || pass.blend.sourceAlpha != pass.blend.sourceColor + || pass.blend.destinationAlpha != pass.blend.destinationColor ) { + return false; + } + const bool replacementBlend = pass.blend.sourceColor == RENDERER_BLEND_ONE + && pass.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( pass.blend.enabled == replacementBlend ) { + return false; + } + if ( !RB_SharedGuiGLMapSourceBlend( pass.blend.sourceColor, stateBits ) + || !RB_SharedGuiGLMapDestinationBlend( + pass.blend.destinationColor, stateBits ) + || !RB_SharedGuiGLMapDepth( pass.depth, inWorld, stateBits ) + || !RB_SharedGuiGLMapCull( pass.cull, cullType ) ) { + return false; + } + + if ( ( pass.colorWriteMask + & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 ) { + return false; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + stateBits |= GLS_REDMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + stateBits |= GLS_GREENMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + stateBits |= GLS_BLUEMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + stateBits |= GLS_ALPHAMASK; + } + + return RB_SharedGuiGLMapAlphaCompare( + pass.alphaTestCompareOperation, alphaFunction ); +} + +static bool RB_SharedGuiGLPassDispositionValid( + const rendererEvaluatedMaterialPass_t &pass ) { + if ( pass.active != ( pass.condition != 0.0f ) ) { + return false; + } + switch ( pass.disposition ) { + case RENDERER_MATERIAL_PASS_DRAW: + case RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND: + case RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE: + case RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA: + return pass.active; + case RENDERER_MATERIAL_PASS_INACTIVE_CONDITION: + return !pass.active; + default: + return false; + } +} + +static bool RB_SharedGuiGLSourceNoopValid( + const classicGuiDomainDraw_t &draw ) { + switch ( draw.sourceSurface ) { + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NULL_SURFACE: + if ( draw.legacyDrawSurf != NULL ) { + return false; + } + break; + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_NO_AMBIENT: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_PORTAL_SKY: + case CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: + if ( draw.legacyDrawSurf == NULL ) { + return false; + } + break; + default: + return false; + } + return !draw.packetBacked && draw.drawPacketIndex == -1 + && draw.firstEvaluatedPass == -1 && draw.evaluatedPassCount == 0 + && draw.activePassCount == 0 && draw.drawablePassCount == 0 + && draw.inactivePassCount == 0 && draw.activeNoopPassCount == 0 + && draw.noopPassCount == 0; +} + +static bool RB_SharedGuiGLTextureBindingValid( + const rendererEvaluatedMaterialPass_t &pass, + const materialResourceTextureBinding_t *binding, idImage *&image ) { + image = NULL; + if ( binding == NULL || pass.textureResourceId == 0 + || binding != R_MaterialResourceTable_ResolveTextureResource( + pass.textureResourceId ) + || binding->textureResourceId != pass.textureResourceId + || binding->image == NULL || binding->stageIndex != pass.sourceStageIndex + || binding->textureHandle == 0 || !binding->loaded + || binding->defaulted || binding->missing + || binding->filter < TF_LINEAR || binding->filter > TF_DEFAULT + || binding->repeat < TR_REPEAT || binding->repeat > TR_CLAMP_TO_ZERO_ALPHA ) { + return false; + } + + image = const_cast( binding->image ); + const idImageOpts &opts = image->GetOpts(); + return image->IsLoaded() && !image->IsDefaulted() + && image->GetDeviceHandle() == binding->textureHandle + && opts.textureType == TT_2D && opts.width > 0 && opts.height > 0; +} + +static bool RB_SharedGuiGLPreflight( const viewDef_t *viewDef, + const classicGuiDomainView_t &view, int &failureDetail ) { + rbSharedGuiGLPreparedView_t &prepared = rbSharedGuiGLPreparedView; + // Clear the plan header and the seen-surface bitmap. The draws/passes pools + // below them are zeroed entry-by-entry as slots are claimed, so no byte of + // them is read before it is written; clearing all ~0.5MB per view per frame + // was pure memory-bandwidth waste. + memset( &prepared, 0, offsetof( rbSharedGuiGLPreparedView_t, draws ) ); + prepared.view = &view; + const bool inWorld = view.scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD; + + if ( view.viewDef != viewDef || !view.ready + || ( !inWorld && viewDef->viewEntitys != NULL ) + || ( inWorld && viewDef->viewEntitys == NULL ) + || ( !inWorld && view.sourceSurfaceCount != viewDef->numDrawSurfs ) + || ( inWorld && view.sourceSurfaceCount > viewDef->numDrawSurfs ) + || view.sourceSurfaceCount < 0 + || view.sourceSurfaceCount > SCENE_PACKET_MAX_DRAWS + || view.drawableSurfaceCount < 0 + || view.drawableSurfaceCount > view.sourceSurfaceCount + || view.noopSurfaceCount + != view.sourceSurfaceCount - view.drawableSurfaceCount + || view.drawCount < 0 || view.drawCount > CLASSIC_GUI_DOMAIN_MAX_DRAWS + || view.evaluatedPassCount < 0 + || view.evaluatedPassCount > CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( RB_SHARED_GUI_GL_REJECT_VIEW ); + return false; + } + + if ( viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || ( !inWorld && ( viewDef->renderWorld != NULL + || viewDef->viewLights != NULL ) ) + || viewDef->numOutlineDrawSurfs != 0 || backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL + || r_showOverDraw.GetInteger() != 0 || r_singleTriangle.GetBool() ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_MUTATED_VIEW ); + return false; + } + + if ( glConfig.maxTextureUnits < 1 || glConfig.maxTextureImageUnits < 1 + || glConfig.maxTextureCoords < 1 || !glConfig.isInitialized + || globalImages == NULL ) { + failureDetail = RB_SharedGuiGLFailureDetail( RB_SHARED_GUI_GL_REJECT_CAPACITY ); + return false; + } + + if ( view.viewportX1 != viewDef->viewport.x1 + || view.viewportY1 != viewDef->viewport.y1 + || view.viewportX2 != viewDef->viewport.x2 + || view.viewportY2 != viewDef->viewport.y2 + || view.scissorX1 != viewDef->scissor.x1 + || view.scissorY1 != viewDef->scissor.y1 + || view.scissorX2 != viewDef->scissor.x2 + || view.scissorY2 != viewDef->scissor.y2 + || memcmp( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ) != 0 + || !RB_SharedGuiGLMatrixValid( view.projectionMatrix, 16 ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_MUTATED_VIEW ); + return false; + } + + int sourceSurfacePrevious = -1; + int evaluatedPasses = 0; + int activePasses = 0; + int inactivePasses = 0; + int activeNoopPasses = 0; + int sourceNoopSurfaces = 0; + for ( int drawIndex = 0; drawIndex < view.drawCount; ++drawIndex ) { + const classicGuiDomainDraw_t *draw = R_ClassicGuiDomain_ViewDraw( + view, drawIndex ); + if ( draw == NULL || draw->sourceSurfaceIndex < 0 + || draw->sourceSurfaceIndex >= viewDef->numDrawSurfs + || draw->sourceSurfaceIndex >= SCENE_PACKET_MAX_DRAWS + || draw->sourceSurfaceIndex <= sourceSurfacePrevious + || prepared.seenSourceSurfaces[draw->sourceSurfaceIndex] + || viewDef->drawSurfs[draw->sourceSurfaceIndex] != draw->legacyDrawSurf ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + if ( inWorld && ( draw->legacyDrawSurf == NULL + || ( draw->legacyDrawSurf->dsFlags & DSF_IN_WORLD_GUI ) == 0 ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + prepared.seenSourceSurfaces[draw->sourceSurfaceIndex] = true; + sourceSurfacePrevious = draw->sourceSurfaceIndex; + if ( draw->sourceSurface != CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE ) { + if ( !RB_SharedGuiGLSourceNoopValid( *draw ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + sourceNoopSurfaces++; + continue; + } + if ( !draw->packetBacked || draw->legacyDrawSurf == NULL ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + + const drawSurf_t *surf = draw->legacyDrawSurf; + const srfTriangles_t *tri = surf->geo; + if ( tri == NULL || draw->firstIndex != 0 || draw->vertexOffset != 0 + || draw->tableGeneration != view.tableGeneration + || draw->hasAmbientCache != ( tri->ambientCache != NULL ) + || draw->hasIndexCache != ( tri->indexCache != NULL ) + || draw->vertexCount <= 0 || draw->indexCount <= 0 + || draw->indexCount % 3 != 0 + || tri->numVerts != draw->vertexCount + || tri->numIndexes != draw->indexCount + || draw->vertexCount > idMath::INT_MAX / static_cast( sizeof( idDrawVert ) ) + || draw->indexCount > idMath::INT_MAX / static_cast( sizeof( glIndex_t ) ) + || !RB_SharedGuiGLMatrixValid( draw->modelViewMatrix, 16 ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_GEOMETRY, drawIndex ); + return false; + } + const long long scissorWidth = static_cast( draw->scissorX2 ) + - draw->scissorX1 + 1; + const long long scissorHeight = static_cast( draw->scissorY2 ) + - draw->scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > idMath::INT_MAX || scissorHeight > idMath::INT_MAX ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_GEOMETRY, drawIndex ); + return false; + } + + if ( !RB_EnsurePackedClassicDrawCaches( surf, false, true ) + || !RB_SharedGuiGLCacheValid( tri->ambientCache, false, + draw->vertexCount * static_cast( sizeof( idDrawVert ) ) ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_VERTEX_CACHE, drawIndex ); + return false; + } + R_TouchVertexCache( tri->ambientCache ); + + if ( tri->indexCache != NULL ) { + if ( !RB_SharedGuiGLCacheValid( tri->indexCache, true, + draw->indexCount * static_cast( sizeof( glIndex_t ) ) ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_INDEX_CACHE, drawIndex ); + return false; + } + R_TouchVertexCache( tri->indexCache ); + } + if ( ( !r_useIndexBuffers.GetBool() || tri->indexCache == NULL ) + && tri->indexes == NULL ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_INDEX_CACHE, drawIndex ); + return false; + } + + if ( surf->decalColorCache != NULL ) { + const long long decalBytes = static_cast( surf->decalColorOffset ) + + static_cast( surf->decalColorStride ) + * surf->decalColorStageCount; + if ( surf->decalColorOffset < 0 || surf->decalColorStride < draw->vertexCount * 4 + || surf->decalColorStageCount <= 0 || decalBytes <= 0 + || decalBytes > idMath::INT_MAX + || !RB_SharedGuiGLCacheValid( surf->decalColorCache, false, + static_cast( decalBytes ) ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_VERTEX_CACHE, drawIndex ); + return false; + } + R_TouchVertexCache( surf->decalColorCache ); + } + + if ( draw->evaluatedPassCount < 0 + || draw->evaluatedPassCount > static_cast( + RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) + || prepared.passCount > CLASSIC_GUI_DOMAIN_MAX_EVALUATED_PASSES + - draw->evaluatedPassCount ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_PASS_RANGE, drawIndex ); + return false; + } + + rbSharedGuiGLPreparedDraw_t &preparedDraw = prepared.draws[prepared.drawCount++]; + std::memset( &preparedDraw, 0, sizeof( preparedDraw ) ); + preparedDraw.draw = draw; + preparedDraw.surf = surf; + preparedDraw.tri = tri; + preparedDraw.firstPass = prepared.passCount; + preparedDraw.passCount = draw->evaluatedPassCount; + + int drawActivePasses = 0; + int drawInactivePasses = 0; + int drawDrawablePasses = 0; + int drawActiveNoopPasses = 0; + for ( int passIndex = 0; passIndex < draw->evaluatedPassCount; ++passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicGuiDomain_DrawPass( *draw, passIndex ); + const materialResourceTextureBinding_t *binding = + R_ClassicGuiDomain_DrawPassTexture( *draw, passIndex ); + if ( pass == NULL || pass->order != static_cast( passIndex ) + || pass->sourceStageIndex < 0 + || pass->kind != ( inWorld + ? RENDERER_MATERIAL_PASS_SURFACE + : RENDERER_MATERIAL_PASS_GUI ) + || pass->textureSemantic != RENDERER_TEXTURE_DIFFUSE + || pass->texgen != RENDERER_TEXGEN_EXPLICIT + || ( pass->programFamily != RENDERER_PROGRAM_GUI + && pass->programFamily != RENDERER_PROGRAM_FIXED ) + || pass->programKey != 0 + || pass->vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || pass->vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || !RB_SharedGuiGLPassDispositionValid( *pass ) + || !RB_SharedGuiGLFloatValid( pass->condition ) + || !RB_SharedGuiGLMatrixValid( pass->color, 4 ) + || !RB_SharedGuiGLMatrixValid( pass->textureMatrix, 6 ) + || !RB_SharedGuiGLFloatValid( pass->alphaTest ) + || !RB_SharedGuiGLFloatValid( pass->polygonOffsetFactor ) + || !RB_SharedGuiGLFloatValid( pass->polygonOffsetUnits ) + || static_cast( pass->textureMatrix[2] ) + < -static_cast( idMath::INT_MAX ) - 1.0 + || static_cast( pass->textureMatrix[2] ) + > static_cast( idMath::INT_MAX ) + || static_cast( pass->textureMatrix[5] ) + < -static_cast( idMath::INT_MAX ) - 1.0 + || static_cast( pass->textureMatrix[5] ) + > static_cast( idMath::INT_MAX ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_PASS_STATE, drawIndex, passIndex ); + return false; + } + + rbSharedGuiGLPreparedPass_t &preparedPass = + prepared.passes[prepared.passCount]; + std::memset( &preparedPass, 0, sizeof( preparedPass ) ); + if ( !RB_SharedGuiGLBuildState( *pass, inWorld, + preparedPass.stateBits, + preparedPass.alphaFunction, preparedPass.cullType ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_PASS_STATE, drawIndex, passIndex ); + return false; + } + + if ( !RB_SharedGuiGLTextureBindingValid( + *pass, binding, preparedPass.image ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_TEXTURE, drawIndex, passIndex ); + return false; + } + preparedPass.pass = pass; + preparedPass.filter = binding->filter; + preparedPass.repeat = binding->repeat; + + if ( pass->disposition == RENDERER_MATERIAL_PASS_DRAW ) { + drawDrawablePasses++; + prepared.drawablePasses++; + } + if ( pass->active ) { + drawActivePasses++; + activePasses++; + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + drawActiveNoopPasses++; + activeNoopPasses++; + } + } else { + drawInactivePasses++; + inactivePasses++; + } + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + prepared.noopPasses++; + } + if ( pass->disposition == RENDERER_MATERIAL_PASS_DRAW + && pass->vertexColor != RENDERER_VERTEX_COLOR_IGNORE + && ( glConfig.maxTextureUnits < 2 + || glConfig.maxTextureImageUnits < 2 + || glConfig.maxTextureCoords < 2 + || globalImages->whiteImage == NULL + || !globalImages->whiteImage->IsLoaded() + || globalImages->whiteImage->IsDefaulted() + || globalImages->whiteImage->GetOpts().textureType != TT_2D + || globalImages->whiteImage->GetDeviceHandle() == 0 ) ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_TEXTURE_CAPACITY, + drawIndex, passIndex ); + return false; + } + + prepared.passCount++; + evaluatedPasses++; + } + + if ( drawActivePasses != draw->activePassCount + || drawInactivePasses != draw->inactivePassCount + || drawDrawablePasses != draw->drawablePassCount + || drawActiveNoopPasses != draw->activeNoopPassCount + || draw->noopPassCount != drawInactivePasses + drawActiveNoopPasses ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_COVERAGE, drawIndex ); + return false; + } + } + + if ( prepared.drawCount != view.drawableSurfaceCount + || sourceNoopSurfaces != view.noopSurfaceCount + || evaluatedPasses != view.evaluatedPassCount + || activePasses != view.activePassCount + || inactivePasses != view.inactivePassCount + || prepared.drawablePasses != view.drawablePassCount + || activeNoopPasses != view.activeNoopPassCount + || prepared.noopPasses != view.noopPassCount ) { + failureDetail = RB_SharedGuiGLFailureDetail( + RB_SHARED_GUI_GL_REJECT_COVERAGE ); + return false; + } + + return true; +} + +static void RB_SharedGuiGLLoadTextureMatrix( + const rendererEvaluatedMaterialPass_t &pass ) { + float matrix[16] = { + pass.textureMatrix[0], pass.textureMatrix[3], 0.0f, 0.0f, + pass.textureMatrix[1], pass.textureMatrix[4], 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + pass.textureMatrix[2], pass.textureMatrix[5], 0.0f, 1.0f + }; + // Match RB_GetShaderTextureMatrix exactly so large scrolling offsets retain + // classic fixed-function precision and wrapping behavior. + if ( matrix[12] < -40.0f || matrix[12] > 40.0f ) { + matrix[12] -= static_cast( matrix[12] ); + } + if ( matrix[13] < -40.0f || matrix[13] > 40.0f ) { + matrix[13] -= static_cast( matrix[13] ); + } + glMatrixMode( GL_TEXTURE ); + glLoadMatrixf( matrix ); + glMatrixMode( GL_MODELVIEW ); +} + +static bool RB_SharedGuiGLHasBakedStageColor( const drawSurf_t *surf, + int sourceStageIndex ) { + return surf->decalColorCache != NULL && sourceStageIndex >= 0 + && sourceStageIndex < surf->decalColorStageCount + && surf->decalColorStride > 0; +} + +static void RB_SharedGuiGLPrepareVertexColor( const drawSurf_t *surf, + idDrawVert *ambientVertices, const rendererEvaluatedMaterialPass_t &pass ) { + if ( pass.vertexColor == RENDERER_VERTEX_COLOR_IGNORE ) { + glDisableClientState( GL_COLOR_ARRAY ); + glColor4fv( pass.color ); + return; + } + + RB_SetStageVertexColorPointer( surf, pass.sourceStageIndex, ambientVertices ); + glEnableClientState( GL_COLOR_ARRAY ); + if ( pass.vertexColor == RENDERER_VERTEX_COLOR_INVERSE_MODULATE ) { + GL_TexEnv( GL_COMBINE_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_MODULATE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_TEXTURE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB, GL_PRIMARY_COLOR_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB_ARB, GL_SRC_COLOR ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB_ARB, GL_ONE_MINUS_SRC_COLOR ); + glTexEnvi( GL_TEXTURE_ENV, GL_RGB_SCALE_ARB, 1 ); + glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA_ARB, GL_MODULATE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA_ARB, GL_TEXTURE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_ALPHA_ARB, GL_PRIMARY_COLOR_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA_ARB, GL_SRC_ALPHA ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_ALPHA_ARB, GL_SRC_ALPHA ); + glTexEnvi( GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1 ); + } + + if ( !RB_SharedGuiGLHasBakedStageColor( surf, pass.sourceStageIndex ) + && ( pass.color[0] != 1.0f || pass.color[1] != 1.0f + || pass.color[2] != 1.0f || pass.color[3] != 1.0f ) ) { + GL_SelectTexture( 1 ); + globalImages->whiteImage->Bind(); + GL_TexEnv( GL_COMBINE_ARB ); + glTexEnvfv( GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, pass.color ); + glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_MODULATE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_PREVIOUS_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB, GL_CONSTANT_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB_ARB, GL_SRC_COLOR ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB_ARB, GL_SRC_COLOR ); + glTexEnvi( GL_TEXTURE_ENV, GL_RGB_SCALE_ARB, 1 ); + glTexEnvi( GL_TEXTURE_ENV, GL_COMBINE_ALPHA_ARB, GL_MODULATE ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE0_ALPHA_ARB, GL_PREVIOUS_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_SOURCE1_ALPHA_ARB, GL_CONSTANT_ARB ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA_ARB, GL_SRC_ALPHA ); + glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_ALPHA_ARB, GL_SRC_ALPHA ); + glTexEnvi( GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1 ); + GL_SelectTexture( 0 ); + } +} + +static void RB_SharedGuiGLFinishPass( + const rendererEvaluatedMaterialPass_t &pass ) { + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + if ( pass.alphaTestEnabled ) { + glDisable( GL_ALPHA_TEST ); + } + if ( pass.polygonOffsetEnabled ) { + glDisable( GL_POLYGON_OFFSET_FILL ); + } + if ( pass.vertexColor != RENDERER_VERTEX_COLOR_IGNORE ) { + glDisableClientState( GL_COLOR_ARRAY ); + GL_SelectTexture( 1 ); + GL_TexEnv( GL_MODULATE ); + globalImages->BindNull(); + GL_SelectTexture( 0 ); + GL_TexEnv( GL_MODULATE ); + } +} + +static void RB_SharedGuiGLRestoreState( bool inWorld ) { + glDisable( GL_ALPHA_TEST ); + glDisable( GL_POLYGON_OFFSET_FILL ); + glDisable( GL_SAMPLE_ALPHA_TO_COVERAGE ); + glDisableClientState( GL_COLOR_ARRAY ); + glEnableClientState( GL_VERTEX_ARRAY ); + GL_SelectTexture( 0 ); + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + GL_TexEnv( GL_MODULATE ); + globalImages->BindNull(); + glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + glBlendEquation( GL_FUNC_ADD ); + glEnable( GL_BLEND ); + glEnable( GL_SCISSOR_TEST ); + if ( inWorld ) { + glEnable( GL_DEPTH_TEST ); + } else { + glDisable( GL_DEPTH_TEST ); + } + glDisable( GL_STENCIL_TEST ); + GL_State( ( inWorld ? GLS_DEPTHFUNC_EQUAL : 0 ) + | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO ); + GL_Cull( CT_FRONT_SIDED ); + backEnd.currentSpace = NULL; +} + +static bool RB_DrawSharedGuiViewForScope( const viewDef_t *viewDef, + classicGuiDomainScope_t scope ) { + const bool inWorld = scope == CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD; + if ( viewDef == NULL ) { + R_ClassicGuiDomain_RecordBackendFallback( NULL, + CLASSIC_GUI_DOMAIN_BACKEND_GL, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + RB_SharedGuiGLFailureDetail( RB_SHARED_GUI_GL_REJECT_VIEW ) ); + return false; + } + + const classicGuiDomainView_t *view = inWorld + ? R_ClassicGuiDomain_FindInWorldView( viewDef ) + : R_ClassicGuiDomain_FindRootView( viewDef ); + if ( view == NULL ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + RB_SharedGuiGLFailureDetail( RB_SHARED_GUI_GL_REJECT_VIEW ) ); + return false; + } + if ( view->scope != scope || view->backendOutcome[ CLASSIC_GUI_DOMAIN_BACKEND_GL ] + == CLASSIC_GUI_DOMAIN_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, + view->failure != CLASSIC_GUI_DOMAIN_FAILURE_NONE + ? view->failure : CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + return false; + } + + int failureDetail = 0; + if ( !RB_SharedGuiGLPreflight( viewDef, *view, failureDetail ) ) { + R_ClassicGuiDomain_RecordBackendFallback( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, + CLASSIC_GUI_DOMAIN_FAILURE_BACKEND_REJECTED, failureDetail ); + return false; + } + + // No return below this point may hand the view back to the legacy aggregate + // path: the complete view has passed preflight and drawing is now committed. + backEnd.viewDef = viewDef; + if ( !inWorld ) { + backEnd.currentRenderCopied = false; + backEnd.currentDepthCopied = false; + } + backEnd.pc.c_surfaces += inWorld ? view->sourceSurfaceCount + : viewDef->numDrawSurfs; + backEnd.depthFunc = GLS_DEPTHFUNC_EQUAL; + RB_LogComment( inWorld + ? "---------- RB_DrawSharedInWorldGuiView ----------\n" + : "---------- RB_DrawSharedGuiView ----------\n" ); + if ( !inWorld ) { + RB_BeginDrawingView(); + } + if ( glConfig.GLSLProgramAvailable ) { + glUseProgramObjectARB( 0 ); + } + if ( glConfig.ARBVertexProgramAvailable ) { + glDisable( GL_VERTEX_PROGRAM_ARB ); + } + if ( glConfig.ARBFragmentProgramAvailable ) { + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + } + glEnable( GL_BLEND ); + glBlendEquation( GL_FUNC_ADD ); + glEnableClientState( GL_VERTEX_ARRAY ); + GL_SelectTexture( 0 ); + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + glDisableClientState( GL_COLOR_ARRAY ); + glDisable( GL_ALPHA_TEST ); + glDisable( GL_POLYGON_OFFSET_FILL ); + if ( inWorld ) { + glEnable( GL_DEPTH_TEST ); + glMatrixMode( GL_MODELVIEW ); + } else { + glDisable( GL_DEPTH_TEST ); + } + backEnd.currentSpace = NULL; + + int drawnPasses = 0; + int noopPasses = 0; + for ( int drawIndex = 0; drawIndex < rbSharedGuiGLPreparedView.drawCount; + drawIndex++ ) { + const rbSharedGuiGLPreparedDraw_t &preparedDraw = + rbSharedGuiGLPreparedView.draws[drawIndex]; + const classicGuiDomainDraw_t &draw = *preparedDraw.draw; + const drawSurf_t *surf = preparedDraw.surf; + const srfTriangles_t *tri = preparedDraw.tri; + + glLoadMatrixf( draw.modelViewMatrix ); + if ( r_useScissor.GetBool() ) { + backEnd.currentScissor.x1 = draw.scissorX1; + backEnd.currentScissor.y1 = draw.scissorY1; + backEnd.currentScissor.x2 = draw.scissorX2; + backEnd.currentScissor.y2 = draw.scissorY2; + glScissor( viewDef->viewport.x1 + draw.scissorX1, + viewDef->viewport.y1 + draw.scissorY1, + draw.scissorX2 + 1 - draw.scissorX1, + draw.scissorY2 + 1 - draw.scissorY1 ); + } + + idDrawVert *ambientVertices = static_cast( + vertexCache.Position( tri->ambientCache ) ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ambientVertices, + offsetof( idDrawVert, xyz ) ) ); + glTexCoordPointer( 2, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ambientVertices, + offsetof( idDrawVert, st ) ) ); + + for ( int passIndex = 0; passIndex < preparedDraw.passCount; ++passIndex ) { + const rbSharedGuiGLPreparedPass_t &preparedPass = + rbSharedGuiGLPreparedView.passes[ + preparedDraw.firstPass + passIndex]; + const rendererEvaluatedMaterialPass_t &pass = *preparedPass.pass; + if ( pass.disposition != RENDERER_MATERIAL_PASS_DRAW ) { + noopPasses++; + continue; + } + + GL_Cull( preparedPass.cullType ); + RB_SharedGuiGLPrepareVertexColor( surf, ambientVertices, pass ); + GL_SelectTexture( 0 ); + preparedPass.image->SetSamplerState( + preparedPass.filter, preparedPass.repeat ); + preparedPass.image->Bind(); + GL_State( preparedPass.stateBits ); + glBlendEquation( GL_FUNC_ADD ); + if ( pass.alphaTestEnabled ) { + glEnable( GL_ALPHA_TEST ); + glAlphaFunc( preparedPass.alphaFunction, pass.alphaTest ); + } + if ( pass.polygonOffsetEnabled ) { + glPolygonOffset( pass.polygonOffsetFactor, pass.polygonOffsetUnits ); + glEnable( GL_POLYGON_OFFSET_FILL ); + } + RB_SharedGuiGLLoadTextureMatrix( pass ); + RB_DrawElementsWithCounters( tri ); + drawnPasses++; + RB_SharedGuiGLFinishPass( pass ); + } + } + + RB_SharedGuiGLRestoreState( inWorld ); + const bool coverageRecorded = R_ClassicGuiDomain_RecordOwned( viewDef, + CLASSIC_GUI_DOMAIN_BACKEND_GL, drawnPasses, noopPasses ); + if ( !coverageRecorded ) { + // Drawing is already committed, so never return false and double-render. + common->Warning( "RB_DrawSharedGuiView: GL ownership coverage rejected after committed draw" ); + } + return true; +} + +bool RB_DrawSharedGuiView( const viewDef_t *viewDef ) { + return RB_DrawSharedGuiViewForScope( viewDef, + CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D ); +} + +bool RB_DrawSharedInWorldGuiView( const viewDef_t *viewDef ) { + return RB_DrawSharedGuiViewForScope( viewDef, + CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD ); +} + +/* +=============================================================================== + + Backend-neutral classic world ambient/material consumer + + ClassicWorldAmbientDomain seals material interpretation and complete-view + coverage before either backend sees the view. This adapter retains only the + legacy geometry bridge. Every fallible cache, texture, and state check is + completed before the first shared ambient draw; after commit the pre-fog and + post-fog ranges execute without a path back to the classic walker. + +=============================================================================== +*/ + +enum rbSharedWorldAmbientGLReject_t { + RB_SHARED_WORLD_AMBIENT_GL_REJECT_VIEW = 1, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_MUTATED_VIEW, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_SPECIAL_EFFECTS, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TARGET, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_CAPACITY, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_DRAW_RANGE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_GEOMETRY, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_VERTEX_CACHE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_INDEX_CACHE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PASS_RANGE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PASS_STATE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TEXTURE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TEXTURE_CAPACITY, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_COVERAGE, + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PHASE +}; + +static float RB_STD_ForceAmbientValue( void ); + +typedef struct rbSharedWorldAmbientGLPreparedPass_s { + const rendererEvaluatedMaterialPass_t *pass; + idImage *image; + textureFilter_t filter; + textureRepeat_t repeat; + int stateBits; + int cullType; + GLenum alphaFunction; +} rbSharedWorldAmbientGLPreparedPass_t; + +typedef struct rbSharedWorldAmbientGLPreparedDraw_s { + const classicWorldAmbientDomainDraw_t *draw; + const drawSurf_t *surf; + const srfTriangles_t *tri; + int firstPass; + int passCount; +} rbSharedWorldAmbientGLPreparedDraw_t; + +typedef struct rbSharedWorldAmbientGLPreparedView_s { + const classicWorldAmbientDomainView_t *view; + const viewDef_t *viewDef; + int drawCount; + int passCount; + int drawablePasses; + int noopPasses; + int submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + int submittedNoops; + bool ready; + bool committed; + bool completedPhase[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ]; + bool seenSourceSurfaces[ SCENE_PACKET_MAX_DRAWS ]; + rbSharedWorldAmbientGLPreparedDraw_t draws[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS ]; + rbSharedWorldAmbientGLPreparedPass_t passes[ CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES ]; +} rbSharedWorldAmbientGLPreparedView_t; + +static rbSharedWorldAmbientGLPreparedView_t rbSharedWorldAmbientGLPreparedView; + +static int RB_SharedWorldAmbientGLFailureDetail( + rbSharedWorldAmbientGLReject_t reason, int drawIndex = -1, + int passIndex = -1 ) { + const int drawDetail = drawIndex >= 0 ? Min( drawIndex + 1, 9999 ) : 0; + const int passDetail = passIndex >= 0 ? Min( passIndex + 1, 255 ) : 0; + return static_cast( reason ) * 1000000 + drawDetail * 256 + passDetail; +} + +static bool RB_SharedWorldAmbientGLFail( const viewDef_t *viewDef, + classicWorldAmbientDomainFailure_t failure, int detail ) { + R_ClassicWorldAmbientDomain_RecordBackendFallback( viewDef, + CLASSIC_WORLD_AMBIENT_BACKEND_GL, failure, detail ); + return false; +} + +static bool RB_SharedWorldAmbientGLSourceNoopValid( + const classicWorldAmbientDomainDraw_t &draw ) { + switch ( draw.sourceSurface ) { + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NULL_SURFACE: + if ( draw.legacyDrawSurf != NULL ) { + return false; + } + break; + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_MISSING_MATERIAL: + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_NO_AMBIENT: + case CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_NOOP_EMPTY_GEOMETRY: + if ( draw.legacyDrawSurf == NULL ) { + return false; + } + break; + default: + return false; + } + return !draw.packetBacked && draw.drawPacketIndex == -1 + && draw.depthDrawPacketIndex == -1 && !draw.depthPrerequisite + && draw.firstEvaluatedPass == -1 && draw.evaluatedPassCount == 0 + && draw.activePassCount == 0 && draw.drawablePassCount == 0 + && draw.inactivePassCount == 0 && draw.activeNoopPassCount == 0 + && draw.noopPassCount == 0; +} + +static bool RB_SharedWorldAmbientGLMapDepth( + const rendererDepthState_t &depth, int &stateBits ) { + if ( !depth.testEnabled ) { + return false; + } + if ( !depth.writeEnabled ) { + stateBits |= GLS_DEPTHMASK; + } + switch ( depth.compareOperation ) { + case RENDERER_COMPARE_LESS_OR_EQUAL: + stateBits |= GLS_DEPTHFUNC_LESS; + return true; + case RENDERER_COMPARE_EQUAL: + stateBits |= GLS_DEPTHFUNC_EQUAL; + return true; + case RENDERER_COMPARE_ALWAYS: + stateBits |= GLS_DEPTHFUNC_ALWAYS; + return true; + default: + return false; + } +} + +static bool RB_SharedWorldAmbientGLBuildState( + const rendererEvaluatedMaterialPass_t &pass, + rbSharedWorldAmbientGLPreparedPass_t &prepared ) { + prepared.stateBits = 0; + prepared.alphaFunction = GL_ALWAYS; + prepared.cullType = CT_FRONT_SIDED; + if ( pass.kind != RENDERER_MATERIAL_PASS_SURFACE + || pass.programFamily != RENDERER_PROGRAM_FIXED + || pass.programKey != 0 + || pass.texgen != RENDERER_TEXGEN_EXPLICIT + || pass.textureSemantic != RENDERER_TEXTURE_DIFFUSE + || pass.textureResourceId == 0 + || pass.blend.colorOperation != RENDERER_BLEND_OP_ADD + || pass.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || pass.blend.sourceAlpha != pass.blend.sourceColor + || pass.blend.destinationAlpha != pass.blend.destinationColor ) { + return false; + } + const bool replacementBlend = pass.blend.sourceColor == RENDERER_BLEND_ONE + && pass.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( pass.blend.enabled == replacementBlend + || !RB_SharedGuiGLMapSourceBlend( + pass.blend.sourceColor, prepared.stateBits ) + || !RB_SharedGuiGLMapDestinationBlend( + pass.blend.destinationColor, prepared.stateBits ) + || !RB_SharedWorldAmbientGLMapDepth( + pass.depth, prepared.stateBits ) + || !RB_SharedGuiGLMapCull( pass.cull, prepared.cullType ) ) { + return false; + } + if ( ( pass.colorWriteMask + & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 ) { + return false; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + prepared.stateBits |= GLS_REDMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + prepared.stateBits |= GLS_GREENMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + prepared.stateBits |= GLS_BLUEMASK; + } + if ( ( pass.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + prepared.stateBits |= GLS_ALPHAMASK; + } + return RB_SharedGuiGLMapAlphaCompare( + pass.alphaTestCompareOperation, prepared.alphaFunction ); +} + +static bool RB_SharedWorldAmbientGLPreflight( const viewDef_t *viewDef, + const classicWorldAmbientDomainView_t &view, int &failureDetail ) { + rbSharedWorldAmbientGLPreparedView_t &prepared = + rbSharedWorldAmbientGLPreparedView; + // Clear the plan header and the seen-surface bitmap; the draws/passes pools + // below them are zeroed entry-by-entry as slots are claimed. + std::memset( &prepared, 0, + offsetof( rbSharedWorldAmbientGLPreparedView_t, draws ) ); + prepared.view = &view; + prepared.viewDef = viewDef; + + if ( viewDef == NULL || view.viewDef != viewDef || !view.ready + || viewDef->viewEntitys == NULL + || view.sourceSurfaceCount != viewDef->numDrawSurfs + || view.sourceSurfaceCount < 0 + || view.sourceSurfaceCount > SCENE_PACKET_MAX_DRAWS + || view.drawableSurfaceCount < 0 + || view.drawableSurfaceCount > view.sourceSurfaceCount + || view.noopSurfaceCount + != view.sourceSurfaceCount - view.drawableSurfaceCount + || view.drawCount < 0 + || view.drawCount > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_DRAWS + || view.evaluatedPassCount < 0 + || view.evaluatedPassCount + > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES + || ( viewDef->numDrawSurfs > 0 && viewDef->drawSurfs == NULL ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_VIEW ); + return false; + } + + const int allowedRenderFlags = + RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderWorld == NULL || viewDef->viewLights != NULL + || viewDef->renderView.viewID < 0 + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->numOutlineDrawSurfs != 0 + || viewDef->renderView.globalMaterial != NULL + || r_showOverDraw.GetInteger() != 0 || r_singleTriangle.GetBool() + || r_skipAmbient.GetBool() || r_skipRender.GetBool() + || r_skipRenderContext.GetBool() + || r_portalsDistanceCull.GetBool() + || r_celShading.GetBool() || r_celShadingWorld.GetBool() + || RB_STD_ForceAmbientValue() > 0.0f ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_MUTATED_VIEW ); + return false; + } + const int supportedSpecialEffects = rbRVSpecialActiveMask + & ( SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL ); + if ( rbRVSpecialCommandFrame == backEnd.frameCount + && rbRVSpecialCommandView == viewDef + && ( supportedSpecialEffects != 0 || rbRVSpecialBlurPrepared + || rbRVSpecialALPrepared ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_SPECIAL_EFFECTS ); + return false; + } + if ( backEnd.renderTexture != NULL || backEnd.feedbackRenderTexture != NULL ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TARGET ); + return false; + } + if ( !glConfig.isInitialized || globalImages == NULL + || glConfig.maxTextureUnits < 1 + || glConfig.maxTextureImageUnits < 1 + || glConfig.maxTextureCoords < 1 ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_CAPACITY ); + return false; + } + if ( view.viewportX1 != viewDef->viewport.x1 + || view.viewportY1 != viewDef->viewport.y1 + || view.viewportX2 != viewDef->viewport.x2 + || view.viewportY2 != viewDef->viewport.y2 + || view.scissorX1 != viewDef->scissor.x1 + || view.scissorY1 != viewDef->scissor.y1 + || view.scissorX2 != viewDef->scissor.x2 + || view.scissorY2 != viewDef->scissor.y2 + || std::memcmp( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ) != 0 + || !RB_SharedGuiGLMatrixValid( view.projectionMatrix, 16 ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_MUTATED_VIEW ); + return false; + } + + int previousSourceSurface = -1; + int evaluatedPasses = 0; + int drawablePasses = 0; + int activePasses = 0; + int inactivePasses = 0; + int activeNoopPasses = 0; + int noopPasses = 0; + int sourceNoopSurfaces = 0; + int phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ] = { 0, 0 }; + int phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_COUNT ] = { 0, 0 }; + for ( int drawIndex = 0; drawIndex < view.drawCount; ++drawIndex ) { + const classicWorldAmbientDomainDraw_t *draw = + R_ClassicWorldAmbientDomain_ViewDraw( view, drawIndex ); + if ( draw == NULL || draw->sourceSurfaceIndex < 0 + || draw->sourceSurfaceIndex >= viewDef->numDrawSurfs + || draw->sourceSurfaceIndex <= previousSourceSurface + || prepared.seenSourceSurfaces[ draw->sourceSurfaceIndex ] + || viewDef->drawSurfs[ draw->sourceSurfaceIndex ] + != draw->legacyDrawSurf ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + prepared.seenSourceSurfaces[ draw->sourceSurfaceIndex ] = true; + previousSourceSurface = draw->sourceSurfaceIndex; + if ( draw->sourceSurface + != CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE ) { + if ( !RB_SharedWorldAmbientGLSourceNoopValid( *draw ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_DRAW_RANGE, drawIndex ); + return false; + } + sourceNoopSurfaces++; + continue; + } + if ( !draw->packetBacked || draw->legacyDrawSurf == NULL + || draw->phase < CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG + || draw->phase >= CLASSIC_WORLD_AMBIENT_PHASE_COUNT ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PHASE, drawIndex ); + return false; + } + + const drawSurf_t *surf = draw->legacyDrawSurf; + const srfTriangles_t *tri = surf->geo; + if ( tri == NULL || draw->firstIndex != 0 || draw->vertexOffset != 0 + || draw->tableGeneration != view.tableGeneration + || draw->hasAmbientCache != ( tri->ambientCache != NULL ) + || draw->hasIndexCache != ( tri->indexCache != NULL ) + || draw->vertexCount <= 0 || draw->indexCount <= 0 + || draw->indexCount % 3 != 0 + || tri->numVerts != draw->vertexCount + || tri->numIndexes != draw->indexCount + || draw->vertexCount > idMath::INT_MAX + / static_cast( sizeof( idDrawVert ) ) + || draw->indexCount > idMath::INT_MAX + / static_cast( sizeof( glIndex_t ) ) + || surf->decalColorCache != NULL + || !RB_SharedGuiGLMatrixValid( draw->modelViewMatrix, 16 ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_GEOMETRY, drawIndex ); + return false; + } + const long long scissorWidth = static_cast( draw->scissorX2 ) + - draw->scissorX1 + 1; + const long long scissorHeight = static_cast( draw->scissorY2 ) + - draw->scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > idMath::INT_MAX + || scissorHeight > idMath::INT_MAX ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_GEOMETRY, drawIndex ); + return false; + } + if ( !RB_EnsurePackedClassicDrawCaches( surf, false, true ) + || !RB_SharedGuiGLCacheValid( tri->ambientCache, false, + draw->vertexCount * static_cast( sizeof( idDrawVert ) ) ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_VERTEX_CACHE, drawIndex ); + return false; + } + R_TouchVertexCache( tri->ambientCache ); + if ( tri->indexCache != NULL ) { + if ( !RB_SharedGuiGLCacheValid( tri->indexCache, true, + draw->indexCount * static_cast( sizeof( glIndex_t ) ) ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_INDEX_CACHE, drawIndex ); + return false; + } + R_TouchVertexCache( tri->indexCache ); + } + if ( ( !r_useIndexBuffers.GetBool() || tri->indexCache == NULL ) + && tri->indexes == NULL ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_INDEX_CACHE, drawIndex ); + return false; + } + if ( draw->evaluatedPassCount < 0 + || draw->evaluatedPassCount + > static_cast( RENDERER_CONTRACT_MAX_MATERIAL_PASSES ) + || prepared.passCount + > CLASSIC_WORLD_AMBIENT_DOMAIN_MAX_EVALUATED_PASSES + - draw->evaluatedPassCount ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PASS_RANGE, drawIndex ); + return false; + } + + rbSharedWorldAmbientGLPreparedDraw_t &preparedDraw = + prepared.draws[ prepared.drawCount++ ]; + std::memset( &preparedDraw, 0, sizeof( preparedDraw ) ); + preparedDraw.draw = draw; + preparedDraw.surf = surf; + preparedDraw.tri = tri; + preparedDraw.firstPass = prepared.passCount; + preparedDraw.passCount = draw->evaluatedPassCount; + + int drawDrawablePasses = 0; + int drawActivePasses = 0; + int drawInactivePasses = 0; + int drawActiveNoopPasses = 0; + for ( int passIndex = 0; passIndex < draw->evaluatedPassCount; + ++passIndex ) { + const rendererEvaluatedMaterialPass_t *pass = + R_ClassicWorldAmbientDomain_DrawPass( *draw, passIndex ); + const materialResourceTextureBinding_t *binding = + R_ClassicWorldAmbientDomain_DrawPassTexture( *draw, passIndex ); + if ( pass == NULL || pass->order != static_cast( passIndex ) + || pass->sourceStageIndex < 0 + || pass->vertexColor < RENDERER_VERTEX_COLOR_IGNORE + || pass->vertexColor > RENDERER_VERTEX_COLOR_INVERSE_MODULATE + || !RB_SharedGuiGLPassDispositionValid( *pass ) + || !RB_SharedGuiGLFloatValid( pass->condition ) + || !RB_SharedGuiGLMatrixValid( pass->color, 4 ) + || !RB_SharedGuiGLMatrixValid( pass->textureMatrix, 6 ) + || !RB_SharedGuiGLFloatValid( pass->alphaTest ) + || !RB_SharedGuiGLFloatValid( pass->polygonOffsetFactor ) + || !RB_SharedGuiGLFloatValid( pass->polygonOffsetUnits ) + || static_cast( pass->textureMatrix[2] ) + < -static_cast( idMath::INT_MAX ) - 1.0 + || static_cast( pass->textureMatrix[2] ) + > static_cast( idMath::INT_MAX ) + || static_cast( pass->textureMatrix[5] ) + < -static_cast( idMath::INT_MAX ) - 1.0 + || static_cast( pass->textureMatrix[5] ) + > static_cast( idMath::INT_MAX ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PASS_STATE, + drawIndex, passIndex ); + return false; + } + rbSharedWorldAmbientGLPreparedPass_t &preparedPass = + prepared.passes[ prepared.passCount++ ]; + std::memset( &preparedPass, 0, sizeof( preparedPass ) ); + preparedPass.pass = pass; + if ( !RB_SharedWorldAmbientGLBuildState( *pass, preparedPass ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_PASS_STATE, + drawIndex, passIndex ); + return false; + } + if ( !RB_SharedGuiGLTextureBindingValid( + *pass, binding, preparedPass.image ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TEXTURE, + drawIndex, passIndex ); + return false; + } + preparedPass.filter = binding->filter; + preparedPass.repeat = binding->repeat; + if ( pass->disposition == RENDERER_MATERIAL_PASS_DRAW ) { + drawDrawablePasses++; + drawablePasses++; + phaseDrawablePasses[ draw->phase ]++; + } + if ( pass->active ) { + drawActivePasses++; + activePasses++; + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + drawActiveNoopPasses++; + activeNoopPasses++; + } + } else { + drawInactivePasses++; + inactivePasses++; + } + if ( pass->disposition != RENDERER_MATERIAL_PASS_DRAW ) { + noopPasses++; + phaseNoopPasses[ draw->phase ]++; + } + if ( pass->disposition == RENDERER_MATERIAL_PASS_DRAW + && pass->vertexColor != RENDERER_VERTEX_COLOR_IGNORE + && ( glConfig.maxTextureUnits < 2 + || glConfig.maxTextureImageUnits < 2 + || glConfig.maxTextureCoords < 2 + || globalImages->whiteImage == NULL + || !globalImages->whiteImage->IsLoaded() + || globalImages->whiteImage->IsDefaulted() + || globalImages->whiteImage->GetOpts().textureType != TT_2D + || globalImages->whiteImage->GetDeviceHandle() == 0 ) ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_TEXTURE_CAPACITY, + drawIndex, passIndex ); + return false; + } + evaluatedPasses++; + } + if ( drawActivePasses != draw->activePassCount + || drawDrawablePasses != draw->drawablePassCount + || drawInactivePasses != draw->inactivePassCount + || drawActiveNoopPasses != draw->activeNoopPassCount + || drawInactivePasses + drawActiveNoopPasses + != draw->noopPassCount ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_COVERAGE, drawIndex ); + return false; + } + } + + if ( prepared.drawCount != view.drawableSurfaceCount + || sourceNoopSurfaces != view.noopSurfaceCount + || evaluatedPasses != view.evaluatedPassCount + || activePasses != view.activePassCount + || drawablePasses != view.drawablePassCount + || inactivePasses != view.inactivePassCount + || activeNoopPasses != view.activeNoopPassCount + || noopPasses != view.noopPassCount + || phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + != view.phaseDrawablePassCount[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + || phaseDrawablePasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + != view.phaseDrawablePassCount[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + || phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + != view.phaseNoopPassCount[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ] + || phaseNoopPasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] + != view.phaseNoopPassCount[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ] ) { + failureDetail = RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_COVERAGE ); + return false; + } + prepared.drawablePasses = drawablePasses; + prepared.noopPasses = noopPasses; + prepared.ready = true; + return true; +} + +static bool RB_PrepareSharedWorldAmbientView( const viewDef_t *viewDef ) { + std::memset( &rbSharedWorldAmbientGLPreparedView, 0, + offsetof( rbSharedWorldAmbientGLPreparedView_t, draws ) ); + const classicWorldAmbientDomainView_t *view = + R_ClassicWorldAmbientDomain_FindView( viewDef ); + if ( view == NULL ) { + return RB_SharedWorldAmbientGLFail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + RB_SharedWorldAmbientGLFailureDetail( + RB_SHARED_WORLD_AMBIENT_GL_REJECT_VIEW ) ); + } + if ( view->backendOutcome[ CLASSIC_WORLD_AMBIENT_BACKEND_GL ] + == CLASSIC_WORLD_AMBIENT_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + return RB_SharedWorldAmbientGLFail( viewDef, + view->failure != CLASSIC_WORLD_AMBIENT_FAILURE_NONE + ? view->failure : CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + int failureDetail = 0; + if ( !RB_SharedWorldAmbientGLPreflight( viewDef, *view, failureDetail ) ) { + return RB_SharedWorldAmbientGLFail( viewDef, + CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_REJECTED, failureDetail ); + } + return true; +} + +static void RB_SharedWorldAmbientGLRestoreState( void ) { + glDisable( GL_ALPHA_TEST ); + glDisable( GL_POLYGON_OFFSET_FILL ); + glDisable( GL_SAMPLE_ALPHA_TO_COVERAGE ); + glDisableClientState( GL_COLOR_ARRAY ); + glEnableClientState( GL_VERTEX_ARRAY ); + GL_SelectTexture( 0 ); + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + GL_TexEnv( GL_MODULATE ); + globalImages->BindNull(); + glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + glBlendEquation( GL_FUNC_ADD ); + glEnable( GL_BLEND ); + glEnable( GL_SCISSOR_TEST ); + glEnable( GL_DEPTH_TEST ); + GL_Cull( CT_FRONT_SIDED ); + backEnd.currentSpace = NULL; +} + +static void RB_DrawSharedWorldAmbientPhase( + classicWorldAmbientPhase_t phase ) { + rbSharedWorldAmbientGLPreparedView_t &prepared = + rbSharedWorldAmbientGLPreparedView; + if ( !prepared.ready || prepared.view == NULL || prepared.viewDef == NULL + || phase < CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG + || phase >= CLASSIC_WORLD_AMBIENT_PHASE_COUNT + || prepared.completedPhase[ phase ] ) { + return; + } + // Commit begins with the first phase. All fallible work completed during the + // complete-view preflight, so neither phase can return to classic rendering. + prepared.committed = true; + prepared.completedPhase[ phase ] = true; + RB_LogComment( "---------- RB_DrawSharedWorldAmbientPhase ----------\n" ); + if ( glConfig.GLSLProgramAvailable ) { + glUseProgramObjectARB( 0 ); + } + if ( glConfig.ARBVertexProgramAvailable ) { + glDisable( GL_VERTEX_PROGRAM_ARB ); + } + if ( glConfig.ARBFragmentProgramAvailable ) { + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + } + glEnable( GL_DEPTH_TEST ); + glEnable( GL_BLEND ); + glBlendEquation( GL_FUNC_ADD ); + glEnableClientState( GL_VERTEX_ARRAY ); + GL_SelectTexture( 0 ); + glEnableClientState( GL_TEXTURE_COORD_ARRAY ); + glDisableClientState( GL_COLOR_ARRAY ); + glDisable( GL_ALPHA_TEST ); + glDisable( GL_POLYGON_OFFSET_FILL ); + + for ( int drawIndex = 0; drawIndex < prepared.drawCount; ++drawIndex ) { + const rbSharedWorldAmbientGLPreparedDraw_t &preparedDraw = + prepared.draws[ drawIndex ]; + const classicWorldAmbientDomainDraw_t &draw = *preparedDraw.draw; + if ( draw.phase != phase ) { + continue; + } + const drawSurf_t *surf = preparedDraw.surf; + const srfTriangles_t *tri = preparedDraw.tri; + glLoadMatrixf( draw.modelViewMatrix ); + if ( r_useScissor.GetBool() ) { + backEnd.currentScissor.x1 = draw.scissorX1; + backEnd.currentScissor.y1 = draw.scissorY1; + backEnd.currentScissor.x2 = draw.scissorX2; + backEnd.currentScissor.y2 = draw.scissorY2; + glScissor( prepared.viewDef->viewport.x1 + draw.scissorX1, + prepared.viewDef->viewport.y1 + draw.scissorY1, + draw.scissorX2 + 1 - draw.scissorX1, + draw.scissorY2 + 1 - draw.scissorY1 ); + } + idDrawVert *ambientVertices = static_cast( + vertexCache.Position( tri->ambientCache ) ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ambientVertices, + offsetof( idDrawVert, xyz ) ) ); + glTexCoordPointer( 2, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ambientVertices, + offsetof( idDrawVert, st ) ) ); + for ( int passIndex = 0; passIndex < preparedDraw.passCount; + ++passIndex ) { + const rbSharedWorldAmbientGLPreparedPass_t &preparedPass = + prepared.passes[ preparedDraw.firstPass + passIndex ]; + const rendererEvaluatedMaterialPass_t &pass = *preparedPass.pass; + if ( pass.disposition != RENDERER_MATERIAL_PASS_DRAW ) { + prepared.submittedNoops++; + continue; + } + GL_Cull( preparedPass.cullType ); + RB_SharedGuiGLPrepareVertexColor( surf, ambientVertices, pass ); + GL_SelectTexture( 0 ); + preparedPass.image->SetSamplerState( + preparedPass.filter, preparedPass.repeat ); + preparedPass.image->Bind(); + GL_State( preparedPass.stateBits ); + glBlendEquation( GL_FUNC_ADD ); + if ( pass.alphaTestEnabled ) { + glEnable( GL_ALPHA_TEST ); + glAlphaFunc( preparedPass.alphaFunction, pass.alphaTest ); + } + if ( pass.polygonOffsetEnabled ) { + glPolygonOffset( + pass.polygonOffsetFactor, pass.polygonOffsetUnits ); + glEnable( GL_POLYGON_OFFSET_FILL ); + } + RB_SharedGuiGLLoadTextureMatrix( pass ); + RB_DrawElementsWithCounters( tri ); + prepared.submittedPasses[ phase ]++; + RB_SharedGuiGLFinishPass( pass ); + } + } + RB_SharedWorldAmbientGLRestoreState(); + if ( phase == CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ) { + const bool coverageRecorded = R_ClassicWorldAmbientDomain_RecordOwned( + prepared.viewDef, CLASSIC_WORLD_AMBIENT_BACKEND_GL, + prepared.submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ], + prepared.submittedPasses[ CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ], + prepared.submittedNoops ); + if ( !coverageRecorded ) { + common->Warning( "OpenGL: shared world ambient coverage rejected after committed view" ); + } + } +} + /* ================== RB_STD_T_RenderShaderPasses @@ -7583,6 +10850,10 @@ void RB_STD_T_RenderShaderPasses( const drawSurf_t *surf ) { const bool hasBakedDecalStageColor = ( surf->decalColorCache != NULL && stage >= 0 && stage < surf->decalColorStageCount && surf->decalColorStride > 0 ); + // tracks whether the unit-1 constant-color combiner block below actually + // ran, so the teardown only touches unit 1 when it was set up + bool unit1Used = false; + // select the vertex color source if ( pStage->vertexColor == SVC_IGNORE ) { glColor4fv( color ); @@ -7604,6 +10875,7 @@ void RB_STD_T_RenderShaderPasses( const drawSurf_t *surf ) { // texture stage. Skip this when decal stages already baked stage // color into per-vertex data; applying both paths darkens decals. if ( !hasBakedDecalStageColor && ( color[0] != 1 || color[1] != 1 || color[2] != 1 || color[3] != 1 ) ) { + unit1Used = true; GL_SelectTexture( 1 ); globalImages->whiteImage->Bind(); @@ -7640,11 +10912,18 @@ void RB_STD_T_RenderShaderPasses( const drawSurf_t *surf ) { if ( pStage->vertexColor != SVC_IGNORE ) { glDisableClientState( GL_COLOR_ARRAY ); - GL_SelectTexture( 1 ); - GL_TexEnv( GL_MODULATE ); - globalImages->BindNull(); - GL_SelectTexture( 0 ); - GL_TexEnv( GL_MODULATE ); + // only tear down unit 1 if the constant-color block set it up + if ( unit1Used ) { + GL_SelectTexture( 1 ); + GL_TexEnv( GL_MODULATE ); + globalImages->BindNull(); + GL_SelectTexture( 0 ); + } + // unit 0's combiner was only changed by SVC_INVERSE_MODULATE + if ( pStage->vertexColor == SVC_INVERSE_MODULATE ) { + GL_SelectTexture( 0 ); + GL_TexEnv( GL_MODULATE ); + } } continue; } @@ -7657,11 +10936,18 @@ void RB_STD_T_RenderShaderPasses( const drawSurf_t *surf ) { if ( pStage->vertexColor != SVC_IGNORE ) { glDisableClientState( GL_COLOR_ARRAY ); - GL_SelectTexture( 1 ); - GL_TexEnv( GL_MODULATE ); - globalImages->BindNull(); - GL_SelectTexture( 0 ); - GL_TexEnv( GL_MODULATE ); + // only tear down unit 1 if the constant-color block set it up + if ( unit1Used ) { + GL_SelectTexture( 1 ); + GL_TexEnv( GL_MODULATE ); + globalImages->BindNull(); + GL_SelectTexture( 0 ); + } + // unit 0's combiner was only changed by SVC_INVERSE_MODULATE + if ( pStage->vertexColor == SVC_INVERSE_MODULATE ) { + GL_SelectTexture( 0 ); + GL_TexEnv( GL_MODULATE ); + } } } @@ -7779,6 +11065,12 @@ int RB_STD_DrawShaderPasses( drawSurf_t **drawSurfs, int numDrawSurfs, rbShaderP if ( filter != NULL && !filter( drawSurfs[i] ) ) { continue; } + if ( R_ModernClusteredLighting_DecalOwnsSurface( + backEnd.viewDef, drawSurfs[i] ) + && R_ModernGLExecutor_SubmitForwardPlusDecalSurface( + backEnd.viewDef, drawSurfs[i] ) ) { + continue; + } if ( drawSurfs[i]->material->SuppressInSubview() ) { continue; } @@ -7822,6 +11114,116 @@ int RB_STD_DrawShaderPasses( drawSurf_t **drawSurfs, int numDrawSurfs, rbShaderP +/* +============================================================================== + + Shared cinematic / authored-post transaction adapters + + The decoder and authored program-stage implementations are intentionally + shared with the established renderer. They are dynamic by definition: a + video frame is keyed to the exact render-view clock and an authored post + stage can require the current color/depth capture made immediately before + the first visible write. The domain seals the complete range before these + adapters run, while these adapters retain the mature stage executor. + +============================================================================== +*/ + +static bool RB_SharedCinematicPostGLReady( const viewDef_t *viewDef, + classicCinematicPostDomainScope_t scope, + const classicCinematicPostDomainView_t *&domainView ) { + domainView = scope == CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC + ? R_ClassicCinematicPostDomain_FindRootCinematicView( viewDef ) + : R_ClassicCinematicPostDomain_FindAuthoredPostView( viewDef ); + if ( domainView == NULL || !domainView->ready + || !R_ClassicCinematicPostDomain_ReadyForBackend( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_GL ) ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_GL, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_NOT_READY, 0 ); + return false; + } + if ( viewDef == NULL || viewDef != domainView->viewDef + || domainView->sourceSurfaceCount <= 0 || viewDef->drawSurfs == NULL + || backEnd.renderTexture != NULL || backEnd.feedbackRenderTexture != NULL + || r_skipRender.GetBool() || r_skipRenderContext.GetBool() + || r_showOverDraw.GetInteger() != 0 || r_singleTriangle.GetBool() ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_GL, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, 1 ); + return false; + } + if ( scope == CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC ) { + if ( viewDef->viewEntitys != NULL || domainView->firstSourceSurface != 0 + || domainView->sourceSurfaceCount != viewDef->numDrawSurfs ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_GL, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, 2 ); + return false; + } + } else if ( viewDef->viewEntitys == NULL || r_skipPostProcess.GetBool() + || domainView->firstSourceSurface < 0 + || domainView->firstSourceSurface > viewDef->numDrawSurfs + || domainView->sourceSurfaceCount + != viewDef->numDrawSurfs - domainView->firstSourceSurface ) { + R_ClassicCinematicPostDomain_RecordBackendFallback( viewDef, scope, + CLASSIC_CINEMATIC_POST_BACKEND_GL, + CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_REJECTED, 3 ); + return false; + } + return true; +} + +bool RB_DrawSharedCinematicRootView( const viewDef_t *viewDef ) { + const classicCinematicPostDomainView_t *domainView = NULL; + if ( !RB_SharedCinematicPostGLReady( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, domainView ) ) { + return false; + } + + // The domain owns this command occurrence after preflight. The established + // stage executor retains exact videoMap/soundMap decoder and clock behavior. + backEnd.viewDef = const_cast( viewDef ); + backEnd.currentRenderCopied = false; + backEnd.currentDepthCopied = false; + backEnd.pc.c_surfaces += domainView->sourceSurfaceCount; + backEnd.depthFunc = GLS_DEPTHFUNC_EQUAL; + RB_LogComment( "---------- RB_DrawSharedCinematicRootView ----------\n" ); + RB_BeginDrawingView(); + const int consumed = RB_STD_DrawShaderPasses( + const_cast( viewDef->drawSurfs ), + domainView->sourceSurfaceCount ); + RB_SharedGuiGLRestoreState( false ); + if ( consumed != domainView->sourceSurfaceCount + || !R_ClassicCinematicPostDomain_RecordOwned( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC, + CLASSIC_CINEMATIC_POST_BACKEND_GL, consumed ) ) { + common->Warning( "RB_DrawSharedCinematicRootView: coverage rejected after committed view" ); + } + return true; +} + +bool RB_DrawSharedAuthoredPostView( const viewDef_t *viewDef, + drawSurf_t **drawSurfs, int firstSourceSurface, int sourceSurfaceCount ) { + const classicCinematicPostDomainView_t *domainView = NULL; + if ( !RB_SharedCinematicPostGLReady( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, domainView ) + || drawSurfs == NULL || firstSourceSurface != domainView->firstSourceSurface + || sourceSurfaceCount != domainView->sourceSurfaceCount ) { + return false; + } + RB_LogComment( "---------- RB_DrawSharedAuthoredPostView ----------\n" ); + const int consumed = RB_STD_DrawShaderPasses( + drawSurfs + firstSourceSurface, sourceSurfaceCount ); + if ( consumed != sourceSurfaceCount + || !R_ClassicCinematicPostDomain_RecordOwned( viewDef, + CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST, + CLASSIC_CINEMATIC_POST_BACKEND_GL, consumed ) ) { + common->Warning( "RB_DrawSharedAuthoredPostView: coverage rejected after committed range" ); + } + return true; +} + /* ============================================================================== @@ -7915,7 +11317,11 @@ static void RB_T_Shadow( const drawSurf_t *surf ) { // set depth bounds if( glConfig.depthBoundsTestAvailable && r_useDepthBoundsTest.GetBool() ) { - glDepthBoundsEXT( surf->scissorRect.zmin, surf->scissorRect.zmax ); + const float minDepth = idMath::ClampFloat( 0.0f, 1.0f, + surf->scissorRect.zmin ); + const float maxDepth = idMath::ClampFloat( minDepth, 1.0f, + surf->scissorRect.zmax ); + glDepthBoundsEXT( minDepth, maxDepth ); } // debug visualization @@ -8461,6 +11867,913 @@ void RB_STD_FogAllLights( void ) { glEnable( GL_STENCIL_TEST ); } +/* +=============================================================================== + + Backend-neutral classic fog/blend consumer + + ClassicFogBlendDomain has already interpreted the complete ordered light, + stage, receiver, and frustum-cap stream. OpenGL validates every cache and + resource for the whole phase before the first framebuffer write, then submits + only sealed values plus the retained geometry bridge. A rejected preflight + leaves RB_STD_FogAllLights as the untouched atomic rollback. + +=============================================================================== +*/ + +enum rbClassicFogBlendGLReject_t { + RB_CLASSIC_FOG_BLEND_GL_REJECT_VIEW = 1, + RB_CLASSIC_FOG_BLEND_GL_REJECT_MUTATED_VIEW, + RB_CLASSIC_FOG_BLEND_GL_REJECT_CAPACITY, + RB_CLASSIC_FOG_BLEND_GL_REJECT_LIGHT_RANGE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_STAGE_RANGE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_STAGE_STATE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_PRIMITIVE_RANGE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_PRIMITIVE_STATE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_GEOMETRY, + RB_CLASSIC_FOG_BLEND_GL_REJECT_VERTEX_CACHE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_INDEX_CACHE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_TEXTURE, + RB_CLASSIC_FOG_BLEND_GL_REJECT_COVERAGE +}; + +typedef struct rbClassicFogBlendGLPreparedStage_s { + const classicFogBlendDomainLightStage_t *stage; + idImage *projectionImage; + idImage *falloffImage; + idImage *fogImage; + idImage *fogEnterImage; +} rbClassicFogBlendGLPreparedStage_t; + +typedef struct rbClassicFogBlendGLPreparedPrimitive_s { + const classicFogBlendDomainPrimitive_t *primitive; + const rbClassicFogBlendGLPreparedStage_t *stage; + const srfTriangles_t *geometry; + int stateBits; + int cullType; + GLenum alphaFunction; +} rbClassicFogBlendGLPreparedPrimitive_t; + +typedef struct rbClassicFogBlendGLPreparedView_s { + const classicFogBlendDomainView_t *view; + const viewDef_t *viewDef; + std::uint64_t hash; + int lightCount; + int stageCount; + int primitiveCount; + int drawablePrimitiveCount; + int fogReceiverDraws; + int fogFrustumDraws; + int blendDraws; + int noopPrimitives; + int noopStages; + int noopLights; + bool ready; + bool committed; + rbClassicFogBlendGLPreparedStage_t stages[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES ]; + rbClassicFogBlendGLPreparedPrimitive_t primitives[ + CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES ]; +} rbClassicFogBlendGLPreparedView_t; + +static rbClassicFogBlendGLPreparedView_t rbClassicFogBlendGLPreparedView; + +static int RB_ClassicFogBlend_GLFailureDetail( + rbClassicFogBlendGLReject_t reason, int lightIndex = -1, + int stageIndex = -1, int primitiveIndex = -1 ) { + const int lightDetail = lightIndex >= 0 ? Min( lightIndex + 1, 255 ) : 0; + const int stageDetail = stageIndex >= 0 ? Min( stageIndex + 1, 1023 ) : 0; + const int primitiveDetail = primitiveIndex >= 0 + ? Min( primitiveIndex + 1, 4095 ) : 0; + return static_cast( reason ) * 100000000 + + lightDetail * 1000000 + stageDetail * 4096 + primitiveDetail; +} + +static bool RB_ClassicFogBlend_GLFail( const viewDef_t *viewDef, + classicFogBlendDomainFailure_t failure, int detail ) { + R_ClassicFogBlendDomain_RecordBackendFallback( viewDef, + CLASSIC_FOG_BLEND_BACKEND_GL, + failure == CLASSIC_FOG_BLEND_FAILURE_NONE + ? CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED : failure, + detail ); + std::memset( &rbClassicFogBlendGLPreparedView, 0, + offsetof( rbClassicFogBlendGLPreparedView_t, stages ) ); + return false; +} + +static bool RB_ClassicFogBlend_GLFiniteArray( const float *values, + int count ) { + return RB_SharedGuiGLMatrixValid( values, count ); +} + +static bool RB_ClassicFogBlend_GLValidateTexture( + std::uint64_t resourceId, idImage *&image ) { + image = NULL; + const classicFogBlendDomainTexture_t *texture = + R_ClassicFogBlendDomain_ResolveTexture( resourceId ); + if ( resourceId == 0 || texture == NULL + || texture->textureResourceId != resourceId + || texture->image == NULL || !texture->loaded + || texture->defaulted || texture->mutableImage + || texture->textureHandle == 0 ) { + return false; + } + idImage *resolved = const_cast( texture->image ); + if ( !resolved->IsLoaded() || resolved->IsDefaulted() + || resolved->GetOpts().textureType != TT_2D + || resolved->GetDeviceHandle() != texture->textureHandle + || resolved->GetStorageGeneration() != texture->storageGeneration + || resolved->GetFilter() != texture->filter + || resolved->GetRepeat() != texture->repeat ) { + return false; + } + image = resolved; + return true; +} + +static bool RB_ClassicFogBlend_GLBuildState( + const classicFogBlendDomainLightStage_t &stage, + const classicFogBlendDomainPrimitive_t &primitive, + int &stateBits, int &cullType, GLenum &alphaFunction ) { + stateBits = 0; + cullType = CT_FRONT_SIDED; + alphaFunction = GL_ALWAYS; + if ( stage.blend.colorOperation != RENDERER_BLEND_OP_ADD + || stage.blend.alphaOperation != RENDERER_BLEND_OP_ADD + || stage.blend.sourceAlpha != stage.blend.sourceColor + || stage.blend.destinationAlpha + != stage.blend.destinationColor ) { + return false; + } + const bool replacementBlend = + stage.blend.sourceColor == RENDERER_BLEND_ONE + && stage.blend.destinationColor == RENDERER_BLEND_ZERO; + if ( stage.blend.enabled == replacementBlend + || !RB_SharedGuiGLMapSourceBlend( + stage.blend.sourceColor, stateBits ) + || !RB_SharedGuiGLMapDestinationBlend( + stage.blend.destinationColor, stateBits ) + || !RB_SharedWorldAmbientGLMapDepth( + primitive.depth, stateBits ) + || !RB_SharedGuiGLMapCull( primitive.cull, cullType ) + || !RB_SharedGuiGLMapAlphaCompare( + stage.alphaTestCompareOperation, alphaFunction ) ) { + return false; + } + if ( ( stage.colorWriteMask + & ~static_cast( RENDERER_COLOR_WRITE_RGBA ) ) != 0 ) { + return false; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_RED ) == 0 ) { + stateBits |= GLS_REDMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_GREEN ) == 0 ) { + stateBits |= GLS_GREENMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_BLUE ) == 0 ) { + stateBits |= GLS_BLUEMASK; + } + if ( ( stage.colorWriteMask & RENDERER_COLOR_WRITE_ALPHA ) == 0 ) { + stateBits |= GLS_ALPHAMASK; + } + return true; +} + +static bool RB_ClassicFogBlend_GLPrimitiveKindValid( + const classicFogBlendDomainLight_t &light, + const classicFogBlendDomainPrimitive_t &primitive ) { + if ( light.kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + if ( primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER ) { + return primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + || primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_LOCAL; + } + return primitive.kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP + && primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM; + } + return light.kind == CLASSIC_FOG_BLEND_LIGHT_BLEND + && primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER + && ( primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + || primitive.receiver == CLASSIC_FOG_BLEND_RECEIVER_LOCAL ); +} + +static bool RB_ClassicFogBlend_GLDispositionMatches( + classicFogBlendDomainLightStageDisposition_t stageDisposition, + classicFogBlendDomainPrimitiveDisposition_t primitiveDisposition ) { + switch ( stageDisposition ) { + case CLASSIC_FOG_BLEND_STAGE_DRAW: + return primitiveDisposition == CLASSIC_FOG_BLEND_PRIMITIVE_DRAW; + case CLASSIC_FOG_BLEND_STAGE_NOOP_INACTIVE_CONDITION: + return primitiveDisposition + == CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_INACTIVE_STAGE; + case CLASSIC_FOG_BLEND_STAGE_NOOP_SKIP_BLEND: + return primitiveDisposition + == CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_SKIP_BLEND; + case CLASSIC_FOG_BLEND_STAGE_NOOP_MISSING_GLOBAL_CHAIN: + return primitiveDisposition + == CLASSIC_FOG_BLEND_PRIMITIVE_NOOP_MISSING_GLOBAL_CHAIN; + default: + return false; + } +} + +static bool RB_ClassicFogBlend_GLGeometryValid( + const classicFogBlendDomainPrimitive_t &primitive, + const srfTriangles_t *&geometry ) { + geometry = primitive.legacyGeometry; + if ( geometry == NULL || primitive.vertexCount <= 0 + || primitive.firstIndex != 0 || primitive.indexCount <= 0 + || primitive.indexCount % 3 != 0 || primitive.vertexOffset != 0 + || geometry->numVerts != primitive.vertexCount + || geometry->numIndexes != primitive.indexCount + || primitive.vertexCount > idMath::INT_MAX + / static_cast( sizeof( idDrawVert ) ) + || primitive.indexCount > idMath::INT_MAX + / static_cast( sizeof( glIndex_t ) ) + || R_TriHasPrimBatchMesh( geometry ) ) { + return false; + } + if ( primitive.kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP ) { + if ( primitive.legacyDrawSurf != NULL + && primitive.legacyDrawSurf->geo != geometry ) { + return false; + } + } else if ( primitive.legacyDrawSurf == NULL + || primitive.legacyDrawSurf->geo != geometry ) { + return false; + } + if ( !RB_SharedGuiGLCacheValid( geometry->ambientCache, false, + primitive.vertexCount * static_cast( sizeof( idDrawVert ) ) ) ) { + return false; + } + R_TouchVertexCache( geometry->ambientCache ); + if ( geometry->indexCache != NULL ) { + if ( !RB_SharedGuiGLCacheValid( geometry->indexCache, true, + primitive.indexCount + * static_cast( sizeof( glIndex_t ) ) ) ) { + return false; + } + R_TouchVertexCache( geometry->indexCache ); + } + return ( r_useIndexBuffers.GetBool() && geometry->indexCache != NULL ) + || geometry->indexes != NULL; +} + +static bool RB_ClassicFogBlend_GLPreflight( + const viewDef_t *viewDef, const classicFogBlendDomainView_t &view, + int &failureDetail ) { + rbClassicFogBlendGLPreparedView_t &prepared = + rbClassicFogBlendGLPreparedView; + std::memset( &prepared, 0, + offsetof( rbClassicFogBlendGLPreparedView_t, stages ) ); + prepared.view = &view; + prepared.viewDef = viewDef; + prepared.hash = view.hash; + + if ( viewDef == NULL || view.viewDef != viewDef || !view.ready + || viewDef->renderWorld == NULL + || view.lightCount < 0 + || view.lightCount > CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHTS + || view.surfaceCount < 0 + || view.surfaceCount > CLASSIC_FOG_BLEND_DOMAIN_MAX_SURFACES + || view.lightStageCount < 0 + || view.lightStageCount + > CLASSIC_FOG_BLEND_DOMAIN_MAX_LIGHT_STAGES + || view.primitiveCount < 0 + || view.primitiveCount + > CLASSIC_FOG_BLEND_DOMAIN_MAX_PRIMITIVES + || view.drawablePrimitiveCount < 0 + || view.drawablePrimitiveCount > view.primitiveCount + || view.noopPrimitiveCount != view.primitiveCount + - view.drawablePrimitiveCount + || view.fogReceiverPrimitiveCount < 0 + || view.fogFrustumPrimitiveCount < 0 + || view.blendPrimitiveCount < 0 + || view.fogReceiverPrimitiveCount + + view.fogFrustumPrimitiveCount + view.blendPrimitiveCount + != view.drawablePrimitiveCount ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_VIEW ); + return false; + } + + const int allowedRenderFlags = RF_NO_GUI | RF_PENUMBRA_MAP | RF_PRIMARY_VIEW; + if ( viewDef->isSubview || viewDef->isMirror || viewDef->isXraySubview + || viewDef->isEditor || viewDef->superView != NULL + || viewDef->subviewSurface != NULL || viewDef->numClipPlanes != 0 + || viewDef->renderView.viewID < 0 + || ( viewDef->renderFlags & ~allowedRenderFlags ) != 0 + || viewDef->renderView.globalMaterial != NULL + || backEnd.renderTexture != NULL + || backEnd.feedbackRenderTexture != NULL + || r_skipFogLights.GetBool() || r_showOverDraw.GetInteger() != 0 + || r_singleTriangle.GetBool() || r_skipRender.GetBool() + || r_skipRenderContext.GetBool() + || view.skipBlendLights != r_skipBlendLights.GetBool() + || view.useScissor != r_useScissor.GetBool() ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_MUTATED_VIEW ); + return false; + } + if ( !glConfig.isInitialized || globalImages == NULL + || glConfig.maxTextureUnits < 2 + || glConfig.maxTextureImageUnits < 2 + || glConfig.maxTextureCoords < 2 ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_CAPACITY ); + return false; + } + if ( view.viewportX1 != viewDef->viewport.x1 + || view.viewportY1 != viewDef->viewport.y1 + || view.viewportX2 != viewDef->viewport.x2 + || view.viewportY2 != viewDef->viewport.y2 + || view.scissorX1 != viewDef->scissor.x1 + || view.scissorY1 != viewDef->scissor.y1 + || view.scissorX2 != viewDef->scissor.x2 + || view.scissorY2 != viewDef->scissor.y2 + || std::memcmp( view.projectionMatrix, viewDef->projectionMatrix, + sizeof( view.projectionMatrix ) ) != 0 + || !RB_ClassicFogBlend_GLFiniteArray( + view.projectionMatrix, 16 ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_MUTATED_VIEW ); + return false; + } + + int lightCursor = 0; + int surfaceCursor = 0; + int stageCursor = 0; + int primitiveCursor = 0; + int drawablePrimitives = 0; + int fogLights = 0; + int blendLights = 0; + int noopLights = 0; + int activeStages = 0; + int inactiveStages = 0; + int noopStages = 0; + int noopPrimitives = 0; + int fogReceiverDraws = 0; + int fogFrustumDraws = 0; + int blendDraws = 0; + int receiverSurfaces[ CLASSIC_FOG_BLEND_RECEIVER_COUNT ] = { 0, 0, 0 }; + + for ( int lightIndex = 0; lightIndex < view.lightCount; ++lightIndex ) { + const int domainLightIndex = view.firstLight + lightIndex; + const classicFogBlendDomainLight_t *light = + R_ClassicFogBlendDomain_ViewLight( view, lightIndex ); + if ( light == NULL || light->sourceOrdinal < 0 + || light->kind < CLASSIC_FOG_BLEND_LIGHT_FOG + || light->kind >= CLASSIC_FOG_BLEND_LIGHT_KIND_COUNT + || light->disposition < CLASSIC_FOG_BLEND_LIGHT_DRAW + || light->disposition + >= CLASSIC_FOG_BLEND_LIGHT_DISPOSITION_COUNT + || light->firstSurface != view.firstSurface + surfaceCursor + || light->surfaceCount < 0 + || light->surfaceCount > view.surfaceCount - surfaceCursor + || light->firstLightStage + != view.firstLightStage + stageCursor + || light->lightStageCount < 0 + || light->lightStageCount + > view.lightStageCount - stageCursor + || light->firstPrimitive + != view.firstPrimitive + primitiveCursor + || light->primitiveCount < 0 + || light->primitiveCount + > view.primitiveCount - primitiveCursor + || light->drawablePrimitiveCount < 0 + || light->drawablePrimitiveCount > light->primitiveCount + || light->noopPrimitiveCount != light->primitiveCount + - light->drawablePrimitiveCount + || !RB_ClassicFogBlend_GLFiniteArray( + &light->lightProject[0][0], 16 ) + || !RB_ClassicFogBlend_GLFiniteArray( light->fogPlane, 4 ) + || !RB_ClassicFogBlend_GLFiniteArray( + &light->fogGlobalTexgen[0][0][0], 16 ) + || !RB_ClassicFogBlend_GLFiniteArray( light->fogColor, 4 ) + || !RB_SharedGuiGLFloatValid( light->fogDensity ) + || !RB_SharedGuiGLFloatValid( light->fogDistanceScale ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_LIGHT_RANGE, lightIndex ); + return false; + } + lightCursor++; + fogLights += light->kind == CLASSIC_FOG_BLEND_LIGHT_FOG ? 1 : 0; + blendLights += light->kind == CLASSIC_FOG_BLEND_LIGHT_BLEND ? 1 : 0; + noopLights += light->disposition != CLASSIC_FOG_BLEND_LIGHT_DRAW ? 1 : 0; + int lightSurfaceTotal = 0; + for ( int receiver = 0; receiver < CLASSIC_FOG_BLEND_RECEIVER_COUNT; + ++receiver ) { + if ( light->receiverSurfaceCount[receiver] < 0 ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_LIGHT_RANGE, + lightIndex ); + return false; + } + lightSurfaceTotal += light->receiverSurfaceCount[receiver]; + receiverSurfaces[receiver] += light->receiverSurfaceCount[receiver]; + } + if ( lightSurfaceTotal != light->surfaceCount ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_LIGHT_RANGE, lightIndex ); + return false; + } + surfaceCursor += light->surfaceCount; + + int lightDrawablePrimitives = 0; + int lightNoopPrimitives = 0; + int lightFogReceivers = 0; + int lightFogFrustums = 0; + int lightBlendDraws = 0; + for ( int localStageIndex = 0; + localStageIndex < light->lightStageCount; ++localStageIndex ) { + const classicFogBlendDomainLightStage_t *stage = + R_ClassicFogBlendDomain_LightStage( *light, localStageIndex ); + const int domainStageIndex = view.firstLightStage + stageCursor; + if ( stage == NULL || stage->lightIndex != domainLightIndex + || stage->sourceStageIndex < 0 + || stage->disposition < CLASSIC_FOG_BLEND_STAGE_DRAW + || stage->disposition + >= CLASSIC_FOG_BLEND_STAGE_DISPOSITION_COUNT + || stage->firstPrimitive + != view.firstPrimitive + primitiveCursor + || stage->primitiveCount < 0 + || stage->primitiveCount + > view.primitiveCount - primitiveCursor + || stage->drawablePrimitiveCount < 0 + || stage->drawablePrimitiveCount > stage->primitiveCount + || stage->noopPrimitiveCount != stage->primitiveCount + - stage->drawablePrimitiveCount + || !RB_SharedGuiGLFloatValid( stage->condition ) + || !RB_ClassicFogBlend_GLFiniteArray( stage->color, 4 ) + || !RB_ClassicFogBlend_GLFiniteArray( + &stage->textureMatrix[0][0], 8 ) + || !RB_SharedGuiGLFloatValid( stage->alphaTestValue ) + || ( light->kind == CLASSIC_FOG_BLEND_LIGHT_FOG + && !stage->conditionIgnored ) + || ( light->kind == CLASSIC_FOG_BLEND_LIGHT_BLEND + && stage->conditionIgnored ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_STAGE_RANGE, + lightIndex, localStageIndex ); + return false; + } + + rbClassicFogBlendGLPreparedStage_t &preparedStage = + prepared.stages[stageCursor]; + std::memset( &preparedStage, 0, sizeof( preparedStage ) ); + preparedStage.stage = stage; + const bool active = stage->conditionIgnored + || stage->condition != 0.0f; + activeStages += active ? 1 : 0; + inactiveStages += active ? 0 : 1; + noopStages += stage->disposition != CLASSIC_FOG_BLEND_STAGE_DRAW + ? 1 : 0; + + if ( stage->disposition == CLASSIC_FOG_BLEND_STAGE_DRAW ) { + if ( light->kind == CLASSIC_FOG_BLEND_LIGHT_FOG ) { + if ( stage->fogTextureResourceId + != light->fogTextureResourceId + || stage->fogEnterTextureResourceId + != light->fogEnterTextureResourceId + || !RB_ClassicFogBlend_GLValidateTexture( + stage->fogTextureResourceId, + preparedStage.fogImage ) + || !RB_ClassicFogBlend_GLValidateTexture( + stage->fogEnterTextureResourceId, + preparedStage.fogEnterImage ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_TEXTURE, + lightIndex, localStageIndex ); + return false; + } + } else if ( stage->falloffTextureResourceId + != light->falloffTextureResourceId + || !RB_ClassicFogBlend_GLValidateTexture( + stage->projectionTextureResourceId, + preparedStage.projectionImage ) + || !RB_ClassicFogBlend_GLValidateTexture( + stage->falloffTextureResourceId, + preparedStage.falloffImage ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_TEXTURE, + lightIndex, localStageIndex ); + return false; + } + } + + int stageDrawablePrimitives = 0; + int stageNoopPrimitives = 0; + classicFogBlendDomainReceiver_t previousReceiver = + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL; + for ( int stagePrimitiveIndex = 0; + stagePrimitiveIndex < stage->primitiveCount; + ++stagePrimitiveIndex ) { + const classicFogBlendDomainPrimitive_t *primitive = + R_ClassicFogBlendDomain_ViewPrimitive( + view, primitiveCursor ); + if ( primitive == NULL + || primitive->lightIndex != domainLightIndex + || primitive->lightStageIndex != domainStageIndex + || primitive->kind + < CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER + || primitive->kind + >= CLASSIC_FOG_BLEND_PRIMITIVE_KIND_COUNT + || primitive->receiver + < CLASSIC_FOG_BLEND_RECEIVER_GLOBAL + || primitive->receiver + >= CLASSIC_FOG_BLEND_RECEIVER_COUNT + || primitive->receiver < previousReceiver + || primitive->disposition + < CLASSIC_FOG_BLEND_PRIMITIVE_DRAW + || primitive->disposition + >= CLASSIC_FOG_BLEND_PRIMITIVE_DISPOSITION_COUNT + || !RB_ClassicFogBlend_GLPrimitiveKindValid( + *light, *primitive ) + || !RB_ClassicFogBlend_GLDispositionMatches( + stage->disposition, primitive->disposition ) + || !RB_ClassicFogBlend_GLFiniteArray( + primitive->modelMatrix, 16 ) + || !RB_ClassicFogBlend_GLFiniteArray( + primitive->modelViewMatrix, 16 ) + || !RB_ClassicFogBlend_GLFiniteArray( + &primitive->localLightProject[0][0], 16 ) + || !RB_ClassicFogBlend_GLFiniteArray( + &primitive->fogTexgen[0][0][0], 16 ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_PRIMITIVE_STATE, + lightIndex, localStageIndex, primitiveCursor ); + return false; + } + previousReceiver = primitive->receiver; + const long long scissorWidth = + static_cast( primitive->scissorX2 ) + - primitive->scissorX1 + 1; + const long long scissorHeight = + static_cast( primitive->scissorY2 ) + - primitive->scissorY1 + 1; + if ( scissorWidth <= 0 || scissorHeight <= 0 + || scissorWidth > idMath::INT_MAX + || scissorHeight > idMath::INT_MAX ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_PRIMITIVE_STATE, + lightIndex, localStageIndex, primitiveCursor ); + return false; + } + if ( primitive->disposition + != CLASSIC_FOG_BLEND_PRIMITIVE_DRAW ) { + stageNoopPrimitives++; + lightNoopPrimitives++; + noopPrimitives++; + primitiveCursor++; + continue; + } + + const srfTriangles_t *geometry = NULL; + if ( !RB_ClassicFogBlend_GLGeometryValid( + *primitive, geometry ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_GEOMETRY, + lightIndex, localStageIndex, primitiveCursor ); + return false; + } + rbClassicFogBlendGLPreparedPrimitive_t &preparedPrimitive = + prepared.primitives[drawablePrimitives]; + std::memset( &preparedPrimitive, 0, + sizeof( preparedPrimitive ) ); + preparedPrimitive.primitive = primitive; + preparedPrimitive.stage = &preparedStage; + preparedPrimitive.geometry = geometry; + if ( !RB_ClassicFogBlend_GLBuildState( *stage, *primitive, + preparedPrimitive.stateBits, + preparedPrimitive.cullType, + preparedPrimitive.alphaFunction ) ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_STAGE_STATE, + lightIndex, localStageIndex, primitiveCursor ); + return false; + } + stageDrawablePrimitives++; + lightDrawablePrimitives++; + drawablePrimitives++; + switch ( primitive->kind ) { + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER: + lightFogReceivers++; + fogReceiverDraws++; + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP: + lightFogFrustums++; + fogFrustumDraws++; + break; + case CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER: + lightBlendDraws++; + blendDraws++; + break; + default: + break; + } + primitiveCursor++; + } + if ( stageDrawablePrimitives != stage->drawablePrimitiveCount + || stageNoopPrimitives != stage->noopPrimitiveCount ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_COVERAGE, + lightIndex, localStageIndex ); + return false; + } + stageCursor++; + } + if ( lightDrawablePrimitives != light->drawablePrimitiveCount + || lightNoopPrimitives != light->noopPrimitiveCount + || lightFogReceivers != light->fogReceiverPrimitiveCount + || lightFogFrustums != light->fogFrustumPrimitiveCount + || lightBlendDraws != light->blendPrimitiveCount ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_COVERAGE, lightIndex ); + return false; + } + } + + if ( lightCursor != view.lightCount || fogLights != view.fogLightCount + || blendLights != view.blendLightCount + || noopLights != view.noopLightCount + || surfaceCursor != view.surfaceCount + || stageCursor != view.lightStageCount + || activeStages != view.activeLightStageCount + || inactiveStages != view.inactiveLightStageCount + || noopStages != view.noopLightStageCount + || primitiveCursor != view.primitiveCount + || drawablePrimitives != view.drawablePrimitiveCount + || noopPrimitives != view.noopPrimitiveCount + || fogReceiverDraws != view.fogReceiverPrimitiveCount + || fogFrustumDraws != view.fogFrustumPrimitiveCount + || blendDraws != view.blendPrimitiveCount + || receiverSurfaces[CLASSIC_FOG_BLEND_RECEIVER_GLOBAL] + != view.receiverSurfaceCount[ + CLASSIC_FOG_BLEND_RECEIVER_GLOBAL] + || receiverSurfaces[CLASSIC_FOG_BLEND_RECEIVER_LOCAL] + != view.receiverSurfaceCount[ + CLASSIC_FOG_BLEND_RECEIVER_LOCAL] + || receiverSurfaces[CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM] + != view.receiverSurfaceCount[ + CLASSIC_FOG_BLEND_RECEIVER_FRUSTUM] ) { + failureDetail = RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_COVERAGE ); + return false; + } + prepared.lightCount = lightCursor; + prepared.stageCount = stageCursor; + prepared.primitiveCount = primitiveCursor; + prepared.drawablePrimitiveCount = drawablePrimitives; + prepared.fogReceiverDraws = fogReceiverDraws; + prepared.fogFrustumDraws = fogFrustumDraws; + prepared.blendDraws = blendDraws; + prepared.noopPrimitives = noopPrimitives; + prepared.noopStages = noopStages; + prepared.noopLights = noopLights; + prepared.ready = true; + return true; +} + +static void RB_ClassicFogBlend_GLLoadTextureMatrix( + const classicFogBlendDomainLightStage_t &stage ) { + float matrix[16] = { + stage.textureMatrix[0][0], stage.textureMatrix[1][0], 0.0f, 0.0f, + stage.textureMatrix[0][1], stage.textureMatrix[1][1], 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + stage.textureMatrix[0][3], stage.textureMatrix[1][3], 0.0f, 1.0f + }; + glMatrixMode( GL_TEXTURE ); + glLoadMatrixf( matrix ); + glMatrixMode( GL_MODELVIEW ); +} + +static void RB_ClassicFogBlend_GLDisableTexgen( int unit ) { + GL_SelectTexture( unit ); + glDisable( GL_TEXTURE_GEN_S ); + glDisable( GL_TEXTURE_GEN_T ); + glDisable( GL_TEXTURE_GEN_R ); + glDisable( GL_TEXTURE_GEN_Q ); +} + +static void RB_ClassicFogBlend_GLPrepareFog( + const rbClassicFogBlendGLPreparedPrimitive_t &prepared ) { + const classicFogBlendDomainPrimitive_t &primitive = *prepared.primitive; + const classicFogBlendDomainLightStage_t &stage = *prepared.stage->stage; + GL_SelectTexture( 0 ); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + RB_ClassicFogBlend_GLDisableTexgen( 0 ); + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + prepared.stage->fogImage->SetSamplerState( + prepared.stage->fogImage->GetFilter(), + prepared.stage->fogImage->GetRepeat() ); + prepared.stage->fogImage->Bind(); + GL_TexEnv( GL_MODULATE ); + glEnable( GL_TEXTURE_GEN_S ); + glEnable( GL_TEXTURE_GEN_T ); + glTexGenfv( GL_S, GL_OBJECT_PLANE, primitive.fogTexgen[0][0] ); + glTexGenfv( GL_T, GL_OBJECT_PLANE, primitive.fogTexgen[0][1] ); + glTexCoord2f( 0.5f, 0.5f ); + + GL_SelectTexture( 1 ); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + RB_ClassicFogBlend_GLDisableTexgen( 1 ); + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + prepared.stage->fogEnterImage->SetSamplerState( + prepared.stage->fogEnterImage->GetFilter(), + prepared.stage->fogEnterImage->GetRepeat() ); + prepared.stage->fogEnterImage->Bind(); + GL_TexEnv( GL_MODULATE ); + glEnable( GL_TEXTURE_GEN_S ); + glEnable( GL_TEXTURE_GEN_T ); + glTexGenfv( GL_S, GL_OBJECT_PLANE, primitive.fogTexgen[1][0] ); + glTexGenfv( GL_T, GL_OBJECT_PLANE, primitive.fogTexgen[1][1] ); + glTexCoord2f( primitive.fogTexgen[1][0][3], FOG_ENTER ); + GL_SelectTexture( 0 ); + glColor4f( stage.color[0], stage.color[1], stage.color[2], 1.0f ); +} + +static void RB_ClassicFogBlend_GLPrepareBlend( + const rbClassicFogBlendGLPreparedPrimitive_t &prepared ) { + const classicFogBlendDomainPrimitive_t &primitive = *prepared.primitive; + const classicFogBlendDomainLightStage_t &stage = *prepared.stage->stage; + GL_SelectTexture( 1 ); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + RB_ClassicFogBlend_GLDisableTexgen( 1 ); + prepared.stage->falloffImage->SetSamplerState( + prepared.stage->falloffImage->GetFilter(), + prepared.stage->falloffImage->GetRepeat() ); + prepared.stage->falloffImage->Bind(); + GL_TexEnv( GL_MODULATE ); + glEnable( GL_TEXTURE_GEN_S ); + glTexGenfv( GL_S, GL_OBJECT_PLANE, primitive.localLightProject[3] ); + glTexCoord2f( 0.0f, 0.5f ); + + GL_SelectTexture( 0 ); + glDisableClientState( GL_TEXTURE_COORD_ARRAY ); + RB_ClassicFogBlend_GLDisableTexgen( 0 ); + prepared.stage->projectionImage->SetSamplerState( + prepared.stage->projectionImage->GetFilter(), + prepared.stage->projectionImage->GetRepeat() ); + prepared.stage->projectionImage->Bind(); + GL_TexEnv( GL_MODULATE ); + glEnable( GL_TEXTURE_GEN_S ); + glEnable( GL_TEXTURE_GEN_T ); + glEnable( GL_TEXTURE_GEN_Q ); + glTexGenfv( GL_S, GL_OBJECT_PLANE, primitive.localLightProject[0] ); + glTexGenfv( GL_T, GL_OBJECT_PLANE, primitive.localLightProject[1] ); + glTexGenfv( GL_Q, GL_OBJECT_PLANE, primitive.localLightProject[2] ); + if ( stage.hasTextureMatrix ) { + RB_ClassicFogBlend_GLLoadTextureMatrix( stage ); + } else { + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + } + glColor4fv( stage.color ); +} + +bool RB_ClassicFogBlend_PreflightView( const viewDef_t *viewDef ) { + std::memset( &rbClassicFogBlendGLPreparedView, 0, + offsetof( rbClassicFogBlendGLPreparedView_t, stages ) ); + const classicFogBlendDomainView_t *view = + R_ClassicFogBlendDomain_FindView( viewDef ); + if ( view == NULL ) { + return RB_ClassicFogBlend_GLFail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + RB_ClassicFogBlend_GLFailureDetail( + RB_CLASSIC_FOG_BLEND_GL_REJECT_VIEW ) ); + } + if ( view->backendOutcome[CLASSIC_FOG_BLEND_BACKEND_GL] + == CLASSIC_FOG_BLEND_BACKEND_FALLBACK ) { + return false; + } + if ( !view->ready ) { + return RB_ClassicFogBlend_GLFail( viewDef, + view->failure != CLASSIC_FOG_BLEND_FAILURE_NONE + ? view->failure : CLASSIC_FOG_BLEND_FAILURE_BACKEND_NOT_READY, + view->failureDetail ); + } + int failureDetail = 0; + if ( !RB_ClassicFogBlend_GLPreflight( + viewDef, *view, failureDetail ) ) { + return RB_ClassicFogBlend_GLFail( viewDef, + CLASSIC_FOG_BLEND_FAILURE_BACKEND_REJECTED, failureDetail ); + } + return true; +} + +void RB_ClassicFogBlend_DrawOwnedView( const viewDef_t *viewDef ) { + rbClassicFogBlendGLPreparedView_t &prepared = + rbClassicFogBlendGLPreparedView; + if ( !prepared.ready || prepared.view == NULL + || prepared.viewDef != viewDef || prepared.view->viewDef != viewDef + || prepared.hash != prepared.view->hash ) { + common->Warning( "RB_ClassicFogBlend_DrawOwnedView: committed view lost its prepared transaction" ); + return; + } + // No operation below this line may return to RB_STD_FogAllLights. All + // fallible caches, resources, state mappings, and exact ranges are sealed. + prepared.committed = true; + RB_LogComment( "---------- RB_ClassicFogBlend_DrawOwnedView ----------\n" ); + if ( glConfig.GLSLProgramAvailable ) { + glUseProgramObjectARB( 0 ); + } + if ( glConfig.ARBVertexProgramAvailable ) { + glDisable( GL_VERTEX_PROGRAM_ARB ); + } + if ( glConfig.ARBFragmentProgramAvailable ) { + glDisable( GL_FRAGMENT_PROGRAM_ARB ); + } + glEnable( GL_DEPTH_TEST ); + glEnable( GL_BLEND ); + glBlendEquation( GL_FUNC_ADD ); + glDisable( GL_STENCIL_TEST ); + glDisableClientState( GL_COLOR_ARRAY ); + glEnableClientState( GL_VERTEX_ARRAY ); + + int fogReceiverDraws = 0; + int fogFrustumDraws = 0; + int blendDraws = 0; + for ( int primitiveIndex = 0; + primitiveIndex < prepared.drawablePrimitiveCount; + ++primitiveIndex ) { + const rbClassicFogBlendGLPreparedPrimitive_t &draw = + prepared.primitives[primitiveIndex]; + const classicFogBlendDomainPrimitive_t &primitive = *draw.primitive; + const classicFogBlendDomainLightStage_t &stage = *draw.stage->stage; + glLoadMatrixf( primitive.modelViewMatrix ); + if ( prepared.view->useScissor ) { + backEnd.currentScissor.x1 = primitive.scissorX1; + backEnd.currentScissor.y1 = primitive.scissorY1; + backEnd.currentScissor.x2 = primitive.scissorX2; + backEnd.currentScissor.y2 = primitive.scissorY2; + glScissor( viewDef->viewport.x1 + primitive.scissorX1, + viewDef->viewport.y1 + primitive.scissorY1, + primitive.scissorX2 + 1 - primitive.scissorX1, + primitive.scissorY2 + 1 - primitive.scissorY1 ); + } + idDrawVert *ambientVertices = static_cast( + vertexCache.Position( draw.geometry->ambientCache ) ); + glVertexPointer( 3, GL_FLOAT, sizeof( idDrawVert ), + RB_DrawVertAttributePointer( ambientVertices, + offsetof( idDrawVert, xyz ) ) ); + GL_State( draw.stateBits ); + GL_Cull( draw.cullType ); + if ( stage.alphaTestEnabled ) { + glEnable( GL_ALPHA_TEST ); + glAlphaFunc( draw.alphaFunction, stage.alphaTestValue ); + } else { + glDisable( GL_ALPHA_TEST ); + } + if ( primitive.kind == CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER ) { + RB_ClassicFogBlend_GLPrepareBlend( draw ); + blendDraws++; + } else { + RB_ClassicFogBlend_GLPrepareFog( draw ); + if ( primitive.kind + == CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP ) { + fogFrustumDraws++; + } else { + fogReceiverDraws++; + } + } + RB_DrawElementsWithCounters( draw.geometry ); + } + + for ( int unit = 1; unit >= 0; --unit ) { + RB_ClassicFogBlend_GLDisableTexgen( unit ); + glMatrixMode( GL_TEXTURE ); + glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + globalImages->BindNull(); + } + GL_SelectTexture( 0 ); + glDisable( GL_ALPHA_TEST ); + glDisableClientState( GL_COLOR_ARRAY ); + glColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + glBlendEquation( GL_FUNC_ADD ); + GL_Cull( CT_FRONT_SIDED ); + glEnable( GL_STENCIL_TEST ); + backEnd.currentSpace = NULL; + + const bool coverageRecorded = R_ClassicFogBlendDomain_RecordOwned( + viewDef, CLASSIC_FOG_BLEND_BACKEND_GL, + fogReceiverDraws, fogFrustumDraws, blendDraws, + prepared.noopPrimitives, prepared.noopStages, prepared.noopLights ); + if ( !coverageRecorded ) { + common->Warning( "RB_ClassicFogBlend_DrawOwnedView: GL ownership coverage rejected after committed draw (lights=%d stages=%d primitives=%d fogReceivers=%d fogCaps=%d blend=%d noopPrimitives=%d noopStages=%d noopLights=%d hash=%llu)", + prepared.lightCount, prepared.stageCount, prepared.primitiveCount, + fogReceiverDraws, fogFrustumDraws, blendDraws, + prepared.noopPrimitives, prepared.noopStages, prepared.noopLights, + static_cast( prepared.hash ) ); + } +} + //========================================================================================= /* @@ -8538,7 +12851,7 @@ RB_STD_ForceAmbient Lift the final scene toward a minimum brightness floor. ================== */ -static void RB_STD_ForceAmbient( void ) { +static float RB_STD_ForceAmbientValue( void ) { const bool useSimpleInteraction = !r_testARBProgram.GetBool() && ( r_useSimpleInteraction.GetBool() || glConfig.preferSimpleInteraction ); const GLuint interactionVertexProgram = r_testARBProgram.GetBool() ? VPROG_TEST : ( useSimpleInteraction ? VPROG_SIMPLE_INTERACTION : VPROG_INTERACTION ); @@ -8549,7 +12862,12 @@ static void RB_STD_ForceAmbient( void ) { !R_IsARBProgramValid( GL_VERTEX_PROGRAM_ARB, interactionVertexProgram ) || !R_IsARBProgramValid( GL_FRAGMENT_PROGRAM_ARB, interactionFragmentProgram ) ); const float ambientFloor = interactionRescueActive ? 0.20f : 0.0f; - const float ambient = idMath::ClampFloat( 0.0f, 1.0f, Max( r_forceAmbient.GetFloat(), ambientFloor ) ); + return idMath::ClampFloat( 0.0f, 1.0f, + Max( r_forceAmbient.GetFloat(), ambientFloor ) ); +} + +static void RB_STD_ForceAmbient( void ) { + const float ambient = RB_STD_ForceAmbientValue(); if ( ambient <= 0.0f || !backEnd.viewDef->viewEntitys ) { return; } @@ -10985,10 +15303,22 @@ void RB_STD_DrawView( void ) { RB_MarkPortalSkyBackdropForSceneTarget( backEnd.viewDef ); - rbSceneScaleState_t sceneScaleState; + // Retained across views (RB_STD_DrawView is the single, non-recursive + // RC_DRAW_VIEW handler) so its rect-restore list and hash keep their storage + // instead of churning the heap every 3D view; RB_ClearSceneScaleState resets + // the contents each view. Aliased so the name stays the same for all uses. + static rbSceneScaleState_t g_sceneScaleState; + rbSceneScaleState_t &sceneScaleState = g_sceneScaleState; RB_ClearSceneScaleState( sceneScaleState ); const bool rootSceneRenderTargetRequested = RB_SceneRenderTargetRequested(); const bool inlineSubviewSceneRenderTargetRequested = RB_InlineSubviewSceneRenderTargetRequested(); + int feedbackTargetWidth = 0; + int feedbackTargetHeight = 0; + int feedbackEffectivePercent = RB_SCREEN_FRACTION_NATIVE; + const bool feedbackSceneTargetScalingRequested = + RB_FeedbackSceneTargetScalingExtent( backEnd.viewDef, + feedbackTargetWidth, feedbackTargetHeight, + feedbackEffectivePercent ); const viewDef_t *portalSkySceneTargetView = RB_PortalSkySceneTargetView( backEnd.viewDef ); if ( portalSkySceneTargetView != NULL ) { RB_MarkSceneRenderTargetPreserveFarDepth( portalSkySceneTargetView ); @@ -10996,10 +15326,25 @@ void RB_STD_DrawView( void ) { const viewDef_t *sceneTargetView = inlineSubviewSceneRenderTargetRequested ? backEnd.viewDef->superView : backEnd.viewDef; - if ( ( rootSceneRenderTargetRequested || inlineSubviewSceneRenderTargetRequested ) - && RB_EnsureSceneRenderTexture( sceneTargetView ) ) { + const bool sceneRenderTargetReady = + ( rootSceneRenderTargetRequested || inlineSubviewSceneRenderTargetRequested ) + && RB_EnsureSceneRenderTexture( sceneTargetView ); + if ( sceneRenderTargetReady ) { backEnd.renderTexture = rbSceneRenderTexture; - RB_BeginSceneSupersampling( sceneScaleState, sceneTargetView ); + RB_BeginSceneScaling( sceneScaleState, sceneTargetView ); + } else if ( feedbackSceneTargetScalingRequested ) { + // The game owns this scene/post target and will composite it to the + // native backbuffer later. Map native view/scissor coordinates into the + // latched target, but do not steal ownership or present it here. + RB_BeginSceneScalingToExtent( sceneScaleState, + feedbackTargetWidth, feedbackTargetHeight, + feedbackEffectivePercent ); + } + if ( rootSceneRenderTargetRequested && !sceneRenderTargetReady ) { + // Temporal jitter is valid only when a later reconstruction pass owns the + // image. If target allocation failed, draw the direct compatibility path + // with the projection centered before any world work reaches the backbuffer. + RB_RecenterDirectTemporalProjection( sceneScaleState, backEnd.viewDef ); } // If we have a backend rendertexture, assign it here. @@ -11062,15 +15407,35 @@ void RB_STD_DrawView( void ) { backEnd.viewDef->renderWorld->RenderPortalFades(); } + // GUI output created by R_RenderGuiSurf is sorted before the normal world + // ambient ranges. Preflight and commit it here, after classic lighting but + // before either ambient walk; failure leaves every tagged drawSurf for the + // unchanged legacy filters below. + if ( r_rendererSharedInWorldGui.GetBool() ) { + (void)RB_DrawSharedInWorldGuiView( backEnd.viewDef ); + } + + // Decide complete ambient/material ownership once, before either authored + // phase changes the framebuffer. A rejected view retains both untouched + // classic walks; a committed view can no longer fall back between phases. + const bool sharedWorldAmbientOwned = + r_rendererSharedWorldAmbient.GetBool() + && RB_PrepareSharedWorldAmbientView( backEnd.viewDef ); + // Draw the non-light dependent base shading that retail Q4 submits before // fog: opaque, decal, and far-sort surfaces. Medium/close translucent // surfaces are held until after fog so additive BSE layers are not painted // over by fog volumes that only owned the opaque scene depth. const int processed = RB_STD_FindPostProcessStart( drawSurfs, numDrawSurfs ); - const bool ambientLegacySkipped = R_ModernGLExecutor_LegacyPassCanSkipForView( RENDER_PASS_AMBIENT, backEnd.viewDef ); + const bool ambientLegacySkipped = !sharedWorldAmbientOwned + && R_ModernGLExecutor_LegacyPassCanSkipForView( + RENDER_PASS_AMBIENT, backEnd.viewDef ); const bool preFogFeedback = ambientLegacySkipped && RB_HasLegacyFeedbackDrawSurfs( drawSurfs, processed, RB_DrawSurfIsPreFogMaterialPass ); const bool postFogFeedback = ambientLegacySkipped && RB_HasLegacyFeedbackDrawSurfs( drawSurfs, processed, RB_DrawSurfIsPostFogMaterialPass ); - if ( ambientLegacySkipped ) { + if ( sharedWorldAmbientOwned ) { + RB_DrawSharedWorldAmbientPhase( + CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG ); + } else if ( ambientLegacySkipped ) { if ( preFogFeedback || postFogFeedback ) { R_ModernGLExecutor_ComposeVisibleSceneForPost(); backEnd.currentRenderCopied = false; @@ -11102,8 +15467,6 @@ void RB_STD_DrawView( void ) { RB_STD_LightGridIndirect(); } - R_ModernGLExecutor_SubmitForwardPlusDecalOverlay( backEnd.viewDef ); - RB_RecordARB2InteractionBypassFramePhase( RENDERER_STARTUP_PHASE_ARB2_INTERACTION_BYPASS_AMBIENT_RESCUE ); // Apply a configurable brightness floor after ambient/material passes. @@ -11121,8 +15484,14 @@ void RB_STD_DrawView( void ) { RB_RecordARB2InteractionBypassFramePhase( RENDERER_STARTUP_PHASE_ARB2_INTERACTION_BYPASS_FRAME_TAIL ); - // fob and blend lights - if ( R_ModernGLExecutor_LegacyPassCanSkipForView( RENDER_PASS_FOG_BLEND, backEnd.viewDef ) ) { + // fog and blend lights + const bool sharedFogBlendOwned = r_rendererSharedWorldFogBlend.GetBool() + && RB_ClassicFogBlend_PreflightView( backEnd.viewDef ); + if ( sharedFogBlendOwned ) { + RB_ClassicFogBlend_DrawOwnedView( backEnd.viewDef ); + backEnd.currentRenderCopied = false; + backEnd.currentDepthCopied = false; + } else if ( R_ModernGLExecutor_LegacyPassCanSkipForView( RENDER_PASS_FOG_BLEND, backEnd.viewDef ) ) { R_ModernGLExecutor_RecordLegacyPassSkipped( RENDER_PASS_FOG_BLEND ); } else { RB_STD_FogAllLights(); @@ -11130,7 +15499,10 @@ void RB_STD_DrawView( void ) { backEnd.currentDepthCopied = false; } - if ( ambientLegacySkipped ) { + if ( sharedWorldAmbientOwned ) { + RB_DrawSharedWorldAmbientPhase( + CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG ); + } else if ( ambientLegacySkipped ) { if ( postFogFeedback ) { backEnd.currentRenderCopied = false; backEnd.currentDepthCopied = false; @@ -11157,7 +15529,23 @@ void RB_STD_DrawView( void ) { // now draw any post-processing effects using _currentRender if ( processed < numDrawSurfs ) { - RB_STD_DrawShaderPasses( drawSurfs+processed, numDrawSurfs-processed ); + const int authoredPostCount = numDrawSurfs - processed; + const classicCinematicPostDomainView_t *sharedAuthoredPostView = + r_rendererSharedCinematicPost.GetBool() + ? R_ClassicCinematicPostDomain_FindAuthoredPostView( + backEnd.viewDef ) + : NULL; + // One rejected dynamic member rolls back its complete special-view + // transaction. Later members must execute through the mature classic + // path without reporting that already-recorded rollback a second time. + const bool sharedAuthoredPost = sharedAuthoredPostView != NULL + && sharedAuthoredPostView->backendOutcome[ + CLASSIC_CINEMATIC_POST_BACKEND_GL] + == CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED; + if ( !sharedAuthoredPost || !RB_DrawSharedAuthoredPostView( + backEnd.viewDef, drawSurfs, processed, authoredPostCount ) ) { + RB_STD_DrawShaderPasses( drawSurfs + processed, authoredPostCount ); + } } // openQ4 BEGIN @@ -11168,10 +15556,23 @@ void RB_STD_DrawView( void ) { RB_RenderDebugTools( drawSurfs, numDrawSurfs ); + // Restore frontend-native view and packet scissors before the temporal pass + // builds normalized reactive regions. The source scene dimensions remain in + // sceneScaleState and the render texture itself for spatial/temporal resolve. + RB_RestoreSceneScaling( sceneScaleState ); if ( rootSceneRenderTargetRequested && RB_IsSceneRenderTexture( backEnd.renderTexture ) ) { - RB_PresentSceneRenderTargetToBackBuffer( sceneScaleState ); + if ( !RB_PresentBackendTemporalScene() ) { + RB_PresentSceneRenderTargetToBackBuffer( sceneScaleState ); + } + } + RB_RestoreDirectTemporalProjection( sceneScaleState, backEnd.viewDef ); + if ( feedbackSceneTargetScalingRequested + && ( backEnd.viewDef->renderFlags & RF_PORTAL_SKY ) == 0 ) { + // The game-owned post chain (including its spatial/SMAA rollback) will + // composite this scaled world at native resolution before drawing UI. + // Suppress the swap-tail scaler so it cannot rescale that native UI. + rbSceneScalePresentedFrame = backEnd.frameCount; } - RB_RestoreSceneSupersampling( sceneScaleState ); if ( inlineSubviewSceneRenderTargetRequested && RB_IsSceneRenderTexture( backEnd.renderTexture ) ) { diff --git a/src/renderer/tr_backend.cpp b/src/renderer/tr_backend.cpp index 318e8fd4..9889b725 100644 --- a/src/renderer/tr_backend.cpp +++ b/src/renderer/tr_backend.cpp @@ -34,6 +34,13 @@ If you have questions concerning this license or the applicable additional terms #include "RenderGraph.h" #include "RenderGraphResources.h" #include "MaterialResourceTable.h" +#include "ClassicGuiDomain.h" +#include "ClassicCinematicPostDomain.h" +#include "ClassicSpecialFrameDomain.h" +#include "ClassicWorldAmbientDomain.h" +#include "ClassicInteractionDomain.h" +#include "ClassicFogBlendDomain.h" +#include "ClassicSubviewDomain.h" #include "ModernGLExecutor.h" #include "ModernClusteredLighting.h" #include "RendererMetrics.h" @@ -584,9 +591,11 @@ const void RB_CopyRender( const void *data ) { if (cmd->image) { if ( cmd->copyDepth ) { - cmd->image->CopyDepthbuffer( cmd->x, cmd->y, cmd->imageWidth, cmd->imageHeight ); + cmd->image->CopyDepthbuffer( cmd->x, cmd->y, cmd->imageWidth, + cmd->imageHeight, cmd->cubeFace ); } else { - cmd->image->CopyFramebuffer( cmd->x, cmd->y, cmd->imageWidth, cmd->imageHeight ); + cmd->image->CopyFramebuffer( cmd->x, cmd->y, cmd->imageWidth, + cmd->imageHeight, cmd->cubeFace ); } } } @@ -631,6 +640,11 @@ static void RB_ResolveMSAA(const void* data) { const resolveRenderTargetCommand_t* cmd; cmd = (resolveRenderTargetCommand_t*)data; + if ( cmd->resolveDepth ) { + // A failed or incomplete attempt must not leave a successful stamp from + // an earlier resolve of the same target in this backend frame. + RB_InvalidateTemporalDepthStamp( cmd->destRenderTexture ); + } if ( cmd->msaaRenderTexture == NULL || cmd->destRenderTexture == NULL || !cmd->msaaRenderTexture->EnsureDeviceHandle() || @@ -673,7 +687,20 @@ static void RB_ResolveMSAA(const void* data) { glDrawBuffer( GL_NONE ); glBlitFramebuffer( 0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST ); - GL_CheckErrors(); + const GLenum depthResolveError = glGetError(); + if ( depthResolveError == GL_NO_ERROR ) { + RB_StampTemporalDepthResolved( cmd->destRenderTexture, + cmd->frameNumber, cmd->historyGeneration ); + } else { + static int lastDepthResolveWarningGeneration = -1; + if ( lastDepthResolveWarningGeneration != tr.glContextGeneration ) { + common->Warning( + "RB_ResolveMSAA: depth resolve failed with GL error 0x%04x; temporal presentation will use spatial reconstruction", + static_cast( depthResolveError ) ); + lastDepthResolveWarningGeneration = tr.glContextGeneration; + } + GL_CheckErrors(); + } } glReadBuffer(GL_COLOR_ATTACHMENT0); @@ -730,11 +757,138 @@ smp extensions, or asyncronously by another thread. ==================== */ int backEndStartTime, backEndFinishTime; + +/* +==================== +RB_ClassicSubview_CopyOwned + +The shared subview corridor owns the *capture edge*, not the child scene +walker. Once the sealed child-scene/capture record exactly matches the legacy +command, source the copy arguments from that record rather than from mutable +command storage. The child draw still deliberately uses the established +classic renderer until its complete material/light ownership has a dedicated +domain. +==================== +*/ +static bool RB_ClassicSubview_CopyOwned( const classicSubviewDomainView_t &view ) { + if ( view.captureImage == NULL || !view.captureImage->IsLoaded() + || view.captureWidth <= 0 || view.captureHeight <= 0 ) { + return false; + } + + RB_LogComment( "***************** RB_ClassicSubview_CopyOwned *****************\n" ); + return view.captureCopyDepth + ? view.captureImage->CopyDepthbuffer( view.captureX, view.captureY, + view.captureWidth, view.captureHeight, view.captureCubeFace ) + : view.captureImage->CopyFramebuffer( view.captureX, view.captureY, + view.captureWidth, view.captureHeight, view.captureCubeFace ); +} + +static const classicSubviewDomainView_t *RB_ClassicSubview_Preflight( + const viewDef_t *viewDef ) { + if ( !r_rendererSharedSubview.GetBool() || viewDef == NULL + || !viewDef->isSubview ) { + return NULL; + } + const classicSubviewDomainView_t *view = + R_ClassicSubviewDomain_FindView( viewDef ); + if ( view == NULL ) { + R_ClassicSubviewDomain_RecordBackendFallback( viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NOT_READY, 0 ); + return NULL; + } + if ( view->backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + // A descendant or sibling has already rejected this sealed nested + // transaction. Continue with the untouched command stream; do not turn + // the remaining parent edge into a mixed shared/classic ownership case. + return NULL; + } + const bool nestedDynamicReady = + R_ClassicCinematicPostDomain_SubviewTransactionReady( viewDef, + CLASSIC_CINEMATIC_POST_BACKEND_GL ); + if ( !view->ready || !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) + || ( R_ClassicSubviewDomain_IsCaptureBacked( *view ) + && ( view->captureImage == NULL || !view->captureImage->IsLoaded() ) ) + || !R_ClassicSubviewDomain_ReadyForBackend( *view, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ) ) { + R_ClassicSubviewDomain_RecordBackendFallback( viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + !view->ready ? view->failure + : ( !R_ClassicSubviewDomain_ViewSemanticsMatch( *view ) + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH + : ( !nestedDynamicReady + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE + : ( !R_ClassicSubviewDomain_ReadyForBackend( *view, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ) + ? CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE + : CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED ) ) ), + !view->ready ? view->failureDetail : 1 ); + return NULL; + } + return view; +} + +static void RB_DrawSharedDirectSubview( const void *data, + const classicSubviewDomainView_t &view ) { + // The direct SS_SUBVIEW path has no RC_COPY_RENDER edge. Its sealed view + // semantics select the mature full 3D executor, then ownership is published + // only after that complete child view returns. + RB_DrawView( data ); + if ( view.backendOutcome[CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + != CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + // A nested cinematic/post range rejected while the mature child-view + // executor was active. Its rollback already covered this complete tree. + return; + } + if ( !R_ClassicSubviewDomain_RecordDirectOwned( view.viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL ) ) { + common->Warning( "OpenGL: shared direct subview coverage rejected after committed view" ); + } +} + +// Render-demo playback is a complete recorded 3D frame. The shared boundary +// seals its packet/session provenance, then deliberately dispatches the mature +// full view executor; it cannot safely substitute only one of that view's +// depth, interaction, ambient, fog, subview, or feedback ranges. +static bool RB_DrawSharedRenderDemoView( const void *data ) { + const drawSurfsCommand_t *cmd = + reinterpret_cast( data ); + const viewDef_t *viewDef = cmd != NULL ? cmd->viewDef : NULL; + if ( !R_ClassicSpecialFrameDomain_ReadyForBackend( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_GL ) ) { + R_ClassicSpecialFrameDomain_RecordBackendFallback( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_NOT_READY, 0 ); + return false; + } + RB_DrawView( data ); + if ( !R_ClassicSpecialFrameDomain_RecordOwned( viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, viewDef->numDrawSurfs ) ) { + common->Warning( "OpenGL: shared render-demo coverage rejected after committed view" ); + } + return true; +} + void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { // r_debugRenderToTexture int c_draw3d = 0, c_draw2d = 0, c_setBuffers = 0, c_swapBuffers = 0, c_copyRenders = 0, c_specialEffects = 0, c_renderTargetOps = 0; R_GLDebugOutput_FlushMessages(); + // Clear frame-local pointers and ownership before every command stream, + // including the empty-frame fast path below. + R_ClassicGuiDomain_ResetFrame(); + R_ClassicCinematicPostDomain_ResetFrame(); + R_ClassicSpecialFrameDomain_ResetFrame(); + R_ClassicWorldAmbientDomain_ResetFrame(); + R_ClassicInteractionDomain_ResetFrame(); + R_ClassicFogBlendDomain_ResetFrame(); + R_ClassicSubviewDomain_ResetFrame(); + R_ModernClusteredLighting_ResetDecalsForFrame(); if ( cmds->commandId == RC_NOP && !cmds->next ) { return; } @@ -743,7 +897,6 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { // The legacy backend issues raw GL between the last modern pass of the previous // frame and this frame's modern submits; cached state from last frame is stale. R_GLStateCache_InvalidateAll( "backend frame begin" ); - if ( R_ScenePackets_SidePipelineRequired() ) { const int packetBuildStart = Sys_Milliseconds(); const idScenePacketFrame *scenePackets = NULL; @@ -788,6 +941,30 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { R_RendererMetrics_RecordRenderGraphResources( R_RenderGraphResources_Stats() ); R_MaterialResourceTable_PrepareFrame( *scenePackets ); R_RendererMetrics_RecordMaterialResourceTable( R_MaterialResourceTable_Stats() ); + if ( r_rendererSharedGui.GetBool() + || r_rendererSharedInWorldGui.GetBool() ) { + R_ClassicGuiDomain_PrepareFrame( *scenePackets ); + } + // Special-view topology must be sealed before cinematic/post ranges can + // join one of its atomic root transactions. + if ( r_rendererSharedSubview.GetBool() ) { + R_ClassicSubviewDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedCinematicPost.GetBool() ) { + R_ClassicCinematicPostDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedSpecialFrame.GetBool() ) { + R_ClassicSpecialFrameDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldAmbient.GetBool() ) { + R_ClassicWorldAmbientDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldInteraction.GetBool() ) { + R_ClassicInteractionDomain_PrepareFrame( *scenePackets ); + } + if ( r_rendererSharedWorldFogBlend.GetBool() ) { + R_ClassicFogBlendDomain_PrepareFrame( *scenePackets ); + } R_ModernGLExecutor_PrepareFrame( *scenePackets, legacyGraph ); rg_modernStatMirrorsZeroed = false; // active frame wrote real stats; re-zero on next dormant frame } else { @@ -823,17 +1000,60 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { backEnd.postProcessSourceColorSpace = tr.postProcessSourceColorSpace; backEnd.postProcessSMAAQuality = tr.postProcessSMAAQuality; idRenderTexture::BindNull(); + const classicSubviewDomainView_t *pendingSharedSubview = NULL; // upload any image loads that have completed //globalImages->CompleteBackgroundImageLoads(); for ( ; cmds ; cmds = (const emptyCommand_t *)cmds->next ) { + if ( pendingSharedSubview != NULL + && cmds->commandId != RC_COPY_RENDER ) { + if ( pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, + static_cast( cmds->commandId ) ); + } + pendingSharedSubview = NULL; + } switch ( cmds->commandId ) { case RC_NOP: break; - case RC_DRAW_VIEW: - R_RendererMetrics_BeginGpuTimer( ((const drawSurfsCommand_t *)cmds)->viewDef->viewEntitys ? RENDERER_GPU_TIMER_DRAW3D : RENDERER_GPU_TIMER_DRAW2D ); - if ( !((const drawSurfsCommand_t *)cmds)->viewDef->viewEntitys && R_ModernGLExecutor_LegacyPassCanSkip( RENDER_PASS_GUI ) ) { + case RC_DRAW_VIEW: { + const viewDef_t *drawView = ((const drawSurfsCommand_t *)cmds)->viewDef; + const classicSubviewDomainView_t *sharedSubview = + RB_ClassicSubview_Preflight( drawView ); + pendingSharedSubview = sharedSubview != NULL + && R_ClassicSubviewDomain_IsCaptureBacked( *sharedSubview ) + ? sharedSubview : NULL; + R_RendererMetrics_BeginGpuTimer( drawView->viewEntitys ? RENDERER_GPU_TIMER_DRAW3D : RENDERER_GPU_TIMER_DRAW2D ); + const bool sharedRenderDemo = drawView->viewEntitys != NULL + && r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRenderDemoView( drawView ) != NULL; + const bool sharedCinematicRoot = !drawView->viewEntitys + && r_rendererSharedCinematicPost.GetBool() + && R_ClassicCinematicPostDomain_FindRootCinematicView( drawView ) != NULL; + if ( sharedSubview != NULL && R_ClassicSubviewDomain_IsDirect( *sharedSubview ) ) { + RB_DrawSharedDirectSubview( cmds, *sharedSubview ); + } else if ( sharedRenderDemo ) { + if ( !RB_DrawSharedRenderDemoView( cmds ) ) { + RB_DrawView( cmds ); + } + } else if ( sharedCinematicRoot ) { + if ( !RB_DrawSharedCinematicRootView( drawView ) ) { + RB_DrawView( cmds ); + } + } else if ( !drawView->viewEntitys + && r_rendererSharedGui.GetBool() ) { + // The shared owner is view-atomic. If its complete preflight or + // execution gate rejects this view, execute the untouched classic + // view immediately; never mix shared and aggregate GUI stages. + if ( !RB_DrawSharedGuiView( drawView ) ) { + RB_DrawView( cmds ); + } + } else if ( !drawView->viewEntitys && R_ModernGLExecutor_LegacyPassCanSkip( RENDER_PASS_GUI ) ) { R_ModernGLExecutor_RecordLegacyPassSkipped( RENDER_PASS_GUI ); } else { RB_DrawView( cmds ); @@ -846,9 +1066,23 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { c_draw2d++; } break; + } case RC_DRAW_SPECIAL_EFFECTS: R_RendererMetrics_BeginGpuTimer( RENDERER_GPU_TIMER_SPECIAL_EFFECTS ); - if ( R_ModernGLExecutor_LegacyPassCanSkip( RENDER_PASS_SPECIAL_EFFECTS ) ) { + if ( r_rendererSharedSpecialFrame.GetBool() + && R_ClassicSpecialFrameDomain_FindRavenEffectsView( + reinterpret_cast( cmds )->viewDef ) != NULL + && !R_ClassicSpecialFrameDomain_ReadyForBackend( + reinterpret_cast( cmds )->viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_GL ) ) { + R_ClassicSpecialFrameDomain_RecordBackendFallback( + reinterpret_cast( cmds )->viewDef, + CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS, + CLASSIC_SPECIAL_FRAME_BACKEND_GL, + CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_NOT_READY, 0 ); + RB_DrawSpecialEffects( cmds ); + } else if ( R_ModernGLExecutor_LegacyPassCanSkip( RENDER_PASS_SPECIAL_EFFECTS ) ) { R_ModernGLExecutor_RecordLegacyPassSkipped( RENDER_PASS_SPECIAL_EFFECTS ); } else { RB_DrawSpecialEffects( cmds ); @@ -869,6 +1103,19 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { R_RendererMetrics_EndGpuTimer(); c_renderTargetOps++; break; + case RC_RESOLVE_TEMPORAL_PRESENTATION: { + R_RendererMetrics_BeginGpuTimer( RENDERER_GPU_TIMER_RENDER_TARGET ); + resolveTemporalPresentationCommand_t executionCommand = + *reinterpret_cast( cmds ); + if ( tr.takingScreenshot ) { + executionCommand.captureFrame = true; + executionCommand.historyValid = false; + } + (void)RB_ResolveTemporalPresentation( executionCommand ); + R_RendererMetrics_EndGpuTimer(); + c_renderTargetOps++; + break; + } case RC_CLEAR_RENDERTARGET: R_RendererMetrics_BeginGpuTimer( RENDERER_GPU_TIMER_RENDER_TARGET ); RB_ClearRenderTarget(cmds); @@ -905,18 +1152,72 @@ void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ) { c_swapBuffers++; break; } - case RC_COPY_RENDER: + case RC_COPY_RENDER: { R_RendererMetrics_BeginGpuTimer( RENDERER_GPU_TIMER_COPY_RENDER ); - RB_CopyRender( cmds ); + const copyRenderCommand_t *copy = + reinterpret_cast( cmds ); + const bool sharedCaptureMatches = pendingSharedSubview != NULL + && pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED + && !r_skipCopyTexture.GetBool() + && R_ClassicSubviewDomain_CaptureMatches( *pendingSharedSubview, + copy->image, copy->x, copy->y, copy->imageWidth, + copy->imageHeight, copy->cubeFace, copy->copyDepth ); + const bool copied = sharedCaptureMatches + ? RB_ClassicSubview_CopyOwned( *pendingSharedSubview ) + : ( !r_skipCopyTexture.GetBool() + ? ( RB_CopyRender( cmds ), true ) : false ); + if ( pendingSharedSubview != NULL + && pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + if ( sharedCaptureMatches && copied ) { + R_ClassicSubviewDomain_RecordOwned( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + pendingSharedSubview->captureImage, + pendingSharedSubview->captureX, + pendingSharedSubview->captureY, + pendingSharedSubview->captureWidth, + pendingSharedSubview->captureHeight, + pendingSharedSubview->captureCubeFace, + pendingSharedSubview->captureCopyDepth ); + } else if ( r_skipCopyTexture.GetBool() || !copied ) { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_REJECTED, 2 ); + } else { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, 2 ); + } + } + pendingSharedSubview = NULL; R_RendererMetrics_EndGpuTimer(); c_copyRenders++; break; + } default: common->Error( "RB_ExecuteBackEndCommands: bad commandId" ); break; } } + if ( pendingSharedSubview != NULL ) { + if ( pendingSharedSubview->backendOutcome[ + CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL] + == CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED ) { + R_ClassicSubviewDomain_RecordBackendFallback( + pendingSharedSubview->viewDef, CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL, + CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH, 3 ); + } + } + R_ClassicSpecialFrameDomain_FinalizeBackendFrame( + CLASSIC_SPECIAL_FRAME_BACKEND_GL ); + // go back to the default texture so the editor doesn't mess up a bound image GL_SelectTexture( 0 ); R_BindTextureForDirectAccess( GL_TEXTURE_2D, 0 ); diff --git a/src/renderer/tr_deform.cpp b/src/renderer/tr_deform.cpp index e7a0c844..f8995599 100644 --- a/src/renderer/tr_deform.cpp +++ b/src/renderer/tr_deform.cpp @@ -63,6 +63,9 @@ static void R_FinishDeform( drawSurf_t *drawSurf, srfTriangles_t *newTri, idDraw } // if we are out of vertex cache, leave it the way it is if ( newTri->ambientCache ) { + // srfTriangles_t::deformedSurface describes borrowed topology ownership + // for generated models; it is not a material-deform execution flag. The + // per-draw ClassicDeformDomain record seals this replacement instead. drawSurf->geo = newTri; } } diff --git a/src/renderer/tr_fontTTF.cpp b/src/renderer/tr_fontTTF.cpp index 47cd3d88..d5617d41 100644 --- a/src/renderer/tr_fontTTF.cpp +++ b/src/renderer/tr_fontTTF.cpp @@ -570,8 +570,14 @@ R_TTFCreateAtlasMaterial The atlas lives in a generated image whose name starts with an underscore, so the image manager hands the same object to every reference regardless of the -sampler parameters asked for. The material is created from source text rather -than shipped as a .mtr, which keeps this feature inside the executable. +sampler parameters asked for. The process-local material deliberately has the +same name as that image. An implicit material initially maps an image named +after itself; sharing the intrinsic atlas identity means that first parse finds +the scratch image which was uploaded immediately before this call instead of +probing a nonexistent file and leaving a redundant defaulted image behind. + +The material is then installed from source text rather than shipped as a .mtr, +which keeps this feature inside the executable and pins the GUI sampler state. ================ */ static const idMaterial *R_TTFCreateAtlasMaterial( const char *imageName, const char *materialName ) { @@ -586,15 +592,11 @@ static const idMaterial *R_TTFCreateAtlasMaterial( const char *imageName, const return NULL; } - // Reinstall unconditionally when the declaration is not already sampling the - // atlas. Returning an existing declaration untouched would hand back a - // material the manager had reset to the implicit default, which samples a - // name no image is registered under and therefore draws nothing at all. - if ( !R_TTFMaterialSamplesAtlas( material, imageName ) ) { - R_TTFInstallAtlasStage( material, source ); - } else { - material->SetSort( SS_GUI ); - } + // Always replace the implicit stage. It already resolves to the atlas (the + // material and image identities match), which keeps creation warning-free, + // but only this generated source pins blend, filtering, clamp and no-picmip + // semantics and survives a declaration purge as a complete definition. + R_TTFInstallAtlasStage( material, source ); for ( int i = 0; i < ttfAtlasMaterials.Num(); i++ ) { if ( ttfAtlasMaterials[i]->materialName.Icmp( materialName ) == 0 ) { @@ -977,7 +979,7 @@ static void R_TTFBuildExtendedPages( idTrueTypeFont &face, const char *fontName, } const idStr imageName = va( "_ttfatlasx_%s_%i", safeName.c_str(), pointSize ); - const idStr materialName = va( "openq4/ttffontx/%s_%i", safeName.c_str(), pointSize ); + const idStr materialName = imageName; glyphInfo_t *glyphs = (glyphInfo_t *)Mem_ClearedAlloc( covered.Num() * sizeof( glyphInfo_t ) ); bool *isCovered = (bool *)Mem_ClearedAlloc( covered.Num() * sizeof( bool ) ); @@ -1047,7 +1049,7 @@ static bool R_TTFBuildSlot( idTrueTypeFont &face, const char *fontName, const q4 idStr safeName = fontName; safeName.Replace( "/", "_" ); const idStr imageName = va( "_ttfatlas_%s_%i", safeName.c_str(), slot.pointSize ); - const idStr materialName = va( "openq4/ttffont/%s_%i", safeName.c_str(), slot.pointSize ); + const idStr materialName = imageName; q4TTFAtlasResult_t result; const bool packed = R_TTFPackAtlas( face, fontName, slot.pointSize, upscale, codePoints, count, true, diff --git a/src/renderer/tr_guisurf.cpp b/src/renderer/tr_guisurf.cpp index d842a640..459f343b 100644 --- a/src/renderer/tr_guisurf.cpp +++ b/src/renderer/tr_guisurf.cpp @@ -310,7 +310,11 @@ void R_RenderGuiSurf( idUserInterface *gui, drawSurf_t *drawSurf ) { // call the gui, which will call the 2D drawing functions tr.guiModel->Clear(); gui->Redraw( tr.viewDef->renderView.time, false ); + // The generated quads become ordinary 3D drawSurfs. Preserve that origin + // explicitly so shared in-world ownership cannot claim root UI by sort. + tr.inWorldGuiEmissionDepth++; tr.guiModel->EmitToCurrentView( modelMatrix, drawSurf->space->weaponDepthHack ); + tr.inWorldGuiEmissionDepth--; tr.guiModel->Clear(); R_UpdateGuiScreenRectState( gui, drawSurf ); diff --git a/src/renderer/tr_light.cpp b/src/renderer/tr_light.cpp index 4587e02e..275c0672 100644 --- a/src/renderer/tr_light.cpp +++ b/src/renderer/tr_light.cpp @@ -246,6 +246,10 @@ bool R_CreateAmbientCache( srfTriangles_t *tri, bool needsLighting ) { if ( tri->ambientCache ) { return true; } + if ( R_GpuSkinning_IsCandidate( tri ) + && R_GpuSkinning_PrepareAmbientCache( tri, needsLighting ) ) { + return true; + } // we are going to use it for drawing, so make sure we have the tangents and normals if ( needsLighting && !tri->tangentsCalculated ) { R_DeriveTangents( tri ); @@ -272,6 +276,9 @@ bool R_CreatePackedSurfaceFrameCaches( srfTriangles_t *tri, bool needsLighting, if ( tri == NULL || tri->numVerts <= 0 ) { return false; } + const bool gpuPrepared = tri->ambientCache == NULL + && R_GpuSkinning_IsCandidate( tri ) + && R_GpuSkinning_PrepareAmbientCache( tri, needsLighting ); idDrawVert *sourceVerts = tri->verts; glIndex_t *sourceIndexes = tri->indexes; @@ -289,18 +296,21 @@ bool R_CreatePackedSurfaceFrameCaches( srfTriangles_t *tri, bool needsLighting, } #endif - if ( sourceVerts == NULL ) { + if ( sourceVerts == NULL && !gpuPrepared ) { return false; } - if ( needsLighting && !tri->tangentsCalculated && sourceVerts == tri->verts ) { + if ( !gpuPrepared && needsLighting && !tri->tangentsCalculated && sourceVerts == tri->verts ) { R_DeriveTangents( tri ); sourceVerts = tri->verts; } - tri->ambientCache = vertexCache.AllocFrameTemp( sourceVerts, tri->numVerts * sizeof( sourceVerts[0] ) ); - if ( !tri->ambientCache ) { - return false; + if ( !gpuPrepared ) { + tri->ambientCache = vertexCache.AllocFrameTemp( sourceVerts, + tri->numVerts * sizeof( sourceVerts[0] ) ); + if ( !tri->ambientCache ) { + return false; + } } if ( r_useIndexBuffers.GetBool() && createIndexCache && tri->numIndexes > 0 && sourceIndexes != NULL ) { @@ -349,6 +359,10 @@ static srfTriangles_t *R_CreateFrameSubmitTri( srfTriangles_t *tri, bool needsLi srfTriangles_t *submitTri = (srfTriangles_t *)R_FrameAlloc( sizeof( *submitTri ) ); *submitTri = *tri; + // The frame snapshot borrows immutable sidecars and the palette from the + // dynamic-model snapshot. It must never free the source allocation. + submitTri->gpuSkinningJointPaletteAlloc = NULL; + submitTri->numGpuSkinningJointPaletteAllocJoints = 0; submitTri->verts = sourceVerts; submitTri->indexes = sourceIndexes; #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) @@ -945,6 +959,7 @@ viewLight_t *R_SetLightDefViewLight( idRenderLightLocal *light ) { vLight->pointLight = light->parms.pointLight; vLight->parallel = light->parms.parallel; vLight->lightRadius = light->parms.lightRadius; + vLight->lightAxis = light->parms.axis; vLight->lightProject[0] = light->lightProject[0]; vLight->lightProject[1] = light->lightProject[1]; vLight->lightProject[2] = light->lightProject[2]; @@ -1124,6 +1139,10 @@ bool R_LinkLightSurf( const drawSurf_t **link, const srfTriangles_t *tri, const } drawSurf = (drawSurf_t *)R_FrameAlloc( sizeof( *drawSurf ) ); + // Receiver and stencil-volume drawSurfs do not run R_FinalizeDrawSurf. + // Clear the embedded slot so whole-drawSurf copies never read indeterminate + // contract bytes; scene packets seal their role-specific NOT_APPLICABLE copy. + memset( &drawSurf->classicDeform, 0, sizeof( drawSurf->classicDeform ) ); drawSurf->geo = tri; drawSurf->space = space; @@ -1185,7 +1204,7 @@ static const portalArea_t *R_DrawSurfAreaForGlobalPoint( const idVec3 &point ) { return NULL; } -static const portalArea_t *R_ResolveDrawSurfArea( const srfTriangles_t *tri, const viewEntity_t *space ) { +static const portalArea_t *R_ResolveDrawSurfAreaUncached( const srfTriangles_t *tri, const viewEntity_t *space ) { if ( tri == NULL || space == NULL || tr.viewDef == NULL || tr.viewDef->renderWorld == NULL ) { return NULL; } @@ -1213,6 +1232,100 @@ static const portalArea_t *R_ResolveDrawSurfArea( const srfTriangles_t *tri, con return R_FallbackDrawSurfArea( space ); } +typedef struct drawSurfAreaMemo_s { + const srfTriangles_t * tri; + const idRenderEntityLocal * entityDef; + const portalArea_t * area; + idBounds bounds; + float modelMatrix[16]; + int entityModFrame; +} drawSurfAreaMemo_t; + +static const int DRAWSURF_AREA_MEMO_MAX = 65536; +static idList s_drawSurfAreaMemo; +static idHashIndex s_drawSurfAreaMemoHash; +static idHashIndex s_drawSurfAreaMemoEntityHash; + +static ID_INLINE int R_DrawSurfAreaMemoKey( const idRenderEntityLocal *entityDef, const srfTriangles_t *tri ) { + return ( int )( ( ( ( uintptr_t )entityDef ) >> 4 ) ^ ( ( ( uintptr_t )tri ) >> 4 ) ); +} + +static ID_INLINE int R_DrawSurfAreaMemoEntityKey( const idRenderEntityLocal *entityDef ) { + return ( int )( ( ( uintptr_t )entityDef ) >> 4 ); +} + +void R_ClearDrawSurfAreaMemo( void ) { + s_drawSurfAreaMemo.Clear(); + s_drawSurfAreaMemoHash.Free(); + s_drawSurfAreaMemoEntityHash.Free(); +} + +void R_InvalidateDrawSurfAreaMemoForEntity( const idRenderEntityLocal *entityDef ) { + // Tombstone instead of removing so hash indexes stay stable; the size cap in + // R_ResolveDrawSurfArea reclaims tombstones by clearing everything. A second + // hash keyed on the entity alone lets invalidation walk only this entity's + // entries rather than scanning the whole memo on every FreeEntityDef. + const int entityKey = R_DrawSurfAreaMemoEntityKey( entityDef ); + for ( int i = s_drawSurfAreaMemoEntityHash.First( entityKey ); i != -1; + i = s_drawSurfAreaMemoEntityHash.Next( i ) ) { + if ( s_drawSurfAreaMemo[i].entityDef == entityDef ) { + s_drawSurfAreaMemo[i].entityDef = NULL; + } + } +} + +static const portalArea_t *R_ResolveDrawSurfArea( const srfTriangles_t *tri, const viewEntity_t *space ) { + if ( tri == NULL || space == NULL || tr.viewDef == NULL || tr.viewDef->renderWorld == NULL ) { + return NULL; + } + + const idRenderEntityLocal *entityDef = space->entityDef; + if ( entityDef == NULL ) { + // frame-temporary spaces (BSE effects) have no stable identity to key on + return R_ResolveDrawSurfAreaUncached( tri, space ); + } + + // A memo hit requires every input the uncached resolve reads to be + // bit-identical to the stored resolve: tri->bounds and space->modelMatrix + // by value, and lastModifiedFrameNum for the entityRefs fallback path, + // so a hit can never change drawSurf->area. + const int key = R_DrawSurfAreaMemoKey( entityDef, tri ); + int entryIndex = -1; + for ( int i = s_drawSurfAreaMemoHash.First( key ); i != -1; i = s_drawSurfAreaMemoHash.Next( i ) ) { + const drawSurfAreaMemo_t &entry = s_drawSurfAreaMemo[i]; + if ( entry.tri != tri || entry.entityDef != entityDef ) { + continue; + } + entryIndex = i; + if ( entry.entityModFrame == entityDef->lastModifiedFrameNum + && memcmp( &entry.bounds, &tri->bounds, sizeof( entry.bounds ) ) == 0 + && memcmp( entry.modelMatrix, space->modelMatrix, sizeof( entry.modelMatrix ) ) == 0 ) { + return entry.area; + } + break; + } + + const portalArea_t *area = R_ResolveDrawSurfAreaUncached( tri, space ); + + if ( entryIndex == -1 ) { + if ( s_drawSurfAreaMemo.Num() >= DRAWSURF_AREA_MEMO_MAX ) { + R_ClearDrawSurfAreaMemo(); + } + entryIndex = s_drawSurfAreaMemo.Num(); + drawSurfAreaMemo_t &newEntry = s_drawSurfAreaMemo.Alloc(); + newEntry.tri = tri; + newEntry.entityDef = entityDef; + s_drawSurfAreaMemoHash.Add( key, entryIndex ); + s_drawSurfAreaMemoEntityHash.Add( R_DrawSurfAreaMemoEntityKey( entityDef ), entryIndex ); + } + drawSurfAreaMemo_t &entry = s_drawSurfAreaMemo[entryIndex]; + entry.area = area; + entry.bounds = tri->bounds; + memcpy( entry.modelMatrix, space->modelMatrix, sizeof( entry.modelMatrix ) ); + entry.entityModFrame = entityDef->lastModifiedFrameNum; + return area; +} + /* ====================== R_ClippedLightScissorRectangle @@ -1363,25 +1476,6 @@ idScreenRect R_CalcLightScissorRectangle( viewLight_t *vLight ) { return r; } -/* -================= -R_ViewLightHasStaticWorldLocalInteractions -================= -*/ -static bool R_ViewLightHasStaticWorldLocalInteractions( const viewLight_t *vLight ) { - for ( const drawSurf_t *surf = vLight->localInteractions; surf != NULL; surf = surf->nextOnLight ) { - const viewEntity_t *space = surf->space; - if ( space == NULL || space->entityDef == NULL || space->entityDef->parms.hModel == NULL ) { - continue; - } - if ( space->entityDef->parms.hModel->IsStaticWorldModel() ) { - return true; - } - } - - return false; -} - static bool R_IsUsablePrelightModel( idRenderModel *model ) { if ( model == NULL ) { return false; @@ -1450,6 +1544,15 @@ bool R_ShadowMapLightWillUseShadowMaps( const idRenderLightLocal *lightDef ) { if ( lightDef == NULL || lightDef->shadowMapStencilFallbackSticky ) { return false; } + // Optimized prelights replace every static-world per-surface volume with + // one combined volume. Whether every such surface is representable in a + // shadow map is known only after the light's complete interaction list is + // walked. Keep entity/dynamic volumes resident for these lights so a later + // prelight map miss can still select a complete whole-light stencil path. + if ( r_useOptimizedShadows.GetBool() + && R_LightHasRealPrelightModel( lightDef->parms ) ) { + return false; + } if ( lightDef->parms.noShadows || lightDef->parms.noDynamicShadows ) { return false; } @@ -1476,11 +1579,11 @@ R_AddOptimizedPrelightShadows ================= */ static void R_AddOptimizedPrelightShadows( viewLight_t *vLight ) { - // Vulkan mapped lights retain per-surface volumes so a partial filtered - // map can stamp only its genuinely missing casters. The combined prelight - // remains available for stencil-only Vulkan lights and the OpenGL path. + // Mapped lights retain per-surface volumes so a partial filtered map can + // stamp only its genuinely missing casters. The combined prelight remains + // available for stencil-only lights. if ( vLight != NULL && - R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( + R_ShadowMapsNeedPerSurfaceStencilVolumes( vLight->lightDef ) ) { return; } @@ -1569,21 +1672,21 @@ static void R_AddOptimizedPrelightShadows( viewLight_t *vLight ) { } #endif - const bool protectStaticWorldNoSelfReceivers = R_ViewLightHasStaticWorldLocalInteractions( vLight ); + // dmap combines every static-world caster into one prelight volume. It + // cannot preserve per-material no-self ownership, so retain the retail + // global-shadow routing: the combined volume must protect both LOCAL and + // GLOBAL receivers. Static surfaces that genuinely need no-self behavior + // remain entities and therefore use their separate local volume. const bool linkedPrelightVolume = R_LinkLightSurf( - protectStaticWorldNoSelfReceivers ? &vLight->localShadows : &vLight->globalShadows, + &vLight->globalShadows, tri, NULL, light, NULL, vLight->scissorRect, true /* FIXME? */ ); vLight->shadowMapIncompleteMapMask |= vLight->shadowMapPrelightMapMissingMask; - const int prelightStencilOwnershipMask = - protectStaticWorldNoSelfReceivers - ? SHADOWMAP_RECEIVER_MASK_GLOBAL - : ( SHADOWMAP_RECEIVER_MASK_LOCAL | - SHADOWMAP_RECEIVER_MASK_GLOBAL ); if ( linkedPrelightVolume ) { vLight->shadowMapPrelightStencilReadyMask |= vLight->shadowMapPrelightStencilRequiredMask & - prelightStencilOwnershipMask; + ( SHADOWMAP_RECEIVER_MASK_LOCAL | + SHADOWMAP_RECEIVER_MASK_GLOBAL ); } } @@ -1620,6 +1723,22 @@ void R_AddLightSurfaces( void ) { if ( !lightShader ) { common->Error( "R_AddLightSurfaces: NULL lightShader" ); } + const bool authoredSpecularProbe = lightShader->HasSpecularProbe(); + const bool specularProbeRequested = authoredSpecularProbe + && r_rendererModernQuality.GetBool() + && r_rendererReflectionProbes.GetBool() + && vLight->pointLight + && !vLight->parallel; + // A namespaced probe light is metadata, never an additive classic light. + // When its leaf/master gate or supported point-volume contract is absent, + // remove it exactly as an inactive light. When admitted, retain the + // visibility/scissor record for clustered probe collection below but do + // not create interactions or shadows for it. + if ( authoredSpecularProbe && !specularProbeRequested ) { + *ptr = vLight->next; + light->viewCount = -1; + continue; + } // see if we are suppressing the light in this view if ( R_ShouldSuppressViewLightForLevelshot( tr.viewDef->renderView.viewID, light->parms.allowLightInViewID ) ) { @@ -1651,7 +1770,7 @@ void R_AddLightSurfaces( void ) { // if this is a purely additive light and no stage in the light shader evaluates // to a positive light value, we can completely skip the light - if ( !lightShader->IsFogLight() && !lightShader->IsBlendLight() ) { + if ( !specularProbeRequested && !lightShader->IsFogLight() && !lightShader->IsBlendLight() ) { int lightStageNum; const int lightStageCount = lightShader->GetNumStages(); for ( lightStageNum = 0 ; lightStageNum < lightStageCount ; lightStageNum++ ) { @@ -1721,6 +1840,10 @@ void R_AddLightSurfaces( void ) { // this one stays on the list ptr = &vLight->next; + if ( specularProbeRequested ) { + tr.pc.c_viewLights++; + continue; + } // if we are doing a soft-shadow novelty test, regenerate the light with // a random offset every time @@ -1981,7 +2104,11 @@ const float *R_SetupDrawSurfShaderRegisters( const viewEntity_t *space, const re soundEmitter = R_GetShaderSoundEmitter( space->entityDef->parms.referenceSoundHandle ); } - if ( renderEntity != NULL && renderEntity->referenceShader != NULL ) { + // GetStage(0) below returns &stages[0], which is NULL-based when the material + // has no stages (idMaterial only allocates the stage array when numStages>0), + // so require at least one stage before reading pStage->color.registers. + if ( renderEntity != NULL && renderEntity->referenceShader != NULL + && renderEntity->referenceShader->GetNumStages() > 0 ) { const shaderStage_t *pStage; renderEntity->referenceShader->EvaluateRegisters( refRegs, renderEntity->shaderParms, tr.viewDef, soundEmitter ); @@ -2015,7 +2142,9 @@ void R_FinalizeDrawSurf( drawSurf_t *drawSurf ) { return; } + R_ClassicDeformDomain_BeginDrawSurf( drawSurf ); R_DeformDrawSurf( drawSurf ); + R_ClassicDeformDomain_EndDrawSurf( drawSurf ); switch( drawSurf->material->Texgen() ) { case TG_SKYBOX_CUBE: @@ -2047,6 +2176,9 @@ void R_AddDrawSurf( const srfTriangles_t *tri, const viewEntity_t *space, const drawSurf->sort = shader->GetSort() + tr.sortOffset; drawSurf->area = NULL; drawSurf->dsFlags = extraDrawSurfFlags; + if ( tr.inWorldGuiEmissionDepth > 0 ) { + drawSurf->dsFlags |= DSF_IN_WORLD_GUI; + } drawSurf->dynamicTexCoords = NULL; drawSurf->texGenTransformAndViewOrg = NULL; drawSurf->decalColorCache = NULL; diff --git a/src/renderer/tr_lightrun.cpp b/src/renderer/tr_lightrun.cpp index 3a4fb4c9..71e39afe 100644 --- a/src/renderer/tr_lightrun.cpp +++ b/src/renderer/tr_lightrun.cpp @@ -595,6 +595,11 @@ void R_ReCreateWorldReferences( void ) { // a particular view tr.viewDef = NULL; + // entityRefs are about to be rebuilt without touching lastModifiedFrameNum, + // and memoized draw-surf areas can depend on the old ref chains through the + // solid-bounds fallback path + R_ClearDrawSurfAreaMemo(); + for ( j = 0; j < tr.worlds.Num(); j++ ) { rw = tr.worlds[j]; diff --git a/src/renderer/tr_local.h b/src/renderer/tr_local.h index d77f1642..e79269b2 100644 --- a/src/renderer/tr_local.h +++ b/src/renderer/tr_local.h @@ -34,6 +34,8 @@ If you have questions concerning this license or the applicable additional terms #include "Image.h" #include "RendererStartupDiagnostics.h" #include "RenderTexture.h" +#include "GpuSkinning.h" +#include "ClassicDeformDomain.h" #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) #include "../idlib/geometry/rvVertex.h" #endif @@ -121,6 +123,7 @@ SURFACES #include "ModelDecal.h" #include "ModelOverlay.h" #include "Interaction.h" +#include "TemporalPresentation.h" // drawSurf_t structures command the back end to render surfaces @@ -138,6 +141,9 @@ static const int DSF_BSE_EFFECT = 2; // viewDef->drawSurfs, so the flag is a statement of provenance for anything // holding one, not a test the main passes have to remember to make. static const int DSF_OUTLINE_ONLY = 4; +// Geometry emitted while R_RenderGuiSurf expands a GUI onto a 3D surface. +// This provenance is independent of the generated material's GUI sort. +static const int DSF_IN_WORLD_GUI = 8; typedef struct drawSurf_s { const srfTriangles_t *geo; @@ -155,6 +161,9 @@ typedef struct drawSurf_s { int decalColorOffset; // bytes from decalColorCache to the first stage color block int decalColorStride; // bytes between stage color blocks (numVerts * 4) int decalColorStageCount; + // Sealed by R_FinalizeDrawSurf. Receiver-only drawSurfs intentionally leave + // this slot untouched and publish NOT_APPLICABLE snapshots per packet role. + classicDeformRecord_t classicDeform; // specular directions for non vertex program cards, skybox texcoords, etc } drawSurf_t; @@ -194,7 +203,7 @@ typedef enum { bool R_ShadowMapCasterAdmissionSelfTest( void ); bool R_ShadowMapLODAdmissionSelfTest( void ); -bool R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( +bool R_ShadowMapsNeedPerSurfaceStencilVolumes( const idRenderLightLocal *lightDef ); // viewLights are allocated on the frame temporary stack memory @@ -230,6 +239,7 @@ typedef struct viewLight_s { bool pointLight; // true for point light projections bool parallel; // true for directional/parallel lights idVec3 lightRadius; // copied from renderLight parms for point-light shadow mapping + idMat3 lightAxis; // immutable authored orientation for backend probe records idPlane lightProject[4]; // light project used by backend idPlane fogPlane; // fog plane for backend fog volume rendering const srfTriangles_t * frustumTris; // light frustum for backend fog volume rendering @@ -339,6 +349,24 @@ typedef struct viewDef_s { float projectionMatrix[16]; viewEntity_t worldSpace; + // Immutable temporal-presentation metadata prepared before projection + // setup. The fields travel with RC_DRAW_VIEW so GL and Vulkan consume the + // same jitter, cut, generation, and per-view history identity. + unsigned long long temporalViewIdentity; + unsigned int temporalHistoryGeneration; + int temporalJitterIndex; + idVec2 temporalJitterPixels; + idVec3 temporalPreviousViewOrigin; + idMat3 temporalPreviousViewAxis; + idVec4 temporalPreviousProjectInfo; + idVec2 temporalPreviousJitterPixels; + temporalHistoryResetReason_t temporalHistoryResetReason; + bool temporalPrepared; + bool temporalJitterEnabled; + bool temporalHistoryValid; + bool temporalCaptureFrame; + bool temporalPreviousProjectionValid; + idRenderWorldLocal *renderWorld; int renderFlags; @@ -463,6 +491,7 @@ typedef enum { RC_COPY_RENDER, RC_SET_RENDERTEXTURE, RC_RESOLVE_MSAA, + RC_RESOLVE_TEMPORAL_PRESENTATION, RC_CLEAR_RENDERTARGET, RC_SET_POSTPROCESS_SOURCE_SIZE, RC_SET_POSTPROCESS_SOURCE_COLOR_SPACE, @@ -515,8 +544,27 @@ typedef struct { idRenderTexture* msaaRenderTexture; idRenderTexture* destRenderTexture; bool resolveDepth; + int frameNumber; + unsigned int historyGeneration; } resolveRenderTargetCommand_t; +typedef struct { + renderCommand_t commandId, * next; + idRenderTexture* sceneColorTarget; + idRenderTexture* sceneDepthTarget; + idRenderTexture* historyReadTarget; + idRenderTexture* historyWriteTarget; + const viewDef_t* viewDef; + renderPresentationState_t presentation; + unsigned int historyGeneration; + bool historyValid; + bool captureFrame; + float feedback; + float reactiveScale; + int debugMode; + advancedScreenSpaceConfig_t advancedScreenSpace; +} resolveTemporalPresentationCommand_t; + typedef struct { renderCommand_t commandId, * next; idVec4 texelSize; @@ -624,6 +672,7 @@ typedef struct { int c_numDecalIndexes; // idRenderModelDecal::AddDecalDrawSurf int c_entityUpdates, c_lightUpdates, c_entityReferences, c_lightReferences; int c_entitySnapshotsReused; // transform-only entity updates that kept the dynamic snapshot + int c_entityUpdatesElided; // bytewise-identical entity updates that also kept refs and interactions int c_guiSurfs; int frontEndMsec; // sum of time in all RE_RenderScene's in a frame } performanceCounters_t; @@ -794,6 +843,7 @@ class idRenderSystemLocal : public idRenderSystem { virtual void CropRenderSize( int width, int height, bool makePowerOfTwo = false, bool forceDimensions = false ); virtual void CaptureRenderToImage( const char *imageName ); virtual void CaptureRenderToFile( const char *fileName, bool fixAlpha ); + virtual void CaptureRenderToFile( const char *fileName, bool fixAlpha, int outputWidth, int outputHeight ); virtual void SetPortalSkyCaptureViewCallback( renderPortalSkyCaptureViewCallback_t callback ); virtual void UnCrop(); virtual void GetCardCaps( bool &oldCard, bool &nv10or20 ); @@ -822,6 +872,14 @@ class idRenderSystemLocal : public idRenderSystem { virtual int GetImageMSAASamples(idImage* image); virtual const glconfig_t & GetGLConfig( void ) const; virtual bool SetUnderwaterView( float amount, const idVec3 &tint, float fogDistance ); + virtual void GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) const; + virtual void ResetGpuFrameTiming( const char *reason ); + virtual void GetPresentationState( renderPresentationState_t &state ) const; + virtual bool ResolveTemporalPresentation( + idRenderTexture *sceneColorTarget, + idRenderTexture *sceneDepthTarget, + idRenderTexture *historyReadTarget, + idRenderTexture *historyWriteTarget ); public: // internal functions idRenderSystemLocal( void ); @@ -914,6 +972,7 @@ class idRenderSystemLocal : public idRenderSystem { // GUI drawing variables for surface creation int guiRecursionLevel; // to prevent infinite overruns + int inWorldGuiEmissionDepth; // provenance scope for R_RenderGuiSurf output class idGuiModel * guiModel; class idGuiModel * demoGuiModel; idList pendingRenderTextureDeletes; @@ -980,6 +1039,12 @@ extern idCVar r_windowHeight; // windowed mode height extern idCVar r_multiSamples; // number of antialiasing samples extern idCVar r_postAA; // post AA mode: 0 = off, 1/2/3 = SMAA medium/high/ultra, 4 = colour-edge prototype extern idCVar r_postAAStatePoisonTest; // intentionally dirty GL texture/client state before SMAA draws +extern idCVar r_pbrMaterials; // allow explicitly PBR-authored materials on modern paths +extern idCVar r_pbrGeneratedLegacyFallback; // allow development-only generated classic fallbacks +extern idCVar r_pbrDebug; // PBR attachment/fallback debug view +extern idCVar r_pbrIBL; // opt-in PBR-only analytic environment contribution +extern idCVar r_pbrIBLIntensity; // PBR analytic environment intensity +extern idCVar r_pbrInferFromLegacyMaterials; // research-only classic-material inference extern idCVar r_bloom; // enable bloom post-process extern idCVar r_bloomThreshold; // bloom bright-pass threshold extern idCVar r_bloomSoftKnee; // relative bloom soft threshold knee @@ -1090,24 +1155,40 @@ extern idCVar r_glDebugContext; // request a debug GL context when the platfor extern idCVar r_glDebugOutput; // report driver debug messages when a debug context is active extern idCVar r_glDebugSynchronous; // synchronously deliver GL debug callbacks for diagnostics extern idCVar r_rendererMetrics; // 0 off, 1 summary, 2 verbose per-frame/pass metrics -extern idCVar r_rendererGpuTimers; // sample GL timer queries when renderer metrics are enabled +extern idCVar r_rendererGpuTimers; // delayed whole-frame GPU timing; GL pass detail also requires renderer metrics extern idCVar r_rendererBenchmarkPreset; // benchmark budget preset extern idCVar r_rendererPerfThresholdP95; // custom P95 benchmark threshold in milliseconds extern idCVar r_rendererPerfThresholdP99; // custom P99 benchmark threshold in milliseconds extern idCVar r_rendererAdaptiveClusterGrid; // use preset-driven cluster-grid dimensions void R_SetLoadingScreenSwapIntervalBypass( bool active ); int R_GetEffectiveSwapInterval( void ); -extern idCVar r_rendererDynamicResolution; // allow benchmark screen-percentage experiments +extern idCVar r_rendererDynamicResolution; // automatic 3D scale from delayed GPU time extern idCVar r_rendererUploadMegs; // dynamic upload stream size in megabytes per frame buffer extern idCVar r_rendererUploadFrameBuffers; // dynamic upload stream frame-buffer rotation depth extern idCVar r_rendererUploadPersistent; // allow persistent-mapped dynamic upload stream extern idCVar r_rendererUploadBufferPool; // recycle static GL buffer names instead of gen/data/delete churn +extern idCVar r_rendererModernQuality; // master permit/rollback for Milestone-F material and lighting domains +extern idCVar r_rendererReflectionProbes; // opt-in authored clustered reflection probes +extern idCVar r_rendererClusteredDecals; // opt-in atomic clustered decal ownership +extern idCVar r_rendererFroxelVolumetrics; // opt-in bounded view-aligned volumetric integration +extern idCVar r_rendererSSR; // opt-in bounded screen-space reflections +extern idCVar r_rendererSSGI; // opt-in bounded screen-space diffuse GI extern idCVar r_rendererModernExecutor; // opt-in modern GL executor prepare path extern idCVar r_rendererModernSubmit; // opt-in modern GL draw submission before ARB2 fallback extern idCVar r_rendererGpuValidation; // compare GL43 GPU-driven compute results against CPU reference data extern idCVar r_rendererGpuValidationReadbackDelay; // defer opt-in GL43 validation readback polling extern idCVar r_rendererBindless; // opt-in experimental bindless texture diagnostics, disabled by default extern idCVar r_rendererModernVisible; // opt-in modern hybrid visible-frame composition +extern idCVar r_rendererSharedGui; // opt-in backend-neutral fixed-function 2D GUI ownership +extern idCVar r_rendererSharedInWorldGui; // opt-in backend-neutral in-world GUI ownership +extern idCVar r_rendererSharedCinematicPost; // opt-in backend-neutral cinematic/post transaction ownership +extern idCVar r_rendererSharedSpecialFrame; // opt-in backend-neutral render-demo/Raven special-frame transaction ownership +extern idCVar r_rendererSharedWorldAmbient; // opt-in backend-neutral whole-view world ambient/material ownership +extern idCVar r_rendererSharedWorldInteraction; // opt-in backend-neutral whole-view unshadowed interaction ownership +extern idCVar r_rendererSharedWorldFogBlend; // opt-in backend-neutral whole-view fog/blend ownership +extern idCVar r_rendererSharedSubview; // opt-in backend-neutral direct/capture special-subview ownership +extern idCVar r_rendererSharedDeform; // opt-in backend-neutral material-deform ownership +extern idCVar r_vkShadowFallbackTest; // diagnostic-only forced Vulkan unshadowed receiver fallback extern idCVar r_rendererModernLightingParity; // diagnostic override forcing lighting-ownership parity contracts proven extern idCVar r_rendererModernAutoPromote; // allow gated default modern-visible promotion extern idCVar r_rendererPromotionEvidence; // Phase 8 evidence token required before auto-promotion @@ -1186,7 +1267,7 @@ extern idCVar r_shadowMapDistantFilterScale; // filter-radius scale for parallel extern idCVar r_shadowMapPCSSLightRadius; // projected PCSS-lite blocker search radius extern idCVar r_shadowMapPCSSMaxRadius; // projected PCSS-lite maximum filter radius extern idCVar r_shadowMapNormalOffsetScale; // normal-offset receiver bias in shadow texels -extern idCVar r_shadowMapCasterCulling; // caster face culling: 0 = two-sided, 1 = light-facing, 2 = back faces +extern idCVar r_shadowMapCasterCulling; // caster face culling: 0 = two-sided, 1 = near shell, 2 = topology-aware automatic extern idCVar r_shadowMapPointHighPrecision; // 1 = store point shadow depth as high-precision float color extern idCVar r_shadowMapPointLights; // 1 = shadow-map point lights (the dominant Q4 light class); 0 = stencil fallback for point lights extern idCVar r_shadowMapPointSize; // point-light cube face resolution, separate from r_shadowMapSize @@ -1386,6 +1467,7 @@ extern idCVar r_shadowMapBias; // constant receiver depth bias used by project extern idCVar r_shadowMapNormalBias; // slope-aware receiver bias used by projected shadow maps extern idCVar r_shadowMapPointBias; // constant receiver depth bias used by point-light shadow maps extern idCVar r_shadowMapPointNormalBias; // slope-aware receiver bias used by point-light shadow maps +extern idCVar r_shadowMapPointMaxWorldBias; // combined point receiver depth/normal-offset cap in world units extern idCVar r_shadowMapFilterRadius; // projected-light PCF radius in texels used by simple shadow maps extern idCVar r_shadowMapPointFilterRadius; // point-light PCF radius in texels used by simple shadow maps extern idCVar r_shadowMapProjectionPad; // normalized padding applied around projected-light shadow-map coverage @@ -1552,6 +1634,12 @@ typedef struct rendererShadowTextureBindings_s { bool projectedAtlasReady; bool projectedPersistentAtlasReady; bool pointAtlasReady; + // Exact identity of pointAtlas when ready. Modern planning may precede + // classic light submission, so binding rechecks these fields to prevent a + // later-selected light's cube from satisfying an older plan. + int pointAtlasLightIndex; + int pointAtlasSignature; + int pointAtlasContentFrame; bool projectedMomentsReady; bool pointMomentsReady; bool projectedDepthCompare; @@ -1565,6 +1653,12 @@ typedef struct rendererShadowTextureBindings_s { bool RB_ShadowMapTextureBindings( rendererShadowTextureBindings_t &bindings ); +// Establish the render-world ownership scope for the persistent ARB2 shadow +// caches. Returns true when a world transition invalidated resident entries. +// Modern GL calls this before planning or sampling so it cannot observe the +// previous world's cache during the frame in which a map changes. +bool RB_ShadowMapPrepareCacheView( const viewDef_t *viewDef ); + // deletes the lazily created CopyFramebuffer/CopyDepthbuffer scratch FBOs; // must be called before GLimp_Shutdown while the old context is still current // so the names cannot alias FBOs of a recreated context after vid_restart @@ -1706,6 +1800,9 @@ idRenderModel *R_EntityDefDynamicModel( idRenderEntityLocal *def, bool collision viewEntity_t *R_SetEntityDefViewEntity( idRenderEntityLocal *def ); viewLight_t *R_SetLightDefViewLight( idRenderLightLocal *def ); +void R_ClearDrawSurfAreaMemo( void ); +void R_InvalidateDrawSurfAreaMemoForEntity( const idRenderEntityLocal *entityDef ); + const float *R_SetupDrawSurfShaderRegisters( const viewEntity_t *space, const renderEntity_t *renderEntity, const idMaterial *shader ); void R_FinalizeDrawSurf( drawSurf_t *drawSurf ); @@ -1820,6 +1917,11 @@ void RB_ApplyFlatDiffuseStage( const drawSurf_t *surf, idImage **diffuseImage, f const shaderStage_t *RB_SetLightTexture( const idRenderLightLocal *light ); void RB_DrawView( const void *data ); +bool RB_DrawSharedGuiView( const viewDef_t *viewDef ); +bool RB_DrawSharedInWorldGuiView( const viewDef_t *viewDef ); +bool RB_DrawSharedCinematicRootView( const viewDef_t *viewDef ); +bool RB_DrawSharedAuthoredPostView( const viewDef_t *viewDef, + drawSurf_t **drawSurfs, int firstSourceSurface, int sourceSurfaceCount ); void RB_DrawSpecialEffects( const void *data ); void RB_ApplyResolutionScaleToBackBuffer( void ); void RB_ApplyCRTToBackBuffer( void ); @@ -2172,6 +2274,19 @@ void RB_LogComment( const char *comment, ... ) id_attribute((format(printf,1,2)) void RB_ShowImages( void ); +// Backend implementation of RC_RESOLVE_TEMPORAL_PRESENTATION. The command is +// copied at dispatch so a capture scope that begins after frontend enqueue can +// suppress history reads/writes without mutating frame-memory ownership. +bool RB_ResolveTemporalPresentation( + const resolveTemporalPresentationCommand_t &command ); + +// GL temporal history may only consume depth copied successfully for the +// command's immutable frame/generation snapshot. Resolve commands invalidate +// before attempting a depth blit and stamp only after an error-free copy. +void RB_InvalidateTemporalDepthStamp( idRenderTexture *target ); +void RB_StampTemporalDepthResolved( idRenderTexture *target, + int frameNumber, unsigned int historyGeneration ); + void RB_ExecuteBackEndCommands( const emptyCommand_t *cmds ); diff --git a/src/renderer/tr_main.cpp b/src/renderer/tr_main.cpp index 7fcc7692..54c93025 100644 --- a/src/renderer/tr_main.cpp +++ b/src/renderer/tr_main.cpp @@ -454,7 +454,15 @@ void *R_FrameAlloc( int bytes ) { frameData_t *frame; frameMemoryBlock_t *block; void *buf; - + + // A negative size (an overflowed count*sizeof in a caller) would round to a + // still-negative value, pass the first-fit test below, rewind block->used, + // and silently hand back overlapping live memory. Reject it, and fold the + // oversize fatal up here so a huge positive request that wraps to negative + // after the +16 rounding is also caught before the block math runs. + if ( bytes < 0 || bytes > MEMORY_BLOCK_SIZE ) { + common->FatalError( "R_FrameAlloc: invalid size %i", bytes ); + } bytes = (bytes+16)&~15; // see if it can be satisfied in the current block frame = frameData; @@ -884,6 +892,14 @@ void R_SetupProjection( void ) { float jitterx, jittery; static idRandom random; + R_TemporalPresentation_PrepareView( tr.viewDef ); + + // Temporal AA owns a deterministic, backend-neutral sub-pixel sequence. + // The legacy random path remains for multi-frame screenshot accumulation. + if ( tr.viewDef->temporalJitterEnabled ) { + jitterx = tr.viewDef->temporalJitterPixels.x; + jittery = tr.viewDef->temporalJitterPixels.y; + } else // random jittering is usefull when multiple // frames are going to be blended together // for motion blurred anti-aliasing @@ -949,6 +965,8 @@ void R_SetupProjection( void ) { tr.viewDef->projectionMatrix[7] = 0; tr.viewDef->projectionMatrix[11] = -1; tr.viewDef->projectionMatrix[15] = 0; + + R_TemporalPresentation_FinalizeViewProjection( tr.viewDef ); } static void R_GetViewFrustumExtents( float &zNear, float &xmin, float &xmax, float &ymin, float &ymax ) { diff --git a/src/renderer/tr_render.cpp b/src/renderer/tr_render.cpp index 0895f02a..0b7e414f 100644 --- a/src/renderer/tr_render.cpp +++ b/src/renderer/tr_render.cpp @@ -543,6 +543,15 @@ void RB_LoadShaderTextureMatrix( const float *shaderRegisters, const textureStag glMatrixMode( GL_MODELVIEW ); } +// Cross-frame dedupe for the shared cinematic scratch image. RoQ decodes at +// ~30fps while the backend can bind the same decoded frame many times per +// second; when the identical frame is already resident we only rebind. +// frameSerial 0 = producer opted out (always upload). storageGeneration +// invalidates across vid_restart / reloadImages (AllocImage bumps it). +static const idCinematic * rb_lastCinematic = NULL; +static int rb_lastCinematicSerial = 0; +static uint64_t rb_lastCinematicStorageGeneration = 0; + /* ====================== RB_BindVariableStageImage @@ -565,7 +574,26 @@ void RB_BindVariableStageImage( const textureStage_t *texture, const float *shad cin = texture->cinematic->ImageForTime( (int)(1000 * ( backEnd.viewDef->floatTime + backEnd.viewDef->renderView.shaderParms[11] ) ) ); if ( cin.image ) { - globalImages->cinematicImage->UploadScratch( cin.image, cin.imageWidth, cin.imageHeight ); + idImage *cinImage = globalImages->cinematicImage; + const bool sameFrameResident = + cin.frameSerial != 0 + && texture->cinematic == rb_lastCinematic + && cin.frameSerial == rb_lastCinematicSerial + && cinImage->IsLoaded() + && cinImage->GetStorageGeneration() == rb_lastCinematicStorageGeneration + && cinImage->GetOpts().textureType == TT_2D + && cin.imageHeight != cin.imageWidth * 6; + if ( sameFrameResident ) { + // identical pixels already resident: rebind exactly the way + // UploadScratch's SubImageUpload would have (direct access with + // tmu-tracker update, no Bind() texture-type enable dance) + R_BindTextureForDirectAccess( GL_TEXTURE_2D, cinImage->GetDeviceHandle() ); + } else { + globalImages->cinematicImage->UploadScratch( cin.image, cin.imageWidth, cin.imageHeight ); + rb_lastCinematic = texture->cinematic; + rb_lastCinematicSerial = cin.frameSerial; + rb_lastCinematicStorageGeneration = cinImage->GetStorageGeneration(); + } } else { globalImages->blackImage->Bind(); } diff --git a/src/renderer/tr_subview.cpp b/src/renderer/tr_subview.cpp index a0349419..92929e97 100644 --- a/src/renderer/tr_subview.cpp +++ b/src/renderer/tr_subview.cpp @@ -337,8 +337,10 @@ static void R_RemoteRender( drawSurf_t *surf, textureStage_t *stage ) { return; } - // if the entity doesn't have a remoteRenderView, do nothing - if ( !surf->space->entityDef->parms.remoteRenderView ) { + // if the entity doesn't have a remoteRenderView, do nothing. BSE effect + // surfaces and in-world GUI emits carry a NULL entityDef, so guard it: an + // entity-less surface can never supply a remoteRenderView. + if ( surf->space->entityDef == NULL || !surf->space->entityDef->parms.remoteRenderView ) { return; } diff --git a/src/renderer/tr_turboshadow.cpp b/src/renderer/tr_turboshadow.cpp index 80e3d700..f38d8b48 100644 --- a/src/renderer/tr_turboshadow.cpp +++ b/src/renderer/tr_turboshadow.cpp @@ -671,17 +671,26 @@ geometry and should never be fed packed surfaces directly. */ srfTriangles_t *R_CreateTurboShadowVolumeForSurface( const idRenderEntityLocal *ent, const srfTriangles_t *tri, const idRenderLightLocal *light, srfCullInfo_t &cullInfo ) { + gpuSkinningSurface_t gpuSurface; + const bool gpuStencilFallback = r_gpuSkinning.GetBool() + && R_GpuSkinning_IsCandidate( tri ) + && R_GpuSkinning_GetSurface( tri, gpuSurface ); + srfTriangles_t *shadowTri = NULL; #if defined( _MD5R_SUPPORT ) || defined( Q4SDK_MD5R ) if ( tri->primBatchMesh != NULL ) { - if ( srfTriangles_t *packedShadowTri = R_CreatePackedTurboShadowVolume( ent, tri, light, cullInfo ) ) { - return packedShadowTri; - } + shadowTri = R_CreatePackedTurboShadowVolume( ent, tri, light, cullInfo ); } #endif - if ( tr.backEndRendererHasVertexPrograms && r_useShadowVertexProgram.GetBool() ) { - return R_CreateVertexProgramTurboShadowVolume( ent, tri, light, cullInfo ); + if ( shadowTri == NULL && tr.backEndRendererHasVertexPrograms + && r_useShadowVertexProgram.GetBool() ) { + shadowTri = R_CreateVertexProgramTurboShadowVolume( ent, tri, light, cullInfo ); } - - return R_CreateTurboShadowVolume( ent, tri, light, cullInfo ); -} \ No newline at end of file + if ( shadowTri == NULL ) { + shadowTri = R_CreateTurboShadowVolume( ent, tri, light, cullInfo ); + } + if ( shadowTri != NULL && gpuStencilFallback ) { + R_GpuSkinning_RecordFallback( GPU_SKINNING_FALLBACK_STENCIL_VOLUME ); + } + return shadowTri; +} diff --git a/src/sound/OpenAL/AL_SoundSample.cpp b/src/sound/OpenAL/AL_SoundSample.cpp index 7ebfd03e..1c6bb22c 100644 --- a/src/sound/OpenAL/AL_SoundSample.cpp +++ b/src/sound/OpenAL/AL_SoundSample.cpp @@ -202,6 +202,26 @@ static void SoundSample_AppendLocalizedVOVariants( idList< idStr >& variants, co } } +/* +======================== +SoundSample_AppendMissingQ4StockFallback + +The retail ambient_water_splash_big shader names a sample that was omitted +from the shipped PK4s. Probe the authored path first so mods can supply it, +then fall back to the shipped splash from the same sound family. +======================== +*/ +static void SoundSample_AppendMissingQ4StockFallback( idList< idStr >& variants, const idStr& baseName ) +{ + idStr canonicalName = baseName; + canonicalName.BackSlashesToSlashes(); + canonicalName.StripFileExtension(); + if( canonicalName.Icmp( "sound/ambience/water/splash_big" ) == 0 ) + { + SoundSample_AppendUniqueSampleVariant( variants, "sound/ambience/water/splash_small02" ); + } +} + static bool openQ4_CanUploadSampleToOpenAL() { ALCcontext* const expectedContext = soundSystemLocal.hardware.GetOpenALContext(); @@ -447,6 +467,7 @@ void idSoundSample_OpenAL::LoadResource() SoundSample_AppendLocalizedVOVariants( sampleVariants, baseSampleName, "english" ); } SoundSample_AppendUniqueSampleVariant( sampleVariants, baseSampleName ); + SoundSample_AppendMissingQ4StockFallback( sampleVariants, baseSampleName ); for( int i = 0; i < sampleVariants.Num(); i++ ) { diff --git a/src/sound/snd_system.cpp b/src/sound/snd_system.cpp index 7b27f50b..63b10b81 100644 --- a/src/sound/snd_system.cpp +++ b/src/sound/snd_system.cpp @@ -715,6 +715,8 @@ idSoundSystemLocal::LoadSample */ idSoundSample* idSoundSystemLocal::LoadSample( const char* name ) { + fileSystem->RecordLevelLoadResource( LEVEL_LOAD_RESOURCE_SOUND, name, + va( "language=%s", cvarSystem->GetCVarString( "sys_lang" ) ), 0, 1 ); idStr canonical = name; canonical.ToLower(); canonical.BackSlashesToSlashes(); @@ -798,8 +800,7 @@ idSoundSystemLocal::FreeVoice cinData_t idSoundSystemLocal::ImageForTime( const int milliseconds, const bool waveform ) { cinData_t cd; - cd.imageWidth = 0; - cd.imageHeight = 0; + memset( &cd, 0, sizeof( cd ) ); cd.status = FMV_IDLE; return cd; } diff --git a/src/sys/URLPolicy.h b/src/sys/URLPolicy.h new file mode 100644 index 00000000..306e86ed --- /dev/null +++ b/src/sys/URLPolicy.h @@ -0,0 +1,292 @@ +/* +=========================================================================== + +openQ4 external URL validation helpers. +Copyright (C) 2026 DarkMatter Productions + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#ifndef __URL_POLICY_H__ +#define __URL_POLICY_H__ + +#include + +namespace idURLPolicy { + +// These helpers intentionally have no engine dependencies so every platform +// launcher and the native safety tests use the same fail-closed policy. + +// Includes the terminating NUL. Accepted URLs therefore contain at most 4095 +// bytes, matching the former macOS bound without allowing an unbounded scan. +const size_t MAX_URL_BYTES = 4096; + +inline char ASCIILower( const char value ) { + return ( value >= 'A' && value <= 'Z' ) ? static_cast( value + ( 'a' - 'A' ) ) : value; +} + +inline bool ASCIIStartsWith( const char *text, const char *prefix ) { + if ( text == NULL || prefix == NULL ) { + return false; + } + while ( *prefix != '\0' ) { + if ( *text == '\0' || ASCIILower( *text ) != ASCIILower( *prefix ) ) { + return false; + } + text++; + prefix++; + } + return true; +} + +inline bool ParsePort( const char *begin, const char *end ) { + if ( begin == NULL || end == NULL || begin == end || end < begin ) { + return false; + } + + unsigned int port = 0; + for ( const char *cursor = begin; cursor != end; cursor++ ) { + if ( *cursor < '0' || *cursor > '9' ) { + return false; + } + const unsigned int digit = static_cast( *cursor - '0' ); + if ( port > 6553 || ( port == 6553 && digit > 5 ) ) { + return false; + } + port = port * 10 + digit; + } + return true; +} + +inline bool IsASCIIAlphaNumeric( const char value ) { + return ( value >= 'a' && value <= 'z' ) || ( value >= 'A' && value <= 'Z' ) || + ( value >= '0' && value <= '9' ); +} + +inline bool IsASCIIHexDigit( const char value ) { + return ( value >= '0' && value <= '9' ) || ( value >= 'a' && value <= 'f' ) || + ( value >= 'A' && value <= 'F' ); +} + +inline bool IsIPv4Literal( const char *begin, const char *end ) { + int components = 0; + const char *cursor = begin; + while ( cursor != end ) { + if ( components == 4 ) { + return false; + } + unsigned int value = 0; + int digits = 0; + while ( cursor != end && *cursor != '.' ) { + if ( *cursor < '0' || *cursor > '9' || digits == 3 ) { + return false; + } + value = value * 10 + static_cast( *cursor - '0' ); + digits++; + cursor++; + } + if ( digits == 0 || value > 255 ) { + return false; + } + components++; + if ( cursor != end ) { + cursor++; + if ( cursor == end ) { + return false; + } + } + } + return components == 4; +} + +inline bool IsIPv6Literal( const char *begin, const char *end ) { + if ( begin == end ) { + return false; + } + + int groups = 0; + bool compressed = false; + const char *cursor = begin; + if ( *cursor == ':' ) { + if ( cursor + 1 == end || cursor[ 1 ] != ':' ) { + return false; + } + compressed = true; + cursor += 2; + if ( cursor == end ) { + return true; + } + } + + while ( cursor != end ) { + const char *groupBegin = cursor; + bool dotted = false; + while ( cursor != end && *cursor != ':' ) { + dotted = dotted || *cursor == '.'; + cursor++; + } + if ( dotted ) { + if ( cursor != end || !IsIPv4Literal( groupBegin, cursor ) ) { + return false; + } + groups += 2; + break; + } + const size_t groupLength = static_cast( cursor - groupBegin ); + if ( groupLength == 0 || groupLength > 4 ) { + return false; + } + for ( const char *digit = groupBegin; digit != cursor; digit++ ) { + if ( !IsASCIIHexDigit( *digit ) ) { + return false; + } + } + if ( ++groups > 8 ) { + return false; + } + if ( cursor == end ) { + break; + } + cursor++; + if ( cursor == end ) { + return false; + } + if ( *cursor == ':' ) { + if ( compressed ) { + return false; + } + compressed = true; + cursor++; + if ( cursor == end ) { + break; + } + } + } + return compressed ? groups < 8 : groups == 8; +} + +inline bool IsDNSOrIPv4Host( const char *begin, const char *end ) { + const size_t hostLength = static_cast( end - begin ); + if ( hostLength == 0 || hostLength > 253 ) { + return false; + } + + bool numericAndDotsOnly = true; + bool sawDot = false; + const char *labelBegin = begin; + for ( const char *cursor = begin; ; cursor++ ) { + if ( cursor == end || *cursor == '.' ) { + const size_t labelLength = static_cast( cursor - labelBegin ); + if ( labelLength == 0 || labelLength > 63 || + !IsASCIIAlphaNumeric( *labelBegin ) || !IsASCIIAlphaNumeric( cursor[ -1 ] ) ) { + return false; + } + if ( cursor == end ) { + break; + } + sawDot = true; + labelBegin = cursor + 1; + continue; + } + if ( !IsASCIIAlphaNumeric( *cursor ) && *cursor != '-' ) { + return false; + } + if ( *cursor < '0' || *cursor > '9' ) { + numericAndDotsOnly = false; + } + } + return !numericAndDotsOnly || ( sawDot && IsIPv4Literal( begin, end ) ); +} + +inline bool AuthorityHasHost( const char *begin, const char *end ) { + if ( begin == NULL || end == NULL || begin == end || end < begin ) { + return false; + } + + // User information and percent-encoded authority components create URL + // display/parser ambiguity. Neither is needed by an engine-owned web link. + for ( const char *cursor = begin; cursor != end; cursor++ ) { + if ( *cursor == '@' || *cursor == '%' ) { + return false; + } + } + + if ( *begin == '[' ) { + const char *closeBracket = begin + 1; + while ( closeBracket != end && *closeBracket != ']' ) { + closeBracket++; + } + if ( closeBracket == end || !IsIPv6Literal( begin + 1, closeBracket ) ) { + return false; + } + if ( closeBracket + 1 == end ) { + return true; + } + return closeBracket[ 1 ] == ':' && ParsePort( closeBracket + 2, end ); + } + + const char *portSeparator = NULL; + for ( const char *cursor = begin; cursor != end; cursor++ ) { + if ( *cursor == '[' || *cursor == ']' ) { + return false; + } + if ( *cursor == ':' ) { + if ( portSeparator != NULL ) { + // IPv6 literals must use brackets so the authority is unambiguous. + return false; + } + portSeparator = cursor; + } + } + + const char *hostEnd = portSeparator != NULL ? portSeparator : end; + if ( hostEnd == begin ) { + return false; + } + if ( !IsDNSOrIPv4Host( begin, hostEnd ) ) { + return false; + } + return portSeparator == NULL || ParsePort( portSeparator + 1, end ); +} + +inline bool IsAllowedHTTPURL( const char *url ) { + if ( url == NULL ) { + return false; + } + + size_t length = 0; + for ( ; length < MAX_URL_BYTES && url[ length ] != '\0'; length++ ) { + const unsigned char value = static_cast( url[ length ] ); + if ( value <= 32 || value == 127 || value == '"' || value == '<' || value == '>' || + value == '\\' || value == '^' || value == '`' || value == '{' || value == '|' || value == '}' ) { + return false; + } + } + if ( length == 0 || length == MAX_URL_BYTES ) { + return false; + } + + const char *authority = NULL; + if ( ASCIIStartsWith( url, "https://" ) ) { + authority = url + 8; + } else if ( ASCIIStartsWith( url, "http://" ) ) { + authority = url + 7; + } else { + return false; + } + + const char *authorityEnd = authority; + while ( *authorityEnd != '\0' && *authorityEnd != '/' && *authorityEnd != '?' && *authorityEnd != '#' ) { + authorityEnd++; + } + return AuthorityHasHost( authority, authorityEnd ); +} + +} // namespace idURLPolicy + +#endif /* !__URL_POLICY_H__ */ diff --git a/src/sys/linux/main.cpp b/src/sys/linux/main.cpp index 4a4484bc..0b04abd2 100644 --- a/src/sys/linux/main.cpp +++ b/src/sys/linux/main.cpp @@ -28,6 +28,7 @@ If you have questions concerning this license or the applicable additional terms #include "../../idlib/precompiled.h" #include "../posix/posix_public.h" #include "../sys_local.h" +#include "../URLPolicy.h" #include #include @@ -227,24 +228,6 @@ static bool Sys_QueueOrStartProcessArgs( char *const argv[], bool quit ) { return Sys_ExecProcessArgs( argv, true ); } -static bool Sys_IsSafeURL( const char *url ) { - if ( url == NULL || url[0] == '\0' || Sys_StringHasControlCharacters( url ) ) { - return false; - } - if ( !isalpha( static_cast( url[0] ) ) ) { - return false; - } - for ( const char *scan = url + 1; *scan != '\0'; ++scan ) { - if ( *scan == ':' ) { - return true; - } - if ( !( isalnum( static_cast( *scan ) ) || *scan == '+' || *scan == '-' || *scan == '.' ) ) { - return false; - } - } - return false; -} - static bool Sys_FindExecutableOnPath( const char *name, idStr &resolvedPath ) { resolvedPath.Clear(); if ( name == NULL || name[0] == '\0' ) { @@ -900,17 +883,18 @@ void idSysLocal::OpenURL( const char *url, bool quit ) { static bool quit_spamguard = false; - if ( quit_spamguard ) { - common->DPrintf( "Sys_OpenURL: already in a doexit sequence, ignoring %s\n", url ? url : "" ); + if ( !idURLPolicy::IsAllowedHTTPURL( url ) ) { + common->Printf( "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host\n" ); return; } - common->Printf( "Open URL: %s\n", url ); - if ( !Sys_IsSafeURL( url ) ) { - common->Printf( "OpenURL '%s' rejected: expected a URL with a safe scheme\n", url ? url : "" ); + if ( quit_spamguard ) { + common->DPrintf( "Sys_OpenURL: already in a doexit sequence, ignoring request\n" ); return; } + common->Printf( "Open URL: %s\n", url ); + // opening an URL on *nix can mean a lot of things .. // prefer a user-provided script, then fall back to freedesktop helpers. diff --git a/src/sys/osx/macosx_misc.mm b/src/sys/osx/macosx_misc.mm index 1b8d425b..1a9fda54 100644 --- a/src/sys/osx/macosx_misc.mm +++ b/src/sys/osx/macosx_misc.mm @@ -40,6 +40,7 @@ #import #import #include "../sys_local.h" +#include "../URLPolicy.h" #if defined(USE_SDL3) #include @@ -49,7 +50,6 @@ static const int MAX_OSX_PROCESS_ARGS = 32; static const int MAX_OSX_PROCESS_COMMAND = 4096; -static const int MAX_OSX_URL_LENGTH = 4096; #if defined(USE_SDL3) static int OSX_WindowBorderExtent( CGFloat extent ) { @@ -206,61 +206,6 @@ static bool OSX_ParseProcessCommandLine( const char *command, char *buffer, size return argc > 0 && argv[0][0] != '\0'; } -static bool OSX_URLHasSafeSchemeSyntax( const char *url ) { - if ( url == NULL || url[0] == '\0' || OSX_StringHasControlCharacters( url ) ) { - return false; - } - if ( strlen( url ) >= MAX_OSX_URL_LENGTH ) { - return false; - } - if ( !isalpha( static_cast( url[0] ) ) ) { - return false; - } - for ( const char *scan = url + 1; *scan != '\0'; ++scan ) { - if ( *scan == ':' ) { - return true; - } - if ( !( isalnum( static_cast( *scan ) ) || *scan == '+' || *scan == '-' || *scan == '.' ) ) { - return false; - } - } - return false; -} - -static bool OSX_ResolvedPathIsUnderDirectory( const char *path, const char *directory ) { - if ( path == NULL || path[0] == '\0' || directory == NULL || directory[0] == '\0' ) { - return false; - } - - char resolvedPath[PATH_MAX]; - char resolvedDirectory[PATH_MAX]; - if ( realpath( path, resolvedPath ) == NULL || realpath( directory, resolvedDirectory ) == NULL ) { - return false; - } - - const size_t directoryLength = strlen( resolvedDirectory ); - if ( directoryLength == 0 || idStr::Cmpn( resolvedPath, resolvedDirectory, static_cast( directoryLength ) ) != 0 ) { - return false; - } - return resolvedPath[directoryLength] == '\0' || resolvedPath[directoryLength] == '/'; -} - -static bool OSX_FileURLIsLocalRuntimeFile( NSURL *url ) { - if ( url == nil || ![url isFileURL] ) { - return false; - } - - NSString *pathString = [url path]; - const char *path = pathString != nil ? [pathString fileSystemRepresentation] : NULL; - if ( path == NULL || OSX_StringHasControlCharacters( path ) || !OSX_IsAbsolutePath( path ) || !OSX_IsRegularFile( path ) ) { - return false; - } - - const char *savePath = cvarSystem != NULL ? cvarSystem->GetCVarString( "fs_savepath" ) : NULL; - const char *basePath = cvarSystem != NULL ? cvarSystem->GetCVarString( "fs_basepath" ) : NULL; - return OSX_ResolvedPathIsUnderDirectory( path, savePath ) || OSX_ResolvedPathIsUnderDirectory( path, basePath ); -} - static bool OSX_IsAllowedURL( NSURL *url ) { if ( url == nil ) { return false; @@ -274,9 +219,6 @@ static bool OSX_IsAllowedURL( NSURL *url ) { NSString *host = [url host]; return host != nil && [host length] > 0; } - if ( [scheme caseInsensitiveCompare:@"file"] == NSOrderedSame ) { - return OSX_FileURLIsLocalRuntimeFile( url ); - } return false; } @@ -378,15 +320,16 @@ static bool OSX_StartProcessArgs( char *const argv[], bool dofork ) { void idSysLocal::OpenURL( const char *url, bool doexit ) { static bool quit_spamguard = false; - if ( quit_spamguard ) { - common->DPrintf( "Sys_OpenURL: already in a doexit sequence, ignoring request\n" ); + if ( !idURLPolicy::IsAllowedHTTPURL( url ) ) { + common->Printf( "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host\n" ); return; } - if ( !OSX_URLHasSafeSchemeSyntax( url ) ) { - common->Printf( "OpenURL rejected: expected a bounded URL with a safe scheme\n" ); + if ( quit_spamguard ) { + common->DPrintf( "Sys_OpenURL: already in a doexit sequence, ignoring request\n" ); return; } + NSString *urlString = [ NSString stringWithUTF8String: url ]; if ( urlString == nil ) { common->Printf( "OpenURL rejected: URL is not valid UTF-8\n" ); @@ -399,7 +342,7 @@ static bool OSX_StartProcessArgs( char *const argv[], bool dofork ) { return; } if ( !OSX_IsAllowedURL( nsURL ) ) { - common->Printf( "OpenURL rejected: scheme is not allowed\n" ); + common->Printf( "OpenURL rejected: Foundation did not preserve the required HTTP(S) host\n" ); return; } diff --git a/src/sys/posix/posix_main.cpp b/src/sys/posix/posix_main.cpp index f8fa5adc..7abd4046 100644 --- a/src/sys/posix/posix_main.cpp +++ b/src/sys/posix/posix_main.cpp @@ -1214,20 +1214,26 @@ char *Posix_ConsoleInput( void ) { const char *inputBuffer = tty_InputState( inputLength, inputCursor ); idStr::Copynz( input_ret, inputBuffer, sizeof( input_ret ) ); } - assert( hidden ); - tty_Show(); - write( STDOUT_FILENO, &key, 1 ); - input_field.Clear(); - if ( history_count < COMMAND_HISTORY ) { - history[ history_count ] = input_ret; - history_count++; - } else { - history[ history_start ] = input_ret; - history_start++; - history_start %= COMMAND_HISTORY; + { + const bool privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( input_ret ); + assert( hidden ); + tty_Show(); + write( STDOUT_FILENO, &key, 1 ); + input_field.Clear(); + if ( !privateCommand ) { + if ( history_count < COMMAND_HISTORY ) { + history[ history_count ] = input_ret; + history_count++; + } else { + history[ history_start ] = input_ret; + history_start++; + history_start %= COMMAND_HISTORY; + } + } + history_current = 0; + return input_ret; } - history_current = 0; - return input_ret; case '\t': input_field.AutoComplete(); break; @@ -1488,6 +1494,10 @@ void Sys_GenerateEvents( void ) { return; } idStr::Copynz( b, s, len ); + if ( cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( s ) ) { + memset( s, 0, len ); + } Posix_QueEvent( SE_CONSOLE, 0, 0, len, b ); } } diff --git a/src/sys/posix/posix_syscon.cpp b/src/sys/posix/posix_syscon.cpp index 3f5275cf..918a439e 100644 --- a/src/sys/posix/posix_syscon.cpp +++ b/src/sys/posix/posix_syscon.cpp @@ -825,12 +825,20 @@ static void Posix_ConsoleSubmitInput( void ) { return; } - Sys_Printf( "]%s\n", command ); + const bool privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( command ); + if ( privateCommand ) { + Sys_Printf( "]\n" ); + } else { + Sys_Printf( "]%s\n", command ); + } Posix_ConsoleQueueCommand( command ); - s_consoleWindow.history[ s_consoleWindow.nextHistoryLine % POSIX_CONSOLE_HISTORY ] = s_consoleWindow.inputField; - s_consoleWindow.nextHistoryLine++; - s_consoleWindow.historyLine = s_consoleWindow.nextHistoryLine; + if ( !privateCommand ) { + s_consoleWindow.history[ s_consoleWindow.nextHistoryLine % POSIX_CONSOLE_HISTORY ] = s_consoleWindow.inputField; + s_consoleWindow.nextHistoryLine++; + s_consoleWindow.historyLine = s_consoleWindow.nextHistoryLine; + } s_consoleWindow.inputField.Clear(); s_consoleWindow.scrollLines = 0; } diff --git a/src/sys/win32/win_main.cpp b/src/sys/win32/win_main.cpp index dcd2e270..4baf9526 100644 --- a/src/sys/win32/win_main.cpp +++ b/src/sys/win32/win_main.cpp @@ -45,6 +45,7 @@ If you have questions concerning this license or the applicable additional terms #endif #include "../sys_local.h" +#include "../URLPolicy.h" #include "win_local.h" #include "win_crash.h" #include "rc/CreateResourceIDs.h" @@ -1600,10 +1601,15 @@ void Sys_GenerateEvents(void) { if (s) { char* b; int len; + const bool privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( s ); len = idLib::SizeToInt( strlen( s ) + 1, "Sys_GenerateEvents console command" ); b = (char*)Mem_Alloc(len); strcpy(b, s); + if ( privateCommand ) { + memset( s, 0, len ); + } Sys_QueEvent(0, SE_CONSOLE, 0, 0, len, b); } @@ -2198,15 +2204,21 @@ void idSysLocal::OpenURL(const char* url, bool doexit) { static bool doexit_spamguard = false; HWND wnd; + if ( !idURLPolicy::IsAllowedHTTPURL( url ) ) { + common->Printf( "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host\n" ); + return; + } + if (doexit_spamguard) { - common->DPrintf("OpenURL: already in an exit sequence, ignoring %s\n", url); + common->DPrintf("OpenURL: already in an exit sequence, ignoring request\n"); return; } common->Printf("Open URL: %s\n", url); - if (!ShellExecute(NULL, "open", url, NULL, NULL, SW_RESTORE)) { - common->Error("Could not open url: '%s' ", url); + const HINSTANCE result = ShellExecute(NULL, "open", url, NULL, NULL, SW_RESTORE); + if (reinterpret_cast(result) <= 32) { + common->Printf("OpenURL failed after validation\n"); return; } diff --git a/src/sys/win32/win_syscon.cpp b/src/sys/win32/win_syscon.cpp index 93558832..2d7f04e6 100644 --- a/src/sys/win32/win_syscon.cpp +++ b/src/sys/win32/win_syscon.cpp @@ -827,6 +827,8 @@ LRESULT CALLBACK InputLineWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lP // enter the line if (key == K_ENTER || key == K_KP_ENTER) { const char *inputText = s_wcd.consoleField.GetBuffer(); + const bool privateCommand = cvarSystem != NULL && cvarSystem->IsInitialized() && + cvarSystem->CommandContainsPrivateCVar( inputText ); size_t used = strlen( s_wcd.consoleText ); if ( used < sizeof( s_wcd.consoleText ) - 1 ) { const size_t available = sizeof( s_wcd.consoleText ) - used - 1; @@ -841,12 +843,18 @@ LRESULT CALLBACK InputLineWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lP } SetWindowText(s_wcd.hwndInputLine, ""); - Sys_Printf("]%s\n", s_wcd.consoleField.GetBuffer()); + if ( privateCommand ) { + Sys_Printf("]\n"); + } else { + Sys_Printf("]%s\n", s_wcd.consoleField.GetBuffer()); + } // copy line to history buffer - s_wcd.historyEditLines[s_wcd.nextHistoryLine % COMMAND_HISTORY] = s_wcd.consoleField; - s_wcd.nextHistoryLine++; - s_wcd.historyLine = s_wcd.nextHistoryLine; + if ( !privateCommand ) { + s_wcd.historyEditLines[s_wcd.nextHistoryLine % COMMAND_HISTORY] = s_wcd.consoleField; + s_wcd.nextHistoryLine++; + s_wcd.historyLine = s_wcd.nextHistoryLine; + } s_wcd.consoleField.Clear(); @@ -1147,7 +1155,7 @@ char* Sys_ConsoleInput(void) { } idStr::Copynz( s_wcd.returnedText, s_wcd.consoleText, sizeof( s_wcd.returnedText ) ); - s_wcd.consoleText[0] = 0; + memset( s_wcd.consoleText, 0, sizeof( s_wcd.consoleText ) ); return s_wcd.returnedText; } diff --git a/src/ui/DeviceContext.cpp b/src/ui/DeviceContext.cpp index 9277b8b6..f50fb03f 100644 --- a/src/ui/DeviceContext.cpp +++ b/src/ui/DeviceContext.cpp @@ -3204,7 +3204,20 @@ bool UI_FontParity_RunSelfTest( void ) { idDeviceContext radioFontDc; radioFontDc.Init(); const int radioFont = radioFontDc.FindFont( "fonts/marine" ); - ok &= openQ4_CheckBool( "hud radio marine font registered", radioFont >= 0, true ); + // With the TrueType path disabled and no retail .fontdat atlases installed + // (an assetless validation startup), no glyph source exists at all, so + // registration cannot succeed. That is a missing environment rather than + // a font regression - say so instead of failing, like the TrueType skip + // above. With the TrueType path enabled, or any .fontdat present, + // registration is still required to succeed. + const bool marineSourceMissing = retailAtlasActive && radioFont < 0 && + fileSystem->ReadFile( "fonts/english/marine_12.fontdat", NULL, NULL ) <= 0; + if ( marineSourceMissing ) { + common->Printf( "uiFontParitySelfTest: skipping marine font cases, " + "no marine glyph source is installed\n" ); + } else { + ok &= openQ4_CheckBool( "hud radio marine font registered", radioFont >= 0, true ); + } // The measurements below are keyed to the retail atlas's exact advances. if ( radioFont >= 0 && retailAtlasActive ) { radioFontDc.SetFont( radioFont ); @@ -3287,8 +3300,9 @@ bool UI_FontParity_RunSelfTest( void ) { idStr fontAtlasLang = cvarSystem->GetCVarString( "sys_lang" ); openQ4_NormalizeFontLanguage( fontAtlasLang ); // The TrueType path never touches the retail .fontdat atlas, so this only - // means anything while the bitmap fonts are in charge. - if ( retailAtlasActive ) { + // means anything while the bitmap fonts are in charge - and only when a + // retail atlas exists to measure against at all. + if ( retailAtlasActive && !marineSourceMissing ) { const idMaterial *fontAtlasMaterial = declManager->FindMaterial( va( "fonts/%s/marine_12.fontdat", fontAtlasLang.c_str() ), false ); if ( fontAtlasMaterial == NULL && idStr::Icmp( fontAtlasLang.c_str(), "english" ) != 0 ) { // A language with no atlases of its own falls back to the English diff --git a/tools/assets/pack_pbr_orm.py b/tools/assets/pack_pbr_orm.py new file mode 100644 index 00000000..ed4cbd43 --- /dev/null +++ b/tools/assets/pack_pbr_orm.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Pack separate ambient-occlusion, roughness, and metallic maps into glTF ORM. + +openQ4 PBR declarations consume packed ORM data as R = ambient occlusion, +G = roughness, B = metallic. Input data is read from the explicitly selected +stored channel (R by default), never from an RGB-to-luminance conversion. The +command-line image backend is optional Pillow and writes PNG or TGA according +to the output extension. The packing core accepts an injected Pillow-compatible +backend so repository validation does not need Pillow installed. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +DATA_CHANNELS = ("R", "G", "B", "A") + + +def normalize_data_channel(channel: str) -> str: + normalized = channel.upper() + if normalized not in DATA_CHANNELS: + raise ValueError(f"input channel must be one of {', '.join(DATA_CHANNELS)}") + return normalized + + +def select_data_channel(image: object, channel: str, label: str) -> object: + """Return one stored 8-bit data band without deriving RGB luminance. + + Pillow exposes an 8-bit grayscale image as an ``L`` band. Treat that band + as R for the default single-channel-map case. All other requested channels + must exist verbatim; in particular, palette and CMYK inputs fail closed + instead of undergoing an implicit colour-space conversion. + """ + + normalized = normalize_data_channel(channel) + bands = tuple(str(band).upper() for band in image.getbands()) + selected = "L" if normalized == "R" and "R" not in bands and "L" in bands else normalized + if selected not in bands: + available = ", ".join(bands) if bands else "none" + raise ValueError( + f"{label} map does not contain stored {normalized} data " + f"(available bands: {available})" + ) + # Pillow's getchannel() returns an independent single-band image. copy() + # also keeps injected validation backends independent of the source image's + # context-manager lifetime. + return image.getchannel(selected).copy() + + +def load_data_channel(path: Path, label: str, image_module: object, channel: str = "R") -> object: + if not path.is_file(): + raise FileNotFoundError(f"{label} map is not a regular file: {path}") + if path.is_symlink(): + raise RuntimeError(f"{label} map must not be a symlink: {path}") + + with image_module.open(path) as image: + return select_data_channel(image, channel, label) + + +def pillow_image_module() -> object: + try: + from PIL import Image + except ModuleNotFoundError as exc: + raise RuntimeError( + "Pillow is required for command-line PBR image I/O. " + "Install it with `python -m pip install Pillow`." + ) from exc + return Image + + +def validate_output(output: Path, force: bool) -> None: + if output.suffix.lower() not in {".png", ".tga"}: + raise ValueError("--output must end in .png or .tga") + if output.is_symlink(): + raise RuntimeError(f"ORM output must not be a symlink: {output}") + if output.exists() and not output.is_file(): + raise RuntimeError(f"ORM output is not a regular file: {output}") + if output.exists() and not force: + raise FileExistsError(f"ORM output already exists: {output} (pass --force to replace it)") + + +def pack_orm( + ao_path: Path | None, + roughness_path: Path, + metallic_path: Path, + output: Path, + force: bool, + *, + input_channel: str = "R", + image_module: object | None = None, +) -> None: + channel = normalize_data_channel(input_channel) + Image = image_module if image_module is not None else pillow_image_module() + + validate_output(output, force) + roughness = load_data_channel(roughness_path, "roughness", Image, channel) + metallic = load_data_channel(metallic_path, "metallic", Image, channel) + if roughness.size != metallic.size: + raise ValueError( + f"roughness and metallic sizes differ: {roughness.size[0]}x{roughness.size[1]} vs " + f"{metallic.size[0]}x{metallic.size[1]}" + ) + + if ao_path is None: + ao = Image.new("L", roughness.size, color=255) + else: + ao = load_data_channel(ao_path, "ambient-occlusion", Image, channel) + if ao.size != roughness.size: + raise ValueError( + f"ambient-occlusion and roughness sizes differ: {ao.size[0]}x{ao.size[1]} vs " + f"{roughness.size[0]}x{roughness.size[1]}" + ) + + packed = Image.merge("RGB", (ao, roughness, metallic)) + output.parent.mkdir(parents=True, exist_ok=True) + packed.save(output, format="PNG" if output.suffix.lower() == ".png" else "TGA") + print( + f"wrote {output} (R=AO, G=roughness, B=metallic; " + f"input={channel}; {packed.width}x{packed.height})" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--roughness", required=True, type=Path, help="linear roughness map (output green)") + parser.add_argument("--metallic", required=True, type=Path, help="linear metallic map (output blue)") + parser.add_argument("--ao", type=Path, help="optional linear ambient-occlusion map (output red; white when omitted)") + parser.add_argument( + "--input-channel", + choices=("r", "g", "b", "a"), + default="r", + help="stored input band to read from every map (default: r; grayscale L is accepted as r)", + ) + parser.add_argument("--output", required=True, type=Path, help="packed .png or .tga output path") + parser.add_argument("--force", action="store_true", help="allow replacement of an existing output file") + args = parser.parse_args() + pack_orm( + args.ao, + args.roughness, + args.metallic, + args.output, + args.force, + input_channel=args.input_channel, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build/generate_release_docs.py b/tools/build/generate_release_docs.py index 2d892a84..298ef0b8 100644 --- a/tools/build/generate_release_docs.py +++ b/tools/build/generate_release_docs.py @@ -10,7 +10,7 @@ import shutil import sys from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Any from urllib.parse import urlsplit @@ -26,6 +26,7 @@ "https", "mailto", } +REPOSITORY_BLOB_BASE = "https://github.com/themuffinator/openQ4/blob/main/" SITE_CSS = r""" @@ -739,7 +740,32 @@ def default_nav_title(relative: Path) -> str: return " ".join(part.capitalize() if part.islower() else part for part in stem.split()) -def rewrite_markdown_links(text: str) -> str: +def resolve_site_relative(source_relative: Path, target: str) -> str | None: + """Resolve a relative link target against a document's site location. + + Returns the normalized root-relative posix path, or None when the target + escapes the documentation root. + """ + parts: list[str] = [] + combined = PurePosixPath(source_relative.parent.as_posix()) / target + for part in combined.parts: + if part == ".": + continue + if part == "..": + if not parts: + return None + parts.pop() + else: + parts.append(part) + return "/".join(parts) + + +def rewrite_markdown_links( + text: str, + *, + source_relative: Path | None = None, + rendered_sources: set[str] | None = None, +) -> str: pattern = re.compile(r"(!?\[[^\]]+\]\()([^)]+)(\))") def replace(match: re.Match[str]) -> str: @@ -762,6 +788,14 @@ def replace(match: re.Match[str]) -> str: hash_suffix = "#" + fragment if stripped_target.lower().endswith(".md"): + if rendered_sources is not None and source_relative is not None: + resolved = resolve_site_relative(source_relative, stripped_target) + if resolved is None or resolved.lower() not in rendered_sources: + # The target exists in the repository but is not part of the + # generated site; keep the link alive by pointing at the + # repository copy instead of a dead local page. + repo_target = resolved if resolved is not None else stripped_target + return prefix + REPOSITORY_BLOB_BASE + repo_target + hash_suffix + suffix stripped_target = stripped_target[:-3] + ".html" return prefix + stripped_target + hash_suffix + suffix @@ -846,7 +880,12 @@ def convert_task_lists(text: str) -> str: return "\n".join(result) -def prepare_markdown(text: str) -> str: +def prepare_markdown( + text: str, + *, + source_relative: Path | None = None, + rendered_sources: set[str] | None = None, +) -> str: normalized = text.replace("\r\n", "\n") normalized = re.sub( r"(?m)^]*markdown=)", @@ -858,7 +897,11 @@ def prepare_markdown(text: str) -> str: '
int: for path in RENDERER_GL_EXCLUDED_SOURCES: remove_source(source_set, ordered_sources, path) if args.emit == "renderer_vk": + try: + for rel_path in RENDERER_VK_REQUIRED_SOURCES: + add_required_source(source_set, ordered_sources, source_root, rel_path) + except SourceListError as exc: + print(exc, file=sys.stderr) + return 1 for path in RENDERER_VK_EXCLUDED_SOURCES: remove_source(source_set, ordered_sources, path) if not ordered_sources: diff --git a/tools/build/spirv_to_header.py b/tools/build/spirv_to_header.py index a2eac665..137757f9 100644 --- a/tools/build/spirv_to_header.py +++ b/tools/build/spirv_to_header.py @@ -65,6 +65,11 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--glslang", help="Explicit glslangValidator path.") parser.add_argument("--header-out", required=True) + parser.add_argument( + "--guard", + default="__VK_GUI_SHADERS_SPV_H__", + help="C preprocessor include guard (defaults to the legacy GUI header guard).", + ) parser.add_argument( "shaders", nargs="+", @@ -72,6 +77,9 @@ def main(argv: list[str]) -> int: ) args = parser.parse_args(argv) + if not args.guard.isidentifier(): + parser.error("--guard must be a valid C preprocessor identifier") + glslang = find_glslang(args.glslang) if glslang is None: print( @@ -84,8 +92,8 @@ def main(argv: list[str]) -> int: "// GENERATED by tools/build/spirv_to_header.py — do not edit by hand.", "// Regenerate after editing src/renderer/Vulkan/shaders/*;", "// tools/tests/vk_shader_header_pin.py guards drift.", - "#ifndef __VK_GUI_SHADERS_SPV_H__", - "#define __VK_GUI_SHADERS_SPV_H__", + f"#ifndef {args.guard}", + f"#define {args.guard}", "", ] with tempfile.TemporaryDirectory() as tmp: diff --git a/tools/build/stage_direct_run_game_module.py b/tools/build/stage_direct_run_game_module.py index d6a38a78..412a5efb 100644 --- a/tools/build/stage_direct_run_game_module.py +++ b/tools/build/stage_direct_run_game_module.py @@ -12,8 +12,14 @@ def atomic_copy_if_changed(source: Path, destination: Path) -> bool: - if destination.is_file() and filecmp.cmp(source, destination, shallow=False): - return False + try: + if destination.is_file() and filecmp.cmp(source, destination, shallow=False): + return False + except FileNotFoundError: + # A Meson regeneration can remove an old direct-run output between the + # existence probe and filecmp opening it. Treat that as a changed file; + # the atomic replacement below is already the recovery path. + pass destination.parent.mkdir(parents=True, exist_ok=True) temporary_path: Path | None = None diff --git a/tools/build/stage_fast_install.py b/tools/build/stage_fast_install.py index 421a8b5a..acde2679 100644 --- a/tools/build/stage_fast_install.py +++ b/tools/build/stage_fast_install.py @@ -4,10 +4,13 @@ from __future__ import annotations import argparse +import stat import sys from pathlib import Path +from typing import NamedTuple from openq4_pak import copy_file_if_changed, is_relative_to +from windows_runtime import cleanup_windows_stage_target, is_windows_host ROOT_RUNTIME_PATTERNS = ( @@ -15,16 +18,46 @@ "openQ4-client_*.pdb", "openQ4-ded_*.exe", "openQ4-ded_*.pdb", + "renderer-gl_*.dll", + "renderer-gl_*.pdb", + "renderer-vk_*.dll", + "renderer-vk_*.pdb", "OpenAL32.dll", ) +POSIX_ENGINE_RUNTIME_PATTERNS = ( + "openQ4-client_*", + "openQ4-ded_*", +) +LINUX_ROOT_RUNTIME_PATTERNS = ( + *POSIX_ENGINE_RUNTIME_PATTERNS, + "renderer-gl_*.so", + "renderer-vk_*.so", +) +MACOS_ROOT_RUNTIME_PATTERNS = ( + *POSIX_ENGINE_RUNTIME_PATTERNS, + "renderer-vk_*.dylib", +) +COMMON_GAME_RUNTIME_PATTERNS = ( + "mod.json", + "pak0.pk4", + "pak1.pk4", +) GAME_RUNTIME_PATTERNS = ( "game-sp_*.dll", "game-sp_*.pdb", "game-mp_*.dll", "game-mp_*.pdb", - "mod.json", - "pak0.pk4", - "pak1.pk4", + *COMMON_GAME_RUNTIME_PATTERNS, +) +LINUX_GAME_RUNTIME_PATTERNS = ( + "game-sp_*.so", + "game-mp_*.so", + *COMMON_GAME_RUNTIME_PATTERNS, +) +MACOS_GAME_RUNTIME_PATTERNS = ( + "game-sp_*.dylib", + "game-mp_*.dylib", + *COMMON_GAME_RUNTIME_PATTERNS, ) NON_RUNTIME_PATTERNS = ( "*.exp", @@ -33,6 +66,59 @@ "*.map", "*.zip", ) +WINDOWS_FOREIGN_RUNTIME_PATTERNS = ( + "*.so", + "*.dylib", +) +POSIX_ENGINE_FOREIGN_RUNTIME_PATTERNS = ( + "openQ4-client_*.exe", + "openQ4-client_*.pdb", + "openQ4-client_*.dll", + "openQ4-client_*.so", + "openQ4-client_*.dylib", + "openQ4-ded_*.exe", + "openQ4-ded_*.pdb", + "openQ4-ded_*.dll", + "openQ4-ded_*.so", + "openQ4-ded_*.dylib", +) + + +class RuntimeStagePolicy(NamedTuple): + root_copy_patterns: tuple[str, ...] + game_copy_patterns: tuple[str, ...] + root_copy_exclude_patterns: tuple[str, ...] + root_remove_patterns: tuple[str, ...] + game_remove_patterns: tuple[str, ...] + + +def runtime_stage_policy(platform_name: str) -> RuntimeStagePolicy: + """Return the exact copy/cleanup policy for a supported host platform.""" + if platform_name == "win32": + return RuntimeStagePolicy( + ROOT_RUNTIME_PATTERNS, + GAME_RUNTIME_PATTERNS, + (), + WINDOWS_FOREIGN_RUNTIME_PATTERNS, + WINDOWS_FOREIGN_RUNTIME_PATTERNS, + ) + if platform_name == "linux": + return RuntimeStagePolicy( + LINUX_ROOT_RUNTIME_PATTERNS, + LINUX_GAME_RUNTIME_PATTERNS, + POSIX_ENGINE_FOREIGN_RUNTIME_PATTERNS, + (), + (), + ) + if platform_name == "darwin": + return RuntimeStagePolicy( + MACOS_ROOT_RUNTIME_PATTERNS, + MACOS_GAME_RUNTIME_PATTERNS, + POSIX_ENGINE_FOREIGN_RUNTIME_PATTERNS, + (), + (), + ) + raise RuntimeError(f"unsupported fast-stage host platform: {platform_name}") def parse_args(argv: list[str]) -> argparse.Namespace: @@ -46,6 +132,14 @@ def parse_args(argv: list[str]) -> argparse.Namespace: default=str(Path(__file__).resolve().parents[2]), help="openQ4 source root used to validate the fast staging target.", ) + parser.add_argument( + "--temporary-runtime", + action="store_true", + help=( + "Allow one fresh alternate runtime below /.tmp/stock-runtime/. " + "This is for isolated compatibility captures and never replaces canonical .install staging." + ), + ) return parser.parse_args(argv[1:]) @@ -53,22 +147,55 @@ def copy_if_changed(source: Path, destination: Path) -> bool: return copy_file_if_changed(source, destination) -def validate_stage_roots(source_root: Path, build_dir: Path, install_dir: Path) -> None: +def is_link_or_junction(path: Path) -> bool: + is_junction = getattr(path, "is_junction", None) + if path.is_symlink() or bool(is_junction and is_junction()): + return True + try: + attributes = getattr(path.lstat(), "st_file_attributes", 0) + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + except OSError: + return False + + +def validate_stage_roots( + source_root: Path, + build_dir: Path, + install_dir: Path, + temporary_runtime: bool = False, +) -> None: for label, path in ( ("source root", source_root), ("build directory", build_dir), ("install directory", install_dir), ): - if path.is_symlink(): - raise RuntimeError(f"fast staging {label} must not be a symlink: {path}") + if is_link_or_junction(path): + raise RuntimeError(f"fast staging {label} must not be a link or junction: {path}") source_root = source_root.resolve() build_dir = build_dir.resolve() install_dir = install_dir.resolve() expected_install_dir = source_root / ".install" + temporary_parent = source_root / ".tmp" / "stock-runtime" if install_dir != expected_install_dir: - raise RuntimeError(f"fast staging install directory must be {expected_install_dir}: {install_dir}") + if not temporary_runtime: + raise RuntimeError(f"fast staging install directory must be {expected_install_dir}: {install_dir}") + if install_dir == temporary_parent or not is_relative_to(install_dir, temporary_parent): + raise RuntimeError( + f"temporary runtime directory must stay below {temporary_parent}: {install_dir}" + ) + if install_dir.exists(): + raise RuntimeError(f"temporary runtime directory must be new: {install_dir}") + current = source_root + for part in install_dir.relative_to(source_root).parts[:-1]: + current /= part + if current.exists() and is_link_or_junction(current): + raise RuntimeError( + f"temporary runtime parent must not be a link or junction: {current}" + ) + elif temporary_runtime: + raise RuntimeError("--temporary-runtime is only valid for an alternate runtime directory") if not is_relative_to(build_dir, source_root): raise RuntimeError(f"fast staging build directory must stay under {source_root}: {build_dir}") if build_dir == install_dir or is_relative_to(install_dir, build_dir) or is_relative_to(build_dir, install_dir): @@ -87,7 +214,24 @@ def remove_matches(root: Path, patterns: tuple[str, ...]) -> list[Path]: return removed -def copy_matches(source_root: Path, destination_root: Path, patterns: tuple[str, ...]) -> list[Path]: +def remove_non_runtime_files( + install_dir: Path, + install_game_dir: Path, + policy: RuntimeStagePolicy, +) -> list[Path]: + removed = remove_matches(install_dir, NON_RUNTIME_PATTERNS) + removed += remove_matches(install_game_dir, NON_RUNTIME_PATTERNS) + removed += remove_matches(install_dir, policy.root_remove_patterns) + removed += remove_matches(install_game_dir, policy.game_remove_patterns) + return removed + + +def copy_matches( + source_root: Path, + destination_root: Path, + patterns: tuple[str, ...], + exclude_patterns: tuple[str, ...] = (), +) -> list[Path]: copied: list[Path] = [] if not source_root.is_dir(): return copied @@ -95,6 +239,8 @@ def copy_matches(source_root: Path, destination_root: Path, patterns: tuple[str, for source in sorted(source_root.glob(pattern)): if not source.is_file(): continue + if any(source.match(exclude) for exclude in exclude_patterns): + continue destination = destination_root / source.name if copy_if_changed(source, destination): copied.append(destination) @@ -108,7 +254,9 @@ def main(argv: list[str]) -> int: install_dir = Path(args.install_dir) try: - validate_stage_roots(source_root, build_dir, install_dir) + validate_stage_roots( + source_root, build_dir, install_dir, args.temporary_runtime + ) source_root = source_root.resolve() build_dir = build_dir.resolve() install_dir = install_dir.resolve() @@ -117,17 +265,31 @@ def main(argv: list[str]) -> int: install_dir.mkdir(parents=True, exist_ok=True) install_game_dir.mkdir(parents=True, exist_ok=True) - removed = remove_matches(install_dir, NON_RUNTIME_PATTERNS) - removed += remove_matches(install_game_dir, NON_RUNTIME_PATTERNS + ("*.so", "*.dylib")) - copied = copy_matches(build_dir, install_dir, ROOT_RUNTIME_PATTERNS) - copied += copy_matches(build_game_dir, install_game_dir, GAME_RUNTIME_PATTERNS) + stage_cleanup = ( + cleanup_windows_stage_target(install_dir) + if is_windows_host() + else {"removed_stale_files": [], "removed_empty_directories": []} + ) + policy = runtime_stage_policy(sys.platform) + removed = remove_non_runtime_files(install_dir, install_game_dir, policy) + copied = copy_matches( + build_dir, + install_dir, + policy.root_copy_patterns, + policy.root_copy_exclude_patterns, + ) + copied += copy_matches( + build_game_dir, install_game_dir, policy.game_copy_patterns + ) except RuntimeError as exc: print(f"error: {exc}", file=sys.stderr) return 1 print( f"fast-staged .install: copied={len(copied)} " - f"removed_non_runtime={len(removed)}" + f"removed_non_runtime={len(removed)} " + f"removed_stale={len(stage_cleanup['removed_stale_files'])} " + f"removed_empty_dirs={len(stage_cleanup['removed_empty_directories'])}" ) for path in copied[:20]: print(f" copied {path}") diff --git a/tools/build/windows_runtime.py b/tools/build/windows_runtime.py index 0f6f9724..094c22c8 100644 --- a/tools/build/windows_runtime.py +++ b/tools/build/windows_runtime.py @@ -5,6 +5,7 @@ import os import shutil +import stat import struct from pathlib import Path @@ -21,6 +22,30 @@ WINDOWS_ROOT_RUNTIME_PATTERNS = ( "OpenAL32.dll", ) +# Meson installs overlay their destination and deliberately do not remove files +# from an earlier build. Keep the Windows cleanup policy as an explicit, +# narrow manifest: these names can only be foreign POSIX engine binaries or +# obsolete renderer-validation backups in a Windows runtime tree. +WINDOWS_STALE_STAGE_FILE_MANIFEST = ( + "openQ4-client_x86", + "openQ4-client_x64", + "openQ4-client_arm64", + "openQ4-ded_x86", + "openQ4-ded_x64", + "openQ4-ded_arm64", + "renderer-gl_x86.dll.mainbak", + "renderer-gl_x64.dll.mainbak", + "renderer-gl_arm64.dll.mainbak", + "renderer-vk_x86.dll.mainbak", + "renderer-vk_x64.dll.mainbak", + "renderer-vk_arm64.dll.mainbak", +) +# These directories were emitted by older content staging. They are removed +# only when empty; populated directories may contain intentional local content +# and are therefore left untouched. +WINDOWS_EMPTY_STAGE_DIRECTORY_MANIFEST = ( + "baseoq4/skins", +) RUNTIME_BINARY_PATTERNS = ( f"{PRODUCT_NAME}-client_*.exe", f"{PRODUCT_NAME}-ded_*.exe", @@ -256,6 +281,85 @@ def clear_staged_runtime_files(root_dir: Path) -> None: path.unlink() +def _manifest_path(root_dir: Path, relative_name: str) -> Path: + relative_path = Path(relative_name) + if relative_path.is_absolute() or not relative_path.parts or ".." in relative_path.parts: + raise RuntimeError(f"invalid Windows stage cleanup manifest path: {relative_name!r}") + parent = root_dir + for part in relative_path.parts[:-1]: + parent /= part + if _is_link_or_junction(parent): + raise RuntimeError(f"Windows stage cleanup parent is a link or junction: {parent}") + if parent.exists() and not parent.is_dir(): + raise RuntimeError(f"Windows stage cleanup parent is not a directory: {parent}") + candidate = root_dir.joinpath(*relative_path.parts) + if not _is_relative_to(candidate, root_dir): + raise RuntimeError(f"Windows stage cleanup path escapes its runtime target: {relative_name!r}") + return candidate + + +def _is_link_or_junction(path: Path) -> bool: + is_junction = getattr(path, "is_junction", None) + if path.is_symlink() or bool(is_junction and is_junction()): + return True + try: + attributes = getattr(path.lstat(), "st_file_attributes", 0) + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + except OSError: + return False + + +def cleanup_windows_stage_target(root_dir: Path) -> dict[str, list[str]]: + """Remove only explicitly known stale entries from a Windows stage root.""" + + root_dir = validate_runtime_directory(root_dir, "Windows runtime target", must_exist=False) + removed_files: list[str] = [] + removed_directories: list[str] = [] + + if not root_dir.exists(): + return { + "removed_stale_files": removed_files, + "removed_empty_directories": removed_directories, + } + + for relative_name in WINDOWS_STALE_STAGE_FILE_MANIFEST: + candidate = _manifest_path(root_dir, relative_name) + if _is_link_or_junction(candidate): + # Unlinking a known stale leaf is safe and does not follow its + # target. Directory links are rejected rather than recursively + # traversed or removed. + if candidate.is_dir(): + raise RuntimeError(f"stale Windows runtime file is a directory link or junction: {candidate}") + candidate.unlink() + removed_files.append(relative_name) + continue + if not candidate.exists(): + continue + if not candidate.is_file(): + raise RuntimeError(f"stale Windows runtime file path is not a regular file: {candidate}") + candidate.unlink() + removed_files.append(relative_name) + + for relative_name in WINDOWS_EMPTY_STAGE_DIRECTORY_MANIFEST: + candidate = _manifest_path(root_dir, relative_name) + if _is_link_or_junction(candidate): + raise RuntimeError(f"empty Windows runtime directory is a link or junction: {candidate}") + if not candidate.exists(): + continue + if not candidate.is_dir(): + raise RuntimeError(f"empty Windows runtime directory path is not a directory: {candidate}") + try: + next(candidate.iterdir()) + except StopIteration: + candidate.rmdir() + removed_directories.append(relative_name) + + return { + "removed_stale_files": removed_files, + "removed_empty_directories": removed_directories, + } + + def _is_relative_to(path: Path, root: Path) -> bool: try: @@ -294,6 +398,8 @@ def _copy_runtime_tree(source_dir: Path, destination_dir: Path, ignore_patterns: def validate_runtime_directory(path: Path, label: str, *, must_exist: bool) -> Path: if path.is_symlink(): raise RuntimeError(f"{label} must not be a symlink: {path}") + if _is_link_or_junction(path): + raise RuntimeError(f"{label} must not be a junction or reparse point: {path}") if path.exists() and not path.is_dir(): raise RuntimeError(f"{label} exists but is not a directory: {path}") if must_exist and not path.is_dir(): @@ -390,6 +496,10 @@ def stage_runtime_payloads( ] staged_build_game = stage_build_game_directory(source_root, build_root) + cleanup_results: dict[str, dict[str, list[str]]] = {} + for target in targets: + cleanup_results[str(target)] = cleanup_windows_stage_target(target) + binaries = collect_runtime_binaries(build_root) if not binaries: return { @@ -397,6 +507,7 @@ def stage_runtime_payloads( "runtime_flavor": RuntimeFlavor.NONE, "targets": [str(target) for target in targets], "copied_files": [], + "stage_cleanup": cleanup_results, "staged_build_game": staged_build_game, "validated_binaries": [], } @@ -438,6 +549,7 @@ def stage_runtime_payloads( "runtime_flavor": flavor, "targets": [str(target) for target in targets], "copied_files": sorted(set(copied_files)), + "stage_cleanup": cleanup_results, "staged_build_game": staged_build_game, "validated_binaries": [str(path) for path in binaries], } diff --git a/tools/debug/renderdoc_capture.ps1 b/tools/debug/renderdoc_capture.ps1 index 1cec6846..cd67ff39 100644 --- a/tools/debug/renderdoc_capture.ps1 +++ b/tools/debug/renderdoc_capture.ps1 @@ -95,6 +95,7 @@ if ($Mode -eq "SP") { } else { $gameArgs += @( "+set", "net_serverDedicated", "0", + "+set", "ui_autoJoin", "1", "+seta", "si_pure", "0", "+set", "net_serverAllowServerMod", "1", "+set", "sv_cheats", "1", diff --git a/tools/debug/start_listen_server_client.ps1 b/tools/debug/start_listen_server_client.ps1 index 17935870..96d2ee7f 100644 --- a/tools/debug/start_listen_server_client.ps1 +++ b/tools/debug/start_listen_server_client.ps1 @@ -135,6 +135,7 @@ $serverArgs = New-openQ4CommonArgs ` $serverArgs += @( "+set", "net_serverDedicated", "0", "+set", "net_port", $Port.ToString(), + "+set", "ui_autoJoin", "1", "+seta", "si_pure", "0", "+set", "net_serverAllowServerMod", "1", "+set", "sv_cheats", "1", @@ -154,6 +155,7 @@ $clientArgs = New-openQ4CommonArgs ` -ShowFramePacing $ShowFramePacing $clientArgs += @( + "+set", "ui_autoJoin", "1", "+set", "ui_name", $ClientName, "+connect", ("127.0.0.1:{0}" -f $Port) ) diff --git a/tools/tests/cmdargs_append_contract.py b/tools/tests/cmdargs_append_contract.py index b327aeb3..2314d0c2 100644 --- a/tools/tests/cmdargs_append_contract.py +++ b/tools/tests/cmdargs_append_contract.py @@ -48,9 +48,12 @@ def validate_tree(root: Path, context: str) -> None: args_body = function_body(source, "const char *idCmdArgs::Args", context) append_body = function_body(source, "void idCmdArgs::AppendArg", context) + if "static idStr cmd_args" not in source: + raise AssertionError(f"{context}: missing dynamic idCmdArgs::Args scratch storage") + if not any(clear in args_body for clear in ("cmd_args.Clear()", "cmd_args.SecureClear()")): + raise AssertionError(f"{context}: idCmdArgs::Args does not clear its scratch storage") + for needle in ( - "static idStr cmd_args", - "cmd_args.Clear()", "if ( start < 0 )", 'cmd_args += "\\\\\\\\"', "cmd_args += *p", diff --git a/tools/tests/competitive_match_layer.py b/tools/tests/competitive_match_layer.py index 8ff6b605..43bb4edd 100644 --- a/tools/tests/competitive_match_layer.py +++ b/tools/tests/competitive_match_layer.py @@ -231,6 +231,18 @@ def main() -> None: require(multiplayer, "gameLocal.gameType == GAME_TDM || roundMode", "round rows") require(multiplayer, 'common->GetLocalizedString( "#str_41404" )', "round limit") + # A casual no-time-limit server may retain the stock positive si_overtime + # default. Timed overtime is valid only when regulation itself is timed; + # otherwise the typed rule import must normalize to sudden death with no + # timed period instead of rejecting the complete rules snapshot. + for token in ( + 'const int timeLimitMinutes = gameLocal.serverInfo.GetInt( "si_timeLimit" );', + "const bool useTimedOvertime = timeLimitMinutes > 0 && overtimeSeconds > 0;", + "useTimedOvertime ? MP_OVERTIME_TIMED_PERIODS : MP_OVERTIME_SUDDEN_DEATH", + "useTimedOvertime ? overtimeSeconds : 0", + ): + require(multiplayer, token, "casual overtime normalization") + # The managed-match HUD and scoreboard are projections of exactly the same # pre-localized state set. One parent gate makes their casual path inert. validate_match_context_surface( diff --git a/tools/tests/filesystem_write_qpath_safety.py b/tools/tests/filesystem_write_qpath_safety.py index 91d7c601..b0599acb 100644 --- a/tools/tests/filesystem_write_qpath_safety.py +++ b/tools/tests/filesystem_write_qpath_safety.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Regression checks for relative filesystem mutation qpath safety.""" +import hashlib from pathlib import Path @@ -131,6 +132,204 @@ def validate_behavior_model() -> None: raise AssertionError(f"Expected rejected relative mutation qpath: {path!r}") +def validate_generated_loadscreen_path_budget() -> None: + final_qpath = "guis/assets/generated/loadscreens/airdefense1_1820x1024.tga" + save_root = "x" * 178 + final_path = f"{save_root}/{final_qpath}" + nonce = "0" * 32 + sibling_stage = f"{save_root}/{final_qpath.rsplit('/', 1)[0]}/_oq4.{nonce}.00000001.tmp" + fixed_stage_qpath = f"_oq4/{nonce}.tmp" + fixed_stage = f"{save_root}/{fixed_stage_qpath}" + minimum_final_qpath = "guis/assets/generated/loadscreens/a_1x1.tga" + + if len(final_path) != 238 or len(sibling_stage) != 263: + raise AssertionError("loadscreen path-budget fixture no longer models the Windows boundary") + if len(fixed_stage) != 220 or len(fixed_stage) >= len(final_path): + raise AssertionError("root-level loadscreen staging path does not preserve MAX_PATH headroom") + if len(fixed_stage_qpath) >= len(minimum_final_qpath): + raise AssertionError("loadscreen staging qpath is not shorter than every generated final qpath") + if not is_safe_relative_write_path_model(fixed_stage_qpath): + raise AssertionError("root-level loadscreen staging path is not a portable mutation qpath") + + +def validate_generated_binary_image_cache_fallback() -> None: + def compact_digest(logical_name: str) -> str: + normalized_name = logical_name.replace("\\", "/").lower() + return hashlib.sha256(normalized_name.encode("utf-8")).hexdigest()[:32] + + logical_name = "makeIntensity( gfx/lights/squarelight1a)#__0500" + digest = compact_digest(logical_name) + legacy_qpath = ( + "generated/images/_programs/" + "makeintensity_gfx_lights_squarelight1a_0500_c92dc23b.bimage" + ) + compact_qpath = f"generated/images/_compact/v1/{digest}.bimage" + save_root_length = 174 + + if digest != "46731237362ef058d9144a9884e3e2e9": + raise AssertionError("compact binary-image identity is not deterministic") + if compact_digest("textures/a b#__0200") == compact_digest("textures/ab#__0200"): + raise AssertionError("compact binary-image normalization erases logical-name identity") + authored_digest = compact_digest( + "gfx/effects/fluids_drips/brown_bubble_half#__0200#q4authored.tga" + ) + fallback_digest = compact_digest( + "gfx/effects/fluids_drips/brown_bubble_half#__0200#q4stockfallback.tga" + ) + if authored_digest == fallback_digest: + raise AssertionError("compact image cache aliases authored and stock-fallback identities") + if save_root_length + 1 + len(legacy_qpath) != 261: + raise AssertionError("binary-image path-budget fixture no longer crosses MAX_PATH") + if save_root_length + 1 + len(compact_qpath) != 243: + raise AssertionError("compact binary-image cache path lost its fixed short budget") + if not is_safe_relative_write_path_model(compact_qpath): + raise AssertionError("compact binary-image cache path is not a portable mutation qpath") + + source = read("src/imagetools/BinaryImage.cpp") + image_header = read("src/imagetools/BinaryImage.h") + image_load_source = read("src/renderer/Image_load.cpp") + compact_name = function_body(source, "static void R_MakeCompactBinaryImageFileName(") + write_cache = function_body(source, "ID_TIME_T idBinaryImage::WriteGeneratedFile(") + checked_read = function_body(source, "ID_TIME_T idBinaryImage::LoadFromGeneratedFile( ID_TIME_T sourceFileTime )") + unchecked_read = function_body(source, "ID_TIME_T idBinaryImage::LoadFromGeneratedFileUnchecked(") + compact_unchecked_read = function_body( + source, "ID_TIME_T idBinaryImage::LoadFromCompactGeneratedFileUnchecked(" + ) + load_image = function_body(image_load_source, "void idImage::ActuallyLoadImage(") + + for token in ( + "normalizedName.BackSlashesToSlashes();", + "normalizedName.ToLower();", + "idCrypto::SHA256(", + "i < 16", + '"generated/images/_compact/v1/%s.bimage"', + ): + require(compact_name, token, "versioned SHA-256/128 compact binary-image identity") + reject(compact_name, 'normalizedName.Replace( " ", "" );', + "compact binary-image identity must preserve meaningful spaces") + + require_order(write_cache, + 'fileSystem->OpenFileWrite( writeFileName, "fs_savepath" )', + "R_MakeCompactBinaryImageFileName( writeFileName, GetName() )", + "legacy binary-image write before compact fallback") + require_order(write_cache, + "R_MakeCompactBinaryImageFileName( writeFileName, GetName() )", + '\t\toutputFile = fileSystem->OpenFileWrite( writeFileName, "fs_savepath" );', + "compact binary-image fallback write") + require_order(write_cache, + '\t\toutputFile = fileSystem->OpenFileWrite( writeFileName, "fs_savepath" );', + "idLib::Warning(", + "binary-image warning only after compact fallback failure") + require(write_cache, "return FILE_NOT_FOUND_TIMESTAMP;", + "recoverable binary-image cache publication failure") + + require_order(checked_read, + "fileSystem->OpenFileRead( binaryFileName )", + "R_MakeCompactBinaryImageFileName( compactFileName, GetName() )", + "timestamp-checked binary-image legacy-first lookup") + require_order(checked_read, + "R_MakeCompactBinaryImageFileName( compactFileName, GetName() )", + "fileSystem->OpenFileRead( compactFileName )", + "timestamp-checked binary-image compact fallback lookup") + require(checked_read, "LoadFromGeneratedFile( compactFile", + "timestamp-checked compact binary-image payload validation") + reject(unchecked_read, "R_MakeCompactBinaryImageFileName(", + "ordinary unchecked lookup must expose legacy rejection to its caller") + require(compact_unchecked_read, "fileSystem->OpenFileRead( compactFileName )", + "pure-policy-preserving compact unchecked lookup") + reject(compact_unchecked_read, "OpenExplicitFileRead", + "compact binary-image lookup must not bypass pure VFS policy") + require(compact_unchecked_read, "LoadFromGeneratedFile( compactFile", + "compact unchecked payload validation") + require(image_header, "LoadFromCompactGeneratedFileUnchecked();", + "compact unchecked retry API") + + accept_generated = function_body(load_image, "auto acceptGeneratedImage =") + for token in ( + "im.GetFileHeader().sourceFileTime != sourceFileTime", + "R_BinaryImageHeaderSupportedByRenderer( im.GetFileHeader() )", + "R_GeneratedImageHeaderMatchesDerivedOpts( im.GetFileHeader(), opts, usage )", + ): + require(accept_generated, token, "shared legacy/compact cache acceptance") + require_order(load_image, + "generatedImageAccepted = acceptGeneratedImage( binaryFileTime );", + "im.LoadFromCompactGeneratedFileUnchecked();", + "caller-rejected legacy before compact cache retry") + require_order(load_image, + "im.LoadFromCompactGeneratedFileUnchecked();", + "\t\tgeneratedImageAccepted = acceptGeneratedImage( binaryFileTime );", + "compact cache receives full caller-level validation") + require_order(load_image, + "im.LoadFromCompactGeneratedFileUnchecked();", + "if ( generatedImageAccepted ) {", + "compact cache retry before source decode") + + +def validate_generated_binary_image_write_integrity() -> None: + source = read("src/imagetools/BinaryImage.cpp") + image_load_source = read("src/renderer/Image_load.cpp") + write_image = function_body(source, "bool idBinaryImage::WriteToFile(") + write_cache = function_body(source, "ID_TIME_T idBinaryImage::WriteGeneratedFile(") + load_image = function_body(image_load_source, "void idImage::ActuallyLoadImage(") + + metadata_fields = ( + "fileData.sourceFileTime", + "fileData.headerMagic", + "fileData.textureType", + "fileData.format", + "fileData.colorFormat", + "fileData.width", + "fileData.height", + "fileData.numLevels", + "img.level", + "img.destZ", + "img.width", + "img.height", + "img.dataSize", + ) + for field in metadata_fields: + require( + write_image, + f"file->WriteBig( {field} ) != sizeof( {field} )", + f"binary-image short write for {field}", + ) + if write_image.count("file->WriteBig(") != len(metadata_fields): + raise AssertionError("binary-image metadata contains an unchecked WriteBig call") + require( + write_image, + "file->WriteBig( fileData.numLevels ) != sizeof( fileData.numLevels ) ) {\n\t\treturn false;\n\t}", + "binary-image file-header short-write rejection", + ) + require( + write_image, + "file->WriteBig( img.dataSize ) != sizeof( img.dataSize ) ) {\n\t\t\treturn false;\n\t\t}", + "binary-image mip-metadata short-write rejection", + ) + require( + write_image, + "if ( file->Write( img.data, img.dataSize ) != img.dataSize ) {\n\t\t\treturn false;\n\t\t}", + "binary-image mip payload short write", + ) + + require( + write_cache, + "if ( !WriteToFile( file, sourceFileTime ) ) {\n\t\treturn FILE_NOT_FOUND_TIMESTAMP;\n\t}", + "generated binary-image write failure remains recoverable", + ) + write_call = "binaryFileTime = im.WriteGeneratedFile( sourceFileTime );" + write_call_index = load_image.find(write_call) + if write_call_index == -1: + raise AssertionError("image loader no longer publishes the generated binary image") + post_write = load_image[write_call_index:] + require_order(post_write, write_call, "AllocImage();", + "binary-image publication failure does not stop allocation") + require_order(post_write, write_call, "SubImageUpload(", + "binary-image publication failure does not stop upload") + allocation_index = post_write.find("AllocImage();") + if "return" in post_write[len(write_call):allocation_index]: + raise AssertionError("binary-image cache publication failure became fatal to image loading") + + def validate_source_contract() -> None: source = read("src/framework/FileSystem.cpp") header = read("src/framework/FileSystem.h") @@ -227,8 +426,6 @@ def validate_generated_loadscreen_publication() -> None: "TGA writer reports short writes") require(image_header, "bool\tR_WriteTGA(", "renderer TGA writer result contract") require(image_tools_header, "bool\tR_WriteTGA(", "imagetools TGA writer result contract") - require(prepare, "static uint32 stagingSequence = 0;", - "generated loadscreen per-process staging sequence") require(prepare, "uint64 stagingNonce[2] = { 0, 0 };", "generated loadscreen 128-bit staging nonce") require(prepare, "Sys_GetSecureRandomBytes( stagingNonce, sizeof( stagingNonce ) )", @@ -237,18 +434,40 @@ def validate_generated_loadscreen_publication() -> None: "generated loadscreen no-CSPRNG source fallback") require(secure_staging_failure, "return false;", "generated loadscreen no-CSPRNG early return") - require(prepare, 'const idStr stagingPath = va( "%s.%016llx%016llx.%u.partial"', - "generated loadscreen unique staging path") + require(prepare, 'const idStr stagingPath = va( "_oq4/%016llx%016llx.tmp"', + "generated loadscreen short root-level staging path") + reject(prepare, "static uint32 stagingSequence", + "generated loadscreen CSPRNG identity needs no shared sequence") + reject(prepare, "stagingPath.StripFilename();", + "generated loadscreen staging must not inherit the long final directory") + reject(prepare, 'stagingPath += va( "/_oq4.', + "generated loadscreen staging must not remain beside the final file") + reject(prepare, 'va( "%s.%016llx%016llx.%u.partial"', + "generated loadscreen must not inflate the final path past platform limits") require(prepare, "R_WriteTGA( stagingPath.c_str()", "generated loadscreen staged write") require(prepare, "fileSystem->PromoteFile( stagingPath.c_str(), generatedPath.c_str()", "generated loadscreen atomic publication") + require(prepare, "else if ( !Session_FileExistsInSearchPaths( generatedPath.c_str() ) )", + "generated loadscreen active-VFS visibility gate") + require(prepare, "declManager->FindMaterial( generatedPath.c_str() )", + "generated loadscreen material preflight") + require(prepare, "generatedMaterial->TestMaterialFlag( MF_DEFAULTED )", + "generated loadscreen default-material rejection") + require(prepare, "using the source levelshot", + "generated loadscreen unavailable-path source fallback") require(prepare, "fileSystem->RemoveFileChecked( stagingPath.c_str()", "generated loadscreen failed-stage cleanup") require(prepare, "return published;", "generated loadscreen failure falls back to source") require_order(prepare, "R_WriteTGA( stagingPath.c_str()", "fileSystem->PromoteFile(", "generated loadscreen write-before-publish order") + require_order(prepare, "fileSystem->PromoteFile(", + "Session_FileExistsInSearchPaths( generatedPath.c_str() )", + "generated loadscreen publish-before-VFS-validation order") + require_order(prepare, "Session_FileExistsInSearchPaths( generatedPath.c_str() )", + "declManager->FindMaterial( generatedPath.c_str() )", + "generated loadscreen VFS-before-material-validation order") require_order(prepare, "Sys_GetSecureRandomBytes( stagingNonce, sizeof( stagingNonce ) )", - 'const idStr stagingPath = va( "%s.%016llx%016llx.%u.partial"', + 'const idStr stagingPath = va( "_oq4/%016llx%016llx.tmp"', "generated loadscreen secure-token-before-path order") require_order(prepare, "Sys_GetSecureRandomBytes( stagingNonce, sizeof( stagingNonce ) )", "R_WriteTGA( stagingPath.c_str()", @@ -261,6 +480,9 @@ def validate_generated_loadscreen_publication() -> None: def main() -> int: validate_behavior_model() + validate_generated_loadscreen_path_budget() + validate_generated_binary_image_cache_fallback() + validate_generated_binary_image_write_integrity() validate_source_contract() validate_generated_loadscreen_publication() print("filesystem relative mutation qpath safety checks passed") diff --git a/tools/tests/game_type_module_selection.py b/tools/tests/game_type_module_selection.py index 3446be2a..1f01a7af 100644 --- a/tools/tests/game_type_module_selection.py +++ b/tools/tests/game_type_module_selection.py @@ -409,9 +409,26 @@ def validate_ttf_persistent_atlas_contract(ttf_source: str) -> None: create = function_body( ttf_source, "static const idMaterial *R_TTFCreateAtlasMaterial", "TrueType atlas material creation" ) - require(create, "R_TTFMaterialSamplesAtlas(", "TrueType atlas material creation") require(create, "R_TTFInstallAtlasStage(", "TrueType atlas material creation") require(create, "ttfAtlasMaterials.Append(", "TrueType atlas material registry") + reject( + create, + "if ( !R_TTFMaterialSamplesAtlas(", + "unconditional TrueType atlas material source install", + ) + + # FindMaterial parses a newly created implicit material before this module + # can replace its source. Its default stage maps an image named after the + # material, so the generated material must share the already-uploaded + # intrinsic atlas identity. A separate openq4/ttffont name probes a file + # which can never exist and emits one warning/default image per atlas. + for signature, context in ( + ("static void R_TTFBuildExtendedPages", "extended TrueType atlas identity"), + ("static bool R_TTFBuildSlot", "base TrueType atlas identity"), + ): + atlas = function_body(ttf_source, signature, context) + require(atlas, "const idStr materialName = imageName;", context) + reject(atlas, "openq4/ttffont", context) restore = function_body( ttf_source, "void R_TTFRestoreAtlasMaterials( void )", "TrueType atlas material restore" @@ -647,7 +664,7 @@ def validate_two_phase_game_api_contract() -> None: return game_api = read(game_api_path) - require(game_api, "const int GAME_API_VERSION\t\t= 43;", "two-phase game API version") + require(game_api, "const int GAME_API_VERSION\t\t= 45;", "current game API version") require( game_api, "virtual void\t\t\t\tShutdownAfterDecls( void ) = 0;", @@ -1015,6 +1032,29 @@ def validate_lifecycle_mutation_sensitivity() -> None: "GUI TrueType atlas is not recreated across full vid_restart", ) + atlas_identity = "\tconst idStr materialName = imageName;" + if ttf_source.count(atlas_identity) != 2: + raise AssertionError("TrueType atlas identity mutation anchors are not exact") + expect_contract_rejection( + validate_ttf_persistent_atlas_contract, + ttf_source.replace(atlas_identity, '\tconst idStr materialName = "detached-atlas-material";', 1), + "generated TrueType material probes a nonexistent image identity on first parse", + ) + + create_install = "\tR_TTFInstallAtlasStage( material, source );" + if ttf_source.count(create_install) != 1: + raise AssertionError("TrueType atlas source-install mutation anchor is not exact") + conditional_install = ( + "\tif ( !R_TTFMaterialSamplesAtlas( material, imageName ) ) {\n" + "\t\tR_TTFInstallAtlasStage( material, source );\n" + "\t}" + ) + expect_contract_rejection( + validate_ttf_persistent_atlas_contract, + ttf_source.replace(create_install, conditional_install, 1), + "generated TrueType material keeps an implicit or restart-stale stage", + ) + for label, source_path, header_path in ( ( "SP", diff --git a/tools/tests/generated_animation_cache.py b/tools/tests/generated_animation_cache.py index b95350d8..039836f0 100644 --- a/tools/tests/generated_animation_cache.py +++ b/tools/tests/generated_animation_cache.py @@ -33,6 +33,8 @@ def require_order(haystack: str, first: str, second: str, context: str) -> None: def validate_engine_file_identity_contract() -> None: engine_header = read(ROOT, "src/framework/File.h") game_header = read(GAME_LIBS_ROOT, "src/framework/File.h") + engine_filesystem_header = read(ROOT, "src/framework/FileSystem.h") + game_filesystem_header = read(GAME_LIBS_ROOT, "src/framework/FileSystem.h") file_source = read(ROOT, "src/framework/File.cpp") filesystem_source = read(ROOT, "src/framework/FileSystem.cpp") @@ -51,6 +53,23 @@ def validate_engine_file_identity_contract() -> None: context, ) require(source, "int\t\t\t\t\t\tcontainerChecksum;", context) + require_order( + source, + "virtual void\t\t\tReadSyncId", + "virtual void\t\t\tMakeReadOnly", + f"{context} append-only staging ABI", + ) + + for source, context in ( + (engine_filesystem_header, "engine filesystem interface"), + (game_filesystem_header, "GameLibs filesystem interface"), + ): + require_order( + source, + "virtual bool\t\t\tInProductionMode() = 0;", + "virtual void\t\t\tBeginLevelLoadCache", + f"{context} append-only level-load ABI", + ) require(file_source, "containerChecksum = 0;", "PK4 file construction") require( @@ -75,32 +94,105 @@ def validate_game_cache_contract() -> None: 'path = "generated/animations/";', "GENERATED_ANIM_MAGIC", "GENERATED_ANIM_END_MAGIC", - "GENERATED_ANIM_VERSION", + "GENERATED_ANIM_VERSION = 3", "MAX_GENERATED_ANIM_FRAMES", "MAX_GENERATED_ANIM_JOINTS", + "MAX_GENERATED_ANIM_FRAME_RATE", "MAX_GENERATED_ANIM_DATA_BYTES", + "MAX_GENERATED_ANIM_RECORD_BYTES", + "GENERATED_ANIM_CRC_TRAILER_BYTES", "info.length = source->Length();", "info.timestamp = source->Timestamp();", "info.containerChecksum = source->GetContainerChecksum();", - "info.fullPath = source->GetFullPath();", + 'cvarSystem->GetCVarBool( "com_binaryRead" )', + "selectedPath.Append( Lexer::sCompiledFileSuffix );", + "NormalizeGeneratedAnimSourcePath( selectedPath, info.normalizedPath )", + "NormalizeGeneratedAnimSourcePath", + "normalized.BackSlashesToSlashes();", + "normalized.ToLower();", + "normalized[ 0 ] == '/'", + "c == 127 || c == ':'", + "segmentLength == 0", + "GeneratedAnimSegmentIsWindowsDevice", + "const unsigned char digit = static_cast( segment[ 3 ] );", + "digit == 0xB9 || digit == 0xB2 || digit == 0xB3", + "stemLength == 5 && digit == 0xC2", + "static_cast( segment[ 4 ] ) == 0xB9", + "static_cast( segment[ 4 ] ) == 0xB2", + "static_cast( segment[ 4 ] ) == 0xB3", + "CRC32_UpdateChecksum( checksum, buffer, readBytes );", "sourceLength == sourceInfo.length", "sourceContainerChecksum == sourceInfo.containerChecksum", + "sourceContentChecksum == sourceInfo.contentChecksum", "sourceTimestamp == sourceInfo.timestamp", - "sourceFullPath.Cmp( sourceInfo.fullPath ) == 0", + "sourcePath.Cmp( sourceInfo.normalizedPath ) == 0", "position < 0 || fileLength < position", "valueLength > MAX_STRING_CHARS", "LittleRevBytes( values, sizeof( float ), count );", "if ( !Swap_IsBigEndian() )", + "GeneratedAnimFloatsAreFinite", + "!FLOAT_IS_NAN( value )", + "CalculateGeneratedAnimLength", + "frameRate > MAX_GENERATED_ANIM_FRAME_RATE", "componentCount <= 0x7fffffff", "dataBytes <= MAX_GENERATED_ANIM_DATA_BYTES", + "GeneratedAnimComponentCount", + "jointComponentCount <= cachedNumAnimatedComponents - joint.firstComponent", + "cachedBounds[ i ][ 0 ][ axis ] <= cachedBounds[ i ][ 1 ][ axis ]", + "quaternionLengthSqr - 1.0f", + "storedLength == (unsigned int)recordLength", + "storedChecksum == CRC32_BlockChecksum( diskBytes.Ptr(), recordLength )", + "fileSystem->GetNewFileMemory();", + "cache->MakeReadOnly();", "endMagic == GENERATED_ANIM_END_MAGIC", "cache->Tell() == cache->Length()", - "fileSystem->RemoveFile( cachePath );", + 'fileSystem->OpenExplicitFileRead( cacheOSPath )', + 'fileSystem->RemoveFileChecked( cachePath, "fs_savepath" );', + "stagedCache->Sync();", + 'fileSystem->PromoteFile( stagedPath, cachePath, "fs_savepath" )', + "LEVEL_LOAD_RESOURCE_ANIMATION", "baseFrame.Clear();", "baseFrame.Allocated()", ): require(sp, token, "generated animation cache implementation") + require_order( + sp, + "if ( !GetGeneratedAnimPath( filename, cachePath ) )", + 'fileSystem->RelativePathToOSPath( cachePath, "fs_savepath" )', + "explicit cache-read path validation", + ) + require_order( + sp, + "storedChecksum == CRC32_BlockChecksum( diskBytes.Ptr(), recordLength )", + "cache = fileSystem->GetNewFileMemory();", + "integrity-before-parse staging", + ) + require_order( + sp, + 'cvarSystem->GetCVarBool( "com_binaryRead" )', + "selectedPath.Append( Lexer::sCompiledFileSuffix );", + "compiled animation source selection", + ) + require_order( + sp, + "selectedPath.Append( Lexer::sCompiledFileSuffix );", + "if ( source == NULL )", + "compiled-source precedence before ASCII fallback", + ) + require_order( + sp, + "NormalizeGeneratedAnimSourcePath( selectedPath, info.normalizedPath )", + "sourcePath.Cmp( sourceInfo.normalizedPath ) == 0", + "selected parser-source identity publication", + ) + + require_order( + sp, + 'cvarSystem->GetCVarBool( "com_levelLoadModernization" )', + "g_useGeneratedAnimCache.GetBool() && LoadGeneratedAnim( filename )", + "cache-before-source load order", + ) require_order( sp, "g_useGeneratedAnimCache.GetBool() && LoadGeneratedAnim( filename )", @@ -109,7 +201,19 @@ def validate_game_cache_contract() -> None: ) require_order( sp, - "animLength = ( ( numFrames - 1 ) * 1000 + frameRate - 1 ) / frameRate;", + 'cvarSystem->GetCVarBool( "com_levelLoadModernization" )', + "g_writeGeneratedAnimCache.GetBool()", + "default-off animation-cache write gate", + ) + require_order( + sp, + "g_writeGeneratedAnimCache.GetBool()", + "WriteGeneratedAnim( filename );", + "source-parse cache write control", + ) + require_order( + sp, + "CalculateGeneratedAnimLength( numFrames, frameRate, animLength )", "WriteGeneratedAnim( filename );", "source-parse cache write order", ) diff --git a/tools/tests/idstr_input_safety.py b/tools/tests/idstr_input_safety.py index e8515787..1d24feed 100644 --- a/tools/tests/idstr_input_safety.py +++ b/tools/tests/idstr_input_safety.py @@ -309,8 +309,12 @@ def validate_allocation_guards() -> None: if bodies["engine idStr"] != bodies["GameLibs idStr"]: raise AssertionError("Engine and GameLibs idStr allocation implementations have drifted") - engine_helper = (ROOT / "src" / "idlib" / "StrAllocation.h").read_bytes() - gamelibs_helper = (GAME_LIBS_ROOT / "src" / "idlib" / "StrAllocation.h").read_bytes() + # Source parity is textual. Universal-newline decoding avoids treating a + # Windows CRLF checkout and a Linux LF checkout as different helpers. + engine_helper = (ROOT / "src" / "idlib" / "StrAllocation.h").read_text(encoding="utf-8") + gamelibs_helper = (GAME_LIBS_ROOT / "src" / "idlib" / "StrAllocation.h").read_text( + encoding="utf-8" + ) if engine_helper != gamelibs_helper: raise AssertionError("Engine and GameLibs string-allocation helpers have drifted") diff --git a/tools/tests/key_bind_presentation.py b/tools/tests/key_bind_presentation.py index 04cc4b19..b3158e23 100644 --- a/tools/tests/key_bind_presentation.py +++ b/tools/tests/key_bind_presentation.py @@ -645,6 +645,21 @@ def validate_bind_menu_and_spectator_consumers() -> None: mphud = read(ROOT / "content" / "baseoq4" / "pak0" / "guis" / "mphud.gui") require(mphud, '"gui::spectatetext1"', "multiplayer spectator HUD binding prompt") + ready_prompt_window = body_of(mphud, "windowDef Spectate1", "mphud.gui Spectate1") + if re.search(r"\brect\s+0\s*,\s*141\s*,\s*640\s*,\s*40\b", ready_prompt_window) is None: + raise AssertionError("The stock non-tourney readiness prompt must retain its 640x40 HUD window") + require(ready_prompt_window, "textscale\t0.31", "stock readiness prompt text scale") + + code_strings = read( + ROOT / "content" / "baseoq4" / "pak0" / "strings" / "english_code.lang" + ) + for localized_string in ("#str_107710", "#str_107711"): + if re.search( + rf'^\s*"{localized_string}"\s*"[^"\r\n]*\\nPress %s to [^"\r\n]+"\s*$', + code_strings, + re.MULTILINE, + ) is None: + raise AssertionError(f"{localized_string} must retain its explicit two-line readiness layout") checked_tree = False for tree in ("mpgame", "game"): @@ -662,7 +677,23 @@ def validate_bind_menu_and_spectator_consumers() -> None: reject(update_hud, 'KeysFromBinding( "_attack" )', f"{tree} non-emphasized spectator attack binding") all_ready = body_of(multiplayer_source, "bool idMultiplayerGame::AllPlayersReady(", str(multiplayer)) - require(all_ready, 'KeysFromBindingForPrompt( "_impulse17" )', f"{tree} emphasized readiness prompt") + for localized_string in ("#str_110017", "#str_110018"): + require( + all_ready, + f'GetLocalizedString( "{localized_string}" ), common->KeysFromBindingForPrompt( "_impulse17" )', + f"{tree} emphasized single-line tourney readiness prompt", + ) + for localized_string in ("#str_107710", "#str_107711"): + require( + all_ready, + f'GetLocalizedString( "{localized_string}" ), common->KeysFromBinding( "_impulse17" )', + f"{tree} inline two-line readiness prompt", + ) + require( + all_ready, + "stock non-tourney HUD fits two normal-height lines", + f"{tree} stock readiness prompt height rationale", + ) start_vote = body_of(multiplayer_source, "void idMultiplayerGame::ClientStartPackedVote(", str(multiplayer)) require(start_vote, 'KeysFromBindingForPrompt("_impulse28")', f"{tree} emphasized vote-yes prompt") require(start_vote, 'KeysFromBindingForPrompt("_impulse29")', f"{tree} emphasized vote-no prompt") @@ -671,6 +702,87 @@ def validate_bind_menu_and_spectator_consumers() -> None: raise AssertionError(f"No companion game-library source trees found below {GAME_LIBS_ROOT}") +def validate_ready_binding_contract() -> None: + default_cfg = read(ROOT / "content" / "baseoq4" / "pak0" / "default.cfg") + controls = read( + ROOT + / "content" + / "baseoq4" + / "pak0" + / "guis" + / "menu" + / "settings" + / "controls.gui" + ) + if re.search(r"(?m)^\s*bind\s+F3\s+_impulse17(?:\s+//.*)?$", default_cfg) is None: + raise AssertionError("The shipped F3 ready default must use stock _impulse17") + for key, impulse in (("F1", "_impulse28"), ("F2", "_impulse29"), ("F6", "_impulse20"), ("F7", "_impulse22")): + if re.search(rf"(?m)^\s*bind\s+{key}\s+{impulse}(?:\s+//.*)?$", default_cfg) is None: + raise AssertionError(f"The shipped {key} default must use stock {impulse}") + if re.search( + r"bindDef\s+set_ctrls_other_ready_key\s*\{.*?\bbind\s+_impulse17\b", + controls, + re.DOTALL, + ) is None: + raise AssertionError("The controls-menu ready row must edit stock _impulse17") + for widget, impulse in (("voteyes", "_impulse28"), ("voteno", "_impulse29")): + if re.search( + rf"bindDef\s+set_ctrls_other_{widget}_key\s*\{{.*?\bbind\s+{impulse}\b", + controls, + re.DOTALL, + ) is None: + raise AssertionError(f"The controls-menu {widget} row must edit stock {impulse}") + + multiplayer_path = GAME_LIBS_ROOT / "src" / "mpgame" / "MultiplayerGame.cpp" + multiplayer = read(multiplayer_path) + toggle_ready = body_of( + multiplayer, + "void idMultiplayerGame::ToggleReady(", + str(multiplayer_path), + ) + require(toggle_ready, "MPSendReady( !ready );", "reliable impulse-17 ready toggle") + reject(toggle_ready, "SetCVarString", "userinfo-only impulse-17 ready toggle") + + send_ready = body_of(multiplayer, "static void MPSendReady(", str(multiplayer_path)) + require( + send_ready, + "GAME_RELIABLE_MESSAGE_READY", + "casual client ready reliable message", + ) + require( + send_ready, + "ServerSetPlayerReady", + "listen-server authoritative ready path", + ) + ready_command = body_of( + multiplayer, + "void idMultiplayerGame::Ready_f(", + str(multiplayer_path), + ) + require(ready_command, "MPSendReady( true );", "idempotent ready command") + + reset = body_of(multiplayer, "void idMultiplayerGame::Reset(", str(multiplayer_path)) + require( + reset, + 'common->BindingFromKey( "F3" )', + "exact legacy F3 ready migration lookup", + ) + require( + reset, + '"bind F3 _impulse17\\n"', + "exact legacy F3 ready migration update", + ) + for key, legacy, impulse in ( + ("F1", "voteyes", "_impulse28"), + ("F2", "voteno", "_impulse29"), + ("F6", "toggleteam", "_impulse20"), + ("F7", "spectate", "_impulse22"), + ): + require(reset, f'common->BindingFromKey( "{key}" )', f"exact legacy {key} migration lookup") + require(reset, f'"{legacy}"', f"exact legacy {key} binding match") + require(reset, f'"bind {key} {impulse}\\n"', f"exact legacy {key} migration update") + + def main() -> int: try: validate_binding_formatter() @@ -680,6 +792,7 @@ def main() -> int: validate_bind_widget_fit_and_capture() validate_localized_controls_hint() validate_bind_menu_and_spectator_consumers() + validate_ready_binding_contract() except AssertionError as error: print(f"key_bind_presentation: FAILED - {error}") return 1 diff --git a/tools/tests/level_load_cache.py b/tools/tests/level_load_cache.py new file mode 100644 index 00000000..0887391b --- /dev/null +++ b/tools/tests/level_load_cache.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +"""Static integration checks for Milestone B loading/cache modernization.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +GAME_ROOT = Path(os.environ.get("OPENQ4_GAMELIBS_REPO", ROOT.parent / "openQ4-game")).resolve() + + +def read(root: Path, relative: str) -> str: + path = root / relative + if not path.is_file(): + raise AssertionError(f"Required file not found: {path}") + return path.read_text(encoding="utf-8") + + +def require(text: str, needle: str, context: str) -> None: + if needle not in text: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def require_order(text: str, first: str, second: str, context: str) -> None: + first_index = text.find(first) + second_index = text.find(second) + if first_index < 0 or second_index < 0 or first_index >= second_index: + raise AssertionError(f"Expected {first!r} before {second!r} in {context}") + + +def validate_abi_contract() -> None: + engine_fs = read(ROOT, "src/framework/FileSystem.h") + game_fs = read(GAME_ROOT, "src/framework/FileSystem.h") + for token in ( + "LEVEL_LOAD_RESOURCE_RENDER_MODEL = 1", + "GENERATED_CACHE_RENDER_MODEL = 1", + "BeginLevelLoadCache", + "FinishLevelLoadCache", + "CancelLevelLoadCache", + "RecordLevelLoadResource", + "OpenGeneratedCacheRead", + "WriteGeneratedCache", + "DiscardGeneratedCache", + ): + require(engine_fs, token, "engine filesystem ABI") + require(game_fs, token, "game filesystem ABI") + require( + read(ROOT, "src/renderer/RenderModuleAPI.h"), + "#define RENDER_API_VERSION\t\t\t11", + "renderer module ABI", + ) + require( + read(GAME_ROOT, "src/game/Game.h"), + "GAME_API_VERSION\t\t= 45", + "game module ABI", + ) + + +def validate_manifest_and_envelope_contract() -> None: + header = read(ROOT, "src/framework/LevelLoadCacheFormat.h") + source = read(ROOT, "src/framework/LevelLoadCacheFormat.cpp") + native_test = read(ROOT, "tools/tests/native/LevelLoadCacheFormatTest.cpp") + meson = read(ROOT, "meson.build") + for token in ( + "SourceIdentity", + "containerPk4Checksum", + "EnvelopeExpectation", + "ManifestExpectation", + "Hash\t\t\t\tgameMode", + "Hash\t\t\t\tentityFilter", + "DecodeLimits", + "DecodeEnvelopeForKey", + "DecodeManifestForKey", + "ValidateDecodedPayload", + ): + require(header, token, "portable cache format API") + for token in ( + "INTEGRITY_MISMATCH", + "TRAILING_DATA", + "SIZE_LIMIT_EXCEEDED", + "CanonicalizeManifest", + ): + require(header + source, token, "hardened format implementation") + require(native_test, "ExerciseEnvelopeFailures", "native malformed-cache coverage") + require(native_test, "ExerciseManifestFailures", "native manifest corruption coverage") + require(meson, "openq4-level-load-cache-format", "native Meson cache-format test") + + +def validate_pipeline_and_lifecycle() -> None: + pipeline = read(ROOT, "src/framework/LevelLoadPipeline.cpp") + pipeline_test = read(ROOT, "tools/tests/native/LevelLoadPipelineTest.cpp") + meson = read(ROOT, "meson.build") + manager = read(ROOT, "src/framework/LevelLoadCacheManager.cpp") + filesystem = read(ROOT, "src/framework/FileSystem.cpp") + session = read(ROOT, "src/framework/Session.cpp") + + for token in ( + "maxEntries", + "maxSourceBytes", + "maxTotalBytes", + "readChunkBytes", + "maxDecodedBytes", + "decodeChunkBytes", + "IsCancellationRequested", + "ReportDecodedBytes", + "idLevelLoadDecodedSource", + "payloadOffset", + "frameUnitCount", + "ComputeFramingSeal", + "idLevelLoadDecodeSourceFrame", + "synchronousFallback", + "DrainOpenFiles", + ): + require(pipeline, token, "bounded cancellable pipeline") + for token in ( + "ExerciseBoundedSynchronousReplay", + "ExerciseCooperativeCancellation", + "ExerciseCooperativeDecodeCancellation", + "ExerciseMalformedFramingFallback", + "ExerciseProductionFramingValidation", + "ExerciseSetupFailureHandleRecovery", + "ExerciseQueueSaturationFallback", + ): + require(pipeline_test, token, "native level-load pipeline coverage") + require(meson, "openq4-level-load-pipeline", "native Meson level-load pipeline test") + require(filesystem, "std::lock_guard lock( readCountMutex )", "worker read accounting") + require(filesystem, "std::this_thread::get_id() == fileSystemMainThread", "main-thread pacifier") + require(filesystem, "UsePreloadedLevelLoadSource", "source-authoritative preload substitution") + if filesystem.count("levelLoadCache = new idLevelLoadCacheManager( this );") != 2: + raise AssertionError("level-load coordinator must be created by initial startup and restart") + + for token in ( + '"com_levelLoadModernization", "0"', + "!com_levelLoadModernization.GetBool()", + '"generated/manifests/"', + 'subdirectory = "models"', + 'subdirectory = "worlds"', + 'subdirectory = "collision"', + "DecodeManifestForKey", + "DecodeEnvelopeForKey", + "RemoveFileChecked", + "file->Sync()", + "PromoteFile", + "OpenExplicitFileRead", + "OpenFileRead( normalized.c_str(), false )", + "completedGeneration = true", + "WriteLearnedManifest", + "OpenPreloadedSource", + "DecodePipelineSource", + "&ReadPipelineSource, nullptr, &DecodePipelineSource", + "idLevelLoadDecodeSourceFrame", + "HASH_DOMAIN", + "read/decompress stage", + "decodeBudgetBytes", + 'extension == ".md5animc"', + 'extension == ".md5rc"', + 'extension == ".md5rmeshc"', + 'extension == ".procc"', + 'extension == ".md5rprocc"', + "compiledName += Lexer::sCompiledFileSuffix.c_str()", + "Generated cache owner payload rejected and removed", + "generatedCorruptions.fetch_add", + "HashHex( expected.gameMode )", + "HashHex( expected.entityFilter )", + '"com_levelLoadPreloadMaxEntries", "64"', + "idCmdSystem::ArgCompletion_Integer<1,64>", + ): + require(manager, token, "level-load coordinator") + for token in ( + "search->pack->checksum", + "search->pack->length", + "search->pack->numfiles", + "serverPaks", + ): + require(filesystem, token, "exact active content signature") + + unload = session[session.index("void idSessionLocal::UnloadMap()") :] + require_order(unload, "CancelLevelLoadCache", "game->MapShutdown", "map teardown join") + execute_start = session.index("void idSessionLocal::ExecuteMapChange") + execute_end = session.index("idSessionLocal::TakeNotes", execute_start) + execute = session[execute_start:execute_end] + if execute.count("fileSystem->ResetReadCount();") != 1: + raise AssertionError("map-change read accounting must have exactly one reset") + require_order( + execute, + "fileSystem->ResetReadCount();", + "fileSystem->BeginLevelLoadCache", + "read accounting reset before worker publication", + ) + require_order(execute, "BeginLevelLoadCache", "rw->InitFromMap", "generation start") + require_order(execute, "FinishLevelLoadCache", "renderSystem->EndLevelLoad", "main-thread join") + require_order(execute, "renderSystem->EndLevelLoad", "FS_ReleaseLevelLoadCache", "render replay retention") + require_order(execute, "uiManager->EndLevelLoad", "FS_ReleaseLevelLoadCache", "final replay consumer") + + +def validate_resource_consumers() -> None: + model = "\n".join( + read(ROOT, path) + for path in ( + "src/renderer/Model.cpp", + "src/renderer/Model_md5.cpp", + "src/renderer/Model_md5r.cpp", + ) + ) + world = read(ROOT, "src/renderer/RenderWorld_load.cpp") + collision = "\n".join( + read(ROOT, path) + for path in ( + "src/cm/CollisionModel_files.cpp", + "src/cm/CollisionModel_load.cpp", + ) + ) + for source, kind, context in ( + (model, "GENERATED_CACHE_RENDER_MODEL", "render-model cache"), + (world, "GENERATED_CACHE_RENDER_WORLD", "render-world cache"), + (collision, "GENERATED_CACHE_COLLISION_MODEL", "collision cache"), + ): + require(source, kind, context) + require(source, "OpenGeneratedCacheRead", context) + require(source, "WriteGeneratedCache", context) + require(source, "DiscardGeneratedCache", context) + require(model, "WriteLevelLoadCachePayload", "render-model bounded payload") + require( + model, + "edge.p2 > batch.silTraceGeoSpec.primitiveCount", + "MD5R open-edge sentinel validation", + ) + require(world, "WriteLevelLoadCachePayload", "render-world bounded payload") + require(world, "R_RenderWorldCacheWriteShadowModel", "render-world shadow-only payload writer") + require(world, "R_RenderWorldCacheReadShadowModel", "render-world shadow-only payload reader") + require(world, "RENDER_WORLD_CACHE_MODEL_SHADOW", "render-world shadow payload discriminator") + require(world, "indexes[i] >= numVerts", "render-world shadow index validation") + require(collision, "WriteGeneratedCollisionCache", "collision bounded payload writer") + require(collision, "LoadGeneratedCollisionCache", "collision bounded payload reader") + require(collision, "CM_CACHE_MAX_ALLOCATION_BYTES", "collision allocation budget") + require_order( + collision, + 'cvarSystem->GetCVarBool( "com_binaryRead" )', + "compiledProcPath += Lexer::sCompiledFileSuffix", + "collision compiled-proc enablement", + ) + require_order( + collision, + "compiledProcPath += Lexer::sCompiledFileSuffix", + "OpenFileRead( compiledProcPath.c_str(), false )", + "collision compiled-proc open", + ) + require_order( + collision, + "OpenFileRead( compiledProcPath.c_str(), false )", + "OpenFileRead( sourceProcPath.c_str(), false )", + "collision text-proc fallback", + ) + for token in ( + "resolvedProcPath = compiledProcPath", + '"mode=map-proc;proc=%s;length=%d;timestamp=%lld;container=%08x;loose-crc=%08x;complete=%d"', + ): + require(collision, token, "collision selected-proc cache key") + + image = read(ROOT, "src/renderer/ImageManager.cpp") + sound = read(ROOT, "src/sound/snd_system.cpp") + sp_anim = read(GAME_ROOT, "src/game/anim/Anim.cpp") + require(image, "LEVEL_LOAD_RESOURCE_IMAGE", "image semantic manifest hook") + require(sound, "LEVEL_LOAD_RESOURCE_SOUND", "sound semantic manifest hook") + require(sp_anim, "LEVEL_LOAD_RESOURCE_ANIMATION", "animation semantic manifest hook") + require(sp_anim, "GENERATED_ANIM_VERSION = 3", "hardened animation cache v3") + require(sp_anim, "OpenExplicitFileRead", "private animation cache read") + require(sp_anim, "PromoteFile", "atomic animation cache write") + + +def main() -> None: + validate_abi_contract() + validate_manifest_and_envelope_contract() + validate_pipeline_and_lifecycle() + validate_resource_consumers() + print("level_load_cache: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/linux_arm64_ci_coverage.py b/tools/tests/linux_arm64_ci_coverage.py index 6fad1035..16614a20 100644 --- a/tools/tests/linux_arm64_ci_coverage.py +++ b/tools/tests/linux_arm64_ci_coverage.py @@ -66,7 +66,11 @@ def validate_push_workflow() -> None: require(source, "libgl1-mesa-dri", "push verification software GL runtime dependency") require(source, "libglx-mesa0", "push verification GLX runtime dependency") require(source, "xvfb-run -a bash tools/validation/validate_push.sh", "push verification runtime smoke") - require(source, 'runtime_cases="renderer-default-safety-selftest"', "push verification runtime smoke") + require( + source, + 'runtime_cases="renderer-foundation-selftests,renderer-pbr-visible-selftest,renderer-cluster-grid-selftest,renderer-default-safety-selftest"', + "push verification runtime smoke", + ) require(source, '--runtime-cases "${runtime_cases}"', "push verification runtime smoke") require(source, "sdl3-x11-display-diagnostics", "push verification X11 display diagnostics case") require(source, "OPENQ4_FORCE_X11=1 xvfb-run -a python tools/tests/renderer_validation_matrix.py", "push verification OPENQ4_FORCE_X11 runtime smoke") @@ -90,7 +94,11 @@ def validate_commit_workflow() -> None: require(source, "libgl1-mesa-dri", "commit validation software GL runtime dependency") require(source, "libglx-mesa0", "commit validation GLX runtime dependency") require(source, "xvfb-run -a bash tools/validation/validate_pr.sh", "commit validation runtime smoke") - require(source, "--runtime-cases renderer-default-safety-selftest", "commit validation runtime smoke") + require( + source, + "--runtime-cases renderer-foundation-selftests,renderer-pbr-visible-selftest,renderer-cluster-grid-selftest,renderer-default-safety-selftest", + "commit validation runtime smoke", + ) require(source, "sdl3-x11-display-diagnostics", "commit validation X11 display diagnostics case") require(source, "OPENQ4_FORCE_X11=1 xvfb-run -a python tools/tests/renderer_validation_matrix.py", "commit validation OPENQ4_FORCE_X11 runtime smoke") require(source, "--cases sdl3-force-x11-display-diagnostics", "commit validation OPENQ4_FORCE_X11 runtime smoke") diff --git a/tools/tests/loading_pacifier_pacing.py b/tools/tests/loading_pacifier_pacing.py index d065eab3..aa75ec9a 100644 --- a/tools/tests/loading_pacifier_pacing.py +++ b/tools/tests/loading_pacifier_pacing.py @@ -38,6 +38,9 @@ def refuse(haystack: str, needle: str, context: str) -> None: def function_body(source: str, signature: str) -> str: start = source.find(signature) + if start == -1: + method = signature.split("(", 1)[0].split()[-1] + start = source.find(f"idFileSystemLocal::{method}(") if start == -1: raise AssertionError(f"Missing function signature {signature!r}") @@ -120,11 +123,15 @@ def validate_read_count_granularity() -> None: require(body, "READCOUNT_PACIFIER_INTERVAL_BYTES", "AddToReadCount pacifier granularity") require(body, "readCountPacifierBytes", "AddToReadCount byte accumulator") - require_order(body, "readCount += c;", "session->PacifierUpdate();", + require(body, "std::lock_guard lock( readCountMutex );", + "AddToReadCount worker-safe accounting") + require(body, "std::this_thread::get_id() == fileSystemMainThread", + "AddToReadCount main-thread presentation") + require_order(body, "readCount =", "session->PacifierUpdate();", "AddToReadCount progress accounting before pacifier offer") # Progress accuracy must not regress: readCount still advances every chunk. - require(body, "readCount += c;", "AddToReadCount progress accounting") + require(body, "readCount =", "AddToReadCount progress accounting") # The accumulator has to be cleared with the read count or the first load after a # reset offers at the wrong point. diff --git a/tools/tests/macos_intel_ci.py b/tools/tests/macos_intel_ci.py index fdd47041..c3445039 100644 --- a/tools/tests/macos_intel_ci.py +++ b/tools/tests/macos_intel_ci.py @@ -85,7 +85,7 @@ def validate_engine_corridor() -> None: "os: macos-15-intel", "artifact_name: macos-x64-opengl", "artifact_name: macos-x64-metal", - 'runtime_cases="renderer-default-safety-selftest"', + 'runtime_cases="renderer-foundation-selftests,renderer-pbr-visible-selftest,renderer-cluster-grid-selftest,renderer-default-safety-selftest"', "python tools/tests/macos_dedicated_server_smoke.py", "push-${{ matrix.artifact_name }}-dedicated-smoke", ): diff --git a/tools/tests/macos_renderer_startup_guard.py b/tools/tests/macos_renderer_startup_guard.py index a9efad36..bfa0e1a7 100644 --- a/tools/tests/macos_renderer_startup_guard.py +++ b/tools/tests/macos_renderer_startup_guard.py @@ -374,14 +374,26 @@ def validate_apple_gl21_simple_interaction_fallback() -> None: ): require(arb2, token, "Apple GL 2.1 simple interaction fallback") - force_ambient = function_body(common, "static void RB_STD_ForceAmbient( void ) {") + force_ambient_value = function_body( + common, "static float RB_STD_ForceAmbientValue( void ) {" + ) for token in ( "glConfig.preferSimpleInteraction", "glConfig.disableARB2Interactions", "VPROG_SIMPLE_INTERACTION", "FPROG_SIMPLE_INTERACTION", ): - require(force_ambient, token, "force ambient interaction rescue family selection") + require( + force_ambient_value, + token, + "force ambient interaction rescue family selection", + ) + force_ambient = function_body(common, "static void RB_STD_ForceAmbient( void ) {") + require( + force_ambient, + "RB_STD_ForceAmbientValue()", + "force ambient shared rescue value", + ) require( gfx_info, diff --git a/tools/tests/macos_sdl3_backend_guard.py b/tools/tests/macos_sdl3_backend_guard.py index e4919d50..d1132b94 100644 --- a/tools/tests/macos_sdl3_backend_guard.py +++ b/tools/tests/macos_sdl3_backend_guard.py @@ -85,7 +85,6 @@ def validate_macos_process_handoff_guards() -> None: filtered_environment = function_body(source, "static char **OSX_CreateFilteredProcessEnvironment() {") do_start_process = function_body(source, "void Sys_DoStartProcess( const char *exeName, bool dofork ) {") allowed_url = function_body(source, "static bool OSX_IsAllowedURL( NSURL *url ) {") - file_url = function_body(source, "static bool OSX_FileURLIsLocalRuntimeFile( NSURL *url ) {") open_url = function_body(source, "void idSysLocal::OpenURL( const char *url, bool doexit ) {") require(source, "static bool OSX_IsAbsolutePath( const char *path )", "macOS process handoff absolute-path helper") @@ -113,14 +112,14 @@ def validate_macos_process_handoff_guards() -> None: require(do_start_process, 'common->Printf( "Sys_DoStartProcess: invalid command line\\n" );', "macOS invalid process command diagnostic") reject(do_start_process, 'invalid command line \'%s\'', "macOS invalid process command diagnostic") - require(source, "MAX_OSX_URL_LENGTH", "macOS URL length guard") - require(open_url, "OSX_URLHasSafeSchemeSyntax( url )", "macOS URL syntax guard") - require(open_url, "OpenURL rejected: expected a bounded URL with a safe scheme", "macOS unsafe URL diagnostic") + require(source, '#include "../URLPolicy.h"', "shared macOS URL policy") + require(open_url, "idURLPolicy::IsAllowedHTTPURL( url )", "macOS HTTP(S)-only URL guard") + require(open_url, "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host", "macOS unsafe URL diagnostic") require(open_url, "OpenURL rejected: URL is not valid UTF-8", "macOS unsafe URL diagnostic") require(open_url, "OpenURL rejected: Foundation could not parse URL", "macOS unsafe URL diagnostic") - require(open_url, "OpenURL rejected: scheme is not allowed", "macOS unsafe URL diagnostic") + require(open_url, "OpenURL rejected: Foundation did not preserve the required HTTP(S) host", "macOS unsafe URL diagnostic") require(open_url, "OpenURL failed after validation", "macOS URL handoff diagnostic") - require_before(open_url, "OSX_URLHasSafeSchemeSyntax( url )", 'NSString *urlString = [ NSString stringWithUTF8String: url ];', "macOS URL syntax before UTF-8 bridge") + require_before(open_url, "idURLPolicy::IsAllowedHTTPURL( url )", 'NSString *urlString = [ NSString stringWithUTF8String: url ];', "macOS URL policy before UTF-8 bridge") require_before(open_url, "OSX_IsAllowedURL( nsURL )", 'common->Printf( "Open URL: %s\\n", url );', "macOS URL logging after allowlist guard") require_before(open_url, 'common->Printf( "Open URL: %s\\n", url );', "openURL: nsURL", "macOS approved URL logging before AppKit handoff") require(open_url, "OSX_IsAllowedURL( nsURL )", "macOS URL allowlist guard") @@ -129,15 +128,9 @@ def validate_macos_process_handoff_guards() -> None: require(allowed_url, '[scheme caseInsensitiveCompare:@"https"]', "macOS URL allowlist") require(allowed_url, '[scheme caseInsensitiveCompare:@"http"]', "macOS URL allowlist") require(allowed_url, "return host != nil && [host length] > 0;", "macOS HTTP URL host guard") - require(allowed_url, '[scheme caseInsensitiveCompare:@"file"]', "macOS URL allowlist") - require(file_url, "[url isFileURL]", "macOS file URL local-file guard") - require(file_url, "OSX_StringHasControlCharacters( path )", "macOS file URL local-file guard") - require(file_url, "OSX_IsAbsolutePath( path )", "macOS file URL local-file guard") - require(file_url, "OSX_IsRegularFile( path )", "macOS file URL local-file guard") - require(file_url, 'GetCVarString( "fs_savepath" )', "macOS file URL runtime-root guard") - require(file_url, 'GetCVarString( "fs_basepath" )', "macOS file URL runtime-root guard") - require(file_url, "OSX_ResolvedPathIsUnderDirectory( path, savePath )", "macOS file URL runtime-root guard") - reject(source, "static bool OSX_IsSafeURL", "macOS URL broad scheme guard") + reject(allowed_url, '[scheme caseInsensitiveCompare:@"file"]', "macOS file URL policy") + reject(source, "OSX_FileURLIsLocalRuntimeFile", "macOS file URL launcher") + reject(source, "OSX_URLHasSafeSchemeSyntax", "macOS broad URL syntax-only policy") def validate_sdl3_context_teardown_guards() -> None: diff --git a/tools/tests/macos_static_policy.py b/tools/tests/macos_static_policy.py index acc50b1a..cb890444 100644 --- a/tools/tests/macos_static_policy.py +++ b/tools/tests/macos_static_policy.py @@ -139,15 +139,15 @@ def validate_url_open_policy() -> None: misc = read("src/sys/osx/macosx_misc.mm") open_url = function_body(misc, "void idSysLocal::OpenURL( const char *url, bool doexit ) {") allowed_url = function_body(misc, "static bool OSX_IsAllowedURL( NSURL *url ) {") - file_url = function_body(misc, "static bool OSX_FileURLIsLocalRuntimeFile( NSURL *url ) {") - require(misc, "MAX_OSX_URL_LENGTH", "macOS URL length guard") - require(open_url, "OpenURL rejected: expected a bounded URL with a safe scheme", "macOS unsafe URL diagnostic") + require(misc, '#include "../URLPolicy.h"', "shared macOS URL policy") + require(open_url, "idURLPolicy::IsAllowedHTTPURL( url )", "macOS HTTP(S)-only URL guard") + require(open_url, "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host", "macOS unsafe URL diagnostic") require(open_url, "OpenURL rejected: URL is not valid UTF-8", "macOS unsafe URL diagnostic") require(open_url, "OpenURL rejected: Foundation could not parse URL", "macOS unsafe URL diagnostic") - require(open_url, "OpenURL rejected: scheme is not allowed", "macOS unsafe URL diagnostic") + require(open_url, "OpenURL rejected: Foundation did not preserve the required HTTP(S) host", "macOS unsafe URL diagnostic") require(open_url, "OpenURL failed after validation", "macOS URL handoff diagnostic") - require_before(open_url, "OSX_URLHasSafeSchemeSyntax( url )", 'NSString *urlString = [ NSString stringWithUTF8String: url ];', "macOS URL syntax before UTF-8 bridge") + require_before(open_url, "idURLPolicy::IsAllowedHTTPURL( url )", 'NSString *urlString = [ NSString stringWithUTF8String: url ];', "macOS URL policy before UTF-8 bridge") require_before(open_url, "OSX_IsAllowedURL( nsURL )", 'common->Printf( "Open URL: %s\\n", url );', "macOS URL logging after allowlist guard") require_before(open_url, 'common->Printf( "Open URL: %s\\n", url );', "openURL: nsURL", "macOS approved URL logging before AppKit handoff") require_before(open_url, "OSX_IsAllowedURL( nsURL )", "openURL: nsURL", "macOS URL allowlist before AppKit handoff") @@ -156,12 +156,9 @@ def validate_url_open_policy() -> None: require(allowed_url, '[scheme caseInsensitiveCompare:@"https"]', "macOS URL scheme allowlist") require(allowed_url, '[scheme caseInsensitiveCompare:@"http"]', "macOS URL scheme allowlist") require(allowed_url, "return host != nil && [host length] > 0;", "macOS HTTP URL host guard") - require(allowed_url, '[scheme caseInsensitiveCompare:@"file"]', "macOS URL scheme allowlist") - require(file_url, "OSX_StringHasControlCharacters( path )", "macOS file URL path control-character guard") - require(file_url, 'GetCVarString( "fs_savepath" )', "macOS file URL runtime-root policy") - require(file_url, 'GetCVarString( "fs_basepath" )', "macOS file URL runtime-root policy") - require(file_url, "OSX_ResolvedPathIsUnderDirectory( path, savePath )", "macOS file URL runtime-root policy") - reject(misc, "static bool OSX_IsSafeURL", "macOS broad URL syntax-only policy") + reject(allowed_url, '[scheme caseInsensitiveCompare:@"file"]', "macOS file URL policy") + reject(misc, "OSX_FileURLIsLocalRuntimeFile", "macOS file URL launcher") + reject(misc, "OSX_URLHasSafeSchemeSyntax", "macOS broad URL syntax-only policy") def validate_deprecated_api_boundaries() -> None: diff --git a/tools/tests/mp_bot_navigation.py b/tools/tests/mp_bot_navigation.py index f703c480..c3ec8160 100644 --- a/tools/tests/mp_bot_navigation.py +++ b/tools/tests/mp_bot_navigation.py @@ -56,6 +56,17 @@ def require_order(haystack: str, first: str, second: str, context: str) -> None: raise AssertionError(f"{first!r} must appear before {second!r} in {context}") +def cvar_signature(source: str, name: str, context: str) -> tuple[str, set[str]]: + match = re.search( + rf'idCVar\s+\w+\(\s*"{re.escape(name)}"\s*,\s*"([^"]*)"\s*,\s*([^,]+),', + source, + re.IGNORECASE, + ) + if match is None: + raise AssertionError(f"Missing CVar declaration for {name!r} in {context}") + return match.group(1), {flag.strip() for flag in match.group(2).split("|")} + + def validate_engine() -> None: source = read(ROOT / "src" / "framework" / "async" / "AsyncServer.cpp") @@ -497,6 +508,7 @@ def validate_cvars() -> None: declared = read(mp / "gamesys" / "SysCvar.cpp") exported = read(mp / "gamesys" / "SysCvar.h") + sp_bots = read(GAME_LIBS_ROOT / "src" / "game" / "bots" / "Bot.cpp") for name in ( "bot_enable", @@ -510,6 +522,17 @@ def validate_cvars() -> None: require(declared, f'idCVar {name}(', "mpgame SysCvar.cpp") require(exported, f"extern idCVar {name};", "mpgame SysCvar.h") + # These legacy SP bot diagnostics share global names with the richer MP + # diagnostics. Keep their declaration signatures aligned so loading SP + # before MP cannot leave the CVar system with conflicting types or policy. + for name in ("bot_debug", "bot_debugnav"): + sp_signature = cvar_signature(sp_bots, name, "game bots/Bot.cpp") + mp_signature = cvar_signature(declared, name, "mpgame SysCvar.cpp") + if sp_signature != mp_signature: + raise AssertionError(f"SP/MP bot CVar signatures differ for {name!r}") + if "CVAR_INTEGER" not in sp_signature[1] or "CVAR_BOOL" in sp_signature[1]: + raise AssertionError(f"{name} must retain the shared integer diagnostic type") + commands = read(mp / "gamesys" / "SysCmds.cpp") for command in ("addbot", "removebot", "kickbots", "botlist", "navmesh"): require(commands, f'"{command}"', "mpgame console commands") diff --git a/tools/tests/multiview_demo.py b/tools/tests/multiview_demo.py index 0f7cd696..3621c86e 100644 --- a/tools/tests/multiview_demo.py +++ b/tools/tests/multiview_demo.py @@ -64,6 +64,7 @@ def validate_legacy_game_api_compatibility(source: str) -> None: for token, context in ( ("MVD_LEGACY_GAME_API_1_0_1_1 = 39u", "retail-compatible MVD API"), ("MVD_LEGACY_GAME_API_PRE_SHUTDOWN_SPLIT = 42u", "pre-shutdown-split MVD API"), + ("MVD_LEGACY_GAME_API_PRE_PRESENTATION_STATE = 44u", "pre-presentation-state MVD API"), ): require(source, token, context) legacy_compatibility = between( @@ -75,6 +76,7 @@ def validate_legacy_game_api_compatibility(source: str) -> None: for token in ( "version == MVD_LEGACY_GAME_API_1_0_1_1", "version == MVD_LEGACY_GAME_API_PRE_SHUTDOWN_SPLIT", + "version == MVD_LEGACY_GAME_API_PRE_PRESENTATION_STATE", "version == static_cast( GAME_API_VERSION )", ): require(legacy_compatibility, token, "known-compatible MVD 1.0/1.1 API allowlist") @@ -211,6 +213,14 @@ def validate_container() -> None: source.replace(api42_allow, "", 1), "legacy MVDs recorded by game API 42 are rejected after the shutdown-only API bump", ) + api44_allow = "\t\tversion == MVD_LEGACY_GAME_API_PRE_PRESENTATION_STATE ||\n" + if source.count(api44_allow) != 1: + raise AssertionError("MVD API-44 compatibility mutation anchor is not unique") + expect_contract_rejection( + validate_legacy_game_api_compatibility, + source.replace(api44_allow, "", 1), + "legacy MVDs recorded by game API 44 are rejected after the presentation-only API bump", + ) require_order( source, diff --git a/tools/tests/mvd_server_api_contract.py b/tools/tests/mvd_server_api_contract.py index 17fd29ea..36795881 100644 --- a/tools/tests/mvd_server_api_contract.py +++ b/tools/tests/mvd_server_api_contract.py @@ -93,7 +93,7 @@ def main() -> None: "// 42: durable server MVD publication results for competitive match evidence", "append-only MVD result ABI history", ) - require(game_api_h, "GAME_API_VERSION\t\t= 43", "current game-module ABI version") + require(game_api_h, "GAME_API_VERSION\t\t= 45", "current game-module ABI version") forwarding = ( "multiViewDemo.StartNamedRecording( name )", diff --git a/tools/tests/native/AdvancedLightingCoreTest.cpp b/tools/tests/native/AdvancedLightingCoreTest.cpp new file mode 100644 index 00000000..5ee5cb5c --- /dev/null +++ b/tools/tests/native/AdvancedLightingCoreTest.cpp @@ -0,0 +1,156 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/AdvancedLightingCore.h" + +#include +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "AdvancedLightingCoreTest: %s\n", message ); + failures++; +} + +static advancedLightingVolume_t Volume( std::uint32_t generation, int stableId ) { + advancedLightingVolume_t volume = {}; + volume.origin[0] = 1.0f; + volume.origin[1] = 2.0f; + volume.origin[2] = 3.0f; + volume.extents[0] = 64.0f; + volume.extents[1] = 32.0f; + volume.extents[2] = 16.0f; + volume.generation = generation; + volume.stableId = stableId; + return volume; +} + +int main() { + advancedLightingTransaction_t transaction; + AdvancedLightingCore_Begin( transaction, true, 7, 2, 1, 4 ); + Check( AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_PROBE, Volume( 7, 11 ) ), + "a current finite probe must be admitted" ); + Check( AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_DECAL, Volume( 7, 21 ) ), + "a current finite decal must be admitted" ); + Check( AdvancedLightingCore_AddReference( transaction, + ADVANCED_LIGHTING_RECORD_PROBE ) + && AdvancedLightingCore_AddReference( transaction, + ADVANCED_LIGHTING_RECORD_DECAL ), + "bounded probe/decal references must be admitted" ); + Check( AdvancedLightingCore_Seal( transaction, true ) + && transaction.complete && transaction.sealed, + "a lossless resource-complete transaction must seal" ); + Check( !AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_PROBE, Volume( 7, 12 ) ), + "sealed transactions must be immutable" ); + + AdvancedLightingCore_Begin( transaction, true, 9, 1, 1, 1 ); + Check( !AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_PROBE, Volume( 8, 1 ) ) + && transaction.firstReject == ADVANCED_LIGHTING_REJECT_INVALID_GENERATION, + "a stale generation must poison the complete transaction" ); + advancedLightingVolume_t malformed = Volume( 9, 2 ); + malformed.extents[1] = std::numeric_limits::quiet_NaN(); + Check( !AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_DECAL, malformed ), + "non-finite volumes must fail closed" ); + Check( !AdvancedLightingCore_Seal( transaction, true ) + && !transaction.complete, + "a rejected record must prevent partial publication" ); + + AdvancedLightingCore_Begin( transaction, true, 10, 1, 1, 1 ); + Check( !AdvancedLightingCore_Admit( transaction, + static_cast( 99 ), Volume( 10, 1 ) ) + && transaction.firstReject == ADVANCED_LIGHTING_REJECT_INVALID_KIND, + "an unknown record kind must fail closed" ); + + AdvancedLightingCore_Begin( transaction, true, 3, 1, 1, 1 ); + Check( AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_PROBE, Volume( 3, 1 ) ) + && !AdvancedLightingCore_Admit( transaction, + ADVANCED_LIGHTING_RECORD_PROBE, Volume( 3, 2 ) ) + && transaction.firstReject == ADVANCED_LIGHTING_REJECT_CAPACITY, + "record overflow must be explicit and atomic" ); + Check( AdvancedLightingCore_AddReference( transaction, + ADVANCED_LIGHTING_RECORD_PROBE ) + && !AdvancedLightingCore_AddReference( transaction, + ADVANCED_LIGHTING_RECORD_DECAL ), + "reference overflow must be bounded" ); + + advancedLightingProbeCandidate_t selected[ADVANCED_LIGHTING_PROBES_PER_CLUSTER] = {}; + for ( int i = 0; i < ADVANCED_LIGHTING_PROBES_PER_CLUSTER; ++i ) { + selected[i].probeIndex = -1; + selected[i].stableId = -1; + } + const advancedLightingProbeCandidate_t candidates[] = { + { 0, 2, 20, 0.8f }, + { 1, 3, 40, 0.2f }, + { 2, 3, 10, 0.9f }, + { 3, 3, 5, 0.9f } + }; + for ( unsigned int i = 0; i < sizeof( candidates ) / sizeof( candidates[0] ); ++i ) { + AdvancedLightingCore_SelectProbe( selected, candidates[i] ); + } + Check( selected[0].probeIndex == 3 && selected[1].probeIndex == 2, + "priority, weight, and stable identity must order probe selection deterministically" ); + + for ( int i = 0; i < ADVANCED_LIGHTING_PROBES_PER_CLUSTER; ++i ) { + selected[i].probeIndex = -1; + selected[i].stableId = -1; + } + const advancedLightingProbeCandidate_t conservativeEdge = { 4, -1, 1, 0.000256f }; + const advancedLightingProbeCandidate_t positiveLowPriority = { 5, 0, 2, 0.1f }; + const advancedLightingProbeCandidate_t positiveSecond = { 6, 0, 3, 0.05f }; + AdvancedLightingCore_SelectProbe( selected, conservativeEdge ); + AdvancedLightingCore_SelectProbe( selected, positiveLowPriority ); + AdvancedLightingCore_SelectProbe( selected, positiveSecond ); + Check( selected[0].probeIndex == 5 && selected[1].probeIndex == 6, + "conservative edge references must not crowd positive probe contributors" ); + + const advancedLightingVolume_t volume = Volume( 1, 1 ); + const float center[3] = { 1.0f, 2.0f, 3.0f }; + const float edge[3] = { 65.0f, 2.0f, 3.0f }; + const float blend[3] = { 49.0f, 2.0f, 3.0f }; + Check( AdvancedLightingCore_ProbeWeight( volume, center, 16.0f ) == 1.0f, + "probe center must have full weight" ); + Check( AdvancedLightingCore_ProbeWeight( volume, edge, 16.0f ) == 0.0f, + "probe boundary must have zero weight" ); + Check( AdvancedLightingCore_ProbeWeight( volume, blend, 32.0f ) > 0.0f + && AdvancedLightingCore_ProbeWeight( volume, blend, 32.0f ) < 1.0f, + "probe blend band must be continuous" ); + + const float sphereOrigin[3] = { 0.0f, 0.0f, 0.0f }; + const float sphereCenter[3] = { 0.0f, 0.0f, 0.0f }; + const float sphereBlend[3] = { 7.0f, 0.0f, 0.0f }; + const float sphereEdge[3] = { 8.0f, 0.0f, 0.0f }; + Check( AdvancedLightingCore_SphericalProbeWeight( + sphereOrigin, 8.0f, sphereCenter, 2.0f ) == 1.0f, + "spherical probe center must have full weight" ); + Check( AdvancedLightingCore_SphericalProbeWeight( + sphereOrigin, 8.0f, sphereBlend, 2.0f ) == 0.5f, + "spherical probe shell must preserve its authored blend width" ); + Check( AdvancedLightingCore_SphericalProbeWeight( + sphereOrigin, 8.0f, sphereEdge, 2.0f ) == 0.0f, + "spherical probe boundary must fail closed" ); + Check( AdvancedLightingCore_SphericalProbeWeight( + sphereOrigin, 8.0f, sphereCenter, + std::numeric_limits::quiet_NaN() ) == 0.0f, + "malformed spherical blend distance must fail closed" ); + + AdvancedLightingCore_Begin( transaction, false, 1, 1, 1, 1 ); + Check( !AdvancedLightingCore_Seal( transaction, true ) + && transaction.firstReject == ADVANCED_LIGHTING_REJECT_DISABLED, + "the default-off master gate must publish no records" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "AdvancedLightingCoreTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/AdvancedScreenSpaceCoreTest.cpp b/tools/tests/native/AdvancedScreenSpaceCoreTest.cpp new file mode 100644 index 00000000..4a8b9072 --- /dev/null +++ b/tools/tests/native/AdvancedScreenSpaceCoreTest.cpp @@ -0,0 +1,78 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/AdvancedScreenSpaceCore.h" + +#include +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "AdvancedScreenSpaceCoreTest: %s\n", message ); + failures++; +} + +int main() { + advancedScreenSpaceConfig_t config = AdvancedScreenSpaceCore_Build( + true, true, true, true, 0.0005f, 4096.0f, 12, + 0.4f, 768.0f, 10, 0.3f ); + Check( config.effectMask == 7u, + "independent leaves must publish one combined bounded mask" ); + Check( config.froxelSlices == 12 && config.ssrSteps == 10, + "valid work bounds must be retained" ); + Check( AdvancedScreenSpaceCore_Requested( config ), + "a non-empty feature mask must request the shared presentation tail" ); + + float packed[8] = {}; + AdvancedScreenSpaceCore_Pack( config, packed ); + Check( packed[0] == 7.0f && packed[1] == 0.0005f + && packed[2] == 4096.0f && packed[3] == 12.0f + && packed[4] == 0.4f && packed[5] == 768.0f + && packed[6] == 10.0f && packed[7] == 0.3f, + "the GL/Vulkan eight-float packet must be stable" ); + + config = AdvancedScreenSpaceCore_Build( + true, true, true, true, + std::numeric_limits::quiet_NaN(), 1.0e9f, 999, + std::numeric_limits::infinity(), -4.0f, -1, + std::numeric_limits::quiet_NaN() ); + Check( config.froxelDensity == 0.00012f + && config.froxelMaxDistance == 8192.0f + && config.froxelSlices == ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES, + "froxel tuning must reject non-finite input and remain bounded" ); + Check( config.ssrIntensity == 0.35f && config.ssrMaxDistance == 32.0f + && config.ssrSteps == 4 + && config.ssrSteps <= ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS + && config.ssgiIntensity == 0.25f, + "SSR and SSGI tuning must reject malformed input and remain bounded" ); + + config = AdvancedScreenSpaceCore_Build( + false, true, true, true, 0.01f, 8192.0f, 16, + 1.0f, 2048.0f, 16, 1.0f ); + AdvancedScreenSpaceCore_Pack( config, packed ); + Check( config.effectMask == 0u && !AdvancedScreenSpaceCore_Requested( config ), + "the master rollback must publish no screen-space work" ); + Check( packed[0] == 0.0f && packed[1] == 0.0f && packed[7] == 0.0f, + "the master rollback packet must be exactly zero-initialized" ); + + config = AdvancedScreenSpaceCore_Build( + true, false, false, false, 0.01f, 8192.0f, 16, + 1.0f, 2048.0f, 16, 1.0f ); + AdvancedScreenSpaceCore_Pack( config, packed ); + bool allZero = true; + for ( int i = 0; i < 8; ++i ) { + allZero = allZero && packed[i] == 0.0f; + } + Check( allZero, + "disabled leaves must ignore dormant tuning and publish a canonical zero packet" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "AdvancedScreenSpaceCoreTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/CoreSafetyTest.cpp b/tools/tests/native/CoreSafetyTest.cpp index 68ce63d0..260eb95e 100644 --- a/tools/tests/native/CoreSafetyTest.cpp +++ b/tools/tests/native/CoreSafetyTest.cpp @@ -1,9 +1,17 @@ #include "src/idlib/NumericString.h" #include "src/idlib/StrAllocation.h" +#include "src/idlib/CryptoHash.h" +#include "src/idlib/PrivateCommand.h" +#include "src/framework/GameDirPolicy.h" +#include "src/framework/RemoteCVarPolicy.h" +#include "src/framework/async/Rcon2Protocol.h" #include "src/sys/NetworkEndpoint.h" +#include "src/sys/URLPolicy.h" #include +#include #include +#include #include static int failures = 0; @@ -42,6 +50,165 @@ static void ExpectAllocation( const bool condition, const char *label ) { } } +static void ExpectPrivateToken( const char *command, const char *name, + const bool expected, const char *label ) { + const bool actual = idPrivateCommand::ContainsBoundedCaseInsensitiveToken( command, name ); + if ( actual != expected ) { + std::fprintf( stderr, "Private command-token match failed for %s: expected %d, got %d\n", + label, expected, actual ); + failures++; + } +} + +static int HexNibble( const char value ) { + if ( value >= '0' && value <= '9' ) { + return value - '0'; + } + if ( value >= 'a' && value <= 'f' ) { + return value - 'a' + 10; + } + if ( value >= 'A' && value <= 'F' ) { + return value - 'A' + 10; + } + return -1; +} + +static void ExpectCryptoBytes( const std::uint8_t *actual, const std::size_t bytes, + const char *expectedHex, const char *label ) { + if ( std::strlen( expectedHex ) != bytes * 2 ) { + std::fprintf( stderr, "Invalid expected crypto vector for %s\n", label ); + failures++; + return; + } + for ( std::size_t index = 0; index < bytes; ++index ) { + const int high = HexNibble( expectedHex[ index * 2 ] ); + const int low = HexNibble( expectedHex[ index * 2 + 1 ] ); + if ( high < 0 || low < 0 || actual[ index ] != static_cast( ( high << 4 ) | low ) ) { + std::fprintf( stderr, "Crypto vector failed for %s at byte %zu\n", label, index ); + failures++; + return; + } + } +} + +static void ExerciseCryptoVectors() { + std::uint8_t digest[idCrypto::SHA256_DIGEST_BYTES]; + idCrypto::SHA256( nullptr, 0, digest ); + ExpectCryptoBytes( digest, sizeof( digest ), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA-256 empty" ); + idCrypto::SHA256( "abc", 3, digest ); + ExpectCryptoBytes( digest, sizeof( digest ), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "SHA-256 abc" ); + + std::uint8_t hmacKey[20]; + std::memset( hmacKey, 0x0b, sizeof( hmacKey ) ); + idCrypto::HMACSHA256( hmacKey, sizeof( hmacKey ), "Hi There", 8, digest ); + ExpectCryptoBytes( digest, sizeof( digest ), + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", "RFC 4231 HMAC-SHA-256" ); + + const char password[] = "password"; + const char salt[] = "salt"; + if ( !idCrypto::PBKDF2HMACSHA256( password, 8, salt, 4, 1, digest, sizeof( digest ) ) ) { + std::fprintf( stderr, "PBKDF2 iteration-1 vector rejected\n" ); + failures++; + } else { + ExpectCryptoBytes( digest, sizeof( digest ), + "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b", "PBKDF2-HMAC-SHA-256 c=1" ); + } + if ( !idCrypto::PBKDF2HMACSHA256( password, 8, salt, 4, 2, digest, sizeof( digest ) ) ) { + std::fprintf( stderr, "PBKDF2 iteration-2 vector rejected\n" ); + failures++; + } else { + ExpectCryptoBytes( digest, sizeof( digest ), + "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43", "PBKDF2-HMAC-SHA-256 c=2" ); + } + if ( !idCrypto::PBKDF2HMACSHA256( password, 8, salt, 4, 4096, digest, sizeof( digest ) ) ) { + std::fprintf( stderr, "PBKDF2 iteration-4096 vector rejected\n" ); + failures++; + } else { + ExpectCryptoBytes( digest, sizeof( digest ), + "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a", "PBKDF2-HMAC-SHA-256 c=4096" ); + } + if ( idCrypto::PBKDF2HMACSHA256( password, 8, salt, 4, 0, digest, sizeof( digest ) ) ) { + std::fprintf( stderr, "PBKDF2 accepted zero iterations\n" ); + failures++; + } + + std::uint8_t verifier[idRcon2::VERIFIER_BYTES]; + std::uint8_t clientNonce[idRcon2::NONCE_BYTES]; + std::uint8_t serverNonce[idRcon2::NONCE_BYTES]; + std::uint8_t endpointBinding[idRcon2::ENDPOINT_BINDING_BYTES]; + std::uint8_t requestDigest[idRcon2::REQUEST_DIGEST_BYTES]; + std::uint8_t proof[idRcon2::PROOF_BYTES]; + for ( std::size_t index = 0; index < sizeof( verifier ); ++index ) { + verifier[index] = static_cast( index ); + } + for ( std::size_t index = 0; index < sizeof( clientNonce ); ++index ) { + clientNonce[index] = static_cast( index ); + serverNonce[index] = static_cast( index + 16 ); + endpointBinding[index] = static_cast( index + 32 ); + } + for ( std::size_t index = 0; index < sizeof( requestDigest ); ++index ) { + requestDigest[index] = static_cast( index + 48 ); + } + idRcon2::ComputeProof( verifier, clientNonce, serverNonce, endpointBinding, requestDigest, proof ); + ExpectCryptoBytes( proof, sizeof( proof ), + "b5a5953f5327179b21f82fb5fe6a158896c82df1126feb48b0c8ce96a2f65c20", "rcon2 proof domain" ); + idRcon2::HashRequest( "status", requestDigest ); + ExpectCryptoBytes( requestDigest, sizeof( requestDigest ), + "073c1634c496cdb649d1afe0a312bbb4b7e1741b271542e4a436c3b8824b1761", "rcon2 request digest" ); + + std::uint8_t different[idCrypto::SHA256_DIGEST_BYTES]; + std::memcpy( different, digest, sizeof( different ) ); + different[31] ^= 1; + if ( !idCrypto::ConstantTimeEquals( digest, digest, sizeof( digest ) ) || + idCrypto::ConstantTimeEquals( digest, different, sizeof( digest ) ) ) { + std::fprintf( stderr, "Constant-time equality result mismatch\n" ); + failures++; + } + idCrypto::SecureZero( different, sizeof( different ) ); + for ( std::uint8_t value : different ) { + if ( value != 0 ) { + std::fprintf( stderr, "SecureZero left data behind\n" ); + failures++; + break; + } + } +} + +static void ExpectOpenURLPolicy( const char *url, const bool expected, const char *label ) { + const bool actual = idURLPolicy::IsAllowedHTTPURL( url ); + if ( actual != expected ) { + std::fprintf( stderr, "Open URL policy failed for %s: expected %d, got %d\n", label, expected, actual ); + failures++; + } +} + +static void ExpectRemoteCVarPolicy( const int variableFlags, const int requiredFlag, + const bool expected, const char *label ) { + const int userInfo = 1 << 0; + const int serverInfo = 1 << 1; + const int networkSync = 1 << 2; + const int privateFlag = 1 << 3; + const int allowed = userInfo | serverInfo | networkSync; + const bool actual = idRemoteCVarPolicy::CanApply( + variableFlags, requiredFlag, allowed, privateFlag ); + if ( actual != expected ) { + std::fprintf( stderr, "Remote CVar policy failed for %s: expected %d, got %d\n", + label, expected, actual ); + failures++; + } +} + +static void ExpectGameDirSegment( const char *segment, const bool expected, const char *label ) { + const bool actual = idGameDirPolicy::IsPortableSegment( segment ); + if ( actual != expected ) { + std::fprintf( stderr, "Game-directory policy failed for %s: expected %d, got %d\n", + label, expected, actual ); + failures++; + } +} + static void ExpectEndpoint( const char *text, const bool expected, const char *expectedHost, const bool expectedHasPort, const unsigned short expectedPort, const char *label ) { char host[256] = "untouched"; @@ -164,6 +331,36 @@ static void ExpectBindPlan( const char *ipv4Text, const char *ipv6Text, const bo } int main() { + ExerciseCryptoVectors(); + const int remoteUserInfo = 1 << 0; + const int remoteServerInfo = 1 << 1; + const int remoteNetworkSync = 1 << 2; + const int remotePrivate = 1 << 3; + const int localInit = 1 << 4; + ExpectRemoteCVarPolicy( remoteUserInfo, remoteUserInfo, true, "matching userinfo authority" ); + ExpectRemoteCVarPolicy( remoteServerInfo, remoteServerInfo, true, "matching serverinfo authority" ); + ExpectRemoteCVarPolicy( remoteNetworkSync, remoteNetworkSync, true, "matching networksync authority" ); + ExpectRemoteCVarPolicy( remoteUserInfo, remoteNetworkSync, false, "cross-class authority" ); + ExpectRemoteCVarPolicy( localInit, remoteUserInfo, false, "local-only CVar" ); + ExpectRemoteCVarPolicy( remoteUserInfo | remotePrivate, remoteUserInfo, false, "private CVar" ); + ExpectRemoteCVarPolicy( remoteUserInfo, 0, false, "empty authority" ); + ExpectRemoteCVarPolicy( remoteUserInfo | remoteNetworkSync, + remoteUserInfo | remoteNetworkSync, false, "combined authority" ); + ExpectRemoteCVarPolicy( localInit, localInit, false, "unknown authority" ); + ExpectPrivateToken( "set net_serverRemoteConsolePassword secret", + "net_serverRemoteConsolePassword", true, "ordinary assignment" ); + ExpectPrivateToken( "status; SET NET_SERVERREMOTECONSOLEPASSWORD secret", + "net_serverRemoteConsolePassword", true, "case-insensitive semicolon assignment" ); + ExpectPrivateToken( "net_serverRemoteConsolePasswor", + "net_serverRemoteConsolePassword", false, "command ends in private-name prefix" ); + ExpectPrivateToken( "net_serverRemoteConsolePasswordSuffix", + "net_serverRemoteConsolePassword", false, "private name is a longer token prefix" ); + ExpectPrivateToken( "xnet_serverRemoteConsolePassword", + "net_serverRemoteConsolePassword", false, "private name lacks left boundary" ); + ExpectPrivateToken( "echo net_serverRemoteConsolePassword", + "net_serverRemoteConsolePassword", true, "private name at command end" ); + ExpectPrivateToken( nullptr, "net_private", false, "null command" ); + ExpectPrivateToken( "set net_private x", "", false, "empty private name" ); const char *validDecimals[] = { "0", "-0", "123", "-123", "1.0", ".5", "-.5", "5." }; @@ -190,6 +387,39 @@ int main() { ExpectBounded( "12x", 127, false, 0, "non-digit" ); ExpectBounded( "0", -1, false, 0, "negative maximum" ); + ExpectGameDirSegment( "baseoq4", true, "ordinary game directory" ); + ExpectGameDirSegment( "my-mod_2", true, "portable punctuation" ); + ExpectGameDirSegment( ".hidden-mod", true, "non-dot hidden directory" ); + ExpectGameDirSegment( nullptr, false, "null game directory" ); + ExpectGameDirSegment( "", false, "empty game directory" ); + ExpectGameDirSegment( ".", false, "dot game directory" ); + ExpectGameDirSegment( "..", false, "parent game directory" ); + ExpectGameDirSegment( "../escape", false, "parent path escape" ); + ExpectGameDirSegment( "mod/child", false, "forward-slash path" ); + ExpectGameDirSegment( "mod\\child", false, "backslash path" ); + ExpectGameDirSegment( "C:mod", false, "volume-relative path" ); + ExpectGameDirSegment( " leading", false, "leading space" ); + ExpectGameDirSegment( "trailing. ", false, "normalized trailing characters" ); + ExpectGameDirSegment( "bad*mod", false, "reserved punctuation" ); + ExpectGameDirSegment( "CON", false, "Windows device directory" ); + ExpectGameDirSegment( "com1.mod", false, "numbered Windows device directory" ); + ExpectGameDirSegment( "lpt\xC2\xB3", false, "UTF-8 superscript Windows device directory" ); + { + char maximumSegment[idGameDirPolicy::MAX_SEGMENT_BYTES + 1]; + for ( int index = 0; index < idGameDirPolicy::MAX_SEGMENT_BYTES; ++index ) { + maximumSegment[index] = 'm'; + } + maximumSegment[idGameDirPolicy::MAX_SEGMENT_BYTES] = '\0'; + ExpectGameDirSegment( maximumSegment, true, "maximum game-directory segment" ); + + char oversizedSegment[idGameDirPolicy::MAX_SEGMENT_BYTES + 2]; + for ( int index = 0; index <= idGameDirPolicy::MAX_SEGMENT_BYTES; ++index ) { + oversizedSegment[index] = 'm'; + } + oversizedSegment[idGameDirPolicy::MAX_SEGMENT_BYTES + 1] = '\0'; + ExpectGameDirSegment( oversizedSegment, false, "oversized game-directory segment" ); + } + using namespace idStrAllocationDetail; const size_t sizeMaximum = ( std::numeric_limits::max )(); ExpectAllocation( SaturatingAdd( 17, 25 ) == 42, "ordinary addition" ); @@ -210,6 +440,61 @@ int main() { ExpectAllocation( !TryRoundUpToInt( sizeMaximum, 32, roundedAmount ), "SIZE_MAX round-up" ); ExpectAllocation( !TryRoundUpToInt( 1, 0, roundedAmount ), "zero granularity" ); + ExpectOpenURLPolicy( "http://example.com", true, "ordinary HTTP URL" ); + ExpectOpenURLPolicy( "HTTPS://example.com:443/releases?q=openq4#download", true, "ordinary HTTPS URL" ); + ExpectOpenURLPolicy( "https://localhost", true, "localhost HTTPS URL" ); + ExpectOpenURLPolicy( "http://127.0.0.1:8080/path", true, "IPv4 HTTP URL" ); + ExpectOpenURLPolicy( "https://[2001:db8::1]:65535/path", true, "IPv6 HTTPS URL" ); + ExpectOpenURLPolicy( "https://example.com/path@name?redirect=%2Fsafe", true, "reserved path characters" ); + ExpectOpenURLPolicy( nullptr, false, "null URL" ); + ExpectOpenURLPolicy( "", false, "empty URL" ); + ExpectOpenURLPolicy( "ftp://example.com/file", false, "FTP scheme" ); + ExpectOpenURLPolicy( "file:///tmp/update", false, "file scheme" ); + ExpectOpenURLPolicy( "javascript:alert(1)", false, "script scheme" ); + ExpectOpenURLPolicy( "https:example.com", false, "HTTPS URL without authority delimiter" ); + ExpectOpenURLPolicy( "https:///missing-host", false, "empty authority" ); + ExpectOpenURLPolicy( "https://:443/path", false, "empty host with port" ); + ExpectOpenURLPolicy( "https://user@example.com/path", false, "userinfo authority" ); + ExpectOpenURLPolicy( "https://example.com%40attacker.invalid/path", false, "encoded authority ambiguity" ); + ExpectOpenURLPolicy( "https://example.com:65536/path", false, "out-of-range URL port" ); + ExpectOpenURLPolicy( "https://example.com:/path", false, "empty URL port" ); + ExpectOpenURLPolicy( "https://example.com:443:444/path", false, "ambiguous URL port" ); + ExpectOpenURLPolicy( "https://2001:db8::1/path", false, "unbracketed IPv6 URL" ); + ExpectOpenURLPolicy( "https://[::1]suffix/path", false, "text after bracketed URL host" ); + ExpectOpenURLPolicy( "https://!/path", false, "invalid hostname character" ); + ExpectOpenURLPolicy( "https://./path", false, "empty DNS labels" ); + ExpectOpenURLPolicy( "https://-example.com/path", false, "leading hostname hyphen" ); + ExpectOpenURLPolicy( "https://example-.com/path", false, "trailing hostname hyphen" ); + ExpectOpenURLPolicy( "https://999.1.2.3/path", false, "invalid IPv4 octet" ); + ExpectOpenURLPolicy( "https://127.1/path", false, "ambiguous abbreviated IPv4" ); + ExpectOpenURLPolicy( "https://[not-an-ip]/path", false, "invalid bracketed IP literal" ); + ExpectOpenURLPolicy( "https://[::::]/path", false, "invalid IPv6 compression" ); + ExpectOpenURLPolicy( "https://example.com/line\nbreak", false, "URL newline" ); + ExpectOpenURLPolicy( "https://example.com/space here", false, "URL space" ); + ExpectOpenURLPolicy( "https://example.com\\attacker.invalid", false, "URL backslash" ); + ExpectOpenURLPolicy( "https://example.com/\x7f", false, "URL delete control" ); + + { + char maximumLengthURL[idURLPolicy::MAX_URL_BYTES]; + const char prefix[] = "https://example.com/"; + for ( size_t index = 0; index < sizeof( prefix ) - 1; index++ ) { + maximumLengthURL[index] = prefix[index]; + } + for ( size_t index = sizeof( prefix ) - 1; index < idURLPolicy::MAX_URL_BYTES - 1; index++ ) { + maximumLengthURL[index] = 'a'; + } + maximumLengthURL[idURLPolicy::MAX_URL_BYTES - 1] = '\0'; + ExpectOpenURLPolicy( maximumLengthURL, true, "maximum bounded URL" ); + + char oversizedURL[idURLPolicy::MAX_URL_BYTES + 1]; + for ( size_t index = 0; index < idURLPolicy::MAX_URL_BYTES; index++ ) { + oversizedURL[index] = maximumLengthURL[index]; + } + oversizedURL[idURLPolicy::MAX_URL_BYTES - 1] = 'a'; + oversizedURL[idURLPolicy::MAX_URL_BYTES] = '\0'; + ExpectOpenURLPolicy( oversizedURL, false, "oversized URL" ); + } + ExpectEndpoint( "127.0.0.1", true, "127.0.0.1", false, 0, "numeric IPv4" ); ExpectEndpoint( "127.0.0.1:65535", true, "127.0.0.1", true, 65535, "maximum IPv4 port" ); ExpectEndpoint( "4.example.com:27960", true, "4.example.com", true, 27960, "digit-leading hostname" ); diff --git a/tools/tests/native/GpuFrameTimingTest.cpp b/tools/tests/native/GpuFrameTimingTest.cpp new file mode 100644 index 00000000..0e49d6fc --- /dev/null +++ b/tools/tests/native/GpuFrameTimingTest.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/GpuFrameTimingCore.h" + +#include +#include +#include +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "GpuFrameTimingTest: %s\n", message ); + failures++; +} + +int main() { + gpuFrameTimingCoreState_t state; + GpuFrameTimingCore_Initialize( state ); + Check( !state.supported && !state.valid && state.frameNumber == -1, + "initial state must be unsupported and invalid" ); + + GpuFrameTimingCore_SetBackend( state, 1, true ); + Check( state.supported && state.backend == 1 && state.generation == 1 + && state.resetCount == 1, + "backend selection must begin a fresh generation" ); + GpuFrameTimingCore_RecordResolved( state, 1, 20, state.generation, 4123, 4 ); + Check( state.valid && state.frameNumber == 20 && state.elapsedMicroseconds == 4123 + && state.latencyFrames == 4 && state.resolvedSamples == 1, + "resolved sample must publish coherently" ); + + const std::uint32_t oldGeneration = state.generation; + GpuFrameTimingCore_Reset( state ); + Check( !state.valid && state.generation == oldGeneration + 1 + && state.resetCount == 2 && state.resolvedSamples == 1, + "reset must invalidate while preserving cumulative counters" ); + GpuFrameTimingCore_RecordResolved( state, 1, 21, oldGeneration, 1000, 1 ); + Check( state.droppedSamples == 1 && !state.valid, + "stale-generation results must be dropped" ); + GpuFrameTimingCore_RecordResolved( state, 1, 22, state.generation, 0, 1 ); + Check( state.droppedSamples == 2 && !state.valid, + "zero-microsecond results must not become budget samples" ); + + Check( GpuFrameTimingCore_TimestampDelta( 100, 180, 64 ) == 80, + "64-bit timestamp delta mismatch" ); + Check( GpuFrameTimingCore_TimestampDelta( 250, 5, 8 ) == 11, + "valid-bit timestamp wrap mismatch" ); + Check( GpuFrameTimingCore_TimestampDelta( 1, 2, 0 ) == 0, + "zero valid bits must reject timestamp math" ); + Check( GpuFrameTimingCore_TicksToMicroseconds( 1500000, 1.0 ) == 1500, + "timestamp-period conversion mismatch" ); + Check( GpuFrameTimingCore_TicksToMicroseconds( 1, 0.1 ) == 0, + "sub-half-microsecond duration must round to unavailable" ); + + Check( GpuFrameTimingCore_FrameLatency( 10, 14 ) == 4, + "ordinary frame latency mismatch" ); + Check( GpuFrameTimingCore_FrameLatency( INT_MAX, INT_MIN ) == 1, + "signed frame-counter wrap must resolve to one frame" ); + Check( GpuFrameTimingCore_FrameLatency( INT_MIN, INT_MAX ) + == std::numeric_limits::max(), + "wide frame latency must clamp without signed overflow" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "GpuFrameTimingTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/LevelLoadCacheFormatTest.cpp b/tools/tests/native/LevelLoadCacheFormatTest.cpp new file mode 100644 index 00000000..66e78837 --- /dev/null +++ b/tools/tests/native/LevelLoadCacheFormatTest.cpp @@ -0,0 +1,589 @@ +/* +=========================================================================== + +openQ4 hardened level-load cache format native tests + +Copyright (C) 2026 openQ4 contributors + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +=========================================================================== +*/ + +#include "src/framework/LevelLoadCacheFormat.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace cache = idLevelLoadCache; + +static int failures = 0; + +static void Expect( const bool condition, const char *label ) { + if ( !condition ) { + std::fprintf( stderr, "level-load cache format failed: %s\n", label ); + failures++; + } +} + +static void ExpectStatus( const cache::Result &result, const cache::Status expected, + const char *label ) { + if ( result.status != expected ) { + std::fprintf( stderr, + "level-load cache format failed: %s expected %s, got %s at %zu (%s)\n", + label, cache::StatusName( expected ), cache::StatusName( result.status ), + result.offset, result.diagnostic.c_str() ); + failures++; + } + if ( expected != cache::Status::OK && result.diagnostic.empty() ) { + std::fprintf( stderr, "level-load cache format failed: %s had no diagnostic\n", label ); + failures++; + } +} + +static cache::Hash HashText( const char *text ) { + return cache::ComputeHash( text, std::strlen( text ) ); +} + +static std::string HashHex( const cache::Hash &hash ) { + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + text.reserve( hash.size() * 2 ); + for ( const std::uint8_t value : hash ) { + text.push_back( digits[value >> 4] ); + text.push_back( digits[value & 15] ); + } + return text; +} + +static void WriteU32( std::vector &bytes, const std::size_t offset, + const std::uint32_t value ) { + Expect( offset <= bytes.size() && bytes.size() - offset >= 4, "test WriteU32 bounds" ); + if ( offset > bytes.size() || bytes.size() - offset < 4 ) { + return; + } + for ( unsigned int index = 0; index < 4; ++index ) { + bytes[offset + index] = static_cast( value >> ( index * 8u ) ); + } +} + +static void WriteU64( std::vector &bytes, const std::size_t offset, + const std::uint64_t value ) { + Expect( offset <= bytes.size() && bytes.size() - offset >= 8, "test WriteU64 bounds" ); + if ( offset > bytes.size() || bytes.size() - offset < 8 ) { + return; + } + for ( unsigned int index = 0; index < 8; ++index ) { + bytes[offset + index] = static_cast( value >> ( index * 8u ) ); + } +} + +static void RewriteIntegrityHash( std::vector &encoded ) { + Expect( encoded.size() >= cache::HASH_BYTES + 4, "test integrity trailer bounds" ); + if ( encoded.size() < cache::HASH_BYTES + 4 ) { + return; + } + const std::size_t protectedBytes = encoded.size() - cache::HASH_BYTES - 4; + const cache::Hash hash = cache::ComputeHash( encoded.data(), protectedBytes ); + std::memcpy( encoded.data() + protectedBytes, hash.data(), hash.size() ); +} + +static cache::SourceIdentity ArchiveSource( const char *path, const std::uint64_t size = 987654, + const std::uint64_t timestamp = 123456789, const std::uint32_t checksum = 0x91a2b3c4u ) { + cache::SourceIdentity source; + source.normalizedPath = path; + source.size = size; + source.timestamp = timestamp; + source.containerKind = cache::SourceContainerKind::PK4_ARCHIVE; + source.containerPk4Checksum = checksum; + return source; +} + +static cache::CacheEnvelope ExampleEnvelope() { + cache::CacheEnvelope envelope; + envelope.kind = cache::CacheKind::RENDER_MODEL; + envelope.parserVersion = 0x01020304u; + envelope.codec = cache::CompressionCodec::NONE; + envelope.source = ArchiveSource( "models/mapobjects/storage/crate.lwo" ); + envelope.contentSignature = HashText( "ordered stock PK4 inventory" ); + envelope.settingsSignature = HashText( "renderer cache settings" ); + envelope.payload = { 0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff, 0x55, 0xaa }; + return envelope; +} + +static cache::EnvelopeExpectation ExpectationFor( const cache::CacheEnvelope &envelope ) { + cache::EnvelopeExpectation expected; + expected.kind = envelope.kind; + expected.parserVersion = envelope.parserVersion; + expected.source = envelope.source; + expected.contentSignature = envelope.contentSignature; + expected.settingsSignature = envelope.settingsSignature; + return expected; +} + +static cache::ManifestEntry ExampleEntry( const cache::ManifestEntryType type, + const char *name, const char *sourcePath, const cache::ManifestPriority priority, + const std::uint64_t order, const std::uint32_t useCount ) { + cache::ManifestEntry entry; + entry.type = type; + entry.priority = priority; + entry.firstUseOrder = order; + entry.useCount = useCount; + entry.flags = 0x10203040u + static_cast( type ); + entry.normalizedName = name; + entry.source = ArchiveSource( sourcePath, 4000 + static_cast( type ), + 5000 + static_cast( type ) ); + entry.options = { static_cast( type ), 0x22, 0x00, 0xff }; + return entry; +} + +static cache::Manifest ExampleManifest() { + cache::Manifest manifest; + manifest.producerVersion = 7; + manifest.mapKey = "game/storage1"; + manifest.gameMode = HashText( "single-player" ); + manifest.entityFilter = HashText( "storage1/default" ); + manifest.contentSignature = HashText( "ordered stock PK4 inventory" ); + manifest.settingsSignature = HashText( "renderer preload settings" ); + manifest.entries.push_back( ExampleEntry( cache::ManifestEntryType::IMAGE, + "textures/base_wall/metal", "textures/base_wall/metal.tga", + cache::ManifestPriority::NORMAL, 18, 2 ) ); + manifest.entries.push_back( ExampleEntry( cache::ManifestEntryType::RENDER_MODEL, + "models/mapobjects/crate.lwo", "models/mapobjects/crate.lwo", + cache::ManifestPriority::CRITICAL, 2, 5 ) ); + manifest.entries.push_back( ExampleEntry( cache::ManifestEntryType::SOUND_SAMPLE, + "sound/ambience/machine.wav", "sound/ambience/machine.wav", + cache::ManifestPriority::HIGH, 9, 3 ) ); + return manifest; +} + +static cache::ManifestExpectation ExpectationFor( const cache::Manifest &manifest ) { + cache::ManifestExpectation expected; + expected.producerVersion = manifest.producerVersion; + expected.mapKey = manifest.mapKey; + expected.gameMode = manifest.gameMode; + expected.entityFilter = manifest.entityFilter; + expected.contentSignature = manifest.contentSignature; + expected.settingsSignature = manifest.settingsSignature; + return expected; +} + +static void ExerciseHashAndPathPrimitives() { + Expect( HashHex( HashText( "abc" ) ) == + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "SHA-256 known vector" ); + Expect( HashHex( cache::ComputeHash( nullptr, 0 ) ) == + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "SHA-256 empty vector" ); + + std::string normalized; + ExpectStatus( cache::NormalizeVirtualPath( "Models\\Weapons//./Gun.LWO", normalized ), + cache::Status::OK, "normalize mixed virtual path" ); + Expect( normalized == "models/weapons/gun.lwo", "normalized virtual path bytes" ); + ExpectStatus( cache::NormalizeVirtualPath( "models/../secret", normalized ), + cache::Status::INVALID_STRING, "reject parent virtual path" ); + ExpectStatus( cache::NormalizeVirtualPath( "/absolute/path", normalized ), + cache::Status::INVALID_STRING, "reject absolute virtual path" ); + ExpectStatus( cache::NormalizeVirtualPath( "c:/drive/path", normalized ), + cache::Status::INVALID_STRING, "reject drive virtual path" ); + ExpectStatus( cache::NormalizeVirtualPath( "models/long", normalized, 4 ), + cache::Status::SIZE_LIMIT_EXCEEDED, "virtual path bound" ); +} + +static void ExerciseEnvelopeRoundTrip( std::vector &encodedOut ) { + const cache::CacheEnvelope source = ExampleEnvelope(); + std::vector encoded; + ExpectStatus( cache::EncodeEnvelope( source, encoded ), cache::Status::OK, + "encode cache envelope" ); + Expect( encoded.size() > 128, "cache envelope has fixed metadata" ); + Expect( encoded.size() >= 16 && encoded[0] == 'O' && encoded[1] == 'Q' && + encoded[2] == '4' && encoded[3] == 'C', "cache magic wire bytes" ); + Expect( encoded.size() >= 16 && encoded[12] == 0x04 && encoded[13] == 0x03 && + encoded[14] == 0x02 && encoded[15] == 0x01, "cache parser version is little endian" ); + + cache::CacheEnvelope decoded; + ExpectStatus( cache::DecodeEnvelopeForKey( encoded, ExpectationFor( source ), decoded ), + cache::Status::OK, "decode cache envelope round trip" ); + cache::CacheEnvelope canonical = source; + canonical.decodedPayloadBytes = source.payload.size(); + canonical.decodedPayloadHash = cache::ComputeHash( source.payload.data(), source.payload.size() ); + Expect( decoded == canonical, "cache envelope round-trip fields" ); + ExpectStatus( cache::ValidateDecodedPayload( decoded, decoded.payload.data(), decoded.payload.size() ), + cache::Status::OK, "validate uncompressed decoded payload" ); + + std::vector second; + ExpectStatus( cache::EncodeEnvelope( source, second ), cache::Status::OK, + "encode deterministic cache envelope" ); + Expect( encoded == second, "deterministic cache envelope bytes" ); + encodedOut = std::move( encoded ); +} + +static void ExerciseEnvelopeFailures( const std::vector &valid ) { + cache::CacheEnvelope decoded; + for ( std::size_t bytes = 0; bytes < valid.size(); ++bytes ) { + const cache::Result result = cache::DecodeEnvelope( valid.data(), bytes, decoded ); + if ( result.Ok() ) { + std::fprintf( stderr, "level-load cache format failed: cache truncation accepted at %zu/%zu\n", + bytes, valid.size() ); + failures++; + break; + } + } + + std::vector damaged = valid; + damaged[0] ^= 0xffu; + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::INVALID_MAGIC, + "reject corrupt cache magic" ); + damaged = valid; + WriteU32( damaged, 4, cache::CACHE_FORMAT_VERSION + 1 ); + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::UNSUPPORTED_VERSION, + "reject unsupported cache version" ); + damaged = valid; + damaged[damaged.size() - cache::HASH_BYTES - 4] ^= 0x80u; + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::INTEGRITY_MISMATCH, + "reject corrupt cache integrity hash" ); + damaged = valid; + const cache::CacheEnvelope source = ExampleEnvelope(); + const std::size_t pathLengthOffset = 20; + const std::size_t storedPayloadLengthOffset = 112 + source.source.normalizedPath.size(); + const std::size_t payloadOffset = storedPayloadLengthOffset + 48; + damaged[payloadOffset] ^= 0x20u; + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::INTEGRITY_MISMATCH, + "reject corrupt stored cache payload" ); + damaged = valid; + damaged[damaged.size() - 1] ^= 1u; + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::INVALID_END_MARKER, + "reject corrupt cache end marker" ); + damaged = valid; + damaged.push_back( 0x7e ); + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::TRAILING_DATA, + "reject trailing cache garbage" ); + + damaged = valid; + WriteU32( damaged, pathLengthOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized cache source path length" ); + damaged = valid; + WriteU64( damaged, storedPayloadLengthOffset, std::numeric_limits::max() ); + const cache::Result oversizedPayload = cache::DecodeEnvelope( damaged, decoded ); + Expect( oversizedPayload.status == cache::Status::SIZE_LIMIT_EXCEEDED || + oversizedPayload.status == cache::Status::INTEGER_OVERFLOW, + "reject oversized cache payload length" ); + + cache::DecodeLimits limits; + limits.maxPayloadBytes = source.payload.size() - 1; + ExpectStatus( cache::DecodeEnvelope( valid, decoded, limits ), cache::Status::SIZE_LIMIT_EXCEEDED, + "caller cache payload limit" ); + limits = cache::DecodeLimits(); + limits.maxDecodedPayloadBytes = source.payload.size() - 1; + ExpectStatus( cache::DecodeEnvelope( valid, decoded, limits ), cache::Status::SIZE_LIMIT_EXCEEDED, + "caller decoded cache limit" ); + + // Alter the decoded hash but repair the whole-envelope hash. This proves the + // independent decoded-payload hash is checked after envelope integrity. + damaged = valid; + const std::size_t decodedHashOffset = storedPayloadLengthOffset + 16; + damaged[decodedHashOffset] ^= 0x40u; + RewriteIntegrityHash( damaged ); + ExpectStatus( cache::DecodeEnvelope( damaged, decoded ), cache::Status::PAYLOAD_HASH_MISMATCH, + "reject cache decoded-payload hash mismatch" ); + + cache::CacheEnvelope sentinel; + sentinel.parserVersion = 777; + sentinel.payload = { 9, 9, 9 }; + damaged = valid; + damaged[0] ^= 1; + Expect( !cache::DecodeEnvelope( damaged, sentinel ).Ok(), "failed cache decode reports failure" ); + Expect( sentinel.parserVersion == 777 && sentinel.payload == std::vector( { 9, 9, 9 } ), + "failed cache decode leaves output unchanged" ); +} + +static void ExerciseEnvelopeKeyAndCompression( const std::vector &valid ) { + const cache::CacheEnvelope source = ExampleEnvelope(); + cache::EnvelopeExpectation expected = ExpectationFor( source ); + cache::CacheEnvelope decoded; + expected.kind = cache::CacheKind::RENDER_WORLD; + ExpectStatus( cache::DecodeEnvelopeForKey( valid, expected, decoded ), cache::Status::KIND_MISMATCH, + "cache kind key mismatch" ); + expected = ExpectationFor( source ); + expected.parserVersion++; + ExpectStatus( cache::DecodeEnvelopeForKey( valid, expected, decoded ), + cache::Status::PARSER_VERSION_MISMATCH, "cache parser key mismatch" ); + expected = ExpectationFor( source ); + expected.source.timestamp++; + ExpectStatus( cache::DecodeEnvelopeForKey( valid, expected, decoded ), cache::Status::SOURCE_MISMATCH, + "cache source key mismatch" ); + expected = ExpectationFor( source ); + expected.contentSignature[0] ^= 1; + ExpectStatus( cache::DecodeEnvelopeForKey( valid, expected, decoded ), + cache::Status::CONTENT_SIGNATURE_MISMATCH, "cache content key mismatch" ); + expected = ExpectationFor( source ); + expected.settingsSignature[0] ^= 1; + ExpectStatus( cache::DecodeEnvelopeForKey( valid, expected, decoded ), + cache::Status::SETTINGS_SIGNATURE_MISMATCH, "cache settings key mismatch" ); + + const std::vector decodedBytes = { 'd', 'e', 'c', 'o', 'd', 'e', 'd' }; + cache::CacheEnvelope compressed = source; + compressed.codec = cache::CompressionCodec::DEFLATE; + compressed.payload = { 0x78, 0x9c, 0x03, 0x00 }; + compressed.decodedPayloadBytes = decodedBytes.size(); + compressed.decodedPayloadHash = cache::ComputeHash( decodedBytes.data(), decodedBytes.size() ); + std::vector encoded; + ExpectStatus( cache::EncodeEnvelope( compressed, encoded ), cache::Status::OK, + "encode externally compressed envelope" ); + ExpectStatus( cache::DecodeEnvelope( encoded, decoded ), cache::Status::OK, + "decode externally compressed envelope" ); + ExpectStatus( cache::ValidateDecodedPayload( decoded, decodedBytes.data(), decodedBytes.size() ), + cache::Status::OK, "validate decompressed payload" ); + std::vector wrong = decodedBytes; + wrong[0] ^= 1; + ExpectStatus( cache::ValidateDecodedPayload( decoded, wrong.data(), wrong.size() ), + cache::Status::PAYLOAD_HASH_MISMATCH, "reject wrong decompressed payload" ); +} + +static void ExerciseManifestRoundTrip( std::vector &encodedOut, + cache::Manifest &canonicalOut ) { + const cache::Manifest source = ExampleManifest(); + cache::Manifest canonical = source; + ExpectStatus( cache::CanonicalizeManifest( canonical ), cache::Status::OK, + "canonicalize manifest" ); + std::vector encoded; + ExpectStatus( cache::EncodeManifest( source, encoded ), cache::Status::OK, "encode manifest" ); + Expect( encoded.size() >= 8 && encoded[0] == 'O' && encoded[1] == 'Q' && + encoded[2] == '4' && encoded[3] == 'M', "manifest magic wire bytes" ); + + cache::Manifest decoded; + ExpectStatus( cache::DecodeManifestForKey( encoded, ExpectationFor( source ), decoded ), + cache::Status::OK, "decode manifest round trip" ); + Expect( decoded == canonical, "manifest round-trip fields" ); + + std::vector second; + ExpectStatus( cache::EncodeManifest( source, second ), cache::Status::OK, + "encode deterministic manifest" ); + Expect( encoded == second, "deterministic manifest bytes" ); + encodedOut = std::move( encoded ); + canonicalOut = std::move( canonical ); +} + +static void ExerciseManifestDeterminism() { + cache::Manifest first = ExampleManifest(); + cache::ManifestEntry target = first.entries[1]; + first.entries[1].useCount = 5; + + cache::Manifest second = first; + second.entries.erase( second.entries.begin() + 1 ); + cache::ManifestEntry early = target; + early.useCount = 3; + early.priority = cache::ManifestPriority::CRITICAL; + early.firstUseOrder = 2; + cache::ManifestEntry late = target; + late.useCount = 2; + late.priority = cache::ManifestPriority::NORMAL; + late.firstUseOrder = 99; + second.entries.push_back( late ); + second.entries.push_back( early ); + std::reverse( second.entries.begin(), second.entries.end() ); + + std::vector firstBytes; + std::vector secondBytes; + ExpectStatus( cache::EncodeManifest( first, firstBytes ), cache::Status::OK, + "encode canonical deterministic manifest" ); + ExpectStatus( cache::EncodeManifest( second, secondBytes ), cache::Status::OK, + "encode reordered duplicate manifest" ); + Expect( firstBytes == secondBytes, "manifest duplicate merge and deterministic ordering" ); + + cache::Manifest overflow = ExampleManifest(); + cache::ManifestEntry duplicate = overflow.entries.front(); + overflow.entries.front().useCount = std::numeric_limits::max(); + duplicate.useCount = 1; + overflow.entries.push_back( duplicate ); + ExpectStatus( cache::CanonicalizeManifest( overflow ), cache::Status::INTEGER_OVERFLOW, + "manifest duplicate use-count overflow" ); +} + +static void ExerciseManifestFailures( const std::vector &valid, + const cache::Manifest &canonical ) { + cache::Manifest decoded; + for ( std::size_t bytes = 0; bytes < valid.size(); ++bytes ) { + const cache::Result result = cache::DecodeManifest( valid.data(), bytes, decoded ); + if ( result.Ok() ) { + std::fprintf( stderr, "level-load cache format failed: manifest truncation accepted at %zu/%zu\n", + bytes, valid.size() ); + failures++; + break; + } + } + + std::vector damaged = valid; + damaged[0] ^= 0xffu; + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::INVALID_MAGIC, + "reject corrupt manifest magic" ); + damaged = valid; + WriteU32( damaged, 4, cache::MANIFEST_FORMAT_VERSION + 1 ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::UNSUPPORTED_VERSION, + "reject unsupported manifest version" ); + damaged = valid; + damaged[damaged.size() - cache::HASH_BYTES - 4] ^= 0x01u; + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::INTEGRITY_MISMATCH, + "reject corrupt manifest integrity hash" ); + damaged = valid; + damaged[damaged.size() - 1] ^= 0x01u; + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::INVALID_END_MARKER, + "reject corrupt manifest end marker" ); + damaged = valid; + damaged.push_back( 0x99 ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::TRAILING_DATA, + "reject trailing manifest garbage" ); + + const std::size_t mapLengthOffset = 140; + const std::size_t entryCountOffset = 144 + canonical.mapKey.size(); + const std::size_t firstEntryOffset = entryCountOffset + 4; + const cache::ManifestEntry &firstEntry = canonical.entries.front(); + const std::size_t firstNameLengthOffset = firstEntryOffset + 24; + const std::size_t firstSourceLengthOffset = firstNameLengthOffset + 4 + + firstEntry.normalizedName.size(); + const std::size_t firstOptionsLengthOffset = firstEntryOffset + 56 + + firstEntry.normalizedName.size() + firstEntry.source.normalizedPath.size(); + damaged = valid; + WriteU32( damaged, mapLengthOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized manifest map-key length" ); + damaged = valid; + WriteU32( damaged, entryCountOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized manifest entry count" ); + damaged = valid; + WriteU32( damaged, firstNameLengthOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized manifest entry-name length" ); + damaged = valid; + WriteU32( damaged, firstSourceLengthOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized manifest source-path length" ); + damaged = valid; + WriteU32( damaged, firstOptionsLengthOffset, std::numeric_limits::max() ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::SIZE_LIMIT_EXCEEDED, + "reject oversized manifest options length" ); + damaged = valid; + WriteU32( damaged, firstEntryOffset, std::numeric_limits::max() ); + RewriteIntegrityHash( damaged ); + ExpectStatus( cache::DecodeManifest( damaged, decoded ), cache::Status::INVALID_ENUM_VALUE, + "reject unknown manifest entry type" ); + + cache::DecodeLimits limits; + limits.maxManifestEntries = canonical.entries.size() - 1; + ExpectStatus( cache::DecodeManifest( valid, decoded, limits ), cache::Status::SIZE_LIMIT_EXCEEDED, + "caller manifest entry-count limit" ); + limits = cache::DecodeLimits(); + limits.maxEntryOptionsBytes = 1; + ExpectStatus( cache::DecodeManifest( valid, decoded, limits ), cache::Status::SIZE_LIMIT_EXCEEDED, + "caller manifest options limit" ); + limits = cache::DecodeLimits(); + limits.maxAggregateStringBytes = canonical.mapKey.size(); + ExpectStatus( cache::DecodeManifest( valid, decoded, limits ), cache::Status::SIZE_LIMIT_EXCEEDED, + "caller manifest aggregate-byte limit" ); + + const cache::Manifest source = ExampleManifest(); + cache::ManifestExpectation expected = ExpectationFor( source ); + expected.mapKey = "game/medlabs"; + ExpectStatus( cache::DecodeManifestForKey( valid, expected, decoded ), + cache::Status::MAP_KEY_MISMATCH, "manifest map key mismatch" ); + expected = ExpectationFor( source ); + expected.gameMode[0] ^= 1; + ExpectStatus( cache::DecodeManifestForKey( valid, expected, decoded ), + cache::Status::GAME_MODE_MISMATCH, "manifest full game-mode key mismatch" ); + expected = ExpectationFor( source ); + expected.entityFilter[31] ^= 1; + ExpectStatus( cache::DecodeManifestForKey( valid, expected, decoded ), + cache::Status::ENTITY_FILTER_MISMATCH, "manifest full entity-filter key mismatch" ); + expected = ExpectationFor( source ); + expected.contentSignature[0] ^= 1; + ExpectStatus( cache::DecodeManifestForKey( valid, expected, decoded ), + cache::Status::CONTENT_SIGNATURE_MISMATCH, "manifest content key mismatch" ); + expected = ExpectationFor( source ); + expected.settingsSignature[0] ^= 1; + ExpectStatus( cache::DecodeManifestForKey( valid, expected, decoded ), + cache::Status::SETTINGS_SIGNATURE_MISMATCH, "manifest settings key mismatch" ); +} + +static void ExerciseManifestCanonicalRejection() { + cache::Manifest manifest; + manifest.producerVersion = 11; + manifest.mapKey = "game/testmap"; + manifest.gameMode = HashText( "single-player" ); + manifest.entityFilter = HashText( "default" ); + manifest.contentSignature = HashText( "content" ); + manifest.settingsSignature = HashText( "settings" ); + manifest.entries.push_back( ExampleEntry( cache::ManifestEntryType::RAW_FILE, + "files/a", "source/a", cache::ManifestPriority::NORMAL, 1, 1 ) ); + manifest.entries.push_back( ExampleEntry( cache::ManifestEntryType::RAW_FILE, + "files/b", "source/b", cache::ManifestPriority::NORMAL, 2, 1 ) ); + Expect( manifest.entries[0].normalizedName.size() == manifest.entries[1].normalizedName.size() && + manifest.entries[0].source.normalizedPath.size() == manifest.entries[1].source.normalizedPath.size(), + "canonical rejection fixture has equal entry sizes" ); + + std::vector encoded; + ExpectStatus( cache::EncodeManifest( manifest, encoded ), cache::Status::OK, + "encode canonical-rejection fixture" ); + const std::size_t entryStart = 148 + manifest.mapKey.size(); + const std::size_t entryBytes = 60 + manifest.entries[0].normalizedName.size() + + manifest.entries[0].source.normalizedPath.size() + manifest.entries[0].options.size(); + Expect( entryStart + entryBytes * 2 + cache::HASH_BYTES + 4 == encoded.size(), + "canonical rejection fixture wire-size calculation" ); + if ( entryStart + entryBytes * 2 + cache::HASH_BYTES + 4 != encoded.size() ) { + return; + } + + std::vector duplicate = encoded; + std::memcpy( duplicate.data() + entryStart + entryBytes, + duplicate.data() + entryStart, entryBytes ); + RewriteIntegrityHash( duplicate ); + cache::Manifest decoded; + ExpectStatus( cache::DecodeManifest( duplicate, decoded ), cache::Status::DUPLICATE_ENTRY, + "reject duplicate manifest entry on disk" ); + + std::vector reversed = encoded; + std::vector firstEntry( entryBytes ); + std::memcpy( firstEntry.data(), reversed.data() + entryStart, entryBytes ); + std::memcpy( reversed.data() + entryStart, + reversed.data() + entryStart + entryBytes, entryBytes ); + std::memcpy( reversed.data() + entryStart + entryBytes, firstEntry.data(), entryBytes ); + RewriteIntegrityHash( reversed ); + ExpectStatus( cache::DecodeManifest( reversed, decoded ), cache::Status::NON_CANONICAL_ORDER, + "reject non-canonical manifest entry order" ); +} + +int main() { + ExerciseHashAndPathPrimitives(); + + std::vector envelopeBytes; + ExerciseEnvelopeRoundTrip( envelopeBytes ); + if ( !envelopeBytes.empty() ) { + ExerciseEnvelopeFailures( envelopeBytes ); + ExerciseEnvelopeKeyAndCompression( envelopeBytes ); + } + + std::vector manifestBytes; + cache::Manifest canonicalManifest; + ExerciseManifestRoundTrip( manifestBytes, canonicalManifest ); + ExerciseManifestDeterminism(); + if ( !manifestBytes.empty() ) { + ExerciseManifestFailures( manifestBytes, canonicalManifest ); + } + ExerciseManifestCanonicalRejection(); + + if ( failures != 0 ) { + std::fprintf( stderr, "level-load cache format native test: %d failure(s)\n", failures ); + return 1; + } + std::printf( "level-load cache format native test: ok\n" ); + return 0; +} diff --git a/tools/tests/native/LevelLoadPipelineTest.cpp b/tools/tests/native/LevelLoadPipelineTest.cpp new file mode 100644 index 00000000..c0559443 --- /dev/null +++ b/tools/tests/native/LevelLoadPipelineTest.cpp @@ -0,0 +1,582 @@ +#include "src/framework/LevelLoadPipeline.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void Expect( const bool condition, const char *label ) { + if ( !condition ) { + std::fprintf( stderr, "LevelLoadPipelineTest failed: %s\n", label ); + ++failures; + } +} + +struct FakeFile { + std::vector bytes; + std::size_t position = 0; + std::atomic reads{ 0 }; + unsigned int delayMilliseconds = 0; +}; + +idFile *OpaqueFile( FakeFile &file ) { + return reinterpret_cast( &file ); +} + +int ReadFakeFile( idFile *opaque, void *buffer, const int byteCount, void * ) { + FakeFile *file = reinterpret_cast( opaque ); + if ( file == nullptr || buffer == nullptr || byteCount <= 0 ) { + return 0; + } + if ( file->delayMilliseconds != 0 ) { + std::this_thread::sleep_for( std::chrono::milliseconds( file->delayMilliseconds ) ); + } + if ( file->position >= file->bytes.size() ) { + return 0; + } + const std::size_t requested = static_cast( byteCount ); + const std::size_t copied = std::min( requested, file->bytes.size() - file->position ); + std::memcpy( buffer, file->bytes.data() + file->position, copied ); + file->position += copied; + file->reads.fetch_add( 1, std::memory_order_release ); + return static_cast( copied ); +} + +struct DecodeHarness { + std::atomic completed{ 0 }; + std::atomic started{ 0 }; + unsigned int delayMilliseconds = 0; +}; + +std::uint64_t UpdateChecksum( std::uint64_t checksum, + const unsigned char *bytes, const std::size_t byteCount ) { + for ( std::size_t index = 0; index < byteCount; ++index ) { + checksum ^= bytes[index]; + checksum *= 1099511628211ull; + } + return checksum; +} + +idLevelLoadDecodeStatus DecodeTestSource( const idLevelLoadPipelineSource &source, + const unsigned char *bytes, const std::size_t byteCount, + const idLevelLoadDecodeContext &context, idLevelLoadDecodeOutput &output, + void *userData ) { + DecodeHarness *harness = static_cast( userData ); + if ( bytes == nullptr || byteCount == 0 || harness == nullptr ) { + return idLevelLoadDecodeStatus::MALFORMED; + } + harness->started.fetch_add( 1, std::memory_order_release ); + const idLevelLoadDecodeStatus frameStatus = idLevelLoadDecodeSourceFrame( + source, bytes, byteCount, output, &context ); + if ( frameStatus != idLevelLoadDecodeStatus::COMPLETE ) { + return frameStatus; + } + std::uint64_t checksum = 14695981039346656037ull; + std::size_t offset = 0; + while ( offset < byteCount ) { + if ( context.IsCancellationRequested() ) { + return idLevelLoadDecodeStatus::CANCELLED; + } + if ( harness->delayMilliseconds != 0 ) { + std::this_thread::sleep_for( + std::chrono::milliseconds( harness->delayMilliseconds ) ); + } + const std::size_t chunkBytes = std::min( context.chunkBytes, byteCount - offset ); + checksum = UpdateChecksum( checksum, bytes + offset, chunkBytes ); + if ( !context.ReportDecodedBytes( chunkBytes ) ) { + return context.IsCancellationRequested() + ? idLevelLoadDecodeStatus::CANCELLED + : idLevelLoadDecodeStatus::MALFORMED; + } + offset += chunkBytes; + } + output.transportChecksum = checksum; + for ( std::size_t index = 0; index < output.contentIntegrity.size(); ++index ) { + output.contentIntegrity[index] = static_cast( + ( checksum >> ( ( index % 8 ) * 8 ) ) ^ index ); + } + harness->completed.fetch_add( 1, std::memory_order_relaxed ); + return idLevelLoadDecodeStatus::COMPLETE; +} + +idLevelLoadPipelineSource MakeSource( const char *path, FakeFile &file, + const unsigned int priority, const unsigned int order, + const std::uint64_t timestamp, const std::uint32_t checksum ) { + idLevelLoadPipelineSource source; + source.normalizedPath = path; + source.type = 1; + source.priority = priority; + source.firstUseOrder = order; + source.file = OpaqueFile( file ); + source.sourceBytes = file.bytes.size(); + source.sourceTimestamp = timestamp; + source.containerChecksum = checksum; + return source; +} + +void AppendTag( std::vector &bytes, const char *tag ) { + bytes.insert( bytes.end(), tag, tag + 4 ); +} + +void AppendLittleU32( std::vector &bytes, const std::uint32_t value ) { + for ( std::size_t index = 0; index < 4; ++index ) { + bytes.push_back( static_cast( value >> ( index * 8 ) ) ); + } +} + +void AppendBigU32( std::vector &bytes, const std::uint32_t value ) { + for ( std::size_t index = 0; index < 4; ++index ) { + bytes.push_back( static_cast( value >> ( ( 3 - index ) * 8 ) ) ); + } +} + +void StoreLittleU32( std::vector &bytes, const std::size_t offset, + const std::uint32_t value ) { + for ( std::size_t index = 0; index < 4; ++index ) { + bytes[offset + index] = static_cast( value >> ( index * 8 ) ); + } +} + +void StoreBigU32( std::vector &bytes, const std::size_t offset, + const std::uint32_t value ) { + for ( std::size_t index = 0; index < 4; ++index ) { + bytes[offset + index] = static_cast( value >> ( ( 3 - index ) * 8 ) ); + } +} + +idLevelLoadPipelineSource FrameSource( const char *path ) { + idLevelLoadPipelineSource source; + source.normalizedPath = path; + source.type = 1; + return source; +} + +void ExpectFrame( const char *path, const std::vector &bytes, + const bool valid, const idLevelLoadDecodedFrameKind expectedKind, + const std::uint32_t expectedUnits, const char *label ) { + idLevelLoadDecodeOutput output; + const idLevelLoadDecodeStatus status = idLevelLoadDecodeSourceFrame( + FrameSource( path ), bytes.data(), bytes.size(), output ); + Expect( ( status == idLevelLoadDecodeStatus::COMPLETE ) == valid, label ); + if ( valid ) { + Expect( output.frameKind == expectedKind && output.decodedBytes == bytes.size() && + output.payloadOffset == 0 && output.payloadBytes == bytes.size() && + output.frameUnitCount == expectedUnits, + "production frame metadata is bounded and complete" ); + } +} + +void ExerciseProductionFramingValidation() { + std::vector wave; + AppendTag( wave, "RIFF" ); + AppendLittleU32( wave, 0 ); + AppendTag( wave, "WAVE" ); + AppendTag( wave, "fmt " ); + AppendLittleU32( wave, 16 ); + wave.insert( wave.end(), 16, 0 ); + AppendTag( wave, "data" ); + AppendLittleU32( wave, 4 ); + wave.insert( wave.end(), 4, 0x7f ); + StoreLittleU32( wave, 4, static_cast( wave.size() - 8 ) ); + ExpectFrame( "sound/valid.wav", wave, true, + idLevelLoadDecodedFrameKind::RIFF_WAVE, 2, "complete RIFF/WAVE framing succeeds" ); + std::vector truncatedWave = wave; + truncatedWave.pop_back(); + ExpectFrame( "sound/truncated.wav", truncatedWave, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "truncated RIFF framing fails closed" ); + std::vector trailingWave = wave; + trailingWave.push_back( 0 ); + ExpectFrame( "sound/trailing.wav", trailingWave, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "RIFF trailing data fails closed" ); + std::vector oversizedWave = wave; + StoreLittleU32( oversizedWave, 16, 0xffffffffu ); + ExpectFrame( "sound/oversized.wav", oversizedWave, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "oversized RIFF chunk fails closed" ); + + std::vector ogg( 27, 0 ); + std::memcpy( ogg.data(), "OggS", 4 ); + ogg[4] = 0; + ogg[26] = 1; + ogg.push_back( 3 ); + ogg.push_back( 1 ); + ogg.push_back( 2 ); + ogg.push_back( 3 ); + ExpectFrame( "sound/valid.ogg", ogg, true, + idLevelLoadDecodedFrameKind::OGG, 1, "complete Ogg page framing succeeds" ); + std::vector truncatedOgg = ogg; + truncatedOgg.pop_back(); + ExpectFrame( "sound/truncated.ogg", truncatedOgg, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "truncated Ogg page fails closed" ); + std::vector trailingOgg = ogg; + trailingOgg.push_back( 0 ); + ExpectFrame( "sound/trailing.ogg", trailingOgg, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "Ogg trailing data fails closed" ); + std::vector oversizedOgg = ogg; + oversizedOgg[26] = 255; + ExpectFrame( "sound/oversized.ogg", oversizedOgg, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "oversized Ogg segment table fails closed" ); + + std::vector png = { 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a }; + AppendBigU32( png, 13 ); + AppendTag( png, "IHDR" ); + AppendBigU32( png, 1 ); + AppendBigU32( png, 1 ); + png.insert( png.end(), 5, 0 ); + AppendBigU32( png, 0 ); + AppendBigU32( png, 1 ); + AppendTag( png, "IDAT" ); + png.push_back( 0 ); + AppendBigU32( png, 0 ); + AppendBigU32( png, 0 ); + AppendTag( png, "IEND" ); + AppendBigU32( png, 0 ); + ExpectFrame( "textures/valid.png", png, true, + idLevelLoadDecodedFrameKind::PNG, 3, "complete PNG chunk framing succeeds" ); + std::vector truncatedPng = png; + truncatedPng.pop_back(); + ExpectFrame( "textures/truncated.png", truncatedPng, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "truncated PNG IEND fails closed" ); + std::vector trailingPng = png; + trailingPng.push_back( 0 ); + ExpectFrame( "textures/trailing.png", trailingPng, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "PNG trailing data fails closed" ); + std::vector oversizedPng = png; + StoreBigU32( oversizedPng, 8, 0xffffffffu ); + ExpectFrame( "textures/oversized.png", oversizedPng, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "oversized PNG chunk fails closed" ); + + const std::vector jpeg = { 0xff, 0xd8, 0xff, 0xd9 }; + ExpectFrame( "textures/valid.jpg", jpeg, true, + idLevelLoadDecodedFrameKind::JPEG, 1, "terminal JPEG EOI succeeds" ); + std::vector trailingJpeg = jpeg; + trailingJpeg.push_back( 0 ); + ExpectFrame( "textures/trailing.jpg", trailingJpeg, false, + idLevelLoadDecodedFrameKind::INVALID, 0, "JPEG trailing data fails closed" ); + + const std::vector dds = { 'n', 'o', 't', '-', 'a', '-', 'd', 'd', 's' }; + ExpectFrame( "textures/deferred.dds", dds, true, + idLevelLoadDecodedFrameKind::OPAQUE_VFS_BYTES, 1, + "DDS remains opaque for renderer-owned complete layout validation" ); +} + +void ExerciseSetupFailureHandleRecovery() { + FakeFile file; + file.bytes.assign( 8, 0x31 ); + std::vector sources; + sources.push_back( MakeSource( "models/setup-failure.ase", file, 1, 0, 2, 0 ) ); + idLevelLoadPipelineConfig config; + config.maxEntries = 1; + config.maxSourceBytes = 8; + config.maxTotalBytes = 8; + config.maxDecodedBytes = 8; + config.readChunkBytes = 4; + config.decodeChunkBytes = 4; + idLevelLoadPipeline pipeline; + Expect( !pipeline.Begin( 2, config, std::move( sources ), &ReadFakeFile ), + "missing production decode stage rejects pipeline setup" ); + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 1, + "setup failure returns every independently opened source handle" ); + pipeline.Reset(); +} + +void ExerciseBoundedSynchronousReplay() { + idJobSystemConfig jobs; + jobs.synchronous = true; + jobs.workerThreads = 0; + jobs.maxQueuedLists = 4; + Expect( jobSystem.Initialize( jobs ), "synchronous job system initializes" ); + + FakeFile first; + first.bytes.assign( 8, 0x11 ); + FakeFile critical; + critical.bytes.assign( 12, 0x22 ); + FakeFile oversized; + oversized.bytes.assign( 40, 0x33 ); + + std::vector sources; + sources.push_back( MakeSource( "models/first.ase", first, 1, 2, 41, 0 ) ); + sources.push_back( MakeSource( "maps/test.proc", critical, 3, 1, 0, 0x12345678u ) ); + sources.push_back( MakeSource( "textures/oversized.tga", oversized, 2, 3, 42, 0 ) ); + + idLevelLoadPipelineConfig config; + config.maxEntries = 2; + config.maxSourceBytes = 40; + config.maxTotalBytes = 60; + config.maxDecodedBytes = 20; + config.readChunkBytes = 3; + config.decodeChunkBytes = 3; + DecodeHarness decoder; + idLevelLoadPipeline pipeline; + Expect( pipeline.Begin( 7, config, std::move( sources ), &ReadFakeFile, nullptr, + &DecodeTestSource, &decoder ), "bounded replay begins" ); + pipeline.Wait(); + + const idLevelLoadPipelineMetrics metrics = pipeline.GetMetrics(); + Expect( metrics.generation == 7, "generation identity retained" ); + Expect( metrics.admittedEntries == 2 && metrics.rejectedEntries == 1, + "entry and byte budgets reject lower-priority overflow" ); + Expect( metrics.completedEntries == 2 && metrics.failedEntries == 0, + "admitted synchronous reads complete" ); + Expect( metrics.synchronousFallback, + "jobs-disabled execution is reported as an explicit synchronous path" ); + Expect( metrics.bytesRead == 20 && metrics.peakStagingBytes <= 20, + "read and peak staging metrics stay bounded" ); + Expect( metrics.decodeBudgetRejectedEntries == 1 && + metrics.admittedDecodedBytes == 20 && metrics.decodeBudgetBytes == 20 && + metrics.peakDecodedBytes <= 20, + "decode admission and retained-result metrics stay within their budget" ); + Expect( metrics.decodeStartedEntries == 2 && metrics.decodeCompletedEntries == 2 && + metrics.bytesDecoded == 20 && decoder.completed.load( std::memory_order_relaxed ) == 2, + "decode callback inspects and publishes every admitted byte" ); + + const std::shared_ptr acquired = pipeline.Acquire( + 7, "maps/test.proc", 1, 12, 0, 0x12345678u ); + Expect( acquired != nullptr && acquired->bytes != nullptr && + acquired->bytes->size() == 12 && ( *acquired->bytes )[0] == 0x22 && + acquired->frameKind == idLevelLoadDecodedFrameKind::OPAQUE_VFS_BYTES && + acquired->payloadOffset == 0 && acquired->payloadBytes == 12 && + acquired->frameUnitCount == 1, + "exact source identity acquires an immutable framed decode result" ); + Expect( pipeline.Acquire( 8, "maps/test.proc", 1, 12, 0, 0x12345678u ) == nullptr, + "generation mismatch cannot adopt a completed result" ); + Expect( pipeline.Acquire( 7, "maps/test.proc", 1, 12, 1, 0x12345678u ) == nullptr, + "timestamp mismatch cannot acquire bytes" ); + + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 3, "admitted and rejected handles are returned after join" ); + pipeline.Reset(); + jobSystem.Shutdown(); +} + +void ExerciseCooperativeCancellation() { + idJobSystemConfig jobs; + jobs.synchronous = false; + jobs.workerThreads = 2; + jobs.maxQueuedLists = 4; + Expect( jobSystem.Initialize( jobs ), "threaded job system initializes" ); + + FakeFile slow; + slow.bytes.assign( 2u * 1024u * 1024u, 0x5a ); + slow.delayMilliseconds = 1; + std::vector sources; + sources.push_back( MakeSource( "maps/cancel.proc", slow, 3, 0, 0, 0x89abcdefu ) ); + + idLevelLoadPipelineConfig config; + config.maxEntries = 1; + config.maxSourceBytes = slow.bytes.size(); + config.maxTotalBytes = slow.bytes.size(); + config.maxDecodedBytes = slow.bytes.size(); + config.readChunkBytes = 4096; + config.decodeChunkBytes = 4096; + DecodeHarness decoder; + idLevelLoadPipeline pipeline; + Expect( pipeline.Begin( 8, config, std::move( sources ), &ReadFakeFile, + nullptr, &DecodeTestSource, &decoder ), + "threaded replay begins" ); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 2 ); + while ( slow.reads.load( std::memory_order_acquire ) < 4 && + std::chrono::steady_clock::now() < deadline ) { + std::this_thread::yield(); + } + Expect( slow.reads.load( std::memory_order_acquire ) >= 4, + "worker enters chunked read before cancellation" ); + pipeline.CancelAndWait(); + const idLevelLoadPipelineMetrics metrics = pipeline.GetMetrics(); + Expect( metrics.cancelled && metrics.cancelledEntries == 1, + "generation cancellation reaches the active item" ); + Expect( metrics.bytesRead < slow.bytes.size() && !metrics.active, + "cancelled item stops before full read and joins" ); + Expect( pipeline.Acquire( 8, "maps/cancel.proc", 1, slow.bytes.size(), 0, + 0x89abcdefu ) == nullptr, + "cancelled bytes are never published" ); + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 1, "cancelled source handle is returned" ); + pipeline.Reset(); + jobSystem.Shutdown(); +} + +void ExerciseCooperativeDecodeCancellation() { + idJobSystemConfig jobs; + jobs.synchronous = false; + jobs.workerThreads = 1; + jobs.maxQueuedLists = 4; + Expect( jobSystem.Initialize( jobs ), "decode-cancellation job system initializes" ); + + FakeFile file; + file.bytes.assign( 64u * 1024u, 0x6d ); + std::vector sources; + sources.push_back( MakeSource( "models/cancel-decode.ase", file, 3, 0, 18, 0 ) ); + idLevelLoadPipelineConfig config; + config.maxEntries = 1; + config.maxSourceBytes = file.bytes.size(); + config.maxTotalBytes = file.bytes.size(); + config.maxDecodedBytes = file.bytes.size(); + config.readChunkBytes = file.bytes.size(); + config.decodeChunkBytes = 256; + DecodeHarness decoder; + decoder.delayMilliseconds = 1; + idLevelLoadPipeline pipeline; + Expect( pipeline.Begin( 18, config, std::move( sources ), &ReadFakeFile, + nullptr, &DecodeTestSource, &decoder ), "decode-cancellation replay begins" ); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds( 2 ); + while ( decoder.started.load( std::memory_order_acquire ) == 0 && + std::chrono::steady_clock::now() < deadline ) { + std::this_thread::yield(); + } + Expect( decoder.started.load( std::memory_order_acquire ) == 1, + "worker enters decode before cancellation" ); + pipeline.CancelAndWait(); + const idLevelLoadPipelineMetrics metrics = pipeline.GetMetrics(); + Expect( metrics.cancelled && metrics.decodeStartedEntries == 1 && + metrics.decodeCancelledEntries == 1 && metrics.cancelledEntries == 1, + "decode observes generation cancellation cooperatively" ); + Expect( metrics.bytesRead == file.bytes.size() && + metrics.bytesDecoded < file.bytes.size() && metrics.decodeCompletedEntries == 0, + "decode cancellation occurs after read and before result publication" ); + Expect( pipeline.Acquire( 18, "models/cancel-decode.ase", 1, + file.bytes.size(), 18, 0 ) == nullptr, + "cancelled decode cannot be adopted" ); + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 1, "decode-cancelled source handle is returned" ); + pipeline.Reset(); + jobSystem.Shutdown(); +} + +void ExerciseMalformedFramingFallback() { + idJobSystemConfig jobs; + jobs.synchronous = true; + jobs.workerThreads = 0; + jobs.maxQueuedLists = 4; + Expect( jobSystem.Initialize( jobs ), "malformed-frame job system initializes" ); + + FakeFile malformed; + malformed.bytes.assign( 128, 0x44 ); + std::vector sources; + sources.push_back( MakeSource( "sound/malformed.wav", malformed, 2, 0, 21, 0 ) ); + idLevelLoadPipelineConfig config; + config.maxEntries = 1; + config.maxSourceBytes = malformed.bytes.size(); + config.maxTotalBytes = malformed.bytes.size(); + config.maxDecodedBytes = malformed.bytes.size(); + config.readChunkBytes = 64; + config.decodeChunkBytes = 64; + DecodeHarness decoder; + idLevelLoadPipeline pipeline; + Expect( pipeline.Begin( 21, config, std::move( sources ), &ReadFakeFile, + nullptr, &DecodeTestSource, &decoder ), "malformed frame replay begins" ); + pipeline.Wait(); + const idLevelLoadPipelineMetrics metrics = pipeline.GetMetrics(); + Expect( metrics.decodeStartedEntries == 1 && metrics.decodeFailedEntries == 1 && + metrics.failedEntries == 1 && metrics.decodeCompletedEntries == 0, + "malformed framing fails only the speculative decode" ); + Expect( pipeline.Acquire( 21, "sound/malformed.wav", 1, + malformed.bytes.size(), 21, 0 ) == nullptr, + "malformed framing leaves authoritative VFS fallback available" ); + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 1, "malformed framed source handle is returned" ); + pipeline.Reset(); + jobSystem.Shutdown(); +} + +struct Blocker { + std::mutex mutex; + std::condition_variable condition; + bool started = false; + bool release = false; +}; + +void BlockingJob( const idJobContext &context ) { + Blocker *blocker = static_cast( context.data ); + std::unique_lock lock( blocker->mutex ); + blocker->started = true; + blocker->condition.notify_all(); + blocker->condition.wait( lock, [blocker, &context]() { + return blocker->release || context.IsCancellationRequested(); + } ); +} + +void ExerciseQueueSaturationFallback() { + idJobSystemConfig jobs; + jobs.synchronous = false; + jobs.workerThreads = 1; + jobs.maxQueuedLists = 1; + Expect( jobSystem.Initialize( jobs ), "saturation job system initializes" ); + Blocker blocker; + std::unique_ptr occupied = jobSystem.CreateJobList( "occupied", 1, 0 ); + Expect( occupied != nullptr && occupied->AddJob( &BlockingJob, &blocker ), + "occupying list is created" ); + Expect( occupied->Submit() == idJobSubmitResult::ACCEPTED, + "occupying list is submitted" ); + { + std::unique_lock lock( blocker.mutex ); + Expect( blocker.condition.wait_for( lock, std::chrono::seconds( 2 ), [&blocker]() { + return blocker.started; + } ), "occupying job starts" ); + } + + FakeFile file; + file.bytes.assign( 16, 0x77 ); + std::vector sources; + sources.push_back( MakeSource( "models/fallback.ase", file, 2, 0, 5, 0 ) ); + idLevelLoadPipelineConfig config; + config.maxEntries = 1; + config.maxSourceBytes = 16; + config.maxTotalBytes = 16; + config.maxDecodedBytes = 16; + config.readChunkBytes = 4; + config.decodeChunkBytes = 4; + DecodeHarness decoder; + idLevelLoadPipeline pipeline; + Expect( pipeline.Begin( 9, config, std::move( sources ), &ReadFakeFile, + nullptr, &DecodeTestSource, &decoder ), + "queue-full replay executes inline" ); + const idLevelLoadPipelineMetrics metrics = pipeline.GetMetrics(); + Expect( metrics.synchronousFallback && metrics.completedEntries == 1, + "queue saturation is explicit and never drops work" ); + + { + std::lock_guard lock( blocker.mutex ); + blocker.release = true; + } + blocker.condition.notify_all(); + occupied->Wait(); + pipeline.Wait(); + std::vector handles; + pipeline.DrainOpenFiles( handles ); + Expect( handles.size() == 1, "fallback source handle is returned" ); + pipeline.Reset(); + jobSystem.Shutdown(); +} + +} // namespace + +int main() { + ExerciseProductionFramingValidation(); + ExerciseSetupFailureHandleRecovery(); + ExerciseBoundedSynchronousReplay(); + ExerciseCooperativeCancellation(); + ExerciseCooperativeDecodeCancellation(); + ExerciseMalformedFramingFallback(); + ExerciseQueueSaturationFallback(); + if ( failures != 0 ) { + std::fprintf( stderr, "LevelLoadPipelineTest: %d failure(s)\n", failures ); + return 1; + } + std::printf( "LevelLoadPipelineTest passed\n" ); + return 0; +} diff --git a/tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp b/tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp new file mode 100644 index 00000000..d9b304ea --- /dev/null +++ b/tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/ModernSpecularProbeAtlas.h" + +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, + "ModernSpecularProbeAtlasPackingTest: %s\n", message ); + failures++; +} + +int main() { + modernSpecularProbeAtlasPlacement_t placements[ + MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES]; + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + Check( ModernSpecularProbeAtlas_BuildPlacement( slot, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE, placements[slot] ), + "every bounded slot must pack" ); + Check( placements[slot].valid && placements[slot].slot == slot, + "a packed placement must publish its stable slot" ); + for ( int face = 0; face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + const int expectedCell = + slot * MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT + face; + Check( placements[slot].faceCells[face] == expectedCell, + "faces must occupy consecutive cells in +X,-X,+Y,-Y,+Z,-Z order" ); + const float *rect = placements[slot].faceRects[face]; + Check( rect[0] >= 0.0f && rect[1] >= 0.0f + && rect[0] + rect[2] <= 1.0f + && rect[1] + rect[3] <= 1.0f, + "face rectangles must stay inside the atlas" ); + } + } + + for ( int slot = 0; slot < MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES; ++slot ) { + for ( int face = 0; face < MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; ++face ) { + const float *rect = placements[slot].faceRects[face]; + const int flat = slot * MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT + face; + for ( int prior = 0; prior < flat; ++prior ) { + const int priorSlot = prior / MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; + const int priorFace = prior % MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT; + const float *other = placements[priorSlot].faceRects[priorFace]; + Check( rect[0] + rect[2] < other[0] + || other[0] + other[2] < rect[0] + || rect[1] + rect[3] < other[1] + || other[1] + other[3] < rect[1], + "face rectangles must be disjoint" ); + } + } + } + + modernSpecularProbeAtlasPlacement_t smaller; + Check( ModernSpecularProbeAtlas_BuildPlacement( 0, 64, smaller ) + && smaller.faceRects[0][2] + == 63.0f / static_cast( MODERN_SPECULAR_PROBE_ATLAS_SIZE ), + "smaller faces must retain texel-centre extents" ); + + modernSpecularProbeAtlasPlacement_t rejected; + rejected.valid = true; + Check( !ModernSpecularProbeAtlas_BuildPlacement( -1, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE, rejected ) + && !rejected.valid && rejected.slot == -1 + && rejected.faceCells[0] == -1 + && rejected.faceRects[0][0] == 0.0f, + "invalid slots must fail closed and clear output" ); + Check( !ModernSpecularProbeAtlas_BuildPlacement( 0, + MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE + 1, rejected ) + && !rejected.valid, + "oversized faces must not produce a placement" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "ModernSpecularProbeAtlasPackingTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/ParallelJobSystemTest.cpp b/tools/tests/native/ParallelJobSystemTest.cpp new file mode 100644 index 00000000..99d89203 --- /dev/null +++ b/tools/tests/native/ParallelJobSystemTest.cpp @@ -0,0 +1,514 @@ +#include "src/framework/ParallelJobSystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void Expect( const bool condition, const char *label ) { + if ( !condition ) { + std::fprintf( stderr, "ParallelJobSystemTest failed: %s\n", label ); + failures++; + } +} + +bool SubmitSucceeded( const idJobSubmitResult result ) { + return result == idJobSubmitResult::ACCEPTED || + result == idJobSubmitResult::EXECUTED_SYNCHRONOUSLY; +} + +struct AppendData { + std::vector *values; + int value; +}; + +void AppendJob( const idJobContext &context ) { + AppendData *data = static_cast( context.data ); + data->values->push_back( data->value ); + Expect( context.workerIndex == 0, "synchronous jobs must use deterministic worker index zero" ); +} + +void RecordJob( const idJobContext &context ) { + AppendData *data = static_cast( context.data ); + data->values->push_back( data->value ); +} + +void IncrementAtomicJob( const idJobContext &context ) { + std::atomic *value = static_cast *>( context.data ); + value->fetch_add( 1, std::memory_order_relaxed ); +} + +struct DependencyData { + std::atomic *phase; + std::atomic *failed; + int expected; + int replacement; +}; + +void DependencyJob( const idJobContext &context ) { + DependencyData *data = static_cast( context.data ); + if ( data->phase->load( std::memory_order_acquire ) != data->expected ) { + data->failed->store( true, std::memory_order_release ); + } + data->phase->store( data->replacement, std::memory_order_release ); +} + +struct BlockingData { + std::mutex mutex; + std::condition_variable condition; + bool started = false; + bool release = false; +}; + +struct ParallelGateData { + std::mutex mutex; + std::condition_variable condition; + int started = 0; + bool release = false; +}; + +void ParallelGateJob( const idJobContext &context ) { + ParallelGateData *data = static_cast( context.data ); + std::unique_lock lock( data->mutex ); + data->started++; + data->condition.notify_all(); + data->condition.wait( lock, [&data, &context]() { + return data->release || context.IsCancellationRequested(); + } ); +} + +void BlockingJob( const idJobContext &context ) { + BlockingData *data = static_cast( context.data ); + std::unique_lock lock( data->mutex ); + data->started = true; + data->condition.notify_all(); + while ( !data->release && !context.IsCancellationRequested() ) { + data->condition.wait_for( lock, std::chrono::milliseconds( 10 ) ); + } +} + +bool WaitForStarted( BlockingData &data ) { + std::unique_lock lock( data.mutex ); + return data.condition.wait_for( lock, std::chrono::seconds( 2 ), [&data]() { + return data.started; + } ); +} + +void ReleaseBlockingJob( BlockingData &data ) { + { + std::lock_guard lock( data.mutex ); + data.release = true; + } + data.condition.notify_all(); +} + +void ThrowingJob( const idJobContext & ) { + throw std::runtime_error( "intentional job failure" ); +} + +struct ObservePriorityData { + std::atomic *highCompleted; + std::atomic *observedHighCompleted; +}; + +void ObservePriorityJob( const idJobContext &context ) { + ObservePriorityData *data = static_cast( context.data ); + data->observedHighCompleted->store( + data->highCompleted->load( std::memory_order_acquire ), + std::memory_order_release ); +} + +void ExerciseConfigurationBounds() { + idJobSystem system; + idJobSystemConfig config; + config.maxQueuedLists = 0; + Expect( !system.Initialize( config ), "zero active-list capacity fails closed" ); + config.maxQueuedLists = 1025; + Expect( !system.Initialize( config ), "oversized active-list capacity fails closed" ); + config.maxQueuedLists = 1; + config.workerThreads = 33; + Expect( !system.Initialize( config ), "oversized worker count fails closed" ); + config.synchronous = true; + config.workerThreads = 0; + Expect( system.Initialize( config ), "system initializes after rejected configurations" ); + Expect( system.CreateJobList( "zero-jobs", 0 ) == nullptr, + "zero per-list job capacity fails closed" ); + Expect( system.CreateJobList( "oversized-jobs", 1024 * 1024 + 1 ) == nullptr, + "hard per-list job bound fails closed" ); + Expect( system.CreateJobList( "oversized-dependencies", 1, 257 ) == nullptr, + "hard per-list dependency bound fails closed" ); + system.Shutdown(); + config.synchronous = false; + config.workerThreads = 2; + config.maxQueuedLists = 4; + Expect( system.Initialize( config ), + "reinitialization refreshes bounded scheduler storage" ); + Expect( system.GetWorkerThreadCount() == 2, + "reinitialized scheduler exposes its reserved worker count" ); + system.Shutdown(); +} + +void ExerciseSynchronousMode() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = true; + config.workerThreads = 8; + config.maxQueuedLists = 2; + Expect( system.Initialize( config ), "synchronous system initializes" ); + Expect( system.IsSynchronous(), "synchronous policy is observable" ); + Expect( system.GetWorkerThreadCount() == 0, "synchronous policy creates no workers" ); + + std::vector values; + AppendData data[] = { { &values, 1 }, { &values, 2 }, { &values, 3 } }; + std::unique_ptr first = system.CreateJobList( "deterministic-first", 2, 1 ); + std::unique_ptr second = system.CreateJobList( "deterministic-second", 1, 1 ); + Expect( first != nullptr && second != nullptr, "bounded synchronous lists allocate" ); + Expect( first->AddJob( AppendJob, &data[0] ), "first deterministic job added" ); + Expect( first->AddJob( AppendJob, &data[1] ), "second deterministic job added" ); + Expect( !first->AddJob( AppendJob, &data[2] ), "per-list job capacity rejects overflow" ); + Expect( !second->AddDependency( *second ), "self dependency fails closed" ); + Expect( !second->AddDependency( *first ), "unsubmitted dependency fails closed" ); + Expect( first->Submit() == idJobSubmitResult::EXECUTED_SYNCHRONOUSLY, + "first list executes synchronously" ); + Expect( second->AddDependency( *first ), "submitted dependency is accepted" ); + Expect( !first->AddDependency( *second ), "reverse dependency cycle fails closed" ); + Expect( second->AddJob( AppendJob, &data[2] ), "dependent job added" ); + Expect( second->Submit() == idJobSubmitResult::EXECUTED_SYNCHRONOUSLY, + "dependent list executes synchronously" ); + Expect( values == std::vector( { 1, 2, 3 } ), "synchronous insertion and dependency order is deterministic" ); + Expect( first->Wait() && second->Wait(), "completed synchronous lists wait successfully" ); + Expect( first->GetMetrics().executedJobs == 2, "per-list synchronous execution metrics" ); + + const idJobSystemMetrics metrics = system.GetMetrics(); + Expect( metrics.submittedLists == 2 && metrics.completedLists == 2, + "synchronous list counters are exact" ); + Expect( metrics.submittedJobs == 3 && metrics.executedJobs == 3, + "synchronous job counters are exact" ); + system.Shutdown(); +} + +void ExerciseThreadedDependenciesAndFailure() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 4; + Expect( system.Initialize( config ), "threaded dependency system initializes" ); + + std::atomic phase( 0 ); + std::atomic dependencyFailed( false ); + DependencyData firstData = { &phase, &dependencyFailed, 0, 1 }; + DependencyData secondData = { &phase, &dependencyFailed, 1, 2 }; + std::unique_ptr first = system.CreateJobList( "dependency-root", 1, 0 ); + std::unique_ptr second = system.CreateJobList( "dependency-leaf", 1, 1 ); + Expect( first->AddJob( DependencyJob, &firstData ), "root dependency job added" ); + Expect( SubmitSucceeded( first->Submit() ), "root dependency list submitted" ); + Expect( second->AddDependency( *first ), "threaded dependency added" ); + Expect( second->AddJob( DependencyJob, &secondData ), "leaf dependency job added" ); + Expect( SubmitSucceeded( second->Submit() ), "leaf dependency list submitted" ); + Expect( second->Wait(), "leaf dependency list completes" ); + Expect( first->Wait(), "root dependency list completes" ); + Expect( !dependencyFailed.load( std::memory_order_acquire ) && phase.load() == 2, + "dependency execution order is enforced" ); + + std::atomic shouldNotRun( 0 ); + std::unique_ptr throwing = system.CreateJobList( "throwing", 2, 0 ); + Expect( throwing->AddJob( ThrowingJob, nullptr ), "throwing job added" ); + Expect( throwing->AddJob( IncrementAtomicJob, &shouldNotRun ), "post-failure job added" ); + Expect( SubmitSucceeded( throwing->Submit() ), "throwing list submitted" ); + Expect( !throwing->Wait(), "throwing list reports failure" ); + Expect( throwing->GetStatus() == idJobListStatus::FAILED, "throwing list has failed terminal state" ); + Expect( shouldNotRun.load() == 0, "failure cancels jobs not yet acquired" ); + Expect( throwing->GetMetrics().failedJobs == 1, "failure counter records thrown job" ); + + system.Shutdown(); +} + +void ExerciseParallelWorkers() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 2; + config.maxQueuedLists = 2; + Expect( system.Initialize( config ), "parallel worker system initializes" ); + ParallelGateData gate; + std::unique_ptr list = system.CreateJobList( "parallel-workers", 2, 0 ); + Expect( list->AddJob( ParallelGateJob, &gate ), "first parallel gate job added" ); + Expect( list->AddJob( ParallelGateJob, &gate ), "second parallel gate job added" ); + Expect( SubmitSucceeded( list->Submit() ), "parallel gate list submitted" ); + { + std::unique_lock lock( gate.mutex ); + Expect( gate.condition.wait_for( lock, std::chrono::seconds( 2 ), [&gate]() { + return gate.started == 2; + } ), "two worker threads execute one list concurrently" ); + gate.release = true; + } + gate.condition.notify_all(); + Expect( list->Wait(), "parallel gate list completes" ); + Expect( list->GetMetrics().executedJobs == 2, "parallel execution metrics are exact" ); + system.Shutdown(); +} + +void ExerciseBoundedSaturationAndCancellation() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 1; + Expect( system.Initialize( config ), "bounded system initializes" ); + + BlockingData blocker; + std::atomic overflowRuns( 0 ); + std::unique_ptr occupied = system.CreateJobList( "occupied-slot", 1, 0 ); + std::unique_ptr overflow = system.CreateJobList( "overflow-slot", 1, 0 ); + Expect( occupied->AddJob( BlockingJob, &blocker ), "blocking job added" ); + Expect( overflow->AddJob( IncrementAtomicJob, &overflowRuns ), "overflow job added" ); + Expect( SubmitSucceeded( occupied->Submit() ), "blocking list submitted" ); + Expect( WaitForStarted( blocker ), "blocking job starts" ); + Expect( overflow->Submit() == idJobSubmitResult::QUEUE_FULL, + "saturated list admission fails immediately and explicitly" ); + Expect( overflow->GetStatus() == idJobListStatus::BUILDING, + "queue-full list remains retryable" ); + ReleaseBlockingJob( blocker ); + Expect( occupied->Wait(), "blocking list drains" ); + Expect( SubmitSucceeded( overflow->Submit() ), "queue-full list can retry after capacity drains" ); + Expect( overflow->Wait() && overflowRuns.load() == 1, "retried list executes once" ); + const idJobSystemMetrics saturationMetrics = system.GetMetrics(); + Expect( saturationMetrics.queueHighWatermark == 1, "queue high-water mark respects configured bound" ); + Expect( saturationMetrics.rejectedSubmissions >= 1, "saturation is observable" ); + system.Shutdown(); + + idJobSystem cancellationSystem; + config.maxQueuedLists = 3; + Expect( cancellationSystem.Initialize( config ), "cancellation system initializes" ); + BlockingData cancellationBlocker; + std::atomic cancelledRuns( 0 ); + std::unique_ptr running = cancellationSystem.CreateJobList( "running-cancel", 1, 0 ); + std::unique_ptr pending = cancellationSystem.CreateJobList( "pending-cancel", 1, 0 ); + Expect( running->AddJob( BlockingJob, &cancellationBlocker ), "cooperative running job added" ); + Expect( pending->AddJob( IncrementAtomicJob, &cancelledRuns ), "pending cancellation job added" ); + Expect( SubmitSucceeded( running->Submit() ), "cooperative running list submitted" ); + Expect( WaitForStarted( cancellationBlocker ), "cooperative running job starts" ); + Expect( SubmitSucceeded( pending->Submit() ), "pending cancellation list submitted" ); + Expect( pending->Cancel(), "pending list cancellation accepted" ); + Expect( !pending->Wait() && pending->GetStatus() == idJobListStatus::CANCELLED, + "pending cancellation reaches terminal state" ); + Expect( cancelledRuns.load() == 0, "cancelled pending job never executes" ); + Expect( running->Cancel(), "running cooperative cancellation accepted" ); + cancellationBlocker.condition.notify_all(); + Expect( !running->Wait() && running->GetStatus() == idJobListStatus::CANCELLED, + "running cooperative job observes cancellation token" ); + cancellationSystem.Shutdown(); +} + +void ExercisePriorityAging() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 2; + Expect( system.Initialize( config ), "priority system initializes" ); + + BlockingData firstHigh; + std::atomic highCompleted( 0 ); + std::atomic observedHighCompleted( -1 ); + ObservePriorityData lowData = { &highCompleted, &observedHighCompleted }; + std::unique_ptr high = system.CreateJobList( "high-stream", 40, 0, idJobPriority::HIGH ); + std::unique_ptr low = system.CreateJobList( "low-starvation-guard", 1, 0, idJobPriority::LOW ); + Expect( high->AddJob( BlockingJob, &firstHigh ), "priority blocker added" ); + for ( int index = 1; index < 40; ++index ) { + Expect( high->AddJob( IncrementAtomicJob, &highCompleted ), "high-priority stream job added" ); + } + Expect( low->AddJob( ObservePriorityJob, &lowData ), "low-priority observation job added" ); + Expect( SubmitSucceeded( high->Submit() ), "high-priority stream submitted" ); + Expect( WaitForStarted( firstHigh ), "high-priority blocker starts" ); + Expect( SubmitSucceeded( low->Submit() ), "low-priority list submitted while high work remains" ); + ReleaseBlockingJob( firstHigh ); + Expect( low->Wait(), "aged low-priority list completes" ); + Expect( observedHighCompleted.load( std::memory_order_acquire ) >= 0 && + observedHighCompleted.load( std::memory_order_acquire ) < 39, + "priority aging prevents low-list starvation" ); + Expect( high->Wait(), "high-priority stream completes" ); + Expect( system.GetMetrics().priorityPromotions > 0, "priority aging promotion is observable" ); + system.Shutdown(); +} + +void ExercisePriorityStarvationClass() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 11; + Expect( system.Initialize( config ), "starvation-class system initializes" ); + + BlockingData gate; + std::unique_ptr blocker = system.CreateJobList( "starvation-class-gate", 1, 0 ); + Expect( blocker->AddJob( BlockingJob, &gate ), "starvation-class gate job added" ); + Expect( SubmitSucceeded( blocker->Submit() ), "starvation-class gate submitted" ); + Expect( WaitForStarted( gate ), "starvation-class gate starts" ); + + const int highListCount = 9; + const int jobsPerHighList = 32; + std::atomic highCompleted( 0 ); + std::atomic observedHighCompleted( -1 ); + ObservePriorityData lowData = { &highCompleted, &observedHighCompleted }; + std::vector > highLists; + highLists.reserve( highListCount ); + for ( int listIndex = 0; listIndex < highListCount; ++listIndex ) { + std::unique_ptr high = system.CreateJobList( + "long-running-high", jobsPerHighList, 0, idJobPriority::HIGH ); + Expect( high != nullptr, "long-running high-priority list created" ); + for ( int jobIndex = 0; jobIndex < jobsPerHighList; ++jobIndex ) { + Expect( high->AddJob( IncrementAtomicJob, &highCompleted ), + "long-running high-priority job added" ); + } + Expect( SubmitSucceeded( high->Submit() ), + "long-running high-priority list submitted" ); + highLists.push_back( std::move( high ) ); + } + + std::unique_ptr low = system.CreateJobList( + "starvation-class-low", 1, 0, idJobPriority::LOW ); + Expect( low->AddJob( ObservePriorityJob, &lowData ), + "starvation-class low-priority job added" ); + Expect( SubmitSucceeded( low->Submit() ), + "starvation-class low-priority list submitted after high lists" ); + + ReleaseBlockingJob( gate ); + Expect( blocker->Wait(), "starvation-class gate drains" ); + Expect( low->Wait(), "starvation-class low-priority list completes" ); + const int observed = observedHighCompleted.load( std::memory_order_acquire ); + Expect( observed >= 0 && observed <= highListCount, + "oldest-age starvation class runs LOW before nine HIGH lists drain" ); + for ( const std::unique_ptr &high : highLists ) { + Expect( high->Wait(), "long-running high-priority list drains" ); + } + Expect( highCompleted.load( std::memory_order_acquire ) == highListCount * jobsPerHighList, + "all long-running high-priority work completes" ); + Expect( system.GetMetrics().priorityPromotions > 0, + "starvation-class dispatch is observable as a promotion" ); + system.Shutdown(); +} + +void ExercisePriorityOrdering() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 4; + Expect( system.Initialize( config ), "priority ordering system initializes" ); + + BlockingData gate; + std::vector order; + AppendData lowData = { &order, 1 }; + AppendData normalData = { &order, 2 }; + AppendData highData = { &order, 3 }; + std::unique_ptr blocker = system.CreateJobList( "priority-order-blocker", 1, 0 ); + std::unique_ptr low = system.CreateJobList( "priority-order-low", 1, 0, idJobPriority::LOW ); + std::unique_ptr normal = system.CreateJobList( "priority-order-normal", 1, 0, idJobPriority::NORMAL ); + std::unique_ptr high = system.CreateJobList( "priority-order-high", 1, 0, idJobPriority::HIGH ); + Expect( blocker->AddJob( BlockingJob, &gate ), "priority ordering blocker added" ); + Expect( SubmitSucceeded( blocker->Submit() ), "priority ordering blocker submitted" ); + Expect( WaitForStarted( gate ), "priority ordering blocker starts" ); + Expect( low->AddJob( RecordJob, &lowData ) && SubmitSucceeded( low->Submit() ), + "low-priority ordering list submitted" ); + Expect( normal->AddJob( RecordJob, &normalData ) && SubmitSucceeded( normal->Submit() ), + "normal-priority ordering list submitted" ); + Expect( high->AddJob( RecordJob, &highData ) && SubmitSucceeded( high->Submit() ), + "high-priority ordering list submitted" ); + ReleaseBlockingJob( gate ); + Expect( blocker->Wait() && low->Wait() && normal->Wait() && high->Wait(), + "priority ordering lists complete" ); + Expect( order == std::vector( { 3, 2, 1 } ), + "initial high/normal/low priority order is enforced" ); + system.Shutdown(); +} + +void ExerciseShutdownCancellation() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = false; + config.workerThreads = 1; + config.maxQueuedLists = 3; + Expect( system.Initialize( config ), "shutdown system initializes" ); + BlockingData blocker; + std::atomic pendingRuns( 0 ); + std::unique_ptr running = system.CreateJobList( "shutdown-running", 1, 0 ); + std::unique_ptr pendingA = system.CreateJobList( "shutdown-pending-a", 1, 0 ); + std::unique_ptr pendingB = system.CreateJobList( "shutdown-pending-b", 1, 0 ); + Expect( running->AddJob( BlockingJob, &blocker ), "shutdown cooperative job added" ); + Expect( pendingA->AddJob( IncrementAtomicJob, &pendingRuns ) && + pendingB->AddJob( IncrementAtomicJob, &pendingRuns ), + "multiple shutdown-pending jobs added" ); + Expect( SubmitSucceeded( running->Submit() ), "shutdown cooperative list submitted" ); + Expect( WaitForStarted( blocker ), "shutdown cooperative job starts" ); + Expect( SubmitSucceeded( pendingA->Submit() ) && SubmitSucceeded( pendingB->Submit() ), + "multiple shutdown-pending lists submitted" ); + system.Shutdown( idJobShutdownMode::CANCEL_PENDING ); + Expect( running->GetStatus() == idJobListStatus::CANCELLED, + "cancel-pending shutdown reaches a terminal list state" ); + Expect( pendingA->GetStatus() == idJobListStatus::CANCELLED && + pendingB->GetStatus() == idJobListStatus::CANCELLED && pendingRuns.load() == 0, + "allocation-free shutdown iteration cancels every shifted pending list" ); + Expect( !system.IsInitialized(), "shutdown releases the service" ); + const idJobSystemMetrics metrics = system.GetMetrics(); + Expect( metrics.queuedLists == 0 && metrics.runningJobs == 0 && metrics.sleepingWorkers == 0, + "shutdown joins workers and leaves stable quiescent counters" ); +} + +void ExerciseSynchronousShutdownJoin() { + idJobSystem system; + idJobSystemConfig config; + config.synchronous = true; + config.workerThreads = 0; + config.maxQueuedLists = 2; + Expect( system.Initialize( config ), "synchronous shutdown system initializes" ); + BlockingData blocker; + std::unique_ptr running = system.CreateJobList( "synchronous-shutdown-running", 1, 0 ); + Expect( running->AddJob( BlockingJob, &blocker ), "synchronous shutdown job added" ); + std::atomic submitResult( static_cast( idJobSubmitResult::INVALID_STATE ) ); + std::thread submitter( [&running, &submitResult]() { + submitResult.store( static_cast( running->Submit() ), std::memory_order_release ); + } ); + Expect( WaitForStarted( blocker ), "synchronous inline job starts on submitting caller" ); + system.Shutdown( idJobShutdownMode::CANCEL_PENDING ); + submitter.join(); + Expect( submitResult.load( std::memory_order_acquire ) == + static_cast( idJobSubmitResult::EXECUTED_SYNCHRONOUSLY ), + "accepted synchronous submission returns after shutdown joins it" ); + Expect( running->GetStatus() == idJobListStatus::CANCELLED, + "synchronous in-flight list is visible to shutdown cancellation" ); + const idJobSystemMetrics metrics = system.GetMetrics(); + Expect( metrics.queuedLists == 0 && metrics.runningJobs == 0, + "synchronous shutdown leaves the shared quiescence contract clean" ); +} + +} // namespace + +int main() { + ExerciseConfigurationBounds(); + ExerciseSynchronousMode(); + ExerciseThreadedDependenciesAndFailure(); + ExerciseParallelWorkers(); + ExerciseBoundedSaturationAndCancellation(); + ExercisePriorityOrdering(); + ExercisePriorityAging(); + ExercisePriorityStarvationClass(); + ExerciseShutdownCancellation(); + ExerciseSynchronousShutdownJoin(); + + if ( failures != 0 ) { + std::fprintf( stderr, "ParallelJobSystemTest: %d failure(s)\n", failures ); + return 1; + } + std::printf( "ParallelJobSystemTest passed\n" ); + return 0; +} diff --git a/tools/tests/native/RendererContractsTest.cpp b/tools/tests/native/RendererContractsTest.cpp new file mode 100644 index 00000000..9e1781e3 --- /dev/null +++ b/tools/tests/native/RendererContractsTest.cpp @@ -0,0 +1,545 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/RendererContracts.h" + +#include +#include +#include +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "RendererContractsTest: %s\n", message ); + failures++; +} + +static bool NearlyEqual( float a, float b ) { + return std::fabs( a - b ) <= 0.00001f; +} + +static void ExerciseMaterialPassContract( void ) { + rendererMaterialPassList_t list; + RendererContracts_ResetMaterialPassList( list ); + + rendererMaterialPass_t first = RendererContracts_DefaultMaterialPass(); + Check( first.blend.sourceColor == RENDERER_BLEND_ONE + && first.blend.destinationColor == RENDERER_BLEND_ZERO + && first.alphaTestCompareOperation == RENDERER_COMPARE_GREATER, + "default material pass blend and alpha-test comparison changed" ); + first.sourceStageIndex = 3; + first.kind = RENDERER_MATERIAL_PASS_SURFACE; + first.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + first.textureResourceId = 0x101u; + first.condition = RendererContracts_Register( 7 ); + for ( int i = 0; i < 4; ++i ) { + first.color[ i ] = RendererContracts_Register( 10 + i ); + } + for ( int i = 0; i < 6; ++i ) { + first.textureMatrix[ i ] = RendererContracts_Register( 20 + i ); + } + first.blend.enabled = true; + first.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA; + first.blend.destinationColor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + first.blend.sourceAlpha = RENDERER_BLEND_SRC_ALPHA_SATURATE; + first.depth.testEnabled = true; + first.depth.writeEnabled = false; + first.depth.compareOperation = RENDERER_COMPARE_EQUAL; + first.cull = RENDERER_CULL_FRONT; + first.colorWriteMask = RENDERER_COLOR_WRITE_RED | RENDERER_COLOR_WRITE_GREEN; + first.alphaTestEnabled = true; + first.alphaTestCompareOperation = RENDERER_COMPARE_GREATER_OR_EQUAL; + first.alphaTest = RendererContracts_Register( 31 ); + first.texgen = RENDERER_TEXGEN_SCREEN; + first.vertexColor = RENDERER_VERTEX_COLOR_MODULATE; + first.polygonOffsetEnabled = true; + first.polygonOffsetFactor = RendererContracts_Register( 32 ); + first.polygonOffsetUnits = RendererContracts_Constant( 2.0f ); + first.programFamily = RENDERER_PROGRAM_CUSTOM; + first.programKey = 0x33445566u; + + rendererMaterialPass_t second = RendererContracts_DefaultMaterialPass(); + second.sourceStageIndex = 9; + second.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + second.textureResourceId = 0x202u; + second.condition = RendererContracts_Register( 41 ); + + Check( RendererContracts_AppendMaterialPass( list, first ), + "first material pass must append" ); + Check( RendererContracts_AppendMaterialPass( list, second ), + "repeated texture semantic must append" ); + Check( list.count == 2 && list.passes[ 0 ].order == 0 && list.passes[ 1 ].order == 1, + "material pass insertion order must be stable" ); + Check( list.passes[ 0 ].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && list.passes[ 1 ].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && list.passes[ 0 ].textureResourceId == 0x101u + && list.passes[ 1 ].textureResourceId == 0x202u, + "repeated semantics must remain distinct" ); + Check( list.passes[ 0 ].condition.index == 7 + && list.passes[ 0 ].color[ 3 ].index == 13 + && list.passes[ 0 ].textureMatrix[ 5 ].index == 25 + && list.passes[ 0 ].alphaTest.index == 31 + && list.passes[ 0 ].polygonOffsetFactor.index == 32, + "material register references must survive append" ); + Check( list.passes[ 0 ].blend.enabled && !list.passes[ 0 ].depth.writeEnabled + && list.passes[ 0 ].blend.sourceAlpha == RENDERER_BLEND_SRC_ALPHA_SATURATE + && list.passes[ 0 ].cull == RENDERER_CULL_FRONT + && list.passes[ 0 ].colorWriteMask == 3u + && list.passes[ 0 ].texgen == RENDERER_TEXGEN_SCREEN + && list.passes[ 0 ].vertexColor == RENDERER_VERTEX_COLOR_MODULATE + && list.passes[ 0 ].polygonOffsetEnabled + && list.passes[ 0 ].alphaTestCompareOperation == RENDERER_COMPARE_GREATER_OR_EQUAL + && list.passes[ 0 ].programFamily == RENDERER_PROGRAM_CUSTOM, + "material pass render state must survive append" ); + + const std::uint32_t validCount = list.count; + rendererMaterialPass_t malformed = RendererContracts_DefaultMaterialPass(); + malformed.sourceStageIndex = 0; + malformed.alphaTestCompareOperation = static_cast( 99 ); + Check( !RendererContracts_ValidateMaterialPass( malformed ) + && !RendererContracts_AppendMaterialPass( list, malformed ) + && list.count == validCount, + "malformed alpha-test comparison enum must fail validation without appending" ); + malformed = RendererContracts_DefaultMaterialPass(); + malformed.sourceStageIndex = 0; + malformed.blend.sourceColor = static_cast( 99 ); + Check( !RendererContracts_ValidateMaterialPass( malformed ) + && !RendererContracts_AppendMaterialPass( list, malformed ) + && list.count == validCount, + "malformed blend-factor enum must fail validation without appending" ); + + RendererContracts_ResetMaterialPassList( list ); + rendererMaterialPass_t bounded = RendererContracts_DefaultMaterialPass(); + bounded.sourceStageIndex = 0; + for ( std::uint32_t i = 0; i < RENDERER_CONTRACT_MAX_MATERIAL_PASSES; ++i ) { + Check( RendererContracts_AppendMaterialPass( list, bounded ), + "bounded material list rejected an in-range pass" ); + } + Check( !RendererContracts_AppendMaterialPass( list, bounded ) && list.overflowed + && list.count == RENDERER_CONTRACT_MAX_MATERIAL_PASSES, + "bounded material list must fail closed at capacity" ); +} + +static void BuildEvaluationPassList( rendererMaterialPassList_t &list ) { + RendererContracts_ResetMaterialPassList( list ); + + rendererMaterialPass_t first = RendererContracts_DefaultMaterialPass(); + first.sourceStageIndex = 3; + first.kind = RENDERER_MATERIAL_PASS_INTERACTION; + first.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + first.textureResourceId = 0x101u; + first.condition = RendererContracts_Register( 0 ); + for ( int i = 0; i < 4; ++i ) { + first.color[ i ] = RendererContracts_Register( 1 + i ); + } + for ( int i = 0; i < 6; ++i ) { + first.textureMatrix[ i ] = RendererContracts_Register( 5 + i ); + } + first.blend.enabled = true; + first.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA; + first.blend.destinationColor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + first.blend.colorOperation = RENDERER_BLEND_OP_REVERSE_SUBTRACT; + first.blend.sourceAlpha = RENDERER_BLEND_SRC_ALPHA_SATURATE; + first.blend.destinationAlpha = RENDERER_BLEND_ZERO; + first.blend.alphaOperation = RENDERER_BLEND_OP_MAX; + first.depth.testEnabled = true; + first.depth.writeEnabled = false; + first.depth.compareOperation = RENDERER_COMPARE_EQUAL; + first.cull = RENDERER_CULL_FRONT; + first.colorWriteMask = RENDERER_COLOR_WRITE_RED | RENDERER_COLOR_WRITE_ALPHA; + first.alphaTestEnabled = true; + first.alphaTestCompareOperation = RENDERER_COMPARE_GREATER_OR_EQUAL; + first.alphaTest = RendererContracts_Register( 11 ); + first.texgen = RENDERER_TEXGEN_SCREEN; + first.vertexColor = RENDERER_VERTEX_COLOR_MODULATE; + first.polygonOffsetEnabled = true; + first.polygonOffsetFactor = RendererContracts_Register( 12 ); + first.polygonOffsetUnits = RendererContracts_Register( 13 ); + first.programFamily = RENDERER_PROGRAM_INTERACTION; + first.programKey = 0x33445566u; + + rendererMaterialPass_t second = RendererContracts_DefaultMaterialPass(); + second.sourceStageIndex = 9; + second.kind = RENDERER_MATERIAL_PASS_SURFACE; + second.textureSemantic = RENDERER_TEXTURE_DIFFUSE; + second.textureResourceId = 0x202u; + second.condition = RendererContracts_Register( 14 ); + second.color[ 0 ] = RendererContracts_Constant( 0.25f ); + second.programFamily = RENDERER_PROGRAM_AMBIENT; + second.programKey = 0x77889900u; + + Check( RendererContracts_AppendMaterialPass( list, first ) + && RendererContracts_AppendMaterialPass( list, second ), + "evaluation pass list setup failed" ); +} + +static bool EvaluateConstantMaterialPass( rendererMaterialPassList_t &list, + rendererEvaluatedMaterialPassList_t &evaluated, + const rendererMaterialPass_t &pass ) { + RendererContracts_ResetMaterialPassList( list ); + return RendererContracts_AppendMaterialPass( list, pass ) + && RendererContracts_EvaluateMaterialPassList( + evaluated, list, NULL, 0 ) == RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS; +} + +static void ExerciseMaterialPassDispositionContract( void ) { + rendererMaterialPassList_t list; + rendererEvaluatedMaterialPassList_t evaluated; + rendererMaterialPass_t pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.count == 1 && evaluated.activeCount == 1 + && evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_DRAW, + "an enabled default pass must have draw disposition" ); + + pass.condition = RendererContracts_Constant( 0.0f ); + pass.blend.enabled = true; + pass.blend.sourceColor = RENDERER_BLEND_ZERO; + pass.blend.destinationColor = RENDERER_BLEND_ONE; + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.count == 1 && evaluated.activeCount == 0 + && !evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_INACTIVE_CONDITION, + "an exactly zero condition must take precedence and mark the pass inactive" ); + + pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + pass.condition = RendererContracts_Constant( -0.0f ); + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && !evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_INACTIVE_CONDITION, + "negative zero must be treated as the exact inactive condition" ); + pass.condition = RendererContracts_Constant( -0.00001f ); + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_DRAW, + "a finite nonzero condition must remain active regardless of sign" ); + + pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + pass.blend.enabled = true; + pass.blend.sourceColor = RENDERER_BLEND_ZERO; + pass.blend.destinationColor = RENDERER_BLEND_ONE; + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.activeCount == 1 && evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND, + "zero/one framebuffer-preserving blend must have no-op disposition" ); + + pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + pass.blend.enabled = true; + pass.blend.sourceColor = RENDERER_BLEND_ONE; + pass.blend.destinationColor = RENDERER_BLEND_ONE; + pass.color[ 0 ] = RendererContracts_Constant( 0.0f ); + pass.color[ 1 ] = RendererContracts_Constant( -0.25f ); + pass.color[ 2 ] = RendererContracts_Constant( 0.0f ); + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.activeCount == 1 && evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE, + "non-positive black additive color must have no-op disposition" ); + + pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + pass.blend.enabled = true; + pass.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA; + pass.blend.destinationColor = RENDERER_BLEND_ONE_MINUS_SRC_ALPHA; + pass.color[ 3 ] = RendererContracts_Constant( 0.0f ); + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.activeCount == 1 && evaluated.passes[ 0 ].active + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA, + "transparent source-alpha blend must have no-op disposition" ); + + pass = RendererContracts_DefaultMaterialPass(); + pass.sourceStageIndex = 0; + pass.blend.enabled = true; + pass.blend.sourceColor = RENDERER_BLEND_SRC_ALPHA_SATURATE; + Check( EvaluateConstantMaterialPass( list, evaluated, pass ) + && evaluated.passes[ 0 ].blend.sourceColor == RENDERER_BLEND_SRC_ALPHA_SATURATE + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_DRAW, + "source-alpha-saturate must validate, survive evaluation, and remain drawable" ); +} + +static void ExerciseMaterialPassEvaluationContract( void ) { + rendererMaterialPassList_t list; + BuildEvaluationPassList( list ); + float registers[ 15 ] = { + 1.0f, + 0.10f, 0.20f, 0.30f, 0.40f, + 1.0f, 0.0f, 0.125f, 0.0f, 1.0f, 0.25f, + 0.60f, 1.50f, 2.50f, + 0.0f + }; + + rendererEvaluatedMaterialPassList_t evaluated; + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS, + "valid per-draw material passes must evaluate" ); + Check( evaluated.count == 2 && evaluated.activeCount == 1, + "inactive conditions must be retained without counting as active" ); + Check( evaluated.passes[ 0 ].order == 0 && evaluated.passes[ 1 ].order == 1 + && evaluated.passes[ 0 ].sourceStageIndex == 3 + && evaluated.passes[ 1 ].sourceStageIndex == 9 + && evaluated.passes[ 0 ].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && evaluated.passes[ 1 ].textureSemantic == RENDERER_TEXTURE_DIFFUSE + && evaluated.passes[ 0 ].textureResourceId == 0x101u + && evaluated.passes[ 1 ].textureResourceId == 0x202u, + "evaluation must preserve repeated semantics, source stages, and order" ); + Check( evaluated.passes[ 0 ].active && NearlyEqual( evaluated.passes[ 0 ].condition, 1.0f ) + && evaluated.passes[ 0 ].disposition == RENDERER_MATERIAL_PASS_DRAW + && !evaluated.passes[ 1 ].active && NearlyEqual( evaluated.passes[ 1 ].condition, 0.0f ) + && evaluated.passes[ 1 ].disposition == RENDERER_MATERIAL_PASS_INACTIVE_CONDITION, + "zero condition must mark a pass inactive without failing evaluation" ); + Check( NearlyEqual( evaluated.passes[ 0 ].color[ 0 ], 0.10f ) + && NearlyEqual( evaluated.passes[ 0 ].color[ 3 ], 0.40f ) + && NearlyEqual( evaluated.passes[ 1 ].color[ 0 ], 0.25f ), + "stage color registers and constants must resolve exactly" ); + Check( NearlyEqual( evaluated.passes[ 0 ].textureMatrix[ 0 ], 1.0f ) + && NearlyEqual( evaluated.passes[ 0 ].textureMatrix[ 2 ], 0.125f ) + && NearlyEqual( evaluated.passes[ 0 ].textureMatrix[ 5 ], 0.25f ), + "texture-matrix registers must resolve exactly" ); + Check( evaluated.passes[ 0 ].kind == RENDERER_MATERIAL_PASS_INTERACTION + && evaluated.passes[ 0 ].blend.enabled + && evaluated.passes[ 0 ].blend.sourceColor == RENDERER_BLEND_SRC_ALPHA + && evaluated.passes[ 0 ].blend.destinationColor == RENDERER_BLEND_ONE_MINUS_SRC_ALPHA + && evaluated.passes[ 0 ].blend.colorOperation == RENDERER_BLEND_OP_REVERSE_SUBTRACT + && evaluated.passes[ 0 ].blend.sourceAlpha == RENDERER_BLEND_SRC_ALPHA_SATURATE + && evaluated.passes[ 0 ].blend.alphaOperation == RENDERER_BLEND_OP_MAX + && evaluated.passes[ 0 ].depth.testEnabled + && !evaluated.passes[ 0 ].depth.writeEnabled + && evaluated.passes[ 0 ].depth.compareOperation == RENDERER_COMPARE_EQUAL + && evaluated.passes[ 0 ].cull == RENDERER_CULL_FRONT + && evaluated.passes[ 0 ].colorWriteMask == ( RENDERER_COLOR_WRITE_RED | RENDERER_COLOR_WRITE_ALPHA ), + "evaluation must preserve pass kind and fixed render state" ); + Check( evaluated.passes[ 0 ].alphaTestEnabled + && evaluated.passes[ 0 ].alphaTestCompareOperation == RENDERER_COMPARE_GREATER_OR_EQUAL + && NearlyEqual( evaluated.passes[ 0 ].alphaTest, 0.60f ) + && evaluated.passes[ 0 ].texgen == RENDERER_TEXGEN_SCREEN + && evaluated.passes[ 0 ].vertexColor == RENDERER_VERTEX_COLOR_MODULATE + && evaluated.passes[ 0 ].polygonOffsetEnabled + && NearlyEqual( evaluated.passes[ 0 ].polygonOffsetFactor, 1.50f ) + && NearlyEqual( evaluated.passes[ 0 ].polygonOffsetUnits, 2.50f ) + && evaluated.passes[ 0 ].programFamily == RENDERER_PROGRAM_INTERACTION + && evaluated.passes[ 0 ].programKey == 0x33445566u, + "evaluation must resolve optional values and preserve program state" ); + + RendererContracts_ResetMaterialPassList( list ); + rendererMaterialPass_t constantPass = RendererContracts_DefaultMaterialPass(); + constantPass.sourceStageIndex = 0; + constantPass.condition = RendererContracts_UnusedRegister(); + for ( int i = 0; i < 4; ++i ) { + constantPass.color[ i ] = RendererContracts_UnusedRegister(); + } + for ( int i = 0; i < 6; ++i ) { + constantPass.textureMatrix[ i ] = RendererContracts_UnusedRegister(); + } + constantPass.alphaTest = RendererContracts_UnusedRegister(); + constantPass.polygonOffsetFactor = RendererContracts_UnusedRegister(); + constantPass.polygonOffsetUnits = RendererContracts_UnusedRegister(); + Check( RendererContracts_AppendMaterialPass( list, constantPass ), + "constant-only pass setup failed" ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, NULL, 0 ) == RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS + && evaluated.count == 1 && evaluated.activeCount == 1 + && NearlyEqual( evaluated.passes[ 0 ].color[ 0 ], 1.0f ) + && NearlyEqual( evaluated.passes[ 0 ].textureMatrix[ 0 ], 1.0f ) + && NearlyEqual( evaluated.passes[ 0 ].textureMatrix[ 4 ], 1.0f ), + "unused references must resolve to neutral state without a register array" ); + + RendererContracts_ResetMaterialPassList( list ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, NULL, 0 ) == RENDERER_MATERIAL_PASS_EVALUATION_SUCCESS + && evaluated.count == 0 && evaluated.activeCount == 0, + "an empty list must evaluate safely without registers" ); + + BuildEvaluationPassList( list ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, NULL, 0 ) == RENDERER_MATERIAL_PASS_EVALUATION_REGISTERS_UNAVAILABLE + && evaluated.count == 0 && evaluated.activeCount == 0, + "indexed passes must reject a missing register array atomically" ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, NULL, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_REGISTERS_UNAVAILABLE + && evaluated.count == 0, + "a nonzero bound with a null register pointer must fail closed" ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 14 ) == RENDERER_MATERIAL_PASS_EVALUATION_REGISTER_OUT_OF_RANGE + && evaluated.count == 0, + "out-of-range register references must reject the complete list" ); + + BuildEvaluationPassList( list ); + list.passes[ 0 ].color[ 2 ] = RendererContracts_Register( -1 ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS + && evaluated.count == 0, + "negative register references must fail closed" ); + BuildEvaluationPassList( list ); + list.passes[ 0 ].condition.source = static_cast( 99 ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS + && evaluated.count == 0, + "unknown register reference sources must fail closed" ); + BuildEvaluationPassList( list ); + list.passes[ 0 ].alphaTestCompareOperation = static_cast( 99 ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_INVALID_PASS + && evaluated.count == 0, + "malformed alpha-test comparison must reject evaluation atomically" ); + + BuildEvaluationPassList( list ); + registers[ 3 ] = std::numeric_limits::quiet_NaN(); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE + && evaluated.count == 0, + "non-finite register values must reject the complete list" ); + registers[ 3 ] = 0.30f; + list.passes[ 1 ].color[ 2 ] = RendererContracts_Constant( + ( std::numeric_limits::infinity )() ); + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_NONFINITE_VALUE + && evaluated.count == 0, + "non-finite constants must reject the complete list" ); + + BuildEvaluationPassList( list ); + list.overflowed = true; + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_SOURCE_OVERFLOW + && evaluated.count == 0, + "an overflowed source list must fail closed" ); + BuildEvaluationPassList( list ); + list.count = RENDERER_CONTRACT_MAX_MATERIAL_PASSES + 1; + Check( RendererContracts_EvaluateMaterialPassList( + evaluated, list, registers, 15 ) == RENDERER_MATERIAL_PASS_EVALUATION_SOURCE_OVERFLOW + && evaluated.count == 0, + "an out-of-bounds source count must fail before reading passes" ); +} + +static void ExerciseClipSpaceContract( void ) { + const rendererClipSpaceConvention_t gl = RendererContracts_GLClipSpace(); + const rendererClipSpaceConvention_t vk = RendererContracts_VulkanClipSpace(); + float identity[ 16 ]; + std::memset( identity, 0, sizeof( identity ) ); + identity[ 0 ] = identity[ 5 ] = identity[ 10 ] = identity[ 15 ] = 1.0f; + + float converted[ 16 ]; + Check( RendererContracts_ConvertClipMatrix( converted, identity, gl, vk ), + "GL to Vulkan clip conversion failed" ); + Check( NearlyEqual( converted[ 10 ], 0.5f ) && NearlyEqual( converted[ 14 ], 0.5f ) + && NearlyEqual( converted[ 0 ], 1.0f ) && NearlyEqual( converted[ 15 ], 1.0f ), + "GL to Vulkan depth conversion must map z from [-w,w] to [0,w]" ); + Check( RendererContracts_ConvertClipMatrix( converted, converted, vk, gl ), + "in-place Vulkan to GL clip conversion failed" ); + for ( int i = 0; i < 16; ++i ) { + Check( NearlyEqual( converted[ i ], identity[ i ] ), + "round-trip clip conversion changed the matrix" ); + } + Check( RendererContracts_ConvertClipMatrix( converted, identity, gl, gl ) + && std::memcmp( converted, identity, sizeof( identity ) ) == 0, + "same-convention clip conversion must be an exact no-op" ); + + rendererCanonicalViewport_t source = { 12.0f, 34.0f, 640.0f, 360.0f, 0.1f, 0.9f }; + rendererBackendViewport_t viewport; + Check( RendererContracts_BuildViewport( viewport, source, 1080.0f, gl ) + && NearlyEqual( viewport.y, 34.0f ) && NearlyEqual( viewport.height, 360.0f ), + "GL viewport must preserve lower-left coordinates" ); + Check( RendererContracts_BuildViewport( viewport, source, 1080.0f, vk ) + && NearlyEqual( viewport.y, 1046.0f ) && NearlyEqual( viewport.height, -360.0f ) + && NearlyEqual( viewport.minDepth, 0.1f ) && NearlyEqual( viewport.maxDepth, 0.9f ), + "Vulkan viewport must use the shared upper-left negative-height convention" ); +} + +static void ExerciseVertexLayoutContract( void ) { + const rendererVertexLayoutDesc_t &legacy = RendererContracts_LegacyDrawVertLayout(); + Check( RendererContracts_ValidateVertexLayout( legacy ), + "legacy idDrawVert layout must validate" ); + Check( legacy.bindingCount == 1 && legacy.bindings[ 0 ].stride == 64 + && legacy.attributeCount == 6, + "legacy idDrawVert descriptor shape mismatch" ); + const rendererVertexAttributeDesc_t *texcoord = RendererContracts_FindVertexAttribute( + legacy, RENDERER_VERTEX_SEMANTIC_TEXCOORD0 ); + Check( texcoord != NULL && texcoord->format == RENDERER_VERTEX_FORMAT_FLOAT32X2 + && texcoord->offset == 56, + "legacy idDrawVert texcoord descriptor mismatch" ); + + const rendererVertexLayoutDesc_t &skinned = RendererContracts_SkinnedDrawVertLayout(); + Check( RendererContracts_ValidateVertexLayout( skinned ) + && skinned.bindingCount == 2 && skinned.bindings[ 1 ].stride == 32, + "skinned vertex layout must expose its dedicated 32-byte binding" ); + const rendererVertexAttributeDesc_t *joints = RendererContracts_FindVertexAttribute( + skinned, RENDERER_VERTEX_SEMANTIC_JOINT_INDICES ); + const rendererVertexAttributeDesc_t *weights = RendererContracts_FindVertexAttribute( + skinned, RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS ); + Check( joints != NULL && joints->binding == 1 && joints->offset == 0 + && joints->format == RENDERER_VERTEX_FORMAT_UINT32X4, + "skin joint indices must be full uint32x4 values" ); + Check( weights != NULL && weights->binding == 1 && weights->offset == 16 + && weights->format == RENDERER_VERTEX_FORMAT_FLOAT32X4, + "skin weights must be full float32x4 values" ); + + rendererVertexLayoutDesc_t invalid = legacy; + invalid.attributes[ 1 ].semantic = invalid.attributes[ 0 ].semantic; + Check( !RendererContracts_ValidateVertexLayout( invalid ), + "duplicate vertex semantics must fail validation" ); + invalid = legacy; + invalid.attributes[ 5 ].offset = 60; + Check( !RendererContracts_ValidateVertexLayout( invalid ), + "out-of-bounds vertex attributes must fail validation" ); + invalid = legacy; + invalid.attributes[ 2 ].offset = 8; + Check( !RendererContracts_ValidateVertexLayout( invalid ), + "overlapping vertex attributes must fail validation" ); +} + +static void ExerciseBufferHandleContract( void ) { + const rendererBufferHandle_t handle = RendererContracts_MakeBufferHandle( + RENDERER_BUFFER_KIND_VERTEX, RENDERER_BUFFER_LIFETIME_FRAME, 12, 7 ); + rendererBufferRecord_t record = { handle, 4096, 99 }; + rendererBufferSlice_t slice = { handle, 128, 512 }; + Check( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 99 ) == RENDERER_BUFFER_SLICE_VALID, + "valid typed buffer slice was rejected" ); + + slice.handle = RendererContracts_MakeBufferHandle( + RENDERER_BUFFER_KIND_VERTEX, RENDERER_BUFFER_LIFETIME_FRAME, 12, 8 ); + Check( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 99 ) == RENDERER_BUFFER_SLICE_STALE_GENERATION, + "stale buffer generation must fail validation" ); + + slice.handle = RendererContracts_MakeBufferHandle( + RENDERER_BUFFER_KIND_INDEX, RENDERER_BUFFER_LIFETIME_FRAME, 12, 7 ); + Check( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 99 ) == RENDERER_BUFFER_SLICE_WRONG_KIND, + "wrong buffer kind must fail validation" ); + + slice.handle = handle; + Check( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 100 ) == RENDERER_BUFFER_SLICE_EXPIRED, + "frame buffer slice must expire with its frame serial" ); + + record.capacityBytes = ( std::numeric_limits::max )(); + slice.offsetBytes = record.capacityBytes - 2; + slice.sizeBytes = 8; + Check( RendererContracts_ValidateBufferSlice( slice, record, + RENDERER_BUFFER_KIND_VERTEX, 99 ) == RENDERER_BUFFER_SLICE_RANGE_OVERFLOW, + "overflowing buffer slice range must fail without wrapping" ); +} + +int main() { + Check( RendererContracts_RunSelfTest(), + "shared runtime self-test must pass without a graphics device" ); + ExerciseMaterialPassContract(); + ExerciseMaterialPassDispositionContract(); + ExerciseMaterialPassEvaluationContract(); + ExerciseClipSpaceContract(); + ExerciseVertexLayoutContract(); + ExerciseBufferHandleContract(); + if ( failures != 0 ) { + std::fprintf( stderr, "RendererContractsTest: %d failure(s)\n", failures ); + return 1; + } + std::printf( "RendererContractsTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/TemporalHistoryCoreTest.cpp b/tools/tests/native/TemporalHistoryCoreTest.cpp new file mode 100644 index 00000000..ffa84022 --- /dev/null +++ b/tools/tests/native/TemporalHistoryCoreTest.cpp @@ -0,0 +1,221 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/TemporalHistoryCore.h" + +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "TemporalHistoryCoreTest: %s\n", message ); + failures++; +} + +static temporalCameraState_t Camera( int frameNumber ) { + temporalCameraState_t state = {}; + state.valid = true; + state.viewIdentity = UINT64_C( 0x1234 ); + state.historyGeneration = 7; + state.frameNumber = frameNumber; + state.renderTimeMsec = frameNumber * 16; + state.outputWidth = 1920; + state.outputHeight = 1080; + state.sceneWidth = 1440; + state.sceneHeight = 808; + state.viewAxis[0] = 1.0f; + state.viewAxis[4] = 1.0f; + state.viewAxis[8] = 1.0f; + state.fovX = 90.0f; + state.fovY = 60.0f; + return state; +} + +int main() { + const temporalJitterSample_t jitter0 = TemporalHistoryCore_Jitter( 0 ); + const temporalJitterSample_t jitter8 = TemporalHistoryCore_Jitter( 8 ); + Check( jitter0.x == jitter8.x && jitter0.y == jitter8.y, + "the jitter sequence must repeat exactly after eight frames" ); + for ( std::uint32_t i = 0; i < 8; ++i ) { + const temporalJitterSample_t sample = TemporalHistoryCore_Jitter( i ); + Check( sample.x >= -0.5f && sample.x <= 0.5f + && sample.y >= -0.5f && sample.y <= 0.5f, + "jitter must remain inside one pixel" ); + } + + const temporalCameraPolicy_t policy = TemporalHistoryCore_DefaultCameraPolicy(); + temporalCameraState_t previous = Camera( 10 ); + temporalCameraState_t current = Camera( 11 ); + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_NONE, + "ordinary adjacent camera samples must retain history" ); + current.renderTimeMsec = previous.renderTimeMsec; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_NONE, + "high-refresh presentation frames may share one simulation time" ); + + previous.valid = false; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_FIRST_SAMPLE, + "the first camera sample must reject uninitialized history" ); + previous = Camera( 10 ); + current.historyGeneration++; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_GENERATION, + "a generation change must reject stale image history" ); + current = Camera( 11 ); + current.sceneWidth = 1280; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_NONE, + "a scene-extent transition must retain native-resolution history" ); + current = Camera( 11 ); + current.outputWidth = 2560; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_OUTPUT_EXTENT, + "a native output-extent transition must reject image history" ); + current = Camera( 11 ); + current.viewOrigin[0] = 513.0f; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_CAMERA_TRANSLATION, + "a teleport must reset history" ); + current = Camera( 11 ); + current.viewAxis[0] = 0.0f; + current.viewAxis[1] = 1.0f; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_CAMERA_ROTATION, + "a hard camera cut must reset history" ); + current = Camera( 11 ); + current.fovX += 1.0f; + Check( TemporalHistoryCore_ValidateCameraHistory( current, previous, policy ) + == TEMPORAL_HISTORY_RESET_PROJECTION, + "an authored projection discontinuity must reset history" ); + + temporalMotionInput_t input = {}; + temporalMotionOwnership_t ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_STATIC_WORLD + && ownership.source == TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH, + "static world motion must be reconstructed from depth" ); + + input = {}; + input.hasEntity = true; + input.hasPreviousTransform = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_RIGID + && ownership.source == TEMPORAL_MOTION_SOURCE_RIGID_TRANSFORM + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_HAS_PREVIOUS_TRANSFORM ) != 0, + "rigid motion must own a stable previous transform" ); + + input = {}; + input.skinned = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_SKINNED + && ownership.source == TEMPORAL_MOTION_SOURCE_CAMERA_DEPTH + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) != 0, + "skinned geometry without a prior palette must fail safe to reactive history" ); + input.hasPreviousSkinningPalette = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.source == TEMPORAL_MOTION_SOURCE_SKINNED_PALETTE + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) == 0, + "skinned geometry with prior joints must own palette motion" ); + + input = {}; + input.particle = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_PARTICLE + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) != 0, + "particles must explicitly reject stale history" ); + input = {}; + input.deform = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_DEFORM + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) != 0, + "deforms without prior vertices must explicitly reject stale history" ); + input = {}; + input.subview = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_SUBVIEW + && ownership.source == TEMPORAL_MOTION_SOURCE_INDEPENDENT_VIEW + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY ) != 0, + "subviews must never borrow their parent history" ); + input = {}; + input.inWorldGui = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_IN_WORLD_GUI + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_REACTIVE ) != 0, + "in-world GUI must remain reactive" ); + input = {}; + input.viewModel = true; + input.hasPreviousTransform = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + Check( ownership.domain == TEMPORAL_MOTION_DOMAIN_VIEW_MODEL + && ( ownership.flags & TEMPORAL_MOTION_OWNERSHIP_DEPTH_HACK ) != 0, + "view models must preserve their depth-hack ownership" ); + + temporalViewMotionPolicy_t motionPolicy = + TemporalHistoryCore_BeginViewMotionPolicy(); + input = {}; + input.hasEntity = true; + input.hasPreviousTransform = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + const unsigned int rigidDomain = TemporalHistoryCore_MotionDomainBit( + TEMPORAL_MOTION_DOMAIN_RIGID ); + TemporalHistoryCore_AddMotionOwnership( motionPolicy, ownership, + rigidDomain, 0.10f, 0.10f, 0.20f, 0.20f ); + Check( motionPolicy.exactMotionDomainMask == rigidDomain + && motionPolicy.reactiveRegionCount == 0, + "a backend with exact rigid vectors must retain history outside explicit reactive flags" ); + + motionPolicy = TemporalHistoryCore_BeginViewMotionPolicy(); + TemporalHistoryCore_AddMotionOwnership( motionPolicy, ownership, 0u, + 0.10f, 0.10f, 0.20f, 0.20f ); + Check( motionPolicy.reactiveDomainMask == rigidDomain + && motionPolicy.reactiveRegionCount == 1, + "a backend without rigid vectors must reject history over the rigid packet scissor" ); + + input = {}; + input.skinned = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + TemporalHistoryCore_AddMotionOwnership( motionPolicy, ownership, 0u, + 0.70f, 0.70f, 0.80f, 0.80f ); + input = {}; + input.particle = true; + ownership = TemporalHistoryCore_ClassifyMotion( input ); + TemporalHistoryCore_AddMotionOwnership( motionPolicy, ownership, 0u, + 0.40f, 0.40f, 0.50f, 0.50f ); + Check( motionPolicy.reactiveRegionCount == 2 + && motionPolicy.reactiveRegionsMerged, + "more than two reactive islands must merge conservatively into the fixed policy budget" ); + const unsigned int expectedReactiveDomains = rigidDomain + | TemporalHistoryCore_MotionDomainBit( TEMPORAL_MOTION_DOMAIN_SKINNED ) + | TemporalHistoryCore_MotionDomainBit( TEMPORAL_MOTION_DOMAIN_PARTICLE ); + Check( motionPolicy.reactiveDomainMask == expectedReactiveDomains, + "every accumulated unsupported motion domain must remain visible to the backend" ); + + motionPolicy = TemporalHistoryCore_BeginViewMotionPolicy(); + TemporalHistoryCore_AddMotionOwnership( motionPolicy, ownership, 0u, + 0.5f, 0.5f, 0.5f, 0.5f ); + Check( motionPolicy.reactiveRegionCount == 1 + && motionPolicy.reactiveRegions[0].x1 == 0.0f + && motionPolicy.reactiveRegions[0].y1 == 0.0f + && motionPolicy.reactiveRegions[0].x2 == 1.0f + && motionPolicy.reactiveRegions[0].y2 == 1.0f, + "a missing packet scissor must fail safe to full-frame history rejection" ); + + std::uint64_t identityA = TemporalHistoryCore_BeginIdentity(); + identityA = TemporalHistoryCore_MixIdentity( identityA, 1 ); + identityA = TemporalHistoryCore_MixIdentity( identityA, 2 ); + std::uint64_t identityB = TemporalHistoryCore_BeginIdentity(); + identityB = TemporalHistoryCore_MixIdentity( identityB, 1 ); + identityB = TemporalHistoryCore_MixIdentity( identityB, 2 ); + Check( identityA != 0 && identityA == identityB, + "view identity hashing must be deterministic" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "TemporalHistoryCoreTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/native/TemporalPresentationCoreTest.cpp b/tools/tests/native/TemporalPresentationCoreTest.cpp new file mode 100644 index 00000000..c6881c10 --- /dev/null +++ b/tools/tests/native/TemporalPresentationCoreTest.cpp @@ -0,0 +1,158 @@ +// Copyright (C) 2026 DarkMatter Productions +// + +#include "src/renderer/TemporalPresentationCore.h" + +#include + +static int failures = 0; + +static void Check( bool condition, const char *message ) { + if ( condition ) { + return; + } + std::fprintf( stderr, "TemporalPresentationCoreTest: %s\n", message ); + failures++; +} + +static temporalResolutionConfig_t DefaultConfig() { + temporalResolutionConfig_t config = {}; + config.enabled = true; + config.minimumScalePercent = 50; + config.maximumScalePercent = 100; + config.dropStepPercent = 10; + config.raiseStepPercent = 2; + config.raiseFrames = 3; + config.raiseThresholdPercent = 85; + config.missingSampleResetFrames = 120; + config.maximumSampleAgeFrames = 32; + config.targetMicroseconds = 16000; + config.dimensionAlignment = 8; + return config; +} + +static temporalResolutionSample_t Sample( int frame, std::uint32_t generation, + std::uint64_t microseconds, int scalePercent = 100, int ageFrames = 4 ) { + temporalResolutionSample_t sample = {}; + sample.supported = true; + sample.valid = true; + sample.backend = 1; + sample.frameNumber = frame; + sample.generation = generation; + sample.elapsedMicroseconds = microseconds; + sample.scalePercent = scalePercent; + sample.ageFrames = ageFrames; + return sample; +} + +int main() { + temporalResolutionState_t state; + TemporalResolutionCore_Initialize( state ); + temporalResolutionConfig_t config = DefaultConfig(); + + temporalResolutionSample_t sample = Sample( 10, 3, 17000 ); + temporalResolutionOutput_t output = TemporalResolutionCore_Update( + state, config, sample, 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET + && output.scalePercent == 100 && output.width == 1920 + && output.height == 1080, + "the first retired sample must establish a native-resolution generation" ); + + sample = Sample( 11, 3, 20000 ); + output = TemporalResolutionCore_Update( state, config, sample, 1920, 1080, false, false ); + Check( output.sampleConsumed + && output.decision == TEMPORAL_RESOLUTION_DECISION_DROP + && output.scalePercent == 90 && output.width == 1728 + && output.height == 968, + "an over-budget sample must drop quickly and align both dimensions" ); + + output = TemporalResolutionCore_Update( state, config, sample, 1920, 1080, false, false ); + Check( !output.sampleConsumed + && output.decision == TEMPORAL_RESOLUTION_DECISION_WAITING_FOR_SAMPLE + && output.scalePercent == 90, + "a delayed timing sample must be consumed at most once" ); + + for ( int frame = 12; frame <= 13; frame++ ) { + output = TemporalResolutionCore_Update( + state, config, Sample( frame, 3, 10000, 90 ), 1920, 1080, false, false ); + Check( output.scalePercent == 90 + && output.decision == TEMPORAL_RESOLUTION_DECISION_HOLD, + "under-budget hysteresis must hold before its full streak" ); + } + output = TemporalResolutionCore_Update( + state, config, Sample( 14, 3, 10000, 90 ), 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_RAISE + && output.scalePercent == 92, + "a complete under-budget streak must raise slowly" ); + + output = TemporalResolutionCore_Update( + state, config, Sample( 15, 3, 15000, 92 ), 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_HOLD + && state.belowBudgetFrames == 0, + "the target deadband must break a pending raise streak" ); + + output = TemporalResolutionCore_Update( + state, config, Sample( 16, 4, 30000 ), 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET + && output.scalePercent == 100 && state.discontinuityResets == 2, + "a timing-generation change must reset rather than consume an outlier" ); + + output = TemporalResolutionCore_Update( + state, config, Sample( 17, 4, 30000 ), 1920, 1080, true, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_FROZEN + && output.scalePercent == 100 && output.width == 1920 + && output.height == 1080 && state.lastSampleFrame == 16, + "ordinary captures must freeze rather than consume timing or mutate scale" ); + + output = TemporalResolutionCore_Update( + state, config, Sample( 17, 4, 30000 ), 1920, 1080, true, true ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_FORCED_NATIVE + && output.scalePercent == 100 && output.width == 1920 + && output.height == 1080, + "an explicit capture-quality override must force native scene resolution" ); + + // Return to a reduced scale, then prove that capture freezing preserves it. + output = TemporalResolutionCore_Update( + state, config, Sample( 17, 4, 30000, 100 ), 1920, 1080, false, false ); + Check( output.scalePercent == 90, "post-capture timing must remain consumable" ); + output = TemporalResolutionCore_Update( + state, config, Sample( 18, 4, 30000, 90 ), 320, 240, true, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_FROZEN + && output.scalePercent == 90 && output.width == 288 + && output.height == 216 && state.nativeWidth == 1920, + "save-preview dimensions must not reset the gameplay controller" ); + + output = TemporalResolutionCore_Update( + state, config, Sample( 15, 4, 1000, 90 ), 1920, 1080, false, false ); + Check( !output.sampleConsumed && output.scalePercent == 90 + && state.rejectedSamples == 1, + "an out-of-order retired sample must never perturb the controller" ); + + config.minimumScalePercent = 80; + output = TemporalResolutionCore_Update( + state, config, Sample( 19, 4, 30000 ), 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_DISCONTINUITY_RESET + && output.scalePercent == 100, + "a live controller configuration change must restart at its safe ceiling" ); + + config.enabled = false; + output = TemporalResolutionCore_Update( + state, config, Sample( 20, 4, 30000 ), 1920, 1080, false, false ); + Check( output.decision == TEMPORAL_RESOLUTION_DECISION_DISABLED + && output.scalePercent == 100, + "the default-off controller must preserve native resolution" ); + + Check( TemporalResolutionCore_AlignedDimension( 13, 50, 8 ) == 7, + "alignment must never collapse a small dimension to zero" ); + Check( TemporalResolutionCore_AlignedDimension( 1920, 75, 8 ) == 1440, + "aligned dimension calculation mismatch" ); + Check( TemporalResolutionCore_AlignedDimension( 1366, 100, 8 ) == 1366 + && TemporalResolutionCore_AlignedDimension( 767, 100, 8 ) == 767, + "100 percent scale must preserve the exact native extent" ); + + if ( failures != 0 ) { + return 1; + } + std::printf( "TemporalPresentationCoreTest: PASS\n" ); + return 0; +} diff --git a/tools/tests/network_ipv4_support.py b/tools/tests/network_ipv4_support.py index 401a1128..05ad99bc 100644 --- a/tools/tests/network_ipv4_support.py +++ b/tools/tests/network_ipv4_support.py @@ -311,7 +311,7 @@ def validate_idport_platform_data_mirror() -> None: if game_api.is_file(): require( game_api.read_text(encoding="utf-8", errors="strict"), - "const int GAME_API_VERSION\t\t= 43;", + "const int GAME_API_VERSION\t\t= 45;", "idPort-layout game API revision", ) @@ -1501,7 +1501,7 @@ def validate_postinit_connect_runtime_lane() -> None: for token in ( "POSTINIT_CONNECT_WAIT_FRAMES = 30", "POSTINIT_RECONNECT_WAIT_FRAMES = 30", - "POSTINIT_SERVER_GRACE_MSEC = 90000", + "MP_SERVER_CLIENT_GRACE_MSEC = 90000", ): require(source, token, "post-init IPv4 benchmark delay") scene_start = source.index('"mp-q4dm1-postinit-connect": {') @@ -1584,7 +1584,7 @@ def validate_postinit_connect_runtime_lane() -> None: "post-init IPv4 dry-run evidence", ) for token in ( - 'server_exec_commands += (f"waitMsec {POSTINIT_SERVER_GRACE_MSEC}",)', + 'f"waitMsec {MP_SERVER_CLIENT_GRACE_MSEC}",', 'client_capture_index = 1 if spec.path_name == "postinit-connect" else 0', "write_postinit_reconnect_cfg(", "client_initial_autoexec_cfg = client_reconnect_cfg", @@ -1606,7 +1606,7 @@ def validate_postinit_connect_runtime_lane() -> None: require(runner, token, "post-init IPv4 connect/reconnect evidence") require_before( runner, - 'server_exec_commands += (f"waitMsec {POSTINIT_SERVER_GRACE_MSEC}",)', + 'f"waitMsec {MP_SERVER_CLIENT_GRACE_MSEC}",', "server_autoexec_cfg, server_screenshot = write_autoexec_cfg(", "post-init IPv4 server grace", ) diff --git a/tools/tests/network_security.py b/tools/tests/network_security.py new file mode 100644 index 00000000..60a09a61 --- /dev/null +++ b/tools/tests/network_security.py @@ -0,0 +1,897 @@ +#!/usr/bin/env python3 +"""Focused source contracts for connection, rcon2, and private-CVar security.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +GAME_LIBS_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", ROOT.parent / "openQ4-game") +).resolve() + + +def read(relative_path: str) -> str: + path = ROOT / relative_path + if not path.is_file(): + raise AssertionError(f"Required file not found: {path}") + return path.read_text(encoding="utf-8", errors="strict") + + +def read_game(relative_path: str) -> str: + path = GAME_LIBS_ROOT / relative_path + if not path.is_file(): + raise AssertionError(f"Required openQ4-game file not found: {path}") + return path.read_text(encoding="utf-8", errors="strict") + + +def require(text: str, token: str, context: str) -> None: + if token not in text: + raise AssertionError(f"Missing {token!r} in {context}") + + +def reject(text: str, token: str, context: str) -> None: + if token in text: + raise AssertionError(f"Unexpected {token!r} in {context}") + + +def require_before(text: str, first: str, second: str, context: str) -> None: + require(text, first, context) + require(text, second, context) + if text.index(first) >= text.index(second): + raise AssertionError(f"Expected {first!r} before {second!r} in {context}") + + +def function_body(source: str, signature: str, context: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"Missing {signature!r} in {context}") + opening = source.find("{", start + len(signature)) + if opening < 0: + raise AssertionError(f"Missing body for {signature!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Unbalanced body for {signature!r} in {context}") + + +def validate_crypto_and_native_vectors() -> None: + crypto = read("src/idlib/CryptoHash.cpp") + protocol = read("src/framework/async/Rcon2Protocol.cpp") + protocol_header = read("src/framework/async/Rcon2Protocol.h") + native = read("tools/tests/native/CoreSafetyTest.cpp") + meson = read("meson.build") + + for token in ( + "original implementation of the published FIPS 180-4 SHA-256", + "void SHA256(", + "void HMACSHA256(", + "bool PBKDF2HMACSHA256(", + "bool ConstantTimeEquals(", + "void SecureZero(", + ): + require(crypto, token, "engine-owned cryptographic primitives") + require(protocol, 'PROOF_DOMAIN[] = "openQ4-rcon2-proof-v1"', "rcon2 domain separation") + require(protocol_header, "PBKDF2_ITERATIONS = 200000", "fixed rcon2 KDF work factor") + require(protocol_header, "MIN_PASSWORD_BYTES = 12", "minimum rcon2 password length") + + for token in ( + "RFC 4231 HMAC-SHA-256", + "PBKDF2-HMAC-SHA-256 c=1", + "PBKDF2-HMAC-SHA-256 c=2", + "PBKDF2-HMAC-SHA-256 c=4096", + "rcon2 proof domain", + "rcon2 request digest", + "SecureZero left data behind", + '"command ends in private-name prefix"', + '"private name is a longer token prefix"', + '"private name at command end"', + '"parent path escape"', + '"forward-slash path"', + '"backslash path"', + '"Windows device directory"', + '"oversized game-directory segment"', + ): + require(native, token, "native crypto/private-token coverage") + for token in ( + "src/idlib/CryptoHash.cpp", + "src/framework/async/Rcon2Protocol.cpp", + ): + require(meson, token, "native safety target sources") + + +def validate_secure_random_and_connection_challenges() -> None: + client = read("src/framework/async/AsyncClient.cpp") + server = read("src/framework/async/AsyncServer.cpp") + + for source, label in ((client, "async client"), (server, "async server")): + reject(source, "rand()", label) + reject(source, "idRandom random", label) + reject(source, "Sys_Milliseconds() & CONNECTIONLESS_MESSAGE_ID_MASK", label) + + client_id = function_body(client, "static bool AsyncClient_SecureConnectionId(", "client ID generator") + server_id = function_body(server, "static bool AsyncServer_SecureConnectionId(", "server ID generator") + issue = function_body(server, "void idAsyncServer::ProcessChallengeMessage(", "connection challenge issuance") + validate = function_body(server, "int idAsyncServer::ValidateChallenge(", "connection challenge validation") + download = function_body(client, "int idAsyncClient::GetDownloadRequest(", "download request ID") + + for body, label in ( + (client_id, "client ID generator"), + (server_id, "server ID generator"), + (issue, "connection challenge issuance"), + (download, "download request ID"), + ): + require(body, "Sys_GetSecureRandomBytes", label) + require(issue, "msg.GetRemainingData() != 4", "bounded challenge payload") + require(issue, "challenges[i].valid = true", "explicit challenge validity") + require(issue, "challenges[i].address = from", "challenge endpoint binding") + require(issue, "challenges[i].clientId = clientId", "challenge client-ID binding") + require(issue, "AsyncServer_Elapsed( serverTime, challenges[ i ].time )", "wrap-safe challenge age") + reject(issue, "challenges[i].time < oldestTime", "raw signed challenge eviction") + + for token in ( + "challenges[ i ].valid", + "!challenges[ i ].connected", + "AsyncServer_Elapsed( serverTime, challenges[ i ].time ) <= CONNECTION_CHALLENGE_TIMEOUT_MSEC", + "AsyncServer_SameEndpoint( from, challenges[i].address )", + "clientId == challenges[ i ].clientId", + "challenge == challenges[i].challenge", + ): + require(validate, token, "bound, expiring challenge validation") + require(validate, "AllowConnectionlessResponse( from, false )", "bounded bad-challenge reply") + require(download, "dlRequest = -1", "download request fail-closed path") + require(server, "AsyncServer_ClearChallenge( challenges[ ichallenge ] );", "one-use successful connection challenge") + + +def validate_oob_budgets() -> None: + server = read("src/framework/async/AsyncServer.cpp") + limiter = function_body(server, "bool idAsyncServer::AllowConnectionlessResponse(", "OOB response limiter") + info = function_body(server, "void idAsyncServer::ProcessGetInfoMessage(", "getInfo handler") + challenge = function_body(server, "void idAsyncServer::ProcessChallengeMessage(", "challenge handler") + rcon_challenge = function_body( + server, + "void idAsyncServer::ProcessRemoteConsole2ChallengeMessage(", + "rcon2 challenge handler", + ) + connect = function_body(server, "void idAsyncServer::ProcessConnectMessage(", "connect handler") + pure = function_body(server, "void idAsyncServer::ProcessPureMessage(", "pure handler") + download = function_body(server, "void idAsyncServer::ProcessDownloadRequestMessage(", "download handler") + connectionless = function_body(server, "bool idAsyncServer::ConnectionlessMessage(", "server OOB dispatcher") + process_message = function_body(server, "bool idAsyncServer::ProcessMessage(", "server packet dispatcher") + + for token in ( + "OOB_INFO_MAX_PER_SOURCE", + "OOB_CHALLENGE_MAX_PER_SOURCE", + "OOB_INFO_MAX_GLOBAL", + "OOB_CHALLENGE_MAX_GLOBAL", + "Sys_IsLANAddress( from )", + "oobInfoResponses >= OOB_INFO_MAX_GLOBAL", + "oobChallengeResponses >= OOB_CHALLENGE_MAX_GLOBAL", + ): + require(limiter, token, "per-source/global OOB response budget") + require(info, "msg.GetRemainingData() != 4", "bounded getInfo request") + require(info, "AllowConnectionlessResponse( from, true )", "bounded infoResponse") + require_before(challenge, "AllowConnectionlessResponse( from, false )", "serverPort.SendPacket", "challenge response budget") + require_before(rcon_challenge, "AllowConnectionlessResponse( from, false )", "serverPort.SendPacket", "rcon2 response budget") + require_before(connect, "ValidateChallenge( from, challenge, clientId )", "protocol != ASYNC_PROTOCOL_VERSION", "challenge before protocol reply") + require_before(connect, "OS < 0 || OS >= MAX_GAME_OS", "ValidateChallenge( from, challenge, clientId )", "client OS bounds before challenge state") + require_before(connect, "AllowConnectionlessResponse( from, false )", "protocol != ASYNC_PROTOCOL_VERSION", "connect reply budget") + require_before(connect, "AllowConnectionlessResponse( from, false )", "clientDataChecksum != serverDataChecksum", "checksum reply budget") + require(pure, "AllowConnectionlessResponse( from, false )", "pure failure reply budget") + require(download, "AllowConnectionlessResponse( from, false )", "download response budget") + inactive = connectionless[connectionless.index("if ( !active )") :] + require_before(inactive, "AllowConnectionlessResponse( from, false )", "PrintOOB", "inactive-server reply budget") + unknown_client = process_message[process_message.index("// if we received a sequenced packet") :] + require_before( + unknown_client, + "AllowConnectionlessResponse( from, false )", + "serverPort.SendPacket", + "unknown sequenced-client disconnect budget", + ) + + +def validate_usercmd_packet_bounds() -> None: + header = read("src/framework/async/AsyncNetwork.h") + network = read("src/framework/async/AsyncNetwork.cpp") + client = read("src/framework/async/AsyncClient.cpp") + server = read("src/framework/async/AsyncServer.cpp") + decoder = function_body( + network, + "bool idAsyncNetwork::ReadUserCmdDelta(", + "bounded user-command decoder", + ) + handler = function_body( + server, + "void idAsyncServer::ProcessUnreliableClientMessage(", + "server unreliable-client handler", + ) + sender = function_body( + client, + "void idAsyncClient::SendUsercmdsToServer(", + "client user-command sender", + ) + connect_response = function_body( + client, + "void idAsyncClient::ProcessConnectResponseMessage(", + "client connect-response handler", + ) + connect_request = function_body( + server, + "void idAsyncServer::ProcessConnectMessage(", + "server connect-request handler", + ) + snapshot_sender = function_body( + server, + "bool idAsyncServer::SendSnapshotToClient(", + "server snapshot sender", + ) + client_handler = function_body( + client, + "void idAsyncClient::ProcessUnreliableServerMessage(", + "client unreliable-server handler", + ) + game_init = client_handler[client_handler.index("case SERVER_UNRELIABLE_MESSAGE_GAMEINIT") :] + snapshot = client_handler[client_handler.index("case SERVER_UNRELIABLE_MESSAGE_SNAPSHOT") :] + + require(header, "MAX_USERCMD_PACKET_COMMANDS = 11", "wire user-command work bound") + require(header, "static bool\t\t\t\tReadUserCmdDelta", "fallible user-command decoder API") + require(client, "MAX_USERCMD_PACKET_COMMANDS - 1", "client packet-count bound") + require(sender, "gameFrame >= requestedUsercmds - 1 ? requestedUsercmds : gameFrame + 1", "startup user-command history clamp") + require(sender, "gameFrame > AsyncClient_MaxNetworkGameFrame()", "client frame-domain guard") + reject(sender, "Min( requestedUsercmds, gameFrame + 1 )", "overflow-prone startup history clamp") + require(network, "AsyncNetwork_CanReadUserCmdDelta", "user-command bit preflight") + require(network, "probe.GetRemainingReadBits() >= 152", "full-command bit bound") + require(decoder, "if ( !AsyncNetwork_CanReadUserCmdDelta", "decode before mutation") + require(decoder, "usercmd_t decoded;", "transactional user-command decode") + require(decoder, "cmd = decoded;", "commit only complete user-command") + require(handler, "msg.GetRemainingReadBits() < 64", "fixed unreliable-message prefix bound") + require(handler, "msg.GetRemainingReadBits() < 32 + 8", "snapshot/id header bound") + ping_handler = handler[handler.index("case CLIENT_UNRELIABLE_MESSAGE_PINGRESPONSE") :] + require_before(ping_handler, "msg.GetRemainingReadBits() < 32", "msg.ReadLong()", "ping-response payload bound") + require(handler, "msg.GetRemainingReadBits() < 16 + 32 + 8", "fixed user-command header bound") + require(handler, "numUsercmds > MAX_USERCMD_PACKET_COMMANDS", "server packet-count bound") + require(handler, "clientGameFrame < numUsercmds - 1", "negative user-command history rejection") + require( + handler, + "static_cast( clientGameFrame ) > static_cast( gameFrame ) + MAX_USERCMD_BACKUP", + "implausible future client-frame rejection", + ) + require(handler, "commandIndex < numUsercmds", "counted user-command loop") + require(handler, "if ( !idAsyncNetwork::ReadUserCmdDelta", "truncated client packet rejection") + require(handler, "echoedPingTime != client.lastPingTime", "ping response challenge binding") + require(handler, "AsyncServer_Elapsed( realTime, echoedPingTime )", "overflow-safe ping calculation") + reject(handler, "i <= clientGameFrame", "overflow-prone frame-sentinel loop") + require(client, "if ( !idAsyncNetwork::ReadUserCmdDelta", "truncated server snapshot rejection") + require(client, "i > MAX_ASYNC_CLIENTS", "snapshot relay terminator bound") + require(client, "i == MAX_ASYNC_CLIENTS", "snapshot relay exact terminator") + require(client, "numUsercmds < 1 || numUsercmds > MAX_USERCMD_RELAY", "snapshot relay count bound") + require(client, "server sent a truncated snapshot header", "snapshot fixed-header bound") + require(client, "server sent invalid snapshot timing", "snapshot timing-domain validation") + require(client, "server sent invalid game-init timing", "game-init timing-domain validation") + + teardown = function_body( + client, + "static void AsyncClient_StopAfterMalformedSnapshot(", + "malformed snapshot session teardown", + ) + require(teardown, "arenaCampaign.AbortMatch();", "malformed snapshot Arena rollback") + require(teardown, "session->Stop();", "malformed snapshot map teardown") + post_game_decode = snapshot[snapshot.index("if ( !game->ClientReadSnapshot(") :] + if post_game_decode.count("AsyncClient_StopAfterMalformedSnapshot();") != 4: + raise AssertionError( + "all post-game snapshot failures must tear down the session before the outer frame continues" + ) + reject(post_game_decode, "DisconnectFromServer();", "post-game snapshot channel-only teardown") + + require(client, "AsyncClient_MaxPredictionMsec", "bounded prediction arithmetic") + require(client, "const std::int64_t adjustedPredictTime", "widened prediction adjustment") + require(client, "adjustedPredictTime > maximumPredictionMsec", "prediction adjustment clamp") + require(client, "static ID_INLINE bool AsyncClient_ValidNetworkTiming", "tick-derived network timing helper") + require(client, "common->GetUserCmdMsecNumerator()", "exact user-command tick frame bound") + require(client, "common->GetUserCmdMSec()", "legacy user-command tick frame bound") + require(connect_response, "msg.GetRemainingReadBits() < 32 + 32 + 32 + 32", "connect-response fixed-header bound") + require(connect_response, "AsyncClient_ValidNetworkTiming( serverGameFrame, serverGameTime )", "connect-response timing bound") + require_before(connect_response, "msg.IsReadOverflowed()", "channel.Init", "connect response validation before state mutation") + require(connect_response, "AsyncClient_Elapsed( clientTime, lastConnectTime )", "overflow-safe initial prediction interval") + require(connect_response, "AsyncClient_MaxPredictionMsec()", "bounded initial prediction clamp") + reject(connect_response, "clientTime - lastConnectTime", "overflow-prone initial prediction interval") + require_before(game_init, "!AsyncClient_ValidNetworkTiming", "InitGame", "game-init timing validation before state mutation") + require_before(snapshot, "!AsyncClient_ValidNetworkTiming", "snapshotGameFrame = receivedSnapshotGameFrame", "snapshot timing validation before state mutation") + require(connect_request, "connect from %s rejected: truncated GUID", "truncated connect GUID rejection") + require(connect_request, "connect from %s rejected: truncated password", "truncated connect password rejection") + require_before(connect_request, "truncated password", "game->ServerAllowClient", "password validation before game callback") + require(connect_request, "AsyncServer_Elapsed( serverTime, challenges[ ichallenge ].pingTime )", "overflow-safe connect ping") + require(snapshot_sender, "const std::int64_t clientAheadTime", "widened client-ahead arithmetic") + require(snapshot_sender, "clientAheadTime < idMath::INT_MIN", "client-ahead lower clamp") + require(snapshot_sender, "clientAheadTime > idMath::INT_MAX", "client-ahead upper clamp") + + +def validate_snapshot_decode_bounds() -> None: + engine_bitmsg = read("src/idlib/BitMsg.cpp") + engine_bitmsg_header = read("src/idlib/BitMsg.h") + game_bitmsg = read_game("src/idlib/BitMsg.cpp") + game_bitmsg_header = read_game("src/idlib/BitMsg.h") + sp_network = read_game("src/game/Game_network.cpp") + mp_network = read_game("src/mpgame/Game_network.cpp") + sp_player = read_game("src/game/Player.cpp") + mp_player = read_game("src/mpgame/Player.cpp") + sp_multiplayer = read_game("src/game/MultiplayerGame.cpp") + mp_multiplayer = read_game("src/mpgame/MultiplayerGame.cpp") + sp_projectile = read_game("src/game/Projectile.cpp") + mp_projectile = read_game("src/mpgame/Projectile.cpp") + sp_particle = read_game("src/game/physics/Physics_Particle.cpp") + mp_particle = read_game("src/mpgame/physics/Physics_Particle.cpp") + sp_player_physics = read_game("src/game/physics/Physics_Player.cpp") + mp_player_physics = read_game("src/mpgame/physics/Physics_Player.cpp") + sp_entity = read_game("src/game/Entity.cpp") + mp_entity = read_game("src/mpgame/Entity.cpp") + sp_weapon = read_game("src/game/Weapon.cpp") + mp_weapon = read_game("src/mpgame/Weapon.cpp") + sp_af = read_game("src/game/physics/Physics_AF.cpp") + mp_af = read_game("src/mpgame/physics/Physics_AF.cpp") + + for header, label in ( + (engine_bitmsg_header, "engine bit-message API"), + (game_bitmsg_header, "game bit-message API"), + ): + require(header, "IsReadOverflowed", label) + require(header, "MarkReadOverflowed", label) + require(header, "return readBit == 8;", label) + require(header, "if ( !IsReadOverflowed() )", label) + + delta_overflow = function_body( + header, + "ID_INLINE bool idBitMsgDelta::IsReadOverflowed(", + f"{label} delta overflow policy", + ) + require( + delta_overflow, + "return readDelta != NULL ? readDelta->IsReadOverflowed()", + f"{label} conditional-tail base exhaustion policy", + ) + reject( + delta_overflow, + "( base != NULL && base->IsReadOverflowed() ) ||", + f"{label} false aggregate base-overflow rejection", + ) + + for source, label in ( + (engine_bitmsg, "engine bit-message implementation"), + (game_bitmsg, "game bit-message implementation"), + ): + read_bits = function_body(source, "int idBitMsg::ReadBits(", label) + read_data = function_body(source, "int idBitMsg::ReadData(", label) + long_counter = function_body(source, "int idBitMsg::ReadDeltaLongCounter(", label) + require_before(read_bits, "numBits > GetRemainingReadBits()", "return -1;", label) + require(read_bits, "MarkReadOverflowed();", label) + require(read_bits, "uint32_t\tvalue;", label) + require(read_bits, "1u << ( numBits - 1 )", label) + require(read_data, "length < 0", label) + require(read_data, "memset( static_cast( data ) + remaining, 0, length - remaining );", label) + require(read_data, "MarkReadOverflowed();", label) + require(long_counter, "i > 31", label) + require(long_counter, "1u << i", label) + reject(long_counter, "1 << i", label) + + delta_write_bits = function_body(source, "void idBitMsgDelta::WriteBits(", label) + delta_read_bits = function_body(source, "int idBitMsgDelta::ReadBits(", label) + delta_read_delta = function_body(source, "int idBitMsgDelta::ReadDelta(", label) + require( + delta_write_bits, + "!base->IsReadOverflowed() && baseValue == value", + f"{label} exhausted base forces explicit replacement", + ) + for reader, reader_label in ( + (delta_read_bits, "plain delta field"), + (delta_read_delta, "old-value delta field"), + ): + require(reader, "const bool baseOverflowed = base->IsReadOverflowed();", f"{label} {reader_label}") + require_before( + reader, + "readDelta->ReadBits( 1 ) == 0", + "readDelta->MarkReadOverflowed();", + f"{label} {reader_label} unavailable-base reuse rejection", + ) + + queue = function_body(game_bitmsg, "void idMsgQueue::ReadFrom(", "game unreliable-message queue") + require(queue, "encodedSize >= MAX_MSG_QUEUE_SIZE", "game unreliable-message queue bound") + require(queue, "encodedSize > remaining", "game unreliable-message queue payload bound") + require(queue, "invalid nested record size", "game unreliable-message nested record bound") + require(queue, "msg.MarkReadOverflowed();", "game unreliable-message semantic failure propagation") + require_before(queue, "encodedSize >= MAX_MSG_QUEUE_SIZE", "msg.ReadData( buffer", "queue bound before copy") + + for source, label, client_bound in ( + (sp_network, "SP snapshot", "clientNum < 0 || clientNum >= MAX_CLIENTS"), + (mp_network, "MP snapshot", "clientNum < 0 || clientNum > MAX_CLIENTS"), + ): + snapshot = function_body(source, "bool idGameLocal::ClientReadSnapshot(", label) + require_before(snapshot, client_bound, "entities[ clientNum ]", f"{label} client bound") + require(snapshot, "truncated unreliable-message queue", label) + require(snapshot, "deltaMsg.IsReadOverflowed()", label) + require(snapshot, "truncated entity state", label) + require(snapshot, "truncated PVS state", label) + require(snapshot, "truncated baseline state", label) + + sp_snapshot = function_body(sp_network, "bool idGameLocal::ClientReadSnapshot(", "SP snapshot") + require_before(sp_snapshot, "targetPlayer < 0 || targetPlayer >= MAX_CLIENTS", "entities[ targetPlayer ]", "SP target-player bound") + require(sp_snapshot, "non-player entity %d has player state", "SP player-state type check") + require(sp_player, "msg.ReadBits( -idMath::BitsForInteger( MAX_WEAPONS ) )", "SP ideal-weapon signed wire decode") + + mp_snapshot = function_body(mp_network, "bool idGameLocal::ClientReadSnapshot(", "MP snapshot") + require_before(mp_snapshot, "clientEntity->IsType( idPlayer::GetClassType() )", "static_cast( clientEntity )", "MP local-player type check before cast") + repeater = function_body(mp_network, "void idGameLocal::ClientReadRepeaterSnapshot(", "MP repeater snapshot") + require(repeater, "if ( !ClientReadSnapshot(", "MP repeater malformed-snapshot propagation") + require(repeater, "common->Error( \"ClientReadRepeaterSnapshot: malformed snapshot %d\"", "MP repeater session abort") + require(repeater, "\n\t\treturn;", "MP repeater decl-validation return") + reject(repeater, "(void)ClientReadSnapshot", "MP repeater ignored decode result") + + for source, label in ((sp_player, "SP player snapshot"), (mp_player, "MP player snapshot")): + player = function_body(source, "void idPlayer::ReadFromSnapshot(", label) + require_before(player, "decodedSpectator < 0 || decodedSpectator >= MAX_CLIENTS", "gameLocal.entities[ spectator ]", label) + require(player, "newIdealWeapon >= MAX_WEAPONS", label) + require(player, "msg.MarkReadOverflowed();", label) + require(player, "static_cast( snapshotSequence )", f"{label} wrap-safe sequence delta") + require(player, "physicsObj.DecodeSnapshotState", label) + require(player, "DecodeBindSnapshotInfo", label) + require(player, "rvWeapon::DecodeSnapshotAmmo", label) + require(player, "if ( msg.IsReadOverflowed() ) {\n\t\treturn;\n\t}\n\n\tlastSnapshotSequence = snapshotSequence;", label) + require_before(player, "if ( msg.IsReadOverflowed() )", "lastSnapshotSequence = snapshotSequence;", label) + require_before(player, "if ( msg.IsReadOverflowed() )", "physicsObj.ApplySnapshotState", label) + require_before(player, "if ( msg.IsReadOverflowed() )", "ApplyBindSnapshotInfo", label) + require_before(player, "if ( msg.IsReadOverflowed() )", "weapon->ApplySnapshotAmmo", label) + reject(player, "physicsObj.ReadFromSnapshot( msg )", label) + reject(player, "ReadBindFromSnapshot( msg )", label) + reject(player, "health = msg.ReadShort()", label) + + for source, label in ( + (sp_multiplayer, "SP multiplayer state"), + (mp_multiplayer, "MP multiplayer state"), + ): + multiplayer = function_body(source, "void idMultiplayerGame::ReadFromSnapshot(", label) + require(multiplayer, "ingame[ MAX_CLIENTS / 8 ] = { 0 }", label) + require(multiplayer, "msg.IsReadOverflowed()", label) + require(multiplayer, "!ent->IsType( idPlayer::GetClassType() )", label) + require(multiplayer, "newInstance >= MAX_INSTANCES", label) + require(multiplayer, "mpPlayerState_t decodedPlayerState[ MAX_CLIENTS ]", label) + require(multiplayer, "hasTourneyState[ MAX_CLIENTS ] = { false }", label) + require(multiplayer, "if ( msg.IsReadOverflowed() ) {\n\t\treturn;\n\t}\n\n\tisBuyingAllowedRightNow = decodedBuyingAllowed;", label) + require_before(multiplayer, "if ( msg.IsReadOverflowed() )", "playerState[ i ].ingame = decodedPlayerState", label) + require_before(multiplayer, "if ( msg.IsReadOverflowed() )", "ent->SetInstance( decodedInstance", label) + + for source, label in ( + (sp_player_physics, "SP player-physics snapshot"), + (mp_player_physics, "MP player-physics snapshot"), + ): + decode = function_body(source, "bool idPhysics_Player::DecodeSnapshotState(", label) + apply = function_body(source, "void idPhysics_Player::ApplySnapshotState(", label) + require(decode, "decoded = current;", label) + require(decode, "return !msg.IsReadOverflowed();", label) + require_before(apply, "current = decoded;", "clipModel->Link", label) + + for source, label in ( + (sp_entity, "SP bind snapshot"), + (mp_entity, "MP bind snapshot"), + ): + bind_reader = function_body(source, "void idEntity::ReadBindFromSnapshot(", label) + require_before(bind_reader, "msg.IsReadOverflowed()", "ApplyBindSnapshotInfo", label) + + for source, label in ( + (sp_weapon, "SP weapon snapshot"), + (mp_weapon, "MP weapon snapshot"), + ): + weapon_reader = function_body(source, "void rvWeapon::ReadFromSnapshot(", label) + require_before(weapon_reader, "msg.IsReadOverflowed()", "ApplySnapshotAmmo", label) + + for source, label in ( + (sp_particle, "SP particle-physics snapshot"), + (mp_particle, "MP particle-physics snapshot"), + ): + decode = function_body(source, "bool rvPhysics_Particle::DecodeSnapshotState(", label) + apply = function_body(source, "void rvPhysics_Particle::ApplySnapshotState(", label) + reader = function_body(source, "void rvPhysics_Particle::ReadFromSnapshot(", label) + require(decode, "decoded = current;", label) + require(decode, "return !msg.IsReadOverflowed();", label) + require_before(apply, "current = decoded;", "clipModel->Link", label) + require_before(reader, "DecodeSnapshotState", "ApplySnapshotState", label) + + for source, label in ( + (sp_projectile, "SP projectile snapshot"), + (mp_projectile, "MP projectile snapshot"), + ): + projectile = function_body(source, "void idProjectile::ReadFromSnapshot(", label) + require(projectile, "physicsObj.DecodeSnapshotState", label) + require(projectile, "newLaunchOrig", label) + require_before(projectile, "if ( msg.IsReadOverflowed() )", "physicsObj.ApplySnapshotState", label) + require_before(projectile, "if ( msg.IsReadOverflowed() )", "launchOrig = newLaunchOrig", label) + require_before(projectile, "if ( msg.IsReadOverflowed() )", "Create(", label) + + mp_projectile_reader = function_body( + mp_projectile, + "void idProjectile::ReadFromSnapshot(", + "MP projectile snapshot", + ) + require(mp_projectile, "msg.WriteBits( ownerNum, idMath::BitsForInteger(MAX_CLIENTS) );", "MP projectile owner wire slot") + require(mp_projectile_reader, "int ownerNum = MAX_CLIENTS;", "MP projectile owner sentinel") + require_before(mp_projectile_reader, "ownerNum >= 0", "gameLocal.entities[ ownerNum ]", "projectile owner bound") + + for source, label in ( + (sp_af, "SP articulated-figure snapshot"), + (mp_af, "MP articulated-figure snapshot"), + ): + af_reader = function_body(source, "void idPhysics_AF::ReadFromSnapshot(", label) + require(af_reader, "AFPState_t decodedCurrent = current;", label) + require(af_reader, "idList< AFBodyPState_t > decodedBodies;", label) + require(af_reader, "num != bodies.Num()", label) + require_before(af_reader, "if ( msg.IsReadOverflowed() )", "current = decodedCurrent;", label) + require_before(af_reader, "current = decodedCurrent;", "UpdateClipModels();", label) + + +def validate_rcon2_flow() -> None: + network = read("src/framework/async/AsyncNetwork.cpp") + server = read("src/framework/async/AsyncServer.cpp") + client = read("src/framework/async/AsyncClient.cpp") + + require(network, 'serverAllowLegacyRcon( "net_serverAllowLegacyRcon", "0"', "legacy server rcon default-off") + require(network, 'clientUseLegacyRcon( "net_clientUseLegacyRcon", "0"', "legacy client rcon default-off") + for name in ("serverRemoteConsolePassword", "clientRemoteConsolePassword"): + line = next((line for line in network.splitlines() if name in line and "idCVar" in line), "") + require(line, "CVAR_PRIVATE", f"{name} private flag") + require(line, "CVAR_CASE_SENSITIVE", f"{name} case-sensitive flag") + + refresh = function_body(server, "bool idAsyncServer::RefreshRcon2Verifier(", "cached rcon2 verifier") + challenge = function_body(server, "void idAsyncServer::ProcessRemoteConsole2ChallengeMessage(", "rcon2 challenge") + proof = function_body(server, "void idAsyncServer::ProcessRemoteConsole2Message(", "rcon2 proof") + legacy = function_body(server, "void idAsyncServer::ProcessRemoteConsoleMessage(", "legacy rcon server") + remote = function_body(client, "void idAsyncClient::RemoteConsole(", "rcon client") + client_reply = function_body(client, "void idAsyncClient::ConnectionlessMessage(", "client OOB source gate") + disconnect = function_body(client, "void idAsyncClient::ProcessDisconnectMessage(", "client disconnect handler") + clear = function_body(client, "void idAsyncClient::ClearRemoteConsoleRequest(", "rcon client cleanup") + + require_before(refresh, "rcon2VerifierInitialized", "DeriveVerifier", "cached verifier before expensive KDF") + require(refresh, "serverRemoteConsolePassword.IsModified()", "password-change verifier invalidation") + for token in ( + "AsyncServer_SameEndpoint( from, candidate.address )", + "RCON2_CHALLENGE_TIMEOUT_MSEC", + "Sys_GetSecureRandomBytes( randomValues", + "issued.clientNonce", + "issued.serverNonce", + "issued.endpointBinding", + "issued.requestDigest", + ): + require(challenge, token, "bound rcon2 challenge") + require_before(proof, "idCrypto::SecureZero( &rcon2Challenges[ slot ]", "msg.ReadString( command", "one-shot proof consumption") + require(proof, "AsyncServer_SameEndpoint( from, candidate.address )", "exact rcon2 proof endpoint") + require(proof, "idRcon2::HashRequest( command, requestDigest )", "rcon2 command binding") + require(proof, "idCrypto::ConstantTimeEquals( suppliedProof", "constant-time proof comparison") + require(proof, "RecordRconFailure( from )", "failed-proof throttling") + require(proof, "SendRemoteConsole2Complete", "transaction completion marker") + + require(legacy, "serverAllowLegacyRcon.GetBool()", "explicit server legacy opt-in") + require(legacy, "ConstantTimeEquals", "legacy password comparison") + require(remote, "clientUseLegacyRcon.GetBool()", "explicit client legacy opt-in") + require(remote, "Sys_GetSecureRandomBytes( rcon2Request.clientNonce", "client rcon2 nonce") + require(clear, "SecureZero( &rcon2Request", "rcon2 state wipe") + require(clear, "memset( &lastRconAddress", "rcon reply-window cleanup") + require(client_reply, "AsyncClient_SameEndpoint( from, rcon2Request.address )", "exact rcon reply source") + require(client_reply, "rcon2Request.state == RCON_REPLY_OUTPUT", "post-proof output window") + require(client_reply, "!fromCurrentServer && !fromPendingRconOutput", "pre-proof print rejection") + require_before( + client_reply, + 'if ( idStr::Icmp( string, "rcon2ChallengeResponse" ) == 0 )', + "if ( !fromCurrentServer )", + "rcon opcode dispatch before game endpoint gate", + ) + require_before( + client_reply, + "if ( !fromCurrentServer )", + 'if ( idStr::Icmp( string, "challengeResponse" ) == 0 )', + "exact game endpoint gate before game control dispatch", + ) + rcon_challenge_dispatch = client_reply[ + client_reply.index('if ( idStr::Icmp( string, "rcon2ChallengeResponse" ) == 0 )') : + client_reply.index('if ( idStr::Icmp( string, "rcon2Complete" ) == 0 )') + ] + require(rcon_challenge_dispatch, "if ( !fromPendingRcon )", "pending-rcon-only challenge reply") + rcon_complete_dispatch = client_reply[ + client_reply.index('if ( idStr::Icmp( string, "rcon2Complete" ) == 0 )') : + client_reply.index('if ( idStr::Icmp( string, "print" ) == 0 )') + ] + require(rcon_complete_dispatch, "if ( !fromPendingRcon )", "pending-rcon-only completion reply") + reject( + client_reply, + "if ( !fromCurrentServer && !fromPendingRcon )", + "union endpoint capability gate", + ) + require(disconnect, "AsyncClient_SameEndpoint( from, serverAddress )", "exact disconnect endpoint") + reject(disconnect, "Sys_CompareNetAdrBase( from, serverAddress )", "base-address-only disconnect gate") + reject(server, '"rcon from %s: %s', "remote command logging") + reject(server, '"bad rcon from %s: %s', "remote password logging") + + +def validate_pure_admission_fail_closed() -> None: + server = read("src/framework/async/AsyncServer.cpp") + connect = function_body(server, "void idAsyncServer::ProcessConnectMessage(", "connect admission") + map_change = function_body(server, "void idAsyncServer::ExecuteMapChange(", "map-change pure admission") + unreliable = function_body( + server, + "void idAsyncServer::ProcessUnreliableClientMessage(", + "wrong-gameinit pure admission", + ) + + resend = connect[ + connect.index("case CDK_PUREWAIT:") : connect.index("case CDK_ONLYLAN:") + ] + for token in ( + "if ( !SendPureServerMessage( from, OS ) )", + "AsyncServer_ClearChallenge( challenges[ ichallenge ] );", + "return;", + ): + require(resend, token, "fail-closed pure challenge resend") + + initial_start = connect.rindex( + 'if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && challenges[ ichallenge ].authState != CDK_PUREOK )' + ) + initial = connect[initial_start : connect.index("// push back decl checksum", initial_start)] + for token in ( + "if ( !SendPureServerMessage( from, OS ) )", + "AsyncServer_ClearChallenge( challenges[ ichallenge ] );", + "challenges[ ichallenge ].authState = CDK_PUREWAIT;", + "return;", + ): + require(initial, token, "fail-closed initial pure challenge") + require_before( + initial, + "if ( !SendPureServerMessage( from, OS ) )", + "challenges[ ichallenge ].authState = CDK_PUREWAIT;", + "pure send before wait-state admission", + ) + reject(initial, "if ( SendPureServerMessage( from, OS ) )", "fallthrough-capable initial pure send") + + for body, context in ( + (map_change, "map-change reliable pure send"), + (unreliable, "wrong-gameinit reliable pure send"), + ): + require(body, "if ( !SendReliablePureToClient(", context) + require(body, "DropClient(", context) + reject(body, "clientState = SCS_CONNECTED;\n\t\t\t\t}", f"{context} failure promotion") + + +def validate_private_cvar_handling() -> None: + header = read("src/framework/CVarSystem.h") + cvars = read("src/framework/CVarSystem.cpp") + matcher = read("src/idlib/PrivateCommand.h") + args = read("src/idlib/CmdArgs.cpp") + strings = read("src/idlib/Str.h") + commands = read("src/framework/CmdSystem.cpp") + console = read("src/framework/Console.cpp") + + require(header, "CVAR_PRIVATE", "private CVar flag") + require(cvars, '( internal->GetFlags() & CVAR_PRIVATE ) ? ""', "direct CVar query redaction") + require(cvars, "!( cvar->GetFlags() & CVAR_PRIVATE )", "private CVar serialization omission") + require(cvars, "ContainsBoundedCaseInsensitiveToken", "private command matcher integration") + require(cvars, "expandedArgs.TokenizeString( commandText, false )", "expanded private-target classification") + require(cvars, "expandedArgs.Argv( argIndex )", "expanded private-target token scan") + require(cvars, "expandedArgs.ClearSensitive()", "expanded command scratch wipe") + require(cvars, "( flags & CVAR_CASE_SENSITIVE ) ?", "case-sensitive CVar update selection") + require(cvars, "valueString.Cmp( newValue ) == 0", "case-only private password update") + require(cvars, "CVar_AssignString( valueString, newValue", "private CVar replacement wipe") + require(cvars, "valueString.SecureClear()", "private CVar destructor wipe") + require(cvars, "resetString.SecureClear()", "private CVar reset-value wipe") + require(cvars, "toggle is unavailable for private CVar", "private toggle rejection") + require(matcher, "nameBytes > commandBytes", "short-command matcher bound") + require(matcher, "offset <= commandBytes - nameBytes", "bounded private-token scan") + require(matcher, "rightOffset == commandBytes", "safe right-boundary check") + require(args, 'token = ""', "$ private-CVar expansion redaction") + require(args, "cmd_args.SecureClear()", "Args shared-scratch wipe") + args_body = function_body(args, "const char *idCmdArgs::Args(", "Args shared scratch") + require_before(args_body, "cmd_args.SecureClear()", "cmd_args +=", "scratch wipe before Args reuse") + require(strings, "ID_INLINE void idStr::SecureClear", "full idStr allocation wipe") + require(strings, "data != baseBuffer ? alloced", "dynamic idStr allocation wipe") + require(commands, "vstr is unavailable for private CVar", "private vstr rejection") + require(commands, "idCmdArgs::ClearArgsScratch()", "private command scratch cleanup") + completion_info = function_body( + console, + "bool idConsoleLocal::GetCompletionCvarInfo(", + "console completion CVar information", + ) + require( + completion_info, + '( cvar->GetFlags() & CVAR_PRIVATE ) ?\n\t\t\t"" : cvar->GetString()', + "private completion-popup value redaction", + ) + + evidence = { + "src/framework/Console.cpp": ( + "CommandContainsPrivateCVar", + "]", + "removedPrivateCommand", + ), + "src/sys/win32/win_syscon.cpp": ( + "CommandContainsPrivateCVar", + "]", + "if ( !privateCommand )", + ), + "src/sys/posix/posix_syscon.cpp": ( + "CommandContainsPrivateCVar", + "]", + "if ( !privateCommand )", + ), + "src/sys/posix/posix_main.cpp": ( + "CommandContainsPrivateCVar", + "memset( s, 0, len )", + "if ( !privateCommand )", + ), + "src/framework/Common.cpp": ( + "", + "com_consoleLines[ i ].ClearSensitive()", + ), + "src/framework/EventLoop.cpp": ( + "EventLoop_IsPrivateConsoleEvent", + "PRIVATE_EVENT_TEXT", + "memset( ev.evPtr, 0, ev.evPtrLength )", + ), + "src/framework/EditField.cpp": ( + "memset( buffer, 0, sizeof( buffer ) )", + "memset( &autoComplete, 0, sizeof( autoComplete ) )", + '"" : cvarSystem->GetCVarString( s )', + "autocomplete is unavailable for private CVar commands", + ), + "src/framework/CmdSystem.cpp": ( + "CommandContainsPrivateCVar", + "ClearSensitive()", + "memset( textBuf + textLength, 0", + ), + } + for path, tokens in evidence.items(): + source = read(path) + for token in tokens: + require(source, token, f"private-data lifecycle in {path}") + + +def validate_remote_dictionary_authority() -> None: + engine_header = read("src/framework/CVarSystem.h") + game_header = read_game("src/framework/CVarSystem.h") + cvars = read("src/framework/CVarSystem.cpp") + policy = read("src/framework/RemoteCVarPolicy.h") + native = read("tools/tests/native/CoreSafetyTest.cpp") + client = read("src/framework/async/AsyncClient.cpp") + server = read("src/framework/async/AsyncServer.cpp") + demo = read("src/framework/async/MultiViewDemo.cpp") + engine_bitmsg = read("src/idlib/BitMsg.cpp") + game_bitmsg = read_game("src/idlib/BitMsg.cpp") + sp_network = read_game("src/game/Game_network.cpp") + mp_network = read_game("src/mpgame/Game_network.cpp") + + for header, label in ( + (engine_header, "engine CVar interface"), + (game_header, "game CVar interface"), + ): + require(header, "SetCVarsFromDictByFlags", label) + require(header, "ABI rule: append new virtual methods here", label) + interface = header.split("class idCVarSystem {", 1)[1].split("};", 1)[0] + legacy_tail = interface.index("SetCVarsFromDict( const idDict &dict )") + flagged_slot = interface.index("SetCVarsFromDictByFlags") + private_slot = interface.index("CommandContainsPrivateCVar") + if not legacy_tail < flagged_slot < private_slot: + raise AssertionError(f"{label} security methods are not appended after the legacy v43 tail") + + require(policy, "IsSingleAllowedAuthority", "production remote-CVar policy") + require(policy, "CanApply", "production remote-CVar policy") + for token in ( + '"matching userinfo authority"', + '"matching serverinfo authority"', + '"matching networksync authority"', + '"cross-class authority"', + '"local-only CVar"', + '"private CVar"', + '"combined authority"', + '"unknown authority"', + ): + require(native, token, "native remote-CVar authority coverage") + + legacy_apply = function_body(cvars, "void idCVarSystemLocal::SetCVarsFromDict(", "legacy dictionary apply") + flagged_apply = function_body(cvars, "bool idCVarSystemLocal::SetCVarsFromDictByFlags(", "flagged dictionary apply") + require(legacy_apply, "CVAR_USERINFO | CVAR_SERVERINFO | CVAR_NETWORKSYNC", "legacy remote-class ceiling") + require(legacy_apply, "CVAR_PRIVATE", "legacy private-CVar exclusion") + require(flagged_apply, "CVAR_USERINFO | CVAR_SERVERINFO | CVAR_NETWORKSYNC", "remote-class allowlist") + require(flagged_apply, "idRemoteCVarPolicy::IsSingleAllowedAuthority", "single remote authority") + require(flagged_apply, "idRemoteCVarPolicy::CanApply", "per-CVar authority check") + require(flagged_apply, "CVAR_PRIVATE", "private-CVar exclusion") + + require(client, "SetCVarsFromDictByFlags( info, CVAR_USERINFO )", "client userinfo authority") + require(client, "SetCVarsFromDictByFlags( info, CVAR_NETWORKSYNC )", "client sync authority") + require(server, "SetCVarsFromDictByFlags( *gameInfo, CVAR_USERINFO )", "listen-server userinfo authority") + if demo.count("SetCVarsFromDictByFlags( sessLocal.mapSpawnData.syncedCVars, CVAR_NETWORKSYNC )") != 2: + raise AssertionError("MVD start/reset must both apply only NETWORKSYNC CVars") + + for source, label in ( + (engine_bitmsg, "engine delta dictionary"), + (game_bitmsg, "game delta dictionary"), + ): + decoder = function_body(source, "bool idBitMsg::ReadDeltaDict(", label) + require(decoder, "idDict\t\tdecoded;", f"{label} transaction scratch") + require(decoder, "if ( IsReadOverflowed() )", f"{label} underflow rejection") + require_before(decoder, "if ( IsReadOverflowed() )", "dict = decoded;", f"{label} commit after validation") + if decoder.count("return false;") < 3: + raise AssertionError(f"{label} does not reject every truncated key/value/tail boundary") + + reliable_client = function_body(client, "void idAsyncClient::ProcessReliableServerMessages(", "client reliable dispatcher") + clientinfo = reliable_client[ + reliable_client.index("case SERVER_RELIABLE_MESSAGE_CLIENTINFO") : + reliable_client.index("case SERVER_RELIABLE_MESSAGE_SYNCEDCVARS") + ] + synced = reliable_client[ + reliable_client.index("case SERVER_RELIABLE_MESSAGE_SYNCEDCVARS") : + reliable_client.index("case SERVER_RELIABLE_MESSAGE_PRINT") + ] + require_before(clientinfo, "msg.IsReadOverflowed()", "SetCVarsFromDictByFlags", "userinfo validation before CVar commit") + require_before(synced, "msg.IsReadOverflowed()", "SetCVarsFromDictByFlags", "sync validation before CVar commit") + + reliable_server = function_body(server, "void idAsyncServer::ProcessReliableClientMessages(", "server reliable dispatcher") + server_clientinfo = reliable_server[ + reliable_server.index("case CLIENT_RELIABLE_MESSAGE_CLIENTINFO") : + reliable_server.index("case CLIENT_RELIABLE_MESSAGE_PRINT") + ] + require_before(server_clientinfo, "msg.IsReadOverflowed()", "SendUserInfoBroadcast", "client userinfo validation before broadcast") + + for source, label in ((sp_network, "SP reliable server-info"), (mp_network, "MP reliable server-info")): + serverinfo = source[source.index("case GAME_RELIABLE_MESSAGE_SERVERINFO") :] + serverinfo = serverinfo[: serverinfo.index("case GAME_RELIABLE_MESSAGE_RESTART")] + require_before(serverinfo, "msg.IsReadOverflowed()", "SetServerInfo( info )", label) + + pure = function_body(server, "void idAsyncServer::ProcessReliablePure(", "reliable pure state recovery") + require(pure, "outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD )", "reliable reload opcode") + require(pure, "SendReliableMessage( clientNum, outMsg )", "reliable reload buffer identity") + reject(pure, "SendReliableMessage( clientNum, msg )", "client-controlled reliable echo") + + +def validate_documentation_and_registration() -> None: + guide = read("docs/user/server-security.md") + setup = read("docs/user/server-setup.md") + for token in ( + "not an encrypted transport", + "PBKDF2-throttled offline password guessing", + "24 or more characters", + "never launch with", + "net_serverAllowLegacyRcon 1", + "net_clientUseLegacyRcon 1", + "FIPS 180-4", + "RFC 2104", + "RFC 8018", + ): + require(guide, token, "rcon2 administrator guidance") + require(setup, "server-security.md", "server setup security-guide link") + + for path in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + workflow = read(path) + require(workflow, "tools/tests/network_security.py \\", f"{path} syntax-check registration") + require(workflow, "python tools/tests/network_security.py", f"{path} execution registration") + require(read("tools/validation/openq4_validate.py"), '"network_security.py"', "local validation registration") + + +def main() -> None: + validate_crypto_and_native_vectors() + validate_secure_random_and_connection_challenges() + validate_oob_budgets() + validate_usercmd_packet_bounds() + validate_snapshot_decode_bounds() + validate_rcon2_flow() + validate_pure_admission_fail_closed() + validate_private_cvar_handling() + validate_remote_dictionary_authority() + validate_documentation_and_registration() + print("network_security: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/openq4_pure_pack.py b/tools/tests/openq4_pure_pack.py index 5edbdc17..dfe5c78f 100644 --- a/tools/tests/openq4_pure_pack.py +++ b/tools/tests/openq4_pure_pack.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Regression checks for openQ4 pure-pack handling.""" +"""Regression checks for openQ4 pure-pack and download handling.""" from __future__ import annotations @@ -128,11 +128,15 @@ def bounded_download_write_model( def validate_filesystem_pure_pack_contract() -> None: source = read("src/framework/FileSystem.cpp") + game_dir_policy = read("src/framework/GameDirPolicy.h") file_system_header = read("src/framework/FileSystem.h") md5_header = read("src/idlib/hashing/MD5.h") md5_source = read("src/idlib/hashing/MD5.cpp") async_client = read("src/framework/async/AsyncClient.cpp") + async_client_header = read("src/framework/async/AsyncClient.h") + async_network_header = read("src/framework/async/AsyncNetwork.h") async_server = read("src/framework/async/AsyncServer.cpp") + licensee_header = read("src/framework/licensee.h") session_menu = read("src/framework/Session_menu.cpp") curl_write = function_body( source, @@ -171,13 +175,60 @@ def validate_filesystem_pure_pack_contract() -> None: async_client, "void idAsyncClient::HandleDownloads(", ) + version_message = function_body( + async_client, + "void idAsyncClient::ProcessVersionMessage(", + ) + version_check = function_body( + async_client, + "void idAsyncClient::SendVersionCheck(", + ) helper = function_body(source, "bool idFileSystemLocal::IsOpenQ4PurePack(") checksum_validator = function_body(source, "bool idFileSystemLocal::ValidateOpenQ4Paks(") misplaced_validator = function_body(source, "bool idFileSystemLocal::FindMisplacedOfficialPaks(") startup = function_body(source, "void idFileSystemLocal::Startup(") status = function_body(source, "pureStatus_t idFileSystemLocal::GetPackStatus(") + update_game_pak_checksums = function_body( + source, + "bool idFileSystemLocal::UpdateGamePakChecksums(", + ) + validate_download_pak = function_body( + source, + "int idFileSystemLocal::ValidateDownloadPakForChecksum(", + ) + clear_pure_checksums = function_body( + source, + "void idFileSystemLocal::ClearPureChecksums(", + ) + set_pure_server_checksums = function_body( + source, + "fsPureReply_t idFileSystemLocal::SetPureServerChecksums(", + ) + get_pure_server_checksums = function_body( + source, + "void idFileSystemLocal::GetPureServerChecksums(", + ) + find_dll = function_body(source, "void idFileSystemLocal::FindDLL(") + get_mod_info = function_body(source, "bool idFileSystemLocal::GetModInfo(") + send_pure_server_message = function_body( + async_server, + "bool idAsyncServer::SendPureServerMessage(", + ) + send_reliable_pure_to_client = function_body( + async_server, + "bool idAsyncServer::SendReliablePureToClient(", + ) + process_connect_message = function_body( + async_server, + "void idAsyncServer::ProcessConnectMessage(", + ) + process_challenge_response = function_body( + async_client, + "void idAsyncClient::ProcessChallengeResponseMessage(", + ) require(source, '#include "openq4_paks_generated.h"', "filesystem generated pack checksum header") + require(source, '#include "GameDirPolicy.h"', "portable game-directory policy integration") require(source, "IsOpenQ4PurePack", "filesystem pure-pack declaration") require(source, "ValidateOpenQ4Paks", "filesystem pack checksum declaration") require(helper, "OPENQ4_GAMEDIR", "openQ4 pure-pack directory check") @@ -342,6 +393,262 @@ def validate_filesystem_pure_pack_contract() -> None: require(async_server, "Server decl checksum: 0x%08x", "server decl checksum diagnostic") require(async_server, "client=0x%08x server=0x%08x (non-pure)", "non-pure mismatch diagnostic") require(async_server, "client=0x%08x server=0x%08x (pure)", "pure mismatch diagnostic") + reject(async_server, 'serverInfo.SetInt( "si_pure", 0 )', "forced pure-server disable") + reject(async_server, "forcing si_pure 0", "forced pure-server disable diagnostic") + require( + async_network_header, + "const int ASYNC_PROTOCOL_MINOR\t\t= 41;", + "Quake 4 1.4.2 protocol compatibility", + ) + require( + source, + "static const int OPENQ4_Q4_142_GAME300_PAK_CHECKSUM = 0x68fb90b1;", + "platform-independent pure game-module compatibility token", + ) + require(file_system_header, "MAX_GAME_OS\t\t\t\t\t= 6;", "pure OS table capacity") + + for origin in ( + "GAME_MODULE_ORIGIN_NONE", + "GAME_MODULE_ORIGIN_ACTIVE_MOD", + "GAME_MODULE_ORIGIN_BASE_GAME", + "GAME_MODULE_ORIGIN_PACKAGE_ROOT", + ): + require(source, origin, "trusted game-module origin model") + require(find_dll, "gameModuleOrigin_t resolvedOrigin = GAME_MODULE_ORIGIN_NONE;", "module-origin reset") + require(find_dll, "GAME_MODULE_ORIGIN_ACTIVE_MOD", "active-mod module origin") + require(find_dll, "GAME_MODULE_ORIGIN_BASE_GAME", "base-game module origin") + require(find_dll, "GAME_MODULE_ORIGIN_PACKAGE_ROOT", "flat package-root module origin") + require(find_dll, "gameModuleOrigin = resolvedOrigin;", "resolved module-origin persistence") + require( + find_dll, + "gamePakChecksum = dllFile ? OPENQ4_Q4_142_GAME300_PAK_CHECKSUM : 0;", + "compatibility token only for a resolved trusted module", + ) + + for token in ( + "MAX_SEGMENT_BYTES = 255", + "IsWindowsDeviceName(", + "IsPortableSegment(", + "*scan == '/'", + "*scan == '\\\\'", + "*scan == ':'", + "segment[ 0 ] == '.'", + "segment[ 1 ] == '.'", + ): + require(game_dir_policy, token, "portable single-segment game-directory policy") + require( + get_mod_info, + "!idGameDirPolicy::IsPortableSegment( modDir )", + "mod manifest directory validation", + ) + require_order( + get_mod_info, + "!idGameDirPolicy::IsPortableSegment( modDir )", + "const char *search[ 3 ]", + "mod directory validation before manifest roots", + ) + require( + get_mod_info, + "mod directory must be one portable directory segment", + "actionable unsafe mod-directory failure", + ) + require( + find_dll, + "!idGameDirPolicy::IsPortableSegment( moduleGameDir )", + "trusted module game-directory validation", + ) + require_order( + find_dll, + "!idGameDirPolicy::IsPortableSegment( moduleGameDir )", + "FS_AppendGameModuleSearchPath(", + "game-directory validation before trusted module path construction", + ) + for token in ( + "gameDLLChecksum = 0;", + "gamePakChecksum = 0;", + "gameModuleOrigin = GAME_MODULE_ORIGIN_NONE;", + "_dllPath[ 0 ] = '\\0';", + "return;", + ): + require(find_dll, token, "unsafe fs_game module lookup fail-closed state") + require(process_challenge_response, "fileSystem->GetModInfo( serverGameBase", "server base-mod validation") + require(process_challenge_response, "fileSystem->GetModInfo( serverGame", "server mod validation") + require_order( + process_challenge_response, + "fileSystem->GetModInfo( serverGame", + 'cvarSystem->SetCVarString( "fs_game", serverGame );', + "server mod validation before fs_game mutation", + ) + + require(update_game_pak_checksums, "gameDLLChecksum == 0", "missing local game-module rejection") + require( + update_game_pak_checksums, + "gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM", + "unexpected local compatibility-token rejection", + ) + require( + update_game_pak_checksums, + "gameModuleOrigin == GAME_MODULE_ORIGIN_NONE", + "untrusted module-origin rejection", + ) + require( + update_game_pak_checksums, + "gameModuleOrigin == GAME_MODULE_ORIGIN_ACTIVE_MOD", + "active-mod module-origin classification", + ) + require( + update_game_pak_checksums, + '!cvarSystem->GetCVarBool( "net_serverAllowServerMod" )', + "explicit server-mod opt-in gate", + ) + require( + update_game_pak_checksums, + "for ( int os = 0; os <= 2; ++os )", + "legacy Windows/Linux/macOS compatibility range", + ) + require_order( + update_game_pak_checksums, + "memset( gamePakForOS, 0, sizeof( gamePakForOS ) );", + "for ( int os = 0; os <= 2; ++os )", + "unsupported pure OS entries cleared before legacy OS mapping", + ) + require( + update_game_pak_checksums, + "gamePakForOS[ os ] = OPENQ4_Q4_142_GAME300_PAK_CHECKSUM;", + "platform-independent compatibility-token mapping", + ) + for obsolete_parser_symbol in ( + "BINARY_CONFIG", + '"binary.conf"', + "HashFileName(", + "ReadFile(", + "idLexer", + "ParseInt(", + "atoi(", + ): + reject( + update_game_pak_checksums, + obsolete_parser_symbol, + "pure game-module mapping without retail binary.conf parsing", + ) + + require(validate_download_pak, "pak->binary == BINARY_UNKNOWN", "lazy package binary classification") + require(validate_download_pak, "HashFileName( BINARY_CONFIG )", "binary.conf presence lookup") + require(validate_download_pak, "pak->binary = BINARY_NO", "content-only package classification") + require(validate_download_pak, "pak->binary = BINARY_YES", "binary-marked package classification") + reject(validate_download_pak, "ReadFile(", "package binary marker presence-only classification") + require(clear_pure_checksums, "memset( gamePakForOS, 0, sizeof( gamePakForOS ) );", "non-pure OS-mask reset") + + require( + set_pure_server_checksums, + "*missingGamePakChecksum = 0;", + "code-pak download output stays empty", + ) + if set_pure_server_checksums.count("*missingGamePakChecksum =") != 1: + raise AssertionError("Pure negotiation must never request a downloadable game-code pak") + require( + set_pure_server_checksums, + "_gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM", + "server compatibility-token validation", + ) + require( + set_pure_server_checksums, + "gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM", + "local compatibility-token validation", + ) + require(set_pure_server_checksums, "gameDLLChecksum == 0", "missing local module rejection") + require( + set_pure_server_checksums, + "gameModuleOrigin == GAME_MODULE_ORIGIN_NONE", + "untrusted local module rejection", + ) + require(set_pure_server_checksums, "return PURE_NODLL;", "unsupported token fail-closed result") + require_order( + set_pure_server_checksums, + "_gamePakChecksum != OPENQ4_Q4_142_GAME300_PAK_CHECKSUM", + "if ( pureChecksums[ 0 ] == 0 )", + "game-module token validation before asset-list processing", + ) + for forbidden_code_pak_path in ( + "GetPackForChecksum( _gamePakChecksum", + "GetPackForChecksum( gamePakChecksum", + "*missingGamePakChecksum = _gamePakChecksum", + "*missingGamePakChecksum = gamePakChecksum", + "gamePakChecksum = _gamePakChecksum", + "restartGamePakChecksum", + "BINARY_CONFIG", + '"binary.conf"', + ): + reject( + set_pure_server_checksums, + forbidden_code_pak_path, + "compatibility token cannot select, download, or restart into game code", + ) + + require(get_pure_server_checksums, "OS >= 0 && OS < MAX_GAME_OS", "pure checksum OS bounds") + require(get_pure_server_checksums, "else if ( OS == -1 )", "local pure-token sentinel") + require(get_pure_server_checksums, "*_gamePakChecksum = 0;", "invalid pure OS fail-closed token") + require_order( + get_pure_server_checksums, + "OS >= 0 && OS < MAX_GAME_OS", + "OS == -1", + "bounded remote OS lookup before local-only sentinel", + ) + + require(process_connect_message, "if ( OS < 0 || OS >= MAX_GAME_OS )", "pure client OS bounds") + require( + process_connect_message, + "const int osMask = fileSystem->GetOSMask();", + "pure supported-OS mask lookup", + ) + require( + process_connect_message, + "static_cast( osMask ) & ( 1u << OS )", + "defined unsigned pure OS-mask shift", + ) + require_order( + process_connect_message, + "if ( OS < 0 || OS >= MAX_GAME_OS )", + "const int osMask = fileSystem->GetOSMask();", + "OS range validation before OS-mask lookup and shift", + ) + require_order( + process_connect_message, + "const int osMask = fileSystem->GetOSMask();", + "ValidateChallenge( from, challenge, clientId )", + "pure supported-OS rejection before challenge state use", + ) + require_order( + process_connect_message, + "static_cast( osMask ) & ( 1u << OS )", + "ValidateChallenge( from, challenge, clientId )", + "pure supported-OS bit test before challenge state use", + ) + require_order( + process_connect_message, + "if ( OS < 0 || OS >= MAX_GAME_OS )", + "challenges[ ichallenge ].OS = OS;", + "OS range validation before challenge-state storage", + ) + + for send_body, message_init, context in ( + (send_pure_server_message, "outMsg.Init(", "connectionless pure send"), + (send_reliable_pure_to_client, "msg.Init(", "reliable pure send"), + ): + require( + send_body, + "if ( !serverChecksums[ 0 ] || !gamePakChecksum )", + f"{context} fail-closed prerequisites", + ) + require_order( + send_body, + "if ( !serverChecksums[ 0 ] || !gamePakChecksum )", + message_init, + f"{context} prerequisites before packet construction", + ) + guard_start = send_body.find("if ( !serverChecksums[ 0 ] || !gamePakChecksum )") + guard_end = send_body.find(message_init, guard_start) + require(send_body[guard_start:guard_end], "return false;", f"{context} fail-closed return") require( download_request, "dlSize[ MAX_PURE_PAKS ] = {};", @@ -431,11 +738,62 @@ def validate_filesystem_pure_pack_contract() -> None: require(invalid_download_type, "return;", "invalid download discriminator fail-closed return") reject(download_info, "assert( pakDl == SERVER_PAK_END );", "release-only download discriminator validation") - update_setup = download_handler[ - download_handler.find("fileSystem->OpenFileWrite( updateFile )") : - download_handler.find("updateState = UPDATE_DLING;") + require( + licensee_header, + '#define PROJECT_RELEASES_URL\t\t\tPROJECT_REPO "/releases"', + "compile-time openQ4 releases destination", + ) + require( + licensee_header, + '#define PROJECT_REPO\t\t\t\t\t"https://github.com/themuffinator/openQ4"', + "HTTPS openQ4 project destination", + ) + require( + version_message, + 'updateMSG = common->GetLanguageDict()->GetString( "#str_104330" );', + "fixed localized update notification", + ) + require( + version_message, + "ignoredNetworkField", + "legacy update fields are consumed without storage or display", + ) + if version_message.count("msg.ReadString(") != 3 or version_message.count("msg.ReadByte()") != 2: + raise AssertionError("Legacy update reply fields must remain wire-compatible and inert") + reject(version_message, "updateMSG = ignoredNetworkField", "network-controlled update instructions") + require(version_check, 'msg.WriteString( "" );', "empty legacy update identity field") + reject(version_check, 'GetCVarString( "com_guid" )', "update-check persistent GUID disclosure") + update_prompt = download_handler[ + download_handler.find("if ( updateState == UPDATE_READY )") : + download_handler.find("} else if ( dlList.Num() )") ] - require(update_setup, "backgroundDownload.url.expectedSize = 0;", "unrestricted updater download") + require(update_prompt, "sys->OpenURL( PROJECT_RELEASES_URL, false );", "fixed update information page") + require(update_prompt, "updateState = UPDATE_DONE;", "update notification terminal state") + require(update_prompt, "showUpdateMessage = false;", "update notification prompt cleanup") + for forbidden in ( + "BackgroundDownload", + "OpenFileWrite", + "StartProcess", + "DLTYPE_URL", + "file://", + "expectedSize", + "RemoveFile", + ): + reject(update_prompt, forbidden, "notification-only updater") + for removed_member in ( + "UPDATE_DLING", + "updateDirectDownload", + "updateURL", + "updateFile", + "updateMime", + "updateFallback", + "SendVersionDLUpdate", + ): + reject(async_client_header, removed_member, "legacy executable updater state") + reject(async_client, removed_member, "legacy executable updater implementation") + reject(file_system_header, "FILE_EXEC", "legacy executable updater MIME action") + reject(file_system_header, "dlMime_t", "legacy updater MIME type") + package_setup = download_handler[ download_handler.find("fileSystem->MakeTemporaryFile( )") : download_handler.find("session->DownloadProgressBox( &backgroundDownload, dltitle") @@ -467,7 +825,7 @@ def validate_filesystem_pure_pack_contract() -> None: if bounded_download_write_model(SIZE_T_MAX, 1, 1, 0) is not None: raise AssertionError("Bounded download model accepted overflowing cumulative bytes") if bounded_download_write_model(9, 1, 2, 0) != 11: - raise AssertionError("Updater download model unexpectedly applied a package-size limit") + raise AssertionError("Unbounded transfer model unexpectedly applied a package-size limit") require_order( download_handler, @@ -525,24 +883,6 @@ def validate_filesystem_pure_pack_contract() -> None: "checksum failure removes only the verified download destination", ) - update_destination = download_handler[ - download_handler.find("fileSystem->OpenFileWrite( updateFile )") : - download_handler.find("dltotal = 0;") - ] - require(update_destination, "if ( f == NULL )", "update destination null guard") - require(update_destination, "updateState = UPDATE_DONE;", "failed update state cleanup") - require(update_destination, "SendVersionDLUpdate( 2 );", "failed update telemetry") - require(update_destination, '"#str_04335"', "localized update failure message") - require(update_destination, "updateFallback.Length()", "failed update fallback") - require(update_destination, "return;", "failed update early return") - - update_download_result = download_handler[ - download_handler.find("if ( backgroundDownload.url.status == DL_DONE )") : - download_handler.find("} else if ( dlList.Num() )") - ] - if update_download_result.count("AsyncClient_CloseBackgroundDownloadFile( backgroundDownload );") != 2: - raise AssertionError("Update download success and failure paths must each release their owned file") - successful_copy_cleanup = download_handler[ download_handler.find("while ( remainlen )") : download_handler.find("fileSystem->CloseFile( saveas );") @@ -575,7 +915,7 @@ def validate_filesystem_pure_pack_contract() -> None: "direct background download member close", ) cleanup_call = "AsyncClient_CloseBackgroundDownloadFile( backgroundDownload );" - if download_handler.count(cleanup_call) != 6: + if download_handler.count(cleanup_call) != 4: raise AssertionError("Every background download completion path must release its file exactly once") valid_download_paths = ( diff --git a/tools/tests/openurl_security.py b/tools/tests/openurl_security.py new file mode 100644 index 00000000..0e6136a7 --- /dev/null +++ b/tools/tests/openurl_security.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Cross-platform source contracts for external URL handoff.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def require_before(haystack: str, first: str, second: str, context: str) -> None: + first_index = haystack.find(first) + second_index = haystack.find(second) + if first_index < 0 or second_index < 0: + raise AssertionError(f"Missing ordered tokens {first!r} and/or {second!r} in {context}") + if first_index >= second_index: + raise AssertionError(f"Expected {first!r} before {second!r} in {context}") + + +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"Missing function signature {signature!r}") + + depth = 0 + for index in range(start, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find end of function {signature!r}") + + +def validate_shared_policy() -> None: + policy = read("src/sys/URLPolicy.h") + validator = function_body(policy, "inline bool IsAllowedHTTPURL( const char *url ) {") + authority = function_body(policy, "inline bool AuthorityHasHost( const char *begin, const char *end ) {") + port = function_body(policy, "inline bool ParsePort( const char *begin, const char *end ) {") + dns_host = function_body(policy, "inline bool IsDNSOrIPv4Host( const char *begin, const char *end ) {") + ipv4 = function_body(policy, "inline bool IsIPv4Literal( const char *begin, const char *end ) {") + ipv6 = function_body(policy, "inline bool IsIPv6Literal( const char *begin, const char *end ) {") + + require(policy, "const size_t MAX_URL_BYTES = 4096;", "bounded URL policy") + require(validator, "length < MAX_URL_BYTES", "bounded URL scan") + require(validator, "length == MAX_URL_BYTES", "unterminated/oversized URL rejection") + require(validator, "value <= 32 || value == 127", "URL whitespace and control-byte rejection") + require(validator, 'ASCIIStartsWith( url, "https://" )', "HTTPS scheme allowlist") + require(validator, 'ASCIIStartsWith( url, "http://" )', "HTTP scheme allowlist") + require(validator, "AuthorityHasHost( authority, authorityEnd )", "non-empty URL host policy") + require(authority, "begin == end", "empty URL authority rejection") + require(authority, "*cursor == '@' || *cursor == '%'", "ambiguous authority rejection") + require(authority, "IPv6 literals must use brackets", "unambiguous IPv6 authority policy") + require(authority, "hostEnd == begin", "empty URL host rejection") + require(authority, "IsDNSOrIPv4Host( begin, hostEnd )", "DNS/IPv4 host syntax validation") + require(authority, "IsIPv6Literal( begin + 1, closeBracket )", "IPv6 literal syntax validation") + require(dns_host, "hostLength > 253", "DNS host-length bound") + require(dns_host, "labelLength > 63", "DNS label-length bound") + require(dns_host, "IsASCIIAlphaNumeric( *labelBegin )", "DNS label boundary syntax") + require(ipv4, "value > 255", "IPv4 octet bound") + require(ipv6, "compressed ? groups < 8 : groups == 8", "IPv6 group/compression bound") + require(port, "port > 6553", "URL port overflow/range guard") + + +def validate_platform_integration() -> None: + platforms = ( + ( + "Windows", + "src/sys/win32/win_main.cpp", + "void idSysLocal::OpenURL(const char* url, bool doexit) {", + "ShellExecute(NULL, \"open\", url", + ), + ( + "Linux", + "src/sys/linux/main.cpp", + "void idSysLocal::OpenURL( const char *url, bool quit ) {", + 'Sys_FindExecutableOnPath( "xdg-open"', + ), + ( + "macOS", + "src/sys/osx/macosx_misc.mm", + "void idSysLocal::OpenURL( const char *url, bool doexit ) {", + "openURL: nsURL", + ), + ) + + for platform, path, signature, handoff in platforms: + source = read(path) + body = function_body(source, signature) + require(source, '#include "../URLPolicy.h"', f"{platform} shared URL policy include") + require(body, "idURLPolicy::IsAllowedHTTPURL( url )", f"{platform} URL guard") + require(body, "OpenURL rejected: expected a bounded HTTP or HTTPS URL with a host", f"{platform} rejection diagnostic") + require_before(body, "idURLPolicy::IsAllowedHTTPURL( url )", "Open URL:", f"{platform} guard before approved URL logging") + require_before(body, "idURLPolicy::IsAllowedHTTPURL( url )", handoff, f"{platform} guard before OS handoff") + reject(body, "rejected: %s", f"{platform} rejected URL raw logging") + reject(body, "ignoring %s", f"{platform} ignored URL raw logging") + + linux = read("src/sys/linux/main.cpp") + macos = read("src/sys/osx/macosx_misc.mm") + reject(linux, "static bool Sys_IsSafeURL", "Linux broad scheme policy") + reject(macos, "OSX_URLHasSafeSchemeSyntax", "macOS broad scheme policy") + reject(macos, "OSX_FileURLIsLocalRuntimeFile", "macOS file URL launcher") + + +def validate_runtime_coverage() -> None: + native = read("tools/tests/native/CoreSafetyTest.cpp") + require(native, '#include "src/sys/URLPolicy.h"', "native URL policy test") + require(native, "idURLPolicy::IsAllowedHTTPURL( url )", "native production-policy execution") + for case in ( + '"http://example.com", true', + '"HTTPS://example.com:443/releases?q=openq4#download", true', + '"https://[2001:db8::1]:65535/path", true', + '"file:///tmp/update", false', + '"javascript:alert(1)", false', + '"https://user@example.com/path", false', + '"https://example.com/line\\nbreak", false', + '"https://example.com\\\\attacker.invalid", false', + '"https://!/path", false', + '"https://999.1.2.3/path", false', + '"https://[not-an-ip]/path", false', + '"oversized URL"', + ): + require(native, case, "native URL policy boundary cases") + + +def validate_network_download_integration() -> None: + client = read("src/framework/async/AsyncClient.cpp") + file_system = read("src/framework/FileSystem.cpp") + download_info = function_body( + client, + "void idAsyncClient::ProcessDownloadInfoMessage( const netadr_t from, const idBitMsg &msg ) {", + ) + background_download = function_body( + file_system, + "void idFileSystemLocal::BackgroundDownload( backgroundDownload_t *bgl ) {", + ) + bounded_transfer = function_body( + file_system, + "static CURLcode FS_ConfigureBoundedHTTPTransfer( CURL *session ) {", + ) + download_worker = function_body( + file_system, + "dword BackgroundDownloadThread( void *parms ) {", + ) + curl_init = function_body( + file_system, + "static bool FS_InitializeCurl() {", + ) + curl_shutdown = function_body( + file_system, + "static void FS_ShutdownCurl() {", + ) + restart = function_body( + file_system, + "void idFileSystemLocal::Restart( void ) {", + ) + + require(client, '#include "../../sys/URLPolicy.h"', "async download URL policy include") + if download_info.count("idURLPolicy::IsAllowedHTTPURL( buf )") < 2: + raise AssertionError("Both redirect and package download URLs must use the shared HTTP/HTTPS policy") + require(file_system, '#include "../sys/URLPolicy.h"', "filesystem download URL policy include") + require(background_download, "bgl->opcode == DLTYPE_URL", "generic URL download discriminator") + require(background_download, "bgl->opcode != DLTYPE_FILE && bgl->opcode != DLTYPE_URL", "unknown download-opcode rejection") + require(background_download, "unsupported background download operation", "unknown download-opcode diagnostic") + require(background_download, "idURLPolicy::IsAllowedHTTPURL( bgl->url.url.c_str() )", "generic URL download guard") + require_before( + background_download, + "idURLPolicy::IsAllowedHTTPURL( bgl->url.url.c_str() )", + "backgroundDownloads = bgl", + "URL guard before background-queue handoff", + ) + require(download_worker, "else if ( bgl->opcode == DLTYPE_URL )", "worker URL opcode allowlist") + require(file_system, "OPENQ4_CURL_CAPABLE_BUILD", "compile-time libcurl capability gate") + require(curl_init, "curl_global_init( CURL_GLOBAL_DEFAULT )", "explicit libcurl global initialization") + require(curl_init, "versionInfo->version_num < 0x071304", "runtime libcurl version gate") + require(curl_init, "CURL_VERSION_ASYNCHDNS", "nonblocking DNS capability gate") + require(curl_init, "hasHTTP", "runtime HTTP protocol capability gate") + require(curl_shutdown, "curl_global_cleanup();", "libcurl global shutdown") + require(download_worker, "if ( !fsCurlHTTPReady )", "worker fail-closed libcurl gate") + require(restart, "StartBackgroundDownloadThread();", "background worker restart lifecycle") + require(bounded_transfer, "CURLOPT_PROTOCOLS", "libcurl protocol allowlist") + require(bounded_transfer, "CURLPROTO_HTTP | CURLPROTO_HTTPS", "libcurl HTTP/HTTPS-only policy") + require(bounded_transfer, "CURLOPT_FOLLOWLOCATION, 0L", "libcurl redirect disable") + require(bounded_transfer, "CURLOPT_CONNECTTIMEOUT, 15L", "bounded connect/DNS wait") + require(bounded_transfer, "CURLOPT_LOW_SPEED_LIMIT, 1024L", "stalled-transfer byte floor") + require(bounded_transfer, "CURLOPT_LOW_SPEED_TIME, 30L", "stalled-transfer timeout") + require(bounded_transfer, "CURLOPT_TIMEOUT, 3600L", "absolute transfer bound") + require(bounded_transfer, "CURLOPT_NOSIGNAL, 1L", "cross-thread libcurl timeout policy") + + +def validate_no_file_url_handoffs() -> None: + for path in (ROOT / "src").rglob("*"): + if path.suffix.lower() not in {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".m", ".mm"}: + continue + text = path.read_text(encoding="utf-8", errors="ignore") + if '"file://' in text: + raise AssertionError(f"Active source still contains a file URL handoff: {path.relative_to(ROOT)}") + + +def validate_registration() -> None: + for path in ( + "tools/validation/openq4_validate.py", + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + require(read(path), "openurl_security.py", f"URL security test registration in {path}") + + +def main() -> None: + validate_shared_policy() + validate_platform_integration() + validate_runtime_coverage() + validate_network_download_integration() + validate_no_file_url_handoffs() + validate_registration() + print("openurl_security: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/p0_governance_evidence.py b/tools/tests/p0_governance_evidence.py new file mode 100644 index 00000000..7f43465b --- /dev/null +++ b/tools/tests/p0_governance_evidence.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Contract checks for P0 provenance and authoritative capability evidence.""" + +from __future__ import annotations + +import importlib.util +import copy +import hashlib +import sys +import tempfile +from pathlib import Path +from types import ModuleType + + +ROOT = Path(__file__).resolve().parents[2] + + +def load_module(name: str, path: Path) -> ModuleType: + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise AssertionError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def read(relative: str) -> str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def require(text: str, snippet: str, context: str) -> None: + if snippet not in text: + raise AssertionError(f"missing {snippet!r} in {context}") + + +def test_provenance_inventory() -> None: + audit = load_module( + "openq4_source_provenance_audit", + ROOT / "tools" / "validation" / "audit_source_provenance.py", + ) + manifest = audit.load_manifest() + report = audit.inventory(ROOT, manifest) + failures = audit.validate(ROOT, manifest, report) + if failures: + raise AssertionError("provenance validation failed:\n" + "\n".join(failures)) + + assert report["families"]["doom3"]["count"] == 581 + bfg_files = report["families"]["doom3_bfg"]["files"] + assert len(bfg_files) == 37 + classifications: dict[str, int] = {} + for entry in bfg_files: + classification = entry.get("classification", "") + classifications[classification] = classifications.get(classification, 0) + 1 + assert entry.get("auditedPath") + assert entry.get("auditedCommit") + assert entry.get("repository") + assert classifications == { + "official-snapshot-path": 31, + "intermediate-fork-lineage": 6, + } + + (ROOT / ".tmp").mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(prefix="provenance-mutation-", dir=ROOT / ".tmp") as temp: + altered = Path(temp) / "altered-terms.txt" + original = ROOT / manifest["families"]["doom3"]["localAdditionalTerms"] + altered.write_bytes(original.read_bytes() + b"altered\n") + mutated_manifest = copy.deepcopy(manifest) + mutated_manifest["families"]["doom3"]["localAdditionalTerms"] = altered.relative_to(ROOT).as_posix() + mutation_failures = audit.validate(ROOT, mutated_manifest, report) + assert any("SHA-256 differs" in failure for failure in mutation_failures) + + official = Path(temp) / "official" + official.mkdir() + copying = official / "COPYING.txt" + copying.write_bytes(b"published copying bytes\r\n") + copying_spec = {"officialCopyingSha256": hashlib.sha256(copying.read_bytes()).hexdigest()} + assert audit.validate_official_copying("test", copying_spec, official) == [] + copying.write_bytes(copying.read_bytes() + b"tampered") + assert any( + "official COPYING.txt SHA-256 differs" in failure + for failure in audit.validate_official_copying("test", copying_spec, official) + ) + + provenance = read("docs/dev/source-provenance.md") + for snippet in ( + "not a legal opinion", + "Doom 3 GPL Source Code", + "Doom 3 BFG Edition GPL Source Code", + "intermediate lineage reference", + "audit_source_provenance.py --check", + ): + require(provenance, snippet, "source provenance documentation") + + +def test_accompanying_terms() -> None: + doom3 = read("LICENSES/DOOM-3-ADDITIONAL-TERMS.txt") + bfg = read("LICENSES/DOOM-3-BFG-ADDITIONAL-TERMS.txt") + require(doom3, "ADDITIONAL TERMS APPLICABLE TO THE DOOM 3 GPL SOURCE CODE", "Doom 3 Additional Terms") + require(bfg, "ADDITIONAL TERMS APPLICABLE TO THE Doom 3 BFG Edition GPL Source Code", "BFG Additional Terms") + for terms in (doom3, bfg): + for section in ( + "Replacement of Section 15", + "Replacement of Section 16", + "LEGAL NOTICES; NO TRADEMARK LICENSE; ORIGIN", + "INDEMNIFICATION", + ): + require(terms, section, "accompanying Additional Terms") + + +def test_capability_matrix_is_authoritative_and_scoped() -> None: + matrix = read("docs/dev/engine-capability-matrix.md") + require(matrix, "authoritative current-state index", "capability matrix") + for status in ("**Implemented**", "**Experimental**", "**Missing**"): + require(matrix, status, "capability matrix status vocabulary") + for capability in ( + "Network-driven executable updater", + "Server-supplied package transport", + "Pure multiplayer game-module boundary", + "Malformed network and snapshot input handling", + "Connection challenge entropy", + "Authenticated remote console (`rcon2`)", + "Rcon abuse limits and secret redaction", + "Legacy plaintext rcon", + "Doom 3 / Doom 3 BFG provenance inventory", + "Reproducible retail-PK4 SP/MP compatibility evidence", + "Backend-neutral renderer contracts", + "Modern visible lighting ownership", + "Shared classic interaction-lighting ownership", + "Vulkan renderer", + "GPU skeletal skinning", + "Automatic dynamic resolution", + "Namespaced PBR materials", + ): + require(matrix, capability, "capability matrix coverage") + require(matrix, "`net_clientUseLegacyRcon 1` / `net_serverAllowLegacyRcon 1`", "legacy rcon containment claim") + require( + matrix, + "**Implemented** (scoped Milestone D consumers; broader modern-renderer use pending)", + "backend-neutral renderer contract scope", + ) + require( + matrix, + "eligible root GUI, world ambient, interaction, fog/blend", + "four implemented shared domains", + ) + require( + matrix, + "Special-view and nested cinematic/post records share exact root/depth identity", + "remaining shared-domain boundary", + ) + require( + matrix, + "The domains remain default-off with clean-package/platform promotion gates", + "shared-domain promotion boundary", + ) + + roadmap = read("docs/dev/idtech5-modernization-roadmap.md") + require( + roadmap, + "domains have retained local runtime qualification", + "roadmap local domain status", + ) + require( + roadmap, + "root cinematic/post, `_currentDepth`, broad subview forms, in-world GUI, special frame, authored-stock breadth, clean-package, retained-review, and target-platform/driver qualification remain open as applicable", + "roadmap promotion boundary", + ) + + renderer_matrix = read("docs/dev/renderer-validation-matrix.md") + proposal = read("docs/dev/proposals/rbdoom3-bfg-parity-modernization-plan.md") + readme = read("README.md") + require(renderer_matrix, "engine capability matrix", "renderer evidence federation") + require(proposal, "historical proposal, not a current-state inventory", "stale proposal warning") + require(readme, "engine capability matrix", "README capability link") + require(readme, "source-provenance inventory", "README provenance link") + + +def test_workflow_registration() -> None: + validator = read("tools/validation/openq4_validate.py") + for test_name in ("p0_governance_evidence.py", "stock_asset_baseline.py"): + require(validator, test_name, "local validation registration") + for workflow in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + text = read(workflow) + require( + text, + "python tools/validation/audit_source_provenance.py --check", + workflow, + ) + for test_name in ("p0_governance_evidence.py", "stock_asset_baseline.py"): + require(text, f"python tools/tests/{test_name}", workflow) + + +def main() -> None: + test_provenance_inventory() + test_accompanying_terms() + test_capability_matrix_is_authoritative_and_scoped() + test_workflow_registration() + print("p0_governance_evidence: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/packaging_safety.py b/tools/tests/packaging_safety.py index fbb286bf..bb5c32f5 100644 --- a/tools/tests/packaging_safety.py +++ b/tools/tests/packaging_safety.py @@ -49,6 +49,7 @@ def load_module(name: str, path: Path) -> ModuleType: VERSION = load_module("openq4_version_safety_test", BUILD_DIR / "openq4_version.py") WINDOWS_RUNTIME = load_module("windows_runtime_safety_test", BUILD_DIR / "windows_runtime.py") MESON_SOURCES = load_module("meson_sources_safety_test", BUILD_DIR / "meson_sources.py") +FAST_STAGE = load_module("stage_fast_install_safety_test", BUILD_DIR / "stage_fast_install.py") def write_file(path: Path, data: bytes = b"data\n") -> None: @@ -345,12 +346,26 @@ def validate_list_sources_guards() -> None: raise AssertionError(f"list_sources.py accepted a symlinked source: {result.stderr}") +# stage_fast_install.py stages by the host's own runtime policy, so the +# guard/copy fixture must use the host platform's artifact names; explicit +# per-platform policy output is pinned by validate_fast_stage_cross_platform_policy. +FAST_STAGE_HOST_FIXTURES = { + "win32": ("openQ4-client_x64.exe", "renderer-gl_x64.dll", "game-sp_x64.dll"), + "linux": ("openQ4-client_x64", "renderer-gl_x64.so", "game-sp_x64.so"), + "darwin": ("openQ4-client_x64", "renderer-vk_x64.dylib", "game-sp_x64.dylib"), +} + + def validate_fast_stage_guards_and_copy() -> None: + if sys.platform not in FAST_STAGE_HOST_FIXTURES: + raise AssertionError(f"unsupported fast-stage host platform: {sys.platform}") + client_name, renderer_name, game_name = FAST_STAGE_HOST_FIXTURES[sys.platform] source_root = WORK / "fast-stage" / "openQ4" build_dir = source_root / "builddir" install_dir = source_root / ".install" - write_file(build_dir / "openQ4-client_x64.exe", b"client\n") - write_file(build_dir / "baseoq4" / "game-sp_x64.dll", b"game\n") + write_file(build_dir / client_name, b"client\n") + write_file(build_dir / renderer_name, b"renderer\n") + write_file(build_dir / "baseoq4" / game_name, b"game\n") write_file(build_dir / "baseoq4" / "pak0.pk4", b"pak0\n") bad_result = run_script( @@ -394,7 +409,7 @@ def validate_fast_stage_guards_and_copy() -> None: "--install-dir", symlink_install_dir, ) - if symlink_result.returncode == 0 or "build directory must not be a symlink" not in symlink_result.stderr: + if symlink_result.returncode == 0 or "build directory must not be a link or junction" not in symlink_result.stderr: raise AssertionError(f"stage_fast_install.py accepted a symlinked build dir: {symlink_result.stderr}") result = run_script( @@ -408,10 +423,213 @@ def validate_fast_stage_guards_and_copy() -> None: ) if result.returncode != 0: raise AssertionError(f"stage_fast_install.py failed safe staging: {result.stderr}") - if not (install_dir / "openQ4-client_x64.exe").is_file(): + if not (install_dir / client_name).is_file(): raise AssertionError("fast stage did not copy root runtime binary") - if not (install_dir / "baseoq4" / "game-sp_x64.dll").is_file(): + if not (install_dir / "baseoq4" / game_name).is_file(): raise AssertionError("fast stage did not copy game runtime binary") + if not (install_dir / renderer_name).is_file(): + raise AssertionError("fast stage did not copy renderer runtime binary") + + escaped_temporary = source_root / ".tmp" / "other-runtime" / "capture" + escaped_result = run_script( + BUILD_DIR / "stage_fast_install.py", + "--source-root", + source_root, + "--build-dir", + build_dir, + "--install-dir", + escaped_temporary, + "--temporary-runtime", + ) + if escaped_result.returncode == 0 or "must stay below" not in escaped_result.stderr: + raise AssertionError( + f"stage_fast_install.py accepted an escaped temporary runtime: {escaped_result.stderr}" + ) + + temporary_runtime = source_root / ".tmp" / "stock-runtime" / "capture" + temporary_result = run_script( + BUILD_DIR / "stage_fast_install.py", + "--source-root", + source_root, + "--build-dir", + build_dir, + "--install-dir", + temporary_runtime, + "--temporary-runtime", + ) + if temporary_result.returncode != 0: + raise AssertionError( + f"stage_fast_install.py failed isolated temporary staging: {temporary_result.stderr}" + ) + if not (temporary_runtime / client_name).is_file(): + raise AssertionError("temporary fast stage did not copy root runtime binary") + if not (temporary_runtime / renderer_name).is_file(): + raise AssertionError("temporary fast stage did not copy renderer runtime binary") + repeated_result = run_script( + BUILD_DIR / "stage_fast_install.py", + "--source-root", + source_root, + "--build-dir", + build_dir, + "--install-dir", + temporary_runtime, + "--temporary-runtime", + ) + if repeated_result.returncode == 0 or "must be new" not in repeated_result.stderr: + raise AssertionError( + f"stage_fast_install.py reused a temporary runtime: {repeated_result.stderr}" + ) + + +def validate_fast_stage_cross_platform_policy() -> None: + source_root = WORK / "fast-stage-platform-policy" / "builddir" + source_game = source_root / "baseoq4" + root_files = ( + "openQ4-client_x64", + "openQ4-ded_x64", + "openQ4-client_x64.exe", + "openQ4-client_x64.pdb", + "openQ4-ded_x64.exe", + "openQ4-ded_x64.pdb", + "renderer-gl_x64.dll", + "renderer-gl_x64.pdb", + "renderer-vk_x64.dll", + "renderer-vk_x64.pdb", + "renderer-gl_x64.so", + "renderer-vk_x64.so", + "renderer-gl_x64.dylib", + "renderer-vk_x64.dylib", + # MoltenVK is provider-staged and verified separately; it must never + # be mistaken for a Meson-produced fast-stage artifact. + "libMoltenVK.dylib", + "OpenAL32.dll", + ) + game_files = ( + "game-sp_x64.dll", + "game-sp_x64.pdb", + "game-mp_x64.dll", + "game-mp_x64.pdb", + "game-sp_x64.so", + "game-mp_x64.so", + "game-sp_x64.dylib", + "game-mp_x64.dylib", + "mod.json", + "pak0.pk4", + "pak1.pk4", + ) + for filename in root_files: + write_file(source_root / filename, filename.encode("utf-8")) + for filename in game_files: + write_file(source_game / filename, filename.encode("utf-8")) + + expected = { + "win32": ( + { + "openQ4-client_x64.exe", + "openQ4-client_x64.pdb", + "openQ4-ded_x64.exe", + "openQ4-ded_x64.pdb", + "renderer-gl_x64.dll", + "renderer-gl_x64.pdb", + "renderer-vk_x64.dll", + "renderer-vk_x64.pdb", + "OpenAL32.dll", + }, + { + "game-sp_x64.dll", + "game-sp_x64.pdb", + "game-mp_x64.dll", + "game-mp_x64.pdb", + "mod.json", + "pak0.pk4", + "pak1.pk4", + }, + ), + "linux": ( + { + "openQ4-client_x64", + "openQ4-ded_x64", + "renderer-gl_x64.so", + "renderer-vk_x64.so", + }, + { + "game-sp_x64.so", + "game-mp_x64.so", + "mod.json", + "pak0.pk4", + "pak1.pk4", + }, + ), + "darwin": ( + { + "openQ4-client_x64", + "openQ4-ded_x64", + "renderer-vk_x64.dylib", + }, + { + "game-sp_x64.dylib", + "game-mp_x64.dylib", + "mod.json", + "pak0.pk4", + "pak1.pk4", + }, + ), + } + for platform_name, (expected_root, expected_game) in expected.items(): + policy = FAST_STAGE.runtime_stage_policy(platform_name) + destination = WORK / "fast-stage-platform-policy" / platform_name + destination_game = destination / "baseoq4" + FAST_STAGE.copy_matches( + source_root, + destination, + policy.root_copy_patterns, + policy.root_copy_exclude_patterns, + ) + FAST_STAGE.copy_matches( + source_game, destination_game, policy.game_copy_patterns + ) + actual_root = { + path.name for path in destination.iterdir() if path.is_file() + } + actual_game = { + path.name for path in destination_game.iterdir() if path.is_file() + } + if actual_root != expected_root: + raise AssertionError( + f"{platform_name} fast-stage root policy copied {sorted(actual_root)!r}; " + f"expected {sorted(expected_root)!r}" + ) + if actual_game != expected_game: + raise AssertionError( + f"{platform_name} fast-stage game policy copied {sorted(actual_game)!r}; " + f"expected {sorted(expected_game)!r}" + ) + + cleanup_root = WORK / "fast-stage-platform-policy" / "windows-cleanup" + cleanup_game = cleanup_root / "baseoq4" + retained_root = cleanup_root / "renderer-gl_x64.dll" + retained_game = cleanup_game / "game-sp_x64.dll" + foreign_paths = ( + cleanup_root / "renderer-gl_x64.so", + cleanup_root / "renderer-vk_x64.dylib", + cleanup_game / "game-sp_x64.so", + cleanup_game / "game-mp_x64.dylib", + ) + for path in (*foreign_paths, retained_root, retained_game): + write_file(path) + FAST_STAGE.remove_non_runtime_files( + cleanup_root, + cleanup_game, + FAST_STAGE.runtime_stage_policy("win32"), + ) + remaining_foreign = [path for path in foreign_paths if path.exists()] + if remaining_foreign: + raise AssertionError( + "Windows fast-stage cleanup retained foreign modules: " + + ", ".join(str(path) for path in remaining_foreign) + ) + if not retained_root.is_file() or not retained_game.is_file(): + raise AssertionError("Windows fast-stage cleanup removed native DLL payloads") def validate_stale_content_prune_symlink_handling() -> None: @@ -901,6 +1119,98 @@ def validate_meson_source_symlink_guards() -> None: def validate_windows_runtime_staging_guards() -> None: + expected_stale_stage_files = { + f"openQ4-{kind}_{arch}" + for kind in ("client", "ded") + for arch in ("x86", "x64", "arm64") + } | { + f"renderer-{renderer}_{arch}.dll.mainbak" + for renderer in ("gl", "vk") + for arch in ("x86", "x64", "arm64") + } + if set(WINDOWS_RUNTIME.WINDOWS_STALE_STAGE_FILE_MANIFEST) != expected_stale_stage_files: + raise AssertionError("Windows stage cleanup must remain an exact stale-file allowlist") + if WINDOWS_RUNTIME.WINDOWS_EMPTY_STAGE_DIRECTORY_MANIFEST != ("baseoq4/skins",): + raise AssertionError("Windows stage cleanup must only prune the known empty skins directory") + + hygiene_root = WORK / "windows-runtime" / "stage-hygiene" + stale_client = hygiene_root / "openQ4-client_x64" + stale_dedicated = hygiene_root / "openQ4-ded_x64" + stale_backup = hygiene_root / "renderer-vk_x64.dll.mainbak" + current_client = hygiene_root / "openQ4-client_x64.exe" + current_renderer = hygiene_root / "renderer-vk_x64.dll" + unlisted_extensionless = hygiene_root / "openQ4-client_custom" + empty_skins = hygiene_root / "baseoq4" / "skins" + for path in (stale_client, stale_dedicated, stale_backup, current_client, current_renderer, unlisted_extensionless): + write_file(path) + empty_skins.mkdir(parents=True) + + hygiene_result = WINDOWS_RUNTIME.cleanup_windows_stage_target(hygiene_root) + if set(hygiene_result["removed_stale_files"]) != { + "openQ4-client_x64", + "openQ4-ded_x64", + "renderer-vk_x64.dll.mainbak", + }: + raise AssertionError(f"unexpected Windows stale-stage cleanup result: {hygiene_result!r}") + if hygiene_result["removed_empty_directories"] != ["baseoq4/skins"]: + raise AssertionError(f"empty Windows stage directory was not pruned: {hygiene_result!r}") + for removed_path in (stale_client, stale_dedicated, stale_backup, empty_skins): + if removed_path.exists() or removed_path.is_symlink(): + raise AssertionError(f"known stale Windows stage entry was retained: {removed_path}") + for preserved_path in (current_client, current_renderer, unlisted_extensionless): + if not preserved_path.is_file(): + raise AssertionError(f"Windows stage cleanup removed an unlisted runtime entry: {preserved_path}") + + populated_root = WORK / "windows-runtime" / "stage-hygiene-populated" + populated_skin = populated_root / "baseoq4" / "skins" / "custom.skin" + write_file(populated_skin) + populated_result = WINDOWS_RUNTIME.cleanup_windows_stage_target(populated_root) + if populated_result["removed_empty_directories"] or not populated_skin.is_file(): + raise AssertionError("Windows stage cleanup must preserve a populated baseoq4/skins directory") + + stale_directory_root = WORK / "windows-runtime" / "stage-hygiene-stale-directory" + (stale_directory_root / "openQ4-client_x64").mkdir(parents=True) + expect_runtime_error( + lambda: WINDOWS_RUNTIME.cleanup_windows_stage_target(stale_directory_root), + "not a regular file", + "Windows stage stale-file directory guard", + ) + + empty_path_file_root = WORK / "windows-runtime" / "stage-hygiene-empty-path-file" + write_file(empty_path_file_root / "baseoq4" / "skins") + expect_runtime_error( + lambda: WINDOWS_RUNTIME.cleanup_windows_stage_target(empty_path_file_root), + "is not a directory", + "Windows stage empty-directory path guard", + ) + + stale_link_root = WORK / "windows-runtime" / "stage-hygiene-stale-link" + stale_link_root.mkdir(parents=True) + stale_link_target = WORK / "windows-runtime" / "stage-hygiene-link-target" + write_file(stale_link_target, b"outside\n") + stale_link = stale_link_root / "renderer-vk_x64.dll.mainbak" + if make_symlink(stale_link_target, stale_link): + link_result = WINDOWS_RUNTIME.cleanup_windows_stage_target(stale_link_root) + if link_result["removed_stale_files"] != ["renderer-vk_x64.dll.mainbak"]: + raise AssertionError("Windows stage cleanup did not report the known stale symlink") + if stale_link.exists() or stale_link.is_symlink(): + raise AssertionError("Windows stage cleanup did not unlink the known stale symlink") + if stale_link_target.read_bytes() != b"outside\n": + raise AssertionError("Windows stage cleanup followed a known stale symlink target") + + linked_game_root = WORK / "windows-runtime" / "stage-hygiene-linked-game-root" + linked_game_target = WORK / "windows-runtime" / "stage-hygiene-linked-game-target" + linked_game_root.mkdir(parents=True) + (linked_game_target / "skins").mkdir(parents=True) + if make_symlink(linked_game_target, linked_game_root / "baseoq4", target_is_directory=True): + expect_runtime_error( + lambda: WINDOWS_RUNTIME.cleanup_windows_stage_target(linked_game_root), + "cleanup parent is a link or junction", + "Windows stage cleanup linked game-root guard", + ) + if not (linked_game_target / "skins").is_dir(): + raise AssertionError("Windows stage cleanup followed a linked baseoq4 directory") + malformed_pe = WORK / "windows-runtime" / "truncated.exe" malformed_pe.parent.mkdir(parents=True, exist_ok=True) data = bytearray(0x48) @@ -1073,6 +1383,7 @@ def main() -> None: validate_legacy_build_pak0_cli_guards() validate_list_sources_guards() validate_fast_stage_guards_and_copy() + validate_fast_stage_cross_platform_policy() validate_stale_content_prune_symlink_handling() validate_package_name_and_copy_guards() validate_renderer_module_staging() diff --git a/tools/tests/parallel_job_system.py b/tools/tests/parallel_job_system.py new file mode 100644 index 00000000..f9e5649d --- /dev/null +++ b/tools/tests/parallel_job_system.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Structural contract checks for the portable bounded job service.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def validate_public_contract() -> None: + header = read("src/framework/ParallelJobSystem.h") + for token in ( + "idJobPriority", + "LOW", + "NORMAL", + "HIGH", + "idJobSubmitResult", + "QUEUE_FULL", + "idJobShutdownMode", + "CANCEL_PENDING", + "idJobCancellationToken", + "IsCancellationRequested", + "maxQueuedLists", + "maxJobs", + "maxDependencies", + "executionTimeMicroseconds", + "queueHighWatermark", + "priorityPromotions", + ): + require(header, token, "public job-system contract") + + +def validate_sleepable_bounded_scheduler() -> None: + source = read("src/framework/ParallelJobSystem.cpp") + require(source, "std::condition_variable workCondition", "sleepable worker scheduler") + require(source, "workCondition.wait", "sleepable worker wait") + require(source, "completionCondition.wait", "sleepable list wait") + require(source, "activeLists.size() >= config.maxQueuedLists", "bounded active-list admission") + require(source, "activeLists.reserve( impl->config.maxQueuedLists )", "preallocated active-list storage") + require(source, "workers.reserve( impl->config.workerThreads )", "preallocated worker storage") + require(source, "return idJobSubmitResult::QUEUE_FULL", "explicit saturation result") + require(source, "state->status != idJobListStatus::BUILDING", "retryable saturation state") + require(source, "MAX_JOBS_PER_LIST", "bounded per-list jobs") + require(source, "MAX_DEPENDENCIES_PER_LIST", "bounded dependency lists") + require(source, "candidate->schedulingAge", "starvation-safe priority aging") + require(source, "JOB_LIST_STARVATION_THRESHOLD", "primary starvation selection class") + require(source, "candidateIsStarved != selectedIsStarved", "starvation class precedence") + require(source, "candidate->submitSequence", "deterministic scheduler tie-break") + reject(source, "std::this_thread::yield", "job scheduler") + reject(source, "Sys_Sleep", "job scheduler") + reject(source, "snapshot = activeLists", "allocation-free waiting-list refresh") + reject(source, "active = impl->activeLists", "allocation-free shutdown cancellation") + + +def validate_engine_lifecycle_and_policy() -> None: + common = read("src/framework/Common.cpp") + for token in ( + 'jobs_enable( "jobs_enable", "1"', + "0 preserves all work by executing job lists synchronously", + "OPENQ4_JOBS_DETERMINISTIC_DEFAULT = \"1\"", + 'jobs_deterministic( "jobs_deterministic"', + 'jobs_numThreads( "jobs_numThreads", "0"', + 'jobs_queueCapacity( "jobs_queueCapacity", "64"', + "!jobs_enable.GetBool() || jobs_deterministic.GetBool()", + "jobSystem.Initialize( jobConfig )", + "jobSystem.Shutdown( idJobShutdownMode::CANCEL_PENDING )", + 'AddCommand( "jobsStats"', + 'AddCommand( "jobsSelfTest"', + "Job system initialized: mode=%s", + "jobsSelfTest PASS v1", + "jobsSelfTest FAILED v1", + "jobsShutdown PASS v1 initialized=0 queued=0 running=0", + "jobsShutdown FAILED v1", + ): + require(common, token, "engine job-service lifecycle") + + +def validate_native_coverage_and_build() -> None: + native = read("tools/tests/native/ParallelJobSystemTest.cpp") + for token in ( + "ExerciseSynchronousMode", + "hard per-list job bound fails closed", + "hard per-list dependency bound fails closed", + "reinitialization refreshes bounded scheduler storage", + "per-list job capacity rejects overflow", + "self dependency fails closed", + "unsubmitted dependency fails closed", + "reverse dependency cycle fails closed", + "ExerciseThreadedDependenciesAndFailure", + "two worker threads execute one list concurrently", + "QUEUE_FULL", + "queue-full list remains retryable", + "pending list cancellation accepted", + "running cooperative job observes cancellation token", + "initial high/normal/low priority order is enforced", + "priority aging prevents low-list starvation", + "oldest-age starvation class runs LOW before nine HIGH lists drain", + "CANCEL_PENDING", + "allocation-free shutdown iteration cancels every shifted pending list", + "ExerciseSynchronousShutdownJoin", + "synchronous in-flight list is visible to shutdown cancellation", + ): + require(native, token, "native job-system coverage") + + meson = read("meson.build") + require(meson, "'openq4-job-system-test'", "native job-system target") + require(meson, "'openq4-job-system'", "native job-system Meson test") + require(meson, "tools/tests/native/ParallelJobSystemTest.cpp", "native job-system source wiring") + require(meson, "src/framework/ParallelJobSystem.cpp", "production job-system source wiring") + validator = read("tools/validation/openq4_validate.py") + require(validator, "parallel_job_system.py", "default validation runner") + for workflow_path in ( + ".github/workflows/push-verification.yml", + ".github/workflows/commit-validation.yml", + ): + workflow = read(workflow_path) + if workflow.count("tools/tests/parallel_job_system.py") < 2: + raise AssertionError( + f"parallel_job_system.py is not compiled and executed by {workflow_path}" + ) + + +def validate_documentation() -> None: + roadmap = read("docs/dev/idtech5-modernization-roadmap.md") + matrix = read("docs/dev/engine-capability-matrix.md") + release = read("docs/dev/release-completion.md") + require(roadmap, "portable bounded job service", "modernization roadmap") + require(matrix, "Background job system for general engine work", "capability matrix") + require(matrix, "ParallelJobSystem.cpp", "capability-matrix implementation evidence") + require(release, "portable job service", "release completion notes") + + +def main() -> None: + validate_public_contract() + validate_sleepable_bounded_scheduler() + validate_engine_lifecycle_and_policy() + validate_native_coverage_and_build() + validate_documentation() + print("parallel_job_system: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/pbr_orm_packer.py b/tools/tests/pbr_orm_packer.py new file mode 100644 index 00000000..a160355f --- /dev/null +++ b/tools/tests/pbr_orm_packer.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +"""Dependency-free regression checks for tools/assets/pack_pbr_orm.py.""" + +from __future__ import annotations + +import importlib.util +import tempfile +from pathlib import Path +from typing import Callable + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "tools" / "assets" / "pack_pbr_orm.py" +TEMP_ROOT = ROOT / ".tmp" +VALIDATOR = ROOT / "tools" / "validation" / "openq4_validate.py" +WORKFLOWS = ( + ROOT / ".github" / "workflows" / "commit-validation.yml", + ROOT / ".github" / "workflows" / "push-verification.yml", +) +SPEC = importlib.util.spec_from_file_location("pack_pbr_orm", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +PACKER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PACKER) + + +class MemoryImage: + """Minimal Pillow-compatible image used to keep this gate dependency-free.""" + + def __init__( + self, + owner: "MemoryImageModule", + size: tuple[int, int], + bands: dict[str, tuple[int, ...]], + ) -> None: + self.owner = owner + self.size = size + self.width, self.height = size + self.bands = {name: tuple(values) for name, values in bands.items()} + expected = self.width * self.height + if not self.bands or any(len(values) != expected for values in self.bands.values()): + raise ValueError("invalid in-memory image") + + def __enter__(self) -> "MemoryImage": + return self + + def __exit__(self, *_args: object) -> None: + return None + + def getbands(self) -> tuple[str, ...]: + return tuple(self.bands) + + def getchannel(self, channel: str) -> "MemoryImage": + return MemoryImage(self.owner, self.size, {"L": self.bands[channel]}) + + def copy(self) -> "MemoryImage": + return MemoryImage(self.owner, self.size, self.bands) + + def pixels(self) -> list[int] | list[tuple[int, ...]]: + names = tuple(self.bands) + if names == ("L",): + return list(self.bands["L"]) + return [tuple(self.bands[name][index] for name in names) for index in range(self.width * self.height)] + + def save(self, path: Path, *, format: str) -> None: + if format not in {"PNG", "TGA"}: + raise AssertionError(f"unexpected fake output format {format}") + resolved = Path(path).resolve() + self.owner.images[resolved] = self.copy() + Path(path).write_bytes(b"in-memory-image\n") + + +class MemoryImageModule: + """In-memory image backend implementing only the packer's required API.""" + + def __init__(self) -> None: + self.images: dict[Path, MemoryImage] = {} + + def add( + self, + path: Path, + bands: dict[str, list[int]], + size: tuple[int, int] = (2, 1), + ) -> None: + path.write_bytes(b"in-memory-source\n") + self.images[path.resolve()] = MemoryImage( + self, + size, + {name: tuple(values) for name, values in bands.items()}, + ) + + def open(self, path: Path) -> MemoryImage: + return self.images[Path(path).resolve()].copy() + + def new(self, mode: str, size: tuple[int, int], *, color: int) -> MemoryImage: + if mode != "L": + raise AssertionError(f"unexpected fake image mode {mode}") + return MemoryImage(self, size, {"L": (color,) * (size[0] * size[1])}) + + def merge(self, mode: str, images: tuple[MemoryImage, ...]) -> MemoryImage: + if mode != "RGB" or len(images) != 3: + raise AssertionError("unexpected fake merge") + if any(image.size != images[0].size for image in images): + raise ValueError("size mismatch") + return MemoryImage( + self, + images[0].size, + { + "R": images[0].bands["L"], + "G": images[1].bands["L"], + "B": images[2].bands["L"], + }, + ) + + def pixels(self, path: Path) -> list[int] | list[tuple[int, ...]]: + return self.images[path.resolve()].pixels() + + +def expect_raises(exception: type[Exception], callback: Callable[[], object]) -> str: + try: + callback() + except exception as exc: + return str(exc) + raise AssertionError(f"expected {exception.__name__}") + + +def test_channel_order_and_implicit_ao() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-orm-packer-") as temporary: + root = Path(temporary) + images = MemoryImageModule() + ao = root / "ao.fake" + roughness = root / "roughness.fake" + metallic = root / "metallic.fake" + output = root / "packed.tga" + images.add(ao, {"L": [3, 13]}) + images.add(roughness, {"L": [7, 17]}) + images.add(metallic, {"L": [11, 19]}) + + PACKER.pack_orm(ao, roughness, metallic, output, False, image_module=images) + assert images.pixels(output) == [(3, 7, 11), (13, 17, 19)] + + white_ao_output = root / "packed-implicit-ao.png" + PACKER.pack_orm(None, roughness, metallic, white_ao_output, False, image_module=images) + assert images.pixels(white_ao_output) == [(255, 7, 11), (255, 17, 19)] + + +def test_explicit_stored_channel_not_luminance() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-orm-packer-") as temporary: + root = Path(temporary) + images = MemoryImageModule() + roughness = root / "roughness-rgb.fake" + metallic = root / "metallic-rgb.fake" + images.add( + roughness, + {"R": [5, 15], "G": [105, 115], "B": [205, 215]}, + ) + images.add( + metallic, + {"R": [7, 17], "G": [107, 117], "B": [207, 217]}, + ) + + red_output = root / "packed-red.tga" + PACKER.pack_orm(None, roughness, metallic, red_output, False, image_module=images) + assert images.pixels(red_output) == [(255, 5, 7), (255, 15, 17)] + + green_output = root / "packed-green.tga" + PACKER.pack_orm( + None, + roughness, + metallic, + green_output, + False, + input_channel="g", + image_module=images, + ) + assert images.pixels(green_output) == [(255, 105, 107), (255, 115, 117)] + message = expect_raises( + ValueError, + lambda: PACKER.pack_orm( + None, + roughness, + metallic, + root / "packed-alpha.tga", + False, + input_channel="a", + image_module=images, + ), + ) + assert "does not contain stored A data" in message + + palette = root / "roughness-palette.fake" + images.add(palette, {"P": [1, 2]}) + message = expect_raises( + ValueError, + lambda: PACKER.pack_orm( + None, + palette, + metallic, + root / "packed-palette.tga", + False, + image_module=images, + ), + ) + assert "does not contain stored R data" in message + + +def test_fail_closed_inputs_and_output() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-orm-packer-") as temporary: + root = Path(temporary) + images = MemoryImageModule() + roughness = root / "roughness.fake" + metallic = root / "metallic.fake" + output = root / "packed.png" + images.add(roughness, {"L": [7, 17]}) + images.add(metallic, {"L": [11]}, (1, 1)) + + expect_raises( + ValueError, + lambda: PACKER.pack_orm(None, roughness, metallic, output, False, image_module=images), + ) + images.add(metallic, {"L": [11, 19]}) + output.write_bytes(b"existing") + expect_raises( + FileExistsError, + lambda: PACKER.pack_orm(None, roughness, metallic, output, False, image_module=images), + ) + expect_raises(ValueError, lambda: PACKER.validate_output(root / "packed.dds", False)) + expect_raises(ValueError, lambda: PACKER.normalize_data_channel("luminance")) + + +def test_module_load_does_not_require_pillow() -> None: + source = SCRIPT.read_text(encoding="utf-8") + assert "image.convert(\"L\")" not in source + assert "image.convert('L')" not in source + # Pillow must remain a function-local, optional command-line dependency. + assert source.index("def pillow_image_module()") < source.index("from PIL import Image") + + +def test_optional_pillow_backend_when_available() -> None: + try: + images = PACKER.pillow_image_module() + except RuntimeError: + return + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-orm-pillow-") as temporary: + root = Path(temporary) + roughness = root / "roughness.png" + metallic = root / "metallic.png" + output = root / "packed.tga" + roughness_image = images.new("RGB", (2, 1)) + roughness_image.putdata([(5, 105, 205), (15, 115, 215)]) + roughness_image.save(roughness, format="PNG") + metallic_image = images.new("RGB", (2, 1)) + metallic_image.putdata([(7, 107, 207), (17, 117, 217)]) + metallic_image.save(metallic, format="PNG") + + PACKER.pack_orm(None, roughness, metallic, output, False, image_module=images) + with images.open(output) as packed: + assert list(packed.getdata()) == [(255, 5, 7), (255, 15, 17)] + + +def test_validation_wiring() -> None: + validator = VALIDATOR.read_text(encoding="utf-8") + assert validator.count('root / "tools" / "tests" / "pbr_orm_packer.py"') == 1 + for workflow_path in WORKFLOWS: + workflow = workflow_path.read_text(encoding="utf-8") + # Once in py_compile and once as an executed dependency-free gate. + assert workflow.count("tools/tests/pbr_orm_packer.py") == 2 + assert workflow.count("tools/assets/pack_pbr_orm.py") == 1 + + +def main() -> int: + test_channel_order_and_implicit_ao() + test_explicit_stored_channel_not_luminance() + test_fail_closed_inputs_and_output() + test_module_load_does_not_require_pillow() + test_optional_pillow_backend_when_available() + test_validation_wiring() + print("pbr_orm_packer: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/pbr_procedural_fixture.py b/tools/tests/pbr_procedural_fixture.py new file mode 100644 index 00000000..6b8b90d9 --- /dev/null +++ b/tools/tests/pbr_procedural_fixture.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Regression checks for the licence-safe procedural PBR fixture generator.""" + +from __future__ import annotations + +import ast +import hashlib +import importlib.util +import json +import os +import subprocess +import tempfile +from pathlib import Path +from typing import Callable + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "tools" / "validation" / "generate_pbr_fixture.py" +TEMP_ROOT = ROOT / ".tmp" +VALIDATOR = ROOT / "tools" / "validation" / "openq4_validate.py" +WORKFLOWS = ( + ROOT / ".github" / "workflows" / "commit-validation.yml", + ROOT / ".github" / "workflows" / "push-verification.yml", +) +EXPECTED_HASHES_16 = { + "baseoq4/materials/openq4_pbr_procedural_fixture.mtr": ( + "ea096bcd6990dc9e690e036365965d69143ddc308de159ced363ad0910adfade" + ), + "baseoq4/textures/openq4/pbrtest/procedural_albedo.tga": ( + "c1eaa28c9aa511199bd03fee41d933816e384d15bbcc92fda570c6ebd5f6d9aa" + ), + "baseoq4/textures/openq4/pbrtest/procedural_normal.tga": ( + "81420b2a13bb77b45770917ccba12f7f048e3cc16e38338982deaee8d331f0b1" + ), + "baseoq4/textures/openq4/pbrtest/procedural_orm.tga": ( + "d6cc68d91332cfedd1cbd4ca51db72fbfe183468ead812f9b17f5a28967596c2" + ), +} +SPEC = importlib.util.spec_from_file_location("generate_pbr_fixture", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +FIXTURE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(FIXTURE) + + +def expect_raises(exception: type[Exception], callback: Callable[[], object]) -> str: + try: + callback() + except exception as exc: + return str(exc) + raise AssertionError(f"expected {exception.__name__}") + + +def decode_tga(path: Path) -> tuple[int, int, list[tuple[int, int, int]]]: + data = path.read_bytes() + assert len(data) >= 18 + assert data[0] == 0 + assert data[1] == 0 + assert data[2] == 2 + width = int.from_bytes(data[12:14], "little") + height = int.from_bytes(data[14:16], "little") + assert data[16] == 24 + assert data[17] == 0x20 + payload = data[18:] + assert len(payload) == width * height * 3 + pixels = [ + (payload[index + 2], payload[index + 1], payload[index]) + for index in range(0, len(payload), 3) + ] + return width, height, pixels + + +def relative_file_bytes(root: Path) -> dict[str, bytes]: + return { + path.relative_to(root).as_posix(): path.read_bytes() + for path in sorted(root.rglob("*")) + if path.is_file() + } + + +def test_generator_is_stdlib_and_original() -> None: + source = SCRIPT.read_text(encoding="utf-8") + tree = ast.parse(source) + imports: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imports.update(alias.name.split(".", 1)[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + imports.add(node.module.split(".", 1)[0]) + assert imports <= { + "__future__", + "argparse", + "hashlib", + "json", + "math", + "pathlib", + "stat", + "struct", + "typing", + } + assert "no third-party texture, shader, or" in source + assert "no external assets" in source + assert "PIL" not in source + assert "urllib" not in source + assert "requests" not in source + + +def test_deterministic_fixture_and_manifest() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-procedural-fixture-") as temporary: + temporary_root = Path(temporary) + first_root = temporary_root / "runtime-a" + second_root = temporary_root / "runtime-b" + first = FIXTURE.generate_fixture(first_root, size=16) + second = FIXTURE.generate_fixture(second_root, size=16) + + assert relative_file_bytes(first_root) == relative_file_bytes(second_root) + assert set(relative_file_bytes(first_root)) == { + "baseoq4/materials/openq4_pbr_procedural_fixture.mtr", + "baseoq4/textures/openq4/pbrtest/procedural_albedo.tga", + "baseoq4/textures/openq4/pbrtest/procedural_normal.tga", + "baseoq4/textures/openq4/pbrtest/procedural_orm.tga", + "openq4-pbr-procedural-fixture.json", + } + + manifest = json.loads(first["manifest"].read_text(encoding="utf-8")) + assert manifest["stockMap"] == "maps/tools/mv2" + assert manifest["benchmarkCase"] == "sp-mv2-interaction" + assert manifest["materialOverride"] == "materials/openq4/pbr_test/procedural_packed" + assert manifest["textureSize"] == [16, 16] + assert "no third-party assets" in manifest["provenance"] + assert "GNU GPL v3" in manifest["license"] + assert ".tmp" in manifest["generatedAssetsPolicy"] + assert manifest["files"] == EXPECTED_HASHES_16 + for relative, expected_hash in manifest["files"].items(): + actual_hash = hashlib.sha256((first_root / relative).read_bytes()).hexdigest() + assert actual_hash == expected_hash + binding = FIXTURE.verify_fixture(first_root, expected_size=16) + assert binding["fixture"] == FIXTURE.FIXTURE_ID + assert binding["materialOverride"] == FIXTURE.MATERIAL_NAME + assert binding["textureSize"] == [16, 16] + assert {record["path"] for record in binding["files"]} == set(EXPECTED_HASHES_16) + + original_orm = first["orm"].read_bytes() + first["orm"].write_bytes(original_orm[:-1] + bytes((original_orm[-1] ^ 1,))) + expect_raises( + RuntimeError, + lambda: FIXTURE.verify_fixture(first_root, expected_size=16), + ) + first["orm"].write_bytes(original_orm) + + original_manifest = first["manifest"].read_bytes() + changed_manifest = json.loads(original_manifest.decode("utf-8")) + changed_manifest["benchmarkCase"] = "sp-storage1" + first["manifest"].write_text( + json.dumps(changed_manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + expect_raises( + RuntimeError, + lambda: FIXTURE.verify_fixture(first_root, expected_size=16), + ) + first["manifest"].write_bytes(original_manifest) + first["manifest"].write_bytes(original_manifest + b"\n") + expect_raises( + RuntimeError, + lambda: FIXTURE.verify_fixture(first_root, expected_size=16), + ) + first["manifest"].write_bytes(original_manifest) + assert FIXTURE.verify_fixture(first_root, expected_size=16) == binding + + material = first["material"].read_text(encoding="utf-8") + assert "Original procedural openQ4 validation fixture" in material + assert "bumpmap _flat" in material + assert "diffusemap textures/openq4/pbrtest/procedural_albedo" in material + assert "specularmap _black" in material + assert "workflow metallicRoughness" in material + assert "normalFormat tangentXYZ" in material + assert "ormMap textures/openq4/pbrtest/procedural_orm" in material + + width, height, albedo = decode_tga(first["albedo"]) + assert (width, height) == (16, 16) + assert len(set(albedo)) > 32 + _, _, normal = decode_tga(first["normal"]) + assert min(pixel[2] for pixel in normal) >= 250 + assert len({(pixel[0], pixel[1]) for pixel in normal}) >= 4 + _, _, orm = decode_tga(first["orm"]) + assert min(pixel[0] for pixel in orm) == 224 + assert max(pixel[0] for pixel in orm) == 255 + assert min(pixel[1] for pixel in orm) == 36 + assert max(pixel[1] for pixel in orm) == 220 + assert {pixel[2] for pixel in orm} == {20, 232} + + baseline_albedo = first["albedo"].read_bytes() + expect_raises(FileExistsError, lambda: FIXTURE.generate_fixture(first_root, size=16)) + first["albedo"].write_bytes(b"modified") + FIXTURE.generate_fixture(first_root, size=16, force=True) + assert first["albedo"].read_bytes() == baseline_albedo + + +def test_output_scope_and_size_fail_closed() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + expect_raises(ValueError, lambda: FIXTURE.generate_fixture(ROOT / "builddir" / "pbr-fixture")) + expect_raises(ValueError, lambda: FIXTURE.generate_fixture(TEMP_ROOT)) + with tempfile.TemporaryDirectory(dir=TEMP_ROOT, prefix="pbr-procedural-fixture-") as temporary: + root = Path(temporary) / "runtime" + expect_raises(ValueError, lambda: FIXTURE.generate_fixture(root, size=12)) + + +def test_nested_links_and_junctions_fail_closed() -> None: + TEMP_ROOT.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory( + dir=TEMP_ROOT, prefix="pbr-procedural-fixture-links-" + ) as temporary: + temporary_root = Path(temporary) + hardlink_runtime = temporary_root / "runtime-hardlink" + hardlink_runtime.mkdir() + hardlink_outside = temporary_root / "outside-hardlink.tga" + hardlink_outside.write_bytes(b"outside-hardlink-sentinel") + hardlink_target = FIXTURE.fixture_paths(hardlink_runtime)["albedo"] + hardlink_target.parent.mkdir(parents=True) + try: + os.link(hardlink_outside, hardlink_target) + except (NotImplementedError, OSError): + pass + else: + message = expect_raises( + RuntimeError, + lambda: FIXTURE.generate_fixture( + hardlink_runtime, size=16, force=True + ), + ) + assert "hard-linked" in message + assert hardlink_outside.read_bytes() == b"outside-hardlink-sentinel" + + outside = temporary_root / "outside-symlink" + outside.mkdir() + sentinel = outside / "sentinel.txt" + sentinel.write_text("unchanged", encoding="utf-8") + runtime = temporary_root / "runtime-symlink" + runtime.mkdir() + nested_link = runtime / "baseoq4" + try: + os.symlink(outside, nested_link, target_is_directory=True) + except (NotImplementedError, OSError): + pass + else: + try: + message = expect_raises( + RuntimeError, + lambda: FIXTURE.generate_fixture(runtime, size=16), + ) + assert "link or junction" in message + assert sentinel.read_text(encoding="utf-8") == "unchanged" + assert list(outside.iterdir()) == [sentinel] + finally: + nested_link.unlink(missing_ok=True) + + if os.name != "nt": + return + junction_outside = temporary_root / "outside-junction" + junction_outside.mkdir() + junction_sentinel = junction_outside / "sentinel.txt" + junction_sentinel.write_text("unchanged", encoding="utf-8") + junction_runtime = temporary_root / "runtime-junction" + junction_runtime.mkdir() + junction = junction_runtime / "baseoq4" + created = subprocess.run( + ["cmd.exe", "/d", "/c", "mklink", "/J", str(junction), str(junction_outside)], + capture_output=True, + text=True, + check=False, + ) + if created.returncode != 0: + return + try: + assert FIXTURE.is_link_or_junction(junction) + message = expect_raises( + RuntimeError, + lambda: FIXTURE.generate_fixture(junction_runtime, size=16), + ) + assert "link or junction" in message + assert junction_sentinel.read_text(encoding="utf-8") == "unchanged" + assert list(junction_outside.iterdir()) == [junction_sentinel] + finally: + os.rmdir(junction) + + +def test_validation_wiring() -> None: + validator = VALIDATOR.read_text(encoding="utf-8") + assert validator.count('root / "tools" / "tests" / "pbr_procedural_fixture.py"') == 1 + for workflow_path in WORKFLOWS: + workflow = workflow_path.read_text(encoding="utf-8") + assert workflow.count("tools/tests/pbr_procedural_fixture.py") == 2 + assert workflow.count("tools/validation/generate_pbr_fixture.py") == 1 + + +def main() -> int: + test_generator_is_stdlib_and_original() + test_deterministic_fixture_and_manifest() + test_output_scope_and_size_fail_closed() + test_nested_links_and_junctions_fail_closed() + test_validation_wiring() + print("pbr_procedural_fixture: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/release_tooling_safety.py b/tools/tests/release_tooling_safety.py index 0f3b0801..6d839e8c 100644 --- a/tools/tests/release_tooling_safety.py +++ b/tools/tests/release_tooling_safety.py @@ -1011,11 +1011,15 @@ def validate_manual_release_linux_runtime_gate() -> None: '["created OpenGL context"]', '["Shutting down OpenGL subsystem (SDL3 backend)"]', "executable = find_client_executable(root)", - "cwd=str(root / \".install\")", + 'runtime_dir = Path(args.runtime_dir).resolve() if args.runtime_dir else root / ".install"', + 'savepath = Path(args.savepath).resolve() if args.savepath else output_dir / "savepath"', + "cwd=str(runtime_dir)", "exit_code == 0 and not timed_out", ): if token not in renderer: raise AssertionError(f"renderer runtime helper is missing release lifecycle contract: {token}") + if 'else root / ".home"' in renderer: + raise AssertionError("renderer validation must not inherit archived user settings by default") def validate_manual_release_companion_checkout() -> None: diff --git a/tools/tests/renderer_advanced_lighting.py b/tools/tests/renderer_advanced_lighting.py new file mode 100644 index 00000000..11b492e5 --- /dev/null +++ b/tools/tests/renderer_advanced_lighting.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 +"""Static integration contracts for the guarded Milestone F lighting work.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +RENDERER = ROOT / "src" / "renderer" + + +def read(relative_path: str) -> str: + path = ROOT / relative_path + if not path.is_file(): + raise AssertionError(f"required source is missing: {relative_path}") + return path.read_text(encoding="utf-8") + + +def require(source: str, token: str, context: str) -> None: + if token not in source: + raise AssertionError(f"missing {token!r} in {context}") + + +def section(source: str, start_marker: str, end_marker: str) -> str: + start = source.find(start_marker) + end = source.find(end_marker, start + len(start_marker)) + if start < 0 or end < 0: + raise AssertionError( + f"missing source section {start_marker!r}..{end_marker!r}" + ) + return source[start:end] + + +def test_conservative_leaf_defaults_and_master_rollback() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + for name, default in ( + ("r_pbrMaterials", "0"), + ("r_rendererModernQuality", "1"), + ("r_rendererReflectionProbes", "0"), + ("r_rendererClusteredDecals", "0"), + ): + require( + init, + f'idCVar {name}( "{name}", "{default}"', + "Milestone F cvar defaults", + ) + require(init, "0 rolls every domain back to classic ownership", "master rollback help") + + bootstrap = read("src/renderer/RendererBootstrap.cpp") + for token in ( + '{ "r_rendererModernQuality", &r_rendererModernQuality, 1 }', + '{ "r_pbrMaterials", &r_pbrMaterials, 0 }', + '{ "r_rendererReflectionProbes", &r_rendererReflectionProbes, 0 }', + '{ "r_rendererClusteredDecals", &r_rendererClusteredDecals, 0 }', + ): + require(bootstrap, token, "default-safety inventory") + + material_table = read("src/renderer/MaterialResourceTable.cpp") + require( + material_table, + "if ( !r_rendererModernQuality.GetBool() || !r_pbrMaterials.GetBool() )", + "PBR master rollback", + ) + require( + material_table, + "return r_rendererModernQuality.GetBool()", + "PBR eligibility master gate", + ) + + light_frontend = read("src/renderer/tr_light.cpp") + probe_gate = section( + light_frontend, + "const bool specularProbeRequested", + "// When its leaf/master gate", + ) + require(probe_gate, "r_rendererModernQuality.GetBool()", "probe master gate") + require(probe_gate, "r_rendererReflectionProbes.GetBool()", "probe leaf gate") + + draw_plan = read("src/renderer/ModernGLDrawPlan.cpp") + decal_gate = section( + draw_plan, + "context.forwardPlusRequested", + "for ( int kind", + ) + require(decal_gate, "r_rendererModernQuality.GetBool()", "decal master gate") + require(decal_gate, "r_rendererClusteredDecals.GetBool()", "decal leaf gate") + + # Either advanced-lighting leaf must request packet construction even when + # no aggregate modern-visible diagnostic is enabled. + scene_packets = read("src/renderer/ScenePackets.cpp") + for token in ( + "advancedLightingLeafRequested", + "r_rendererReflectionProbes.GetBool() || r_rendererClusteredDecals.GetBool()", + "|| advancedLightingLeafRequested", + ): + require(scene_packets, token, "advanced-lighting leaf packet activation") + + backend = read("src/renderer/tr_backend.cpp") + require( + backend, + "R_ModernClusteredLighting_ResetDecalsForFrame();", + "empty-command stale decal reset", + ) + + +def test_probe_decal_bounds_and_shader_abi_markers() -> None: + core = read("src/renderer/AdvancedLightingCore.h") + require(core, "const int ADVANCED_LIGHTING_PROBES_PER_CLUSTER = 2;", "top-two probe bound") + for token in ( + "ADVANCED_LIGHTING_RECORD_PROBE = 0", + "ADVANCED_LIGHTING_RECORD_DECAL", + "ADVANCED_LIGHTING_REJECT_CAPACITY", + "ADVANCED_LIGHTING_REJECT_REFERENCE_CAPACITY", + "volume.generation == 0 || volume.generation != generation", + "const int total = state.probeReferences + state.decalReferences;", + "total >= state.referenceCapacity", + ): + require(core, token, "advanced-lighting CPU contract") + + atlas = read("src/renderer/ModernSpecularProbeAtlas.h") + for token in ( + "MODERN_SPECULAR_PROBE_ATLAS_SIZE = 2048", + "MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE = 256", + "MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT = 6", + "MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES = 8", + "slot >= MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES", + "faceSize > MODERN_SPECULAR_PROBE_ATLAS_FACE_SIZE", + "std::uint64_t atlasGeneration", + "std::uint64_t residencyGeneration", + "std::uint64_t sourceStorageGeneration", + "faceRects[MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT][4]", + ): + require(atlas, token, "specular-probe atlas bound/ABI") + + clustered_header = read("src/renderer/ModernClusteredLighting.h") + for token in ( + "RENDERER_CLUSTER_DECAL_MAX_RECORDS = 1024", + "RENDERER_CLUSTER_DECAL_MAX_REFERENCES = 65536", + "unsigned int\t\tmaterialStableId;", + "unsigned int\t\tgeometryStableId;", + "unsigned int\t\tinstanceStableId;", + "unsigned int\t\tgeneration;", + "RENDERER_CLUSTER_DECAL_REJECT_STALE_GENERATION", + "RENDERER_CLUSTER_DECAL_REJECT_REFERENCE_CAPACITY", + ): + require(clustered_header, token, "clustered-decal bound/identity ABI") + + clustered = read("src/renderer/ModernClusteredLighting.cpp") + for token in ( + "sourceCount > RENDERER_CLUSTER_DECAL_MAX_RECORDS", + "referenceCount > RENDERER_CLUSTER_DECAL_MAX_REFERENCES", + "totalReferences ) + referenceCount > RENDERER_CLUSTER_DECAL_MAX_REFERENCES", + "!rg_clusteredDecalStats.ownershipReady", + "candidate.priority = sphericalWeight > 0.0f ? probe.priority : -1;", + "rg_clusteredDecalStats.generation != R_ModernClusteredLighting_CurrentDecalGeneration()", + ): + require(clustered, token, "clustered-decal bounded publication") + + shader = read("src/renderer/ModernGLShaderLibrary.cpp") + for token in ( + '"#define MODERN_SPECULAR_PROBE_MAX_RECORDS 32\\n"', + '"#define MODERN_SPECULAR_PROBE_ATLAS_SLOTS 8\\n"', + '" vec4 positionRadius;\\n"', + '" vec4 tintIntensity;\\n"', + '" vec4 axisXPriority;\\n"', + '" vec4 axisYBlend;\\n"', + '" vec4 axisZSlot;\\n"', + '" vec4 identity;\\n"', + '"layout(std140) uniform ModernSpecularProbeRecords {\\n"', + "probeIndex = candidate == 0 ? clusterRange.z : clusterRange.w", + "probe.identity.w != frameGeneration", + ): + require(shader, token, "specular-probe shader ABI") + + +def test_clustered_decal_executor_is_atomic_and_exact() -> None: + executor = read("src/renderer/ModernGLExecutor.cpp") + clustered = read("src/renderer/ModernClusteredLighting.cpp") + exact_surface = section( + executor, + "static bool R_ModernGLExecutor_IsExactSingleStageClusteredDecalSurface", + "static bool R_ModernGLExecutor_IsExactSingleStageClusteredDecal", + ) + for token in ( + "material->IsPortalSky()", + "material->SuppressInSubview()", + "stage->texture.image->GetOpts().textureType != TT_2D", + ): + require(exact_surface, token, "classic-suppressed clustered-decal rejection") + + transaction = section( + executor, + "static void R_ModernGLExecutor_PrepareClusteredDecalTransaction", + "static bool R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed", + ) + for token in ( + "packetFrame.NumScenes()", + "packetFrame.NumDrawPackets()", + "packetStats.overflow", + "R_ModernGLExecutor_IsRootWorldDrawScene", + "sourceInventoryCount != authoritativeCount", + "scene.firstDrawPacket", + "viewDef->numDrawSurfs", + "R_ModernGLExecutor_IsAuthoritativeClusteredDecalDraw", + "command.drawPlanEntry->drawPacket == &draw", + "R_ModernGLExecutor_ClusteredDecalCommandReady", + "sourceCount != authoritativeCount", + "R_ModernClusteredLighting_BindGridForView", + "R_ModernClusteredLighting_PrepareDecals", + "R_ModernClusteredLighting_SealDecals", + ): + require(transaction, token, "authoritative clustered-decal transaction") + for token in ( + "geometry->ambientCacheBytes - surface->decalColorOffset", + "R_ModernGLExecutor_BindClusteredDecalColorStream", + "R_ModernGLExecutor_RestoreDrawVertColorStream", + "MODERN_GL_DECAL_COLOR_BINDING_INDEX", + "clusteredDecalSurface != NULL", + "tr.viewportOffset[0]", + "tr.viewportOffset[1]", + "|| command.negativeScale", + "R_ModernGLExecutor_TextureForCommand( command ) != binding->textureHandle", + "binding->image->GetOpts().textureType != TT_2D", + ): + require(executor, token, "clustered-decal baked color-stream binding") + + texture_binding = section( + executor, + "static void R_ModernGLExecutor_BindMaterialTextures", + "static bool R_ModernGLExecutor_ExerciseMaterialTextureTableForSelfTest", + ) + for token in ( + "forceClassicTextureObjectSampling", + "R_ModernGLExecutor_SetTextureTableMode( command, false )", + "R_GLStateCache().BindTexture(", + "R_GLStateCache().BindSampler( MODERN_GL_MATERIAL_TEXTURE_MAIN, 0 )", + ): + require(texture_binding, token, "clustered-decal exact texture sampling") + + legacy_view_restore = section( + executor, + "static void R_ModernGLExecutor_RestoreLegacyViewRect", + "static void R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay", + ) + for token in ( + "tr.viewportOffset[0] + viewDef->viewport.x1", + "tr.viewportOffset[1] + viewDef->viewport.y1", + "R_GLStateCache().SetViewport", + "R_GLStateCache().SetScissor", + "backEnd.currentScissor = viewDef->scissor", + ): + require(legacy_view_restore, token, "clustered-decal legacy view restoration") + if "r_useScissor.GetBool()" in legacy_view_restore: + raise AssertionError("clustered-decal view restoration must not depend on r_useScissor") + + overlay_gate = section( + executor, + "static bool R_ModernGLExecutor_ForwardPlusDecalOverlayAllowed", + "static void R_ModernGLExecutor_RestoreAfterForwardPlusDecalOverlay", + ) + for token in ( + "!r_rendererModernQuality.GetBool()", + "!r_rendererClusteredDecals.GetBool()", + "r_showOverDraw.GetInteger() != 0", + "r_rendererSharedWorldAmbient.GetBool()", + "R_ModernClusteredLighting_DecalViewStats", + ): + require(overlay_gate, token, "clustered-decal master/leaf/view gate") + + ownership_gate = section( + clustered, + "bool R_ModernClusteredLighting_DecalOwnsSurface", + "static void R_ModernClusteredLighting_CountClusterReference", + ) + if ownership_gate.count("r_skipDecals.GetBool()") != 2: + raise AssertionError( + "clustered-decal surface and command ownership must both honor r_skipDecals" + ) + + overlay = section( + executor, + "bool R_ModernGLExecutor_SubmitForwardPlusDecalSurface", + "static bool R_ModernGLExecutor_SharedGuiOwnsCommand", + ) + for token in ( + "R_ModernClusteredLighting_DecalSealedCommandForSurface(", + "Modern clustered decal surface has duplicate sealed commands", + "R_ModernClusteredLighting_BindGridForView( viewDef )", + "GL_DST_COLOR, GL_ZERO", + "GL_ZERO, GL_SRC_COLOR", + "Modern clustered decal command failed after complete preflight", + ): + require(overlay, token, "exact sealed decal surface submission") + if "R_ModernGLExecutor_RejectClusteredDecals" in overlay: + raise AssertionError("sealed clustered-decal submission must not attempt late rollback") + for forbidden in ( + "CommandMatchesForwardPlusDecalOverlayView", + "CommandVisibleForModernPath", + "ForwardPlusMaterialSupported", + ): + if forbidden in overlay: + raise AssertionError( + f"sealed clustered-decal overlay must not re-filter through {forbidden}" + ) + + polygon_offset = section( + executor, + "static bool R_ModernGLExecutor_ClusteredDecalPolygonOffset", + "static bool R_ModernGLExecutor_ClusteredDecalDepthRange", + ) + for token in ( + "stage->privatePolygonOffset != 0.0f", + "polygonOffset = stage->privatePolygonOffset", + "materialRecord->hasMaterialPolygonOffset", + "polygonOffset = materialRecord->polygonOffset", + ): + require(polygon_offset, token, "classic clustered-decal polygon-offset precedence") + if polygon_offset.index("stage->privatePolygonOffset != 0.0f") > polygon_offset.index( + "materialRecord->hasMaterialPolygonOffset" + ): + raise AssertionError("stage-private polygon offset must override the material-wide value") + for token in ( + "R_ModernGLExecutor_ClusteredDecalPolygonOffset(", + "r_offsetUnits.GetFloat() * polygonOffset", + ): + require(overlay, token, "clustered-decal polygon-offset submission") + + executor_selftest = section( + executor, + "static bool R_ModernGLExecutor_ClusteredDecalPolygonOffsetSelfTest", + "static bool R_ModernGLExecutor_InitSelfTestTriangleGeometry", + ) + for token in ( + "privateOverridesMaterial", + "privateAcceptedAlone", + "polygonOffset == -3.0f", + "polygonOffset == 4.0f", + ): + require(executor_selftest, token, "clustered-decal polygon-offset self-test") + require( + executor, + "if ( !R_ModernGLExecutor_ClusteredDecalPolygonOffsetSelfTest() )", + "clustered-decal polygon-offset self-test wiring", + ) + + clustered_header = read("src/renderer/ModernClusteredLighting.h") + require( + clustered_header, + "R_ModernClusteredLighting_RejectDecalsForFrame", + "explicit zero-ownership rejection API", + ) + + classic = read("src/renderer/draw_common.cpp") + for token in ( + "RB_STD_DrawShaderPasses( drawSurfs, processed, RB_DrawSurfIsPreFogMaterialPass )", + "R_ModernGLExecutor_SubmitForwardPlusDecalSurface", + ): + require(classic, token, "ordered one-for-one clustered-decal replacement") + + +def test_hiz_and_diagnostic_submit_own_complete_framebuffer_targets() -> None: + executor = read("src/renderer/ModernGLExecutor.cpp") + hiz_restore = section( + executor, + "static void R_ModernGLExecutor_RestoreAfterHiZBuild", + "static bool R_ModernGLExecutor_PrimeGpuDrivenSelfTestHiZ", + ) + diagnostic_submit = section( + executor, + "static void R_ModernGLExecutor_SubmitPlan", + "static void R_ModernGLExecutor_SubmitModernGui", + ) + for source, label in ( + (hiz_restore, "Hi-Z soft handoff"), + (diagnostic_submit, "diagnostic submit"), + ): + require(source, "BindFramebuffer( GL_FRAMEBUFFER, 0 )", f"{label} framebuffer ownership") + require(source, "glReadBuffer( GL_BACK )", f"{label} read target") + require(source, "glDrawBuffer( GL_BACK )", f"{label} draw target") + + +def test_native_cpu_and_atlas_tests_are_wired() -> None: + meson = read("meson.build") + for token in ( + "'openq4-advanced-lighting-core-test'", + "files('tools/tests/native/AdvancedLightingCoreTest.cpp')", + "'openq4-advanced-lighting-core'", + "'openq4-advanced-screen-space-core-test'", + "files('tools/tests/native/AdvancedScreenSpaceCoreTest.cpp')", + "'openq4-advanced-screen-space-core'", + "'openq4-specular-probe-atlas-packing-test'", + "files('tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp')", + "'openq4-specular-probe-atlas-packing'", + ): + require(meson, token, "native Milestone F Meson wiring") + + validator = read("tools/validation/openq4_validate.py") + require(validator, 'base.append("-Dbuild_native_tests=true")', "local native-test setup") + require(validator, "wrapper + test_args(build_dir)", "local native-test execution") + + cpu_test = read("tools/tests/native/AdvancedLightingCoreTest.cpp") + for token in ( + "ADVANCED_LIGHTING_REJECT_INVALID_GENERATION", + "ADVANCED_LIGHTING_REJECT_CAPACITY", + "reference overflow must be bounded", + "numeric_limits::quiet_NaN()", + "the default-off master gate must publish no records", + ): + require(cpu_test, token, "native advanced-lighting coverage") + + atlas_test = read("tools/tests/native/ModernSpecularProbeAtlasPackingTest.cpp") + for token in ( + "MODERN_SPECULAR_PROBE_ATLAS_MAX_ENTRIES", + "MODERN_SPECULAR_PROBE_ATLAS_FACE_COUNT", + "face rectangles must be disjoint", + "oversized faces must not produce a placement", + ): + require(atlas_test, token, "native atlas-packing coverage") + + screen_space_test = read("tools/tests/native/AdvancedScreenSpaceCoreTest.cpp") + for token in ( + "ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES", + "ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS", + "the GL/Vulkan eight-float packet must be stable", + "the master rollback packet must be exactly zero-initialized", + ): + require(screen_space_test, token, "native screen-space lighting coverage") + + source_discovery = read("tools/build/meson_sources.py") + if source_discovery.count('"src/renderer/ModernSpecularProbeAtlas.cpp"') != 1: + raise AssertionError("specular-probe atlas implementation must be discovered exactly once") + + +def test_validation_entry_points_include_this_contract() -> None: + validator = read("tools/validation/openq4_validate.py") + if validator.count('root / "tools" / "tests" / "renderer_advanced_lighting.py"') != 1: + raise AssertionError("local validation must include this contract exactly once") + + for workflow_path in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + workflow = read(workflow_path) + if workflow.count("tools/tests/renderer_advanced_lighting.py") != 2: + raise AssertionError( + f"{workflow_path} must syntax-check and execute this contract exactly once" + ) + + +def test_parity_stays_unpromoted_and_follow_ons_have_separate_gates() -> None: + executor = read("src/renderer/ModernGLExecutor.cpp") + matches = re.findall( + r"static const int\s+MODERN_LIGHTING_PARITY_PROVEN_DOMAINS\s*=\s*([^;]+);", + executor, + ) + if matches != ["0"]: + raise AssertionError( + "MODERN_LIGHTING_PARITY_PROVEN_DOMAINS must have one declaration equal to 0" + ) + + docs = "\n".join( + ( + read("docs/dev/engine-capability-matrix.md"), + read("docs/dev/idtech5-modernization-roadmap.md"), + read("docs/dev/renderer-validation-matrix.md"), + ) + ) + for feature in ("Froxel", "SSR", "SSGI"): + require(docs, feature, "separate advanced-lighting follow-on documentation") + if not re.search( + r"Froxel[^\n]*(?:SSR[^\n]*SSGI|volumetrics)[^\n]*(?:separate|independent)", + docs, + re.IGNORECASE, + ): + raise AssertionError("froxel, SSR, and SSGI must remain independently gated") + + temporal = read("src/renderer/TemporalPresentation.cpp") + for feature, symbol in ( + ("froxel", "r_rendererFroxelVolumetrics"), + ("SSR", "r_rendererSSR"), + ("SSGI", "r_rendererSSGI"), + ): + require( + temporal, + f'idCVar {symbol}( "{symbol}", "0"', + f"{feature} explicit default-off gate", + ) + require( + temporal, + "AdvancedScreenSpaceCore_Build(\n\t\tr_rendererModernQuality.GetBool()", + "Milestone F master admission", + ) + + core = read("src/renderer/AdvancedScreenSpaceCore.h") + for token in ( + "ADVANCED_SCREEN_SPACE_EFFECT_FROXEL = 1 << 0", + "ADVANCED_SCREEN_SPACE_EFFECT_SSR = 1 << 1", + "ADVANCED_SCREEN_SPACE_EFFECT_SSGI = 1 << 2", + "ADVANCED_SCREEN_SPACE_FROXEL_MAX_SLICES = 16", + "ADVANCED_SCREEN_SPACE_SSR_MAX_STEPS = 16", + "ADVANCED_SCREEN_SPACE_SSGI_TAPS = 8", + ): + require(core, token, "bounded independent screen-space contract") + + gl = read("src/renderer/draw_common.cpp") + vk = read("src/renderer/Vulkan/shaders/temporal_resolve.frag") + for source, label in ((gl, "OpenGL"), (vk, "Vulkan")): + for token in ( + "ApplyFroxelVolumetrics", + "ApplyScreenSpaceReflection", + "ApplyScreenSpaceGI", + ): + require(source, token, f"{label} screen-space implementation") + + +def main() -> int: + tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")] + for test in tests: + test() + print("renderer_advanced_lighting: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_budget_contract.py b/tools/tests/renderer_budget_contract.py new file mode 100644 index 00000000..0fbd847e --- /dev/null +++ b/tools/tests/renderer_budget_contract.py @@ -0,0 +1,1164 @@ +#!/usr/bin/env python3 +"""Deterministic contract tests for per-map renderer CPU/GPU budgets.""" + +from __future__ import annotations + +import contextlib +import importlib.util +import io +import json +import os +import struct +import sys +import tempfile +from pathlib import Path +from types import ModuleType + + +ROOT = Path(__file__).resolve().parents[2] +VALIDATION_DIR = ROOT / "tools" / "validation" +if str(VALIDATION_DIR) not in sys.path: + sys.path.insert(0, str(VALIDATION_DIR)) + +import renderer_budget_contract as budget +import generate_pbr_fixture as pbr_fixture + + +def load_benchmark() -> ModuleType: + path = ROOT / "tools" / "tests" / "renderer_gameplay_benchmark.py" + spec = importlib.util.spec_from_file_location("openq4_renderer_gameplay_benchmark", path) + if spec is None or spec.loader is None: + raise AssertionError("could not load renderer_gameplay_benchmark.py") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def timing_marker( + *, + map_name: str = "game/storage1", + backend: str = "opengl", + profile: str = "baseline", + cpu_samples: int = 256, + cpu: tuple[int, int, int] = (7000, 11000, 14000), + gpu_available: bool = True, + gpu_samples: int = 252, + gpu: tuple[int, int, int] = (5000, 8000, 10000), +) -> str: + return ( + f"{budget.MARKER_PREFIX} map={map_name} backend={backend} profile={profile} " + f"cpuSamples={cpu_samples} cpuP50Us={cpu[0]} cpuP95Us={cpu[1]} cpuP99Us={cpu[2]} " + f"gpuAvailable={int(gpu_available)} gpuSamples={gpu_samples} " + f"gpuP50Us={gpu[0]} gpuP95Us={gpu[1]} gpuP99Us={gpu[2]}" + ) + + +def write_tga(path: Path, width: int = 1280, height: int = 720) -> None: + header = bytearray(18) + header[2] = 2 + struct.pack_into(" None: + contract, binding = budget.load_contract(budget.DEFAULT_CONTRACT_PATH) + assert contract["contractId"] == "stock-baseline-targets-v1" + assert len(contract["budgets"]) == 16 + assert binding["path"] == "tools/validation/renderer_per_map_budgets.json" + assert not Path(binding["path"]).is_absolute() + + marker = timing_marker() + evidence, failures = budget.evaluate_timing_evidence( + (marker,), contract, "game/storage1", "opengl", "baseline" + ) + assert failures == [] and evidence["status"] == "pass" + assert evidence["budgetId"] == "storage1-opengl-baseline" + assert evidence["measurement"]["cpu"]["p95Us"] == 11000 + assert evidence["measurement"]["gpu"]["p99Us"] == 10000 + + # A single console line mirrored once into stdout is accepted, but a + # command that emits the same marker twice in one authoritative stream is + # rejected as an ambiguous capture window. + _, mirrored_failures = budget.parse_timing_evidence((marker, marker)) + assert mirrored_failures == [] + _, duplicate_failures = budget.parse_timing_evidence((marker + "\n" + marker,)) + assert any("contains 2" in failure for failure in duplicate_failures) + _, conflicting_failures = budget.parse_timing_evidence( + (marker, timing_marker(cpu=(7000, 12000, 14000))) + ) + assert any("conflicting" in failure for failure in conflicting_failures) + + for changed, fragment in ( + ({"expected_map": "game/storage2"}, "does not match expected"), + ({"expected_backend": "vulkan"}, "timing backend"), + ({"expected_profile": "modern"}, "timing profile"), + ): + _, identity_failures = budget.evaluate_timing_evidence( + (marker,), + contract, + changed.get("expected_map", "game/storage1"), + changed.get("expected_backend", "opengl"), + changed.get("expected_profile", "baseline"), + ) + assert any(fragment in failure for failure in identity_failures) + + unavailable = timing_marker( + gpu_available=False, gpu_samples=0, gpu=(-1, -1, -1) + ) + _, unavailable_failures = budget.evaluate_timing_evidence( + (unavailable,), contract, "game/storage1", "opengl", "baseline" + ) + assert "required GPU frame timing is unavailable" in unavailable_failures + _, malformed_unavailable = budget.parse_timing_evidence( + (timing_marker(gpu_available=False, gpu_samples=1, gpu=(-1, -1, -1)),) + ) + assert any("zero samples" in failure for failure in malformed_unavailable) + + _, sample_failures = budget.evaluate_timing_evidence( + (timing_marker(cpu_samples=119, gpu_samples=63),), + contract, + "game/storage1", + "opengl", + "baseline", + ) + assert any("CPU samples" in failure for failure in sample_failures) + assert any("GPU samples" in failure for failure in sample_failures) + _, threshold_failures = budget.evaluate_timing_evidence( + (timing_marker(cpu=(7000, 20001, 28001), gpu=(5000, 20001, 28001)),), + contract, + "game/storage1", + "opengl", + "baseline", + ) + assert sum("exceeds" in failure for failure in threshold_failures) == 4 + + +def test_schema_rejection(base: Path) -> None: + payload = json.loads(budget.DEFAULT_CONTRACT_PATH.read_text(encoding="utf-8")) + payload["budgets"].append(dict(payload["budgets"][0])) + duplicate = base / "duplicate.json" + duplicate.write_text(json.dumps(payload), encoding="utf-8") + try: + budget.load_contract(duplicate) + except ValueError as exc: + assert "duplicate budget id" in str(exc) + else: + raise AssertionError("duplicate contract identity must fail") + + payload = json.loads(budget.DEFAULT_CONTRACT_PATH.read_text(encoding="utf-8")) + payload["budgets"][0]["gpu"]["required"] = False + optional_gpu = base / "optional-gpu.json" + optional_gpu.write_text(json.dumps(payload), encoding="utf-8") + try: + budget.load_contract(optional_gpu) + except ValueError as exc: + assert "gpu.required must be true" in str(exc) + else: + raise AssertionError("promotion budget must fail closed on unavailable GPU timing") + + +def test_benchmark_replay(base: Path) -> None: + harness = load_benchmark() + relative_base = os.path.relpath(base, ROOT) + assert harness.resolve_basepath(relative_base) == str(base.resolve()) + assert harness.resolve_basepath(str(base / "missing")) == "" + contract, binding = budget.load_contract(budget.DEFAULT_CONTRACT_PATH) + runtime_dir = base / "runtime" + runtime_dir.mkdir() + try: + harness.validate_runtime_dir(runtime_dir, ROOT) + except ValueError as exc: + assert ".tmp\\stock-runtime" in str(exc) or ".tmp/stock-runtime" in str(exc) + else: + raise AssertionError("alternate benchmark runtime outside .tmp/stock-runtime must fail") + suffix = ".exe" if os.name == "nt" else "" + executable = runtime_dir / f"openQ4-client_{harness.host_arch()}{suffix}" + executable.write_bytes(b"deterministic-client") + fixture_paths = pbr_fixture.generate_fixture( + runtime_dir, size=pbr_fixture.DEFAULT_TEXTURE_SIZE + ) + launch_args = harness.parse_args( + [ + "--cases", + "mp-q4dm1-listen", + "--render-api", + "vk", + "--set-cvar", + "r_mode=5", + "--dry-run", + ] + ) + launch_args.runtime_dir_path = runtime_dir + launch_args.budget_contract = contract + mp_spec = harness.build_specs(launch_args)[0] + dry_mp = harness.run_mp_spec( + ROOT, + executable, + base / "mp-plan", + "", + "test-run", + mp_spec, + 0, + launch_args, + ) + + def cvar_values(arguments: list[str], name: str) -> list[str]: + return [ + arguments[index + 2] + for index in range(len(arguments) - 2) + if arguments[index] in ("+set", "+seta") and arguments[index + 1] == name + ] + + assert cvar_values(dry_mp["serverArgs"], "ui_autoJoin") == ["1"] + assert cvar_values(dry_mp["clientArgs"], "ui_autoJoin") == ["1"] + assert cvar_values(dry_mp["serverArgs"], "si_pure") == ["1"] + assert cvar_values(dry_mp["serverArgs"], "net_serverAllowServerMod") == ["0"] + assert cvar_values(dry_mp["serverArgs"], "r_renderApi") == ["vulkan"] + assert cvar_values(dry_mp["clientArgs"], "r_renderApi") == ["vulkan"] + assert cvar_values(dry_mp["serverArgs"], "logFileName") == [ + f"logs/{harness.ROLE_LOG_NAME}" + ] + assert cvar_values(dry_mp["clientArgs"], "logFileName") == [ + f"logs/{harness.ROLE_LOG_NAME}" + ] + server_cfg = ( + base + / "mp-plan" + / "savepaths" + / f"{mp_spec.id}_server" + / "baseoq4" + / dry_mp["serverAutoexecCfg"] + ).read_text(encoding="utf-8") + client_cfg = ( + base + / "mp-plan" + / "savepaths" + / f"{mp_spec.id}_client" + / "baseoq4" + / dry_mp["clientAutoexecCfg"] + ).read_text(encoding="utf-8") + assert f"waitMsec {harness.MP_SERVER_CLIENT_GRACE_MSEC}" in server_cfg + assert f"waitMsec {harness.MP_SERVER_CLIENT_GRACE_MSEC}" not in client_cfg + for payload in (server_cfg, client_cfg): + lines = payload.splitlines() + reset_index = lines.index("framePacingReset") + for name, value in harness.budget_display_contract()["cvars"].items(): + matches = [ + line + for line in lines[:reset_index] + if line.startswith(f"{name} ") or line.startswith(f"set {name} ") + ] + assert matches + assert matches[-1] == f"{name} {value}" + assert [ + line + for line in lines[:reset_index] + if line.startswith("r_mode ") or line.startswith("set r_mode ") + ] == [ + "set r_mode 5", + "r_mode -1", + ] + for arguments in (dry_mp["serverArgs"], dry_mp["clientArgs"]): + for name, value in { + "r_fullscreen": "0", + "r_borderless": "0", + "r_borderlessDefaultMigrated": "1", + "r_fullscreenDesktop": "0", + "r_windowWidth": "1280", + "r_windowHeight": "720", + "r_mode": "-1", + "r_customWidth": "1280", + "r_customHeight": "720", + }.items(): + assert cvar_values(arguments, name) == [value] + assert dry_mp["displayContract"] == harness.budget_display_contract() + assert cvar_values(dry_mp["serverArgs"], "fs_devpath") == [ + str(base / "mp-plan" / "savepaths" / f"{mp_spec.id}_server") + ] + savepath = base / "evidence" / "savepaths" / "case" + log = savepath / "baseoq4" / "logs" / "case.log" + stdout = base / "evidence" / "case.out.txt" + stderr = base / "evidence" / "case.err.txt" + screenshot = savepath / "baseoq4" / "screenshots" / "renderer-bench" / "sp_0.tga" + log.parent.mkdir(parents=True) + stdout.parent.mkdir(parents=True, exist_ok=True) + marker = timing_marker() + log.write_text( + marker + + "\nMap: tools/mv2\n" + + "\nrendererBenchmark capture(preset=baseline samples=256 frame(avg=10 p50=8 p95=11 p99=14 max=15 latest=10 thresholds=20/28 pass=1)\n" + + "Renderer benchmark: preset=baseline samples=256 frame(avg=10 p50=8 p95=11 p99=14 max=15) thresholds(p95=20 p99=28 pass=1)\n" + + "Selected renderer tier: LowOverheadGL45\n" + + "PBR material resources: records=3 resourceReady=3 modernReady=3 packed=3 separate=0 fallback=0\n" + + "Modern GL executor: available, drawPlan=1 planDraws=5 planFallback=0 pbrOwners=2 pbrConsumed=4\n" + + "Modern forward+: cvar=1, req=1 exec=1 resources=1 sceneColor=1 sceneDepth=1 program=1 cluster=1 draws=2 fallback=0\n" + + "Modern visible frame: cvar=1, req=1 exec=1 resources=1 program=1 source=1 hybrid=1 backBuffer=1 composed=1\n" + + "MODE: -1, 1280 x 720 windowed hz:N/A\n", + encoding="utf-8", + ) + stdout.write_text(log.read_text(encoding="utf-8"), encoding="utf-8") + stderr.write_text("", encoding="utf-8") + write_tga(screenshot) + spec = harness.RunSpec( + case_id="sp-storage1", + mode="SP", + map_name="game/storage1", + budget_map_name="game/storage1", + purpose="test", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="default", + renderer="best", + render_api="gl", + ) + autoexec_cfg, _ = harness.write_autoexec_cfg( + savepath, + spec, + "sp", + "contract-test", + 360, + 600, + 0, + harness.PBR_ACCEPTANCE_GL_CVARS, + gpu_timers=True, + ) + autoexec_path = savepath / "baseoq4" / autoexec_cfg + role = harness.evaluate_role_result( + spec, + "sp", + 0, + False, + 1.0, + savepath, + "case.log", + stdout, + stderr, + "screenshots/renderer-bench/sp_0.tga", + None, + 2.0, + 24, + False, + True, + 0.0, + 0.0, + 0.0, + contract, + "baseline", + pbr_fixture_acceptance_required=True, + autoexec_path=autoexec_path, + ) + assert role["status"] == "pass", role["missing"] + result = { + "id": spec.id, + "mode": "SP", + "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "pbrFixtureAcceptanceRequired": True, + "displayContract": harness.budget_display_contract(), + "purpose": "test", + "tier": "auto", + "maxfps": "240", + "swapInterval": "0", + "display": "windowed", + "shadowPreset": "default", + "renderer": "best", + "status": "pass", + "roles": [role], + } + harness.attach_result_artifacts(base / "evidence", [result]) + runtime_files = harness.collect_runtime_files(runtime_dir) + metadata = { + "generated": "2026-08-19 00:00:00 +0000", + "host": "test", + "executable": str(executable), + "runtime": { + "path": harness.path_hint(runtime_dir, ROOT), + "executable": executable.relative_to(runtime_dir).as_posix(), + "files": runtime_files, + }, + "git": harness.git_state(ROOT), + "budgetContract": binding, + "budgetEnforced": True, + "budgetDisplayContract": harness.budget_display_contract(), + "basepath": "", + "profile": "smoke", + "benchmarkPreset": "baseline", + "renderApi": "gl", + "dryRun": False, + "autoexecDelayMs": 1000, + "settleFrames": 360, + "mpClientDelayFrames": 480, + "sampleFrames": 600, + "sampleMsec": 0, + "minPacingHz": 0.0, + "maxP95Ms": 0.0, + "maxP99Ms": 0.0, + "profileCvars": {}, + "effectivePostMapCvars": harness.effective_post_map_cvars( + harness.PBR_ACCEPTANCE_GL_CVARS + ), + "pbrFixtureAcceptanceRequired": True, + "pbrFixtureBinding": harness.verify_procedural_pbr_fixture( + runtime_dir, harness.PBR_FIXTURE_TEXTURE_SIZE + ), + "profileExecCommands": [], + "launchCvars": {}, + "execCommands": [], + } + report_path, _ = harness.write_reports(base / "evidence", [result], metadata) + report = json.loads(report_path.read_text(encoding="utf-8")) + assert harness.verify_benchmark_report( + report, report_path.parent, ROOT, runtime_dir, executable, contract, binding + ) == [] + + for field, expected_fragment in ( + ("effectivePostMapCvars", "post-map cvar provenance"), + ("pbrFixtureAcceptanceRequired", "acceptance provenance"), + ("pbrFixtureBinding", "binding provenance"), + ("execCommands", "exec-command provenance"), + ): + missing_metadata = json.loads(json.dumps(report)) + missing_metadata["metadata"].pop(field) + assert any( + expected_fragment in failure + for failure in harness.verify_benchmark_report( + missing_metadata, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + missing_result_selection = json.loads(json.dumps(report)) + missing_result_selection["results"][0].pop("pbrFixtureAcceptanceRequired") + assert any( + "acceptance selection is missing" in failure + for failure in harness.verify_benchmark_report( + missing_result_selection, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + missing_role_evidence = json.loads(json.dumps(report)) + missing_role_evidence["results"][0]["roles"][0].pop("pbrFixtureEvidence") + assert any( + "recorded PBR fixture evidence differs" in failure + for failure in harness.verify_benchmark_report( + missing_role_evidence, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + missing_config = json.loads(json.dumps(report)) + missing_config_role = missing_config["results"][0]["roles"][0] + missing_config_role["artifacts"] = [ + item + for item in missing_config_role["artifacts"] + if item["kind"] != "benchmarkConfig" + ] + assert any( + "recorded benchmarkConfig artifact is missing" in failure + for failure in harness.verify_benchmark_report( + missing_config, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + original_config = autoexec_path.read_text(encoding="utf-8") + autoexec_path.write_text( + original_config.replace("r_pbrmaterials 1", "r_pbrmaterials 0"), + encoding="utf-8", + ) + changed_config_report = json.loads(json.dumps(report)) + changed_config_role = changed_config_report["results"][0]["roles"][0] + changed_config_artifact = next( + item + for item in changed_config_role["artifacts"] + if item["kind"] == "benchmarkConfig" + ) + changed_config_artifact.update( + harness.file_record(autoexec_path, report_path.parent) + ) + assert any( + "benchmark config CVar provenance differs" in failure + for failure in harness.verify_benchmark_report( + changed_config_report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + autoexec_path.write_text(original_config, encoding="utf-8") + + autoexec_path.write_text( + original_config.replace( + harness.EXEC_COMMANDS_BEGIN, + harness.EXEC_COMMANDS_BEGIN + "\ngod", + ), + encoding="utf-8", + ) + changed_commands_report = json.loads(json.dumps(report)) + changed_commands_role = changed_commands_report["results"][0]["roles"][0] + changed_commands_artifact = next( + item + for item in changed_commands_role["artifacts"] + if item["kind"] == "benchmarkConfig" + ) + changed_commands_artifact.update( + harness.file_record(autoexec_path, report_path.parent) + ) + assert any( + "benchmark config command provenance differs" in failure + for failure in harness.verify_benchmark_report( + changed_commands_report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + autoexec_path.write_text(original_config, encoding="utf-8") + + autoexec_path.write_text( + "echo PBR material resources: records=9 resourceReady=9 modernReady=9 " + "packed=9 separate=0 fallback=0\n" + original_config, + encoding="utf-8", + ) + injected_config_report = json.loads(json.dumps(report)) + injected_config_role = injected_config_report["results"][0]["roles"][0] + injected_config_artifact = next( + item + for item in injected_config_role["artifacts"] + if item["kind"] == "benchmarkConfig" + ) + injected_config_artifact.update( + harness.file_record(autoexec_path, report_path.parent) + ) + assert any( + "benchmark config provenance is invalid" in failure + for failure in harness.verify_benchmark_report( + injected_config_report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + autoexec_path.write_text(original_config, encoding="utf-8") + + original_fixture_orm = fixture_paths["orm"].read_bytes() + fixture_paths["orm"].write_bytes( + original_fixture_orm[:-1] + bytes((original_fixture_orm[-1] ^ 1,)) + ) + assert any( + "controlled PBR fixture verification failed" in failure + for failure in harness.verify_benchmark_report( + report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + fixture_paths["orm"].write_bytes(original_fixture_orm) + + original_executable = executable.read_bytes() + executable.write_bytes(b"mutated-runtime-client") + assert any( + "runtime file differs" in failure + for failure in harness.verify_benchmark_report( + report, report_path.parent, ROOT, runtime_dir, executable, contract, binding + ) + ) + executable.write_bytes(original_executable) + + original_log = log.read_text(encoding="utf-8") + log.write_text(original_log.replace("cpuP95Us=11000", "cpuP95Us=12000"), encoding="utf-8") + changed_log_report = json.loads(json.dumps(report)) + changed_log_role = changed_log_report["results"][0]["roles"][0] + changed_log_artifact = next( + item for item in changed_log_role["artifacts"] if item["kind"] == "engineLog" + ) + changed_log_artifact.update(harness.file_record(log, report_path.parent)) + assert any( + "recorded renderer budget evidence differs" in failure + for failure in harness.verify_benchmark_report( + changed_log_report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + log.write_text(original_log, encoding="utf-8") + + log.write_text( + original_log.replace("MODE: -1, 1280 x 720 windowed", "MODE: -1, 1279 x 720 windowed"), + encoding="utf-8", + ) + changed_mode_report = json.loads(json.dumps(report)) + changed_mode_role = changed_mode_report["results"][0]["roles"][0] + changed_mode_artifact = next( + item for item in changed_mode_role["artifacts"] if item["kind"] == "engineLog" + ) + changed_mode_artifact.update(harness.file_record(log, report_path.parent)) + assert any( + "runtime MODE" in failure or "recorded display evidence differs" in failure + for failure in harness.verify_benchmark_report( + changed_mode_report, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + log.write_text(original_log, encoding="utf-8") + + original_screenshot = screenshot.read_bytes() + changed_screenshot = bytearray(original_screenshot) + struct.pack_into(" tuple[dict, Path]: + evidence_root = base / f"functional-{render_api}-{tier}" + fixture_savepath = evidence_root / "savepaths" / "case" + fixture_log = fixture_savepath / "baseoq4" / "logs" / "case.log" + fixture_stdout = evidence_root / "case.out.txt" + fixture_stderr = evidence_root / "case.err.txt" + fixture_screenshot = ( + fixture_savepath + / "baseoq4" + / "screenshots" + / "renderer-bench" + / "sp_0.tga" + ) + fixture_log.parent.mkdir(parents=True) + fixture_stdout.parent.mkdir(parents=True, exist_ok=True) + api_lines = ( + [ + "Selected renderer tier: " + harness.GL_TIER_RUNTIME_NAMES[tier], + "Modern GL executor: available, drawPlan=1 planDraws=5 planFallback=0 pbrOwners=2 pbrConsumed=4", + "Modern forward+: cvar=1, req=1 exec=1 resources=1 sceneColor=1 sceneDepth=1 program=1 cluster=1 draws=2 fallback=0", + "Modern visible frame: cvar=1, req=1 exec=1 resources=1 program=1 source=1 hybrid=1 backBuffer=1 composed=1", + ] + if render_api == "gl" + else ["Vulkan: native packed PBR direct interactions active (7 draws)"] + ) + fixture_log_text = "\n".join( + [ + "Map: tools/mv2", + "PBR material resources: records=3 resourceReady=3 modernReady=3 packed=3 separate=0 fallback=0", + *api_lines, + "MODE: -1, 1280 x 720 windowed hz:N/A", + ] + ) + "\n" + fixture_log.write_text(fixture_log_text, encoding="utf-8") + fixture_stdout.write_text(fixture_log_text, encoding="utf-8") + fixture_stderr.write_text("", encoding="utf-8") + write_tga(fixture_screenshot) + fixture_spec = harness.RunSpec( + case_id=harness.PBR_FIXTURE_BENCHMARK_CASE, + mode="SP", + map_name=harness.PBR_FIXTURE_STOCK_MAP, + budget_map_name=harness.PBR_FIXTURE_STOCK_MAP, + purpose="functional PBR replay", + path_name="spawn-static", + tier=tier, + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="default", + renderer="best", + render_api=render_api, + ) + fixture_cvars = harness.pbr_acceptance_fixture_cvars(render_api) + fixture_cfg, _ = harness.write_autoexec_cfg( + fixture_savepath, + fixture_spec, + "sp", + "functional-test", + 360, + 600, + 0, + fixture_cvars, + gpu_timers=False, + renderer_metrics=False, + ) + fixture_cfg_path = fixture_savepath / "baseoq4" / fixture_cfg + fixture_role = harness.evaluate_role_result( + fixture_spec, + "sp", + 0, + False, + 1.0, + fixture_savepath, + "case.log", + fixture_stdout, + fixture_stderr, + "screenshots/renderer-bench/sp_0.tga", + None, + 2.0, + 24, + False, + False, + 0.0, + 0.0, + 0.0, + contract, + "baseline", + pbr_fixture_acceptance_required=True, + autoexec_path=fixture_cfg_path, + ) + assert fixture_role["status"] == "pass", fixture_role["missing"] + fixture_result = { + "id": fixture_spec.id, + "mode": "SP", + "map": fixture_spec.map_name, + "budgetMap": fixture_spec.budget_map_name, + "expectedBackend": fixture_spec.expected_backend, + "renderApi": fixture_spec.render_api, + "pbrFixtureAcceptanceRequired": True, + "displayContract": harness.budget_display_contract(), + "purpose": fixture_spec.purpose, + "tier": fixture_spec.tier, + "maxfps": fixture_spec.maxfps, + "swapInterval": fixture_spec.swap_interval, + "display": fixture_spec.display_mode, + "shadowPreset": fixture_spec.shadow_preset, + "renderer": fixture_spec.renderer, + "status": "pass", + "roles": [fixture_role], + } + harness.attach_result_artifacts(evidence_root, [fixture_result]) + fixture_metadata = { + "generated": "2026-08-23 00:00:00 +0000", + "host": "test", + "executable": str(executable), + "runtime": { + "path": harness.path_hint(runtime_dir, ROOT), + "executable": executable.relative_to(runtime_dir).as_posix(), + "files": harness.collect_runtime_files(runtime_dir), + }, + "runtimeVerificationFailures": [], + "git": harness.git_state(ROOT), + "budgetContract": binding, + "budgetEnforced": False, + "budgetDisplayContract": None, + "basepath": "", + "profile": "interaction", + "benchmarkPreset": "baseline", + "renderApi": render_api, + "dryRun": False, + "autoexecDelayMs": 1000, + "settleFrames": 360, + "mpClientDelayFrames": 480, + "sampleFrames": 600, + "sampleMsec": 0, + "minPacingHz": 0.0, + "maxP95Ms": 0.0, + "maxP99Ms": 0.0, + "profileCvars": {}, + "effectivePostMapCvars": harness.effective_post_map_cvars( + fixture_cvars + ), + "pbrFixtureAcceptanceRequired": True, + "pbrFixtureBinding": harness.verify_procedural_pbr_fixture( + runtime_dir, harness.PBR_FIXTURE_TEXTURE_SIZE + ), + "profileExecCommands": [], + "launchCvars": {}, + "execCommands": [], + } + fixture_report_path, _ = harness.write_reports( + evidence_root, [fixture_result], fixture_metadata + ) + return ( + json.loads(fixture_report_path.read_text(encoding="utf-8")), + fixture_report_path, + ) + + functional_gl, functional_gl_path = functional_fixture_report("gl", "gl45") + assert harness.verify_benchmark_report( + functional_gl, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) == [] + functional_vk, functional_vk_path = functional_fixture_report("vk", "auto") + assert harness.verify_benchmark_report( + functional_vk, + functional_vk_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) == [] + functional_gl33, functional_gl33_path = functional_fixture_report("gl", "gl33") + assert harness.verify_benchmark_report( + functional_gl33, + functional_gl33_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) == [] + + ordinary_non_budget = json.loads(json.dumps(functional_gl)) + ordinary_non_budget["metadata"]["effectivePostMapCvars"] = {} + ordinary_non_budget["metadata"]["pbrFixtureAcceptanceRequired"] = False + ordinary_non_budget["metadata"]["pbrFixtureBinding"] = None + ordinary_non_budget["results"][0]["pbrFixtureAcceptanceRequired"] = False + assert any( + "functional replay is accepted only" in failure + for failure in harness.verify_benchmark_report( + ordinary_non_budget, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + duplicated_functional_result = json.loads(json.dumps(functional_gl)) + duplicated_functional_result["results"].append( + json.loads(json.dumps(duplicated_functional_result["results"][0])) + ) + assert any( + "exactly one controlled result" in failure + for failure in harness.verify_benchmark_report( + duplicated_functional_result, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + tampered_mode = json.loads(json.dumps(report)) + tampered_mode["budgetEnforced"] = False + tampered_mode["metadata"]["budgetDisplayContract"] = None + tampered_mode_failures = harness.verify_benchmark_report( + tampered_mode, + report_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + assert any("config capture mode differs" in failure for failure in tampered_mode_failures) + assert any("budget evidence must be empty" in failure for failure in tampered_mode_failures) + + tampered_budget_evidence = json.loads(json.dumps(functional_gl)) + tampered_budget_evidence["results"][0]["roles"][0]["budgetEvidence"] = { + "status": "pass" + } + assert any( + "budget evidence must be empty" in failure + for failure in harness.verify_benchmark_report( + tampered_budget_evidence, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + missing_null_display_contract = json.loads(json.dumps(functional_gl)) + missing_null_display_contract["metadata"].pop("budgetDisplayContract") + assert any( + "display contract differs" in failure + for failure in harness.verify_benchmark_report( + missing_null_display_contract, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + tampered_outcome = json.loads(json.dumps(functional_gl)) + tampered_outcome["results"][0]["roles"][0]["exitCode"] = 1 + tampered_outcome["results"][0]["roles"][0]["timedOut"] = True + outcome_failures = harness.verify_benchmark_report( + tampered_outcome, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + assert any("exit code is not zero" in failure for failure in outcome_failures) + assert any("timed-out state is not false" in failure for failure in outcome_failures) + + tampered_identity = json.loads(json.dumps(functional_gl)) + tampered_identity["results"][0]["map"] = "game/storage1" + assert any( + "case/result identity differs" in failure + for failure in harness.verify_benchmark_report( + tampered_identity, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + functional_log_artifact = next( + item + for item in functional_gl["results"][0]["roles"][0]["artifacts"] + if item["kind"] == "engineLog" + ) + functional_log_path = functional_gl_path.parent / functional_log_artifact["path"] + functional_log_original = functional_log_path.read_text(encoding="utf-8") + functional_log_path.write_text( + functional_log_original.replace("Map: tools/mv2", "Map: game/storage1"), + encoding="utf-8", + ) + tampered_final_map = json.loads(json.dumps(functional_gl)) + tampered_final_map_artifact = next( + item + for item in tampered_final_map["results"][0]["roles"][0]["artifacts"] + if item["kind"] == "engineLog" + ) + tampered_final_map_artifact.update( + harness.file_record(functional_log_path, functional_gl_path.parent) + ) + assert any( + "final engine Map differs" in failure + for failure in harness.verify_benchmark_report( + tampered_final_map, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + functional_log_path.write_text(functional_log_original, encoding="utf-8") + + tampered_tier = json.loads(json.dumps(functional_gl)) + tampered_tier_result = tampered_tier["results"][0] + tampered_tier_result["tier"] = "gl43" + tampered_tier_result["id"] = tampered_tier_result["id"].replace( + "_gl45_", "_gl43_" + ) + assert any( + "selected tier differs" in failure + for failure in harness.verify_benchmark_report( + tampered_tier, + functional_gl_path.parent, + ROOT, + runtime_dir, + executable, + contract, + binding, + ) + ) + + for invalid_args in ( + ["--width", "1920"], + ["--height", "1080"], + ["--display-modes", "fullscreen"], + ["--set-launch-cvar", "r_windowWidth=1920"], + ["--set-launch-cvar", "r_renderApi=vk"], + ["--set-launch-cvar", "r_pbrMaterials=1"], + ): + try: + with contextlib.redirect_stderr(io.StringIO()): + harness.parse_args(invalid_args) + except SystemExit as exc: + assert exc.code == 2 + else: + raise AssertionError(f"noncanonical budget display must fail: {invalid_args}") + + +def main() -> None: + test_contract_and_marker() + harness = load_benchmark() + runtime_parent = ROOT / ".tmp" / "stock-runtime" + runtime_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="renderer-runtime-contract-", dir=runtime_parent) as raw: + runtime = Path(raw) + assert harness.validate_runtime_dir(runtime, ROOT) == runtime.resolve() + temp_parent = ROOT / ".tmp" / "renderer-budget-tests" + temp_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="contract-", dir=temp_parent) as raw: + base = Path(raw) + test_schema_rejection(base) + test_benchmark_replay(base) + print("renderer_budget_contract: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/renderer_classic_cinematic_post_domain.py b/tools/tests/renderer_classic_cinematic_post_domain.py new file mode 100644 index 00000000..44b5f6f5 --- /dev/null +++ b/tools/tests/renderer_classic_cinematic_post_domain.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +"""Static regression contract for shared cinematic and authored-post ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_cinematic_post_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def require_before(haystack: str, first: str, second: str, context: str) -> None: + first_at = haystack.find(first) + second_at = haystack.find(second) + if first_at < 0 or second_at < 0 or first_at >= second_at: + raise AssertionError(f"Expected {first!r} before {second!r} in {context}") + + +def function_body(source: str, signature: str, context: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"Missing {signature!r} in {context}") + opening = source.find("{", start + len(signature)) + if opening < 0: + raise AssertionError(f"Missing opening brace for {signature!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[opening + 1 : index] + raise AssertionError(f"Unterminated {signature!r} in {context}") + + +def validate_sealed_domain() -> None: + header = read("src/renderer/ClassicCinematicPostDomain.h") + source = read("src/renderer/ClassicCinematicPostDomain.cpp") + packets = read("src/renderer/ScenePackets.cpp") + + for token in ( + "CLASSIC_CINEMATIC_POST_SCOPE_ROOT_CINEMATIC", + "CLASSIC_CINEMATIC_POST_SCOPE_AUTHORED_POST", + "CLASSIC_CINEMATIC_POST_FAILURE_SOURCE_PACKET_MISMATCH", + "CLASSIC_CINEMATIC_POST_FAILURE_CINEMATIC_CLOCK", + "CLASSIC_CINEMATIC_POST_FAILURE_MISSING_SPECIAL_VIEW", + "CLASSIC_CINEMATIC_POST_FAILURE_SPECIAL_VIEW_TRANSACTION_REJECTED", + "CLASSIC_CINEMATIC_POST_FAILURE_BACKEND_COVERAGE_MISMATCH", + "classicCinematicPostDomainView_t", + "specialRootViewDef", + "specialRootScenePacketIndex", + "specialNestingDepth", + "nestedInSpecialView", + "backendCompleted", + "cinematicTimeMilliseconds", + "currentRenderStageCount", + "currentDepthStageCount", + "R_ClassicCinematicPostDomain_PrepareFrame", + "R_ClassicCinematicPostDomain_RecordOwned", + "R_ClassicCinematicPostDomain_RecordBackendFallback", + "R_ClassicCinematicPostDomain_SubviewTransactionReady", + "R_ClassicCinematicPostDomain_SubviewTransactionCompleted", + "R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned", + "R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback", + "RendererClassicCinematicPostDomain_RunSelfTest", + ): + require(header, token, "cinematic/post domain interface") + + for token in ( + "R_ClassicCinematicPostDomain_IsRootGUIPacketEligible", + "stage->texture.cinematic", + "MF_NEED_CURRENT_RENDER", + "RENDER_PASS_GUI", + "RENDER_PASS_AUTHORED_POST", + "SCENE_PACKET_CATEGORY_POST_PROCESS", + "packet.legacyDrawSurf != drawSurf", + "viewDef->floatTime", + "cinematicClock", + "specialRootViewDef", + "nestedInSpecialView", + "!stats.prepared || !stats.frameValid || stats.overflow", + "R_ClassicSubviewDomain_RecordBackendFallback", + "RecordBackendFallback", + "R_ClassicCinematicPostDomain_RangeFits", + "R_ClassicCinematicPostDomain_RangeContains", + "packetFrame.NumDrawPackets()", + "classic cinematic/post and subview GL backend ordinals must match", + "classic cinematic/post and subview Vulkan backend ordinals must match", + "classic cinematic/post and subview backend counts must match", + ): + require(source, token, "sealed source, timing, and fallback handling") + completed = function_body( + source, + "bool R_ClassicCinematicPostDomain_SubviewTransactionCompleted(", + "atomic nested cinematic completion", + ) + for token in ( + "CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED", + "backendCompleted[ backend ]", + "backendDrawnSurfaces[ backend ] != view.sourceSurfaceCount", + ): + require(completed, token, "atomic nested cinematic completion") + require_before( + source, + "int packetCursor = pass.firstDrawPacket;", + "R_ClassicCinematicPostDomain_CommitView( view );", + "root packet reconciliation before commit", + ) + require( + packets, + "R_ScenePackets_AddFilteredDrawSurfPass( packetFrame, viewDef, RENDER_PASS_AUTHORED_POST", + "authored post packet production", + ) + + +def validate_backend_handoffs() -> None: + gl = read("src/renderer/draw_common.cpp") + gl_backend = read("src/renderer/tr_backend.cpp") + vk = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + vk_backend = read("src/renderer/Vulkan/vk_Backend.cpp") + subview = read("src/renderer/ClassicSubviewDomain.cpp") + + for token in ( + "RB_DrawSharedCinematicRootView", + "RB_DrawSharedAuthoredPostView", + "RB_STD_DrawShaderPasses", + "R_ClassicCinematicPostDomain_RecordOwned", + "R_ClassicCinematicPostDomain_RecordBackendFallback", + ): + require(gl, token, "OpenGL dynamic-stage adapter") + root = function_body(gl, "bool RB_DrawSharedCinematicRootView(", "OpenGL root adapter") + require(root, "RB_BeginDrawingView();", "OpenGL root view setup") + require(root, "RB_STD_DrawShaderPasses", "OpenGL root dynamic-stage execution") + post = function_body(gl, "bool RB_DrawSharedAuthoredPostView(", "OpenGL post adapter") + require(post, "drawSurfs + firstSourceSurface", "OpenGL exact authored post tail") + gl_world = function_body(gl, "RB_STD_DrawView( void )", "OpenGL world adapter") + require_before( + gl_world, + "sharedAuthoredPostView->backendOutcome[", + "RB_DrawSharedAuthoredPostView(", + "OpenGL already-rolled-back member bypass", + ) + require( + gl_world, + "CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED", + "OpenGL already-rolled-back member bypass", + ) + + for source, backend, direct_signature in ( + (gl_backend, "OpenGL", "static void RB_DrawSharedDirectSubview("), + (vk_backend, "Vulkan", "static void VK_DrawSharedDirectSubview("), + ): + require(source, "R_ClassicCinematicPostDomain_ResetFrame();", f"{backend} reset") + require(source, "R_ClassicCinematicPostDomain_PrepareFrame( *scenePackets );", f"{backend} packet preparation") + require_before( + source, + "R_ClassicSubviewDomain_PrepareFrame( *scenePackets );", + "R_ClassicCinematicPostDomain_PrepareFrame( *scenePackets );", + f"{backend} special topology before nested dynamic ranges", + ) + copy_case = function_body( + source, "case RC_COPY_RENDER:", f"{backend} copy transaction" + ) + if copy_case.count("== CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED") < 2: + raise AssertionError( + f"Expected sealed-match and late-outcome guards in {backend} copy transaction" + ) + direct = function_body(source, direct_signature, f"{backend} direct transaction") + require_before( + direct, + "!= CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED", + "R_ClassicSubviewDomain_RecordDirectOwned", + f"{backend} late nested rollback before direct publication", + ) + require(gl_backend, "RB_DrawSharedCinematicRootView( drawView )", "OpenGL root dispatch") + + for token in ( + "sharedCinematicRoot", + "sharedAuthoredPostCandidate", + "VK_Exec_DrawAmbientStages", + "R_ClassicCinematicPostDomain_RecordOwned", + "R_ClassicCinematicPostDomain_RecordBackendFallback", + ): + require(vk, token, "Vulkan dynamic-stage adapter") + fallback = function_body( + vk, + "static void VK_RecordCinematicPostFallbackIfPending(", + "Vulkan pending-outcome fallback", + ) + for token in ( + "CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED", + "backendCompleted[CLASSIC_CINEMATIC_POST_BACKEND_VULKAN]", + "R_ClassicCinematicPostDomain_RecordBackendFallback", + ): + require(fallback, token, "Vulkan pending-outcome fallback") + vk_root = function_body(vk, "void VK_GuiExecutor_Draw2DView(", "Vulkan root adapter") + for token in ( + "VK_RecordCinematicPostFallbackIfPending", + "VK_CLASSIC_CINEMATIC_POST_REJECT_VIEWPORT", + ): + require(vk_root, token, "Vulkan root early-return accounting") + vk_world = function_body(vk, "void VK_GuiExecutor_Draw3DView(", "Vulkan world adapter") + for token in ( + "VK_CLASSIC_CINEMATIC_POST_REJECT_SKIP_OR_EMPTY", + "VK_CLASSIC_CINEMATIC_POST_REJECT_BEGIN_FRAME", + "VK_CLASSIC_CINEMATIC_POST_REJECT_GPU_SKINNING", + "VK_CLASSIC_CINEMATIC_POST_REJECT_VIEWPORT", + "VK_CLASSIC_CINEMATIC_POST_REJECT_NO_POST_WALK", + "!sharedAuthoredPostView->backendCompleted[", + ): + require(vk_world, token, "Vulkan authored-post outcome closure") + if vk_world.count("VK_RecordCinematicPostFallbackIfPending") < 5: + raise AssertionError( + "Expected every Vulkan authored-post early/terminal exit to record fallback" + ) + require_before( + vk_world, + "sharedAuthoredPostView->backendOutcome[", + "sharedAuthoredPostReady", + "Vulkan already-rolled-back member bypass", + ) + require( + vk_world, + "CLASSIC_CINEMATIC_POST_BACKEND_UNRECORDED", + "Vulkan already-rolled-back member bypass", + ) + require_before( + vk_world, + "sharedAuthoredPostCandidate", + "R_ClassicCinematicPostDomain_RecordOwned( viewDef,", + "Vulkan post ownership after the complete dynamic walk", + ) + for token in ( + "R_ClassicCinematicPostDomain_SubviewTransactionCompleted", + "R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned", + "R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE", + ): + require(subview, token, "atomic special-view/cinematic transaction coupling") + + +def validate_controls_and_evidence() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + local = read("src/renderer/tr_local.h") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + matrix = read("tools/tests/renderer_validation_matrix.py") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + registry = read("tools/validation/openq4_validate.py") + roadmap = read("docs/dev/idtech5-modernization-roadmap.md") + domain_doc = read("docs/dev/classic-cinematic-post-domain-modernization.md") + fixture = read("tools/tests/renderer_milestone_d_fixture.py") + acceptance = read("tools/tests/renderer_milestone_d_acceptance.py") + hardening = read("tools/tests/validation_hardening.py") + workflows = ( + read(".github/workflows/commit-validation.yml"), + read(".github/workflows/push-verification.yml"), + ) + + require(init, 'r_rendererSharedCinematicPost( "r_rendererSharedCinematicPost", "0"', "default-off cvar") + require(init, 'cmdSystem->AddCommand( "rendererClassicCinematicPostDomainSelfTest"', "native self-test") + require(init, "Renderer shared cinematic/post:", "gfxInfo diagnostics") + require(init, "coverageMismatches", "cinematic/post mismatch diagnostics") + require(init, "duplicateReports", "cinematic/post duplicate diagnostics") + require(local, "extern idCVar r_rendererSharedCinematicPost;", "renderer cvar API") + require(bootstrap, '{ "r_rendererSharedCinematicPost", &r_rendererSharedCinematicPost, 0 }', "default safety") + for source, context in ( + (matrix, "safe renderer matrix"), + (benchmark, "benchmark isolation"), + (baseline, "stock-baseline isolation"), + ): + require(source, "r_rendererSharedCinematicPost", context) + require(matrix, "+rendererClassicCinematicPostDomainSelfTest", "safe native self-test") + require(registry, '"renderer_classic_cinematic_post_domain.py"', "validation registry") + require(roadmap, "Milestone D scoped implementation is complete", "Milestone D completion boundary") + for token in ( + "maps/tools/mv2.map", + "video/idlogo.roq", + "412fc796e569660c92dca5d55359e5c4eb3d572754bd1cdff8ce994b0b802fe2", + "375c9641c96359c2e0d49cdd3905cc2cdc66a7b067c9a8910d80bf36fb06c8f4", + "milestone_d_nested_dynamic.map", + "milestoneD/nestedCinematicPost", + "shaderDemos/transparentMirror", + "Temporary Milestone D validation material; never shipped.", + '"shippingContent": False', + '"r_fullscreen", "0"', + '"in_mouse", "0"', + '"+dmap"', + '"--temporary-runtime"', + 'ROOT / ".tmp/stock-runtime"', + "is_link_or_junction", + 'for suffix in (".proc", ".cm")', + "def stock_dependency_inventory(basepath: Path)", + '"stockDependencies": stock_dependencies_before', + "stock_dependencies_after = stock_dependency_inventory(basepath)", + "if stock_dependencies_after != stock_dependencies_before:", + "def validate_runtime_components(runtime_dir: Path)", + '"runtimeComponents": runtime_components', + "prepare_macos_moltenvk.sh", + "libMoltenVK.dylib", + "validation_map_payload(retrieved_map)", + "timeout=timeout_seconds", + "allow_nan=False", + ): + require(fixture, token, "temporary stock-derived Milestone D fixture") + for case_block in ( + '"classic-normal",\n "classic subview and cinematic/post paths, normal post processing",\n 0,\n 0,\n 0,', + '"subview-only-normal",\n "shared subview only, with cinematic/post domain disabled",\n 1,\n 0,\n 0,', + '"cinematic-only-normal",\n "shared cinematic/post only; nested authored post remains vacuous",\n 0,\n 1,\n 0,', + '"both-normal",\n "shared subview plus nested cinematic/post ownership",\n 1,\n 1,\n 0,', + '"classic-skip",\n "classic paths with r_skipPostProcess forced",\n 0,\n 0,\n 1,', + '"both-skip",\n "shared domains requested with deterministic nested post fallback",\n 1,\n 1,\n 1,', + ): + require(acceptance, case_block, "Milestone D six-case CVar matrix") + for token in ( + 'return ("gl", "vk") if value == "all"', + '"milestone-d-nested-dynamic"', + '"-B"', + '"--pacing-only"', + '"--no-gpu-timers"', + '"windowed"', + "CINEMATIC_PATTERN", + "SUBVIEW_PATTERN", + "SUBVIEW_BACKEND_PATTERN", + '"classic-normal-vs-subview-only-normal"', + '"classic-normal-vs-cinematic-only-normal"', + '"classic-normal-vs-both-normal"', + '"classic-skip-vs-both-skip"', + '"classic-normal-vs-classic-skip"', + '"both-normal-vs-both-skip"', + "runtime_before = benchmark.collect_runtime_files(runtime_dir)", + "runtime_after = benchmark.collect_runtime_files(runtime_dir)", + '"--fixture-manifest"', + "def validate_fixture_manifest(", + "fixture_builder.stock_dependency_inventory(basepath)", + 'manifest.get("stockDependencies") != stock_dependencies', + "fixture_builder.validate_runtime_components(runtime_dir)", + 'manifest.get("runtimeComponents") != runtime_components', + '"manifestArtifact": {', + "prefix_count = sum(", + "backend_prefix_count = sum(", + 'inactive_outcome = f"{inactive_label}=0/none/0"', + "math.isfinite(args.difference_min_rms)", + "benchmark.evaluate_display_evidence(", + 'failures.extend(f"actual display evidence: {failure}"', + 'f"{prefix}_mismatch"', + 'f"{prefix}_duplicate"', + "fixture_after = validate_fixture_manifest(", + "_reject_link_ancestry", + "allow_nan=False", + '"screenshotSource": "engine screenshot command"', + "REPORT_JSON_NAME", + "REPORT_MD_NAME", + ): + require(acceptance, token, "Milestone D acceptance contract") + for runtime_driver in ( + "tools/tests/renderer_milestone_d_acceptance.py", + "tools/tests/renderer_milestone_d_fixture.py", + ): + require(hardening, Path(runtime_driver).name, "runtime-driver validation allowlist") + for workflow in workflows: + if workflow.count(runtime_driver) != 1: + raise AssertionError( + f"Expected exactly one compile-only {runtime_driver!r} workflow entry" + ) + require(benchmark, '"milestone-d-nested-dynamic"', "nested runtime benchmark profile") + require( + benchmark, + '("r_rendererSharedSubview", "1")', + "nested runtime benchmark shared subview default", + ) + require( + benchmark, + '("r_rendererSharedCinematicPost", "1")', + "nested runtime benchmark shared cinematic/post default", + ) + for token in ( + "videoMap", + "soundMap", + "_currentRender", + "r_rendererSharedCinematicPost 1", + "classic fallback", + "engine-written", + "nested", + "atomic", + ): + require(domain_doc, token, "domain documentation") + + +def main() -> int: + validate_sealed_domain() + validate_backend_handoffs() + validate_controls_and_evidence() + print("renderer_classic_cinematic_post_domain: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_classic_deform_domain.py b/tools/tests/renderer_classic_deform_domain.py new file mode 100644 index 00000000..605e9ef6 --- /dev/null +++ b/tools/tests/renderer_classic_deform_domain.py @@ -0,0 +1,807 @@ +#!/usr/bin/env python3 +"""Static regression contract for sealed classic material-deform ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_deform_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def require_count(haystack: str, needle: str, expected: int, context: str) -> None: + actual = haystack.count(needle) + if actual != expected: + raise AssertionError( + f"Expected {expected} occurrence(s) of {needle!r} in {context}, got {actual}" + ) + + +def require_order(haystack: str, needles: tuple[str, ...], context: str) -> None: + previous = -1 + for needle in needles: + position = haystack.find(needle, previous + 1) + if position == -1: + raise AssertionError(f"Missing {needle!r} in {context}") + if position <= previous: + raise AssertionError( + f"Expected ordered contracts in {context}: {needles!r}" + ) + previous = position + + +def braced_body(source: str, marker: str, context: str) -> str: + start = source.find(marker) + if start == -1: + raise AssertionError(f"Missing {marker!r} in {context}") + opening = source.find("{", start + len(marker)) + if opening == -1: + raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find closing brace for {marker!r} in {context}") + + +def validate_sealed_record_contract() -> None: + header = read("src/renderer/ClassicDeformDomain.h") + source = read("src/renderer/ClassicDeformDomain.cpp") + for token in ( + "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER", + "CLASSIC_DEFORM_ROLE_FOG_RECEIVER", + "CLASSIC_DEFORM_ROLE_SHADOW_VOLUME", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "CLASSIC_DEFORM_OUTCOME_SKIPPED", + "CLASSIC_DEFORM_OUTCOME_COMPLETED", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + "CLASSIC_DEFORM_OUTCOME_FAILED", + "CLASSIC_DEFORM_OUTCOME_UNSUPPORTED", + "CLASSIC_DEFORM_CACHE_FRAME_TEMP", + "ambientCacheHasBacking", + "indexCacheHasBacking", + "frameToken", + "sourceMaterialNameHash", + "resultMaterialNameHash", + "deformDeclNameHash", + "inputSemanticHash", + "semanticHash", + "R_ClassicDeformDomain_CurrentFrameToken", + "R_ClassicDeformDomain_SnapshotDrawSurf", + "R_ClassicDeformDomain_ValidateRecordForFrame", + "R_ClassicDeformDomain_RecordFreshForFrame", + "R_ClassicDeformDomain_RecordMatchesDrawSurf", + "R_ClassicDeformDomain_SameProvenance", + "R_ClassicDeformDomain_HasCompletedOutput", + "R_ClassicDeformDomain_HasEmptyOutput", + "R_ClassicDeformDomain_IsFailClosed", + "RendererClassicDeformDomain_RunSelfTest", + ): + require(header, token, "sealed material-deform API") + + initializer = braced_body( + source, + "static void InitializeRecordFromDrawSurf(", + "role-aware material-deform initialization", + ) + for token in ( + "record.role = role;", + "record.frameToken = frameToken;", + "role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "r_skipDeforms.GetBool()", + "CLASSIC_DEFORM_OUTCOME_SKIPPED", + "KindIsUnsupported( record.kind )", + "CLASSIC_DEFORM_OUTCOME_UNSUPPORTED", + "CLASSIC_DEFORM_OUTCOME_FAILED", + ): + require(initializer, token, "role/outcome initialization") + + snapshot = braced_body( + source, + "void R_ClassicDeformDomain_SnapshotDrawSurf(", + "explicit receiver snapshot", + ) + require_order( + snapshot, + ( + "role != CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "InitializeRecordFromDrawSurf( drawSurf, role, frameToken, false, record );", + "SealRecord( record );", + "return;", + ), + "receiver NOT_APPLICABLE sealing", + ) + + completed = braced_body( + source, "static bool CompletedOutputProvable(", "completed deform proof" + ) + for token in ( + "MaterialIdentityMatches( record )", + "result.geometry != record.sourceGeometry.geometry", + "result.cacheStateValid", + "result.ambientCacheHasBacking", + "result.indexCacheHasBacking", + "result.ambientTag == TAG_TEMP", + "result.indexTag == TAG_TEMP", + "CLASSIC_DEFORM_CACHE_FRAME_TEMP", + ): + require(completed, token, "completed output cache proof") + + empty = braced_body(source, "static bool EmptyOutputProvable(", "empty deform proof") + for token in ( + "MaterialIdentityMatches( record )", + "result.geometry != record.sourceGeometry.geometry", + "result.indexCount == 0", + "record.cpuFinalized", + ): + require(empty, token, "empty output proof") + + validator = braced_body( + source, "static bool ValidateRecordInternal(", "sealed-record validation" + ) + for token in ( + "record.frameToken == 0", + "record.role <= CLASSIC_DEFORM_ROLE_UNKNOWN", + "record.sourceGeometry.cacheStateValid", + "record.resultGeometry.cacheStateValid", + "R_ClassicDeformDomain_ComputeSemanticHash( record )", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "CLASSIC_DEFORM_OUTCOME_COMPLETED", + "CompletedOutputProvable( record )", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + "EmptyOutputProvable( record )", + "CLASSIC_DEFORM_OUTCOME_FAILED", + "CLASSIC_DEFORM_OUTCOME_UNSUPPORTED", + ): + require(validator, token, "role/outcome/cache/hash validation") + + semantic_hash = braced_body( + source, + "std::uint64_t R_ClassicDeformDomain_ComputeSemanticHash(", + "address-independent deform hash", + ) + for token in ( + "CONTRACT_HASH_VERSION", + "record.role", + "record.kind", + "record.outcome", + "record.sourceMaterialNameHash", + "record.resultMaterialNameHash", + "record.deformDeclNameHash", + "record.inputSemanticHash", + "record.sourceGeometry", + "record.resultGeometry", + "record.parameterValues", + "record.cpuFinalized", + ): + require(semantic_hash, token, "semantic deform hash") + for token in ("record.frameToken", "ambientBuffer", "indexBuffer"): + reject(semantic_hash, token, "address/frame/backend-independent semantic hash") + + freshness = braced_body( + source, + "bool R_ClassicDeformDomain_RecordFreshForFrame(", + "frame freshness proof", + ) + for token in ("frameToken != 0", "record.initialized", "record.frameToken == frameToken"): + require(freshness, token, "frame freshness proof") + + provenance = braced_body( + source, + "bool R_ClassicDeformDomain_SameProvenance(", + "material-deform provenance identity", + ) + for token in ( + "a.frameToken == b.frameToken", + "a.semanticHash == b.semanticHash", + "a.role == b.role", + "a.outcome == b.outcome", + "a.sourceMaterial == b.sourceMaterial", + "a.resultMaterial == b.resultMaterial", + "GeometryStateMatches( a.sourceGeometry, b.sourceGeometry )", + "GeometryStateMatches( a.resultGeometry, b.resultGeometry )", + ): + require(provenance, token, "material-deform provenance identity") + + fail_closed = braced_body( + source, "bool R_ClassicDeformDomain_IsFailClosed(", "atomic deform fallback" + ) + for token in ( + "!R_ClassicDeformDomain_ValidateRecord( record )", + "CLASSIC_DEFORM_OUTCOME_COMPLETED", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + ): + require(fail_closed, token, "completed/empty acceptance and fail-closed outcomes") + + selftest = braced_body( + source, + "bool RendererClassicDeformDomain_RunSelfTest(", + "material-deform runtime self-test", + ) + for token in ( + "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER", + "CLASSIC_DEFORM_OUTCOME_COMPLETED", + "R_ClassicDeformDomain_ValidateRecordForFrame( completed, 1 )", + "R_ClassicDeformDomain_HasCompletedOutput( completed )", + "driftedMaterial.resultMaterial =", + "driftedMaterial.resultMaterialIndex++;", + "driftedMaterial.resultMaterialNameHash++;", + "R_ClassicDeformDomain_HasCompletedOutput( driftedMaterial )", + "ambientCacheHasBacking = true", + "same.frameToken = 2", + "same.resultGeometry.ambientBuffer = 99", + "R_ClassicDeformDomain_SameProvenance( completed, same )", + "same.parameterValues[ 0 ] = 3.0f", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + "R_ClassicDeformDomain_HasEmptyOutput( empty )", + "R_ClassicDeformDomain_HasEmptyOutput( driftedMaterial )", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "CLASSIC_DEFORM_OUTCOME_SKIPPED", + "CLASSIC_DEFORM_OUTCOME_FAILED", + "CLASSIC_DEFORM_KIND_PARTICLE", + "CLASSIC_DEFORM_OUTCOME_UNSUPPORTED", + "R_ClassicDeformDomain_IsFailClosed", + ): + require(selftest, token, "outcome/freshness/hash/cache runtime coverage") + + +def validate_frontend_packet_provenance() -> None: + local = read("src/renderer/tr_local.h") + light = read("src/renderer/tr_light.cpp") + packet_header = read("src/renderer/ScenePackets.h") + packet_source = read("src/renderer/ScenePackets.cpp") + require(local, '#include "ClassicDeformDomain.h"', "draw-surface deform API") + require(local, "classicDeformRecord_t\tclassicDeform;", "draw-surface sealed record") + + material_init = braced_body( + read("src/renderer/Material.cpp"), + "void idMaterial::CommonInit()", + "material deform-declaration initialization", + ) + require(material_init, "deformDecl = NULL;", "safe non-table deform capture") + + finalize = braced_body(light, "void R_FinalizeDrawSurf(", "authoritative CPU deform call") + require_order( + finalize, + ( + "R_ClassicDeformDomain_BeginDrawSurf( drawSurf );", + "R_DeformDrawSurf( drawSurf );", + "R_ClassicDeformDomain_EndDrawSurf( drawSurf );", + ), + "CPU deform begin/end bracket", + ) + + if packet_header.count("hasClassicDeformRecord;") < 2: + raise AssertionError("Draw and geometry packets must both seal deform provenance") + require( + packet_header, + "const classicDeformRecord_t *classicDeformRecord;", + "compact draw-packet deform reference", + ) + require_count( + packet_header, + "classicDeformRecord_t\tclassicDeform;", + 1, + "single geometry-owned deform record", + ) + for token in ( + "drawPacketsWithClassicDeformRecord", + "materialDeformDrawPackets", + "deformFinalizedDrawPackets", + "deformInteractionReceiverPackets", + "deformFogReceiverPackets", + "deformShadowVolumePackets", + "deformCompletedPackets", + "deformEmptyPackets", + "deformNotApplicablePackets", + "deformSkippedPackets", + "deformFailedPackets", + "deformUnsupportedPackets", + "deformFallbackPackets", + ): + require(packet_header, token, "scene-packet deform diagnostics") + + role_classifier = braced_body( + packet_source, + "static classicDeformRole_t R_ScenePackets_ClassicDeformRoleForPass(", + "explicit packet deform roles", + ) + for category, role in ( + ("RENDER_PASS_ARB2_INTERACTION", "CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER"), + ("RENDER_PASS_FOG_BLEND", "CLASSIC_DEFORM_ROLE_FOG_RECEIVER"), + ("RENDER_PASS_STENCIL_SHADOW", "CLASSIC_DEFORM_ROLE_SHADOW_VOLUME"), + ("default:", "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW"), + ): + require_order(role_classifier, (category, role), "packet deform-role classification") + + lookup_key = braced_body( + packet_source, + "static int R_ScenePackets_GeometryLookupKey(", + "provenance-sensitive geometry lookup key", + ) + for token in ( + "classicDeform.semanticHash", + "classicDeform.frameToken", + "classicDeform.role", + ): + require(lookup_key, token, "provenance-sensitive geometry lookup key") + + geometry = braced_body( + packet_source, + "int idScenePacketFrame::FindOrAddGeometryRecord(", + "provenance-sensitive geometry record", + ) + for token in ( + "R_ScenePackets_GeometryLookupKey( geo, classicDeform )", + "R_ClassicDeformDomain_SameProvenance(", + "record.classicDeform = classicDeform;", + "record.hasClassicDeformRecord =", + "R_ClassicDeformDomain_ValidateRecordForFrame( classicDeform,", + ): + require(geometry, token, "provenance-sensitive geometry record") + if geometry.count("R_ClassicDeformDomain_SameProvenance(") < 2: + raise AssertionError("Both hashed and linear geometry lookup paths must verify provenance") + + add_draw = braced_body( + packet_source, "bool idScenePacketFrame::AddDrawPacket(", "draw-packet deform seal" + ) + require_order( + add_draw, + ( + "R_ScenePackets_ClassicDeformRoleForPass( category )", + "R_ClassicDeformDomain_SnapshotDrawSurf(", + "FindOrAddGeometryRecord(", + "drawSurf, classicDeform )", + "packet.classicDeformRecord = packet.geometryRecord != NULL", + "&packet.geometryRecord->classicDeform", + "packet.hasClassicDeformRecord =", + ), + "role snapshot before geometry provenance lookup", + ) + for token in ( + "packet.classicDeformRecord != NULL", + "*packet.classicDeformRecord, deformFrameToken", + "*packet.classicDeformRecord, classicDeform", + ): + require(add_draw, token, "geometry-owned draw-packet deform provenance") + + submit_plan = braced_body( + read("src/renderer/ModernGLSubmitPlan.cpp"), + "bool RendererModernGLSubmitPlan_RunSelfTest(", + "large packet-arena self-test storage", + ) + require_count( + submit_plan, + "idAutoPtr", + 4, + "heap-backed simultaneous packet fixtures", + ) + reject( + submit_plan, + "idScenePacketFrame packetFrame;", + "render-thread stack packet fixture", + ) + + +def validate_shared_domain_admission() -> None: + domains = ( + ( + "GUI", + "src/renderer/ClassicGuiDomain.h", + "src/renderer/ClassicGuiDomain.cpp", + "CLASSIC_GUI_DOMAIN_FAILURE_DEFORM_CONTRACT", + ("materialDeformSurfaces", "completedDeformSurfaces", "emptyDeformSurfaces"), + ), + ( + "world ambient", + "src/renderer/ClassicWorldAmbientDomain.h", + "src/renderer/ClassicWorldAmbientDomain.cpp", + "CLASSIC_WORLD_AMBIENT_FAILURE_DEFORM_CONTRACT", + ("materialDeformSurfaces", "completedDeformSurfaces", "emptyDeformSurfaces"), + ), + ( + "interaction", + "src/renderer/ClassicInteractionDomain.h", + "src/renderer/ClassicInteractionDomain.cpp", + "CLASSIC_INTERACTION_FAILURE_DEFORM_CONTRACT", + ("materialDeformReceivers", "completedDeformCasters", "emptyDeformCasters"), + ), + ( + "fog/blend", + "src/renderer/ClassicFogBlendDomain.h", + "src/renderer/ClassicFogBlendDomain.cpp", + "CLASSIC_FOG_BLEND_FAILURE_DEFORM_CONTRACT", + ("materialDeformReceivers",), + ), + ) + for name, header_path, source_path, failure, counters in domains: + header = read(header_path) + source = read(source_path) + require(header, failure, f"{name} named deform-contract fallback") + for token in ("deformRole", "deformOutcome", "deformContractHash", *counters): + require(header, token, f"{name} deform disposition and accounting") + for token in ( + "r_rendererSharedDeform.GetBool()", + "R_ClassicDeformDomain_ValidateRecordForFrame(", + failure, + ): + require(source, token, f"{name} fail-closed deform admission") + + gui = read("src/renderer/ClassicGuiDomain.cpp") + ambient = read("src/renderer/ClassicWorldAmbientDomain.cpp") + for source, context in ((gui, "GUI"), (ambient, "world ambient")): + for token in ( + "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "R_ClassicDeformDomain_RecordMatchesDrawSurf(", + "R_ClassicDeformDomain_SameProvenance(", + "R_ClassicDeformDomain_HasCompletedOutput( record )", + "R_ClassicDeformDomain_HasEmptyOutput( record )", + "CLASSIC_DEFORM_OUTCOME_COMPLETED", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + ): + require(source, token, f"{context} completed/empty deform acceptance") + + interaction = read("src/renderer/ClassicInteractionDomain.cpp") + for token in ( + "CLASSIC_DEFORM_ROLE_INTERACTION_RECEIVER", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "R_ClassicDeformDomain_HasCompletedOutput(", + "R_ClassicDeformDomain_HasEmptyOutput(", + "R_ClassicDeformDomain_SameProvenance(", + ): + require(interaction, token, "interaction receiver/caster deform split") + mapped_chain = braced_body( + interaction, + "static bool PrepareMappedShadowChain(", + "mapped deform-caster isolation", + ) + require_order( + mapped_chain, + ( + "if ( MaterialRequestsClassicDeform( drawSurf ) )", + "if ( !AppendEmptyMappedDeformCaster(", + "return false;", + ), + "disabled deform ownership must block empty mapped casters", + ) + reject( + mapped_chain, + "MaterialRequestsClassicDeform( drawSurf )\n\t\t\t\t\t&& r_rendererSharedDeform.GetBool()", + "disabled deform ownership mapped-caster bypass", + ) + + interaction_validator = braced_body( + interaction, + "static bool ValidateDeformContract(", + "interaction deform authorization", + ) + require( + interaction_validator, + "!r_rendererSharedDeform.GetBool() || r_skipDeforms.GetBool()", + "interaction independent switch and skip isolation", + ) + + noop_validator = braced_body( + interaction, + "bool R_ClassicInteractionDomain_ShadowCasterNoopValid(", + "backend-neutral mapped no-op validation", + ) + for token in ( + "CLASSIC_INTERACTION_SHADOW_CASTER_NOOP_EMPTY", + "caster.selectedIndexCount != 0", + "caster.alphaStageCount != 0", + "caster.firstAlphaStage != -1", + "caster.deformContractHash != 0", + "CLASSIC_DEFORM_ROLE_FINALIZED_DRAW", + "CLASSIC_DEFORM_OUTCOME_EMPTY", + "caster.vertexCount == 0", + "caster.totalIndexCount == 0", + "caster.deformContractHash == 0", + "caster.materialCoverage == MC_PERFORATED", + "caster.vertexCount > 0", + "caster.totalIndexCount > 0", + ): + require(noop_validator, token, "sealed-empty versus inactive-perforated proof") + interaction_selftest = braced_body( + interaction, + "bool RendererClassicInteractionDomain_RunSelfTest(", + "mapped no-op native fixtures", + ) + if interaction_selftest.count( + "R_ClassicInteractionDomain_ShadowCasterNoopValid( noopCaster )" + ) < 5: + raise AssertionError("Native interaction fixtures must cover all no-op variants") + + fog = read("src/renderer/ClassicFogBlendDomain.cpp") + for token in ( + "CLASSIC_DEFORM_ROLE_FOG_RECEIVER", + "CLASSIC_DEFORM_OUTCOME_NOT_APPLICABLE", + "packet.classicDeformRecord\n\t\t\t\t!= &packet.geometryRecord->classicDeform", + ): + require(fog, token, "fog receiver NOT_APPLICABLE contract") + prepare_fog = braced_body( + fog, "static bool PrepareView(", "fog/blend deform isolation" + ) + reject( + prepare_fog, + "MaterialRequestsClassicDeform( drawSurf )\n\t\t\t\t\t\t\t&& r_rendererSharedDeform.GetBool()", + "disabled deform ownership no-op bypass", + ) + fog_validator = braced_body( + fog, + "static bool ValidateReceiverDeformContract(", + "fog/blend deform authorization", + ) + require( + fog_validator, + "!r_rendererSharedDeform.GetBool() || r_skipDeforms.GetBool()", + "fog/blend independent switch and skip isolation", + ) + + +def validate_backend_preflight_transactions() -> None: + gl = read("src/renderer/draw_common.cpp") + vk = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + + for backend_path in ( + "src/renderer/draw_arb2.cpp", + "src/renderer/Vulkan/vk_ShadowMap.cpp", + ): + require( + read(backend_path), + "R_ClassicInteractionDomain_ShadowCasterNoopValid(", + f"{backend_path} shared no-op predicate", + ) + + gl_preflight = braced_body( + gl, + "static bool RB_SharedWorldAmbientGLPreflight(", + "OpenGL complete-view preflight", + ) + reject(gl_preflight, "RB_DrawElementsWithCounters(", "draw-free OpenGL preflight") + gl_handoff = braced_body(gl, "void\tRB_STD_DrawView(", "OpenGL ambient handoff") + require_order( + gl_handoff, + ( + "RB_PrepareSharedWorldAmbientView( backEnd.viewDef )", + "RB_DrawSharedWorldAmbientPhase(", + ), + "OpenGL preflight before deform-bearing commit", + ) + + vk_preflight = braced_body( + vk, + "static bool VK_ClassicWorldAmbient_Preflight(", + "Vulkan complete-view geometry transaction", + ) + for token in ( + "VK_Exec_SharedGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometryOffsets(", + "VK_Exec_SharedGeometryRestore();", + "VK_Exec_SharedGeometryCommit();", + "prepared.ready = true;", + ): + require(vk_preflight, token, "Vulkan checkpoint/restore/commit transaction") + require_order( + vk_preflight, + ( + "VK_Exec_SharedGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometryOffsets(", + "VK_Exec_SharedGeometryCommit();", + "prepared.ready = true;", + ), + "Vulkan geometry preflight before commit", + ) + for token in ("vkCmdDrawIndexed(", "vkCmdClearAttachments("): + reject(vk_preflight, token, "framebuffer-write-free Vulkan preflight") + + vk_handoff = braced_body( + vk, "void VK_GuiExecutor_Draw3DView(", "Vulkan framebuffer handoff" + ) + require_order( + vk_handoff, + ( + "VK_ClassicWorldAmbient_Preflight( viewDef )", + "vkCmdClearAttachments(", + "VK_ClassicWorldAmbient_DrawPhase(", + ), + "Vulkan preflight before framebuffer commit", + ) + + +def validate_controls_diagnostics_and_tooling() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + local = read("src/renderer/tr_local.h") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + scene_packets = read("src/renderer/ScenePackets.cpp") + modern = read("src/renderer/ModernGLExecutor.cpp") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + baseline_test = read("tools/tests/stock_asset_baseline.py") + matrix = read("tools/tests/renderer_validation_matrix.py") + registry = read("tools/validation/openq4_validate.py") + + require( + init, + 'idCVar r_rendererSharedDeform( "r_rendererSharedDeform", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL', + "default-off archived deform control", + ) + for token in ( + '"rendererClassicDeformDomainSelfTest"', + "RendererClassicDeformDomain self-test passed", + "Renderer shared material deform: requested=%d frameToken=%llu records=%d none=%d notApplicable=%d completed=%d empty=%d skipped=%d failed=%d unsupported=%d invalid=%d hash=%016llx", + ): + require(init, token, "deform self-test and gfxInfo diagnostics") + require(local, "extern idCVar r_rendererSharedDeform;", "deform cvar API") + require( + bootstrap, + '{ "r_rendererSharedDeform", &r_rendererSharedDeform, 0 }', + "bootstrap default safety", + ) + require(bootstrap, "sharedDeform=%d", "bootstrap deform diagnostics") + require(scene_packets, "|| r_rendererSharedDeform.GetBool()", "scene-packet activation") + require(modern, "&& !r_rendererSharedDeform.GetBool()", "aggregate modern isolation") + + for source, tokens, context in ( + ( + benchmark, + ( + 'append_set(args, "r_rendererSharedDeform", "0")', + '"r_rendererSharedDeform 0"', + ), + "benchmark default isolation", + ), + ( + baseline, + ( + 'add_set(args, "r_rendererSharedDeform", 0)', + '"r_rendererSharedDeform 0"', + ), + "stock baseline isolation", + ), + ( + baseline_test, + ('cvar_value(plan.args, "r_rendererSharedDeform") == "0"',), + "stock baseline unit contract", + ), + ): + for token in tokens: + require(source, token, context) + + require(benchmark, '"sp-mv2-deform":', "controlled deform gameplay scene") + deform_profile = braced_body(benchmark, '"deform":', "controlled deform profile") + for token in ( + '"display": ("windowed",)', + '("com_fixedTic", "1")', + '("g_stopTime", "1")', + '("g_stopTime", "0")', + '("in_mouse", "0")', + '"g_stopTime 1"', + '"setviewpos 0 0 256 80 0 0"', + '("r_rendererSharedWorldAmbient", "1")', + '("r_rendererSharedDeform", "1")', + '("r_materialOverride", "shaderDemos/move")', + '("r_skipDeforms", "0")', + ): + require(deform_profile, token, "fixed stock material-deform capture") + require( + benchmark, + "parsed.extra_cvars = profile_cvars + parse_extra_cvars(parsed.set_cvar)", + "standard --set-cvar override ordering", + ) + difference_helper = braced_body( + benchmark, + "def compare_screenshot_difference_if_requested(", + "generic feature-delta comparison", + ) + for token in ( + "controlled feature-on scene", + "feature-disabled capture", + "interaction shadows, fog/blend, and material deforms", + ): + require(difference_helper, token, "generic feature-delta comparison") + reject( + difference_helper, + "controlled shadows-on scene", + "shadow-only feature-delta documentation", + ) + + role_start = benchmark.find("def evaluate_role_result(") + role_end = benchmark.find("\ndef launch_and_wait(", role_start) + if role_start == -1 or role_end == -1: + raise AssertionError("Missing evaluate_role_result function boundaries") + role_evaluation = benchmark[role_start:role_end] + for token in ( + "is_deform_scene = spec.case_id in DEFORM_SCENES", + "difference_reference_dir is not None and difference_feature_case", + "difference_reference_case_id = (", + "is_deform_scene", + "else spec.id_for_shadow_preset(\"unshadowed\")", + "if difference_required", + 'if image_difference.get("pass") is False:', + 'missing.append(', + '"image difference "', + ): + require(role_evaluation, token, "deform effect-delta gate") + require_order( + role_evaluation, + ( + "difference_reference_case_id = (", + "spec.id", + "is_deform_scene", + "else spec.id_for_shadow_preset(\"unshadowed\")", + "difference_reference_case_id,", + "if difference_required", + ), + "same-run deform difference-reference routing", + ) + require( + benchmark, + "each eligible interaction-shadow, fog/blend, or material-deform capture must differ from", + "generic difference-reference CLI help", + ) + + if matrix.count("+rendererClassicDeformDomainSelfTest") < 2: + raise AssertionError("Foundation and Vulkan safe cases must run the deform self-test") + if matrix.count("RendererClassicDeformDomain self-test passed") < 2: + raise AssertionError("Foundation and Vulkan safe cases must check the deform marker") + require(matrix, '"id": "sp-mv2-deform"', "manual deform validation case") + require(matrix, '"profile": "deform"', "deform gameplay harness entry") + require_count( + registry, + '"renderer_classic_deform_domain.py"', + 1, + "validation registry", + ) + + for workflow_path in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + workflow = read(workflow_path) + require_count(workflow, TEST_PATH, 2, f"{workflow_path} compile/run registration") + + require(read("meson.build"), "meson_sources.py", "renderer Meson source discovery") + require( + read("tools/build/meson_sources.py"), + '"renderer/*.cpp"', + "ClassicDeformDomain.cpp source discovery", + ) + + +def main() -> int: + validate_sealed_record_contract() + validate_frontend_packet_provenance() + validate_shared_domain_admission() + validate_backend_preflight_transactions() + validate_controls_diagnostics_and_tooling() + print("renderer_classic_deform_domain: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_classic_fog_blend_domain.py b/tools/tests/renderer_classic_fog_blend_domain.py new file mode 100644 index 00000000..fcd1c25e --- /dev/null +++ b/tools/tests/renderer_classic_fog_blend_domain.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +"""Static regression contract for atomic shared classic fog/blend ownership.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_fog_blend_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def require_any(haystack: str, needles: tuple[str, ...], context: str) -> None: + if not any(needle in haystack for needle in needles): + raise AssertionError(f"Missing one of {needles!r} in {context}") + + +def braced_body(source: str, marker: str, context: str) -> str: + start = source.find(marker) + if start == -1: + raise AssertionError(f"Missing {marker!r} in {context}") + opening = source.find("{", start + len(marker)) + if opening == -1: + raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find closing brace for {marker!r} in {context}") + + +def preflight_bodies(source: str, context: str) -> list[str]: + markers = re.findall( + r"(?:static\s+)?bool\s+([A-Za-z0-9_]*FogBlend[A-Za-z0-9_]*Preflight[A-Za-z0-9_]*)\s*\(", + source, + ) + return [braced_body(source, f"{name}(", context) for name in markers] + + +def validate_domain_contract() -> None: + header = read("src/renderer/ClassicFogBlendDomain.h") + source = read("src/renderer/ClassicFogBlendDomain.cpp") + combined = header + source + + for token in ( + "CLASSIC_FOG_BLEND_LIGHT_FOG", + "CLASSIC_FOG_BLEND_LIGHT_BLEND", + "CLASSIC_FOG_BLEND_RECEIVER_GLOBAL", + "CLASSIC_FOG_BLEND_RECEIVER_LOCAL", + "CLASSIC_FOG_BLEND_PRIMITIVE_FOG_RECEIVER", + "CLASSIC_FOG_BLEND_BACKEND_GL", + "CLASSIC_FOG_BLEND_BACKEND_VULKAN", + "R_ClassicFogBlendDomain_ResetFrame", + "R_ClassicFogBlendDomain_PrepareFrame", + "R_ClassicFogBlendDomain_Stats", + "R_ClassicFogBlendDomain_NumViews", + "R_ClassicFogBlendDomain_ViewByIndex", + "R_ClassicFogBlendDomain_ViewForScenePacket", + "R_ClassicFogBlendDomain_FindView", + "R_ClassicFogBlendDomain_ViewLight", + "R_ClassicFogBlendDomain_LightStage", + "R_ClassicFogBlendDomain_ViewPrimitive", + "R_ClassicFogBlendDomain_ResolveTexture", + "R_ClassicFogBlendDomain_RecordOwned", + "R_ClassicFogBlendDomain_RecordBackendFallback", + "R_ClassicFogBlendDomain_BackendCoverage", + "ClassicFogBlendDomainLightKind_Name", + "ClassicFogBlendDomainReceiver_Name", + "ClassicFogBlendDomainPrimitiveKind_Name", + "ClassicFogBlendDomainFailure_Name", + "ClassicFogBlendDomainBackend_Name", + ): + require(combined, token, "shared fog/blend domain API") + require_any( + header, + ( + "CLASSIC_FOG_BLEND_PRIMITIVE_FOG_FRUSTUM_CAP", + "CLASSIC_FOG_BLEND_PRIMITIVE_FOG_CAP", + ), + "sealed fog frustum-cap primitive", + ) + require_any( + header, + ( + "CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_RECEIVER", + "CLASSIC_FOG_BLEND_PRIMITIVE_BLEND_DRAW", + ), + "sealed blend receiver primitive", + ) + + prepare = braced_body( + source, + "R_ClassicFogBlendDomain_PrepareFrame(", + "atomic fog/blend frame preparation", + ) + for token in ( + "R_ClassicFogBlendDomain_ResetFrame();", + "PrepareView(", + "frameValid", + ): + require(prepare, token, "atomic fog/blend frame preparation") + + for token in ( + "IsFogLight()", + "IsBlendLight()", + "globalInteractions", + "localInteractions", + "frustumTris", + "conditionRegister", + "drawStateBits", + "falloff", + "texture", + "ready = true", + "ready = false", + "fallbackViews", + "CLASSIC_FOG_BLEND_LIGHT_NOOP_MISSING_GLOBAL_CHAIN", + "CLASSIC_FOG_BLEND_LIGHT_NOOP_SKIP_BLEND", + "ValidateDrawPacketIdentity", + "noopBlend", + ): + require(source, token, "sealed ordered fog/blend semantic snapshot") + require_any(source, ("semanticHash", "SemanticHash", "Hash"), "semantic hash") + require_any( + source, + ("DEFAULT_FOG_DISTANCE", "defaultFogDistance", "fogDistance"), + "fog distance semantics", + ) + require_any( + source, + ("FOG_ENTER", "fogEnter", "viewInsideLight"), + "fog-entry/cap semantics", + ) + + +def validate_scene_packet_identity() -> None: + header = read("src/renderer/ScenePackets.h") + source = read("src/renderer/ScenePackets.cpp") + for token in ( + "sceneFogBlendReceiverClass_t", + "SCENE_FOG_BLEND_RECEIVER_GLOBAL", + "SCENE_FOG_BLEND_RECEIVER_LOCAL", + "fogBlendLight", + "fogBlendLightOrdinal", + "fogBlendReceiverOrdinal", + "fogBlendSourceOrdinal", + "fogBlendReceiverClass", + "AddFogBlendDrawPacket", + ): + require(header, token, "authoritative fog/blend packet identity") + append = braced_body( + source, + "R_ScenePackets_AppendFogBlendChain(", + "ordered fog/blend packet chain", + ) + require(append, "AddFogBlendDrawPacket(", "dedicated fog/blend packet append") + fog_pass = braced_body( + source, + "R_ScenePackets_AddFogBlendPass(", + "global-to-local fog/blend source order", + ) + global_at = fog_pass.find("globalInteractions") + local_at = fog_pass.find("localInteractions") + if global_at == -1 or local_at <= global_at: + raise AssertionError("Fog/blend packets must preserve global-to-local order") + classifier = braced_body( + source, + "R_ScenePackets_ViewLightIsFogOrBlend(", + "unfiltered fog/blend packet identity", + ) + reject(classifier, "r_skipFogLights", "unfiltered fog/blend packet identity") + reject(classifier, "r_skipBlendLights", "unfiltered fog/blend packet identity") + + +def validate_backend_transactions() -> None: + gl = read("src/renderer/draw_common.cpp") + read("src/renderer/tr_backend.cpp") + vk = ( + read("src/renderer/Vulkan/vk_Backend.cpp") + + read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + + read("src/renderer/Vulkan/vk_Interactions.cpp") + ) + for source, backend, draw_token in ( + (gl, "GL", "RB_DrawElementsWithCounters"), + (vk, "Vulkan", "vkCmdDrawIndexed"), + ): + for token in ( + "R_ClassicFogBlendDomain_FindView", + "R_ClassicFogBlendDomain_RecordOwned", + "R_ClassicFogBlendDomain_RecordBackendFallback", + ): + require(source, token, f"{backend} fog/blend ownership transaction") + bodies = preflight_bodies(source, f"{backend} fog/blend preflight") + if not bodies: + raise AssertionError(f"Missing fog/blend preflight in {backend} backend") + for body in bodies: + reject(body, draw_token, f"draw-free {backend} fog/blend preflight") + reject(body, "RecordOwned", f"uncommitted {backend} preflight") + + for token in ("RB_STD_FogAllLights", "RB_FogPass", "RB_BlendLight"): + require(gl, token, "GL legacy rollback and fog/blend semantics") + for token in ("VK_Fog_DrawAllLights", "VK_Fog_DrawFogLight", "VK_Fog_DrawBlendLight"): + require(vk, token, "Vulkan legacy rollback and fog/blend semantics") + + handoff = read("src/renderer/tr_backend.cpp") + require( + handoff, + "R_ClassicFogBlendDomain_ResetFrame();", + "backend-frame reset", + ) + require( + handoff, + "R_ClassicFogBlendDomain_PrepareFrame( *scenePackets );", + "packet-derived backend handoff", + ) + + +def validate_controls_diagnostics_and_build() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + local = read("src/renderer/tr_local.h") + meson = read("meson.build") + source_discovery = read("tools/build/meson_sources.py") + for token in ( + 'r_rendererSharedWorldFogBlend( "r_rendererSharedWorldFogBlend", "0"', + '"rendererClassicFogBlendDomainSelfTest"', + "RendererClassicFogBlendDomain self-test passed", + "classicFogBlendDomain requested=", + "classicFogBlendDomain backend=", + "noopLights=%d", + "ownedNoopStages=%d", + "ownedNoopLights=%d", + ): + require(init, token, "default-off control, self-test, and diagnostics") + require(local, "extern idCVar r_rendererSharedWorldFogBlend;", "renderer cvar API") + require(meson, "meson_sources.py", "renderer Meson source discovery") + require( + source_discovery, + '"renderer/*.cpp"', + "renderer source glob includes ClassicFogBlendDomain.cpp", + ) + + bootstrap = read("src/renderer/RendererBootstrap.cpp") + require(bootstrap, "sharedWorldFogBlend", "bootstrap default-off diagnostics") + + +def validate_tooling_registration() -> None: + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + baseline_test = read("tools/tests/stock_asset_baseline.py") + matrix = read("tools/tests/renderer_validation_matrix.py") + registry = read("tools/validation/openq4_validate.py") + for token in ( + '"fog-blend"', + '"sp-mv2-fog-blend"', + '"r_rendererSharedWorldFogBlend", "1"', + "lights/fog_generic", + "lights/fog_ambient", + "classicFogBlendDomain requested=", + "classicFogBlendDomain backend=", + '"owned-skip-blend"', + 'spec.fog_blend_expectation in ("owned", "owned-skip-blend")', + '"sharedFogBlendViews"', + ): + require(benchmark, token, "controlled stock fog/blend profile") + for source, context in ( + (benchmark, "benchmark default isolation"), + (baseline, "stock baseline default isolation"), + (baseline_test, "stock baseline unit contract"), + ): + require(source, "r_rendererSharedWorldFogBlend", context) + for token in ( + "rendererClassicFogBlendDomainSelfTest", + "RendererClassicFogBlendDomain self-test passed", + "sharedWorldFogBlend=0", + ): + require(matrix, token, "renderer validation matrix") + require( + registry, + '"renderer_classic_fog_blend_domain.py"', + "validation registry", + ) + + for workflow_path in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + workflow = read(workflow_path) + if workflow.count(TEST_PATH) != 2: + raise AssertionError( + f"Expected compile and execution registration in {workflow_path}" + ) + + +def main() -> int: + validate_scene_packet_identity() + validate_domain_contract() + validate_backend_transactions() + validate_controls_diagnostics_and_build() + validate_tooling_registration() + print("renderer_classic_fog_blend_domain: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_classic_gui_domain.py b/tools/tests/renderer_classic_gui_domain.py new file mode 100644 index 00000000..538efcdc --- /dev/null +++ b/tools/tests/renderer_classic_gui_domain.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +"""Static regression contract for shared root and in-world classic GUI ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_gui_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def require_order(haystack: str, needles: tuple[str, ...], context: str) -> None: + previous = -1 + for needle in needles: + position = haystack.find(needle, previous + 1) + if position == -1: + raise AssertionError(f"Missing {needle!r} in {context}") + if position <= previous: + raise AssertionError( + f"Expected ordered contracts in {context}: {needles!r}" + ) + previous = position + + +def braced_body(source: str, marker: str, context: str) -> str: + start = source.find(marker) + if start == -1: + raise AssertionError(f"Missing {marker!r} in {context}") + opening_brace = source.find("{", start + len(marker)) + if opening_brace == -1: + raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + + depth = 0 + for index in range(opening_brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find closing brace for {marker!r} in {context}") + + +def validate_neutral_contract() -> None: + header = read("src/renderer/RendererContracts.h") + source = read("src/renderer/RendererContracts.cpp") + for token in ( + "rendererEvaluatedMaterialPass_t", + "RENDERER_MATERIAL_PASS_INACTIVE_CONDITION", + "RENDERER_MATERIAL_PASS_NOOP_ZERO_ONE_BLEND", + "RENDERER_MATERIAL_PASS_NOOP_BLACK_ADDITIVE", + "RENDERER_MATERIAL_PASS_NOOP_TRANSPARENT_ALPHA", + "RendererContracts_EvaluateMaterialPassList", + ): + require(header, token, "backend-neutral evaluated material contract") + evaluator = braced_body( + source, + "rendererMaterialPassEvaluationStatus_t RendererContracts_EvaluateMaterialPassList(", + "atomic evaluated material-list implementation", + ) + require_order( + evaluator, + ( + "RendererContracts_ResetEvaluatedMaterialPassList( destination );", + "RendererContracts_EvaluateMaterialPass(", + "RendererContracts_ResetEvaluatedMaterialPassList( destination );", + "return status;", + ), + "atomic evaluated material-list failure", + ) + + +def validate_material_table() -> None: + header = read("src/renderer/MaterialResourceTable.h") + source = read("src/renderer/MaterialResourceTable.cpp") + for token in ( + "guiDomainReferenced", + "firstGuiPass", + "guiPassCount", + "worldDomainReferenced", + "firstWorldPass", + "worldPassCount", + "R_MaterialResourceTable_ResolveTextureResource", + "R_MaterialResourceTable_CopyGuiPassList", + "R_MaterialResourceTable_CopyWorldPassList", + ): + require(header, token, "ordered GUI material resource contract") + + prepare = braced_body( + source, + "void R_MaterialResourceTable_PrepareFrame(", + "GUI packet-derived material-table preparation", + ) + require_order( + prepare, + ( + "drawPacket.passCategory == RENDER_PASS_GUI", + "guiDomainReferenced[drawPacket.materialRecordIndex] = true;", + "drawPacket.packetCategory == SCENE_PACKET_CATEGORY_WORLD", + "worldDomainReferenced[drawPacket.materialRecordIndex] = true;", + "R_MaterialResourceTable_AddRecordFromSource(", + ), + "packet-derived GUI material admission", + ) + + add_binding = braced_body( + source, + "static int R_MaterialResourceTable_AddTextureBinding(", + "ordered material texture bindings", + ) + require( + add_binding, + "stage == NULL && R_MaterialResourceTable_HasSemanticBinding", + "same-semantic authored-stage preservation", + ) + require( + source, + "record.semanticBindingIndex[semantic] < 0", + "first-binding semantic convenience index", + ) + + +def validate_transactional_domain() -> None: + header = read("src/renderer/ClassicGuiDomain.h") + source = read("src/renderer/ClassicGuiDomain.cpp") + for token in ( + "CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_DRAWABLE", + "CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DEPTH_HACK", + "CLASSIC_GUI_DOMAIN_SOURCE_SURFACE_FALLBACK_DECAL_COLOR_STREAM", + "CLASSIC_GUI_DOMAIN_SCOPE_ROOT_2D", + "CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD", + "classicGuiDomainBackendCoverage_t", + "R_ClassicGuiDomain_RecordOwned", + "R_ClassicGuiDomain_RecordBackendFallback", + "R_ClassicGuiDomain_FindRootView", + "R_ClassicGuiDomain_FindInWorldView", + "R_ClassicGuiDomain_IsLegacyInWorldDrawOwned", + ): + require(header, token, "whole-view GUI domain API") + + prepare = braced_body( + source, + "void R_ClassicGuiDomain_PrepareFrame(", + "whole-view GUI domain preparation", + ) + require(prepare, "R_ClassicGuiDomain_PrepareView", "per-view transaction") + require( + source, + "RendererContracts_EvaluateMaterialPassList(", + "single neutral register evaluator", + ) + require( + source, + "R_MaterialResourceTable_ResolveTextureResource(", + "opaque texture-resource resolution", + ) + require( + source, + "R_MaterialResourceTable_CopyWorldPassList( *materialRecord,", + "in-world depth-aware material stream", + ) + require( + source, + "RendererClassicGuiDomain_RunSelfTest", + "whole-view rollback self-test", + ) + + +def validate_gl_consumer() -> None: + backend = read("src/renderer/tr_backend.cpp") + draw = read("src/renderer/draw_common.cpp") + modern = read("src/renderer/ModernGLExecutor.cpp") + handoff = braced_body( + backend, + "void RB_ExecuteBackEndCommands(", + "OpenGL command handoff", + ) + require_order( + handoff, + ( + "R_ClassicGuiDomain_ResetFrame();", + "cmds->commandId == RC_NOP && !cmds->next", + "R_ClassicGuiDomain_PrepareFrame(", + "RB_DrawSharedGuiView(", + "RB_DrawView( cmds );", + ), + "OpenGL shared-view then untouched classic fallback", + ) + + consumer = braced_body( + draw, + "static bool RB_DrawSharedGuiViewForScope(", + "OpenGL shared root/in-world GUI consumer", + ) + require_order( + consumer, + ( + "RB_SharedGuiGLPreflight(", + "RB_BeginDrawingView();", + "RB_DrawElementsWithCounters( tri );", + "R_ClassicGuiDomain_RecordOwned(", + ), + "OpenGL preflight-before-commit ordering", + ) + reject(consumer, "shaderRegisters", "OpenGL sealed material consumer") + reject(consumer, "GetStage(", "OpenGL sealed material consumer") + require( + draw, + "R_ClassicGuiDomain_DrawPassTexture(", + "OpenGL opaque resource consumption", + ) + require( + draw, + "RB_SharedGuiGLSourceNoopValid(", + "OpenGL recognized source no-op accounting", + ) + require_order( + draw, + ( + "bool RB_DrawSharedInWorldGuiView(", + "CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD", + "void\tRB_STD_DrawView( void )", + "RB_DrawSharedInWorldGuiView( backEnd.viewDef )", + "RB_PrepareSharedWorldAmbientView( backEnd.viewDef )", + ), + "OpenGL in-world GUI pre-ambient handoff", + ) + require( + draw, + "R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( backEnd.viewDef,", + "OpenGL classic duplicate suppression after ownership", + ) + + gui_surface = read("src/renderer/tr_guisurf.cpp") + gui_add = read("src/renderer/tr_light.cpp") + require_order( + gui_surface, + ( + "tr.inWorldGuiEmissionDepth++;", + "tr.guiModel->EmitToCurrentView(", + "tr.inWorldGuiEmissionDepth--;", + ), + "in-world GUI front-end provenance scope", + ) + require( + gui_add, + "drawSurf->dsFlags |= DSF_IN_WORLD_GUI;", + "in-world GUI generated draw provenance", + ) + + packets = read("src/renderer/ScenePackets.cpp") + require( + packets, + "R_ScenePackets_HasInWorldGUIDrawSurfs( viewDef )", + "in-world GUI packet admission", + ) + require( + packets, + "R_ScenePackets_DrawSurfInWorldGUIEligible", + "provenance-filtered in-world GUI packet stream", + ) + + submit_plan = braced_body( + modern, + "static void R_ModernGLExecutor_SubmitPlan(", + "older aggregate OpenGL submit plan", + ) + require_order( + submit_plan, + ( + "R_ModernGLExecutor_SharedGuiOwnsCommand( command )", + "continue;", + ), + "shared GUI suppression in the older aggregate submit plan", + ) + shared_owner = braced_body( + modern, + "static bool R_ModernGLExecutor_SharedGuiOwnsCommand(", + "root and in-world aggregate GUI suppression", + ) + require_order( + shared_owner, + ( + "r_rendererSharedGui.GetBool()", + "r_rendererSharedInWorldGui.GetBool()", + "drawPacket->packetCategory == SCENE_PACKET_CATEGORY_WORLD", + ), + "root/in-world aggregate GUI ownership separation", + ) + submit_gui = braced_body( + modern, + "static void R_ModernGLExecutor_SubmitModernGui(", + "older aggregate OpenGL GUI replay", + ) + require( + submit_gui, + "r_rendererSharedGui.GetBool()", + "shared GUI suppression in older aggregate GUI replay", + ) + require( + submit_gui, + "R_ModernGLExecutor_SharedGuiOwnsCommand( command )", + "in-world GUI suppression in older aggregate GUI replay", + ) + visible_request = braced_body( + modern, + "static bool R_ModernGLExecutor_ModernVisibleRequested(", + "modern visible request gate", + ) + require(visible_request, "!r_skipRender.GetBool()", "skip-render GUI ordering gate") + require( + visible_request, + "!r_skipRenderContext.GetBool()", + "skip-render-context GUI ordering gate", + ) + + +def validate_vulkan_consumer() -> None: + backend = read("src/renderer/Vulkan/vk_Backend.cpp") + executor = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + require_order( + backend, + ( + "R_MaterialResourceTable_PrepareFrame(", + "R_ClassicGuiDomain_PrepareFrame(", + ), + "Vulkan neutral frame preparation", + ) + + consumer = braced_body( + executor, + "static bool VK_ClassicGui_DrawOwnedViewForScope(", + "Vulkan shared root/in-world GUI consumer", + ) + require_order( + consumer, + ( + "VK_GuiExecutor_GetPipelineStrict(", + "VK_Exec_SharedGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometryOffsets(", + "VK_Exec_SharedGeometryCommit();", + "// Commit:", + "vkCmdDrawIndexed(", + "R_ClassicGuiDomain_RecordOwned(", + ), + "Vulkan preflight-before-commit ordering", + ) + require( + consumer, + "VK_Exec_SharedGeometryRestore();", + "Vulkan failed-preflight geometry rollback", + ) + precommit = consumer[: consumer.index("VK_Exec_SharedGeometryCommit();")] + for command in ("vkCmdDraw(", "vkCmdDrawIndexed(", "vkCmdClearAttachments("): + reject(precommit, command, "Vulkan attachment write during GUI preflight") + reject(consumer, "shaderRegisters", "Vulkan sealed material consumer") + reject(consumer, "GetStage(", "Vulkan sealed material consumer") + + draw_2d = braced_body( + executor, + "void VK_GuiExecutor_Draw2DView(", + "Vulkan 2D view handoff", + ) + require_order( + draw_2d, + ( + "VK_ClassicGui_DrawOwnedView( viewDef )", + "VK_Exec_DrawAmbientStages( viewDef, drawSurf, tri, mvp, false );", + ), + "Vulkan shared-view then untouched classic fallback", + ) + require( + executor, + "VK_ClassicGui_SourceNoopValid(", + "Vulkan recognized source no-op accounting", + ) + require( + consumer, + "planDrawCount", + "Vulkan drawable-only commit plan", + ) + require_order( + executor, + ( + "static bool VK_ClassicGui_DrawOwnedInWorldView(", + "CLASSIC_GUI_DOMAIN_SCOPE_IN_WORLD", + "void VK_GuiExecutor_Draw3DView(", + "VK_ClassicGui_DrawOwnedInWorldView( viewDef )", + "// ---- passes 2-4: ambient walks split at fog, then post-process ----", + ), + "Vulkan in-world GUI pre-ambient handoff", + ) + require( + executor, + "R_ClassicGuiDomain_IsLegacyInWorldDrawOwned( viewDef,", + "Vulkan classic duplicate suppression after ownership", + ) + + gui_fragment = read("src/renderer/Vulkan/shaders/gui.frag") + require(gui_fragment, "color.a != pc.params.z", "exact Vulkan EQ_255 alpha test") + reject(gui_fragment, "0.5 / 255.0", "approximate Vulkan EQ_255 alpha test") + + +def validate_control_and_evidence() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + baseline_contract = read("tools/tests/stock_asset_baseline.py") + validation_matrix = read("tools/tests/renderer_validation_matrix.py") + require( + init, + 'idCVar r_rendererSharedGui( "r_rendererSharedGui", "0"', + "default-off shared GUI control", + ) + require(bootstrap, '{ "r_rendererSharedGui", &r_rendererSharedGui, 0 }', "rollback report") + require( + init, + 'idCVar r_rendererSharedInWorldGui( "r_rendererSharedInWorldGui", "0"', + "default-off shared in-world GUI control", + ) + require( + bootstrap, + '{ "r_rendererSharedInWorldGui", &r_rendererSharedInWorldGui, 0 }', + "in-world GUI rollback report", + ) + require(benchmark, 'append_set(args, "r_rendererSharedGui", "0")', "benchmark isolation") + require( + benchmark, + 'append_set(args, "r_rendererSharedInWorldGui", "0")', + "in-world GUI benchmark isolation", + ) + require(baseline, 'add_set(args, "r_rendererSharedGui", 0)', "stock baseline isolation") + require( + baseline, + 'add_set(args, "r_rendererSharedInWorldGui", 0)', + "in-world GUI stock baseline isolation", + ) + require( + baseline_contract, + 'cvar_value(plan.args, "r_rendererSharedInWorldGui") == "0"', + "in-world GUI stock baseline assertion", + ) + require( + validation_matrix, + '"r_rendererSharedInWorldGui",\n "0",', + "in-world GUI renderer validation isolation", + ) + + +def validate_ci_registration() -> None: + validator = read("tools/validation/openq4_validate.py") + commit = read(".github/workflows/commit-validation.yml") + push = read(".github/workflows/push-verification.yml") + if validator.count("renderer_classic_gui_domain.py") != 1: + raise AssertionError("Local validation must register this test exactly once") + for workflow, context in ( + (commit, "commit validation workflow"), + (push, "push verification workflow"), + ): + if workflow.count(TEST_PATH) != 2: + raise AssertionError(f"{context} must compile and directly run {TEST_PATH}") + require(workflow, f"python {TEST_PATH}", context) + + +def main() -> None: + validate_neutral_contract() + validate_material_table() + validate_transactional_domain() + validate_gl_consumer() + validate_vulkan_consumer() + validate_control_and_evidence() + validate_ci_registration() + print("renderer_classic_gui_domain: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/renderer_classic_interaction_domain.py b/tools/tests/renderer_classic_interaction_domain.py new file mode 100644 index 00000000..7f10bee1 --- /dev/null +++ b/tools/tests/renderer_classic_interaction_domain.py @@ -0,0 +1,1758 @@ +#!/usr/bin/env python3 +"""Static ownership guards for the shared classic interaction domain.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def require(text: str, needle: str, label: str) -> None: + if needle not in text: + raise AssertionError(f"missing {label}: {needle}") + + +def compact(value: str) -> str: + """Ignore indentation and line wrapping while retaining token order.""" + return " ".join(value.split()) + + +def require_compact(text: str, needle: str, label: str) -> None: + compact_needle = compact(needle) + if compact_needle not in compact(text): + raise AssertionError(f"missing {label}: {compact_needle}") + + +def require_before(text: str, first: str, second: str, label: str) -> None: + first_at = text.find(first) + second_at = text.find(second) + if first_at < 0 or second_at < 0 or first_at >= second_at: + raise AssertionError(f"ordering guard failed for {label}: {first!r} before {second!r}") + + +def require_order(text: str, needles: tuple[str, ...], label: str) -> None: + compact_text = compact(text) + previous = -1 + for needle in needles: + compact_needle = compact(needle) + position = compact_text.find(compact_needle, previous + 1) + if position < 0: + raise AssertionError(f"missing {label}: {compact_needle}") + previous = position + + +def reject(text: str, needle: str, label: str) -> None: + if needle in text: + raise AssertionError(f"forbidden {label}: {needle}") + + +def function_body(text: str, signature: str, label: str = "function") -> str: + start = text.find(signature) + if start < 0: + raise AssertionError(f"missing {label}: {signature}") + brace = text.find("{", start + len(signature)) + if brace < 0: + raise AssertionError(f"missing {label} body: {signature}") + depth = 0 + for index in range(brace, len(text)): + if text[index] == "{": + depth += 1 + elif text[index] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1 : index] + raise AssertionError(f"unterminated {label} body: {signature}") + + +def reject_raw_authored_reads(body: str, label: str) -> None: + forbidden = ( + "GetStage(", + "shaderRegisters", + "conditionRegister", + "color.registers", + "texture.matrix", + ) + found = [token for token in forbidden if token in body] + if found: + raise AssertionError(f"{label} rereads authored material state: {', '.join(found)}") + + +def main() -> None: + header = read("src/renderer/ClassicInteractionDomain.h") + source = read("src/renderer/ClassicInteractionDomain.cpp") + shadow_classification = read("src/renderer/ShadowMapClassification.cpp") + packets_h = read("src/renderer/ScenePackets.h") + packets_cpp = read("src/renderer/ScenePackets.cpp") + init = read("src/renderer/RenderSystem_init.cpp") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + backend = read("src/renderer/tr_backend.cpp") + vk_backend = read("src/renderer/Vulkan/vk_Backend.cpp") + modern = read("src/renderer/ModernGLExecutor.cpp") + gl = read("src/renderer/draw_arb2.cpp") + vk = read("src/renderer/Vulkan/vk_Interactions.cpp") + vk_shadow = read("src/renderer/Vulkan/vk_ShadowMap.cpp") + vk_shadow_h = read("src/renderer/Vulkan/vk_ShadowMap.h") + vk_executor = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + matrix = read("tools/tests/renderer_validation_matrix.py") + validation = read("tools/validation/openq4_validate.py") + commit_ci = read(".github/workflows/commit-validation.yml") + push_ci = read(".github/workflows/push-verification.yml") + roadmap = read("docs/dev/idtech5-modernization-roadmap.md") + domain_doc = read("docs/dev/classic-interaction-domain-modernization.md") + + for symbol in ( + "classicInteractionDomainView_t", + "classicInteractionDomainLight_t", + "classicInteractionDomainSurface_t", + "classicInteractionDomainPrimitive_t", + "classicInteractionDomainShadowCaster_t", + "classicInteractionDomainShadowAlphaStage_t", + "classicInteractionDomainShadowMapPass_t", + "classicInteractionDomainShadowProjectedState_t", + "classicInteractionDomainShadowPointState_t", + "classicInteractionDomainTexture_t", + "R_ClassicInteractionDomain_ResetFrame", + "R_ClassicInteractionDomain_PrepareFrame", + "R_ClassicInteractionDomain_FindView", + "R_ClassicInteractionDomain_ViewLight", + "R_ClassicInteractionDomain_ViewSurface", + "R_ClassicInteractionDomain_ViewPrimitive", + "R_ClassicInteractionDomain_ViewShadowCaster", + "R_ClassicInteractionDomain_LightShadowCaster", + "R_ClassicInteractionDomain_ShadowAlphaStage", + "R_ClassicInteractionDomain_LightShadowMapPass", + "R_ClassicInteractionDomain_ResolveTexture", + "R_ClassicInteractionDomain_RecordOwned", + "R_ClassicInteractionDomain_RecordBackendFallback", + "RendererClassicInteractionDomain_RunSelfTest", + ): + require(header, symbol, "shared interaction contract") + + for shadow_contract in ( + "CLASSIC_INTERACTION_SHADOW_STENCIL", + "CLASSIC_INTERACTION_SHADOW_PROJECTED", + "CLASSIC_INTERACTION_SHADOW_POINT", + "CLASSIC_INTERACTION_SHADOW_HYBRID", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID", + "shadowMapPassIndex[ 2 ]", + "shadowMapPassCount", + "hybridShadowPassCount", + "projectedShadowMapPassCount", + "csmShadowMapPassCount", + "pointShadowMapPassCount", + "projectedShadowLightCount", + "pointShadowLightCount", + "backendShadowMapPasses", + "backendHybridPasses", + ): + require(header, shadow_contract, "sealed interaction-shadow contract") + + for state_field in ( + "resourceOwner", + "receiverMask", + "mappedCasterCount", + "supplementCasterCount", + "drawableMappedCasters", + "noopMappedCasters", + "drawableSupplementCasters", + "noopSupplementCasters", + "casterSignature", + "incompleteMapMask", + "incompleteStencilMask", + "hybridIncompleteMask", + "prelightMapMissingMask", + "prelightStencilRequiredMask", + "prelightStencilReadyMask", + "resourcePlanId", + "resourceGeneration", + "resourceAlias", + "mapRequired", + "mapComplete", + "stencilComplete", + "hybridComplete", + "hasStaticCasters", + "hasDynamicCasters", + "hasAlphaCasters", + "hasTranslucentCasters", + "allowCacheReuse", + "allowCacheUpdate", + "allowScratch", + "hashedAlpha", + "stableAlphaHash", + "casterCullMode", + "polygonFactor", + "polygonOffset", + "projected", + "point", + ): + require(header, state_field, "sealed mapped-shadow state") + + for blocker in ( + "CLASSIC_INTERACTION_FAILURE_SHADOWS", + "CLASSIC_INTERACTION_FAILURE_SHADOW_MAP", + "CLASSIC_INTERACTION_FAILURE_SHADOW_PACKET_MISMATCH", + "CLASSIC_INTERACTION_FAILURE_SHADOW_GEOMETRY", + "CLASSIC_INTERACTION_FAILURE_CUSTOM_LIGHTING", + "CLASSIC_INTERACTION_FAILURE_DEFORM", + "CLASSIC_INTERACTION_FAILURE_SKINNING", + "CLASSIC_INTERACTION_FAILURE_DEPTH_HACK", + "CLASSIC_INTERACTION_FAILURE_DYNAMIC_RESOURCE", + "CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH", + ): + require(header, blocker, "named whole-view blocker") + require(source, blocker, "implemented whole-view blocker") + + for identity in ( + "interactionLight", + "interactionLightOrdinal", + "interactionReceiverClass", + "interactionReceiverOrdinal", + "interactionSourceOrdinal", + "SCENE_INTERACTION_RECEIVER_LOCAL", + "SCENE_INTERACTION_RECEIVER_GLOBAL", + "SCENE_INTERACTION_RECEIVER_TRANSLUCENT", + ): + require(packets_h, identity, "explicit packet interaction identity") + require(packets_cpp, identity, "populated packet interaction identity") + require(source, identity, "validated packet interaction identity") + + for identity in ( + "shadowLight", + "shadowLightOrdinal", + "shadowCasterClass", + "shadowChainOrdinal", + "shadowSourceOrdinal", + "SCENE_SHADOW_CASTER_STENCIL_GLOBAL", + "SCENE_SHADOW_CASTER_STENCIL_LOCAL", + "SCENE_SHADOW_CASTER_MAP_GLOBAL_STATIC", + "SCENE_SHADOW_CASTER_MAP_LOCAL_STATIC", + "SCENE_SHADOW_CASTER_MAP_GLOBAL_DYNAMIC", + "SCENE_SHADOW_CASTER_MAP_LOCAL_DYNAMIC", + "SCENE_SHADOW_CASTER_MAP_GLOBAL_TRANSLUCENT", + "SCENE_SHADOW_CASTER_MAP_LOCAL_TRANSLUCENT", + "SCENE_SHADOW_CASTER_SUPPLEMENT_GLOBAL", + "SCENE_SHADOW_CASTER_SUPPLEMENT_LOCAL", + ): + require(packets_h, identity, "explicit packet shadow identity") + require(packets_cpp, identity, "populated packet shadow identity") + require(source, identity, "validated packet shadow identity") + + require( + packets_cpp, + "static void R_ScenePackets_AddShadowMapPass(", + "shadow-map packet pass builder", + ) + require( + packets_cpp, + "RENDER_PASS_SHADOW_MAP", + "explicit shadow-map packet category", + ) + + stencil_eligibility = function_body( + packets_cpp, + "static bool R_ScenePackets_ViewLightCanCastShadows(", + ) + reject( + stencil_eligibility, + "noDynamicShadows", + "noDynamicShadows static/prelight stencil exclusion", + ) + map_eligibility = function_body( + packets_cpp, + "static bool R_ScenePackets_ViewLightCanUseShadowMaps(", + ) + require( + map_eligibility, + "noDynamicShadows", + "dynamic/map-only noDynamicShadows exclusion", + ) + shadow_map_pass = function_body( + packets_cpp, + "static void R_ScenePackets_AddShadowMapPass(", + ) + require( + shadow_map_pass, + "R_ScenePackets_ViewLightCanUseShadowMaps", + "shadow-map-specific light eligibility", + ) + stencil_pass = function_body( + packets_cpp, + "static void R_ScenePackets_AddStencilShadowPass(", + ) + require( + stencil_pass, + "R_ScenePackets_ViewLightCanCastShadows", + "static/prelight stencil light eligibility", + ) + + for receiver in ( + "shadowMapPassIndex[ CLASSIC_INTERACTION_RECEIVER_LOCAL ]", + "shadowMapPassIndex[ CLASSIC_INTERACTION_RECEIVER_GLOBAL ]", + ): + require(source, receiver, "explicit mapped receiver pass index") + require( + source, + "R_ClassicInteractionDomain_LightShadowMapPass(", + "mapped pass accessor implementation", + ) + + for planned_replay in ( + "RecomputePlannedStencilWork( domain.lights[ 0 ] )", + "const bool mixedStencilPlan = domain.lights[ 0 ].logicalVolumeDraws == 5", + "domain.lights[ 0 ].preloadVolumeDraws == 3", + "const bool reusedHybridPlan = domain.lights[ 0 ].logicalVolumeDraws == 2", + "domain.lights[ 0 ].preloadVolumeDraws == 1", + ): + require( + source, + planned_replay, + "receiver-order physical stencil replay self-test", + ) + + map_hash = function_body(source, "static std::uint64_t HashShadowMapPass(") + for hashed_state in ( + "pass.receiver", + "pass.resourceOwner", + "pass.disposition", + "pass.mode", + "pass.lightClass", + "pass.receiverMask", + "pass.mappedCasterCount", + "pass.supplementCasterCount", + "pass.casterSignature", + "pass.incompleteMapMask", + "pass.incompleteStencilMask", + "pass.hybridIncompleteMask", + "pass.prelightMapMissingMask", + "pass.prelightStencilRequiredMask", + "pass.prelightStencilReadyMask", + "pass.resourceAlias", + "pass.mapRequired", + "pass.mapComplete", + "pass.stencilComplete", + "pass.hybridComplete", + "pass.hasStaticCasters", + "pass.hasDynamicCasters", + "pass.hasAlphaCasters", + "pass.hasTranslucentCasters", + "pass.allowCacheReuse", + "pass.allowCacheUpdate", + "pass.allowScratch", + "pass.hashedAlpha", + "pass.stableAlphaHash", + "pass.casterCullMode", + "pass.polygonFactor", + "pass.polygonOffset", + "pass.projected", + "pass.point", + ): + require(map_hash, hashed_state, "mapped-pass semantic hash") + for backend_identity in ( + "textureHandle", + "legacyViewLight", + "resourcePlanId", + "resourceGeneration", + ): + reject(map_hash, backend_identity, "backend/frame identity in semantic hash") + + prepare = function_body(source, "static bool R_ClassicInteractionDomain_PrepareView(") + for token in ( + "firstLight", + "firstSurface", + "firstPrimitive", + "firstShadowCaster", + "shadowMapPassPacketIndex", + "shadowMapPassCount", + "ready = true", + "interactionPassPacketIndex", + "packetDrawCount", + ): + require(prepare, token, "transactional view preparation") + require_before(prepare, "firstLight", "ready = true", "light arena before publication") + require_before(prepare, "firstSurface", "ready = true", "surface arena before publication") + require_before(prepare, "firstPrimitive", "ready = true", "primitive arena before publication") + require_before(prepare, "firstShadowCaster", "ready = true", "shadow arena before publication") + require_before(prepare, "shadowMapPassCount", "ready = true", "mapped-shadow arena before publication") + reject( + prepare, + "bool mappedShadowState", + "blanket mapped-shadow whole-view rejection", + ) + + for mapped_chain in ( + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_TRANSLUCENT", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_TRANSLUCENT", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL", + ): + require(prepare, mapped_chain, "exact mapped/supplement chain preparation") + + self_test = function_body(source, "bool RendererClassicInteractionDomain_RunSelfTest(") + for native_case in ( + "CLASSIC_INTERACTION_SHADOW_PROJECTED", + "CLASSIC_INTERACTION_SHADOW_POINT", + "CLASSIC_INTERACTION_SHADOW_HYBRID", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID", + "shadowMapPassIndex", + "HashShadowMapPass", + "shadowMapPassCount", + "hybridShadowPassCount", + "CLASSIC_INTERACTION_FAILURE_BACKEND_COVERAGE_MISMATCH", + ): + require(self_test, native_case, "native mapped/hybrid transaction coverage") + + for semantic in ( + "SL_BUMP", + "SL_DIFFUSE", + "SL_SPECULAR", + "r_skipBump", + "r_skipDiffuse", + "r_skipSpecular", + "globalImages->blackImage", + "globalImages->flatNormalMap", + "CLASSIC_INTERACTION_PRIMITIVE_NOOP_BLACK", + ): + require(source, semantic, "classic interaction decomposition") + + for integration in (backend, vk_backend): + require(integration, "R_ClassicInteractionDomain_ResetFrame();", "frame reset") + require(integration, "R_ClassicInteractionDomain_PrepareFrame( *scenePackets );", "frame preparation") + + require(init, 'idCVar r_rendererSharedWorldInteraction( "r_rendererSharedWorldInteraction", "0"', "default-off archived cvar") + require(bootstrap, '{ "r_rendererSharedWorldInteraction", &r_rendererSharedWorldInteraction, 0 }', "bootstrap rollback safety") + require(init, 'cmdSystem->AddCommand( "rendererClassicInteractionDomainSelfTest"', "native self-test command") + require(init, "R_ClassicInteractionDomain_Stats()", "gfxInfo diagnostics") + require(modern, "!r_rendererSharedWorldInteraction.GetBool()", "aggregate visible-owner suppression") + require(modern, "category == RENDER_PASS_ARB2_INTERACTION", "interaction legacy skip guard") + require(modern, "category == RENDER_PASS_STENCIL_SHADOW", "shadow legacy skip guard") + + gl_preflight = function_body(gl, "static bool RB_ARB2_SharedInteractionPreflight(") + gl_draw = function_body(gl, "static bool RB_ARB2_DrawSharedInteractionView(") + gl_primitive_draw = function_body( + gl, "static void RB_SharedWorldInteractionGLDrawPrimitive(" + ) + gl_shadow_draw = function_body( + gl, "static void RB_SharedWorldInteractionGLDrawShadowCaster(" + ) + gl_interaction_state = function_body( + gl, "static void RB_SharedWorldInteractionGLBeginInteractionState(" + ) + reject_raw_authored_reads(gl_preflight, "OpenGL shared preflight") + reject_raw_authored_reads(gl_draw, "OpenGL shared draw") + reject_raw_authored_reads(gl_primitive_draw, "OpenGL sealed primitive draw") + reject_raw_authored_reads(gl_shadow_draw, "OpenGL sealed shadow draw") + if "RB_GLSLPrepareInteractionVertexCache(" in gl_preflight: + raise AssertionError( + "OpenGL shared preflight must not re-enter the legacy material/cache helper" + ) + require(gl_preflight, "R_ClassicInteractionDomain_ResolveTexture", "OpenGL sealed texture preflight") + require(gl_preflight, "RB_SharedWorldInteractionGLCacheValid", "OpenGL sealed cache validation") + require(gl_preflight, "R_TouchVertexCache", "OpenGL retained cache lifetime") + require(gl, "R_ClassicInteractionDomain_ViewPrimitive", "OpenGL sealed primitive consumption") + require(gl_preflight, "R_ClassicInteractionDomain_LightShadowCaster", "OpenGL sealed shadow consumption") + require(gl_primitive_draw, "preparedPrimitive", "OpenGL retained sealed draw plan") + require(gl_shadow_draw, "preparedCaster", "OpenGL retained sealed shadow plan") + require(gl_preflight, "preparedPrimitive.vertexBuffer = tri->ambientCache->vbo", "OpenGL retained validated vertex buffer") + require(gl_preflight, "preparedPrimitive.indexPointer", "OpenGL retained validated index pointer") + require(gl_primitive_draw, "idVertexCache::BindArrayBuffer", "OpenGL retained vertex binding") + require(gl_primitive_draw, "idVertexCache::BindIndexBuffer", "OpenGL retained index binding") + require(gl_primitive_draw, "glDrawElements", "OpenGL retained indexed draw") + require(gl_shadow_draw, "RB_SharedWorldInteractionGLSubmitShadowElements", "OpenGL sealed stencil submission") + require(gl_draw, "R_ClassicInteractionDomain_RecordOwned", "OpenGL ownership reconciliation") + for forbidden in ( + "legacyDrawSurf", + "vertexCache.Position(", + "RB_DrawElementsWithCounters(", + ): + if forbidden in gl_draw or forbidden in gl_primitive_draw or forbidden in gl_shadow_draw: + raise AssertionError( + f"OpenGL committed shared draw re-enters unsealed geometry via {forbidden}" + ) + for attrib in (1, 2, 5, 6, 7): + require( + gl_interaction_state, + f"glDisableVertexAttribArrayARB( {attrib} )", + f"OpenGL canonical interaction attribute {attrib} reset", + ) + gl_entry = function_body(gl, "void RB_ARB2_DrawInteractions( void )") + require_before(gl_entry, "RB_ARB2_SharedInteractionPreflight", "RB_ARB2_DrawSharedInteractionView", "OpenGL preflight before shared draw") + require(gl_entry, "R_ClassicInteractionDomain_RecordBackendFallback", "OpenGL whole-view fallback accounting") + + gl_estimate_pass = function_body( + gl, "static void RB_ShadowMapEstimateArb2CachePass(" + ) + estimate_incomplete_start = gl_estimate_pass.find( + "RB_ShadowMapMapOrHybridAvailableForPass(" + ) + estimate_cache_start = gl_estimate_pass.find("const bool cacheable") + if not 0 <= estimate_incomplete_start < estimate_cache_start: + raise AssertionError( + "ARB2 incomplete-map estimation must precede cache lookup/admission" + ) + estimate_incomplete_gate = gl_estimate_pass[ + estimate_incomplete_start:estimate_cache_start + ] + require_compact( + gl_estimate_pass, + """if ( !RB_ShadowMapMapOrHybridAvailableForPass( + vLight, passKind, NULL ) ) {""", + "negated direct ARB2 estimator incomplete-map gate", + ) + for token in ( + "RB_ShadowMapMapOrHybridAvailableForPass(", + "vLight, passKind, NULL", + "estimate.stencilOnlyPasses++", + "return;", + ): + require( + estimate_incomplete_gate, + token, + "direct ARB2 estimator incomplete-map fail-closed gate", + ) + for forbidden in ( + "RB_ShadowMapFindPointCacheEntry(", + "RB_ShadowMapFindProjectedCacheEntry(", + "r_shadowMapMaxUpdatesPerView", + "estimate.freshUpdatePasses++", + ): + reject( + estimate_incomplete_gate, + forbidden, + "cache or admission work before incomplete-map failover", + ) + require_before( + gl_estimate_pass, + "RB_ShadowMapMapOrHybridAvailableForPass(", + "RB_ShadowMapStaticCacheableReadOnly( vLight, cachePassKind", + "incomplete-map estimator gate before cacheability", + ) + map_or_hybrid = function_body( + gl, + "static bool RB_ShadowMapMapOrHybridAvailableForPass(\n\t\tconst viewLight_t *vLight", + ) + for token in ( + "RB_ShadowMapReceiverMaskForPass( passKind )", + "vLight->shadowMapIncompleteMapMask", + "vLight->shadowMapPrelightMapMissingMask", + "RB_ShadowMapHybridAvailableForPass(", + "*hybridOut = hybrid", + "return hybrid", + ): + require( + map_or_hybrid, + token, + "shared direct/estimator complete-map-or-hybrid predicate", + ) + require_before( + gl_estimate_pass, + "RB_ShadowMapStaticCacheableReadOnly( vLight, cachePassKind", + "const int updateBudget", + "cache lookup before fresh-update admission", + ) + for token in ( + "passKind == SHADOWMAP_PASS_GLOBAL", + "vLight->localTranslucentShadowMapCasters", + ): + require( + gl_estimate_pass, + token, + "GLOBAL estimate includes local/noSelf translucent casters", + ) + + gl_translucent_global_receiver = function_body( + gl, "static bool RB_ShadowMapTranslucentGlobalReceiverNeeded(" + ) + for token in ( + "vLight != NULL", + "vLight->translucentInteractions != NULL", + "r_shadowMapTranslucentReceivers.GetBool()", + "!r_skipTranslucent.GetBool()", + ): + require( + gl_translucent_global_receiver, + token, + "shared translucent-only GLOBAL receiver predicate", + ) + + gl_light_support = function_body( + gl, "static shadowMapLightSupportReason_t RB_ShadowMapLightSupportReason(" + ) + require_compact( + gl_light_support, + """if ( vLight->globalInteractions == NULL + && vLight->localInteractions == NULL + && !RB_ShadowMapTranslucentGlobalReceiverNeeded( vLight ) ) { + return SHADOWMAP_SUPPORT_NO_INTERACTIONS; + }""", + "translucent-only GLOBAL receiver admission at the earliest light support gate", + ) + + gl_estimate_ownership = function_body( + gl, "bool RB_ShadowMapEstimateArb2CacheOwnership(" + ) + for token in ( + "bool deferGlobalReceiverDraw = false", + "const drawSurf_t *globalReceiverInteractions", + "RB_ShadowMapGlobalReceiverPlanningChain(", + "vLight, deferGlobalReceiverDraw", + "globalReceiverInteractions == NULL && vLight->localInteractions == NULL", + "vLight->localInteractions", + "vLight->localShadowMapCasters", + "vLight->globalShadowMapDynamicCasters", + "vLight->localShadowMapDynamicCasters", + "vLight->localShadows", + "globalReceiverInteractions )", + ): + require( + gl_estimate_ownership, + token, + "local opaque plus translucent GLOBAL estimate ownership", + ) + require_before( + gl_estimate_ownership, + "RB_ShadowMapGlobalReceiverPlanningChain(", + "globalReceiverInteractions == NULL && vLight->localInteractions == NULL", + "translucent-only GLOBAL receiver admitted before empty-light rejection", + ) + for token in ( + "glConfig.maxTextureUnits < 6", + "glConfig.maxTextureImageUnits < 6", + "!glConfig.cubeMapAvailable", + ): + require( + gl_estimate_ownership, + token, + "read-only estimator/direct capability parity", + ) + if gl_estimate_ownership.count("globalReceiverInteractions )") != 2: + raise AssertionError( + "Point and projected GLOBAL estimates must both consume the resolved receiver chain" + ) + if gl_estimate_ownership.count("vLight->localShadowMapCasters") != 2: + raise AssertionError( + "Point and projected GLOBAL estimates must both retain local/noSelf casters" + ) + estimate_deferred_fallback = function_body( + gl_estimate_ownership, "if ( deferGlobalReceiverDraw" + ) + require_compact( + gl_estimate_ownership, + """if ( deferGlobalReceiverDraw + && RB_DrawSurfChainHasFilteredSurface( vLight->globalInteractions, + RB_SurfaceNeedsShadowMapReceiverFallback ) ) {""", + "deferred GLOBAL estimate charges opaque receiver fallback ownership", + ) + require( + estimate_deferred_fallback, + "estimate.receiverFallbackPasses++;", + "deferred GLOBAL opaque receiver fallback estimate", + ) + if gl_estimate_ownership.count("estimate.receiverFallbackPasses++") != 1: + raise AssertionError( + "Deferred GLOBAL opaque receiver fallback must be estimated exactly once" + ) + + gl_direct_shadow_pass = function_body(gl, "static void RB_ShadowMapRunPass(") + require( + gl, + "const bool deferReceiverDraw = false )", + "direct ARB2 receiver deferral API", + ) + for token in ( + "RB_ShadowMapDrawReceiverFallbacks( vLight, passKind, primaryShadowSurfs, secondaryShadowSurfs, interactions, !deferReceiverDraw )", + "if ( deferReceiverDraw )", + "SHADOWMAP_PASS_RESULT_CACHE_REUSE", + "maskOk = true;", + "if ( !deferReceiverDraw )", + ): + require( + gl_direct_shadow_pass, + token, + "deferred direct ARB2 receiver presentation", + ) + reuse_defer_start = gl_direct_shadow_pass.find("if ( deferReceiverDraw )") + reuse_mask_start = gl_direct_shadow_pass.find( + "shadowMapTimedPhase_t cacheReuseTimer", reuse_defer_start + ) + if not 0 <= reuse_defer_start < reuse_mask_start: + raise AssertionError("Missing deferred cache-reuse handoff before receiver masking") + reuse_defer_block = gl_direct_shadow_pass[reuse_defer_start:reuse_mask_start] + for token in ( + "g_shadowMapGlobalPassMapped = pointLight", + "g_shadowMapDeferredGlobalReportPending = true", + "g_shadowMapDeferredGlobalPassResult =", + "SHADOWMAP_PASS_RESULT_CACHE_REUSE", + "return;", + ): + require(reuse_defer_block, token, "deferred cache-reuse resource publication") + for forbidden in ( + "g_shadowMapStats.mappedGlobalPasses++", + "RB_ShadowMapPassReport(", + "RB_GLSLPointShadowMap_CreateDrawInteractions(", + "RB_GLSLShadowMap_CreateDrawInteractions(", + "RB_ShadowMapStencilFallback(", + ): + reject(reuse_defer_block, forbidden, "receiver draw during deferred cache reuse") + fresh_mask_start = gl_direct_shadow_pass.find("bool maskOk = false;") + fresh_defer_start = gl_direct_shadow_pass.find( + "if ( deferReceiverDraw )", fresh_mask_start + ) + fresh_defer_end = gl_direct_shadow_pass.find("} else {", fresh_defer_start) + if not 0 <= fresh_mask_start < fresh_defer_start < fresh_defer_end: + raise AssertionError("Missing deferred fresh-map receiver handoff") + fresh_defer_block = gl_direct_shadow_pass[fresh_defer_start:fresh_defer_end] + require(fresh_defer_block, "maskOk = true;", "deferred fresh-map publication") + for forbidden in ( + "RB_GLSLPointShadowMap_CreateDrawInteractions(", + "RB_GLSLShadowMap_CreateDrawInteractions(", + "RB_ShadowMapStencilFallback(", + ): + reject(fresh_defer_block, forbidden, "receiver draw during deferred fresh map") + fresh_publish_start = gl_direct_shadow_pass.find( + "if ( deferReceiverDraw )", gl_direct_shadow_pass.find("if ( mapped )") + ) + fresh_publish_end = gl_direct_shadow_pass.find( + "RB_ShadowMapDebugOverlayCapture(", fresh_publish_start + ) + if not 0 <= fresh_publish_start < fresh_publish_end: + raise AssertionError("Missing deferred fresh-map result publication") + fresh_publish_block = gl_direct_shadow_pass[ + fresh_publish_start:fresh_publish_end + ] + for token in ( + "g_shadowMapGlobalPassMapped = pointLight", + "g_shadowMapDeferredGlobalReportPending = true", + "g_shadowMapDeferredGlobalPassResult =", + "SHADOWMAP_PASS_RESULT_MAPPED", + "return;", + ): + require( + fresh_publish_block, + token, + "deferred fresh-map resource publication", + ) + for forbidden in ( + "g_shadowMapStats.mappedGlobalPasses++", + "RB_ShadowMapPassReport(", + ): + reject( + fresh_publish_block, + forbidden, + "optimistic deferred fresh-map diagnostics", + ) + + deferred_global_complete = function_body( + gl, "static void RB_ShadowMapCompleteDeferredGlobalReceiver(" + ) + for token in ( + "g_shadowMapDeferredGlobalReportPending", + "g_shadowMapDeferredGlobalPassResult", + "SHADOWMAP_PASS_RESULT_MASK_FAIL", + "g_shadowMapStats.mappedGlobalPasses++", + "g_shadowMapStats.fallbackGlobalPasses++", + "g_shadowMapStats.maskFailGlobalPasses++", + "RB_ShadowMapDebugOverlayCapture(", + "RB_ShadowMapPassReport(", + "g_shadowMapDeferredGlobalReportPending = false", + ): + require( + deferred_global_complete, + token, + "deferred GLOBAL final-result diagnostics", + ) + + gl_global_receiver_plan = function_body( + gl, "static const drawSurf_t *RB_ShadowMapGlobalReceiverPlanningChain(" + ) + for token in ( + "vLight->globalInteractions", + "RB_SurfaceEligibleForShadowMapReceiver", + "RB_ShadowMapTranslucentGlobalReceiverNeeded( vLight )", + "vLight->translucentInteractions", + "!opaqueEligible && translucentEligible", + "deferReceiverDraw = true", + ): + require( + gl_global_receiver_plan, + token, + "GLOBAL receiver union planning", + ) + require_compact( + gl_global_receiver_plan, + """if ( !opaqueEligible && translucentEligible ) { + deferReceiverDraw = true; + return vLight->translucentInteractions; + } + return vLight->globalInteractions;""", + "branch-exact translucent-only GLOBAL receiver planning", + ) + + direct_setup_start = gl_entry.find("bool deferGlobalReceiverDraw") + translucent_phase_start = gl_entry.find( + "if ( !r_skipTranslucent.GetBool() )", direct_setup_start + ) + direct_branch_end = gl_entry.find( + "if ( supportReason >= 0", translucent_phase_start + ) + if not 0 <= direct_setup_start < translucent_phase_start < direct_branch_end: + raise AssertionError("Missing direct ARB2 deferred GLOBAL receiver phase") + direct_map_setup = gl_entry[direct_setup_start:translucent_phase_start] + for token in ( + "RB_ShadowMapGlobalReceiverPlanningChain(", + "vLight, deferGlobalReceiverDraw", + "vLight->localInteractions", + "vLight->localShadowMapCasters", + "vLight->localShadowMapDynamicCasters", + "vLight->localShadows", + "globalReceiverInteractions, deferGlobalReceiverDraw )", + "RB_ShadowMapDrawReceiverFallbacks( vLight", + "vLight->globalInteractions", + ): + require( + direct_map_setup, + token, + "local opaque plus deferred translucent GLOBAL direct ownership", + ) + if direct_map_setup.count("RB_ShadowMapRunPass(") != 4: + raise AssertionError( + "Direct point/projected paths must each prepare LOCAL and GLOBAL ownership" + ) + if direct_map_setup.count("globalReceiverInteractions, deferGlobalReceiverDraw )") != 2: + raise AssertionError( + "Direct point/projected GLOBAL passes must both defer translucent-only receivers" + ) + for local_chain in ( + "vLight->localShadowMapCasters", + "vLight->localShadowMapDynamicCasters", + "vLight->localShadows", + ): + if direct_map_setup.count(local_chain) < 2: + raise AssertionError( + f"Direct point/projected GLOBAL ownership lost {local_chain}" + ) + + require_compact( + direct_map_setup, + """if ( deferGlobalReceiverDraw + && vLight->globalInteractions != NULL ) {""", + "deferred GLOBAL opaque fallback draw guard", + ) + direct_opaque_fallback = function_body( + direct_map_setup, "if ( deferGlobalReceiverDraw" + ) + require_compact( + direct_opaque_fallback, + """RB_ShadowMapDrawReceiverFallbacks( vLight, + SHADOWMAP_PASS_GLOBAL, vLight->globalShadows, + vLight->localShadows, vLight->globalInteractions );""", + "deferred GLOBAL opaque fallback draw", + ) + if direct_map_setup.count("RB_ShadowMapDrawReceiverFallbacks( vLight") != 1: + raise AssertionError( + "Deferred GLOBAL opaque fallback subset must be submitted exactly once" + ) + + translucent_phase = gl_entry[translucent_phase_start:direct_branch_end] + for token in ( + "g_shadowMapGlobalPassMapped != SHADOWMAP_GLOBAL_MAPPED_NONE", + "RB_ShadowMapPrepareMappedReceiverStencil( vLight", + "SHADOWMAP_PASS_GLOBAL", + "g_shadowMapGlobalPassHybrid", + "shadowMapTimedPhase_t translucentMaskTimer", + "RB_ShadowMapBeginTimedPhase( translucentMaskTimer", + "SHADOWMAP_TIMING_MASK_PASS", + "const bool translucentMaskOk = receiverStencilReady", + "RB_ShadowMapEndTimedPhase( translucentMaskTimer )", + "RB_GLSLPointShadowMap_CreateDrawInteractions(", + "RB_GLSLShadowMap_CreateDrawInteractions(", + "if ( translucentMaskOk )", + "RB_ShadowMapCompleteDeferredGlobalReceiver( vLight", + "RB_ShadowMapTrackWrappedCustomGLSLReceivers(", + "RB_ShadowMapDrawReceiverFallbacks( vLight", + "RB_ShadowMapStencilFallbackFiltered(", + "RB_SurfaceShadowMapReceiverPrepareFailed", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "r_stencilTranslucentShadows.GetBool()", + "vLight->globalShadows != NULL", + "vLight->localShadows != NULL", + "RB_ShadowMapStencilFallback( vLight", + "vLight->translucentInteractions", + ): + require( + translucent_phase, + token, + "single deferred translucent mapped-or-stencil presentation", + ) + require_before( + translucent_phase, + "RB_ShadowMapPrepareMappedReceiverStencil( vLight", + "const bool translucentMaskOk = receiverStencilReady", + "deferred mapped receiver stencil before translucent draw", + ) + require_before( + translucent_phase, + "if ( translucentMaskOk )", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "mapped translucent receiver failure before full-stencil failover", + ) + if translucent_phase.count("RB_ShadowMapCompleteDeferredGlobalReceiver( vLight") != 2: + raise AssertionError( + "Deferred GLOBAL receiver success and failure must both finalize diagnostics" + ) + translucent_success = function_body( + translucent_phase, "if ( translucentMaskOk )" + ) + require( + translucent_success, + "g_shadowMapStats.translucentReceiverPasses++", + "successful mapped translucent receiver statistic", + ) + reject( + translucent_phase[: translucent_phase.find("if ( translucentMaskOk )")], + "g_shadowMapStats.translucentReceiverPasses++", + "optimistic mapped translucent receiver statistic", + ) + + gl_map_values = function_body( + gl, "static bool RB_SharedWorldInteractionGLMapPassValuesValid(" + ) + gl_map_preflight = function_body( + gl, "static bool RB_SharedWorldInteractionGLPreflightMapTransaction(" + ) + gl_map_prepare = function_body( + gl, "static bool RB_SharedWorldInteractionGLPrepareMappedPass(" + ) + gl_map_abort = function_body( + gl, "static void RB_SharedWorldInteractionGLAbortMapTransaction(" + ) + gl_map_activate = function_body( + gl, "static void RB_SharedWorldInteractionGLActivateMapResource(" + ) + gl_map_capture = function_body( + gl, "static bool RB_SharedWorldInteractionGLCaptureMapResource(" + ) + gl_map_schedule = function_body( + gl, "static bool RB_SharedWorldInteractionGLScheduleSealedMapPass(" + ) + gl_map_complete = function_body( + gl, "static void RB_SharedWorldInteractionGLCompleteSealedCacheUpdate(" + ) + gl_projected_scratch = function_body( + gl, "static bool RB_SharedWorldInteractionGLReserveProjectedScratch(" + ) + gl_point_scratch = function_body( + gl, "static bool RB_SharedWorldInteractionGLReservePointScratch(" + ) + gl_projected_resource_match = function_body( + gl, "static bool RB_SharedWorldInteractionGLProjectedEntryMatches(" + ) + gl_point_resource_match = function_body( + gl, "static bool RB_SharedWorldInteractionGLPointEntryMatches(" + ) + gl_map_alias = function_body( + gl, "static bool RB_SharedWorldInteractionGLAliasMappedPass(" + ) + gl_projected_map = function_body( + gl, "static bool RB_SharedWorldInteractionGLRenderProjectedMap(" + ) + gl_point_map = function_body( + gl, "static bool RB_SharedWorldInteractionGLRenderPointMap(" + ) + gl_map_caster = function_body( + gl, "static int RB_SharedWorldInteractionGLDrawMapCasterChain(" + ) + mapped_shadow_chain = function_body( + source, "static bool PrepareMappedShadowChain(" + ) + gl_mapped_begin = function_body( + gl, "static void RB_SharedWorldInteractionGLBeginMappedReceiver(" + ) + gl_mapped_draw = function_body( + gl, "static void RB_SharedWorldInteractionGLDrawMappedPrimitive(" + ) + gl_mapped_commit = function_body( + gl, "static int RB_SharedWorldInteractionGLCommitMappedPass(" + ) + gl_shadow_draw = function_body( + gl, "static void RB_SharedWorldInteractionGLDrawShadowRange(" + ) + point_pass_build = function_body(source, "static bool BuildShadowMapPasses(") + for token in ( + "R_ShadowMapPointFarDistance( viewLight )", + "R_ShadowMapPointReceiverSettings(", + "point.constantBias = receiverSettings.constantBias", + "point.normalBias = receiverSettings.normalBias", + "point.normalOffsetScale = receiverSettings.normalOffsetScale", + "point.texelBiasScale = receiverSettings.texelBiasScale", + ): + require(point_pass_build, token, "sealed bounded point receiver state") + require( + shadow_classification, + "r_shadowMapPointMaxWorldBias.GetFloat()", + "shared point receiver world-bias policy", + ) + for token in ( + "pass.point.constantBias", + "pass.point.normalBias", + "pass.point.texelBiasScale", + "pass.point.normalOffsetScale", + "R_ShadowMapPointStorageAdjustedReceiverSettings(", + ): + require(gl_mapped_begin, token, "OpenGL sealed bounded point receiver upload") + for token in ( + "pass.mapRequired", + "pass.mapComplete", + "pass.hybridComplete", + "pass.mappedCasterCount", + "pass.supplementCasterCount", + "pass.drawableMappedCasters", + "pass.noopMappedCasters", + "pass.drawableSupplementCasters", + "pass.noopSupplementCasters", + "pass.resourcePlanId", + "pass.resourceGeneration", + "pass.hasStaticCasters", + "pass.hasDynamicCasters", + "pass.hasTranslucentCasters", + "pass.allowCacheReuse", + "pass.allowCacheUpdate", + "pass.hashedAlpha", + "pass.stableAlphaHash", + "pass.projected.state", + "pass.point", + ): + require(gl_map_values, token, "OpenGL sealed mapped-pass validation") + require( + gl_map_values, + "|| pass.hasTranslucentCasters", + "OpenGL intentional translucent-moment fallback boundary", + ) + reject( + gl_map_values, + "|| pass.hasDynamicCasters", + "OpenGL dynamic mapped-caster rejection", + ) + for token in ( + "RB_SharedWorldInteractionGLPrepareMappedPass(", + "prepared.shadowMapPassCount", + "RB_SharedWorldInteractionGLPointEntryMatches(", + "RB_SharedWorldInteractionGLProjectedEntryMatches(", + "RB_SharedWorldInteractionGLAbortMapTransaction( viewDef )", + "prepared.mapTransactionPrepared = true", + "preparedPass.sampleTextureHandle", + "preparedPass.projectedOriginX", + "preparedPass.projectedSpanPixels", + "physicalAlias", + "alias.pass->resourceAlias", + "permittedAlias", + ): + require(gl_map_preflight, token, "OpenGL mapped transaction preflight") + reject( + gl_map_preflight, + "glDrawElements(", + "direct mapped draw outside retained map-render adapters", + ) + for map_body, label in ( + (gl_projected_map, "OpenGL sealed projected map render"), + (gl_point_map, "OpenGL sealed point map render"), + ): + require( + map_body, + "RB_SharedWorldInteractionGLDrawMapCasterChain(", + label, + ) + for legacy_chain in ( + "globalShadowMapCasters", + "localShadowMapCasters", + "globalShadowMapDynamicCasters", + "localShadowMapDynamicCasters", + "globalTranslucentShadowMapCasters", + "localTranslucentShadowMapCasters", + ): + reject(map_body, legacy_chain, f"legacy authored caster walk in {label}") + for sealed_dynamic_chain in ( + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC", + ): + require(map_body, sealed_dynamic_chain, f"dynamic caster coverage in {label}") + require(gl_map_caster, "prepared.mapCasters", "OpenGL sealed mapped caster draw") + require( + gl, + "prepared.shadowAlphaStages[preparedCaster.firstAlphaStage + i]", + "OpenGL sealed perforated caster stages", + ) + for legacy_renderer in ("RB_RenderShadowMap(", "RB_RenderPointShadowMap("): + reject(gl_map_prepare, legacy_renderer, "legacy shadow renderer re-entry") + reject( + gl_map_prepare, + "pass.hasDynamicCasters", + "OpenGL dynamic mapped-pass rejection", + ) + reject( + gl_map_prepare, + "if ( !schedule.cacheable )", + "OpenGL non-cacheable mapped-pass rejection", + ) + for token in ( + "pass.hasTranslucentCasters", + "pass.allowCacheUpdate", + "RB_SharedWorldInteractionGLScheduleSealedMapPass(", + "RB_SharedWorldInteractionGLReservePointScratch(", + "RB_SharedWorldInteractionGLReserveProjectedScratch(", + "RB_SharedWorldInteractionGLCaptureMapResource(", + ): + require(gl_map_prepare, token, "OpenGL cache/update/scratch map preparation") + for token in ( + "pass.allowCacheReuse", + "RB_ShadowMapBuildPassSignatureForView(", + "pass.legacyViewLight, viewDef, passKind, pass.point.valid", + "RB_SharedWorldInteractionGLMapUpdateAdmitted()", + "RB_ShadowMapFindPointCacheEntry(", + "RB_ShadowMapFindProjectedCacheEntry(", + "RB_ShadowMapAllocPointCacheEntry( false )", + "RB_ShadowMapAllocProjectedCacheEntry(", + "pass.projected.state.atlasDiv, false", + "schedule.cacheable = false", + ): + require(gl_map_schedule, token, "OpenGL sealed cache scheduler") + reject( + gl_map_schedule, + "RB_SharedWorldInteractionGLSealedMapSignature(", + "folded sealed-pass cache signature", + ) + for token in ( + "preparedPass.cacheLightIndex", + "preparedPass.cachePassKind", + "pass.lightClass", + "schedule.signature", + "entry->valid", + "entry->atlasStorageGeneration = preparedPass.projectedDepthImage != NULL", + "preparedPass.projectedDepthImage->GetStorageGeneration()", + ): + require(gl_map_complete, token, "OpenGL sealed cache publication") + for field in ("lightOrigin[3];", "farDistance;"): + require(gl, field, "point-cube render-time projection provenance") + point_active = function_body( + gl, + "static bool RB_ShadowMapActivePointCacheContentReady(", + "active point-cube provenance validation", + ) + require_order( + point_active, + ( + "entry->farDistance > 0.0f", + "entry->lightOrigin[0]", + "entry->colorStorageGeneration == 0", + "entry->depthStorageGeneration == 0", + "entry->colorImage->GetStorageGeneration()", + "entry->depthImage->GetStorageGeneration()", + ), + "active point cubes require published projection and storage provenance", + ) + point_projection = function_body( + gl, + "static bool RB_ShadowMapPointCacheEntryProjectionMatches(", + "point-cube render-time projection identity", + ) + require_order( + point_projection, + ( + "entry->farDistance == R_ShadowMapPointFarDistance( vLight )", + "entry->lightOrigin[0] == vLight->globalLightOrigin[0]", + "entry->lightOrigin[1] == vLight->globalLightOrigin[1]", + "entry->lightOrigin[2] == vLight->globalLightOrigin[2]", + ), + "stale point cubes cannot cross light-origin or far-plane changes", + ) + point_stale_reuse = function_body( + gl, + "static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntryAnySignature(", + "newest compatible point stale reuse", + ) + require_order( + point_stale_reuse, + ( + "RB_ShadowMapPointCacheEntryStorageValid( entry )", + "entry->size == requiredSize", + "entry->highPrecision == requiredHighPrecision", + "entry->depthCompare == requiredDepthCompare", + "RB_ShadowMapPointCacheEntryProjectionMatches( entry, vLight )", + "entry->lastUpdatedFrame > newest->lastUpdatedFrame", + "newest = entry;", + "return newest;", + ), + "point stale reuse selects the newest projection-compatible allocation", + ) + projected_stale_reuse = function_body( + gl, + "static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntryAnySignature(", + "newest projected stale reuse", + ) + require_order( + projected_stale_reuse, + ( + "RB_ShadowMapProjectedCacheEntryStorageValid( entry )", + "entry->lastUpdatedFrame > newest->lastUpdatedFrame", + "newest = entry;", + "return newest;", + ), + "projected stale reuse selects the most recently rendered sibling", + ) + direct_cache_completion = function_body( + gl, + "static void RB_ShadowMapCompleteCacheUpdate(", + "direct point-cache provenance publication", + ) + require_order( + direct_cache_completion, + ( + "entry->lightOrigin[0] = vLight->globalLightOrigin[0];", + "entry->lightOrigin[1] = vLight->globalLightOrigin[1];", + "entry->lightOrigin[2] = vLight->globalLightOrigin[2];", + "entry->farDistance = R_ShadowMapPointFarDistance( vLight );", + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->colorStorageGeneration = entry->colorImage != NULL", + "entry->depthStorageGeneration = entry->depthImage != NULL", + ), + "direct point-cache publication stamps projection and storage provenance", + ) + require_order( + gl_map_complete, + ( + "entry->lightOrigin[0] = pass.point.lightOrigin[0];", + "entry->lightOrigin[1] = pass.point.lightOrigin[1];", + "entry->lightOrigin[2] = pass.point.lightOrigin[2];", + "entry->farDistance = pass.point.farDistance;", + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->colorStorageGeneration = entry->colorImage != NULL", + "entry->depthStorageGeneration = entry->depthImage != NULL", + ), + "shared point-cache publication stamps sealed projection and storage provenance", + ) + require( + gl_map_prepare, + "RB_SharedWorldInteractionGLRenderPointMap(", + "OpenGL sealed point map adapter", + ) + require( + gl_map_prepare, + "RB_SharedWorldInteractionGLRenderProjectedMap(", + "OpenGL sealed projected map adapter", + ) + for token in ( + "g_pointShadowMapProgram.programObject", + "g_shadowMapProgram.programObject", + "preparedPass.pointLight", + "preparedPass.pointColorImage", + "preparedPass.pointDepthImage", + "preparedPass.projectedDepthImage", + "preparedPass.resourceWidth", + "preparedPass.resourceHeight", + "pass.point", + "pass.projected", + ): + require(gl_mapped_begin, token, "OpenGL sealed mapped receiver state") + for token in ( + "preparedPass.pointEntry", + "preparedPass.projectedEntry", + "preparedPass.pointColorImage", + "preparedPass.pointDepthImage", + "preparedPass.projectedDepthImage", + "preparedPass.renderTexture", + ): + require(gl_map_activate, token, "OpenGL sealed cache/scratch activation") + for token in ( + "sampleImage->GetDeviceHandle()", + "sampleImage->GetStorageGeneration()", + "preparedPass.sampleTextureHandle", + "preparedPass.sampleStorageGeneration", + "preparedPass.resourceWidth", + "preparedPass.resourceHeight", + ): + require(gl_map_capture, token, "OpenGL retained map-resource identity") + for scratch_body, scratch_label in ( + (gl_projected_scratch, "OpenGL projected scratch resource"), + (gl_point_scratch, "OpenGL point scratch resource"), + ): + for token in ( + "preparedPass.pass->allowScratch", + "globalImages->ScratchImage(", + "preparedPass.scratchResource = true", + "preparedPass.renderTexture", + "RB_SharedWorldInteractionGLCaptureMapResource(", + ): + require(scratch_body, token, scratch_label) + for resource_match, resource_label in ( + (gl_projected_resource_match, "OpenGL projected cache/scratch revalidation"), + (gl_point_resource_match, "OpenGL point cache/scratch revalidation"), + ): + for token in ( + "preparedPass.sampleTextureHandle", + "preparedPass.sampleStorageGeneration", + "preparedPass.scratchResource", + "preparedPass.renderTexture", + ): + require(resource_match, token, resource_label) + for token in ( + "entry->atlasStorageGeneration != 0", + "entry->atlasStorageGeneration", + "preparedPass.sampleStorageGeneration", + ): + require( + gl_projected_resource_match, + token, + "OpenGL projected cache storage-generation revalidation", + ) + require_order( + gl_point_resource_match, + ( + "entry->lightOrigin[0] == pass.point.lightOrigin[0]", + "entry->lightOrigin[1] == pass.point.lightOrigin[1]", + "entry->lightOrigin[2] == pass.point.lightOrigin[2]", + "entry->farDistance == pass.point.farDistance", + "entry->colorStorageGeneration != 0", + "entry->depthStorageGeneration != 0", + ), + "OpenGL point cache projection/storage provenance revalidation", + ) + for token in ( + "owner.sampleStorageGeneration", + "owner.sampleTextureHandle", + "owner.renderTexture", + "owner.scratchResource", + ): + require(gl_map_alias, token, "OpenGL exact mapped-resource alias") + require(gl_mapped_draw, "preparedPrimitive", "OpenGL retained mapped receiver geometry") + require(gl_mapped_draw, "pass.point", "OpenGL sealed point receiver state") + require(gl_mapped_draw, "pass.projected", "OpenGL sealed projected receiver state") + require( + mapped_shadow_chain, + "caster.selectedIndexCount = 0", + "perforated zero-alpha mapped no-op normalization", + ) + for token in ( + "caster.materialCoverage == MC_PERFORATED", + "caster.selectedIndexCount != 0", + "caster.vertexCount > 0", + "caster.totalIndexCount > 0", + ): + require( + function_body( + source, + "bool R_ClassicInteractionDomain_ShadowCasterNoopValid(", + ), + token, + "backend-neutral mapped no-op contract", + ) + require( + function_body( + gl, "static bool RB_SharedWorldInteractionGLMapCasterValuesValid(" + ), + "R_ClassicInteractionDomain_ShadowCasterNoopValid( caster )", + "OpenGL mapped no-op contract", + ) + require(gl_mapped_commit, "RB_SharedWorldInteractionGLSelectMapPass", "OpenGL mapped resource bind") + require(gl_mapped_commit, "RB_SharedWorldInteractionGLBeginMappedReceiver", "OpenGL mapped receiver bind") + require(gl_mapped_commit, "RB_SharedWorldInteractionGLDrawMappedPrimitive", "OpenGL mapped receiver draw") + require( + gl_mapped_commit, + "preparedPass.pointEntry->lastUsedFrame = tr.frameCount", + "OpenGL point shadow-map cache residency refresh", + ) + require( + gl_mapped_commit, + "preparedPass.projectedEntry->lastUsedFrame = tr.frameCount", + "OpenGL projected shadow-map cache residency refresh", + ) + require_before( + gl_mapped_commit, + "RB_SharedWorldInteractionGLDrawMappedPrimitive", + "preparedPass.pointEntry->lastUsedFrame = tr.frameCount", + "OpenGL cache residency refresh after mapped commit", + ) + for token in ( + "g_pointShadowMapProgram.translucentShadowMap[i]", + "g_shadowMapProgram.translucentShadowMap[i]", + "glUniform1iARB( momentMap, 6 )", + ): + require(gl_mapped_begin, token, "OpenGL neutral mapped moment samplers") + reject_raw_authored_reads(gl_mapped_begin, "OpenGL mapped receiver state") + reject_raw_authored_reads(gl_mapped_draw, "OpenGL mapped receiver draw") + for supplement in ( + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL", + ): + require(gl_draw, supplement, "OpenGL hybrid supplement order") + require(gl_draw, "prepared.shadowMapPassCount", "OpenGL map-pass coverage") + require(gl_draw, "prepared.hybridShadowPassCount", "OpenGL hybrid-pass coverage") + for token in ( + "preparedVolumeMode != mode", + "includeLocal && !preparedVolumeIncludesLocal", + "CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL", + ): + require(gl_draw, token, "OpenGL receiver-order physical stencil replay") + require_before( + gl_shadow_draw, + "RB_SharedWorldInteractionGLDrawShadowCaster(", + "logicalVolumeDraws++", + "OpenGL physical logical-volume count after draw", + ) + require_before( + gl_shadow_draw, + "RB_SharedWorldInteractionGLDrawShadowCaster(", + "preloadVolumeDraws += caster.caster->preload ? 1 : 0", + "OpenGL physical preload count after draw", + ) + for token in ( + "backEnd.renderTexture->MakeCurrent()", + "idRenderTexture::BindNull()", + "glViewport(", + "glScissor(", + "glUseProgramObjectARB( 0 )", + "GL_ClearStateDelta()", + ): + require(gl_map_abort, token, "OpenGL mapped-transaction state restoration") + require_before( + gl_preflight, + "RB_SharedWorldInteractionGLPreflightMapTransaction(", + "prepared.ready = true", + "OpenGL mapped transaction before whole-view publication", + ) + + vk_preflight = function_body(vk, "bool VK_ClassicInteraction_Preflight(") + vk_mapped_shadow_block = function_body( + vk, "static bool VK_ClassicInteraction_BuildMappedShadowBlock(" + ) + for token in ( + "mapPass.point.constantBias", + "mapPass.point.normalBias", + "mapPass.point.texelBiasScale", + "mapPass.point.normalOffsetScale", + ): + require( + vk_mapped_shadow_block, + token, + "Vulkan sealed bounded point receiver upload", + ) + vk_draw = function_body(vk, "void VK_ClassicInteraction_DrawOwnedView(") + vk_receiver_draw = function_body( + vk, "static void VK_ClassicInteraction_DrawReceiverRange(" + ) + vk_shadow_draw = function_body( + vk, "static void VK_ClassicInteraction_DrawShadowRange(" + ) + vk_fail = function_body(vk, "static bool VK_ClassicInteraction_Fail(") + reject_raw_authored_reads(vk_preflight, "Vulkan shared preflight") + reject_raw_authored_reads(vk_draw, "Vulkan shared draw") + reject_raw_authored_reads(vk_receiver_draw, "Vulkan sealed receiver draw") + reject_raw_authored_reads(vk_shadow_draw, "Vulkan sealed shadow draw") + require( + vk_preflight, + "r_singleTriangle.GetBool()", + "Vulkan single-triangle debug whole-view fallback", + ) + require(vk, "R_ClassicInteractionDomain_ResolveTexture", "Vulkan sealed texture preflight") + require(vk_preflight, "R_ClassicInteractionDomain_ViewPrimitive", "Vulkan sealed primitive consumption") + require(vk_preflight, "R_ClassicInteractionDomain_LightShadowCaster", "Vulkan sealed shadow consumption") + require(vk_receiver_draw, "prepared.draws", "Vulkan retained sealed draw plan") + require(vk_shadow_draw, "prepared.shadows", "Vulkan retained sealed shadow plan") + require(vk_fail, "VK_Exec_InteractionUniformRestore", "Vulkan uniform rollback helper") + require(vk_fail, "VK_Exec_SharedInteractionGeometryRestore", "Vulkan geometry rollback helper") + require(vk_draw, "R_ClassicInteractionDomain_RecordOwned", "Vulkan ownership reconciliation") + + for api in ( + "VK_ShadowMap_PreflightClassicInteractionView", + "VK_ShadowMap_CommitClassicInteractionView", + "VK_ShadowMap_AbortClassicInteractionView", + ): + require(vk_shadow_h, api, "Vulkan mapped transaction API") + require(vk_shadow, api, "Vulkan mapped transaction implementation") + vk_map_preflight = function_body( + vk_shadow, "bool VK_ShadowMap_PreflightClassicInteractionView(" + ) + vk_map_abort = function_body( + vk_shadow, "void VK_ShadowMap_AbortClassicInteractionView(" + ) + vk_map_commit = function_body( + vk_shadow, "void VK_ShadowMap_CommitClassicInteractionView(" + ) + vk_shadow_descriptor = function_body( + vk_shadow, "static bool VK_ClassicShadow_ResolveDescriptor(" + ) + for token in ( + "texture->filter != texture->image->GetFilter()", + "texture->repeat != texture->image->GetRepeat()", + ): + require(vk_shadow_descriptor, token, "Vulkan sealed shadow sampler state") + vk_shadow_physical_pass = function_body( + vk_shadow, "static bool VK_ClassicShadow_ValidatePhysicalPass(" + ) + for token in ( + "sealed.casterSignature", + "light.vLight->shadowMapCasterSignature", + "sealed.hashedAlpha", + "r_shadowMapHashedAlpha.GetBool()", + "sealed.stableAlphaHash", + "r_shadowMapStableAlphaHash.GetBool()", + "sealed.casterCullMode", + "r_shadowMapCasterCulling.GetInteger()", + "sealed.polygonFactor", + "r_shadowMapPolygonFactor.GetFloat()", + "sealed.polygonOffset", + "r_shadowMapPolygonOffset.GetFloat()", + ): + require(vk_shadow_physical_pass, token, "Vulkan sealed shadow cache identity") + for token in ( + "R_ClassicInteractionDomain_ViewLight(", + "R_ClassicInteractionDomain_LightShadowMapPass(", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_MAPPED", + "CLASSIC_INTERACTION_SHADOW_MAP_PASS_HYBRID", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC", + "sealed->mappedCasterCount", + "sealed->drawableMappedCasters", + "sealed->noopMappedCasters", + "reconciledPasses != view->shadowMapPassCount", + "VK_ShadowMap_PrepareViewLights( view->viewDef, true )", + "transaction.ready = true", + ): + require(vk_map_preflight, token, "Vulkan sealed mapped preflight") + for command in ("vkCmdDraw", "vkCmdClear", "VK_ShadowMap_RenderAtlas("): + reject(vk_map_preflight, command, "Vulkan attachment write during mapped preflight") + require(vk_map_abort, "VK_ShadowMap_ReleasePreparedLights( false )", "Vulkan mapped resource rollback") + require(vk_map_abort, "memset( &vkClassicShadowTransaction", "Vulkan mapped transaction reset") + require(vk_map_commit, "VK_ShadowMap_RenderAtlas( view->viewDef )", "Vulkan mapped transaction commit") + require(vk_map_commit, "common->FatalError(", "Vulkan infallible commit assertion") + require_before( + vk_fail, + "VK_ShadowMap_AbortClassicInteractionView(", + "VK_Exec_SharedInteractionGeometryRestore", + "Vulkan mapped abort before shared geometry rollback", + ) + require_before( + vk_preflight, + "VK_Exec_SharedInteractionGeometryCheckpoint()", + "VK_ShadowMap_PreflightClassicInteractionView( view )", + "Vulkan geometry checkpoint before mapped reservations", + ) + require_before( + vk_preflight, + "VK_Exec_PrepareTriGeometry(", + "VK_Exec_SharedInteractionGeometryCommit();", + "Vulkan receiver geometry preflight before transaction commit", + ) + require_before( + vk_preflight, + "VK_Exec_PrepareShadowGeometry(", + "VK_Exec_SharedInteractionGeometryCommit();", + "Vulkan shadow geometry preflight before transaction commit", + ) + require_before( + vk_preflight, + "VK_Exec_SharedInteractionGeometryCommit();", + "prepared.ready = true", + "Vulkan geometry transaction before whole-view publication", + ) + for command in ("vkCmdDraw(", "vkCmdDrawIndexed(", "vkCmdClearAttachments("): + reject(vk_preflight, command, "Vulkan attachment write during interaction preflight") + for token in ( + "VK_Exec_ShadowUniformAlloc(", + "plan.projectedShadowBlock", + "plan.pointShadowBlock", + "plan.shadowUniformOffset", + ): + require(vk_preflight, token, "Vulkan mapped receiver uniform preflight") + for token in ( + "prepared.projectedInteractionPipeline", + "prepared.pointInteractionPipeline", + "prepared.mappedInteractionLayout", + "setCount = plan.mappedShadowMode != 0 ? 8u : 7u", + "plan.mappedShadowMode != 0 ? 2u : 1u", + ): + require(vk_receiver_draw, token, "Vulkan mapped receiver submission") + require_before( + vk_draw, + "VK_ShadowMap_CommitClassicInteractionView( prepared.view )", + "vkCmdSetViewport(", + "Vulkan mapped commit before main-target interaction writes", + ) + for supplement in ( + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL", + ): + require(vk_draw, supplement, "Vulkan hybrid supplement order") + require(vk_draw, "prepared.view->shadowMapPassCount", "Vulkan map-pass coverage") + require(vk_draw, "prepared.view->hybridShadowPassCount", "Vulkan hybrid-pass coverage") + vk_atlas = function_body(vk_shadow, "bool VK_ShadowMap_RenderAtlas(") + for token in ( + "classicCommit", + "VK_ClassicShadow_FindPassPlan(", + "VK_ClassicShadow_DrawProjectedPass(", + "VK_ClassicShadow_DrawPointPass(", + ): + require(vk_atlas, token, "Vulkan sealed mapped commit path") + vk_view = function_body(vk_executor, "void VK_GuiExecutor_Draw3DView(") + require_before(vk_view, "VK_ClassicInteraction_Preflight", "VK_ClassicInteraction_DrawOwnedView", "Vulkan preflight before shared draw") + require(vk_view, "VK_Interactions_DrawLights( viewDef )", "untouched Vulkan classic fallback") + vk_bind_tri = function_body( + vk_executor, "bool VK_Exec_BindTriGeometry(" + ) + vk_bind_shadow = function_body( + vk_executor, "bool VK_Exec_BindShadowGeometry(" + ) + for token in ( + "VK_Exec_CPUCacheValid(", + "requiredBytes <= static_cast( INT_MAX )", + "cache->tag != TAG_FREE", + "cache->size >= static_cast( requiredBytes )", + "cache->vbo == 0", + "cache->virtMem != NULL", + ): + require(vk_executor, token, "Vulkan shared geometry cache validation") + require( + vk_bind_tri, + "VK_Exec_UploadTriGeometry(", + "Vulkan receiver/map-caster validated upload path", + ) + require( + vk_bind_shadow, + "VK_Exec_CPUCacheValid(", + "Vulkan stencil-volume cache validation", + ) + vk_prepare_tri = function_body( + vk_executor, "bool VK_Exec_PrepareTriGeometry(" + ) + require( + vk_prepare_tri, + "VK_Exec_PrepareTriGeometryOffsets(", + "Vulkan checkpointed receiver geometry preparation", + ) + + require(benchmark, 'append_set(args, "r_rendererSharedWorldInteraction", "0")', "benchmark launch isolation") + require(benchmark, '"r_rendererSharedWorldInteraction 0"', "benchmark script isolation") + require(benchmark, '"interaction": {', "controlled interaction runtime profile") + require(benchmark, '"setviewpos 0 -192 96 20 90 0"', "controlled interaction camera") + require( + benchmark, + '"testModel models/mapobjects/strogg/crates/crate1_small.lwo"', + "controlled stock interaction caster", + ) + require( + benchmark, + "spawn func_static model models/mapobjects/strogg/crates/crate1_medium.lwo origin \"-90 -70 -5.7\"", + "controlled stock interaction shadow receiver", + ) + require( + benchmark, + "testPointLight 300 origin \"96 -128 0\"", + "controlled side point shadow light", + ) + require(benchmark, '"testLight"', "controlled stock projected shadow light") + for preset in ( + '"unshadowed": {', + '"stencil": {', + '"mapped": {', + '"csm": {', + '"mixed": {', + '"map-budget-fallback": {', + ): + require(benchmark, preset, "controlled interaction shadow preset") + for runtime_target in ( + '"interaction-shadow-stock": {', + '"interactionShadowTarget": "dynamic"', + '"interactionShadowTarget": "perforated"', + '"interactionShadowTarget": "hybrid"', + '"interactionShadowTarget": "fallback"', + '"sharedInteractionMaps"', + 'def shared_interaction_map_records(', + '"shared interaction per-map reconciliation="', + '"shared interaction dynamic caster feature missing="', + '"shared interaction perforated caster feature missing="', + '"shared interaction same-light hybrid="', + "atomic fallback=", + ): + require(benchmark, runtime_target, "complete interaction-shadow runtime gate") + require( + benchmark, + "def compare_screenshot_difference_if_requested(", + "visible shadow image-difference gate", + ) + require( + benchmark, + '"--difference-reference-dir"', + "shadow-off engine-TGA reference option", + ) + require(benchmark, '"sharedInteraction": extract_last_line', "runtime ownership diagnostics") + require( + init, + "features=%d+%d+%d+%d", + "per-map static/dynamic/alpha/translucent diagnostics", + ) + require(benchmark, "def interaction_expectation(", "runtime ownership expectation") + require(benchmark, "def evaluate_shared_interaction_evidence(", "runtime ownership evidence gate") + require( + benchmark, + 'match = re.match(r"^\\s*([+-]?\\d+)", value)', + "engine-compatible boolean expectation parsing", + ) + require( + benchmark, + 'for backend_name in ("GL", "VK"):', + "disabled runtime zero-coverage gate", + ) + require( + benchmark, + '"shared interaction primitive reconciliation="', + "owned runtime primitive reconciliation gate", + ) + require( + benchmark, + '"shared interaction rollback {name}="', + "fallback runtime zero-draw gate", + ) + require( + benchmark, + '"shared interaction complete-view fallback="', + "fallback runtime complete-view coverage gate", + ) + require(baseline, 'add_set(args, "r_rendererSharedWorldInteraction", 0)', "stock baseline isolation") + require(matrix, "+rendererClassicInteractionDomainSelfTest", "safe startup self-test") + require(matrix, '"id": "sp-mv2-interaction"', "safe/manual interaction case") + + test_path = "tools/tests/renderer_classic_interaction_domain.py" + require(validation, '"renderer_classic_interaction_domain.py"', "local validation registration") + require(commit_ci, test_path, "commit CI registration") + require(push_ci, test_path, "push CI registration") + require(roadmap, "Milestone D scoped implementation is complete.", "roadmap delivery status") + require(roadmap, "Milestone E temporal", "roadmap implemented successor") + require(roadmap, "Milestone F now has a guarded", "roadmap next target") + require(domain_doc, "shadow-coupled interaction ownership", "completed shadow domain") + require(domain_doc, "translucent moment-map casters", "intentional shadow fallback") + require(domain_doc, "features=static+dynamic+alpha+translucent", "mapped feature diagnostics") + require(domain_doc, "whole-view fallback", "domain documentation") + require(domain_doc, "original openQ4 work", "source provenance statement") + + print("renderer_classic_interaction_domain: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/renderer_classic_special_frame_domain.py b/tools/tests/renderer_classic_special_frame_domain.py new file mode 100644 index 00000000..1630b223 --- /dev/null +++ b/tools/tests/renderer_classic_special_frame_domain.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Static regression contract for render-demo and Raven special-frame ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def require_before(haystack: str, first: str, second: str, context: str) -> None: + first_at = haystack.find(first) + second_at = haystack.find(second) + if first_at < 0 or second_at < 0 or first_at >= second_at: + raise AssertionError(f"Expected {first!r} before {second!r} in {context}") + + +def function_body(source: str, signature: str, context: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"Missing {signature!r} in {context}") + opening = source.find("{", start + len(signature)) + if opening < 0: + raise AssertionError(f"Missing opening brace for {signature!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[opening + 1 : index] + raise AssertionError(f"Unterminated {signature!r} in {context}") + + +def validate_sealed_domain() -> None: + header = read("src/renderer/ClassicSpecialFrameDomain.h") + source = read("src/renderer/ClassicSpecialFrameDomain.cpp") + packets = read("src/renderer/ScenePackets.cpp") + packets_header = read("src/renderer/ScenePackets.h") + + for token in ( + "CLASSIC_SPECIAL_FRAME_SCOPE_RENDER_DEMO", + "CLASSIC_SPECIAL_FRAME_SCOPE_RAVEN_EFFECTS", + "CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH", + "CLASSIC_SPECIAL_FRAME_FAILURE_RENDER_DEMO_STATE", + "CLASSIC_SPECIAL_FRAME_FAILURE_SPECIAL_EFFECT_STATE", + "CLASSIC_SPECIAL_FRAME_FAILURE_BACKEND_COVERAGE_MISMATCH", + "classicSpecialFrameDomainView_t", + "renderDemoVersion", + "specialEffectsMask", + "R_ClassicSpecialFrameDomain_PrepareFrame", + "R_ClassicSpecialFrameDomain_RecordOwned", + "R_ClassicSpecialFrameDomain_FinalizeBackendFrame", + "RendererClassicSpecialFrameDomain_RunSelfTest", + ): + require(header, token, "special-frame domain interface") + + for token in ( + "scene.renderDemoPlayback", + "session->renderdemoVersion", + "session->rw != view->viewDef->renderWorld", + "RENDER_PASS_DEPTH", + "RENDER_PASS_ARB2_INTERACTION", + "RENDER_PASS_AMBIENT", + "RENDER_PASS_SPECIAL_EFFECTS", + "SPECIAL_EFFECT_BLUR | SPECIAL_EFFECT_AL", + "CLASSIC_SPECIAL_FRAME_FAILURE_SOURCE_PACKET_MISMATCH", + "RecordBackendFallback", + ): + require(source, token, "special-frame source validation") + require_before( + source, + "R_ClassicSpecialFrameDomain_ValidateSceneRange( packetFrame,", + "R_ClassicSpecialFrameDomain_Commit( *view );", + "source reconciliation before ownership", + ) + + demo_classifier = function_body( + packets, + "static bool R_ScenePackets_IsRenderDemoPlaybackView(", + "render-demo packet classifier", + ) + for token in ( + "session->readDemo != NULL", + "session->rw == viewDef->renderWorld", + "!viewDef->isSubview", + ): + require(demo_classifier, token, "exact render-demo provenance") + require(packets, "SetLastSceneSpecialEffectsMask(", "special-effects source capture") + require(packets, "R_ScenePackets_ActiveSpecialEffectsMask", "backend packet reconstruction") + require(packets_header, "bool\t\t\t\t\trenderDemoPlayback;", "render-demo packet state") + require(packets_header, "void SetLastSceneSpecialEffectsMask", "packet mutation API") + + +def validate_backend_handoffs() -> None: + gl_backend = read("src/renderer/tr_backend.cpp") + gl = read("src/renderer/draw_common.cpp") + vk_backend = read("src/renderer/Vulkan/vk_Backend.cpp") + vk = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + + for source, backend in ((gl_backend, "OpenGL"), (vk_backend, "Vulkan")): + require(source, "R_ClassicSpecialFrameDomain_ResetFrame();", f"{backend} reset") + require(source, "R_ClassicSpecialFrameDomain_PrepareFrame( *scenePackets );", f"{backend} preparation") + require(source, "R_ClassicSpecialFrameDomain_FinalizeBackendFrame(", f"{backend} final fallback") + + demo = function_body(gl_backend, "static bool RB_DrawSharedRenderDemoView(", "OpenGL demo adapter") + require(demo, "RB_DrawView( data );", "OpenGL complete demo execution") + require(demo, "R_ClassicSpecialFrameDomain_RecordOwned", "OpenGL demo ownership") + for token in ( + "RB_CompositeRVSpecialBlur", + "RB_DrawRVSpecialALLight", + "SPECIAL_EFFECT_BLUR", + "SPECIAL_EFFECT_AL", + "R_ClassicSpecialFrameDomain_RecordOwned", + ): + require(gl, token, "OpenGL special-effect completion") + + vk_view = function_body(vk, "void VK_GuiExecutor_Draw3DView(", "Vulkan demo adapter") + require(vk_view, "sharedRenderDemoReady", "Vulkan demo readiness") + require(vk_view, "R_ClassicSpecialFrameDomain_RecordOwned", "Vulkan demo ownership") + vk_effects = function_body(vk, "static void VK_Exec_DrawRVSpecialEffects(", "Vulkan special adapter") + require(vk_effects, "drewBlur", "Vulkan blur completion") + require(vk_effects, "drewAL", "Vulkan AL completion") + require(vk_effects, "R_ClassicSpecialFrameDomain_RecordOwned", "Vulkan special ownership") + + +def validate_controls_and_evidence() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + local = read("src/renderer/tr_local.h") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + matrix = read("tools/tests/renderer_validation_matrix.py") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + registry = read("tools/validation/openq4_validate.py") + roadmap = read("docs/dev/idtech5-modernization-roadmap.md") + domain_doc = read("docs/dev/classic-special-frame-domain-modernization.md") + + require(init, 'r_rendererSharedSpecialFrame( "r_rendererSharedSpecialFrame", "0"', "default-off cvar") + require(init, 'cmdSystem->AddCommand( "rendererClassicSpecialFrameDomainSelfTest"', "native self-test") + require(init, "Renderer shared special frame:", "gfxInfo diagnostics") + require(local, "extern idCVar r_rendererSharedSpecialFrame;", "renderer cvar API") + require(bootstrap, '{ "r_rendererSharedSpecialFrame", &r_rendererSharedSpecialFrame, 0 }', "default safety") + for source, context in ( + (matrix, "safe renderer matrix"), + (benchmark, "benchmark isolation"), + (baseline, "stock-baseline isolation"), + ): + require(source, "r_rendererSharedSpecialFrame", context) + require(matrix, "+rendererClassicSpecialFrameDomainSelfTest", "safe native self-test") + require(registry, '"renderer_classic_special_frame_domain.py"', "validation registry") + require(roadmap, "Milestone D scoped implementation is complete.", "advanced roadmap status") + require(roadmap, "Milestone E temporal", "advanced roadmap status") + require(roadmap, "Milestone F now has a guarded", "advanced roadmap target") + for token in ( + "session stream", + "portal-sky", + "RC_DRAW_SPECIAL_EFFECTS", + "r_rendererSharedSpecialFrame 1", + "classic fallback", + "engine-written", + ): + require(domain_doc, token, "domain documentation") + + +def main() -> int: + validate_sealed_domain() + validate_backend_handoffs() + validate_controls_and_evidence() + print("renderer_classic_special_frame_domain: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_classic_subview_domain.py b/tools/tests/renderer_classic_subview_domain.py new file mode 100644 index 00000000..1e57a8c8 --- /dev/null +++ b/tools/tests/renderer_classic_subview_domain.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +"""Static regression contract for sealed direct, capture, and nested subview ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_subview_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def braced_body(source: str, marker: str, context: str) -> str: + start = source.find(marker) + if start == -1: + raise AssertionError(f"Missing {marker!r} in {context}") + opening = source.find("{", start + len(marker)) + if opening == -1: + raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find closing brace for {marker!r} in {context}") + + +def validate_packet_capture_edge() -> None: + header = read("src/renderer/ScenePackets.h") + source = read("src/renderer/ScenePackets.cpp") + for token in ( + "SCENE_PACKET_MAX_SUBVIEW_CAPTURES", + "SCENE_PACKET_OVERFLOW_SUBVIEW_CAPTURES", + "sceneSubviewCapture_t", + "viewScenePacketIndex", + "AddSubviewCapture", + "NumSubviewCaptures", + "SubviewCapture", + "R_ScenePackets_AddCopyRender( idImage *image, int x, int y", + ): + require(header, token, "packet-level capture edge") + for token in ( + "rg_frontEndLastDrawView", + "R_ScenePackets_FrontEndCaptureRequired", + "r_rendererSharedSubview.GetBool()", + "R_ScenePackets_AddCopyRender( idImage *image, int x, int y", + "rg_frontEndScenePacketFrame.AddSubviewCapture", + "rg_frontEndLastDrawView = NULL;", + "R_ScenePackets_BuildLegacyCommandStream", + "packetFrame.AddSubviewCapture( lastDrawView", + "subviewCaptures=%d", + ): + require(source, token, "front-end and legacy capture association") + + +def validate_domain_contract() -> None: + header = read("src/renderer/ClassicSubviewDomain.h") + source = read("src/renderer/ClassicSubviewDomain.cpp") + combined = header + source + for token in ( + "CLASSIC_SUBVIEW_DOMAIN_KIND_DIRECT_MIRROR", + "CLASSIC_SUBVIEW_DOMAIN_KIND_REMOTE_CAMERA", + "CLASSIC_SUBVIEW_DOMAIN_KIND_MIRROR", + "CLASSIC_SUBVIEW_DOMAIN_KIND_REFLECTION", + "CLASSIC_SUBVIEW_DOMAIN_KIND_REFRACTION", + "CLASSIC_SUBVIEW_DOMAIN_KIND_XRAY", + "CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_2D", + "CLASSIC_SUBVIEW_DOMAIN_CAPTURE_COLOR_CUBEMAP", + "CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_2D", + "CLASSIC_SUBVIEW_DOMAIN_CAPTURE_DEPTH_CUBEMAP", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_CAPTURE", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_CAPTURE_VIEWPORT_MISMATCH", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_CAPTURE_TARGET", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNEXPECTED_CAPTURE", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_UNSUPPORTED_SPECIAL_SEMANTICS", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_VIEW_SEMANTICS_MISMATCH", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_MISSING_NESTED_PARENT", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_COMMAND_ORDER", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_PARENT_FALLBACK", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CHILD_FALLBACK", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_NESTED_CINEMATIC_POST_FALLBACK", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTING_INCOMPLETE", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_NESTED_CINEMATIC_POST_INCOMPLETE", + "CLASSIC_SUBVIEW_DOMAIN_FAILURE_BACKEND_CAPTURE_MISMATCH", + "CLASSIC_SUBVIEW_DOMAIN_BACKEND_GL", + "CLASSIC_SUBVIEW_DOMAIN_BACKEND_VULKAN", + "R_ClassicSubviewDomain_ResetFrame", + "R_ClassicSubviewDomain_PrepareFrame", + "R_ClassicSubviewDomain_CaptureMatches", + "R_ClassicSubviewDomain_IsCaptureBacked", + "R_ClassicSubviewDomain_IsDirect", + "R_ClassicSubviewDomain_ViewSemanticsMatch", + "R_ClassicSubviewDomain_ReadyForBackend", + "R_ClassicSubviewDomain_RecordOwned", + "R_ClassicSubviewDomain_RecordDirectOwned", + "R_ClassicSubviewDomain_RecordBackendFallback", + "RendererClassicSubviewDomain_RunSelfTest", + ): + require(combined, token, "shared subview domain API") + for token in ( + "FindScenePacket( packetFrame, view.parentViewDef )", + "ParentContainsSurface", + "FindDirectKind", + "FindCaptureKind", + "ClassifyCaptureTarget", + "DI_REMOTE_RENDER", + "DI_MIRROR_RENDER", + "DI_REFLECTION_RENDER", + "DI_REFRACTION_RENDER", + "DI_XRAY_RENDER", + "HasSupportedSpecialSemantics", + "CaptureViewSemantics", + "ViewSemanticsMatch", + "ResolveNestedOwnership", + "parentViewIndex", + "rootViewIndex", + "nestingDepth", + "subtreeViewCount", + "backendCompleted", + "FinalizeTransaction", + "PublishOwned", + "RecordSingleFallback", + "ownedNestedTransactions", + "fallbackNestedTransactions", + "nestedTopology", + "semanticClipPlanes", + "viewDef->isMirror", + "viewDef->isXraySubview", + "capture.copyDepth", + "capture.cubeFace", + "TT_CUBIC", + "FMT_DEPTH_STENCIL", + "captureTextureType", + "captureTextureFormat", + "captureCopyDepth", + "ownedDepthCubemapCaptures", + "capture.x != viewDef->viewport.x1", + "ready = true", + "fallbackViews", + "nestedCommandOrder", + "backendNestingIncomplete", + "nestedCinematicPostFallback", + "backendNestedCinematicPostIncomplete", + "!domain.stats.prepared || domain.stats.overflow", + "R_ClassicCinematicPostDomain_SubviewTransactionReady", + "R_ClassicCinematicPostDomain_SubviewTransactionCompleted", + "R_ClassicCinematicPostDomain_PublishSubviewTransactionOwned", + "R_ClassicCinematicPostDomain_RecordSubviewTransactionFallback", + "classic subview and cinematic/post GL backend ordinals must match", + "classic subview and cinematic/post Vulkan backend ordinals must match", + "classic subview and cinematic/post backend counts must match", + "emptyCinematicFrame.MarkBackendDerived()", + "R_ClassicCinematicPostDomain_PrepareFrame( emptyCinematicFrame )", + ): + require(source, token, "bounded subview admission and fallback") + ready = braced_body( + source, + "bool R_ClassicSubviewDomain_ReadyForBackend(", + "subview backend readiness", + ) + require(ready, "view.backendCompleted[backend]", "completed-view readiness seal") + for marker, context in ( + ("bool R_ClassicSubviewDomain_RecordOwned(", "capture ownership reporting"), + ("bool R_ClassicSubviewDomain_RecordDirectOwned(", "direct ownership reporting"), + ): + reporter = braced_body(source, marker, context) + require( + reporter, + "|| view->backendCompleted[backend]", + f"{context} pre-publication duplicate guard", + ) + require(source, "nestedChildReadyInitially", "nested duplicate native regression") + require(source, "nestedChildNotReadyTwice", "nested duplicate native regression") + require(source, "nestedDuplicateSealed", "nested duplicate native regression") + + +def validate_backend_capture_ownership() -> None: + gl = read("src/renderer/tr_backend.cpp") + vk = read("src/renderer/Vulkan/vk_Backend.cpp") + image_gl = read("src/renderer/Image_load.cpp") + image_vk = read("src/renderer/Vulkan/vk_Image.cpp") + executor_vk = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + for source, backend in ((gl, "GL"), (vk, "Vulkan")): + for token in ( + "R_ClassicSubviewDomain_PrepareFrame( *scenePackets );", + "R_ClassicSubviewDomain_CaptureMatches", + "R_ClassicSubviewDomain_RecordOwned", + "R_ClassicSubviewDomain_IsCaptureBacked", + "R_ClassicSubviewDomain_IsDirect", + "R_ClassicSubviewDomain_RecordDirectOwned", + "R_ClassicSubviewDomain_RecordBackendFallback", + "R_ClassicSubviewDomain_ReadyForBackend", + "pendingSharedSubview", + "== CLASSIC_SUBVIEW_DOMAIN_BACKEND_UNRECORDED", + ): + require(source, token, f"{backend} capture ownership") + copy_case = braced_body(source, "case RC_COPY_RENDER:", f"{backend} copy transaction") + require(copy_case, "sharedCaptureMatches", f"{backend} sealed copy decision") + require(copy_case, "pendingSharedSubview->captureImage", f"{backend} sealed capture image") + require(copy_case, "RecordOwned", f"{backend} post-copy ownership") + require(gl, "RB_ClassicSubview_CopyOwned", "GL sealed capture consumer") + require(gl, "RB_DrawSharedDirectSubview", "GL direct subview consumer") + for token in ( + "CopyFramebuffer( view.captureX, view.captureY", + "CopyDepthbuffer( view.captureX, view.captureY", + "view.captureCubeFace", + "CopyFramebuffer( cmd->x, cmd->y, cmd->imageWidth", + "cmd->cubeFace", + ): + require(gl, token, "GL exact capture dispatch") + for token in ( + "GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT + cubeFace", + "CopyDepthbuffer", + "glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, copyTarget", + ): + require(image_gl, token, "GL cubemap/depth transfer") + require(vk, "VK_Exec_CopyRender(", "Vulkan capture consumer") + require(vk, "VK_DrawSharedDirectSubview", "Vulkan direct subview consumer") + for token in ( + "VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT", + "VK_IMAGE_VIEW_TYPE_CUBE", + "entry->isCube = isCube", + ): + require(image_vk, token, "Vulkan depth cubemap target") + for token in ( + "targetIsCube", + "targetIsDepth", + "cubeFace < 0 || cubeFace >= 6", + "rows[ row ].dstSubresource.baseArrayLayer = targetIsCube", + "region.dstSubresource.baseArrayLayer = targetIsCube", + ): + require(executor_vk, token, "Vulkan exact target/aspect/face transfer") + + +def validate_controls_diagnostics_and_registration() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + local = read("src/renderer/tr_local.h") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + matrix = read("tools/tests/renderer_validation_matrix.py") + baseline = read("tools/validation/stock_asset_baseline.py") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + registry = read("tools/validation/openq4_validate.py") + docs = read("docs/dev/classic-subview-domain-modernization.md") + for token in ( + 'r_rendererSharedSubview( "r_rendererSharedSubview", "0"', + '"rendererClassicSubviewDomainSelfTest"', + "RendererClassicSubviewDomain self-test passed", + "classicSubviewDomain requested=", + "classicSubviewDomain backend=", + "colorCubemap=", + "depthCubemap=", + "nestedTransactions=", + "maxNestingDepth=", + ): + require(init, token, "default-off subview control and diagnostics") + require(local, "extern idCVar r_rendererSharedSubview;", "renderer cvar API") + require(bootstrap, '"r_rendererSharedSubview", &r_rendererSharedSubview, 0', "bootstrap default") + for source, context in ( + (matrix, "renderer validation matrix"), + (baseline, "stock-baseline isolation"), + (benchmark, "benchmark isolation"), + ): + require(source, "r_rendererSharedSubview", context) + require(registry, '"renderer_classic_subview_domain.py"', "validation registry") + for token in ( + "capture-backed", + "cubemap", + "depth capture", + "unsupportedCaptureTarget", + "direct", + "remote-camera", + "reflection", + "x-ray", + "refraction", + "mirror", + "r_rendererSharedSubview 1", + "nested", + "atomic", + "classic fallback", + ): + require(docs, token, "subview contract documentation") + for workflow_path in ( + ".github/workflows/commit-validation.yml", + ".github/workflows/push-verification.yml", + ): + workflow = read(workflow_path) + if workflow.count(TEST_PATH) != 2: + raise AssertionError( + f"Expected compile and execution registration in {workflow_path}" + ) + + +def main() -> int: + validate_packet_capture_edge() + validate_domain_contract() + validate_backend_capture_ownership() + validate_controls_diagnostics_and_registration() + print("renderer_classic_subview_domain: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_classic_world_ambient_domain.py b/tools/tests/renderer_classic_world_ambient_domain.py new file mode 100644 index 00000000..4022158b --- /dev/null +++ b/tools/tests/renderer_classic_world_ambient_domain.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python3 +"""Static regression contract for shared classic world ambient ownership.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +TEST_PATH = "tools/tests/renderer_classic_world_ambient_domain.py" + + +def read(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def require(haystack: str, needle: str, context: str) -> None: + if needle not in haystack: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject(haystack: str, needle: str, context: str) -> None: + if needle in haystack: + raise AssertionError(f"Unexpected {needle!r} in {context}") + + +def require_order(haystack: str, needles: tuple[str, ...], context: str) -> None: + previous = -1 + for needle in needles: + position = haystack.find(needle, previous + 1) + if position == -1: + raise AssertionError(f"Missing {needle!r} in {context}") + if position <= previous: + raise AssertionError( + f"Expected ordered contracts in {context}: {needles!r}" + ) + previous = position + + +def braced_body(source: str, marker: str, context: str) -> str: + start = source.find(marker) + if start == -1: + raise AssertionError(f"Missing {marker!r} in {context}") + opening_brace = source.find("{", start + len(marker)) + if opening_brace == -1: + raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + + depth = 0 + for index in range(opening_brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"Could not find closing brace for {marker!r} in {context}") + + +def validate_world_material_table() -> None: + header = read("src/renderer/MaterialResourceTable.h") + source = read("src/renderer/MaterialResourceTable.cpp") + for token in ( + "MATERIAL_RESOURCE_TABLE_MAX_WORLD_PASSES", + "firstWorldPass", + "worldPassCount", + "worldDomainReferenced", + "worldPassEligible", + "worldPassFailure", + "worldPassFailureStage", + "worldDomainReferencedRecords", + "worldPassEligibleRecords", + "worldPassFallbackRecords", + "worldPassPoolOverflows", + "R_MaterialResourceTable_WorldPassEligible", + "R_MaterialResourceTable_WorldPasses", + "R_MaterialResourceTable_CopyWorldPassList", + "MaterialResourceWorldPassFailure_Name", + ): + require(header, token, "distinct ordered world material-pass contract") + + for token in ( + "rendererMaterialPass_t\t\t\tworldPasses[", + "rendererMaterialPass_t\t\t\tguiPasses[", + ): + require(source, token, "separate GUI and world pass pools") + reject( + source, + "record.firstWorldPass = record.firstGuiPass", + "distinct world material-pass span", + ) + + prepare = braced_body( + source, + "void R_MaterialResourceTable_PrepareFrame(", + "world packet-derived material-table preparation", + ) + require_order( + prepare, + ( + "drawPacket.passCategory == RENDER_PASS_AMBIENT", + "worldDomainReferenced[drawPacket.materialRecordIndex] = true;", + "R_MaterialResourceTable_AddRecordFromSource(", + ), + "packet-derived world material admission", + ) + + compiler = braced_body( + source, + "static bool R_MaterialResourceTable_CompileOrderedPasses(", + "ordered GUI/world material compiler", + ) + for token in ( + "pass.kind = worldDomain", + "RENDERER_MATERIAL_PASS_SURFACE", + "R_MaterialResourceTable_MapWorldDepthState(", + "pass.programFamily = worldDomain", + "RENDERER_PROGRAM_FIXED", + "record.firstWorldPass = rg_materialResourceTable.worldPassCount;", + "rg_materialResourceTable.worldPasses[record.firstWorldPass]", + ): + require(compiler, token, "sealed world surface-pass compilation") + + depth = braced_body( + source, + "static bool R_MaterialResourceTable_MapWorldDepthState(", + "world depth-state translation", + ) + for token in ( + "depth.testEnabled = true;", + "depth.writeEnabled = ( drawStateBits & GLS_DEPTHMASK ) == 0;", + "RENDERER_COMPARE_LESS_OR_EQUAL", + "RENDERER_COMPARE_EQUAL", + "RENDERER_COMPARE_ALWAYS", + ): + require(depth, token, "depth-enabled world material pass") + + +def validate_transactional_domain() -> None: + header = read("src/renderer/ClassicWorldAmbientDomain.h") + source = read("src/renderer/ClassicWorldAmbientDomain.cpp") + for token in ( + "CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG", + "CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_DRAWABLE", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_IN_WORLD_GUI", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SUBVIEW", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_SPECIAL_SURFACE", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEFORM", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DEPTH_HACK", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_DECAL", + "CLASSIC_WORLD_AMBIENT_SOURCE_SURFACE_FALLBACK_POST_PROCESS", + "CLASSIC_WORLD_AMBIENT_FAILURE_INVALID_AMBIENT_PASS", + "CLASSIC_WORLD_AMBIENT_FAILURE_FORBIDDEN_PASS", + "CLASSIC_WORLD_AMBIENT_FAILURE_DEPTH_PREREQUISITE_MISSING", + "CLASSIC_WORLD_AMBIENT_FAILURE_BACKEND_COVERAGE_MISMATCH", + "classicWorldAmbientDomainBackendCoverage_t", + "R_ClassicWorldAmbientDomain_RecordOwned", + "R_ClassicWorldAmbientDomain_RecordBackendFallback", + ): + require(header, token, "whole-view world ambient domain API") + + prepare = braced_body( + source, + "void R_ClassicWorldAmbientDomain_PrepareFrame(", + "whole-view world ambient domain preparation", + ) + require_order( + prepare, + ( + "R_ClassicWorldAmbientDomain_ResetFrame();", + "R_ClassicWorldAmbientDomain_PrepareView(", + "domain.stats.frameValid", + ), + "per-view transactional world preparation", + ) + + prepare_view = braced_body( + source, + "static bool R_ClassicWorldAmbientDomain_PrepareView(", + "per-view world ambient transaction", + ) + require_order( + prepare_view, + ( + "CLASSIC_WORLD_AMBIENT_FAILURE_FORBIDDEN_PASS", + "const int drawCheckpoint = domain.drawCount;", + "const int passCheckpoint = domain.passCount;", + "draw.phase = phase;", + "ValidateDepthPrerequisite(", + "R_MaterialResourceTable_CopyWorldPassList(", + "RendererContracts_EvaluateMaterialPassList(", + "pass.kind != RENDERER_MATERIAL_PASS_SURFACE", + "!pass.depth.testEnabled", + "R_MaterialResourceTable_ResolveTextureResource(", + "view.firstDraw = drawCheckpoint;", + "domain.drawCount += view.drawCount;", + "domain.passCount += view.evaluatedPassCount;", + "view.ready = true;", + ), + "validate and stage the complete view before atomic publication", + ) + + failure = braced_body( + source, + "static bool FailView(", + "transactional world-view rollback", + ) + for token in ( + "view.ready = false;", + "view.firstDraw = -1;", + "view.drawCount = 0;", + "view.firstEvaluatedPass = -1;", + "view.evaluatedPassCount = 0;", + "domain.stats.fallbackViews++;", + ): + require(failure, token, "failed view publishes no partial span") + + phase = braced_body( + source, + "static classicWorldAmbientPhase_t PhaseForSurface(", + "classic fog split classification", + ) + require_order( + phase, + ( + "material->GetSort() >= SS_MEDIUM", + "CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG", + "CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG", + ), + "pre-fog/post-fog source ordering", + ) + + depth = braced_body( + source, + "static bool ValidateDepthPrerequisite(", + "opaque/perforated depth prerequisite", + ) + for token in ( + "drawSurf->material->Coverage() == MC_TRANSLUCENT", + "packet.passCategory != RENDER_PASS_DEPTH", + "packet.materialRecordIndex != ambientPacket.materialRecordIndex", + "packet.geometryRecordIndex != ambientPacket.geometryRecordIndex", + "packet.instanceRecordIndex != ambientPacket.instanceRecordIndex", + "depthPacketIndex = packetIndex;", + ): + require(depth, token, "matching depth-packet prerequisite") + + blockers = braced_body( + source, + "static bool ForbiddenPassCategory(", + "explicit non-owned pass blockers", + ) + for token in ( + "RENDER_PASS_STENCIL_SHADOW", + "RENDER_PASS_SHADOW_MAP", + "RENDER_PASS_ARB2_INTERACTION", + "RENDER_PASS_LIGHT_GRID", + "RENDER_PASS_FOG_BLEND", + "RENDER_PASS_AUTHORED_POST", + "RENDER_PASS_SPECIAL_EFFECTS", + "RENDER_PASS_GUI", + ): + require(blockers, token, "whole-view non-owned domain blocker") + require( + source, + "RendererClassicWorldAmbientDomain_RunSelfTest", + "whole-view rollback and coverage self-test", + ) + + +def validate_gl_consumer() -> None: + backend = read("src/renderer/tr_backend.cpp") + draw = read("src/renderer/draw_common.cpp") + handoff = braced_body( + backend, + "void RB_ExecuteBackEndCommands(", + "OpenGL command handoff", + ) + require_order( + handoff, + ( + "R_ClassicWorldAmbientDomain_ResetFrame();", + "cmds->commandId == RC_NOP && !cmds->next", + "R_MaterialResourceTable_PrepareFrame(", + "R_ClassicWorldAmbientDomain_PrepareFrame(", + ), + "OpenGL world-domain frame preparation", + ) + + preflight = braced_body( + draw, + "static bool RB_SharedWorldAmbientGLPreflight(", + "complete OpenGL world ambient preflight", + ) + require_order( + preflight, + ( + "R_ClassicWorldAmbientDomain_ViewDraw(", + "RB_EnsurePackedClassicDrawCaches(", + "R_ClassicWorldAmbientDomain_DrawPass(", + "R_ClassicWorldAmbientDomain_DrawPassTexture(", + "RB_SharedWorldAmbientGLBuildState(", + "RB_SharedGuiGLTextureBindingValid(", + "prepared.ready = true;", + ), + "all-view OpenGL preflight before commit", + ) + reject(preflight, "RB_DrawElementsWithCounters(", "fallible OpenGL preflight") + + prepare = braced_body( + draw, + "static bool RB_PrepareSharedWorldAmbientView(", + "OpenGL whole-view shared ownership handoff", + ) + require_order( + prepare, + ( + "R_ClassicWorldAmbientDomain_FindView(", + "RB_SharedWorldAmbientGLPreflight(", + "return true;", + ), + "OpenGL preflight completes before ownership", + ) + + consumer = braced_body( + draw, + "static void RB_DrawSharedWorldAmbientPhase(", + "OpenGL shared world ambient consumer", + ) + require_order( + consumer, + ( + "prepared.committed = true;", + "RB_DrawElementsWithCounters( tri );", + "R_ClassicWorldAmbientDomain_RecordOwned(", + ), + "OpenGL commit, execution, and coverage ordering", + ) + for body, context in ( + (preflight, "OpenGL sealed world preflight"), + (prepare, "OpenGL sealed world handoff"), + (consumer, "OpenGL sealed world consumer"), + ): + reject(body, "shaderRegisters", context) + reject(body, "GetStage(", context) + + draw_view = braced_body( + draw, + "void\tRB_STD_DrawView(", + "OpenGL classic 3D view phase handoff", + ) + require_order( + draw_view, + ( + "RB_PrepareSharedWorldAmbientView(", + "RB_DrawSharedWorldAmbientPhase(", + "CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG", + "RB_STD_FogAllLights();", + "RB_DrawSharedWorldAmbientPhase(", + "CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG", + ), + "OpenGL pre-fog/fog/post-fog split", + ) + require( + draw_view, + "RB_STD_DrawShaderPasses( drawSurfs, processed, RB_DrawSurfIsPreFogMaterialPass );", + "untouched OpenGL pre-fog classic fallback", + ) + require( + draw_view, + "RB_STD_DrawShaderPasses( drawSurfs, processed, RB_DrawSurfIsPostFogMaterialPass );", + "untouched OpenGL post-fog classic fallback", + ) + + +def validate_vulkan_consumer() -> None: + backend = read("src/renderer/Vulkan/vk_Backend.cpp") + executor = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + handoff = braced_body( + backend, + "void RB_ExecuteBackEndCommands(", + "Vulkan command handoff", + ) + require_order( + handoff, + ( + "R_ClassicWorldAmbientDomain_ResetFrame();", + "R_MaterialResourceTable_PrepareFrame(", + "R_ClassicWorldAmbientDomain_PrepareFrame(", + ), + "Vulkan world-domain frame preparation", + ) + + preflight = braced_body( + executor, + "static bool VK_ClassicWorldAmbient_Preflight(", + "complete Vulkan world ambient preflight", + ) + require_order( + preflight, + ( + "R_ClassicWorldAmbientDomain_FindView(", + "R_ClassicWorldAmbientDomain_ViewDraw(", + "R_ClassicWorldAmbientDomain_DrawPass(", + "R_ClassicWorldAmbientDomain_DrawPassTexture(", + "VK_ClassicWorldAmbient_MapState(", + "R_MaterialResourceTable_ResolveTextureResource(", + "VK_GuiExecutor_GetImageDescriptor(", + "VK_GuiExecutor_GetPipelineStrict(", + "VK_Exec_SharedGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometryOffsets(", + "VK_Exec_SharedGeometryCommit();", + "prepared.ready = true;", + ), + "all-view Vulkan preflight before commit", + ) + require( + preflight, + "VK_Exec_SharedGeometryRestore();", + "Vulkan failed-preflight geometry rollback", + ) + for command in ("vkCmdDraw(", "vkCmdDrawIndexed(", "vkCmdClearAttachments("): + reject(preflight, command, "Vulkan attachment write during world preflight") + + consumer = braced_body( + executor, + "static void VK_ClassicWorldAmbient_DrawPhase(", + "Vulkan shared world ambient consumer", + ) + require_order( + consumer, + ( + "prepared.committed = true;", + "vkCmdDrawIndexed(", + "R_ClassicWorldAmbientDomain_RecordOwned(", + ), + "Vulkan commit, execution, and coverage ordering", + ) + for body, context in ( + (preflight, "Vulkan sealed world preflight"), + (consumer, "Vulkan sealed world consumer"), + ): + reject(body, "shaderRegisters", context) + reject(body, "GetStage(", context) + + draw_view = braced_body( + executor, + "void VK_GuiExecutor_Draw3DView(", + "Vulkan classic 3D view phase handoff", + ) + require_order( + draw_view, + ( + "VK_ClassicWorldAmbient_Preflight( viewDef )", + "for ( int pass = 0; pass < 3; pass++ )", + "if ( pass == 1 )", + "VK_Fog_DrawAllLights( viewDef );", + "if ( sharedWorldAmbientOwned && pass < 2 )", + "VK_ClassicWorldAmbient_DrawPhase(", + "pass == 0", + "CLASSIC_WORLD_AMBIENT_PHASE_PRE_FOG", + "CLASSIC_WORLD_AMBIENT_PHASE_POST_FOG", + "continue;", + ), + "Vulkan preflight/pre-fog/fog/post-fog split", + ) + require( + draw_view, + "VK_Exec_DrawAmbientStages( viewDef, drawSurf, tri, mvp, true );", + "untouched Vulkan classic ambient fallback", + ) + + +def validate_control_and_evidence() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + bootstrap = read("src/renderer/RendererBootstrap.cpp") + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + baseline = read("tools/validation/stock_asset_baseline.py") + validation_matrix = read("tools/tests/renderer_validation_matrix.py") + require( + init, + 'idCVar r_rendererSharedWorldAmbient( "r_rendererSharedWorldAmbient", "0"', + "default-off shared world ambient control", + ) + require( + bootstrap, + '{ "r_rendererSharedWorldAmbient", &r_rendererSharedWorldAmbient, 0 }', + "renderer bootstrap safety expectation", + ) + require(bootstrap, "sharedWorldAmbient=%d", "renderer bootstrap safety report") + require( + benchmark, + 'append_set(args, "r_rendererSharedWorldAmbient", "0")', + "benchmark launch isolation", + ) + require( + benchmark, + '"r_rendererSharedWorldAmbient 0"', + "benchmark capture-script isolation", + ) + require(benchmark, '"sp-mv2-ambient":', "stock world-ambient gameplay case") + world_ambient_profile = braced_body( + benchmark, + '"world-ambient":', + "world-ambient gameplay profile", + ) + for token, context in ( + ('("ui_showGun", "0")', "view-model isolation"), + ('("g_showHud", "0")', "root-HUD isolation"), + ('("r_multiSamples", "0")', "direct swapchain ownership isolation"), + ('("g_renderFastNoPost", "1")', "fast no-post selection"), + ('("g_renderFastNoPostDirect", "1")', "direct no-post selection"), + ('("r_postAA", "0")', "post-AA isolation"), + ('("r_singleLight", "2147483647")', "interaction-light isolation"), + ('("r_skipSubviews", "1")', "subview isolation"), + ('("r_useLightGrid", "0")', "light-grid isolation"), + ('("r_skipPlayerVisibilityEffects", "1")', "player-overlay isolation"), + ('("r_portalsDistanceCull", "0")', "portal-fade isolation"), + ('("r_forceAmbient", "0")', "forced-ambient isolation"), + ('("r_celShading", "0")', "cel-shading isolation"), + ('("r_celShadingWorld", "0")', "world cel-shading isolation"), + ('("r_showOverDraw", "0")', "overdraw diagnostic isolation"), + ('("r_singleTriangle", "0")', "single-triangle diagnostic isolation"), + ('("r_skipAmbient", "0")', "ambient ownership retention"), + ('("r_skipNewAmbient", "0")', "new-ambient ownership retention"), + ('("r_skipDeforms", "0")', "deform rollback retention"), + ('("r_skipRender", "0")', "normal rendering retention"), + ('"noclip"', "fixed world-ambient camera lock"), + ('"setviewpos 0 0 256 80 0 0"', "fixed world-ambient camera pose"), + ): + require(world_ambient_profile, token, context) + for token in ( + 'g_stopTime', + 'r_skipGuiShaders', + 'r_skipPostProcess', + ): + reject(world_ambient_profile, token, "world-ambient gameplay profile") + require( + baseline, + 'add_set(args, "r_rendererSharedWorldAmbient", 0)', + "stock baseline launch isolation", + ) + require( + baseline, + '"r_rendererSharedWorldAmbient 0"', + "stock baseline capture-script isolation", + ) + require( + init, + "RendererClassicWorldAmbientDomain_RunSelfTest()", + "registered world ambient domain self-test", + ) + require( + init, + 'cmdSystem->AddCommand( "rendererClassicWorldAmbientDomainSelfTest"', + "world ambient self-test command", + ) + require( + init, + "R_ClassicWorldAmbientDomain_Stats()", + "gfx-info world ambient diagnostics", + ) + require( + init, + "Renderer shared world ambient: requested=%d prepared=%d valid=%d", + "world ambient gfx-info marker", + ) + require( + validation_matrix, + '"+rendererClassicWorldAmbientDomainSelfTest"', + "runtime self-test validation matrix", + ) + require( + validation_matrix, + '"RendererClassicWorldAmbientDomain self-test passed"', + "runtime self-test success marker", + ) + + +def validate_ci_registration() -> None: + validator = read("tools/validation/openq4_validate.py") + commit = read(".github/workflows/commit-validation.yml") + push = read(".github/workflows/push-verification.yml") + if validator.count("renderer_classic_world_ambient_domain.py") != 1: + raise AssertionError("Local validation must register this test exactly once") + for workflow, context in ( + (commit, "commit validation workflow"), + (push, "push verification workflow"), + ): + if workflow.count(TEST_PATH) != 2: + raise AssertionError(f"{context} must compile and directly run {TEST_PATH}") + require(workflow, f"python {TEST_PATH}", context) + + +def main() -> None: + validate_world_material_table() + validate_transactional_domain() + validate_gl_consumer() + validate_vulkan_consumer() + validate_control_and_evidence() + validate_ci_registration() + print("renderer_classic_world_ambient_domain: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/renderer_gameplay_benchmark.py b/tools/tests/renderer_gameplay_benchmark.py index a63e1634..40992333 100644 --- a/tools/tests/renderer_gameplay_benchmark.py +++ b/tools/tests/renderer_gameplay_benchmark.py @@ -18,22 +18,137 @@ import os import platform import re +import shlex +import stat import struct import subprocess import sys import time from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, Iterable + +VALIDATION_DIR = Path(__file__).resolve().parents[1] / "validation" +if str(VALIDATION_DIR) not in sys.path: + sys.path.insert(0, str(VALIDATION_DIR)) + +from renderer_budget_contract import ( # noqa: E402 + DEFAULT_CONTRACT_PATH, + MARKER_PREFIX as TIMING_EVIDENCE_MARKER, + evaluate_timing_evidence, + load_contract, + verify_contract_binding, + verify_recorded_evidence, +) +from generate_pbr_fixture import ( # noqa: E402 + BENCHMARK_CASE as PBR_FIXTURE_BENCHMARK_CASE, + DEFAULT_TEXTURE_SIZE as PBR_FIXTURE_TEXTURE_SIZE, + MATERIAL_NAME as PBR_FIXTURE_MATERIAL_NAME, + STOCK_MAP as PBR_FIXTURE_STOCK_MAP, + verify_fixture as verify_procedural_pbr_fixture, +) SAFE_TIERS = ("auto", "legacy", "gl33", "gl41", "gl43", "gl45", "gl46") +GL_TIER_RUNTIME_NAMES = { + "legacy": "LegacyGL2Compat", + "gl33": "ModernGL33", + "gl41": "ModernGL41", + "gl43": "GpuDrivenGL43", + "gl45": "LowOverheadGL45", + "gl46": "TopGL46", +} PRESENTATION_MAXFPS = ("0", "120", "240") PRESENTATION_SWAP_INTERVALS = ("0", "1") DISPLAY_MODES = ("windowed", "fullscreen") POSTINIT_CONNECT_WAIT_FRAMES = 30 POSTINIT_RECONNECT_WAIT_FRAMES = 30 -POSTINIT_SERVER_GRACE_MSEC = 90000 +MP_SERVER_CLIENT_GRACE_MSEC = 90000 +REPORT_SCHEMA_VERSION = 4 +GIT_PROVENANCE_POLICY = "current-openq4-head-and-dirty-state-v1" +BUDGET_DISPLAY_CONTRACT_ID = "bordered-window-1280x720-v1" +BUDGET_WIDTH = 1280 +BUDGET_HEIGHT = 720 +PBR_ACCEPTANCE_COMMON_CVARS = ( + ("r_materialoverride", PBR_FIXTURE_MATERIAL_NAME), + ("r_pbrmaterials", "1"), + ("r_renderermodernquality", "1"), +) +PBR_ACCEPTANCE_GL_CVARS = ( + *PBR_ACCEPTANCE_COMMON_CVARS, + ("r_renderermodernvisible", "1"), + ("r_rendererforwardplus", "1"), +) +PBR_PROTECTED_CVARS = frozenset(name for name, _ in PBR_ACCEPTANCE_GL_CVARS) +PBR_BOOL_CVARS = PBR_PROTECTED_CVARS - {"r_materialoverride"} +PBR_CVAR_MUTATOR_COMMANDS = frozenset( + { + "cycle", + "reset", + "set", + "seta", + "sets", + "sett", + "setu", + "toggle", + "togglecvar", + } +) +PBR_INDIRECT_COMMANDS = frozenset( + { + "cvar_restart", + "connect", + "disconnect", + "exec", + "execmachinespec", + "devmap", + "map", + "maprestart", + "nextmap", + "reconnect", + "reloadmap", + "reloadengine", + "setmachinespec", + "spawnserver", + "vstr", + } +) +PBR_EVIDENCE_OUTPUT_COMMANDS = frozenset( + {"echo", "rendererbenchmarkcapture", "say", "sayteam"} +) +PBR_EVIDENCE_MARKERS = ( + "PBR material resources:", + "Modern GL executor:", + "Modern forward+:", + "Modern visible frame:", + "Vulkan: native packed PBR direct interactions active (", +) +BENCHMARK_EVIDENCE_MARKERS = ( + *PBR_EVIDENCE_MARKERS, + TIMING_EVIDENCE_MARKER, + "rendererBenchmark capture(", + "Renderer benchmark:", + "Frame pacing", + "Selected renderer tier:", + "MODE:", + "Map:", +) +POST_MAP_CVARS_BEGIN = "// OPENQ4_BENCHMARK_POST_MAP_CVARS_V1_BEGIN" +POST_MAP_CVARS_END = "// OPENQ4_BENCHMARK_POST_MAP_CVARS_V1_END" +EXEC_COMMANDS_BEGIN = "// OPENQ4_BENCHMARK_EXEC_COMMANDS_V1_BEGIN" +EXEC_COMMANDS_END = "// OPENQ4_BENCHMARK_EXEC_COMMANDS_V1_END" +# Each role already owns an isolated save path. Keep the engine-side log name +# deliberately short so the complete fs_savepath/baseoq4/logs path remains +# below the legacy Windows MAX_PATH boundary even for descriptive MP case IDs. +ROLE_LOG_NAME = "openq4_gameplay.log" +RUNTIME_DISPLAY_MODE_PATTERN = re.compile( + r"^MODE:\s*([^,\r\n]+),\s*(\d+)\s+x\s+(\d+)\s+" + r"(windowed|borderless|fullscreen)\b", + re.IGNORECASE | re.MULTILINE, +) +ENGINE_MAP_PATTERN = re.compile( + r"^Map:\s*([A-Za-z0-9_./\\-]+)\s*$", re.IGNORECASE | re.MULTILINE +) REQUIRED_SCENES: dict[str, dict[str, Any]] = { "sp-storage1": { @@ -131,6 +246,105 @@ "map": "game/mcc_2", "purpose": "scripted campaign transition chain from MCC 2 through Storage 1 first/second state handling into Tram 1", "path": "triggered-campaign-transition", + "budgetMap": "game/tram1", + }, +} + +LOAD_REGRESSION_SCENES: dict[str, dict[str, Any]] = { + "mp-q4dm9-listen": { + "mode": "MP", + "map": "mp/q4dm9", + "purpose": "listen-server load-time and Vulkan shadow-fallback regression coverage", + "path": "spawn-static", + }, +} + +WORLD_AMBIENT_SCENES: dict[str, dict[str, Any]] = { + "sp-mv2-ambient": { + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock fixed-function world-ambient ownership and whole-view rollback evidence", + "path": "spawn-static", + }, +} + +INTERACTION_SCENES: dict[str, dict[str, Any]] = { + "sp-mv2-interaction": { + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock caster/receiver crates, projected light, and side point light for fixed-classic interaction/shadow ownership evidence", + "path": "spawn-static", + }, +} + +FOG_BLEND_SCENES: dict[str, dict[str, Any]] = { + "sp-mv2-fog-blend": { + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock fog and blend lights for atomic fixed-classic ownership evidence", + "path": "spawn-static", + }, +} + +DEFORM_SCENES: dict[str, dict[str, Any]] = { + "sp-mv2-deform": { + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock material-move deformation ownership and published-geometry parity evidence", + "path": "spawn-static", + }, +} + +MILESTONE_D_SCENES: dict[str, dict[str, Any]] = { + "sp-milestone-d-nested-dynamic": { + "mode": "SP", + "map": "maps/tools/milestone_d_nested_dynamic", + "purpose": "temporary stock-derived mirror fixture with an authored post/video tail visible only in the captured child view", + "path": "spawn-static", + }, +} + +# Stock-map qualification candidates complement the tightly controlled +# tools-map case. The ordinary shadow-regression scenes do not by themselves +# establish a fixed camera or feature-bearing interaction view, so every target +# remains fail-closed until its exact map records prove the advertised class or +# caster feature. Release evidence must retain the final camera pose together +# with matching classic/shadows-off references. +INTERACTION_SHADOW_SCENES: dict[str, dict[str, Any]] = { + "shadow-projected-airdefense2": { + **SHADOW_SCENES["shadow-projected-airdefense2"], + "purpose": "projected-map qualification candidate requiring a retained authored-projector camera", + "interactionShadowTarget": "projected", + }, + "shadow-point-airdefense2": { + **SHADOW_SCENES["shadow-projected-airdefense2"], + "purpose": "ordinary stock spawn candidate for point-cube interaction ownership", + "interactionShadowTarget": "point", + }, + "shadow-csm-airdefense1": { + **SHADOW_SCENES["shadow-csm-airdefense1"], + "purpose": "multi-cascade projected/parallel qualification candidate requiring a retained camera", + "interactionShadowTarget": "csm", + }, + "shadow-character-airdefense2": { + **SHADOW_SCENES["shadow-character-airdefense2"], + "purpose": "dynamic mapped-caster qualification candidate; the gate does not assume character eligibility", + "interactionShadowTarget": "dynamic", + }, + "shadow-cutout-storage2": { + **SHADOW_SCENES["shadow-cutout-storage2"], + "purpose": "perforated mapped-caster qualification candidate requiring a retained cutout view", + "interactionShadowTarget": "perforated", + }, + "shadow-hybrid-storage2": { + **SHADOW_SCENES["shadow-point-storage2"], + "purpose": "same-light mapped/stencil-supplement qualification candidate requiring a retained hybrid view", + "interactionShadowTarget": "hybrid", + }, + "shadow-translucent-medlabs": { + **SHADOW_SCENES["shadow-translucent-medlabs"], + "purpose": "translucent-moment atomic-fallback qualification candidate", + "interactionShadowTarget": "fallback", }, } @@ -150,10 +364,28 @@ "openq4_assertMapState game/tram1", ) -ALL_SCENES = {**REQUIRED_SCENES, **SHADOW_SCENES, **CAMPAIGN_TRANSITION_SCENES} +FULL_BUDGET_SCENES = { + **REQUIRED_SCENES, + **SHADOW_SCENES, + **CAMPAIGN_TRANSITION_SCENES, +} +ALL_SCENES = { + **FULL_BUDGET_SCENES, + **WORLD_AMBIENT_SCENES, + **INTERACTION_SCENES, + **FOG_BLEND_SCENES, + **DEFORM_SCENES, + **MILESTONE_D_SCENES, + **INTERACTION_SHADOW_SCENES, + **LOAD_REGRESSION_SCENES, +} SHADOW_PRESETS: dict[str, dict[str, str]] = { "default": {}, + "unshadowed": { + "r_shadows": "0", + "r_useShadowMap": "0", + }, "stencil": { "r_shadows": "1", "r_useShadowMap": "0", @@ -165,6 +397,14 @@ "r_shadowMapHashedAlpha": "1", "r_shadowMapTranslucentMoments": "0", }, + "mixed": { + "r_shadows": "1", + "r_useShadowMap": "1", + "r_shadowMapCSM": "0", + "r_shadowMapPointLights": "0", + "r_shadowMapHashedAlpha": "1", + "r_shadowMapTranslucentMoments": "0", + }, "csm": { "r_shadows": "1", "r_useShadowMap": "1", @@ -179,6 +419,18 @@ "r_shadowMapHashedAlpha": "1", "r_shadowMapTranslucentMoments": "1", }, + "map-budget-fallback": { + "r_shadows": "1", + "r_useShadowMap": "1", + "r_shadowMapCSM": "0", + "r_shadowMapHashedAlpha": "1", + "r_shadowMapTranslucentMoments": "0", + # The controlled scene contains multiple ownership passes. With no + # resident reuse and one admitted update, both backends must reject + # the whole shared view before its first interaction write. + "r_shadowMapStaticCache": "0", + "r_shadowMapMaxUpdatesPerView": "1", + }, } SHADOW_DEBUG_PRESET_MODES = (1, 2, 3, 4, 5, 6, 7, 12, 13, 14) @@ -228,6 +480,313 @@ "shadows": ("default",), "execCommands": CAMPAIGN_MCC2_TO_TRAM1_COMMANDS, }, + "world-ambient": { + "cases": tuple(WORLD_AMBIENT_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ("default",), + "launchCvars": ( + ("ui_showGun", "0"), + ("g_showHud", "0"), + ("r_multiSamples", "0"), + ), + # The tools-map spawn looks into an all-caulk horizon. After normal + # spawn settles, lock the player in noclip and place the engine view + # over the static stock floor for a repeatable authored-surface capture. + "execCommands": ( + "noclip", + "setviewpos 0 0 256 80 0 0", + ), + "cvars": ( + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("r_postAA", "0"), + ("r_singleLight", "2147483647"), + ("r_skipSubviews", "1"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_forceAmbient", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipAmbient", "0"), + ("r_skipNewAmbient", "0"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ), + }, + "deform": { + "cases": tuple(DEFORM_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ("default",), + "launchCvars": ( + # shaderDemos/move evaluates a time-based deform expression. Run + # exactly one game tic per rendered frame so separate classic and + # shared captures reach the same deform value on every backend. + ("com_fixedTic", "1"), + ("g_stopTime", "1"), + ("in_mouse", "0"), + ("ui_showGun", "0"), + ("g_showHud", "0"), + ("r_multiSamples", "0"), + ), + "execCommands": ( + "g_stopTime 1", + "noclip", + "setviewpos 0 0 256 80 0 0", + ), + # Keep the stock material override in the ordinary profile-cvar stream. + # User --set-cvar entries are appended afterwards and can still replace + # it for targeted deform-kind or atomic-fallback captures. + "cvars": ( + # Advance only the common post-map settle interval, then the first + # exec command freezes that exact tic for sampling and screenshot. + ("g_stopTime", "0"), + ("r_rendererSharedWorldAmbient", "1"), + ("r_rendererSharedDeform", "1"), + ("r_materialOverride", "shaderDemos/move"), + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("r_postAA", "0"), + ("r_singleLight", "2147483647"), + ("r_skipSubviews", "1"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_forceAmbient", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipAmbient", "0"), + ("r_skipNewAmbient", "0"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ), + }, + "milestone-d-nested-dynamic": { + "cases": tuple(MILESTONE_D_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ("default",), + "launchCvars": ( + ("com_fixedTic", "1"), + ("g_stopTime", "1"), + ("in_mouse", "0"), + ("ui_showGun", "0"), + ("g_showHud", "0"), + ("r_multiSamples", "0"), + ), + "execCommands": ( + "g_stopTime 1", + "noclip", + "setviewpos 0 -384 96 0 90 0", + ), + "cvars": ( + # Let map startup advance through the ordinary settle interval, + # then freeze the exact child-view video clock before sampling. + ("g_stopTime", "0"), + ("r_rendererSharedSubview", "1"), + ("r_rendererSharedCinematicPost", "1"), + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("g_renderCasUpscale", "0"), + ("r_postAA", "0"), + ("r_screenFraction", "100"), + ("r_bloom", "0"), + ("r_motionBlur", "0"), + ("r_ssao", "0"), + ("r_hdrToneMap", "0"), + ("r_hdrDebugView", "0"), + ("r_skipSubviews", "0"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_forceAmbient", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipAmbient", "0"), + ("r_skipNewAmbient", "0"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ("r_skipRenderContext", "0"), + ("r_skipDynamicTextures", "0"), + ("r_skipCopyTexture", "0"), + ("r_skipPostProcess", "0"), + ), + }, + "interaction": { + "cases": tuple(INTERACTION_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ( + "unshadowed", + "stencil", + "mapped", + "mixed", + "map-budget-fallback", + ), + "launchCvars": ( + ("ui_showGun", "0"), + ("g_showHud", "0"), + ("r_multiSamples", "0"), + ), + "execCommands": ( + "noclip", + "setviewpos 0 -192 96 20 90 0", + "testModel models/mapobjects/strogg/crates/crate1_small.lwo", + 'spawn func_static model models/mapobjects/strogg/crates/crate1_medium.lwo origin "-90 -70 -5.7"', + 'testPointLight 300 origin "96 -128 0"', + "testLight", + ), + "cvars": ( + ("r_rendererSharedWorldInteraction", "1"), + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("g_renderCasUpscale", "0"), + ("r_postAA", "0"), + ("r_screenFraction", "100"), + ("r_bloom", "0"), + ("r_motionBlur", "0"), + ("r_ssao", "0"), + ("r_hdrToneMap", "0"), + ("r_hdrDebugView", "0"), + ("r_skipSubviews", "1"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_forceAmbient", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipAmbient", "1"), + ("r_skipNewAmbient", "1"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ), + }, + "fog-blend": { + "cases": tuple(FOG_BLEND_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ("default",), + "launchCvars": ( + ("ui_showGun", "0"), + ("g_showHud", "0"), + ("r_multiSamples", "0"), + ), + # Use only stock map/model/light declarations. The two test lights + # deliberately exercise both halves of the atomic fog/blend domain; + # lights/fog_generic is a fogLight and lights/fog_ambient is a + # deterministic blendLight in the shipped Quake 4 declarations. + # Do not use lights/stream_fog here: its time-driven flicker tables + # make independent shared-off/shared-on image captures non-comparable. + "execCommands": ( + "noclip", + "setviewpos 0 -192 96 20 90 0", + "testModel models/mapobjects/strogg/crates/crate1_small.lwo", + 'testPointLight 256 texture lights/fog_generic origin "0 96 48" _color "0.18 0.26 0.34" shaderParm3 "384" noShadows "1"', + 'testPointLight 256 texture lights/fog_ambient origin "96 -96 48" _color "0.28 0.10 0.05" shaderParm3 "0.65" noShadows "1"', + ), + "cvars": ( + ("r_rendererSharedWorldFogBlend", "1"), + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("g_renderCasUpscale", "0"), + ("r_postAA", "0"), + ("r_screenFraction", "100"), + ("r_bloom", "0"), + ("r_motionBlur", "0"), + ("r_ssao", "0"), + ("r_hdrToneMap", "0"), + ("r_hdrDebugView", "0"), + ("r_skipSubviews", "1"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_forceAmbient", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipAmbient", "1"), + ("r_skipNewAmbient", "1"), + ("r_skipFogLights", "0"), + ("r_skipBlendLights", "0"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ), + }, + "interaction-shadow-stock": { + "cases": tuple(INTERACTION_SHADOW_SCENES.keys()), + "tiers": ("auto",), + "maxfps": ("240",), + "swap": ("0",), + "display": ("windowed",), + "shadows": ("unshadowed", "mapped", "csm", "translucent"), + "caseShadows": { + "shadow-projected-airdefense2": ("unshadowed", "mapped"), + "shadow-point-airdefense2": ("unshadowed", "mapped"), + "shadow-csm-airdefense1": ("unshadowed", "csm"), + "shadow-character-airdefense2": ("unshadowed", "csm"), + "shadow-cutout-storage2": ("unshadowed", "csm"), + "shadow-hybrid-storage2": ("unshadowed", "mapped"), + "shadow-translucent-medlabs": ("translucent",), + }, + "launchCvars": ( + ("g_stopTime", "1"), + ("ui_showGun", "0"), + ("r_multiSamples", "0"), + ), + # Start deterministic at tic zero, then advance the real SP scene + # through the normal settle interval before freezing it again for + # sampling. Keeping tic zero frozen only captures dark/empty spawn + # state; leaving the viewmodel visible turns the aggregate interaction + # packet into an unsupported weapon-depth-hack pass. + "execCommands": ( + "g_stopTime 1", + ), + "cvars": ( + ("r_rendererSharedWorldInteraction", "1"), + ("g_stopTime", "0"), + ("g_renderFastNoPost", "1"), + ("g_renderFastNoPostDirect", "1"), + ("g_renderCasUpscale", "0"), + ("r_postAA", "0"), + ("r_screenFraction", "100"), + ("r_bloom", "0"), + ("r_motionBlur", "0"), + ("r_ssao", "0"), + ("r_hdrToneMap", "0"), + ("r_hdrDebugView", "0"), + ("r_skipSubviews", "1"), + ("r_useLightGrid", "0"), + ("r_skipPlayerVisibilityEffects", "1"), + ("r_portalsDistanceCull", "0"), + ("r_celShading", "0"), + ("r_celShadingWorld", "0"), + ("r_showOverDraw", "0"), + ("r_singleTriangle", "0"), + ("r_skipDeforms", "0"), + ("r_skipRender", "0"), + ), + }, "tiers": { "cases": ("sp-airdefense1",), "tiers": SAFE_TIERS, @@ -324,7 +883,7 @@ ), }, "full": { - "cases": tuple(ALL_SCENES.keys()), + "cases": tuple(FULL_BUDGET_SCENES.keys()), "tiers": SAFE_TIERS, "maxfps": PRESENTATION_MAXFPS, "swap": PRESENTATION_SWAP_INTERVALS, @@ -336,6 +895,14 @@ WARNING_PATTERNS = { "snPrintfOverflow": re.compile(r"idStr::snPrintf:\s*overflow", re.IGNORECASE), "idStrWarning": re.compile(r"WARNING:\s+idStr", re.IGNORECASE), + "binaryImageCacheWrite": re.compile( + r"WARNING:\s*(?:\^[0-9]\s*)*idBinaryImage:\s+Could not open (?:file|generated cache)\b", + re.IGNORECASE, + ), + "expandedLoadscreenPublish": re.compile( + r"WARNING:\s*(?:\^[0-9]\s*)*Could not publish expanded loading background\b", + re.IGNORECASE, + ), "shaderCompile": re.compile(r"(shader compile|program link).*(failed|error)|failed to compile", re.IGNORECASE), "glError": re.compile( r"\bGL_(?:INVALID_[A-Z_]+|OUT_OF_MEMORY|STACK_(?:OVERFLOW|UNDERFLOW)|CONTEXT_LOST)\b" @@ -382,6 +949,7 @@ class RunSpec: case_id: str mode: str map_name: str + budget_map_name: str purpose: str path_name: str tier: str @@ -390,20 +958,31 @@ class RunSpec: display_mode: str shadow_preset: str renderer: str + render_api: str + interaction_expectation: str = "none" + interaction_shadow_expectation: str = "none" + fog_blend_expectation: str = "none" @property def fullscreen(self) -> bool: return self.display_mode == "fullscreen" + @property + def expected_backend(self) -> str: + return "vulkan" if self.render_api == "vk" else "opengl" + @property def id(self) -> str: + return self.id_for_shadow_preset(self.shadow_preset) + + def id_for_shadow_preset(self, shadow_preset: str) -> str: parts = [ self.case_id, self.tier, f"fps{self.maxfps}", f"vsync{self.swap_interval}", self.display_mode, - self.shadow_preset, + shadow_preset, ] parts.append(self.renderer) return sanitize_case_id("_".join(parts)) @@ -424,19 +1003,18 @@ def host_arch() -> str: return machine -def find_client_executable(root: Path) -> Path: - install_dir = root / ".install" +def find_client_executable(runtime_dir: Path) -> Path: suffix = ".exe" if os.name == "nt" else "" candidate_prefixes = ("openQ4-client", "openQ4-client") for prefix in candidate_prefixes: - preferred = install_dir / f"{prefix}_{host_arch()}{suffix}" + preferred = runtime_dir / f"{prefix}_{host_arch()}{suffix}" if preferred.exists(): return preferred candidates: list[Path] = [] seen: set[Path] = set() for prefix in candidate_prefixes: - for candidate in sorted(install_dir.glob(f"{prefix}_*{suffix}")): + for candidate in sorted(runtime_dir.glob(f"{prefix}_*{suffix}")): if candidate not in seen: candidates.append(candidate) seen.add(candidate) @@ -447,7 +1025,153 @@ def find_client_executable(root: Path) -> Path: for candidate in candidates: if candidate.is_file(): return candidate - raise FileNotFoundError(f"openQ4 client executable not found under {install_dir}") + raise FileNotFoundError(f"openQ4 client executable not found under {runtime_dir}") + + +def is_link_or_junction(path: Path) -> bool: + is_junction = getattr(path, "is_junction", None) + if path.is_symlink() or bool(is_junction and is_junction()): + return True + try: + attributes = getattr(path.lstat(), "st_file_attributes", 0) + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + except OSError: + return False + + +def validate_runtime_dir(runtime_dir: Path, root: Path) -> Path: + """Require canonical .install or a named ordinary package below .tmp.""" + root_absolute = root.absolute() + candidate = runtime_dir.absolute() + if is_link_or_junction(root_absolute): + raise ValueError(f"source root must not be a link or junction: {root_absolute}") + try: + relative = candidate.relative_to(root_absolute) + except ValueError as exc: + raise ValueError( + f"runtime directory must stay below source root {root_absolute}: {candidate}" + ) from exc + current = root_absolute + for part in relative.parts: + current /= part + if current.exists() and is_link_or_junction(current): + raise ValueError( + f"runtime directory ancestry must not contain a link or junction: {current}" + ) + resolved = candidate.resolve() + if not resolved.is_dir(): + raise FileNotFoundError(f"runtime directory does not exist: {resolved}") + canonical = (root / ".install").resolve() + temporary_parent = (root / ".tmp" / "stock-runtime").resolve() + if resolved != canonical: + try: + isolated_name = resolved.relative_to(temporary_parent) + except ValueError as exc: + raise ValueError( + f"alternate runtime directory must stay below {temporary_parent}: {resolved}" + ) from exc + if not isolated_name.parts: + raise ValueError( + f"alternate runtime directory must be a named child below {temporary_parent}" + ) + return resolved + + +def prepare_output_directory(output_dir: Path) -> None: + if output_dir.exists(): + if not output_dir.is_dir(): + raise ValueError(f"benchmark output path is not a directory: {output_dir}") + if any(output_dir.iterdir()): + raise ValueError( + f"benchmark output directory must be new or empty: {output_dir}" + ) + else: + output_dir.mkdir(parents=True) + + +def file_record(path: Path, root: Path) -> dict[str, Any]: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return { + "path": path.relative_to(root).as_posix(), + "size": path.stat().st_size, + "sha256": digest.hexdigest(), + } + + +def collect_runtime_files(runtime_dir: Path) -> list[dict[str, Any]]: + records: list[dict[str, Any]] = [] + for path in sorted(runtime_dir.rglob("*")): + if is_link_or_junction(path): + raise ValueError(f"runtime package must not contain links or junctions: {path}") + if path.is_file(): + records.append(file_record(path, runtime_dir)) + if not records: + raise ValueError(f"runtime package is empty: {runtime_dir}") + return records + + +def git_state(root: Path) -> dict[str, Any]: + def run(*arguments: str) -> str: + completed = subprocess.run( + ["git", *arguments], cwd=root, capture_output=True, text=True, check=False + ) + return completed.stdout.strip() if completed.returncode == 0 else "" + + return { + "policy": GIT_PROVENANCE_POLICY, + "revision": run("rev-parse", "HEAD"), + "dirty": bool(run("status", "--porcelain")), + } + + +def path_hint(path: Path, root: Path) -> str: + try: + return path.resolve().relative_to(root.resolve()).as_posix() + except ValueError: + return f"external:{path.name}" + + +def attach_result_artifacts(output_dir: Path, results: list[dict[str, Any]]) -> None: + for result in results: + for role in result.get("roles", []): + artifacts: list[dict[str, Any]] = [] + for kind, field in ( + ("engineLog", "log"), + ("processStdout", "stdout"), + ("processStderr", "stderr"), + ("screenshot", "screenshot"), + ("benchmarkConfig", "autoexecCfg"), + ): + value = role.get(field) + if not value: + continue + path = Path(value) + if path.is_file(): + artifacts.append({"kind": kind, **file_record(path, output_dir)}) + role["artifacts"] = artifacts + + +def compare_file_records( + expected: Any, actual: list[dict[str, Any]], description: str +) -> list[str]: + if not isinstance(expected, list): + return [f"recorded {description} inventory is missing or malformed"] + expected_by_path = { + item.get("path"): item + for item in expected + if isinstance(item, dict) and isinstance(item.get("path"), str) + } + actual_by_path = {item["path"]: item for item in actual} + failures: list[str] = [] + if len(expected_by_path) != len(expected) or set(expected_by_path) != set(actual_by_path): + failures.append(f"{description} path inventory differs") + for path in sorted(set(expected_by_path) & set(actual_by_path)): + if expected_by_path[path] != actual_by_path[path]: + failures.append(f"{description} differs: {path}") + return failures def default_basepath() -> str: @@ -456,6 +1180,13 @@ def default_basepath() -> str: return "" +def resolve_basepath(value: str) -> str: + if not value: + return "" + path = Path(value) + return str(path.resolve()) if path.exists() else "" + + def sanitize_case_id(case_id: str) -> str: return re.sub(r"[^A-Za-z0-9_.-]+", "_", case_id) @@ -466,9 +1197,26 @@ def split_csv(value: str, defaults: tuple[str, ...]) -> tuple[str, ...]: return tuple(item.strip() for item in value.split(",") if item.strip()) +def has_command_delimiter_or_control(value: str) -> bool: + return ";" in value or any(not character.isprintable() for character in value) + + +def has_engine_token_expansion_or_comment(value: str) -> bool: + return "$" in value or any(token in value for token in ("/*", "*/", "//")) + + +def contains_benchmark_evidence_text(value: str) -> bool: + folded = value.casefold() + return any(marker.casefold() in folded for marker in BENCHMARK_EVIDENCE_MARKERS) + + def parse_extra_cvars(values: list[str]) -> tuple[tuple[str, str], ...]: parsed: list[tuple[str, str]] = [] for raw in values: + if has_command_delimiter_or_control(raw): + raise ValueError( + "extra cvar contains a command delimiter or control character" + ) item = raw.strip() if not item: continue @@ -485,22 +1233,294 @@ def parse_extra_cvars(values: list[str]) -> tuple[tuple[str, str], ...]: raise ValueError(f"extra cvar name '{name}' is not a valid cvar identifier") if not value: raise ValueError(f"extra cvar '{name}' needs a value") + if has_command_delimiter_or_control(value): + raise ValueError( + f"extra cvar '{name}' contains a command delimiter or control character" + ) + if contains_benchmark_evidence_text(value): + raise ValueError( + f"extra cvar '{name}' may not contain benchmark evidence text" + ) + if name.casefold() in PBR_PROTECTED_CVARS and ( + has_engine_token_expansion_or_comment(value) + or any(character in value for character in ('"', "'", "\\")) + or (name.casefold() in PBR_BOOL_CVARS and value.startswith("+")) + ): + raise ValueError( + f"protected PBR cvar '{name}' contains ambiguous engine token syntax" + ) parsed.append((name, value)) return tuple(parsed) +def effective_post_map_cvars( + values: Iterable[tuple[str, str]], +) -> dict[str, str]: + """Return the engine-style, last-write-wins post-map cvar selection.""" + effective: dict[str, str] = {} + for name, value in values: + effective[name.casefold()] = value + return effective + + +def pbr_acceptance_fixture_cvars(render_api: str) -> tuple[tuple[str, str], ...]: + if render_api == "gl": + return PBR_ACCEPTANCE_GL_CVARS + if render_api == "vk": + return PBR_ACCEPTANCE_COMMON_CVARS + raise ValueError(f"unsupported renderer API for PBR fixture acceptance: {render_api!r}") + + +def requires_pbr_fixture_acceptance( + values: Iterable[tuple[str, str]], render_api: str, +) -> bool: + """Recognize only the explicit controlled Milestone-F fixture for one API.""" + effective = effective_post_map_cvars(values) + for name, expected in pbr_acceptance_fixture_cvars(render_api): + value = effective.get(name) + if value is None: + return False + if name in PBR_BOOL_CVARS: + integer_prefix = re.match(r"^[+-]?\d+", value) + if integer_prefix is None or int(integer_prefix.group(0)) == 0: + return False + elif value != expected: + return False + return True + + +def command_tokens(command: str) -> tuple[str, ...]: + try: + return tuple(shlex.split(command, posix=True)) + except ValueError as exc: + raise ValueError(f"invalid --exec-command quoting: {command!r}") from exc + + +def exec_command_pbr_provenance_failure(command: str) -> str: + """Explain command forms that can mutate protected fixture state.""" + tokens = command_tokens(command) + if not tokens: + return "empty command" + first = tokens[0].lstrip("+/\\").casefold() + if first in PBR_PROTECTED_CVARS: + return f"directly mutates protected cvar {tokens[0]!r}" + if first in PBR_CVAR_MUTATOR_COMMANDS and len(tokens) >= 2: + target = tokens[1].casefold() + if target in PBR_PROTECTED_CVARS: + return f"mutates protected cvar {tokens[1]!r} through {tokens[0]!r}" + if first in PBR_INDIRECT_COMMANDS: + return f"uses indirect command source {tokens[0]!r}" + if first in PBR_EVIDENCE_OUTPUT_COMMANDS: + return f"uses evidence-output command {tokens[0]!r}" + return "" + + def parse_exec_commands(values: list[str]) -> tuple[str, ...]: commands: list[str] = [] for raw in values: + if has_command_delimiter_or_control(raw): + raise ValueError( + f"--exec-command contains a command delimiter or control character: {raw!r}" + ) + if has_engine_token_expansion_or_comment(raw): + raise ValueError( + f"--exec-command contains engine token expansion or comment syntax: {raw!r}" + ) + if contains_benchmark_evidence_text(raw): + raise ValueError( + f"--exec-command may not contain benchmark evidence text: {raw!r}" + ) command = raw.strip() if not command: raise ValueError("empty --exec-command value") - if any(ord(ch) < 32 for ch in command): - raise ValueError(f"--exec-command contains a control character: {raw!r}") + provenance_failure = exec_command_pbr_provenance_failure(command) + if provenance_failure: + raise ValueError( + f"--exec-command cannot preserve PBR fixture provenance: {provenance_failure}" + ) commands.append(command) return tuple(commands) +def marked_cfg_block(lines: list[str], begin: str, end: str) -> list[str]: + if lines.count(begin) != 1 or lines.count(end) != 1: + raise ValueError(f"benchmark config must contain exactly one {begin!r}/{end!r} block") + start = lines.index(begin) + finish = lines.index(end) + if finish <= start: + raise ValueError(f"benchmark config block {begin!r} is out of order") + return lines[start + 1 : finish] + + +@dataclass(frozen=True) +class BenchmarkConfigEvidence: + cvars: tuple[tuple[str, str], ...] + commands: tuple[str, ...] + capture_mode: str + settle_frames: int + sample_kind: str + sample_value: int + role: str + capture_index: int + screenshot_request: str + + +def parse_benchmark_config(text: str) -> BenchmarkConfigEvidence: + """Validate the complete generated capture template and return its inputs.""" + lines = text.splitlines() + prefix = [ + "r_rendererSharedGui 0", + "r_rendererSharedInWorldGui 0", + "r_rendererSharedCinematicPost 0", + "r_rendererSharedSpecialFrame 0", + "r_rendererSharedWorldAmbient 0", + "r_rendererSharedWorldInteraction 0", + "r_rendererSharedWorldFogBlend 0", + "r_rendererSharedSubview 0", + "r_rendererSharedDeform 0", + "r_rendererModernVisible 0", + "r_rendererModernVisibleDepth 0", + "r_rendererModernOpaque 0", + "r_rendererModernDeferred 0", + "r_rendererForwardPlus 0", + "r_rendererModernSubmit 0", + "r_rendererGpuValidation 0", + "r_rendererBindless 0", + "r_rendererShaderReload 0", + POST_MAP_CVARS_BEGIN, + ] + if lines[: len(prefix)] != prefix: + raise ValueError("generated benchmark config prefix differs from the capture template") + if lines.count(POST_MAP_CVARS_BEGIN) != 1 or lines.count(POST_MAP_CVARS_END) != 1: + raise ValueError("generated benchmark config must contain one post-map CVar block") + if lines.count(EXEC_COMMANDS_BEGIN) != 1 or lines.count(EXEC_COMMANDS_END) != 1: + raise ValueError("generated benchmark config must contain one exec-command block") + + cursor = len(prefix) + try: + cvar_finish = lines.index(POST_MAP_CVARS_END, cursor) + except ValueError as exc: + raise ValueError("generated benchmark config post-map CVar block is unterminated") from exc + cvar_lines = lines[cursor:cvar_finish] + cvars: list[tuple[str, str]] = [] + for line in cvar_lines: + if has_command_delimiter_or_control(line): + raise ValueError("unsafe generated post-map cvar line") + parts = line.strip().split(None, 2) + if len(parts) != 3 or parts[0].casefold() != "set": + raise ValueError(f"malformed generated post-map cvar line: {line!r}") + _, name, value = parts + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name): + raise ValueError(f"invalid generated post-map cvar name: {name!r}") + if not value or has_command_delimiter_or_control(value): + raise ValueError(f"unsafe generated post-map cvar value for {name!r}") + if contains_benchmark_evidence_text(value): + raise ValueError( + f"generated post-map cvar {name!r} contains benchmark evidence text" + ) + if name.casefold() in PBR_PROTECTED_CVARS and ( + has_engine_token_expansion_or_comment(value) + or any(character in value for character in ('"', "'", "\\")) + or (name.casefold() in PBR_BOOL_CVARS and value.startswith("+")) + ): + raise ValueError(f"ambiguous generated protected PBR cvar value for {name!r}") + cvars.append((name, value)) + + cursor = cvar_finish + 1 + if cursor >= len(lines): + raise ValueError("generated benchmark config ends before the settle command") + settle_match = re.fullmatch(r"wait ([1-9][0-9]*)", lines[cursor]) + if settle_match is None: + raise ValueError("generated benchmark config has an invalid settle command") + settle_frames = int(settle_match.group(1)) + cursor += 1 + fixed_before_exec = ["god", "notarget", EXEC_COMMANDS_BEGIN] + if lines[cursor : cursor + len(fixed_before_exec)] != fixed_before_exec: + raise ValueError("generated benchmark config pre-exec scaffold differs") + cursor += len(fixed_before_exec) + try: + exec_finish = lines.index(EXEC_COMMANDS_END, cursor) + except ValueError as exc: + raise ValueError("generated benchmark config exec-command block is unterminated") from exc + commands = parse_exec_commands(lines[cursor:exec_finish]) + cursor = exec_finish + 1 + if cursor >= len(lines) or lines[cursor] != "viewpos": + raise ValueError("generated benchmark config is missing the post-command viewpos") + cursor += 1 + + display_lines = [ + f"{name} {value}" for name, value in budget_display_contract()["cvars"].items() + ] + if lines[cursor : cursor + len(display_lines)] == display_lines: + capture_mode = "budget" + cursor += len(display_lines) + else: + capture_mode = "functional-replay" + if cursor >= len(lines) or lines[cursor] != "framePacingReset": + raise ValueError("generated benchmark config sampling scaffold differs") + cursor += 1 + + if capture_mode == "budget": + sampling_prefix = ["r_rendererMetrics 1", "r_rendererGpuTimers 1"] + else: + sampling_prefix = ["r_rendererMetrics 0", "r_rendererGpuTimers 0"] + if lines[cursor : cursor + len(sampling_prefix)] != sampling_prefix: + raise ValueError( + f"generated benchmark config does not prove {capture_mode} capture settings" + ) + cursor += len(sampling_prefix) + if cursor >= len(lines): + raise ValueError("generated benchmark config is missing its sample wait") + sample_match = re.fullmatch(r"(wait|waitMsec) ([1-9][0-9]*)", lines[cursor]) + if sample_match is None: + raise ValueError("generated benchmark config has an invalid sample wait") + sample_kind = sample_match.group(1) + sample_value = int(sample_match.group(2)) + cursor += 1 + if capture_mode == "budget": + budget_tail = ["rendererBenchmarkCapture", "r_rendererMetrics 0"] + if lines[cursor : cursor + len(budget_tail)] != budget_tail: + raise ValueError("generated budget config is missing its benchmark capture") + cursor += len(budget_tail) + + fixed_tail = ["framePacingSnapshot", "gfxInfo"] + if lines[cursor : cursor + len(fixed_tail)] != fixed_tail: + raise ValueError("generated benchmark config evidence tail differs") + cursor += len(fixed_tail) + if cursor >= len(lines): + raise ValueError("generated benchmark config is missing its screenshot command") + screenshot_match = re.fullmatch( + r'screenshot "(screenshots/renderer-bench/(sp|server|client)_([0-9]+)\.tga)"', + lines[cursor], + ) + if screenshot_match is None: + raise ValueError("generated benchmark config screenshot request differs") + screenshot_request, role, capture_index_text = screenshot_match.groups() + cursor += 1 + if lines[cursor:] != ["wait 5", "quit"]: + raise ValueError("generated benchmark config final commands differ") + + return BenchmarkConfigEvidence( + cvars=tuple(cvars), + commands=commands, + capture_mode=capture_mode, + settle_frames=settle_frames, + sample_kind=sample_kind, + sample_value=sample_value, + role=role, + capture_index=int(capture_index_text), + screenshot_request=screenshot_request, + ) + + +def benchmark_cfg_provenance( + text: str, +) -> tuple[tuple[tuple[str, str], ...], tuple[str, ...]]: + """Extract provenance after validating every line of the generated config.""" + evidence = parse_benchmark_config(text) + return evidence.cvars, evidence.commands + + def append_set(args: list[str], name: str, value: Any) -> None: args += ["+set", name, str(value)] @@ -510,12 +1530,61 @@ def append_command(args: list[str], name: str, *values: Any) -> None: args.extend(str(value) for value in values) +def display_launch_contract(spec: RunSpec, width: int, height: int) -> dict[str, Any]: + """Return the exact, reportable display contract applied before startup.""" + promotable = ( + not spec.fullscreen and (width, height) == (BUDGET_WIDTH, BUDGET_HEIGHT) + ) + return { + "contractId": ( + BUDGET_DISPLAY_CONTRACT_ID + if promotable + else "non-promotable-diagnostic-display-v1" + ), + "width": width, + "height": height, + "cvars": { + "r_fullscreen": "1" if spec.fullscreen else "0", + "r_borderless": "0", + "r_borderlessDefaultMigrated": "1", + "r_fullscreenDesktop": "0", + "r_windowWidth": str(width), + "r_windowHeight": str(height), + "r_mode": "-1", + "r_customWidth": str(width), + "r_customHeight": str(height), + }, + } + + +def budget_display_contract() -> dict[str, Any]: + spec = RunSpec( + case_id="budget-display-contract", + mode="SP", + map_name="game/storage1", + budget_map_name="game/storage1", + purpose="budget display contract", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="default", + renderer="best", + render_api="gl", + ) + return display_launch_contract(spec, BUDGET_WIDTH, BUDGET_HEIGHT) + + def common_args( root: Path, + runtime_dir: Path, savepath: Path, log_name: str, basepath: str, spec: RunSpec, + width: int, + height: int, benchmark_preset: str, modern_executor: bool, show_fps_overlay: bool, @@ -530,12 +1599,12 @@ def common_args( append_set(args, "logFileName", f"logs/{log_name}") append_set(args, "developer", "1") append_set(args, "r_ignoreGLErrors", "0") - append_set(args, "r_fullscreen", "1" if spec.fullscreen else "0") append_set(args, "r_swapInterval", spec.swap_interval) append_set(args, "com_maxfps", spec.maxfps) append_set(args, "com_showFPS", "1" if show_fps_overlay else "0") append_set(args, "com_skipLoadingContinue", "1") append_set(args, "com_loadingContinueAutoAdvance", "1") + append_set(args, "com_levelLoadModernization", "0") append_set(args, "g_autoSkipCinematics", "1") append_set(args, "g_autoScreenshot", "0") if autoexec_cfg: @@ -547,9 +1616,20 @@ def common_args( append_set(args, "r_rendererGpuTimers", "0") append_set(args, "r_rendererModernExecutor", "1" if modern_executor and spec.tier != "legacy" else "0") append_set(args, "r_rendererModernAutoPromote", "0") + append_set(args, "r_rendererSharedGui", "0") + append_set(args, "r_rendererSharedInWorldGui", "0") + append_set(args, "r_rendererSharedCinematicPost", "0") + append_set(args, "r_rendererSharedSpecialFrame", "0") + append_set(args, "r_rendererSharedWorldAmbient", "0") + append_set(args, "r_rendererSharedWorldInteraction", "0") + append_set(args, "r_rendererSharedWorldFogBlend", "0") + append_set(args, "r_rendererSharedSubview", "0") + append_set(args, "r_rendererSharedDeform", "0") append_set(args, "r_rendererBenchmarkPreset", benchmark_preset) append_set(args, "fs_savepath", str(savepath)) - append_set(args, "fs_devpath", str(root / ".install")) + # Keep generated cache/config output in the isolated evidence root. The + # package itself is mounted by the locked fs_cdpath derived from cwd. + append_set(args, "fs_devpath", str(savepath)) append_set(args, "fs_game", "baseoq4") if basepath: append_set(args, "fs_basepath", basepath) @@ -557,6 +1637,14 @@ def common_args( for name, value in launch_cvars: append_set(args, name, value) + # These startup-sensitive values are deliberately appended after optional + # launch CVars. Archived configs and ad-hoc A/B knobs therefore cannot + # change the framebuffer size, presentation mode, or selected backend used + # by replay-verifiable budget evidence. + for name, value in display_launch_contract(spec, width, height)["cvars"].items(): + append_set(args, name, value) + append_set(args, "r_renderApi", "vulkan" if spec.render_api == "vk" else "gl") + for name, value in SHADOW_PRESETS[spec.shadow_preset].items(): append_set(args, name, value) @@ -578,6 +1666,15 @@ def build_scripted_capture_lines( ) -> tuple[list[str], str]: shot_name = f"screenshots/renderer-bench/{role}_{capture_index}.tga" lines: list[str] = [ + "r_rendererSharedGui 0", + "r_rendererSharedInWorldGui 0", + "r_rendererSharedCinematicPost 0", + "r_rendererSharedSpecialFrame 0", + "r_rendererSharedWorldAmbient 0", + "r_rendererSharedWorldInteraction 0", + "r_rendererSharedWorldFogBlend 0", + "r_rendererSharedSubview 0", + "r_rendererSharedDeform 0", "r_rendererModernVisible 0", "r_rendererModernVisibleDepth 0", "r_rendererModernOpaque 0", @@ -587,16 +1684,31 @@ def build_scripted_capture_lines( "r_rendererGpuValidation 0", "r_rendererBindless 0", "r_rendererShaderReload 0", + POST_MAP_CVARS_BEGIN, ] for name, value in extra_cvars: - lines.append(f"{name} {value}") + lines.append(f"set {name} {value}") lines += [ + POST_MAP_CVARS_END, f"wait {max(1, settle_frames)}", "god", "notarget", - "getviewpos", + EXEC_COMMANDS_BEGIN, ] lines.extend(exec_commands) + lines.append(EXEC_COMMANDS_END) + # Record the pose that is actually sampled. In particular, profile scene + # commands may move the player after the initial map settle; getviewpos + # before those commands described the wrong camera and omitted pitch/roll. + lines.append("viewpos") + if renderer_metrics: + # A client can reload the game module while connecting and re-exec an + # archived config after the launch arguments were applied. Reassert + # the promotion display contract immediately before sampling; actual + # drawable dimensions are still verified independently from gfxInfo + # and the engine-written TGA. + for name, value in budget_display_contract()["cvars"].items(): + lines.append(f"{name} {value}") lines.append("framePacingReset") sample_wait = f"waitMsec {max(1, sample_msec)}" if sample_msec > 0 else f"wait {max(1, sample_frames)}" if renderer_metrics: @@ -762,8 +1874,14 @@ def format_failure_diagnostic(diagnostic: dict[str, Any]) -> str: ) -def extract_last_line(text: str, token: str) -> str: - lines = [line.strip() for line in text.splitlines() if token in line] +def extract_last_line( + text: str, token: str, required_tokens: tuple[str, ...] = () +) -> str: + lines = [ + line.strip() + for line in text.splitlines() + if token in line and all(required in line for required in required_tokens) + ] return lines[-1] if lines else "" @@ -795,49 +1913,1639 @@ def parse_frame_pacing(line: str) -> dict[str, str]: return result -def extract_summary(text: str) -> dict[str, str]: - summary: dict[str, str] = { - "benchmarkCapture": extract_last_line(text, "rendererBenchmark capture("), - "benchmarkInfo": extract_last_line(text, "Renderer benchmark:"), - "framePacing": extract_last_line(text, "Frame pacing"), - "selectedTier": extract_last_line(text, "Selected renderer tier:"), - "tierContract": extract_last_line(text, "Renderer tier contract:"), - } - matches = re.findall( - r"rendererBenchmark capture\(.*?samples=(\d+).*?p50=(\d+).*?p95=(\d+).*?p99=(\d+)" - r".*?\b(?:thresholdPass|pass)\s*=\s*(\d+)\b", - text, - re.IGNORECASE, - ) - if matches: - samples, p50, p95, p99, threshold_pass = matches[-1] - summary.update( - { - "samples": samples, - "p50": p50, - "p95": p95, - "p99": p99, - "thresholdPass": threshold_pass, - } +def extract_summary(text: str) -> dict[str, str]: + interaction_summary_offset = text.rfind("Renderer shared interaction:") + interaction_tail = ( + text[interaction_summary_offset:] if interaction_summary_offset >= 0 else "" + ) + fog_blend_summary_offset = text.rfind("classicFogBlendDomain requested=") + fog_blend_tail = ( + text[fog_blend_summary_offset:] if fog_blend_summary_offset >= 0 else "" + ) + summary: dict[str, str] = { + "benchmarkCapture": extract_last_line(text, "rendererBenchmark capture("), + "benchmarkInfo": extract_last_line(text, "Renderer benchmark:"), + "framePacing": extract_last_line(text, "Frame pacing"), + "selectedTier": extract_last_line(text, "Selected renderer tier:"), + "tierContract": extract_last_line(text, "Renderer tier contract:"), + "pbrMaterialResources": extract_last_line( + text, "PBR material resources:" + ), + "modernGLExecutor": extract_last_line( + text, + "Modern GL executor:", + ("drawPlan=", "planFallback=", "pbrOwners=", "pbrConsumed="), + ), + "modernForwardPlus": extract_last_line(text, "Modern forward+:"), + "modernVisibleFrame": extract_last_line(text, "Modern visible frame:"), + "vulkanPackedPBR": extract_last_line( + text, "Vulkan: native packed PBR direct interactions active (" + ), + "sharedInteraction": extract_last_line(text, "Renderer shared interaction:"), + "sharedInteractionView": extract_last_line( + text, "Renderer shared interaction view[" + ), + # Keep only the per-map records emitted by the latest gfxInfo block. + # Earlier samples may contain a different frame/generation and must not + # be reconciled with the final aggregate counters. + "sharedInteractionMaps": "\n".join( + line.strip() + for line in interaction_tail.splitlines() + if "Renderer shared interaction map view[" in line + ), + "sharedFogBlend": extract_last_line( + text, "classicFogBlendDomain requested=" + ), + # Keep backend records paired with the latest aggregate gfxInfo line. + # Repeated samples can otherwise mix generations and make a backend + # that was inactive in the final frame look owned. + "sharedFogBlendBackends": "\n".join( + line.strip() + for line in fog_blend_tail.splitlines() + if "classicFogBlendDomain backend=" in line + ), + "sharedFogBlendViews": "\n".join( + line.strip() + for line in fog_blend_tail.splitlines() + if "classicFogBlendDomain view[" in line + ), + } + matches = re.findall( + r"rendererBenchmark capture\(.*?samples=(\d+).*?p50=(\d+).*?p95=(\d+).*?p99=(\d+)" + r".*?\b(?:thresholdPass|pass)\s*=\s*(\d+)\b", + text, + re.IGNORECASE, + ) + if matches: + samples, p50, p95, p99, threshold_pass = matches[-1] + summary.update( + { + "samples": samples, + "p50": p50, + "p95": p95, + "p99": p99, + "thresholdPass": threshold_pass, + } + ) + if "thresholdPass" not in summary: + for benchmark_line in (summary["benchmarkCapture"], summary["benchmarkInfo"]): + match = re.search(r"\b(?:thresholdPass|pass)\s*=\s*(\d+)\b", benchmark_line, re.IGNORECASE) + if match: + summary["thresholdPass"] = match.group(1) + break + summary.update(parse_frame_pacing(summary["framePacing"])) + return summary + + +PBR_RESOURCE_TELEMETRY_PATTERN = re.compile( + r"PBR material resources: " + r"records=-?\d+ resourceReady=-?\d+ modernReady=-?\d+ " + r"packed=-?\d+ separate=-?\d+ fallback=-?\d+" + r"(?: disabled=-?\d+ workflow=-?\d+ class=-?\d+ albedo=-?\d+ " + r"normalFormat=-?\d+ conflict=-?\d+ layout=-?\d+ image=-?\d+ " + r"classic=-?\d+ units=-?\d+ shader=-?\d+ authored=-?\d+ " + r"explicitGenerated=-?\d+ generated=-?\d+ approximate=-?\d+ " + r"missingFallback=-?\d+ mapsMissing=-?\d+/-?\d+/-?\d+)?" +) +MODERN_GL_EXECUTOR_TELEMETRY_PATTERN = re.compile( + r"Modern GL executor: (?:available|unavailable), " + r"(?=[^\r\n]*\bdrawPlan=-?\d+\b)" + r"(?=[^\r\n]*\bplanDraws=-?\d+\b)" + r"(?=[^\r\n]*\bplanFallback=-?\d+\b)" + r"(?=[^\r\n]*\bpbrOwners=-?\d+\b)" + r"(?=[^\r\n]*\bpbrConsumed=-?\d+\b)" + r"[A-Za-z0-9_(),='./ -]+" +) +MODERN_FORWARD_PLUS_TELEMETRY_PATTERN = re.compile( + r"Modern forward\+: cvar=-?\d+, " + r"(?=[^\r\n]*\breq=-?\d+\b)" + r"(?=[^\r\n]*\bexec=-?\d+\b)" + r"(?=[^\r\n]*\bresources=-?\d+\b)" + r"(?=[^\r\n]*\bsceneColor=-?\d+\b)" + r"(?=[^\r\n]*\bsceneDepth=-?\d+\b)" + r"(?=[^\r\n]*\bprogram=-?\d+\b)" + r"(?=[^\r\n]*\bcluster=-?\d+\b)" + r"(?=[^\r\n]*\bdraws=-?\d+\b)" + r"(?=[^\r\n]*\bfallback=-?\d+\b)" + r"[A-Za-z0-9_(),='./ -]+" +) +MODERN_VISIBLE_TELEMETRY_PATTERN = re.compile( + r"Modern visible frame: cvar=-?\d+, " + r"(?=[^\r\n]*\breq=-?\d+\b)" + r"(?=[^\r\n]*\bexec=-?\d+\b)" + r"(?=[^\r\n]*\bresources=-?\d+\b)" + r"(?=[^\r\n]*\bprogram=-?\d+\b)" + r"(?=[^\r\n]*\bsource=-?\d+\b)" + r"(?=[^\r\n]*\bhybrid=-?\d+\b)" + r"(?=[^\r\n]*\bbackBuffer=-?\d+\b)" + r"(?=[^\r\n]*\bcomposed=-?\d+\b)" + r"[A-Za-z0-9_(),='./ -]+" +) +PBR_TELEMETRY_PATTERNS = { + "pbrMaterialResources": PBR_RESOURCE_TELEMETRY_PATTERN, + "modernGLExecutor": MODERN_GL_EXECUTOR_TELEMETRY_PATTERN, + "modernForwardPlus": MODERN_FORWARD_PLUS_TELEMETRY_PATTERN, + "modernVisibleFrame": MODERN_VISIBLE_TELEMETRY_PATTERN, +} + + +def normalize_engine_map_name(value: str) -> str: + normalized = value.strip().replace("\\", "/").casefold().removesuffix(".map") + while "//" in normalized: + normalized = normalized.replace("//", "/") + if not normalized.startswith("maps/"): + normalized = "maps/" + normalized.lstrip("/") + parts = normalized.split("/") + if any(part in ("", ".", "..") for part in parts): + return "" + return normalized + + +def evaluate_controlled_map_evidence( + sources: Iterable[tuple[str, str]], required: bool +) -> tuple[dict[str, Any], list[str]]: + expected = normalize_engine_map_name(PBR_FIXTURE_STOCK_MAP) + evidence: dict[str, Any] = { + "required": required, + "expected": expected if required else None, + "observed": [], + "status": "not-required", + } + if not required: + return evidence, [] + observed: list[dict[str, str]] = [] + for source_name, source_text in sources: + matches = ENGINE_MAP_PATTERN.findall(source_text) + if matches: + observed.append( + { + "source": source_name, + "map": normalize_engine_map_name(matches[-1]), + } + ) + evidence["observed"] = observed + failures: list[str] = [] + log_observation = next( + (item for item in observed if item["source"] == "log"), None + ) + if log_observation is None: + failures.append("controlled fixture final engine Map evidence is missing from the log") + for item in observed: + if not item["map"] or item["map"] != expected: + failures.append( + "controlled fixture final engine Map differs: " + f"{item['source']}={item['map'] or 'malformed'} expected={expected}" + ) + evidence["status"] = "pass" if not failures else "fail" + return evidence, failures + + +def evaluate_pbr_fixture_evidence( + summary: dict[str, str], required: bool, render_api: str +) -> tuple[dict[str, Any], list[str]]: + """Fail closed when the controlled PBR fixture did not get a real owner.""" + if render_api not in ("gl", "vk"): + raise ValueError(f"unsupported PBR fixture evidence API: {render_api!r}") + evidence: dict[str, Any] = { + "required": required, + "renderApi": render_api, + "status": "not-required", + } + if not required: + return evidence, [] + + common_telemetry_specs = ( + ( + "pbrResources", + "pbrMaterialResources", + "PBR material resources", + ("records", "resourceReady", "modernReady", "packed", "fallback"), + ), + ) + gl_telemetry_specs = ( + ( + "modernVisible", + "modernVisibleFrame", + "Modern visible frame", + ( + "req", + "exec", + "resources", + "program", + "source", + "hybrid", + "backBuffer", + "composed", + ), + ), + ( + "drawPlan", + "modernGLExecutor", + "Modern GL executor", + ( + "drawPlan", + "planDraws", + "planFallback", + "pbrOwners", + "pbrConsumed", + ), + ), + ( + "forwardPlus", + "modernForwardPlus", + "Modern forward+", + ( + "req", + "exec", + "resources", + "sceneColor", + "sceneDepth", + "program", + "cluster", + "draws", + "fallback", + ), + ), + ) + telemetry_specs = common_telemetry_specs + ( + gl_telemetry_specs if render_api == "gl" else () + ) + failures: list[str] = [] + fields_by_name: dict[str, dict[str, int]] = {} + telemetry_present: dict[str, bool] = {} + for evidence_name, summary_name, display_name, field_names in telemetry_specs: + line = summary.get(summary_name, "") + line_pattern = PBR_TELEMETRY_PATTERNS[summary_name] + line_is_exact = bool(line) and line_pattern.fullmatch(line) is not None + telemetry_present[evidence_name] = line_is_exact + fields: dict[str, int] = {} + for name in field_names: + field_match = re.search(rf"\b{re.escape(name)}=(-?\d+)\b", line) + if field_match is not None: + fields[name] = int(field_match.group(1)) + fields_by_name[evidence_name] = fields + evidence[evidence_name] = fields + if not line: + failures.append(f"PBR fixture {display_name} telemetry missing") + elif not line_is_exact: + failures.append(f"PBR fixture {display_name} telemetry is malformed") + + common_checks = ( + ("pbrResources", "records", lambda value: value > 0, "PBR records>0"), + ("pbrResources", "resourceReady", lambda value: value > 0, "PBR resourceReady>0"), + ("pbrResources", "modernReady", lambda value: value > 0, "PBR modernReady>0"), + ("pbrResources", "packed", lambda value: value > 0, "PBR packed resources>0"), + ("pbrResources", "fallback", lambda value: value == 0, "PBR fallback=0"), + ) + gl_checks = ( + ("modernVisible", "req", lambda value: value == 1, "modern visible req=1"), + ("modernVisible", "exec", lambda value: value == 1, "modern visible exec=1"), + ("modernVisible", "resources", lambda value: value == 1, "modern visible resources=1"), + ("modernVisible", "program", lambda value: value == 1, "modern visible program=1"), + ("modernVisible", "source", lambda value: value == 1, "modern visible source=1"), + ("modernVisible", "hybrid", lambda value: value == 1, "modern visible hybrid=1"), + ("modernVisible", "backBuffer", lambda value: value == 1, "modern visible backBuffer=1"), + ("modernVisible", "composed", lambda value: value > 0, "modern visible composed>0"), + ("drawPlan", "drawPlan", lambda value: value == 1, "draw plan ready=1"), + ("drawPlan", "planDraws", lambda value: value > 0, "draw-plan draws>0"), + ("drawPlan", "pbrOwners", lambda value: value > 0, "clustered PBR owners>0"), + ("drawPlan", "pbrConsumed", lambda value: value > 0, "clustered PBR consumed interactions>0"), + ("drawPlan", "planFallback", lambda value: value == 0, "draw-plan fallback=0"), + ("forwardPlus", "req", lambda value: value == 1, "forward+ req=1"), + ("forwardPlus", "exec", lambda value: value == 1, "forward+ exec=1"), + ("forwardPlus", "resources", lambda value: value == 1, "forward+ resources=1"), + ("forwardPlus", "sceneColor", lambda value: value == 1, "forward+ sceneColor=1"), + ("forwardPlus", "sceneDepth", lambda value: value == 1, "forward+ sceneDepth=1"), + ("forwardPlus", "program", lambda value: value == 1, "forward+ program=1"), + ("forwardPlus", "cluster", lambda value: value == 1, "forward+ cluster=1"), + ("forwardPlus", "draws", lambda value: value > 0, "forward+ draws>0"), + ("forwardPlus", "fallback", lambda value: value == 0, "forward+ fallback=0"), + ) + checks = common_checks + (gl_checks if render_api == "gl" else ()) + if ( + render_api == "gl" + and telemetry_present.get("drawPlan", False) + and not summary.get("modernGLExecutor", "").startswith( + "Modern GL executor: available, " + ) + ): + failures.append("PBR fixture Modern GL executor is not available") + for telemetry_name, field_name, accepted, requirement in checks: + if not telemetry_present[telemetry_name]: + continue + fields = fields_by_name[telemetry_name] + if field_name not in fields: + failures.append(f"PBR fixture {requirement} (missing)") + continue + value = fields[field_name] + if not accepted(value): + failures.append(f"PBR fixture {requirement} (got {value})") + + if render_api == "vk": + marker = summary.get("vulkanPackedPBR", "") + match = re.fullmatch( + r"Vulkan: native packed PBR direct interactions active \((\d+) draws\)", + marker, + ) + draws = int(match.group(1)) if match else None + evidence["vulkan"] = {} if draws is None else {"draws": draws} + if match is None: + failures.append("PBR fixture Vulkan native packed-PBR draw telemetry missing") + elif draws <= 0: + failures.append(f"PBR fixture Vulkan native packed-PBR draws>0 (got {draws})") + + evidence["status"] = "pass" if not failures else "fail" + return evidence, failures + + +def summary_float(summary: dict[str, str], key: str) -> float | None: + value = summary.get(key) + if value is None: + return None + try: + return float(value) + except ValueError: + return None + + +def shared_interaction_fields(summary: dict[str, str]) -> dict[str, str]: + line = summary.get("sharedInteraction", "") + return { + key: value + for key, value in re.findall(r"\b([A-Za-z]+)=([^\s]+)", line) + } + + +def shared_fog_blend_fields(summary: dict[str, str]) -> dict[str, str]: + return { + key: value + for key, value in re.findall( + r"\b([A-Za-z]+)=([^\s]+)", summary.get("sharedFogBlend", "") + ) + } + + +def shared_fog_blend_backend_records( + summary: dict[str, str] +) -> dict[str, dict[str, str]]: + records: dict[str, dict[str, str]] = {} + for line in summary.get("sharedFogBlendBackends", "").splitlines(): + fields = { + key: value + for key, value in re.findall(r"\b([A-Za-z]+)=([^\s]+)", line) + } + backend = fields.get("backend") + if backend: + records[backend] = fields + return records + + +def shared_fog_blend_view_records( + summary: dict[str, str] +) -> list[dict[str, int | str]]: + records: list[dict[str, int | str]] = [] + pattern = re.compile( + r"classicFogBlendDomain view\[(?P\d+)\].*?" + r"\bready=(?P\d+)\s+failure=(?P[^\s]+)\s+" + r"detail=(?P-?\d+).*?" + r"\bGL=(?P\d+)/(?P[^/\s]+)/(?P-?\d+)\s+" + r"Vulkan=(?P\d+)/(?P[^/\s]+)/(?P-?\d+)" + ) + for line in summary.get("sharedFogBlendViews", "").splitlines(): + match = pattern.search(line) + if match is None: + continue + fields: dict[str, int | str] = match.groupdict() + for name in ( + "index", + "ready", + "detail", + "glOutcome", + "glDetail", + "vkOutcome", + "vkDetail", + ): + fields[name] = int(str(fields[name])) + records.append(fields) + return records + + +def shared_interaction_shadow_counts( + fields: dict[str, str] +) -> tuple[int, int, int, int, int, int] | None: + shadow = re.fullmatch(r"(\d+)/(\d+)/(\d+)\+(\d+)", fields.get("shadow", "")) + volumes = re.fullmatch(r"(\d+)\+(\d+)", fields.get("volumes", "")) + if shadow is None or volumes is None: + return None + return tuple(int(value) for value in (*shadow.groups(), *volumes.groups())) + + +def shared_interaction_map_counts( + fields: dict[str, str] +) -> tuple[int, int, int, int, int] | None: + maps = re.fullmatch(r"(\d+)\+(\d+)", fields.get("maps", "")) + modes = re.fullmatch(r"(\d+)\+(\d+)", fields.get("modes", "")) + csm = re.fullmatch(r"\d+", fields.get("csm", "")) + if maps is None or modes is None or csm is None: + return None + return tuple( + int(value) for value in (*maps.groups(), *modes.groups(), csm.group()) + ) + + +def shared_interaction_map_records(text: str) -> list[dict[str, int | str]]: + pattern = re.compile( + r"^Renderer shared interaction map view\[(\d+)\] light=(\d+) " + r"receiver=(local|global) mode=(none|stencil|projected|point|hybrid) " + r"class=(point|parallel|global|projected) cascades=(\d+) " + r"alias=(\d+) plan=([0-9a-fA-F]{16}) generation=(\d+) " + r"casters=(\d+)\+(\d+) features=(\d+)\+(\d+)\+(\d+)\+(\d+) " + r"hash=([0-9a-fA-F]{16})$" + ) + records: list[dict[str, int | str]] = [] + for line in text.splitlines(): + match = pattern.fullmatch(line.strip()) + if match is None: + continue + ( + view, + light, + receiver, + mode, + light_class, + cascades, + alias, + plan, + generation, + casters, + supplements, + static_casters, + dynamic_casters, + alpha_casters, + translucent_casters, + semantic_hash, + ) = match.groups() + records.append( + { + "view": int(view), + "light": int(light), + "receiver": receiver, + "mode": mode, + "class": light_class, + "cascades": int(cascades), + "alias": int(alias), + "plan": int(plan, 16), + "generation": int(generation), + "casters": int(casters), + "supplements": int(supplements), + "static": int(static_casters), + "dynamic": int(dynamic_casters), + "alpha": int(alpha_casters), + "translucent": int(translucent_casters), + "hash": int(semantic_hash, 16), + } + ) + return records + + +def shared_interaction_backend_counts( + value: str, +) -> tuple[int, str, int, int, int, int, int, int, int, int, int] | None: + match = re.fullmatch( + r"(\d+)/([^/]+)/(-?\d+)/(\d+)\+(\d+)/" + r"(\d+)\+(\d+)\+(\d+)\+(\d+)/(\d+)\+(\d+)", + value, + ) + if match is None: + return None + ( + outcome, + failure, + detail, + drawn_primitives, + noop_primitives, + submitted_shadow_casters, + noop_shadow_casters, + logical_volume_draws, + preload_volume_draws, + shadow_map_passes, + hybrid_passes, + ) = match.groups() + return ( + int(outcome), + failure, + int(detail), + int(drawn_primitives), + int(noop_primitives), + int(submitted_shadow_casters), + int(noop_shadow_casters), + int(logical_volume_draws), + int(preload_volume_draws), + int(shadow_map_passes), + int(hybrid_passes), + ) + + +# The controlled map-budget scene exposes four shadow lights: one projected +# pass followed by three point-light passes. Its one-update admission limit +# rejects the second light in the GL map transaction: reason 24, one-based +# light detail 2, +# and no surface/primitive detail. +GL_MAP_BUDGET_FALLBACK_DETAIL = 2_402_000_000 +CONTROLLED_MAP_BUDGET_COUNTS = (4, 0, 1, 3, 0) +VK_SHADOW_STATE_REJECT = 7 +TRANSLUCENT_SHADOW_CHAIN_DETAILS = (6, 7) + + +def evaluate_shared_interaction_evidence( + spec: RunSpec, summary: dict[str, str] +) -> list[str]: + expectation = spec.interaction_expectation + if expectation == "none": + return [] + + fields = shared_interaction_fields(summary) + view_fields = { + key: value + for key, value in re.findall( + r"\b([A-Za-z]+)=([^\s]+)", summary.get("sharedInteractionView", "") + ) + } + shadow_counts = shared_interaction_shadow_counts(fields) + map_counts = shared_interaction_map_counts(fields) + map_records = shared_interaction_map_records( + summary.get("sharedInteractionMaps", "") + ) + failures: list[str] = [] + if not fields: + return ["shared interaction evidence line"] + + def integer(name: str) -> int | None: + value = fields.get(name) + try: + return int(value) if value is not None else None + except ValueError: + return None + + requested = integer("requested") + if expectation == "disabled": + if requested != 0: + failures.append(f"shared interaction requested={requested}") + for name in ( + "prepared", + "valid", + "views", + "ready", + "fallback", + "lights", + "surfaces", + "primitives", + "draw", + "noop", + ): + if integer(name) != 0: + failures.append( + f"shared interaction {name}={fields.get(name, 'missing')}" + ) + if shadow_counts != (0, 0, 0, 0, 0, 0): + failures.append( + "shared interaction disabled shadow accounting=" + f"{fields.get('shadow', 'missing')}/" + f"{fields.get('volumes', 'missing')}" + ) + if map_counts != (0, 0, 0, 0, 0) or map_records: + failures.append( + "shared interaction disabled map accounting=" + f"{fields.get('maps', 'missing')}/" + f"{fields.get('modes', 'missing')}/" + f"{fields.get('csm', 'missing')} records={len(map_records)}" + ) + if fields.get("status") != "empty": + failures.append( + f"shared interaction status={fields.get('status', 'missing')}" + ) + for backend_name in ("GL", "VK"): + if fields.get(backend_name) != "0/0/0": + failures.append( + "shared interaction " + f"{backend_name} coverage={fields.get(backend_name, 'missing')}" + ) + return failures + + for name in ("requested", "prepared"): + if integer(name) != 1: + failures.append(f"shared interaction {name}={fields.get(name, 'missing')}") + + backend_name = "VK" if spec.expected_backend == "vulkan" else "GL" + backend_value = fields.get(backend_name, "") + backend_match = re.fullmatch(r"(\d+)/(\d+)/(\d+)", backend_value) + if backend_match is None: + failures.append(f"shared interaction {backend_name} coverage={backend_value or 'missing'}") + owned_views = fallback_views = mismatches = -1 + else: + owned_views, fallback_views, mismatches = ( + int(value) for value in backend_match.groups() + ) + inactive_backend_name = "GL" if backend_name == "VK" else "VK" + inactive_backend_value = fields.get(inactive_backend_name, "") + if inactive_backend_value != "0/0/0": + failures.append( + "shared interaction inactive backend coverage=" + f"{inactive_backend_name}:{inactive_backend_value or 'missing'}" + ) + inactive_backend_counts = shared_interaction_backend_counts( + view_fields.get(inactive_backend_name, "") + ) + expected_inactive_backend_counts = ( + 0, + "none", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + if inactive_backend_counts != expected_inactive_backend_counts: + failures.append( + "shared interaction inactive backend execution=" + f"{inactive_backend_name}:{view_fields.get(inactive_backend_name, 'missing')}" + ) + + if expectation == "owned": + if integer("valid") != 1: + failures.append( + f"shared interaction valid={fields.get('valid', 'missing')}" + ) + for name in ("ready", "lights", "surfaces", "primitives", "draw"): + value = integer(name) + if value is None or value <= 0: + failures.append(f"shared interaction {name}={fields.get(name, 'missing')}") + if integer("fallback") != 0: + failures.append( + f"shared interaction fallback={fields.get('fallback', 'missing')}" + ) + if fields.get("status") != "ready": + failures.append( + f"shared interaction status={fields.get('status', 'missing')}" + ) + ready_views = integer("ready") + if integer("views") != ready_views: + failures.append( + "shared interaction complete-view readiness=" + f"{fields.get('ready', 'missing')}/{fields.get('views', 'missing')}" + ) + primitive_count = integer("primitives") + draw_count = integer("draw") + noop_count = integer("noop") + if ( + primitive_count is None + or draw_count is None + or noop_count is None + or primitive_count != draw_count + noop_count + ): + failures.append( + "shared interaction primitive reconciliation=" + f"{fields.get('primitives', 'missing')}/" + f"{fields.get('draw', 'missing')}+{fields.get('noop', 'missing')}" + ) + if ( + ready_views is None + or owned_views != ready_views + or fallback_views != 0 + or mismatches != 0 + ): + failures.append(f"shared interaction {backend_name} coverage={backend_value}") + shadow_expectation = spec.interaction_shadow_expectation + drawable_shadow_casters = None + noop_shadow_casters = None + logical_volume_draws = None + preload_volume_draws = None + if shadow_counts is None: + failures.append("shared interaction shadow/volume counters=missing") + else: + ( + shadow_lights, + shadow_casters, + drawable_shadow_casters, + noop_shadow_casters, + logical_volume_draws, + preload_volume_draws, + ) = shadow_counts + if shadow_casters != drawable_shadow_casters + noop_shadow_casters: + failures.append( + "shared interaction shadow caster reconciliation=" + f"{shadow_casters}/{drawable_shadow_casters}+" + f"{noop_shadow_casters}" + ) + if preload_volume_draws > logical_volume_draws: + failures.append( + "shared interaction volume preload reconciliation=" + f"{logical_volume_draws}+{preload_volume_draws}" + ) + light_count = integer("lights") + if light_count is None or shadow_lights > light_count: + failures.append( + "shared interaction shadow light reconciliation=" + f"{shadow_lights}/{fields.get('lights', 'missing')}" + ) + if shadow_expectation == "none": + if shadow_counts != (0, 0, 0, 0, 0, 0): + failures.append( + "shared interaction unshadowed accounting=" + f"{fields.get('shadow', 'missing')}/" + f"{fields.get('volumes', 'missing')}" + ) + elif shadow_expectation == "stencil": + if ( + shadow_lights <= 0 + or shadow_casters <= 0 + or drawable_shadow_casters <= 0 + or logical_volume_draws <= 0 + ): + failures.append( + "shared interaction stencil ownership=" + f"{fields.get('shadow', 'missing')}/" + f"{fields.get('volumes', 'missing')}" + ) + if view_fields.get("mode") != "stencil": + failures.append( + "shared interaction stencil view mode=" + f"{view_fields.get('mode', 'missing')}" + ) + if view_fields.get("failure") != "none": + failures.append( + "shared interaction stencil view failure=" + f"{view_fields.get('failure', 'missing')}" + ) + if map_counts is None: + failures.append("shared interaction map/mode counters=missing") + map_passes = hybrid_passes = projected_passes = point_passes = csm_passes = -1 + else: + ( + map_passes, + hybrid_passes, + projected_passes, + point_passes, + csm_passes, + ) = map_counts + if projected_passes + point_passes != map_passes: + failures.append( + "shared interaction map class reconciliation=" + f"{map_passes}/{projected_passes}+{point_passes}" + ) + if hybrid_passes > map_passes or csm_passes > projected_passes: + failures.append( + "shared interaction map subtype reconciliation=" + f"hybrid={hybrid_passes}/{map_passes} " + f"csm={csm_passes}/{projected_passes}" + ) + + record_keys = { + (record["view"], record["light"], record["receiver"]) + for record in map_records + } + if len(map_records) != map_passes or len(record_keys) != map_passes: + failures.append( + "shared interaction per-map reconciliation=" + f"records={len(map_records)} unique={len(record_keys)} " + f"passes={map_passes}" + ) + record_projected = sum( + record["class"] != "point" for record in map_records + ) + record_point = sum( + record["class"] == "point" for record in map_records + ) + record_csm = sum( + record["class"] != "point" and int(record["cascades"]) > 1 + for record in map_records + ) + record_hybrid = sum( + record["mode"] == "hybrid" for record in map_records + ) + if ( + record_projected, + record_point, + record_csm, + record_hybrid, + ) != ( + projected_passes, + point_passes, + csm_passes, + hybrid_passes, + ): + failures.append( + "shared interaction per-map subtype evidence=" + f"{record_projected}+{record_point}/csm={record_csm}/" + f"hybrid={record_hybrid} expected=" + f"{projected_passes}+{point_passes}/csm={csm_passes}/" + f"hybrid={hybrid_passes}" + ) + for record in map_records: + cascades = int(record["cascades"]) + feature_values = tuple( + int(record[name]) + for name in ("static", "dynamic", "alpha", "translucent") + ) + if ( + int(record["casters"]) <= 0 + or int(record["generation"]) <= 0 + or int(record["plan"]) == 0 + or int(record["hash"]) == 0 + or int(record["alias"]) not in (0, 1) + or any(value not in (0, 1) for value in feature_values) + or not (int(record["static"]) or int(record["dynamic"])) + or int(record["translucent"]) != 0 + or (record["class"] == "point" and cascades != 6) + or (record["class"] != "point" and not 1 <= cascades <= 4) + or ( + record["mode"] == "hybrid" + and int(record["supplements"]) <= 0 + ) + ): + failures.append( + "shared interaction invalid per-map record=" + f"{record}" + ) + if int(record["alias"]) == 1: + owner_key = (record["view"], record["light"], "local") + if record["receiver"] != "global" or owner_key not in record_keys: + failures.append( + "shared interaction invalid map alias=" + f"{record}" + ) + + if map_passes > 0: + if shadow_counts is None: + failures.append( + "shared interaction mapped caster accounting=missing" + ) + else: + per_light_record_casters: dict[tuple[int, int], int] = {} + for record in map_records: + light_key = (int(record["view"]), int(record["light"])) + per_light_record_casters[light_key] = max( + per_light_record_casters.get(light_key, 0), + int(record["casters"]) + + int(record["supplements"]), + ) + record_caster_floor = sum(per_light_record_casters.values()) + if ( + shadow_counts[0] <= 0 + or shadow_counts[1] <= 0 + or len(per_light_record_casters) > shadow_counts[0] + or record_caster_floor > shadow_counts[1] + ): + failures.append( + "shared interaction mapped caster accounting=" + f"recordsMin={record_caster_floor} lights=" + f"{len(per_light_record_casters)} shadow=" + f"{fields.get('shadow', 'missing')}" + ) + + if shadow_expectation == "dynamic" and not any( + int(record["dynamic"]) == 1 for record in map_records + ): + failures.append( + "shared interaction dynamic caster feature missing=" + f"{map_records}" + ) + if shadow_expectation == "perforated" and not any( + int(record["alpha"]) == 1 for record in map_records + ): + failures.append( + "shared interaction perforated caster feature missing=" + f"{map_records}" + ) + + allowed_modes = { + "none": {"none"}, + "stencil": {"stencil"}, + "projected": {"projected", "hybrid"}, + "point": {"point", "hybrid"}, + "csm": {"projected", "hybrid"}, + "mapped": {"hybrid"}, + "mixed": {"hybrid"}, + "dynamic": {"projected", "point", "hybrid"}, + "perforated": {"projected", "point", "hybrid"}, + "hybrid": {"hybrid"}, + }.get(shadow_expectation) + if ( + allowed_modes is not None + and view_fields.get("mode") not in allowed_modes + ): + failures.append( + "shared interaction shadow view mode=" + f"{view_fields.get('mode', 'missing')} " + f"expected={sorted(allowed_modes)}" + ) + if view_fields.get("failure") != "none": + failures.append( + "shared interaction shadow view failure=" + f"{view_fields.get('failure', 'missing')}" + ) + + expected_map_shape = { + "none": (0, 0, 0, 0, 0), + "stencil": (0, 0, 0, 0, 0), + }.get(shadow_expectation) + if expected_map_shape is not None and map_counts != expected_map_shape: + failures.append( + "shared interaction unexpected mapped work=" + f"{map_counts} expected={expected_map_shape}" + ) + elif shadow_expectation == "projected" and not ( + map_passes > 0 + and projected_passes > 0 + and csm_passes == 0 + and any( + record["class"] == "projected" + and int(record["cascades"]) == 1 + for record in map_records + ) + ): + failures.append(f"shared interaction projected map shape={map_counts}") + elif shadow_expectation == "point" and not ( + map_passes > 0 + and point_passes > 0 + ): + failures.append(f"shared interaction point map shape={map_counts}") + elif shadow_expectation == "csm" and not ( + map_passes > 0 + and projected_passes > 0 + and csm_passes > 0 + ): + failures.append(f"shared interaction CSM map shape={map_counts}") + elif shadow_expectation == "mapped" and not ( + map_passes > 0 + and hybrid_passes == 0 + and projected_passes > 0 + and point_passes > 0 + and csm_passes == 0 + ): + failures.append(f"shared interaction mapped-light shape={map_counts}") + elif shadow_expectation == "mixed" and not ( + map_passes > 0 + and hybrid_passes == 0 + and projected_passes > 0 + and point_passes == 0 + and csm_passes == 0 + and shadow_counts is not None + and shadow_counts[4] > 0 + and shadow_counts[0] + > len({int(record["light"]) for record in map_records}) + ): + failures.append( + "shared interaction map/stencil mix=" + f"maps={map_counts} volumes=" + f"{fields.get('volumes', 'missing')} shadowLights=" + f"{fields.get('shadow', 'missing')} mappedLights=" + f"{len({int(record['light']) for record in map_records})}" + ) + elif shadow_expectation in ("dynamic", "perforated") and not ( + map_passes > 0 and projected_passes + point_passes > 0 + ): + failures.append( + f"shared interaction {shadow_expectation} mapped ownership=" + f"{map_counts}" + ) + elif shadow_expectation == "hybrid" and not ( + map_passes > 0 + and hybrid_passes > 0 + and shadow_counts is not None + and shadow_counts[4] > 0 + ): + failures.append( + "shared interaction same-light hybrid=" + f"maps={map_counts} volumes=" + f"{fields.get('volumes', 'missing')}" + ) + backend_counts = shared_interaction_backend_counts( + view_fields.get(backend_name, "") + ) + if backend_counts is None: + failures.append( + f"shared interaction {backend_name} view coverage=" + f"{view_fields.get(backend_name, 'missing')}" + ) + else: + ( + backend_outcome, + backend_failure, + backend_detail, + backend_drawn_primitives, + backend_noop_primitives, + backend_shadow_casters, + backend_noop_shadow_casters, + backend_logical_volume_draws, + backend_preload_volume_draws, + backend_shadow_map_passes, + backend_hybrid_passes, + ) = backend_counts + if ( + backend_outcome != 1 + or backend_failure != "none" + or backend_detail != 0 + ): + failures.append( + f"shared interaction {backend_name} view outcome=" + f"{view_fields.get(backend_name, 'missing')}" + ) + expected_backend_counts = ( + draw_count, + noop_count, + drawable_shadow_casters, + noop_shadow_casters, + logical_volume_draws, + preload_volume_draws, + map_passes, + hybrid_passes, + ) + actual_backend_counts = ( + backend_drawn_primitives, + backend_noop_primitives, + backend_shadow_casters, + backend_noop_shadow_casters, + backend_logical_volume_draws, + backend_preload_volume_draws, + backend_shadow_map_passes, + backend_hybrid_passes, + ) + if actual_backend_counts != expected_backend_counts: + failures.append( + f"shared interaction {backend_name} execution=" + f"{actual_backend_counts} expected={expected_backend_counts}" + ) + elif expectation == "fallback": + valid = integer("valid") + ready_count = integer("ready") + core_fallback_count = integer("fallback") + view_count = integer("views") + if valid == 0: + if ready_count != 0 or (core_fallback_count or 0) <= 0: + failures.append( + "shared interaction whole-view core fallback accounting=" + f"{fields.get('ready', 'missing')}/" + f"{fields.get('fallback', 'missing')}" + ) + if fields.get("status") != "fallback": + failures.append( + f"shared interaction status={fields.get('status', 'missing')}" + ) + for name in ("lights", "surfaces", "primitives", "draw", "noop"): + if integer(name) != 0: + failures.append( + f"shared interaction rollback {name}=" + f"{fields.get(name, 'missing')}" + ) + if shadow_counts != (0, 0, 0, 0, 0, 0): + failures.append( + "shared interaction rollback shadow accounting=" + f"{fields.get('shadow', 'missing')}/" + f"{fields.get('volumes', 'missing')}" + ) + if map_counts != (0, 0, 0, 0, 0) or map_records: + failures.append( + "shared interaction rollback map accounting=" + f"{fields.get('maps', 'missing')}/" + f"{fields.get('modes', 'missing')}/" + f"{fields.get('csm', 'missing')} records={len(map_records)}" + ) + if view_count != core_fallback_count: + failures.append( + "shared interaction complete-view fallback=" + f"{fields.get('fallback', 'missing')}/" + f"{fields.get('views', 'missing')}" + ) + elif valid == 1: + if ( + ready_count is None + or ready_count <= 0 + or core_fallback_count != 0 + or view_count != ready_count + ): + failures.append( + "shared interaction backend fallback preparation=" + f"{fields.get('ready', 'missing')}/" + f"{fields.get('fallback', 'missing')}/" + f"{fields.get('views', 'missing')}" + ) + if fields.get("status") != "ready": + failures.append( + f"shared interaction status={fields.get('status', 'missing')}" + ) + primitive_count = integer("primitives") + draw_count = integer("draw") + noop_count = integer("noop") + if ( + primitive_count is None + or draw_count is None + or noop_count is None + or primitive_count != draw_count + noop_count + ): + failures.append( + "shared interaction fallback primitive plan=" + f"{fields.get('primitives', 'missing')}/" + f"{fields.get('draw', 'missing')}+" + f"{fields.get('noop', 'missing')}" + ) + if shadow_counts is None: + failures.append("shared interaction fallback shadow plan=missing") + else: + _, casters, drawable, noop, logical, preload = shadow_counts + if casters != drawable + noop or preload > logical: + failures.append( + "shared interaction fallback shadow plan=" + f"{fields.get('shadow', 'missing')}/" + f"{fields.get('volumes', 'missing')}" + ) + if map_counts is None: + failures.append("shared interaction fallback map plan=missing") + else: + maps, hybrid, projected, point, csm = map_counts + fallback_record_keys = { + (record["view"], record["light"], record["receiver"]) + for record in map_records + } + fallback_record_shape = ( + sum(record["class"] != "point" for record in map_records), + sum(record["class"] == "point" for record in map_records), + sum( + record["class"] != "point" + and int(record["cascades"]) > 1 + for record in map_records + ), + sum(record["mode"] == "hybrid" for record in map_records), + ) + if ( + projected + point != maps + or hybrid > maps + or csm > projected + or len(map_records) != maps + or len(fallback_record_keys) != maps + or fallback_record_shape != (projected, point, csm, hybrid) + ): + failures.append( + "shared interaction fallback mapped plan=" + f"{map_counts} records={len(map_records)}/" + f"{len(fallback_record_keys)} shape={fallback_record_shape}" + ) + fallback_light_count = integer("lights") + fallback_view_count = integer("views") + for record in map_records: + cascades = int(record["cascades"]) + feature_values = tuple( + int(record[name]) + for name in ("static", "dynamic", "alpha", "translucent") + ) + if ( + int(record["casters"]) <= 0 + or int(record["generation"]) <= 0 + or int(record["plan"]) == 0 + or int(record["hash"]) == 0 + or int(record["alias"]) not in (0, 1) + or any(value not in (0, 1) for value in feature_values) + or not (int(record["static"]) or int(record["dynamic"])) + or int(record["translucent"]) != 0 + or (record["class"] == "point" and cascades != 6) + or (record["class"] != "point" and not 1 <= cascades <= 4) + or ( + record["mode"] == "hybrid" + and int(record["supplements"]) <= 0 + ) + or fallback_light_count is None + or not 0 <= int(record["light"]) < fallback_light_count + or fallback_view_count is None + or not 0 <= int(record["view"]) < fallback_view_count + ): + failures.append( + "shared interaction invalid fallback per-map record=" + f"{record}" + ) + if int(record["alias"]) == 1: + owner_key = (record["view"], record["light"], "local") + if ( + record["receiver"] != "global" + or owner_key not in fallback_record_keys + ): + failures.append( + "shared interaction invalid fallback map alias=" + f"{record}" + ) + if maps > 0: + fallback_per_light_casters: dict[tuple[int, int], int] = {} + for record in map_records: + light_key = (int(record["view"]), int(record["light"])) + fallback_per_light_casters[light_key] = max( + fallback_per_light_casters.get(light_key, 0), + int(record["casters"]) + + int(record["supplements"]), + ) + fallback_record_caster_floor = sum( + fallback_per_light_casters.values() + ) + if ( + shadow_counts is None + or shadow_counts[0] < len(fallback_per_light_casters) + or shadow_counts[1] <= 0 + or fallback_record_caster_floor > shadow_counts[1] + ): + failures.append( + "shared interaction fallback mapped caster accounting=" + f"recordsMin={fallback_record_caster_floor} lights=" + f"{len(fallback_per_light_casters)} shadow=" + f"{fields.get('shadow', 'missing')}" + ) + else: + failures.append( + f"shared interaction valid={fields.get('valid', 'missing')}" + ) + + backend_counts = shared_interaction_backend_counts( + view_fields.get(backend_name, "") + ) + if backend_counts is None: + failures.append( + f"shared interaction {backend_name} fallback detail=" + f"{view_fields.get(backend_name, 'missing')}" + ) + else: + ( + backend_outcome, + backend_failure, + backend_detail, + backend_drawn, + backend_noop, + backend_shadow, + backend_shadow_noop, + backend_logical, + backend_preload, + backend_maps, + backend_hybrid, + ) = backend_counts + if backend_outcome != 2 or backend_failure in ("none", "unknown"): + failures.append( + f"shared interaction {backend_name} named fallback=" + f"{view_fields.get(backend_name, 'missing')}" + ) + shadow_expectation = spec.interaction_shadow_expectation + if shadow_expectation == "translucent-fallback": + try: + view_failure_detail = int(view_fields.get("detail", "")) + except ValueError: + view_failure_detail = -1 + if ( + valid != 0 + or view_fields.get("failure") != "shadowMap" + or view_failure_detail not in TRANSLUCENT_SHADOW_CHAIN_DETAILS + or backend_failure != "shadowMap" + or backend_detail != view_failure_detail + ): + failures.append( + "shared interaction translucent-moment fallback=" + f"view={view_fields.get('failure', 'missing')}/" + f"{view_fields.get('detail', 'missing')} backend=" + f"{backend_name}:{backend_failure}/{backend_detail}" + ) + elif shadow_expectation == "map-budget-fallback": + expected_backend_failure = ( + backend_failure == "backendRejected" + and (backend_detail & 0xFFFFFFFF) + == GL_MAP_BUDGET_FALLBACK_DETAIL + if backend_name == "GL" + else backend_failure == "shadowMap" + and backend_detail == VK_SHADOW_STATE_REJECT + ) + if ( + valid != 1 + or view_fields.get("failure") != "none" + or view_fields.get("detail") != "0" + or map_counts is None + or map_counts != CONTROLLED_MAP_BUDGET_COUNTS + or not expected_backend_failure + ): + failures.append( + "shared interaction map-budget admission fallback=" + f"view={view_fields.get('failure', 'missing')}/" + f"{view_fields.get('detail', 'missing')} maps={map_counts} " + f"backend={backend_name}:{backend_failure}/{backend_detail}" + ) + if any( + value != 0 + for value in ( + backend_drawn, + backend_noop, + backend_shadow, + backend_shadow_noop, + backend_logical, + backend_preload, + backend_maps, + backend_hybrid, + ) + ): + failures.append( + f"shared interaction {backend_name} atomic fallback=" + f"{view_fields.get(backend_name, 'missing')}" + ) + if ( + view_count is None + or owned_views != 0 + or fallback_views != view_count + or mismatches != 0 + ): + failures.append(f"shared interaction {backend_name} coverage={backend_value}") + else: + failures.append(f"unknown shared interaction expectation {expectation}") + return failures + + +def evaluate_shared_fog_blend_evidence( + spec: RunSpec, summary: dict[str, str] +) -> list[str]: + expectation = spec.fog_blend_expectation + if expectation == "none": + return [] + + fields = shared_fog_blend_fields(summary) + backends = shared_fog_blend_backend_records(summary) + view_records = shared_fog_blend_view_records(summary) + if not fields: + return ["classicFogBlendDomain evidence line"] + + failures: list[str] = [] + + def integer(record: dict[str, str], name: str) -> int | None: + try: + return int(record[name]) + except (KeyError, ValueError): + return None + + aggregate_counts = ( + "overflow", + "views", + "ready", + "fallback", + "lights", + "fog", + "blend", + "noopLights", + "surfaces", + "global", + "local", + "stages", + "active", + "inactive", + "noopStages", + "primitives", + "fogReceivers", + "fogCaps", + "blendDraws", + "noop", + "textures", + ) + backend_counts = ( + "ownedViews", + "fallbackViews", + "ownedFogReceivers", + "ownedFogCaps", + "ownedBlend", + "ownedNoops", + "ownedNoopStages", + "ownedNoopLights", + "mismatches", + "duplicate", + "untracked", + ) + if expectation == "disabled": + for name in ("requested", "prepared", "frameValid", *aggregate_counts): + if integer(fields, name) != 0: + failures.append( + f"classicFogBlendDomain {name}={fields.get(name, 'missing')}" + ) + for backend_name in ("GL", "Vulkan"): + record = backends.get(backend_name, {}) + for name in backend_counts: + if integer(record, name) != 0: + failures.append( + "classicFogBlendDomain disabled backend " + f"{backend_name} {name}={record.get(name, 'missing')}" + ) + return failures + + views = integer(fields, "views") + active_backend = "Vulkan" if spec.render_api == "vk" else "GL" + inactive_backend = "GL" if active_backend == "Vulkan" else "Vulkan" + active = backends.get(active_backend, {}) + inactive = backends.get(inactive_backend, {}) + if expectation == "fallback": + for name in ("requested", "prepared"): + if integer(fields, name) != 1: + failures.append( + f"classicFogBlendDomain {name}={fields.get(name, 'missing')}" + ) + if integer(fields, "overflow") != 0 or views is None or views <= 0: + failures.append( + "classicFogBlendDomain fallback aggregate=" + f"overflow={fields.get('overflow', 'missing')} " + f"views={fields.get('views', 'missing')}" + ) + domain_fallback = integer(fields, "frameValid") == 0 + backend_fallback = integer(fields, "frameValid") == 1 + if domain_fallback: + if ( + integer(fields, "ready") != 0 + or integer(fields, "fallback") != views + or fields.get("status") != "fallback" + ): + failures.append( + "classicFogBlendDomain domain fallback state=" + f"ready={fields.get('ready', 'missing')} " + f"fallback={fields.get('fallback', 'missing')} " + f"status={fields.get('status', 'missing')}" + ) + for name in aggregate_counts[4:]: + if integer(fields, name) != 0: + failures.append( + "classicFogBlendDomain domain fallback retained " + f"{name}={fields.get(name, 'missing')}" + ) + elif backend_fallback: + if ( + integer(fields, "ready") != views + or integer(fields, "fallback") != 0 + or fields.get("status") != "ready" + ): + failures.append( + "classicFogBlendDomain backend fallback state=" + f"ready={fields.get('ready', 'missing')} " + f"fallback={fields.get('fallback', 'missing')} " + f"status={fields.get('status', 'missing')}" + ) + else: + failures.append( + "classicFogBlendDomain fallback frameValid=" + f"{fields.get('frameValid', 'missing')}" + ) + + if integer(active, "ownedViews") != 0 or integer( + active, "fallbackViews" + ) != views: + failures.append( + f"classicFogBlendDomain {active_backend} fallback coverage" + ) + for name in ( + "ownedFogReceivers", + "ownedFogCaps", + "ownedBlend", + "ownedNoops", + "ownedNoopStages", + "ownedNoopLights", + ): + if integer(active, name) != 0: + failures.append( + f"classicFogBlendDomain {active_backend} atomic fallback " + f"{name}={active.get(name, 'missing')}" + ) + active_prefix = "vk" if active_backend == "Vulkan" else "gl" + inactive_prefix = "gl" if active_prefix == "vk" else "vk" + if len(view_records) != views: + failures.append( + "classicFogBlendDomain fallback view records=" + f"{len(view_records)}/{views}" + ) + for record in view_records: + active_failure = record.get(f"{active_prefix}Failure") + active_detail = record.get(f"{active_prefix}Detail") + valid_domain_failure = ( + domain_fallback + and record.get("ready") == 0 + and record.get("failure") not in ("none", "unknown") + and active_failure == record.get("failure") + and active_detail == record.get("detail") + ) + valid_backend_failure = ( + backend_fallback + and record.get("ready") == 1 + and record.get("failure") == "none" + and active_failure not in ("none", "unknown") + ) + if ( + record.get(f"{active_prefix}Outcome") != 2 + or active_detail == 0 + or record.get(f"{inactive_prefix}Outcome") != 0 + or not (valid_domain_failure or valid_backend_failure) + ): + failures.append( + "classicFogBlendDomain named atomic fallback=" + f"{record}" + ) + for name in ("mismatches", "duplicate", "untracked"): + if integer(active, name) != 0: + failures.append( + f"classicFogBlendDomain {active_backend} {name}=" + f"{active.get(name, 'missing')}" + ) + for name in backend_counts: + if integer(inactive, name) != 0: + failures.append( + f"classicFogBlendDomain inactive {inactive_backend} {name}=" + f"{inactive.get(name, 'missing')}" + ) + return failures + + for name in ("requested", "prepared", "frameValid"): + if integer(fields, name) != 1: + failures.append( + f"classicFogBlendDomain {name}={fields.get(name, 'missing')}" + ) + if integer(fields, "overflow") != 0 or integer(fields, "fallback") != 0: + failures.append( + "classicFogBlendDomain rollback accounting=" + f"{fields.get('overflow', 'missing')}/{fields.get('fallback', 'missing')}" + ) + if views is None or views <= 0 or integer(fields, "ready") != views: + failures.append( + "classicFogBlendDomain complete-view readiness=" + f"{fields.get('ready', 'missing')}/{fields.get('views', 'missing')}" + ) + positive_counts = [ + "lights", + "fog", + "blend", + "surfaces", + "stages", + "active", + "primitives", + "fogReceivers", + "fogCaps", + "textures", + ] + if expectation != "owned-skip-blend": + positive_counts.append("blendDraws") + for name in positive_counts: + value = integer(fields, name) + if value is None or value <= 0: + failures.append( + f"classicFogBlendDomain {name}={fields.get(name, 'missing')}" + ) + if expectation == "owned-skip-blend": + if integer(fields, "blendDraws") != 0: + failures.append( + "classicFogBlendDomain skip-blend draw accounting=" + f"{fields.get('blendDraws', 'missing')}" + ) + noop_lights = integer(fields, "noopLights") + if noop_lights is None or noop_lights <= 0: + failures.append( + "classicFogBlendDomain skip-blend light accounting=" + f"{fields.get('noopLights', 'missing')}" + ) + if integer(fields, "lights") != ( + (integer(fields, "fog") or 0) + (integer(fields, "blend") or 0) + ): + failures.append("classicFogBlendDomain light-class accounting") + if integer(fields, "surfaces") != ( + (integer(fields, "global") or 0) + (integer(fields, "local") or 0) + ): + failures.append("classicFogBlendDomain receiver-class accounting") + if fields.get("status") != "ready": + failures.append( + f"classicFogBlendDomain status={fields.get('status', 'missing')}" ) - if "thresholdPass" not in summary: - for benchmark_line in (summary["benchmarkCapture"], summary["benchmarkInfo"]): - match = re.search(r"\b(?:thresholdPass|pass)\s*=\s*(\d+)\b", benchmark_line, re.IGNORECASE) - if match: - summary["thresholdPass"] = match.group(1) - break - summary.update(parse_frame_pacing(summary["framePacing"])) - return summary - -def summary_float(summary: dict[str, str], key: str) -> float | None: - value = summary.get(key) - if value is None: - return None - try: - return float(value) - except ValueError: - return None + if integer(active, "ownedViews") != views or integer(active, "fallbackViews") != 0: + failures.append( + f"classicFogBlendDomain {active_backend} view coverage" + ) + for backend_name, aggregate_name in ( + ("ownedFogReceivers", "fogReceivers"), + ("ownedFogCaps", "fogCaps"), + ("ownedBlend", "blendDraws"), + ("ownedNoops", "noop"), + ("ownedNoopStages", "noopStages"), + ("ownedNoopLights", "noopLights"), + ): + if integer(active, backend_name) != integer(fields, aggregate_name): + failures.append( + f"classicFogBlendDomain {active_backend} {backend_name} accounting" + ) + for name in ("mismatches", "duplicate", "untracked"): + if integer(active, name) != 0: + failures.append( + f"classicFogBlendDomain {active_backend} {name}=" + f"{active.get(name, 'missing')}" + ) + for name in backend_counts: + if integer(inactive, name) != 0: + failures.append( + f"classicFogBlendDomain inactive {inactive_backend} {name}=" + f"{inactive.get(name, 'missing')}" + ) + return failures def load_tga_rgb(path: Path) -> tuple[int, int, bytes]: @@ -901,6 +3609,71 @@ def compare_tga(actual: Path, reference: Path) -> dict[str, Any]: } +def evaluate_display_evidence( + sources: Iterable[str], + screenshot: Path | None, + expected_width: int = BUDGET_WIDTH, + expected_height: int = BUDGET_HEIGHT, +) -> tuple[dict[str, Any], list[str]]: + """Bind budget evidence to the renderer's actual mode and TGA dimensions.""" + expected = { + "modeSelector": "-1", + "width": expected_width, + "height": expected_height, + "presentation": "windowed", + } + evidence: dict[str, Any] = { + "expected": expected, + "runtime": None, + "screenshot": None, + } + failures: list[str] = [] + runtime_values = { + (selector.strip(), int(width), int(height), presentation.casefold()) + for source in sources + for selector, width, height, presentation in RUNTIME_DISPLAY_MODE_PATTERN.findall(source) + } + if not runtime_values: + failures.append("runtime MODE evidence is missing") + elif len(runtime_values) != 1: + failures.append("runtime MODE evidence is conflicting") + else: + selector, width, height, presentation = next(iter(runtime_values)) + runtime = { + "modeSelector": selector, + "width": width, + "height": height, + "presentation": presentation, + } + evidence["runtime"] = runtime + if runtime != expected: + failures.append( + "runtime MODE is " + f"{selector}, {width}x{height} {presentation}; expected -1, " + f"{expected_width}x{expected_height} windowed" + ) + + if screenshot is None: + failures.append("engine screenshot is missing for display verification") + else: + try: + width, height, _ = load_tga_rgb(screenshot) + except (OSError, ValueError) as exc: + failures.append(f"engine screenshot is not a valid TGA: {exc}") + else: + screenshot_evidence = {"width": width, "height": height} + evidence["screenshot"] = screenshot_evidence + if screenshot_evidence != { + "width": expected_width, + "height": expected_height, + }: + failures.append( + f"engine screenshot is {width}x{height}; expected " + f"{expected_width}x{expected_height}" + ) + return evidence, failures + + def screenshot_reference_candidates( reference_dir: Path, screenshot: Path, savepath: Path, case_id: str | None = None ) -> list[Path]: @@ -917,6 +3690,10 @@ def screenshot_reference_candidates( candidates.insert(0, reference_dir / rel) if case_id: candidates.insert(0, reference_dir / case_id / rel) + # A reference tree produced by this runner retains both the + # case savepath and game-directory components: + # savepaths//baseoq4/screenshots/... + candidates.insert(0, reference_dir / case_id / game_dir / rel) except ValueError: pass return candidates @@ -954,6 +3731,52 @@ def compare_screenshot_if_requested( return result +def compare_screenshot_difference_if_requested( + screenshot: Path | None, + savepath: Path, + reference_dir: Path | None, + min_rms: float, + min_differing_channels: int, + case_id: str | None = None, +) -> dict[str, Any]: + """Require a material image delta from a second engine-TGA reference. + + Equivalence and effectiveness are separate claims. The ordinary reference + comparison proves shared-off/shared-on parity under identical renderer + settings; this comparison proves that a controlled feature-on scene is + actually different from its feature-disabled capture. Eligible features + include interaction shadows, fog/blend, and material deforms. + """ + if screenshot is None: + return {"status": "missing-screenshot", "pass": False} + if reference_dir is None: + return {"status": "not-requested"} + for candidate in screenshot_reference_candidates( + reference_dir, screenshot, savepath, case_id + ): + if not candidate.exists(): + continue + comparison = compare_tga(screenshot, candidate) + comparison["actual"] = str(screenshot) + comparison["reference"] = str(candidate) + comparison["minimumRms"] = min_rms + comparison["minimumDifferingChannels"] = min_differing_channels + if comparison["status"] == "compared": + comparison["status"] = "difference-compared" + comparison["pass"] = ( + comparison["rms"] >= min_rms + and comparison["differingChannels"] >= min_differing_channels + ) + else: + comparison["pass"] = False + return comparison + return { + "status": "missing-difference-reference", + "referenceDir": str(reference_dir), + "pass": False, + } + + def evaluate_role_result( spec: RunSpec, role: str, @@ -973,6 +3796,13 @@ def evaluate_role_result( min_pacing_hz: float = 0.0, max_p95_ms: float = 0.0, max_p99_ms: float = 0.0, + budget_contract: dict[str, Any] | None = None, + budget_profile: str = "baseline", + difference_reference_dir: Path | None = None, + difference_min_rms: float = 0.1, + difference_min_channels: int = 1000, + pbr_fixture_acceptance_required: bool = False, + autoexec_path: Path | None = None, ) -> dict[str, Any]: log_path = find_log(savepath, log_name) diagnostic_sources = ( @@ -985,6 +3815,12 @@ def evaluate_role_result( warnings = warning_counts(text) failure_diagnostics, failure_diagnostics_omitted = collect_failure_diagnostics(diagnostic_sources) summary = extract_summary(text) + pbr_fixture_evidence, pbr_fixture_failures = evaluate_pbr_fixture_evidence( + summary, pbr_fixture_acceptance_required, spec.render_api + ) + map_evidence, map_evidence_failures = evaluate_controlled_map_evidence( + diagnostic_sources, pbr_fixture_acceptance_required + ) image = compare_screenshot_if_requested( screenshot, savepath, @@ -994,7 +3830,49 @@ def evaluate_role_result( require_reference, spec.id, ) + is_deform_scene = spec.case_id in DEFORM_SCENES + difference_feature_case = ( + is_deform_scene + or spec.fog_blend_expectation in ("owned", "owned-skip-blend") + or spec.interaction_shadow_expectation + in ( + "stencil", + "projected", + "point", + "mapped", + "csm", + "mixed", + "dynamic", + "perforated", + "hybrid", + ) + ) + difference_required = ( + difference_reference_dir is not None and difference_feature_case + ) + difference_reference_case_id = ( + spec.id + if ( + is_deform_scene + or spec.fog_blend_expectation in ("owned", "owned-skip-blend") + ) + else spec.id_for_shadow_preset("unshadowed") + ) + image_difference = ( + compare_screenshot_difference_if_requested( + screenshot, + savepath, + difference_reference_dir, + difference_min_rms, + difference_min_channels, + difference_reference_case_id, + ) + if difference_required + else {"status": "not-requested"} + ) missing: list[str] = [] + budget_evidence: dict[str, Any] = {} + display_evidence: dict[str, Any] = {} if timed_out: missing.append("timeout") if log_path is None: @@ -1026,14 +3904,66 @@ def evaluate_role_result( missing.append("frame pacing p99") elif p99_ms > max_p99_ms: missing.append(f"pacingP99={p99_ms:.1f}>{max_p99_ms:.1f}") - if "Selected renderer tier:" not in text: + missing.extend(evaluate_shared_interaction_evidence(spec, summary)) + missing.extend(evaluate_shared_fog_blend_evidence(spec, summary)) + missing.extend(pbr_fixture_failures) + missing.extend(map_evidence_failures) + if spec.render_api == "gl" and "Selected renderer tier:" not in text: missing.append("selected tier line") + if pbr_fixture_acceptance_required and not require_benchmark: + if ( + spec.case_id != PBR_FIXTURE_BENCHMARK_CASE + or spec.mode != "SP" + or normalize_engine_map_name(spec.map_name) + != normalize_engine_map_name(PBR_FIXTURE_STOCK_MAP) + ): + missing.append("controlled fixture case/mode/map identity") + if spec.render_api == "gl": + selected_tiers: set[str] = set() + for _, source_text in diagnostic_sources: + tier_matches = list( + re.finditer( + r"^Selected renderer tier:\s*([A-Za-z0-9]+)\s*$", + source_text, + re.IGNORECASE | re.MULTILINE, + ) + ) + if tier_matches: + selected_tiers.add(tier_matches[-1].group(1).casefold()) + expected_runtime_tier = GL_TIER_RUNTIME_NAMES.get(spec.tier) + if ( + expected_runtime_tier is None + or selected_tiers != {expected_runtime_tier.casefold()} + ): + missing.append( + "controlled GL fixture selected tier must match the requested tier" + ) if screenshot is None: missing.append("screenshot") if any(count > 0 for count in warnings.values()): missing += [f"{name}={count}" for name, count in warnings.items() if count > 0] if image.get("pass") is False: missing.append(f"image comparison {image.get('status')}") + if image_difference.get("pass") is False: + missing.append( + "image difference " + f"{image_difference.get('status')} rms=" + f"{image_difference.get('rms', 'missing')} channels=" + f"{image_difference.get('differingChannels', 'missing')}" + ) + display_evidence, display_failures = evaluate_display_evidence( + (item[1] for item in diagnostic_sources), screenshot + ) + missing.extend(f"display evidence: {failure}" for failure in display_failures) + if require_benchmark and budget_contract is not None: + budget_evidence, budget_failures = evaluate_timing_evidence( + (item[1] for item in diagnostic_sources), + budget_contract, + spec.budget_map_name, + spec.expected_backend, + budget_profile, + ) + missing.extend(f"renderer budget: {failure}" for failure in budget_failures) ok = exit_code == 0 and not timed_out and not missing return { @@ -1046,6 +3976,7 @@ def evaluate_role_result( "log": str(log_path) if log_path is not None else "", "stdout": str(stdout_path), "stderr": str(stderr_path), + "autoexecCfg": str(autoexec_path) if autoexec_path is not None else "", "screenshot": str(screenshot) if screenshot is not None else "", "screenshotRequest": screenshot_rel, "warnings": warnings, @@ -1054,6 +3985,11 @@ def evaluate_role_result( "missing": missing, "summary": summary, "image": image, + "imageDifference": image_difference, + "displayEvidence": display_evidence, + "budgetEvidence": budget_evidence, + "pbrFixtureEvidence": pbr_fixture_evidence, + "mapEvidence": map_evidence, } @@ -1093,9 +4029,12 @@ def run_sp_spec( spec: RunSpec, args: argparse.Namespace, ) -> dict[str, Any]: + pbr_fixture_acceptance_required = requires_pbr_fixture_acceptance( + args.extra_cvars, spec.render_api + ) savepath = output_dir / "savepaths" / spec.id savepath.mkdir(parents=True, exist_ok=True) - log_name = f"openq4_gameplay_{spec.id}.log" + log_name = ROLE_LOG_NAME log_path = find_log(savepath, log_name) if log_path is not None: log_path.unlink() @@ -1116,10 +4055,13 @@ def run_sp_spec( ) game_args = common_args( root, + args.runtime_dir_path, savepath, log_name, basepath, spec, + args.width, + args.height, args.benchmark_preset, args.modern_executor, args.show_fps_overlay, @@ -1135,6 +4077,14 @@ def run_sp_spec( "id": spec.id, "mode": spec.mode, "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "interactionExpectation": spec.interaction_expectation, + "interactionShadowExpectation": spec.interaction_shadow_expectation, + "fogBlendExpectation": spec.fog_blend_expectation, + "pbrFixtureAcceptanceRequired": pbr_fixture_acceptance_required, + "displayContract": display_launch_contract(spec, args.width, args.height), "status": "planned", "args": game_args, "autoexecCfg": autoexec_cfg, @@ -1145,7 +4095,7 @@ def run_sp_spec( exit_code, timed_out, elapsed = launch_and_wait( executable, game_args, - root / ".install", + args.runtime_dir_path, stdout_path, stderr_path, args.timeout, @@ -1165,15 +4115,30 @@ def run_sp_spec( args.image_rms_threshold, args.image_max_threshold, args.require_references, - not args.pacing_only, - args.min_pacing_hz, - args.max_p95_ms, - args.max_p99_ms, + require_benchmark=not args.pacing_only, + min_pacing_hz=args.min_pacing_hz, + max_p95_ms=args.max_p95_ms, + max_p99_ms=args.max_p99_ms, + budget_contract=args.budget_contract, + budget_profile=args.benchmark_preset, + difference_reference_dir=args.difference_reference_dir_path, + difference_min_rms=args.image_difference_min_rms, + difference_min_channels=args.image_difference_min_channels, + pbr_fixture_acceptance_required=pbr_fixture_acceptance_required, + autoexec_path=savepath / "baseoq4" / Path(autoexec_cfg), ) return { "id": spec.id, "mode": spec.mode, "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "interactionExpectation": spec.interaction_expectation, + "interactionShadowExpectation": spec.interaction_shadow_expectation, + "fogBlendExpectation": spec.fog_blend_expectation, + "pbrFixtureAcceptanceRequired": pbr_fixture_acceptance_required, + "displayContract": display_launch_contract(spec, args.width, args.height), "purpose": spec.purpose, "tier": spec.tier, "maxfps": spec.maxfps, @@ -1196,14 +4161,17 @@ def run_mp_spec( index: int, args: argparse.Namespace, ) -> dict[str, Any]: + pbr_fixture_acceptance_required = requires_pbr_fixture_acceptance( + args.extra_cvars, spec.render_api + ) port = args.mp_port + index server_savepath = output_dir / "savepaths" / f"{spec.id}_server" client_savepath = output_dir / "savepaths" / f"{spec.id}_client" server_savepath.mkdir(parents=True, exist_ok=True) client_savepath.mkdir(parents=True, exist_ok=True) - server_log = f"openq4_gameplay_{spec.id}_server.log" - client_log = f"openq4_gameplay_{spec.id}_client.log" + server_log = ROLE_LOG_NAME + client_log = ROLE_LOG_NAME for savepath, log_name in ((server_savepath, server_log), (client_savepath, client_log)): log_path = find_log(savepath, log_name) if log_path is not None: @@ -1215,9 +4183,12 @@ def run_mp_spec( client_stderr = output_dir / f"{spec.id}_client.err.txt" server_settle_frames = args.settle_frames + args.mp_client_delay_frames - server_exec_commands = args.exec_commands - if spec.path_name == "postinit-connect": - server_exec_commands += (f"waitMsec {POSTINIT_SERVER_GRACE_MSEC}",) + # Keep the listen server alive while a fresh client initializes and builds + # its local caches. A frame-count grace collapses to only a few seconds at + # the 240 FPS budget workload and can let the server quit mid-connect. + server_exec_commands = args.exec_commands + ( + f"waitMsec {MP_SERVER_CLIENT_GRACE_MSEC}", + ) server_autoexec_cfg, server_screenshot = write_autoexec_cfg( server_savepath, spec, @@ -1233,10 +4204,13 @@ def run_mp_spec( ) server_args = common_args( root, + args.runtime_dir_path, server_savepath, server_log, basepath, spec, + args.width, + args.height, args.benchmark_preset, args.modern_executor, args.show_fps_overlay, @@ -1246,8 +4220,9 @@ def run_mp_spec( ) append_set(server_args, "net_serverDedicated", "0") append_set(server_args, "net_port", str(port)) - server_args += ["+seta", "si_pure", "0"] - append_set(server_args, "net_serverAllowServerMod", "1") + append_set(server_args, "ui_autoJoin", "1") + server_args += ["+seta", "si_pure", "1"] + append_set(server_args, "net_serverAllowServerMod", "0") append_set(server_args, "sv_cheats", "1") append_set(server_args, "si_gameType", "DM") append_command(server_args, "spawnServer", spec.map_name) @@ -1278,10 +4253,13 @@ def run_mp_spec( client_initial_autoexec_cfg = client_reconnect_cfg client_args = common_args( root, + args.runtime_dir_path, client_savepath, client_log, basepath, spec, + args.width, + args.height, args.benchmark_preset, args.modern_executor, args.show_fps_overlay, @@ -1289,6 +4267,7 @@ def run_mp_spec( client_initial_autoexec_cfg, args.autoexec_delay_ms, ) + append_set(client_args, "ui_autoJoin", "1") append_set(client_args, "ui_name", "RendererBenchClient") client_postinit_connect_cfg = "" if spec.path_name == "postinit-connect": @@ -1303,6 +4282,14 @@ def run_mp_spec( "id": spec.id, "mode": spec.mode, "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "interactionExpectation": spec.interaction_expectation, + "interactionShadowExpectation": spec.interaction_shadow_expectation, + "fogBlendExpectation": spec.fog_blend_expectation, + "pbrFixtureAcceptanceRequired": pbr_fixture_acceptance_required, + "displayContract": display_launch_contract(spec, args.width, args.height), "status": "planned", "serverArgs": server_args, "clientArgs": client_args, @@ -1322,7 +4309,7 @@ def run_mp_spec( with server_stdout.open("w", encoding="utf-8", errors="replace") as server_out, server_stderr.open("w", encoding="utf-8", errors="replace") as server_err: server_process = subprocess.Popen( [str(executable)] + server_args, - cwd=str(root / ".install"), + cwd=str(args.runtime_dir_path), stdout=server_out, stderr=server_err, ) @@ -1330,7 +4317,7 @@ def run_mp_spec( with client_stdout.open("w", encoding="utf-8", errors="replace") as client_out, client_stderr.open("w", encoding="utf-8", errors="replace") as client_err: client_process = subprocess.Popen( [str(executable)] + client_args, - cwd=str(root / ".install"), + cwd=str(args.runtime_dir_path), stdout=client_out, stderr=client_err, ) @@ -1366,10 +4353,17 @@ def run_mp_spec( args.image_rms_threshold, args.image_max_threshold, args.require_references, - not args.pacing_only, - args.min_pacing_hz, - args.max_p95_ms, - args.max_p99_ms, + require_benchmark=not args.pacing_only, + min_pacing_hz=args.min_pacing_hz, + max_p95_ms=args.max_p95_ms, + max_p99_ms=args.max_p99_ms, + budget_contract=args.budget_contract, + budget_profile=args.benchmark_preset, + difference_reference_dir=args.difference_reference_dir_path, + difference_min_rms=args.image_difference_min_rms, + difference_min_channels=args.image_difference_min_channels, + pbr_fixture_acceptance_required=pbr_fixture_acceptance_required, + autoexec_path=server_savepath / "baseoq4" / Path(server_autoexec_cfg), ) client_result = evaluate_role_result( spec, @@ -1386,10 +4380,17 @@ def run_mp_spec( args.image_rms_threshold, args.image_max_threshold, args.require_references, - not args.pacing_only, - args.min_pacing_hz, - args.max_p95_ms, - args.max_p99_ms, + require_benchmark=not args.pacing_only, + min_pacing_hz=args.min_pacing_hz, + max_p95_ms=args.max_p95_ms, + max_p99_ms=args.max_p99_ms, + budget_contract=args.budget_contract, + budget_profile=args.benchmark_preset, + difference_reference_dir=args.difference_reference_dir_path, + difference_min_rms=args.image_difference_min_rms, + difference_min_channels=args.image_difference_min_channels, + pbr_fixture_acceptance_required=pbr_fixture_acceptance_required, + autoexec_path=client_savepath / "baseoq4" / Path(client_autoexec_cfg), ) postinit_connect_responses: dict[str, int] = {} postinit_ttf_rebuilds: dict[str, int] = {} @@ -1428,6 +4429,14 @@ def run_mp_spec( "id": spec.id, "mode": spec.mode, "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "interactionExpectation": spec.interaction_expectation, + "interactionShadowExpectation": spec.interaction_shadow_expectation, + "fogBlendExpectation": spec.fog_blend_expectation, + "pbrFixtureAcceptanceRequired": pbr_fixture_acceptance_required, + "displayContract": display_launch_contract(spec, args.width, args.height), "purpose": spec.purpose, "tier": spec.tier, "maxfps": spec.maxfps, @@ -1469,6 +4478,12 @@ def harness_failure_result(spec: RunSpec, exc: Exception) -> dict[str, Any]: "id": spec.id, "mode": spec.mode, "map": spec.map_name, + "budgetMap": spec.budget_map_name, + "expectedBackend": spec.expected_backend, + "renderApi": spec.render_api, + "interactionExpectation": spec.interaction_expectation, + "interactionShadowExpectation": spec.interaction_shadow_expectation, + "fogBlendExpectation": spec.fog_blend_expectation, "purpose": spec.purpose, "tier": spec.tier, "maxfps": spec.maxfps, @@ -1482,6 +4497,143 @@ def harness_failure_result(spec: RunSpec, exc: Exception) -> dict[str, Any]: } +def cvar_value_enabled(value: str) -> bool: + # CVAR_BOOL canonicalization uses atoi() in the engine. Mirror its leading + # integer semantics so values such as "0.0", "00", and "true" do not make + # the harness expect ownership that the renderer will leave disabled. + match = re.match(r"^\s*([+-]?\d+)", value) + return match is not None and int(match.group(1)) != 0 + + +def effective_interaction_cvars( + args: argparse.Namespace, shadow_preset: str +) -> dict[str, str]: + effective = { + "r_renderersharedworldinteraction": "0", + **{ + name.casefold(): value + for name, value in SHADOW_PRESETS[shadow_preset].items() + }, + } + for name, value in args.extra_cvars: + effective[name.casefold()] = value + return effective + + +def interaction_expectation( + args: argparse.Namespace, case_id: str, shadow_preset: str +) -> str: + scene = ALL_SCENES.get(case_id, {}) + if case_id not in INTERACTION_SCENES and "interactionShadowTarget" not in scene: + return "none" + effective = effective_interaction_cvars(args, shadow_preset) + if not cvar_value_enabled( + effective.get("r_renderersharedworldinteraction", "0") + ): + return "disabled" + shadows_enabled = cvar_value_enabled(effective.get("r_shadows", "0")) + maps_enabled = shadows_enabled and cvar_value_enabled( + effective.get("r_useshadowmap", "0") + ) + if shadow_preset == "map-budget-fallback" and maps_enabled: + return "fallback" + # Debug overlays and translucent moment-map caster updates intentionally + # stay outside the shared fixed-classic receiver corridor. Ordinary + # stencil, projected/CSM, point-cube, and mixed map/stencil presets are + # expected to retain whole-view shared ownership once their shadow state + # has settled. + if ( + maps_enabled + and ( + cvar_value_enabled(effective.get("r_shadowmapdebugoverlay", "0")) + or cvar_value_enabled( + effective.get("r_shadowmaptranslucentmoments", "0") + ) + ) + ): + return "fallback" + if ( + scene.get("interactionShadowTarget") == "fallback" + and maps_enabled + ): + return "fallback" + return "owned" + + +def interaction_shadow_expectation( + args: argparse.Namespace, case_id: str, shadow_preset: str +) -> str: + scene = ALL_SCENES.get(case_id, {}) + if case_id not in INTERACTION_SCENES and "interactionShadowTarget" not in scene: + return "none" + effective = effective_interaction_cvars(args, shadow_preset) + if ( + not cvar_value_enabled( + effective.get("r_renderersharedworldinteraction", "0") + ) + or not cvar_value_enabled(effective.get("r_shadows", "0")) + ): + return "none" + maps_enabled = cvar_value_enabled(effective.get("r_useshadowmap", "0")) + if shadow_preset == "map-budget-fallback" and maps_enabled: + return "map-budget-fallback" + target = scene.get("interactionShadowTarget") + if target == "fallback" and maps_enabled: + return "translucent-fallback" + if ( + maps_enabled + and ( + cvar_value_enabled(effective.get("r_shadowmapdebugoverlay", "0")) + or cvar_value_enabled( + effective.get("r_shadowmaptranslucentmoments", "0") + ) + ) + ): + return "fallback" + if not maps_enabled: + return "stencil" + if target in ( + "projected", + "point", + "csm", + "dynamic", + "perforated", + "hybrid", + ): + return target + if not cvar_value_enabled(effective.get("r_shadowmappointlights", "1")): + return "mixed" + if cvar_value_enabled(effective.get("r_shadowmapcsm", "0")): + return "csm" + return "mapped" + + +def fog_blend_expectation(args: argparse.Namespace, case_id: str) -> str: + if case_id not in FOG_BLEND_SCENES: + return "none" + effective = {"r_renderersharedworldfogblend": "0"} + for name, value in args.extra_cvars: + effective[name.casefold()] = value + if not cvar_value_enabled( + effective.get("r_renderersharedworldfogblend", "0") + ): + return "disabled" + if any( + cvar_value_enabled(effective.get(name, "0")) + for name in ( + "r_skipfoglights", + "r_showoverdraw", + "r_singletriangle", + "r_skiprender", + "r_skiprendercontext", + ) + ): + return "fallback" + if cvar_value_enabled(effective.get("r_skipblendlights", "0")): + return "owned-skip-blend" + return "owned" + + def build_specs(args: argparse.Namespace) -> list[RunSpec]: defaults = PROFILE_DEFAULTS[args.profile] case_ids = split_csv(args.cases, defaults["cases"]) @@ -1490,6 +4642,12 @@ def build_specs(args: argparse.Namespace) -> list[RunSpec]: swap_values = split_csv(args.swap_intervals, defaults["swap"]) display_values = split_csv(args.display_modes, defaults["display"]) shadow_values = split_csv(args.shadow_presets, defaults["shadows"]) + case_shadow_values = defaults.get("caseShadows", {}) + if not args.pacing_only and any(display != "windowed" for display in display_values): + raise ValueError( + "per-map CPU/GPU budget evidence requires windowed display; " + "fullscreen profiles are pacing-only" + ) specs: list[RunSpec] = [] for case_id in case_ids: @@ -1504,7 +4662,12 @@ def build_specs(args: argparse.Namespace) -> list[RunSpec]: for display in display_values: if display not in DISPLAY_MODES: raise ValueError(f"unknown display mode '{display}'") - for shadow in shadow_values: + selected_shadow_values = ( + case_shadow_values.get(case_id, shadow_values) + if not args.shadow_presets + else shadow_values + ) + for shadow in selected_shadow_values: if shadow not in SHADOW_PRESETS: raise ValueError(f"unknown shadow preset '{shadow}'") specs.append( @@ -1512,6 +4675,7 @@ def build_specs(args: argparse.Namespace) -> list[RunSpec]: case_id=case_id, mode=scene["mode"], map_name=scene["map"], + budget_map_name=scene.get("budgetMap", scene["map"]), purpose=scene["purpose"], path_name=scene["path"], tier=tier, @@ -1520,6 +4684,16 @@ def build_specs(args: argparse.Namespace) -> list[RunSpec]: display_mode=display, shadow_preset=shadow, renderer=args.renderer, + render_api=args.render_api, + interaction_expectation=interaction_expectation( + args, case_id, shadow + ), + interaction_shadow_expectation=interaction_shadow_expectation( + args, case_id, shadow + ), + fog_blend_expectation=fog_blend_expectation( + args, case_id + ), ) ) if args.limit > 0: @@ -1530,9 +4704,42 @@ def build_specs(args: argparse.Namespace) -> list[RunSpec]: def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dict[str, Any]) -> tuple[Path, Path]: report_json = output_dir / "renderer_gameplay_benchmark_report.json" report_md = output_dir / "renderer_gameplay_benchmark_report.md" + report_metadata = { + key: value + for key, value in metadata.items() + if key not in { + "git", + "runtime", + "runtimeVerificationFailures", + "budgetContract", + "budgetEnforced", + } + } payload = { - "metadata": metadata, + "schemaVersion": REPORT_SCHEMA_VERSION, + "status": ( + "planned" + if metadata["dryRun"] + else ( + "pass" + if results + and all(item["status"] == "pass" for item in results) + and not metadata.get("runtimeVerificationFailures", []) + else "fail" + ) + ), + "dryRun": metadata["dryRun"], + "git": metadata["git"], + "runtime": metadata["runtime"], + "runtimeVerificationFailures": metadata.get("runtimeVerificationFailures", []), + "budgetContract": metadata["budgetContract"], + "budgetEnforced": metadata["budgetEnforced"], + "metadata": report_metadata, "requiredScenes": REQUIRED_SCENES, + "worldAmbientScenes": WORLD_AMBIENT_SCENES, + "interactionScenes": INTERACTION_SCENES, + "fogBlendScenes": FOG_BLEND_SCENES, + "loadRegressionScenes": LOAD_REGRESSION_SCENES, "shadowScenes": SHADOW_SCENES, "campaignTransitionScenes": CAMPAIGN_TRANSITION_SCENES, "shadowPresets": SHADOW_PRESETS, @@ -1549,8 +4756,14 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic f"- Generated: {metadata['generated']}", f"- Host: {metadata['host']}", f"- Executable: `{metadata['executable']}`", + f"- Runtime: `{metadata['runtime']['path']}` ({len(metadata['runtime']['files'])} hashed files)", + f"- Runtime remained immutable: `{str(not metadata.get('runtimeVerificationFailures', [])).lower()}`", f"- Base path: `{metadata['basepath'] or 'not set'}`", f"- Profile: `{metadata['profile']}`", + f"- Benchmark preset: `{metadata['benchmarkPreset']}`", + f"- Per-map budget contract: `{metadata['budgetContract']['contractId']}` (`{metadata['budgetContract']['sha256']}`)", + f"- Per-map budgets enforced: `{str(metadata['budgetEnforced']).lower()}`", + f"- Budget display contract: `{metadata.get('budgetDisplayContract', {}).get('contractId', 'not enforced') if metadata.get('budgetDisplayContract') else 'not enforced'}`", f"- Sample: `{metadata['sampleMsec']} ms`" if metadata.get("sampleMsec", 0) > 0 else f"- Sample: `{metadata['sampleFrames']} frames`", f"- Cases: {passed} passed, {failed} failed, {planned} planned", "", @@ -1581,6 +4794,13 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic image_status = f"compared rms={image.get('rms', '?')} max={image.get('maxDelta', '?')} pass={int(bool(image.get('pass', False)))}" elif image_status in ("not-requested", "reference-not-found"): image_status = f"{image_status} {image.get('sha256', '')[:12]}".strip() + difference = role.get("imageDifference", {}) or {} + if difference.get("status") != "not-requested": + image_status += ( + f"; shadow-delta rms={difference.get('rms', '?')} " + f"channels={difference.get('differingChannels', '?')} " + f"pass={int(bool(difference.get('pass', False)))}" + ) screenshot = role.get("screenshot", "") log = role.get("log", "") lines.append( @@ -1592,6 +4812,38 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic f"| | `{role_result['role']}` missing | | | | | | | | {'; '.join(role_result['missing'])} | | | | |" ) + budget_roles = [ + (result, role) + for result in results + for role in result.get("roles", []) + if role.get("budgetEvidence") + ] + if budget_roles: + lines += [ + "", + "## Per-Map CPU/GPU Budget Evidence", + "", + "| Status | Case / role | Map | Backend | Profile | CPU samples / P95 / P99 | GPU samples / P95 / P99 | Budget |", + "|---|---|---|---|---|---|---|---|", + ] + for result, role in budget_roles: + evidence = role["budgetEvidence"] + measurement = evidence.get("measurement", {}) + cpu = measurement.get("cpu", {}) + gpu = measurement.get("gpu", {}) + gpu_summary = ( + f"{gpu.get('samples', '?')} / {gpu.get('p95Us', '?')} / {gpu.get('p99Us', '?')} us" + if gpu.get("available") + else "unavailable" + ) + lines.append( + f"| {evidence.get('status', 'fail')} | `{result['id']}` / `{role['role']}` | " + f"`{measurement.get('map', 'missing')}` | `{measurement.get('backend', 'missing')}` | " + f"`{measurement.get('profile', 'missing')}` | {cpu.get('samples', '?')} / " + f"{cpu.get('p95Us', '?')} / {cpu.get('p99Us', '?')} us | {gpu_summary} | " + f"`{evidence.get('budgetId', 'missing')}` |" + ) + diagnostic_roles = [ (result, role_result) for result in results @@ -1638,6 +4890,59 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic for case_id, scene in SHADOW_SCENES.items(): lines.append(f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | {scene['purpose']} |") + lines += [ + "", + "## World Ambient Ownership Coverage", + "", + "| Case | Mode | Map | Purpose |", + "|---|---|---|---|", + ] + for case_id, scene in WORLD_AMBIENT_SCENES.items(): + lines.append(f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | {scene['purpose']} |") + + lines += [ + "", + "## Interaction Ownership Coverage", + "", + "| Case | Mode | Map | Purpose |", + "|---|---|---|---|", + ] + for case_id, scene in INTERACTION_SCENES.items(): + lines.append(f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | {scene['purpose']} |") + + lines += [ + "", + "## Fog/Blend Ownership Coverage", + "", + "| Case | Mode | Map | Purpose |", + "|---|---|---|---|", + ] + for case_id, scene in FOG_BLEND_SCENES.items(): + lines.append(f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | {scene['purpose']} |") + + lines += [ + "", + "## Stock Interaction-Shadow Ownership Coverage", + "", + "| Case | Mode | Map | Target | Purpose |", + "|---|---|---|---|---|", + ] + for case_id, scene in INTERACTION_SHADOW_SCENES.items(): + lines.append( + f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | " + f"`{scene['interactionShadowTarget']}` | {scene['purpose']} |" + ) + + lines += [ + "", + "## Load/Shadow Regression Coverage", + "", + "| Case | Mode | Map | Purpose |", + "|---|---|---|---|", + ] + for case_id, scene in LOAD_REGRESSION_SCENES.items(): + lines.append(f"| `{case_id}` | {scene['mode']} | `{scene['map']}` | {scene['purpose']} |") + lines += [ "", "## Shadow Presets", @@ -1653,6 +4958,478 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic return report_json, report_md +def _verified_artifact( + report_dir: Path, record: Any, kind: str +) -> tuple[Path | None, list[str]]: + if not isinstance(record, dict) or record.get("kind") != kind: + return None, [f"recorded {kind} artifact is missing or malformed"] + raw_path = record.get("path") + if not isinstance(raw_path, str) or not raw_path or Path(raw_path).is_absolute(): + return None, [f"recorded {kind} artifact path is not a safe relative path"] + path = (report_dir / Path(raw_path)).resolve() + try: + path.relative_to(report_dir.resolve()) + except ValueError: + return None, [f"recorded {kind} artifact escapes the report directory"] + if not path.is_file(): + return None, [f"recorded {kind} artifact is missing: {raw_path}"] + actual = {"kind": kind, **file_record(path, report_dir)} + return path, ([] if actual == record else [f"recorded {kind} artifact differs: {raw_path}"]) + + +def verify_benchmark_report( + report: Any, + report_dir: Path, + root: Path, + runtime_dir: Path, + executable: Path, + contract: dict[str, Any], + contract_binding: dict[str, Any], +) -> list[str]: + if not isinstance(report, dict): + return ["benchmark report root is not an object"] + failures: list[str] = [] + if report.get("schemaVersion") != REPORT_SCHEMA_VERSION: + failures.append(f"unsupported benchmark report schema: {report.get('schemaVersion')!r}") + if report.get("status") != "pass": + failures.append(f"benchmark report status is {report.get('status')!r}, not 'pass'") + if report.get("dryRun") is not False: + failures.append("passing benchmark evidence must record dryRun=false") + recorded_budget_enforced = report.get("budgetEnforced") + if type(recorded_budget_enforced) is not bool: + failures.append("benchmark report budget-enforcement mode is missing or malformed") + budget_enforced = True + else: + budget_enforced = recorded_budget_enforced + if report.get("runtimeVerificationFailures") != []: + failures.append("benchmark report recorded runtime mutation during capture") + failures.extend( + verify_contract_binding(report.get("budgetContract"), contract, contract_binding) + ) + + current_git = git_state(root) + if report.get("git") != current_git: + failures.append("benchmark Git provenance differs from the current checkout") + current_files = collect_runtime_files(runtime_dir) + runtime = report.get("runtime") + if not isinstance(runtime, dict): + failures.append("benchmark runtime binding is missing or malformed") + else: + if runtime.get("path") != path_hint(runtime_dir, root): + failures.append("benchmark runtime path binding differs") + try: + executable_path = executable.relative_to(runtime_dir).as_posix() + except ValueError: + executable_path = "" + if runtime.get("executable") != executable_path: + failures.append("benchmark executable binding differs") + failures.extend(compare_file_records(runtime.get("files"), current_files, "runtime file")) + + metadata = report.get("metadata") + expected_display_contract = budget_display_contract() + if not isinstance(metadata, dict): + failures.append("benchmark metadata is missing or malformed") + metadata = {} + expected_metadata_display_contract = ( + expected_display_contract if budget_enforced else None + ) + if ( + "budgetDisplayContract" not in metadata + or metadata.get("budgetDisplayContract") != expected_metadata_display_contract + ): + failures.append( + "benchmark budget display contract differs from the recorded capture mode" + ) + benchmark_profile = metadata.get("benchmarkPreset") + if not isinstance(benchmark_profile, str) or not benchmark_profile: + failures.append("benchmark preset provenance is missing") + benchmark_profile = "" + recorded_render_api = metadata.get("renderApi") + if recorded_render_api not in ("gl", "vk"): + failures.append("benchmark renderer API provenance is missing or malformed") + recorded_render_api = "" + pbr_fixture_acceptance_required = False + recorded_post_map_cvars = metadata.get("effectivePostMapCvars") + valid_post_map_cvars = isinstance(recorded_post_map_cvars, dict) and all( + isinstance(name, str) + and name == name.casefold() + and re.fullmatch(r"[a-z_][a-z0-9_]*", name) is not None + and isinstance(value, str) + and bool(value) + and not has_command_delimiter_or_control(value) + and ( + name not in PBR_PROTECTED_CVARS + or ( + not has_engine_token_expansion_or_comment(value) + and not any(character in value for character in ('"', "'", "\\")) + and not (name in PBR_BOOL_CVARS and value.startswith("+")) + ) + ) + for name, value in ( + recorded_post_map_cvars.items() + if isinstance(recorded_post_map_cvars, dict) + else () + ) + ) + if not valid_post_map_cvars: + failures.append("effective post-map cvar provenance is missing or malformed") + recorded_post_map_cvars = {} + elif recorded_render_api: + pbr_fixture_acceptance_required = requires_pbr_fixture_acceptance( + recorded_post_map_cvars.items(), recorded_render_api + ) + recorded_pbr_required = metadata.get("pbrFixtureAcceptanceRequired") + if type(recorded_pbr_required) is not bool: + failures.append("PBR fixture acceptance provenance is missing or malformed") + elif recorded_pbr_required is not pbr_fixture_acceptance_required: + failures.append("PBR fixture acceptance provenance differs") + + if "pbrFixtureBinding" not in metadata: + failures.append("PBR fixture binding provenance is missing") + recorded_fixture_binding = metadata.get("pbrFixtureBinding") + if pbr_fixture_acceptance_required: + try: + current_fixture_binding = verify_procedural_pbr_fixture( + runtime_dir, PBR_FIXTURE_TEXTURE_SIZE + ) + except (FileNotFoundError, OSError, RuntimeError, ValueError) as exc: + failures.append(f"controlled PBR fixture verification failed: {exc}") + else: + if recorded_fixture_binding != current_fixture_binding: + failures.append("controlled PBR fixture binding differs") + elif recorded_fixture_binding is not None: + failures.append("ordinary benchmark report must not bind a controlled PBR fixture") + if not budget_enforced and not pbr_fixture_acceptance_required: + failures.append( + "functional replay is accepted only for the controlled PBR fixture" + ) + recorded_exec_values = metadata.get("execCommands") + if not isinstance(recorded_exec_values, list) or not all( + isinstance(command, str) for command in recorded_exec_values + ): + failures.append("benchmark exec-command provenance is missing or malformed") + recorded_exec_commands: tuple[str, ...] = () + else: + try: + recorded_exec_commands = parse_exec_commands(recorded_exec_values) + except ValueError as exc: + failures.append(f"benchmark exec-command provenance is invalid: {exc}") + recorded_exec_commands = () + results = report.get("results") + if not isinstance(results, list) or not results: + return [*failures, "benchmark results are missing or empty"] + if not budget_enforced and len(results) != 1: + failures.append("functional replay requires exactly one controlled result") + for result in results: + if not isinstance(result, dict): + failures.append("benchmark result is malformed") + continue + case_id = str(result.get("id", "unknown")) + if result.get("status") != "pass": + failures.append(f"{case_id}: result is not a pass") + if not budget_enforced: + identity_fields = ( + result.get("tier"), + result.get("maxfps"), + result.get("swapInterval"), + result.get("display"), + result.get("shadowPreset"), + result.get("renderer"), + ) + expected_fixture_id = ( + sanitize_case_id( + "_".join( + ( + PBR_FIXTURE_BENCHMARK_CASE, + identity_fields[0], + f"fps{identity_fields[1]}", + f"vsync{identity_fields[2]}", + identity_fields[3], + identity_fields[4], + identity_fields[5], + ) + ) + ) + if all(isinstance(value, str) and value for value in identity_fields) + else "" + ) + if ( + result.get("mode") != "SP" + or normalize_engine_map_name(str(result.get("map", ""))) + != normalize_engine_map_name(PBR_FIXTURE_STOCK_MAP) + or normalize_engine_map_name(str(result.get("budgetMap", ""))) + != normalize_engine_map_name(PBR_FIXTURE_STOCK_MAP) + or case_id != expected_fixture_id + ): + failures.append( + f"{case_id}: functional replay case/result identity differs from the controlled fixture" + ) + expected_map = result.get("budgetMap") + if not isinstance(expected_map, str) or not expected_map: + failures.append(f"{case_id}: budgetMap identity is missing") + continue + expected_backend = result.get("expectedBackend") + render_api = result.get("renderApi") + if render_api != recorded_render_api: + failures.append(f"{case_id}: renderer API differs from report provenance") + derived_backend = "vulkan" if render_api == "vk" else ( + "opengl" if render_api == "gl" else "" + ) + if expected_backend not in ("opengl", "vulkan") or expected_backend != derived_backend: + failures.append(f"{case_id}: expected backend/launch render API binding differs") + continue + if result.get("display") != "windowed": + failures.append(f"{case_id}: budget evidence was not captured windowed") + if result.get("displayContract") != expected_display_contract: + failures.append(f"{case_id}: launch display contract differs from bordered 1280x720") + if ( + not budget_enforced + and render_api == "gl" + and result.get("tier") not in GL_TIER_RUNTIME_NAMES + ): + failures.append( + f"{case_id}: controlled GL replay must request an explicit supported tier" + ) + if type(result.get("pbrFixtureAcceptanceRequired")) is not bool: + failures.append(f"{case_id}: PBR fixture acceptance selection is missing") + elif ( + result.get("pbrFixtureAcceptanceRequired") + is not pbr_fixture_acceptance_required + ): + failures.append(f"{case_id}: PBR fixture acceptance selection differs") + roles = result.get("roles") + if not isinstance(roles, list) or not roles: + failures.append(f"{case_id}: role evidence is missing") + continue + if not budget_enforced and ( + len(roles) != 1 + or not isinstance(roles[0], dict) + or roles[0].get("role") != "sp" + ): + failures.append(f"{case_id}: functional replay requires exactly one SP role") + for role in roles: + if not isinstance(role, dict): + failures.append(f"{case_id}: role evidence is malformed") + continue + role_name = str(role.get("role", "unknown")) + if role.get("status") != "pass" or role.get("missing") != []: + failures.append(f"{case_id}/{role_name}: role is not a clean pass") + if type(role.get("exitCode")) is not int or role.get("exitCode") != 0: + failures.append(f"{case_id}/{role_name}: process exit code is not zero") + if role.get("timedOut") is not False: + failures.append(f"{case_id}/{role_name}: process timed-out state is not false") + artifact_values = role.get("artifacts") + artifacts = { + item.get("kind"): item + for item in artifact_values + if isinstance(item, dict) and item.get("kind") + } if isinstance(artifact_values, list) else {} + source_pairs: list[tuple[str, str]] = [] + source_names = { + "engineLog": "log", + "processStdout": "stdout", + "processStderr": "stderr", + } + for kind in ("engineLog", "processStdout", "processStderr"): + path, artifact_failures = _verified_artifact( + report_dir, artifacts.get(kind), kind + ) + failures.extend(f"{case_id}/{role_name}: {item}" for item in artifact_failures) + if path is not None: + source_pairs.append( + ( + source_names[kind], + path.read_text(encoding="utf-8", errors="replace"), + ) + ) + sources = [source_text for _, source_text in source_pairs] + config_path, config_failures = _verified_artifact( + report_dir, artifacts.get("benchmarkConfig"), "benchmarkConfig" + ) + failures.extend( + f"{case_id}/{role_name}: {item}" for item in config_failures + ) + if config_path is not None: + try: + config_evidence = parse_benchmark_config( + config_path.read_text(encoding="utf-8") + ) + except (OSError, UnicodeError, ValueError) as exc: + failures.append( + f"{case_id}/{role_name}: benchmark config provenance is invalid: {exc}" + ) + else: + config_cvars = config_evidence.cvars + config_commands = config_evidence.commands + config_effective_cvars = effective_post_map_cvars(config_cvars) + if config_effective_cvars != recorded_post_map_cvars: + failures.append( + f"{case_id}/{role_name}: benchmark config CVar provenance differs" + ) + if render_api in ("gl", "vk") and ( + requires_pbr_fixture_acceptance(config_cvars, render_api) + is not pbr_fixture_acceptance_required + ): + failures.append( + f"{case_id}/{role_name}: benchmark config PBR selection differs" + ) + expected_config_commands = recorded_exec_commands + ( + (f"waitMsec {MP_SERVER_CLIENT_GRACE_MSEC}",) + if role_name == "server" + else () + ) + if config_commands != expected_config_commands: + failures.append( + f"{case_id}/{role_name}: benchmark config command provenance differs" + ) + expected_capture_mode = ( + "budget" if budget_enforced else "functional-replay" + ) + if config_evidence.capture_mode != expected_capture_mode: + failures.append( + f"{case_id}/{role_name}: benchmark config capture mode differs" + ) + if ( + config_evidence.role != role_name + or config_evidence.screenshot_request + != role.get("screenshotRequest") + ): + failures.append( + f"{case_id}/{role_name}: benchmark config role/screenshot binding differs" + ) + recorded_settle_frames = metadata.get("settleFrames") + recorded_mp_delay_frames = metadata.get("mpClientDelayFrames") + if ( + type(recorded_settle_frames) is not int + or ( + role_name == "server" + and type(recorded_mp_delay_frames) is not int + ) + ): + failures.append( + f"{case_id}/{role_name}: benchmark settle provenance is malformed" + ) + else: + expected_settle_frames = max(1, recorded_settle_frames) + if role_name == "server": + expected_settle_frames = max( + 1, recorded_settle_frames + recorded_mp_delay_frames + ) + if config_evidence.settle_frames != expected_settle_frames: + failures.append( + f"{case_id}/{role_name}: benchmark config settle interval differs" + ) + recorded_sample_msec = metadata.get("sampleMsec") + recorded_sample_frames = metadata.get("sampleFrames") + if ( + type(recorded_sample_msec) is not int + or type(recorded_sample_frames) is not int + ): + failures.append( + f"{case_id}/{role_name}: benchmark sample provenance is malformed" + ) + else: + expected_sample_kind = ( + "waitMsec" if recorded_sample_msec > 0 else "wait" + ) + expected_sample_value = max( + 1, + recorded_sample_msec + if recorded_sample_msec > 0 + else recorded_sample_frames, + ) + if ( + config_evidence.sample_kind != expected_sample_kind + or config_evidence.sample_value != expected_sample_value + ): + failures.append( + f"{case_id}/{role_name}: benchmark config sample interval differs" + ) + screenshot_path, screenshot_failures = _verified_artifact( + report_dir, artifacts.get("screenshot"), "screenshot" + ) + failures.extend( + f"{case_id}/{role_name}: {item}" for item in screenshot_failures + ) + display_evidence, display_failures = evaluate_display_evidence( + sources, screenshot_path + ) + failures.extend( + f"{case_id}/{role_name}: display evidence: {item}" + for item in display_failures + ) + if role.get("displayEvidence") != display_evidence: + failures.append( + f"{case_id}/{role_name}: recorded display evidence differs" + ) + combined_sources = "\n".join(sources) + recorded_warnings = role.get("warnings") + current_warnings = warning_counts(combined_sources) + if recorded_warnings != current_warnings: + failures.append(f"{case_id}/{role_name}: recorded warning evidence differs") + if any(count > 0 for count in current_warnings.values()): + failures.append(f"{case_id}/{role_name}: warning evidence is not clean") + pbr_summary = extract_summary(combined_sources) + pbr_evidence, pbr_failures = evaluate_pbr_fixture_evidence( + pbr_summary, pbr_fixture_acceptance_required, render_api + ) + failures.extend( + f"{case_id}/{role_name}: {item}" for item in pbr_failures + ) + if role.get("pbrFixtureEvidence") != pbr_evidence: + failures.append( + f"{case_id}/{role_name}: recorded PBR fixture evidence differs" + ) + map_evidence, map_failures = evaluate_controlled_map_evidence( + source_pairs, pbr_fixture_acceptance_required + ) + failures.extend( + f"{case_id}/{role_name}: {item}" for item in map_failures + ) + if role.get("mapEvidence") != map_evidence: + failures.append( + f"{case_id}/{role_name}: recorded controlled-map evidence differs" + ) + if not budget_enforced and render_api == "gl": + selected_tiers = [ + match.group(1).casefold() + for source_name, source_text in source_pairs + for match in re.finditer( + r"^Selected renderer tier:\s*([A-Za-z0-9]+)\s*$", + source_text, + re.IGNORECASE | re.MULTILINE, + ) + if source_name == "log" + ] + expected_runtime_tier = GL_TIER_RUNTIME_NAMES.get(result.get("tier")) + if ( + expected_runtime_tier is None + or not selected_tiers + or selected_tiers[-1].casefold() + != expected_runtime_tier.casefold() + ): + failures.append( + f"{case_id}/{role_name}: final GL selected tier differs from the result" + ) + if budget_enforced: + failures.extend( + f"{case_id}/{role_name}: {item}" + for item in verify_recorded_evidence( + role.get("budgetEvidence"), + sources, + contract, + expected_map, + expected_backend, + benchmark_profile, + ) + ) + elif role.get("budgetEvidence") != {}: + failures.append( + f"{case_id}/{role_name}: functional replay budget evidence must be empty" + ) + return failures + + def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--profile", choices=tuple(PROFILE_DEFAULTS.keys()), default="smoke", help="Preset case/dimension profile.") @@ -1661,11 +5438,14 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--maxfps", default="", help="Comma-separated com_maxfps values. Overrides profile values.") parser.add_argument("--swap-intervals", default="", help="Comma-separated r_swapInterval values. Overrides profile values.") parser.add_argument("--display-modes", default="", help="Comma-separated display modes: windowed,fullscreen.") + parser.add_argument("--width", type=int, default=BUDGET_WIDTH, help=f"Drawable width. Budget evidence requires {BUDGET_WIDTH}.") + parser.add_argument("--height", type=int, default=BUDGET_HEIGHT, help=f"Drawable height. Budget evidence requires {BUDGET_HEIGHT}.") parser.add_argument("--shadow-presets", default="", help="Comma-separated shadow presets. Use --list to inspect values.") parser.add_argument("--renderer", default="best", help="Value for r_renderer, usually best or arb2.") + parser.add_argument("--render-api", choices=("gl", "vk"), default="gl", help="Exact renderer backend launch contract: gl or vk.") parser.add_argument("--benchmark-preset", default="baseline", help="Value for r_rendererBenchmarkPreset.") parser.add_argument("--modern-executor", action="store_true", help="Opt into r_rendererModernExecutor for gameplay benchmarking. Defaults off so ARB2/high-FPS baselines are not polluted by side-path work.") - parser.add_argument("--gpu-timers", action="store_true", help="Enable GL timer queries during the sampled benchmark window. Defaults off for acceptance FPS runs because timer queries can perturb frame pacing.") + parser.add_argument("--gpu-timers", action=argparse.BooleanOptionalAction, default=True, help="Enable backend-neutral whole-frame GPU timestamps during sampled budget runs. Enabled by default; --no-gpu-timers is only valid with --pacing-only.") parser.add_argument("--show-fps-overlay", action="store_true", help="Draw the in-game FPS overlay during the run. Defaults off so acceptance timings measure renderer/gameplay cost, not diagnostic text drawing.") parser.add_argument("--pacing-only", action="store_true", help="Measure frame pacing without enabling r_rendererMetrics or rendererBenchmarkCapture. Use this for high-FPS acceptance runs after diagnostic captures are clean.") parser.add_argument("--min-pacing-hz", type=float, default=0.0, help="Fail when the parsed frame-pacing snapshot falls below this average presentation rate.") @@ -1680,11 +5460,17 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--sample-msec", type=int, default=0, help="Real milliseconds to sample before dumping metrics and screenshots. Overrides --sample-frames when positive.") parser.add_argument("--timeout", type=int, default=180, help="Per-case process timeout in seconds.") parser.add_argument("--basepath", default=default_basepath(), help="Quake 4 install/base path. Omit or set empty to skip fs_basepath.") + parser.add_argument("--runtime-dir", default="", help="Staged runtime package. Defaults to .install; alternates must be named ordinary directories below .tmp/stock-runtime/.") + parser.add_argument("--budget-contract", default=str(DEFAULT_CONTRACT_PATH), help="Versioned per-map CPU/GPU budget JSON. Its stable id and SHA-256 are bound into the report.") + parser.add_argument("--verify-report", default="", help="Replay-verify a prior real benchmark report, runtime, artifacts, timing marker, and budget contract without launching openQ4.") parser.add_argument("--output-dir", default="", help="Report/output directory. Defaults to /.tmp/renderer-gameplay/.") parser.add_argument("--reference-dir", default="", help="Optional TGA reference screenshot root for deterministic image comparison.") parser.add_argument("--require-references", action="store_true", help="Fail captures when --reference-dir has no matching reference image.") parser.add_argument("--image-rms-threshold", type=float, default=2.0, help="Allowed RMS channel delta for TGA comparisons.") parser.add_argument("--image-max-threshold", type=int, default=24, help="Allowed maximum channel delta for TGA comparisons.") + parser.add_argument("--difference-reference-dir", default="", help="Optional engine-TGA reference root that each eligible interaction-shadow, fog/blend, or material-deform capture must differ from; use a feature-disabled capture to prove the controlled effect is visible.") + parser.add_argument("--image-difference-min-rms", type=float, default=0.1, help="Minimum RMS channel delta required by --difference-reference-dir.") + parser.add_argument("--image-difference-min-channels", type=int, default=1000, help="Minimum changed RGB-channel count required by --difference-reference-dir.") parser.add_argument("--mp-port", type=int, default=28110, help="Base listen-server port for MP runs.") parser.add_argument("--mp-client-delay", type=int, default=12, help="Seconds to wait before launching the MP loopback client.") parser.add_argument("--mp-client-delay-frames", type=int, default=480, help="Extra server frames before server-side capture in MP runs.") @@ -1692,29 +5478,84 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--dry-run", action="store_true", help="Write the planned command lines without launching openQ4.") parser.add_argument("--list", action="store_true", help="List profiles, cases, and shadow presets without running.") parsed = parser.parse_args(argv) + if not parsed.pacing_only and not parsed.gpu_timers: + parser.error("--no-gpu-timers is only valid with --pacing-only; budget evidence requires GPU timing") + if not (320 <= parsed.width <= 16384) or not (240 <= parsed.height <= 16384): + parser.error("--width/--height must stay within the engine's 320x240 to 16384x16384 range") + if parsed.image_difference_min_rms < 0.0: + parser.error("--image-difference-min-rms must be non-negative") + if parsed.image_difference_min_channels < 1: + parser.error("--image-difference-min-channels must be positive") + if not parsed.pacing_only and (parsed.width, parsed.height) != (BUDGET_WIDTH, BUDGET_HEIGHT): + parser.error( + f"budget evidence requires the canonical bordered {BUDGET_WIDTH}x{BUDGET_HEIGHT} display contract" + ) + selected_displays = split_csv( + parsed.display_modes, tuple(PROFILE_DEFAULTS[parsed.profile]["display"]) + ) + if not parsed.pacing_only and any(display != "windowed" for display in selected_displays): + parser.error( + "budget evidence requires windowed display; use --pacing-only for fullscreen presentation tests" + ) try: profile_cvars = tuple(PROFILE_DEFAULTS[parsed.profile].get("cvars", ())) profile_launch_cvars = tuple(PROFILE_DEFAULTS[parsed.profile].get("launchCvars", ())) profile_exec_commands = tuple(PROFILE_DEFAULTS[parsed.profile].get("execCommands", ())) parsed.extra_cvars = profile_cvars + parse_extra_cvars(parsed.set_cvar) parsed.launch_cvars = profile_launch_cvars + parse_extra_cvars(parsed.set_launch_cvar) - parsed.exec_commands = profile_exec_commands + parse_exec_commands(parsed.exec_command) + parsed.exec_commands = parse_exec_commands( + [*profile_exec_commands, *parsed.exec_command] + ) except ValueError as exc: parser.error(str(exc)) + pbr_launch_conflicts = sorted( + name + for name, _ in parsed.launch_cvars + if name.casefold() in PBR_PROTECTED_CVARS + ) + if pbr_launch_conflicts: + parser.error( + "protected PBR fixture CVars must be set post-map with --set-cvar: " + + ", ".join(pbr_launch_conflicts) + ) + if not parsed.pacing_only: + protected_launch_cvars = { + name.casefold() for name in budget_display_contract()["cvars"] + } | {"r_renderapi"} + conflicting = sorted( + name for name, _ in parsed.launch_cvars if name.casefold() in protected_launch_cvars + ) + if conflicting: + parser.error( + "budget evidence launch CVars may not override the display/backend contract: " + + ", ".join(conflicting) + ) parsed.reference_dir_path = Path(parsed.reference_dir).resolve() if parsed.reference_dir else None + if parsed.require_references and parsed.reference_dir_path is None: + parser.error("--require-references requires --reference-dir") + parsed.difference_reference_dir_path = ( + Path(parsed.difference_reference_dir).resolve() + if parsed.difference_reference_dir + else None + ) return parsed def print_list() -> None: print("Profiles:") for profile, defaults in PROFILE_DEFAULTS.items(): + case_shadows = defaults.get("caseShadows") + case_shadow_count = ( + sum(len(case_shadows.get(case_id, defaults["shadows"])) for case_id in defaults["cases"]) + if case_shadows + else len(defaults["cases"]) * len(defaults["shadows"]) + ) count = ( - len(defaults["cases"]) + case_shadow_count * len(defaults["tiers"]) * len(defaults["maxfps"]) * len(defaults["swap"]) * len(defaults["display"]) - * len(defaults["shadows"]) ) profile_cvars = defaults.get("cvars", ()) profile_exec_commands = defaults.get("execCommands", ()) @@ -1734,6 +5575,24 @@ def print_list() -> None: print("\nCampaign transition cases:") for case_id, scene in CAMPAIGN_TRANSITION_SCENES.items(): print(f" {case_id}: {scene['mode']} {scene['map']} - {scene['purpose']}") + print("\nWorld ambient ownership cases (run with --pacing-only):") + for case_id, scene in WORLD_AMBIENT_SCENES.items(): + print(f" {case_id}: {scene['mode']} {scene['map']} - {scene['purpose']}") + print("\nInteraction ownership cases (run with --pacing-only):") + for case_id, scene in INTERACTION_SCENES.items(): + print(f" {case_id}: {scene['mode']} {scene['map']} - {scene['purpose']}") + print("\nFog/blend ownership cases (run with --pacing-only):") + for case_id, scene in FOG_BLEND_SCENES.items(): + print(f" {case_id}: {scene['mode']} {scene['map']} - {scene['purpose']}") + print("\nMaterial-deform ownership cases (run with --pacing-only):") + for case_id, scene in DEFORM_SCENES.items(): + print(f" {case_id}: {scene['mode']} {scene['map']} - {scene['purpose']}") + print("\nStock interaction-shadow cases (run with --pacing-only):") + for case_id, scene in INTERACTION_SHADOW_SCENES.items(): + print( + f" {case_id}: {scene['mode']} {scene['map']} " + f"[{scene['interactionShadowTarget']}] - {scene['purpose']}" + ) print("\nShadow presets:") for preset, cvars in SHADOW_PRESETS.items(): cvar_text = ", ".join(f"{key}={value}" for key, value in cvars.items()) or "stock defaults" @@ -1746,18 +5605,58 @@ def main(argv: list[str]) -> int: return 0 root = repo_root() - executable = find_client_executable(root) - basepath = args.basepath - if basepath and not Path(basepath).exists(): - print(f"warning: basepath does not exist, omitting fs_basepath: {basepath}", file=sys.stderr) - basepath = "" + runtime_dir = validate_runtime_dir( + Path(args.runtime_dir) if args.runtime_dir else root / ".install", root + ) + args.runtime_dir_path = runtime_dir + executable = find_client_executable(runtime_dir) + runtime_files_before = collect_runtime_files(runtime_dir) + budget_contract_path = Path(args.budget_contract) + args.budget_contract, budget_binding = load_contract(budget_contract_path) + if args.verify_report: + report_path = Path(args.verify_report).resolve() + report = json.loads(report_path.read_text(encoding="utf-8")) + failures = verify_benchmark_report( + report, + report_path.parent, + root, + runtime_dir, + executable, + args.budget_contract, + budget_binding, + ) + for failure in failures: + print(f"error: {failure}", file=sys.stderr) + if not failures: + print("renderer gameplay benchmark verification: pass") + return 1 if failures else 0 + args.pbr_fixture_acceptance_required = requires_pbr_fixture_acceptance( + args.extra_cvars, args.render_api + ) + args.pbr_fixture_binding = ( + verify_procedural_pbr_fixture(runtime_dir, PBR_FIXTURE_TEXTURE_SIZE) + if args.pbr_fixture_acceptance_required + else None + ) + requested_basepath = args.basepath + basepath = resolve_basepath(requested_basepath) + if requested_basepath and not basepath: + print(f"warning: basepath does not exist, omitting fs_basepath: {requested_basepath}", file=sys.stderr) if args.reference_dir_path is not None and not args.reference_dir_path.exists(): raise FileNotFoundError(f"reference directory does not exist: {args.reference_dir_path}") + if ( + args.difference_reference_dir_path is not None + and not args.difference_reference_dir_path.exists() + ): + raise FileNotFoundError( + "difference reference directory does not exist: " + f"{args.difference_reference_dir_path}" + ) specs = build_specs(args) timestamp = time.strftime("%Y%m%d-%H%M%S") output_dir = Path(args.output_dir).resolve() if args.output_dir else root / ".tmp" / "renderer-gameplay" / timestamp - output_dir.mkdir(parents=True, exist_ok=True) + prepare_output_directory(output_dir) run_id = output_dir.name results: list[dict[str, Any]] = [] @@ -1775,21 +5674,52 @@ def main(argv: list[str]) -> int: print(f" {result['status']}", flush=True) results.append(result) + runtime_files_after = collect_runtime_files(runtime_dir) + runtime_verification_failures = compare_file_records( + runtime_files_before, runtime_files_after, "runtime file" + ) + if runtime_verification_failures: + for result in results: + result["status"] = "fail" + for role in result.get("roles", []): + role["status"] = "fail" + role.setdefault("missing", []).extend( + f"runtime mutation: {failure}" + for failure in runtime_verification_failures + ) + attach_result_artifacts(output_dir, results) + metadata = { "generated": time.strftime("%Y-%m-%d %H:%M:%S %z"), "host": f"{platform.system()} {platform.release()} {platform.machine()}", "executable": str(executable), + "runtime": { + "path": path_hint(runtime_dir, root), + "executable": executable.relative_to(runtime_dir).as_posix(), + "files": runtime_files_before, + }, + "runtimeVerificationFailures": runtime_verification_failures, + "git": git_state(root), + "budgetContract": budget_binding, + "budgetEnforced": not args.pacing_only, + "budgetDisplayContract": budget_display_contract() if not args.pacing_only else None, "basepath": basepath, "profile": args.profile, + "benchmarkPreset": args.benchmark_preset, + "renderApi": args.render_api, "dryRun": args.dry_run, "autoexecDelayMs": args.autoexec_delay_ms, "settleFrames": args.settle_frames, + "mpClientDelayFrames": args.mp_client_delay_frames, "sampleFrames": args.sample_frames, "sampleMsec": args.sample_msec, "minPacingHz": args.min_pacing_hz, "maxP95Ms": args.max_p95_ms, "maxP99Ms": args.max_p99_ms, "profileCvars": dict(PROFILE_DEFAULTS[args.profile].get("cvars", ())), + "effectivePostMapCvars": effective_post_map_cvars(args.extra_cvars), + "pbrFixtureAcceptanceRequired": args.pbr_fixture_acceptance_required, + "pbrFixtureBinding": args.pbr_fixture_binding, "profileExecCommands": list(PROFILE_DEFAULTS[args.profile].get("execCommands", ())), "launchCvars": dict(args.launch_cvars), "execCommands": list(args.exec_commands), diff --git a/tools/tests/renderer_gpu_frame_timing.py b/tools/tests/renderer_gpu_frame_timing.py new file mode 100644 index 00000000..4847f592 --- /dev/null +++ b/tools/tests/renderer_gpu_frame_timing.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Static contracts for backend-neutral, nonblocking whole-frame GPU timing.""" + +from __future__ import annotations + +import os +from pathlib import Path +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[2] +RENDERER = ROOT / "src" / "renderer" +GAME_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", str(ROOT.parent / "openQ4-game")) +).resolve() + + +def read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def require(source: str, token: str, label: str) -> None: + if token not in source: + raise AssertionError(f"missing {label}: {token}") + + +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"missing function: {signature}") + brace = source.find("{", start) + if brace < 0: + raise AssertionError(f"missing body for: {signature}") + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[brace + 1 : index] + raise AssertionError(f"unterminated body for: {signature}") + + +def current_branch(repository: Path) -> str | None: + result = subprocess.run( + ["git", "-C", str(repository), "branch", "--show-current"], + check=False, + capture_output=True, + text=True, + ) + branch = result.stdout.strip() + return branch if result.returncode == 0 and branch else None + + +def main() -> int: + public_header = read(RENDERER / "RenderSystem.h") + companion_header_path = GAME_ROOT / "src" / "renderer" / "RenderSystem.h" + if not companion_header_path.is_file(): + raise AssertionError(f"missing companion renderer header: {companion_header_path}") + companion_header = read(companion_header_path) + engine_branch = current_branch(ROOT) + companion_branch = current_branch(GAME_ROOT) + matching_source_pair = ( + engine_branch is not None + and companion_branch is not None + and engine_branch == companion_branch + ) + companion_has_timing_api = "GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) const" in companion_header + if matching_source_pair or companion_has_timing_api: + if public_header != companion_header: + raise AssertionError("engine and companion RenderSystem.h files are not byte-identical") + else: + print( + "renderer GPU frame timing contracts: companion parity skipped " + "(workflow source pair is on different revisions)" + ) + for token in ( + "RENDER_GPU_TIMING_BACKEND_OPENGL", + "RENDER_GPU_TIMING_BACKEND_VULKAN", + "bool\t\t\t\tvalid;", + "unsigned long long\telapsedMicroseconds;", + "unsigned long long\tunavailableSamples;", + "unsigned long long\tdroppedSamples;", + "GetGpuFrameTiming( renderGpuFrameTiming_t &timing ) const", + "ResetGpuFrameTiming( const char *reason )", + "renderPresentationState_s", + "int\t\t\t\tsceneWidth;", + "int\t\t\t\toutputWidth;", + "GetPresentationState( renderPresentationState_t &state ) const", + "ResolveTemporalPresentation(", + ): + require(public_header, token, "public renderer module ABI") + + module_api = read(RENDERER / "RenderModuleAPI.h") + require(module_api, "#define RENDER_API_VERSION\t\t\t11", "renderer module ABI v11") + + core = read(RENDERER / "GpuFrameTimingCore.h") + require(core, "elapsedMicroseconds == 0", "zero-duration rejection") + require(core, "static_cast( currentFrame )", "wide latency arithmetic") + require(core, "std::int64_t( 1 ) << 32", "signed frame-counter wrap") + + render_system = read(RENDERER / "RenderSystem.cpp") + for token in ( + "idRenderSystemLocal::GetPresentationState", + "R_TemporalPresentation_GetFrameState()", + "R_TemporalPresentation_HistoryGeneration()", + "idRenderSystemLocal::ResolveTemporalPresentation", + "RC_RESOLVE_TEMPORAL_PRESENTATION", + ): + require(render_system, token, "frame-latched presentation ABI") + temporal_resolve = function_body( + render_system, "bool idRenderSystemLocal::ResolveTemporalPresentation(" + ) + for token in ( + "presentation.captureFrozen", + "presentation.captureForcedNative", + "cmd->sceneColorTarget = sceneColorTarget;", + "cmd->sceneDepthTarget = sceneDepthTarget;", + "cmd->historyReadTarget = historyReadTarget;", + "cmd->historyWriteTarget = historyWriteTarget;", + "cmd->viewDef = primaryView;", + "cmd->presentation = presentation;", + ): + require(temporal_resolve, token, "immutable temporal resolve command") + + tr_local = read(RENDERER / "tr_local.h") + for token in ( + "RC_RESOLVE_TEMPORAL_PRESENTATION", + "} resolveTemporalPresentationCommand_t;", + "renderPresentationState_t presentation;", + "unsigned int\t\thistoryGeneration;", + ): + require(tr_local, token, "temporal resolve render command") + + backend = read(RENDERER / "tr_backend.cpp") + require(backend, "case RC_RESOLVE_TEMPORAL_PRESENTATION:", "OpenGL temporal dispatch") + require(backend, "executionCommand.captureFrame = true;", "late capture temporal bypass") + + game_render_path = GAME_ROOT / "src" / "game" / "Game_render.cpp" + if not game_render_path.is_file(): + raise AssertionError(f"missing companion game render integration: {game_render_path}") + game_render = read(game_render_path) + for token in ( + "_temporalHistoryAlbedo%d", + "presentation.outputWidth", + "presentation.outputHeight", + "!presentation.temporalAARequested", + "blurEnabled || presentation.temporalAARequested", + "openQ4_ResolveTemporalPresentation(", + "gameRender.postProcessRT[1]", + "gameRender.forwardRenderPassResolvedRT", + ): + require(game_render, token, "game temporal presentation integration") + begin_frame = function_body(render_system, "void idRenderSystemLocal::BeginFrame(") + capture_depth = function_body( + render_system, "void idRenderSystemLocal::CaptureDepthRenderToImage(" + ) + require(begin_frame, "R_RendererMetrics_MarkCpuFrameBegin();", "CPU frame start") + if "R_RendererMetrics_MarkCpuFrameBegin();" in capture_depth: + raise AssertionError("CPU frame start must not be anchored to depth capture") + if render_system.count("R_RendererMetrics_MarkCpuFrameBegin();") != 1: + raise AssertionError("CPU frame timing must have exactly one BeginFrame anchor") + + metrics = read(RENDERER / "RendererMetrics.cpp") + gl_poll = function_body(metrics, "static bool R_RendererMetrics_PollGlFullFrameTiming(") + availability = gl_poll.find("GL_QUERY_RESULT_AVAILABLE") + result_read = gl_poll.find("glGetQueryObjectui64v") + if availability < 0 or result_read < 0 or availability >= result_read: + raise AssertionError("OpenGL timestamp results must be availability-checked first") + gl_self_test = function_body(metrics, "bool RendererGpuTimer_RunSelfTest(") + if "glFinish" in gl_self_test: + raise AssertionError("GPU timing self-test must not force GL completion") + require(metrics, "renderer metrics capture window started", "capture-window reset") + + benchmarks = read(RENDERER / "RendererBenchmarks.cpp") + marker = ( + "OPENQ4_FRAME_TIMING_V1 map=%s backend=%s profile=%s " + "cpuSamples=%d cpuP50Us=%llu cpuP95Us=%llu cpuP99Us=%llu " + "gpuAvailable=%d gpuSamples=%d gpuP50Us=%lld gpuP95Us=%lld gpuP99Us=%lld" + ) + require(benchmarks, marker, "versioned timing marker") + require(benchmarks, "mapName.ToLower();", "canonical lowercase map") + require(benchmarks, "const int gpuCount = timing.supported", "supported GPU aggregation") + require(benchmarks, "acceptedFrames[existing] == sample.gpuFrameNumber", "GPU dedup") + require(benchmarks, "delayedCount != 2", "GPU dedup self-test") + require(benchmarks, 'normalizedMap != "game/storage1"', "map normalization self-test") + + render_init = read(RENDERER / "RenderSystem_init.cpp") + if render_init.count("RendererBenchmarks_PrintTimingMarker();") != 1: + raise AssertionError("rendererBenchmarkCapture must emit the V1 marker exactly once") + require(render_init, 'ResetGpuFrameTiming( "begin level load" )', "map begin reset") + require(render_init, 'ResetGpuFrameTiming( "end level load" )', "map end reset") + require(render_init, 'ResetGpuFrameTiming( "renderer init" )', "renderer init reset") + require(render_init, 'ResetGpuFrameTiming( "renderer shutdown" )', "renderer shutdown reset") + require(render_init, 'ResetGpuFrameTiming( "vid_restart" )', "video restart reset") + + session = read(ROOT / "src" / "framework" / "Session.cpp") + unload_map = function_body(session, "void idSessionLocal::UnloadMap(") + require(unload_map, 'ResetGpuFrameTiming( "session unload" )', "session reset") + + vk_timing = read(RENDERER / "Vulkan" / "VulkanGpuFrameTiming.cpp") + require(vk_timing, "VK_QUERY_RESULT_64_BIT );", "64-bit non-waiting query read") + if "VK_QUERY_RESULT_64_BIT |" in vk_timing or "| VK_QUERY_RESULT_WAIT_BIT" in vk_timing: + raise AssertionError("Vulkan timestamp query reads must not use WAIT_BIT") + require(vk_timing, "GpuFrameTimingCore_TimestampDelta", "valid-bit timestamp math") + require(vk_timing, "elapsedMicroseconds", "Vulkan microsecond result") + + vk_gui = read(RENDERER / "Vulkan" / "vk_GuiExecutor.cpp") + wait_at = vk_gui.find("const VkResult frameFenceResult = vkWaitForFences") + resolve_at = vk_gui.find("VK_GpuFrameTiming_BeginFrame( cmd, slot, tr.frameCount )") + if wait_at < 0 or resolve_at < 0 or wait_at >= resolve_at: + raise AssertionError("Vulkan timestamps must resolve only after the slot fence retires") + require(vk_gui, "readbackFenceResult != VK_SUCCESS", "screenshot-fence result check") + + vk_device = read(RENDERER / "Vulkan" / "VulkanDevice.cpp") + require(vk_device, "graphicsTimestampValidBits", "Vulkan queue timestamp width") + require(vk_device, 'ResetGpuFrameTiming( "Vulkan swapchain recreation" )', "swapchain reset") + + discovery = subprocess.run( + [sys.executable, str(ROOT / "tools" / "build" / "meson_sources.py"), "--emit", "renderer_vk"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.splitlines() + timing_source = "src/renderer/Vulkan/VulkanGpuFrameTiming.cpp" + if discovery.count(timing_source) != 1: + raise AssertionError("Vulkan timing source must occur exactly once in module discovery") + source_contract = read(ROOT / "tools" / "build" / "meson_sources.py") + require(source_contract, "RENDERER_VK_REQUIRED_SOURCES", "required Vulkan source contract") + + meson = read(ROOT / "meson.build") + require(meson, "'openq4-gpu-frame-timing-test'", "native timing test target") + native_test = read(ROOT / "tools" / "tests" / "native" / "GpuFrameTimingTest.cpp") + require(native_test, "INT_MAX, INT_MIN", "native frame-wrap test") + require(native_test, "state.generation, 0, 1", "native zero-duration test") + + print("renderer GPU frame timing contracts: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_gpu_skinning.py b/tools/tests/renderer_gpu_skinning.py new file mode 100644 index 00000000..62bfe1b2 --- /dev/null +++ b/tools/tests/renderer_gpu_skinning.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 +"""Static integration contract for Milestone C renderer/GPU animation work.""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +RENDERER = ROOT / "src" / "renderer" +GAME_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", str(ROOT.parent / "openQ4-game")) +).resolve() + + +def read(path: Path) -> str: + if not path.is_file(): + raise AssertionError(f"required file is missing: {path}") + return path.read_text(encoding="utf-8") + + +def require(source: str, token: str, label: str) -> None: + if token not in source: + raise AssertionError(f"missing {label}: {token!r}") + + +def require_all(source: str, tokens: tuple[str, ...], label: str) -> None: + for token in tokens: + require(source, token, label) + + +def reject(source: str, token: str, label: str) -> None: + if token in source: + raise AssertionError(f"forbidden {label}: {token!r}") + + +def require_regex(source: str, pattern: str, label: str) -> None: + if re.search(pattern, source, re.MULTILINE) is None: + raise AssertionError(f"missing {label}: /{pattern}/") + + +def reject_regex(source: str, pattern: str, label: str) -> None: + if re.search(pattern, source, re.MULTILINE | re.IGNORECASE) is not None: + raise AssertionError(f"forbidden {label}: /{pattern}/") + + +def require_order(source: str, tokens: tuple[str, ...], label: str) -> None: + cursor = -1 + for token in tokens: + position = source.find(token, cursor + 1) + if position < 0: + raise AssertionError(f"missing ordered {label}: {token!r}") + if position <= cursor: + raise AssertionError(f"out-of-order {label}: {tokens!r}") + cursor = position + + +def braced_body(source: str, marker: str, label: str) -> str: + start = source.find(marker) + if start < 0: + raise AssertionError(f"missing {label}: {marker!r}") + opening = source.find("{", start + len(marker)) + if opening < 0: + raise AssertionError(f"missing body for {label}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + raise AssertionError(f"unterminated body for {label}") + + +def normalized_source(source: str) -> str: + return source.replace("\r\n", "\n").replace("\r", "\n") + + +def validate_shared_renderer_contracts() -> None: + header = read(RENDERER / "RendererContracts.h") + source = read(RENDERER / "RendererContracts.cpp") + native = read(ROOT / "tools" / "tests" / "native" / "RendererContractsTest.cpp") + meson = read(ROOT / "meson.build") + gl_executor = read(RENDERER / "ModernGLExecutor.cpp") + vk_executor = read(RENDERER / "Vulkan" / "vk_GuiExecutor.cpp") + + for backend_token in ("GLuint", "GLenum", "VkBuffer", "VkFormat", "vulkan.h"): + reject(header, backend_token, "backend type in shared renderer contract") + + require_all( + header, + ( + "rendererMaterialPass_t", + "sourceStageIndex", + "textureResourceId", + "rendererRegisterRef_t\t\t\tcondition", + "rendererRegisterRef_t\t\t\tcolor[ 4 ]", + "rendererRegisterRef_t\t\t\ttextureMatrix[ 6 ]", + "rendererBlendState_t", + "rendererDepthState_t", + "colorWriteMask", + "alphaTestEnabled", + "polygonOffsetEnabled", + "programFamily", + "RendererContracts_AppendMaterialPass", + ), + "ordered material/pass contract", + ) + require_all( + source + native, + ( + "passes.passes[ 0 ].order != 0", + "passes.passes[ 1 ].order != 1", + "repeated texture semantic must append", + "repeated semantics must remain distinct", + "bounded material list must fail closed at capacity", + ), + "ordered/repeated material self-test", + ) + + require_all( + header + source, + ( + "RENDERER_CLIP_DEPTH_NEGATIVE_ONE_TO_ONE", + "RENDERER_CLIP_DEPTH_ZERO_TO_ONE", + "RENDERER_VIEWPORT_Y_NEGATIVE", + "RendererContracts_GLClipSpace", + "RendererContracts_VulkanClipSpace", + "RendererContracts_ConvertClipMatrix", + "RendererContracts_BuildViewport", + "destination[ z ] = 0.5f * ( canonical[ z ] + canonical[ w ] )", + ), + "explicit clip-space convention", + ) + require_all( + vk_executor, + ( + "RendererContracts_ConvertClipMatrix", + "RendererContracts_GLClipSpace(), RendererContracts_VulkanClipSpace()", + "RendererContracts_BuildViewport", + "RendererContracts_LegacyDrawVertLayout", + ), + "Vulkan shared clip/layout consumption", + ) + require(gl_executor, "RendererContracts_LegacyDrawVertLayout", "OpenGL shared vertex layout consumption") + + require_all( + header + source + native, + ( + "RENDERER_VERTEX_SEMANTIC_JOINT_INDICES", + "RENDERER_VERTEX_SEMANTIC_JOINT_WEIGHTS", + "RENDERER_VERTEX_FORMAT_UINT32X4", + "RendererContracts_SkinnedDrawVertLayout", + "bindings[ 1 ].stride == 32", + "weights->binding == 1 && weights->offset == 16", + "RENDERER_BUFFER_KIND_JOINT_PALETTE", + "RENDERER_BUFFER_LIFETIME_FRAME", + "RENDERER_BUFFER_SLICE_STALE_GENERATION", + "RENDERER_BUFFER_SLICE_EXPIRED", + "RENDERER_BUFFER_SLICE_RANGE_OVERFLOW", + "static_assert( sizeof( rendererBufferHandle_t )", + ), + "semantic layout and typed generational buffer contract", + ) + require(meson, "'openq4-renderer-contracts-test'", "native renderer contract target") + require(meson, "'src/renderer/RendererContracts.cpp'", "native renderer contract source") + + +def validate_four_weight_contract() -> None: + header = read(RENDERER / "GpuSkinning.h") + source = read(RENDERER / "GpuSkinning.cpp") + + require_all( + header, + ( + "GPU_SKINNING_INFLUENCES = 4", + "uint32\tjointIndices[ GPU_SKINNING_INFLUENCES ]", + "float\tjointWeights[ GPU_SKINNING_INFLUENCES ]", + "GPU_SKINNING_JOINT_FLOATS = 12", + "GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS", + "GPU_SKINNING_FALLBACK_JOINT_INDEX", + "GPU_SKINNING_FALLBACK_MALFORMED_WEIGHTS", + "GPU_SKINNING_FALLBACK_STENCIL_VOLUME", + "GPU_SKINNING_FALLBACK_DECAL_OVERLAY", + "GPU_SKINNING_FALLBACK_STALE_PALETTE", + "R_GpuSkinning_PackVertexExact", + "R_GpuSkinning_ValidateSurface", + "R_GpuSkinning_RunSelfTest", + ), + "exact four-weight API", + ) + packer = braced_body(source, "bool R_GpuSkinning_PackVertexExact(", "exact four-weight packer") + require_all( + packer, + ( + "!std::isfinite( weight )", + "!allowSignedWeights && weight < 0.0f", + "influences[i].jointIndex >= static_cast( numJoints )", + "result.meaningfulInfluences > GPU_SKINNING_INFLUENCES", + "GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS", + "packed.jointIndices[ packedIndex ] = influences[i].jointIndex", + "packed.jointWeights[ packedIndex ] = weight", + ), + "exact packer validation", + ) + for forbidden in (r"\bsort\s*\(", r"normali[sz]", r"\bclamp"): + reject_regex(packer, forbidden, "four-weight truncation/renormalization") + + deform = braced_body(source, "bool R_GpuSkinning_DeformVertexCPU(", "CPU parity deform") + require_order( + deform, + ( + "deformed = bindPose;", + "deformed.xyz = position;", + "deformed.normal = normal;", + "deformed.tangents[0] = tangent0;", + "deformed.tangents[1] = tangent1;", + ), + "color-preserving CPU parity deform", + ) + reject(deform, "SetColor", "CPU parity color mutation") + require_all( + source, + ( + "source[valueIndex]", + "gpuSkinningJointPalette", + "gpuSkinningPaletteGeneration = rg_gpuSkinningGeneration", + "surface.palette.generation != rg_gpuSkinningGeneration", + "R_BackendGpuSkinning_PrepareAmbientCache", + '"GPU skinning: enabled=%d', + "cumulative=1", + '"GPU skinning fallbacks:', + ), + "copied joint palette, backend seam, and stable diagnostics", + ) + + self_test = braced_body(source, "bool R_GpuSkinning_RunSelfTest(", "GPU skinning self-test") + require_all( + self_test, + ( + "R_GpuSkinning_PackVertexExact( influences, 4, 4, false", + "R_GpuSkinning_PackVertexExact( influences, 5, 4, false", + "GPU_SKINNING_FALLBACK_RESIDUAL_WEIGHTS", + "R_GpuSkinning_PackVertexExact( influences, 1, 4, true", + "deformed.GetColor() != bindPose.GetColor()", + "R_GpuSkinning_CompareVertexCPU", + ), + "rigid/exact/residual/signed/color parity self-test", + ) + + +def validate_model_integration_and_ownership() -> None: + model_header = read(RENDERER / "Model.h") + companion_path = GAME_ROOT / "src" / "renderer" / "Model.h" + companion_header = read(companion_path) + if normalized_source(model_header) != normalized_source(companion_header): + raise AssertionError( + f"engine and companion srfTriangles_t ABI headers differ: {companion_path}" + ) + require_all( + model_header, + ( + "gpuSkinningBindPoseVerts", + "gpuSkinningVerts", + "numGpuSkinningVerts", + "gpuSkinningJointPalette", + "gpuSkinningJointPaletteAlloc", + "numGpuSkinningJoints", + "gpuSkinningPaletteGeneration", + "gpuSkinningFallbackReason", + "gpuSkinningSignedWeights", + ), + "mirrored triangle-surface GPU contract ABI", + ) + + geometry_header = read(ROOT / "src" / "render_geo" / "RenderGeometry.h") + geometry = read(ROOT / "src" / "render_geo" / "RenderGeometryTriSurf.cpp") + require_all( + geometry_header + geometry, + ( + "R_AllocStaticGpuSkinningJointPalette", + "R_ClearStaticGpuSkinningJointPalette", + "R_ReferenceStaticGpuSkinning", + "R_CopyStaticGpuSkinning", + "gpuSkinningJointPaletteAllocator.Free", + "gpuSkinningJointPaletteAlloc = NULL", + ), + "triangle-surface palette ownership", + ) + reference = braced_body( + geometry, "void R_ReferenceStaticGpuSkinning(", "GPU skinning reference ownership" + ) + reject(reference, "gpuSkinningJointPaletteAlloc = reference", "borrowed palette allocation marker") + copied = braced_body( + geometry, "bool R_CopyStaticGpuSkinning(", "GPU skinning copied ownership" + ) + require(copied, "memcpy( tri->gpuSkinningJointPalette", "deep copied palette") + + model_local = read(RENDERER / "Model_local.h") + md5 = read(RENDERER / "Model_md5.cpp") + require_all( + model_local + md5, + ( + "idList\t\t\tgpuBindPoseVerts", + "idList gpuSkinningVerts", + "BuildGpuSkinningSidecar", + "R_GpuSkinning_PackVertexExact", + "false, gpuSkinningVerts[vertexIndex]", + "deformInfo->mirroredVerts", + "R_GpuSkinning_AttachSurfaceContract", + "GPU_SKINNING_FALLBACK_SKIN_SCALE", + "SIMDProcessor->TransformVertsNew( tri->verts", + "R_GpuSkinning_RecordCpuSkinning", + ), + "classic MD5 exact sidecar and retained CPU positions", + ) + update = braced_body(md5, "void idMD5Mesh::UpdateSurface(", "classic MD5 update") + require_order( + update, + ( + "R_GpuSkinning_AttachSurfaceContract(", + "SIMDProcessor->TransformVertsNew( tri->verts", + "R_GpuSkinning_RecordCpuSkinning", + ), + "classic MD5 GPU contract plus CPU position ownership", + ) + require_all( + md5, + ( + "r_useNewSkinning.GetBool() && !collisionOnly", + "mesh->UpdateSurface( ent, entJoints, surf, !collisionOnly,", + "!collisionOnly && gpuJointPaletteReady );", + ), + "classic MD5 collision-only CPU skinning", + ) + + md5r = read(RENDERER / "Model_md5r.cpp") + require_all( + md5r, + ( + "R_MD5R_GetImplicitBlendWeightIndex", + "R_MD5R_GetSkinningBlendWeight", + "idMath::Fabs( blendWeights[ influenceIndex ] )", + "R_GpuSkinning_PackVertexExact", + "true,", + "R_GpuSkinning_AttachSurfaceContract", + "R_GpuSkinning_RecordCpuSkinning", + ), + "MD5R signed exact sidecar and CPU parity", + ) + signed_weight = braced_body( + md5r, + "static ID_INLINE float R_MD5R_GetSkinningBlendWeight(", + "MD5R signed implicit fourth weight", + ) + require_order( + signed_weight, + ( + "influenceIndex == implicitWeightIndex", + "blendWeights[ influenceIndex ]", + "idMath::Fabs( blendWeights[ influenceIndex ] )", + ), + "signed implicit/fabs authored MD5R weights", + ) + md5r_sidecar = braced_body( + md5r, + "void rvRenderModelMD5R::BuildGpuSkinningSidecar(", + "MD5R immutable GPU sidecar", + ) + reject(md5r_sidecar, ".color2", "MD5R authored secondary-color mutation") + reject(md5r_sidecar, "SetColor", "MD5R authored primary-color mutation") + require( + md5r, + "UpdateDynamicSurface( mesh, entJoints, *surface, !collisionOnly, skinScale, !collisionOnly )", + "MD5R collision-only CPU skinning", + ) + + +def validate_backend_execution_and_cpu_invariants() -> None: + shared = read(RENDERER / "GpuSkinning.cpp") + tr_light = read(RENDERER / "tr_light.cpp") + gl = read(RENDERER / "OpenGL" / "GpuSkinningGL.cpp") + vk = read(RENDERER / "Vulkan" / "vk_GuiExecutor.cpp") + interaction = read(RENDERER / "Interaction.cpp") + gl_interactions = read(RENDERER / "draw_arb2.cpp") + vk_interactions = read(RENDERER / "Vulkan" / "vk_Interactions.cpp") + vk_shader = read(RENDERER / "Vulkan" / "shaders" / "gpu_skinning.comp") + shader_header = read(RENDERER / "Vulkan" / "shaders" / "gui_shaders_spv.h") + shader_pin = read(ROOT / "tools" / "tests" / "vk_shader_header_pin.py") + + ambient = braced_body(tr_light, "bool R_CreateAmbientCache(", "ambient cache creation") + require_order( + ambient, + ( + "if ( tri->ambientCache )", + "R_GpuSkinning_PrepareAmbientCache( tri, needsLighting )", + "R_DeriveTangents( tri )", + "vertexCache.Alloc( tri->verts", + ), + "GPU ambient admission before complete CPU fallback", + ) + attach = braced_body(shared, "bool R_GpuSkinning_AttachSurfaceContract(", "surface attachment") + reject(attach, "tri->verts =", "GPU contract mutation of CPU geometry") + shadow_volume = read(RENDERER / "tr_turboshadow.cpp") + require( + shadow_volume, + "SIMDProcessor->CreateShadowCache( &shadowVerts->xyz, vertRemap, localLightOrigin, tri->verts", + "CPU stencil-volume geometry", + ) + require_regex( + tr_light + shadow_volume + interaction + gl_interactions + vk_interactions + vk, + r"R_GpuSkinning_RecordFallback\s*\(\s*GPU_SKINNING_FALLBACK_STENCIL_VOLUME\s*\)", + "classified CPU stencil-volume fallback", + ) + + require_all( + gl, + ( + "void R_BackendGpuSkinning_Init", + "bool R_BackendGpuSkinning_PrepareAmbientCache", + "caps.hasCompute", + "caps.hasSSBO", + "GL_COMPUTE_SHADER", + "layout(std430, binding = 0) readonly buffer SourceWords", + "layout(std430, binding = 1) readonly buffer SkinWords", + "layout(std430, binding = 2) readonly buffer JointWords", + "layout(std430, binding = 3) writeonly buffer OutputWords", + "for (uint word = 0u; word < 16u; ++word)", + "vertexCache.AllocFrameTemp", + "glDispatchCompute", + "GL_SHADER_STORAGE_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT", + "tri->ambientCache = outputCache", + ), + "OpenGL bounded compute corridor", + ) + expected_store_offsets = ["0u", "4u", "8u", "11u"] + gl_store_offsets = re.findall( + r"StoreVec3\s*\(\s*outputBase\s*,\s*([0-9]+u)\s*,", gl + ) + if gl_store_offsets != expected_store_offsets: + raise AssertionError( + "OpenGL compute must overwrite only position/normal/tangent words: " + f"{gl_store_offsets!r}" + ) + + require_all( + vk + vk_shader + shader_header + shader_pin, + ( + "vk_gpu_skinning_comp_spv", + "VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT", + "VK_GpuSkinning_PrepareView", + "VK_Ring_Reserve", + "VK_PIPELINE_STAGE_2_HOST_BIT", + "VK_ACCESS_2_HOST_WRITE_BIT", + "VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT", + "VK_ACCESS_2_SHADER_WRITE_BIT", + "VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT", + "VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT", + "vkCmdDispatch", + "R_GpuSkinning_RecordBackendPrepared", + "void R_BackendGpuSkinning_Init", + "bool R_BackendGpuSkinning_PrepareAmbientCache", + "gpu_skinning.comp", + ), + "Vulkan bounded compute corridor and generated shader pin", + ) + require_all( + vk_shader, + ( + "for ( uint word = 0u; word < 16u; ++word )", + "outputData.words[ outputBase + word ] = sourceData.words[ sourceBase + word ]", + ), + "Vulkan verbatim draw-vertex copy", + ) + vk_store_offsets = re.findall( + r"StoreVec3\s*\(\s*outputBase\s*,\s*([0-9]+u)\s*,", vk_shader + ) + if vk_store_offsets != expected_store_offsets: + raise AssertionError( + "Vulkan compute must overwrite only position/normal/tangent words: " + f"{vk_store_offsets!r}" + ) + upload_tri = braced_body( + vk, + "static bool VK_Exec_UploadTriGeometry(", + "Vulkan visible geometry upload", + ) + require(upload_tri, "gpuSkinningMemo", "Vulkan visible GPU deformation memo") + bind_tri = braced_body(vk, "bool VK_Exec_BindTriGeometry(", "Vulkan visible geometry binding") + require(bind_tri, "VK_Exec_UploadTriGeometry(", "Vulkan validated visible geometry upload") + bind_shadow = braced_body(vk, "bool VK_Exec_BindShadowGeometry(", "Vulkan stencil geometry binding") + require(bind_shadow, "tri->shadowCache", "Vulkan CPU stencil cache") + for token in ("gpuSkinningMemo", "VK_GpuSkinning_PrepareView", "vkCmdDispatch"): + reject(bind_shadow, token, "GPU-deformed stencil volume") + + +def validate_runtime_commands_matrix_and_docs() -> None: + render_init = read(RENDERER / "RenderSystem_init.cpp") + matrix = read(ROOT / "tools" / "tests" / "renderer_validation_matrix.py") + docs = { + "roadmap": read(ROOT / "docs" / "dev" / "idtech5-modernization-roadmap.md"), + "capability matrix": read(ROOT / "docs" / "dev" / "engine-capability-matrix.md"), + "GPU modernization": read(ROOT / "docs" / "dev" / "gpu-skinning-modernization.md"), + "validation matrix": read(ROOT / "docs" / "dev" / "renderer-validation-matrix.md"), + "release completion": read(ROOT / "docs" / "dev" / "release-completion.md"), + "release notes": read(ROOT / "docs" / "dev" / "releases" / "v0.12.0.md"), + } + require_all( + render_init, + ( + 'cmdSystem->AddCommand( "rendererContractsSelfTest"', + 'cmdSystem->AddCommand( "rendererGpuSkinningSelfTest"', + '"RendererContracts self-test passed\\n"', + '"RendererGpuSkinning self-test passed\\n"', + "R_GpuSkinning_ContractInit", + "R_GpuSkinning_ContractShutdown", + "R_GpuSkinning_PrintGfxInfo", + ), + "opt-in CVar, lifecycle, runtime self-tests, and diagnostics", + ) + require_regex( + render_init, + r'idCVar\s+r_gpuSkinning\s*\(\s*"r_gpuSkinning"\s*,\s*"0"\s*,\s*' + r'CVAR_RENDERER\s*\|\s*CVAR_ARCHIVE\s*\|\s*CVAR_BOOL', + "default-off archived GPU skinning CVar", + ) + require_all( + matrix, + ( + '"RendererContracts self-test passed"', + '"RendererGpuSkinning self-test passed"', + '"GPU skinning:"', + '"+rendererContractsSelfTest"', + '"+rendererGpuSkinningSelfTest"', + ), + "renderer validation matrix GPU contract coverage", + ) + doc_tokens = { + "roadmap": ("Milestone C", "four-weight", "CPU fallback", "complete"), + "capability matrix": ("GPU", "skinning", "OpenGL", "Vulkan"), + "GPU modernization": ( + "r_gpuSkinning=0", + "r_gpuSkinning=1", + "--shadow-presets stencil", + "--shadow-presets mapped", + "collision", + "stencil", + "gpu_skinning_evidence.py", + ), + "validation matrix": ("GPU animation", "r_gpuSkinning 0", "r_gpuSkinning 1"), + "release completion": ("GPU", "skinning", "four-weight"), + "release notes": ("GPU animation", "r_gpuSkinning 1", "collision"), + } + for name, tokens in doc_tokens.items(): + require_all(docs[name], tokens, name) + + +def validate_source_discovery() -> None: + source_tool = ROOT / "tools" / "build" / "meson_sources.py" + for emitter in ("renderer_gl", "renderer_vk"): + completed = subprocess.run( + [sys.executable, str(source_tool), "--emit", emitter], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + raise AssertionError( + f"{emitter} source discovery failed:\n{completed.stdout}{completed.stderr}" + ) + sources = completed.stdout.splitlines() + if sources.count("src/renderer/GpuSkinning.cpp") != 1: + raise AssertionError(f"{emitter} must compile shared GpuSkinning.cpp exactly once") + if sources.count("src/renderer/RendererContracts.cpp") != 1: + raise AssertionError(f"{emitter} must compile RendererContracts.cpp exactly once") + gl_adapter_count = sources.count("src/renderer/OpenGL/GpuSkinningGL.cpp") + expected = 1 if emitter == "renderer_gl" else 0 + if gl_adapter_count != expected: + raise AssertionError( + f"{emitter} OpenGL GPU adapter count is {gl_adapter_count}, expected {expected}" + ) + + +def validate_paired_evidence_verifier() -> None: + verifier = ROOT / "tools" / "validation" / "gpu_skinning_evidence.py" + source = read(verifier) + require_all( + source, + ( + "openq4-gpu-skinning-paired-evidence", + "minimum-cpu-p95-improvement", + "GPU skinning fallbacks:", + "stencilVolume", + "compare_images", + "run_self_test", + ), + "paired GPU skinning evidence verifier", + ) + completed = subprocess.run( + [sys.executable, str(verifier), "--self-test"], + cwd=ROOT, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0 or "paired evidence self-test: PASS" not in completed.stdout: + raise AssertionError( + "GPU skinning paired evidence self-test failed:\n" + f"{completed.stdout}{completed.stderr}" + ) + + +def main() -> int: + validate_shared_renderer_contracts() + validate_four_weight_contract() + validate_model_integration_and_ownership() + validate_backend_execution_and_cpu_invariants() + validate_runtime_commands_matrix_and_docs() + validate_source_discovery() + validate_paired_evidence_verifier() + print("renderer GPU skinning contracts: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_milestone_d_acceptance.py b/tools/tests/renderer_milestone_d_acceptance.py new file mode 100644 index 00000000..4aaa3d94 --- /dev/null +++ b/tools/tests/renderer_milestone_d_acceptance.py @@ -0,0 +1,2064 @@ +#!/usr/bin/env python3 +"""Run deterministic Milestone D nested dynamic renderer acceptance evidence. + +This tool deliberately delegates each capture to renderer_gameplay_benchmark.py +so the launch, windowing, engine screenshot, isolated savepath, warning scan, and +runtime-package checks retain one implementation. It adds the Milestone D +six-case matrix, fail-closed renderer-domain diagnostics, exact image parity, +and controlled normal-versus-skip image-difference requirements. + +Only named runtime packages directly below .tmp/stock-runtime are accepted. +Every generated artifact stays below a fresh named directory under +.tmp/renderer-milestone-d. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import math +import os +from pathlib import Path +import re +import shlex +import subprocess +import sys +import time +from dataclasses import dataclass +from typing import Any, Iterable + +# Importing the shared benchmark helper must not create a repository-local +# __pycache__; acceptance writes are restricted to the selected .tmp root. +sys.dont_write_bytecode = True +import renderer_gameplay_benchmark as benchmark +import renderer_milestone_d_fixture as fixture_builder + + +ROOT = Path(__file__).resolve().parents[2] +RUNTIME_PARENT = ROOT / ".tmp" / "stock-runtime" +OUTPUT_PARENT = ROOT / ".tmp" / "renderer-milestone-d" +BENCHMARK_SCRIPT = Path(benchmark.__file__).resolve() +REPORT_JSON_NAME = "renderer_milestone_d_acceptance_report.json" +REPORT_MD_NAME = "renderer_milestone_d_acceptance_report.md" +BENCHMARK_REPORT_JSON = "renderer_gameplay_benchmark_report.json" +BENCHMARK_REPORT_MD = "renderer_gameplay_benchmark_report.md" +SCHEMA_VERSION = 1 +FIXTURE_MANIFEST_NAME = "fixture_manifest.json" +FIXTURE_PURPOSE = ( + "temporary Milestone D nested special-view cinematic/post qualification" +) +FIXTURE_MAP = "maps/tools/milestone_d_nested_dynamic" +FIXTURE_CAMERA = "setviewpos 0 -384 96 0 90 0" +BENCHMARK_RESULT_ID = ( + "sp-milestone-d-nested-dynamic_auto_fps240_vsync0_windowed_default_best" +) +BENCHMARK_PURPOSE = ( + "temporary stock-derived mirror fixture with an authored post/video tail " + "visible only in the captured child view" +) +FIXTURE_RETRIEVED_SHA256 = { + "maps/tools/mv2.map": ( + "412fc796e569660c92dca5d55359e5c4eb3d572754bd1cdff8ce994b0b802fe2" + ), + "video/idlogo.roq": ( + "375c9641c96359c2e0d49cdd3905cc2cdc66a7b067c9a8910d80bf36fb06c8f4" + ), +} +FIXTURE_GENERATED_PATHS = { + "maps/tools/milestone_d_nested_dynamic.map", + "materials/milestone_d_validation.mtr", + "video/milestone_d_idlogo.roq", + "maps/tools/milestone_d_nested_dynamic.proc", + "maps/tools/milestone_d_nested_dynamic.cm", +} + + +class EvidenceError(RuntimeError): + """Raised when an artifact or renderer diagnostic is missing or ambiguous.""" + + +@dataclass(frozen=True) +class CaseSpec: + case_id: str + description: str + shared_subview: int + shared_cinematic_post: int + skip_post_process: int + + @property + def cvars(self) -> dict[str, str]: + return { + "r_rendererSharedSubview": str(self.shared_subview), + "r_rendererSharedCinematicPost": str(self.shared_cinematic_post), + "r_skipPostProcess": str(self.skip_post_process), + } + + +CASES: tuple[CaseSpec, ...] = ( + CaseSpec( + "classic-normal", + "classic subview and cinematic/post paths, normal post processing", + 0, + 0, + 0, + ), + CaseSpec( + "subview-only-normal", + "shared subview only, with cinematic/post domain disabled", + 1, + 0, + 0, + ), + CaseSpec( + "cinematic-only-normal", + "shared cinematic/post only; nested authored post remains vacuous", + 0, + 1, + 0, + ), + CaseSpec( + "both-normal", + "shared subview plus nested cinematic/post ownership", + 1, + 1, + 0, + ), + CaseSpec( + "classic-skip", + "classic paths with r_skipPostProcess forced", + 0, + 0, + 1, + ), + CaseSpec( + "both-skip", + "shared domains requested with deterministic nested post fallback", + 1, + 1, + 1, + ), +) + +CASE_BY_ID = {case.case_id: case for case in CASES} + + +CINEMATIC_PATTERN = re.compile( + r"^Renderer shared cinematic/post: " + r"requested=(?P\d+) prepared=(?P\d+) " + r"valid=(?P\d+) overflow=(?P\d+) " + r"scenes=(?P\d+) views=(?P\d+)" + r"\(root=(?P\d+) post=(?P\d+) " + r"nested=(?P\d+)/(?P\d+) " + r"nestedCinematic=(?P\d+)\) " + r"ready=(?P\d+) fallback=(?P\d+) " + r"cinematicStages=(?P\d+) " + r"currentRender=(?P\d+) " + r"currentDepth=(?P\d+) " + r"hash=(?P[0-9A-Fa-f]{16}) status=(?P\S+) " + r"GL=(?P\d+)/(?P\d+)/" + r"(?P\d+)/(?P\d+) " + r"VK=(?P\d+)/(?P\d+)/" + r"(?P\d+)/(?P\d+)\r?$", + re.MULTILINE, +) + +SUBVIEW_PATTERN = re.compile( + r"^classicSubviewDomain " + r"requested=(?P\d+) prepared=(?P\d+) " + r"frameValid=(?P\d+) overflow=(?P\d+) " + r"status=(?P\S+) scenes=(?P\d+) " + r"subviews=(?P\d+) captures=(?P\d+) " + r"ready=(?P\d+) fallback=(?P\d+) " + r"nested=(?P\d+) " + r"nestedTransactions=(?P\d+) " + r"maxNestingDepth=(?P\d+) " + r"directMirror=(?P\d+) remote=(?P\d+) " + r"mirror=(?P\d+) reflection=(?P\d+) " + r"refraction=(?P\d+) xray=(?P\d+) " + r"colorCubemap=(?P\d+) " + r"depth2D=(?P\d+) " + r"depthCubemap=(?P\d+) " + r"hash=(?P[0-9A-Fa-f]{16})\r?$", + re.MULTILINE, +) + +SUBVIEW_BACKEND_PATTERN = re.compile( + r"^classicSubviewDomain backend=(?PGL|Vulkan) " + r"ownedViews=(?P\d+) fallbackViews=(?P\d+) " + r"nestedOwned=(?P\d+) " + r"nestedTransactions=(?P\d+) " + r"nestedFallback=(?P\d+) " + r"nestedFallbackTransactions=(?P\d+) " + r"directMirror=(?P\d+) remote=(?P\d+) " + r"mirror=(?P\d+) reflection=(?P\d+) " + r"refraction=(?P\d+) xray=(?P\d+) " + r"colorCubemap=(?P\d+) " + r"depth2D=(?P\d+) " + r"depthCubemap=(?P\d+) " + r"mismatches=(?P\d+) duplicate=(?P\d+) " + r"untracked=(?P\d+)\r?$", + re.MULTILINE, +) + +SUBVIEW_VIEW_PREFIX = "classicSubviewDomain view[" +CINEMATIC_PREFIX = "Renderer shared cinematic/post: " +SUBVIEW_PREFIX = "classicSubviewDomain requested=" +SUBVIEW_BACKEND_PREFIX = "classicSubviewDomain backend=" + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--runtime-dir", + required=True, + help="Existing named runtime package directly below .tmp/stock-runtime.", + ) + parser.add_argument( + "--fixture-manifest", + required=True, + help=( + "fixture_manifest.json from the matching fresh fixture evidence " + "directory below .tmp/renderer-milestone-d." + ), + ) + parser.add_argument( + "--render-api", + choices=("gl", "vk", "all"), + default="all", + help="Backend matrix to execute. 'all' runs GL then Vulkan.", + ) + parser.add_argument( + "--output-dir", + default="", + help=( + "Fresh output name or path directly below .tmp/renderer-milestone-d. " + "Defaults to acceptance-." + ), + ) + parser.add_argument( + "--basepath", + default=benchmark.default_basepath(), + help="Quake 4 installation/base path passed to the benchmark harness.", + ) + parser.add_argument( + "--settle-frames", + type=int, + default=60, + help="Deterministic post-load settle frames for each engine capture.", + ) + parser.add_argument( + "--sample-frames", + type=int, + default=5, + help="Pacing-only sample frames before gfxInfo and engine screenshot.", + ) + parser.add_argument( + "--timeout", + type=int, + default=180, + help="Per-case engine timeout forwarded to the benchmark harness.", + ) + parser.add_argument( + "--difference-min-rms", + type=float, + default=0.1, + help="Minimum RMS RGB delta required between normal and skip captures.", + ) + parser.add_argument( + "--difference-min-channels", + type=int, + default=1000, + help="Minimum changed RGB-channel count for normal versus skip.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Validate paths and print the exact six-case command plan without writing or launching.", + ) + args = parser.parse_args(argv) + if args.settle_frames < 1: + parser.error("--settle-frames must be positive") + if args.sample_frames < 1: + parser.error("--sample-frames must be positive") + if args.timeout < 10: + parser.error("--timeout must be at least 10 seconds") + if not math.isfinite(args.difference_min_rms) or args.difference_min_rms <= 0.0: + parser.error("--difference-min-rms must be finite and greater than zero") + if args.difference_min_channels < 1: + parser.error("--difference-min-channels must be positive") + return args + + +def _relative_to(path: Path, parent: Path, description: str) -> Path: + try: + return path.relative_to(parent) + except ValueError as exc: + raise ValueError(f"{description} must stay below {parent}: {path}") from exc + + +def _reject_link_ancestry(path: Path, stop: Path, description: str) -> None: + relative = _relative_to(path, stop, description) + current = stop + if current.exists() and benchmark.is_link_or_junction(current): + raise ValueError(f"{description} ancestry is a link or junction: {current}") + for part in relative.parts: + current /= part + if current.exists() and benchmark.is_link_or_junction(current): + raise ValueError(f"{description} ancestry is a link or junction: {current}") + + +def validate_runtime_dir(raw: str) -> Path: + root = ROOT.absolute() + parent = RUNTIME_PARENT.absolute() + candidate_input = Path(raw) + if candidate_input.is_absolute(): + candidate = candidate_input.absolute() + elif len(candidate_input.parts) == 1: + candidate = (parent / candidate_input).absolute() + else: + candidate = (root / candidate_input).absolute() + if candidate.parent != parent or not candidate.name: + raise ValueError( + "runtime directory must be a named direct child of " + f"{parent}: {candidate}" + ) + _reject_link_ancestry(candidate, root, "runtime directory") + resolved_parent = parent.resolve() + resolved = candidate.resolve() + if resolved.parent != resolved_parent: + raise ValueError( + "resolved runtime directory must remain a direct child of " + f"{resolved_parent}: {resolved}" + ) + candidate = resolved + if not candidate.is_dir(): + raise FileNotFoundError(f"runtime directory does not exist: {candidate}") + benchmark.find_client_executable(candidate) + return candidate + + +def select_output_dir(raw: str) -> Path: + root = ROOT.absolute() + parent = OUTPUT_PARENT.absolute() + if raw: + candidate_input = Path(raw) + if candidate_input.is_absolute(): + candidate = candidate_input.absolute() + elif len(candidate_input.parts) == 1: + candidate = (parent / candidate_input).absolute() + else: + candidate = (root / candidate_input).absolute() + else: + stamp = time.strftime("%Y%m%d-%H%M%S") + candidate = (parent / f"acceptance-{stamp}").absolute() + if candidate.parent != parent or not candidate.name: + raise ValueError( + "output directory must be a named direct child of " + f"{parent}: {candidate}" + ) + _reject_link_ancestry(candidate, root, "output directory") + resolved_parent = parent.resolve() + resolved = candidate.resolve() + if resolved.parent != resolved_parent: + raise ValueError( + "resolved output directory must remain a direct child of " + f"{resolved_parent}: {resolved}" + ) + if candidate.exists(): + raise FileExistsError(f"acceptance output directory already exists: {candidate}") + return candidate + + +def prepare_output_dir(output_dir: Path) -> None: + root = ROOT.absolute() + parent = OUTPUT_PARENT.absolute() + candidate = output_dir.absolute() + if candidate.parent != parent: + raise ValueError(f"output directory escaped {parent}: {candidate}") + _reject_link_ancestry(candidate, root, "output directory") + parent.mkdir(parents=True, exist_ok=True) + _reject_link_ancestry(candidate, root, "output directory") + output_dir.mkdir(exist_ok=False) + + +def selected_apis(value: str) -> tuple[str, ...]: + return ("gl", "vk") if value == "all" else (value,) + + +def command_display(command: Iterable[str]) -> str: + values = list(command) + return subprocess.list2cmdline(values) if os.name == "nt" else shlex.join(values) + + +def build_benchmark_command( + args: argparse.Namespace, + runtime_dir: Path, + output_dir: Path, + api: str, + case: CaseSpec, +) -> list[str]: + command = [ + sys.executable, + "-B", + str(BENCHMARK_SCRIPT), + "--profile", + "milestone-d-nested-dynamic", + "--render-api", + api, + "--runtime-dir", + str(runtime_dir), + "--output-dir", + str(output_dir), + "--pacing-only", + "--no-gpu-timers", + "--display-modes", + "windowed", + "--width", + "1280", + "--height", + "720", + "--settle-frames", + str(args.settle_frames), + "--sample-frames", + str(args.sample_frames), + "--timeout", + str(args.timeout), + "--basepath", + args.basepath, + ] + for name, value in case.cvars.items(): + command.extend(("--set-cvar", f"{name}={value}")) + return command + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def inventory_summary(records: list[dict[str, Any]]) -> dict[str, Any]: + encoded = json.dumps( + records, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + allow_nan=False, + ).encode("utf-8") + return { + "fileCount": len(records), + "totalBytes": sum(int(record["size"]) for record in records), + "manifestSha256": hashlib.sha256(encoded).hexdigest(), + "files": records, + } + + +def path_hint(path: Path) -> str: + try: + return path.resolve().relative_to(ROOT.resolve()).as_posix() + except ValueError: + return str(path.resolve()) + + +def validate_fixture_manifest_path(raw: str) -> Path: + root = ROOT.absolute() + parent = OUTPUT_PARENT.absolute() + manifest_input = Path(raw) + candidate = ( + manifest_input.absolute() + if manifest_input.is_absolute() + else (root / manifest_input).absolute() + ) + if ( + candidate.name != FIXTURE_MANIFEST_NAME + or candidate.parent.parent != parent + or not candidate.parent.name + ): + raise ValueError( + "fixture manifest must be /fixture_manifest.json directly below " + f"{parent}: {candidate}" + ) + _reject_link_ancestry(candidate, root, "fixture manifest") + resolved_parent = parent.resolve() + resolved = candidate.resolve() + if resolved.parent.parent != resolved_parent: + raise ValueError( + "resolved fixture manifest must remain in a direct child of " + f"{resolved_parent}: {resolved}" + ) + if not resolved.is_file(): + raise FileNotFoundError(f"fixture manifest does not exist: {resolved}") + return resolved + + +def _validated_fixture_records( + value: Any, + expected_paths: set[str], + roots: tuple[Path, ...], + description: str, + expected_hashes: dict[str, str] | None = None, +) -> tuple[list[dict[str, Any]], list[Path]]: + if not isinstance(value, list): + raise EvidenceError(f"fixture {description} records are missing or malformed") + records: dict[str, dict[str, Any]] = {} + for item in value: + if not isinstance(item, dict): + raise EvidenceError(f"fixture {description} record is not an object") + relative = item.get("path") + size = item.get("size") + digest = item.get("sha256") + if ( + not isinstance(relative, str) + or type(size) is not int + or size <= 0 + or not isinstance(digest, str) + or re.fullmatch(r"[0-9A-Fa-f]{64}", digest) is None + ): + raise EvidenceError(f"fixture {description} record is malformed: {item!r}") + if relative in records: + raise EvidenceError(f"duplicate fixture {description} path: {relative}") + records[relative] = { + "path": relative, + "size": size, + "sha256": digest.lower(), + } + if set(records) != expected_paths: + raise EvidenceError( + f"fixture {description} paths={sorted(records)!r}; " + f"expected {sorted(expected_paths)!r}" + ) + if expected_hashes is not None: + for relative, expected in expected_hashes.items(): + if records[relative]["sha256"] != expected: + raise EvidenceError( + f"fixture {description} hash mismatch for {relative}: " + f"{records[relative]['sha256']} != {expected}" + ) + + verified_paths: list[Path] = [] + for root in roots: + raw_root = root.absolute() + resolved_root = raw_root.resolve() + for relative in sorted(expected_paths): + raw_path = (raw_root / Path(relative)).absolute() + _reject_link_ancestry(raw_path, ROOT.absolute(), f"fixture {description}") + path = raw_path.resolve() + _relative_to(path, resolved_root, f"fixture {description}") + if not path.is_file(): + raise EvidenceError( + f"fixture {description} file is missing from {resolved_root}: {relative}" + ) + actual = benchmark.file_record(path, resolved_root) + if actual != records[relative]: + raise EvidenceError( + f"fixture {description} file does not match its manifest: " + f"{path}" + ) + verified_paths.append(path) + return [records[path] for path in sorted(records)], verified_paths + + +def validate_fixture_manifest( + raw: str, runtime_dir: Path, basepath: Path +) -> dict[str, Any]: + manifest_path = validate_fixture_manifest_path(raw) + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise EvidenceError(f"fixture manifest is unreadable: {exc}") from exc + if not isinstance(manifest, dict): + raise EvidenceError("fixture manifest top-level value is not an object") + for field, expected in ( + ("schemaVersion", 1), + ("purpose", FIXTURE_PURPOSE), + ("shippingContent", False), + ("map", FIXTURE_MAP), + ("cameraCommand", FIXTURE_CAMERA), + ): + if manifest.get(field) != expected: + raise EvidenceError( + f"fixture manifest {field}={manifest.get(field)!r}; expected {expected!r}" + ) + manifest_runtime = manifest.get("runtimeDir") + if not isinstance(manifest_runtime, str) or Path(manifest_runtime).resolve() != runtime_dir: + raise EvidenceError( + "fixture manifest runtimeDir does not identify the selected runtime: " + f"{manifest_runtime!r}" + ) + manifest_basepath = manifest.get("basepath") + if ( + not isinstance(manifest_basepath, str) + or Path(manifest_basepath).resolve() != basepath + ): + raise EvidenceError( + "fixture manifest basepath does not identify the selected stock root: " + f"{manifest_basepath!r}" + ) + stock_dependencies = fixture_builder.stock_dependency_inventory(basepath) + if manifest.get("stockDependencies") != stock_dependencies: + raise EvidenceError( + "selected stock basepath dependencies do not match the fixture manifest" + ) + runtime_components = fixture_builder.validate_runtime_components(runtime_dir) + if manifest.get("runtimeComponents") != runtime_components: + raise EvidenceError( + "selected runtime components do not match the fixture manifest" + ) + + evidence_root = manifest_path.parent + retrieved, retrieved_paths = _validated_fixture_records( + manifest.get("retrieved"), + set(FIXTURE_RETRIEVED_SHA256), + (evidence_root / "retrieved/baseoq4",), + "retrieved", + FIXTURE_RETRIEVED_SHA256, + ) + generated, generated_paths = _validated_fixture_records( + manifest.get("generated"), + FIXTURE_GENERATED_PATHS, + (evidence_root / "compile/baseoq4", runtime_dir / "baseoq4"), + "generated", + ) + generated_by_path = {record["path"]: record for record in generated} + if ( + generated_by_path["video/milestone_d_idlogo.roq"]["sha256"] + != FIXTURE_RETRIEVED_SHA256["video/idlogo.roq"] + ): + raise EvidenceError("staged fixture video does not match the pinned retail ROQ") + + runtime_game = runtime_dir / "baseoq4" + text_contracts = { + "maps/tools/milestone_d_nested_dynamic.map": ( + "shaderDemos/transparentMirror", + "milestoneD/nestedCinematicPost", + ), + "materials/milestone_d_validation.mtr": ( + "milestoneD/nestedCinematicPost", + "sort postProcess", + "videoMap loop video/milestone_d_idlogo.roq", + "map _currentRender", + ), + "maps/tools/milestone_d_nested_dynamic.proc": ( + "shaderDemos/transparentMirror", + "milestoneD/nestedCinematicPost", + ), + } + for relative, tokens in text_contracts.items(): + try: + text = (runtime_game / relative).read_text( + encoding="utf-8", errors="strict" + ) + except (OSError, UnicodeError) as exc: + raise EvidenceError(f"fixture contract file is unreadable: {relative}: {exc}") from exc + folded = text.casefold() + for token in tokens: + if token.casefold() not in folded: + raise EvidenceError( + f"fixture contract file {relative} lacks {token!r}" + ) + + evidence_files = [manifest_path, *retrieved_paths, *generated_paths] + evidence_inventory = inventory_summary( + sorted( + ( + benchmark.file_record(path, evidence_root) + for path in evidence_files + if path.is_relative_to(evidence_root) + ), + key=lambda record: record["path"], + ) + ) + return { + "manifestPath": path_hint(manifest_path), + "manifestSha256": sha256_file(manifest_path), + "manifestArtifact": { + "kind": "fixtureManifest", + "path": path_hint(manifest_path), + "size": manifest_path.stat().st_size, + "sha256": sha256_file(manifest_path), + }, + "purpose": FIXTURE_PURPOSE, + "shippingContent": False, + "runtimeDir": path_hint(runtime_dir), + "basepath": str(basepath), + "stockDependencies": stock_dependencies, + "runtimeComponents": runtime_components, + "map": FIXTURE_MAP, + "cameraCommand": FIXTURE_CAMERA, + "retrieved": retrieved, + "generated": generated, + "evidenceInventory": evidence_inventory, + } + + +def _record_artifact(path: Path, output_root: Path, kind: str) -> dict[str, Any]: + resolved = path.resolve() + _relative_to(resolved, output_root.resolve(), "evidence artifact") + if not resolved.is_file(): + raise EvidenceError(f"required {kind} artifact is missing: {resolved}") + record = benchmark.file_record(resolved, output_root.resolve()) + return {"kind": kind, **record} + + +def _resolve_role_artifact(value: Any, case_dir: Path, label: str) -> Path: + if not isinstance(value, str) or not value: + raise EvidenceError(f"benchmark role has no {label} path") + path = Path(value).resolve() + _relative_to(path, case_dir.resolve(), f"benchmark {label}") + if not path.is_file(): + raise EvidenceError(f"benchmark {label} is missing: {path}") + return path + + +def _converted_match(match: re.Match[str], textual: set[str]) -> dict[str, Any]: + record: dict[str, Any] = {} + for name, value in match.groupdict().items(): + record[name] = value if name in textual else int(value) + if "hash" in record: + record["hash"] = str(record["hash"]).lower() + return record + + +def _single_match( + pattern: re.Pattern[str], + text: str, + description: str, + textual: set[str], + prefix: str, +) -> tuple[dict[str, Any], str]: + prefix_count = sum(line.startswith(prefix) for line in text.splitlines()) + matches = list(pattern.finditer(text)) + if prefix_count != 1 or len(matches) != prefix_count: + raise EvidenceError( + f"expected exactly one well-formed {description} diagnostic, " + f"found prefixes={prefix_count} parsed={len(matches)}" + ) + match = matches[0] + return _converted_match(match, textual), match.group(0).rstrip("\r") + + +def parse_renderer_diagnostics(text: str) -> dict[str, Any]: + """Parse the exact gfxInfo diagnostics used by Milestone D acceptance.""" + cinematic, cinematic_line = _single_match( + CINEMATIC_PATTERN, + text, + "shared cinematic/post", + {"hash", "status"}, + CINEMATIC_PREFIX, + ) + subview, subview_line = _single_match( + SUBVIEW_PATTERN, + text, + "classic subview summary", + {"hash", "status"}, + SUBVIEW_PREFIX, + ) + backend_prefix_count = sum( + line.startswith(SUBVIEW_BACKEND_PREFIX) for line in text.splitlines() + ) + backend_matches = list(SUBVIEW_BACKEND_PATTERN.finditer(text)) + if backend_prefix_count != 2 or len(backend_matches) != backend_prefix_count: + raise EvidenceError( + "expected exactly two well-formed classic subview backend diagnostics, " + f"found prefixes={backend_prefix_count} parsed={len(backend_matches)}" + ) + backends: dict[str, dict[str, Any]] = {} + backend_lines: dict[str, str] = {} + for match in backend_matches: + record = _converted_match(match, {"backend"}) + key = "gl" if record.pop("backend") == "GL" else "vulkan" + if key in backends: + raise EvidenceError(f"duplicate classic subview backend diagnostic: {key}") + backends[key] = record + backend_lines[key] = match.group(0).rstrip("\r") + if set(backends) != {"gl", "vulkan"}: + raise EvidenceError("classic subview diagnostics do not cover GL and Vulkan") + view_lines = [ + line.rstrip("\r") + for line in text.splitlines() + if line.startswith(SUBVIEW_VIEW_PREFIX) + ] + return { + "cinematicPost": cinematic, + "subview": subview, + "subviewBackends": backends, + "subviewViewLines": view_lines, + "sourceLines": { + "cinematicPost": cinematic_line, + "subview": subview_line, + "subviewBackends": backend_lines, + "subviewViews": view_lines, + }, + } + + +def _expect_equal( + failures: list[str], + record: dict[str, Any], + field: str, + expected: Any, + label: str, +) -> None: + actual = record.get(field) + if actual != expected: + failures.append(f"{label} {field}={actual!r}; expected {expected!r}") + + +def _expect_positive( + failures: list[str], record: dict[str, Any], field: str, label: str +) -> None: + actual = record.get(field) + if not isinstance(actual, int) or actual <= 0: + failures.append(f"{label} {field}={actual!r}; expected a positive count") + + +def _expect_zero_fields( + failures: list[str], + record: dict[str, Any], + fields: Iterable[str], + label: str, +) -> None: + for field in fields: + _expect_equal(failures, record, field, 0, label) + + +def validate_case_diagnostics( + case: CaseSpec, api: str, diagnostics: dict[str, Any] +) -> list[str]: + failures: list[str] = [] + cinematic = diagnostics["cinematicPost"] + subview = diagnostics["subview"] + backends = diagnostics["subviewBackends"] + active_key = "gl" if api == "gl" else "vulkan" + inactive_key = "vulkan" if api == "gl" else "gl" + active_label = "GL" if api == "gl" else "Vulkan" + inactive_label = "Vulkan" if api == "gl" else "GL" + active = backends[active_key] + inactive = backends[inactive_key] + + _expect_equal(failures, cinematic, "overflow", 0, "cinematic/post") + _expect_equal(failures, subview, "overflow", 0, "subview") + _expect_equal( + failures, + cinematic, + "status", + "prepared" if case.shared_cinematic_post else "reset", + "cinematic/post", + ) + _expect_equal( + failures, + subview, + "status", + "ready" if case.shared_subview else "empty", + "subview", + ) + for prefix in ("gl", "vk"): + _expect_zero_fields( + failures, + cinematic, + (f"{prefix}_mismatch", f"{prefix}_duplicate"), + f"cinematic/post {prefix}", + ) + for backend_name, backend in backends.items(): + _expect_zero_fields( + failures, + backend, + ("mismatches", "duplicate", "untracked"), + f"subview {backend_name}", + ) + _expect_zero_fields( + failures, + inactive, + ( + "owned", + "fallback", + "nested_owned", + "nested_transactions", + "nested_fallback", + "nested_fallback_transactions", + "direct_mirror", + "remote", + "mirror", + "reflection", + "refraction", + "xray", + "color_cubemap", + "depth_2d", + "depth_cubemap", + ), + f"subview inactive {inactive_key}", + ) + inactive_prefix = "vk" if api == "gl" else "gl" + _expect_equal( + failures, + cinematic, + f"{inactive_prefix}_owned", + 0, + "cinematic/post inactive backend", + ) + _expect_equal( + failures, + cinematic, + f"{inactive_prefix}_fallback", + 0, + "cinematic/post inactive backend", + ) + + disabled_cinematic_fields = ( + "requested", + "prepared", + "valid", + "scenes", + "views", + "root", + "post", + "nested_views", + "nested_transactions", + "nested_cinematic", + "ready", + "fallback", + "cinematic_stages", + "current_render", + "current_depth", + "gl_owned", + "gl_fallback", + "vk_owned", + "vk_fallback", + ) + disabled_subview_fields = ( + "requested", + "prepared", + "valid", + "scenes", + "subviews", + "captures", + "ready", + "fallback", + "nested", + "nested_transactions", + "max_depth", + "direct_mirror", + "remote", + "mirror", + "reflection", + "refraction", + "xray", + "color_cubemap", + "depth_2d", + "depth_cubemap", + ) + + if not case.shared_cinematic_post: + _expect_zero_fields( + failures, cinematic, disabled_cinematic_fields, "cinematic/post disabled" + ) + elif case.case_id == "cinematic-only-normal": + for field in ("requested", "prepared", "valid"): + _expect_equal(failures, cinematic, field, 1, "cinematic-only") + _expect_positive(failures, cinematic, "scenes", "cinematic-only") + _expect_zero_fields( + failures, + cinematic, + ( + "views", + "root", + "post", + "nested_views", + "nested_transactions", + "nested_cinematic", + "ready", + "fallback", + "cinematic_stages", + "current_render", + "current_depth", + "gl_owned", + "gl_fallback", + "vk_owned", + "vk_fallback", + ), + "cinematic-only vacuous nested authored post", + ) + else: + for field in ("requested", "prepared", "valid"): + _expect_equal(failures, cinematic, field, 1, "cinematic/post enabled") + _expect_positive(failures, cinematic, "scenes", "cinematic/post enabled") + for field, expected in ( + ("views", 1), + ("root", 0), + ("post", 1), + ("nested_views", 1), + ("nested_transactions", 1), + ("ready", 1), + ("fallback", 0), + ("current_depth", 0), + ): + _expect_equal(failures, cinematic, field, expected, "cinematic/post enabled") + _expect_positive( + failures, cinematic, "nested_cinematic", "cinematic/post enabled" + ) + _expect_positive( + failures, cinematic, "cinematic_stages", "cinematic/post enabled" + ) + _expect_positive( + failures, cinematic, "current_render", "cinematic/post enabled" + ) + if cinematic.get("hash") == "0000000000000000": + failures.append("cinematic/post enabled hash is zero") + prefix = "gl" if api == "gl" else "vk" + expected_owned = 0 if case.skip_post_process else 1 + expected_fallback = 1 if case.skip_post_process else 0 + _expect_equal( + failures, + cinematic, + f"{prefix}_owned", + expected_owned, + "cinematic/post active backend", + ) + _expect_equal( + failures, + cinematic, + f"{prefix}_fallback", + expected_fallback, + "cinematic/post active backend", + ) + + if not case.shared_subview: + _expect_zero_fields( + failures, subview, disabled_subview_fields, "subview disabled" + ) + _expect_zero_fields( + failures, + active, + ( + "owned", + "fallback", + "nested_owned", + "nested_transactions", + "nested_fallback", + "nested_fallback_transactions", + "direct_mirror", + "remote", + "mirror", + "reflection", + "refraction", + "xray", + "color_cubemap", + "depth_2d", + "depth_cubemap", + ), + "subview disabled active backend", + ) + else: + for field in ("requested", "prepared", "valid"): + _expect_equal(failures, subview, field, 1, "subview enabled") + _expect_positive(failures, subview, "scenes", "subview enabled") + for field, expected in ( + ("subviews", 1), + ("captures", 1), + ("ready", 1), + ("fallback", 0), + ("nested", 0), + ("nested_transactions", 0), + ("max_depth", 0), + ("direct_mirror", 0), + ("remote", 0), + ("mirror", 1), + ("reflection", 0), + ("refraction", 0), + ("xray", 0), + ("color_cubemap", 0), + ("depth_2d", 0), + ("depth_cubemap", 0), + ): + _expect_equal(failures, subview, field, expected, "subview enabled") + if subview.get("hash") == "0000000000000000": + failures.append("subview enabled hash is zero") + expected_owned = 0 if case.skip_post_process else 1 + expected_fallback = 1 if case.skip_post_process else 0 + _expect_equal( + failures, active, "owned", expected_owned, "subview active backend" + ) + _expect_equal( + failures, + active, + "fallback", + expected_fallback, + "subview active backend", + ) + _expect_equal( + failures, + active, + "mirror", + expected_owned, + "subview active backend", + ) + _expect_zero_fields( + failures, + active, + ( + "nested_owned", + "nested_transactions", + "nested_fallback", + "nested_fallback_transactions", + "direct_mirror", + "remote", + "reflection", + "refraction", + "xray", + "color_cubemap", + "depth_2d", + "depth_cubemap", + ), + "subview active backend", + ) + + view_lines = diagnostics["subviewViewLines"] + if case.shared_subview: + if len(view_lines) != 1: + failures.append( + f"expected exactly one prepared subview view diagnostic, found {len(view_lines)}" + ) + else: + view_line = view_lines[0] + required_tokens = ( + "parentView=-1 root=0 depth=0 subtree=1", + "kind=mirror captureType=color2D ready=1 failure=none", + "captureImage=_scratch rect=0,0 1024x256", + ) + for token in required_tokens: + if token not in view_line: + failures.append(f"subview view diagnostic is missing {token!r}") + inactive_outcome = f"{inactive_label}=0/none/0" + if inactive_outcome not in view_line: + failures.append( + "subview view diagnostic lacks inactive backend outcome " + f"{inactive_outcome!r}" + ) + if case.skip_post_process: + outcome_prefix = ( + f"{active_label}=2/nestedCinematicPostFallback/" + ) + outcome_match = re.search( + rf"{re.escape(outcome_prefix)}([1-9][0-9]*)(?:\s|$)", + view_line, + ) + if outcome_match is None: + failures.append( + "both-on skip lacks a named nestedCinematicPostFallback " + "diagnostic with a positive backend detail" + ) + else: + outcome = f"{active_label}=1/none/0" + if outcome not in view_line: + failures.append( + f"subview view diagnostic lacks active backend outcome {outcome!r}" + ) + elif view_lines: + failures.append( + f"disabled shared subview unexpectedly emitted {len(view_lines)} view diagnostic(s)" + ) + + return failures + + +def _verify_recorded_artifacts( + report: dict[str, Any], case_dir: Path, failures: list[str] +) -> None: + results = report.get("results") + if not isinstance(results, list) or len(results) != 1: + return + roles = results[0].get("roles") + if not isinstance(roles, list) or len(roles) != 1: + return + artifacts = roles[0].get("artifacts") + if not isinstance(artifacts, list): + failures.append("benchmark role artifact manifest is missing or malformed") + return + for artifact in artifacts: + if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str): + failures.append("benchmark role artifact record is malformed") + continue + path = (case_dir / artifact["path"]).resolve() + try: + _relative_to(path, case_dir.resolve(), "benchmark recorded artifact") + except ValueError as exc: + failures.append(str(exc)) + continue + if not path.is_file(): + failures.append(f"benchmark recorded artifact is missing: {artifact['path']}") + continue + actual = benchmark.file_record(path, case_dir.resolve()) + for field in ("path", "size", "sha256"): + if actual.get(field) != artifact.get(field): + failures.append( + f"benchmark artifact {artifact['path']} has mismatched {field}" + ) + + +def inspect_benchmark_output( + case_dir: Path, + output_root: Path, + runtime_records: list[dict[str, Any]], + api: str, + case: CaseSpec, + process_returncode: int, + command: list[str], + expected_basepath: Path, +) -> dict[str, Any]: + failures: list[str] = [] + artifacts: list[dict[str, Any]] = [] + diagnostics: dict[str, Any] | None = None + screenshot_path: Path | None = None + log_path: Path | None = None + engine_stdout_path: Path | None = None + engine_stderr_path: Path | None = None + capture_script_path: Path | None = None + capture_script: dict[str, Any] | None = None + display_evidence: dict[str, Any] | None = None + report_path = case_dir / BENCHMARK_REPORT_JSON + report_md_path = case_dir / BENCHMARK_REPORT_MD + report: dict[str, Any] = {} + + if process_returncode != 0: + failures.append(f"benchmark process exited with code {process_returncode}") + if not report_path.is_file(): + failures.append(f"benchmark JSON report is missing: {report_path}") + else: + try: + loaded = json.loads(report_path.read_text(encoding="utf-8")) + if not isinstance(loaded, dict): + raise ValueError("top-level JSON value is not an object") + report = loaded + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + failures.append(f"benchmark JSON report is unreadable: {exc}") + + if report: + if report.get("schemaVersion") != benchmark.REPORT_SCHEMA_VERSION: + failures.append( + f"benchmark schemaVersion={report.get('schemaVersion')!r}; " + f"expected {benchmark.REPORT_SCHEMA_VERSION}" + ) + if report.get("status") != "pass": + failures.append(f"benchmark report status={report.get('status')!r}; expected 'pass'") + if report.get("dryRun") is not False: + failures.append("benchmark report is not real-run evidence") + if report.get("budgetEnforced") is not False: + failures.append("benchmark report is not pacing-only evidence") + if report.get("runtimeVerificationFailures") != []: + failures.append( + "benchmark detected runtime mutation: " + f"{report.get('runtimeVerificationFailures')!r}" + ) + runtime = report.get("runtime") + report_runtime_files = runtime.get("files") if isinstance(runtime, dict) else None + runtime_differences = benchmark.compare_file_records( + runtime_records, report_runtime_files, "benchmark runtime file" + ) if isinstance(report_runtime_files, list) else [ + "benchmark runtime file inventory is missing or malformed" + ] + failures.extend(runtime_differences) + metadata = report.get("metadata") + if not isinstance(metadata, dict): + failures.append("benchmark metadata is missing or malformed") + else: + for field, expected in ( + ("profile", "milestone-d-nested-dynamic"), + ("renderApi", api), + ("dryRun", False), + ("basepath", str(expected_basepath)), + ): + if metadata.get(field) != expected: + failures.append( + f"benchmark metadata {field}={metadata.get(field)!r}; " + f"expected {expected!r}" + ) + results = report.get("results") + if not isinstance(results, list) or len(results) != 1: + failures.append("benchmark report must contain exactly one result") + else: + result = results[0] + if result.get("status") != "pass": + failures.append( + f"benchmark result status={result.get('status')!r}; expected 'pass'" + ) + expected_backend = "opengl" if api == "gl" else "vulkan" + for field, expected in ( + ("id", BENCHMARK_RESULT_ID), + ("mode", "SP"), + ("map", FIXTURE_MAP), + ("budgetMap", FIXTURE_MAP), + ("purpose", BENCHMARK_PURPOSE), + ("renderApi", api), + ("expectedBackend", expected_backend), + ("display", "windowed"), + ): + if result.get(field) != expected: + failures.append( + f"benchmark result {field}={result.get(field)!r}; " + f"expected {expected!r}" + ) + display_contract = result.get("displayContract") + display_cvars = ( + display_contract.get("cvars") + if isinstance(display_contract, dict) + else None + ) + if not isinstance(display_cvars, dict): + failures.append("benchmark display contract is missing") + else: + for name, expected in ( + ("r_fullscreen", "0"), + ("r_borderless", "0"), + ("r_windowWidth", "1280"), + ("r_windowHeight", "720"), + ): + if display_cvars.get(name) != expected: + failures.append( + f"benchmark display contract {name}=" + f"{display_cvars.get(name)!r}; expected {expected!r}" + ) + roles = result.get("roles") + if not isinstance(roles, list) or len(roles) != 1: + failures.append("benchmark result must contain exactly one role") + else: + role = roles[0] + if ( + role.get("id") != BENCHMARK_RESULT_ID + or role.get("role") != "sp" + or role.get("status") != "pass" + ): + failures.append( + "benchmark role must be a passing SP capture; " + f"got id={role.get('id')!r} role={role.get('role')!r} " + f"status={role.get('status')!r}" + ) + if role.get("timedOut") is not False or role.get("exitCode") != 0: + failures.append( + "benchmark engine process did not exit cleanly: " + f"exit={role.get('exitCode')!r} timedOut={role.get('timedOut')!r}" + ) + warnings = role.get("warnings") + if not isinstance(warnings, dict): + failures.append("benchmark warning buckets are missing") + else: + nonzero_warnings = { + name: value for name, value in warnings.items() if value != 0 + } + if nonzero_warnings: + failures.append( + f"benchmark fatal/error warning buckets are nonzero: {nonzero_warnings}" + ) + if role.get("missing") != []: + failures.append(f"benchmark role reports missing evidence: {role.get('missing')!r}") + if role.get("failureDiagnostics") != []: + failures.append( + "benchmark role reports failure diagnostics: " + f"{role.get('failureDiagnostics')!r}" + ) + try: + log_path = _resolve_role_artifact(role.get("log"), case_dir, "engine log") + screenshot_path = _resolve_role_artifact( + role.get("screenshot"), case_dir, "engine screenshot" + ) + engine_stdout_path = _resolve_role_artifact( + role.get("stdout"), case_dir, "engine process stdout" + ) + engine_stderr_path = _resolve_role_artifact( + role.get("stderr"), case_dir, "engine process stderr" + ) + except EvidenceError as exc: + failures.append(str(exc)) + if role.get("screenshotRequest") != "screenshots/renderer-bench/sp_0.tga": + failures.append( + "benchmark did not request the canonical engine TGA screenshot" + ) + image = role.get("image") + image_status = image.get("status") if isinstance(image, dict) else None + if image_status not in ("not-requested", "compared"): + failures.append( + "benchmark image status is neither an unreferenced engine capture " + "nor a completed comparison" + ) + elif image_status == "not-requested": + if screenshot_path is not None and image.get("sha256") != sha256_file( + screenshot_path + ): + failures.append( + "benchmark image SHA-256 does not match engine screenshot" + ) + elif not ( + image.get("pass") is True + and image.get("rms") == 0.0 + and image.get("maxDelta") == 0 + and image.get("differingChannels") == 0 + ): + failures.append("benchmark's pre-existing image comparison is not exact") + + metadata = report.get("metadata") + if isinstance(metadata, dict): + expected_commands = ["g_stopTime 1", "noclip", FIXTURE_CAMERA] + if metadata.get("profileExecCommands") != expected_commands: + failures.append( + "benchmark profile commands do not match the fixed-clock camera contract" + ) + launch_cvars = metadata.get("launchCvars") + if not isinstance(launch_cvars, dict): + failures.append("benchmark launch CVar contract is missing") + else: + for name, expected in (("in_mouse", "0"), ("g_stopTime", "1")): + if launch_cvars.get(name) != expected: + failures.append( + f"benchmark launch CVar {name}={launch_cvars.get(name)!r}; " + f"expected {expected!r}" + ) + _verify_recorded_artifacts(report, case_dir, failures) + + diagnostic_sources: list[str] = [] + if log_path is not None: + try: + log_text = log_path.read_text(encoding="utf-8", errors="replace") + diagnostic_sources.append(log_text) + diagnostics = parse_renderer_diagnostics(log_text) + failures.extend(validate_case_diagnostics(case, api, diagnostics)) + except (OSError, EvidenceError) as exc: + failures.append(f"renderer diagnostic parse failed: {exc}") + + capture_script_path = log_path.parent.parent / "renderer-bench" / "sp_0.cfg" + if not capture_script_path.is_file(): + failures.append(f"generated capture script is missing: {capture_script_path}") + else: + try: + script_text = capture_script_path.read_text( + encoding="utf-8", errors="strict" + ) + final_cvars: dict[str, str] = {} + expected_cvars = { + **case.cvars, + "r_rendererMetrics": "0", + "r_rendererGpuTimers": "0", + } + for name, expected in expected_cvars.items(): + values = re.findall( + rf"^{re.escape(name)}\s+(\S+)\s*$", + script_text, + flags=re.MULTILINE, + ) + if not values: + failures.append(f"capture script does not set {name}") + continue + final_cvars[name] = values[-1] + if values[-1] != expected: + failures.append( + f"capture script final {name}={values[-1]!r}; expected {expected!r}" + ) + required_commands = ( + "g_stopTime 1", + "noclip", + FIXTURE_CAMERA, + "viewpos", + "gfxInfo", + 'screenshot "screenshots/renderer-bench/sp_0.tga"', + ) + for required in required_commands: + if required not in script_text.splitlines(): + failures.append( + f"capture script lacks required engine command {required!r}" + ) + capture_script = { + "path": capture_script_path.resolve().relative_to( + output_root.resolve() + ).as_posix(), + "sha256": sha256_file(capture_script_path), + "finalCvars": final_cvars, + "commands": list(required_commands), + } + except (OSError, UnicodeError) as exc: + failures.append(f"generated capture script is unreadable: {exc}") + + screenshot: dict[str, Any] | None = None + if screenshot_path is not None: + try: + width, height, _ = benchmark.load_tga_rgb(screenshot_path) + screenshot = { + "path": screenshot_path.resolve().relative_to(output_root.resolve()).as_posix(), + "width": width, + "height": height, + "sha256": sha256_file(screenshot_path), + } + if (width, height) != (1280, 720): + failures.append( + f"engine screenshot is {width}x{height}; expected 1280x720" + ) + except (OSError, ValueError) as exc: + failures.append(f"engine screenshot is not a valid supported TGA: {exc}") + + for description, path in ( + ("engine process stdout", engine_stdout_path), + ("engine process stderr", engine_stderr_path), + ): + if path is None: + continue + try: + diagnostic_sources.append(path.read_text(encoding="utf-8", errors="replace")) + except OSError as exc: + failures.append(f"{description} is unreadable for display evidence: {exc}") + display_evidence, display_failures = benchmark.evaluate_display_evidence( + diagnostic_sources, screenshot_path, 1280, 720 + ) + failures.extend(f"actual display evidence: {failure}" for failure in display_failures) + + artifact_candidates = ( + (report_path, "benchmarkReportJson"), + (report_md_path, "benchmarkReportMarkdown"), + (log_path, "engineLog"), + (engine_stdout_path, "engineProcessStdout"), + (engine_stderr_path, "engineProcessStderr"), + (screenshot_path, "engineScreenshot"), + (capture_script_path, "generatedCaptureScript"), + (case_dir / "acceptance_benchmark_process.out.txt", "benchmarkProcessStdout"), + (case_dir / "acceptance_benchmark_process.err.txt", "benchmarkProcessStderr"), + ) + for path, kind in artifact_candidates: + if path is None or not path.is_file(): + continue + try: + artifacts.append(_record_artifact(path, output_root, kind)) + except EvidenceError as exc: + failures.append(str(exc)) + + return { + "id": case.case_id, + "description": case.description, + "cvars": case.cvars, + "command": command, + "commandDisplay": command_display(command), + "benchmarkExitCode": process_returncode, + "status": "pass" if not failures else "fail", + "failures": failures, + "diagnostics": diagnostics, + "screenshot": screenshot, + "displayEvidence": display_evidence, + "captureScript": capture_script, + "artifacts": artifacts, + } + + +def run_case( + args: argparse.Namespace, + runtime_dir: Path, + output_root: Path, + runtime_records: list[dict[str, Any]], + api: str, + case: CaseSpec, +) -> dict[str, Any]: + case_dir = output_root / api / case.case_id + command = build_benchmark_command(args, runtime_dir, case_dir, api, case) + print(f"running {api} {case.case_id}...", flush=True) + try: + completed = subprocess.run( + command, + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + returncode = completed.returncode + stdout = completed.stdout + stderr = completed.stderr + except OSError as exc: + returncode = 127 + stdout = "" + stderr = f"{type(exc).__name__}: {exc}\n" + case_dir.mkdir(parents=True, exist_ok=True) + (case_dir / "acceptance_benchmark_process.out.txt").write_text( + stdout, encoding="utf-8", newline="\n" + ) + (case_dir / "acceptance_benchmark_process.err.txt").write_text( + stderr, encoding="utf-8", newline="\n" + ) + try: + result = inspect_benchmark_output( + case_dir, + output_root, + runtime_records, + api, + case, + returncode, + command, + Path(args.basepath).resolve(), + ) + except Exception as exc: # Keep a report even for an unexpected parser failure. + result = { + "id": case.case_id, + "description": case.description, + "cvars": case.cvars, + "command": command, + "commandDisplay": command_display(command), + "benchmarkExitCode": returncode, + "status": "fail", + "failures": [f"unexpected evidence inspection failure: {type(exc).__name__}: {exc}"], + "diagnostics": None, + "screenshot": None, + "captureScript": None, + "artifacts": [], + } + print(f" {result['status']}", flush=True) + return result + + +def compare_images( + api: str, + comparison_id: str, + baseline: dict[str, Any], + candidate: dict[str, Any], + output_root: Path, + expectation: str, + min_rms: float, + min_channels: int, +) -> dict[str, Any]: + failures: list[str] = [] + baseline_image = baseline.get("screenshot") + candidate_image = candidate.get("screenshot") + comparison: dict[str, Any] | None = None + if not isinstance(baseline_image, dict) or not isinstance(candidate_image, dict): + failures.append("one or both engine screenshots are unavailable") + else: + try: + first = (output_root / baseline_image["path"]).resolve() + second = (output_root / candidate_image["path"]).resolve() + _relative_to(first, output_root.resolve(), "comparison screenshot") + _relative_to(second, output_root.resolve(), "comparison screenshot") + comparison = benchmark.compare_tga(second, first) + except (KeyError, OSError, ValueError) as exc: + failures.append(f"TGA comparison failed: {exc}") + if comparison is not None: + if comparison.get("status") != "compared": + failures.append( + f"image comparison status={comparison.get('status')!r}; expected 'compared'" + ) + elif expectation == "exact": + for field, expected in ( + ("rms", 0.0), + ("maxDelta", 0), + ("differingChannels", 0), + ): + if comparison.get(field) != expected: + failures.append( + f"exact parity {field}={comparison.get(field)!r}; expected {expected!r}" + ) + elif expectation == "different": + rms = comparison.get("rms") + channels = comparison.get("differingChannels") + if not isinstance(rms, (int, float)) or rms < min_rms: + failures.append( + f"normal-vs-skip RMS={rms!r}; required at least {min_rms}" + ) + if not isinstance(channels, int) or channels < min_channels: + failures.append( + "normal-vs-skip differingChannels=" + f"{channels!r}; required at least {min_channels}" + ) + else: + failures.append(f"unknown comparison expectation: {expectation}") + return { + "id": comparison_id, + "api": api, + "baselineCase": baseline["id"], + "candidateCase": candidate["id"], + "expectation": expectation, + "minimums": ( + {"rms": min_rms, "differingChannels": min_channels} + if expectation == "different" + else None + ), + "result": comparison, + "status": "pass" if not failures else "fail", + "failures": failures, + } + + +def build_comparisons( + api: str, + cases: list[dict[str, Any]], + output_root: Path, + min_rms: float, + min_channels: int, +) -> list[dict[str, Any]]: + by_id = {case["id"]: case for case in cases} + specs = ( + ( + "classic-normal-vs-subview-only-normal", + "classic-normal", + "subview-only-normal", + "exact", + ), + ( + "classic-normal-vs-cinematic-only-normal", + "classic-normal", + "cinematic-only-normal", + "exact", + ), + ("classic-normal-vs-both-normal", "classic-normal", "both-normal", "exact"), + ("classic-skip-vs-both-skip", "classic-skip", "both-skip", "exact"), + ( + "classic-normal-vs-classic-skip", + "classic-normal", + "classic-skip", + "different", + ), + ("both-normal-vs-both-skip", "both-normal", "both-skip", "different"), + ) + comparisons = [ + compare_images( + api, + comparison_id, + by_id[baseline_id], + by_id[candidate_id], + output_root, + expectation, + min_rms, + min_channels, + ) + for comparison_id, baseline_id, candidate_id, expectation in specs + ] + return comparisons + + +def markdown_escape(value: Any) -> str: + return str(value).replace("|", "\\|").replace("\n", " ") + + +def diagnostic_summary(case: dict[str, Any]) -> tuple[str, str]: + diagnostics = case.get("diagnostics") + if not isinstance(diagnostics, dict): + return "unavailable", "unavailable" + cinematic = diagnostics["cinematicPost"] + subview = diagnostics["subview"] + cinematic_text = ( + f"req/prep/valid={cinematic['requested']}/{cinematic['prepared']}/{cinematic['valid']} " + f"post={cinematic['post']} nested={cinematic['nested_views']}/" + f"{cinematic['nested_transactions']} cin={cinematic['nested_cinematic']} " + f"GL={cinematic['gl_owned']}/{cinematic['gl_fallback']}/" + f"{cinematic['gl_mismatch']}/{cinematic['gl_duplicate']} " + f"VK={cinematic['vk_owned']}/{cinematic['vk_fallback']}/" + f"{cinematic['vk_mismatch']}/{cinematic['vk_duplicate']}" + ) + backends = diagnostics["subviewBackends"] + subview_text = ( + f"req/prep/valid={subview['requested']}/{subview['prepared']}/{subview['valid']} " + f"views/captures={subview['subviews']}/{subview['captures']} " + f"GL={backends['gl']['owned']}/{backends['gl']['fallback']} " + f"VK={backends['vulkan']['owned']}/{backends['vulkan']['fallback']}" + ) + return cinematic_text, subview_text + + +def render_markdown(report: dict[str, Any], json_sha256: str) -> str: + lines = [ + "# Renderer Milestone D Acceptance", + "", + f"- Status: `{report['status']}`", + f"- Generated: `{report['generated']}`", + f"- Render APIs: `{', '.join(report['renderApis'])}`", + f"- Runtime: `{report['runtime']['path']}`", + f"- Runtime file count: `{report['runtime']['fileCount']}`", + f"- Runtime manifest SHA-256: `{report['runtime']['manifestSha256']}`", + f"- Runtime remained immutable: `{str(not report['runtimeVerificationFailures']).lower()}`", + f"- Fixture manifest: `{report['fixture']['manifestPath']}`", + f"- Fixture manifest SHA-256: `{report['fixture']['manifestSha256']}`", + f"- Fixture evidence remained immutable: `{str(not report['fixtureVerificationFailures']).lower()}`", + f"- JSON evidence SHA-256: `{json_sha256}`", + "- Capture contract: `pacing-only, GPU timers off, bordered windowed 1280x720, engine TGA screenshot`", + ( + "- Normal-vs-skip minimum: " + f"`RMS >= {report['differenceMinimums']['rms']}, " + f"differing channels >= {report['differenceMinimums']['differingChannels']}`" + ), + "", + ] + if report["failures"]: + lines.extend(("## Failures", "")) + lines.extend(f"- {failure}" for failure in report["failures"]) + lines.append("") + + for api_result in report["apis"]: + lines.extend( + ( + f"## {api_result['api'].upper()}", + "", + f"API status: `{api_result['status']}`", + "", + "| Case | Status | Cinematic/post | Subview | Screenshot SHA-256 |", + "|---|---|---|---|---|", + ) + ) + for case in api_result["cases"]: + cinematic, subview = diagnostic_summary(case) + screenshot = case.get("screenshot") or {} + lines.append( + "| " + + " | ".join( + markdown_escape(value) + for value in ( + case["id"], + case["status"], + cinematic, + subview, + screenshot.get("sha256", "missing"), + ) + ) + + " |" + ) + lines.extend(("", "### Comparisons", "")) + lines.extend( + ( + "| Comparison | Expectation | Status | RMS | Max delta | Differing channels |", + "|---|---|---|---:|---:|---:|", + ) + ) + for comparison in api_result["comparisons"]: + values = comparison.get("result") or {} + lines.append( + "| " + + " | ".join( + markdown_escape(value) + for value in ( + comparison["id"], + comparison["expectation"], + comparison["status"], + values.get("rms", "n/a"), + values.get("maxDelta", "n/a"), + values.get("differingChannels", "n/a"), + ) + ) + + " |" + ) + lines.extend(("", "### Commands", "")) + for case in api_result["cases"]: + lines.extend( + ( + f"#### {case['id']}", + "", + "```text", + case["commandDisplay"], + "```", + "", + ) + ) + if case["failures"]: + lines.extend(f"- Failure: {failure}" for failure in case["failures"]) + lines.append("") + lines.extend(("### Artifact manifest", "")) + lines.extend(("| Kind | Relative path | Bytes | SHA-256 |", "|---|---|---:|---|")) + for case in api_result["cases"]: + for artifact in case["artifacts"]: + lines.append( + "| " + + " | ".join( + markdown_escape(value) + for value in ( + artifact["kind"], + artifact["path"], + artifact["size"], + artifact["sha256"], + ) + ) + + " |" + ) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def dry_run_plan( + args: argparse.Namespace, + runtime_dir: Path, + output_dir: Path, + fixture: dict[str, Any], +) -> dict[str, Any]: + apis = selected_apis(args.render_api) + return { + "schemaVersion": SCHEMA_VERSION, + "status": "plan", + "dryRun": True, + "runtime": path_hint(runtime_dir), + "fixture": { + "manifestPath": fixture["manifestPath"], + "manifestSha256": fixture["manifestSha256"], + "map": fixture["map"], + }, + "output": path_hint(output_dir), + "renderApis": list(apis), + "casesPerApi": len(CASES), + "commands": [ + { + "api": api, + "case": case.case_id, + "command": build_benchmark_command( + args, runtime_dir, output_dir / api / case.case_id, api, case + ), + } + for api in apis + for case in CASES + ], + } + + +def run_acceptance(args: argparse.Namespace) -> int: + basepath = Path(args.basepath).resolve() if args.basepath else None + if basepath is None or not basepath.is_dir(): + raise FileNotFoundError("--basepath must identify an installed Quake 4 root") + args.basepath = str(basepath) + runtime_dir = validate_runtime_dir(args.runtime_dir) + fixture_info = validate_fixture_manifest( + args.fixture_manifest, runtime_dir, basepath + ) + output_dir = select_output_dir(args.output_dir) + if args.dry_run: + print( + json.dumps( + dry_run_plan(args, runtime_dir, output_dir, fixture_info), + indent=2, + allow_nan=False, + ) + ) + return 0 + + prepare_output_dir(output_dir) + runtime_before = benchmark.collect_runtime_files(runtime_dir) + runtime_info = inventory_summary(runtime_before) + runtime_info["path"] = path_hint(runtime_dir) + api_results: list[dict[str, Any]] = [] + top_failures: list[str] = [] + + for api in selected_apis(args.render_api): + cases = [ + run_case(args, runtime_dir, output_dir, runtime_before, api, case) + for case in CASES + ] + comparisons = build_comparisons( + api, + cases, + output_dir, + args.difference_min_rms, + args.difference_min_channels, + ) + api_failures = [ + f"{api}/{case['id']}: {failure}" + for case in cases + for failure in case["failures"] + ] + [ + f"{api}/{comparison['id']}: {failure}" + for comparison in comparisons + for failure in comparison["failures"] + ] + api_results.append( + { + "api": api, + "status": "pass" if not api_failures else "fail", + "failures": api_failures, + "cases": cases, + "comparisons": comparisons, + } + ) + top_failures.extend(api_failures) + + try: + runtime_after = benchmark.collect_runtime_files(runtime_dir) + runtime_verification_failures = benchmark.compare_file_records( + runtime_before, runtime_after, "runtime file" + ) + except Exception as exc: + runtime_verification_failures = [ + f"runtime post-run inventory failed: {type(exc).__name__}: {exc}" + ] + top_failures.extend( + f"runtime immutability: {failure}" + for failure in runtime_verification_failures + ) + try: + fixture_after = validate_fixture_manifest( + args.fixture_manifest, runtime_dir, basepath + ) + fixture_verification_failures = ( + [] + if fixture_after == fixture_info + else ["fixture manifest, evidence, or bound runtime records changed during acceptance"] + ) + except Exception as exc: + fixture_verification_failures = [ + f"fixture post-run verification failed: {type(exc).__name__}: {exc}" + ] + top_failures.extend( + f"fixture immutability: {failure}" + for failure in fixture_verification_failures + ) + + report: dict[str, Any] = { + "schemaVersion": SCHEMA_VERSION, + "status": "pass" if not top_failures else "fail", + "dryRun": False, + "generated": dt.datetime.now().astimezone().isoformat(timespec="seconds"), + "profile": "milestone-d-nested-dynamic", + "renderApis": list(selected_apis(args.render_api)), + "casesPerApi": len(CASES), + "captureContract": { + "pacingOnly": True, + "gpuTimers": False, + "display": "windowed", + "width": 1280, + "height": 720, + "screenshotSource": "engine screenshot command", + "screenshotFormat": "TGA", + "settleFrames": args.settle_frames, + "sampleFrames": args.sample_frames, + }, + "differenceMinimums": { + "rms": args.difference_min_rms, + "differingChannels": args.difference_min_channels, + }, + "runtime": runtime_info, + "runtimeVerificationFailures": runtime_verification_failures, + "fixture": fixture_info, + "fixtureVerificationFailures": fixture_verification_failures, + "outputRoot": path_hint(output_dir), + "reports": { + "json": REPORT_JSON_NAME, + "markdown": REPORT_MD_NAME, + }, + "failures": top_failures, + "apis": api_results, + } + json_path = output_dir / REPORT_JSON_NAME + md_path = output_dir / REPORT_MD_NAME + json_path.write_text( + json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n", + encoding="utf-8", + newline="\n", + ) + json_sha = sha256_file(json_path) + md_path.write_text( + render_markdown(report, json_sha), encoding="utf-8", newline="\n" + ) + print(f"wrote {md_path}") + print(f"wrote {json_path}") + print(f"renderer Milestone D acceptance: {report['status']}") + return 0 if report["status"] == "pass" else 1 + + +def main(argv: list[str]) -> int: + try: + args = parse_args(argv) + return run_acceptance(args) + except (EvidenceError, FileExistsError, FileNotFoundError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/tests/renderer_milestone_d_fixture.py b/tools/tests/renderer_milestone_d_fixture.py new file mode 100644 index 00000000..3209ba0a --- /dev/null +++ b/tools/tests/renderer_milestone_d_fixture.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +"""Build the temporary stock-asset fixture used to qualify renderer Milestone D. + +The fixture is deliberately not shipping content. It retrieves the stock +``maps/tools/mv2.map`` shell and a stock ROQ from a user-owned extracted Quake 4 +asset tree, adds original test-only mirror and authored-post geometry, compiles +the result with openQ4's in-tree dmap, and stages an isolated runtime below +``.tmp/stock-runtime`` for the gameplay benchmark harness. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import shutil +import stat +import subprocess +import sys +import time +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[2] +SOURCE_MAP_REL = Path("maps/tools/mv2.map") +SOURCE_VIDEO_REL = Path("video/idlogo.roq") +SOURCE_SHA256 = { + SOURCE_MAP_REL: "412fc796e569660c92dca5d55359e5c4eb3d572754bd1cdff8ce994b0b802fe2", + SOURCE_VIDEO_REL: "375c9641c96359c2e0d49cdd3905cc2cdc66a7b067c9a8910d80bf36fb06c8f4", +} +STOCK_GAME_DIRECTORIES = ("q4base", "baseoq4") +FIXTURE_MAP_REL = Path("maps/tools/milestone_d_nested_dynamic.map") +FIXTURE_MATERIAL_REL = Path("materials/milestone_d_validation.mtr") +FIXTURE_VIDEO_REL = Path("video/milestone_d_idlogo.roq") + +FIXTURE_MATERIAL = """// Temporary Milestone D validation material; never shipped. +milestoneD/nestedCinematicPost +{ + qer_editorimage textures/editor/video.tga + translucent + twoSided + noShadows + noSelfShadow + sort postProcess + { + blend add + videoMap loop video/milestone_d_idlogo.roq + rgb 1 + } + { + blend blend + map _currentRender + alpha 0.35 + } +} +""" + +# The player capture looks north at the mirror. Its reflected camera looks +# south at the video panel behind the player, so the post-process/video tail is +# present only in the mirror child view. All other faces are stock caulk. +FIXTURE_ENTITIES = r""" +// entity 7: capture-backed mirror facing the validation camera +{ +"classname" "func_static" +"name" "milestone_d_capture_mirror" +"model" "milestone_d_capture_mirror" +// primitive 0 +{ + brushDef3 + { + ( -1 0 0 -160 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 1 0 0 -160 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 -1 0 0 ) ( ( 0.00390625 0 0.5 ) ( 0 -0.005208333333 0.5 ) ) "shaderDemos/transparentMirror" + ( 0 1 0 -8 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 0 -1 16 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 0 1 -224 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + } +} +} +// entity 8: post-process video panel visible only through the mirror child view +{ +"classname" "func_static" +"name" "milestone_d_nested_post_panel" +"model" "milestone_d_nested_post_panel" +// primitive 0 +{ + brushDef3 + { + ( -1 0 0 -192 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 1 0 0 -192 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 -1 0 -648 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 1 0 640 ) ( ( 0.002604166667 0 0.5 ) ( 0 -0.005681818182 0.5 ) ) "milestoneD/nestedCinematicPost" + ( 0 0 -1 32 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + ( 0 0 1 -208 ) ( ( 0.015625 0 0 ) ( 0 0.015625 0 ) ) "textures/common/caulk" + } +} +} +""" + + +def default_asset_root() -> str: + return r"E:\_SOURCE\_ASSETS\Q4" if os.name == "nt" else "" + + +def default_basepath() -> str: + if os.name == "nt": + return r"C:\Program Files (x86)\Steam\steamapps\common\Quake 4" + return "" + + +def host_arch() -> str: + machine = platform.machine().lower() + if machine in ("amd64", "x86_64"): + return "x64" + if machine in ("arm64", "aarch64"): + return "arm64" + if machine in ("x86", "i386", "i686"): + return "x86" + return machine + + +def filesystem_path(path: Path) -> Path: + """Use extended Windows paths so effective loose assets past MAX_PATH hash.""" + if os.name != "nt": + return path + absolute = os.path.abspath(path) + if absolute.startswith("\\\\?\\"): + return Path(absolute) + if absolute.startswith("\\\\"): + return Path("\\\\?\\UNC\\" + absolute[2:]) + return Path("\\\\?\\" + absolute) + + +def is_link_or_junction(path: Path) -> bool: + native = filesystem_path(path) + is_junction = getattr(native, "is_junction", None) + if native.is_symlink() or bool(is_junction and is_junction()): + return True + if not native.exists(): + return False + try: + attributes = getattr(native.lstat(), "st_file_attributes", 0) + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + except OSError: + return False + + +def require_new_directory(path: Path, parent: Path, description: str) -> Path: + root_absolute = ROOT.absolute() + parent_absolute = parent.absolute() + candidate = path.absolute() if path.is_absolute() else (root_absolute / path).absolute() + try: + relative = candidate.relative_to(parent_absolute) + except ValueError as exc: + raise ValueError( + f"{description} must stay below {parent_absolute}: {candidate}" + ) from exc + if len(relative.parts) != 1: + raise ValueError( + f"{description} must be a named direct child below {parent_absolute}: " + f"{candidate}" + ) + try: + root_relative = candidate.relative_to(root_absolute) + except ValueError as exc: + raise ValueError(f"{description} escaped source root {root_absolute}") from exc + current = root_absolute + if current.exists() and is_link_or_junction(current): + raise ValueError(f"{description} may not traverse a link or junction: {current}") + for part in root_relative.parts: + current /= part + if current.exists() and is_link_or_junction(current): + raise ValueError(f"{description} may not traverse a link or junction: {current}") + if candidate.exists(): + raise FileExistsError(f"{description} must be new: {candidate}") + resolved_parent = parent_absolute.resolve() + resolved = candidate.resolve() + if resolved.parent != resolved_parent: + raise ValueError( + f"resolved {description} must remain below {resolved_parent}: {resolved}" + ) + return resolved + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with filesystem_path(path).open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def file_record(path: Path, root: Path) -> dict[str, Any]: + return { + "path": path.relative_to(root).as_posix(), + "size": filesystem_path(path).stat().st_size, + "sha256": sha256(path), + } + + +def inventory_summary(records: list[dict[str, Any]]) -> dict[str, Any]: + encoded = json.dumps( + records, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + allow_nan=False, + ).encode("utf-8") + return { + "fileCount": len(records), + "totalBytes": sum(int(record["size"]) for record in records), + "manifestSha256": hashlib.sha256(encoded).hexdigest(), + "files": records, + } + + +def stock_dependency_inventory(basepath: Path) -> dict[str, Any]: + """Hash every effective stock PK4/loose file below q4base/baseoq4.""" + basepath = basepath.resolve() + records: list[dict[str, Any]] = [] + + def raise_walk_error(error: OSError) -> None: + raise error + + for game_directory_name in STOCK_GAME_DIRECTORIES: + game_directory = basepath / game_directory_name + if not game_directory.exists(): + continue + if not game_directory.is_dir() or is_link_or_junction(game_directory): + raise ValueError( + f"stock dependency directory must be an ordinary directory: {game_directory}" + ) + for directory, directory_names, file_names in os.walk( + game_directory, followlinks=False, onerror=raise_walk_error + ): + directory_path = Path(directory) + directory_names.sort(key=str.casefold) + file_names.sort(key=str.casefold) + for name in tuple(directory_names): + child = directory_path / name + if is_link_or_junction(child): + raise ValueError( + f"stock dependency tree contains a link or junction: {child}" + ) + for name in file_names: + path = directory_path / name + if is_link_or_junction(path): + raise ValueError( + f"stock dependency tree contains a linked file: {path}" + ) + if not filesystem_path(path).is_file(): + raise ValueError(f"stock dependency entry is not a file: {path}") + records.append(file_record(path, basepath)) + records.sort(key=lambda record: record["path"].casefold()) + if not records: + raise FileNotFoundError( + f"stock base path has no effective q4base/baseoq4 files: {basepath}" + ) + return inventory_summary(records) + + +def find_dmap_executable(runtime_dir: Path) -> Path: + suffix = ".exe" if os.name == "nt" else "" + architecture = host_arch() + candidates = ( + runtime_dir / f"openQ4-ded_{architecture}{suffix}", + runtime_dir / f"openQ4-client_{architecture}{suffix}", + ) + for candidate in candidates: + if candidate.is_file(): + return candidate + raise FileNotFoundError( + "temporary runtime has no host-matching openQ4 dedicated/client executable" + ) + + +def validate_runtime_components(runtime_dir: Path) -> list[dict[str, Any]]: + architecture = host_arch() + executable_suffix = ".exe" if os.name == "nt" else "" + module_suffix = ( + ".dll" if os.name == "nt" else (".dylib" if sys.platform == "darwin" else ".so") + ) + required = [ + runtime_dir / f"openQ4-client_{architecture}{executable_suffix}", + runtime_dir / "baseoq4" / f"game-sp_{architecture}{module_suffix}", + runtime_dir / f"renderer-vk_{architecture}{module_suffix}", + ] + # macOS keeps OpenGL statically linked and intentionally does not build the + # renderer-gl module; Windows and Linux acceptance require the module. + if sys.platform != "darwin": + required.append(runtime_dir / f"renderer-gl_{architecture}{module_suffix}") + else: + # Meson intentionally does not build or install MoltenVK. The fixture + # stages the project's pinned, verified provider beside the loose + # client so both SDL and the renderer module bind the same image. + required.append(runtime_dir / "libMoltenVK.dylib") + missing = [path for path in required if not path.is_file()] + if missing: + raise FileNotFoundError( + "temporary runtime is incomplete for GL/Vulkan SP acceptance: " + + ", ".join(str(path) for path in missing) + ) + return [file_record(path, runtime_dir) for path in required] + + +def validation_map_payload(source_map: Path) -> str: + payload = source_map.read_text(encoding="utf-8") + for token in ( + "Version 3", + '"classname" "worldspawn"', + '"classname" "info_player_start"', + '"classname" "func_cameraview"', + ): + if token not in payload: + raise ValueError(f"stock fixture source is missing {token!r}: {source_map}") + return payload.rstrip() + "\n" + FIXTURE_ENTITIES.lstrip() + + +def timeout_output(value: str | bytes | None) -> str: + if value is None: + return "" + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + return value + + +def run_checked( + command: list[str], cwd: Path, log_path: Path, timeout_seconds: int +) -> None: + log_path.parent.mkdir(parents=True, exist_ok=True) + try: + completed = subprocess.run( + command, + cwd=cwd, + capture_output=True, + text=True, + errors="replace", + check=False, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired as exc: + log_path.write_text( + "COMMAND\n" + + subprocess.list2cmdline(command) + + f"\n\nTIMEOUT\nExceeded {timeout_seconds} seconds; direct child terminated." + + "\n\nSTDOUT\n" + + timeout_output(exc.stdout) + + "\nSTDERR\n" + + timeout_output(exc.stderr), + encoding="utf-8", + ) + raise TimeoutError( + f"command exceeded {timeout_seconds} seconds; see {log_path}" + ) from exc + log_path.write_text( + "COMMAND\n" + subprocess.list2cmdline(command) + "\n\nSTDOUT\n" + + completed.stdout + "\nSTDERR\n" + completed.stderr, + encoding="utf-8", + ) + if completed.returncode != 0: + raise RuntimeError( + f"command failed with exit code {completed.returncode}; see {log_path}" + ) + + +def stage_temporary_runtime( + runtime_dir: Path, output_dir: Path, timeout_seconds: int +) -> None: + run_checked( + [ + sys.executable, + "-B", + str(ROOT / "tools/build/stage_fast_install.py"), + "--source-root", + str(ROOT), + "--build-dir", + str(ROOT / "builddir"), + "--install-dir", + str(runtime_dir), + "--temporary-runtime", + ], + ROOT, + output_dir / "stage-runtime.log", + timeout_seconds, + ) + if sys.platform == "darwin": + run_checked( + [ + "/bin/bash", + str(ROOT / "tools/build/prepare_macos_moltenvk.sh"), + "--output-dir", + str(runtime_dir), + ], + ROOT, + output_dir / "stage-moltenvk.log", + timeout_seconds, + ) + + +def compile_fixture( + runtime_dir: Path, + compile_root: Path, + basepath: Path | None, + output_dir: Path, + timeout_seconds: int, +) -> None: + executable = find_dmap_executable(runtime_dir) + command = [ + str(executable), + "+set", "r_fullscreen", "0", + "+set", "r_borderless", "0", + "+set", "in_mouse", "0", + "+set", "developer", "1", + "+set", "logFile", "2", + "+set", "logFileName", "logs/milestone_d_dmap.log", + "+set", "fs_savepath", str(compile_root), + "+set", "fs_devpath", str(compile_root), + "+set", "fs_game", "baseoq4", + ] + if basepath is not None: + command += ["+set", "fs_basepath", str(basepath)] + command += ["+dmap", FIXTURE_MAP_REL.with_suffix("").as_posix(), "+quit"] + # Source lookup and collision output use the isolated compile/save tree. + # The engine deliberately locks fs_cdpath to its executable content root, + # so dmap's .proc output first lands in this newly created temporary runtime + # (never .install). Copy it back to the compile tree before the allowlisted + # fixture payload is verified and staged as one unit. + run_checked( + command, compile_root, output_dir / "dmap-process.log", timeout_seconds + ) + + runtime_proc = runtime_dir / "baseoq4" / FIXTURE_MAP_REL.with_suffix(".proc") + compile_proc = compile_root / "baseoq4" / FIXTURE_MAP_REL.with_suffix(".proc") + if not runtime_proc.is_file() or runtime_proc.stat().st_size == 0: + raise FileNotFoundError(f"dmap did not produce {runtime_proc}") + compile_proc.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(runtime_proc, compile_proc) + + engine_log = compile_root / "baseoq4/logs/milestone_d_dmap.log" + if not engine_log.is_file(): + raise FileNotFoundError(f"dmap engine log was not written: {engine_log}") + log_text = engine_log.read_text(encoding="utf-8", errors="replace") + for marker in ("ERROR:", "Fatal Error", "leaked"): + if marker.casefold() in log_text.casefold(): + raise RuntimeError(f"dmap log contains {marker!r}: {engine_log}") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--asset-root", + default=default_asset_root(), + help="Extracted stock Quake 4 asset root containing maps/ and video/.", + ) + parser.add_argument( + "--basepath", + default=default_basepath(), + help="Installed Quake 4 root used to resolve stock PK4 dependencies.", + ) + parser.add_argument( + "--output-dir", + default="", + help="Fresh evidence directory below .tmp/renderer-milestone-d/.", + ) + parser.add_argument( + "--runtime-dir", + default="", + help="Fresh temporary runtime below .tmp/stock-runtime/.", + ) + parser.add_argument( + "--stage-timeout", + type=int, + default=600, + help="Maximum seconds allowed for temporary runtime staging.", + ) + parser.add_argument( + "--dmap-timeout", + type=int, + default=180, + help="Maximum seconds allowed for the windowed dmap invocation.", + ) + args = parser.parse_args(argv) + if args.stage_timeout < 1: + parser.error("--stage-timeout must be positive") + if args.dmap_timeout < 1: + parser.error("--dmap-timeout must be positive") + return args + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + stamp = time.strftime("%Y%m%d-%H%M%S") + output_parent = ROOT / ".tmp/renderer-milestone-d" + runtime_parent = ROOT / ".tmp/stock-runtime" + output_dir = require_new_directory( + Path(args.output_dir) if args.output_dir else output_parent / stamp, + output_parent, + "fixture output directory", + ) + runtime_dir = require_new_directory( + Path(args.runtime_dir) if args.runtime_dir else runtime_parent / f"milestone-d-{stamp}", + runtime_parent, + "temporary runtime directory", + ) + asset_root = Path(args.asset_root).resolve() if args.asset_root else None + if asset_root is None or not asset_root.is_dir(): + raise FileNotFoundError("--asset-root must identify an extracted Quake 4 asset tree") + basepath = Path(args.basepath).resolve() if args.basepath else None + if basepath is None or not basepath.is_dir(): + raise FileNotFoundError("--basepath must identify an installed Quake 4 root") + stock_dependencies_before = stock_dependency_inventory(basepath) + + source_map = asset_root / SOURCE_MAP_REL + source_video = asset_root / SOURCE_VIDEO_REL + for source in (source_map, source_video): + if not source.is_file(): + raise FileNotFoundError(f"required stock fixture source is missing: {source}") + for relative, expected_hash in SOURCE_SHA256.items(): + source = asset_root / relative + actual_hash = sha256(source) + if actual_hash != expected_hash: + raise ValueError( + f"stock fixture source hash mismatch for {source}: " + f"expected {expected_hash}, got {actual_hash}" + ) + + output_dir.mkdir(parents=True) + retrieved_root = output_dir / "retrieved/baseoq4" + compile_root = output_dir / "compile" + for source, relative in ( + (source_map, SOURCE_MAP_REL), + (source_video, SOURCE_VIDEO_REL), + ): + target = retrieved_root / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + copied_hash = sha256(target) + if copied_hash != SOURCE_SHA256[relative]: + raise ValueError( + f"retrieved stock snapshot hash mismatch for {target}: " + f"expected {SOURCE_SHA256[relative]}, got {copied_hash}" + ) + + compile_game = compile_root / "baseoq4" + map_path = compile_game / FIXTURE_MAP_REL + material_path = compile_game / FIXTURE_MATERIAL_REL + video_path = compile_game / FIXTURE_VIDEO_REL + map_path.parent.mkdir(parents=True, exist_ok=True) + material_path.parent.mkdir(parents=True, exist_ok=True) + video_path.parent.mkdir(parents=True, exist_ok=True) + retrieved_map = retrieved_root / SOURCE_MAP_REL + retrieved_video = retrieved_root / SOURCE_VIDEO_REL + map_path.write_text(validation_map_payload(retrieved_map), encoding="utf-8") + material_path.write_text(FIXTURE_MATERIAL, encoding="utf-8") + shutil.copy2(retrieved_video, video_path) + + stage_temporary_runtime(runtime_dir, output_dir, args.stage_timeout) + runtime_components = validate_runtime_components(runtime_dir) + compile_fixture( + runtime_dir, compile_root, basepath, output_dir, args.dmap_timeout + ) + + generated_files = [map_path, material_path, video_path] + for suffix in (".proc", ".cm"): + generated = map_path.with_suffix(suffix) + if not generated.is_file() or generated.stat().st_size == 0: + raise FileNotFoundError(f"dmap did not produce {generated}") + generated_files.append(generated) + + proc_text = map_path.with_suffix(".proc").read_text( + encoding="utf-8", errors="replace" + ) + for material in ( + "shaderDemos/transparentMirror", + "milestoneD/nestedCinematicPost", + ): + if material.casefold() not in proc_text.casefold(): + raise ValueError( + f"compiled fixture does not retain material {material!r}: " + f"{map_path.with_suffix('.proc')}" + ) + + runtime_game = runtime_dir / "baseoq4" + for source in generated_files: + relative = source.relative_to(compile_game) + target = runtime_game / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + + stock_dependencies_after = stock_dependency_inventory(basepath) + if stock_dependencies_after != stock_dependencies_before: + raise RuntimeError("stock basepath dependencies changed while building the fixture") + + manifest = { + "schemaVersion": 1, + "purpose": "temporary Milestone D nested special-view cinematic/post qualification", + "shippingContent": False, + "stockAssetRoot": str(asset_root), + "basepath": str(basepath), + "stockDependencies": stock_dependencies_before, + "runtimeDir": str(runtime_dir), + "runtimeComponents": runtime_components, + "map": FIXTURE_MAP_REL.with_suffix("").as_posix(), + "cameraCommand": "setviewpos 0 -384 96 0 90 0", + "retrieved": [ + file_record(retrieved_root / SOURCE_MAP_REL, retrieved_root), + file_record(retrieved_root / SOURCE_VIDEO_REL, retrieved_root), + ], + "generated": [file_record(path, compile_game) for path in generated_files], + } + manifest_path = output_dir / "fixture_manifest.json" + manifest_path.write_text( + json.dumps(manifest, indent=2, allow_nan=False) + "\n", encoding="utf-8" + ) + print(f"fixture runtime: {runtime_dir}") + print(f"fixture map: {manifest['map']}") + print(f"fixture manifest: {manifest_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/tests/renderer_mp_flat_items.py b/tools/tests/renderer_mp_flat_items.py index 3d90de0a..1a26e477 100644 --- a/tools/tests/renderer_mp_flat_items.py +++ b/tools/tests/renderer_mp_flat_items.py @@ -570,7 +570,7 @@ def test_render_entity_render_demo_and_module_abi_are_versioned(): require(gate, "ent.flatDiffuseFlags", "version 10 demos must restore flat flags") api = read(RENDERER / "RenderModuleAPI.h") - require(api, "#define RENDER_API_VERSION\t\t\t7", "the extended renderEntity ABI must be version 7") + require(api, "#define RENDER_API_VERSION\t\t\t11", "the current renderer ABI must be version 11") loader = read(RENDERER / "RendererModule.cpp") require( diff --git a/tools/tests/renderer_pbr_materials.py b/tools/tests/renderer_pbr_materials.py new file mode 100644 index 00000000..e2ecf1f9 --- /dev/null +++ b/tools/tests/renderer_pbr_materials.py @@ -0,0 +1,1023 @@ +#!/usr/bin/env python3 +"""Compatibility contracts for opt-in PBR material metadata and resources.""" + +from __future__ import annotations + +import contextlib +import io +import os +import re +import sys +from pathlib import Path + +sys.dont_write_bytecode = True +import renderer_gameplay_benchmark as gameplay + + +ROOT = Path(__file__).resolve().parents[2] +GAME_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", ROOT.parent / "openQ4-game") +).resolve() + + +def read(path: Path) -> str: + if not path.is_file(): + raise AssertionError(f"required source is missing: {path}") + return path.read_text(encoding="utf-8") + + +def require(source: str, token: str, context: str) -> None: + if token not in source: + raise AssertionError(f"missing {token!r} in {context}") + + +def reject(source: str, token: str, context: str) -> None: + if token in source: + raise AssertionError(f"unexpected {token!r} in {context}") + + +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"missing function {signature!r}") + opening = source.find("{", start) + if opening < 0: + raise AssertionError(f"missing body for {signature!r}") + depth = 0 + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[opening + 1 : index] + raise AssertionError(f"unterminated body for {signature!r}") + + +def section(source: str, start_marker: str, end_marker: str) -> str: + start = source.find(start_marker) + end = source.find(end_marker, start + len(start_marker)) + if start < 0 or end < 0: + raise AssertionError(f"missing source section {start_marker!r}..{end_marker!r}") + return source[start:end] + + +def test_shared_material_abi_and_opt_in_defaults() -> None: + engine_header = read(ROOT / "src/renderer/Material.h") + game_header = read(GAME_ROOT / "src/renderer/Material.h") + if engine_header != game_header: + raise AssertionError("engine and companion Material.h must remain byte-identical") + + require(engine_header, "legacyFallbackMissing", "explicit missing-fallback state") + + init = read(ROOT / "src/renderer/RenderSystem_init.cpp") + for name, default in ( + ("r_pbrMaterials", "0"), + ("r_pbrGeneratedLegacyFallback", "1"), + ("r_pbrDebug", "0"), + ("r_pbrIBL", "1"), + ("r_pbrIBLIntensity", "1"), + ("r_pbrInferFromLegacyMaterials", "0"), + ): + require(init, f'idCVar {name}( "{name}", "{default}"', "PBR cvar defaults") + require(init, "PBR materials: parser=1 modernLighting=1", "honest gfxInfo capability") + + +def test_parser_is_namespaced_and_fail_closed() -> None: + material = read(ROOT / "src/renderer/Material.cpp") + parse_material = section(material, "void idMaterial::ParseMaterial( idLexer &src )", "void idMaterial::SetGui(") + require(parse_material, '!token.Icmp( "pbr" )', "top-level PBR namespace") + require(parse_material, '!token.Icmp( "physicallyBased" )', "PBR namespace alias") + reject(parse_material, "r_pbrInferFromLegacyMaterials", "classic parser path") + reject(parse_material, "pbrInfo.metallicRegister = GetExpressionConstant", "stock register allocation") + + parse_pbr = section(material, "bool idMaterial::ParsePBRBlock(", "void idMaterial::AddPBRLegacyFallbackStages(") + for token in ( + "workflow", + "albedoMap", + "normalMap", + "normalFormat", + "ormMap", + "metallicMap", + "roughnessMap", + "aoMap", + "emissiveMap", + "legacyBumpMap", + "legacyDiffuseMap", + "legacySpecularMap", + "legacyEmissiveMap", + "autoLegacyFallback", + ): + require(parse_pbr, f'!token.Icmp( "{token}" )', "PBR block parser") + require(parse_pbr, "unknown PBR parameter", "unknown-token rejection") + require(parse_pbr, "requires normalFormat", "normal encoding contract") + require(parse_pbr, "cannot combine ormMap", "packed/separate exclusivity") + require(parse_pbr, "pbrInfo.metallicRegister = GetExpressionConstant", "opt-in PBR register defaults") + require(parse_pbr, '( value != "0" && value != "1" )', "literal auto-fallback boolean grammar") + reject(parse_pbr, "value.GetIntValue()", "fractional auto-fallback coercion") + require(material, "autoLegacyFallback 0.5", "fractional auto-fallback negative self-test") + require(material, "albedoMap reflectionRenderMap", "dynamic image negative self-test") + require(material, "albedoMap glslProgram", "shader-token image negative self-test") + require(material, "albedoMap add( _white, reflectionRenderMap )", "nested dynamic image negative self-test") + require(material, "albedoMap add( _white, glslProgram )", "nested shader-token image negative self-test") + require(material, "albedoMap add( _white, _currentRender )", "nested scene-capture negative self-test") + require(material, "albedoMap add( _white, _reflectionRender )", "nested mutable-target negative self-test") + require(material, "albedoMap add( _white, alphaTest )", "nested stage-state negative self-test") + + unsupported_tokens = function_body(material, "static bool R_IsUnsupportedPBRImageProgramToken(") + parse_image = section(material, "bool idMaterial::ParsePBRImage(", "bool idMaterial::ParsePBRBlock(") + require(unsupported_tokens, "R_IsMutableRenderImageName( token.c_str() )", "central mutable-render-image rejection") + image_manager = read(ROOT / "src/renderer/ImageManager.cpp") + for mutable_name in ( + "_reflectionRender", + "_refractionRender", + "_currentRender", + "_forwardRenderResolved", + "_postProcessAlbedo", + "_shadowMap", + "_pointShadowMap", + "_hdrScene", + "_ssao", + "_bloom", + ): + require(image_manager, f'"{mutable_name}"', "mutable render-image classifier") + require(image_manager, "image->scratchImage = true;", "dynamic scratch-image classification") + for rejected in ( + "videoMap", + "soundMap", + "mirrorRenderMap", + "remoteRenderMap", + "reflectionRenderMap", + "refractionRenderMap", + "xrayRenderMap", + "cameraCubeMap", + "cubeMap", + "program", + "glslProgram", + "blend", + "map", + "screen", + "screen2", + "glassWarp", + "texGen", + "alphaTest", + "alphaFunc", + "translate", + "centerScale", + "shear", + "rotate", + "vertexColor", + "color", + "maskColor", + "privatePolygonOffset", + ): + require(unsupported_tokens, f'!token.Icmp( "{rejected}" )', "dynamic PBR image rejection") + require(parse_image, "R_IsUnsupportedPBRImageProgramToken( token )", "direct PBR image-token rejection") + require(parse_image, "R_IsUnsupportedPBRImageProgramToken( imageProgramToken )", "nested PBR image-token rejection") + require(parse_image, "R_IsMutableRenderImage( target.image )", "loaded mutable-image rejection") + require(parse_image, "while ( imageProgram.ReadToken", "complete PBR image-program validation") + for token in ( + "target.filter", + "target.repeat", + "target.allowPicmip", + "target.noMips", + "target.highQuality", + "target.forceHighQuality", + ): + require(parse_image, token, "retained PBR image options") + + fallback = section( + material, + "void idMaterial::AddPBRLegacyFallbackStages( const textureRepeat_t trpDefault )", + "void idMaterial::ParseDeform( idLexer &src )", + ) + require(fallback, "const bool hasUsableClassicInteraction = hasBump && hasDiffuse;", "complete classic fallback authority") + require(fallback, "!hasUsableClassicInteraction", "approximation requires incomplete classic interaction") + require(fallback, "const bool allowApproximate", "approximation gate") + require(fallback, "pbrInfo.normalFormat == PBR_NORMAL_QUAKE4_AGB", "classic normal encoding gate") + for token in ( + 'buffer.Append( "nearest\\n" )', + 'buffer.Append( "noclamp\\n" )', + 'buffer.Append( "nopicmip\\n" )', + 'buffer.Append( "nomips\\n" )', + 'buffer.Append( "forceHighQuality\\n" )', + ): + require(fallback, token, "classic fallback image-option replay") + require(material, "diffuseStage->texture.image == expectedDiffuse", "runtime fallback image identity assertion") + require(material, "pbrInfo.legacyFallbackMissing = !( hasBump && hasDiffuse );", "missing fallback classification") + require(material, "has no usable classic bump+diffuse fallback", "missing fallback diagnostic") + require(material, "_pbr_selftest_missing_fallback", "missing fallback runtime self-test") + require(material, "_pbr_selftest_tangent_normal_fallback", "tangent normal fallback runtime self-test") + require(material, "_pbr_selftest_quake4_normal_fallback", "Quake 4 normal fallback runtime self-test") + require(material, "bumpStage->texture.image == globalImages->flatNormalMap", "tangent normal neutral fallback assertion") + require(material, "bumpStage->texture.image == info.normal.image", "Quake 4 normal reuse assertion") + require(material, "const bool oldGeneratedFallback = r_pbrGeneratedLegacyFallback.GetBool();", "fallback self-test cvar save") + require(material, "r_pbrGeneratedLegacyFallback.SetBool( oldGeneratedFallback );", "fallback self-test cvar restore") + + +def test_texture_usage_and_lifecycle_contracts() -> None: + image_header = read(ROOT / "src/renderer/Image.h") + order = [image_header.find(name) for name in ("TD_HIGH_QUALITY", "TD_PBR_COLOR", "TD_MATERIAL_DATA")] + if any(position < 0 for position in order) or order != sorted(order): + raise AssertionError("PBR image usages must append after historical cache identities") + + image_load = read(ROOT / "src/renderer/Image_load.cpp") + require(image_load, "case TD_PBR_COLOR:", "PBR color derivation") + require(image_load, "opts.gammaMips = true;", "gamma-correct PBR color mips") + require(image_load, "case TD_MATERIAL_DATA:", "linear material-data derivation") + gamma_downsize = function_body(image_load, "static bool R_ImageUsageUsesGammaMips(") + require(gamma_downsize, "TD_PBR_COLOR", "gamma-correct PBR color prefiltering") + declared_usage = function_body(image_load, "static void R_LoadImageProgramForDeclaredUsage(") + require(declared_usage, "declaredUsage != TD_PBR_COLOR", "PBR color image-program usage preservation") + require(declared_usage, "declaredUsage != TD_MATERIAL_DATA", "PBR data image-program usage preservation") + load_image = function_body(image_load, "void idImage::ActuallyLoadImage(") + if load_image.count("R_LoadImageProgramForDeclaredUsage(") != 5: + raise AssertionError("every image-program decode in ActuallyLoadImage must preserve declared PBR usage") + if "R_LoadImageProgram(" in load_image: + raise AssertionError("ActuallyLoadImage must not bypass declared PBR usage preservation") + + require(image_header, "textureUsage_t GetUsage() const", "PBR image usage runtime introspection") + material = read(ROOT / "src/renderer/Material.cpp") + require(material, "albedoMap heightmap( _white, 1 )", "PBR color usage mutation regression self-test") + require(material, "ormMap smoothnormals( _flat )", "PBR data usage mutation regression self-test") + require(material, "sameAlbedo == info.albedo.image", "PBR color cache identity assertion") + require(material, "sameORM == info.orm.image", "PBR data cache identity assertion") + require(material, "registerMatches( info.metallicRegister, 0.25f )", "PBR metallic register value assertion") + require(material, "registerMatches( info.emissiveColorRegisters[2], 0.3f )", "PBR emissive register value assertion") + + image_manager = read(ROOT / "src/renderer/ImageManager.cpp") + namespace_usage = function_body(image_manager, "static textureUsage_t R_ImageUsageForName(") + require(namespace_usage, "requestedUsage != TD_DEFAULT", "explicit image-usage namespace preservation") + require(namespace_usage, "return requestedUsage;", "explicit PBR image usage return") + if image_manager.count("usage = R_ImageUsageForName( _name, usage );") != 3: + raise AssertionError("all immediate, lookup, and deferred image paths must share namespace usage policy") + + for signature in ( + "void idMaterial::AddReference()", + "void idMaterial::ResolveUse()", + "void idMaterial::ReloadImages( bool force ) const", + ): + body = function_body(material, signature) + require(body, "pbrInfo.albedo", f"PBR lifecycle in {signature}") + require(body, "pbrInfo.legacyEmissive", f"complete PBR lifecycle in {signature}") + reload_images = function_body(material, "void idMaterial::ReloadImages( bool force ) const") + require(reload_images, "specularProbeInfo.cubeImage->Reload( force );", "specular-probe reload lifecycle") + add_reference = function_body(material, "void idMaterial::AddReference()") + require(add_reference, "specularProbeInfo.cubeImage->AddReference();", "specular-probe reference lifecycle") + resolve_use = function_body(material, "void idMaterial::ResolveUse()") + require(resolve_use, "specularProbeInfo.cubeImage->AddUseCount( useCount );", "specular-probe use-count lifecycle") + print_body = function_body(material, "void idMaterial::Print() const") + require(print_body, "Specular probe: cubeMap=%s", "specular-probe diagnostic lifecycle") + free_data = function_body(material, "void idMaterial::FreeData()") + require(free_data, "memset( &pbrInfo, 0, sizeof( pbrInfo ) );", "purged PBR metadata reset") + require(free_data, "pbrInfo.normalFormat = PBR_NORMAL_UNSPECIFIED;", "purged normal-format reset") + + +def test_scene_packet_and_resource_table_drive_the_guarded_visible_path() -> None: + packets_h = read(ROOT / "src/renderer/ScenePackets.h") + packets_cpp = read(ROOT / "src/renderer/ScenePackets.cpp") + table_h = read(ROOT / "src/renderer/MaterialResourceTable.h") + table_cpp = read(ROOT / "src/renderer/MaterialResourceTable.cpp") + + for token in ( + "hasPBR", + "pbrAlbedoImage", + "pbrNormalImage", + "pbrORMImage", + "pbrWorkflow", + "pbrNormalFormat", + "pbrUsesApproximateLegacyFallback", + "pbrLegacyFallbackMissing", + ): + require(packets_h, token, "scene-packet PBR record") + require(packets_cpp, token, "scene-packet PBR capture") + + ambient_filter = function_body( + packets_cpp, + "static bool R_ScenePackets_DrawSurfAmbientEligible(", + ) + require( + ambient_filter, + "!material->HasAmbient() && !material->HasPBR()", + "PBR ambient-packet surface owner admission", + ) + + for semantic in ( + "ALBEDO", + "NORMAL", + "ORM", + "METALLIC", + "ROUGHNESS", + "AO", + "EMISSIVE_PBR", + ): + require(table_h, f"MATERIAL_RESOURCE_TEXTURE_{semantic}", "PBR resource semantics") + require(table_h, "pbrResourceReady", "resource readiness split") + require(table_h, "pbrModernReady", "visible readiness split") + for token in ( + "classicRecords", + "pbrExplicitGeneratedFallbackRecords", + "pbrMissingLegacyFallbackRecords", + "pbrMissingAlbedoMapRecords", + "pbrMissingNormalMapRecords", + "pbrMissingORMMapRecords", + ): + require(table_h, token, "PBR fallback/map observability") + require(table_cpp, token, "PBR fallback/map metrics") + + texture_table = function_body( + table_cpp, + "static void R_MaterialResourceTable_BuildTextureArrayTable(", + ) + require(texture_table, "if ( record.hasPBR )", "PBR isolation from classic texture table") + require(texture_table, "binding.textureArrayLayer = -1;", "PBR texture-table layer reset") + finalize = section( + table_cpp, + "static void R_MaterialResourceTable_FinalizePBRContract(", + "static void R_MaterialResourceTable_FinalizeShadowContract(", + ) + require(finalize, "record.pbrModernReady = true;", "guarded visible-PBR readiness") + reject(finalize, "MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_TEXTURE_LAYOUT", "obsolete separate-map fallback gate") + require(finalize, "albedo, normal, packed ORM or", "separate-map direct sampler contract") + pbr_binding_ready = function_body( + table_cpp, + "static bool R_MaterialResourceTable_PBRBindingReady(", + ) + require(pbr_binding_ready, "!R_IsMutableRenderImage( binding.image )", "mutable PBR resource rejection") + + classic_gate = function_body( + table_cpp, + "bool R_MaterialResourceTable_ClassicModernPathEligible(", + ) + require(classic_gate, "return !record.hasPBR;", "classic-modern PBR exclusion") + pbr_gate = function_body( + table_cpp, + "bool R_MaterialResourceTable_PBRModernPathEligible(", + ) + require(pbr_gate, "r_rendererModernQuality.GetBool()", "master modern-quality PBR rollback") + require(pbr_gate, "record.pbrModernReady", "separate PBR visible gate") + transparent_gate = function_body( + table_cpp, + "bool R_MaterialResourceTable_PBRTransparentPathEligible(", + ) + require(transparent_gate, "R_MaterialResourceTable_PBRModernPathEligible", "transparent PBR visible gate") + require(transparent_gate, "R_MaterialResourceTable_HasPBRSourceAlphaBlendContract", "transparent PBR blend gate") + source_alpha_contract = function_body( + table_cpp, + "static bool R_MaterialResourceTable_HasPBRSourceAlphaBlendContract(", + ) + for token in ( + "MATERIAL_RESOURCE_BLEND_BLEND", + "record.blendStageCount != 1", + "record.additiveStageCount != 0", + "record.filterStageCount != 0", + "record.alphaTest", + "MATERIAL_RESOURCE_TEXTURE_ALBEDO", + "MATERIAL_RESOURCE_TEXTURE_EMISSIVE", + "R_MaterialResourceTable_BindingsSampleSameImage", + "R_MaterialResourceTable_BindingHasIdentityColor", + "TG_EXPLICIT", + "SVC_IGNORE", + ): + require(source_alpha_contract, token, "source-alpha PBR contract") + require(table_cpp, "sourceAlphaIntent", "transparent PBR fail-closed intent gate") + require(table_cpp, "MATERIAL_RESOURCE_PBR_FALLBACK_CLASSIC_FEATURE", "transparent PBR fail-closed fallback") + stage_color = function_body( + table_cpp, + "static void R_MaterialResourceTable_ValidateStageColorContract(", + ) + ambient_overlay = function_body( + table_cpp, + "static void R_MaterialResourceTable_ValidateAmbientOverlayContract(", + ) + for body, context in ((stage_color, "stage-color contract"), (ambient_overlay, "ambient-overlay contract")): + require(body, "R_MaterialResourceTable_HasPBRSourceAlphaBlendContract", f"source-alpha PBR {context}") + require(table_cpp, "!R_MaterialResourceTable_ClassicModernPathEligible( *record )", "native PBR ownership self-test") + require(table_cpp, "pbrBindingsExcludedFromClassicTable", "native PBR texture-table isolation self-test") + require(table_cpp, "stats.textureArrayTableDescriptors == 0", "PBR classic-table descriptor isolation") + require(table_cpp, "stats.classicRecords == 0", "PBR records excluded from classic record count") + require(table_cpp, "stats.classicRecords != expectedRecords", "classic packet record count assertion") + require(table_cpp, 'RendererMaterialResourceTable PBR contract self-test passed', "native PBR table pass marker") + require(table_cpp, "savedMaxClassicTextureUnits", "uninitialized-backend PBR table unit budget scope") + require(table_cpp, "rg_materialResourceTable.maxClassicTextureUnits = savedMaxClassicTextureUnits;", "PBR table unit budget restoration") + for token in ( + "separateSource", + "scalarSource", + "explicitSource", + "unsupportedSource", + "missingFallbackSource", + "missingAlbedoSource", + "mutableImageSource", + "redundantExplicitSource", + ): + require(table_cpp, token, "PBR material-table layout self-tests") + require(table_cpp, "sourceAlphaContractAccepted", "source-alpha PBR acceptance self-test") + for negative in ( + "sourceAlphaComplexBlendRejected", + "sourceAlphaMismatchedAlbedoRejected", + "sourceAlphaWrongPassRejected", + "sourceAlphaTintedRejected", + "sourceAlphaTransformedUVRejected", + ): + require(table_cpp, negative, "source-alpha PBR negative self-test") + require(table_cpp, "unsupportedRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_UNSUPPORTED_WORKFLOW", "unsupported workflow table assertion") + require(table_cpp, "missingAlbedoRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_ALBEDO", "missing albedo table assertion") + require(table_cpp, "mutableImageRecord->pbrFallbackReason == MATERIAL_RESOURCE_PBR_FALLBACK_MISSING_IMAGE", "mutable image table assertion") + require(table_cpp, "!redundantExplicitRecord->pbrUsesGeneratedLegacyFallback", "redundant explicit-map metric assertion") + require(table_cpp, "rg_materialResourceTable.records[i].material = NULL;", "self-test declaration lifetime cleanup") + + draw_plan = read(ROOT / "src/renderer/ModernGLDrawPlan.cpp") + submit_plan = read(ROOT / "src/renderer/ModernGLSubmitPlan.cpp") + executor = read(ROOT / "src/renderer/ModernGLExecutor.cpp") + for source, context in ( + (draw_plan, "draw plan"), + (submit_plan, "submit plan"), + (executor, "executor"), + ): + require(source, "R_MaterialResourceTable_PBRModernPathEligible", f"guarded PBR {context} admission") + require(draw_plan, "const bool pbrGBufferCandidate", "dedicated PBR G-buffer admission") + require(draw_plan, "const bool pbrDepthCandidate", "PBR depth-prepass admission") + require(draw_plan, "R_ModernGLDrawPlan_IsDepthPipeline( pipeline )", "PBR depth-prepass route") + forward_selection = function_body(draw_plan, "static bool R_ModernGLDrawPlan_ShouldUseForwardPlus(") + require(forward_selection, "R_MaterialResourceTable_PBRTransparentPathEligible", "source-alpha PBR forward admission") + require(forward_selection, "MODERN_GL_DRAW_PLAN_PIPELINE_FORWARD_PLUS_TRANSPARENT", "source-alpha PBR ordered forward route") + require(forward_selection, "draw.passCategory != RENDER_PASS_AMBIENT", "ambient-only clustered PBR ownership") + require(draw_plan, "R_ModernGLDrawPlan_SameStableSurface", "stable clustered PBR surface identity") + stable_surface = function_body(draw_plan, "static bool R_ModernGLDrawPlan_SameStableSurface(") + require(stable_surface, "R_ModernGLDrawPlan_SourceAmbientSurface( a )", "interaction source-surface identity") + require(stable_surface, "aSource == bSource", "shared ambient-source match") + reject(stable_surface, "a.legacyDrawSurf == b.legacyDrawSurf", "drawSurf pointer identity cannot match per-light receivers") + reject(stable_surface, "a.indexCount == b.indexCount", "culled interaction subsets must retain their ambient owner") + require(draw_plan, "R_ModernGLDrawPlan_PBRInteractionHasReadyClusteredOwner", "ready clustered PBR owner preflight") + require(draw_plan, "stats.pbrClusteredConsumedInteractions++", "clustered PBR duplicate consumption") + require(executor, "clusteredStats.pbrClusteredSurfaceOwners != 2", "clustered PBR owner self-test") + require(executor, "clusteredStats.pbrClusteredConsumedInteractions != 4", "clustered PBR dedupe self-test") + require(executor, "interactionTris[interactionIndex].ambientSurface", "distinct interaction subset self-test") + require(executor, "pbrOwners=%d, pbrConsumed=%d", "clustered PBR gameplay telemetry") + require(executor, "drawPlanStats.pbrClusteredSurfaceOwners", "clustered PBR owner telemetry copy") + require(executor, "drawPlanStats.pbrClusteredConsumedInteractions", "clustered PBR interaction telemetry copy") + require(draw_plan, "materialRecord->hasPBR && !forwardPlusCandidate && !pbrGBufferCandidate", "PBR flat-material rejection") + require(submit_plan, "MATERIAL_RESOURCE_TEXTURE_ALBEDO", "PBR albedo direct sampler") + require(submit_plan, "MATERIAL_RESOURCE_TEXTURE_ORM", "PBR ORM direct sampler") + for semantic in ("METALLIC", "ROUGHNESS", "AO"): + require(submit_plan, f"MATERIAL_RESOURCE_TEXTURE_{semantic}", "PBR separate-map direct sampler") + shader_library = read(ROOT / "src/renderer/ModernGLShaderLibrary.cpp") + require(shader_library, "ModernClusterEvaluatePBRLight", "PBR direct-light BRDF") + require(shader_library, "ModernPBRMaterialData", "PBR G-buffer inputs") + for token in ("uPBRIBL", "ModernPBRAnalyticEnvironment", "ModernPBRIndirect"): + require(shader_library, token, "analytic PBR IBL shader contract") + for token in ( + "struct ModernSpecularProbeRecord", + "layout(std140) uniform ModernSpecularProbeRecords", + "ModernSpecularProbeRecord probes[MODERN_SPECULAR_PROBE_MAX_RECORDS]", + "uniform sampler2D uModernSpecularProbeAtlas", + "float countValue = uClusterGrid.projectionDepth.z", + "float frameGeneration = uClusterGrid.projectionDepth.w", + "probe.identity.w != frameGeneration", + "probeIndex = candidate == 0 ? clusterRange.z : clusterRange.w", + "probeIndex >= probeCount", + "ModernSpecularProbeExactInteger(probe.axisZSlot.w", + "dot(reflectionDirection, normalize(probe.axisXPriority.xyz))", + "dot(reflectionDirection, normalize(probe.axisYBlend.xyz))", + "dot(reflectionDirection, normalize(probe.axisZSlot.xyz))", + "weight = clamp((radius - distanceToProbe) / blendWidth, 0.0, 1.0)", + "environment /= totalWeight", + "textureLod(uModernSpecularProbeAtlas, atlasUV, 0.0)", + "faceUV * float(MODERN_SPECULAR_PROBE_FACE_SIZE - 1)", + "prefiltered = mix(edgeBlendedProbe, analyticPrefiltered, clamp(roughness, 0.0, 1.0))", + ): + require(shader_library, token, "authored specular-probe shader contract") + if shader_library.count("ModernPBRIndirect(") != 4: + raise AssertionError("every deferred/opaque-alpha/transparent PBR indirect call must carry clusterRange") + if shader_library.count("pbrData.z, clusterRange)") != 2: + raise AssertionError("opaque-alpha and transparent PBR indirect calls must carry clusterRange") + require( + shader_library, + "material.b, clusterRange", + "deferred PBR indirect cluster-range handoff", + ) + for token in ( + '#include "ModernSpecularProbeAtlas.h"', + "MODERN_GL_CLUSTER_UBO_BINDING_SPECULAR_PROBES = 7", + '"ModernSpecularProbeRecords", MODERN_GL_CLUSTER_UBO_BINDING_SPECULAR_PROBES', + 'glUniform1i( specularProbeAtlas, MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT )', + "R_ModernGLExecutor_ShadowTextureUnitLimit() > MODERN_SPECULAR_PROBE_ATLAS_TEXTURE_UNIT", + 'glGetUniformBlockIndex( program->program, "ModernSpecularProbeRecords" )', + "R_ModernClusteredLighting_ProbeUboBlockBytes()", + "specular-probe UBO layout drift", + "R_ModernSpecularProbeAtlas_Init( caps, features )", + "R_ModernSpecularProbeAtlas_Shutdown()", + "R_ModernSpecularProbeAtlas_BeginFrame()", + "R_ModernSpecularProbeAtlas_FlushUploads()", + "R_ModernSpecularProbeAtlas_FrameReady()", + "R_ModernGLExecutor_ShadowSlotPlaceholderTexture( GL_TEXTURE_2D )", + "R_ModernSpecularProbeAtlas_PrintGfxInfo()", + "RendererSpecularProbeAtlas_RunSelfTest()", + ): + require(executor, token, "authored specular-probe executor contract") + for sampler in ("uMetallicTexture", "uRoughnessTexture", "uAOTexture"): + require(shader_library, sampler, "PBR separate-map shader sampler") + require(executor, "pbrSeparateMaterialData", "separate-map texture-table bypass") + require(executor, "R_ModernGLExecutor_SetPBRIBL", "PBR IBL uniform binding") + require(executor, "r_pbrIBLIntensity.GetFloat()", "PBR IBL intensity cvar binding") + require(shader_library, "float pbrDebug = floor(uDebugColor.a + 0.5)", "deferred PBR debug routing") + require(shader_library, "out_Emissive = vec4(emissive, pbr ? 1.0 : 0.0);", "explicit G-buffer PBR layout marker") + require(shader_library, "bool pbr = emissiveRecord.a > 0.5;", "deferred explicit PBR marker decode") + reject(shader_library, "bool pbr = material.a > 0.5;", "collision-prone material-alpha PBR marker") + require(executor, "r_pbrDebug", "PBR debug controls") + require(executor, "RendererPBRVisible self-test passed", "native PBR visible self-test") + require(executor, "modern-quality rollback admitted PBR", "master-off native PBR rejection self-test") + require(executor, "material->EvaluateRegisters( shaderRegisters", "PBR scalar-register self-test inputs") + require(read(ROOT / "src/renderer/RenderSystem_init.cpp"), "PBR materials: parser=1 modernLighting=1", "PBR capability reporting") + + scene_packet_selftest = function_body(packets_cpp, "bool RendererScenePacket_RunSelfTest(") + require(scene_packet_selftest, "pbrPacketFrame.AddDrawPacket", "native PBR scene-packet capture") + require(scene_packet_selftest, "pbrDrawPacket->materialRecord == pbrRecord", "public draw-packet material linkage") + require(scene_packet_selftest, "pbrRecord->pbrLegacyFallbackMissing", "PBR missing-fallback packet propagation assertion") + require(scene_packet_selftest, "validRegister( pbrRecord->pbrMetallicRegister )", "PBR packet register range assertion") + + +def test_runtime_selftest_is_registered_and_required() -> None: + init = read(ROOT / "src/renderer/RenderSystem_init.cpp") + material = read(ROOT / "src/renderer/Material.cpp") + table = read(ROOT / "src/renderer/MaterialResourceTable.cpp") + matrix = read(ROOT / "tools/tests/renderer_validation_matrix.py") + require(init, '"rendererPBRMaterialSelfTest"', "renderer command registration") + require(init, '"RendererPBRMaterial self-test passed\\n"', "runtime success marker") + require(matrix, '"RendererPBRMaterial self-test passed"', "matrix result contract") + require(matrix, '"+rendererPBRMaterialSelfTest"', "matrix command contract") + require(material, "generatedClassicInteractionFallback", "complete classic fallback parser coverage") + for token in ( + "material->GetNumStages() == 3", + "bumpStage->texture.image == globalImages->flatNormalMap", + "diffuseStage->texture.image == info.albedo.image", + "specularStage->texture.image == globalImages->blackImage", + ): + require(material, token, "complete classic fallback parser assertions") + for source, context in ((material, "parser self-test"), (table, "resource-table self-test")): + require(source, "declManager->AllocateDecl( DECL_MATERIAL )", context) + require(source, "DeclManager_FreeAllocatedDecl", context) + reject(source, "idMaterial dual;", context) + + +def test_controlled_fixture_gameplay_acceptance_gate() -> None: + benchmark_source = read(ROOT / "tools/tests/renderer_gameplay_benchmark.py") + for token in ( + "PBR_ACCEPTANCE_COMMON_CVARS", + "PBR_ACCEPTANCE_GL_CVARS", + "missing.extend(pbr_fixture_failures)", + '"pbrFixtureEvidence": pbr_fixture_evidence', + '"effectivePostMapCvars": effective_post_map_cvars(args.extra_cvars)', + '"pbrFixtureBinding": args.pbr_fixture_binding', + '("benchmarkConfig", "autoexecCfg")', + 'lines.append(f"set {name} {value}")', + "verify_procedural_pbr_fixture", + "recorded PBR fixture evidence differs", + ): + require(benchmark_source, token, "gameplay PBR acceptance integration") + if benchmark_source.count( + "pbr_fixture_acceptance_required=pbr_fixture_acceptance_required" + ) < 3: + raise AssertionError("SP and both MP roles must enforce PBR fixture acceptance") + + gl_fixture_cvars = ( + ("r_materialOverride", "materials/openq4/pbr_test/procedural_packed"), + ("r_pbrMaterials", "1"), + ("r_rendererModernQuality", "1"), + ("r_rendererModernVisible", "1"), + ("r_rendererForwardPlus", "1"), + ) + vk_fixture_cvars = gl_fixture_cvars[:3] + if not gameplay.requires_pbr_fixture_acceptance(gl_fixture_cvars, "gl"): + raise AssertionError("exact controlled GL PBR fixture must activate gameplay acceptance") + if not gameplay.requires_pbr_fixture_acceptance(vk_fixture_cvars, "vk"): + raise AssertionError("exact controlled Vulkan PBR fixture must activate gameplay acceptance") + if gameplay.requires_pbr_fixture_acceptance(vk_fixture_cvars, "gl"): + raise AssertionError("GL acceptance must retain its Modern GL fixture controls") + if not gameplay.requires_pbr_fixture_acceptance(gl_fixture_cvars, "vk"): + raise AssertionError("Vulkan acceptance must ignore unrelated Modern GL controls") + if not gameplay.requires_pbr_fixture_acceptance( + (("r_pbrMaterials", "0"), *gl_fixture_cvars), "gl" + ): + raise AssertionError("controlled PBR fixture must use last-write-wins cvar semantics") + for index, (name, value) in enumerate(gl_fixture_cvars): + changed = list(gl_fixture_cvars) + changed[index] = ( + (name, value + "_different") + if name.casefold() == "r_materialoverride" + else (name, "0") + ) + if gameplay.requires_pbr_fixture_acceptance(changed, "gl"): + raise AssertionError(f"non-exact {name} unexpectedly activated PBR acceptance") + if gameplay.requires_pbr_fixture_acceptance( + (*gl_fixture_cvars, ("R_PBRMATERIALS", "0")), "gl" + ): + raise AssertionError("case-insensitive final cvar override must disable PBR acceptance") + for engine_true in ("01", "10", "1junk", "-1"): + engine_semantics = tuple( + (name, engine_true) if name.casefold() != "r_materialoverride" else (name, value) + for name, value in gl_fixture_cvars + ) + if not gameplay.requires_pbr_fixture_acceptance(engine_semantics, "gl"): + raise AssertionError( + f"engine-true CVAR_BOOL value {engine_true!r} bypassed fixture acceptance" + ) + + def assert_value_error(callback: object, context: str) -> None: + try: + callback() # type: ignore[operator] + except ValueError: + return + raise AssertionError(f"unsafe {context} did not fail closed") + + for unsafe_cvar in ( + "r_pbrMaterials=1;quit", + "r_pbrMaterials=1\nquit", + "r_pbrMaterials=1\x7fquit", + "r_pbrMaterials=1\u2028quit", + "r_pbrMaterials=$ fixtureValue", + 'r_pbrMaterials="1"', + "r_pbrMaterials=+1", + "r_materialOverride=materials/openq4/*mutable*/procedural", + "fake=PBR material resources: records=9 resourceReady=9 modernReady=9 packed=9 separate=0 fallback=0", + "fake=Map: tools/mv2", + ): + assert_value_error( + lambda value=unsafe_cvar: gameplay.parse_extra_cvars([value]), + f"CVar {unsafe_cvar!r}", + ) + for unsafe_command in ( + "r_pbrMaterials 0", + "/r_pbrMaterials 0", + "\\set r_pbrMaterials 0", + "set r_rendererModernQuality 0", + "sett r_pbrMaterials 0", + "set /*mutable*/ r_pbrMaterials 0", + "$ mutatorCommand r_pbrMaterials 0", + "toggle r_rendererForwardPlus", + "exec mutable.cfg", + "vstr mutableCommands", + "cvar_restart", + "rendererBenchmarkCapture", + "map game/storage1", + "devmap game/storage1", + "echo Vulkan: native packed PBR direct interactions active (9 draws)", + "say PBR material resources: records=9 resourceReady=9 modernReady=9", + "god; r_pbrMaterials 0", + "god\nr_pbrMaterials 0", + ): + assert_value_error( + lambda value=unsafe_command: gameplay.parse_exec_commands([value]), + f"command {unsafe_command!r}", + ) + forged_resource_line = ( + "PBR material resources: records=9 resourceReady=9 modernReady=9 " + "packed=9 separate=0 fallback=0" + ) + for cvar_flag in ("--set-cvar", "--set-launch-cvar"): + try: + with contextlib.redirect_stderr(io.StringIO()): + gameplay.parse_args( + [ + cvar_flag, + f"fake={forged_resource_line}", + "--exec-command", + "fake", + ] + ) + except SystemExit as exc: + if exc.code != 2: + raise AssertionError("unsafe CLI CVar failed with the wrong status") + else: + raise AssertionError( + f"CLI CVar-print telemetry forgery through {cvar_flag} was accepted" + ) + if gameplay.parse_exec_commands(["god", "impulse 13", "waitMsec 100"]) != ( + "god", + "impulse 13", + "waitMsec 100", + ): + raise AssertionError("ordinary benchmark commands must remain accepted") + + cfg_spec = gameplay.RunSpec( + case_id="sp-mv2-interaction", + mode="SP", + map_name="maps/tools/mv2", + budget_map_name="maps/tools/mv2", + purpose="config provenance", + path_name="spawn-static", + tier="gl45", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="default", + renderer="best", + render_api="gl", + ) + cfg_lines, _ = gameplay.build_scripted_capture_lines( + cfg_spec, + "sp", + "test", + 360, + 600, + 0, + gl_fixture_cvars, + ("god",), + True, + True, + ) + cfg_text = "\n".join(cfg_lines) + "\n" + cfg_cvars, cfg_commands = gameplay.benchmark_cfg_provenance(cfg_text) + if cfg_cvars != gl_fixture_cvars or cfg_commands != ("god",): + raise AssertionError("generated config provenance did not preserve exact inputs") + assert_value_error( + lambda: gameplay.benchmark_cfg_provenance( + cfg_text.replace( + gameplay.POST_MAP_CVARS_BEGIN, + "echo PBR material resources: records=9 resourceReady=9 modernReady=9 " + "packed=9 separate=0 fallback=0\n" + + gameplay.POST_MAP_CVARS_BEGIN, + ) + ), + "pre-marker renderer evidence injection", + ) + + gl_summary = gameplay.extract_summary( + "\n".join( + ( + "PBR material resources: records=3 resourceReady=3 modernReady=3 packed=3 separate=0 fallback=0", + "Modern GL executor: available, drawPlan=1 planDraws=5 planFallback=0 pbrOwners=2 pbrConsumed=4", + "Modern forward+: cvar=1, req=1 exec=1 resources=1 sceneColor=1 sceneDepth=1 program=1 cluster=1 draws=2 fallback=0", + "Modern visible frame: cvar=1, req=1 exec=1 resources=1 program=1 source=1 hybrid=1 backBuffer=1 composed=1", + ) + ) + ) + evidence, failures = gameplay.evaluate_pbr_fixture_evidence(gl_summary, True, "gl") + if failures or evidence.get("status") != "pass": + raise AssertionError(f"complete PBR fixture telemetry failed: {failures!r}") + + zero_checks = ( + ("modernVisibleFrame", "exec"), + ("modernVisibleFrame", "req"), + ("modernVisibleFrame", "resources"), + ("modernVisibleFrame", "program"), + ("modernVisibleFrame", "source"), + ("modernVisibleFrame", "hybrid"), + ("modernVisibleFrame", "backBuffer"), + ("modernVisibleFrame", "composed"), + ("pbrMaterialResources", "records"), + ("pbrMaterialResources", "resourceReady"), + ("pbrMaterialResources", "modernReady"), + ("pbrMaterialResources", "packed"), + ("modernGLExecutor", "drawPlan"), + ("modernGLExecutor", "planDraws"), + ("modernGLExecutor", "pbrOwners"), + ("modernGLExecutor", "pbrConsumed"), + ("modernForwardPlus", "req"), + ("modernForwardPlus", "exec"), + ("modernForwardPlus", "resources"), + ("modernForwardPlus", "sceneColor"), + ("modernForwardPlus", "sceneDepth"), + ("modernForwardPlus", "program"), + ("modernForwardPlus", "cluster"), + ("modernForwardPlus", "draws"), + ) + for summary_name, field_name in zero_checks: + broken = dict(gl_summary) + broken[summary_name] = re.sub( + rf"\b{field_name}=\d+\b", + f"{field_name}=0", + broken[summary_name], + count=1, + ) + broken_evidence, broken_failures = gameplay.evaluate_pbr_fixture_evidence( + broken, True, "gl" + ) + if not broken_failures or broken_evidence.get("status") != "fail": + raise AssertionError(f"zero {field_name} did not fail PBR fixture acceptance") + + fallback = dict(gl_summary) + fallback["modernGLExecutor"] = fallback["modernGLExecutor"].replace( + "planFallback=0", "planFallback=1" + ) + _, fallback_failures = gameplay.evaluate_pbr_fixture_evidence(fallback, True, "gl") + if not fallback_failures: + raise AssertionError("draw-plan fallback did not fail PBR fixture acceptance") + for summary_name, field_name in ( + ("pbrMaterialResources", "fallback"), + ("modernForwardPlus", "fallback"), + ): + broken = dict(gl_summary) + broken[summary_name] = broken[summary_name].replace( + f"{field_name}=0", f"{field_name}=1" + ) + _, broken_failures = gameplay.evaluate_pbr_fixture_evidence( + broken, True, "gl" + ) + if not broken_failures: + raise AssertionError(f"{summary_name} fallback did not fail acceptance") + nested_shadow_fallback = dict(gl_summary) + nested_shadow_fallback["modernForwardPlus"] = nested_shadow_fallback[ + "modernForwardPlus" + ].replace( + "fallback=0", + "fallback=1 shadow(mapped=1 fallback=0 skipped=0 descriptors=1)", + 1, + ) + _, nested_shadow_failures = gameplay.evaluate_pbr_fixture_evidence( + nested_shadow_fallback, True, "gl" + ) + if not any("forward+ fallback=0" in failure for failure in nested_shadow_failures): + raise AssertionError( + "top-level forward+ fallback was hidden by the nested shadow fallback" + ) + unavailable = dict(gl_summary) + unavailable["modernGLExecutor"] = unavailable["modernGLExecutor"].replace( + "Modern GL executor: available,", "Modern GL executor: unavailable," + ) + _, unavailable_failures = gameplay.evaluate_pbr_fixture_evidence( + unavailable, True, "gl" + ) + if not unavailable_failures: + raise AssertionError("unavailable Modern GL executor did not fail acceptance") + forged = dict(gl_summary) + forged["pbrMaterialResources"] = ( + 'fake = "' + forged["pbrMaterialResources"] + '"' + ) + _, forged_failures = gameplay.evaluate_pbr_fixture_evidence(forged, True, "gl") + if not any("malformed" in failure for failure in forged_failures): + raise AssertionError("unanchored CVar-print telemetry did not fail acceptance") + vk_summary = gameplay.extract_summary( + "\n".join( + ( + "PBR material resources: records=2 resourceReady=2 modernReady=2 packed=2 separate=0 fallback=0", + "Vulkan: native packed PBR direct interactions active (7 draws)", + ) + ) + ) + vk_evidence, vk_failures = gameplay.evaluate_pbr_fixture_evidence( + vk_summary, True, "vk" + ) + if vk_failures or vk_evidence.get("status") != "pass": + raise AssertionError(f"complete Vulkan PBR fixture telemetry failed: {vk_failures!r}") + if any(name in vk_evidence for name in ("modernVisible", "drawPlan", "forwardPlus")): + raise AssertionError("Vulkan acceptance must not depend on Modern GL telemetry") + for marker in ("", "Vulkan: native packed PBR direct interactions active (0 draws)"): + broken_vk = dict(vk_summary) + broken_vk["vulkanPackedPBR"] = marker + broken_evidence, broken_failures = gameplay.evaluate_pbr_fixture_evidence( + broken_vk, True, "vk" + ) + if not broken_failures or broken_evidence.get("status") != "fail": + raise AssertionError("missing/zero Vulkan packed-PBR draw marker did not fail") + final_zero_summary = gameplay.extract_summary( + "\n".join( + ( + vk_summary["pbrMaterialResources"], + "Vulkan: native packed PBR direct interactions active (7 draws)", + "Vulkan: native packed PBR direct interactions active (0 draws)", + ) + ) + ) + _, final_zero_failures = gameplay.evaluate_pbr_fixture_evidence( + final_zero_summary, True, "vk" + ) + if not final_zero_failures: + raise AssertionError("Vulkan acceptance did not use the final packed-PBR marker") + for field_name in ("records", "resourceReady", "modernReady", "packed"): + broken_vk = dict(vk_summary) + broken_vk["pbrMaterialResources"] = re.sub( + rf"\b{field_name}=\d+\b", + f"{field_name}=0", + broken_vk["pbrMaterialResources"], + count=1, + ) + _, broken_failures = gameplay.evaluate_pbr_fixture_evidence( + broken_vk, True, "vk" + ) + if not broken_failures: + raise AssertionError(f"zero Vulkan {field_name} did not fail acceptance") + vk_fallback = dict(vk_summary) + vk_fallback["pbrMaterialResources"] = vk_fallback[ + "pbrMaterialResources" + ].replace("fallback=0", "fallback=1") + _, vk_fallback_failures = gameplay.evaluate_pbr_fixture_evidence( + vk_fallback, True, "vk" + ) + if not vk_fallback_failures: + raise AssertionError("Vulkan common-resource fallback did not fail acceptance") + + optional_evidence, optional_failures = gameplay.evaluate_pbr_fixture_evidence( + {}, False, "vk" + ) + if optional_failures or optional_evidence.get("status") != "not-required": + raise AssertionError("ordinary gameplay run unexpectedly required PBR telemetry") + + +def test_vulkan_pbr_support_stays_narrow_and_fail_closed() -> None: + vulkan_backend = read(ROOT / "src/renderer/Vulkan/vk_Backend.cpp") + require( + vulkan_backend, + "VK_GL_SELFTEST_STUB( RendererPBRVisible_RunSelfTest )", + "Vulkan PBR self-test link closure and honest GL-only skip", + ) + interactions = read(ROOT / "src/renderer/Vulkan/vk_Interactions.cpp") + topology = function_body( + interactions, + "static bool VK_PBRHasSingleClassicInteractionTopology(", + ) + for token in ( + "int bumpStage = -1;", + "int diffuseStage = -1;", + "int specularStage = -1;", + "surfaceStage->newStage->customLighting", + "*ownerStage >= 0", + "surfaceStage->newStage != NULL", + "surfaceStage->texture.image == NULL", + "surfaceStage->conditionRegister >= registerCount", + "const float condition = registers[ surfaceStage->conditionRegister ];", + "!std::isfinite( condition )", + "condition == 0.0f", + "bumpStage >= 0 && diffuseStage > bumpStage && specularStage > diffuseStage", + ): + require(topology, token, "single-owner Vulkan packed-PBR topology") + if topology.find("*ownerStage >= 0") > topology.find( + "const float condition = registers[ surfaceStage->conditionRegister ];" + ): + raise AssertionError( + "declared duplicate Vulkan interaction stages must be rejected before conditions" + ) + packed_admission = function_body( + interactions, + "static bool VK_PackedPBRInteraction(", + ) + require( + packed_admission, + "!VK_PBRHasSingleClassicInteractionTopology( din->surf )", + "fail-closed Vulkan packed-PBR topology admission", + ) + decomposition = function_body( + interactions, + "static void VK_CreateSingleDrawInteractions(", + ) + require( + decomposition, + "const bool packedPBROwnerEligible = VK_PBRHasSingleClassicInteractionTopology( surf );", + "stable Vulkan packed-PBR surface owner", + ) + if decomposition.count("VK_SubmitInteraction( &inter, false );") != 3: + raise AssertionError("every intermediate Vulkan interaction flush must stay classic") + if decomposition.count( + "VK_SubmitInteraction( &inter, packedPBROwnerEligible );" + ) != 1: + raise AssertionError("only the final Vulkan interaction submit may own packed PBR") + reject( + decomposition, + "VK_SubmitInteraction( &inter );", + "ungated Vulkan packed-PBR interaction submission", + ) + for token in ( + "VK_PackedPBRInteraction", + "!r_rendererModernQuality.GetBool()", + "PBR_WORKFLOW_METALLIC_ROUGHNESS", + "PBR_NORMAL_TANGENT_XYZ", + "material->Coverage() != MC_OPAQUE", + "R_MaterialResourceTable_FindRecordForMaterial( material )", + "R_MaterialResourceTable_PBRModernPathEligible( *resourceRecord )", + "VK_PBRImageReady( info.albedo.image, TD_PBR_COLOR )", + "VK_PBRImageReady( info.orm.image, TD_MATERIAL_DATA )", + "r_pbrDebug.GetInteger() == 7", + "native packed PBR direct interactions active", + "VK_DrawSingleInteractionMode( din, false, 0.0f, 0.0f, allowNativePBR )", + "VK_DrawSingleInteractionMode( din, parallax, scaleBias[ 0 ], scaleBias[ 1 ], false )", + ): + require(interactions, token, "narrow Vulkan packed-PBR admission") + for relative_path in ( + "src/renderer/Vulkan/shaders/interaction.frag", + "src/renderer/Vulkan/shaders/interaction_shadow.frag", + "src/renderer/Vulkan/shaders/interaction_shadow_point.frag", + ): + shader = read(ROOT / relative_path) + for token in ( + "EvaluatePackedPBR", + "pow(max(texture(diffuseMap, albedoTexCoord).rgb, vec3(0.0)), vec3(2.2))", + "vec3 orm = texture(specularMap, ormTexCoord).rgb;", + "float metallic = clamp(orm.b * pc.d.y, 0.0, 1.0);", + "float roughness = clamp(orm.g * pc.d.z, 0.045, 1.0);", + "vec3 diffuse = (vec3(1.0) - fresnel) * (1.0 - metallic)", + "if (pc.d.x > 1.5)", + "outColor = vec4(0.0, 1.0, 0.0, 0.0);", + ): + require(shader, token, f"Vulkan packed-PBR shader {relative_path}") + + +def main() -> int: + tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")] + for test in tests: + test() + print("renderer_pbr_materials: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_picmip_policy.py b/tools/tests/renderer_picmip_policy.py index b0b9d8f0..009f543c 100644 --- a/tools/tests/renderer_picmip_policy.py +++ b/tools/tests/renderer_picmip_policy.py @@ -215,8 +215,8 @@ def test_exact_halvings_reuse_the_mip_chain_filter(): "the shrink filter should follow the same gamma choice DeriveOpts makes", ) assert_true( - "return usage == TD_FONT || usage == TD_LIGHT;" in image_load, - "gamma mips are only used for the font and light buckets", + "return usage == TD_FONT || usage == TD_LIGHT || usage == TD_PBR_COLOR;" in image_load, + "gamma mips are used for font, light, and explicitly color-managed PBR buckets", ) assert_true( image_load.count("return R_ResampleTexture( pic, width, height, scaledWidth, scaledHeight );") >= 3, diff --git a/tools/tests/renderer_screenshot_readback.py b/tools/tests/renderer_screenshot_readback.py index 93ca61b1..bedb0c9c 100644 --- a/tools/tests/renderer_screenshot_readback.py +++ b/tools/tests/renderer_screenshot_readback.py @@ -16,12 +16,34 @@ def require(source: str, snippet: str, context: str) -> None: raise AssertionError(f"Missing {snippet!r} in {context}") +def reject(source: str, snippet: str, context: str) -> None: + if snippet in source: + raise AssertionError(f"Unexpected {snippet!r} in {context}") + + def require_order(source: str, snippets: tuple[str, ...], context: str) -> None: positions = [source.find(snippet) for snippet in snippets] if any(position < 0 for position in positions) or positions != sorted(positions): raise AssertionError(f"Expected ordered snippets in {context}: {snippets!r}") +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"Missing function signature {signature!r}") + + depth = 0 + for index in range(start, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[start : index + 1] + + raise AssertionError(f"Could not find end of function {signature!r}") + + def test_screenshot_reads_the_unpresented_back_buffer() -> None: init_cpp = read("src/renderer/RenderSystem_init.cpp") require_order( @@ -71,10 +93,202 @@ def test_vulkan_capture_resumes_the_acquired_back_buffer() -> None: ) +def test_vulkan_swapchain_preserves_legacy_sdr_code_values() -> None: + device_cpp = read("src/renderer/Vulkan/VulkanDevice.cpp") + create_swapchain = function_body( + device_cpp, "static bool VK_Device_CreateSwapchain( void )" + ) + for snippet in ( + "VK_FORMAT_B8G8R8A8_UNORM", + "VK_FORMAT_R8G8B8A8_UNORM", + "VK_COLOR_SPACE_SRGB_NONLINEAR_KHR", + "formats[ i ].format == VK_FORMAT_UNDEFINED", + "chosen.format = VK_FORMAT_B8G8R8A8_UNORM;", + "if ( !compatibleSurfaceFormat )", + "Vulkan: surface has no compatible legacy SDR UNORM + SRGB_NONLINEAR format", + "format=%d colorSpace=%d", + ): + require(create_swapchain, snippet, "Vulkan legacy SDR swapchain selection") + reject( + create_swapchain, + "chosen = formats[ 0 ];", + "Vulkan arbitrary surface-format fallback", + ) + reject( + create_swapchain, + "VK_FORMAT_B8G8R8A8_SRGB", + "Vulkan double-encoding swapchain format", + ) + reject( + create_swapchain, + "VK_FORMAT_R8G8B8A8_SRGB", + "Vulkan double-encoding swapchain format", + ) + guard_start = create_swapchain.index("if ( !compatibleSurfaceFormat )") + create_start = create_swapchain.index("vkCreateSwapchainKHR") + require_order( + create_swapchain[guard_start:create_start], + ( + "Vulkan: surface has no compatible legacy SDR UNORM + SRGB_NONLINEAR format", + "return false;", + ), + "Vulkan fail-closed surface-format guard", + ) + + +def test_save_preview_resamples_a_coherent_full_frame() -> None: + session_cpp = read("src/framework/Session.cpp") + save_game = function_body(session_cpp, "bool idSessionLocal::SaveGame(") + require_order( + save_game, + ( + "sessionRenderCropGuard_t previewCrop( renderSystem->GetScreenWidth(), renderSystem->GetScreenHeight() );", + "game->Draw( 0 );", + "renderSystem->CaptureRenderToFile( tempPreviewFile, true, 320, 240 );", + ), + "physical full-frame save-preview capture and CPU resize", + ) + guard = function_body(session_cpp, "class sessionRenderCropGuard_t") + require_order( + guard, + ( + "renderSystem->CropRenderSize( width, height, false, true );", + "active = true;", + "~sessionRenderCropGuard_t()", + "renderSystem->UnCrop();", + ), + "exception-safe physical render crop", + ) + + renderer_header = read("src/renderer/RenderSystem.h") + require_order( + renderer_header, + ( + "CaptureRenderToFile( const char *fileName, bool fixAlpha = false )", + "SetUnderwaterView( float amount, const idVec3 &tint, float fogDistance )", + "CaptureRenderToFile( const char *fileName, bool fixAlpha, int outputWidth, int outputHeight )", + ), + "append-only resized-capture renderer interface", + ) + + +def test_capture_skips_rgb_pack_padding() -> None: + renderer_cpp = read("src/renderer/RenderSystem.cpp") + capture = function_body( + renderer_cpp, + "void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha,", + ) + require_order( + capture, + ( + "const int sourceStride = ( rc->width * 3 + 3 ) & ~3;", + "const size_t sourceBytes = (size_t)sourceStride * (size_t)rc->height;", + "R_StaticAlloc( sourceBytes )", + "memset( data, 0, sourceBytes );", + "glReadPixels(", + "const byte *sourceRow = data + (size_t)y * sourceStride;", + "byte *destinationRow = data2 + (size_t)y * rc->width * 4;", + "R_StaticFree( data );", + ), + "zeroed padded GL_RGB capture conversion", + ) + + width = 3 + rows = ( + bytes((1, 2, 3, 4, 5, 6, 7, 8, 9)), + bytes((11, 12, 13, 14, 15, 16, 17, 18, 19)), + ) + stride = (width * 3 + 3) & ~3 + packed = b"".join(row + bytes(stride - len(row)) for row in rows) + converted = b"".join( + packed[offset : offset + width * 3] + for offset in range(0, len(packed), stride) + ) + if converted != b"".join(rows): + raise AssertionError("row-stride model included GL_RGB pack padding") + + +def aspect_crop( + source_width: int, + source_height: int, + output_width: int, + output_height: int, +) -> tuple[int, int, int, int]: + crop_x = 0 + crop_y = 0 + crop_width = source_width + crop_height = source_height + source_product = source_width * output_height + output_product = source_height * output_width + if source_product > output_product: + crop_width = max(source_height * output_width // output_height, 1) + crop_x = (source_width - crop_width) // 2 + elif source_product < output_product: + crop_height = max(source_width * output_height // output_width, 1) + crop_y = (source_height - crop_height) // 2 + return crop_x, crop_y, crop_width, crop_height + + +def test_capture_center_crops_and_resamples_safely() -> None: + renderer_cpp = read("src/renderer/RenderSystem.cpp") + helper = function_body(renderer_cpp, "static byte *R_ResampleCaptureToAspectRGBA(") + capture = function_body( + renderer_cpp, + "void idRenderSystemLocal::CaptureRenderToFile( const char *fileName, bool fixAlpha,", + ) + + require_order( + helper, + ( + "const int64 sourceAspectProduct = (int64)sourceWidth * outputHeight;", + "cropX = ( sourceWidth - cropWidth ) / 2;", + "cropY = ( sourceHeight - cropHeight ) / 2;", + "const double sourceY = cropY +", + "byte *outputRow = output + (size_t)y * (size_t)outputWidth * 4;", + ), + "orientation-preserving center-aspect resize", + ) + require(capture, "outputWidth > MAX_CAPTURE_DIMENSION", "bounded capture output width") + require(capture, "outputHeight > MAX_CAPTURE_DIMENSION", "bounded capture output height") + require(renderer_cpp, "static const int64 MAX_CAPTURE_PIXELS = 33554432;", "capture pixel budget") + require(capture, "const int64 sourcePixelCount = (int64)rc->width * rc->height;", "64-bit source pixel count") + require(capture, "sourcePixelCount > MAX_CAPTURE_PIXELS", "bounded source pixel count") + require(capture, "const int64 outputPixelCount = (int64)outputWidth * outputHeight;", "64-bit output pixel count") + require(capture, "outputPixelCount > MAX_CAPTURE_PIXELS", "bounded output pixel count") + require(capture, "R_ResampleCaptureToAspectRGBA(", "CPU save-preview resample") + require_order( + capture, + ( + "R_StaticFree( data );", + "outputData = R_ResampleCaptureToAspectRGBA(", + "R_StaticFree( data2 );", + "R_WriteTGA(", + "R_StaticFree( outputData );", + ), + "bounded capture allocation lifetimes", + ) + + expected_crops = { + (1280, 720, 320, 240): (160, 0, 960, 720), + (1024, 768, 320, 240): (0, 0, 1024, 768), + (720, 1280, 320, 240): (0, 370, 720, 540), + } + for dimensions, expected in expected_crops.items(): + actual = aspect_crop(*dimensions) + if actual != expected: + raise AssertionError( + f"aspect crop for {dimensions!r} was {actual!r}, expected {expected!r}" + ) + + def main() -> None: test_screenshot_reads_the_unpresented_back_buffer() test_capture_defers_only_the_window_present() test_vulkan_capture_resumes_the_acquired_back_buffer() + test_vulkan_swapchain_preserves_legacy_sdr_code_values() + test_save_preview_resamples_a_coherent_full_frame() + test_capture_skips_rgb_pack_padding() + test_capture_center_crops_and_resamples_safely() print("renderer_screenshot_readback: ok") diff --git a/tools/tests/renderer_supersampling_safety.py b/tools/tests/renderer_supersampling_safety.py index f8143734..7f82a20b 100644 --- a/tools/tests/renderer_supersampling_safety.py +++ b/tools/tests/renderer_supersampling_safety.py @@ -33,12 +33,12 @@ def test_scene_target_supersampling_is_guarded_and_scales_clipping(): assert_true("RB_SCREEN_FRACTION_MAX = 200" in draw_common, "renderer should keep a conservative supersampling ceiling") assert_true("RB_MaxSceneScaleDimension" in draw_common and "glConfig.maxTextureSize" in draw_common, "supersampling should clamp against GL texture limits") - assert_true("RB_SupersampledSceneTargetRequested" in draw_common, "supersampling should request the scene render target") - assert_true("!supersampledScene && requestedSamples > 1" in draw_common, "supersampled scene targets should remain single-sample instead of layering MSAA onto the oversized FBO") - assert_true("RB_BeginSceneSupersampling" in draw_common, "supersampling should scale the backend scene command") - assert_true("RB_ScaleLocalScreenRect( vLight->scissorRect" in draw_common, "light scissors should scale with the supersampled viewport") + assert_true("RB_ScaledSceneTargetRequested" in draw_common, "supersampling should request the scaled scene render target") + assert_true("!scaledScene && requestedSamples > 1" in draw_common, "scaled scene targets should remain single-sample instead of layering MSAA onto the resized FBO") + assert_true("RB_BeginSceneScaling" in draw_common, "supersampling should scale the backend scene command") + assert_true("RB_ScaleTrackedRect( state, &vLight->scissorRect" in draw_common, "light scissors should scale with the supersampled viewport") assert_true("RB_ScaleDrawSurfChainScissors" in draw_common, "light draw-surface chains should have scaled scissors") - assert_true("RB_ScaleLocalScreenRect( vEntity->scissorRect" in draw_common, "entity scissors should scale with the supersampled viewport") + assert_true("RB_ScaleTrackedRect( state, &vEntity->scissorRect" in draw_common, "entity scissors should scale with the supersampled viewport") assert_true("RB_DrawFullscreenPostProcessQuad( sourceViewportWidth, sourceViewportHeight" in draw_common, "present should sample the supersampled source region when resolving") assert_true("targetViewport = scaleState.active ? scaleState.nativeViewport" in draw_common, "present should resolve back to the native viewport") diff --git a/tools/tests/renderer_temporal_presentation.py b/tools/tests/renderer_temporal_presentation.py new file mode 100644 index 00000000..94a410f0 --- /dev/null +++ b/tools/tests/renderer_temporal_presentation.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python3 +"""Static contracts for Milestone E temporal presentation and dynamic resolution.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +RENDERER = ROOT / "src" / "renderer" +GAME_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", str(ROOT.parent / "openQ4-game")) +).resolve() + + +def read(path: Path) -> str: + if not path.is_file(): + raise AssertionError(f"missing required source: {path}") + return path.read_text(encoding="utf-8") + + +def require(source: str, token: str, label: str) -> None: + if token not in source: + raise AssertionError(f"missing {label}: {token}") + + +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"missing function: {signature}") + brace = source.find("{", start) + if brace < 0: + raise AssertionError(f"missing body for: {signature}") + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[brace + 1 : index] + raise AssertionError(f"unterminated body for: {signature}") + + +def main() -> int: + public_header = read(RENDERER / "RenderSystem.h") + companion_header = read(GAME_ROOT / "src" / "renderer" / "RenderSystem.h") + if public_header != companion_header: + raise AssertionError("engine and companion RenderSystem.h files are not byte-identical") + for token in ( + "renderPresentationState_s", + "sceneWidth;", + "outputWidth;", + "historyGeneration;", + "GetPresentationState( renderPresentationState_t &state ) const", + "ResolveTemporalPresentation(", + ): + require(public_header, token, "shared renderer ABI") + + require( + read(RENDERER / "RenderModuleAPI.h"), + "#define RENDER_API_VERSION\t\t\t11", + "renderer ABI v11", + ) + require( + read(GAME_ROOT / "src" / "game" / "Game.h"), + "const int GAME_API_VERSION\t\t= 45;", + "game ABI v45", + ) + + render_init = read(RENDERER / "RenderSystem_init.cpp") + require( + render_init, + 'idCVar r_rendererDynamicResolution( "r_rendererDynamicResolution", "0"', + "default-off dynamic resolution", + ) + temporal = read(RENDERER / "TemporalPresentation.cpp") + for token in ( + 'idCVar r_temporalAA( "r_temporalAA", "0"', + 'idCVar r_dynamicResolutionCaptureNative( "r_dynamicResolutionCaptureNative", "0"', + "R_TemporalPresentation_FindSampleScale(", + "eligibleForFeedback", + "TemporalResolutionCore_Update(", + "R_TemporalPresentation_RecordFrameScale(", + "R_TemporalPresentation_InvalidateHistory(", + "R_TemporalPresentation_MarkCurrentFrameCapture(", + "viewDef->isSubview", + "TemporalHistoryCore_Jitter(", + "TemporalHistoryCore_ValidateCameraHistory(", + "viewDef->renderView.forceUpdate", + "TEMPORAL_HISTORY_RESET_EXPLICIT_CUT", + ): + require(temporal, token, "temporal frontend policy") + capture_body = function_body( + temporal, "void R_TemporalPresentation_MarkCurrentFrameCapture(" + ) + require(capture_body, "entry.eligibleForFeedback = false;", "capture timing rejection") + require(capture_body, "R_TemporalPresentation_InvalidateHistory(", "late-capture history reset") + + resolution_core = read(RENDERER / "TemporalPresentationCore.h") + for token in ( + "maximumSampleAgeFrames", + "TemporalResolutionCore_SampleIsNewer(", + "sample.scalePercent - config.dropStepPercent", + "state.belowBudgetFrames >= config.raiseFrames", + "TemporalResolutionCore_AlignedDimension(", + ): + require(resolution_core, token, "dynamic-resolution controller") + + history_core = read(RENDERER / "TemporalHistoryCore.h") + for domain in ( + "STATIC_WORLD", + "RIGID", + "SKINNED", + "PARTICLE", + "DEFORM", + "SUBVIEW", + "IN_WORLD_GUI", + "VIEW_MODEL", + ): + require( + history_core, + f"TEMPORAL_MOTION_DOMAIN_{domain}", + f"{domain.lower()} motion ownership", + ) + for token in ( + "TEMPORAL_MOTION_OWNERSHIP_REACTIVE", + "TEMPORAL_MOTION_OWNERSHIP_DISOCCLUSION_TEST", + "TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY", + "TEMPORAL_MOTION_OWNERSHIP_DEPTH_HACK", + "current.outputWidth != previous.outputWidth", + "current.renderTimeMsec < previous.renderTimeMsec", + "TEMPORAL_MAX_REACTIVE_REGIONS = 2", + "TemporalHistoryCore_AddMotionOwnership(", + "backendExactMotionDomainMask", + "unsupportedObjectMotion", + ): + require(history_core, token, "temporal history policy") + if "current.sceneWidth != previous.sceneWidth" in function_body( + history_core, "TemporalHistoryCore_ValidateCameraHistory(" + ): + raise AssertionError("scene-only scale changes must preserve native history") + + scene_packets = read(RENDERER / "ScenePackets.cpp") + for token in ( + "R_TemporalPresentation_TemporalAARequested()", + "TemporalHistoryCore_ClassifyMotion( temporalInput )", + "hasPreviousModelMatrix", + "TEMPORAL_MOTION_OWNERSHIP_SEPARATE_HISTORY", + "R_ScenePackets_BuildTemporalViewMotionPolicy(", + "R_ScenePackets_TemporalRigidMotionEligible(", + "packet.temporalExactRigidEligible", + "TemporalHistoryCore_AddMotionOwnership( policy,", + ): + require(scene_packets, token, "scene-packet temporal ownership") + + for camera_source in ( + GAME_ROOT / "src" / "game" / "Camera.cpp", + GAME_ROOT / "src" / "mpgame" / "Camera.cpp", + ): + camera = read(camera_source) + require(camera, "view->forceUpdate = prevCut != cut;", "authored camera-cut signal") + require(camera, "cameraDef->GetAnim(1)->GetCut( i )", "authored cut traversal") + + metrics = read(RENDERER / "RendererMetrics.cpp") + gl_timing = function_body(metrics, "void R_RendererMetrics_BeginGpuBackendFrame(") + require(gl_timing, "r_rendererDynamicResolution.GetBool()", "automatic GL timing") + require(metrics, "GL_QUERY_RESULT_AVAILABLE", "nonblocking GL timing poll") + vk_timing = read(RENDERER / "Vulkan" / "VulkanGpuFrameTiming.cpp") + require(vk_timing, "r_rendererDynamicResolution.GetBool()", "automatic Vulkan timing") + if "VK_QUERY_RESULT_WAIT_BIT" in vk_timing.replace( + "Deliberately no VK_QUERY_RESULT_WAIT_BIT", "" + ): + raise AssertionError("Vulkan dynamic timing must not use WAIT_BIT") + + render_system = read(RENDERER / "RenderSystem.cpp") + begin_frame = function_body(render_system, "idRenderSystemLocal::BeginFrame(") + for token in ( + "!R_TemporalPresentation_DynamicResolutionRequested()", + "!R_TemporalPresentation_TemporalAARequested()", + "screenFraction < 100 && r_resolutionScaleMode.GetInteger() == 0", + ): + require(begin_frame, token, "legacy-crop temporal bypass") + enqueue = function_body( + render_system, "bool idRenderSystemLocal::ResolveTemporalPresentation(" + ) + for token in ( + "tr.takingScreenshot", + "presentation.captureFrozen", + "presentation.captureForcedNative", + "primaryView->temporalCaptureFrame", + "RC_RESOLVE_TEMPORAL_PRESENTATION", + "cmd->historyGeneration = historyGeneration;", + ): + require(enqueue, token, "validated temporal command enqueue") + require( + read(RENDERER / "tr_backend.cpp"), + "case RC_RESOLVE_TEMPORAL_PRESENTATION:", + "OpenGL temporal dispatch", + ) + + gl_backend = read(RENDERER / "draw_common.cpp") + gl_scene_request = function_body( + gl_backend, "static bool RB_ScaledSceneTargetRequested(" + ) + for token in ( + "r_resolutionScaleMode.GetInteger() == 0", + "!R_TemporalPresentation_TemporalAARequested()", + ): + require(gl_scene_request, token, "OpenGL manual-scale temporal ownership") + gl_draw_view = function_body(gl_backend, "RB_STD_DrawView( void )") + for token in ( + "const bool sceneRenderTargetReady =", + "rootSceneRenderTargetRequested && !sceneRenderTargetReady", + "RB_RecenterDirectTemporalProjection( sceneScaleState, backEnd.viewDef );", + "RB_RestoreDirectTemporalProjection( sceneScaleState, backEnd.viewDef );", + ): + require(gl_draw_view, token, "OpenGL direct temporal ownership fallback") + recenter_at = gl_draw_view.find("RB_RecenterDirectTemporalProjection(") + draw_at = gl_draw_view.find("RB_BeginDrawingView();") + if recenter_at < 0 or draw_at <= recenter_at: + raise AssertionError("OpenGL direct fallback must recenter before world drawing") + restore_at = gl_draw_view.find("RB_RestoreSceneScaling( sceneScaleState );") + present_at = gl_draw_view.find("RB_PresentBackendTemporalScene()") + if restore_at < 0 or present_at <= restore_at: + raise AssertionError( + "OpenGL reactive policy must see restored native view/scissor coordinates" + ) + gl_resolve = function_body(gl_backend, "bool RB_ResolveTemporalPresentation(") + for token in ( + "captureFrame", + "RB_RenderMotionVectorBuffer(", + "RB_DrawTemporalResolvePass(", + "RB_PresentTemporalSpatialFallback(", + "R_ScenePackets_BuildTemporalViewMotionPolicy(", + "exactMotionDomains", + "rbSceneScalePresentedFrame = backEnd.frameCount;", + ): + require(gl_backend if token.startswith("rbScene") else gl_resolve, token, "OpenGL temporal resolve") + for token in ( + "neighborhoodMin", + "historyClamped", + "depthReactive", + "historyWeight", + "ReactiveRegion0", + "packetReactive", + "R_ScenePackets_TemporalRigidMotionEligible(", + ): + require(gl_backend, token, "OpenGL disocclusion/reactive resolve") + + gl_swap_scale = function_body( + gl_backend, "void RB_ApplyResolutionScaleToBackBuffer( void )" + ) + for token in ( + "presentation.dynamicResolutionRequested", + "presentation.temporalAARequested", + ): + require(gl_swap_scale, token, "OpenGL native UI scale ownership") + temporal_guard_at = gl_swap_scale.find("presentation.dynamicResolutionRequested") + scene_marker_guard_at = gl_swap_scale.find( + "if ( rbSceneScalePresentedFrame", temporal_guard_at + ) + backbuffer_copy_at = gl_swap_scale.find("sceneImage->CopyFramebuffer(") + if ( + temporal_guard_at < 0 + or scene_marker_guard_at <= temporal_guard_at + or backbuffer_copy_at <= scene_marker_guard_at + ): + raise AssertionError( + "OpenGL temporal ownership must reject swap-tail scaling before copying UI" + ) + require( + gl_swap_scale[temporal_guard_at:scene_marker_guard_at], + "return;", + "OpenGL temporal native UI early-out", + ) + + vk_backend = read(RENDERER / "Vulkan" / "vk_Backend.cpp") + require(vk_backend, "RC_RESOLVE_TEMPORAL_PRESENTATION", "Vulkan temporal dispatch") + vk_gui = read(RENDERER / "Vulkan" / "vk_GuiExecutor.cpp") + vk_scene_request = function_body( + vk_gui, "static bool VK_TemporalPresentation_BackendSceneRequested(" + ) + for token in ( + "r_resolutionScaleMode.GetInteger() == 0", + "!R_TemporalPresentation_TemporalAARequested()", + ): + require(vk_scene_request, token, "Vulkan manual-scale temporal ownership") + vk_resolve_signature = "static bool VK_TemporalPresentation_ResolveTargets(" + vk_resolve = function_body( + vk_gui[vk_gui.rfind(vk_resolve_signature) :], vk_resolve_signature + ) + for token in ( + "command.captureFrame", + "historyResourceChanged", + "generationMatches", + "VK_TemporalPresentation_DrawResolve(", + "VK_TemporalPresentation_BlitColorToSwap(", + "VK_TemporalPresentation_DepthStampMatches(", + "&& depthReady && writeMatches", + '"Vulkan temporal depth is not current"', + ): + require(vk_resolve, token, "Vulkan temporal resolve") + vk_direct = function_body( + vk_gui, "static bool VK_TemporalPresentation_ResolvePendingSceneTemporal(" + ) + for token in ( + "temporalDirectHistoryRenderTextures[readIndex]", + "temporalDirectHistoryRenderTextures[writeIndex]", + "temporalDirectHistoryFrame == backEnd.frameCount - 1", + "VK_TemporalPresentation_ResolveTargets(", + "if ( historyAdvanced )", + ): + require(vk_direct, token, "Vulkan direct-view history ownership") + for token in ( + "FMT_RGBA16F", + "VK_TemporalPresentation_EnsureDirectHistoryTargets(", + "VK_TemporalPresentation_DrawPendingSceneSpatial(", + "temporalDepthStampFrame == backEnd.frameCount", + "destinationRenderTexture, depthResolved", + "R_ScenePackets_BuildTemporalViewMotionPolicy(", + "block.reactiveRect0", + "block.reactiveRect1", + ): + require(vk_gui, token, "Vulkan backend-owned temporal resources") + vk_fill_resolve = function_body( + vk_gui, "static void VK_TemporalPresentation_FillResolveBlock(" + ) + for token in ( + "block.reactiveRect0[2] = 1.0f;", + "const bool motionPolicyAvailable = viewDef != NULL", + "if ( motionPolicyAvailable )", + "block.reactiveRect0[component] = -1.0f;", + "block.reactiveRect1[component] = -1.0f;", + ): + require(vk_fill_resolve, token, "Vulkan reactive-policy fail-safe") + vk_shader = read(RENDERER / "Vulkan" / "shaders" / "temporal_resolve.frag") + for token in ( + "neighborhoodMin", + "historyClamped", + "depthReactive", + "historyWeight", + "InsideReactiveRect(", + "packetReactive", + ): + require(vk_shader, token, "Vulkan disocclusion/reactive resolve") + + game_local_header = read(GAME_ROOT / "src" / "game" / "Game_local.h") + require(game_local_header, "temporalHistoryRT[2]", "game history ping-pong ownership") + game_render = read(GAME_ROOT / "src" / "game" / "Game_render.cpp") + for token in ( + "presentation.sceneWidth", + "presentation.outputWidth", + "blurEnabled || presentation.temporalAARequested", + "!presentation.temporalAARequested", + "presentation.captureFrozen", + "presentation.captureForcedNative", + "openQ4_ResolveTemporalPresentation(", + "gameRender.temporalHistoryReadIndex = historyWriteIndex;", + ): + require(game_render, token, "game temporal scheduling") + game_resolve = function_body( + game_render, "static bool openQ4_ResolveTemporalPresentation(" + ) + call_at = game_resolve.find("renderSystem->ResolveTemporalPresentation(") + swap_at = game_resolve.find( + "gameRender.temporalHistoryReadIndex = historyWriteIndex;" + ) + if call_at < 0 or swap_at <= call_at: + raise AssertionError("game history must swap only after resolve acceptance") + + temporal_content = [ + path + for path in (ROOT / "content" / "baseoq4").rglob("*") + if path.is_file() and "temporal" in path.name.lower() + ] + if temporal_content: + raise AssertionError( + "temporal presentation must not depend on loose content assets: " + + ", ".join(str(path.relative_to(ROOT)) for path in temporal_content) + ) + + meson = read(ROOT / "meson.build") + require(meson, "openq4-temporal-presentation-core-test", "controller native test") + require(meson, "openq4-temporal-history-core-test", "history native test") + + print("renderer temporal presentation contracts: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/renderer_validation_matrix.py b/tools/tests/renderer_validation_matrix.py index e65cea9d..e175eeb9 100644 --- a/tools/tests/renderer_validation_matrix.py +++ b/tools/tests/renderer_validation_matrix.py @@ -20,8 +20,15 @@ from pathlib import Path from typing import Any +VALIDATION_DIR = Path(__file__).resolve().parents[1] / "validation" +if str(VALIDATION_DIR) not in sys.path: + sys.path.insert(0, str(VALIDATION_DIR)) + +from renderer_budget_contract import DEFAULT_CONTRACT_PATH, load_contract # noqa: E402 + SAFE_TIERS = ("auto", "legacy", "gl33", "gl41", "gl43", "gl45", "gl46") +PER_MAP_BUDGET_CONTRACT, PER_MAP_BUDGET_BINDING = load_contract(DEFAULT_CONTRACT_PATH) # Keep in sync with MAX_CONSOLE_LINES in src/framework/Common.cpp. The engine # silently ignores any "+command" beyond this limit, which would drop "+quit" @@ -39,6 +46,16 @@ ["RendererRenderGraph self-test passed"], ["RendererRenderGraphResource self-test passed", "RendererRenderGraphResource self-test skipped"], ["RendererMaterialResourceTable self-test passed", "RendererMaterialResourceTable self-test skipped"], + ["RendererClassicGuiDomain self-test passed"], + ["RendererClassicCinematicPostDomain self-test passed"], + ["RendererClassicWorldAmbientDomain self-test passed"], + ["RendererClassicInteractionDomain self-test passed"], + ["RendererClassicFogBlendDomain self-test passed"], + ["RendererClassicSubviewDomain self-test passed"], + ["RendererClassicDeformDomain self-test passed"], + ["RendererContracts self-test passed"], + ["RendererGpuSkinning self-test passed"], + ["RendererPBRMaterial self-test passed"], ["RendererGeometryResource self-test passed"], ["RendererGLStateCache self-test passed", "RendererGLStateCache self-test skipped"], ["RendererModernGLShaderLibrary self-test passed"], @@ -89,6 +106,24 @@ "map": "game/mcc_landing", "purpose": "subviews, remote cameras, cinematic and GUI interaction", }, + { + "id": "sp-mv2-ambient", + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock fixed-function world-ambient ownership and whole-view rollback", + }, + { + "id": "sp-mv2-interaction", + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock fixed-classic interaction and shadow ownership parity", + }, + { + "id": "sp-mv2-deform", + "mode": "SP", + "map": "maps/tools/mv2", + "purpose": "controlled stock material-move deformation ownership and published-geometry parity", + }, { "id": "mp-q4dm1-listen", "mode": "MP", @@ -209,6 +244,40 @@ "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile campaign-split-state-transition --timeout 360", "coverage": "real SP end-level target chain from game/mcc_2 through storage1 first, storage2, storage1 second, and game/tram1 with active map/filter assertions after each load", }, + { + "profile": "world-ambient", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile world-ambient --pacing-only --no-gpu-timers", + "coverage": "bordered stock maps/tools/mv2 capture with light, subview, GUI, post, portal-fade, and overlay islands disabled for shared world-ambient A/B ownership and explicit-blocker rollback evidence", + }, + { + "profile": "deform", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile deform --pacing-only --no-gpu-timers", + "coverage": "bordered stock maps/tools/mv2 capture with the stock shaderDemos/move material override, fixed camera, shared world-ambient ownership, and explicit material-deform contract ownership", + }, + { + "profile": "interaction", + "renderApi": "gl", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile interaction --render-api gl --pacing-only --no-gpu-timers --reference-dir .tmp\\renderer-references\\interaction\\gl\\classic\\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\\renderer-references\\interaction\\gl\\shadows-off\\savepaths", + "coverage": "OpenGL controlled unshadowed, stencil, projected-plus-point mapped, mixed mapped/stencil, and named map-admission fallback ownership with exact shared/classic TGA parity and a required shadows-on/off image delta", + }, + { + "profile": "interaction", + "renderApi": "vk", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile interaction --render-api vk --pacing-only --no-gpu-timers --reference-dir .tmp\\renderer-references\\interaction\\vk\\classic\\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\\renderer-references\\interaction\\vk\\shadows-off\\savepaths", + "coverage": "Vulkan controlled unshadowed, stencil, projected-plus-point mapped, mixed mapped/stencil, and named map-admission fallback ownership with exact shared/classic TGA parity and a required shadows-on/off image delta", + }, + { + "profile": "interaction-shadow-stock", + "renderApi": "gl", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile interaction-shadow-stock --render-api gl --pacing-only --no-gpu-timers --reference-dir .tmp\\renderer-references\\interaction-shadow-stock\\gl\\classic\\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\\renderer-references\\interaction-shadow-stock\\gl\\shadows-off\\savepaths", + "coverage": "OpenGL stock projected, point, CSM/parallel, dynamic, perforated, same-light hybrid, and exact translucent-moment fallback ownership with exact shared/classic TGA parity and required shadows-on/off image deltas", + }, + { + "profile": "interaction-shadow-stock", + "renderApi": "vk", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile interaction-shadow-stock --render-api vk --pacing-only --no-gpu-timers --reference-dir .tmp\\renderer-references\\interaction-shadow-stock\\vk\\classic\\savepaths --require-references --image-rms-threshold 0 --image-max-threshold 0 --difference-reference-dir .tmp\\renderer-references\\interaction-shadow-stock\\vk\\shadows-off\\savepaths", + "coverage": "Vulkan stock projected, point, CSM/parallel, dynamic, perforated, same-light hybrid, and exact translucent-moment fallback ownership with exact shared/classic TGA parity and required shadows-on/off image deltas", + }, { "profile": "tiers", "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile tiers", @@ -216,7 +285,7 @@ }, { "profile": "presentation", - "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile presentation", + "command": "python tools\\tests\\renderer_gameplay_benchmark.py --profile presentation --pacing-only", "coverage": "windowed/fullscreen coverage for r_swapInterval 0/1 and com_maxfps 0/120/240 while preserving uncapped high-refresh presentation behavior", }, { @@ -348,7 +417,7 @@ }, { "criterion": "conservative defaults", - "required": "`r_renderer best` or explicit `r_renderer arb2` keeps ARB2 visible; modern executor, submit, visible, side-path, debug, GPU-validation, bindless, shader-reload, and auto-promotion cvars remain off in a clean startup", + "required": "`r_renderer best` or explicit `r_renderer arb2` keeps ARB2 visible; shared classic domains, modern executor, submit, visible, side-path, debug, GPU-validation, bindless, shader-reload, and auto-promotion cvars remain off in a clean startup", }, { "criterion": "validation evidence", @@ -363,6 +432,14 @@ WARNING_PATTERNS = { "snPrintfOverflow": re.compile(r"idStr::snPrintf:\s*overflow", re.IGNORECASE), "idStrWarning": re.compile(r"WARNING:\s+idStr", re.IGNORECASE), + "binaryImageCacheWrite": re.compile( + r"WARNING:\s*(?:\^[0-9]\s*)*idBinaryImage:\s+Could not open (?:file|generated cache)\b", + re.IGNORECASE, + ), + "expandedLoadscreenPublish": re.compile( + r"WARNING:\s*(?:\^[0-9]\s*)*Could not publish expanded loading background\b", + re.IGNORECASE, + ), "shaderCompileOrLink": re.compile(r"(shader compile|program link).*(failed|error)|failed to compile", re.IGNORECASE), "glError": re.compile( r"\bGL_(?:INVALID_[A-Z_]+|OUT_OF_MEMORY|STACK_(?:OVERFLOW|UNDERFLOW)|CONTEXT_LOST)\b" @@ -456,7 +533,7 @@ def sanitize_case_id(case_id: str) -> str: def common_args( - root: Path, + runtime_dir: Path, case_id: str, basepath: str, savepath: Path, @@ -495,11 +572,23 @@ def common_args( "r_rendererModernSubmit", "0", "+set", + "r_rendererSharedSubview", + "0", + "+set", + "r_rendererSharedInWorldGui", + "0", + "+set", + "r_rendererSharedCinematicPost", + "0", + "+set", + "r_rendererSharedSpecialFrame", + "0", + "+set", "fs_savepath", str(savepath), "+set", "fs_devpath", - str(root / ".install"), + str(runtime_dir), "+set", "fs_game", "baseoq4", @@ -520,6 +609,9 @@ def common_args( def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: selftest_commands = [ + "+set", + "r_renderApi", + "gl", "+set", "r_rendererMetrics", "2", @@ -546,6 +638,17 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: "+rendererRenderGraphSelfTest", "+rendererRenderGraphResourceSelfTest", "+rendererMaterialResourceTableSelfTest", + "+rendererClassicGuiDomainSelfTest", + "+rendererClassicCinematicPostDomainSelfTest", + "+rendererClassicSpecialFrameDomainSelfTest", + "+rendererClassicWorldAmbientDomainSelfTest", + "+rendererClassicInteractionDomainSelfTest", + "+rendererClassicFogBlendDomainSelfTest", + "+rendererClassicSubviewDomainSelfTest", + "+rendererClassicDeformDomainSelfTest", + "+rendererContractsSelfTest", + "+rendererGpuSkinningSelfTest", + "+rendererPBRMaterialSelfTest", "+rendererGeometryResourceSelfTest", "+rendererGLStateCacheSelfTest", "+rendererModernGLExecutorSelfTest", @@ -558,7 +661,8 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: { "id": "renderer-foundation-selftests", "category": "selftest", - "description": "Renderer foundation, upload, metrics, packet, graph, material, geometry, shader, draw, submit, and executor self-tests.", + "description": "Renderer foundation, shared contracts, GPU-animation, upload, metrics, packet, graph, material, geometry, shader, draw, submit, and executor self-tests.", + "preservesConfig": True, "args": selftest_commands, "checks": SELFTEST_CHECKS + [["Selected renderer tier:"], ["GL context request:"], ["Renderer API: requested="]], }, @@ -617,6 +721,32 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: ["GL context request:"], ], }, + { + "id": "renderer-pbr-visible-selftest", + "category": "selftest", + "description": "Guarded PBR material admission, sampler packing, and G-buffer/deferred/forward shader corridor self-test.", + "args": [ + "+set", + "r_rendererMetrics", + "2", + "+set", + "r_rendererModernExecutor", + "1", + "+set", + "r_rendererModernOpaque", + "1", + "+rendererPBRVisibleSelfTest", + "+gfxInfo", + ], + "checks": [ + ["RendererPBRVisible self-test passed"], + ["gbuffer=1"], + ["deferred=1"], + ["forward=1"], + ["Selected renderer tier:"], + ["GL context request:"], + ], + }, { "id": "renderer-light-image-atlas-selftest", "category": "selftest", @@ -977,16 +1107,16 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: { "id": "renderer-default-safety-selftest", "category": "selftest", - "description": "Phase 13 conservative-default safety gate for ARB2 default visibility, rollback escape, and default-off modern diagnostic side paths.", + "description": "Phase 13 conservative-default safety gate for ARB2 default visibility, rollback escape, shared classic domains, and default-off modern diagnostic side paths.", "args": [ "+rendererDefaultSafetySelfTest", "+gfxInfo", ], "checks": [ ["RendererDefaultSafety self-test passed"], - ["Renderer default safety:", "conservative=1", "rollback=available", "issues=none"], + ["Renderer default safety:", "conservative=1", "sharedDeform=0", "rollback=available", "issues=none"], ["Renderer default promotion:", "active=0"], - ["Renderer bootstrap:", "defaultVisible=ARB2"], + ["Renderer bootstrap:", "defaultVisible=ARB2", "sharedWorldFogBlend=0", "sharedSubview=0"], ], }, { @@ -1383,6 +1513,15 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: "+set", "r_vkValidation", "1", + "+rendererContractsSelfTest", + "+rendererGpuSkinningSelfTest", + "+rendererClassicCinematicPostDomainSelfTest", + "+rendererClassicSpecialFrameDomainSelfTest", + "+rendererClassicWorldAmbientDomainSelfTest", + "+rendererClassicInteractionDomainSelfTest", + "+rendererClassicFogBlendDomainSelfTest", + "+rendererClassicSubviewDomainSelfTest", + "+rendererClassicDeformDomainSelfTest", "+gfxInfo", ], "checks": [ @@ -1391,6 +1530,16 @@ def build_safe_cases(tiers: tuple[str, ...]) -> list[dict[str, Any]]: ["Vulkan: created swapchain"], ["Vulkan: GUI executor initialized"], ["Vulkan renderer initialized"], + ["RendererContracts self-test passed"], + ["RendererGpuSkinning self-test passed"], + ["RendererClassicCinematicPostDomain self-test passed"], + ["RendererClassicSpecialFrameDomain self-test passed"], + ["RendererClassicWorldAmbientDomain self-test passed"], + ["RendererClassicInteractionDomain self-test passed"], + ["RendererClassicFogBlendDomain self-test passed"], + ["RendererClassicSubviewDomain self-test passed"], + ["RendererClassicDeformDomain self-test passed"], + ["GPU skinning:"], ], }, { @@ -1567,23 +1716,23 @@ def filter_driver_specific_cases(cases: list[dict[str, Any]]) -> list[dict[str, ] -def vk_module_path(root: Path) -> Path: +def vk_module_path(runtime_dir: Path) -> Path: if os.name == "nt": suffix = ".dll" elif sys.platform == "darwin": suffix = ".dylib" else: suffix = ".so" - return root / ".install" / f"renderer-vk_{host_arch()}{suffix}" + return runtime_dir / f"renderer-vk_{host_arch()}{suffix}" -def filter_vulkan_module_cases(cases: list[dict[str, Any]], root: Path) -> list[dict[str, Any]]: +def filter_vulkan_module_cases(cases: list[dict[str, Any]], runtime_dir: Path) -> list[dict[str, Any]]: # the Vulkan cases need a staged renderer-vk module and a live Vulkan # driver. Headless Linux legs (Xvfb/WSL) offer neither, so they stay # dropped there. Windows has a native driver; macOS runs the module on # MoltenVK, which is bundled with the package, so both hosts qualify once # the module is staged next to the executable. - if (os.name == "nt" or sys.platform == "darwin") and vk_module_path(root).exists(): + if (os.name == "nt" or sys.platform == "darwin") and vk_module_path(runtime_dir).exists(): return cases dropped = [case["id"] for case in cases if case.get("requiresVulkanModule")] if dropped: @@ -1705,6 +1854,7 @@ def print_failure_details(result: dict[str, Any]) -> None: def run_case( root: Path, executable: Path, + runtime_dir: Path, output_dir: Path, savepath: Path, basepath: str, @@ -1723,7 +1873,7 @@ def run_case( case_assetless = bool(case.get("assetless", False)) case_basepath = "" if case_assetless else basepath case_skip_official_pak_validation = skip_official_pak_validation or case_assetless - args = common_args(root, case_id, case_basepath, savepath, case_skip_official_pak_validation) + case["args"] + ["+quit"] + args = common_args(runtime_dir, case_id, case_basepath, savepath, case_skip_official_pak_validation) + case["args"] + ["+quit"] startup_commands = sum(1 for arg in args if arg.startswith("+")) if startup_commands > ENGINE_MAX_STARTUP_COMMANDS: raise RuntimeError( @@ -1732,7 +1882,7 @@ def run_case( ) # drill lever: hide the staged renderer-vk module so the loader's # fallback ladder is exercised for real, restoring it afterwards - module_path = vk_module_path(root) + module_path = vk_module_path(runtime_dir) hidden_module_path = module_path.with_name(module_path.name + ".drill-hidden") hide_vk_module = bool(case.get("hideVkModule", False)) # cvars set on the command line are archived on exit; cases that opt @@ -1750,7 +1900,7 @@ def run_case( with stdout_path.open("w", encoding="utf-8", errors="replace") as stdout_file, stderr_path.open("w", encoding="utf-8", errors="replace") as stderr_file: process = subprocess.Popen( [str(executable)] + args, - cwd=str(root / ".install"), + cwd=str(runtime_dir), stdout=stdout_file, stderr=stderr_file, ) @@ -1825,6 +1975,8 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic "shaderLibraryTierMatrix": SHADER_LIBRARY_TIER_MATRIX, "longRunValidationMatrix": LONG_RUN_VALIDATION_MATRIX, "perfRegressionThresholds": PERF_REGRESSION_THRESHOLDS, + "perMapBudgetContract": PER_MAP_BUDGET_BINDING, + "perMapBudgets": PER_MAP_BUDGET_CONTRACT["budgets"], "promotionEvidenceGate": { "cvar": "r_rendererPromotionEvidence", "requiredTokens": PROMOTION_EVIDENCE_REQUIRED_TOKENS, @@ -1980,6 +2132,23 @@ def write_reports(output_dir: Path, results: list[dict[str, Any]], metadata: dic for item in PERF_REGRESSION_THRESHOLDS: lines.append(f"| `{item['preset']}` | {item['p95Ms']} ms | {item['p99Ms']} ms | {item['budget']} |") + lines += [ + "", + "## Per-Map CPU/GPU Budget Contract", + "", + f"Contract `{PER_MAP_BUDGET_BINDING['contractId']}` / `{PER_MAP_BUDGET_BINDING['sha256']}` uses integer microseconds and exact map/backend/profile selection. These are target ceilings; measured values are retained by the gameplay and stock evidence reports.", + "", + "| Budget | Map | Backend | Profile | Minimum CPU/GPU samples | CPU P95/P99 | GPU P95/P99 |", + "|---|---|---|---|---:|---:|---:|", + ] + for item in PER_MAP_BUDGET_CONTRACT["budgets"]: + lines.append( + f"| `{item['id']}` | `{item['map']}` | `{item['backend']}` | `{item['profile']}` | " + f"{item['minimumSamples']['cpu']}/{item['minimumSamples']['gpu']} | " + f"{item['cpu']['p95Us']}/{item['cpu']['p99Us']} us | " + f"{item['gpu']['p95Us']}/{item['gpu']['p99Us']} us |" + ) + lines += [ "", "## Promotion Evidence Gate", @@ -2023,13 +2192,22 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--cases", default="", help="Comma-separated automated safe case ids to run. Defaults to all cases.") parser.add_argument("--timeout", type=int, default=60, help="Per-case timeout in seconds.") parser.add_argument("--basepath", default=default_basepath(), help="Quake 4 install/base path. Omit or set empty to skip fs_basepath.") - parser.add_argument("--savepath", default="", help="Save path root. Defaults to /.home.") + parser.add_argument( + "--savepath", + default="", + help="Save path root. Defaults to /savepath for an isolated run.", + ) parser.add_argument("--output-dir", default="", help="Report/output directory. Defaults to /.tmp/renderer-validation/.") parser.add_argument( "--executable", default="", help="Explicit client or launcher to test. Defaults to the host-matching staged client.", ) + parser.add_argument( + "--runtime-dir", + default="", + help="Runtime root for working directory, renderer modules, and fs_devpath. Defaults to /.install.", + ) parser.add_argument( "--skip-official-pak-validation", action="store_true", @@ -2054,10 +2232,6 @@ def main(argv: list[str]) -> int: return 2 requested = set(requested_cases) safe_cases = [case for case in safe_cases if case["id"] in requested] - elif not args.list: - safe_cases = filter_driver_specific_cases(safe_cases) - safe_cases = filter_vulkan_module_cases(safe_cases, root) - if args.list: print("Automated safe cases:") for case in safe_cases: @@ -2089,6 +2263,16 @@ def main(argv: list[str]) -> int: print("\nPerformance regression thresholds:") for item in PERF_REGRESSION_THRESHOLDS: print(f" {item['preset']}: P95 <= {item['p95Ms']} ms, P99 <= {item['p99Ms']} ms - {item['budget']}") + print( + "\nPer-map CPU/GPU budget contract: " + f"{PER_MAP_BUDGET_BINDING['contractId']} ({PER_MAP_BUDGET_BINDING['sha256']})" + ) + for item in PER_MAP_BUDGET_CONTRACT["budgets"]: + print( + f" {item['id']}: {item['map']} {item['backend']} {item['profile']} " + f"CPU {item['cpu']['p95Us']}/{item['cpu']['p99Us']} us, " + f"GPU {item['gpu']['p95Us']}/{item['gpu']['p99Us']} us" + ) print("\nDefault promotion criteria:") for item in DEFAULT_PROMOTION_CRITERIA: print(f" {item['criterion']}: {item['required']}") @@ -2104,11 +2288,18 @@ def main(argv: list[str]) -> int: return 2 else: executable = find_client_executable(root) - savepath = Path(args.savepath).resolve() if args.savepath else root / ".home" - savepath.mkdir(parents=True, exist_ok=True) + runtime_dir = Path(args.runtime_dir).resolve() if args.runtime_dir else root / ".install" + if not runtime_dir.is_dir(): + print(f"runtime directory does not exist: {runtime_dir}", file=sys.stderr) + return 2 + if not requested_cases: + safe_cases = filter_driver_specific_cases(safe_cases) + safe_cases = filter_vulkan_module_cases(safe_cases, runtime_dir) timestamp = time.strftime("%Y%m%d-%H%M%S") output_dir = Path(args.output_dir).resolve() if args.output_dir else root / ".tmp" / "renderer-validation" / timestamp output_dir.mkdir(parents=True, exist_ok=True) + savepath = Path(args.savepath).resolve() if args.savepath else output_dir / "savepath" + savepath.mkdir(parents=True, exist_ok=True) basepath = args.basepath if basepath and not Path(basepath).exists(): @@ -2121,6 +2312,7 @@ def main(argv: list[str]) -> int: result = run_case( root, executable, + runtime_dir, output_dir, savepath, basepath, @@ -2137,6 +2329,7 @@ def main(argv: list[str]) -> int: "generated": time.strftime("%Y-%m-%d %H:%M:%S %z"), "host": f"{platform.system()} {platform.release()} {platform.machine()}", "executable": str(executable), + "runtimeDir": str(runtime_dir), "savepath": str(savepath), "basepath": basepath, "skipOfficialPakValidation": args.skip_official_pak_validation, diff --git a/tools/tests/renderer_vulkan_decal_compatibility.py b/tools/tests/renderer_vulkan_decal_compatibility.py index 33cc721b..89cb1998 100644 --- a/tools/tests/renderer_vulkan_decal_compatibility.py +++ b/tools/tests/renderer_vulkan_decal_compatibility.py @@ -217,7 +217,7 @@ def validate_draw_order_and_state() -> None: "surfaceShader->GetCullType()", "surfaceShader->TestMaterialFlag( MF_POLYGONOFFSET )", "surfaceShader->GetPolygonOffset()", - "VK_SubmitInteraction( &inter )", + "VK_SubmitInteraction( &inter, packedPBROwnerEligible )", "vkCmdSetDepthBiasEnable( interPass.cmd, VK_FALSE )", ), "Vulkan lit decal interaction state", diff --git a/tools/tests/renderer_vulkan_shadow_compatibility.py b/tools/tests/renderer_vulkan_shadow_compatibility.py index 4b91f09c..8ba230ec 100644 --- a/tools/tests/renderer_vulkan_shadow_compatibility.py +++ b/tools/tests/renderer_vulkan_shadow_compatibility.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 -"""Regression contracts for Vulkan shadow ownership and fail-closed safety.""" +"""Regression contracts for Vulkan shadow ownership and graceful fallback.""" from __future__ import annotations +import contextlib import importlib.util +import io +import math import sys import tempfile from pathlib import Path @@ -47,13 +50,26 @@ def require_order(haystack: str, needles: tuple[str, ...], context: str) -> None def braced_body(source: str, marker: str, context: str) -> str: - start = source.find(marker) - if start == -1: - raise AssertionError(f"Missing {marker!r} in {context}") - - opening_brace = source.find("{", start + len(marker)) - if opening_brace == -1: - raise AssertionError(f"Missing opening brace after {marker!r} in {context}") + search_from = 0 + while True: + start = source.find(marker, search_from) + if start == -1: + raise AssertionError(f"Missing function definition {marker!r} in {context}") + + suffix_start = start + len(marker) + marker_brace = marker.rfind("{") + if marker_brace != -1: + opening_brace = start + marker_brace + break + opening_brace = source.find("{", suffix_start) + semicolon = source.find(";", suffix_start) + if opening_brace != -1 and (semicolon == -1 or opening_brace < semicolon): + break + + # Skip forward declarations and continue at the next occurrence of the + # marker. Shadow-cache storage validators intentionally need prototypes + # because cache expiry calls them before their definitions. + search_from = suffix_start depth = 0 for index in range(opening_brace, len(source)): @@ -81,6 +97,794 @@ def load_test_module(relative_path: str, module_name: str) -> ModuleType: return module +def write_rgb_tga(path: Path, width: int, height: int, value: int) -> None: + header = bytearray(18) + header[2] = 2 + header[12:14] = width.to_bytes(2, "little") + header[14:16] = height.to_bytes(2, "little") + header[16] = 24 + pixel = bytes((value, value, value)) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(bytes(header) + pixel * (width * height)) + + +def validate_shadow_effectiveness_image_gate() -> None: + gameplay = load_test_module( + "tools/tests/renderer_gameplay_benchmark.py", + "openq4_renderer_gameplay_shadow_image_contract", + ) + tmp_root = ROOT / ".tmp" + tmp_root.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory( + prefix="shadow-image-contract-", dir=tmp_root + ) as temp_name: + temp = Path(temp_name) + savepath = temp / "save" + actual = savepath / "baseoq4" / "screenshots" / "renderer-bench" / "sp_0.tga" + references = temp / "references" + reference = references / actual.name + write_rgb_tga(actual, 4, 4, 0) + write_rgb_tga(reference, 4, 4, 0) + + identical = gameplay.compare_screenshot_difference_if_requested( + actual, savepath, references, 0.1, 1, "shadow-contract" + ) + if identical.get("status") != "difference-compared" or identical.get("pass"): + raise AssertionError( + "Identical shadows-on/off TGAs must not satisfy the shadow effectiveness gate" + ) + + write_rgb_tga(reference, 4, 4, 255) + parity = gameplay.compare_screenshot_if_requested( + actual, savepath, references, 0.0, 0, True, "shadow-contract" + ) + if parity.get("status") != "compared" or parity.get("pass"): + raise AssertionError( + "A non-identical shared/classic TGA must not satisfy exact parity thresholds" + ) + + different = gameplay.compare_screenshot_difference_if_requested( + actual, savepath, references, 1.0, 48, "shadow-contract" + ) + if ( + different.get("status") != "difference-compared" + or not different.get("pass") + or different.get("differingChannels") != 48 + ): + raise AssertionError( + "A material engine-TGA shadow delta must satisfy the effectiveness gate" + ) + + generated_references = temp / "generated" / "savepaths" + generated_reference = ( + generated_references + / "shadow-contract" + / "baseoq4" + / "screenshots" + / "renderer-bench" + / "sp_0.tga" + ) + write_rgb_tga(generated_reference, 4, 4, 0) + generated_tree = gameplay.compare_screenshot_if_requested( + actual, + savepath, + generated_references, + 0.0, + 0, + True, + "shadow-contract", + ) + if generated_tree.get("status") != "compared" or not generated_tree.get("pass"): + raise AssertionError( + "A reference tree generated by the gameplay runner must be consumable directly" + ) + + missing = gameplay.compare_screenshot_difference_if_requested( + actual, savepath, temp / "missing", 0.1, 1, "shadow-contract" + ) + if missing.get("status") != "missing-difference-reference" or missing.get("pass"): + raise AssertionError("A missing shadows-off TGA reference must fail closed") + + +def validate_shared_interaction_shadow_runtime_contract() -> None: + gameplay = load_test_module( + "tools/tests/renderer_gameplay_benchmark.py", + "openq4_renderer_gameplay_shadow_runtime_contract", + ) + aggregate = ( + "Renderer shared interaction: requested=1 prepared=1 valid=1 views=1 " + "ready=1 fallback=0 lights=1 surfaces=1 primitives=2 draw=2 noop=0 " + "shadow=1/2/2+0 volumes=1+0 maps=2+1 modes=2+0 csm=2 " + "stages=1/0+1/0 receivers=1/1/0 hash=0123456789abcdef status=ready " + "GL=1/0/0 VK=0/0/0" + ) + view = ( + "Renderer shared interaction view[0]: scene=0 pass=1 shadowPass=2/3 " + "mode=hybrid ready=1 failure=none detail=0 drawPacket=-1 light=-1 " + "receiver=-1 stage=-1 lights=1 surfaces=1 primitives=2/2 noop=0 " + "shadow=1/2/2+0 volumes=1+0 maps=2+1 modes=2+0 csm=2 " + "hash=0123456789abcdef " + "GL=1/none/0/2+0/2+0+1+0/2+1 " + "VK=0/none/0/0+0/0+0+0+0/0+0" + ) + maps = ( + "Renderer shared interaction map view[0] light=0 receiver=local " + "mode=hybrid class=projected cascades=3 alias=0 " + "plan=0000000100000001 generation=1 casters=1+1 " + "features=1+0+0+0 hash=1111111111111111\n" + "Renderer shared interaction map view[0] light=0 receiver=global " + "mode=projected class=projected cascades=3 alias=1 " + "plan=0000000100000002 generation=1 casters=1+0 " + "features=1+0+0+0 hash=2222222222222222" + ) + summary = gameplay.extract_summary("\n".join((aggregate, view, maps))) + spec = gameplay.RunSpec( + case_id="synthetic-hybrid", + mode="SP", + map_name="maps/tools/mv2", + budget_map_name="maps/tools/mv2", + purpose="synthetic mapped receiver accounting", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="mapped", + renderer="best", + render_api="gl", + interaction_expectation="owned", + interaction_shadow_expectation="hybrid", + ) + failures = gameplay.evaluate_shared_interaction_evidence(spec, summary) + if failures: + raise AssertionError( + "Complete hybrid map/stencil runtime evidence must pass: " + f"{failures!r}" + ) + + bad_backend = dict(summary) + bad_backend["sharedInteractionView"] = view.replace( + "/2+1 VK=", "/0+0 VK=" + ) + failures = gameplay.evaluate_shared_interaction_evidence(spec, bad_backend) + if not any("execution=" in failure for failure in failures): + raise AssertionError( + "Mapped backend pass-count mismatch must fail exact execution reconciliation" + ) + + malformed_shadow = dict(summary) + malformed_shadow["sharedInteraction"] = aggregate.replace( + "shadow=1/2/2+0 volumes=1+0", "shadow=malformed volumes=malformed" + ) + failures = gameplay.evaluate_shared_interaction_evidence(spec, malformed_shadow) + if not any("shadow/volume counters=missing" in failure for failure in failures): + raise AssertionError( + "Malformed shadow counters must fail closed without raising during backend reconciliation" + ) + + duplicate_map = dict(summary) + duplicate_map["sharedInteractionMaps"] = maps + "\n" + maps.splitlines()[0] + failures = gameplay.evaluate_shared_interaction_evidence(spec, duplicate_map) + if not any("per-map reconciliation=" in failure for failure in failures): + raise AssertionError( + "Duplicate per-map evidence must fail exact pass identity reconciliation" + ) + + dynamic_spec = gameplay.RunSpec( + case_id="synthetic-dynamic", + mode="SP", + map_name="game/airdefense2", + budget_map_name="game/airdefense2", + purpose="synthetic dynamic mapped-caster evidence", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="csm", + renderer="best", + render_api="gl", + interaction_expectation="owned", + interaction_shadow_expectation="dynamic", + ) + failures = gameplay.evaluate_shared_interaction_evidence(dynamic_spec, summary) + if not any("dynamic caster feature missing=" in failure for failure in failures): + raise AssertionError( + "A dynamic target must fail unless a sealed map pass reports dynamic=1" + ) + + perforated_spec = gameplay.RunSpec( + case_id="synthetic-perforated", + mode="SP", + map_name="game/storage2", + budget_map_name="game/storage2", + purpose="synthetic perforated mapped-caster evidence", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="csm", + renderer="best", + render_api="gl", + interaction_expectation="owned", + interaction_shadow_expectation="perforated", + ) + failures = gameplay.evaluate_shared_interaction_evidence(perforated_spec, summary) + if not any("perforated caster feature missing=" in failure for failure in failures): + raise AssertionError( + "A perforated target must fail unless a sealed map pass reports alpha=1" + ) + + feature_summary = dict(summary) + feature_summary["sharedInteractionMaps"] = maps.replace( + "features=1+0+0+0", "features=1+1+1+0" + ) + for feature_spec in (dynamic_spec, perforated_spec): + failures = gameplay.evaluate_shared_interaction_evidence( + feature_spec, feature_summary + ) + if failures: + raise AssertionError( + "Explicit dynamic/alpha per-map evidence must satisfy its stock " + f"target: {feature_spec.interaction_shadow_expectation} {failures!r}" + ) + + zero_shadow_summary = dict(summary) + zero_shadow_summary["sharedInteraction"] = aggregate.replace( + "shadow=1/2/2+0", "shadow=0/0/0+0" + ) + zero_shadow_summary["sharedInteractionView"] = ( + view.replace("shadow=1/2/2+0", "shadow=0/0/0+0") + .replace("/2+0+1+0/2+1 VK=", "/0+0+1+0/2+1 VK=") + ) + failures = gameplay.evaluate_shared_interaction_evidence( + spec, zero_shadow_summary + ) + if not any("mapped caster accounting=" in failure for failure in failures): + raise AssertionError( + "Per-map caster records must not reconcile against zero aggregate shadow casters" + ) + + dual_backend_summary = dict(summary) + dual_backend_summary["sharedInteraction"] = aggregate.replace( + "VK=0/0/0", "VK=1/0/0" + ) + dual_backend_summary["sharedInteractionView"] = view.replace( + "VK=0/none/0/0+0/0+0+0+0/0+0", + "VK=1/none/0/2+0/2+0+1+0/2+1", + ) + failures = gameplay.evaluate_shared_interaction_evidence( + spec, dual_backend_summary + ) + if not any("inactive backend" in failure for failure in failures): + raise AssertionError( + "A selected GL run must reject simultaneous Vulkan ownership and execution" + ) + + mixed_aggregate = ( + "Renderer shared interaction: requested=1 prepared=1 valid=1 views=1 " + "ready=1 fallback=0 lights=1 surfaces=1 primitives=1 draw=1 noop=0 " + "shadow=1/1/1+0 volumes=1+0 maps=1+0 modes=1+0 csm=0 " + "stages=1/0+1/0 receivers=1/0/0 hash=0123456789abcdef status=ready " + "GL=1/0/0 VK=0/0/0" + ) + mixed_view = ( + "Renderer shared interaction view[0]: scene=0 pass=1 shadowPass=2/3 " + "mode=hybrid ready=1 failure=none detail=0 drawPacket=-1 light=-1 " + "receiver=-1 stage=-1 lights=1 surfaces=1 primitives=1/1 noop=0 " + "shadow=1/1/1+0 volumes=1+0 maps=1+0 modes=1+0 csm=0 " + "hash=0123456789abcdef " + "GL=1/none/0/1+0/1+0+1+0/1+0 " + "VK=0/none/0/0+0/0+0+0+0/0+0" + ) + mixed_map = ( + "Renderer shared interaction map view[0] light=0 receiver=local " + "mode=projected class=projected cascades=1 alias=0 " + "plan=0000000100000001 generation=1 casters=1+0 " + "features=1+0+0+0 hash=1111111111111111" + ) + mixed_spec = gameplay.RunSpec( + case_id="synthetic-mixed", + mode="SP", + map_name="maps/tools/mv2", + budget_map_name="maps/tools/mv2", + purpose="synthetic mixed-light identity", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="mixed", + renderer="best", + render_api="gl", + interaction_expectation="owned", + interaction_shadow_expectation="mixed", + ) + failures = gameplay.evaluate_shared_interaction_evidence( + mixed_spec, + gameplay.extract_summary("\n".join((mixed_aggregate, mixed_view, mixed_map))), + ) + if not any("map/stencil mix=" in failure for failure in failures): + raise AssertionError( + "Mixed ownership must include a stencil shadow light outside the mapped-light identities" + ) + + fallback_aggregate = ( + "Renderer shared interaction: requested=1 prepared=1 valid=0 views=1 " + "ready=0 fallback=1 lights=0 surfaces=0 primitives=0 draw=0 noop=0 " + "shadow=0/0/0+0 volumes=0+0 maps=0+0 modes=0+0 csm=0 " + "stages=0/0+0/0 receivers=0/0/0 hash=0000000000000000 status=fallback " + "GL=0/1/0 VK=0/0/0" + ) + translucent_view = ( + "Renderer shared interaction view[0]: scene=0 pass=1 shadowPass=2/3 " + "mode=none ready=0 failure=shadowMap detail=6 drawPacket=-1 light=0 " + "receiver=-1 stage=-1 lights=0 surfaces=0 primitives=0/0 noop=0 " + "shadow=0/0/0+0 volumes=0+0 maps=0+0 modes=0+0 csm=0 " + "hash=0000000000000000 " + "GL=2/shadowMap/6/0+0/0+0+0+0/0+0 " + "VK=0/none/0/0+0/0+0+0+0/0+0" + ) + translucent_spec = gameplay.RunSpec( + case_id="synthetic-translucent", + mode="SP", + map_name="game/medlabs", + budget_map_name="game/medlabs", + purpose="synthetic translucent-moment fallback", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="translucent", + renderer="best", + render_api="gl", + interaction_expectation="fallback", + interaction_shadow_expectation="translucent-fallback", + ) + translucent_summary = gameplay.extract_summary( + "\n".join((fallback_aggregate, translucent_view)) + ) + failures = gameplay.evaluate_shared_interaction_evidence( + translucent_spec, translucent_summary + ) + if failures: + raise AssertionError( + f"Exact translucent shadow-chain fallback must pass: {failures!r}" + ) + wrong_fallback_summary = dict(translucent_summary) + wrong_fallback_summary["sharedInteractionView"] = ( + translucent_view.replace("failure=shadowMap detail=6", "failure=deform detail=13") + .replace("GL=2/shadowMap/6/", "GL=2/deform/13/") + ) + failures = gameplay.evaluate_shared_interaction_evidence( + translucent_spec, wrong_fallback_summary + ) + if not any( + "translucent-moment fallback=" in failure for failure in failures + ): + raise AssertionError( + "An unrelated named fallback must not satisfy translucent-moment acceptance" + ) + + budget_aggregate = ( + aggregate.replace( + "lights=1 surfaces=1 primitives=2 draw=2", + "lights=4 surfaces=4 primitives=4 draw=4", + ) + .replace( + "shadow=1/2/2+0 volumes=1+0 maps=2+1 modes=2+0 csm=2", + "shadow=4/4/4+0 volumes=0+0 maps=4+0 modes=1+3 csm=0", + ) + .replace("GL=1/0/0 VK=0/0/0", "GL=0/1/0 VK=0/0/0") + ) + budget_view = ( + view.replace( + "lights=1 surfaces=1 primitives=2/2", + "lights=4 surfaces=4 primitives=4/4", + ) + .replace( + "shadow=1/2/2+0 volumes=1+0 maps=2+1 modes=2+0 csm=2", + "shadow=4/4/4+0 volumes=0+0 maps=4+0 modes=1+3 csm=0", + ) + .replace( + "GL=1/none/0/2+0/2+0+1+0/2+1", + "GL=2/backendRejected/-1892967296/0+0/0+0+0+0/0+0", + ) + ) + budget_maps = ( + "Renderer shared interaction map view[0] light=0 receiver=local " + "mode=projected class=projected cascades=1 alias=0 " + "plan=0000000100000001 generation=1 casters=1+0 " + "features=1+0+0+0 hash=1111111111111111\n" + "Renderer shared interaction map view[0] light=1 receiver=local " + "mode=point class=point cascades=6 alias=0 " + "plan=0000000100000002 generation=1 casters=1+0 " + "features=1+0+0+0 hash=2222222222222222\n" + "Renderer shared interaction map view[0] light=2 receiver=local " + "mode=point class=point cascades=6 alias=0 " + "plan=0000000100000003 generation=1 casters=1+0 " + "features=1+0+0+0 hash=3333333333333333\n" + "Renderer shared interaction map view[0] light=3 receiver=local " + "mode=point class=point cascades=6 alias=0 " + "plan=0000000100000004 generation=1 casters=1+0 " + "features=1+0+0+0 hash=4444444444444444" + ) + budget_spec = gameplay.RunSpec( + case_id="synthetic-map-budget", + mode="SP", + map_name="maps/tools/mv2", + budget_map_name="maps/tools/mv2", + purpose="synthetic map-admission fallback", + path_name="spawn-static", + tier="auto", + maxfps="240", + swap_interval="0", + display_mode="windowed", + shadow_preset="map-budget-fallback", + renderer="best", + render_api="gl", + interaction_expectation="fallback", + interaction_shadow_expectation="map-budget-fallback", + ) + budget_summary = gameplay.extract_summary( + "\n".join((budget_aggregate, budget_view, budget_maps)) + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, budget_summary + ) + if failures: + raise AssertionError( + f"Exact map-transaction admission fallback must pass: {failures!r}" + ) + wrong_budget_summary = dict(budget_summary) + wrong_budget_summary["sharedInteractionView"] = budget_view.replace( + "-1892967296", "1801000001" + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, wrong_budget_summary + ) + if not any( + "map-budget admission fallback=" in failure for failure in failures + ): + raise AssertionError( + "An unrelated shadow-cache rejection must not satisfy map-admission fallback acceptance" + ) + + wrong_budget_light_summary = dict(budget_summary) + wrong_budget_light_summary["sharedInteractionView"] = budget_view.replace( + "-1892967296", "-1893967296" + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, wrong_budget_light_summary + ) + if not any( + "map-budget admission fallback=" in failure for failure in failures + ): + raise AssertionError( + "A map-transaction rejection on the wrong controlled light must not satisfy the exact budget target" + ) + + duplicate_budget_maps = dict(budget_summary) + budget_map_lines = budget_maps.splitlines() + duplicate_budget_maps["sharedInteractionMaps"] = "\n".join( + (*budget_map_lines[:-1], budget_map_lines[0]) + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, duplicate_budget_maps + ) + if not any("fallback mapped plan=" in failure for failure in failures): + raise AssertionError( + "Duplicate map identities must not satisfy a prepared map-budget fallback plan" + ) + + zero_budget_shadow = dict(budget_summary) + zero_budget_shadow["sharedInteraction"] = budget_aggregate.replace( + "shadow=4/4/4+0", "shadow=0/0/0+0" + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, zero_budget_shadow + ) + if not any( + "fallback mapped caster accounting=" in failure for failure in failures + ): + raise AssertionError( + "Prepared budget maps must not reconcile against zero shadow-light/caster accounting" + ) + + undercounted_budget_shadow = dict(budget_summary) + undercounted_budget_shadow["sharedInteraction"] = budget_aggregate.replace( + "shadow=4/4/4+0", "shadow=4/1/1+0" + ) + failures = gameplay.evaluate_shared_interaction_evidence( + budget_spec, undercounted_budget_shadow + ) + if not any( + "fallback mapped caster accounting=" in failure for failure in failures + ): + raise AssertionError( + "Per-light mapped caster minima must not exceed aggregate caster accounting" + ) + + +def validate_shared_interaction_shadow_profile_contract() -> None: + gameplay = load_test_module( + "tools/tests/renderer_gameplay_benchmark.py", + "openq4_renderer_gameplay_shadow_profile_contract", + ) + + interaction_args = gameplay.parse_args( + ["--profile", "interaction", "--pacing-only", "--dry-run"] + ) + interaction_specs = gameplay.build_specs(interaction_args) + interaction_contract = [ + ( + spec.case_id, + spec.shadow_preset, + spec.interaction_expectation, + spec.interaction_shadow_expectation, + ) + for spec in interaction_specs + ] + expected_interaction_contract = [ + ("sp-mv2-interaction", "unshadowed", "owned", "none"), + ("sp-mv2-interaction", "stencil", "owned", "stencil"), + ("sp-mv2-interaction", "mapped", "owned", "mapped"), + ("sp-mv2-interaction", "mixed", "owned", "mixed"), + ( + "sp-mv2-interaction", + "map-budget-fallback", + "fallback", + "map-budget-fallback", + ), + ] + if interaction_contract != expected_interaction_contract: + raise AssertionError( + "Controlled interaction profile must cover unshadowed, stencil, " + "mapped, mixed, and atomic map-admission fallback exactly: " + f"{interaction_contract!r}" + ) + + stock_args = gameplay.parse_args( + ["--profile", "interaction-shadow-stock", "--pacing-only", "--dry-run"] + ) + if ("g_stopTime", "1") not in stock_args.launch_cvars: + raise AssertionError( + "Stock interaction-shadow startup must begin from deterministic tic zero" + ) + if ("ui_showGun", "0") not in stock_args.launch_cvars: + raise AssertionError( + "Stock interaction-shadow captures must exclude unsupported viewmodel interactions" + ) + if ("r_multiSamples", "0") not in stock_args.launch_cvars: + raise AssertionError( + "Stock interaction-shadow captures must disable multisampled game targets" + ) + direct_target_cvars = { + "g_renderFastNoPost": "1", + "g_renderFastNoPostDirect": "1", + "g_renderCasUpscale": "0", + "r_postAA": "0", + "r_screenFraction": "100", + "r_bloom": "0", + "r_motionBlur": "0", + "r_ssao": "0", + "r_hdrToneMap": "0", + "r_hdrDebugView": "0", + } + stock_extra_cvars = dict(stock_args.extra_cvars) + if any( + stock_extra_cvars.get(name) != value + for name, value in direct_target_cvars.items() + ): + raise AssertionError( + "Stock interaction-shadow captures must make the direct root target deterministic" + ) + if ("g_stopTime", "0") not in stock_args.extra_cvars: + raise AssertionError( + "Stock interaction-shadow scenes must advance during their settle interval" + ) + if "g_stopTime 1" not in stock_args.exec_commands: + raise AssertionError( + "Stock interaction-shadow scenes must freeze again before sampling" + ) + stock_specs = gameplay.build_specs(stock_args) + stock_contract = [ + ( + spec.case_id, + spec.shadow_preset, + spec.interaction_expectation, + spec.interaction_shadow_expectation, + ) + for spec in stock_specs + ] + expected_stock_contract = [ + ("shadow-projected-airdefense2", "unshadowed", "owned", "none"), + ("shadow-projected-airdefense2", "mapped", "owned", "projected"), + ("shadow-point-airdefense2", "unshadowed", "owned", "none"), + ("shadow-point-airdefense2", "mapped", "owned", "point"), + ("shadow-csm-airdefense1", "unshadowed", "owned", "none"), + ("shadow-csm-airdefense1", "csm", "owned", "csm"), + ("shadow-character-airdefense2", "unshadowed", "owned", "none"), + ("shadow-character-airdefense2", "csm", "owned", "dynamic"), + ("shadow-cutout-storage2", "unshadowed", "owned", "none"), + ("shadow-cutout-storage2", "csm", "owned", "perforated"), + ("shadow-hybrid-storage2", "unshadowed", "owned", "none"), + ("shadow-hybrid-storage2", "mapped", "owned", "hybrid"), + ( + "shadow-translucent-medlabs", + "translucent", + "fallback", + "translucent-fallback", + ), + ] + if stock_contract != expected_stock_contract: + raise AssertionError( + "Stock interaction-shadow profile must cover projected, point, CSM, " + "dynamic, perforated, same-light hybrid, and translucent-moment " + f"fallback exactly: {stock_contract!r}" + ) + + for spec in (*interaction_specs, *stock_specs): + unshadowed_id = spec.id_for_shadow_preset("unshadowed") + if spec.shadow_preset == "unshadowed" and unshadowed_id != spec.id: + raise AssertionError( + "Unshadowed runs must use their own capture id as the image-difference reference key" + ) + if spec.shadow_preset != "unshadowed" and unshadowed_id == spec.id: + raise AssertionError( + "Shadowed runs must resolve image-difference evidence through the matching unshadowed capture id" + ) + + with contextlib.redirect_stderr(io.StringIO()): + try: + gameplay.parse_args( + [ + "--profile", + "interaction", + "--pacing-only", + "--require-references", + ] + ) + except SystemExit as exc: + if exc.code != 2: + raise + else: + raise AssertionError( + "--require-references without --reference-dir must fail during argument validation" + ) + + shadows_off_args = gameplay.parse_args( + [ + "--profile", + "interaction", + "--pacing-only", + "--shadow-presets", + "map-budget-fallback", + "--set-cvar", + "r_shadows=0", + "--set-cvar", + "r_useShadowMap=0", + ] + ) + shadows_off_specs = gameplay.build_specs(shadows_off_args) + if any( + spec.interaction_expectation != "owned" + or spec.interaction_shadow_expectation != "none" + for spec in shadows_off_specs + ): + raise AssertionError( + "Shadows-off reference generation must not retain a shadow fallback expectation" + ) + + matrix = load_test_module( + "tools/tests/renderer_validation_matrix.py", + "openq4_renderer_validation_shadow_profile_contract", + ) + expected_matrix_keys = { + (profile, backend) + for profile in ("interaction", "interaction-shadow-stock") + for backend in ("gl", "vk") + } + + def interaction_matrix_failures(entries: list[dict[str, object]]) -> list[str]: + indexed: dict[tuple[object, object], dict[str, object]] = {} + failures: list[str] = [] + for entry in entries: + profile = entry.get("profile") + if profile not in ("interaction", "interaction-shadow-stock"): + continue + key = (profile, entry.get("renderApi")) + if key in indexed: + failures.append(f"duplicate {key[0]}/{key[1]}") + continue + indexed[key] = entry + + for profile, backend in sorted(expected_matrix_keys - set(indexed)): + failures.append(f"missing {profile}/{backend}") + for profile, backend in sorted( + set(indexed) - expected_matrix_keys, + key=lambda item: (str(item[0]), str(item[1])), + ): + failures.append(f"unexpected {profile}/{backend}") + + for profile, backend in sorted(expected_matrix_keys & set(indexed)): + command = str(indexed[(profile, backend)].get("command", "")) + for required_option in ( + f"--render-api {backend}", + "--reference-dir", + "--require-references", + "--image-rms-threshold 0", + "--image-max-threshold 0", + "--difference-reference-dir", + ): + if required_option not in command: + failures.append( + f"{profile}/{backend} missing option {required_option}" + ) + if ( + f"{profile}\\{backend}\\classic\\savepaths" not in command + or f"{profile}\\{backend}\\shadows-off\\savepaths" + not in command + ): + failures.append( + f"{profile}/{backend} references are not backend-scoped savepaths" + ) + return failures + + matrix_entries = list(matrix.GAMEPLAY_BENCHMARK_HARNESS) + matrix_failures = interaction_matrix_failures(matrix_entries) + if matrix_failures: + raise AssertionError( + "Renderer validation matrix must register exactly one GL and Vulkan " + "gate for each interaction profile: " + "; ".join(matrix_failures) + ) + + missing_fixture = [ + entry + for entry in matrix_entries + if (entry.get("profile"), entry.get("renderApi")) + != ("interaction", "gl") + ] + if "missing interaction/gl" not in interaction_matrix_failures(missing_fixture): + raise AssertionError( + "Interaction matrix guard must reject a missing profile/backend pair" + ) + + duplicate_fixture = list(matrix_entries) + duplicate_fixture.append( + dict( + next( + entry + for entry in matrix_entries + if (entry.get("profile"), entry.get("renderApi")) + == ("interaction", "gl") + ) + ) + ) + if "duplicate interaction/gl" not in interaction_matrix_failures( + duplicate_fixture + ): + raise AssertionError( + "Interaction matrix guard must reject a duplicate profile/backend pair" + ) + + foundation = next( + case + for case in matrix.build_safe_cases(("auto", "gl33", "gl43", "gl45")) + if case["id"] == "renderer-foundation-selftests" + ) + foundation_args = foundation["args"] + if not any( + foundation_args[index : index + 3] == ["+set", "r_renderApi", "gl"] + for index in range(len(foundation_args) - 2) + ) or not foundation.get("preservesConfig"): + raise AssertionError( + "Foundation self-tests must force OpenGL without persisting the archived API choice" + ) + + def validate_runtime_failure_gates() -> None: gameplay = load_test_module( "tools/tests/renderer_gameplay_benchmark.py", @@ -92,6 +896,8 @@ def validate_runtime_failure_gates() -> None: ) signature_lines = { + "binaryImageCacheWrite": "^3WARNING: ^1idBinaryImage: Could not open generated cache 'generated/images/fixture.bimage' or compact fallback 'generated/images/_compact/v1/fixture.bimage'", + "expandedLoadscreenPublish": "^3WARNING: ^1Could not publish expanded loading background 'guis/assets/generated/loadscreens/fixture_1820x1024.tga'; using the source levelshot", "vulkanValidation": "Vulkan validation: descriptor binding mismatch", "vulkanVuid": "Validation ID VUID-vkCmdDrawIndexed-commandBuffer-recording", "vulkanCallFailed": "Vulkan: vkCreateGraphicsPipelines failed (-3)", @@ -101,6 +907,18 @@ def validate_runtime_failure_gates() -> None: failure_text = "\n".join(signature_lines.values()) native_fatal = "FATAL: renderer bootstrap stopped" benign_vk_result = "Vulkan: swapchain returned VK_ERROR_OUT_OF_DATE_KHR; retry scheduled" + legacy_binary_image_warning = ( + "^3WARNING: ^1idBinaryImage: Could not open file " + "'generated/images/legacy-fixture.bimage'" + ) + binary_image_near_miss = ( + "idBinaryImage: Could not open generated cache 'generated/images/fixture.bimage' " + "or compact fallback 'generated/images/_compact/v1/fixture.bimage'" + ) + expanded_loadscreen_near_miss = ( + "Could not publish expanded loading background " + "'guis/assets/generated/loadscreens/fixture_1820x1024.tga'; using the source levelshot" + ) for module, counter_name, context in ( (gameplay, "warning_counts", "gameplay benchmark"), @@ -150,6 +968,18 @@ def validate_runtime_failure_gates() -> None: raise AssertionError( f"{context} must preserve the engine-native FATAL: diagnostic line" ) + if counter(legacy_binary_image_warning).get("binaryImageCacheWrite") != 1: + raise AssertionError( + f"{context} must retain the legacy binary-image cache warning signature" + ) + if counter(binary_image_near_miss).get("binaryImageCacheWrite") != 0: + raise AssertionError( + f"{context} must require WARNING: for binary-image cache write failures" + ) + if counter(expanded_loadscreen_near_miss).get("expandedLoadscreenPublish") != 0: + raise AssertionError( + f"{context} must require WARNING: for expanded-loadscreen publication failures" + ) checks_ok, missing = matrix.evaluate_checks( failure_text, @@ -189,12 +1019,15 @@ def validate_runtime_failure_gates() -> None: screenshot_path.parent.mkdir(parents=True) stdout_path.write_text("", encoding="utf-8") stderr_path.write_text("", encoding="utf-8") - screenshot_path.write_bytes(b"source-contract") + write_rgb_tga( + screenshot_path, gameplay.BUDGET_WIDTH, gameplay.BUDGET_HEIGHT, 0 + ) spec = gameplay.RunSpec( case_id="vulkan-shadow-contract", mode="SP", map_name="game/contract", + budget_map_name="game/contract", purpose="synthetic failure-gate coverage", path_name="spawn-static", tier="auto", @@ -203,11 +1036,14 @@ def validate_runtime_failure_gates() -> None: display_mode="windowed", shadow_preset="mapped", renderer="vk", + render_api="vk", ) def evaluate(log_body: str) -> dict[str, object]: log_path.write_text( - f"Selected renderer tier: Vulkan\n{log_body}\n", + "Selected renderer tier: Vulkan\n" + f"MODE: -1, {gameplay.BUDGET_WIDTH} x {gameplay.BUDGET_HEIGHT} " + f"windowed hz:N/A\n{log_body}\n", encoding="utf-8", ) return gameplay.evaluate_role_result( @@ -982,8 +1818,10 @@ def validate_exact_filter_tiers(sample_body: str, sample_call: str, context: str raise AssertionError(f"{context} is missing the center sample that starts its tiered kernel") if sample_body[tier_start:].count(sample_call) != 13: raise AssertionError(f"{context} must issue exactly 13 samples at its maximum tier") - if sample_body.count("RotateShadowOffset(") != 12: - raise AssertionError(f"{context} must rotate exactly the twelve off-center Poisson taps") + if sample_body.count("rotation * vec2(") != 12: + raise AssertionError( + f"{context} must transform exactly the twelve off-center Poisson taps" + ) require_order( sample_body, ( @@ -996,222 +1834,2127 @@ def validate_exact_filter_tiers(sample_body: str, sample_call: str, context: str "return result * (1.0 / 9.0);", "return result * (1.0 / 13.0);", ), - f"{context} exact 1/5/9/13 tiers", + f"{context} exact 1/5/9/13 tiers", + ) + + +def validate_shadow_filtering_contract() -> None: + classification = read("src/renderer/ShadowMapClassification.cpp") + shared_settings = braced_body( + classification, + "shadowMapProjectedFilterSettings_t R_ShadowMapProjectedFilterSettings(", + "shared projected shadow filter policy", + ) + require_order( + shared_settings, + ( + "const shadowMapLightClassification_t classification = R_ClassifyShadowMapLight( vLight );", + "settings.filterScale = settings.distantSource", + "r_shadowMapDistantFilterScale.GetFloat()", + "settings.filterRadius = Max( 0.0f, r_shadowMapFilterRadius.GetFloat() ) * settings.filterScale;", + "settings.filterTaps = idMath::ClampInt( 1, 13, r_shadowMapFilterTaps.GetInteger() );", + "settings.filterMode = idMath::ClampInt( 0, 2, r_shadowMapFilterMode.GetInteger() );", + "settings.pcssLightRadius = Max( 0.0f, r_shadowMapPCSSLightRadius.GetFloat() ) * settings.filterScale;", + "settings.pcssMaxRadius = Max( 0.0f, r_shadowMapPCSSMaxRadius.GetFloat() ) * settings.filterScale;", + "if ( settings.filterMode == 2 )", + "Max( settings.pcssLightRadius, settings.pcssMaxRadius )", + ), + "shared projected shadow filter policy", + ) + + interactions = read("src/renderer/Vulkan/vk_Interactions.cpp") + shadow_slice = braced_body( + interactions, + "static int VK_Inter_WriteShadowSlice(", + "Vulkan shadow filter parameter upload", + ) + require_order( + shadow_slice, + ( + "r_shadowMapPointFilterRadius.GetFloat()", + "idMath::ClampInt( 1, 13, r_shadowMapPointFilterTaps.GetInteger() )", + "idMath::ClampInt( 0, 1, r_shadowMapPointFilterMode.GetInteger() )", + "r_shadowMapPointDepthCompare.GetBool() ? 1.0f : 0.0f", + "R_ShadowMapProjectedFilterSettings( state->vLight )", + "block.filterParams[ 0 ] = filterSettings.filterRadius;", + "block.filterParams[ 1 ] = (float)filterSettings.filterTaps;", + "block.filterParams[ 2 ] = (float)filterSettings.filterMode;", + "r_shadowMapDepthCompare.GetBool() && filterSettings.filterMode != 2", + "block.pcssParams[ 0 ] = filterSettings.pcssLightRadius;", + "block.pcssParams[ 1 ] = filterSettings.pcssMaxRadius;", + "block.pcssParams[ 2 ] = filterSettings.effectiveFilterRadius;", + "r_shadowMapReceiverPlaneBias.GetBool() ? 1.0f : 0.0f", + ), + "shared projected and point runtime filter upload", + ) + + projected = read("src/renderer/Vulkan/shaders/interaction_shadow.frag") + projected_compare = braced_body( + projected, + "float SampleShadowCompare(", + "projected runtime depth comparison", + ) + require_order( + projected_compare, + ( + "float compareDepth = depth - ShadowReceiverBias(cascadeIndex);", + "if (shadow.filterParams.w > 0.5)", + "texture(shadowCompareMap, vec3(uv, compareDepth))", + "texture(shadowRawMap, uv).r", + "compareDepth <= storedDepth ? 1.0 : 0.0", + ), + "projected runtime compare/raw selection", + ) + + projected_rotation = braced_body( + projected, + "mat2 ShadowOffsetRotation(", + "projected stable Poisson rotation", + ) + require_order( + projected_rotation, + ( + "if (shadow.filterParams.z < 0.5)", + "StableShadowHash(vec3(", + "floor(uv / max(shadow.texelSize.x, 1.0e-6))", + "floor(depth * 1024.0)", + "* 6.2831853", + "return mat2(c, s, -s, c);", + ), + "projected stable Poisson rotation", + ) + + receiver_bias = braced_body( + projected, + "float ShadowReceiverBias(", + "projected derivative receiver bias", + ) + require_order( + receiver_bias, + ( + "float receiverPlaneBias = 0.0;", + "if (shadow.pcssParams.w > 0.5)", + "ShadowDepthGradient(cascadeIndex)", + "max(shadow.pcssParams.z, 1.0)", + "max(max(texelBias, receiverPlaneBias), 0.0)", + ), + "projected derivative receiver bias", + ) + + blocker_search = braced_body( + projected, + "float ProjectedPCSSRadius(", + "projected PCSS blocker search", + ) + require_order( + blocker_search, + ( + "shadow.filterParams.z < 1.5 || shadow.filterParams.w > 0.5", + "float compareDepth = depth - ShadowReceiverBias(cascadeIndex);", + "float blockerDepth = 0.0;", + "float blockerCount = 0.0;", + "float d0 = RawShadowDepth(uv);", + "float d1 = RawShadowDepth(", + "float d2 = RawShadowDepth(", + "float d3 = RawShadowDepth(", + "float d4 = RawShadowDepth(", + "float d5 = RawShadowDepth(", + "float d6 = RawShadowDepth(", + "float d7 = RawShadowDepth(", + "float d8 = RawShadowDepth(", + "if (blockerCount <= 0.0)", + "return 0.0;", + "float averageBlocker = blockerDepth / blockerCount;", + "float separation = max(compareDepth - averageBlocker, 0.0);", + "float penumbra = separation / max(averageBlocker, 1.0e-4);", + "return clamp(max(baseRadius, penumbra * shadow.pcssParams.x), baseRadius, maxRadius);", + ), + "projected PCSS blocker search", + ) + if blocker_search.count("RawShadowDepth(") != 9: + raise AssertionError("Projected PCSS blocker search must use its fixed nine raw-depth probes") + require(blocker_search, "mat2 rotation", "projected PCSS shared kernel rotation") + if blocker_search.count("ShadowOffsetRotation(") != 0: + raise AssertionError("Projected PCSS must reuse the caller's stable rotation") + + projected_samples = braced_body( + projected, + "float SampleShadowCascade(", + "projected Poisson shadow filter", + ) + validate_exact_filter_tiers( + projected_samples, + "SampleShadowCompare(", + "projected Poisson shadow filter", + ) + if projected_samples.count("ShadowOffsetRotation(") != 1: + raise AssertionError("Projected blocker search and PCF taps must share one stable rotation") + + projected_main = braced_body(projected, "void main()", "projected shadow fragment main") + if projected_main.count("dFdx(") != 4 or projected_main.count("dFdy(") != 4: + raise AssertionError( + "Projected receiver-plane bias must derive all four cascade depths before selection" + ) + require_order( + projected_main, + ( + "if (shadow.pcssParams.w > 0.5)", + "gShadowDepthGradients = vec4(", + "abs(dFdx(vShadowCoord0.z)) + abs(dFdy(vShadowCoord0.z))", + "abs(dFdx(vShadowCoord3.z)) + abs(dFdy(vShadowCoord3.z))", + "light *= SampleShadowFactor();", + ), + "projected derivative calculation before divergent cascade sampling", + ) + + point = read("src/renderer/Vulkan/shaders/interaction_shadow_point.frag") + point_compare = braced_body( + point, + "float SamplePointShadowCompare(", + "point runtime depth comparison", + ) + require_order( + point_compare, + ( + "float compareDepth = depth - ShadowReceiverBias();", + "if (shadow.samplingParams.x > 0.5)", + "texture(shadowCompareMap, vec4(direction, compareDepth))", + "texture(shadowRawMap, direction).r", + "compareDepth <= storedDepth ? 1.0 : 0.0", + ), + "point runtime compare/raw selection", + ) + + point_rotation = braced_body( + point, + "mat2 ShadowOffsetRotation(", + "point stable tangent-disc rotation", + ) + require_order( + point_rotation, + ( + "if (shadow.filterParams.z < 0.5)", + "StableShadowHash(floor(direction * 37.0))", + "* 6.2831853", + "return mat2(c, s, -s, c);", + ), + "point stable tangent-disc rotation", + ) + + point_samples = braced_body( + point, + "float SampleShadowFactor()", + "point tangent-disc shadow filter", + ) + require_order( + point_samples, + ( + "vec3 direction = SafeNormalize(vPointShadowVector);", + "vec3 tangent = SafeNormalize(cross(up, direction));", + "vec3 bitangent = cross(direction, tangent);", + "float tap = shadow.filterParams.w * filterRadius;", + "tangent * o1.x + bitangent * o1.y", + ), + "point tangent-disc sampling basis", + ) + validate_exact_filter_tiers( + point_samples, + "SamplePointShadowCompare(", + "point tangent-disc shadow filter", + ) + if point_samples.count("ShadowOffsetRotation(") != 1: + raise AssertionError("Point PCF taps must share one stable rotation") + + # The shipped OpenGL 1.10 programs are manually maintained counterparts, + # not products of the Vulkan SPIR-V header generator. Keep their blocker, + # filter-tier, and rotation contracts pinned independently so one backend + # cannot silently drift or compile with a different kernel. + gl_projected = read("content/baseoq4/pak0/glprogs/shadow_interaction.fs") + gl_blocker_search = braced_body( + gl_projected, + "float ProjectedPCSSRadius(", + "OpenGL projected PCSS blocker search", + ) + require_order( + gl_blocker_search, + ( + "float compareDepth = depth - ShadowReceiverBias( cascadeIndex, depth );", + "float d0 = RawShadowDepth( uv );", + "float d1 = RawShadowDepth(", + "float d8 = RawShadowDepth(", + "if ( blockerCount <= 0.0 )", + "return 0.0;", + "float separation = max( compareDepth - averageBlocker, 0.0 );", + ), + "OpenGL bias-consistent nine-probe PCSS search", + ) + if gl_blocker_search.count("RawShadowDepth(") != 9: + raise AssertionError("OpenGL PCSS blocker search must use exactly nine raw-depth probes") + if gl_blocker_search.count("rotation * vec2(") != 8: + raise AssertionError("OpenGL PCSS blocker search must use eight unique off-center probes") + if gl_blocker_search.count("ShadowOffsetRotation(") != 0: + raise AssertionError("OpenGL PCSS blocker search must reuse the caller's rotation") + for tap_index in range(1, 9): + require_compact( + gl_blocker_search, + f"float d{tap_index} = RawShadowDepth( clamp( uv + o{tap_index} * searchTap, clampMin, clampMax ) );", + f"OpenGL clamped PCSS probe d{tap_index}", + ) + require_compact( + gl_blocker_search, + f"if ( d{tap_index} < compareDepth ) {{ blockerDepth += d{tap_index}; blockerCount += 1.0; }}", + f"OpenGL PCSS probe d{tap_index} contribution", + ) + + gl_projected_samples = braced_body( + gl_projected, + "vec4 SampleShadowCascade(", + "OpenGL projected Poisson shadow filter", + ) + gl_projected_tier_start = gl_projected_samples.find("float shadow = 0.0;") + if gl_projected_tier_start == -1 or gl_projected_samples[gl_projected_tier_start:].count("SampleShadowCompare(") != 13: + raise AssertionError("OpenGL projected PCF must issue exactly 13 samples at its maximum tier") + if gl_projected_samples.count("rotation * vec2(") != 12: + raise AssertionError("OpenGL projected PCF must transform twelve off-center taps") + if gl_projected_samples.count("ShadowOffsetRotation(") != 1: + raise AssertionError("OpenGL blocker search and PCF must share one stable rotation") + require_order( + gl_projected_samples, + ( + "mat2 rotation = ShadowOffsetRotation( uv, depth );", + "ProjectedPCSSRadius( uv, depth, cascadeIndex, clampMin, clampMax, rotation )", + "shadow += SampleShadowCompare( uv, depth, cascadeIndex );", + "if ( uShadowFilterTaps <= 1.0 )", + "if ( uShadowFilterTaps <= 5.0 )", + "if ( uShadowFilterTaps <= 9.0 )", + "shadow * ( 1.0 / 13.0 )", + ), + "OpenGL exact projected filter tiers", + ) + + gl_point = read("content/baseoq4/pak0/glprogs/shadow_point_interaction.fs") + gl_point_samples = braced_body( + gl_point, + "float SamplePointShadow()", + "OpenGL point tangent-disc shadow filter", + ) + if gl_point_samples.count("SamplePointShadowCompare(") != 14: + # One early unfiltered return plus the complete 13-sample tiered kernel. + raise AssertionError("OpenGL point receiver must preserve one direct and thirteen tiered samples") + if gl_point_samples.count("rotation * vec2(") != 12: + raise AssertionError("OpenGL point PCF must transform twelve off-center taps") + if gl_point_samples.count("ShadowOffsetRotation(") != 1: + raise AssertionError("OpenGL point PCF taps must share one stable rotation") + require_order( + gl_point_samples, + ( + "vec3 direction = SafeNormalize( vPointShadowVector );", + "vec3 tangent = SafeNormalize( cross( up, direction ) );", + "vec3 bitangent = cross( direction, tangent );", + "float tap = uPointShadowTexelScale * filterRadius;", + "if ( uShadowFilterTaps <= 1.0 )", + "if ( uShadowFilterTaps <= 5.0 )", + "if ( uShadowFilterTaps <= 9.0 )", + "shadow * ( 1.0 / 13.0 )", + ), + "OpenGL exact point filter tiers", + ) + + +def point_receiver_settings( + far_distance: float, + face_size: int, + constant_bias: float, + normal_bias: float, + texel_bias_scale: float, + normal_offset_scale: float, + max_world_bias: float, +) -> tuple[float, float, float, float, float]: + def nonnegative_finite(value: float) -> float: + return value if math.isfinite(value) and value > 0.0 else 0.0 + + far = far_distance if math.isfinite(far_distance) and far_distance > 0.0 else 1.0 + face = max(face_size, 1) + values = [ + nonnegative_finite(constant_bias), + nonnegative_finite(normal_bias), + nonnegative_finite(texel_bias_scale), + nonnegative_finite(normal_offset_scale), + ] + cap = nonnegative_finite(max_world_bias) + depth_world = far * max(values[0] + values[1], values[2] / face * 5.0) + offset_world = far * 2.0 * values[3] / face + requested = depth_world + offset_world + scale = cap / requested if cap > 0.0 and requested > cap else 1.0 + return (*(value * scale for value in values), scale) + + +def storage_adjusted_point_receiver_settings( + base: tuple[float, float, float, float, float], + far_distance: float, + face_size: int, + depth_compare: bool, + high_precision: bool, + max_world_bias: float, +) -> tuple[float, float, float, float, float]: + if depth_compare: + return base + storage_step = 1.0 / 2048.0 if high_precision else 1.0 / 65025.0 + return point_receiver_settings( + far_distance, + face_size, + max(base[0], storage_step * 1.5), + base[1], + base[2], + base[3], + max_world_bias, + ) + + +def validate_point_receiver_world_bias_contract() -> None: + # Ordinary local lights retain the authored defaults. + ordinary = point_receiver_settings(256.0, 512, 0.00010, 0.0010, 0.45, 1.0, 4.0) + if ordinary[4] != 1.0: + raise AssertionError("Ordinary point lights must remain below the world-bias cap") + + # Retail airdefense1 light_146: radius + abs(center), then the default 1.25 far scale. + far_distance = math.sqrt(15488.0**2 + 16040.0**2 + 10568.0**2) * 1.25 + bounded = point_receiver_settings( + far_distance, 512, 0.00010, 0.0010, 0.45, 1.0, 4.0 + ) + expected_scale = 0.0156235087883939 + if not math.isclose(bounded[4], expected_scale, rel_tol=1.0e-6): + raise AssertionError(f"Unexpected airdefense1 point-bias scale: {bounded[4]}") + bounded_world = far_distance * ( + max(bounded[0] + bounded[1], bounded[2] / 512.0 * 5.0) + + 2.0 * bounded[3] / 512.0 + ) + if bounded_world > 4.00001: + raise AssertionError(f"Point receiver world-bias cap exceeded: {bounded_world}") + + if storage_adjusted_point_receiver_settings( + bounded, far_distance, 512, True, False, 4.0 + ) != bounded: + raise AssertionError("Hardware point comparison must not apply a manual storage floor") + for high_precision in (False, True): + adjusted = storage_adjusted_point_receiver_settings( + bounded, far_distance, 512, False, high_precision, 4.0 + ) + adjusted_world = far_distance * ( + max(adjusted[0] + adjusted[1], adjusted[2] / 512.0 * 5.0) + + 2.0 * adjusted[3] / 512.0 + ) + if adjusted_world > 4.00001 or not all(math.isfinite(value) for value in adjusted): + raise AssertionError( + f"Manual point storage floor escaped its world cap (fp16={high_precision})" + ) + + modern_depth_only = point_receiver_settings( + far_distance, 512, 0.00010, 0.0010, 0.45, 0.0, 4.0 + ) + if modern_depth_only[3] != 0.0: + raise AssertionError("A receiver without geometric normal offset must reserve no offset budget") + modern_adjusted = storage_adjusted_point_receiver_settings( + modern_depth_only, far_distance, 512, False, False, 4.0 + ) + modern_world = far_distance * max( + modern_adjusted[0] + modern_adjusted[1], + modern_adjusted[2] / 512.0 * 5.0, + ) + if modern_world > 4.00001: + raise AssertionError("Modern manual point depth escaped the depth-only world cap") + if point_receiver_settings( + far_distance, 512, 0.00010, 0.0010, 0.45, 1.0, 0.0 + )[4] != 1.0: + raise AssertionError("A zero point receiver world-bias cap must disable clamping") + if not all( + math.isfinite(value) + for value in point_receiver_settings( + math.nan, 0, math.nan, -1.0, math.inf, -math.inf, math.nan + ) + ): + raise AssertionError("Invalid point receiver inputs must sanitize to finite values") + + init = read("src/renderer/RenderSystem_init.cpp") + require_compact( + init, + '''idCVar r_shadowMapPointMaxWorldBias( "r_shadowMapPointMaxWorldBias", "4.0", + CVAR_RENDERER | CVAR_ARCHIVE | CVAR_FLOAT''', + "point receiver world-bias cap default", + ) + require(init, '0.0f, 64.0f );', "point receiver world-bias cap range") + + classification_h = read("src/renderer/ShadowMapClassification.h") + classification = read("src/renderer/ShadowMapClassification.cpp") + require( + classification_h, + "SHADOWMAP_POINT_RECEIVER_MAX_SLOPE = 4.0f", + "CPU point receiver slope bound", + ) + clamp = braced_body( + classification, + "shadowMapPointReceiverSettings_t R_ClampShadowMapPointReceiverSettings(", + "pure point receiver world-bias clamp", + ) + require_order( + clamp, + ( + "const double scalarDepthBias", + "const double texelDepthBias", + "SHADOWMAP_POINT_RECEIVER_MAX_SLOPE", + "const double depthWorldBias", + "const double normalOffsetWorldBias", + "const double requestedWorldBias", + "cap / requestedWorldBias", + "settings.constantBias *= settings.worldBiasScale;", + "settings.normalBias *= settings.worldBiasScale;", + "settings.texelBiasScale *= settings.worldBiasScale;", + "settings.normalOffsetScale *= settings.worldBiasScale;", + ), + "pure point receiver world-bias clamp", + ) + + storage_floor = braced_body( + classification, + "R_ShadowMapPointStorageAdjustedReceiverSettings(", + "shared cap-bounded manual point-depth floor", + ) + require_order( + storage_floor, + ( + "if ( depthCompare )", + "const float storageStep", + "R_ClampShadowMapPointReceiverSettings(", + "Max( baseSettings.constantBias, storageStep * 1.5f )", + "r_shadowMapPointMaxWorldBias.GetFloat()", + ), + "shared cap-bounded manual point-depth floor", + ) + gl = read("src/renderer/draw_arb2.cpp") + direct_gl = braced_body( + gl, + "static bool RB_GLSLPointShadowMap_CreateDrawInteractions(", + "direct GL bounded point receiver upload", + ) + require_order( + direct_gl, + ( + "R_ShadowMapPointFarDistance( backEnd.vLight )", + "R_ShadowMapPointReceiverSettings(", + "R_ShadowMapPointStorageAdjustedReceiverSettings(", + "receiverSettings.constantBias", + "receiverSettings.normalBias", + "receiverSettings.texelBiasScale / pointFaceSize", + "receiverSettings.normalOffsetScale / pointFaceSize", + ), + "direct GL bounded point receiver upload", + ) + + modern = read("src/renderer/ModernShadowPlanner.cpp") + modern_contract = braced_body( + modern, + "static void R_ModernShadowPlanner_InitDescriptorContract(", + "modern bounded point receiver descriptor", + ) + require( + modern_contract, + "MODERN_SHADOW_COMPARE_MANUAL_PACKED_DEPTH", + "modern OpenGL point descriptor uses manual color-depth decoding", + ) + require_order( + modern_contract, + ( + "if ( descriptor.pointLight )", + "R_ShadowMapPointFarDistance( vLight )", + "R_ClampShadowMapPointReceiverSettings(", + "r_shadowMapPointBias.GetFloat()", + "r_shadowMapPointNormalBias.GetFloat()", + "r_shadowMapTexelBiasScale.GetFloat()", + "0.0f", + "r_shadowMapPointMaxWorldBias.GetFloat()", + "#ifndef OPENQ4_RENDERER_VK_MODULE", + "R_ShadowMapPointStorageAdjustedReceiverSettings(", + "false", + "r_shadowMapPointHighPrecision.GetBool()", + "#endif", + "descriptor.bias[0] = pointReceiverSettings.constantBias;", + "descriptor.bias[1] = pointReceiverSettings.normalBias;", + "descriptor.texelDepthBias[0] =", + "pointReceiverSettings.texelBiasScale", + ), + "modern bounded point receiver descriptor", + ) + + texture_bindings = braced_body( + gl, + "bool RB_ShadowMapTextureBindings(", + "modern OpenGL manual point-depth binding", + ) + require_order( + texture_bindings, + ( + "bindings.pointDepthCompare = RB_PointShadowMapDepthCompareEnabled();", + "idImage *pointAtlasImage = g_pointShadowMapColorImage;", + "bindings.pointAtlas", + "pointAtlasImage", + "RB_ShadowMapActivePointCacheContentReady()", + ), + "modern OpenGL manual point-depth binding independent of classic compare", + ) + + point_content_ready = braced_body( + gl, + "static bool RB_ShadowMapActivePointCacheContentReady(", + "exact active point-cache content gate", + ) + require_order( + point_content_ready, + ( + "g_activePointShadowMapCache", + "entry == &g_pointShadowMapCache[i]", + "r_shadowMapPointCacheSize.GetInteger()", + "entry->passKind != static_cast( SHADOWMAP_PASS_GLOBAL )", + "entry->lastUpdatedFrame > tr.frameCount", + "entry->size != RB_ShadowMapPointSizeValue()", + "entry->colorImage != g_pointShadowMapColorImage", + "entry->depthImage != g_pointShadowMapDepthImage", + "entry->renderTexture != g_pointShadowMapRenderTexture", + "entry->colorImage->IsLoaded()", + "entry->depthImage->IsLoaded()", + ), + "point binding requires exact valid GLOBAL cache content", + ) + if "? g_pointShadowMapDepthImage : g_pointShadowMapColorImage" in texture_bindings: + raise AssertionError( + "Modern GL point binding must not reinterpret the hardware depth cube as packed color depth" + ) + modern_executor = read("src/renderer/ModernGLExecutor.cpp") + modern_bind = braced_body( + modern_executor, + "static void R_ModernGLExecutor_BindShadowTextureSlot(", + "modern OpenGL manual shadow sampler binding", + ) + require( + modern_bind, + "glTexParameteri( target, GL_TEXTURE_COMPARE_MODE, GL_NONE );", + "modern OpenGL point sampler disables hardware comparison", + ) + modern_shader = read("src/renderer/ModernGLShaderLibrary.cpp") + require_order( + modern_shader, + ( + "float ModernClusterDecodePointDepth", + "uModernShadowSamplerState.z > 0.5", + "return encodedDepth.r;", + "encodedDepth.r + encodedDepth.g * (1.0 / 255.0)", + ), + "modern OpenGL fp16/packed color-depth decoding", + ) + + shadow_h = read("src/renderer/Vulkan/vk_ShadowMap.h") + light_state = braced_body( + shadow_h, "typedef struct vkShadowLightState_s", "Vulkan point shadow state" + ) + require_order( + light_state, + ("float\t\t\t\tconstantBias;", "float\t\t\t\tnormalBias;", "float\t\t\t\ttexelDepthBias;", "float\t\t\t\tnormalOffsetWorld;"), + "Vulkan sealed point receiver settings", + ) + interactions = read("src/renderer/Vulkan/vk_Interactions.cpp") + shadow_slice = braced_body( + interactions, + "static int VK_Inter_WriteShadowSlice(", + "direct Vulkan bounded point receiver upload", + ) + require_order( + shadow_slice, + ( + "pointBlock.biasParams[ 0 ] = state->constantBias;", + "pointBlock.biasParams[ 1 ] = state->normalBias;", + "pointBlock.biasParams[ 2 ] = state->texelDepthBias;", + "pointBlock.biasParams[ 3 ] = state->normalOffsetWorld;", + ), + "direct Vulkan bounded point receiver upload", + ) + + vk = read("src/renderer/Vulkan/vk_ShadowMap.cpp") + vk_prepare = braced_body( + vk, + "int VK_ShadowMap_PrepareViewLights(", + "Vulkan sealed point receiver policy", + ) + require_order( + vk_prepare, + ( + "R_ShadowMapPointReceiverSettings(", + "entry.constantBias = receiverSettings.constantBias;", + "entry.normalBias = receiverSettings.normalBias;", + "entry.texelDepthBias = receiverSettings.texelBiasScale", + "entry.normalOffsetWorld =", + "receiverSettings.normalOffsetScale", + ), + "Vulkan sealed point receiver policy", + ) + + gl_point_vertex = read( + "content/baseoq4/pak0/glprogs/shadow_point_interaction.vs" + ) + require_order( + gl_point_vertex, + ( + "vec3 pointShadowVector = worldPos - uGlobalLightOrigin.xyz;", + "float pointShadowSinTheta", + "uPointShadowNormalOffsetWorld * length( pointShadowVector )", + "vPointShadowVector = pointShadowVector + worldNormal * pointShadowNormalOffset;", + ), + "OpenGL bounded point normal-offset consumption", + ) + vk_point_vertex = read( + "src/renderer/Vulkan/shaders/interaction_shadow_point.vert" + ) + require_order( + vk_point_vertex, + ( + "vec3 pointShadowVector = worldPos - shadow.lightOriginFar.xyz;", + "float shadowSinTheta", + "shadow.biasParams.w * length(pointShadowVector)", + "vPointShadowVector = pointShadowVector + worldNormal * normalOffset;", + ), + "Vulkan bounded point normal-offset consumption", + ) + + +def validate_shadow_contact_and_gl_robustness_contract() -> None: + init = read("src/renderer/RenderSystem_init.cpp") + common = read("src/framework/Common.cpp") + interaction = read("src/renderer/Interaction.cpp") + domain_h = read("src/renderer/ClassicInteractionDomain.h") + domain = read("src/renderer/ClassicInteractionDomain.cpp") + arb2_parity_h = read("src/renderer/ShadowMapArb2Parity.h") + gl = read("src/renderer/draw_arb2.cpp") + modern = read("src/renderer/ModernShadowPlanner.cpp") + modern_h = read("src/renderer/ModernShadowPlanner.h") + modern_executor = read("src/renderer/ModernGLExecutor.cpp") + modern_shader = read("src/renderer/ModernGLShaderLibrary.cpp") + tr_local = read("src/renderer/tr_local.h") + vk = read("src/renderer/Vulkan/vk_ShadowMap.cpp") + + init_cvars = braced_body(init, "void R_InitCvars( void )", "renderer cvar registration") + if "r_shadowMapContactQualityMigrated" in init_cvars: + raise AssertionError( + "Shadow quality migration must not run before archived configs are executed" + ) + migration = braced_body( + init, + "static void R_MigrateLegacyShadowMapContactQuality( void )", + "post-config shadow quality migration", + ) + require_order( + migration, + ( + "!r_shadowMapContactQualityMigrated.GetBool()", + "r_shadowMapFilterRadius.GetFloat() - 2.0f", + "r_shadowMapPointFilterRadius.GetFloat() - 2.5f", + "r_shadowMapFilterTaps.GetInteger() == 13", + "r_shadowMapPointFilterTaps.GetInteger() == 13", + "r_shadowMapPolygonFactor.GetFloat() - 0.75f", + "r_shadowMapFilterRadius.SetFloat( 0.75f )", + "r_shadowMapPointFilterRadius.SetFloat( 1.0f )", + "r_shadowMapFilterTaps.SetInteger( 9 )", + "r_shadowMapPointFilterTaps.SetInteger( 9 )", + "r_shadowMapPolygonFactor.SetFloat( 0.25f )", + "r_shadowMapContactQualityMigrated.SetBool( true )", + ), + "complete legacy shadow tuple migration", + ) + + legacy_profile = (2.0, 2.5, 13, 13, 0.75) + balanced_profile = (0.75, 1.0, 9, 9, 0.25) + + def migrate_profile( + profile: tuple[float, float, int, int, float], migrated: bool + ) -> tuple[tuple[float, float, int, int, float], bool]: + if migrated: + return profile, True + return (balanced_profile if profile == legacy_profile else profile), True + + if migrate_profile(legacy_profile, False) != (balanced_profile, True): + raise AssertionError("The exact legacy shadow tuple must migrate once") + custom_profile = (1.5, 2.5, 13, 13, 0.75) + if migrate_profile(custom_profile, False) != (custom_profile, True): + raise AssertionError("Customized shadow profiles must be preserved") + if migrate_profile(legacy_profile, True) != (legacy_profile, True): + raise AssertionError("The shadow contact migration flag must make migration one-shot") + for compiled_default in ( + 'r_shadowMapFilterRadius( "r_shadowMapFilterRadius", "0.75"', + 'r_shadowMapPointFilterRadius( "r_shadowMapPointFilterRadius", "1.0"', + 'r_shadowMapFilterTaps( "r_shadowMapFilterTaps", "9"', + 'r_shadowMapPointFilterTaps( "r_shadowMapPointFilterTaps", "9"', + 'r_shadowMapPolygonFactor( "r_shadowMapPolygonFactor", "0.25"', + ): + require(init, compiled_default, "compiled balanced contact-shadow defaults") + renderer_device_init = braced_body( + init, + "void idRenderSystemLocal::InitOpenGL( void )", + "post-config renderer device startup", + ) + require_order( + renderer_device_init, + ( + "R_MigrateLegacyShadowMapContactQuality();", + "if ( !glConfig.isInitialized )", + ), + "migration before either renderer device starts", + ) + common_init = braced_body(common, "void idCommonLocal::InitGame( void )", "engine startup") + require_order( + common_init, + ( + "renderSystem->Init();", + '"exec " CONFIG_FILE "\\n"', + "cmdSystem->ExecuteCommandBuffer();", + "StartupVariable( NULL, false );", + "InitRenderSystem();", + ), + "archived config and command-line ordering before renderer device startup", + ) + common_outer_init = braced_body( + common, + "void idCommonLocal::Init( int argc, const char **argv, const char *cmdline )", + "outer engine startup", + ) + require_order( + common_outer_init, + ( + "InitGame();", + "AddStartupCommands()", + ), + "late explicit +set commands remain authoritative after archived migration", + ) + + require(domain_h, "bool\t\t\tperfectHull;", "sealed caster topology") + require(domain, "HashBool( hash, caster.perfectHull );", "caster topology identity") + for token in ( + "caster.perfectHull = drawSurf->geo->perfectHull;", + "caster.perfectHull = casterGeometry->perfectHull;", + ): + require(domain, token, "actual caster topology capture") + + caster_signature = braced_body( + interaction, + "static void R_RecordShadowMapCaster(", + "physical shadow caster cache signature", + ) + require_order( + caster_signature, + ( + "shader->GetCullType()", + "casterTris->perfectHull", + "casterTris->numVerts", + "casterTris->numIndexes", + "casterTris->bounds[ corner ][ component ]", + "entityDef->lastModifiedFrameNum", + "entityDef->dynamicModelFrameCount", + "entityDef->modelMatrix[i]", + ), + "topology/material/transform-aware physical caster signature", + ) + if interaction.count("casterTris, false, shadowMapCasterOnly") != 1 or interaction.count( + "casterTris, true, shadowMapCasterOnly" + ) != 1: + raise AssertionError("Both opaque and translucent caster records must hash actual geometry") + + classification_h = read("src/renderer/ShadowMapClassification.h") + classification = read("src/renderer/ShadowMapClassification.cpp") + require( + classification_h, + "R_ShadowMapLightOriginInsideCasterBounds(", + "shared enclosing-light caster guard declaration", + ) + require( + classification_h, + "R_ShadowMapCasterTransformNeedsTwoSided(", + "shared unsafe-transform caster guard declaration", + ) + transform_guard = braced_body( + classification, + "bool R_ShadowMapCasterTransformNeedsTwoSided(", + "shared unsafe-transform caster guard", + ) + require_order( + transform_guard, + ( + "if ( modelMatrix == NULL )", + "return true;", + "const double determinant =", + "static_cast( modelMatrix[ 0 ] )", + "return !std::isfinite( determinant ) || determinant <= 0.0;", + ), + "fail-conservative mirrored/non-finite transform guard", + ) + enclosing_guard = braced_body( + classification, + "bool R_ShadowMapLightOriginInsideCasterBounds(", + "shared enclosing-light caster guard", + ) + require_order( + enclosing_guard, + ( + "R_GlobalPointToLocal(", + "minimum > maximum", + "return true;", + "local < minimum || local > maximum", + "return false;", + ), + "fail-conservative enclosing-light bounds guard", + ) + + def expected_cull( + mode: int, + material: str, + perfect_hull: bool | None, + light_inside_bounds: bool, + unsafe_transform: bool = False, + ) -> str: + if mode == 0 or material == "two": + return "none" + if mode == 2 and ( + perfect_hull is not True or unsafe_transform or light_inside_bounds + ): + return "none" + return "back" if material == "back" else "front" + + for material in ("front", "back", "two"): + if expected_cull(0, material, True, False) != "none": + raise AssertionError("Caster culling mode 0 must be unconditionally two-sided") + if expected_cull(1, "front", False, True) != "front": + raise AssertionError("Caster culling mode 1 must force the near-shell orientation") + if expected_cull(1, "back", True, False) != "back": + raise AssertionError("Back-sided material orientation must reverse forced near-shell culling") + for topology, inside in ((False, False), (None, False), (True, True)): + if expected_cull(2, "front", topology, inside) != "none": + raise AssertionError("AUTO culling must fail conservatively to two-sided depth") + if expected_cull(2, "front", True, False) != "front": + raise AssertionError("AUTO may cull only a sealed hull whose bounds exclude the light") + if expected_cull(2, "front", True, False, True) != "none": + raise AssertionError("AUTO must render mirrored/non-finite transforms two-sided") + if expected_cull(1, "front", True, False, True) != "front": + raise AssertionError("Explicit caster culling mode 1 must retain force semantics") + + gl_cull = braced_body( + gl, + "static void RB_ShadowMapApplyCasterCull(", + "direct OpenGL topology-aware caster culling", + ) + require_order( + gl_cull, + ( + "mode == 2", + "casterGeo == NULL || !casterGeo->perfectHull", + "R_ShadowMapCasterTransformNeedsTwoSided( modelMatrix )", + "R_ShadowMapLightOriginInsideCasterBounds( backEnd.vLight", + "mode != 0 && !automaticTwoSided", + "GLenum cullFace = GL_FRONT;", + "materialCull == CT_BACK_SIDED", + ), + "direct OpenGL near-shell/uncertain-hull policy", + ) + gl_shared_cull = braced_body( + gl, + "static void RB_SharedWorldInteractionGLApplyMapCasterCull(", + "shared OpenGL topology-aware caster culling", + ) + require_order( + gl_shared_cull, + ( + "pass.casterCullMode == 2", + "!caster.perfectHull", + "R_ShadowMapLightOriginInsideCasterBounds(", + "pass.legacyViewLight", + "pass.casterCullMode != 0 && !automaticTwoSided", + "GLenum face = GL_FRONT;", + ), + "shared OpenGL near-shell/uncertain-hull policy", + ) + vk_cull = braced_body( + vk, + "static VkCullModeFlags VK_ShadowMap_CasterCullMode(", + "direct Vulkan topology-aware caster culling", + ) + require_order( + vk_cull, + ( + "mode == 0 || materialCull == CT_TWO_SIDED", + "casterGeo == NULL || !casterGeo->perfectHull", + "surf == NULL || surf->space == NULL", + "R_ShadowMapCasterTransformNeedsTwoSided(", + "surf->space->modelMatrix", + "R_ShadowMapLightOriginInsideCasterBounds( vLight", + "bool cullFront = true;", + ), + "direct Vulkan near-shell/uncertain-hull policy", + ) + vk_shared_cull = braced_body( + vk, + "static VkCullModeFlags VK_ClassicShadow_EffectiveCull(", + "shared Vulkan topology-aware caster culling", + ) + require_order( + vk_shared_cull, + ( + "pass.casterCullMode == 0", + "pass.casterCullMode == 2", + "!casterPlan.caster->perfectHull", + "R_ShadowMapLightOriginInsideCasterBounds(", + "pass.legacyViewLight", + "bool cullFront = true;", + ), + "shared Vulkan near-shell/uncertain-hull policy", + ) + for forbidden in ("mode == 1 ) ? GL_FRONT : GL_BACK", "mode 2 (default) stores engine-back"): + if forbidden in gl or forbidden in vk: + raise AssertionError(f"Far-shell caster policy survived: {forbidden!r}") + + gl_signature = braced_body( + gl, + "static int RB_ShadowMapBuildPassSignatureForView(", + "OpenGL world-scoped cache signature", + ) + for token in ( + "renderWorld->mapFileCRC", + "RB_ShadowMapMapNameHash( viewDef )", + "vLight->shadowMapIncompleteMapMask", + "vLight->shadowMapHybridIncompleteMask", + "vLight->shadowMapPrelightMapMissingMask", + "R_ShadowMapPointFarDistance( vLight )", + "vLight->lightDef->parms.lightCenter[i]", + ): + require(gl_signature, token, "OpenGL world-scoped cache signature") + + for token in ( + "unsigned int\t\t\t\t\tcacheKeyHitMask;", + "unsigned int\t\t\t\t\tplannedCacheUpdateKeyMask;", + ): + require(arb2_parity_h, token, "canonical ARB2 cache estimate ABI") + cache_estimate = braced_body( + gl, + "static void RB_ShadowMapEstimateArb2CachePass(", + "canonical ARB2 cache estimate", + ) + require_order( + cache_estimate, + ( + "const unsigned int passMask", + "RB_ShadowMapCachePassKind( vLight, passKind )", + "const unsigned int cacheKeyMask", + "RB_ShadowMapStaticCacheableReadOnly( vLight, cachePassKind", + "RB_ShadowMapBuildPassSignatureForView( vLight, viewDef, cachePassKind", + "estimate.cacheKeyHitMask |= cacheKeyMask;", + "estimate.plannedCacheUpdateKeyMask & cacheKeyMask", + "estimate.cacheHitPassMask |= passMask;", + "estimate.freshUpdatePasses++;", + "estimate.plannedCacheUpdateKeyMask |= cacheKeyMask;", + ), + "requested receiver masks remain separate from canonical cache keys", + ) + cache_admission = braced_body( + gl, + "static void RB_ShadowMapBuildUpdateAdmissions(", + "canonical ARB2 update admission cost", + ) + require_compact( + cache_admission, + """const int cost = estimate.freshUpdatePasses + + estimate.budgetFallbackPasses;""", + "simplified canonical update admission cost", + ) + for obsolete_cost in ( + "collapsedFreshUpdatePasses", + "estimate.freshUpdatePassMask & SHADOWMAP_ARB2_CACHE_PASS_GLOBAL", + ): + if obsolete_cost in cache_admission: + raise AssertionError( + f"Obsolete LOCAL/GLOBAL admission collapse survived: {obsolete_cost}" + ) + gl_cache_view = braced_body( + gl, + "bool RB_ShadowMapPrepareCacheView(", + "OpenGL map transition invalidation", + ) + require_order( + gl_cache_view, + ( + "renderWorld->mapFileCRC", + "RB_ShadowMapMapNameHash( viewDef )", + "g_projectedShadowMapCache[i].valid = false;", + "g_pointShadowMapCache[i].valid = false;", + "memset( g_shadowMapLightHistory, 0", + "g_activeProjectedShadowMapCache = NULL;", + "g_activePointShadowMapCache = NULL;", + "g_shadowMapDepthImage = NULL;", + "g_pointShadowMapColorImage = NULL;", + "memset( &g_projectedShadowMapState, 0", + "g_projectedTranslucentShadowPassReady = false;", + "g_pointTranslucentShadowPassReady = false;", + "g_shadowMapCacheRenderWorld = renderWorld;", + ), + "OpenGL map transition invalidates cache, aliases, and moment readiness", + ) + + projected_scratch = braced_body( + gl, + "static void RB_ShadowMapSelectProjectedScratchResources(", + "projected-only scratch selection", + ) + point_scratch = braced_body( + gl, + "static void RB_ShadowMapSelectPointScratchResources(", + "point-only scratch selection", + ) + for token in ( + "g_activeProjectedShadowMapCache = NULL;", + "g_shadowMapDepthImage = g_shadowMapScratchDepthImage;", + "g_shadowMapRenderTexture = g_shadowMapScratchRenderTexture;", + ): + require(projected_scratch, token, "projected-only scratch selection") + for forbidden in ("g_activePointShadowMapCache", "g_pointShadowMap"): + if forbidden in projected_scratch: + raise AssertionError( + f"Projected scratch selection invalidates point provenance: {forbidden}" + ) + for token in ( + "g_activePointShadowMapCache = NULL;", + "g_pointShadowMapColorImage = g_pointShadowMapScratchColorImage;", + "g_pointShadowMapDepthImage = g_pointShadowMapScratchDepthImage;", + "g_pointShadowMapRenderTexture = g_pointShadowMapScratchRenderTexture;", + ): + require(point_scratch, token, "point-only scratch selection") + for forbidden in ("g_activeProjectedShadowMapCache", "g_shadowMapDepthImage"): + if forbidden in point_scratch: + raise AssertionError( + f"Point scratch selection invalidates projected provenance: {forbidden}" + ) + direct_schedule = braced_body( + gl, + "static shadowMapSchedule_t RB_ShadowMapSchedulePass(", + "type-specific direct scratch selection", + ) + require_order( + direct_schedule, + ( + "if ( pointLight )", + "RB_ShadowMapSelectPointScratchResources();", + "else", + "RB_ShadowMapSelectProjectedScratchResources();", + "schedule.cacheable =", + ), + "point and projected scratch aliases are selected independently", + ) + + point_storage = braced_body( + gl, + "static bool RB_ShadowMapPointCacheEntryStorageValid(", + "point-cube physical storage validation", + ) + require_order( + point_storage, + ( + "entry->colorStorageGeneration != 0", + "entry->depthStorageGeneration != 0", + "entry->colorImage->GetStorageGeneration()", + "entry->depthImage->GetStorageGeneration()", + "entry->colorImage->IsLoaded()", + "!entry->colorImage->IsDefaulted()", + "entry->colorImage->GetDeviceHandle() != 0", + "entry->depthImage->IsLoaded()", + "!entry->depthImage->IsDefaulted()", + "entry->depthImage->GetDeviceHandle() != 0", + "entry->colorImage->GetOpts().textureType == TT_CUBIC", + "entry->depthImage->GetOpts().textureType == TT_CUBIC", + "entry->colorImage->GetOpts().format", + "entry->highPrecision ? FMT_RGBA16F : FMT_RGBA8", + "entry->depthImage->GetOpts().format == FMT_DEPTH", + "entry->renderTexture->GetWidth() == entry->size", + "entry->renderTexture->GetHeight() == entry->size", + ), + "point-cube storage identity is generation/type/format/dimension exact", + ) + for field in ("lightOrigin[3];", "farDistance;"): + require(gl, field, "point-cube render-time projection provenance") + point_active = braced_body( + gl, + "static bool RB_ShadowMapActivePointCacheContentReady(", + "active point-cube provenance validation", + ) + require_order( + point_active, + ( + "entry->farDistance > 0.0f", + "entry->lightOrigin[0]", + "entry->colorStorageGeneration == 0", + "entry->depthStorageGeneration == 0", + "entry->colorImage->GetStorageGeneration()", + "entry->depthImage->GetStorageGeneration()", + ), + "active point cubes require published projection and storage provenance", + ) + point_projection = braced_body( + gl, + "static bool RB_ShadowMapPointCacheEntryProjectionMatches(", + "point-cube render-time projection identity", + ) + require_order( + point_projection, + ( + "entry->farDistance == R_ShadowMapPointFarDistance( vLight )", + "entry->lightOrigin[0] == vLight->globalLightOrigin[0]", + "entry->lightOrigin[1] == vLight->globalLightOrigin[1]", + "entry->lightOrigin[2] == vLight->globalLightOrigin[2]", + ), + "stale point cubes cannot cross light-origin or far-plane changes", + ) + point_stale_reuse = braced_body( + gl, + "static pointShadowMapCacheEntry_t *RB_ShadowMapFindPointCacheEntryAnySignature(", + "point-cube signature-agnostic reuse compatibility", + ) + require_order( + point_stale_reuse, + ( + "requiredSize = RB_ShadowMapPointSizeValue()", + "requiredHighPrecision = RB_PointShadowMapHighPrecisionEnabled()", + "requiredDepthCompare = RB_PointShadowMapDepthCompareEnabled()", + "RB_ShadowMapPointCacheEntryStorageValid( entry )", + "entry->size == requiredSize", + "entry->highPrecision == requiredHighPrecision", + "entry->depthCompare == requiredDepthCompare", + "RB_ShadowMapPointCacheEntryProjectionMatches( entry, vLight )", + "entry->lastUpdatedFrame > newest->lastUpdatedFrame", + "newest = entry;", + "return newest;", + ), + "stale point-cube reuse selects the newest projection-compatible allocation", + ) + projected_storage = braced_body( + gl, + "static bool RB_ShadowMapProjectedCacheEntryStorageValid(", + "projected-atlas physical storage validation", + ) + require_order( + projected_storage, + ( + "entry->atlasStorageGeneration != 0", + "g_shadowMapAtlasDepthImage != NULL", + "g_shadowMapAtlasRenderTexture != NULL", + "g_shadowMapAtlasDepthImage->GetStorageGeneration()", + "entry->atlasStorageGeneration", + "g_shadowMapAtlasDepthImage->IsLoaded()", + "!g_shadowMapAtlasDepthImage->IsDefaulted()", + "g_shadowMapAtlasDepthImage->GetDeviceHandle() != 0", + "g_shadowMapAtlasDepthImage->GetOpts().textureType == TT_2D", + "g_shadowMapAtlasDepthImage->GetOpts().format == FMT_DEPTH", + "g_shadowMapAtlasRenderTexture->GetWidth() > 0", + "g_shadowMapAtlasRenderTexture->GetHeight() > 0", + ), + "projected cache entries require the atlas allocation they rendered into", + ) + for lookup_name in ( + "static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntryAnySignature(", + "static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntry(", + "static projectedShadowMapCacheEntry_t *RB_ShadowMapNewestProjectedGlobalEntry( const int lightIndex ) {", + ): + require( + braced_body(gl, lookup_name, "projected cache lookup generation gate"), + "RB_ShadowMapProjectedCacheEntryStorageValid( entry )", + "projected cache lookups reject stale atlas allocations", + ) + projected_stale_reuse = braced_body( + gl, + "static projectedShadowMapCacheEntry_t *RB_ShadowMapFindProjectedCacheEntryAnySignature(", + "newest projected signature-agnostic reuse", + ) + require_order( + projected_stale_reuse, + ( + "projectedShadowMapCacheEntry_t *newest = NULL;", + "entry->lastUpdatedFrame > newest->lastUpdatedFrame", + "newest = entry;", + "return newest;", + ), + "projected stale reuse selects the most recently rendered sibling", + ) + direct_cache_completion = braced_body( + gl, + "static void RB_ShadowMapCompleteCacheUpdate(", + "direct projected cache storage publication", + ) + require_order( + direct_cache_completion, + ( + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->atlasStorageGeneration = g_shadowMapAtlasDepthImage != NULL", + "g_shadowMapAtlasDepthImage->GetStorageGeneration()", + "entry->state = g_projectedShadowMapState;", + ), + "direct projected cache publication stamps the rendered atlas allocation", + ) + require_order( + direct_cache_completion, + ( + "entry->lightOrigin[0] = vLight->globalLightOrigin[0];", + "entry->lightOrigin[1] = vLight->globalLightOrigin[1];", + "entry->lightOrigin[2] = vLight->globalLightOrigin[2];", + "entry->farDistance = R_ShadowMapPointFarDistance( vLight );", + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->colorStorageGeneration = entry->colorImage != NULL", + "entry->depthStorageGeneration = entry->depthImage != NULL", + ), + "direct point-cache publication stamps projection and storage provenance", + ) + shared_cache_completion = braced_body( + gl, + "static void RB_SharedWorldInteractionGLCompleteSealedCacheUpdate(", + "shared projected cache storage publication", + ) + require_order( + shared_cache_completion, + ( + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->atlasStorageGeneration = preparedPass.projectedDepthImage != NULL", + "preparedPass.projectedDepthImage->GetStorageGeneration()", + "entry->state = pass.projected.state;", + ), + "shared projected cache publication stamps the rendered atlas allocation", + ) + require_order( + shared_cache_completion, + ( + "entry->lightOrigin[0] = pass.point.lightOrigin[0];", + "entry->lightOrigin[1] = pass.point.lightOrigin[1];", + "entry->lightOrigin[2] = pass.point.lightOrigin[2];", + "entry->farDistance = pass.point.farDistance;", + "entry->lastUpdatedFrame = tr.frameCount;", + "entry->colorStorageGeneration = entry->colorImage != NULL", + "entry->depthStorageGeneration = entry->depthImage != NULL", + ), + "shared point-cache publication stamps sealed projection and storage provenance", + ) + shared_projected_match = braced_body( + gl, + "static bool RB_SharedWorldInteractionGLProjectedEntryMatches(", + "shared projected cache generation revalidation", + ) + require_order( + shared_projected_match, + ( + "entry->atlasStorageGeneration != 0", + "entry->atlasStorageGeneration", + "preparedPass.sampleStorageGeneration", + ), + "shared projected consumption preserves cache-to-storage provenance", + ) + shared_point_match = braced_body( + gl, + "static bool RB_SharedWorldInteractionGLPointEntryMatches(", + "shared point-cache projection revalidation", + ) + require_order( + shared_point_match, + ( + "entry->lightOrigin[0] == pass.point.lightOrigin[0]", + "entry->lightOrigin[1] == pass.point.lightOrigin[1]", + "entry->lightOrigin[2] == pass.point.lightOrigin[2]", + "entry->farDistance == pass.point.farDistance", + "entry->colorStorageGeneration != 0", + "entry->depthStorageGeneration != 0", + ), + "shared point-cache consumption preserves projection and storage provenance", + ) + point_bindings = braced_body( + gl, + "bool RB_ShadowMapTextureBindings(", + "point-cube bind-time identity export", + ) + require_order( + point_bindings, + ( + "bindings.pointAtlasLightIndex = -1;", + "bindings.pointAtlasContentFrame = -1;", + "RB_ShadowMapActivePointCacheContentReady()", + "bindings.pointAtlasLightIndex = g_activePointShadowMapCache->lightIndex;", + "bindings.pointAtlasSignature = g_activePointShadowMapCache->signature;", + "bindings.pointAtlasContentFrame = g_activePointShadowMapCache->lastUpdatedFrame;", + ), + "point sampler binding carries exact cache provenance", + ) + for field in ( + "pointAtlasLightIndex;", + "pointAtlasSignature;", + "pointAtlasContentFrame;", + ): + require(tr_local, field, "renderer point-cube binding ABI") + + for declaration in ( + "bool RB_ShadowMapProjectedAtlasSlotForLight( const viewLight_t *vLight,", + "const viewDef_t *viewDef, shadowMapArb2AtlasSlot_t &slot );", + "bool RB_ShadowMapProjectedAtlasSlotMarkUsed( int lightDefIndex,", + "int signature, std::uint64_t storageGeneration,", + "int cellX, int cellY, int cellSpan );", + ): + require(arb2_parity_h, declaration, "exact projected-atlas provenance API") + require(arb2_parity_h, "storageGeneration;", "projected-atlas slot generation ABI") + projected_slot_export = braced_body( + gl, + "bool RB_ShadowMapProjectedAtlasSlotForLight(", + "exact projected-atlas provenance export", + ) + require_order( + projected_slot_export, + ( + "viewDef->renderWorld == NULL", + "g_shadowMapCacheRenderWorld != viewDef->renderWorld", + "g_shadowMapCacheMapFileCRC != viewDef->renderWorld->mapFileCRC", + "g_shadowMapCacheMapNameHash != RB_ShadowMapMapNameHash( viewDef )", + "g_shadowMapAtlasDepthImage->IsDefaulted()", + "g_shadowMapAtlasDepthImage->GetOpts().textureType != TT_2D", + "g_shadowMapAtlasDepthImage->GetOpts().format != FMT_DEPTH", + "RB_ShadowMapBuildPassSignatureForView(", + "SHADOWMAP_PASS_GLOBAL, false", + "RB_ShadowMapFindProjectedCacheEntry(", + "lightDefIndex, SHADOWMAP_PASS_GLOBAL, signature", + "slot.signature = entry->signature;", + "slot.storageGeneration = entry->atlasStorageGeneration;", + "slot.lastUpdatedFrame = entry->lastUpdatedFrame;", + "slot.valid = true;", + ), + "projected slot export requires the exact current GLOBAL signature", + ) + if "RB_ShadowMapNewestProjectedGlobalEntry(" in projected_slot_export: + raise AssertionError( + "Projected provenance export must not select a merely newest stale sibling" + ) + projected_slot_pin = braced_body( + gl, + "bool RB_ShadowMapProjectedAtlasSlotMarkUsed(", + "exact projected-atlas residency pin", + ) + require_order( + projected_slot_pin, + ( + "RB_ShadowMapFindProjectedCacheEntry(", + "lightDefIndex, SHADOWMAP_PASS_GLOBAL, signature", + "entry->atlasStorageGeneration != storageGeneration", + "entry->atlasCellX != cellX", + "entry->atlasCellY != cellY", + "entry->atlasCellSpan != cellSpan", + "return false;", + "entry->lastUsedFrame = tr.frameCount;", + "return true;", + ), + "projected slot pin revalidates signature, storage generation, and physical cell identity", + ) + + point_cube_export = braced_body( + gl, + "bool RB_ShadowMapPointCubeForLight(", + "exact point-cube provenance export", + ) + require_order( + point_cube_export, + ( + "viewDef->renderWorld == NULL", + "g_shadowMapCacheRenderWorld != viewDef->renderWorld", + "g_shadowMapCacheMapFileCRC != viewDef->renderWorld->mapFileCRC", + "g_shadowMapCacheMapNameHash != RB_ShadowMapMapNameHash( viewDef )", + "!RB_ShadowMapActivePointCacheContentReady()", + "RB_ShadowMapBuildPassSignatureForView(", + "SHADOWMAP_PASS_GLOBAL", + "entry->lightIndex != cube.lightIndex", + "entry->signature != signature", + "cube.lastUpdatedFrame = entry->lastUpdatedFrame;", + "return true;", + ), + "point cube export is scoped to exact world, light, pass, and signature", + ) + + point_cube_mark_used = braced_body( + gl, + "void RB_ShadowMapPointCubeMarkUsed(", + "exact point-cube residency pin", + ) + require_order( + point_cube_mark_used, + ( + "RB_ShadowMapActivePointCacheContentReady()", + "g_activePointShadowMapCache->lightIndex == lightDefIndex", + "lastUsedFrame = tr.frameCount;", + ), + "only the selected valid point cube can be pinned", + ) + + hybrid_available = braced_body( + gl, + "static bool RB_ShadowMapHybridAvailableForPass( const viewLight_t *vLight,\n\t\tconst shadowMapPassKind_t passKind )", + "direct ARB2 hybrid availability", + ) + require_order( + hybrid_available, + ( + "RB_ShadowMapReceiverMaskForPass( passKind )", + "vLight->shadowMapIncompleteMapMask", + "vLight->shadowMapPrelightMapMissingMask", + "incompleteMapMask & receiverMask", + "vLight->shadowMapPrelightMapMissingMask & receiverMask", + "vLight->shadowMapHybridIncompleteMask & receiverMask", + "vLight->globalShadowMapStencilSupplements != NULL", + "passKind == SHADOWMAP_PASS_GLOBAL", + "vLight->localShadowMapStencilSupplements != NULL", + ), + "direct hybrid accepts only exact ordinary supplement ownership", + ) + hybrid_stencil = braced_body( + gl, + "static bool RB_ShadowMapPrepareMappedReceiverStencil(", + "direct ARB2 mapped supplement stencil", + ) + require_order( + hybrid_stencil, + ( + "if ( !hybrid )", + "glStencilFunc( GL_ALWAYS, 128, 255 );", + "vLight->globalShadowMapStencilSupplements", + "passKind == SHADOWMAP_PASS_GLOBAL", + "vLight->localShadowMapStencilSupplements", + "globalSupplements == NULL && localSupplements == NULL", + "glScissor(", + "glClear( GL_STENCIL_BUFFER_BIT );", + "VPROG_STENCIL_SHADOW", + "RB_StencilShadowPass( globalSupplements );", + "RB_StencilShadowPass( localSupplements );", + "glStencilFunc( GL_GEQUAL, 128, 255 );", + ), + "direct hybrid clears and stamps only the missing caster supplements", + ) + direct_shadow_pass = braced_body( + gl, + "static void RB_ShadowMapRunPass(", + "direct ARB2 incomplete-map failover", + ) + map_or_hybrid = braced_body( + gl, + "static bool RB_ShadowMapMapOrHybridAvailableForPass(\n\t\tconst viewLight_t *vLight", + "shared direct/estimator completeness predicate", + ) + require_order( + map_or_hybrid, + ( + "RB_ShadowMapReceiverMaskForPass( passKind )", + "vLight->shadowMapIncompleteMapMask", + "vLight->shadowMapPrelightMapMissingMask", + "if ( !mapIncomplete )", + "RB_ShadowMapHybridAvailableForPass(", + "*hybridOut = hybrid;", + "return hybrid;", + ), + "complete maps or exact hybrids share one fail-closed predicate", + ) + require_order( + direct_shadow_pass, + ( + "RB_ShadowMapMapOrHybridAvailableForPass(", + "vLight, passKind, &hybrid", + "passKind == SHADOWMAP_PASS_GLOBAL", + "g_shadowMapGlobalPassHybrid = hybrid;", + "if ( !mapOrHybridAvailable )", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "if ( !deferReceiverDraw )", + "RB_ShadowMapSchedulePass(", + ), + "incomplete direct maps fail closed before scheduling unless hybrid-complete", + ) + if direct_shadow_pass.count("RB_ShadowMapPrepareMappedReceiverStencil(") != 2: + raise AssertionError( + "Direct ARB2 must stamp hybrid supplements on both cache reuse and fresh map paths" + ) + reuse_path = braced_body( + direct_shadow_pass, + "if ( schedule.action == SHADOWMAP_SCHEDULE_REUSE )", + "direct ARB2 cache-reuse mask failover", + ) + require_order( + reuse_path, + ( + "RB_ShadowMapPrepareMappedReceiverStencil(", + "const bool maskOk = receiverStencilReady", + "if ( maskOk )", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "RB_ShadowMapStencilFallback(", + ), + "hybrid cache reuse falls back to full stencil if the mapped mask fails", + ) + fresh_mask_start = direct_shadow_pass.find("bool maskOk = false;") + if fresh_mask_start < 0: + raise AssertionError("Missing fresh direct ARB2 receiver-mask result") + require_order( + direct_shadow_pass[fresh_mask_start:], + ( + "RB_ShadowMapPrepareMappedReceiverStencil(", + "maskOk = receiverStencilReady", + "const bool mapped =", + "if ( mapped )", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "RB_ShadowMapStencilFallback(", + ), + "fresh hybrid map mask failure falls back to full stencil", + ) + direct_entry = braced_body( + gl, + "void RB_ARB2_DrawInteractions( void )", + "direct ARB2 translucent hybrid handoff", + ) + require_order( + direct_entry, + ( + "g_shadowMapGlobalPassHybrid = false;", + "RB_ShadowMapRunPass( vLight, SHADOWMAP_PASS_GLOBAL", + "g_shadowMapGlobalPassMapped != SHADOWMAP_GLOBAL_MAPPED_NONE", + "RB_ShadowMapPrepareMappedReceiverStencil( vLight", + "SHADOWMAP_PASS_GLOBAL", + "g_shadowMapGlobalPassHybrid", + "const bool translucentMaskOk = receiverStencilReady", + "if ( translucentMaskOk )", + "else", + "RB_ShadowMapMarkStencilFallbackSticky( vLight );", + "RB_ShadowMapStencilFallback( vLight", + ), + "translucent mapped receivers restamp hybrid supplements and fail closed", + ) + + require( + modern_h, + "unsigned int\t\tarb2CacheKeyHitMask;", + "modern descriptor canonical cache-key provenance", + ) + modern_single_resource = braced_body( + modern, + "static bool R_ModernShadowPlanner_Arb2SingleResourceComplete(", + "modern single-resource ownership fail-closed gate", + ) + require_order( + modern_single_resource, + ( + "vLight->localInteractions != NULL", + "activeReceiverMask |= SHADOWMAP_RECEIVER_MASK_LOCAL;", + "vLight->globalInteractions != NULL", + "vLight->translucentInteractions != NULL", + "activeReceiverMask |= SHADOWMAP_RECEIVER_MASK_GLOBAL;", + "vLight->shadowMapIncompleteMapMask", + "vLight->shadowMapHybridIncompleteMask", + "vLight->shadowMapPrelightMapMissingMask", + "incompleteMapMask & activeReceiverMask", + "vLight->localShadowMapCasters != NULL", + "vLight->localShadowMapDynamicCasters != NULL", + "vLight->localTranslucentShadowMapCasters != NULL", + "vLight->globalShadowMapStencilSupplements != NULL", + "vLight->localShadowMapStencilSupplements != NULL", + "return activeReceiverMask != 0;", + ), + "modern projected and point resources cannot collapse receiver ownership or stencil supplements", + ) + + modern_point_resource = braced_body( + modern, + "static void R_ModernShadowPlanner_ResolveArb2AtlasSlot(", + "modern exact point-cube planner resolution", + ) + require_order( + modern_point_resource, + ( + "descriptor.arb2AtlasSlotReady = false;", + "descriptor.arb2AtlasSignature = 0;", + "descriptor.arb2AtlasStorageGeneration = 0;", + "descriptor.arb2PointCubeReady = false;", + "descriptor.arb2PointCubeSignature = 0;", + "descriptor.arb2PointCubeContentFrame = -1;", + "descriptor.arb2CacheKeyHitMask & SHADOWMAP_ARB2_CACHE_PASS_GLOBAL", + "vLight->shadowMapDynamicCasterCount == 0", + "R_ModernShadowPlanner_Arb2SingleResourceComplete( vLight )", + "if ( descriptor.pointLight )", + "RB_ShadowMapPointCubeForLight( vLight, viewDef, cube )", + "cube.size != descriptor.resolution", + "descriptor.arb2PointCubeSignature = cube.signature;", + "descriptor.arb2PointCubeContentFrame = cube.lastUpdatedFrame;", + "descriptor.arb2PointCubeReady = true;", + "RB_ShadowMapProjectedAtlasSlotForLight( vLight, viewDef, slot )", + "descriptor.arb2AtlasSignature = slot.signature;", + "descriptor.arb2AtlasStorageGeneration = slot.storageGeneration;", + "descriptor.arb2AtlasContentFrame = slot.lastUpdatedFrame;", + "descriptor.arb2AtlasSlotReady = true;", + ), + "modern descriptors require exact complete static GLOBAL physical content", + ) + require( + modern_h, + "arb2AtlasStorageGeneration;", + "modern projected-atlas generation provenance ABI", + ) + + modern_cache_isolation = braced_body( + modern, + "static bool R_ModernShadowPlanner_CanIsolateArb2CacheOwnership(", + "modern physical shadow resource isolation", + ) + require_order( + modern_cache_isolation, + ( + "descriptor.pointLight", + "descriptor.arb2PointCubeReady", + "descriptor.arb2AtlasSlotReady", + "descriptor.modernReceiverSamplingReady && !physicalResourceReady", + ), + "cache isolation follows point-cube or projected-atlas provenance", + ) + + vk_backend = read("src/renderer/Vulkan/vk_Backend.cpp") + vk_projected_slot_stub = braced_body( + vk_backend, + "bool RB_ShadowMapProjectedAtlasSlotForLight(", + "Vulkan fail-closed GL projected-slot provenance stub", + ) + require_order( + vk_projected_slot_stub, + ( + "(void)vLight; (void)viewDef;", + "memset( &slot, 0, sizeof( slot ) );", + "return false;", + ), + "Vulkan must not advertise the GL projected atlas", + ) + vk_projected_pin_stub = braced_body( + vk_backend, + "bool RB_ShadowMapProjectedAtlasSlotMarkUsed(", + "Vulkan fail-closed GL projected-slot pin stub", + ) + require_order( + vk_projected_pin_stub, + ( + "(void)lightDefIndex; (void)signature; (void)cellX; (void)cellY;", + "(void)storageGeneration; (void)cellSpan;", + "return false;", + ), + "Vulkan projected-slot pin cannot validate GL residency", + ) + vk_point_cube_stub = braced_body( + vk_backend, + "bool RB_ShadowMapPointCubeForLight(", + "Vulkan fail-closed GL point-cube provenance stub", + ) + require_order( + vk_point_cube_stub, + ( + "memset( &cube, 0, sizeof( cube ) );", + "return false;", + ), + "Vulkan must not advertise the GL point cube", ) + vk_point_mark_stub = braced_body( + vk_backend, + "void RB_ShadowMapPointCubeMarkUsed(", + "Vulkan no-op GL point-cube residency stub", + ) + require(vk_point_mark_stub, "(void)lightDefIndex;", "Vulkan point-cube pin is a no-op") - -def validate_shadow_filtering_contract() -> None: - classification = read("src/renderer/ShadowMapClassification.cpp") - shared_settings = braced_body( - classification, - "shadowMapProjectedFilterSettings_t R_ShadowMapProjectedFilterSettings(", - "shared projected shadow filter policy", + clustered = read("src/renderer/ModernClusteredLighting.cpp") + clustered_flags = braced_body( + clustered, + "static int R_ModernClusteredLighting_ShadowDescriptorFlags(", + "clustered shadow physical-resource flag", + ) + require( + clustered_flags, + "shadow.arb2AtlasSlotReady || shadow.arb2PointCubeReady", + "point cubes and projected atlas cells share the GPU provenance flag", + ) + clustered_copy = braced_body( + clustered, + "static void R_ModernClusteredLighting_CopyPlannerShadowDescriptor(", + "clustered point-cube freshness handoff", ) require_order( - shared_settings, + clustered_copy, ( - "const shadowMapLightClassification_t classification = R_ClassifyShadowMapLight( vLight );", - "settings.filterScale = settings.distantSource", - "r_shadowMapDistantFilterScale.GetFloat()", - "settings.filterRadius = Max( 0.0f, r_shadowMapFilterRadius.GetFloat() ) * settings.filterScale;", - "settings.filterTaps = idMath::ClampInt( 1, 13, r_shadowMapFilterTaps.GetInteger() );", - "settings.filterMode = idMath::ClampInt( 0, 2, r_shadowMapFilterMode.GetInteger() );", - "settings.pcssLightRadius = Max( 0.0f, r_shadowMapPCSSLightRadius.GetFloat() ) * settings.filterScale;", - "settings.pcssMaxRadius = Max( 0.0f, r_shadowMapPCSSMaxRadius.GetFloat() ) * settings.filterScale;", - "if ( settings.filterMode == 2 )", - "Max( settings.pcssLightRadius, settings.pcssMaxRadius )", + "src.pointLight", + "src.arb2PointCubeContentFrame", + "src.arb2AtlasContentFrame", + "src.arb2AtlasSlotReady || src.arb2PointCubeReady", ), - "shared projected shadow filter policy", + "clustered descriptors carry exact point or projected resource freshness", ) - - interactions = read("src/renderer/Vulkan/vk_Interactions.cpp") - shadow_slice = braced_body( - interactions, - "static int VK_Inter_WriteShadowSlice(", - "Vulkan shadow filter parameter upload", + clustered_apply = braced_body( + clustered, + "static void R_ModernClusteredLighting_ApplyShadowDescriptor(", + "clustered per-light physical-resource gate", ) require_order( - shadow_slice, + clustered_apply, ( - "r_shadowMapPointFilterRadius.GetFloat()", - "idMath::ClampInt( 1, 13, r_shadowMapPointFilterTaps.GetInteger() )", - "idMath::ClampInt( 0, 1, r_shadowMapPointFilterMode.GetInteger() )", - "r_shadowMapPointDepthCompare.GetBool() ? 1.0f : 0.0f", - "R_ShadowMapProjectedFilterSettings( state->vLight )", - "block.filterParams[ 0 ] = filterSettings.filterRadius;", - "block.filterParams[ 1 ] = (float)filterSettings.filterTaps;", - "block.filterParams[ 2 ] = (float)filterSettings.filterMode;", - "r_shadowMapDepthCompare.GetBool() && filterSettings.filterMode != 2", - "block.pcssParams[ 0 ] = filterSettings.pcssLightRadius;", - "block.pcssParams[ 1 ] = filterSettings.pcssMaxRadius;", - "block.pcssParams[ 2 ] = filterSettings.effectiveFilterRadius;", - "r_shadowMapReceiverPlaneBias.GetBool() ? 1.0f : 0.0f", + "bool projectedSlotReady = shadow->arb2AtlasSlotReady;", + "RB_ShadowMapProjectedAtlasSlotMarkUsed(", + "shadow->lightDefIndex, shadow->arb2AtlasSignature", + "shadow->arb2AtlasStorageGeneration", + "shadow->arb2AtlasCellX, shadow->arb2AtlasCellY", + "shadow->arb2AtlasCellSpan", + "const bool projectedSlotBlocked", + "projectedSlotBlocked || pointCubeBlocked", + "stats.shadowAtlasSlotBlockedLights++;", ), - "shared projected and point runtime filter upload", + "projected resource identity is revalidated before modern consumption", ) - - projected = read("src/renderer/Vulkan/shaders/interaction_shadow.frag") - projected_compare = braced_body( - projected, - "float SampleShadowCompare(", - "projected runtime depth comparison", + require_order( + clustered_apply, + ( + "const bool projectedSlotBlocked", + "const bool pointCubeBlocked", + "!shadow->arb2PointCubeReady", + "projectedSlotBlocked || pointCubeBlocked", + "MODERN_SHADOW_FALLBACK_RESOURCE_UNAVAILABLE", + "RB_ShadowMapPointCubeMarkUsed( shadow->lightDefIndex )", + ), + "clustered point lights fail closed without their exact cube", + ) + clustered_lossless = braced_body( + clustered, + "bool R_ModernClusteredLighting_FrameLossless(", + "clustered frame lossless physical-resource guard", + ) + require( + clustered_lossless, + "rg_clusteredLightingStats.shadowAtlasSlotBlockedLights == 0", + "slot pin/bind failure blocks visible modern ownership", + ) + gl_cache_gate = braced_body( + gl, + "static bool RB_ShadowMapStaticCacheable(", + "OpenGL mutable-caster cache gate", + ) + gl_cache_read_gate = braced_body( + gl, + "static bool RB_ShadowMapStaticCacheableReadOnly(", + "OpenGL mutable-caster read gate", + ) + require(gl_cache_gate, "vLight->shadowMapAlphaCasterCount > 0", "live alpha caster cache exclusion") + require(gl_cache_read_gate, "vLight->shadowMapAlphaCasterCount > 0", "read-only alpha caster cache exclusion") + stats_reset = braced_body(gl, "static void RB_ShadowMapStatsReset( void )", "shadow frame reset") + require(stats_reset, "RB_ShadowMapPrepareCacheView( backEnd.viewDef );", "per-view cache scope check") + + modern_cache_owner = braced_body( + modern_executor, + "static const viewDef_t *R_ModernGLExecutor_ShadowCacheOwnerView(", + "modern OpenGL shadow cache owner selection", ) require_order( - projected_compare, + modern_cache_owner, ( - "float compareDepth = depth - ShadowReceiverBias(cascadeIndex);", - "if (shadow.filterParams.w > 0.5)", - "texture(shadowCompareMap, vec3(uv, compareDepth))", - "texture(shadowRawMap, uv).r", - "compareDepth <= storedDepth ? 1.0 : 0.0", + "packetFrame.NumScenes() - 1", + "viewDef->renderWorld == NULL", + "!viewDef->isSubview", + "return fallback;", ), - "projected runtime compare/raw selection", + "modern OpenGL root-view cache owner selection", ) - - projected_rotation = braced_body( - projected, - "vec2 RotateShadowOffset(", - "projected stable Poisson rotation", + modern_prepare = braced_body( + modern_executor, + "void R_ModernGLExecutor_PrepareFrame(", + "modern OpenGL pre-plan cache transition", ) require_order( - projected_rotation, + modern_prepare, ( - "if (shadow.filterParams.z < 0.5)", - "StableShadowHash(vec3(", - "floor(uv / max(shadow.texelSize.x, 1.0e-6))", - "floor(depth * 1024.0)", - "* 6.2831853", - "return vec2(c * offset.x - s * offset.y, s * offset.x + c * offset.y);", + "R_ModernGLExecutor_ShadowCacheOwnerView( packetFrame )", + "RB_ShadowMapPrepareCacheView( shadowCacheOwnerView )", + "R_ModernGLExecutor_ResetPassOwnershipTable( \"frame-start\" )", + "R_ModernShadowPlanner_PrepareFrame( packetFrame, shadowPlanningRequested );", ), - "projected stable Poisson rotation", + "modern OpenGL cache transition precedes planning", ) - - receiver_bias = braced_body( - projected, - "float ShadowReceiverBias(", - "projected derivative receiver bias", + require_compact( + modern_prepare, + """rg_modernGLShadowTextureBindingsCurrent = shadowCacheOwnerView != NULL + && !RB_ShadowMapPrepareCacheView( shadowCacheOwnerView );""", + "modern GL first-frame and map-transition binding freshness assignment", + ) + modern_shadow_bind_marker = "static bool R_ModernGLExecutor_BindModernShadowTextures(" + modern_shadow_bind_definition = modern_executor.find( + modern_shadow_bind_marker, + modern_executor.find(modern_shadow_bind_marker) + len(modern_shadow_bind_marker), + ) + if modern_shadow_bind_definition < 0: + raise AssertionError("Missing modern OpenGL shadow binding definition") + modern_shadow_bindings = braced_body( + modern_executor[modern_shadow_bind_definition:], + modern_shadow_bind_marker, + "modern OpenGL transition-frame binding guard", ) require_order( - receiver_bias, + modern_shadow_bindings, ( - "float receiverPlaneBias = 0.0;", - "if (shadow.pcssParams.w > 0.5)", - "ShadowDepthGradient(cascadeIndex)", - "max(shadow.pcssParams.z, 1.0)", - "max(max(texelBias, receiverPlaneBias), 0.0)", + "RB_ShadowMapTextureBindings( bindings );", + "R_ModernGLExecutor_PointCubeDescriptorReady( bindings )", + "if ( !rg_modernGLShadowTextureBindingsCurrent )", + "bindings.projectedPersistentAtlas.ready = false;", + "bindings.pointAtlas.ready = false;", + "bindings.projectedMoments[i].ready = false;", + "bindings.pointMoments[i].ready = false;", + "if ( !exactPointCubeReady )", + "if ( bindings.projectedPersistentAtlasReady )", ), - "projected derivative receiver bias", + "modern OpenGL transition frame cannot expose stale atlas resources", ) - - blocker_search = braced_body( - projected, - "float ProjectedPCSSRadius(", - "projected PCSS blocker search", + modern_exact_point_mismatch = braced_body( + modern_shadow_bindings, + "if ( !exactPointCubeReady )", + "modern OpenGL exact point-cube mismatch clear", ) require_order( - blocker_search, + modern_exact_point_mismatch, ( - "shadow.filterParams.z < 1.5 || shadow.filterParams.w > 0.5", - "float compareDepth = depth - ShadowReceiverBias(cascadeIndex);", - "float blockerDepth = 0.0;", - "float blockerCount = 0.0;", - "float d0 = RawShadowDepth(uv);", - "float d1 = RawShadowDepth(", - "float d2 = RawShadowDepth(", - "float d3 = RawShadowDepth(", - "float d4 = RawShadowDepth(", - "if (blockerCount <= 0.0)", - "float averageBlocker = blockerDepth / blockerCount;", - "float penumbra = (depth - averageBlocker) / max(averageBlocker, 1.0e-4);", - "return clamp(max(baseRadius, penumbra * shadow.pcssParams.x), baseRadius, maxRadius);", + "bindings.pointAtlas.ready = false;", + "bindings.pointAtlasReady = false;", + "for ( int i = 0; i < RENDERER_SHADOW_TEXTURE_MOMENT_COUNT; ++i )", + "bindings.pointMoments[i].ready = false;", + "bindings.pointMomentsReady = false;", ), - "projected PCSS blocker search", + "point cube and moments fail closed together on exact-descriptor mismatch", ) - if blocker_search.count("RawShadowDepth(") != 5: - raise AssertionError("Projected PCSS blocker search must use its fixed five raw-depth probes") - projected_samples = braced_body( - projected, - "float SampleShadowCascade(", - "projected Poisson shadow filter", + modern_point_descriptor_gate = braced_body( + modern_executor, + "static bool R_ModernGLExecutor_PointCubeDescriptorReady(", + "modern GL exact point-cube descriptor gate", ) - validate_exact_filter_tiers( - projected_samples, - "SampleShadowCompare(", - "projected Poisson shadow filter", + require_order( + modern_point_descriptor_gate, + ( + "bindings.pointAtlasReady", + "R_ModernShadowPlanner_DescriptorByIndex(", + "descriptor->pointLight", + "descriptor->arb2PointCubeReady", + "bindings.pointAtlasLightIndex == descriptor->lightDefIndex", + "bindings.pointAtlasSignature", + "descriptor->arb2PointCubeSignature", + "bindings.pointAtlasContentFrame", + "descriptor->arb2PointCubeContentFrame", + "bindings.pointAtlas.width == descriptor->resolution", + "bindings.pointAtlas.height == descriptor->resolution", + "descriptor->modernReceiverSamplingReady", + "descriptor->policy == MODERN_SHADOW_POLICY_MAPPED", + "descriptor->policy == MODERN_SHADOW_POLICY_CACHE_REUSE", + ), + "modern GL advertises a point cube only for a consumable exact descriptor", ) - - projected_main = braced_body(projected, "void main()", "projected shadow fragment main") - if projected_main.count("dFdx(") != 4 or projected_main.count("dFdy(") != 4: + modern_single_point_cube = braced_body( + modern_executor, + "static bool R_ModernGLExecutor_ModernVisibleShadowReceiversReady(", + "modern GL single point-cube ownership constraint", + ) + duplicate_point_descriptor = braced_body( + modern_single_point_cube, + "if ( distinctPointLightDefs[i] == descriptor->lightDefIndex )", + "modern GL duplicate point-light descriptor deduplication", + ) + require_order( + duplicate_point_descriptor, + ("seen = true;", "break;"), + "duplicate descriptors for one point light share its cube", + ) + unseen_point_descriptor = braced_body( + modern_single_point_cube, + "if ( !seen )", + "modern GL distinct point-light accounting", + ) + require_order( + unseen_point_descriptor, + ( + "distinctPointLightDefCount < static_cast( sizeof( distinctPointLightDefs ) / sizeof( distinctPointLightDefs[0] ) )", + "distinctPointLightDefs[distinctPointLightDefCount++] = descriptor->lightDefIndex;", + "distinctPointLightOverflow = true;", + "consumablePointLights++;", + ), + "only distinct point lights consume the single-cube budget and overflow fails closed", + ) + if modern_single_point_cube.count("consumablePointLights++") != 1: raise AssertionError( - "Projected receiver-plane bias must derive all four cascade depths before selection" + "Point-cube ownership must count each distinct light exactly once" ) + point_cube_overflow = braced_body( + modern_single_point_cube, + "if ( consumablePointLights > 1 || distinctPointLightOverflow )", + "modern GL second-point-light fail-closed gate", + ) require_order( - projected_main, + point_cube_overflow, ( - "if (shadow.pcssParams.w > 0.5)", - "gShadowDepthGradients = vec4(", - "abs(dFdx(vShadowCoord0.z)) + abs(dFdy(vShadowCoord0.z))", - "abs(dFdx(vShadowCoord3.z)) + abs(dFdy(vShadowCoord3.z))", - "light *= SampleShadowFactor();", + "blockedLights += Max( 1, consumablePointLights - 1 );", + "stats.modernVisibleShadowPointConstraintLights = consumablePointLights;", ), - "projected derivative calculation before divergent cascade sampling", + "multiple distinct point lights cannot share the one bound samplerCube", ) - - point = read("src/renderer/Vulkan/shaders/interaction_shadow_point.frag") - point_compare = braced_body( - point, - "float SamplePointShadowCompare(", - "point runtime depth comparison", + require_order( + modern_single_point_cube, + ( + "if ( consumablePointLights > 1 || distinctPointLightOverflow )", + "stats.modernVisibleShadowBlockedLights = blockedLights;", + "if ( blockedLights > 0 )", + "return false;", + ), + "point-cube constraint blocks modern visible ownership", + ) + require( + modern_shader, + "mapType == MODERN_SHADOW_MAP_POINT && ModernClusterShadowFlag(descriptor, MODERN_SHADOW_FLAG_ATLAS_SLOT) && descriptor.freshness.y > 0.5", + "modern point shader rejects missing or stale cube provenance", ) + + atlas_grid = braced_body(gl, "static int RB_ShadowMapAtlasGridDim( void )", "physical atlas grid") require_order( - point_compare, + atlas_grid, ( - "float compareDepth = depth - ShadowReceiverBias();", - "if (shadow.samplingParams.x > 0.5)", - "texture(shadowCompareMap, vec4(direction, compareDepth))", - "texture(shadowRawMap, direction).r", - "compareDepth <= storedDepth ? 1.0 : 0.0", + "g_shadowMapAtlasRenderTexture->GetWidth()", + "g_shadowMapAtlasRenderTexture->GetHeight()", + "atlasSize / g_shadowMapAtlasCellSize", + "gridDim > 0", ), - "point runtime compare/raw selection", + "physical atlas grid bounds", ) - point_rotation = braced_body( - point, - "vec2 RotateShadowOffset(", - "point stable tangent-disc rotation", + def atlas_grid_dim(width: int, height: int, cell: int) -> int: + if cell <= 0: + return 0 + raw = min(width, height) // cell + return min(8, max(1, raw)) if raw > 0 else 0 + + atlas_cases = { + (4096, 2048, 512): 4, + (4095, 4096, 512): 7, + (256, 512, 512): 0, + (8192, 8192, 512): 8, + (4096, 4096, 0): 0, + } + for inputs, expected in atlas_cases.items(): + actual = atlas_grid_dim(*inputs) + if actual != expected: + raise AssertionError( + f"Physical atlas grid case {inputs} produced {actual}, expected {expected}" + ) + + atlas_find = braced_body( + gl, + "static bool RB_ShadowMapAtlasFindFreeBlock(", + "bounded atlas block allocation", + ) + + cache_expire = braced_body( + gl, + "static void RB_ShadowMapExpireCaches( void )", + "live OpenGL cache limit enforcement", ) require_order( - point_rotation, + cache_expire, ( - "if (shadow.filterParams.z < 0.5)", - "StableShadowHash(floor(direction * 37.0))", - "* 6.2831853", - "return vec2(c * offset.x - s * offset.y, s * offset.x + c * offset.y);", + "RB_ShadowMapProjectedCacheSlotLimit()", + "RB_ShadowMapPointCacheSlotLimit()", + "RB_ShadowMapProjectedCacheEntryStorageValid(", + "i >= projectedLimit", + "g_projectedShadowMapCache[i].valid = false;", + "RB_ShadowMapPointCacheEntryStorageValid(", + "i >= pointLimit", + "g_pointShadowMapCache[i].valid = false;", ), - "point stable tangent-disc rotation", + "live OpenGL cache generation, limit, and residency enforcement", + ) + cache_estimate_counts = braced_body( + gl, + "static void RB_ShadowMapArb2CacheSlotCountsReadOnly(", + "ARB2 cache-estimate live-storage counts", + ) + require_order( + cache_estimate_counts, + ( + "RB_ShadowMapProjectedCacheEntryStorageValid(", + "estimate.projectedCacheSlotsUsed++;", + "RB_ShadowMapPointCacheEntryStorageValid(", + "estimate.pointCacheSlotsUsed++;", + ), + "ARB2 cache estimates exclude generation-stale metadata", + ) + for stale_flag in ( + "g_projectedShadowMapCache[i].valid", + "g_pointShadowMapCache[i].valid", + ): + if stale_flag in cache_estimate_counts: + raise AssertionError( + "ARB2 cache estimates must validate physical storage, not raw cache metadata: " + + stale_flag + ) + require_order( + atlas_find, + ( + "span <= 0 || span > gridDim", + "y + span <= gridDim", + "x + span <= gridDim", + "occupied[y + by][x + bx]", + ), + "bounded atlas block allocation", + ) + atlas_live_occupancy = braced_body( + atlas_find, + "if ( !RB_ShadowMapProjectedCacheEntryStorageValid( &entry ) )", + "projected-atlas live-storage occupancy gate", + ) + require( + atlas_live_occupancy, + "continue;", + "stale projected-atlas generation excluded from occupancy", + ) + require_order( + atlas_find, + ( + "RB_ShadowMapProjectedCacheEntryStorageValid( &entry )", + "occupied[y][x] = true;", + "occupied[y + by][x + bx]", + ), + "only live projected-atlas storage reserves cells before free-block search", ) - point_samples = braced_body( - point, - "float SampleShadowFactor()", - "point tangent-disc shadow filter", + timed_begin = braced_body( + gl, + "static void RB_ShadowMapBeginTimedPhase(", + "non-nested OpenGL shadow timing begin", ) require_order( - point_samples, + timed_begin, ( - "vec3 direction = SafeNormalize(vPointShadowVector);", - "vec3 tangent = SafeNormalize(cross(up, direction));", - "vec3 bitangent = cross(direction, tangent);", - "float tap = shadow.filterParams.w * filterRadius;", - "tangent * o1.x + bitangent * o1.y", + "RB_ShadowMapGpuTimerQueriesAvailable()", + "R_RendererMetrics_PauseGpuTimer( timerSlot )", + "RB_ShadowMapBeginGpuTimerQuery(", + "timedPhase.gpuTimerQuery == NULL && timedPhase.parentGpuTimerPaused", + "R_RendererMetrics_ResumeGpuTimer(", ), - "point tangent-disc sampling basis", + "non-nested OpenGL shadow timing begin", ) - validate_exact_filter_tiers( - point_samples, - "SamplePointShadowCompare(", - "point tangent-disc shadow filter", + timed_end = braced_body( + gl, + "static float RB_ShadowMapEndTimedPhase(", + "non-nested OpenGL shadow timing end", + ) + require_order( + timed_end, + ( + "RB_ShadowMapEndGpuTimerQuery( timedPhase.gpuTimerQuery );", + "if ( timedPhase.parentGpuTimerPaused )", + "R_RendererMetrics_ResumeGpuTimer(", + ), + "inner query completion before parent timer resume", ) @@ -1348,7 +4091,7 @@ def validate_exact_static_cache_and_admission_contract() -> None: ("static_cast( classification.lightClass )", "light-class identity"), ("vLight->shadowMapCasterSignature", "caster-content identity"), ("resourceSize", "resource-size identity"), - ("VK_ShadowMap_PointLightFar( vLight )", "point far-envelope identity"), + ("R_ShadowMapPointFarDistance( vLight )", "point far-envelope identity"), ("vLight->lightDef->parms.lightCenter[ i ]", "point center identity"), ("vLight->globalLightOrigin[ i ]", "receiver light-origin identity"), ("vLight->lightRadius[ i ]", "light-radius identity"), @@ -1497,6 +4240,8 @@ def validate_exact_static_cache_and_admission_contract() -> None: ( "renderWorld->mapFileCRC", "VK_ShadowMap_MapNameHash( viewDef )", + "VK_ShadowMap_ProjectedCacheSlotLimit()", + "VK_ShadowMap_PointCacheSlotLimit()", "r_shadowMapResidentFrames.GetInteger()", "vkShadow.cacheRenderWorld != renderWorld", "vkShadow.cacheMapFileCRC != mapFileCRC", @@ -1504,9 +4249,11 @@ def validate_exact_static_cache_and_admission_contract() -> None: "VK_ShadowMap_ClearProjectedEntryMetadata(", "VK_ShadowMap_ClearPointEntryMetadata(", "projected.reserved = false;", + "i >= projectedLimit", "projected.generation != tr.videoRestartCount", "tr.frameCount - projected.lastUsedFrame", "point.reserved = false;", + "i >= pointLimit", "point.generation != tr.videoRestartCount", "tr.frameCount - point.lastUsedFrame", ), @@ -2054,6 +4801,18 @@ def validate_packed_shadow_geometry() -> None: def validate_fail_closed_target_and_stencil_behavior() -> None: + render_init = read("src/renderer/RenderSystem_init.cpp") + gl_backend = read("src/renderer/draw_arb2.cpp") + require( + render_init, + 'idCVar r_vkShadowFallbackTest( "r_vkShadowFallbackTest", "0"', + "default-off Vulkan shadow fallback injection", + ) + require( + read("src/renderer/RendererBootstrap.cpp"), + '{ "r_vkShadowFallbackTest", &r_vkShadowFallbackTest, 0 }', + "default-safety Vulkan shadow fallback injection", + ) executor = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") target_has_stencil = braced_body( executor, @@ -2069,6 +4828,11 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ) interactions = read("src/renderer/Vulkan/vk_Interactions.cpp") + require( + interactions, + "!r_vkShadowFallbackTest.GetBool()", + "forced missing-stencil fallback coverage", + ) draw_lights = braced_body( interactions, "void VK_Interactions_DrawLights(", @@ -2120,8 +4884,11 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: "VK_StencilShadowPass( globalGlobalVolumes );", "VK_StencilShadowPass( globalLocalVolumes );", "VK_DrawInteractionChain( vLight->globalInteractions );", + "if ( !localReceiverDrawn && vLight->localInteractions != NULL )", + "if ( !globalOpaqueReceiverDrawn && vLight->globalInteractions != NULL )", "const bool translucentUsesStencilFallback =", "const bool translucentUsesEmptyFallback =", + "const bool translucentUsesUnshadowedFallback =", "const bool drawTranslucentReceiver =", "if ( drawTranslucentReceiver )", "VK_DrawInteractionChain( vLight->translucentInteractions );", @@ -2137,7 +4904,8 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ( """const bool stencilResourcesReady = activeTargetHasStencil && - interPass.pipelineStencilShadow != VK_NULL_HANDLE;""", + interPass.pipelineStencilShadow != VK_NULL_HANDLE && + !r_vkShadowFallbackTest.GetBool();""", "stencil target and pipeline readiness", ), ( @@ -2183,18 +4951,18 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: !localReceiverDrewWithStencil ) || ( globalStencilFallback && !globalStencilPassComplete );""", - "late stencil submission fail-closed gate", + "late stencil submission degradation gate", ), ): require_compact(draw_lights, snippet, label) require( draw_lights, - "stencil shadow volume submission incomplete; affected light receivers are skipped fail-closed", + "stencil shadow volume submission incomplete; affected light receivers fall back unshadowed", "late stencil submission diagnostic", ) expected_draw_counts = { - "VK_DrawInteractionChain( vLight->localInteractions );": 3, - "VK_DrawInteractionChain( vLight->globalInteractions );": 3, + "VK_DrawInteractionChain( vLight->localInteractions );": 4, + "VK_DrawInteractionChain( vLight->globalInteractions );": 4, "VK_DrawInteractionChain( vLight->translucentInteractions );": 1, } for call, expected_count in expected_draw_counts.items(): @@ -2218,8 +4986,8 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ) require( draw_lights, - "required shadow resource unavailable; affected light receivers are skipped fail-closed", - "fail-closed shadow diagnostic", + "required shadow resource unavailable; affected light receivers fall back unshadowed", + "unshadowed shadow-degradation diagnostic", ) shared = read("src/renderer/tr_local.h") @@ -2248,6 +5016,13 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ) frontend = read("src/renderer/Interaction.cpp") + interaction_domain = read("src/renderer/ClassicInteractionDomain.cpp") + require_compact( + frontend, + """hash = R_ShadowMapHashInt( hash, entityDef->lastModifiedFrameNum ); + hash = R_ShadowMapHashInt( hash, entityDef->dynamicModelFrameCount );""", + "settled dynamic-model caster signature revision", + ) moments_support = braced_body( frontend, "static bool R_TranslucentShadowMapMomentsSupportedForLight(", @@ -2295,9 +5070,7 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: !sint->shadowStencilUsesPrelight && !linkedShadowMapCaster && ( admittedShadowMapCaster || - shadowTris != NULL ) && - ( !shadowMapCasterOnly || - admittedShadowMapCaster );""", + sint->shadowStencilEligible );""", "actual-caster map completeness provenance", ) require_compact( @@ -2309,10 +5082,18 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: shadowMapLocalLightOrigin, lightDef->parms.lightRadius );""", "point-emitter stencil generation probe", ) + require_compact( + interaction_domain, + """( geometry.skinningMode != GEOMETRY_SKINNING_NONE + && geometry.skinningMode != GEOMETRY_SKINNING_CPU )""", + "CPU-skinned shared interaction geometry admission", + ) require_compact( frontend, """const bool suppressDynamicShadowVolume = surfaceCanCastStencilShadowVolume && shadowLODAdmitted && + ( surfaceCanCastDedicatedShadowMap || + surfaceCanCastTranslucentShadowMap ) && model->IsDynamicModel() != DM_STATIC && !forcePointEmitterStencilGeneration && R_ShadowMapLightWillUseShadowMaps( lightDef );""", @@ -2320,8 +5101,8 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ) per_surface_volume_gate = braced_body( frontend, - "bool R_VulkanShadowMapsNeedPerSurfaceStencilVolumes(", - "Vulkan mapped per-surface stencil-volume policy", + "bool R_ShadowMapsNeedPerSurfaceStencilVolumes(", + "backend-neutral mapped per-surface stencil-volume policy", ) require_order( per_surface_volume_gate, @@ -2330,11 +5111,41 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: "!r_useShadowMap.GetBool()", "lightDef->parms.pointLight", "!r_shadowMapPointLights.GetBool()", - 'cvarSystem->GetCVarString( "r_actualRenderApi" )', - 'idStr::Icmp( activeRenderApi, "vulkan" ) == 0', + "return false;", + "return true;", ), - "Vulkan mapped per-surface stencil-volume policy", + "backend-neutral mapped per-surface stencil-volume policy", + ) + + require_compact( + gl_backend, + """static pointShadowMapCacheEntry_t *RB_ShadowMapAllocPointCacheEntry( + const bool allowEviction = true )""", + "GL point shadow cache non-evicting transaction allocator", + ) + require_compact( + gl_backend, + """if ( !allowEviction ) { + return NULL; + } + oldest->valid = false;""", + "GL point shadow cache transaction spill", + ) + require_compact( + gl_backend, + """schedule.pointEntry = RB_ShadowMapAllocPointCacheEntry( false );""", + "GL sealed point shadow transaction cache preservation", ) + require_compact( + gl_backend, + """schedule.projectedEntry = RB_ShadowMapAllocProjectedCacheEntry( + pass.projected.state.atlasDiv, false );""", + "GL sealed projected shadow transaction cache preservation", + ) + if "r_actualRenderApi" in per_surface_volume_gate: + raise AssertionError( + "Mapped per-surface prelight retention must not be Vulkan-only" + ) require_compact( frontend, """sint->shadowStencilUsesPrelight = @@ -2342,8 +5153,8 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: R_LightHasRealPrelightModel( lightDef->parms ) && model->IsStaticWorldModel() && r_useOptimizedShadows.GetBool() && - !R_VulkanShadowMapsNeedPerSurfaceStencilVolumes( lightDef );""", - "mapped Vulkan optimized-prelight split", + !R_ShadowMapsNeedPerSurfaceStencilVolumes( lightDef );""", + "mapped backend-neutral optimized-prelight split", ) render_system = read("src/renderer/RenderSystem.cpp") require_order( @@ -2412,7 +5223,7 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: require_order( prelight, ( - "R_VulkanShadowMapsNeedPerSurfaceStencilVolumes(", + "R_ShadowMapsNeedPerSurfaceStencilVolumes(", "return;", "idRenderModel *prelightModel = R_ViewLightPrelightModel( vLight );", ), @@ -2430,21 +5241,38 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: ), "optimized-prelight completeness resolution", ) + + map_admission = braced_body( + prelight_source, + "bool R_ShadowMapLightWillUseShadowMaps(", + "front-end shadow-map stencil-elision admission", + ) + require_compact( + map_admission, + """if ( r_useOptimizedShadows.GetBool() + && R_LightHasRealPrelightModel( lightDef->parms ) ) { + return false; + }""", + "optimized-prelight complete stencil fallback retention", + ) + if "protectStaticWorldNoSelfReceivers" in prelight: + raise AssertionError( + "Combined optimized prelights cannot conditionally lose LOCAL receiver ownership" + ) require_compact( prelight, - """const int prelightStencilOwnershipMask = - protectStaticWorldNoSelfReceivers - ? SHADOWMAP_RECEIVER_MASK_GLOBAL - : ( SHADOWMAP_RECEIVER_MASK_LOCAL | - SHADOWMAP_RECEIVER_MASK_GLOBAL );""", - "optimized-prelight receiver ownership routing", + """const bool linkedPrelightVolume = R_LinkLightSurf( + &vLight->globalShadows, + tri, NULL, light, NULL, vLight->scissorRect, true /* FIXME? */ );""", + "optimized-prelight retail global-shadow routing", ) require_compact( prelight, """vLight->shadowMapPrelightStencilReadyMask |= vLight->shadowMapPrelightStencilRequiredMask & - prelightStencilOwnershipMask;""", - "optimized-prelight ownership-specific readiness", + ( SHADOWMAP_RECEIVER_MASK_LOCAL | + SHADOWMAP_RECEIVER_MASK_GLOBAL );""", + "optimized-prelight complete receiver readiness", ) shadow_map = read("src/renderer/Vulkan/vk_ShadowMap.cpp") @@ -2477,14 +5305,25 @@ def validate_fail_closed_target_and_stencil_behavior() -> None: "void VK_ShadowMap_AbandonPreparedLights(", "prepared-shadow abandonment", ) - require_order( + release_prepared = braced_body( + shadow_map, + "static void VK_ShadowMap_ReleasePreparedLights(", + "prepared-shadow release transaction", + ) + require_compact( abandon, + "VK_ShadowMap_ReleasePreparedLights( true );", + "prepared-shadow abandonment", + ) + require_order( + release_prepared, ( - "VK_ShadowMap_MarkStencilFallbackSticky( vkShadow.lights[ i ].vLight );", - "vkShadow.lights[ i ].passes[ passIndex ].valid = false;", + "if ( markSticky && vkShadow.lights[ i ].valid )", + "VK_ShadowMap_MarkStencilFallbackSticky(", + "vkShadow.lights[ i ].passes[ passIndex ]", "vkShadow.lights[ i ].valid = false;", ), - "prepared-shadow abandonment", + "prepared-shadow release transaction", ) render = braced_body( @@ -2524,12 +5363,17 @@ def validate_ci_registration() -> None: def main() -> None: + validate_shadow_effectiveness_image_gate() + validate_shared_interaction_shadow_runtime_contract() + validate_shared_interaction_shadow_profile_contract() validate_runtime_failure_gates() validate_receiver_ownership_split() validate_shadow_depth_format_selection() validate_csm_atlas_and_receiver_contract() validate_shadow_descriptor_abi() validate_shadow_filtering_contract() + validate_point_receiver_world_bias_contract() + validate_shadow_contact_and_gl_robustness_contract() validate_exact_static_cache_and_admission_contract() validate_packed_shadow_geometry() validate_fail_closed_target_and_stencil_behavior() diff --git a/tools/tests/renderer_vulkan_world_interaction_compatibility.py b/tools/tests/renderer_vulkan_world_interaction_compatibility.py index ca962d2a..31f91a39 100644 --- a/tools/tests/renderer_vulkan_world_interaction_compatibility.py +++ b/tools/tests/renderer_vulkan_world_interaction_compatibility.py @@ -4,6 +4,7 @@ from __future__ import annotations from pathlib import Path +import re ROOT = Path(__file__).resolve().parents[2] @@ -109,13 +110,16 @@ def validate_interaction_decomposition() -> None: "surfaceShader->TestMaterialFlag( MF_POLYGONOFFSET )", "R_GlobalPointToLocal(", "R_GlobalPlaneToLocal(", + "const bool packedPBROwnerEligible = VK_PBRHasSingleClassicInteractionTopology( surf );", "if ( !lightRegs[ lightStage->conditionRegister ] )", "RB_BakeTextureMatrixIntoTexgen(", "case SL_BUMP:", - "VK_SubmitInteraction( &inter );", + "VK_SubmitInteraction( &inter, false );", "case SL_DIFFUSE:", + "VK_SubmitInteraction( &inter, false );", "case SL_SPECULAR:", - "VK_SubmitInteraction( &inter );", + "VK_SubmitInteraction( &inter, false );", + "VK_SubmitInteraction( &inter, packedPBROwnerEligible );", "VK_DrawCustomLightingStage(", "vkCmdSetDepthBiasEnable( interPass.cmd, VK_FALSE );", ), @@ -142,7 +146,7 @@ def validate_interaction_decomposition() -> None: "r_skipBump.GetBool()", "globalImages->blackImage", "globalImages->flatNormalMap", - "VK_DrawSingleInteraction( din )", + "VK_DrawSingleInteraction( din, allowNativePBR )", ): require(submit, token, "stock interaction debug substitutions") @@ -158,7 +162,7 @@ def validate_interaction_decomposition() -> None: ) require( custom_submit, - "VK_DrawSingleInteractionMode( din, parallax, scaleBias[ 0 ], scaleBias[ 1 ] );", + "VK_DrawSingleInteractionMode( din, parallax, scaleBias[ 0 ], scaleBias[ 1 ], false );", "customLighting direct interaction submission", ) @@ -206,7 +210,7 @@ def validate_light_ownership_and_draw_state() -> None: "r_skipTranslucent.GetBool()", "r_shadowMapTranslucentReceivers.GetBool()", "r_stencilTranslucentShadows.GetBool()", - "required shadow resource unavailable; affected light receivers are skipped fail-closed", + "required shadow resource unavailable; affected light receivers fall back unshadowed", ): require(draw_lights, token, "Vulkan world-light ownership and draw state") require_order( @@ -298,6 +302,359 @@ def validate_scissor_and_depth_bounds_state() -> None: "stencil shadow depth-bounds lifetime", ) + classic_draw = read("src/renderer/draw_common.cpp") + classic_shadow = braced_body( + classic_draw, + "static void RB_T_Shadow(", + "OpenGL stencil shadow pass", + ) + require_compact( + classic_shadow, + """const float minDepth = idMath::ClampFloat( 0.0f, 1.0f, + surf->scissorRect.zmin ); + const float maxDepth = idMath::ClampFloat( minDepth, 1.0f, + surf->scissorRect.zmax ); + glDepthBoundsEXT( minDepth, maxDepth );""", + "OpenGL ordered depth-bounds clamp", + ) + + +def validate_shared_interaction_consumer() -> None: + source = read("src/renderer/Vulkan/vk_Interactions.cpp") + executor = read("src/renderer/Vulkan/vk_GuiExecutor.cpp") + shadow_map = read("src/renderer/Vulkan/vk_ShadowMap.cpp") + require( + source, + '#include "../ClassicInteractionDomain.h"', + "Vulkan shared interaction contract include", + ) + + preflight = braced_body( + source, + "bool VK_ClassicInteraction_Preflight(", + "Vulkan shared interaction preflight", + ) + for token in ( + "R_ClassicInteractionDomain_FindView(", + "backEnd.renderTexture != NULL", + "backEnd.feedbackRenderTexture != NULL", + "VK_Exec_SharedInteractionTargetReady()", + "VK_Exec_InteractionPipeline(", + "VK_Exec_InteractionPipelineLayout(", + "VK_Exec_SharedInteractionGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometry(", + "VK_ClassicInteraction_ResolveDescriptor(", + "VK_Exec_InteractionUniformCheckpoint(", + "VK_Exec_InteractionUniformAlloc(", + "VK_Exec_ShadowUniformAlloc(", + "VK_Exec_SharedInteractionGeometryCommit()", + "VK_ShadowMap_PreflightClassicInteractionView( view )", + "R_ClassicInteractionDomain_LightShadowCaster(", + "VK_Exec_PrepareShadowGeometry(", + "prepared.ready = true;", + ): + require(preflight, token, "Vulkan shared interaction preflight") + for forbidden in ("vkCmdDrawIndexed(", "VK_Interactions_DrawLights("): + if forbidden in preflight: + raise AssertionError( + f"Shared interaction preflight must not submit owned work via {forbidden}" + ) + require_order( + preflight, + ( + "VK_Exec_SharedInteractionTargetReady()", + "VK_Exec_SharedInteractionGeometryCheckpoint()", + "VK_Exec_PrepareTriGeometry(", + "VK_Exec_InteractionUniformCheckpoint(", + "VK_Exec_SharedInteractionGeometryCommit()", + "prepared.ready = true;", + ), + "Vulkan target and geometry transaction before shared interaction allocation", + ) + checkpoint_tail = preflight[ + preflight.index("VK_Exec_SharedInteractionGeometryCheckpoint()") : + ] + if re.search(r"(?m)^\s*return false;\s*$", checkpoint_tail): + raise AssertionError( + "Shared interaction preflight bypasses transactional failure cleanup" + ) + + fail = braced_body( + source, + "static bool VK_ClassicInteraction_Fail(", + "Vulkan shared interaction transactional failure", + ) + require( + fail, + "VK_ShadowMap_AbortClassicInteractionView(", + "Vulkan shared mapped-shadow rollback", + ) + require( + fail, + "VK_Exec_SharedInteractionGeometryRestore();", + "Vulkan shared interaction geometry rollback", + ) + require( + fail, + "VK_Exec_InteractionUniformRestore(", + "Vulkan shared interaction uniform rollback", + ) + + geometry_checkpoint_entry = braced_body( + executor, + "bool VK_Exec_SharedInteractionGeometryCheckpoint(", + "Vulkan shared interaction geometry checkpoint entry", + ) + require( + geometry_checkpoint_entry, + "VK_Exec_SharedGeometryCheckpoint()", + "Vulkan shared interaction geometry checkpoint delegation", + ) + geometry_checkpoint = braced_body( + executor, + "static bool VK_Exec_SharedGeometryCheckpoint(", + "Vulkan shared geometry checkpoint", + ) + for token in ( + "vkExec.vertexRings[ vkExec.frameSlot ]", + "vkExec.indexRings[ vkExec.frameSlot ]", + "checkpoint.vertexCursor", + "checkpoint.indexCursor", + "checkpoint.boundVertexOffset", + "memcpy( checkpoint.vertMemo, vkExec.vertMemo", + "memcpy( checkpoint.idxMemo, vkExec.idxMemo", + "checkpoint.active = true;", + ): + require(geometry_checkpoint, token, "Vulkan bounded geometry checkpoint") + + geometry_restore_entry = braced_body( + executor, + "void VK_Exec_SharedInteractionGeometryRestore(", + "Vulkan shared interaction geometry restore entry", + ) + require( + geometry_restore_entry, + "VK_Exec_SharedGeometryRestore();", + "Vulkan shared interaction geometry restore delegation", + ) + geometry_restore = braced_body( + executor, + "static void VK_Exec_SharedGeometryRestore(", + "Vulkan shared geometry restore", + ) + for token in ( + "vkExec.vertexRings[ checkpoint.frameSlot ].cursor", + "vkExec.indexRings[ checkpoint.frameSlot ].cursor", + "vkExec.boundVertexOffset = checkpoint.boundVertexOffset", + "memcpy( vkExec.vertMemo, checkpoint.vertMemo", + "memcpy( vkExec.idxMemo, checkpoint.idxMemo", + "checkpoint.active = false;", + ): + require(geometry_restore, token, "Vulkan complete geometry restore") + + target_ready = braced_body( + executor, + "bool VK_Exec_SharedInteractionTargetReady(", + "Vulkan shared interaction main-target query", + ) + for token in ( + "vkExec.frameOpen", + "vkExec.mainScopeOpen", + "vkExec.activeRenderTexture == NULL", + "vkExec.activeColorEntry == NULL", + "vkExec.activeDepthEntry == NULL", + "vkExec.pendingSpecialEffectsView == NULL", + "vkExec.pendingSpecialEffectsSource == NULL", + "!vkExec.pendingSpecialEffectsNeedsResolve", + "VK_Exec_PipelineTargetsMatch(", + "VK_Exec_SwapchainPipelineTarget()", + ): + require(target_ready, token, "Vulkan shared interaction main-target query") + + owned = braced_body( + source, + "void VK_ClassicInteraction_DrawOwnedView(", + "Vulkan sealed shared interaction draw", + ) + receiver_draw = braced_body( + source, + "static void VK_ClassicInteraction_DrawReceiverRange(", + "Vulkan sealed shared receiver draw", + ) + shadow_draw = braced_body( + source, + "static void VK_ClassicInteraction_DrawShadowRange(", + "Vulkan sealed shared stencil draw", + ) + planned_shadow_work = braced_body( + source, + "static void VK_ClassicInteraction_CountPlannedShadowWork(", + "Vulkan receiver-order physical stencil plan", + ) + for token in ( + "VK_Exec_BindPreparedTriGeometry(", + "vkCmdBindDescriptorSets(", + "vkCmdPushConstants(", + "vkCmdDrawIndexed(", + "prepared.projectedInteractionPipeline", + "prepared.pointInteractionPipeline", + "prepared.mappedInteractionLayout", + "setCount = plan.mappedShadowMode != 0 ? 8u : 7u", + "plan.mappedShadowMode != 0 ? 2u : 1u", + ): + require(receiver_draw, token, "Vulkan sealed shared receiver draw") + for token in ( + "prepared.shadows[", + "VK_Exec_BindPreparedTriGeometry(", + "vkCmdSetStencilOp(", + "vkCmdDrawIndexed(", + "submittedShadowCasters", + "submittedLogicalVolumeDraws", + "submittedPreloadVolumeDraws", + ): + require(shadow_draw, token, "Vulkan sealed shared stencil draw") + for token in ( + "CLASSIC_INTERACTION_RECEIVER_LOCAL", + "CLASSIC_INTERACTION_RECEIVER_GLOBAL", + "CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT", + "VK_ClassicInteraction_VolumeMode(", + "preparedMode != globalMode", + "preparedMode != globalMode || !preparedIncludesLocal", + "preparedMode != translucentMode", + "else if ( !preparedIncludesLocal )", + "VK_ClassicInteraction_CountShadowRange(", + ): + require( + planned_shadow_work, + token, + "Vulkan receiver-order physical stencil plan", + ) + for token in ( + "VK_ClassicInteraction_CountPlannedShadowWork( prepared, lightPlan", + "lightLogicalVolumeDraws != light->logicalVolumeDraws", + "lightPreloadVolumeDraws != light->preloadVolumeDraws", + "prepared.logicalVolumeDrawCount != view->logicalVolumeDrawCount", + "prepared.preloadVolumeDrawCount != view->preloadVolumeDrawCount", + ): + require(preflight, token, "Vulkan physical stencil-plan reconciliation") + for forbidden in ("logicalSubmitted", "preloadSubmitted"): + if forbidden in source: + raise AssertionError( + "Vulkan physical replay coverage must not collapse receiver-order " + f"submissions through {forbidden}" + ) + require_order( + shadow_draw, + ( + "if ( caster.preload )", + "vkCmdDrawIndexed(", + "prepared.submittedPreloadVolumeDraws++", + "prepared.submittedLogicalVolumeDraws++", + ), + "Vulkan physical volume counters follow draw submission", + ) + require( + owned, + "modeChanged || !preparedVolumeIncludesLocal", + "Vulkan GLOBAL receiver rebuild after mode switch or missing LOCAL family", + ) + require_order( + owned, + ( + "preparedVolumeMode != translucentMode", + "VK_ClassicInteraction_ClearStencil( prepared, lightPlan )", + "lightPlan.firstShadow[ globalChain ]", + "lightPlan.firstShadow[ localChain ]", + ), + "Vulkan translucent receiver physical replay after shadow-mode switch", + ) + require( + owned, + "R_ClassicInteractionDomain_RecordOwned(", + "Vulkan sealed ownership reconciliation", + ) + require_order( + owned, + ( + "VK_ShadowMap_CommitClassicInteractionView( prepared.view )", + "vkCmdSetViewport(", + "R_ClassicInteractionDomain_RecordOwned(", + ), + "Vulkan mapped commit before visible shared interaction writes", + ) + for token in ( + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_GLOBAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_SUPPLEMENT_LOCAL", + "prepared.view->shadowMapPassCount", + "prepared.view->hybridShadowPassCount", + ): + require(owned, token, "Vulkan mapped/hybrid ownership submission") + require_order( + owned, + ( + "VK_ClassicInteraction_ClearStencil( prepared, lightPlan );", + "CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_GLOBAL", + "CLASSIC_INTERACTION_RECEIVER_LOCAL", + "CLASSIC_INTERACTION_SHADOW_CHAIN_STENCIL_LOCAL", + "CLASSIC_INTERACTION_RECEIVER_GLOBAL", + "CLASSIC_INTERACTION_RECEIVER_TRANSLUCENT", + "R_ClassicInteractionDomain_RecordOwned(", + ), + "Vulkan sealed stencil/receiver ownership order", + ) + for forbidden in ( + "GetStage(", + "shaderRegisters", + "legacyDrawSurf", + "legacyViewLight", + "VK_Exec_ImageDescriptor(", + "VK_Exec_InteractionUniformAlloc(", + "R_ClassicInteractionDomain_ResolveTexture(", + ): + if forbidden in owned or forbidden in receiver_draw or forbidden in shadow_draw: + raise AssertionError( + f"Shared Vulkan draw must consume sealed plans, not {forbidden}" + ) + + map_preflight = braced_body( + shadow_map, + "bool VK_ShadowMap_PreflightClassicInteractionView(", + "Vulkan mapped-shadow transaction preflight", + ) + for token in ( + "R_ClassicInteractionDomain_LightShadowMapPass(", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_GLOBAL_DYNAMIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_STATIC", + "CLASSIC_INTERACTION_SHADOW_CHAIN_MAP_LOCAL_DYNAMIC", + "sealed->mappedCasterCount", + "sealed->drawableMappedCasters", + "sealed->noopMappedCasters", + "transaction.ready = true", + ): + require(map_preflight, token, "Vulkan mapped-shadow transaction preflight") + for forbidden in ("vkCmdDraw", "vkCmdClear", "VK_ShadowMap_RenderAtlas("): + if forbidden in map_preflight: + raise AssertionError( + f"Mapped-shadow preflight must not mutate attachments via {forbidden}" + ) + + draw_view = braced_body( + executor, + "void VK_GuiExecutor_Draw3DView(", + "Vulkan 3D-view interaction ownership slot", + ) + require_order( + draw_view, + ( + "VK_ClassicInteraction_Preflight( viewDef )", + "if ( sharedWorldInteractionOwned )", + "VK_ClassicInteraction_DrawOwnedView( viewDef );", + "VK_Interactions_DrawLights( viewDef );", + ), + "Vulkan transactional shared interaction handoff", + ) + def validate_ci_registration() -> None: validator = read("tools/validation/openq4_validate.py") @@ -322,6 +679,7 @@ def main() -> None: validate_interaction_decomposition() validate_light_ownership_and_draw_state() validate_scissor_and_depth_bounds_state() + validate_shared_interaction_consumer() validate_ci_registration() print("renderer_vulkan_world_interaction_compatibility: ok") diff --git a/tools/tests/rigid_body_recovery_diagnostics.py b/tools/tests/rigid_body_recovery_diagnostics.py new file mode 100644 index 00000000..2238868d --- /dev/null +++ b/tools/tests/rigid_body_recovery_diagnostics.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Regression checks for recovered rigid-body diagnostic severity.""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +GAME_LIBS_ROOT = Path( + os.environ.get("OPENQ4_GAMELIBS_REPO", ROOT.parent / "openQ4-game") +).resolve() +RIGID_BODY_SOURCES = ( + "src/game/physics/Physics_RigidBody.cpp", + "src/mpgame/physics/Physics_RigidBody.cpp", +) + +DROP_DIAGNOSTIC = ( + "rigid body rest heuristic did not settle entity '%s' type '%s' at (%s); " + "forcing rest\\n" +) +INERTIA_DIAGNOSTIC = ( + "idPhysics_RigidBody::SetClipModel: clamping unbalanced inertia tensor for " + "entity '%s' type '%s'\\n" +) + + +def read(path: Path) -> str: + if not path.is_file(): + raise AssertionError(f"Required file not found: {path}") + return path.read_text(encoding="utf-8", errors="strict") + + +def require(text: str, needle: str, context: str) -> None: + if needle not in text: + raise AssertionError(f"Missing {needle!r} in {context}") + + +def reject_regex(text: str, pattern: str, context: str) -> None: + if re.search(pattern, text): + raise AssertionError(f"Unexpected pattern {pattern!r} in {context}") + + +def require_order(text: str, needles: tuple[str, ...], context: str) -> None: + cursor = -1 + for needle in needles: + index = text.find(needle, cursor + 1) + if index < 0: + raise AssertionError(f"Missing {needle!r} in {context}") + if index <= cursor: + raise AssertionError(f"Expected ordered token {needle!r} in {context}") + cursor = index + + +def function_body(source: str, signature: str, context: str) -> str: + signature_index = source.find(signature) + if signature_index < 0: + raise AssertionError(f"Missing function signature {signature!r} in {context}") + + opening_brace = source.find("{", signature_index + len(signature)) + if opening_brace < 0: + raise AssertionError(f"Missing function body for {signature!r} in {context}") + + depth = 0 + for index in range(opening_brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[opening_brace + 1 : index] + + raise AssertionError(f"Unbalanced function body for {signature!r} in {context}") + + +def block_body(text: str, prefix: str, context: str) -> str: + prefix_index = text.find(prefix) + if prefix_index < 0: + raise AssertionError(f"Missing block prefix {prefix!r} in {context}") + + opening_brace = text.find("{", prefix_index + len(prefix)) + if opening_brace < 0: + raise AssertionError(f"Missing block body after {prefix!r} in {context}") + + depth = 0 + for index in range(opening_brace, len(text)): + if text[index] == "{": + depth += 1 + elif text[index] == "}": + depth -= 1 + if depth == 0: + return text[opening_brace + 1 : index] + + raise AssertionError(f"Unbalanced block after {prefix!r} in {context}") + + +def validate_rigid_body_source(relative_path: str) -> None: + source_path = GAME_LIBS_ROOT / relative_path + source = read(source_path) + + drop_function = function_body( + source, + "void idPhysics_RigidBody::DropToFloorAndRest( void )", + relative_path, + ) + drop_recovery = block_body( + drop_function, + "if ( tr.fraction == 0.0f )", + f"{relative_path} drop-to-floor recovery", + ) + + require(drop_recovery, "gameLocal.DPrintf(", f"{relative_path} drop diagnostic") + require(drop_recovery, DROP_DIAGNOSTIC, f"{relative_path} drop diagnostic wording") + reject_regex( + drop_recovery, + r"gameLocal\s*\.\s*DWarning\s*\(", + f"{relative_path} recovered drop condition", + ) + require_order( + drop_recovery, + (DROP_DIAGNOSTIC, "Rest();", "dropToFloor = false;"), + f"{relative_path} recovered drop ordering", + ) + + # Starting in solid and escaping the world are not recovered placement + # heuristics; retain their warning severity. + require( + drop_function, + 'gameLocal.DWarning( "rigid body in solid for entity', + f"{relative_path} in-solid warning", + ) + require( + drop_function, + 'gameLocal.Warning( "rigid body outside world bounds for entity', + f"{relative_path} outside-world warning", + ) + + clip_function = function_body( + source, + "void idPhysics_RigidBody::SetClipModel( idClipModel *model, const float density, int id, bool freeOld )", + relative_path, + ) + inertia_recovery = block_body( + clip_function, + "if ( inertiaScale[0][0] > MAX_INERTIA_SCALE", + f"{relative_path} inertia recovery", + ) + + require(inertia_recovery, "gameLocal.DPrintf(", f"{relative_path} inertia diagnostic") + require(inertia_recovery, INERTIA_DIAGNOSTIC, f"{relative_path} inertia diagnostic wording") + reject_regex( + inertia_recovery, + r"gameLocal\s*\.\s*DWarning\s*\(", + f"{relative_path} recovered inertia condition", + ) + require_order( + inertia_recovery, + ( + INERTIA_DIAGNOSTIC, + "float min = inertiaTensor[minIndex][minIndex] * MAX_INERTIA_SCALE;", + "inertiaTensor *= inertiaScale;", + ), + f"{relative_path} inertia clamp ordering", + ) + + require( + clip_function, + 'gameLocal.Warning( "idPhysics_RigidBody::SetClipModel: invalid mass for entity', + f"{relative_path} invalid-mass warning", + ) + + +def validate_runner_wiring() -> None: + validator = read(ROOT / "tools" / "validation" / "openq4_validate.py") + require( + validator, + 'root / "tools" / "tests" / "rigid_body_recovery_diagnostics.py",', + "validation runner", + ) + + +def main() -> None: + for relative_path in RIGID_BODY_SOURCES: + validate_rigid_body_source(relative_path) + validate_runner_wiring() + print("rigid_body_recovery_diagnostics: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/settings_menu_coverage.py b/tools/tests/settings_menu_coverage.py index f1e7ec00..1900f765 100644 --- a/tools/tests/settings_menu_coverage.py +++ b/tools/tests/settings_menu_coverage.py @@ -25,6 +25,29 @@ def reject(haystack: str, needle: str, context: str) -> None: raise AssertionError(f"Unexpected {needle!r} in {context}") +def cvar_statement(source_text: str, name: str, context: str) -> str: + match = re.search( + rf'^idCVar\s+\w+\(\s*"{re.escape(name)}".*?;\s*$', + source_text, + re.IGNORECASE | re.MULTILINE, + ) + if match is None: + raise AssertionError(f"Missing CVar declaration for {name!r} in {context}") + return " ".join(match.group(0).split()) + + +def cvar_signature(source_text: str, name: str, context: str) -> tuple[str, set[str]]: + statement = cvar_statement(source_text, name, context) + match = re.match( + rf'idCVar\s+\w+\(\s*"{re.escape(name)}"\s*,\s*"([^"]*)"\s*,\s*([^,]+),', + statement, + re.IGNORECASE, + ) + if match is None: + raise AssertionError(f"Cannot parse CVar declaration for {name!r} in {context}") + return match.group(1), {flag.strip() for flag in match.group(2).split("|")} + + def gui_block(source_text: str, widget_name: str) -> str: pattern = re.compile(rf"\b(?:bindDef|choiceDef|editDef|sliderDef|windowDef)\s+{re.escape(widget_name)}\b") match = pattern.search(source_text) @@ -626,10 +649,10 @@ def validate_controls_pane_extraction(mainmenu: str, controls_gui: str) -> None: '"loadgame quick"', "clientMessageMode", '"clientMessageMode 1"', - "voteyes", - "voteno", + "_impulse28", + "_impulse29", "_ingamestats", - "ready", + "_impulse17", '"emote salute"', '"emote cheer"', '"emote taunt"', @@ -962,6 +985,35 @@ def main() -> None: for text, context in ((sp_cvars, "SP game cvars"), (mp_cvars, "MP game cvars")): require(text, 'g_autoSkipCinematics",\t\t"0",\t\t\tCVAR_GAME | PC_CVAR_ARCHIVE | CVAR_BOOL', context) + # Stock multiplayer GUIs bind these names directly. Both game modules can + # be the first module loaded by the unified executable, so each must + # register an identical typed declaration rather than leaving a typeless + # GUI-created placeholder behind. + for name in ( + "net_menulanserver", + "net_serverMenuDedicated", + "s_voiceChatSend", + "s_voiceChatReceive", + "s_voiceChatEcho", + "s_voiceVolume", + "s_micInputLevel", + ): + sp_statement = cvar_statement(sp_cvars, name, "SP game cvars") + mp_statement = cvar_statement(mp_cvars, name, "MP game cvars") + if sp_statement != mp_statement: + raise AssertionError(f"SP/MP GUI CVar declarations differ for {name!r}") + + # The modules intentionally retain Quake 4's distinct SP (box) and MP + # (cylinder) defaults, but they share an integer type so an in-process + # module switch cannot register the same global CVar with two types. + sp_default, sp_flags = cvar_signature(sp_cvars, "pm_usecylinder", "SP game cvars") + mp_default, mp_flags = cvar_signature(mp_cvars, "pm_usecylinder", "MP game cvars") + if (sp_default, mp_default) != ("0", "1"): + raise AssertionError("pm_usecylinder no longer preserves the stock SP/MP defaults") + for flags, context in ((sp_flags, "SP"), (mp_flags, "MP")): + if "CVAR_INTEGER" not in flags or "CVAR_BOOL" in flags: + raise AssertionError(f"{context} pm_usecylinder must use the shared integer type") + for cvar in ("cl_gun_x", "cl_gun_y", "cl_gun_z"): require(mp_cvars, f'idCVar {cvar}', "MP game cvar definitions") require(mp_header, f"extern idCVar\t{cvar}", "MP game cvar declarations") diff --git a/tools/tests/stock_asset_baseline.py b/tools/tests/stock_asset_baseline.py new file mode 100644 index 00000000..f37cf35f --- /dev/null +++ b/tools/tests/stock_asset_baseline.py @@ -0,0 +1,1486 @@ +#!/usr/bin/env python3 +"""Focused unit/contract tests for the P0 stock-asset baseline harness.""" + +from __future__ import annotations + +import contextlib +import importlib.util +import io +import json +import os +import struct +import sys +import tempfile +from pathlib import Path +from types import ModuleType +from zipfile import ZipFile + + +ROOT = Path(__file__).resolve().parents[2] +TOOL_PATH = ROOT / "tools" / "validation" / "stock_asset_baseline.py" + + +def load_tool() -> ModuleType: + spec = importlib.util.spec_from_file_location("openq4_stock_asset_baseline", TOOL_PATH) + if spec is None or spec.loader is None: + raise AssertionError("could not load stock_asset_baseline.py") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def cvar_value(arguments: list[str], name: str) -> str | None: + for index in range(len(arguments) - 2): + if arguments[index] == "+set" and arguments[index + 1] == name: + return arguments[index + 2] + return None + + +def timing_marker(map_name: str) -> str: + return ( + f"OPENQ4_FRAME_TIMING_V1 map={map_name} backend=opengl profile=baseline " + "cpuSamples=256 cpuP50Us=7000 cpuP95Us=11000 cpuP99Us=14000 " + "gpuAvailable=1 gpuSamples=252 gpuP50Us=5000 gpuP95Us=8000 gpuP99Us=10000" + ) + + +def patterned_rgb(width: int, height: int) -> bytes: + pixels = bytearray(width * height * 3) + for y in range(height): + for x in range(width): + index = (y * width + x) * 3 + pixels[index] = (x * 17 + y * 43 + x * y * 3) & 0xFF + pixels[index + 1] = (x * 71 + y * 11 + x * y * 5) & 0xFF + pixels[index + 2] = (x * 29 + y * 97 + x * y * 7) & 0xFF + return bytes(pixels) + + +def write_rgb_tga(path: Path, width: int, height: int, rgb: bytes) -> None: + assert len(rgb) == width * height * 3 + header = bytearray(18) + header[2] = 2 + struct.pack_into(" None: + write_rgb_tga(path, width, height, patterned_rgb(width, height)) + + +def recursive_strip_rgb(width: int, height: int) -> bytes: + assert width % 4 == 0 and height % 8 == 0 + pixels = bytearray(patterned_rgb(width, height)) + for factor in (2, 4): + target_width = width // factor + target_height = (height // 2) // factor + target_y = height - height // factor + block_pixels = factor * factor + for y in range(target_height): + for x in range(target_width): + channels = [0, 0, 0] + for block_y in range(factor): + for block_x in range(factor): + source_index = ( + ((y * factor + block_y) * width + x * factor + block_x) * 3 + ) + for channel in range(3): + channels[channel] += pixels[source_index + channel] + target_index = ((target_y + y) * width + x) * 3 + for channel in range(3): + pixels[target_index + channel] = ( + channels[channel] + block_pixels // 2 + ) // block_pixels + return bytes(pixels) + + +def scene_rgb(width: int, height: int) -> bytes: + """Low-frequency synthetic game frame with geometry, lighting and HUD detail.""" + + pixels = bytearray(width * height * 3) + for y in range(height): + for x in range(width): + index = (y * width + x) * 3 + red = 18 + 72 * x // max(1, width - 1) + 24 * y // max(1, height - 1) + green = 28 + 45 * x // max(1, width - 1) + 36 * y // max(1, height - 1) + blue = 42 + 28 * x // max(1, width - 1) + 54 * y // max(1, height - 1) + if height * 5 // 9 < y: + floor_band = ((x // max(1, width // 12)) + (y // max(1, height // 10))) & 1 + red = 48 + floor_band * 16 + 18 * x // max(1, width - 1) + green = 42 + floor_band * 12 + blue = 38 + floor_band * 9 + if width * 7 // 16 < x < width * 9 // 16 and height // 5 < y < height * 4 // 5: + red += 50 + green += 25 + blue -= 12 + distance = (x - width * 11 // 16) ** 2 + (y - height * 2 // 5) ** 2 + if distance < max(1, width // 14) ** 2: + red += 95 + green += 70 + blue += 24 + if y >= height * 9 // 10 and width // 8 < x < width * 7 // 8: + red = 18 + 80 * x // max(1, width - 1) + green = 115 + 70 * x // max(1, width - 1) + blue = 72 + pixels[index] = min(255, max(0, red)) + pixels[index + 1] = min(255, max(0, green)) + pixels[index + 2] = min(255, max(0, blue)) + return bytes(pixels) + + +def postprocess_variant(rgb: bytes, width: int, height: int) -> bytes: + """Model mild exposure drift, a one-pixel scene shift and a changed effect.""" + + output = bytearray(len(rgb)) + for y in range(height): + for x in range(width): + source_x = max(0, x - 1) + source = (y * width + source_x) * 3 + target = (y * width + x) * 3 + output[target] = min(255, rgb[source] * 94 // 100 + 8) + output[target + 1] = min(255, rgb[source + 1] * 97 // 100 + 5) + output[target + 2] = min(255, rgb[source + 2] * 92 // 100 + 10) + if width * 2 // 3 < x < width * 3 // 4 and height // 3 < y < height // 2: + output[target] = min(255, output[target] + 20) + output[target + 1] = min(255, output[target + 1] + 12) + return bytes(output) + + +def compressed_exposure_variant(rgb: bytes) -> bytes: + """Model a bright, low-contrast frame that needs shared exposure correction.""" + + return bytes( + min(255, max(0, (channel + 192) * 1000 // 2040)) for channel in rgb + ) + + +def severe_color_defect(rgb: bytes) -> bytes: + """Apply a high-correlation red cast that exposure alone cannot repair.""" + + output = bytearray(len(rgb)) + for index in range(0, len(rgb), 3): + output[index] = min(255, rgb[index] + 90) + output[index + 1] = rgb[index + 1] // 2 + output[index + 2] = rgb[index + 2] // 2 + return bytes(output) + + +def feedback_corruption(tool: ModuleType, rgb: bytes, width: int, height: int) -> bytes: + """Overlay offset recursive views without matching the legacy strip signature.""" + + output = bytearray(rgb) + for target_x, target_y, target_width, target_height in ( + (5, height // 4, width * 3 // 4, height * 3 // 4), + (2, height * 11 // 16, width * 3 // 8, height * 5 // 16), + ): + scaled = tool.center_aspect_resample_rgb( + rgb, width, height, target_width, target_height + ) + for y in range(target_height): + if target_y + y >= height: + break + source = y * target_width * 3 + target = ((target_y + y) * width + target_x) * 3 + copy_width = min(target_width, width - target_x) + output[target : target + copy_width * 3] = scaled[ + source : source + copy_width * 3 + ] + return bytes(output) + + +def test_save_preview_coherence(tool: ModuleType, base: Path) -> None: + screenshot = base / "coherent-shot.tga" + preview = base / "coherent-preview.tga" + screenshot_width, screenshot_height = 640, 360 + preview_width, preview_height = tool.SP_SAVE_PREVIEW_DIMENSIONS + screenshot_rgb = scene_rgb(screenshot_width, screenshot_height) + preview_rgb = tool.center_aspect_resample_rgb( + screenshot_rgb, + screenshot_width, + screenshot_height, + preview_width, + preview_height, + ) + write_rgb_tga(screenshot, screenshot_width, screenshot_height, screenshot_rgb) + write_rgb_tga(preview, preview_width, preview_height, preview_rgb) + comparison, failure = tool.save_preview_comparison(screenshot, preview) + assert failure is None and comparison is not None + assert comparison["referenceArtifact"] == "saveReferenceScreenshot" + assert comparison["algorithm"].startswith("same-state-") + assert comparison["lumaCorrelation"] > 0.99 + + write_rgb_tga( + preview, + preview_width, + preview_height, + postprocess_variant(preview_rgb, preview_width, preview_height), + ) + variant_comparison, failure = tool.save_preview_comparison(screenshot, preview) + assert failure is None and variant_comparison is not None + assert variant_comparison["lumaCorrelation"] > tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + + write_rgb_tga( + preview, + preview_width, + preview_height, + compressed_exposure_variant(preview_rgb), + ) + exposure_comparison, failure = tool.save_preview_comparison(screenshot, preview) + assert failure is None and exposure_comparison is not None + assert ( + exposure_comparison["meanAbsoluteRgbError"] + > tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ) + assert ( + exposure_comparison["exposureCompensatedMeanAbsoluteRgbError"] + <= tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ) + + write_rgb_tga( + preview, + preview_width, + preview_height, + severe_color_defect(preview_rgb), + ) + color_comparison, failure = tool.save_preview_comparison(screenshot, preview) + assert color_comparison is not None + assert color_comparison["lumaCorrelation"] > tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + assert failure is not None and "exposure-compensated" in failure + + corrupted_rgb = feedback_corruption( + tool, preview_rgb, preview_width, preview_height + ) + write_rgb_tga(preview, preview_width, preview_height, corrupted_rgb) + assert tool.validate_tga(preview, tool.SP_SAVE_PREVIEW_DIMENSIONS) is None + corrupted_comparison, failure = tool.save_preview_comparison(screenshot, preview) + assert corrupted_comparison is not None + assert failure is not None and "luma correlation" in failure + + final6 = ( + ROOT + / ".tmp" + / "stock-baseline" + / "p0-20260819-save-preview-final6-sp-default" + / "savepaths" + / "sp" + / "baseoq4" + ) + final6_screenshot = final6 / "screenshots" / "stock-baseline" / "sp_after_load.tga" + final6_preview = final6 / "savegames" / "StockBaselineSP.tga" + final6_comparison = None + if final6_screenshot.is_file() and final6_preview.is_file(): + final6_comparison, failure = tool.save_preview_comparison( + final6_screenshot, final6_preview + ) + assert final6_comparison is not None and failure is None + assert final6_comparison["lumaCorrelation"] >= tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + assert ( + final6_comparison["exposureCompensatedMeanAbsoluteRgbError"] + <= tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ) + + legacy_preview = ( + ROOT + / ".tmp" + / "stock-baseline" + / "p0-20260819-save-preview-final6-sp-legacy50" + / "savepaths" + / "sp" + / "baseoq4" + / "savegames" + / "StockBaselineSP.tga" + ) + if legacy_preview.is_file(): + default_decoded = tool.decode_tga_rgb(final6_preview.read_bytes()) + legacy_decoded = tool.decode_tga_rgb(legacy_preview.read_bytes()) + assert not isinstance(default_decoded, str) + assert not isinstance(legacy_decoded, str) + default_width, default_height, default_rgb = default_decoded + legacy_width, legacy_height, legacy_rgb = legacy_decoded + analysis_width, analysis_height = tool.SP_SAVE_PREVIEW_ANALYSIS_DIMENSIONS + default_normalized = tool.center_aspect_resample_rgb( + default_rgb, + default_width, + default_height, + analysis_width, + analysis_height, + ) + legacy_normalized = tool.center_aspect_resample_rgb( + legacy_rgb, + legacy_width, + legacy_height, + analysis_width, + analysis_height, + ) + preview_metrics = tool.rgb_similarity_metrics( + default_normalized, legacy_normalized + ) + assert not isinstance(preview_metrics, str) + assert preview_metrics[0] >= tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + compensated_metrics = tool.exposure_compensated_rgb_metrics( + default_normalized, legacy_normalized + ) + assert not isinstance(compensated_metrics, str) + assert compensated_metrics[2] <= tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + + final12 = ( + ROOT + / ".tmp" + / "stock-baseline" + / "p0-20260819-final12" + / "savepaths" + / "sp" + / "baseoq4" + ) + final12_screenshot = final12 / "screenshots" / "stock-baseline" / "sp_after_load.tga" + final12_preview = final12 / "savegames" / "StockBaselineSP.tga" + if final12_screenshot.is_file() and final12_preview.is_file(): + assert tool.validate_tga( + final12_preview, tool.SP_SAVE_PREVIEW_DIMENSIONS + ) is None + final12_comparison, failure = tool.save_preview_comparison( + final12_screenshot, final12_preview + ) + assert final12_comparison is not None and failure is None + assert final12_comparison["lumaCorrelation"] >= tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + assert ( + final12_comparison["meanAbsoluteRgbError"] + > tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ) + assert ( + final12_comparison["exposureCompensatedMeanAbsoluteRgbError"] + <= tool.SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ) + + final11 = ( + ROOT + / ".tmp" + / "stock-baseline" + / "p0-20260819-final11" + / "savepaths" + / "sp" + / "baseoq4" + ) + final11_screenshot = final11 / "screenshots" / "stock-baseline" / "sp_after_load.tga" + final11_preview = final11 / "savegames" / "StockBaselineSP.tga" + if final11_screenshot.is_file() and final11_preview.is_file(): + final11_comparison, failure = tool.save_preview_comparison( + final11_screenshot, final11_preview + ) + assert final11_comparison is not None + assert final11_comparison["lumaCorrelation"] < tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + assert failure is not None and "luma correlation" in failure + if final6_comparison is not None: + assert ( + final6_comparison["lumaCorrelation"] + - final11_comparison["lumaCorrelation"] + >= 0.10 + ) + + +def test_asset_inventory_and_comparison(tool: ModuleType, base: Path) -> None: + asset_root = base / "assets" + q4base = asset_root / "q4base" + q4mp = asset_root / "q4mp" + q4base.mkdir(parents=True) + q4mp.mkdir() + (q4base / "pak001.pk4").write_bytes(b"stock-one") + (q4base / "zpak_english.pk4").write_bytes(b"language") + (q4mp / "game300.pk4").write_bytes(b"mp") + + records = tool.collect_pk4s(asset_root) + assert [item["path"] for item in records] == [ + "q4base/pak001.pk4", + "q4base/zpak_english.pk4", + "q4mp/game300.pk4", + ] + assert tool.compare_asset_records(records, records) == [] + changed = json.loads(json.dumps(records)) + changed[0]["sha256"] = "0" * 64 + failures = tool.compare_asset_records(changed, records) + assert len(failures) == 1 and "SHA" not in failures[0] and "sha256 differs" in failures[0] + (q4base / "generated.cfg").write_text("set test 1", encoding="utf-8") + loose = tool.collect_loose_asset_files(asset_root) + assert [item["path"] for item in loose] == ["q4base/generated.cfg"] + + contaminated = base / "contaminated" + (contaminated / "q4base").mkdir(parents=True) + (contaminated / "q4base" / "pak001.pk4").write_bytes(b"stock") + (contaminated / "baseoq4").mkdir() + (contaminated / "baseoq4" / "override.cfg").write_text("override", encoding="utf-8") + try: + tool.collect_pk4s(contaminated) + except ValueError as exc: + assert "non-empty" in str(exc) and "unapproved content" in str(exc) + else: + raise AssertionError("non-empty asset-root/baseoq4 must fail closed") + + original_link_check = tool.is_link_or_junction + tool.is_link_or_junction = lambda path: path.name == "q4base" + try: + try: + tool.validate_asset_root(asset_root) + except ValueError as exc: + assert "--allow-asset-dir-links" in str(exc) + else: + raise AssertionError("linked asset directories require explicit opt-in") + tool.validate_asset_root(asset_root, allow_asset_dir_links=True) + views = tool.asset_directory_views(asset_root) + assert next(view for view in views if view["path"] == "q4base")["linked"] is True + finally: + tool.is_link_or_junction = original_link_check + + stale = base / "stale-evidence" + stale.mkdir() + (stale / "old.log").write_text("stale", encoding="utf-8") + try: + tool.prepare_output_directory(stale) + except ValueError as exc: + assert "new or empty" in str(exc) + else: + raise AssertionError("non-empty evidence directories must be rejected") + + collision_assets = base / "collision-assets" + collision_runtime = base / "collision-runtime" + (collision_assets / "q4base").mkdir(parents=True) + (collision_runtime / "baseoq4").mkdir(parents=True) + with ZipFile(collision_assets / "q4base" / "pak001.pk4", "w") as archive: + archive.writestr("guis/mainmenu.gui", "retail") + archive.writestr("materials/retail_only.mtr", "retail") + with ZipFile(collision_runtime / "baseoq4" / "pak0.pk4", "w") as archive: + archive.writestr("GUIS/MainMenu.gui", "overlay") + archive.writestr("materials/openq4_only.mtr", "overlay") + collision_records = tool.collect_retail_path_collisions( + collision_assets, + tool.collect_pk4s(collision_assets), + collision_runtime, + tool.collect_overlay_pk4s(collision_runtime), + ) + assert collision_records == [ + { + "path": "guis/mainmenu.gui", + "retailPk4s": ["q4base/pak001.pk4"], + "openQ4OverlayPk4s": ["baseoq4/pak0.pk4"], + } + ] + report_dir = base / "collision-report" + report_dir.mkdir() + tool.write_reports( + report_dir, + { + "schemaVersion": tool.SCHEMA_VERSION, + "status": "pass", + "generatedUtc": "2026-08-19T00:00:00Z", + "git": tool.git_state(ROOT), + "mpPort": 28140, + "runtimeRoot": str(collision_runtime.resolve()), + "runtimeFiles": [], + "expectedAssets": {"supplied": True, "sha256": "1" * 64}, + "assets": { + "compatibilityModel": tool.ASSET_COMPATIBILITY_MODEL, + "root": str(collision_assets.resolve()), + "allowLinkedGameDirectories": False, + "directoryViews": tool.asset_directory_views(collision_assets), + "stockPk4s": tool.collect_pk4s(collision_assets), + "looseFiles": [], + "openQ4OverlayPk4s": tool.collect_overlay_pk4s(collision_runtime), + "openQ4OverlayLooseFiles": [], + "retailArchiveBytesMatchExpected": True, + "retailPathNamespaceUntouched": False, + "retailPathCollisionCount": 1, + "retailPathCollisions": collision_records, + }, + "assetComparisonFailures": [], + "preflightFailures": [], + "postCaptureVerificationFailures": [], + "results": [], + }, + ) + report_markdown = (report_dir / "stock_asset_baseline_report.md").read_text( + encoding="utf-8" + ) + assert "not an overlay-free or stock-only run" in report_markdown + assert "`baseoq4/pak0.pk4` supersedes 1 retail virtual path" in report_markdown + assert "clean logs" not in report_markdown.casefold() + assert "none of the harness denylist classes" in report_markdown + assert "engine `ERROR` records" in report_markdown + assert "shader compile/program-link failures" in report_markdown + assert "Vulkan validation messages or VUIDs" in report_markdown + assert "OpenGL errors" in report_markdown + assert "does not assert warning-free logs" in report_markdown + + +def test_plan_is_windowed_and_engine_only(tool: ModuleType, base: Path) -> None: + root = base / "repo" + output = base / "evidence" + asset_root = base / "assets" + (root / ".install").mkdir(parents=True) + plans = tool.prepare_plans(root, asset_root, output, 1280, 720, 28140) + assert set(plans) == {"sp-capture", "sp-demo-playback", "mp-server", "mp-client"} + for plan in plans.values(): + assert cvar_value(plan.args, "r_fullscreen") == "0" + assert cvar_value(plan.args, "r_borderless") == "0" + assert cvar_value(plan.args, "r_borderlessDefaultMigrated") == "1" + assert cvar_value(plan.args, "r_fullscreenDesktop") == "0" + assert cvar_value(plan.args, "r_windowWidth") == "1280" + assert cvar_value(plan.args, "r_windowHeight") == "720" + assert cvar_value(plan.args, "r_mode") == "-1" + assert cvar_value(plan.args, "r_customWidth") == "1280" + assert cvar_value(plan.args, "r_customHeight") == "720" + assert cvar_value(plan.args, "r_renderApi") == "gl" + assert cvar_value(plan.args, "r_rendererSharedGui") == "0" + assert cvar_value(plan.args, "r_rendererSharedInWorldGui") == "0" + assert cvar_value(plan.args, "r_rendererSharedCinematicPost") == "0" + assert cvar_value(plan.args, "r_rendererSharedSpecialFrame") == "0" + assert cvar_value(plan.args, "r_rendererSharedWorldAmbient") == "0" + assert cvar_value(plan.args, "r_rendererSharedWorldInteraction") == "0" + assert cvar_value(plan.args, "r_rendererSharedWorldFogBlend") == "0" + assert cvar_value(plan.args, "r_rendererSharedSubview") == "0" + assert cvar_value(plan.args, "r_rendererSharedDeform") == "0" + assert cvar_value(plan.args, "r_rendererBenchmarkPreset") == "baseline" + assert cvar_value(plan.args, "r_rendererMetrics") == "0" + assert cvar_value(plan.args, "r_rendererGpuTimers") == "1" + assert cvar_value(plan.args, "fs_devpath") == str(plan.savepath) + assert cvar_value(plan.args, "fs_cdpath") is None + assert plan.args.count("+vid_restart") == 1 + assert plan.args.index("+vid_restart") > plan.args.index("r_windowHeight") + record = tool.plan_record(plan) + assert record["windowed"] is True + assert record["captureMethod"] == "engine screenshot command" + if plan.role_id.startswith("mp-"): + assert cvar_value(plan.args, "ui_autoJoin") == "1" + assert plan.args.index("ui_autoJoin") < plan.args.index("+vid_restart") + if plan.role_id == "mp-server": + assert cvar_value(plan.args, "si_pure") == "1" + assert cvar_value(plan.args, "net_serverAllowServerMod") == "0" + + sp_stage1 = (output / "savepaths" / "sp" / "baseoq4" / "stock-baseline" / "sp_stage1.cfg").read_text(encoding="utf-8") + sp_stage2 = (output / "savepaths" / "sp" / "baseoq4" / "stock-baseline" / "sp_after_load.cfg").read_text(encoding="utf-8") + for token in ("recordDemo stock_baseline_sp", "saveGame StockBaselineSP", "loadGame StockBaselineSP"): + assert token in sp_stage1 + sp_stage1_lines = sp_stage1.splitlines() + save_reference_line = sp_stage1_lines.index( + 'screenshot "screenshots/stock-baseline/sp_before_save.tga"' + ) + save_game_line = sp_stage1_lines.index("saveGame StockBaselineSP") + assert save_reference_line + 1 == save_game_line + assert plans["sp-capture"].expected.count( + ( + "saveReferenceScreenshot", + "baseoq4/screenshots/stock-baseline/sp_before_save.tga", + ) + ) == 1 + assert "OPENQ4_STOCK_BASELINE_SP_SAVE_LOAD_COMPLETE" in sp_stage2 + assert 'screenshot "screenshots/stock-baseline/sp_after_load.tga"' in sp_stage2 + mp_server_cfg = (output / "savepaths" / "mp-server" / "baseoq4" / "stock-baseline" / "server.cfg").read_text(encoding="utf-8") + mp_client_cfg = (output / "savepaths" / "mp-client" / "baseoq4" / "stock-baseline" / "client.cfg").read_text(encoding="utf-8") + assert "waitMsec 120000" in mp_server_cfg + assert "waitMsec 10000" in mp_client_cfg + assert "rendererBenchmarkCapture" in mp_server_cfg + assert "rendererBenchmarkCapture" in mp_client_cfg + assert "r_rendererMetrics 1" in mp_server_cfg + assert "r_rendererMetrics 0" in mp_server_cfg + + def assert_canonical_display_before_sampling(payload: str) -> None: + lines = payload.splitlines() + reset_index = lines.index("framePacingReset") + for name, value in tool.display_contract()["cvars"].items(): + matches = [line for line in lines[:reset_index] if line.startswith(f"{name} ")] + assert matches == [f"{name} {value}"] + + assert_canonical_display_before_sampling(sp_stage2) + assert_canonical_display_before_sampling(mp_server_cfg) + assert_canonical_display_before_sampling(mp_client_cfg) + assert "openq4_joinGame" not in mp_client_cfg + assert "openq4_assertMPClientActive" in mp_client_cfg + assert "openq4_assertMPGameplayView" in mp_client_cfg + mp_client_lines = mp_client_cfg.splitlines() + screenshot_line = mp_client_lines.index('screenshot "screenshots/stock-baseline/mp_client.tga"') + assertion_lines = [ + index for index, line in enumerate(mp_client_lines) if line == "openq4_assertMPClientActive" + ] + assert len(assertion_lines) == 3 + assert assertion_lines[-2] < screenshot_line < assertion_lines[-1] + view_assertion_lines = [ + index for index, line in enumerate(mp_client_lines) if line == "openq4_assertMPGameplayView" + ] + assert len(view_assertion_lines) == 3 + assert view_assertion_lines[-2] < screenshot_line < view_assertion_lines[-1] + assert mp_client_lines[view_assertion_lines[-1] + 1] == ( + "echo OPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE" + ) + assert "expected exact budget workload" in tool.runtime_window_failure( + " [0] * Test Display (contentScale 1.50)\nMODE: -1, 1920 x 1080 windowed hz:N/A\n", + 1280, + 720, + ) + assert tool.runtime_window_failure( + "MODE: -1, 1280 x 720 windowed hz:N/A\n", 1280, 720 + ) is None + assert "selector" in tool.runtime_window_failure( + "MODE: 5, 1280 x 720 windowed hz:N/A\n", 1280, 720 + ) + assert "borderless" in tool.runtime_window_failure( + "MODE: 5, 2560 x 1440 borderless hz:N/A\n", 1280, 720 + ) + assert "missing" in tool.runtime_window_failure("no renderer evidence\n") + assert "screenshot-write" in tool.mp_client_active_proof_failure( + tool.MP_CLIENT_ACTIVE_MARKER + "\nOPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE\n" + ) + legacy_active_line = ( + tool.MP_CLIENT_ACTIVE_MARKER + + " client=1 spectating=0 wantSpectate=0 ingame=1\n" + ) + view_line = tool.MP_CLIENT_VIEW_MARKER + " gui=0\n" + screenshot_write = "Wrote screenshots/stock-baseline/mp_client.tga\n" + assert "two exact" in tool.mp_client_active_proof_failure( + legacy_active_line + + view_line + + screenshot_write + + legacy_active_line + + view_line + + "OPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE\n" + ) + active_line = ( + tool.MP_CLIENT_ACTIVE_MARKER + + " client=1 spectating=0 wantSpectate=0 ingame=1 menu=0 disableHud=0\n" + ) + completion_line = "OPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE\n" + assert tool.mp_client_active_proof_failure( + active_line + view_line + screenshot_write + active_line + view_line + completion_line + ) is None + assert "bracket" in tool.mp_client_active_proof_failure( + active_line + view_line + active_line + view_line + screenshot_write + completion_line + ) + assert "bracket" in tool.mp_client_active_proof_failure( + screenshot_write + active_line + view_line + active_line + view_line + completion_line + ) + assert "2560x1440" in tool.runtime_window_failure( + " [0] * Test Display (contentScale 1.50)\nMODE: -1, 2560 x 1440 windowed hz:N/A\n", + 1280, + 720, + ) + + with contextlib.redirect_stderr(io.StringIO()): + try: + tool.parse_args(["--asset-root", str(asset_root), "--width", "640", "--height", "480"]) + except SystemExit as exc: + assert exc.code == 2 + else: + raise AssertionError("noncanonical real baseline dimensions must fail") + dry_custom = tool.parse_args( + [ + "--asset-root", + str(asset_root), + "--dry-run", + "--width", + "640", + "--height", + "480", + ] + ) + assert dry_custom.dry_run and (dry_custom.width, dry_custom.height) == (640, 480) + + source = TOOL_PATH.read_text(encoding="utf-8") + for forbidden in ("pyautogui", "ImageGrab", "BitBlt", "PrintWindow", "mss.mss", "pynput"): + assert forbidden not in source + + +def test_diagnostic_authority_and_shared_mp_deadline( + tool: ModuleType, base: Path +) -> None: + log = base / "authoritative.log" + stdout = base / "mirrored.stdout.txt" + stderr = base / "mirrored.stderr.txt" + active_line = ( + tool.MP_CLIENT_ACTIVE_MARKER + + " client=1 spectating=0 wantSpectate=0 ingame=1 menu=0 disableHud=0\n" + ) + view_line = tool.MP_CLIENT_VIEW_MARKER + " gui=0\n" + screenshot_write = "Wrote screenshots/stock-baseline/mp_client.tga\n" + completion_line = "OPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE\n" + valid_sequence = ( + active_line + + view_line + + screenshot_write + + active_line + + view_line + + completion_line + ) + log.write_text(valid_sequence, encoding="utf-8") + stdout.write_text(valid_sequence, encoding="utf-8") + stderr.write_text("", encoding="utf-8") + authoritative, all_diagnostics = tool.collect_role_diagnostics( + log, stdout, stderr + ) + assert authoritative == valid_sequence + assert all_diagnostics.count(screenshot_write.strip()) == 2 + assert tool.mp_client_active_proof_failure(authoritative) is None + assert "exactly one" in tool.mp_client_active_proof_failure(all_diagnostics) + + # A valid mirrored stdout sequence must not repair broken ordering in the + # authoritative engine log. + log.write_text( + active_line + view_line + active_line + view_line + screenshot_write + completion_line, + encoding="utf-8", + ) + authoritative, _ = tool.collect_role_diagnostics(log, stdout, stderr) + assert "bracket" in tool.mp_client_active_proof_failure(authoritative) + + clock = [100.0] + kill_times: list[float] = [] + + class HangingProcess: + def poll(self) -> None: + return None + + def kill(self) -> None: + kill_times.append(clock[0]) + + def wait(self, timeout: int) -> int: + assert timeout == 10 + return -9 + + original_monotonic = tool.time.monotonic + original_sleep = tool.time.sleep + tool.time.monotonic = lambda: clock[0] + tool.time.sleep = lambda seconds: clock.__setitem__(0, clock[0] + seconds) + try: + results = tool.wait_processes_until( + {"server": HangingProcess(), "client": HangingProcess()}, 101.0 + ) + finally: + tool.time.monotonic = original_monotonic + tool.time.sleep = original_sleep + assert results == {"server": (-9, True), "client": (-9, True)} + assert len(kill_times) == 2 + assert all(abs(kill_time - 101.0) < 1e-9 for kill_time in kill_times) + + +def make_runtime_package(tool: ModuleType, runtime_dir: Path) -> tuple[Path, list[dict[str, object]]]: + game_dir = runtime_dir / "baseoq4" + game_dir.mkdir(parents=True) + suffix = ".exe" if os.name == "nt" else "" + shared = ".dll" if os.name == "nt" else (".dylib" if sys.platform == "darwin" else ".so") + executable = runtime_dir / f"openQ4-client_{tool.host_arch()}{suffix}" + executable.write_bytes(b"client") + (runtime_dir / f"openQ4-ded_{tool.host_arch()}{suffix}").write_bytes(b"dedicated") + (runtime_dir / f"openQ4-client_{tool.host_arch()}.pdb").write_bytes(b"symbols") + (runtime_dir / f"renderer-gl_{tool.host_arch()}{shared}").write_bytes(b"renderer") + (runtime_dir / f"OpenAL32{shared}").write_bytes(b"audio") + (game_dir / f"game-sp_{tool.host_arch()}{shared}").write_bytes(b"sp") + (game_dir / f"game-mp_{tool.host_arch()}{shared}").write_bytes(b"mp") + (game_dir / "mod.json").write_text("{}", encoding="utf-8") + (game_dir / f"game-sp_{tool.host_arch()}.pdb").write_bytes(b"symbols") + records = tool.collect_runtime_files(runtime_dir, executable) + assert {item["kind"] for item in records} >= { + "clientExecutable", + "singlePlayerGameModule", + "multiplayerGameModule", + "dedicatedServerExecutable", + "diagnosticSymbols", + "runtimeLibrary", + } + loose, unexpected = tool.collect_overlay_loose_files(runtime_dir, records) + assert unexpected == [] and any(item["path"] == "baseoq4/mod.json" for item in loose) + unexpected_content = game_dir / "maps" / "override.map" + unexpected_content.parent.mkdir() + unexpected_content.write_text("override", encoding="utf-8") + _, unexpected = tool.collect_overlay_loose_files(runtime_dir, records) + assert any("override.map" in failure for failure in unexpected) + unexpected_content.unlink() + return executable, records + + +def test_tga_and_report_verification(tool: ModuleType, base: Path) -> None: + screenshot = base / "shot.tga" + write_minimal_tga(screenshot) + assert tool.validate_tga(screenshot) is None + assert tool.validate_tga(screenshot, (16, 8)) is None + assert "differ" in tool.validate_tga(screenshot, (32, 16)) + screenshot.write_bytes(b"short") + assert tool.validate_tga(screenshot) == "TGA header is truncated" + blank = base / "blank.tga" + write_rgb_tga(blank, 16, 8, bytes(16 * 8 * 3)) + assert "blank or near-solid" in tool.validate_tga(blank) + recursive = base / "recursive.tga" + write_rgb_tga(recursive, 64, 32, recursive_strip_rgb(64, 32)) + assert "recursive scaled-strip" in tool.validate_tga(recursive) + malformed = base / "malformed.tga" + write_minimal_tga(malformed) + malformed.write_bytes(malformed.read_bytes() + b"trailing") + assert "payload length" in tool.validate_tga(malformed) + + asset_root = base / "verify-assets" + (asset_root / "q4base").mkdir(parents=True) + stock = asset_root / "q4base" / "pak001.pk4" + stock.write_bytes(b"retail") + asset_records = tool.collect_pk4s(asset_root) + runtime_dir = base / "runtime" + runtime_executable, runtime_records = make_runtime_package(tool, runtime_dir) + overlay_loose, unexpected = tool.collect_overlay_loose_files(runtime_dir, runtime_records) + assert unexpected == [] + report_width, report_height = tool.DEFAULT_WIDTH, tool.DEFAULT_HEIGHT + expected_assets_path = base / "expected-assets.json" + expected_assets_path.write_text( + json.dumps( + { + "schemaVersion": 1, + "allowLinkedGameDirectories": False, + "directoryViews": tool.asset_directory_views(asset_root), + "stockPk4s": asset_records, + "looseFiles": [], + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + budget_contract, budget_binding = tool.load_contract(tool.DEFAULT_CONTRACT_PATH) + report: dict[str, object] = { + "schemaVersion": tool.SCHEMA_VERSION, + "status": "pass", + "dryRun": False, + "git": tool.git_state(ROOT), + "budgetContract": budget_binding, + "mpPort": 28140, + "runtimeRoot": str(runtime_dir.resolve()), + "expectedAssets": { + "supplied": True, + "path": str(expected_assets_path.resolve()), + "sha256": tool.sha256_file(expected_assets_path), + }, + "runtimeFiles": runtime_records, + "safety": { + "windowedOnly": True, + "borderless": False, + "windowSize": {"width": report_width, "height": report_height}, + "displayContract": tool.display_contract(), + "engineScreenshotOnly": True, + "operatingSystemCapture": False, + "inputInjection": False, + }, + "assets": { + "compatibilityModel": tool.ASSET_COMPATIBILITY_MODEL, + "root": str(asset_root.resolve()), + "allowLinkedGameDirectories": False, + "directoryViews": tool.asset_directory_views(asset_root), + "stockPk4s": asset_records, + "looseFiles": [], + "openQ4OverlayPk4s": [], + "openQ4OverlayLooseFiles": overlay_loose, + "retailArchiveBytesMatchExpected": True, + "retailPathNamespaceUntouched": True, + "retailPathCollisionCount": 0, + "retailPathCollisions": [], + }, + "assetComparisonFailures": [], + "preflightFailures": [], + "postCaptureVerificationFailures": [], + "plan": [], + "results": [], + } + plans: list[dict[str, object]] = report["plan"] # type: ignore[assignment] + results: list[dict[str, object]] = report["results"] # type: ignore[assignment] + generated_plans = tool.prepare_plans( + runtime_dir, asset_root, base, report_width, report_height, 28140 + ) + for role, contract in tool.ROLE_EVIDENCE_CONTRACT.items(): + savepath = base / "savepaths" / contract["saveDir"] + plans.append(tool.plan_record(generated_plans[role])) + artifacts: list[dict[str, object]] = [] + log = base / "savepaths" / contract["saveDir"] / "baseoq4" / "logs" / contract["logName"] + log.parent.mkdir(parents=True, exist_ok=True) + active_proofs = "" + if role == "mp-client": + active_line = ( + tool.MP_CLIENT_ACTIVE_MARKER + + " client=1 spectating=0 wantSpectate=0 ingame=1 menu=0 disableHud=0\n" + ) + view_line = tool.MP_CLIENT_VIEW_MARKER + " gui=0\n" + active_proofs = ( + active_line + + view_line + + "Wrote screenshots/stock-baseline/mp_client.tga\n" + + active_line + + view_line + ) + log.write_text( + " [0] * Test Display (contentScale 1.00)\n" + f"MODE: -1, {report_width} x {report_height} windowed hz:N/A\n" + + active_proofs + + ( + timing_marker(contract["budget"]["map"]) + "\n" + if contract.get("budget") + else "" + ) + + contract["marker"] + + "\n", + encoding="utf-8", + ) + artifacts.append({"kind": "engineLog", **tool.file_record(log, base)}) + for kind, stream in (("processStdout", "stdout"), ("processStderr", "stderr")): + path = base / f"{role}.{stream}.txt" + path.write_text("", encoding="utf-8") + artifacts.append({"kind": kind, **tool.file_record(path, base)}) + for kind, relative in contract["expected"].items(): + path = base / "savepaths" / contract["saveDir"] / relative + path.parent.mkdir(parents=True, exist_ok=True) + if kind == "screenshot": + if role == "sp-capture": + write_rgb_tga( + path, + report_width, + report_height, + patterned_rgb(report_width, report_height), + ) + else: + write_minimal_tga(path, report_width, report_height) + elif kind == "saveReferenceScreenshot": + write_rgb_tga( + path, + report_width, + report_height, + scene_rgb(report_width, report_height), + ) + elif kind == "savePreview": + reference_path = ( + base + / "savepaths" + / contract["saveDir"] + / contract["expected"]["saveReferenceScreenshot"] + ) + reference_decoded = tool.decode_tga_rgb(reference_path.read_bytes()) + assert not isinstance(reference_decoded, str) + reference_width, reference_height, reference_rgb = reference_decoded + preview_width, preview_height = tool.SP_SAVE_PREVIEW_DIMENSIONS + write_rgb_tga( + path, + preview_width, + preview_height, + tool.center_aspect_resample_rgb( + reference_rgb, + reference_width, + reference_height, + preview_width, + preview_height, + ), + ) + else: + path.write_bytes(b"evidence") + artifacts.append({"kind": kind, **tool.file_record(path, base)}) + result: dict[str, object] = { + "role": role, + "mode": generated_plans[role].mode, + "status": "pass", + "exitCode": 0, + "timedOut": False, + "failures": [], + "artifacts": artifacts, + "budgetEvidence": {}, + } + if contract.get("budget"): + identity = contract["budget"] + evidence, evidence_failures = tool.evaluate_timing_evidence( + (log.read_text(encoding="utf-8"), "", ""), + budget_contract, + identity["map"], + identity["backend"], + identity["profile"], + ) + assert evidence_failures == [] + result["budgetEvidence"] = evidence + if role == "sp-capture": + reference_path = ( + base + / "savepaths" + / contract["saveDir"] + / contract["expected"]["saveReferenceScreenshot"] + ) + preview_path = ( + base + / "savepaths" + / contract["saveDir"] + / contract["expected"]["savePreview"] + ) + comparison, failure = tool.save_preview_comparison( + reference_path, preview_path + ) + assert comparison is not None and failure is None + result["savePreviewComparison"] = comparison + results.append(result) + live_sp_result = tool.evaluate_role( + generated_plans["sp-capture"], base, 0, False + ) + assert live_sp_result["status"] == "pass" + assert live_sp_result["savePreviewComparison"] == next( + result for result in results if result["role"] == "sp-capture" + )["savePreviewComparison"] + + # Report replay is intentionally exercised through many independent + # mutations below. Cache the expensive full-pixel TGA policy result by + # exact bytes so the canonical 1280x720 fixture does not turn each + # unrelated provenance mutation into another full image decode. Any image + # byte mutation receives a different digest and is validated afresh. + uncached_validate_tga = tool.validate_tga + tga_validation_cache: dict[tuple[str, tuple[int, int] | None], str | None] = {} + + def cached_validate_tga( + path: Path, expected_dimensions: tuple[int, int] | None = None + ) -> str | None: + key = (tool.sha256_file(path), expected_dimensions) + if key not in tga_validation_cache: + tga_validation_cache[key] = uncached_validate_tga(path, expected_dimensions) + return tga_validation_cache[key] + + tool.validate_tga = cached_validate_tga + uncached_save_preview_comparison = tool.save_preview_comparison + preview_comparison_cache: dict[ + tuple[str, str], tuple[dict[str, object] | None, str | None] + ] = {} + + def cached_save_preview_comparison( + reference_path: Path, preview_path: Path + ) -> tuple[dict[str, object] | None, str | None]: + key = ( + tool.sha256_file(reference_path), + tool.sha256_file(preview_path), + ) + if key not in preview_comparison_cache: + preview_comparison_cache[key] = uncached_save_preview_comparison( + reference_path, preview_path + ) + return preview_comparison_cache[key] + + tool.save_preview_comparison = cached_save_preview_comparison + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + assert tool.verify_recorded_files(report, base, asset_root) == [] + + wrong_display_contract = json.loads(json.dumps(report)) + wrong_display_contract["safety"]["displayContract"]["width"] = 1279 + assert any( + "safety/window contract differs" in failure + for failure in tool.verify_recorded_files( + wrong_display_contract, base, asset_root, runtime_dir + ) + ) + wrong_window_size = json.loads(json.dumps(report)) + wrong_window_size["safety"]["windowSize"]["width"] = 640 + assert any( + "safety/window contract differs" in failure + for failure in tool.verify_recorded_files( + wrong_window_size, base, asset_root, runtime_dir + ) + ) + + for field, value in ( + ("contractId", "wrong-contract-v1"), + ("path", "tools/validation/wrong.json"), + ("sha256", "0" * 64), + ): + wrong_budget_binding = json.loads(json.dumps(report)) + wrong_budget_binding["budgetContract"][field] = value + assert any( + "budget contract binding differs" in failure + for failure in tool.verify_recorded_files( + wrong_budget_binding, base, asset_root, runtime_dir + ) + ) + stock_sp_result = next( + result for result in results if result["role"] == "sp-capture" + ) + stock_sp_log_artifact = next( + artifact + for artifact in stock_sp_result["artifacts"] # type: ignore[union-attr] + if artifact["kind"] == "engineLog" + ) + stock_sp_log = base / str(stock_sp_log_artifact["path"]) + stock_sp_log_text = stock_sp_log.read_text(encoding="utf-8") + stock_sp_log.write_text( + stock_sp_log_text.replace("backend=opengl", "backend=vulkan"), + encoding="utf-8", + ) + stock_sp_log_artifact.update(tool.file_record(stock_sp_log, base)) + mutated_marker_failures = tool.verify_recorded_files( + report, base, asset_root, runtime_dir + ) + assert any("timing backend" in failure for failure in mutated_marker_failures) + assert any( + "recorded renderer budget evidence differs" in failure + for failure in mutated_marker_failures + ) + stock_sp_log.write_text(stock_sp_log_text, encoding="utf-8") + stock_sp_log_artifact.update(tool.file_record(stock_sp_log, base)) + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + + stock_sp_log.write_text( + stock_sp_log_text.replace( + "MODE: -1, 1280 x 720 windowed", + "MODE: -1, 1279 x 720 windowed", + ), + encoding="utf-8", + ) + stock_sp_log_artifact.update(tool.file_record(stock_sp_log, base)) + assert any( + "expected exact budget workload 1280x720" in failure + for failure in tool.verify_recorded_files(report, base, asset_root, runtime_dir) + ) + stock_sp_log.write_text(stock_sp_log_text, encoding="utf-8") + stock_sp_log_artifact.update(tool.file_record(stock_sp_log, base)) + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + + stock_sp_screenshot_artifact = next( + artifact + for artifact in stock_sp_result["artifacts"] # type: ignore[union-attr] + if artifact["kind"] == "screenshot" + ) + stock_sp_screenshot = base / str(stock_sp_screenshot_artifact["path"]) + stock_sp_screenshot_bytes = stock_sp_screenshot.read_bytes() + changed_screenshot = bytearray(stock_sp_screenshot_bytes) + struct.pack_into(" dict[str, object]: + candidate = json.loads(json.dumps(report)) + role_plan = next(plan for plan in candidate["plan"] if plan["role"] == role) + token_index = role_plan["arguments"].index(token) + role_plan["arguments"][token_index + 1] = value + return candidate + + for role, token, value, expected_failure in ( + ("sp-capture", "fs_game", "wronggame", "launch CVar fs_game differs"), + ("sp-capture", "+map", "game/wrong", "launch command map differs"), + ("sp-capture", "si_gameType", "DM", "launch CVar si_gameType differs"), + ("mp-server", "+spawnServer", "mp/wrong", "launch command spawnServer differs"), + ("mp-server", "net_serverDedicated", "1", "launch CVar net_serverDedicated differs"), + ("mp-server", "si_gameType", "Tourney", "launch CVar si_gameType differs"), + ("mp-client", "+connect", "127.0.0.1:1", "launch command connect differs"), + ): + assert any( + expected_failure in failure + for failure in tool.verify_recorded_files( + mutated_launch(role, token, value), base, asset_root, runtime_dir + ) + ) + + mp_client_result = next( + result for result in results if result["role"] == "mp-client" + ) + mp_client_log_artifact = next( + artifact + for artifact in mp_client_result["artifacts"] # type: ignore[union-attr] + if artifact["kind"] == "engineLog" + ) + mp_client_stdout_artifact = next( + artifact + for artifact in mp_client_result["artifacts"] # type: ignore[union-attr] + if artifact["kind"] == "processStdout" + ) + mp_client_log = base / str(mp_client_log_artifact["path"]) + mp_client_stdout = base / str(mp_client_stdout_artifact["path"]) + mp_client_stdout.write_text( + mp_client_log.read_text(encoding="utf-8"), encoding="utf-8" + ) + mp_client_stdout_artifact.update(tool.file_record(mp_client_stdout, base)) + mirrored_live_result = tool.evaluate_role( + generated_plans["mp-client"], base, 0, False + ) + assert mirrored_live_result["status"] == "pass" + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + mp_client_stdout.write_text("", encoding="utf-8") + mp_client_stdout_artifact.update(tool.file_record(mp_client_stdout, base)) + save_preview_artifact = next( + artifact + for result in results + for artifact in result["artifacts"] # type: ignore[index] + if artifact["kind"] == "savePreview" + ) + save_preview_path = base / str(save_preview_artifact["path"]) + coherent_preview_bytes = save_preview_path.read_bytes() + sp_capture_result = next(result for result in results if result["role"] == "sp-capture") + coherent_comparison = sp_capture_result["savePreviewComparison"] + sp_reference_artifact = next( + artifact + for artifact in sp_capture_result["artifacts"] # type: ignore[union-attr] + if artifact["kind"] == "saveReferenceScreenshot" + ) + sp_reference_path = base / str(sp_reference_artifact["path"]) + preview_width, preview_height = tool.SP_SAVE_PREVIEW_DIMENSIONS + write_rgb_tga( + save_preview_path, + preview_width, + preview_height, + recursive_strip_rgb(preview_width, preview_height), + ) + save_preview_artifact.update(tool.file_record(save_preview_path, base)) + assert any( + "save preview: TGA contains recursive scaled-strip repetition" in failure + for failure in tool.verify_recorded_files(report, base, asset_root, runtime_dir) + ) + save_preview_path.write_bytes(coherent_preview_bytes) + save_preview_artifact.update(tool.file_record(save_preview_path, base)) + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + coherent_decoded = tool.decode_tga_rgb(coherent_preview_bytes) + assert not isinstance(coherent_decoded, str) + _, _, coherent_rgb = coherent_decoded + write_rgb_tga( + save_preview_path, + preview_width, + preview_height, + severe_color_defect(coherent_rgb), + ) + assert tool.validate_tga(save_preview_path, tool.SP_SAVE_PREVIEW_DIMENSIONS) is None + save_preview_artifact.update(tool.file_record(save_preview_path, base)) + corrupted_comparison, comparison_failure = tool.save_preview_comparison( + sp_reference_path, save_preview_path + ) + assert corrupted_comparison is not None and comparison_failure is not None + assert "exposure-compensated" in comparison_failure + assert corrupted_comparison["lumaCorrelation"] >= tool.SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION + live_corrupted_result = tool.evaluate_role( + generated_plans["sp-capture"], base, 0, False + ) + assert live_corrupted_result["status"] == "fail" + assert any( + "save preview differs from same-state save reference" in failure + for failure in live_corrupted_result["failures"] + ) + sp_capture_result["savePreviewComparison"] = corrupted_comparison + assert any( + "save preview differs from same-state save reference" in failure + for failure in tool.verify_recorded_files(report, base, asset_root, runtime_dir) + ) + save_preview_path.write_bytes(coherent_preview_bytes) + save_preview_artifact.update(tool.file_record(save_preview_path, base)) + sp_capture_result["savePreviewComparison"] = coherent_comparison + assert tool.verify_recorded_files(report, base, asset_root, runtime_dir) == [] + missing_comparison = json.loads(json.dumps(report)) + del next( + result for result in missing_comparison["results"] if result["role"] == "sp-capture" + )["savePreviewComparison"] + assert any( + "recorded save-preview comparison metrics differ" in failure + for failure in tool.verify_recorded_files( + missing_comparison, base, asset_root, runtime_dir + ) + ) + original_link_check = tool.is_link_or_junction + for linked_path in (runtime_dir, runtime_dir.parent): + tool.is_link_or_junction = lambda path, linked=linked_path: path == linked + try: + assert any( + "runtime directory ancestry must not contain a link or junction" in failure + for failure in tool.verify_recorded_files(report, base, asset_root) + ) + finally: + tool.is_link_or_junction = original_link_check + wrong_runtime_root = json.loads(json.dumps(report)) + wrong_runtime_root["runtimeRoot"] = str(base / "wrong-runtime") + assert any( + "runtime root differs" in failure + for failure in tool.verify_recorded_files( + wrong_runtime_root, base, asset_root, runtime_dir + ) + ) + missing_runtime_root = json.loads(json.dumps(report)) + del missing_runtime_root["runtimeRoot"] + assert any( + "does not record its runtime root" in failure + for failure in tool.verify_recorded_files( + missing_runtime_root, base, asset_root, runtime_dir + ) + ) + runtime_executable.write_bytes(b"mutated client") + assert any( + "runtime file" in failure and "differs" in failure + for failure in tool.verify_recorded_files(report, base, asset_root, runtime_dir) + ) + runtime_executable.write_bytes(b"client") + disabled_autojoin = json.loads(json.dumps(report)) + disabled_plan = next( + plan for plan in disabled_autojoin["plan"] if plan["role"] == "mp-client" + ) + disabled_index = disabled_plan["arguments"].index("ui_autoJoin") + disabled_plan["arguments"][disabled_index + 1] = "0" + assert any( + "mp-client: launch CVar ui_autoJoin differs" in failure + for failure in tool.verify_recorded_files( + disabled_autojoin, base, asset_root, runtime_dir + ) + ) + missing_autojoin = json.loads(json.dumps(report)) + missing_plan = next( + plan for plan in missing_autojoin["plan"] if plan["role"] == "mp-server" + ) + missing_index = missing_plan["arguments"].index("ui_autoJoin") + del missing_plan["arguments"][missing_index - 1 : missing_index + 2] + assert any( + "mp-server: launch CVar ui_autoJoin differs" in failure + for failure in tool.verify_recorded_files( + missing_autojoin, base, asset_root, runtime_dir + ) + ) + disabled_pure = json.loads(json.dumps(report)) + pure_plan = next( + plan for plan in disabled_pure["plan"] if plan["role"] == "mp-server" + ) + pure_index = pure_plan["arguments"].index("si_pure") + pure_plan["arguments"][pure_index + 1] = "0" + assert any( + "mp-server: launch CVar si_pure differs" in failure + for failure in tool.verify_recorded_files( + disabled_pure, base, asset_root, runtime_dir + ) + ) + empty_pass = dict(report) + empty_pass["results"] = [] + assert any( + "required role" in failure + for failure in tool.verify_recorded_files(empty_pass, base, asset_root, runtime_dir) + ) + missing_artifact = json.loads(json.dumps(report)) + missing_artifact["results"][0]["artifacts"] = [ + item + for item in missing_artifact["results"][0]["artifacts"] + if item["kind"] != "processStderr" + ] + assert any( + "required artifact kinds differ" in failure + for failure in tool.verify_recorded_files( + missing_artifact, base, asset_root, runtime_dir + ) + ) + stdout_artifact = next( + item for item in results[0]["artifacts"] if item["kind"] == "processStdout" # type: ignore[union-attr] + ) + artifact = base / str(stdout_artifact["path"]) + artifact.write_text("tampered stream", encoding="utf-8") + assert any( + "differs" in failure + for failure in tool.verify_recorded_files(report, base, asset_root, runtime_dir) + ) + + +def main() -> None: + tool = load_tool() + temp_parent = ROOT / ".tmp" / "stock-runtime" + temp_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="stock-baseline-test-", dir=temp_parent) as temp: + base = Path(temp) + test_asset_inventory_and_comparison(tool, base) + test_plan_is_windowed_and_engine_only(tool, base) + test_diagnostic_authority_and_shared_mp_deadline(tool, base) + test_save_preview_coherence(tool, base) + test_tga_and_report_verification(tool, base) + print("stock_asset_baseline: ok") + + +if __name__ == "__main__": + main() diff --git a/tools/tests/stock_media_fallbacks.py b/tools/tests/stock_media_fallbacks.py new file mode 100644 index 00000000..6f99a1a6 --- /dev/null +++ b/tools/tests/stock_media_fallbacks.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Pin fail-after-primary compatibility fallbacks for incomplete retail media.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(relative: str) -> str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def function_body(source: str, signature: str) -> str: + start = source.find(signature) + if start < 0: + raise AssertionError(f"missing function {signature!r}") + brace = source.find("{", start) + if brace < 0: + raise AssertionError(f"missing body for {signature!r}") + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[brace : index + 1] + raise AssertionError(f"unterminated body for {signature!r}") + + +def require_in_order(source: str, snippets: tuple[str, ...], message: str) -> None: + cursor = 0 + for snippet in snippets: + position = source.find(snippet, cursor) + if position < 0: + raise AssertionError(f"{message}: missing or out of order {snippet!r}") + cursor = position + len(snippet) + + +def test_images_try_authored_media_before_shape_compatible_stock_fallbacks() -> None: + source = read("src/renderer/Image_load.cpp") + image_header = read("src/renderer/Image.h") + image_program = read("src/imagetools/Image_program.cpp") + if "bool R_LoadImageProgram(" not in image_header or "bool R_LoadImageProgram(" not in image_program: + raise AssertionError("image-program source selection must expose its load-success result") + if "return loaded;" not in image_program: + raise AssertionError("image-program source selection must return the parser load result") + + resolver = function_body(source, "static const char *R_FindMissingQ4StockImageFallback(") + expected = { + "gfx/effects/fluids_drips/brown_bubble": + "gfx/effects/fluids_drips/bubble_alpha.tga", + "gfx/effects/fluids_drips/brown_bubble_half": + "gfx/effects/fluids_drips/bubble_half.tga", + "gfx/effects/fluids_drips/brown_splash_line": + "gfx/effects/fluids_drips/splash_line.tga", + } + for missing, replacement in expected.items(): + if missing not in resolver or replacement not in resolver: + raise AssertionError(f"missing retail image fallback {missing!r} -> {replacement!r}") + + selector = function_body(source, "static bool R_SelectMissingQ4StockImageSource(") + require_in_order( + selector, + ( + "R_FindMissingQ4StockImageFallback( imageName )", + "if ( stockFallbackName == NULL ) {", + "selectedSourceName = imageName;", + "selectedSourceTime = 0;", + "if ( R_LoadImageProgram( imageName, NULL, NULL, NULL, &selectedSourceTime ) ) {", + "selectedSourceTime = 0;", + "if ( R_LoadImageProgram( stockFallbackName, NULL, NULL, NULL, &selectedSourceTime ) ) {", + "selectedSourceName = stockFallbackName;", + "stockFallbackSelected = true;", + ), + "exact stock candidates must prefer a successfully loaded authored source before fallback", + ) + if "selectedSourceTime > 0" in selector or "selectedSourceTime <= 0" in selector: + raise AssertionError("stock source selection must not infer load success from PK4 timestamps") + + cache_identity = function_body( + source, "static void R_AddMissingQ4StockImageCacheIdentity(" + ) + require_in_order( + cache_identity, + ( + "generatedName.ExtractFileExtension( extension );", + "generatedName.StripFileExtension();", + 'stockFallbackSelected ? "#q4stockfallback" : "#q4authored"', + "generatedName.SetFileExtension( extension );", + ), + "authored and fallback sources need distinct non-legacy generated cache paths", + ) + + load = function_body(source, "void idImage::ActuallyLoadImage(") + require_in_order( + load, + ( + "R_LoadImageProgramForDeclaredUsage( fallbackLoadSourceName, &pic", + "if ( pic == NULL && !q4StockImageCandidateResolved ) {", + "R_FindMissingQ4StockImageFallback( GetName() )", + "R_LoadImageProgramForDeclaredUsage( stockFallbackName, &pic", + "selectedSourceName = stockFallbackName;", + 'idLib::Warning( "Couldn\'t load image:', + ), + "image compatibility must preserve overrides and only default after the fallback fails", + ) + require_in_order( + load, + ( + "R_ResolvePreferredDDSImageSource( GetName(), preferredDDSName", + "if ( preferredDDSImage && !fileSystem->InProductionMode() ) {", + "bool q4StockImageCandidateResolved = false;", + "if ( !explicitDDSImage && !preferredDDSImage && cubeFiles == CF_2D ) {", + "q4StockImageCandidateResolved = R_SelectMissingQ4StockImageSource( GetName(), selectedSourceName,", + "sourceFileTimeKnown = true;", + "R_AddMissingQ4StockImageCacheIdentity( generatedName, stockFallbackSelected );", + "idStr selectedLoadSourceName = preferredDDSImage ? preferredDDSName : selectedSourceName;", + "idBinaryImage im( generatedName );", + "binaryFileTime = im.LoadFromGeneratedFileUnchecked();", + ), + "exact fallback source and cache identity must be selected before an existing generated cache is opened", + ) + require_in_order( + load, + ( + "q4StockImageCandidateResolved = R_SelectMissingQ4StockImageSource( GetName(), selectedSourceName,", + "idStr selectedLoadSourceName = preferredDDSImage ? preferredDDSName : selectedSourceName;", + "R_LoadImageProgramForDeclaredUsage( fallbackLoadSourceName, &pic", + "if ( pic == NULL && !q4StockImageCandidateResolved ) {", + ), + "decode must remain locked to the source identity selected for the generated cache", + ) + cache_open = load.find("idBinaryImage im( generatedName );") + cache_validation = load.find("auto acceptGeneratedImage =") + if cache_open < 0 or cache_validation < cache_open: + raise AssertionError("generated cache validation ordering is malformed") + if "R_FindMissingQ4StockImageFallback(" in load[cache_open:cache_validation]: + raise AssertionError("stock fallback selection must not be deferred until after cache open") + + reload_body = function_body(source, "void idImage::Reload(") + require_in_order( + reload_body, + ( + "idStr currentSourceName = imgName;", + "R_ResolvePreferredDDSImageSource( imgName, preferredDDSName", + "R_SelectMissingQ4StockImageSource( imgName, currentSourceName,", + "R_LoadImageProgram( imgName, NULL, NULL, NULL, ¤t );", + "loadedSourceName.Icmp( currentSourceName )", + "if ( !sourceSelectionChanged && current <= sourceFileTime ) {", + ), + "reload must keep a fallback source stable while allowing a new authored source to take priority", + ) + if "R_LoadImageProgramForDeclaredUsage(" in reload_body: + raise AssertionError("timestamp-only reload checks must not mutate the image's declared usage") + + +def test_sound_tries_authored_sample_before_the_retail_family_fallback() -> None: + source = read("src/sound/OpenAL/AL_SoundSample.cpp") + resolver = function_body(source, "static void SoundSample_AppendMissingQ4StockFallback(") + for snippet in ( + '"sound/ambience/water/splash_big"', + '"sound/ambience/water/splash_small02"', + "canonicalName.BackSlashesToSlashes();", + "canonicalName.StripFileExtension();", + ): + if snippet not in resolver: + raise AssertionError(f"missing sound fallback contract {snippet!r}") + + load = function_body(source, "void idSoundSample_OpenAL::LoadResource(") + require_in_order( + load, + ( + "SoundSample_AppendUniqueSampleVariant( sampleVariants, baseSampleName );", + "SoundSample_AppendMissingQ4StockFallback( sampleVariants, baseSampleName );", + "for( int i = 0; i < sampleVariants.Num(); i++ )", + 'idLib::Warning( "Couldn\'t load sound', + ), + "sound compatibility must probe the authored sample before its fallback", + ) + + +def main() -> int: + test_images_try_authored_media_before_shape_compatible_stock_fallbacks() + test_sound_tries_authored_sample_before_the_retail_family_fallback() + print("stock media fallback checks passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/validation_hardening.py b/tools/tests/validation_hardening.py index 8334bb0f..c8815bb5 100644 --- a/tools/tests/validation_hardening.py +++ b/tools/tests/validation_hardening.py @@ -626,6 +626,8 @@ def validate_validation_wiring() -> None: "linux_wayland_stock_sp_smoke.py", "macos_dedicated_server_smoke.py", "renderer_gameplay_benchmark.py", + "renderer_milestone_d_acceptance.py", + "renderer_milestone_d_fixture.py", } discovered_tests = sorted(path.name for path in (ROOT / "tools" / "tests").glob("*.py")) if not discovered_tests: diff --git a/tools/tests/vk_shader_header_pin.py b/tools/tests/vk_shader_header_pin.py index 5bae45de..cdbd05e8 100644 --- a/tools/tests/vk_shader_header_pin.py +++ b/tools/tests/vk_shader_header_pin.py @@ -18,6 +18,7 @@ GENERATOR = REPO_ROOT / "tools" / "build" / "spirv_to_header.py" COMMITTED = REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" / "gui_shaders_spv.h" SHADERS = [ + REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" / "gpu_skinning.comp", REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" / "gui.vert", REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" / "gui.frag", REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" / "screen.vert", diff --git a/tools/tests/vk_temporal_resolve_shader_pin.py b/tools/tests/vk_temporal_resolve_shader_pin.py new file mode 100644 index 00000000..c2a16da6 --- /dev/null +++ b/tools/tests/vk_temporal_resolve_shader_pin.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Pin the standalone Vulkan temporal-resolve SPIR-V header to GLSL.""" + +from __future__ import annotations + +import pathlib +import subprocess +import sys +import tempfile + + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +GENERATOR = REPO_ROOT / "tools" / "build" / "spirv_to_header.py" +SHADER_DIR = REPO_ROOT / "src" / "renderer" / "Vulkan" / "shaders" +COMMITTED = SHADER_DIR / "temporal_resolve_spv.h" +SHADERS = [ + SHADER_DIR / "temporal_resolve.vert", + SHADER_DIR / "temporal_resolve.frag", +] + + +def main() -> int: + sys.path.insert(0, str(GENERATOR.parent)) + import spirv_to_header # noqa: E402 + + if spirv_to_header.find_glslang(None) is None: + print("vk_temporal_resolve_shader_pin: skipped (glslangValidator unavailable)") + return 0 + if not COMMITTED.is_file(): + print(f"missing committed temporal shader header: {COMMITTED}", file=sys.stderr) + return 1 + + with tempfile.TemporaryDirectory() as temporary_directory: + regenerated = pathlib.Path(temporary_directory) / COMMITTED.name + result = subprocess.run( + [ + sys.executable, + str(GENERATOR), + "--header-out", + str(regenerated), + "--guard", + "__VK_TEMPORAL_RESOLVE_SPV_H__", + *[str(shader) for shader in SHADERS], + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print( + "temporal shader regeneration failed:\n" + f"{result.stdout}\n{result.stderr}", + file=sys.stderr, + ) + return 1 + normalize = lambda data: data.replace(b"\r\n", b"\n") + if normalize(regenerated.read_bytes()) != normalize(COMMITTED.read_bytes()): + shader_args = " ".join( + shader.relative_to(REPO_ROOT).as_posix() for shader in SHADERS + ) + print( + "committed temporal shader header is stale; regenerate with:\n" + " python tools/build/spirv_to_header.py --header-out " + "src/renderer/Vulkan/shaders/temporal_resolve_spv.h --guard " + "__VK_TEMPORAL_RESOLVE_SPV_H__ " + f"{shader_args}", + file=sys.stderr, + ) + return 1 + + print("vk_temporal_resolve_shader_pin: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/vscode_fast_build.py b/tools/tests/vscode_fast_build.py index dd14e4a6..bdaa3713 100644 --- a/tools/tests/vscode_fast_build.py +++ b/tools/tests/vscode_fast_build.py @@ -71,16 +71,95 @@ def validate_wrapper() -> None: stager = read("tools/build/stage_fast_install.py") require(stager, "copy_file_if_changed", "fast install copy-if-changed behavior") + require( + stager, + "from windows_runtime import cleanup_windows_stage_target, is_windows_host", + "shared Windows stage hygiene helper", + ) + require( + stager, + "cleanup_windows_stage_target(install_dir)", + "fast-build Windows stale-runtime cleanup", + ) + require(stager, '"renderer-gl_*.dll"', "fast install renderer staging") + require(stager, '"--temporary-runtime"', "isolated compatibility runtime staging") + require(stager, 'source_root / ".tmp" / "stock-runtime"', "temporary runtime containment") require(stager, '"pak0.pk4"', "fast install stages pak0") require(stager, '"pak1.pk4"', "fast install stages pak1") reject(stager, '"*.lib",\n "pak0.pk4"', "fast install must not copy linker artifacts as runtime content") + windows_runtime = read("tools/build/windows_runtime.py") + require( + windows_runtime, + "WINDOWS_STALE_STAGE_FILE_MANIFEST", + "narrow Windows stale-runtime cleanup manifest", + ) + require( + windows_runtime, + '"baseoq4/skins"', + "known empty Windows stage directory manifest", + ) + require( + windows_runtime, + "cleanup_results[str(target)] = cleanup_windows_stage_target(target)", + "full-install Windows stale-runtime cleanup", + ) + def validate_launch_configs() -> None: launch = json.loads(read(".vscode/launch.json")) + mp_configs = [] for config in launch.get("configurations", []): if "preLaunchTask" in config: raise AssertionError(f"Launch config {config.get('name')!r} must not define preLaunchTask") + if "(MP)" in str(config.get("name", "")): + mp_configs.append(config) + args = config.get("args", []) + values = [ + str(args[index + 2]) + for index, token in enumerate(args[:-2]) + if token in ("+set", "+seta") and args[index + 1] == "ui_autoJoin" + ] + if values != ["1"]: + raise AssertionError( + f"MP launch config {config.get('name')!r} must set ui_autoJoin exactly once to 1" + ) + if not mp_configs: + raise AssertionError("Expected at least one VS Code MP launch configuration") + + +def validate_mp_autojoin_policy() -> None: + listen_script = read("tools/debug/start_listen_server_client.ps1") + if listen_script.count('"+set", "ui_autoJoin", "1"') != 2: + raise AssertionError("MP listen-server helper must enable auto-join for host and client") + + renderdoc_script = read("tools/debug/renderdoc_capture.ps1") + if renderdoc_script.count('"+set", "ui_autoJoin", "1"') != 1: + raise AssertionError("MP RenderDoc helper must enable auto-join for its listen host") + + benchmark = read("tools/tests/renderer_gameplay_benchmark.py") + for target in ("server_args", "client_args"): + require( + benchmark, + f'append_set({target}, "ui_autoJoin", "1")', + f"MP renderer benchmark {target}", + ) + + baseline = read("tools/validation/stock_asset_baseline.py") + require( + baseline, + 'args[restart_index:restart_index] = ("+set", "ui_autoJoin", "1")', + "stock baseline MP roles", + ) + require( + baseline, + 'launch_contract["ui_autoJoin"] = "1"', + "recorded stock baseline MP contract", + ) + + guide = read("AGENTS.md") + require(guide, "Keep `ui_autoJoin 1` enabled for multiplayer testing", "agent MP test policy") + require(guide, "explicit `+set ui_autoJoin 0`", "join-menu test exception") def validate_validation_coverage() -> None: @@ -99,6 +178,7 @@ def main() -> None: validate_tasks() validate_wrapper() validate_launch_configs() + validate_mp_autojoin_policy() validate_validation_coverage() print("vscode_fast_build: ok") diff --git a/tools/validation/audit_source_provenance.py b/tools/validation/audit_source_provenance.py new file mode 100644 index 00000000..d0b8b4d0 --- /dev/null +++ b/tools/validation/audit_source_provenance.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +"""Inventory retained Doom 3 license-header families and verify their notices. + +The header is an attribution/license-family signal, not proof that the local +file is byte-identical to a file at the audited upstream commit. This tool is +offline and deliberately makes no legal-compatibility determination. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[2] +MANIFEST_PATH = ROOT / "docs" / "dev" / "source-provenance-manifest.json" +ADDITIONAL_TERMS_SIGNAL = "subject to certain additional terms" + + +def canonical_text_bytes(payload: bytes) -> bytes: + text = payload.decode("utf-8") + return text.replace("\r\n", "\n").replace("\r", "\n").encode("utf-8") + + +def load_manifest(path: Path = MANIFEST_PATH) -> dict[str, Any]: + payload = json.loads(path.read_text(encoding="utf-8")) + if payload.get("schemaVersion") != 1: + raise ValueError(f"unsupported provenance manifest schema: {payload.get('schemaVersion')!r}") + families = payload.get("families") + if not isinstance(families, dict) or not families: + raise ValueError("provenance manifest must define at least one header family") + return payload + + +def tracked_files(root: Path = ROOT) -> list[Path]: + try: + completed = subprocess.run( + ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"], + cwd=root, + check=True, + capture_output=True, + ) + except (OSError, subprocess.CalledProcessError) as exc: + raise RuntimeError("source provenance audit requires a Git worktree") from exc + + paths: list[Path] = [] + for raw in completed.stdout.split(b"\0"): + if not raw: + continue + relative = Path(raw.decode("utf-8", errors="surrogateescape")) + path = root / relative + if path.is_file(): + paths.append(path) + return sorted(paths) + + +def read_header(path: Path, limit: int = 16 * 1024) -> str: + with path.open("rb") as stream: + return stream.read(limit).decode("utf-8", errors="replace") + + +def bfg_official_path(local_path: str) -> str: + if not local_path.startswith("src/"): + raise ValueError(f"BFG-marked path is outside src/: {local_path}") + relative = local_path.removeprefix("src/") + if relative.startswith("imagetools/"): + relative = "renderer/" + relative.removeprefix("imagetools/") + return "neo/" + relative + + +def bfg_origin(local_path: str, family_spec: dict[str, Any]) -> dict[str, str]: + intermediate_sources = family_spec.get("intermediateSources", {}) + for source_name, source in intermediate_sources.items(): + path = source.get("pathOverrides", {}).get(local_path) + if path: + return { + "classification": "intermediate-fork-lineage", + "source": source_name, + "repository": source["repository"], + "auditedCommit": source["auditedCommit"], + "auditedPath": path, + } + return { + "classification": "official-snapshot-path", + "source": "doom3_bfg_official", + "repository": family_spec["officialRepository"], + "auditedCommit": family_spec["auditedCommit"], + "auditedPath": bfg_official_path(local_path), + } + + +def inventory(root: Path, manifest: dict[str, Any]) -> dict[str, Any]: + family_specs = manifest["families"] + family_files: dict[str, list[dict[str, str]]] = {name: [] for name in family_specs} + unclassified: list[str] = [] + + # Match the more specific BFG marker before the Doom 3 marker. + ordered_families = sorted( + family_specs.items(), key=lambda item: len(item[1]["headerMarker"]), reverse=True + ) + for path in tracked_files(root): + relative = path.relative_to(root).as_posix() + if not relative.startswith("src/"): + continue + try: + header = read_header(path) + except OSError: + continue + matched = None + for family_name, spec in ordered_families: + if spec["headerMarker"].casefold() in header.casefold(): + matched = family_name + break + if matched is None: + if ADDITIONAL_TERMS_SIGNAL.casefold() in header.casefold(): + unclassified.append(relative) + continue + + entry = {"localPath": relative} + if matched == "doom3_bfg": + entry.update(bfg_origin(relative, family_specs[matched])) + family_files[matched].append(entry) + + return { + "schemaVersion": 1, + "manifest": MANIFEST_PATH.relative_to(root).as_posix(), + "families": { + name: { + "displayName": family_specs[name]["displayName"], + "auditedCommit": family_specs[name]["auditedCommit"], + "count": len(entries), + "files": entries, + } + for name, entries in family_files.items() + }, + "unclassifiedAdditionalTermsHeaders": unclassified, + } + + +def validate(root: Path, manifest: dict[str, Any], report: dict[str, Any]) -> list[str]: + failures: list[str] = [] + if manifest.get("textHashNormalization") != "UTF-8; CRLF and CR normalized to LF": + failures.append("manifest textHashNormalization is missing or unsupported") + for family_name, spec in manifest["families"].items(): + actual = report["families"].get(family_name, {}) + if actual.get("count") != spec.get("expectedFileCount"): + failures.append( + f"{family_name}: expected {spec.get('expectedFileCount')} marked files, " + f"found {actual.get('count')}" + ) + + commit = spec.get("auditedCommit", "") + if len(commit) != 40 or any(ch not in "0123456789abcdef" for ch in commit): + failures.append(f"{family_name}: auditedCommit is not a lowercase 40-digit Git object id") + copying_hash = spec.get("officialCopyingSha256", "") + if len(copying_hash) != 64 or any(ch not in "0123456789abcdef" for ch in copying_hash): + failures.append(f"{family_name}: officialCopyingSha256 is not a lowercase SHA-256 digest") + + local_terms_hash = spec.get("localAdditionalTermsSha256", "") + if len(local_terms_hash) != 64 or any(ch not in "0123456789abcdef" for ch in local_terms_hash): + failures.append( + f"{family_name}: localAdditionalTermsSha256 is not a lowercase SHA-256 digest" + ) + + terms_path = root / spec["localAdditionalTerms"] + if not terms_path.is_file(): + failures.append(f"{family_name}: missing {spec['localAdditionalTerms']}") + else: + terms_bytes = terms_path.read_bytes() + canonical_terms = canonical_text_bytes(terms_bytes) + actual_terms_hash = hashlib.sha256(canonical_terms).hexdigest() + if actual_terms_hash != local_terms_hash: + failures.append( + f"{family_name}: {spec['localAdditionalTerms']} SHA-256 differs: " + f"expected {local_terms_hash}, got {actual_terms_hash}" + ) + terms = canonical_terms.decode("utf-8") + if "ADDITIONAL TERMS APPLICABLE" not in terms: + failures.append(f"{family_name}: local Additional Terms heading is missing") + for section in ("Replacement of Section 15", "Replacement of Section 16", "LEGAL NOTICES", "INDEMNIFICATION"): + if section not in terms: + failures.append(f"{family_name}: local Additional Terms omit {section!r}") + + for source_name, source in spec.get("intermediateSources", {}).items(): + source_commit = source.get("auditedCommit", "") + if len(source_commit) != 40 or any(ch not in "0123456789abcdef" for ch in source_commit): + failures.append(f"{family_name}/{source_name}: auditedCommit is not a lowercase 40-digit Git object id") + overrides = source.get("pathOverrides", {}) + inventoried = {item["localPath"] for item in actual.get("files", [])} + unknown = sorted(set(overrides) - inventoried) + if unknown: + failures.append(f"{family_name}/{source_name}: overrides do not identify inventoried files: {', '.join(unknown)}") + + unclassified = report["unclassifiedAdditionalTermsHeaders"] + if unclassified: + failures.append( + "unclassified source headers refer to Additional Terms: " + ", ".join(unclassified) + ) + return failures + + +def validate_reference_tree( + report: dict[str, Any], + source_name: str, + source_root: Path | None, +) -> list[str]: + if source_root is None: + return [] + failures: list[str] = [] + if not source_root.is_dir(): + return [f"{source_name}: reference root does not exist: {source_root}"] + for entry in report["families"]["doom3_bfg"]["files"]: + if entry.get("source") != source_name: + continue + path = source_root / Path(entry["auditedPath"]) + if not path.is_file(): + failures.append( + f"{source_name}: configured audited path is absent: {entry['auditedPath']} " + f"(for {entry['localPath']})" + ) + return failures + + +def validate_official_copying( + family_name: str, + family_spec: dict[str, Any], + source_root: Path | None, +) -> list[str]: + if source_root is None: + return [] + if not source_root.is_dir(): + return [f"{family_name}: reference root does not exist: {source_root}"] + copying = source_root / "COPYING.txt" + if not copying.is_file(): + return [f"{family_name}: reference root is missing COPYING.txt: {source_root}"] + actual = hashlib.sha256(copying.read_bytes()).hexdigest() + expected = family_spec["officialCopyingSha256"] + if actual != expected: + return [ + f"{family_name}: official COPYING.txt SHA-256 differs: expected {expected}, got {actual}" + ] + return [] + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Fail if counts, families, or accompanying terms differ from the audited manifest.") + parser.add_argument("--family", choices=("all", "doom3", "doom3_bfg"), default="all", help="Limit the displayed inventory; checks always cover every family.") + parser.add_argument("--format", choices=("text", "json"), default="text", help="Inventory output format.") + parser.add_argument("--output", default="", help="Optional output file. The default is stdout.") + parser.add_argument("--doom3-source", default="", help="Optional official DOOM-3 checkout used to verify COPYING.txt.") + parser.add_argument("--doom3-bfg-source", default="", help="Optional official DOOM-3-BFG checkout used to verify configured audited paths.") + parser.add_argument("--rbdoom3-bfg-source", default="", help="Optional RBDOOM-3-BFG checkout used to verify intermediate-fork audited paths.") + return parser.parse_args(argv) + + +def render_text(report: dict[str, Any], family_filter: str) -> str: + lines = ["openQ4 source provenance inventory"] + for family_name, family in report["families"].items(): + if family_filter != "all" and family_filter != family_name: + continue + lines.append(f"\n{family['displayName']}: {family['count']} files") + for entry in family["files"]: + upstream = entry.get("auditedPath") + source = entry.get("source") + suffix = f" -> {source}:{upstream}" if upstream else "" + lines.append(f" {entry['localPath']}{suffix}") + if report["unclassifiedAdditionalTermsHeaders"]: + lines.append("\nUnclassified Additional-Terms headers:") + lines.extend(f" {path}" for path in report["unclassifiedAdditionalTermsHeaders"]) + return "\n".join(lines) + "\n" + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + manifest = load_manifest() + report = inventory(ROOT, manifest) + failures = validate(ROOT, manifest, report) + doom3_source = Path(args.doom3_source).resolve() if args.doom3_source else None + bfg_source = Path(args.doom3_bfg_source).resolve() if args.doom3_bfg_source else None + failures += validate_official_copying( + "doom3", manifest["families"]["doom3"], doom3_source + ) + failures += validate_official_copying( + "doom3_bfg", manifest["families"]["doom3_bfg"], bfg_source + ) + failures += validate_reference_tree( + report, + "doom3_bfg_official", + bfg_source, + ) + failures += validate_reference_tree( + report, + "rbdoom3_bfg", + Path(args.rbdoom3_bfg_source).resolve() if args.rbdoom3_bfg_source else None, + ) + + if args.format == "json": + displayed = report + if args.family != "all": + displayed = dict(report) + displayed["families"] = {args.family: report["families"][args.family]} + output = json.dumps(displayed, indent=2) + "\n" + else: + output = render_text(report, args.family) + + if args.output: + output_path = Path(args.output).resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output, encoding="utf-8") + else: + sys.stdout.write(output) + + if args.check and failures: + for failure in failures: + print(f"error: {failure}", file=sys.stderr) + return 1 + if args.check: + print("source_provenance: ok", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/validation/generate_pbr_fixture.py b/tools/validation/generate_pbr_fixture.py new file mode 100644 index 00000000..d2080662 --- /dev/null +++ b/tools/validation/generate_pbr_fixture.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Generate a deterministic, licence-safe temporary PBR runtime fixture. + +The texture pixels and material declaration are original openQ4 procedural +test data produced entirely by this file; no third-party texture, shader, or +material asset is read or copied. The fixture is intentionally constrained to +the repository's ignored ``.tmp`` tree and is meant to override stock geometry +on ``maps/tools/mv2`` during opt-in validation. Generated files remain covered +by the openQ4 repository licence and must not be committed as content assets. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import stat +import struct +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[2] +REPO_TEMP_ROOT = ROOT / ".tmp" +FIXTURE_SCHEMA_VERSION = 1 +FIXTURE_ID = "openq4-procedural-pbr-packed-orm" +DEFAULT_TEXTURE_SIZE = 64 +MATERIAL_NAME = "materials/openq4/pbr_test/procedural_packed" +STOCK_MAP = "maps/tools/mv2" +BENCHMARK_CASE = "sp-mv2-interaction" +TEXTURE_PREFIX = "textures/openq4/pbrtest/procedural" +MATERIAL_QPATH = "baseoq4/materials/openq4_pbr_procedural_fixture.mtr" +MANIFEST_NAME = "openq4-pbr-procedural-fixture.json" + + +def is_link_or_junction(path: Path) -> bool: + """Recognize POSIX links and Windows junction/reparse-point entries.""" + is_junction = getattr(path, "is_junction", None) + if path.is_symlink() or bool(is_junction and is_junction()): + return True + try: + attributes = getattr(path.lstat(), "st_file_attributes", 0) + except OSError: + return False + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + + +def validate_ordinary_ancestry(path: Path, anchor: Path, label: str) -> None: + """Reject every existing link/reparse entry from anchor through path.""" + anchor_absolute = anchor.absolute() + candidate = path.absolute() + try: + relative = candidate.relative_to(anchor_absolute) + except ValueError as exc: + raise ValueError(f"{label} escapes its required root: {candidate}") from exc + + current = anchor_absolute + if is_link_or_junction(current): + raise RuntimeError(f"{label} ancestry contains a link or junction: {current}") + for part in relative.parts: + current /= part + if is_link_or_junction(current): + raise RuntimeError(f"{label} ancestry contains a link or junction: {current}") + + +def validate_contained_target(runtime_root: Path, target: Path) -> Path: + """Resolve one output immediately before use and keep it inside runtime_root.""" + root = runtime_root.resolve(strict=False) + validate_ordinary_ancestry(target, root, "fixture output") + resolved = target.resolve(strict=False) + try: + relative = resolved.relative_to(root) + except ValueError as exc: + raise RuntimeError( + f"fixture output resolves outside runtime root {root}: {resolved}" + ) from exc + if not relative.parts: + raise RuntimeError("fixture output may not replace the runtime root") + return resolved + + +def reject_hardlinked_file(path: Path, label: str) -> None: + if path.exists() and path.stat().st_nlink != 1: + raise RuntimeError(f"{label} must not be a hard-linked file: {path}") + + +def validate_runtime_root(runtime_root: Path) -> Path: + validate_ordinary_ancestry(runtime_root, REPO_TEMP_ROOT, "runtime root") + temporary_root = REPO_TEMP_ROOT.resolve() + resolved = runtime_root.resolve(strict=False) + try: + relative = resolved.relative_to(temporary_root) + except ValueError as exc: + raise ValueError( + f"--runtime-root must be a child of the repository .tmp directory: {temporary_root}" + ) from exc + if not relative.parts: + raise ValueError("--runtime-root must name a child directory below .tmp, not .tmp itself") + if resolved.exists() and not resolved.is_dir(): + raise RuntimeError(f"runtime root is not a directory: {resolved}") + return resolved + + +def validate_size(size: int) -> None: + if size < 8 or size > 2048 or size & (size - 1): + raise ValueError("--size must be a power of two from 8 through 2048") + + +def fixture_paths(runtime_root: Path) -> dict[str, Path]: + return { + "albedo": runtime_root / f"baseoq4/{TEXTURE_PREFIX}_albedo.tga", + "normal": runtime_root / f"baseoq4/{TEXTURE_PREFIX}_normal.tga", + "orm": runtime_root / f"baseoq4/{TEXTURE_PREFIX}_orm.tga", + "material": runtime_root / MATERIAL_QPATH, + "manifest": runtime_root / MANIFEST_NAME, + } + + +def validate_targets(runtime_root: Path, paths: dict[str, Path], force: bool) -> None: + existing: list[Path] = [] + for path in paths.values(): + resolved = validate_contained_target(runtime_root, path) + if resolved.exists() and not resolved.is_file(): + raise RuntimeError(f"fixture output is not a regular file: {resolved}") + reject_hardlinked_file(resolved, "fixture output") + if resolved.exists() and not force: + existing.append(resolved) + if existing: + listed = ", ".join(str(path) for path in existing) + raise FileExistsError(f"fixture output already exists: {listed} (pass --force to replace it)") + + +def tga_bytes(width: int, height: int, pixels: list[tuple[int, int, int]]) -> bytes: + if width <= 0 or height <= 0 or len(pixels) != width * height: + raise ValueError("invalid TGA dimensions or pixel count") + # Uncompressed 24-bit true-colour TGA, top-left origin. TGA stores BGR. + header = struct.pack( + " dict[str, list[tuple[int, int, int]]]: + """Create visibly distinct albedo, tangent-XYZ normal, and RGB ORM maps.""" + + validate_size(size) + tile = max(2, size // 8) + albedo: list[tuple[int, int, int]] = [] + normal: list[tuple[int, int, int]] = [] + orm: list[tuple[int, int, int]] = [] + for y in range(size): + for x in range(size): + checker = ((x // tile) + (y // tile)) & 1 + grain = (x * 17 + y * 31) % 23 + if checker: + albedo.append((184 + grain, 62 + grain // 2, 24 + grain // 3)) + else: + albedo.append((30 + grain // 3, 86 + grain, 142 + grain // 2)) + + span = tile - 1 + nx = -24 + ((x % tile) * 48 // span) + ny = -24 + ((y % tile) * 48 // span) + nz = math.isqrt(max(0, 127 * 127 - nx * nx - ny * ny)) + normal.append((128 + nx, 128 + ny, min(255, 128 + nz))) + + ambient_occlusion = 224 + ((x * 5 + y * 3) % 32) + roughness = 36 + (x * 184 // (size - 1)) + metallic = 232 if checker else 20 + orm.append((ambient_occlusion, roughness, metallic)) + return {"albedo": albedo, "normal": normal, "orm": orm} + + +def material_text() -> str: + return f"""// Original procedural openQ4 validation fixture; no external assets. +// Generated below .tmp by tools/validation/generate_pbr_fixture.py. Do not ship. +{MATERIAL_NAME} +{{ + bumpmap _flat + diffusemap {TEXTURE_PREFIX}_albedo + specularmap _black + + pbr {{ + workflow metallicRoughness + albedoMap {TEXTURE_PREFIX}_albedo + normalMap {TEXTURE_PREFIX}_normal + normalFormat tangentXYZ + ormMap {TEXTURE_PREFIX}_orm + metallic 1.0 + roughness 1.0 + ao 1.0 + normalScale 1.0 + }} +}} +""" + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def fixture_payloads(size: int) -> dict[str, bytes]: + """Return the complete canonical generated payload for one texture size.""" + pixels = procedural_pixels(size) + return { + "albedo": tga_bytes(size, size, pixels["albedo"]), + "normal": tga_bytes(size, size, pixels["normal"]), + "orm": tga_bytes(size, size, pixels["orm"]), + "material": material_text().encode("utf-8"), + } + + +def canonical_file_hashes(size: int) -> dict[str, str]: + paths = fixture_paths(Path(".")) + payloads = fixture_payloads(size) + return { + paths[name].as_posix(): hashlib.sha256(payload).hexdigest() + for name, payload in payloads.items() + } + + +def fixture_manifest(size: int, files: dict[str, str]) -> dict[str, Any]: + return { + "schemaVersion": FIXTURE_SCHEMA_VERSION, + "fixture": FIXTURE_ID, + "provenance": ( + "Original deterministic pixels and material text generated only by " + "tools/validation/generate_pbr_fixture.py; no third-party assets are incorporated." + ), + "license": "Covered by the openQ4 repository GNU GPL v3 licence.", + "generatedAssetsPolicy": "Temporary validation output below .tmp; do not commit as content.", + "stockMap": STOCK_MAP, + "benchmarkCase": BENCHMARK_CASE, + "materialOverride": MATERIAL_NAME, + "textureSize": [size, size], + "files": dict(sorted(files.items())), + } + + +def verify_fixture(runtime_root: Path, expected_size: int = DEFAULT_TEXTURE_SIZE) -> dict[str, Any]: + """Verify the canonical fixture identity and every generated byte before use.""" + root = validate_runtime_root(runtime_root) + if not root.is_dir(): + raise FileNotFoundError(f"fixture runtime root does not exist: {root}") + validate_size(expected_size) + paths = fixture_paths(root) + resolved_paths: dict[str, Path] = {} + for name, path in paths.items(): + resolved = validate_contained_target(root, path) + if not resolved.is_file(): + raise FileNotFoundError(f"fixture file is missing or not regular: {resolved}") + reject_hardlinked_file(resolved, "fixture file") + resolved_paths[name] = resolved + + try: + manifest_bytes = resolved_paths["manifest"].read_bytes() + manifest = json.loads(manifest_bytes.decode("utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise RuntimeError( + f"fixture manifest is unreadable or malformed: {resolved_paths['manifest']}" + ) from exc + if not isinstance(manifest, dict): + raise RuntimeError("fixture manifest root must be an object") + + expected_payloads = fixture_payloads(expected_size) + expected_hashes = { + paths[name].relative_to(root).as_posix(): hashlib.sha256(payload).hexdigest() + for name, payload in expected_payloads.items() + } + expected_manifest = fixture_manifest(expected_size, expected_hashes) + if manifest != expected_manifest: + raise RuntimeError("fixture manifest identity or canonical file inventory differs") + expected_manifest_bytes = ( + json.dumps(expected_manifest, indent=2, sort_keys=True) + "\n" + ).encode("utf-8") + if manifest_bytes != expected_manifest_bytes: + raise RuntimeError("fixture manifest bytes differ from the canonical manifest") + + file_records: list[dict[str, Any]] = [] + for name in sorted(expected_payloads): + path = validate_contained_target(root, paths[name]) + relative = path.relative_to(root).as_posix() + actual_bytes = path.read_bytes() + actual_hash = hashlib.sha256(actual_bytes).hexdigest() + expected_hash = expected_hashes[relative] + if actual_hash != expected_hash or actual_bytes != expected_payloads[name]: + raise RuntimeError(f"fixture file differs from canonical generated bytes: {relative}") + file_records.append( + {"path": relative, "size": len(actual_bytes), "sha256": actual_hash} + ) + + return { + "schemaVersion": FIXTURE_SCHEMA_VERSION, + "fixture": FIXTURE_ID, + "materialOverride": MATERIAL_NAME, + "stockMap": STOCK_MAP, + "benchmarkCase": BENCHMARK_CASE, + "textureSize": [expected_size, expected_size], + "manifest": { + "path": MANIFEST_NAME, + "size": len(manifest_bytes), + "sha256": hashlib.sha256(manifest_bytes).hexdigest(), + }, + "files": file_records, + } + + +def generate_fixture( + runtime_root: Path, size: int = DEFAULT_TEXTURE_SIZE, force: bool = False +) -> dict[str, Path]: + root = validate_runtime_root(runtime_root) + validate_size(size) + paths = fixture_paths(root) + validate_targets(root, paths, force) + for path in paths.values(): + resolved = validate_contained_target(root, path) + resolved.parent.mkdir(parents=True, exist_ok=True) + validate_contained_target(root, resolved) + + payloads = fixture_payloads(size) + for name, payload in payloads.items(): + path = validate_contained_target(root, paths[name]) + reject_hardlinked_file(path, "fixture output") + path.write_bytes(payload) + + files = { + path.relative_to(root).as_posix(): sha256(path) + for name, path in paths.items() + if name != "manifest" + } + manifest = fixture_manifest(size, files) + manifest_path = validate_contained_target(root, paths["manifest"]) + reject_hardlinked_file(manifest_path, "fixture output") + manifest_path.write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + return paths + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--runtime-root", + required=True, + type=Path, + help="existing or new runtime root below the repository .tmp directory", + ) + parser.add_argument( + "--size", + type=int, + default=DEFAULT_TEXTURE_SIZE, + help=f"power-of-two texture size (default: {DEFAULT_TEXTURE_SIZE})", + ) + parser.add_argument("--force", action="store_true", help="replace only this generator's existing files") + args = parser.parse_args() + outputs = generate_fixture(args.runtime_root, args.size, args.force) + print(f"wrote procedural PBR fixture: {outputs['manifest']}") + print(f"stock map: {STOCK_MAP}") + print(f"material override: {MATERIAL_NAME}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validation/gpu_skinning_evidence.py b/tools/validation/gpu_skinning_evidence.py new file mode 100644 index 00000000..1d2485c5 --- /dev/null +++ b/tools/validation/gpu_skinning_evidence.py @@ -0,0 +1,1025 @@ +#!/usr/bin/env python3 +"""Verify a fail-closed CPU/GPU renderer benchmark evidence pair. + +The two inputs must be real ``renderer_gameplay_benchmark.py`` JSON reports +captured from the same clean checkout, staged runtime, backend, cases, and +windowed display contract. The first report is the ``r_gpuSkinning 0`` CPU +reference and the second is the ``r_gpuSkinning 1`` run. This verifier reads +the retained engine logs and screenshots instead of trusting summary fields. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import re +import struct +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +REPORT_SCHEMA_VERSION = 3 +EVIDENCE_SCHEMA_VERSION = 1 +EVIDENCE_KIND = "openq4-gpu-skinning-paired-evidence" +DEFAULT_MINIMUM_CPU_P95_IMPROVEMENT = 1.0 +DEFAULT_IMAGE_RMS_THRESHOLD = 2.0 +DEFAULT_IMAGE_MAX_THRESHOLD = 24 + +FALLBACK_NAMES = ( + "disabled", + "backendUnavailable", + "missingBindPose", + "missingSkinVertices", + "vertexCount", + "residualWeights", + "jointCount", + "jointIndex", + "malformedWeights", + "skinScale", + "unsupportedMaterial", + "unsupportedPass", + "stencilVolume", + "decalOverlay", + "paletteAllocation", + "stalePalette", +) +SKINNING_COUNTER_NAMES = ( + "enabled", + "generation", + "packed", + "exact", + "attempts", + "admitted", + "prepared", + "cpuSkinVerts", + "cpuSkinUs", + "positionOnlyVerts", + "positionOnlyUs", + "preparedVertices", + "paletteUploads", + "paletteJoints", + "paletteBytes", + "cumulative", +) + +SKINNING_MARKER = re.compile( + r"^GPU skinning:\s*(?P[^\r\n]+)$", re.MULTILINE +) +FALLBACK_MARKER = re.compile( + r"^GPU skinning fallbacks:\s*(?P[^\r\n]+)$", re.MULTILINE +) +COUNTER = re.compile(r"(?P[A-Za-z][A-Za-z0-9]*)=(?P\d+)") +TIMING_MARKER = re.compile( + r"^OPENQ4_FRAME_TIMING_V1\s+map=(?P\S+)\s+" + r"backend=(?P\S+)\s+profile=(?P\S+)\s+" + r"cpuSamples=(?P\d+)\s+cpuP50Us=(?P\d+)\s+" + r"cpuP95Us=(?P\d+)\s+cpuP99Us=(?P\d+)\s+" + r"gpuAvailable=(?P[01])\s+gpuSamples=(?P\d+)\s+" + r"gpuP50Us=(?P-?\d+)\s+gpuP95Us=(?P-?\d+)\s+" + r"gpuP99Us=(?P-?\d+)$", + re.MULTILINE, +) + + +@dataclass(frozen=True) +class RoleRecord: + result: dict[str, Any] + role: dict[str, Any] + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def current_sha256(path: Path) -> str | None: + try: + return sha256(path) + except OSError: + return None + + +def load_report( + path: Path, label: str, failures: list[str] +) -> tuple[dict[str, Any], str | None]: + try: + raw = path.read_bytes() + except OSError as exc: + failures.append(f"{label}: cannot read UTF-8 JSON report: {exc}") + return {}, None + digest = hashlib.sha256(raw).hexdigest() + try: + decoded = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + failures.append(f"{label}: cannot read UTF-8 JSON report: {exc}") + return {}, digest + if not isinstance(decoded, dict): + failures.append(f"{label}: report root is not an object") + return {}, digest + return decoded, digest + + +def require_clean_report(report: dict[str, Any], label: str, failures: list[str]) -> None: + if report.get("schemaVersion") != REPORT_SCHEMA_VERSION: + failures.append( + f"{label}: unsupported renderer benchmark schema {report.get('schemaVersion')!r}" + ) + if report.get("status") != "pass": + failures.append(f"{label}: report status is not pass") + if report.get("dryRun") is not False: + failures.append(f"{label}: dry-run/planned evidence is not admissible") + if report.get("runtimeVerificationFailures") != []: + failures.append(f"{label}: staged runtime changed during capture") + if report.get("budgetEnforced") is not True: + failures.append(f"{label}: per-map CPU/GPU budget was not enforced") + git = report.get("git") + if not isinstance(git, dict): + failures.append(f"{label}: Git provenance is missing") + else: + if re.fullmatch(r"[0-9a-fA-F]{40,64}", str(git.get("revision", ""))) is None: + failures.append(f"{label}: Git revision is missing or malformed") + if git.get("dirty") is not False: + failures.append(f"{label}: evidence must come from a clean checkout") + runtime = report.get("runtime") + if not isinstance(runtime, dict) or not isinstance(runtime.get("files"), list) or not runtime["files"]: + failures.append(f"{label}: staged runtime inventory is missing") + else: + paths: set[str] = set() + for record in runtime["files"]: + if not isinstance(record, dict): + failures.append(f"{label}: malformed staged runtime file record") + continue + raw_path = record.get("path") + if ( + not isinstance(raw_path, str) + or not raw_path + or Path(raw_path).is_absolute() + or ".." in Path(raw_path).parts + or raw_path in paths + or not isinstance(record.get("size"), int) + or isinstance(record.get("size"), bool) + or record["size"] < 0 + or re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", ""))) is None + ): + failures.append(f"{label}: malformed/duplicate staged runtime file record") + continue + paths.add(raw_path) + + +def normalized_launch_cvars( + report: dict[str, Any], label: str, failures: list[str] +) -> tuple[dict[str, str], str | None]: + metadata = report.get("metadata") + if not isinstance(metadata, dict): + return {}, None + raw = metadata.get("launchCvars") + if not isinstance(raw, dict): + return {}, None + normalized: dict[str, str] = {} + for key, value in raw.items(): + folded = str(key).casefold() + if folded in normalized: + failures.append(f"{label}: duplicate case-insensitive launch CVar {key!r}") + normalized[folded] = str(value) + skinning = normalized.pop("r_gpuskinning", None) + return normalized, skinning + + +def require_pair_identity( + cpu: dict[str, Any], gpu: dict[str, Any], failures: list[str] +) -> None: + for key, label in ( + ("git", "Git provenance"), + ("runtime", "staged runtime inventory"), + ("budgetContract", "renderer budget contract"), + ): + if cpu.get(key) != gpu.get(key): + failures.append(f"pair: {label} differs") + + cpu_metadata = cpu.get("metadata") + gpu_metadata = gpu.get("metadata") + if not isinstance(cpu_metadata, dict) or not isinstance(gpu_metadata, dict): + failures.append("pair: benchmark metadata is missing") + return + cpu_identity = dict(cpu_metadata) + gpu_identity = dict(gpu_metadata) + for identity in (cpu_identity, gpu_identity): + identity.pop("generated", None) + identity.pop("launchCvars", None) + for key in sorted(set(cpu_identity) | set(gpu_identity)): + if cpu_identity.get(key) != gpu_identity.get(key): + failures.append(f"pair: metadata identity differs for {key}") + render_api = cpu_metadata.get("renderApi") + if render_api not in ("gl", "vk"): + failures.append(f"pair: unsupported renderer API identity {render_api!r}") + + cpu_cvars, cpu_skinning = normalized_launch_cvars(cpu, "CPU", failures) + gpu_cvars, gpu_skinning = normalized_launch_cvars(gpu, "GPU", failures) + if cpu_cvars != gpu_cvars: + failures.append("pair: non-skinning launch CVar identity differs") + if cpu_skinning != "0": + failures.append("CPU report must record launchCvars.r_gpuSkinning=0") + if gpu_skinning != "1": + failures.append("GPU report must record launchCvars.r_gpuSkinning=1") + + +def collect_roles( + report: dict[str, Any], label: str, failures: list[str] +) -> dict[tuple[str, str], RoleRecord]: + results = report.get("results") + if not isinstance(results, list) or not results: + failures.append(f"{label}: results are missing or empty") + return {} + records: dict[tuple[str, str], RoleRecord] = {} + for result in results: + if not isinstance(result, dict): + failures.append(f"{label}: malformed result") + continue + case_id = result.get("id") + if not isinstance(case_id, str) or not case_id: + failures.append(f"{label}: result case id is missing") + continue + if result.get("status") != "pass": + failures.append(f"{label}/{case_id}: result is not a pass") + if result.get("display") != "windowed": + failures.append(f"{label}/{case_id}: capture was not windowed") + display_contract = result.get("displayContract") + if ( + not isinstance(display_contract, dict) + or display_contract.get("width") != 1280 + or display_contract.get("height") != 720 + or display_contract.get("contractId") != "bordered-window-1280x720-v1" + ): + failures.append(f"{label}/{case_id}: canonical display contract is missing") + roles = result.get("roles") + if not isinstance(roles, list) or not roles: + failures.append(f"{label}/{case_id}: role evidence is missing") + continue + for role in roles: + if not isinstance(role, dict) or not isinstance(role.get("role"), str): + failures.append(f"{label}/{case_id}: malformed role evidence") + continue + role_name = role["role"] + key = (case_id, role_name) + if key in records: + failures.append(f"{label}/{case_id}/{role_name}: duplicate role evidence") + continue + if role.get("status") != "pass" or role.get("missing") != []: + failures.append(f"{label}/{case_id}/{role_name}: role is not a clean pass") + if role.get("failureDiagnostics") != []: + failures.append(f"{label}/{case_id}/{role_name}: failure diagnostics were retained") + if role.get("exitCode") != 0 or role.get("timedOut") is not False: + failures.append(f"{label}/{case_id}/{role_name}: process did not exit cleanly") + warnings = role.get("warnings") + if ( + not isinstance(warnings, dict) + or not warnings + or any( + not isinstance(value, int) or isinstance(value, bool) or value != 0 + for value in warnings.values() + ) + ): + failures.append(f"{label}/{case_id}/{role_name}: warning counters are not clean") + budget = role.get("budgetEvidence") + if ( + not isinstance(budget, dict) + or budget.get("status") != "pass" + or budget.get("failures") != [] + ): + failures.append(f"{label}/{case_id}/{role_name}: timing budget evidence is not a pass") + records[key] = RoleRecord(result, role) + return records + + +def require_result_identity( + key: tuple[str, str], cpu: RoleRecord, gpu: RoleRecord, failures: list[str] +) -> None: + identity_fields = ( + "id", + "mode", + "map", + "budgetMap", + "expectedBackend", + "renderApi", + "displayContract", + "tier", + "maxfps", + "swapInterval", + "display", + "shadowPreset", + "renderer", + ) + for field in identity_fields: + if cpu.result.get(field) != gpu.result.get(field): + failures.append(f"{key[0]}/{key[1]}: result identity differs for {field}") + backend = cpu.result.get("expectedBackend") + api = cpu.result.get("renderApi") + expected = "opengl" if api == "gl" else "vulkan" if api == "vk" else None + if backend != expected: + failures.append(f"{key[0]}/{key[1]}: backend/renderApi binding is inconsistent") + + +def verified_artifact( + report_path: Path, + role: dict[str, Any], + kind: str, + label: str, + failures: list[str], +) -> Path | None: + raw_artifacts = role.get("artifacts") + if not isinstance(raw_artifacts, list): + failures.append(f"{label}: artifact inventory is missing") + return None + matches = [ + item + for item in raw_artifacts + if isinstance(item, dict) and item.get("kind") == kind + ] + if len(matches) != 1: + failures.append(f"{label}: expected exactly one {kind} artifact") + return None + record = matches[0] + raw_path = record.get("path") + if not isinstance(raw_path, str) or not raw_path or Path(raw_path).is_absolute(): + failures.append(f"{label}: {kind} artifact path is not a safe relative path") + return None + report_dir = report_path.parent.resolve() + candidate = (report_dir / raw_path).resolve() + try: + candidate.relative_to(report_dir) + except ValueError: + failures.append(f"{label}: {kind} artifact escapes the report directory") + return None + if not candidate.is_file() or candidate.is_symlink(): + failures.append(f"{label}: {kind} artifact is missing or linked") + return None + actual_size = candidate.stat().st_size + actual_hash = sha256(candidate) + if record.get("size") != actual_size or record.get("sha256") != actual_hash: + failures.append(f"{label}: {kind} artifact hash/size differs from its report") + return None + return candidate + + +def unique_marker( + pattern: re.Pattern[str], text: str, name: str, label: str, failures: list[str] +) -> re.Match[str] | None: + matches = list(pattern.finditer(text)) + unique = {match.group(0) for match in matches} + if not matches: + failures.append(f"{label}: {name} marker is missing") + return None + if len(unique) != 1: + failures.append(f"{label}: conflicting {name} markers were retained") + return None + return matches[-1] + + +def parse_skinning_log( + text: str, enabled: bool, label: str, failures: list[str] +) -> dict[str, Any] | None: + skin_match = unique_marker(SKINNING_MARKER, text, "GPU skinning", label, failures) + fallback_match = unique_marker( + FALLBACK_MARKER, text, "GPU skinning fallback", label, failures + ) + timing_match = unique_marker(TIMING_MARKER, text, "frame timing", label, failures) + if skin_match is None or fallback_match is None or timing_match is None: + return None + counters = parse_counter_inventory( + skin_match.group("counters"), SKINNING_COUNTER_NAMES, "skinning", label, failures + ) + if counters is None: + return None + if counters["enabled"] != int(enabled): + failures.append(f"{label}: runtime skinning enabled marker does not match the report") + if counters["cumulative"] != 1: + failures.append(f"{label}: runtime marker is not the cumulative evidence contract") + if counters["generation"] <= 0: + failures.append(f"{label}: runtime contract generation is invalid") + if counters["exact"] > counters["packed"]: + failures.append(f"{label}: exact packed-vertex count exceeds pack attempts") + if counters["cpuSkinVerts"] < counters["positionOnlyVerts"]: + failures.append(f"{label}: position-only vertices exceed total CPU-skinned vertices") + if enabled: + if counters["attempts"] <= 0 or counters["admitted"] <= 0 or counters["prepared"] <= 0: + failures.append(f"{label}: GPU work was not attempted, admitted, and prepared") + if counters["admitted"] != counters["prepared"]: + failures.append(f"{label}: admitted/prepared surface counters differ") + if ( + counters["preparedVertices"] <= 0 + or counters["paletteUploads"] <= 0 + or counters["paletteJoints"] <= 0 + or counters["paletteBytes"] <= 0 + ): + failures.append(f"{label}: GPU vertices/joint-palette upload counters are empty") + if counters["paletteUploads"] != counters["prepared"]: + failures.append(f"{label}: prepared/palette-upload counters differ") + if counters["paletteBytes"] != counters["paletteJoints"] * 12 * 4: + failures.append(f"{label}: joint-palette byte accounting is inconsistent") + if counters["exact"] <= 0: + failures.append(f"{label}: exact four-weight work was not observed") + if counters["positionOnlyVerts"] <= 0: + failures.append(f"{label}: retained CPU position work was not observed") + else: + if counters["admitted"] != 0 or counters["prepared"] != 0: + failures.append(f"{label}: disabled CPU reference admitted GPU work") + if any( + counters[name] != 0 + for name in ( + "preparedVertices", + "paletteUploads", + "paletteJoints", + "paletteBytes", + "positionOnlyVerts", + "positionOnlyUs", + ) + ): + failures.append(f"{label}: disabled CPU reference retained GPU/position-only work") + if counters["cpuSkinVerts"] <= 0: + failures.append(f"{label}: CPU reference did not observe skeletal work") + + fallback_values = parse_counter_inventory( + fallback_match.group("counters"), FALLBACK_NAMES, "fallback", label, failures + ) + if fallback_values is None: + return None + + timing = {name: value for name, value in timing_match.groupdict().items()} + for name in ( + "cpuSamples", + "cpuP50Us", + "cpuP95Us", + "cpuP99Us", + "gpuAvailable", + "gpuSamples", + "gpuP50Us", + "gpuP95Us", + "gpuP99Us", + ): + timing[name] = int(timing[name]) + if not ( + timing["cpuSamples"] > 0 + and timing["cpuP50Us"] >= 0 + and timing["cpuP50Us"] <= timing["cpuP95Us"] <= timing["cpuP99Us"] + ): + failures.append(f"{label}: CPU timing marker is invalid") + if timing["gpuAvailable"]: + if not ( + timing["gpuSamples"] > 0 + and timing["gpuP50Us"] >= 0 + and timing["gpuP50Us"] <= timing["gpuP95Us"] <= timing["gpuP99Us"] + ): + failures.append(f"{label}: available GPU timing marker is invalid") + elif not ( + timing["gpuSamples"] == 0 + and timing["gpuP50Us"] == -1 + and timing["gpuP95Us"] == -1 + and timing["gpuP99Us"] == -1 + ): + failures.append(f"{label}: unavailable GPU timing marker is invalid") + if enabled: + if fallback_values["disabled"] != 0: + failures.append(f"{label}: enabled run recorded disabled fallbacks") + return {"counters": counters, "fallbacks": fallback_values, "timing": timing} + + +def parse_counter_inventory( + raw: str, + expected_names: tuple[str, ...], + inventory_name: str, + label: str, + failures: list[str], +) -> dict[str, int] | None: + tokens = raw.split() + pairs: list[tuple[str, str]] = [] + for token in tokens: + match = COUNTER.fullmatch(token) + if match is None: + failures.append(f"{label}: malformed {inventory_name} counter token {token!r}") + continue + pairs.append((match.group("name"), match.group("value"))) + values = {name: int(value) for name, value in pairs} + duplicate = len(pairs) != len(values) + if duplicate: + failures.append(f"{label}: duplicate {inventory_name} counter") + missing = sorted(set(expected_names) - set(values)) + unknown = sorted(set(values) - set(expected_names)) + if missing or unknown: + failures.append( + f"{label}: {inventory_name} inventory differs (missing={missing}, unknown={unknown})" + ) + return values if not missing and not unknown and not duplicate else None + + +def budget_measurement( + role: dict[str, Any], label: str, failures: list[str] +) -> dict[str, Any] | None: + budget = role.get("budgetEvidence") + measurement = budget.get("measurement") if isinstance(budget, dict) else None + if not isinstance(measurement, dict): + failures.append(f"{label}: recorded timing measurement is missing") + return None + return measurement + + +def marker_measurement(parsed: dict[str, Any]) -> dict[str, Any]: + timing = parsed["timing"] + return { + "map": timing["map"], + "backend": timing["backend"], + "profile": timing["profile"], + "cpu": { + "samples": timing["cpuSamples"], + "p50Us": timing["cpuP50Us"], + "p95Us": timing["cpuP95Us"], + "p99Us": timing["cpuP99Us"], + }, + "gpu": { + "available": timing["gpuAvailable"] == 1, + "samples": timing["gpuSamples"], + "p50Us": timing["gpuP50Us"], + "p95Us": timing["gpuP95Us"], + "p99Us": timing["gpuP99Us"], + }, + } + + +def load_tga_rgb(path: Path) -> tuple[int, int, bytes]: + data = path.read_bytes() + if len(data) < 18: + raise ValueError("TGA header is truncated") + id_length, color_map_type, image_type = data[0], data[1], data[2] + if color_map_type != 0 or image_type not in (2, 3): + raise ValueError("only uncompressed true-color/grayscale TGA is supported") + width = struct.unpack_from(" len(data): + raise ValueError("TGA pixel payload is truncated") + source = data[start:end] + rgb = bytearray(width * height * 3) + for pixel in range(width * height): + src = pixel * pixel_size + dst = pixel * 3 + if image_type == 3: + rgb[dst : dst + 3] = source[src : src + 1] * 3 + else: + rgb[dst : dst + 3] = bytes((source[src + 2], source[src + 1], source[src])) + return width, height, bytes(rgb) + + +def compare_images(cpu_path: Path, gpu_path: Path) -> dict[str, Any]: + cpu_width, cpu_height, cpu_pixels = load_tga_rgb(cpu_path) + gpu_width, gpu_height, gpu_pixels = load_tga_rgb(gpu_path) + if (cpu_width, cpu_height) != (gpu_width, gpu_height): + return { + "status": "dimension-mismatch", + "cpuSize": f"{cpu_width}x{cpu_height}", + "gpuSize": f"{gpu_width}x{gpu_height}", + } + total_squared = 0 + maximum = 0 + differing = 0 + for cpu_value, gpu_value in zip(cpu_pixels, gpu_pixels): + delta = abs(cpu_value - gpu_value) + if delta: + differing += 1 + total_squared += delta * delta + maximum = max(maximum, delta) + return { + "status": "compared", + "size": f"{cpu_width}x{cpu_height}", + "rms": round(math.sqrt(total_squared / max(1, len(cpu_pixels))), 6), + "maxDelta": maximum, + "differingChannels": differing, + "cpuSha256": sha256(cpu_path), + "gpuSha256": sha256(gpu_path), + } + + +def verify_pair( + cpu_path: Path, + gpu_path: Path, + minimum_improvement: float, + image_rms_threshold: float, + image_max_threshold: int, +) -> tuple[dict[str, Any], list[str]]: + failures: list[str] = [] + cpu_report, cpu_digest = load_report(cpu_path, "CPU", failures) + gpu_report, gpu_digest = load_report(gpu_path, "GPU", failures) + require_clean_report(cpu_report, "CPU", failures) + require_clean_report(gpu_report, "GPU", failures) + require_pair_identity(cpu_report, gpu_report, failures) + cpu_roles = collect_roles(cpu_report, "CPU", failures) + gpu_roles = collect_roles(gpu_report, "GPU", failures) + if set(cpu_roles) != set(gpu_roles): + failures.append( + "pair: case/role inventory differs " + f"(CPU-only={sorted(set(cpu_roles) - set(gpu_roles))}, " + f"GPU-only={sorted(set(gpu_roles) - set(cpu_roles))})" + ) + + comparisons: list[dict[str, Any]] = [] + observed_stencil_fallbacks = 0 + for key in sorted(set(cpu_roles) & set(gpu_roles)): + cpu_record = cpu_roles[key] + gpu_record = gpu_roles[key] + label = f"{key[0]}/{key[1]}" + require_result_identity(key, cpu_record, gpu_record, failures) + + cpu_log_path = verified_artifact( + cpu_path, cpu_record.role, "engineLog", f"CPU/{label}", failures + ) + gpu_log_path = verified_artifact( + gpu_path, gpu_record.role, "engineLog", f"GPU/{label}", failures + ) + cpu_shot = verified_artifact( + cpu_path, cpu_record.role, "screenshot", f"CPU/{label}", failures + ) + gpu_shot = verified_artifact( + gpu_path, gpu_record.role, "screenshot", f"GPU/{label}", failures + ) + + cpu_skin = None + gpu_skin = None + if cpu_log_path is not None: + cpu_skin = parse_skinning_log( + cpu_log_path.read_text(encoding="utf-8", errors="replace"), + False, + f"CPU/{label}", + failures, + ) + if gpu_log_path is not None: + gpu_skin = parse_skinning_log( + gpu_log_path.read_text(encoding="utf-8", errors="replace"), + True, + f"GPU/{label}", + failures, + ) + if gpu_skin is not None: + observed_stencil_fallbacks += gpu_skin["fallbacks"].get("stencilVolume", 0) + + expected_map = str(cpu_record.result.get("budgetMap", "")).casefold() + expected_backend = str(cpu_record.result.get("expectedBackend", "")).casefold() + cpu_metadata = cpu_report.get("metadata", {}) + expected_profile = str(cpu_metadata.get("benchmarkPreset", "")).casefold() + for parsed, run_label in ((cpu_skin, "CPU"), (gpu_skin, "GPU")): + if parsed is None: + continue + timing = parsed["timing"] + if timing["map"].casefold() != expected_map: + failures.append(f"{run_label}/{label}: timing marker map identity differs") + if timing["backend"].casefold() != expected_backend: + failures.append(f"{run_label}/{label}: timing marker backend identity differs") + if timing["profile"].casefold() != expected_profile: + failures.append(f"{run_label}/{label}: timing marker profile identity differs") + + cpu_measurement = budget_measurement( + cpu_record.role, f"CPU/{label}", failures + ) + gpu_measurement = budget_measurement( + gpu_record.role, f"GPU/{label}", failures + ) + for parsed, recorded, run_label in ( + (cpu_skin, cpu_measurement, "CPU"), + (gpu_skin, gpu_measurement, "GPU"), + ): + if parsed is not None and recorded is not None: + retained = marker_measurement(parsed) + if retained != recorded: + failures.append( + f"{run_label}/{label}: retained timing marker disagrees with report measurement" + ) + + cpu = cpu_measurement.get("cpu") if isinstance(cpu_measurement, dict) else None + gpu = gpu_measurement.get("cpu") if isinstance(gpu_measurement, dict) else None + cpu_p95 = cpu.get("p95Us") if isinstance(cpu, dict) else None + gpu_p95 = gpu.get("p95Us") if isinstance(gpu, dict) else None + for value, run_label in ((cpu_p95, "CPU"), (gpu_p95, "GPU")): + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + failures.append(f"{run_label}/{label}: recorded CPU P95 is missing or invalid") + improvement = None + if ( + isinstance(cpu_p95, int) + and not isinstance(cpu_p95, bool) + and cpu_p95 > 0 + and isinstance(gpu_p95, int) + and not isinstance(gpu_p95, bool) + and gpu_p95 > 0 + ): + improvement = 100.0 * (cpu_p95 - gpu_p95) / cpu_p95 + if improvement + 1e-9 < minimum_improvement: + failures.append( + f"{label}: CPU P95 improvement {improvement:.3f}% is below " + f"the required {minimum_improvement:.3f}%" + ) + + image = None + if cpu_shot is not None and gpu_shot is not None: + try: + image = compare_images(cpu_shot, gpu_shot) + except (OSError, ValueError) as exc: + failures.append(f"{label}: screenshot comparison failed: {exc}") + else: + if image.get("status") != "compared": + failures.append(f"{label}: screenshot dimensions differ") + elif ( + image["rms"] > image_rms_threshold + or image["maxDelta"] > image_max_threshold + ): + failures.append( + f"{label}: screenshot delta rms={image['rms']} max={image['maxDelta']} " + f"exceeds {image_rms_threshold}/{image_max_threshold}" + ) + display = cpu_record.result.get("displayContract", {}) + expected_size = f"{display.get('width')}x{display.get('height')}" + if image.get("status") == "compared" and image.get("size") != expected_size: + failures.append( + f"{label}: screenshot size {image.get('size')} differs from {expected_size}" + ) + + comparisons.append( + { + "case": key[0], + "role": key[1], + "backend": cpu_record.result.get("expectedBackend"), + "cpuP95Us": cpu_p95, + "gpuP95Us": gpu_p95, + "cpuP95ImprovementPercent": ( + round(improvement, 6) if improvement is not None else None + ), + "cpuSkinning": cpu_skin, + "gpuSkinning": gpu_skin, + "image": image, + } + ) + + if comparisons and observed_stencil_fallbacks <= 0: + failures.append("pair: no classified CPU stencil-volume fallback was observed") + + for path, recorded_digest, label in ( + (cpu_path, cpu_digest, "CPU"), + (gpu_path, gpu_digest, "GPU"), + ): + if recorded_digest is not None and current_sha256(path) != recorded_digest: + failures.append(f"{label}: report changed during verification") + + payload = { + "schemaVersion": EVIDENCE_SCHEMA_VERSION, + "kind": EVIDENCE_KIND, + "status": "pass" if not failures else "fail", + "cpuReport": {"path": str(cpu_path), "sha256": cpu_digest}, + "gpuReport": {"path": str(gpu_path), "sha256": gpu_digest}, + "requirements": { + "minimumCpuP95ImprovementPercent": minimum_improvement, + "imageRmsThreshold": image_rms_threshold, + "imageMaxThreshold": image_max_threshold, + }, + "comparisons": comparisons, + "failures": failures, + } + return payload, failures + + +def artifact_record(path: Path, root: Path, kind: str) -> dict[str, Any]: + return { + "kind": kind, + "path": path.relative_to(root).as_posix(), + "size": path.stat().st_size, + "sha256": sha256(path), + } + + +def write_test_tga( + path: Path, pixel: tuple[int, int, int], width: int = 1280, height: int = 720 +) -> None: + if width <= 0 or height <= 0 or any(value < 0 or value > 255 for value in pixel): + raise ValueError("invalid self-test TGA parameters") + header = bytearray(18) + header[2] = 2 + struct.pack_into(" Path: + root.mkdir() + log = root / "engine.log" + screenshot = root / "frame.tga" + fallback_values = {name: 0 for name in FALLBACK_NAMES} + fallback_values["stencilVolume"] = 1 + skin_line = ( + "GPU skinning: enabled=%d cumulative=1 generation=2 packed=100 exact=100 attempts=%d " + "admitted=%d prepared=%d preparedVertices=%d paletteUploads=%d paletteJoints=%d " + "paletteBytes=%d cpuSkinVerts=1000 cpuSkinUs=%d " + "positionOnlyVerts=%d positionOnlyUs=%d" + % ( + int(enabled), + 10 if enabled else 0, + 8 if enabled else 0, + 8 if enabled else 0, + 1000 if enabled else 0, + 8 if enabled else 0, + 64 if enabled else 0, + 3072 if enabled else 0, + 2000 if enabled else 9000, + 1000 if enabled else 0, + 1500 if enabled else 0, + ) + ) + fallback_line = "GPU skinning fallbacks: " + " ".join( + f"{name}={fallback_values[name]}" for name in FALLBACK_NAMES + ) + timing_line = ( + "OPENQ4_FRAME_TIMING_V1 map=game/airdefense2 backend=opengl profile=baseline " + f"cpuSamples=600 cpuP50Us=7000 cpuP95Us={cpu_p95} cpuP99Us={cpu_p95 + 2000} " + "gpuAvailable=1 gpuSamples=598 gpuP50Us=5000 gpuP95Us=7000 gpuP99Us=8000" + ) + log.write_text(f"{skin_line}\n{fallback_line}\n{timing_line}\n", encoding="utf-8") + write_test_tga(screenshot, (10, 20, 30)) + + role = { + "role": "sp", + "status": "pass", + "exitCode": 0, + "timedOut": False, + "missing": [], + "failureDiagnostics": [], + "warnings": {"rendererError": 0}, + "budgetEvidence": { + "status": "pass", + "failures": [], + "measurement": { + "map": "game/airdefense2", + "backend": "opengl", + "profile": "baseline", + "cpu": { + "samples": 600, + "p50Us": 7000, + "p95Us": cpu_p95, + "p99Us": cpu_p95 + 2000, + }, + "gpu": { + "available": True, + "samples": 598, + "p50Us": 5000, + "p95Us": 7000, + "p99Us": 8000, + }, + }, + }, + "artifacts": [ + artifact_record(log, root, "engineLog"), + artifact_record(screenshot, root, "screenshot"), + ], + } + result = { + "id": "sp-airdefense2-auto-fps240-vsync0-windowed-shadowstencil-gl", + "mode": "SP", + "map": "game/airdefense2", + "budgetMap": "game/airdefense2", + "expectedBackend": "opengl", + "renderApi": "gl", + "displayContract": { + "contractId": "bordered-window-1280x720-v1", + "width": 1280, + "height": 720, + }, + "purpose": "self-test", + "tier": "auto", + "maxfps": "240", + "swapInterval": "0", + "display": "windowed", + "shadowPreset": "stencil", + "renderer": "best", + "status": "pass", + "roles": [role], + } + report = { + "schemaVersion": REPORT_SCHEMA_VERSION, + "status": "pass", + "dryRun": False, + "git": {"policy": "current-openq4-head-and-dirty-state-v1", "revision": "a" * 40, "dirty": False}, + "runtime": {"path": ".install", "executable": "openQ4-client_x64.exe", "files": [{"path": "openQ4-client_x64.exe", "size": 1, "sha256": "0" * 64}]}, + "runtimeVerificationFailures": [], + "budgetContract": {"contractId": "self-test", "sha256": "1" * 64}, + "budgetEnforced": True, + "metadata": { + "profile": "smoke", + "benchmarkPreset": "baseline", + "renderApi": "gl", + "autoexecDelayMs": 1000, + "settleFrames": 360, + "sampleFrames": 600, + "sampleMsec": 0, + "profileCvars": {}, + "profileExecCommands": [], + "execCommands": [], + "budgetDisplayContract": {"contractId": "bordered-window-1280x720-v1"}, + "launchCvars": {"r_gpuSkinning": "1" if enabled else "0"}, + }, + "results": [result], + } + path = root / "renderer_gameplay_benchmark_report.json" + path.write_text(json.dumps(report, indent=2), encoding="utf-8") + return path + + +def run_self_test() -> None: + with tempfile.TemporaryDirectory(prefix="openq4-gpu-skinning-evidence-") as raw: + root = Path(raw) + cpu = build_test_report(root / "cpu", False, 10000) + gpu = build_test_report(root / "gpu", True, 8500) + payload, failures = verify_pair(cpu, gpu, 10.0, 0.0, 0) + if failures or payload["status"] != "pass": + raise AssertionError(f"valid synthetic pair failed: {failures}") + + decoded = json.loads(gpu.read_text(encoding="utf-8")) + decoded["git"]["dirty"] = True + gpu.write_text(json.dumps(decoded), encoding="utf-8") + _, dirty_failures = verify_pair(cpu, gpu, 10.0, 0.0, 0) + if not any("clean checkout" in failure for failure in dirty_failures): + raise AssertionError("dirty synthetic evidence did not fail closed") + print("GPU skinning paired evidence self-test: PASS") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("cpu_report", nargs="?", help="r_gpuSkinning=0 benchmark JSON") + parser.add_argument("gpu_report", nargs="?", help="r_gpuSkinning=1 benchmark JSON") + parser.add_argument( + "--minimum-cpu-p95-improvement", + type=float, + default=DEFAULT_MINIMUM_CPU_P95_IMPROVEMENT, + help="Minimum required reduction for every paired CPU P95 (percent).", + ) + parser.add_argument( + "--image-rms-threshold", + type=float, + default=DEFAULT_IMAGE_RMS_THRESHOLD, + help="Maximum backend-local CPU/GPU screenshot RMS channel delta.", + ) + parser.add_argument( + "--image-max-threshold", + type=int, + default=DEFAULT_IMAGE_MAX_THRESHOLD, + help="Maximum backend-local CPU/GPU screenshot channel delta.", + ) + parser.add_argument("--output", help="Optional path for the verification JSON payload.") + parser.add_argument("--self-test", action="store_true", help="Run dependency-free synthetic fixtures.") + args = parser.parse_args(argv) + if args.minimum_cpu_p95_improvement < 0 or not math.isfinite(args.minimum_cpu_p95_improvement): + parser.error("--minimum-cpu-p95-improvement must be finite and non-negative") + if args.image_rms_threshold < 0 or not math.isfinite(args.image_rms_threshold): + parser.error("--image-rms-threshold must be finite and non-negative") + if not (0 <= args.image_max_threshold <= 255): + parser.error("--image-max-threshold must be in the range 0..255") + if not args.self_test and (not args.cpu_report or not args.gpu_report): + parser.error("cpu_report and gpu_report are required unless --self-test is used") + return args + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + if args.self_test: + run_self_test() + if not args.cpu_report and not args.gpu_report: + return 0 + if not args.cpu_report or not args.gpu_report: + return 0 + cpu_path = Path(args.cpu_report).resolve() + gpu_path = Path(args.gpu_report).resolve() + payload, failures = verify_pair( + cpu_path, + gpu_path, + args.minimum_cpu_p95_improvement, + args.image_rms_threshold, + args.image_max_threshold, + ) + encoded = json.dumps(payload, indent=2) + "\n" + if args.output: + Path(args.output).resolve().write_text(encoded, encoding="utf-8") + else: + sys.stdout.write(encoded) + for failure in failures: + print(f"error: {failure}", file=sys.stderr) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/validation/openq4_validate.py b/tools/validation/openq4_validate.py index f209e8ae..4220d33d 100644 --- a/tools/validation/openq4_validate.py +++ b/tools/validation/openq4_validate.py @@ -414,6 +414,7 @@ def run_python_tests(args: argparse.Namespace, root: Path, env: dict[str, str]) root / "tools" / "tests" / "game_type_module_selection.py", root / "tools" / "tests" / "gamelibs_staging.py", root / "tools" / "tests" / "generated_animation_cache.py", + root / "tools" / "tests" / "level_load_cache.py", root / "tools" / "tests" / "hdr_postprocess_math.py", root / "tools" / "tests" / "idstr_input_safety.py", root / "tools" / "tests" / "lexer_peek_contract.py", @@ -481,26 +482,47 @@ def run_python_tests(args: argparse.Namespace, root: Path, env: dict[str, str]) root / "tools" / "tests" / "native_glx_shutdown.py", root / "tools" / "tests" / "network_ipv4_support.py", root / "tools" / "tests" / "network_ipv6_support.py", + root / "tools" / "tests" / "network_security.py", + root / "tools" / "tests" / "openurl_security.py", root / "tools" / "tests" / "openq4_pure_pack.py", + root / "tools" / "tests" / "p0_governance_evidence.py", root / "tools" / "tests" / "packaging_safety.py", + root / "tools" / "tests" / "parallel_job_system.py", + root / "tools" / "tests" / "pbr_orm_packer.py", + root / "tools" / "tests" / "pbr_procedural_fixture.py", root / "tools" / "tests" / "preprocessor_macro_safety.py", root / "tools" / "tests" / "posix_memory_management.py", root / "tools" / "tests" / "posix_monotonic_time.py", root / "tools" / "tests" / "posix_network_resolution.py", root / "tools" / "tests" / "posix_thread_shutdown.py", root / "tools" / "tests" / "release_tooling_safety.py", + root / "tools" / "tests" / "renderer_advanced_lighting.py", + root / "tools" / "tests" / "renderer_budget_contract.py", root / "tools" / "tests" / "renderer_cel_shading.py", + root / "tools" / "tests" / "renderer_gpu_frame_timing.py", + root / "tools" / "tests" / "renderer_gpu_skinning.py", root / "tools" / "tests" / "renderer_mp_flat_items.py", + root / "tools" / "tests" / "renderer_pbr_materials.py", + root / "tools" / "tests" / "renderer_classic_gui_domain.py", + root / "tools" / "tests" / "renderer_classic_cinematic_post_domain.py", + root / "tools" / "tests" / "renderer_classic_special_frame_domain.py", + root / "tools" / "tests" / "renderer_classic_world_ambient_domain.py", + root / "tools" / "tests" / "renderer_classic_interaction_domain.py", + root / "tools" / "tests" / "renderer_classic_fog_blend_domain.py", + root / "tools" / "tests" / "renderer_classic_subview_domain.py", + root / "tools" / "tests" / "renderer_classic_deform_domain.py", root / "tools" / "tests" / "renderer_msaa_cvar_safety.py", root / "tools" / "tests" / "renderer_picmip_policy.py", root / "tools" / "tests" / "renderer_player_visibility.py", root / "tools" / "tests" / "renderer_screenshot_readback.py", root / "tools" / "tests" / "renderer_supersampling_safety.py", + root / "tools" / "tests" / "renderer_temporal_presentation.py", root / "tools" / "tests" / "renderer_vulkan_decal_compatibility.py", root / "tools" / "tests" / "renderer_vulkan_gui_residency.py", root / "tools" / "tests" / "renderer_vulkan_md5r_compatibility.py", root / "tools" / "tests" / "renderer_vulkan_shadow_compatibility.py", root / "tools" / "tests" / "renderer_vulkan_world_interaction_compatibility.py", + root / "tools" / "tests" / "rigid_body_recovery_diagnostics.py", root / "tools" / "tests" / "savegame_corruption_contract.py", root / "tools" / "tests" / "savegame_pointer_width_safety.py", root / "tools" / "tests" / "savegame_v3_contract.py", @@ -512,9 +534,12 @@ def run_python_tests(args: argparse.Namespace, root: Path, env: dict[str, str]) root / "tools" / "tests" / "system_console_presentation.py", root / "tools" / "tests" / "lang_table_encoding.py", root / "tools" / "tests" / "startup_language_override.py", + root / "tools" / "tests" / "stock_asset_baseline.py", + root / "tools" / "tests" / "stock_media_fallbacks.py", root / "tools" / "tests" / "ui_embedded_icons.py", root / "tools" / "tests" / "validation_hardening.py", root / "tools" / "tests" / "vk_shader_header_pin.py", + root / "tools" / "tests" / "vk_temporal_resolve_shader_pin.py", root / "tools" / "tests" / "vscode_fast_build.py", ] failures: list[str] = [] diff --git a/tools/validation/renderer_budget_contract.py b/tools/validation/renderer_budget_contract.py new file mode 100644 index 00000000..67698696 --- /dev/null +++ b/tools/validation/renderer_budget_contract.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +"""Versioned per-map CPU/GPU renderer budget contracts and evidence checks. + +This module deliberately owns no renderer implementation. It validates the +backend-neutral timing marker emitted by ``rendererBenchmarkCapture`` and is +shared by the gameplay benchmark and retail-PK4 evidence harnesses. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any, Iterable + + +CONTRACT_SCHEMA_VERSION = 1 +CONTRACT_KIND = "openq4-renderer-per-map-budgets" +TIME_UNIT = "microseconds" +MARKER_PREFIX = "OPENQ4_FRAME_TIMING_V1" +DEFAULT_CONTRACT_PATH = Path(__file__).with_name("renderer_per_map_budgets.json") +SUPPORTED_BACKENDS = ("opengl", "vulkan") + +_ROOT_KEYS = {"schemaVersion", "kind", "contractId", "timeUnit", "budgets"} +_BUDGET_KEYS = {"id", "map", "backend", "profile", "minimumSamples", "cpu", "gpu"} +_MINIMUM_SAMPLE_KEYS = {"cpu", "gpu"} +_CPU_KEYS = {"p95Us", "p99Us"} +_GPU_KEYS = {"required", "p95Us", "p99Us"} +_SAFE_ID = re.compile(r"[a-z0-9][a-z0-9_.-]*") +_SAFE_MAP = re.compile(r"[a-z0-9][a-z0-9_./-]*") +_SAFE_PROFILE = re.compile(r"[a-z0-9][a-z0-9_.-]*") +_SHA256 = re.compile(r"[0-9a-f]{64}") +_MARKER_PATTERN = re.compile( + rf"^{MARKER_PREFIX} " + r"map=(?P[a-z0-9][a-z0-9_./-]*) " + r"backend=(?Popengl|vulkan) " + r"profile=(?P[a-z0-9][a-z0-9_.-]*) " + r"cpuSamples=(?P[0-9]+) " + r"cpuP50Us=(?P[0-9]+) " + r"cpuP95Us=(?P[0-9]+) " + r"cpuP99Us=(?P[0-9]+) " + r"gpuAvailable=(?P[01]) " + r"gpuSamples=(?P[0-9]+) " + r"gpuP50Us=(?P-?[0-9]+) " + r"gpuP95Us=(?P-?[0-9]+) " + r"gpuP99Us=(?P-?[0-9]+)$" +) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def normalize_map_name(value: str) -> str: + normalized = value.strip().replace("\\", "/").casefold() + while "//" in normalized: + normalized = normalized.replace("//", "/") + return normalized.removesuffix(".map") + + +def _exact_keys(value: dict[str, Any], expected: set[str], context: str) -> None: + actual = set(value) + missing = sorted(expected - actual) + extra = sorted(actual - expected) + if missing or extra: + details: list[str] = [] + if missing: + details.append("missing " + ", ".join(missing)) + if extra: + details.append("unknown " + ", ".join(extra)) + raise ValueError(f"{context} fields differ ({'; '.join(details)})") + + +def _positive_int(value: Any, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError(f"{context} must be a positive integer") + return value + + +def _validate_thresholds(value: Any, expected_keys: set[str], context: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise ValueError(f"{context} must be an object") + _exact_keys(value, expected_keys, context) + p95 = _positive_int(value.get("p95Us"), f"{context}.p95Us") + p99 = _positive_int(value.get("p99Us"), f"{context}.p99Us") + if p99 < p95: + raise ValueError(f"{context}.p99Us must be greater than or equal to p95Us") + return {**value, "p95Us": p95, "p99Us": p99} + + +def validate_contract(payload: Any) -> dict[str, Any]: + if not isinstance(payload, dict): + raise ValueError("budget contract root must be an object") + _exact_keys(payload, _ROOT_KEYS, "budget contract") + if payload.get("schemaVersion") != CONTRACT_SCHEMA_VERSION: + raise ValueError( + f"unsupported budget contract schema: {payload.get('schemaVersion')!r}" + ) + if payload.get("kind") != CONTRACT_KIND: + raise ValueError(f"budget contract kind must be {CONTRACT_KIND!r}") + contract_id = payload.get("contractId") + if not isinstance(contract_id, str) or _SAFE_ID.fullmatch(contract_id) is None: + raise ValueError("budget contract contractId is not a canonical identifier") + if payload.get("timeUnit") != TIME_UNIT: + raise ValueError(f"budget contract timeUnit must be {TIME_UNIT!r}") + budgets = payload.get("budgets") + if not isinstance(budgets, list) or not budgets: + raise ValueError("budget contract budgets must be a nonempty array") + + normalized_budgets: list[dict[str, Any]] = [] + seen_ids: set[str] = set() + seen_keys: set[tuple[str, str, str]] = set() + for index, raw in enumerate(budgets): + context = f"budget[{index}]" + if not isinstance(raw, dict): + raise ValueError(f"{context} must be an object") + _exact_keys(raw, _BUDGET_KEYS, context) + budget_id = raw.get("id") + if not isinstance(budget_id, str) or _SAFE_ID.fullmatch(budget_id) is None: + raise ValueError(f"{context}.id is not a canonical identifier") + if budget_id in seen_ids: + raise ValueError(f"duplicate budget id: {budget_id}") + seen_ids.add(budget_id) + + map_name = normalize_map_name(str(raw.get("map", ""))) + if _SAFE_MAP.fullmatch(map_name) is None or map_name != raw.get("map"): + raise ValueError(f"{context}.map is not a canonical lowercase VFS map path") + backend = raw.get("backend") + if backend not in SUPPORTED_BACKENDS: + raise ValueError(f"{context}.backend must be one of {SUPPORTED_BACKENDS}") + profile = raw.get("profile") + if not isinstance(profile, str) or _SAFE_PROFILE.fullmatch(profile) is None: + raise ValueError(f"{context}.profile is not a canonical identifier") + identity = (map_name, backend, profile) + if identity in seen_keys: + raise ValueError( + "duplicate map/backend/profile budget: " + "/".join(identity) + ) + seen_keys.add(identity) + + minimum = raw.get("minimumSamples") + if not isinstance(minimum, dict): + raise ValueError(f"{context}.minimumSamples must be an object") + _exact_keys(minimum, _MINIMUM_SAMPLE_KEYS, f"{context}.minimumSamples") + minimum_cpu = _positive_int(minimum.get("cpu"), f"{context}.minimumSamples.cpu") + minimum_gpu = _positive_int(minimum.get("gpu"), f"{context}.minimumSamples.gpu") + cpu = _validate_thresholds(raw.get("cpu"), _CPU_KEYS, f"{context}.cpu") + gpu = _validate_thresholds(raw.get("gpu"), _GPU_KEYS, f"{context}.gpu") + if gpu.get("required") is not True: + raise ValueError(f"{context}.gpu.required must be true for promotion evidence") + + normalized_budgets.append( + { + "id": budget_id, + "map": map_name, + "backend": backend, + "profile": profile, + "minimumSamples": {"cpu": minimum_cpu, "gpu": minimum_gpu}, + "cpu": cpu, + "gpu": gpu, + } + ) + return { + "schemaVersion": CONTRACT_SCHEMA_VERSION, + "kind": CONTRACT_KIND, + "contractId": contract_id, + "timeUnit": TIME_UNIT, + "budgets": normalized_budgets, + } + + +def load_contract(path: Path) -> tuple[dict[str, Any], dict[str, Any]]: + resolved = path.resolve() + if not resolved.is_file(): + raise ValueError(f"renderer budget contract does not exist: {resolved}") + raw = resolved.read_bytes() + try: + decoded = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError(f"renderer budget contract is not valid UTF-8 JSON: {exc}") from exc + contract = validate_contract(decoded) + repository_root = Path(__file__).resolve().parents[2] + try: + path_hint = resolved.relative_to(repository_root).as_posix() + except ValueError: + path_hint = f"external:{resolved.name}" + binding = { + "schemaVersion": CONTRACT_SCHEMA_VERSION, + "kind": CONTRACT_KIND, + "contractId": contract["contractId"], + "path": path_hint, + "sha256": hashlib.sha256(raw).hexdigest(), + } + return contract, binding + + +def verify_contract_binding( + recorded: Any, contract: dict[str, Any], binding: dict[str, Any] +) -> list[str]: + failures: list[str] = [] + if not isinstance(recorded, dict): + return ["report does not contain a renderer budget contract binding"] + if recorded != binding: + failures.append("renderer budget contract binding differs from the selected contract") + if recorded.get("schemaVersion") != contract.get("schemaVersion"): + failures.append("renderer budget contract schema binding differs") + if _SHA256.fullmatch(str(recorded.get("sha256", ""))) is None: + failures.append("renderer budget contract SHA-256 is malformed") + return failures + + +def _measurement_from_match(match: re.Match[str]) -> dict[str, Any]: + values = match.groupdict() + return { + "map": values["map"], + "backend": values["backend"], + "profile": values["profile"], + "cpu": { + "samples": int(values["cpuSamples"]), + "p50Us": int(values["cpuP50Us"]), + "p95Us": int(values["cpuP95Us"]), + "p99Us": int(values["cpuP99Us"]), + }, + "gpu": { + "available": values["gpuAvailable"] == "1", + "samples": int(values["gpuSamples"]), + "p50Us": int(values["gpuP50Us"]), + "p95Us": int(values["gpuP95Us"]), + "p99Us": int(values["gpuP99Us"]), + }, + } + + +def parse_timing_evidence(sources: Iterable[str]) -> tuple[dict[str, Any] | None, list[str]]: + marker_lines: set[str] = set() + failures: list[str] = [] + for source_index, text in enumerate(sources): + source_markers = [ + line.strip() for line in text.splitlines() if MARKER_PREFIX in line + ] + if len(source_markers) > 1: + failures.append( + f"source {source_index} contains {len(source_markers)} {MARKER_PREFIX} markers; expected exactly one" + ) + marker_lines.update(source_markers) + if not marker_lines: + return None, [*failures, f"{MARKER_PREFIX} marker is missing"] + if len(marker_lines) != 1: + return None, [*failures, f"conflicting {MARKER_PREFIX} markers were recorded"] + marker = next(iter(marker_lines)) + match = _MARKER_PATTERN.fullmatch(marker) + if match is None: + return None, [*failures, f"{MARKER_PREFIX} marker has a malformed or non-versioned shape"] + measurement = _measurement_from_match(match) + cpu = measurement["cpu"] + gpu = measurement["gpu"] + if not (cpu["p50Us"] <= cpu["p95Us"] <= cpu["p99Us"]): + failures.append("CPU timing percentiles are not monotonic") + if gpu["available"]: + if gpu["samples"] <= 0: + failures.append("GPU timing is available but gpuSamples is zero") + if min(gpu["p50Us"], gpu["p95Us"], gpu["p99Us"]) < 0: + failures.append("available GPU timing contains a negative percentile") + elif not (gpu["p50Us"] <= gpu["p95Us"] <= gpu["p99Us"]): + failures.append("GPU timing percentiles are not monotonic") + elif gpu != { + "available": False, + "samples": 0, + "p50Us": -1, + "p95Us": -1, + "p99Us": -1, + }: + failures.append( + "unavailable GPU timing must use zero samples and -1 percentiles" + ) + return {"marker": marker, "measurement": measurement}, failures + + +def select_budget( + contract: dict[str, Any], map_name: str, backend: str, profile: str +) -> dict[str, Any] | None: + identity = (normalize_map_name(map_name), backend.casefold(), profile.casefold()) + matches = [ + budget + for budget in contract["budgets"] + if (budget["map"], budget["backend"], budget["profile"]) == identity + ] + return matches[0] if len(matches) == 1 else None + + +def evaluate_timing_evidence( + sources: Iterable[str], + contract: dict[str, Any], + expected_map: str, + expected_backend: str, + expected_profile: str, +) -> tuple[dict[str, Any], list[str]]: + parsed, failures = parse_timing_evidence(sources) + if parsed is None: + return { + "status": "fail", + "expected": { + "map": normalize_map_name(expected_map), + "backend": expected_backend.casefold(), + "profile": expected_profile.casefold(), + }, + "failures": failures, + }, failures + + measurement = parsed["measurement"] + expected_map_normalized = normalize_map_name(expected_map) + expected_backend_normalized = expected_backend.casefold() + expected_profile_normalized = expected_profile.casefold() + if measurement["map"] != expected_map_normalized: + failures.append( + f"timing map {measurement['map']!r} does not match expected {expected_map_normalized!r}" + ) + if measurement["backend"] != expected_backend_normalized: + failures.append( + "timing backend " + f"{measurement['backend']!r} does not match expected {expected_backend_normalized!r}" + ) + if measurement["profile"] != expected_profile_normalized: + failures.append( + "timing profile " + f"{measurement['profile']!r} does not match expected {expected_profile_normalized!r}" + ) + budget = select_budget( + contract, measurement["map"], measurement["backend"], measurement["profile"] + ) + if budget is None: + failures.append( + "no exact per-map budget for " + f"{measurement['map']}/{measurement['backend']}/{measurement['profile']}" + ) + thresholds: dict[str, Any] = {} + budget_id = "" + else: + budget_id = budget["id"] + thresholds = { + "minimumSamples": budget["minimumSamples"], + "cpu": budget["cpu"], + "gpu": budget["gpu"], + } + cpu = measurement["cpu"] + gpu = measurement["gpu"] + if cpu["samples"] < budget["minimumSamples"]["cpu"]: + failures.append( + f"CPU samples {cpu['samples']} below required {budget['minimumSamples']['cpu']}" + ) + for percentile in ("p95Us", "p99Us"): + if cpu[percentile] > budget["cpu"][percentile]: + failures.append( + f"CPU {percentile} {cpu[percentile]} exceeds {budget['cpu'][percentile]}" + ) + if budget["gpu"]["required"] and not gpu["available"]: + failures.append("required GPU frame timing is unavailable") + if gpu["available"]: + if gpu["samples"] < budget["minimumSamples"]["gpu"]: + failures.append( + f"GPU samples {gpu['samples']} below required {budget['minimumSamples']['gpu']}" + ) + for percentile in ("p95Us", "p99Us"): + if gpu[percentile] > budget["gpu"][percentile]: + failures.append( + f"GPU {percentile} {gpu[percentile]} exceeds {budget['gpu'][percentile]}" + ) + + evidence = { + "status": "pass" if not failures else "fail", + "budgetId": budget_id, + "expected": { + "map": expected_map_normalized, + "backend": expected_backend_normalized, + "profile": expected_profile_normalized, + }, + "marker": parsed["marker"], + "measurement": measurement, + "thresholds": thresholds, + "failures": failures, + } + return evidence, failures + + +def verify_recorded_evidence( + recorded: Any, + sources: Iterable[str], + contract: dict[str, Any], + expected_map: str, + expected_backend: str, + expected_profile: str, +) -> list[str]: + recomputed, failures = evaluate_timing_evidence( + sources, contract, expected_map, expected_backend, expected_profile + ) + if not isinstance(recorded, dict): + return ["recorded renderer budget evidence is missing or malformed", *failures] + if recorded != recomputed: + failures = [*failures, "recorded renderer budget evidence differs from verified diagnostics"] + if recorded.get("status") != "pass" or recorded.get("failures") != []: + failures.append("recorded renderer budget evidence is not a clean pass") + return list(dict.fromkeys(failures)) diff --git a/tools/validation/renderer_per_map_budgets.json b/tools/validation/renderer_per_map_budgets.json new file mode 100644 index 00000000..b9aca2fc --- /dev/null +++ b/tools/validation/renderer_per_map_budgets.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "kind": "openq4-renderer-per-map-budgets", + "contractId": "stock-baseline-targets-v1", + "timeUnit": "microseconds", + "budgets": [ + { "id": "storage1-opengl-baseline", "map": "game/storage1", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "storage1-vulkan-baseline", "map": "game/storage1", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "airdefense1-opengl-baseline", "map": "game/airdefense1", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "airdefense1-vulkan-baseline", "map": "game/airdefense1", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "airdefense2-opengl-baseline", "map": "game/airdefense2", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "airdefense2-vulkan-baseline", "map": "game/airdefense2", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "storage2-opengl-baseline", "map": "game/storage2", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "storage2-vulkan-baseline", "map": "game/storage2", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "medlabs-opengl-baseline", "map": "game/medlabs", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "medlabs-vulkan-baseline", "map": "game/medlabs", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "mcc-landing-opengl-baseline", "map": "game/mcc_landing", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "mcc-landing-vulkan-baseline", "map": "game/mcc_landing", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "q4dm1-opengl-baseline", "map": "mp/q4dm1", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "q4dm1-vulkan-baseline", "map": "mp/q4dm1", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "tram1-opengl-baseline", "map": "game/tram1", "backend": "opengl", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } }, + { "id": "tram1-vulkan-baseline", "map": "game/tram1", "backend": "vulkan", "profile": "baseline", "minimumSamples": { "cpu": 120, "gpu": 64 }, "cpu": { "p95Us": 20000, "p99Us": 28000 }, "gpu": { "required": true, "p95Us": 20000, "p99Us": 28000 } } + ] +} diff --git a/tools/validation/stock_asset_baseline.py b/tools/validation/stock_asset_baseline.py new file mode 100644 index 00000000..70c5da6c --- /dev/null +++ b/tools/validation/stock_asset_baseline.py @@ -0,0 +1,2699 @@ +#!/usr/bin/env python3 +"""Capture and verify the retail-PK4 openQ4 SP/MP compatibility baseline. + +The harness is intentionally non-interactive. It launches only windowed +clients, drives registered console commands through generated cfg files, and +uses the engine's ``screenshot`` command. It never controls or captures host +mouse/keyboard input and never uses an operating-system screen-capture API. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import re +import stat +import struct +import subprocess +import sys +import time +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any +from zipfile import BadZipFile, ZipFile + +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from renderer_budget_contract import ( # noqa: E402 + DEFAULT_CONTRACT_PATH, + evaluate_timing_evidence, + load_contract, + verify_contract_binding, + verify_recorded_evidence, +) + +SCHEMA_VERSION = 10 +RETAIL_MANIFEST_SCHEMA_VERSION = 1 +RUNTIME_MANIFEST_SCHEMA_VERSION = 2 +ASSET_COMPATIBILITY_MODEL = "retail-pk4-fallback-with-packaged-openq4-overlays-v1" +GIT_PROVENANCE_POLICY = "current-openq4-head-and-dirty-state-v1" +TOP_LEVEL_FAILURE_ARRAYS = ( + "assetComparisonFailures", + "preflightFailures", + "postCaptureVerificationFailures", +) +SP_MAP = "game/storage1" +MP_MAP = "mp/q4dm1" +SP_SAVE_NAME = "StockBaselineSP" +SP_DEMO_NAME = "stock_baseline_sp" +BASELINE_DIR = "stock-baseline" +DEFAULT_WIDTH = 1280 +DEFAULT_HEIGHT = 720 +DISPLAY_CONTRACT_ID = "bordered-window-1280x720-v1" +SP_SAVE_PREVIEW_DIMENSIONS = (320, 240) +SP_SAVE_PREVIEW_ANALYSIS_DIMENSIONS = (80, 60) +SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION = 0.75 +SP_SAVE_PREVIEW_MIN_EXPOSURE_GAIN = 0.25 +SP_SAVE_PREVIEW_MAX_EXPOSURE_GAIN = 4.0 +SP_SAVE_PREVIEW_MIN_EXPOSURE_BIAS = -192.0 +SP_SAVE_PREVIEW_MAX_EXPOSURE_BIAS = 192.0 +SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR = 48.0 +SP_SAVE_PREVIEW_COMPARISON_ALGORITHM = ( + "same-state-center-aspect-bilinear-luma-affine-rgb-v3" +) +MP_CLIENT_ACTIVE_MARKER = "OPENQ4_STOCK_BASELINE_MP_CLIENT_ACTIVE" +MP_CLIENT_ACTIVE_PATTERN = re.compile( + rf"^{MP_CLIENT_ACTIVE_MARKER} client=\d+ spectating=0 wantSpectate=0 ingame=1 menu=0 disableHud=0$", + re.MULTILINE, +) +MP_CLIENT_VIEW_MARKER = "OPENQ4_STOCK_BASELINE_MP_CLIENT_VIEW" +MP_CLIENT_VIEW_PATTERN = re.compile( + rf"^{MP_CLIENT_VIEW_MARKER} gui=0$", + re.MULTILINE, +) +MP_CLIENT_SCREENSHOT_WRITE_PATTERN = re.compile( + rf"^Wrote screenshots/{BASELINE_DIR}/mp_client\.tga$", + re.MULTILINE, +) +FATAL_PATTERNS = { + "fatal": re.compile(r"\bFatal Error\b|^[ \t]*(?:\*+[ \t]*)?FATAL[ \t]*:", re.IGNORECASE | re.MULTILINE), + "engineError": re.compile(r"^[ \t]*(?:\*+[ \t]*)?ERROR(?:[ \t]*:|[ \t]*$)", re.MULTILINE), + "shaderFailure": re.compile(r"(shader compile|program link).*(failed|error)|failed to compile", re.IGNORECASE), + "vulkanValidation": re.compile(r"\bVulkan validation:|\bVUID-[A-Za-z0-9_.-]+\b", re.IGNORECASE), + "graphicsError": re.compile(r"\bGL_(?:INVALID_[A-Z_]+|OUT_OF_MEMORY|CONTEXT_LOST)\b|OpenGL\s+error", re.IGNORECASE), +} +RUNTIME_WINDOW_MODE_PATTERN = re.compile( + r"^MODE:\s*([^,\r\n]+),\s*(\d+)\s+x\s+(\d+)\s+" + r"(windowed|borderless|fullscreen)\b", + re.IGNORECASE | re.MULTILINE, +) + + +def display_contract() -> dict[str, Any]: + return { + "contractId": DISPLAY_CONTRACT_ID, + "width": DEFAULT_WIDTH, + "height": DEFAULT_HEIGHT, + "cvars": { + "r_fullscreen": "0", + "r_borderless": "0", + "r_borderlessDefaultMigrated": "1", + "r_fullscreenDesktop": "0", + "r_windowWidth": str(DEFAULT_WIDTH), + "r_windowHeight": str(DEFAULT_HEIGHT), + "r_mode": "-1", + "r_customWidth": str(DEFAULT_WIDTH), + "r_customHeight": str(DEFAULT_HEIGHT), + }, + } + + +ROLE_EVIDENCE_CONTRACT: dict[str, dict[str, Any]] = { + "sp-capture": { + "marker": "OPENQ4_STOCK_BASELINE_SP_SAVE_LOAD_COMPLETE", + "saveDir": "sp", + "logName": "stock_baseline_sp.log", + "budget": {"map": SP_MAP, "backend": "opengl", "profile": "baseline"}, + "expected": { + "screenshot": f"baseoq4/screenshots/{BASELINE_DIR}/sp_after_load.tga", + "saveReferenceScreenshot": ( + f"baseoq4/screenshots/{BASELINE_DIR}/sp_before_save.tga" + ), + "renderDemo": f"baseoq4/demos/{SP_DEMO_NAME}.demo", + "savePayload": f"baseoq4/savegames/{SP_SAVE_NAME}.save", + "savePreview": f"baseoq4/savegames/{SP_SAVE_NAME}.tga", + "saveDescription": f"baseoq4/savegames/{SP_SAVE_NAME}.txt", + }, + }, + "sp-demo-playback": { + "marker": "frames rendered in", + "saveDir": "sp", + "logName": "stock_baseline_demo_playback.log", + "expected": {}, + }, + "mp-server": { + "marker": "OPENQ4_STOCK_BASELINE_MP_SERVER_COMPLETE", + "saveDir": "mp-server", + "logName": "stock_baseline_server.log", + "budget": {"map": MP_MAP, "backend": "opengl", "profile": "baseline"}, + "expected": { + "screenshot": f"baseoq4/screenshots/{BASELINE_DIR}/mp_server.tga", + "renderDemo": f"baseoq4/demos/stock_baseline_mp_server.demo", + }, + }, + "mp-client": { + "marker": "OPENQ4_STOCK_BASELINE_MP_CLIENT_COMPLETE", + "requiredMarkers": [MP_CLIENT_ACTIVE_MARKER, MP_CLIENT_VIEW_MARKER], + "saveDir": "mp-client", + "logName": "stock_baseline_client.log", + "budget": {"map": MP_MAP, "backend": "opengl", "profile": "baseline"}, + "expected": { + "screenshot": f"baseoq4/screenshots/{BASELINE_DIR}/mp_client.tga", + "renderDemo": f"baseoq4/demos/stock_baseline_mp_client.demo", + }, + }, +} + + +@dataclass +class RolePlan: + role_id: str + mode: str + savepath: Path + log_name: str + marker: str + args: list[str] + expected: list[tuple[str, str]] = field(default_factory=list) + stdout_path: Path | None = None + stderr_path: Path | None = None + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def utc_now() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def prepare_output_directory(output_dir: Path) -> None: + if output_dir.exists(): + if not output_dir.is_dir(): + raise ValueError(f"evidence output path is not a directory: {output_dir}") + if is_link_or_junction(output_dir): + raise ValueError(f"evidence output directory must not be a link or junction: {output_dir}") + if any(output_dir.iterdir()): + raise ValueError(f"evidence output directory must be new or empty: {output_dir}") + else: + output_dir.mkdir(parents=True) + + +def host_arch() -> str: + machine = platform.machine().lower() + if machine in {"amd64", "x86_64"}: + return "x64" + if machine in {"arm64", "aarch64"}: + return "arm64" + return machine + + +def default_asset_root() -> str: + if os.name == "nt": + return r"C:\Program Files (x86)\Steam\steamapps\common\Quake 4" + return "" + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(4 * 1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def file_record(path: Path, root: Path) -> dict[str, Any]: + return { + "path": path.relative_to(root).as_posix(), + "size": path.stat().st_size, + "sha256": sha256_file(path), + } + + +def is_link_or_junction(path: Path) -> bool: + is_junction = getattr(path, "is_junction", None) + if path.is_symlink() or bool(is_junction and is_junction()): + return True + try: + # Python before 3.12 has no Path.is_junction(). Inspect the path's own + # Windows reparse-point bit instead of comparing resolved paths: every + # ordinary child below an explicitly allowed junction resolves through + # that parent and must not be misclassified as a nested link. + attributes = getattr(path.lstat(), "st_file_attributes", 0) + return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)) + except OSError: + return False + + +def validate_asset_root(asset_root: Path, allow_asset_dir_links: bool = False) -> None: + if not asset_root.is_dir(): + raise FileNotFoundError(f"Quake 4 asset root does not exist: {asset_root}") + direct_game_dir = asset_root / "baseoq4" + if direct_game_dir.is_dir() and any(direct_game_dir.iterdir()): + raise ValueError( + f"asset root contains non-empty {direct_game_dir}; fs_game=baseoq4 would load " + "that unapproved content ahead of the stock q4base fallback. Use a clean asset " + "view containing q4base/q4mp only." + ) + for game_dir_name in ("q4base", "q4mp"): + game_dir = asset_root / game_dir_name + if game_dir.exists() and is_link_or_junction(game_dir) and not allow_asset_dir_links: + raise ValueError( + f"asset directory is a symlink or junction: {game_dir}; pass " + "--allow-asset-dir-links only for an intentionally constructed clean view" + ) + + +def asset_directory_views(asset_root: Path) -> list[dict[str, Any]]: + views: list[dict[str, Any]] = [] + for game_dir_name in ("q4base", "q4mp"): + game_dir = asset_root / game_dir_name + if game_dir.exists(): + views.append( + { + "path": game_dir_name, + "linked": is_link_or_junction(game_dir), + "resolvedTarget": str(game_dir.resolve()), + } + ) + return views + + +def collect_pk4s( + asset_root: Path, allow_asset_dir_links: bool = False +) -> list[dict[str, Any]]: + validate_asset_root(asset_root, allow_asset_dir_links) + files: list[Path] = [] + for game_dir_name in ("q4base", "q4mp"): + game_dir = asset_root / game_dir_name + if game_dir.is_dir(): + files.extend(path for path in game_dir.iterdir() if path.is_file() and path.suffix.casefold() == ".pk4") + files.sort(key=lambda path: path.relative_to(asset_root).as_posix().casefold()) + if not files: + raise FileNotFoundError(f"no retail PK4 files found under {asset_root}/q4base or q4mp") + if not any(item.relative_to(asset_root).as_posix().casefold() == "q4base/pak001.pk4" for item in files): + raise FileNotFoundError("retail asset set is missing q4base/pak001.pk4") + return [file_record(path, asset_root) for path in files] + + +def collect_loose_asset_files( + asset_root: Path, allow_asset_dir_links: bool = False +) -> list[dict[str, Any]]: + """Hash every non-PK4 file below q4base/q4mp that the fallback can see.""" + validate_asset_root(asset_root, allow_asset_dir_links) + files: list[Path] = [] + for game_dir_name in ("q4base", "q4mp"): + game_dir = asset_root / game_dir_name + if not game_dir.is_dir(): + continue + for parent_name, dir_names, file_names in os.walk(game_dir, followlinks=False): + parent = Path(parent_name) + for dir_name in list(dir_names): + directory = parent / dir_name + if is_link_or_junction(directory): + raise ValueError(f"loose asset tree contains a symlink or junction: {directory}") + for file_name in file_names: + path = parent / file_name + if is_link_or_junction(path): + raise ValueError(f"loose asset tree contains a symlink or junction: {path}") + if path.suffix.casefold() != ".pk4": + files.append(path) + files.sort(key=lambda path: path.relative_to(asset_root).as_posix().casefold()) + return [file_record(path, asset_root) for path in files] + + +def collect_overlay_pk4s(runtime_dir: Path) -> list[dict[str, Any]]: + game_dir = runtime_dir / "baseoq4" + if not game_dir.is_dir(): + return [] + files = sorted( + (path for path in game_dir.iterdir() if path.is_file() and path.suffix.casefold() == ".pk4"), + key=lambda path: path.name.casefold(), + ) + return [file_record(path, runtime_dir) for path in files] + + +def normalized_pk4_member_path(name: str, archive_path: Path) -> str: + """Return the case-preserving VFS path for one ordinary PK4 member.""" + + normalized = name.replace("\\", "/") + parts = normalized.split("/") + if ( + not normalized + or normalized.startswith("/") + or any(part in {"", ".", ".."} for part in parts) + or ":" in parts[0] + ): + raise ValueError(f"PK4 contains an unsafe or ambiguous member path: {archive_path}: {name!r}") + return "/".join(parts) + + +def pk4_member_index( + root: Path, records: list[dict[str, Any]], label: str +) -> dict[str, dict[str, Any]]: + """Index archive members by the case-insensitive idTech virtual path.""" + + index: dict[str, dict[str, Any]] = {} + for record in records: + relative = str(record.get("path", "")) + relative_path = Path(relative) + if not relative or relative_path.is_absolute() or ".." in relative_path.parts: + raise ValueError(f"{label} manifest contains an unsafe PK4 path: {relative!r}") + archive_path = root / relative_path + if not archive_path.is_file(): + raise FileNotFoundError(f"{label} PK4 is missing: {archive_path}") + try: + with ZipFile(archive_path) as archive: + for member in archive.infolist(): + if member.is_dir(): + continue + member_path = normalized_pk4_member_path(member.filename, archive_path) + key = member_path.casefold() + entry = index.setdefault( + key, + {"path": member_path, "archives": set()}, + ) + if member_path.casefold() == str(entry["path"]).casefold(): + entry["path"] = min(str(entry["path"]), member_path) + entry["archives"].add(relative.replace("\\", "/")) + except BadZipFile as exc: + raise ValueError(f"{label} PK4 is not a readable ZIP archive: {archive_path}") from exc + return index + + +def collect_retail_path_collisions( + asset_root: Path, + stock_pk4s: list[dict[str, Any]], + runtime_dir: Path, + overlay_pk4s: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """Inventory packaged openQ4 members that supersede retail virtual paths.""" + + if not overlay_pk4s: + return [] + retail_index = pk4_member_index(asset_root, stock_pk4s, "retail") + overlay_index = pk4_member_index(runtime_dir, overlay_pk4s, "openQ4 overlay") + collisions: list[dict[str, Any]] = [] + for key in sorted(set(retail_index) & set(overlay_index)): + retail = retail_index[key] + overlay = overlay_index[key] + collisions.append( + { + "path": str(retail["path"]), + "retailPk4s": sorted(retail["archives"], key=str.casefold), + "openQ4OverlayPk4s": sorted(overlay["archives"], key=str.casefold), + } + ) + collisions.sort(key=lambda item: str(item["path"]).casefold()) + return collisions + + +def is_shared_library(path: Path) -> bool: + name = path.name.casefold() + return name.endswith((".dll", ".dylib")) or name.endswith(".so") or ".so." in name + + +def runtime_kind(path: Path, executable: Path) -> str: + if path.resolve() == executable.resolve(): + return "clientExecutable" + name = path.name.casefold() + if name.startswith("openq4-ded_"): + return "dedicatedServerExecutable" if path.suffix.casefold() != ".pdb" else "diagnosticSymbols" + if name.startswith("openq4-client_") and path.suffix.casefold() == ".pdb": + return "diagnosticSymbols" + if name.startswith("renderer-gl_"): + return "rendererModuleOpenGL" if path.suffix.casefold() != ".pdb" else "diagnosticSymbols" + if name.startswith("renderer-vk_"): + return "rendererModuleVulkan" if path.suffix.casefold() != ".pdb" else "diagnosticSymbols" + if name.startswith("game-sp_"): + return "singlePlayerGameModule" if path.suffix.casefold() != ".pdb" else "diagnosticSymbols" + if name.startswith("game-mp_"): + return "multiplayerGameModule" if path.suffix.casefold() != ".pdb" else "diagnosticSymbols" + if path.suffix.casefold() == ".pdb": + return "diagnosticSymbols" + return "runtimeLibrary" + + +def collect_runtime_files(runtime_dir: Path, executable: Path) -> list[dict[str, Any]]: + """Hash the selected client and every packaged shared library it may load.""" + candidates = {executable.resolve(): executable} + for parent_name, dir_names, file_names in os.walk(runtime_dir, followlinks=False): + parent = Path(parent_name) + for dir_name in list(dir_names): + directory = parent / dir_name + if is_link_or_junction(directory): + raise ValueError(f"runtime tree contains a symlink or junction: {directory}") + for file_name in file_names: + path = parent / file_name + if is_link_or_junction(path): + raise ValueError(f"runtime tree contains a symlink or junction: {path}") + lower_name = path.name.casefold() + if ( + is_shared_library(path) + or lower_name.startswith("openq4-client_") + or lower_name.startswith("openq4-ded_") + or path.suffix.casefold() == ".pdb" + ): + candidates[path.resolve()] = path + + records = [ + {"kind": runtime_kind(path, executable), **file_record(path, runtime_dir)} + for path in candidates.values() + ] + records.sort(key=lambda item: (item["kind"], item["path"].casefold())) + kinds = {item["kind"] for item in records} + required = {"clientExecutable", "singlePlayerGameModule", "multiplayerGameModule"} + if sys.platform != "darwin": + required.add("rendererModuleOpenGL") + missing = sorted(required - kinds) + if missing: + raise FileNotFoundError("runtime package is missing: " + ", ".join(missing)) + return records + + +def collect_overlay_loose_files( + runtime_dir: Path, runtime_files: list[dict[str, Any]] +) -> tuple[list[dict[str, Any]], list[str]]: + """Hash loose fs_game files and reject content outside the minimal package surface.""" + game_dir = runtime_dir / "baseoq4" + if not game_dir.is_dir(): + return [], [f"openQ4 overlay directory is missing: {game_dir}"] + runtime_paths = {item["path"].casefold() for item in runtime_files} + files: list[Path] = [] + for parent_name, dir_names, file_names in os.walk(game_dir, followlinks=False): + parent = Path(parent_name) + for dir_name in list(dir_names): + directory = parent / dir_name + if is_link_or_junction(directory): + raise ValueError(f"openQ4 overlay contains a symlink or junction: {directory}") + for file_name in file_names: + path = parent / file_name + if is_link_or_junction(path): + raise ValueError(f"openQ4 overlay contains a symlink or junction: {path}") + if path.suffix.casefold() != ".pk4": + files.append(path) + files.sort(key=lambda path: path.relative_to(runtime_dir).as_posix().casefold()) + records = [file_record(path, runtime_dir) for path in files] + allowed = runtime_paths | {"baseoq4/mod.json"} + unexpected = [ + f"unexpected loose openQ4 overlay file: {item['path']}" + for item in records + if item["path"].casefold() not in allowed + and not ( + Path(item["path"]).parent.as_posix().casefold() == "baseoq4" + and Path(item["path"]).suffix.casefold() == ".pdb" + ) + ] + return records, unexpected + + +def asset_manifest_from_payload(payload: dict[str, Any]) -> list[dict[str, Any]]: + records = payload.get("stockPk4s") + if records is None and isinstance(payload.get("assets"), dict): + records = payload["assets"].get("stockPk4s") + if not isinstance(records, list): + raise ValueError("expected asset manifest does not contain a stockPk4s array") + return records + + +def loose_manifest_from_payload(payload: dict[str, Any]) -> list[dict[str, Any]]: + records = payload.get("looseFiles") + if records is None and isinstance(payload.get("assets"), dict): + records = payload["assets"].get("looseFiles") + if records is None: + return [] + if not isinstance(records, list): + raise ValueError("expected asset manifest looseFiles field is not an array") + return records + + +def compare_file_records( + expected: list[dict[str, Any]], actual: list[dict[str, Any]], label: str +) -> list[str]: + failures: list[str] = [] + expected_by_path = {str(item.get("path", "")).casefold(): item for item in expected} + actual_by_path = {str(item.get("path", "")).casefold(): item for item in actual} + for path in sorted(set(expected_by_path) - set(actual_by_path)): + failures.append(f"missing {label}: {expected_by_path[path].get('path', path)}") + for path in sorted(set(actual_by_path) - set(expected_by_path)): + failures.append(f"unexpected {label}: {actual_by_path[path].get('path', path)}") + for path in sorted(set(expected_by_path) & set(actual_by_path)): + expected_item = expected_by_path[path] + actual_item = actual_by_path[path] + for key in ("size", "sha256"): + if expected_item.get(key) != actual_item.get(key): + failures.append( + f"{label} {actual_item.get('path', path)} {key} differs: " + f"expected {expected_item.get(key)!r}, got {actual_item.get(key)!r}" + ) + return failures + + +def compare_asset_records(expected: list[dict[str, Any]], actual: list[dict[str, Any]]) -> list[str]: + return compare_file_records(expected, actual, "retail PK4") + + +def find_client(runtime_dir: Path) -> Path: + suffix = ".exe" if os.name == "nt" else "" + preferred = runtime_dir / f"openQ4-client_{host_arch()}{suffix}" + if preferred.is_file(): + return preferred + candidates = sorted(runtime_dir.glob(f"openQ4-client_*{suffix}")) + for candidate in candidates: + if candidate.is_file(): + return candidate + raise FileNotFoundError(f"openQ4 client executable not found under {runtime_dir}") + + +def validate_runtime_dir(runtime_dir: Path, source_root: Path) -> Path: + """Require canonical .install or an ordinary isolated temporary package.""" + source_absolute = source_root.absolute() + runtime_absolute = runtime_dir.absolute() + if is_link_or_junction(source_absolute): + raise ValueError(f"source root must not be a link or junction: {source_absolute}") + try: + relative_to_source = runtime_absolute.relative_to(source_absolute) + except ValueError as exc: + raise ValueError( + f"runtime directory must stay below the source root {source_absolute}: {runtime_absolute}" + ) from exc + current = source_absolute + for part in relative_to_source.parts: + current /= part + if current.exists() and is_link_or_junction(current): + raise ValueError( + f"runtime directory ancestry must not contain a link or junction: {current}" + ) + resolved = runtime_absolute.resolve() + if not resolved.is_dir(): + raise FileNotFoundError(f"runtime directory does not exist: {resolved}") + canonical = (source_root / ".install").resolve() + temporary_parent = (source_root / ".tmp" / "stock-runtime").resolve() + if resolved != canonical: + try: + relative = resolved.relative_to(temporary_parent) + except ValueError as exc: + raise ValueError( + f"alternate runtime directory must stay below {temporary_parent}: {resolved}" + ) from exc + if not relative.parts: + raise ValueError( + f"alternate runtime directory must be a named child below {temporary_parent}" + ) + return resolved + + +def add_set(args: list[str], name: str, value: Any) -> None: + args.extend(("+set", name, str(value))) + + +def add_command(args: list[str], name: str, *values: Any) -> None: + args.append("+" + name) + args.extend(str(value) for value in values) + + +def write_cfg(savepath: Path, relative: str, lines: list[str]) -> None: + payload = "\n".join(lines) + "\n" + # baseoq4 is authoritative; q4base mirrors the file for legacy path + # diagnostics without adding any asset override to the repository. + for game_dir in ("baseoq4", "q4base"): + path = savepath / game_dir / Path(relative) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(payload, encoding="utf-8") + + +def display_cfg_lines(width: int, height: int) -> list[str]: + """Reassert the measured display contract after any game-module reload.""" + return [ + "r_rendererSharedGui 0", + "r_rendererSharedInWorldGui 0", + "r_rendererSharedCinematicPost 0", + "r_rendererSharedSpecialFrame 0", + "r_rendererSharedWorldAmbient 0", + "r_rendererSharedWorldInteraction 0", + "r_rendererSharedWorldFogBlend 0", + "r_rendererSharedSubview 0", + "r_rendererSharedDeform 0", + "r_fullscreen 0", + "r_borderless 0", + "r_borderlessDefaultMigrated 1", + "r_fullscreenDesktop 0", + f"r_windowWidth {width}", + f"r_windowHeight {height}", + "r_mode -1", + f"r_customWidth {width}", + f"r_customHeight {height}", + ] + + +def common_args( + runtime_dir: Path, + asset_root: Path, + savepath: Path, + log_name: str, + cfg_path: str | None, + width: int, + height: int, +) -> list[str]: + args: list[str] = [] + add_set(args, "win_allowMultipleInstances" if os.name == "nt" else "sys_allowMultipleInstances", 1) + add_set(args, "logFile", 2) + add_set(args, "logFileName", f"logs/{log_name}") + add_set(args, "developer", 1) + add_set(args, "r_ignoreGLErrors", 0) + add_set(args, "r_fullscreen", 0) + add_set(args, "r_borderless", 0) + # Suppress the one-time legacy migration before the renderer starts. A + # queued r_borderless=0 alone is too late: the migration otherwise creates + # the initial window as desktop-sized borderless before command execution. + add_set(args, "r_borderlessDefaultMigrated", 1) + add_set(args, "r_fullscreenDesktop", 0) + add_set(args, "r_windowWidth", width) + add_set(args, "r_windowHeight", height) + add_set(args, "r_mode", -1) + add_set(args, "r_customWidth", width) + add_set(args, "r_customHeight", height) + add_set(args, "r_swapInterval", 0) + add_set(args, "r_renderApi", "gl") + add_set(args, "r_rendererSharedGui", 0) + add_set(args, "r_rendererSharedInWorldGui", 0) + add_set(args, "r_rendererSharedCinematicPost", 0) + add_set(args, "r_rendererSharedSpecialFrame", 0) + add_set(args, "r_rendererSharedWorldAmbient", 0) + add_set(args, "r_rendererSharedWorldInteraction", 0) + add_set(args, "r_rendererSharedWorldFogBlend", 0) + add_set(args, "r_rendererSharedSubview", 0) + add_set(args, "r_rendererSharedDeform", 0) + add_set(args, "r_rendererBenchmarkPreset", "baseline") + add_set(args, "r_rendererMetrics", 0) + add_set(args, "r_rendererGpuTimers", 1) + add_set(args, "com_maxfps", 240) + add_set(args, "com_skipLoadingContinue", 1) + add_set(args, "com_loadingContinueAutoAdvance", 1) + # Milestone B evidence records the exact cache/preload mode and its bounded + # per-generation counters in every role log. + add_set(args, "com_levelLoadModernization", 1) + add_set(args, "com_levelLoadCache", 1) + add_set(args, "com_levelLoadCacheWrite", 1) + add_set(args, "com_levelLoadPreload", 1) + add_set(args, "com_levelLoadCacheReport", 1) + add_set(args, "g_autoSkipCinematics", 1) + add_set(args, "g_autoScreenshot", 0) + add_set(args, "sv_cheats", 1) + if cfg_path: + add_set(args, "g_autoExecAfterMapLoad", cfg_path) + add_set(args, "g_autoExecAfterMapLoadDelayMs", 1000) + add_set(args, "fs_basepath", asset_root) + add_set(args, "fs_savepath", savepath) + # The process is launched with this exact recorded runtime directory as its + # working directory, so the engine's locked fs_cdpath mounts that staged + # package. Keep fs_devpath in the isolated evidence tree: retail-style + # generated collision caches and other developer outputs must never mutate + # the package after it is hashed. + add_set(args, "fs_devpath", savepath) + add_set(args, "fs_game", "baseoq4") + # Re-apply the complete display contract after queued +set commands. This + # makes the renderer state, not only the eventual CVar values, authoritative. + add_command(args, "vid_restart") + return args + + +def expected_role_arguments( + role: str, + runtime_dir: Path, + asset_root: Path, + output_dir: Path, + width: int, + height: int, + mp_port: int, +) -> list[str]: + """Build the one canonical command line accepted for an evidence role.""" + + if role not in ROLE_EVIDENCE_CONTRACT: + raise ValueError(f"unknown baseline role: {role}") + contract = ROLE_EVIDENCE_CONTRACT[role] + savepath = output_dir / "savepaths" / contract["saveDir"] + cfg_by_role = { + "sp-capture": f"{BASELINE_DIR}/sp_stage1.cfg", + "sp-demo-playback": None, + "mp-server": f"{BASELINE_DIR}/server.cfg", + "mp-client": f"{BASELINE_DIR}/client.cfg", + } + args = common_args( + runtime_dir, + asset_root, + savepath, + contract["logName"], + cfg_by_role[role], + width, + height, + ) + if role == "sp-capture": + add_set(args, "si_gameType", "singleplayer") + add_command(args, "map", SP_MAP) + elif role == "sp-demo-playback": + add_command(args, "gfxInfo") + add_command(args, "timeDemoQuit", SP_DEMO_NAME) + else: + # Automated MP evidence always exercises the archived auto-join path. + # Join/spectator-menu tests are separate and explicitly set this to 0. + restart_index = args.index("+vid_restart") + args[restart_index:restart_index] = ("+set", "ui_autoJoin", "1") + if role == "mp-server": + add_set(args, "net_serverDedicated", 0) + add_set(args, "net_port", mp_port) + add_set(args, "si_pure", 1) + add_set(args, "net_serverAllowServerMod", 0) + add_set(args, "si_gameType", "DM") + add_command(args, "spawnServer", MP_MAP) + else: + add_set(args, "ui_name", "StockBaselineClient") + add_command(args, "connect", f"127.0.0.1:{mp_port}") + return args + + +def prepare_plans( + runtime_dir: Path, + asset_root: Path, + output_dir: Path, + width: int, + height: int, + mp_port: int, +) -> dict[str, RolePlan]: + plans: dict[str, RolePlan] = {} + + sp_savepath = output_dir / "savepaths" / "sp" + sp_stage1 = f"{BASELINE_DIR}/sp_stage1.cfg" + sp_stage2 = f"{BASELINE_DIR}/sp_after_load.cfg" + write_cfg( + sp_savepath, + sp_stage1, + [ + "waitMsec 5000", + "god", + "notarget", + f"recordDemo {SP_DEMO_NAME}", + "waitMsec 2500", + "stopRecording", + f'screenshot "screenshots/{BASELINE_DIR}/sp_before_save.tga"', + f"saveGame {SP_SAVE_NAME}", + "waitMsec 1000", + f'set g_autoExecAfterMapLoad "{sp_stage2}"', + "set g_autoExecAfterMapLoadDelayMs 1000", + f"loadGame {SP_SAVE_NAME}", + ], + ) + write_cfg( + sp_savepath, + sp_stage2, + [ + "echo OPENQ4_STOCK_BASELINE_SP_SAVE_LOAD_COMPLETE", + "waitMsec 2500", + *display_cfg_lines(width, height), + "framePacingReset", + "r_rendererMetrics 1", + "waitMsec 2500", + "rendererBenchmarkCapture", + "r_rendererMetrics 0", + "framePacingSnapshot", + "gfxInfo", + f'screenshot "screenshots/{BASELINE_DIR}/sp_after_load.tga"', + "wait 5", + "quit", + ], + ) + sp_args = expected_role_arguments( + "sp-capture", runtime_dir, asset_root, output_dir, width, height, mp_port + ) + plans["sp-capture"] = RolePlan( + "sp-capture", + "SP", + sp_savepath, + "stock_baseline_sp.log", + "OPENQ4_STOCK_BASELINE_SP_SAVE_LOAD_COMPLETE", + sp_args, + [ + ("screenshot", f"baseoq4/screenshots/{BASELINE_DIR}/sp_after_load.tga"), + ( + "saveReferenceScreenshot", + f"baseoq4/screenshots/{BASELINE_DIR}/sp_before_save.tga", + ), + ("renderDemo", f"baseoq4/demos/{SP_DEMO_NAME}.demo"), + ("savePayload", f"baseoq4/savegames/{SP_SAVE_NAME}.save"), + ("savePreview", f"baseoq4/savegames/{SP_SAVE_NAME}.tga"), + ("saveDescription", f"baseoq4/savegames/{SP_SAVE_NAME}.txt"), + ], + ) + + playback_args = expected_role_arguments( + "sp-demo-playback", + runtime_dir, + asset_root, + output_dir, + width, + height, + mp_port, + ) + plans["sp-demo-playback"] = RolePlan( + "sp-demo-playback", + "SP demo playback", + sp_savepath, + "stock_baseline_demo_playback.log", + "frames rendered in", + playback_args, + ) + + for role_id, role_name, wait_msec in ( + # Leave enough headroom for a completely cold loopback client to build + # its isolated binary image/animation and collision caches before the + # listen server records its own evidence and exits. The server still + # finishes inside the shared four-minute default role timeout after a + # representative cold q4dm1 load. + ("mp-server", "server", 120000), + # At roughly 48-50 fps, five seconds does not replace all 256 timing + # samples retained across the first active post-load frame. Give the + # client a full steady-state window before enforcing renderer budgets. + ("mp-client", "client", 10000), + ): + savepath = output_dir / "savepaths" / role_id + cfg = f"{BASELINE_DIR}/{role_name}.cfg" + marker = f"OPENQ4_STOCK_BASELINE_MP_{role_name.upper()}_COMPLETE" + demo_name = f"stock_baseline_mp_{role_name}" + role_commands = [ + *display_cfg_lines(width, height), + "framePacingReset", + "r_rendererMetrics 1", + "r_rendererGpuTimers 1", + f"waitMsec {wait_msec}", + ] + if role_name == "client": + role_commands.extend( + ( + "openq4_assertMPClientActive", + "openq4_assertMPGameplayView", + ) + ) + role_commands.extend( + ( + f"recordDemo {demo_name}", + "waitMsec 2500", + "stopRecording", + "rendererBenchmarkCapture", + "r_rendererMetrics 0", + "framePacingSnapshot", + "gfxInfo", + ) + ) + if role_name == "client": + role_commands.extend( + ("openq4_assertMPClientActive", "openq4_assertMPGameplayView") + ) + role_commands.append(f'screenshot "screenshots/{BASELINE_DIR}/mp_{role_name}.tga"') + if role_name == "client": + role_commands.extend( + ("openq4_assertMPClientActive", "openq4_assertMPGameplayView") + ) + role_commands.extend((f"echo {marker}", "wait 5", "quit")) + write_cfg( + savepath, + cfg, + role_commands, + ) + log_name = f"stock_baseline_{role_name}.log" + role_args = expected_role_arguments( + role_id, runtime_dir, asset_root, output_dir, width, height, mp_port + ) + plans[role_id] = RolePlan( + role_id, + "MP", + savepath, + log_name, + marker, + role_args, + [ + ("screenshot", f"baseoq4/screenshots/{BASELINE_DIR}/mp_{role_name}.tga"), + ("renderDemo", f"baseoq4/demos/{demo_name}.demo"), + ], + ) + + for plan in plans.values(): + plan.stdout_path = output_dir / f"{plan.role_id}.stdout.txt" + plan.stderr_path = output_dir / f"{plan.role_id}.stderr.txt" + return plans + + +def plan_record(plan: RolePlan) -> dict[str, Any]: + return { + "role": plan.role_id, + "mode": plan.mode, + "windowed": True, + "captureMethod": "engine screenshot command", + "savepath": str(plan.savepath), + "logName": plan.log_name, + "marker": plan.marker, + "requiredMarkers": ROLE_EVIDENCE_CONTRACT[plan.role_id].get("requiredMarkers", []), + "arguments": plan.args, + "expected": [{"kind": kind, "path": path} for kind, path in plan.expected], + } + + +def planned_cvar_values(arguments: Any, name: str) -> list[str]: + if not isinstance(arguments, list): + return [] + values: list[str] = [] + for index in range(len(arguments) - 2): + if arguments[index] in {"+set", "+seta"} and arguments[index + 1] == name: + values.append(str(arguments[index + 2])) + return values + + +def planned_command_values(arguments: Any, name: str) -> list[list[str]]: + if not isinstance(arguments, list): + return [] + values: list[list[str]] = [] + command = "+" + name + for index, argument in enumerate(arguments): + if argument != command: + continue + command_values: list[str] = [] + for value in arguments[index + 1 :]: + if isinstance(value, str) and value.startswith("+"): + break + command_values.append(str(value)) + values.append(command_values) + return values + + +def launch(executable: Path, plan: RolePlan, cwd: Path) -> subprocess.Popen[Any]: + assert plan.stdout_path is not None and plan.stderr_path is not None + stdout = plan.stdout_path.open("w", encoding="utf-8", errors="replace") + stderr = plan.stderr_path.open("w", encoding="utf-8", errors="replace") + try: + process = subprocess.Popen([str(executable), *plan.args], cwd=cwd, stdout=stdout, stderr=stderr) + finally: + stdout.close() + stderr.close() + return process + + +def wait_process(process: subprocess.Popen[Any], timeout: int) -> tuple[int, bool]: + try: + return process.wait(timeout=timeout), False + except subprocess.TimeoutExpired: + process.kill() + return process.wait(timeout=10), True + + +def wait_processes_until( + processes: dict[str, subprocess.Popen[Any]], deadline: float +) -> dict[str, tuple[int, bool]]: + """Wait for a process group without giving sequential waits fresh budgets.""" + + pending = dict(processes) + results: dict[str, tuple[int, bool]] = {} + while pending: + for name, process in list(pending.items()): + exit_code = process.poll() + if exit_code is not None: + results[name] = (exit_code, False) + del pending[name] + if not pending: + break + remaining = deadline - time.monotonic() + if remaining <= 0: + break + time.sleep(min(0.05, remaining)) + + # Kill every process still pending before reaping any of them. This keeps + # the absolute deadline common to the whole group even if process teardown + # takes time on one platform. + for process in pending.values(): + try: + process.kill() + except OSError: + # The process may have exited between the final poll and kill. + pass + for name, process in pending.items(): + results[name] = (process.wait(timeout=10), True) + return results + + +def find_log(plan: RolePlan) -> Path | None: + for game_dir in ("baseoq4", "q4base"): + candidate = plan.savepath / game_dir / "logs" / plan.log_name + if candidate.is_file(): + return candidate + return None + + +def read_text(path: Path | None) -> str: + if path is None or not path.is_file(): + return "" + return path.read_text(encoding="utf-8", errors="replace") + + +def collect_role_diagnostics( + log_path: Path | None, + stdout_path: Path | None, + stderr_path: Path | None, +) -> tuple[str, str]: + """Return authoritative engine diagnostics and all captured diagnostics. + + On POSIX, the engine can mirror the same diagnostic line to both its + logfile and stdout. Ordered and exact lifecycle proofs therefore use only + the engine logfile, while the combined text remains useful for fail-closed + fatal/error scanning across every captured channel. + """ + + engine_log = read_text(log_path) + all_diagnostics = "\n".join( + (engine_log, read_text(stdout_path), read_text(stderr_path)) + ) + return engine_log, all_diagnostics + + +def runtime_window_evidence(diagnostics: str) -> tuple[str, int, int, str] | None: + modes = RUNTIME_WINDOW_MODE_PATTERN.findall(diagnostics) + if not modes: + return None + selector, width, height, mode = modes[-1] + return selector.strip(), int(width), int(height), mode.casefold() + + +def mp_client_active_proof_failure(diagnostics: str) -> str | None: + active_proofs = list(MP_CLIENT_ACTIVE_PATTERN.finditer(diagnostics)) + view_proofs = list(MP_CLIENT_VIEW_PATTERN.finditer(diagnostics)) + screenshot_writes = list(MP_CLIENT_SCREENSHOT_WRITE_PATTERN.finditer(diagnostics)) + if len(screenshot_writes) != 1: + return "exactly one MP client screenshot-write marker is required" + completion_pattern = re.compile( + rf"^{re.escape(ROLE_EVIDENCE_CONTRACT['mp-client']['marker'])}[ \t]*$", + re.MULTILINE, + ) + completions = list(completion_pattern.finditer(diagnostics)) + if len(completions) != 1: + return "exactly one MP client completion marker is required" + screenshot_write = screenshot_writes[0] + completion = completions[0] + if completion.start() <= screenshot_write.end(): + return "MP client completion marker must follow the screenshot write" + if len(active_proofs) < 2 or len(view_proofs) < 2: + return ( + "two exact active-player and gameplay-view proofs " + "(before and after screenshot) are required" + ) + for label, proofs in (("active-player", active_proofs), ("gameplay-view", view_proofs)): + before = [proof for proof in proofs if proof.end() <= screenshot_write.start()] + after = [ + proof + for proof in proofs + if proof.start() >= screenshot_write.end() and proof.end() <= completion.start() + ] + if not before or not after: + return f"exact {label} proofs must bracket the MP client screenshot write" + if completion.start() - after[-1].end() > 512: + return f"final {label} proof is not immediately before the completion marker" + return None + + +def runtime_window_failure( + diagnostics: str, + requested_width: int | None = None, + requested_height: int | None = None, +) -> str | None: + evidence = runtime_window_evidence(diagnostics) + if evidence is None: + return "runtime display mode evidence missing" + selector, actual_width, actual_height, actual_mode = evidence + if actual_mode != "windowed": + return f"runtime display mode is {actual_mode}, not bordered windowed" + if selector != "-1": + return f"runtime display mode selector is {selector}, not canonical -1" + if requested_width is not None and requested_height is not None: + if (actual_width, actual_height) != (requested_width, requested_height): + return ( + f"runtime window/render size is {actual_width}x{actual_height}, " + f"expected exact budget workload {requested_width}x{requested_height}" + ) + return None + + +def decode_tga_rgb(data: bytes) -> tuple[int, int, bytes] | str: + """Decode the uncompressed true-colour TGA subset written by openQ4.""" + + if len(data) < 18: + return "TGA header is truncated" + id_length = data[0] + color_map_type = data[1] + image_type = data[2] + width, height = struct.unpack_from(" tuple[float, float] | None: + """Compare the top half with the recursive lower-left strip signature.""" + + if factor not in (2, 4) or width % factor or (height // 2) % factor: + return None + target_width = width // factor + target_height = (height // 2) // factor + if target_width < 1 or target_height < 1: + return None + target_y = height - height // factor + if target_y + target_height > height: + return None + + count = target_width * target_height + source_luma_sum = 0 + target_luma_sum = 0 + source_luma_squared = 0 + target_luma_squared = 0 + luma_products = 0 + absolute_rgb_error = 0 + block_pixels = factor * factor + + for y in range(target_height): + for x in range(target_width): + source_rgb = [0, 0, 0] + for block_y in range(factor): + source_y = y * factor + block_y + for block_x in range(factor): + source_x = x * factor + block_x + source_index = (source_y * width + source_x) * 3 + source_rgb[0] += rgb[source_index] + source_rgb[1] += rgb[source_index + 1] + source_rgb[2] += rgb[source_index + 2] + source_rgb = [ + (channel + block_pixels // 2) // block_pixels for channel in source_rgb + ] + target_index = ((target_y + y) * width + x) * 3 + target_rgb = rgb[target_index : target_index + 3] + absolute_rgb_error += sum( + abs(source_rgb[channel] - target_rgb[channel]) for channel in range(3) + ) + source_luma = ( + 77 * source_rgb[0] + 150 * source_rgb[1] + 29 * source_rgb[2] + ) >> 8 + target_luma = ( + 77 * target_rgb[0] + 150 * target_rgb[1] + 29 * target_rgb[2] + ) >> 8 + source_luma_sum += source_luma + target_luma_sum += target_luma + source_luma_squared += source_luma * source_luma + target_luma_squared += target_luma * target_luma + luma_products += source_luma * target_luma + + source_variance_term = count * source_luma_squared - source_luma_sum**2 + target_variance_term = count * target_luma_squared - target_luma_sum**2 + if source_variance_term <= count * count or target_variance_term <= count * count: + return None + correlation = ( + count * luma_products - source_luma_sum * target_luma_sum + ) / (source_variance_term * target_variance_term) ** 0.5 + mean_absolute_error = absolute_rgb_error / (count * 3) + return correlation, mean_absolute_error + + +def recursive_scaled_strip_failure(rgb: bytes, width: int, height: int) -> str | None: + if width < 16 or height < 16 or width % 4 or height % 8: + return None + metrics = [recursive_scaled_strip_metrics(rgb, width, height, factor) for factor in (2, 4)] + if all( + metric is not None and metric[0] >= 0.98 and metric[1] <= 3.0 + for metric in metrics + ): + return "TGA contains recursive scaled-strip repetition" + return None + + +def validate_tga(path: Path, expected_dimensions: tuple[int, int] | None = None) -> str | None: + decoded = decode_tga_rgb(path.read_bytes()) + if isinstance(decoded, str): + return decoded + width, height, rgb = decoded + if expected_dimensions is not None and (width, height) != expected_dimensions: + return ( + f"TGA dimensions {width}x{height} differ from expected " + f"{expected_dimensions[0]}x{expected_dimensions[1]}" + ) + pixel_count = width * height + sample_step = max(1, pixel_count // 4096) + sampled_luma = [ + (77 * rgb[index] + 150 * rgb[index + 1] + 29 * rgb[index + 2]) >> 8 + for index in range(0, len(rgb), sample_step * 3) + ] + if sampled_luma and max(sampled_luma) - min(sampled_luma) <= 2: + return "TGA image is blank or near-solid" + strip_failure = recursive_scaled_strip_failure(rgb, width, height) + if strip_failure: + return strip_failure + return None + + +def center_aspect_resample_rgb( + rgb: bytes, + source_width: int, + source_height: int, + output_width: int, + output_height: int, +) -> bytes: + """Centre-crop and bilinearly resize RGB pixels using integer arithmetic.""" + + if ( + source_width < 1 + or source_height < 1 + or output_width < 1 + or output_height < 1 + or len(rgb) != source_width * source_height * 3 + ): + raise ValueError("invalid RGB dimensions or payload for centre-aspect resampling") + + crop_x = 0 + crop_y = 0 + crop_width = source_width + crop_height = source_height + source_aspect_product = source_width * output_height + output_aspect_product = source_height * output_width + if source_aspect_product > output_aspect_product: + crop_width = max(1, source_height * output_width // output_height) + crop_x = (source_width - crop_width) // 2 + elif source_aspect_product < output_aspect_product: + crop_height = max(1, source_width * output_height // output_width) + crop_y = (source_height - crop_height) // 2 + + output = bytearray(output_width * output_height * 3) + crop_max_x = crop_x + crop_width - 1 + crop_max_y = crop_y + crop_height - 1 + x_denominator = 2 * output_width + y_denominator = 2 * output_height + interpolation_denominator = x_denominator * y_denominator + + for output_y in range(output_height): + source_y_numerator = (2 * output_y + 1) * crop_height - output_height + source_y = crop_y + source_y_numerator // y_denominator + y_fraction = source_y_numerator % y_denominator + if source_y < crop_y: + source_y = crop_y + y_fraction = 0 + elif source_y >= crop_max_y: + source_y = crop_max_y + y_fraction = 0 + next_y = min(source_y + 1, crop_max_y) + + for output_x in range(output_width): + source_x_numerator = (2 * output_x + 1) * crop_width - output_width + source_x = crop_x + source_x_numerator // x_denominator + x_fraction = source_x_numerator % x_denominator + if source_x < crop_x: + source_x = crop_x + x_fraction = 0 + elif source_x >= crop_max_x: + source_x = crop_max_x + x_fraction = 0 + next_x = min(source_x + 1, crop_max_x) + + top_left = (source_y * source_width + source_x) * 3 + top_right = (source_y * source_width + next_x) * 3 + bottom_left = (next_y * source_width + source_x) * 3 + bottom_right = (next_y * source_width + next_x) * 3 + target = (output_y * output_width + output_x) * 3 + for channel in range(3): + top = ( + rgb[top_left + channel] * (x_denominator - x_fraction) + + rgb[top_right + channel] * x_fraction + ) + bottom = ( + rgb[bottom_left + channel] * (x_denominator - x_fraction) + + rgb[bottom_right + channel] * x_fraction + ) + output[target + channel] = ( + top * (y_denominator - y_fraction) + + bottom * y_fraction + + interpolation_denominator // 2 + ) // interpolation_denominator + return bytes(output) + + +def rgb_similarity_metrics(reference_rgb: bytes, candidate_rgb: bytes) -> tuple[float, float] | str: + """Return Pearson luma correlation and mean absolute RGB error.""" + + if not reference_rgb or len(reference_rgb) != len(candidate_rgb) or len(reference_rgb) % 3: + return "RGB comparison payloads differ or are empty" + pixel_count = len(reference_rgb) // 3 + reference_luma_sum = 0 + candidate_luma_sum = 0 + reference_luma_squared = 0 + candidate_luma_squared = 0 + luma_products = 0 + absolute_rgb_error = 0 + for index in range(0, len(reference_rgb), 3): + reference_luma = ( + 77 * reference_rgb[index] + + 150 * reference_rgb[index + 1] + + 29 * reference_rgb[index + 2] + ) >> 8 + candidate_luma = ( + 77 * candidate_rgb[index] + + 150 * candidate_rgb[index + 1] + + 29 * candidate_rgb[index + 2] + ) >> 8 + reference_luma_sum += reference_luma + candidate_luma_sum += candidate_luma + reference_luma_squared += reference_luma * reference_luma + candidate_luma_squared += candidate_luma * candidate_luma + luma_products += reference_luma * candidate_luma + absolute_rgb_error += ( + abs(reference_rgb[index] - candidate_rgb[index]) + + abs(reference_rgb[index + 1] - candidate_rgb[index + 1]) + + abs(reference_rgb[index + 2] - candidate_rgb[index + 2]) + ) + + reference_variance_term = ( + pixel_count * reference_luma_squared - reference_luma_sum**2 + ) + candidate_variance_term = ( + pixel_count * candidate_luma_squared - candidate_luma_sum**2 + ) + if reference_variance_term <= 0 or candidate_variance_term <= 0: + return "RGB comparison has insufficient luma variance" + correlation = ( + pixel_count * luma_products - reference_luma_sum * candidate_luma_sum + ) / (reference_variance_term * candidate_variance_term) ** 0.5 + mean_absolute_rgb_error = absolute_rgb_error / (pixel_count * 3) + return correlation, mean_absolute_rgb_error + + +def exposure_compensated_rgb_metrics( + reference_rgb: bytes, candidate_rgb: bytes +) -> tuple[float, float, float] | str: + """Fit one bounded luma exposure transform and return its RGB residual.""" + + if not reference_rgb or len(reference_rgb) != len(candidate_rgb) or len(reference_rgb) % 3: + return "RGB comparison payloads differ or are empty" + pixel_count = len(reference_rgb) // 3 + reference_luma_sum = 0 + candidate_luma_sum = 0 + candidate_luma_squared = 0 + luma_products = 0 + for index in range(0, len(reference_rgb), 3): + reference_luma = ( + 77 * reference_rgb[index] + + 150 * reference_rgb[index + 1] + + 29 * reference_rgb[index + 2] + ) >> 8 + candidate_luma = ( + 77 * candidate_rgb[index] + + 150 * candidate_rgb[index + 1] + + 29 * candidate_rgb[index + 2] + ) >> 8 + reference_luma_sum += reference_luma + candidate_luma_sum += candidate_luma + candidate_luma_squared += candidate_luma * candidate_luma + luma_products += reference_luma * candidate_luma + + candidate_variance_term = ( + pixel_count * candidate_luma_squared - candidate_luma_sum**2 + ) + if candidate_variance_term <= 0: + return "RGB comparison has insufficient luma variance" + covariance_term = ( + pixel_count * luma_products - reference_luma_sum * candidate_luma_sum + ) + exposure_gain = covariance_term / candidate_variance_term + exposure_gain = max( + SP_SAVE_PREVIEW_MIN_EXPOSURE_GAIN, + min(SP_SAVE_PREVIEW_MAX_EXPOSURE_GAIN, exposure_gain), + ) + reference_luma_mean = reference_luma_sum / pixel_count + candidate_luma_mean = candidate_luma_sum / pixel_count + exposure_bias = reference_luma_mean - exposure_gain * candidate_luma_mean + exposure_bias = max( + SP_SAVE_PREVIEW_MIN_EXPOSURE_BIAS, + min(SP_SAVE_PREVIEW_MAX_EXPOSURE_BIAS, exposure_bias), + ) + + absolute_rgb_error = 0 + for reference_channel, candidate_channel in zip(reference_rgb, candidate_rgb): + compensated_channel = int(exposure_gain * candidate_channel + exposure_bias + 0.5) + compensated_channel = max(0, min(255, compensated_channel)) + absolute_rgb_error += abs(reference_channel - compensated_channel) + mean_absolute_rgb_error = absolute_rgb_error / len(reference_rgb) + return exposure_gain, exposure_bias, mean_absolute_rgb_error + + +def save_preview_comparison( + reference_path: Path, preview_path: Path +) -> tuple[dict[str, Any] | None, str | None]: + """Compare the save preview with the adjacent pre-save frame from the same SP run.""" + + reference = decode_tga_rgb(reference_path.read_bytes()) + if isinstance(reference, str): + return None, f"same-state save-reference screenshot cannot be compared: {reference}" + preview = decode_tga_rgb(preview_path.read_bytes()) + if isinstance(preview, str): + return None, f"save preview cannot be compared: {preview}" + reference_width, reference_height, reference_rgb = reference + preview_width, preview_height, preview_rgb = preview + analysis_width, analysis_height = SP_SAVE_PREVIEW_ANALYSIS_DIMENSIONS + reference_rgb = center_aspect_resample_rgb( + reference_rgb, + reference_width, + reference_height, + analysis_width, + analysis_height, + ) + candidate_rgb = center_aspect_resample_rgb( + preview_rgb, + preview_width, + preview_height, + analysis_width, + analysis_height, + ) + metrics = rgb_similarity_metrics(reference_rgb, candidate_rgb) + if isinstance(metrics, str): + return None, metrics + luma_correlation, mean_absolute_rgb_error = metrics + compensated_metrics = exposure_compensated_rgb_metrics(reference_rgb, candidate_rgb) + if isinstance(compensated_metrics, str): + return None, compensated_metrics + exposure_gain, exposure_bias, compensated_mean_absolute_rgb_error = compensated_metrics + record = { + "algorithm": SP_SAVE_PREVIEW_COMPARISON_ALGORITHM, + "referenceArtifact": "saveReferenceScreenshot", + "analysisDimensions": {"width": analysis_width, "height": analysis_height}, + "lumaCorrelation": round(luma_correlation, 6), + "meanAbsoluteRgbError": round(mean_absolute_rgb_error, 3), + "minimumLumaCorrelation": SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION, + "exposureCompensation": { + "model": "shared-luma-affine", + "gain": round(exposure_gain, 6), + "bias": round(exposure_bias, 3), + "minimumGain": SP_SAVE_PREVIEW_MIN_EXPOSURE_GAIN, + "maximumGain": SP_SAVE_PREVIEW_MAX_EXPOSURE_GAIN, + "minimumBias": SP_SAVE_PREVIEW_MIN_EXPOSURE_BIAS, + "maximumBias": SP_SAVE_PREVIEW_MAX_EXPOSURE_BIAS, + }, + "exposureCompensatedMeanAbsoluteRgbError": round( + compensated_mean_absolute_rgb_error, 3 + ), + "maximumExposureCompensatedMeanAbsoluteRgbError": ( + SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR + ), + } + policy_failures: list[str] = [] + if luma_correlation < SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION: + policy_failures.append( + f"luma correlation {luma_correlation:.6f} is below " + f"{SP_SAVE_PREVIEW_MIN_LUMA_CORRELATION:.6f}" + ) + if compensated_mean_absolute_rgb_error > SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR: + policy_failures.append( + "exposure-compensated mean RGB error " + f"{compensated_mean_absolute_rgb_error:.3f} exceeds " + f"{SP_SAVE_PREVIEW_MAX_COMPENSATED_RGB_ERROR:.3f}" + ) + failure = "; ".join(policy_failures) if policy_failures else None + return record, failure + + +def evaluate_role( + plan: RolePlan, + output_dir: Path, + exit_code: int, + timed_out: bool, + budget_contract: dict[str, Any] | None = None, +) -> dict[str, Any]: + log_path = find_log(plan) + authoritative_diagnostics, all_diagnostics = collect_role_diagnostics( + log_path, plan.stdout_path, plan.stderr_path + ) + failures: list[str] = [] + if timed_out: + failures.append("process timeout") + if exit_code != 0: + failures.append(f"exit code {exit_code}") + if log_path is None: + failures.append("engine log missing") + if plan.marker.casefold() not in authoritative_diagnostics.casefold(): + failures.append(f"completion marker missing: {plan.marker}") + for marker in ROLE_EVIDENCE_CONTRACT[plan.role_id].get("requiredMarkers", []): + if marker.casefold() not in authoritative_diagnostics.casefold(): + failures.append(f"required gameplay marker missing: {marker}") + if plan.role_id == "mp-client": + active_failure = mp_client_active_proof_failure(authoritative_diagnostics) + if active_failure: + failures.append(active_failure) + width_values = planned_cvar_values(plan.args, "r_windowWidth") + height_values = planned_cvar_values(plan.args, "r_windowHeight") + requested_width = int(width_values[-1]) if len(width_values) == 1 else None + requested_height = int(height_values[-1]) if len(height_values) == 1 else None + display_failure = runtime_window_failure( + authoritative_diagnostics, requested_width, requested_height + ) + if display_failure: + failures.append(display_failure) + for name, pattern in FATAL_PATTERNS.items(): + count = len(pattern.findall(all_diagnostics)) + if count: + failures.append(f"{name} diagnostics={count}") + + budget_evidence: dict[str, Any] = {} + budget_identity = ROLE_EVIDENCE_CONTRACT[plan.role_id].get("budget") + if budget_identity is not None: + if budget_contract is None: + budget_contract, _ = load_contract(DEFAULT_CONTRACT_PATH) + budget_evidence, budget_failures = evaluate_timing_evidence( + ( + read_text(log_path), + read_text(plan.stdout_path), + read_text(plan.stderr_path), + ), + budget_contract, + budget_identity["map"], + budget_identity["backend"], + budget_identity["profile"], + ) + failures.extend(f"renderer budget: {failure}" for failure in budget_failures) + + artifacts: list[dict[str, Any]] = [] + if log_path is not None: + artifacts.append({"kind": "engineLog", **file_record(log_path, output_dir)}) + for kind, path in (("processStdout", plan.stdout_path), ("processStderr", plan.stderr_path)): + if path is None or not path.is_file(): + failures.append(f"{kind} capture missing") + else: + artifacts.append({"kind": kind, **file_record(path, output_dir)}) + for kind, relative in plan.expected: + path = plan.savepath / Path(relative) + if not path.is_file() or path.stat().st_size == 0: + failures.append(f"missing or empty {kind}: {relative}") + continue + if kind in ("screenshot", "saveReferenceScreenshot", "savePreview"): + runtime_evidence = runtime_window_evidence(authoritative_diagnostics) + expected_dimensions = SP_SAVE_PREVIEW_DIMENSIONS if kind == "savePreview" else ( + (runtime_evidence[1], runtime_evidence[2]) if runtime_evidence is not None else None + ) + tga_failure = validate_tga(path, expected_dimensions) + if tga_failure: + failures.append(f"{relative}: {tga_failure}") + artifacts.append({"kind": kind, **file_record(path, output_dir)}) + + result = { + "role": plan.role_id, + "mode": plan.mode, + "status": "pass" if not failures else "fail", + "exitCode": exit_code, + "timedOut": timed_out, + "failures": failures, + "artifacts": artifacts, + "budgetEvidence": budget_evidence, + } + if plan.role_id == "sp-capture": + expected_by_kind = {kind: plan.savepath / Path(relative) for kind, relative in plan.expected} + reference_path = expected_by_kind.get("saveReferenceScreenshot") + preview_path = expected_by_kind.get("savePreview") + if ( + reference_path is not None + and preview_path is not None + and reference_path.is_file() + and preview_path.is_file() + ): + comparison, comparison_failure = save_preview_comparison( + reference_path, preview_path + ) + if comparison is not None: + result["savePreviewComparison"] = comparison + if comparison_failure: + failures.append( + "save preview differs from same-state save reference: " + f"{comparison_failure}" + ) + else: + failures.append("save preview comparison artifacts are unavailable") + result["status"] = "pass" if not failures else "fail" + return result + + +def run_capture( + runtime_dir: Path, + executable: Path, + output_dir: Path, + plans: dict[str, RolePlan], + timeout: int, + mp_client_delay: int, + budget_contract: dict[str, Any], +) -> list[dict[str, Any]]: + results: list[dict[str, Any]] = [] + + sp_plan = plans["sp-capture"] + sp_process = launch(executable, sp_plan, runtime_dir) + exit_code, timed_out = wait_process(sp_process, timeout) + results.append(evaluate_role(sp_plan, output_dir, exit_code, timed_out, budget_contract)) + if results[-1]["status"] == "pass": + playback_plan = plans["sp-demo-playback"] + playback_process = launch(executable, playback_plan, runtime_dir) + exit_code, timed_out = wait_process(playback_process, timeout) + results.append(evaluate_role(playback_plan, output_dir, exit_code, timed_out, budget_contract)) + else: + results.append({ + "role": "sp-demo-playback", + "mode": "SP demo playback", + "status": "fail", + "exitCode": None, + "timedOut": False, + "failures": ["not run because SP capture/save-load failed"], + "artifacts": [], + }) + + server_plan = plans["mp-server"] + client_plan = plans["mp-client"] + mp_deadline = time.monotonic() + timeout + server_process = launch(executable, server_plan, runtime_dir) + time.sleep(max(1, mp_client_delay)) + client_process = launch(executable, client_plan, runtime_dir) + mp_results = wait_processes_until( + {"server": server_process, "client": client_process}, mp_deadline + ) + server_exit, server_timeout = mp_results["server"] + client_exit, client_timeout = mp_results["client"] + results.append(evaluate_role(server_plan, output_dir, server_exit, server_timeout, budget_contract)) + results.append(evaluate_role(client_plan, output_dir, client_exit, client_timeout, budget_contract)) + return results + + +def git_state(root: Path) -> dict[str, Any]: + def run(*args: str) -> str: + completed = subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, check=False) + return completed.stdout.strip() if completed.returncode == 0 else "" + + return { + "policy": GIT_PROVENANCE_POLICY, + "root": str(root.resolve()), + "revision": run("rev-parse", "HEAD"), + "dirty": bool(run("status", "--porcelain")), + } + + +def verify_recorded_files( + report: dict[str, Any], + report_dir: Path, + asset_root: Path, + runtime_dir: Path | None = None, + budget_contract: dict[str, Any] | None = None, + budget_binding: dict[str, Any] | None = None, +) -> list[str]: + failures: list[str] = [] + if budget_contract is None or budget_binding is None: + budget_contract, budget_binding = load_contract(DEFAULT_CONTRACT_PATH) + if report.get("status") != "pass": + failures.append(f"baseline report status is {report.get('status')!r}, not 'pass'") + if report.get("dryRun") is not False: + failures.append("passing baseline must record dryRun=false") + for field_name in TOP_LEVEL_FAILURE_ARRAYS: + field_value = report.get(field_name) + if not isinstance(field_value, list): + failures.append(f"baseline {field_name} field is missing or is not an array") + elif field_value: + failures.append(f"baseline {field_name} field is nonempty") + failures.extend( + verify_contract_binding( + report.get("budgetContract"), budget_contract, budget_binding + ) + ) + + current_git = git_state(repo_root()) + recorded_git = report.get("git") + if not isinstance(recorded_git, dict): + failures.append("baseline report does not contain git provenance") + else: + revision = recorded_git.get("revision") + if ( + recorded_git.get("policy") != GIT_PROVENANCE_POLICY + or recorded_git.get("root") != current_git.get("root") + or not isinstance(revision, str) + or re.fullmatch(r"[0-9a-f]{40}", revision) is None + or not isinstance(recorded_git.get("dirty"), bool) + ): + failures.append("baseline git provenance policy/shape differs") + if not current_git.get("revision"): + failures.append("current openQ4 git revision is unavailable") + elif revision != current_git.get("revision"): + failures.append( + "baseline git revision is not the current openQ4 HEAD: " + f"recorded {revision!r}, current {current_git.get('revision')!r}" + ) + if isinstance(recorded_git.get("dirty"), bool) and ( + recorded_git.get("dirty") != current_git.get("dirty") + ): + failures.append("baseline git dirty state differs from the current checkout") + + assets_value = report.get("assets") + if not isinstance(assets_value, dict): + failures.append("baseline report assets field is missing or is not an object") + assets: dict[str, Any] = {} + else: + assets = assets_value + selected_asset_root = asset_root.resolve() + recorded_asset_root = assets.get("root") + if not isinstance(recorded_asset_root, str) or not recorded_asset_root: + failures.append("baseline report does not record its retail asset root") + elif Path(recorded_asset_root).resolve() != selected_asset_root: + failures.append( + "retail asset root differs: " + f"recorded {Path(recorded_asset_root).resolve()}, selected {selected_asset_root}" + ) + allow_value = assets.get("allowLinkedGameDirectories") + if not isinstance(allow_value, bool): + failures.append("allowLinkedGameDirectories must be a boolean") + allow_asset_dir_links = False + else: + allow_asset_dir_links = allow_value + + expected_binding = report.get("expectedAssets", {}) + if ( + not isinstance(expected_binding, dict) + or expected_binding.get("supplied") is not True + or not isinstance(expected_binding.get("path"), str) + or not expected_binding.get("path") + or not isinstance(expected_binding.get("sha256"), str) + or re.fullmatch(r"[0-9a-f]{64}", expected_binding.get("sha256", "")) is None + ): + failures.append("passing baseline is not bound to a recorded --expected-assets manifest") + else: + expected_path = Path(expected_binding["path"]) + if not expected_path.is_absolute(): + failures.append("recorded expected-assets manifest path is not absolute") + elif not expected_path.is_file(): + failures.append(f"recorded expected-assets manifest is missing: {expected_path}") + elif is_link_or_junction(expected_path): + failures.append(f"recorded expected-assets manifest must not be a link: {expected_path}") + else: + actual_expected_sha256 = sha256_file(expected_path) + if actual_expected_sha256 != expected_binding["sha256"]: + failures.append( + "recorded expected-assets manifest SHA-256 differs: " + f"expected {expected_binding['sha256']}, got {actual_expected_sha256}" + ) + try: + expected_payload = json.loads(expected_path.read_text(encoding="utf-8")) + if not isinstance(expected_payload, dict): + raise ValueError("expected asset manifest root is not an object") + expected_records = asset_manifest_from_payload(expected_payload) + recorded_records = asset_manifest_from_payload(assets) + if expected_records != recorded_records: + failures.append( + "recorded retail PK4 inventory differs from the bound expected-assets manifest" + ) + expected_loose = loose_manifest_from_payload(expected_payload) + recorded_loose = loose_manifest_from_payload(assets) + if expected_loose != recorded_loose: + failures.append( + "recorded loose retail inventory differs from the bound expected-assets manifest" + ) + except (json.JSONDecodeError, OSError, ValueError) as exc: + failures.append(f"recorded expected-assets manifest is invalid: {exc}") + + current_assets: list[dict[str, Any]] | None = None + try: + current_assets = collect_pk4s(selected_asset_root, allow_asset_dir_links) + failures.extend(compare_asset_records(asset_manifest_from_payload(assets), current_assets)) + if assets.get("directoryViews") != asset_directory_views(selected_asset_root): + failures.append("recorded retail directory views differ") + current_loose = collect_loose_asset_files(selected_asset_root, allow_asset_dir_links) + failures.extend( + compare_file_records(loose_manifest_from_payload(assets), current_loose, "loose asset file") + ) + if current_loose: + failures.append( + f"retail fallback asset root contains {len(current_loose)} loose q4base/q4mp files" + ) + if loose_manifest_from_payload(assets): + failures.append("recorded baseline includes loose q4base/q4mp files") + except (OSError, ValueError) as exc: + failures.append(str(exc)) + + recorded_runtime_root = report.get("runtimeRoot") + selected_runtime_dir: Path | None = runtime_dir + if not isinstance(recorded_runtime_root, str) or not recorded_runtime_root: + failures.append("baseline report does not record its runtime root") + else: + recorded_runtime_dir = Path(recorded_runtime_root).absolute() + if selected_runtime_dir is None: + selected_runtime_dir = recorded_runtime_dir + elif selected_runtime_dir.resolve() != recorded_runtime_dir: + failures.append( + "runtime root differs: " + f"recorded {recorded_runtime_dir}, selected {selected_runtime_dir.resolve()}" + ) + if selected_runtime_dir is None: + selected_runtime_dir = repo_root() / ".install" + current_overlay_pk4s: list[dict[str, Any]] | None = None + try: + selected_runtime_dir = validate_runtime_dir(selected_runtime_dir, repo_root()) + executable = find_client(selected_runtime_dir) + current_runtime = collect_runtime_files(selected_runtime_dir, executable) + failures.extend( + compare_file_records(report.get("runtimeFiles", []), current_runtime, "runtime file") + ) + current_overlay_pk4s = collect_overlay_pk4s(selected_runtime_dir) + failures.extend( + compare_file_records( + assets.get("openQ4OverlayPk4s", []), current_overlay_pk4s, "openQ4 overlay PK4" + ) + ) + current_overlay_loose, unexpected = collect_overlay_loose_files( + selected_runtime_dir, current_runtime + ) + failures.extend( + compare_file_records( + assets.get("openQ4OverlayLooseFiles", []), + current_overlay_loose, + "openQ4 overlay loose file", + ) + ) + failures.extend(unexpected) + except (OSError, ValueError) as exc: + failures.append(str(exc)) + + recorded_collisions = assets.get("retailPathCollisions") + if assets.get("compatibilityModel") != ASSET_COMPATIBILITY_MODEL: + failures.append("asset compatibility model differs") + if assets.get("retailArchiveBytesMatchExpected") is not True: + failures.append("report does not establish that retail archive bytes match the expected manifest") + if not isinstance(recorded_collisions, list): + failures.append("retailPathCollisions field is missing or is not an array") + recorded_collisions = [] + collision_count = assets.get("retailPathCollisionCount") + if not isinstance(collision_count, int) or isinstance(collision_count, bool): + failures.append("retailPathCollisionCount field is missing or is not an integer") + elif collision_count != len(recorded_collisions): + failures.append("retailPathCollisionCount does not match the recorded collision inventory") + namespace_untouched = assets.get("retailPathNamespaceUntouched") + if namespace_untouched is not (len(recorded_collisions) == 0): + failures.append("retailPathNamespaceUntouched claim differs from the collision inventory") + if current_assets is not None and current_overlay_pk4s is not None: + try: + current_collisions = collect_retail_path_collisions( + selected_asset_root, + current_assets, + selected_runtime_dir, + current_overlay_pk4s, + ) + if recorded_collisions != current_collisions: + failures.append( + "recorded retail/openQ4 virtual-path collision inventory differs" + ) + except (OSError, ValueError) as exc: + failures.append(str(exc)) + + safety = report.get("safety", {}) + window_size = safety.get("windowSize", {}) if isinstance(safety, dict) else {} + width = window_size.get("width") if isinstance(window_size, dict) else None + height = window_size.get("height") if isinstance(window_size, dict) else None + if ( + not isinstance(safety, dict) + or safety.get("windowedOnly") is not True + or safety.get("borderless") is not False + or safety.get("displayContract") != display_contract() + or safety.get("engineScreenshotOnly") is not True + or safety.get("operatingSystemCapture") is not False + or safety.get("inputInjection") is not False + or not isinstance(width, int) + or not isinstance(height, int) + or width != DEFAULT_WIDTH + or height != DEFAULT_HEIGHT + ): + failures.append("baseline safety/window contract differs") + + mp_port_value = report.get("mpPort") + if ( + not isinstance(mp_port_value, int) + or isinstance(mp_port_value, bool) + or not (1024 <= mp_port_value <= 65535) + ): + failures.append("baseline mpPort is missing or outside 1024..65535") + mp_port = 28140 + else: + mp_port = mp_port_value + + plans_value = report.get("plan") + if not isinstance(plans_value, list): + failures.append("baseline plan field is missing or is not an array") + plans: list[Any] = [] + else: + plans = plans_value + plan_by_role = { + plan.get("role"): plan for plan in plans if isinstance(plan, dict) and plan.get("role") + } + if len(plans) != len(ROLE_EVIDENCE_CONTRACT) or set(plan_by_role) != set(ROLE_EVIDENCE_CONTRACT): + failures.append("baseline plan does not contain each required role exactly once") + for role, contract in ROLE_EVIDENCE_CONTRACT.items(): + plan = plan_by_role.get(role, {}) + expected_mode = "SP demo playback" if role == "sp-demo-playback" else ( + "SP" if role == "sp-capture" else "MP" + ) + canonical_savepath = report_dir / "savepaths" / contract["saveDir"] + if plan.get("mode") != expected_mode: + failures.append(f"{role}: plan mode differs") + if plan.get("savepath") != str(canonical_savepath): + failures.append(f"{role}: plan savepath differs") + if plan.get("logName") != contract["logName"]: + failures.append(f"{role}: plan log name differs") + if plan.get("marker") != contract["marker"]: + failures.append(f"{role}: plan completion marker differs") + if plan.get("requiredMarkers", []) != contract.get("requiredMarkers", []): + failures.append(f"{role}: plan required gameplay markers differ") + if plan.get("windowed") is not True or plan.get("captureMethod") != "engine screenshot command": + failures.append(f"{role}: plan safety/capture contract differs") + launch_contract = { + "r_rendererSharedGui": "0", + "r_rendererSharedInWorldGui": "0", + "r_rendererSharedCinematicPost": "0", + "r_rendererSharedSpecialFrame": "0", + "r_rendererSharedWorldAmbient": "0", + "r_rendererSharedWorldInteraction": "0", + "r_rendererSharedWorldFogBlend": "0", + "r_rendererSharedSubview": "0", + "r_rendererSharedDeform": "0", + "r_fullscreen": "0", + "r_borderless": "0", + "r_borderlessDefaultMigrated": "1", + "r_fullscreenDesktop": "0", + "r_windowWidth": str(width), + "r_windowHeight": str(height), + "r_renderApi": "gl", + "fs_basepath": str(selected_asset_root), + "fs_savepath": str(canonical_savepath), + "fs_devpath": str(canonical_savepath), + "fs_game": "baseoq4", + } + if role in ("mp-server", "mp-client"): + launch_contract["ui_autoJoin"] = "1" + if role == "sp-capture": + launch_contract["si_gameType"] = "singleplayer" + elif role == "mp-server": + launch_contract.update( + { + "net_serverDedicated": "0", + "net_port": str(mp_port), + "si_pure": "1", + "net_serverAllowServerMod": "0", + "si_gameType": "DM", + } + ) + for cvar, expected_value in launch_contract.items(): + if planned_cvar_values(plan.get("arguments"), cvar) != [expected_value]: + failures.append(f"{role}: launch CVar {cvar} differs") + command_contract: dict[str, list[list[str]]] = {} + if role == "sp-capture": + command_contract["map"] = [[SP_MAP]] + elif role == "sp-demo-playback": + command_contract["timeDemoQuit"] = [[SP_DEMO_NAME]] + elif role == "mp-server": + command_contract["spawnServer"] = [[MP_MAP]] + else: + command_contract["connect"] = [[f"127.0.0.1:{mp_port}"]] + for command, expected_values in command_contract.items(): + if planned_command_values(plan.get("arguments"), command) != expected_values: + failures.append(f"{role}: launch command {command} differs") + arguments = plan.get("arguments", []) + if not isinstance(arguments, list) or arguments.count("+vid_restart") != 1: + failures.append(f"{role}: launch must contain exactly one post-CVar vid_restart") + elif any( + cvar in arguments and arguments.index(cvar) > arguments.index("+vid_restart") + for cvar in ( + "r_fullscreen", + "r_borderless", + "r_borderlessDefaultMigrated", + "r_fullscreenDesktop", + "r_windowWidth", + "r_windowHeight", + "r_renderApi", + "r_rendererSharedGui", + "r_rendererSharedInWorldGui", + "r_rendererSharedCinematicPost", + "r_rendererSharedSpecialFrame", + "r_rendererSharedWorldAmbient", + "r_rendererSharedWorldInteraction", + "r_rendererSharedWorldFogBlend", + "r_rendererSharedSubview", + "r_rendererSharedDeform", + "fs_basepath", + "fs_savepath", + "fs_devpath", + "fs_game", + *(("ui_autoJoin",) if role in ("mp-server", "mp-client") else ()), + ) + ): + failures.append(f"{role}: vid_restart must follow the display/filesystem launch CVars") + if planned_cvar_values(plan.get("arguments"), "fs_cdpath"): + failures.append(f"{role}: launch must not override locked fs_cdpath") + expected_arguments = expected_role_arguments( + role, + selected_runtime_dir, + selected_asset_root, + report_dir, + width if isinstance(width, int) else DEFAULT_WIDTH, + height if isinstance(height, int) else DEFAULT_HEIGHT, + mp_port, + ) + if arguments != expected_arguments: + failures.append(f"{role}: launch arguments differ from the exact role contract") + expected_artifacts = [ + {"kind": kind, "path": path} for kind, path in contract["expected"].items() + ] + if plan.get("expected") != expected_artifacts: + failures.append(f"{role}: plan expected-artifact contract differs") + + results_value = report.get("results") + if not isinstance(results_value, list): + failures.append("baseline results field is missing or is not an array") + results: list[Any] = [] + else: + results = results_value + result_by_role = { + result.get("role"): result + for result in results + if isinstance(result, dict) and result.get("role") + } + if len(results) != len(ROLE_EVIDENCE_CONTRACT) or set(result_by_role) != set(ROLE_EVIDENCE_CONTRACT): + failures.append("baseline results do not contain each required role exactly once") + + for result in results: + for artifact in result.get("artifacts", []): + path = report_dir / Path(str(artifact.get("path", ""))) + if not path.is_file(): + failures.append(f"recorded artifact is missing: {artifact.get('path')}") + continue + if path.stat().st_size != artifact.get("size"): + failures.append(f"recorded artifact size differs: {artifact.get('path')}") + elif sha256_file(path) != artifact.get("sha256"): + failures.append(f"recorded artifact SHA-256 differs: {artifact.get('path')}") + for role, contract in ROLE_EVIDENCE_CONTRACT.items(): + result = result_by_role.get(role, {}) + expected_mode = "SP demo playback" if role == "sp-demo-playback" else ( + "SP" if role == "sp-capture" else "MP" + ) + if ( + result.get("status") != "pass" + or result.get("mode") != expected_mode + or result.get("exitCode") != 0 + or result.get("timedOut") is not False + or result.get("failures") != [] + ): + failures.append(f"{role}: passing result lifecycle contract is not satisfied") + artifacts = result.get("artifacts", []) + artifact_by_kind = { + item.get("kind"): item for item in artifacts if isinstance(item, dict) and item.get("kind") + } + required_kinds = {"engineLog", "processStdout", "processStderr", *contract["expected"]} + if len(artifacts) != len(required_kinds) or set(artifact_by_kind) != required_kinds: + failures.append(f"{role}: required artifact kinds differ") + continue + for kind, relative in contract["expected"].items(): + expected_suffix = f"savepaths/{'sp' if role.startswith('sp-') else role}/{relative}" + if str(artifact_by_kind[kind].get("path", "")).replace("\\", "/") != expected_suffix: + failures.append(f"{role}: {kind} artifact path differs") + for kind in ("processStdout", "processStderr"): + if artifact_by_kind[kind].get("path") != f"{role}.{'stdout' if kind == 'processStdout' else 'stderr'}.txt": + failures.append(f"{role}: {kind} artifact path differs") + log_path = str(artifact_by_kind["engineLog"].get("path", "")).replace("\\", "/") + allowed_log_paths = { + f"savepaths/{contract['saveDir']}/{game_dir}/logs/{contract['logName']}" + for game_dir in ("baseoq4", "q4base") + } + if log_path not in allowed_log_paths: + failures.append(f"{role}: engineLog artifact path differs") + authoritative_diagnostics, all_diagnostics = collect_role_diagnostics( + report_dir / Path(str(artifact_by_kind["engineLog"].get("path", ""))), + report_dir / Path(str(artifact_by_kind["processStdout"].get("path", ""))), + report_dir / Path(str(artifact_by_kind["processStderr"].get("path", ""))), + ) + budget_identity = contract.get("budget") + if budget_identity is not None: + budget_sources = ( + read_text(report_dir / Path(str(artifact_by_kind["engineLog"].get("path", "")))), + read_text(report_dir / Path(str(artifact_by_kind["processStdout"].get("path", "")))), + read_text(report_dir / Path(str(artifact_by_kind["processStderr"].get("path", "")))), + ) + failures.extend( + f"{role}: {failure}" + for failure in verify_recorded_evidence( + result.get("budgetEvidence"), + budget_sources, + budget_contract, + budget_identity["map"], + budget_identity["backend"], + budget_identity["profile"], + ) + ) + elif result.get("budgetEvidence") not in ({}, None): + failures.append(f"{role}: unexpected renderer budget evidence") + if contract["marker"].casefold() not in authoritative_diagnostics.casefold(): + failures.append(f"{role}: completion marker is absent from verified diagnostics") + for marker in contract.get("requiredMarkers", []): + if marker.casefold() not in authoritative_diagnostics.casefold(): + failures.append(f"{role}: required gameplay marker is absent: {marker}") + if role == "mp-client": + active_failure = mp_client_active_proof_failure(authoritative_diagnostics) + if active_failure: + failures.append(f"{role}: {active_failure}") + for name, pattern in FATAL_PATTERNS.items(): + count = len(pattern.findall(all_diagnostics)) + if count: + failures.append(f"{role}: {name} diagnostics={count}") + display_failure = runtime_window_failure(authoritative_diagnostics, width, height) + if display_failure: + failures.append(f"{role}: {display_failure}") + runtime_evidence = runtime_window_evidence(authoritative_diagnostics) + if runtime_evidence is not None: + for screenshot_kind in ("screenshot", "saveReferenceScreenshot"): + if screenshot_kind not in artifact_by_kind: + continue + screenshot_path = report_dir / Path( + str(artifact_by_kind[screenshot_kind].get("path", "")) + ) + if not screenshot_path.is_file(): + continue + tga_failure = validate_tga( + screenshot_path, (runtime_evidence[1], runtime_evidence[2]) + ) + if tga_failure: + failures.append(f"{role}: {screenshot_kind}: {tga_failure}") + if "savePreview" in artifact_by_kind: + preview_path = report_dir / Path( + str(artifact_by_kind["savePreview"].get("path", "")) + ) + if preview_path.is_file(): + tga_failure = validate_tga(preview_path, SP_SAVE_PREVIEW_DIMENSIONS) + if tga_failure: + failures.append(f"{role}: save preview: {tga_failure}") + if role == "sp-capture": + reference_path = report_dir / Path( + str(artifact_by_kind["saveReferenceScreenshot"].get("path", "")) + ) + preview_path = report_dir / Path( + str(artifact_by_kind["savePreview"].get("path", "")) + ) + if reference_path.is_file() and preview_path.is_file(): + comparison, comparison_failure = save_preview_comparison( + reference_path, preview_path + ) + if comparison is None: + failures.append( + f"{role}: save preview comparison could not be computed: " + f"{comparison_failure or 'unknown error'}" + ) + else: + if result.get("savePreviewComparison") != comparison: + failures.append( + f"{role}: recorded save-preview comparison metrics differ" + ) + if comparison_failure: + failures.append( + f"{role}: save preview differs from same-state save reference: " + f"{comparison_failure}" + ) + else: + failures.append(f"{role}: save preview comparison artifacts are unavailable") + return failures + + +def write_reports(output_dir: Path, payload: dict[str, Any]) -> tuple[Path, Path, Path, Path]: + report_json = output_dir / "stock_asset_baseline_report.json" + report_md = output_dir / "stock_asset_baseline_report.md" + asset_json = output_dir / "stock_pk4_manifest.json" + runtime_json = output_dir / "openq4_runtime_manifest.json" + report_json.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + asset_json.write_text( + json.dumps( + { + "schemaVersion": RETAIL_MANIFEST_SCHEMA_VERSION, + "allowLinkedGameDirectories": payload["assets"]["allowLinkedGameDirectories"], + "directoryViews": payload["assets"]["directoryViews"], + "stockPk4s": payload["assets"]["stockPk4s"], + "looseFiles": payload["assets"]["looseFiles"], + }, + indent=2, + ) + "\n", + encoding="utf-8", + ) + runtime_json.write_text( + json.dumps( + { + "schemaVersion": RUNTIME_MANIFEST_SCHEMA_VERSION, + "runtimeRoot": payload["runtimeRoot"], + "runtimeFiles": payload["runtimeFiles"], + "openQ4OverlayPk4s": payload["assets"]["openQ4OverlayPk4s"], + "openQ4OverlayLooseFiles": payload["assets"]["openQ4OverlayLooseFiles"], + "compatibilityModel": payload["assets"]["compatibilityModel"], + "retailPathCollisionCount": payload["assets"]["retailPathCollisionCount"], + "retailPathCollisions": payload["assets"]["retailPathCollisions"], + }, + indent=2, + ) + "\n", + encoding="utf-8", + ) + + results = payload.get("results", []) + collisions = payload["assets"].get("retailPathCollisions", []) + collision_counts_by_overlay: dict[str, int] = {} + for collision in collisions: + for archive in collision.get("openQ4OverlayPk4s", []): + collision_counts_by_overlay[archive] = collision_counts_by_overlay.get(archive, 0) + 1 + lines = [ + "# Retail-PK4 Compatibility Baseline With Packaged openQ4 Overlays", + "", + f"- Status: **{payload['status']}**", + f"- Generated: {payload['generatedUtc']}", + f"- Compatibility model: `{payload['assets']['compatibilityModel']}`", + f"- Git revision: `{payload['git']['revision'] or 'unavailable'}` (dirty: `{str(payload['git']['dirty']).lower()}`)", + f"- Git provenance policy: `{payload['git']['policy']}`", + f"- Per-map renderer budget contract: `{payload.get('budgetContract', {}).get('contractId', 'missing')}` (`{payload.get('budgetContract', {}).get('sha256', 'missing')}`)", + f"- Retail fallback asset root: `{payload['assets']['root']}`", + f"- Linked q4base/q4mp view allowed: `{str(payload['assets']['allowLinkedGameDirectories']).lower()}`", + f"- Retail PK4s: {len(payload['assets']['stockPk4s'])}", + f"- Loose q4base/q4mp files: {len(payload['assets']['looseFiles'])} (must be zero)", + "- Retail archive bytes match bound expected manifest: " + f"`{str(payload['assets']['retailArchiveBytesMatchExpected']).lower()}`", + f"- Expected-assets manifest bound: `{str(payload['expectedAssets']['supplied']).lower()}`", + f"- Expected-assets SHA-256: `{payload['expectedAssets']['sha256'] or 'not supplied'}`", + f"- openQ4 runtime root: `{payload['runtimeRoot']}`", + f"- openQ4 runtime files: {len(payload['runtimeFiles'])}", + f"- openQ4 overlay PK4s: {len(payload['assets']['openQ4OverlayPk4s'])}", + f"- openQ4 loose overlay files: {len(payload['assets']['openQ4OverlayLooseFiles'])}", + f"- Retail virtual paths superseded by packaged overlays: {len(collisions)}", + "- Retail virtual-path namespace untouched: " + f"`{str(payload['assets']['retailPathNamespaceUntouched']).lower()}`", + f"- MP loopback port: {payload['mpPort']}", + f"- Display: `{payload.get('safety', {}).get('displayContract', {}).get('contractId', 'not promotable') if payload.get('safety', {}).get('displayContract') else 'not promotable'}`", + "- Screenshots: engine `screenshot` command only", + "- Input automation: none", + "", + "## Asset directory view", + "", + "| Path | Linked | Resolved target |", + "|---|---|---|", + ] + for view in payload["assets"]["directoryViews"]: + lines.append( + f"| `{view['path']}` | `{str(view['linked']).lower()}` | `{view['resolvedTarget']}` |" + ) + lines += [ + "", + "## Packaged-overlay precedence", + "", + ] + if collisions: + lines.append( + "The verified retail PK4 archive bytes are unchanged, but packaged openQ4 " + f"overlays supersede **{len(collisions)}** retail virtual paths. This is a " + "retail-asset compatibility run, not an overlay-free or stock-only run." + ) + lines.append("") + for archive, count in sorted(collision_counts_by_overlay.items(), key=lambda item: item[0].casefold()): + noun = "path" if count == 1 else "paths" + lines.append(f"- `{archive}` supersedes {count} retail virtual {noun}.") + lines.append("") + lines.append( + "The complete path-by-path collision inventory is recorded in the JSON report." + ) + else: + lines.append( + "No member of a packaged openQ4 overlay PK4 supersedes a path present in the " + "verified retail PK4 set." + ) + lines += [ + "", + "## Results", + "", + "| Status | Role | Mode | Artifacts | Failures |", + "|---|---|---|---:|---|", + ] + for result in results: + failures = "; ".join(result.get("failures", [])) or "none" + lines.append( + f"| {result.get('status')} | `{result.get('role')}` | {result.get('mode')} | " + f"{len(result.get('artifacts', []))} | {failures} |" + ) + budget_results = [ + result for result in results if result.get("budgetEvidence") + ] + if budget_results: + lines += [ + "", + "## Per-map CPU/GPU budget evidence", + "", + "| Status | Role | Map | Backend | Profile | CPU samples / P95 / P99 | GPU samples / P95 / P99 | Budget |", + "|---|---|---|---|---|---|---|---|", + ] + for result in budget_results: + evidence = result["budgetEvidence"] + measurement = evidence.get("measurement", {}) + cpu = measurement.get("cpu", {}) + gpu = measurement.get("gpu", {}) + gpu_summary = ( + f"{gpu.get('samples', '?')} / {gpu.get('p95Us', '?')} / {gpu.get('p99Us', '?')} us" + if gpu.get("available") + else "unavailable" + ) + lines.append( + f"| {evidence.get('status', 'fail')} | `{result.get('role')}` | " + f"`{measurement.get('map', 'missing')}` | `{measurement.get('backend', 'missing')}` | " + f"`{measurement.get('profile', 'missing')}` | {cpu.get('samples', '?')} / " + f"{cpu.get('p95Us', '?')} / {cpu.get('p99Us', '?')} us | {gpu_summary} | " + f"`{evidence.get('budgetId', 'missing')}` |" + ) + sp_capture = next( + (result for result in results if result.get("role") == "sp-capture"), None + ) + comparison = ( + sp_capture.get("savePreviewComparison", {}) + if isinstance(sp_capture, dict) + else {} + ) + if comparison: + dimensions = comparison.get("analysisDimensions", {}) + exposure = comparison.get("exposureCompensation", {}) + lines += [ + "", + "## SP save-preview coherence", + "", + f"- Algorithm: `{comparison.get('algorithm')}`", + f"- Reference artifact: `{comparison.get('referenceArtifact')}`", + f"- Analysis size: {dimensions.get('width')}x{dimensions.get('height')}", + f"- Luma correlation: {comparison.get('lumaCorrelation')} " + f"(minimum {comparison.get('minimumLumaCorrelation')})", + f"- Raw mean absolute RGB error: {comparison.get('meanAbsoluteRgbError')}", + f"- Exposure compensation: gain {exposure.get('gain')}, " + f"bias {exposure.get('bias')}", + "- Exposure-compensated mean absolute RGB error: " + f"{comparison.get('exposureCompensatedMeanAbsoluteRgbError')} " + "(maximum " + f"{comparison.get('maximumExposureCompensatedMeanAbsoluteRgbError')})", + ] + if payload.get("assetComparisonFailures"): + lines += ["", "## Asset comparison failures", ""] + lines.extend(f"- {failure}" for failure in payload["assetComparisonFailures"]) + if payload.get("preflightFailures"): + lines += ["", "## Runtime/overlay preflight failures", ""] + lines.extend(f"- {failure}" for failure in payload["preflightFailures"]) + if payload.get("postCaptureVerificationFailures"): + lines += ["", "## Post-capture verification failures", ""] + lines.extend(f"- {failure}" for failure in payload["postCaptureVerificationFailures"]) + lines += [ + "", + "## Manual review still required", + "", + "Automated pass status proves file identity, map lifecycle, render-demo playback, save/restore completion, artifact integrity, broad save-preview coherence with the same-state pre-save frame, and that captured diagnostics contain none of the harness denylist classes: fatal errors, engine `ERROR` records, shader compile/program-link failures, Vulkan validation messages or VUIDs, and OpenGL errors. It does not assert warning-free logs; other warnings remain retained for manual review. A human must still review the engine screenshots for visual correctness and play representative SP/MP sequences for behavior, audio, and input feel.", + ] + report_md.write_text("\n".join(lines) + "\n", encoding="utf-8") + return report_json, report_md, asset_json, runtime_json + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--asset-root", default=default_asset_root(), help="Quake 4 installation root containing q4base/ and optionally q4mp/.") + parser.add_argument( + "--runtime-dir", + default="", + help=( + "Resolved openQ4 package directory containing the client and baseoq4/. " + "Defaults to repository .install; alternates must be fresh ordinary directories below .tmp/stock-runtime/." + ), + ) + parser.add_argument("--output-dir", default="", help="Evidence directory; defaults to .tmp/stock-baseline/.") + parser.add_argument("--expected-assets", default="", help="Optional prior stock_pk4_manifest.json or baseline report. Any mismatch fails before launch.") + parser.add_argument("--budget-contract", default=str(DEFAULT_CONTRACT_PATH), help="Versioned per-map CPU/GPU budget JSON bound by stable id and SHA-256.") + parser.add_argument( + "--allow-asset-dir-links", + action="store_true", + help="Permit q4base/q4mp symlinks or junctions in an intentionally clean asset view; resolved targets are recorded and nested links remain forbidden.", + ) + parser.add_argument("--verify-report", default="", help="Verify an existing report's PK4 and artifact hashes without launching openQ4.") + parser.add_argument("--dry-run", action="store_true", help="Write the exact windowed launch/config plan and manifests without launching openQ4.") + parser.add_argument("--list", action="store_true", help="List baseline cases and safety invariants without hashing or launching.") + parser.add_argument( + "--timeout", + type=int, + default=180, + help="Maximum seconds for each SP role and for the shared MP server/client session.", + ) + parser.add_argument("--mp-client-delay", type=int, default=8, help="Seconds between starting the listen server and loopback client.") + parser.add_argument("--mp-port", type=int, default=28140, help="Loopback listen-server UDP port.") + parser.add_argument("--width", type=int, default=DEFAULT_WIDTH) + parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT) + args = parser.parse_args(argv) + if args.width < 640 or args.height < 480: + parser.error("baseline window must be at least 640x480") + if ( + not args.dry_run + and not args.verify_report + and (args.width, args.height) != (DEFAULT_WIDTH, DEFAULT_HEIGHT) + ): + parser.error( + f"passing baseline budget evidence requires the exact bordered " + f"{DEFAULT_WIDTH}x{DEFAULT_HEIGHT} display contract" + ) + if not (1024 <= args.mp_port <= 65535): + parser.error("--mp-port must be between 1024 and 65535") + if args.timeout < 30: + parser.error("--timeout must be at least 30 seconds") + return args + + +def print_list() -> None: + print( + f"SP: {SP_MAP} -> render demo -> save-reference screenshot -> save -> " + "load -> post-load engine screenshot" + ) + print(f"SP demo: play back {SP_DEMO_NAME}.demo with timeDemoQuit") + print(f"MP: {MP_MAP} listen server + loopback client -> demos + engine screenshots") + print("Safety: r_fullscreen=0, fixed window, no OS capture, no mouse/keyboard injection") + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + if args.list: + print_list() + return 0 + if not args.asset_root: + raise ValueError("--asset-root is required on this platform") + asset_root = Path(args.asset_root).resolve() + source_root = repo_root() + budget_contract, budget_binding = load_contract(Path(args.budget_contract)) + requested_runtime_dir = Path(args.runtime_dir).absolute() if args.runtime_dir else None + + if args.verify_report: + report_path = Path(args.verify_report).resolve() + report = json.loads(report_path.read_text(encoding="utf-8")) + if report.get("schemaVersion") != SCHEMA_VERSION: + raise ValueError(f"unsupported baseline report schema: {report.get('schemaVersion')!r}") + failures = verify_recorded_files( + report, + report_path.parent, + asset_root, + requested_runtime_dir, + budget_contract, + budget_binding, + ) + for failure in failures: + print(f"error: {failure}", file=sys.stderr) + if not failures: + print("stock_asset_baseline verification: pass") + return 1 if failures else 0 + + runtime_dir = validate_runtime_dir( + requested_runtime_dir or source_root / ".install", source_root + ) + executable = find_client(runtime_dir) + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ") + output_dir = Path(args.output_dir).resolve() if args.output_dir else source_root / ".tmp" / "stock-baseline" / timestamp + prepare_output_directory(output_dir) + + stock_pk4s = collect_pk4s(asset_root, args.allow_asset_dir_links) + loose_files = collect_loose_asset_files(asset_root, args.allow_asset_dir_links) + runtime_files = collect_runtime_files(runtime_dir, executable) + overlay_pk4s = collect_overlay_pk4s(runtime_dir) + overlay_loose_files, preflight_failures = collect_overlay_loose_files( + runtime_dir, runtime_files + ) + try: + retail_path_collisions = collect_retail_path_collisions( + asset_root, stock_pk4s, runtime_dir, overlay_pk4s + ) + except (OSError, ValueError) as exc: + retail_path_collisions = [] + preflight_failures.append(str(exc)) + asset_failures: list[str] = [] + expected_assets_path: Path | None = None + expected_assets_sha256 = "" + directory_views = asset_directory_views(asset_root) + if loose_files: + asset_failures.append( + f"retail fallback asset root contains {len(loose_files)} loose q4base/q4mp files; " + "use a PK4-only asset view" + ) + if args.expected_assets: + expected_assets_path = Path(args.expected_assets).resolve() + expected_assets_sha256 = sha256_file(expected_assets_path) + expected_payload = json.loads(expected_assets_path.read_text(encoding="utf-8")) + if not isinstance(expected_payload, dict): + raise ValueError("expected asset manifest root is not an object") + asset_failures += compare_asset_records(asset_manifest_from_payload(expected_payload), stock_pk4s) + asset_failures += compare_file_records( + loose_manifest_from_payload(expected_payload), loose_files, "loose asset file" + ) + elif not args.dry_run: + asset_failures.append( + "passing capture requires --expected-assets from a separately generated PK4-only manifest" + ) + plans = prepare_plans( + runtime_dir, asset_root, output_dir, args.width, args.height, args.mp_port + ) + + if asset_failures or preflight_failures: + results: list[dict[str, Any]] = [] + elif args.dry_run: + results = [ + {"role": plan.role_id, "mode": plan.mode, "status": "planned", "failures": [], "artifacts": [], "budgetEvidence": {}} + for plan in plans.values() + ] + else: + results = run_capture( + runtime_dir, + executable, + output_dir, + plans, + args.timeout, + args.mp_client_delay, + budget_contract, + ) + + passed = bool(results) and all(result["status"] == "pass" for result in results) + status = ( + "planned" + if args.dry_run and not asset_failures and not preflight_failures + else ("pass" if passed and not asset_failures and not preflight_failures else "fail") + ) + payload = { + "schemaVersion": SCHEMA_VERSION, + "generatedUtc": utc_now(), + "status": status, + "dryRun": args.dry_run, + "git": git_state(source_root), + "budgetContract": budget_binding, + "mpPort": args.mp_port, + "runtimeRoot": str(runtime_dir), + "host": { + "platform": platform.platform(), + "architecture": platform.machine(), + }, + "runtimeFiles": runtime_files, + "expectedAssets": { + "supplied": expected_assets_path is not None, + "path": str(expected_assets_path) if expected_assets_path is not None else "", + "sha256": expected_assets_sha256, + }, + "assets": { + "compatibilityModel": ASSET_COMPATIBILITY_MODEL, + "root": str(asset_root), + "allowLinkedGameDirectories": args.allow_asset_dir_links, + "directoryViews": directory_views, + "stockPk4s": stock_pk4s, + "looseFiles": loose_files, + "openQ4OverlayPk4s": overlay_pk4s, + "openQ4OverlayLooseFiles": overlay_loose_files, + "retailArchiveBytesMatchExpected": ( + expected_assets_path is not None and not asset_failures + ), + "retailPathNamespaceUntouched": not retail_path_collisions, + "retailPathCollisionCount": len(retail_path_collisions), + "retailPathCollisions": retail_path_collisions, + }, + "assetComparisonFailures": asset_failures, + "preflightFailures": preflight_failures, + "postCaptureVerificationFailures": [], + "safety": { + "windowedOnly": True, + "borderless": False, + "windowSize": {"width": args.width, "height": args.height}, + "displayContract": display_contract() if not args.dry_run else None, + "engineScreenshotOnly": True, + "operatingSystemCapture": False, + "inputInjection": False, + }, + "plan": [plan_record(plan) for plan in plans.values()], + "results": results, + } + if status == "pass": + # Re-hash the retail assets, staged runtime/overlays, and every captured + # artifact before declaring success. This catches a launch that writes + # through an incorrectly mounted package path as well as any concurrent + # or stale-file mutation during the run. + post_capture_failures = verify_recorded_files( + payload, + output_dir, + asset_root, + runtime_dir, + budget_contract, + budget_binding, + ) + payload["postCaptureVerificationFailures"] = post_capture_failures + if post_capture_failures: + status = "fail" + payload["status"] = status + report_json, report_md, asset_json, runtime_json = write_reports(output_dir, payload) + print(f"wrote {report_json}") + print(f"wrote {report_md}") + print(f"wrote {asset_json}") + print(f"wrote {runtime_json}") + for failure in asset_failures: + print(f"error: {failure}", file=sys.stderr) + for failure in preflight_failures: + print(f"error: {failure}", file=sys.stderr) + for failure in payload["postCaptureVerificationFailures"]: + print(f"error: {failure}", file=sys.stderr) + if args.dry_run: + return 1 if asset_failures or preflight_failures else 0 + return 0 if status == "pass" else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main(sys.argv[1:])) + except (OSError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + raise SystemExit(1) from None