From 6d0b4fcd2f902c75a489ecc3e99ec93a1496c216 Mon Sep 17 00:00:00 2001 From: Toastconcern Date: Sun, 25 Oct 2026 13:15:07 -0500 Subject: [PATCH] v0.2.0: splats, copy/paste, faster homes, seat facing, and a full bug sweep Frame Studio - Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies - Copy, cut, paste and duplicate for objects, splats and world items - Walkable-area rebake moved off the UI thread; viewport one frame sooner; refresh-locked pacing (FS_PACE=timer for the old one) - Fixed: bundle thumbnail use-after-free crash, auto-save deleting environments, saves into scratch copies, edits lost on non-ASCII paths, unopenable manifests with inf/nan, bundle save data loss, opening a home marking it changed, gizmo NaN, long paths, many malformed-file crashes - Updater verifies sha256, journals the swap and never leaves the exe missing Frame Home - Seats turn you to face their way - Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass, late head-pose read before submit - Proper queue-family hand-over to the compositor layer; layer descriptor generations and seqlocked slots; structural SPIR-V check - Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass validation error, sync hazard in the foveation smoothing pass, unverified update downloads, half-done installs Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9 --- .github/workflows/build.yml | 73 +- .gitignore | 43 +- CHANGELOG.md | 155 + CMakeLists.txt | 57 +- README.md | 3 + VERSION | 2 +- art/icon/frame-glyph-256.png | Bin 0 -> 4417 bytes art/icon/frame-glyph-64.png | Bin 0 -> 1182 bytes art/icon/frame-glyph.svg | 1 + art/icon/frame-studio-1024.png | Bin 0 -> 27895 bytes art/icon/frame-studio-128.png | Bin 0 -> 3058 bytes art/icon/frame-studio-16.png | Bin 0 -> 512 bytes art/icon/frame-studio-24.png | Bin 0 -> 773 bytes art/icon/frame-studio-256.png | Bin 0 -> 6034 bytes art/icon/frame-studio-32.png | Bin 0 -> 988 bytes art/icon/frame-studio-48.png | Bin 0 -> 1470 bytes art/icon/frame-studio-512.png | Bin 0 -> 12250 bytes art/icon/frame-studio-64.png | Bin 0 -> 1701 bytes art/icon/frame-studio-square-1024.png | Bin 0 -> 18257 bytes art/icon/frame-studio-square-512.png | Bin 0 -> 7862 bytes art/icon/frame-studio-square.svg | 1 + art/icon/frame-studio.icns | Bin 0 -> 52502 bytes art/icon/frame-studio.ico | Bin 0 -> 14654 bytes art/icon/frame-studio.svg | 1 + cmake/frame_version.cmake | 32 + docs/studio-guide.md | 2 +- frame/install-frame-home.ps1 | 24 +- frame/install-frame-home.sh | 25 +- frame/payload/install.sh | 30 +- layer/env_ps.frag | 10 +- layer/frame_home_layer.cpp | 668 +- src/clay_spirv.h | 76 + src/dashboard_icon.h | 72 + src/env_export.h | 19 +- src/env_share.h | 120 +- src/fenv.h | 350 +- src/fenv_bundle.h | 6 +- src/fenv_tool.cpp | 53 +- src/gravity.h | 8 +- src/home_edits.h | 31 +- src/home_platform.h | 4 +- src/hsl_renderer.cpp | 451 +- src/hsl_renderer.h | 81 +- src/hsl_renderer_splat.cpp | 266 + src/hsl_scene.cpp | 179 +- src/hsl_scene.h | 19 + src/locomotion.h | 108 +- src/main.cpp | 314 +- src/model_import.cpp | 392 +- src/model_import.h | 7 +- src/scene_bake.cpp | 5 +- src/scene_bake.h | 6 +- src/shader_bake.h | 19 +- src/splat_import.cpp | 428 + src/splat_import.h | 34 + src/splat_sort.h | 139 + src/splat_spirv.h | 265 + src/spv_check.h | 206 + src/spv_opt.cpp | 336 + src/spv_opt.h | 51 + src/spv_specialize.h | 41 +- src/surface_mesh.h | 11 +- src/switcher.cpp | 127 +- src/tex_cache.h | 38 +- src/walkgrid.h | 10 +- src/world_runtime.h | 9 +- src/world_settings.h | 16 +- studio/CMakeLists.txt | 44 +- studio/frame_pacer.h | 119 +- studio/platform_posix.cpp | 35 +- studio/studio.cpp | 2492 +++- studio/studio.manifest | 11 + studio/studio.rc | 2 + studio/updater.h | 454 +- tests_loco/env_share_test.cpp | 247 + tests_loco/loco_test.cpp | 181 +- tests_loco/spv_check_test.cpp | 99 + third_party/cgltf/cgltf.h | 8 +- third_party/qhe/core/audio.h | 9 +- third_party/qhe/core/parallel.h | 14 +- third_party/qhe/core/tinyjson.h | 5 +- third_party/qhe/loaders/rendmesh_parser.h | 28 + third_party/qhe/loaders/rendskel_parser.h | 10 +- third_party/qhe/loaders/rendtxtr_parser.h | 26 + third_party/qhe/loaders/scene_loader.h | 115 +- third_party/smolv/VERSION.txt | 2 + third_party/smolv/smolv.cpp | 25 +- third_party/spirv-headers/LICENSE | 502 + .../include/spirv/1.2/GLSL.std.450.h | 115 + .../include/spirv/unified1/AMD_gcn_shader.h | 35 + .../spirv/unified1/AMD_shader_ballot.h | 36 + .../AMD_shader_explicit_vertex_parameter.h | 33 + .../unified1/AMD_shader_trinary_minmax.h | 41 + .../unified1/ArmExperimentalMLOperations.h | 26 + .../spirv/unified1/ArmMotionEngine.100.h | 32 + .../include/spirv/unified1/DebugInfo.h | 128 + .../include/spirv/unified1/GLSL.std.450.h | 115 + .../unified1/NonSemanticClspvReflection.h | 81 + .../spirv/unified1/NonSemanticDebugBreak.h | 33 + .../spirv/unified1/NonSemanticDebugPrintf.h | 33 + .../unified1/NonSemanticGraphDebugInfo.h | 28 + .../unified1/NonSemanticShaderDebugInfo.h | 157 + .../unified1/NonSemanticShaderDebugInfo100.h | 159 + .../unified1/NonSemanticVkspReflection.h | 40 + .../include/spirv/unified1/OpenCL.std.h | 385 + .../spirv/unified1/OpenCLDebugInfo100.h | 142 + .../include/spirv/unified1/TOSA.001000.1.h | 95 + .../include/spirv/unified1/spirv.h | 5717 ++++++++ .../include/spirv/unified1/spirv.hpp | 5782 ++++++++ .../include/spirv/unified1/spirv.hpp11 | 5782 ++++++++ third_party/spirv-tools/CMakeLists.txt | 26 + third_party/spirv-tools/LICENSE | 202 + third_party/spirv-tools/generated/DebugInfo.h | 122 + .../generated/OpenCLDebugInfo100.h | 136 + .../spirv-tools/generated/build-version.inc | 1 + .../generated/core_tables_body.inc | 11099 ++++++++++++++++ .../generated/core_tables_header.inc | 237 + .../spirv-tools/generated/generators.inc | 52 + .../include/spirv-tools/libspirv.h | 1104 ++ .../include/spirv-tools/libspirv.hpp | 432 + .../include/spirv-tools/linker.hpp | 128 + .../include/spirv-tools/linter.hpp | 48 + .../include/spirv-tools/optimizer.hpp | 1044 ++ .../spirv-tools/source/assembly_grammar.cpp | 241 + .../spirv-tools/source/assembly_grammar.h | 99 + third_party/spirv-tools/source/binary.cpp | 988 ++ third_party/spirv-tools/source/binary.h | 51 + third_party/spirv-tools/source/cfa.h | 398 + .../spirv-tools/source/common_debug_info.h | 64 + third_party/spirv-tools/source/diagnostic.cpp | 195 + third_party/spirv-tools/source/diagnostic.h | 82 + .../spirv-tools/source/disassemble.cpp | 1148 ++ third_party/spirv-tools/source/disassemble.h | 129 + third_party/spirv-tools/source/enum_set.h | 472 + third_party/spirv-tools/source/ext_inst.cpp | 102 + third_party/spirv-tools/source/ext_inst.h | 30 + third_party/spirv-tools/source/extensions.cpp | 55 + third_party/spirv-tools/source/extensions.h | 35 + third_party/spirv-tools/source/instruction.h | 49 + .../latest_version_glsl_std_450_header.h | 20 + .../source/latest_version_opencl_std_header.h | 20 + .../source/latest_version_spirv_header.h | 20 + third_party/spirv-tools/source/libspirv.cpp | 171 + third_party/spirv-tools/source/macro.h | 25 + .../spirv-tools/source/name_mapper.cpp | 379 + third_party/spirv-tools/source/name_mapper.h | 125 + third_party/spirv-tools/source/opcode.cpp | 734 + third_party/spirv-tools/source/opcode.h | 152 + third_party/spirv-tools/source/operand.cpp | 671 + third_party/spirv-tools/source/operand.h | 128 + .../opt/aggressive_dead_code_elim_pass.cpp | 1287 ++ .../opt/aggressive_dead_code_elim_pass.h | 279 + .../spirv-tools/source/opt/amd_ext_to_khr.cpp | 1116 ++ .../spirv-tools/source/opt/amd_ext_to_khr.h | 51 + .../source/opt/analyze_live_input_pass.cpp | 45 + .../source/opt/analyze_live_input_pass.h | 57 + .../spirv-tools/source/opt/basic_block.cpp | 292 + .../spirv-tools/source/opt/basic_block.h | 342 + .../source/opt/block_merge_pass.cpp | 51 + .../spirv-tools/source/opt/block_merge_pass.h | 62 + .../source/opt/block_merge_util.cpp | 233 + .../spirv-tools/source/opt/block_merge_util.h | 44 + .../spirv-tools/source/opt/build_module.cpp | 88 + .../spirv-tools/source/opt/build_module.h | 54 + .../source/opt/canonicalize_ids_pass.cpp | 532 + .../source/opt/canonicalize_ids_pass.h | 115 + .../spirv-tools/source/opt/ccp_pass.cpp | 388 + third_party/spirv-tools/source/opt/ccp_pass.h | 133 + third_party/spirv-tools/source/opt/cfg.cpp | 363 + third_party/spirv-tools/source/opt/cfg.h | 189 + .../source/opt/cfg_cleanup_pass.cpp | 44 + .../spirv-tools/source/opt/cfg_cleanup_pass.h | 41 + .../spirv-tools/source/opt/code_sink.cpp | 326 + .../spirv-tools/source/opt/code_sink.h | 107 + .../source/opt/combine_access_chains.cpp | 314 + .../source/opt/combine_access_chains.h | 83 + .../source/opt/compact_ids_pass.cpp | 106 + .../spirv-tools/source/opt/compact_ids_pass.h | 42 + .../spirv-tools/source/opt/composite.cpp | 52 + .../spirv-tools/source/opt/composite.h | 51 + .../source/opt/const_folding_rules.cpp | 2332 ++++ .../source/opt/const_folding_rules.h | 136 + .../spirv-tools/source/opt/constants.cpp | 774 ++ .../spirv-tools/source/opt/constants.h | 774 ++ .../source/opt/control_dependence.cpp | 153 + .../source/opt/control_dependence.h | 197 + .../source/opt/convert_to_half_pass.cpp | 648 + .../source/opt/convert_to_half_pass.h | 164 + .../opt/convert_to_sampled_image_pass.cpp | 447 + .../opt/convert_to_sampled_image_pass.h | 207 + .../source/opt/convert_to_untyped.cpp | 779 ++ .../source/opt/convert_to_untyped.h | 108 + .../source/opt/copy_prop_arrays.cpp | 992 ++ .../spirv-tools/source/opt/copy_prop_arrays.h | 276 + .../spirv-tools/source/opt/dataflow.cpp | 90 + third_party/spirv-tools/source/opt/dataflow.h | 148 + .../source/opt/dead_branch_elim_pass.cpp | 678 + .../source/opt/dead_branch_elim_pass.h | 176 + .../source/opt/dead_insert_elim_pass.cpp | 262 + .../source/opt/dead_insert_elim_pass.h | 90 + .../source/opt/dead_variable_elimination.cpp | 112 + .../source/opt/dead_variable_elimination.h | 56 + .../source/opt/debug_info_manager.cpp | 966 ++ .../source/opt/debug_info_manager.h | 284 + .../source/opt/decoration_manager.cpp | 645 + .../source/opt/decoration_manager.h | 212 + .../source/opt/def_use_manager.cpp | 313 + .../spirv-tools/source/opt/def_use_manager.h | 230 + .../spirv-tools/source/opt/desc_sroa.cpp | 480 + .../spirv-tools/source/opt/desc_sroa.h | 159 + .../spirv-tools/source/opt/desc_sroa_util.cpp | 133 + .../spirv-tools/source/opt/desc_sroa_util.h | 58 + .../source/opt/dominator_analysis.cpp | 81 + .../source/opt/dominator_analysis.h | 143 + .../spirv-tools/source/opt/dominator_tree.cpp | 404 + .../spirv-tools/source/opt/dominator_tree.h | 308 + .../opt/eliminate_dead_constant_pass.cpp | 103 + .../source/opt/eliminate_dead_constant_pass.h | 35 + .../opt/eliminate_dead_functions_pass.cpp | 55 + .../opt/eliminate_dead_functions_pass.h | 44 + .../opt/eliminate_dead_functions_util.cpp | 68 + .../opt/eliminate_dead_functions_util.h | 36 + .../opt/eliminate_dead_io_components_pass.cpp | 295 + .../opt/eliminate_dead_io_components_pass.h | 75 + .../opt/eliminate_dead_members_pass.cpp | 727 + .../source/opt/eliminate_dead_members_pass.h | 146 + .../opt/eliminate_dead_output_stores_pass.cpp | 240 + .../opt/eliminate_dead_output_stores_pass.h | 81 + .../spirv-tools/source/opt/empty_pass.h | 36 + .../source/opt/feature_manager.cpp | 132 + .../spirv-tools/source/opt/feature_manager.h | 113 + .../source/opt/fix_func_call_arguments.cpp | 119 + .../source/opt/fix_func_call_arguments.h | 47 + .../source/opt/fix_storage_class.cpp | 377 + .../source/opt/fix_storage_class.h | 93 + .../source/opt/flatten_decoration_pass.cpp | 165 + .../source/opt/flatten_decoration_pass.h | 35 + third_party/spirv-tools/source/opt/fold.cpp | 710 + third_party/spirv-tools/source/opt/fold.h | 195 + ...ld_spec_constant_op_and_composite_pass.cpp | 357 + ...fold_spec_constant_op_and_composite_pass.h | 89 + .../spirv-tools/source/opt/folding_rules.cpp | 4729 +++++++ .../spirv-tools/source/opt/folding_rules.h | 124 + .../opt/freeze_spec_constant_value_pass.cpp | 57 + .../opt/freeze_spec_constant_value_pass.h | 35 + .../spirv-tools/source/opt/function.cpp | 284 + third_party/spirv-tools/source/opt/function.h | 328 + third_party/spirv-tools/source/opt/graph.cpp | 156 + third_party/spirv-tools/source/opt/graph.h | 147 + .../opt/graphics_robust_access_pass.cpp | 1156 ++ .../source/opt/graphics_robust_access_pass.h | 156 + .../spirv-tools/source/opt/if_conversion.cpp | 299 + .../spirv-tools/source/opt/if_conversion.h | 89 + .../source/opt/inline_exhaustive_pass.cpp | 90 + .../source/opt/inline_exhaustive_pass.h | 53 + .../source/opt/inline_opaque_pass.cpp | 132 + .../source/opt/inline_opaque_pass.h | 60 + .../spirv-tools/source/opt/inline_pass.cpp | 951 ++ .../spirv-tools/source/opt/inline_pass.h | 260 + .../spirv-tools/source/opt/instruction.cpp | 1124 ++ .../spirv-tools/source/opt/instruction.h | 924 ++ .../source/opt/instruction_list.cpp | 36 + .../spirv-tools/source/opt/instruction_list.h | 177 + .../source/opt/interface_var_sroa.cpp | 1031 ++ .../source/opt/interface_var_sroa.h | 402 + .../source/opt/interp_fixup_pass.cpp | 122 + .../source/opt/interp_fixup_pass.h | 54 + .../invocation_interlock_placement_pass.cpp | 522 + .../opt/invocation_interlock_placement_pass.h | 158 + .../spirv-tools/source/opt/ir_builder.h | 765 ++ .../spirv-tools/source/opt/ir_context.cpp | 1272 ++ .../spirv-tools/source/opt/ir_context.h | 1352 ++ .../spirv-tools/source/opt/ir_loader.cpp | 415 + .../spirv-tools/source/opt/ir_loader.h | 104 + third_party/spirv-tools/source/opt/iterator.h | 350 + .../opt/legalize_multidim_array_pass.cpp | 275 + .../source/opt/legalize_multidim_array_pass.h | 55 + .../spirv-tools/source/opt/licm_pass.cpp | 138 + .../spirv-tools/source/opt/licm_pass.h | 72 + .../spirv-tools/source/opt/liveness.cpp | 352 + third_party/spirv-tools/source/opt/liveness.h | 98 + .../opt/local_access_chain_convert_pass.cpp | 521 + .../opt/local_access_chain_convert_pass.h | 145 + .../opt/local_redundancy_elimination.cpp | 67 + .../source/opt/local_redundancy_elimination.h | 68 + .../opt/local_single_block_elim_pass.cpp | 321 + .../source/opt/local_single_block_elim_pass.h | 107 + .../opt/local_single_store_elim_pass.cpp | 335 + .../source/opt/local_single_store_elim_pass.h | 108 + third_party/spirv-tools/source/opt/log.h | 222 + .../source/opt/loop_dependence.cpp | 1674 +++ .../spirv-tools/source/opt/loop_dependence.h | 560 + .../source/opt/loop_dependence_helpers.cpp | 539 + .../source/opt/loop_descriptor.cpp | 1029 ++ .../spirv-tools/source/opt/loop_descriptor.h | 583 + .../spirv-tools/source/opt/loop_fission.cpp | 522 + .../spirv-tools/source/opt/loop_fission.h | 78 + .../spirv-tools/source/opt/loop_fusion.cpp | 733 + .../spirv-tools/source/opt/loop_fusion.h | 114 + .../source/opt/loop_fusion_pass.cpp | 72 + .../spirv-tools/source/opt/loop_fusion_pass.h | 51 + .../spirv-tools/source/opt/loop_peeling.cpp | 1178 ++ .../spirv-tools/source/opt/loop_peeling.h | 340 + .../spirv-tools/source/opt/loop_unroller.cpp | 1223 ++ .../spirv-tools/source/opt/loop_unroller.h | 49 + .../source/opt/loop_unswitch_pass.cpp | 644 + .../source/opt/loop_unswitch_pass.h | 44 + .../spirv-tools/source/opt/loop_utils.cpp | 738 + .../spirv-tools/source/opt/loop_utils.h | 184 + .../spirv-tools/source/opt/mem_pass.cpp | 532 + third_party/spirv-tools/source/opt/mem_pass.h | 164 + .../source/opt/merge_return_pass.cpp | 1016 ++ .../source/opt/merge_return_pass.h | 334 + .../opt/modify_maximal_reconvergence.cpp | 103 + .../source/opt/modify_maximal_reconvergence.h | 53 + third_party/spirv-tools/source/opt/module.cpp | 339 + third_party/spirv-tools/source/opt/module.h | 616 + .../spirv-tools/source/opt/null_pass.h | 34 + .../opt/opextinst_forward_ref_fixup_pass.cpp | 112 + .../opt/opextinst_forward_ref_fixup_pass.h | 48 + .../spirv-tools/source/opt/optimizer.cpp | 1327 ++ third_party/spirv-tools/source/opt/pass.cpp | 173 + third_party/spirv-tools/source/opt/pass.h | 169 + .../spirv-tools/source/opt/pass_manager.cpp | 93 + .../spirv-tools/source/opt/pass_manager.h | 158 + third_party/spirv-tools/source/opt/passes.h | 99 + .../source/opt/private_to_local_pass.cpp | 238 + .../source/opt/private_to_local_pass.h | 73 + .../spirv-tools/source/opt/propagator.cpp | 316 + .../spirv-tools/source/opt/propagator.h | 320 + .../source/opt/reduce_load_size.cpp | 189 + .../spirv-tools/source/opt/reduce_load_size.h | 73 + .../source/opt/redundancy_elimination.cpp | 62 + .../source/opt/redundancy_elimination.h | 55 + third_party/spirv-tools/source/opt/reflect.h | 64 + .../source/opt/register_pressure.cpp | 582 + .../source/opt/register_pressure.h | 196 + .../source/opt/relax_float_ops_pass.cpp | 180 + .../source/opt/relax_float_ops_pass.h | 86 + .../source/opt/remove_dontinline_pass.cpp | 50 + .../source/opt/remove_dontinline_pass.h | 42 + .../source/opt/remove_duplicates_pass.cpp | 257 + .../source/opt/remove_duplicates_pass.h | 65 + ...remove_unused_interface_variables_pass.cpp | 105 + .../remove_unused_interface_variables_pass.h | 31 + ...lace_desc_array_access_using_var_index.cpp | 454 + ...eplace_desc_array_access_using_var_index.h | 205 + .../source/opt/replace_invalid_opc.cpp | 221 + .../source/opt/replace_invalid_opc.h | 67 + .../opt/resolve_binding_conflicts_pass.cpp | 328 + .../opt/resolve_binding_conflicts_pass.h | 38 + .../source/opt/scalar_analysis.cpp | 987 ++ .../spirv-tools/source/opt/scalar_analysis.h | 314 + .../source/opt/scalar_analysis_nodes.h | 347 + .../opt/scalar_analysis_simplification.cpp | 538 + .../source/opt/scalar_replacement_pass.cpp | 1023 ++ .../source/opt/scalar_replacement_pass.h | 296 + .../set_spec_constant_default_value_pass.cpp | 419 + .../set_spec_constant_default_value_pass.h | 114 + .../source/opt/simplification_pass.cpp | 168 + .../source/opt/simplification_pass.h | 58 + .../opt/split_combined_image_sampler_pass.cpp | 694 + .../opt/split_combined_image_sampler_pass.h | 169 + .../source/opt/spread_volatile_semantics.cpp | 300 + .../source/opt/spread_volatile_semantics.h | 117 + .../source/opt/ssa_rewrite_pass.cpp | 711 + .../spirv-tools/source/opt/ssa_rewrite_pass.h | 306 + .../source/opt/strength_reduction_pass.cpp | 198 + .../source/opt/strength_reduction_pass.h | 65 + .../source/opt/strip_debug_info_pass.cpp | 117 + .../source/opt/strip_debug_info_pass.h | 35 + .../opt/strip_nonsemantic_info_pass.cpp | 118 + .../source/opt/strip_nonsemantic_info_pass.h | 44 + .../source/opt/struct_cfg_analysis.cpp | 262 + .../source/opt/struct_cfg_analysis.h | 171 + .../source/opt/struct_packing_pass.cpp | 482 + .../source/opt/struct_packing_pass.h | 81 + .../source/opt/switch_descriptorset_pass.cpp | 46 + .../source/opt/switch_descriptorset_pass.h | 52 + .../spirv-tools/source/opt/tree_iterator.h | 246 + .../source/opt/trim_capabilities_pass.cpp | 772 ++ .../source/opt/trim_capabilities_pass.h | 215 + .../spirv-tools/source/opt/type_manager.cpp | 1343 ++ .../spirv-tools/source/opt/type_manager.h | 298 + third_party/spirv-tools/source/opt/types.cpp | 1118 ++ third_party/spirv-tools/source/opt/types.h | 912 ++ .../source/opt/unify_const_pass.cpp | 175 + .../spirv-tools/source/opt/unify_const_pass.h | 35 + .../source/opt/upgrade_memory_model.cpp | 828 ++ .../source/opt/upgrade_memory_model.h | 150 + .../source/opt/value_number_table.cpp | 299 + .../source/opt/value_number_table.h | 103 + .../spirv-tools/source/opt/vector_dce.cpp | 431 + .../spirv-tools/source/opt/vector_dce.h | 160 + .../spirv-tools/source/opt/workaround1209.cpp | 69 + .../spirv-tools/source/opt/workaround1209.h | 41 + .../spirv-tools/source/opt/wrap_opkill.cpp | 201 + .../spirv-tools/source/opt/wrap_opkill.h | 80 + .../spirv-tools/source/parsed_operand.cpp | 123 + .../spirv-tools/source/parsed_operand.h | 33 + third_party/spirv-tools/source/print.cpp | 108 + third_party/spirv-tools/source/print.h | 75 + .../spirv-tools/source/software_version.cpp | 27 + .../spirv-tools/source/spirv_constant.h | 101 + .../spirv-tools/source/spirv_definition.h | 33 + .../spirv-tools/source/spirv_endian.cpp | 77 + third_party/spirv-tools/source/spirv_endian.h | 37 + .../source/spirv_optimizer_options.cpp | 51 + .../source/spirv_optimizer_options.h | 49 + .../spirv-tools/source/spirv_target_env.cpp | 511 + .../spirv-tools/source/spirv_target_env.h | 56 + .../source/spirv_validator_options.cpp | 169 + .../source/spirv_validator_options.h | 81 + third_party/spirv-tools/source/table.cpp | 60 + third_party/spirv-tools/source/table.h | 38 + third_party/spirv-tools/source/table2.cpp | 440 + third_party/spirv-tools/source/table2.h | 263 + third_party/spirv-tools/source/text.cpp | 1015 ++ third_party/spirv-tools/source/text.h | 53 + .../spirv-tools/source/text_handler.cpp | 455 + third_party/spirv-tools/source/text_handler.h | 272 + third_party/spirv-tools/source/to_string.cpp | 44 + third_party/spirv-tools/source/to_string.h | 29 + .../spirv-tools/source/util/bit_vector.cpp | 92 + .../spirv-tools/source/util/bit_vector.h | 123 + .../spirv-tools/source/util/bitutils.h | 229 + .../spirv-tools/source/util/hash_combine.h | 60 + .../spirv-tools/source/util/hex_float.h | 2259 ++++ third_party/spirv-tools/source/util/ilist.h | 366 + .../spirv-tools/source/util/ilist_node.h | 265 + .../spirv-tools/source/util/index_range.h | 71 + .../spirv-tools/source/util/make_unique.h | 30 + .../spirv-tools/source/util/parse_number.cpp | 326 + .../spirv-tools/source/util/parse_number.h | 274 + .../spirv-tools/source/util/small_vector.h | 491 + third_party/spirv-tools/source/util/span.h | 72 + third_party/spirv-tools/source/util/status.h | 31 + .../spirv-tools/source/util/string_utils.cpp | 58 + .../spirv-tools/source/util/string_utils.h | 133 + third_party/spirv-tools/source/util/timer.cpp | 102 + third_party/spirv-tools/source/util/timer.h | 401 + .../spirv-tools/source/val/basic_block.cpp | 187 + .../spirv-tools/source/val/basic_block.h | 320 + .../spirv-tools/source/val/construct.cpp | 222 + .../spirv-tools/source/val/construct.h | 170 + .../spirv-tools/source/val/decoration.h | 109 + .../spirv-tools/source/val/function.cpp | 435 + third_party/spirv-tools/source/val/function.h | 403 + .../spirv-tools/source/val/instruction.cpp | 55 + .../spirv-tools/source/val/instruction.h | 155 + .../spirv-tools/source/val/validate.cpp | 519 + third_party/spirv-tools/source/val/validate.h | 305 + .../source/val/validate_adjacency.cpp | 139 + .../source/val/validate_annotation.cpp | 760 ++ .../source/val/validate_arithmetics.cpp | 939 ++ .../source/val/validate_atomics.cpp | 441 + .../source/val/validate_barriers.cpp | 158 + .../source/val/validate_bitwise.cpp | 276 + .../source/val/validate_builtins.cpp | 5340 ++++++++ .../source/val/validate_capability.cpp | 474 + .../spirv-tools/source/val/validate_cfg.cpp | 1351 ++ .../source/val/validate_composites.cpp | 1198 ++ .../source/val/validate_constants.cpp | 811 ++ .../source/val/validate_conversion.cpp | 1189 ++ .../spirv-tools/source/val/validate_debug.cpp | 72 + .../source/val/validate_decorations.cpp | 1340 ++ .../source/val/validate_derivatives.cpp | 124 + .../source/val/validate_dot_product.cpp | 407 + .../val/validate_execution_limitations.cpp | 69 + .../source/val/validate_explicit_layout.cpp | 1168 ++ .../source/val/validate_extensions.cpp | 4514 +++++++ .../source/val/validate_function.cpp | 461 + .../spirv-tools/source/val/validate_graph.cpp | 585 + .../spirv-tools/source/val/validate_group.cpp | 229 + .../spirv-tools/source/val/validate_id.cpp | 306 + .../spirv-tools/source/val/validate_image.cpp | 2886 ++++ .../source/val/validate_instruction.cpp | 543 + .../source/val/validate_interfaces.cpp | 800 ++ .../source/val/validate_invalid_type.cpp | 260 + .../source/val/validate_layout.cpp | 468 + .../source/val/validate_literals.cpp | 96 + .../source/val/validate_logical_pointers.cpp | 1026 ++ .../source/val/validate_logicals.cpp | 350 + .../source/val/validate_memory.cpp | 3919 ++++++ .../source/val/validate_memory_semantics.cpp | 325 + .../source/val/validate_memory_semantics.h | 29 + .../source/val/validate_mesh_shading.cpp | 238 + .../spirv-tools/source/val/validate_misc.cpp | 279 + .../source/val/validate_mode_setting.cpp | 1163 ++ .../source/val/validate_non_uniform.cpp | 496 + .../spirv-tools/source/val/validate_pipe.cpp | 353 + .../source/val/validate_primitives.cpp | 73 + .../source/val/validate_ray_query.cpp | 393 + .../source/val/validate_ray_tracing.cpp | 221 + .../val/validate_ray_tracing_reorder.cpp | 1410 ++ .../source/val/validate_scopes.cpp | 340 + .../spirv-tools/source/val/validate_scopes.h | 33 + .../source/val/validate_small_type_uses.cpp | 57 + .../source/val/validate_tensor.cpp | 250 + .../source/val/validate_tensor_layout.cpp | 183 + .../spirv-tools/source/val/validate_type.cpp | 1174 ++ .../source/val/validation_state.cpp | 3651 +++++ .../spirv-tools/source/val/validation_state.h | 1256 ++ third_party/stb/stb_image_impl.c | 6 + third_party/ufbx/ufbx.c | 2 +- third_party/zstd/LICENSE | 30 + third_party/zstd/common/allocations.h | 55 + third_party/zstd/common/bits.h | 200 + third_party/zstd/common/bitstream.h | 457 + third_party/zstd/common/compiler.h | 450 + third_party/zstd/common/cpu.h | 249 + third_party/zstd/common/debug.c | 30 + third_party/zstd/common/debug.h | 116 + third_party/zstd/common/entropy_common.c | 340 + third_party/zstd/common/error_private.c | 63 + third_party/zstd/common/error_private.h | 168 + third_party/zstd/common/fse.h | 640 + third_party/zstd/common/fse_decompress.c | 313 + third_party/zstd/common/huf.h | 286 + third_party/zstd/common/mem.h | 426 + third_party/zstd/common/pool.h | 90 + third_party/zstd/common/portability_macros.h | 158 + third_party/zstd/common/threading.h | 150 + third_party/zstd/common/xxhash.c | 18 + third_party/zstd/common/xxhash.h | 7020 ++++++++++ third_party/zstd/common/zstd_common.c | 48 + third_party/zstd/common/zstd_deps.h | 111 + third_party/zstd/common/zstd_internal.h | 392 + third_party/zstd/common/zstd_trace.h | 163 + third_party/zstd/decompress/huf_decompress.c | 1944 +++ third_party/zstd/decompress/zstd_ddict.c | 244 + third_party/zstd/decompress/zstd_ddict.h | 44 + third_party/zstd/decompress/zstd_decompress.c | 2407 ++++ .../zstd/decompress/zstd_decompress_block.c | 2215 +++ .../zstd/decompress/zstd_decompress_block.h | 73 + .../decompress/zstd_decompress_internal.h | 240 + third_party/zstd/zstd.h | 3089 +++++ third_party/zstd/zstd_errors.h | 114 + tools/gen_clay_shader.py | 15 +- tools/gen_spv_ops.py | 24 + tools/splat/gen_splat_spirv.sh | 29 + tools/splat/splat.frag | 13 + tools/splat/splat.vert | 62 + tools/walktest/CMakeLists.txt | 12 +- 544 files changed, 210660 insertions(+), 796 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 art/icon/frame-glyph-256.png create mode 100644 art/icon/frame-glyph-64.png create mode 100644 art/icon/frame-glyph.svg create mode 100644 art/icon/frame-studio-1024.png create mode 100644 art/icon/frame-studio-128.png create mode 100644 art/icon/frame-studio-16.png create mode 100644 art/icon/frame-studio-24.png create mode 100644 art/icon/frame-studio-256.png create mode 100644 art/icon/frame-studio-32.png create mode 100644 art/icon/frame-studio-48.png create mode 100644 art/icon/frame-studio-512.png create mode 100644 art/icon/frame-studio-64.png create mode 100644 art/icon/frame-studio-square-1024.png create mode 100644 art/icon/frame-studio-square-512.png create mode 100644 art/icon/frame-studio-square.svg create mode 100644 art/icon/frame-studio.icns create mode 100644 art/icon/frame-studio.ico create mode 100644 art/icon/frame-studio.svg create mode 100644 cmake/frame_version.cmake create mode 100644 src/dashboard_icon.h create mode 100644 src/hsl_renderer_splat.cpp create mode 100644 src/splat_import.cpp create mode 100644 src/splat_import.h create mode 100644 src/splat_sort.h create mode 100644 src/splat_spirv.h create mode 100644 src/spv_check.h create mode 100644 src/spv_opt.cpp create mode 100644 src/spv_opt.h create mode 100644 studio/studio.manifest create mode 100644 studio/studio.rc create mode 100644 tests_loco/env_share_test.cpp create mode 100644 tests_loco/spv_check_test.cpp create mode 100644 third_party/spirv-headers/LICENSE create mode 100644 third_party/spirv-headers/include/spirv/1.2/GLSL.std.450.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/AMD_gcn_shader.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/AMD_shader_ballot.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/AMD_shader_explicit_vertex_parameter.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/AMD_shader_trinary_minmax.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/ArmExperimentalMLOperations.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/ArmMotionEngine.100.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/DebugInfo.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/GLSL.std.450.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticClspvReflection.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticDebugBreak.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticDebugPrintf.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticGraphDebugInfo.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticShaderDebugInfo.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticShaderDebugInfo100.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/NonSemanticVkspReflection.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/OpenCL.std.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/OpenCLDebugInfo100.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/TOSA.001000.1.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/spirv.h create mode 100644 third_party/spirv-headers/include/spirv/unified1/spirv.hpp create mode 100644 third_party/spirv-headers/include/spirv/unified1/spirv.hpp11 create mode 100644 third_party/spirv-tools/CMakeLists.txt create mode 100644 third_party/spirv-tools/LICENSE create mode 100644 third_party/spirv-tools/generated/DebugInfo.h create mode 100644 third_party/spirv-tools/generated/OpenCLDebugInfo100.h create mode 100644 third_party/spirv-tools/generated/build-version.inc create mode 100644 third_party/spirv-tools/generated/core_tables_body.inc create mode 100644 third_party/spirv-tools/generated/core_tables_header.inc create mode 100644 third_party/spirv-tools/generated/generators.inc create mode 100644 third_party/spirv-tools/include/spirv-tools/libspirv.h create mode 100644 third_party/spirv-tools/include/spirv-tools/libspirv.hpp create mode 100644 third_party/spirv-tools/include/spirv-tools/linker.hpp create mode 100644 third_party/spirv-tools/include/spirv-tools/linter.hpp create mode 100644 third_party/spirv-tools/include/spirv-tools/optimizer.hpp create mode 100644 third_party/spirv-tools/source/assembly_grammar.cpp create mode 100644 third_party/spirv-tools/source/assembly_grammar.h create mode 100644 third_party/spirv-tools/source/binary.cpp create mode 100644 third_party/spirv-tools/source/binary.h create mode 100644 third_party/spirv-tools/source/cfa.h create mode 100644 third_party/spirv-tools/source/common_debug_info.h create mode 100644 third_party/spirv-tools/source/diagnostic.cpp create mode 100644 third_party/spirv-tools/source/diagnostic.h create mode 100644 third_party/spirv-tools/source/disassemble.cpp create mode 100644 third_party/spirv-tools/source/disassemble.h create mode 100644 third_party/spirv-tools/source/enum_set.h create mode 100644 third_party/spirv-tools/source/ext_inst.cpp create mode 100644 third_party/spirv-tools/source/ext_inst.h create mode 100644 third_party/spirv-tools/source/extensions.cpp create mode 100644 third_party/spirv-tools/source/extensions.h create mode 100644 third_party/spirv-tools/source/instruction.h create mode 100644 third_party/spirv-tools/source/latest_version_glsl_std_450_header.h create mode 100644 third_party/spirv-tools/source/latest_version_opencl_std_header.h create mode 100644 third_party/spirv-tools/source/latest_version_spirv_header.h create mode 100644 third_party/spirv-tools/source/libspirv.cpp create mode 100644 third_party/spirv-tools/source/macro.h create mode 100644 third_party/spirv-tools/source/name_mapper.cpp create mode 100644 third_party/spirv-tools/source/name_mapper.h create mode 100644 third_party/spirv-tools/source/opcode.cpp create mode 100644 third_party/spirv-tools/source/opcode.h create mode 100644 third_party/spirv-tools/source/operand.cpp create mode 100644 third_party/spirv-tools/source/operand.h create mode 100644 third_party/spirv-tools/source/opt/aggressive_dead_code_elim_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/aggressive_dead_code_elim_pass.h create mode 100644 third_party/spirv-tools/source/opt/amd_ext_to_khr.cpp create mode 100644 third_party/spirv-tools/source/opt/amd_ext_to_khr.h create mode 100644 third_party/spirv-tools/source/opt/analyze_live_input_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/analyze_live_input_pass.h create mode 100644 third_party/spirv-tools/source/opt/basic_block.cpp create mode 100644 third_party/spirv-tools/source/opt/basic_block.h create mode 100644 third_party/spirv-tools/source/opt/block_merge_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/block_merge_pass.h create mode 100644 third_party/spirv-tools/source/opt/block_merge_util.cpp create mode 100644 third_party/spirv-tools/source/opt/block_merge_util.h create mode 100644 third_party/spirv-tools/source/opt/build_module.cpp create mode 100644 third_party/spirv-tools/source/opt/build_module.h create mode 100644 third_party/spirv-tools/source/opt/canonicalize_ids_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/canonicalize_ids_pass.h create mode 100644 third_party/spirv-tools/source/opt/ccp_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/ccp_pass.h create mode 100644 third_party/spirv-tools/source/opt/cfg.cpp create mode 100644 third_party/spirv-tools/source/opt/cfg.h create mode 100644 third_party/spirv-tools/source/opt/cfg_cleanup_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/cfg_cleanup_pass.h create mode 100644 third_party/spirv-tools/source/opt/code_sink.cpp create mode 100644 third_party/spirv-tools/source/opt/code_sink.h create mode 100644 third_party/spirv-tools/source/opt/combine_access_chains.cpp create mode 100644 third_party/spirv-tools/source/opt/combine_access_chains.h create mode 100644 third_party/spirv-tools/source/opt/compact_ids_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/compact_ids_pass.h create mode 100644 third_party/spirv-tools/source/opt/composite.cpp create mode 100644 third_party/spirv-tools/source/opt/composite.h create mode 100644 third_party/spirv-tools/source/opt/const_folding_rules.cpp create mode 100644 third_party/spirv-tools/source/opt/const_folding_rules.h create mode 100644 third_party/spirv-tools/source/opt/constants.cpp create mode 100644 third_party/spirv-tools/source/opt/constants.h create mode 100644 third_party/spirv-tools/source/opt/control_dependence.cpp create mode 100644 third_party/spirv-tools/source/opt/control_dependence.h create mode 100644 third_party/spirv-tools/source/opt/convert_to_half_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/convert_to_half_pass.h create mode 100644 third_party/spirv-tools/source/opt/convert_to_sampled_image_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/convert_to_sampled_image_pass.h create mode 100644 third_party/spirv-tools/source/opt/convert_to_untyped.cpp create mode 100644 third_party/spirv-tools/source/opt/convert_to_untyped.h create mode 100644 third_party/spirv-tools/source/opt/copy_prop_arrays.cpp create mode 100644 third_party/spirv-tools/source/opt/copy_prop_arrays.h create mode 100644 third_party/spirv-tools/source/opt/dataflow.cpp create mode 100644 third_party/spirv-tools/source/opt/dataflow.h create mode 100644 third_party/spirv-tools/source/opt/dead_branch_elim_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/dead_branch_elim_pass.h create mode 100644 third_party/spirv-tools/source/opt/dead_insert_elim_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/dead_insert_elim_pass.h create mode 100644 third_party/spirv-tools/source/opt/dead_variable_elimination.cpp create mode 100644 third_party/spirv-tools/source/opt/dead_variable_elimination.h create mode 100644 third_party/spirv-tools/source/opt/debug_info_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/debug_info_manager.h create mode 100644 third_party/spirv-tools/source/opt/decoration_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/decoration_manager.h create mode 100644 third_party/spirv-tools/source/opt/def_use_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/def_use_manager.h create mode 100644 third_party/spirv-tools/source/opt/desc_sroa.cpp create mode 100644 third_party/spirv-tools/source/opt/desc_sroa.h create mode 100644 third_party/spirv-tools/source/opt/desc_sroa_util.cpp create mode 100644 third_party/spirv-tools/source/opt/desc_sroa_util.h create mode 100644 third_party/spirv-tools/source/opt/dominator_analysis.cpp create mode 100644 third_party/spirv-tools/source/opt/dominator_analysis.h create mode 100644 third_party/spirv-tools/source/opt/dominator_tree.cpp create mode 100644 third_party/spirv-tools/source/opt/dominator_tree.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_constant_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_constant_pass.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_functions_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_functions_pass.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_functions_util.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_functions_util.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_io_components_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_io_components_pass.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_members_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_members_pass.h create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_output_stores_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/eliminate_dead_output_stores_pass.h create mode 100644 third_party/spirv-tools/source/opt/empty_pass.h create mode 100644 third_party/spirv-tools/source/opt/feature_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/feature_manager.h create mode 100644 third_party/spirv-tools/source/opt/fix_func_call_arguments.cpp create mode 100644 third_party/spirv-tools/source/opt/fix_func_call_arguments.h create mode 100644 third_party/spirv-tools/source/opt/fix_storage_class.cpp create mode 100644 third_party/spirv-tools/source/opt/fix_storage_class.h create mode 100644 third_party/spirv-tools/source/opt/flatten_decoration_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/flatten_decoration_pass.h create mode 100644 third_party/spirv-tools/source/opt/fold.cpp create mode 100644 third_party/spirv-tools/source/opt/fold.h create mode 100644 third_party/spirv-tools/source/opt/fold_spec_constant_op_and_composite_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/fold_spec_constant_op_and_composite_pass.h create mode 100644 third_party/spirv-tools/source/opt/folding_rules.cpp create mode 100644 third_party/spirv-tools/source/opt/folding_rules.h create mode 100644 third_party/spirv-tools/source/opt/freeze_spec_constant_value_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/freeze_spec_constant_value_pass.h create mode 100644 third_party/spirv-tools/source/opt/function.cpp create mode 100644 third_party/spirv-tools/source/opt/function.h create mode 100644 third_party/spirv-tools/source/opt/graph.cpp create mode 100644 third_party/spirv-tools/source/opt/graph.h create mode 100644 third_party/spirv-tools/source/opt/graphics_robust_access_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/graphics_robust_access_pass.h create mode 100644 third_party/spirv-tools/source/opt/if_conversion.cpp create mode 100644 third_party/spirv-tools/source/opt/if_conversion.h create mode 100644 third_party/spirv-tools/source/opt/inline_exhaustive_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/inline_exhaustive_pass.h create mode 100644 third_party/spirv-tools/source/opt/inline_opaque_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/inline_opaque_pass.h create mode 100644 third_party/spirv-tools/source/opt/inline_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/inline_pass.h create mode 100644 third_party/spirv-tools/source/opt/instruction.cpp create mode 100644 third_party/spirv-tools/source/opt/instruction.h create mode 100644 third_party/spirv-tools/source/opt/instruction_list.cpp create mode 100644 third_party/spirv-tools/source/opt/instruction_list.h create mode 100644 third_party/spirv-tools/source/opt/interface_var_sroa.cpp create mode 100644 third_party/spirv-tools/source/opt/interface_var_sroa.h create mode 100644 third_party/spirv-tools/source/opt/interp_fixup_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/interp_fixup_pass.h create mode 100644 third_party/spirv-tools/source/opt/invocation_interlock_placement_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/invocation_interlock_placement_pass.h create mode 100644 third_party/spirv-tools/source/opt/ir_builder.h create mode 100644 third_party/spirv-tools/source/opt/ir_context.cpp create mode 100644 third_party/spirv-tools/source/opt/ir_context.h create mode 100644 third_party/spirv-tools/source/opt/ir_loader.cpp create mode 100644 third_party/spirv-tools/source/opt/ir_loader.h create mode 100644 third_party/spirv-tools/source/opt/iterator.h create mode 100644 third_party/spirv-tools/source/opt/legalize_multidim_array_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/legalize_multidim_array_pass.h create mode 100644 third_party/spirv-tools/source/opt/licm_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/licm_pass.h create mode 100644 third_party/spirv-tools/source/opt/liveness.cpp create mode 100644 third_party/spirv-tools/source/opt/liveness.h create mode 100644 third_party/spirv-tools/source/opt/local_access_chain_convert_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/local_access_chain_convert_pass.h create mode 100644 third_party/spirv-tools/source/opt/local_redundancy_elimination.cpp create mode 100644 third_party/spirv-tools/source/opt/local_redundancy_elimination.h create mode 100644 third_party/spirv-tools/source/opt/local_single_block_elim_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/local_single_block_elim_pass.h create mode 100644 third_party/spirv-tools/source/opt/local_single_store_elim_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/local_single_store_elim_pass.h create mode 100644 third_party/spirv-tools/source/opt/log.h create mode 100644 third_party/spirv-tools/source/opt/loop_dependence.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_dependence.h create mode 100644 third_party/spirv-tools/source/opt/loop_dependence_helpers.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_descriptor.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_descriptor.h create mode 100644 third_party/spirv-tools/source/opt/loop_fission.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_fission.h create mode 100644 third_party/spirv-tools/source/opt/loop_fusion.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_fusion.h create mode 100644 third_party/spirv-tools/source/opt/loop_fusion_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_fusion_pass.h create mode 100644 third_party/spirv-tools/source/opt/loop_peeling.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_peeling.h create mode 100644 third_party/spirv-tools/source/opt/loop_unroller.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_unroller.h create mode 100644 third_party/spirv-tools/source/opt/loop_unswitch_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_unswitch_pass.h create mode 100644 third_party/spirv-tools/source/opt/loop_utils.cpp create mode 100644 third_party/spirv-tools/source/opt/loop_utils.h create mode 100644 third_party/spirv-tools/source/opt/mem_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/mem_pass.h create mode 100644 third_party/spirv-tools/source/opt/merge_return_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/merge_return_pass.h create mode 100644 third_party/spirv-tools/source/opt/modify_maximal_reconvergence.cpp create mode 100644 third_party/spirv-tools/source/opt/modify_maximal_reconvergence.h create mode 100644 third_party/spirv-tools/source/opt/module.cpp create mode 100644 third_party/spirv-tools/source/opt/module.h create mode 100644 third_party/spirv-tools/source/opt/null_pass.h create mode 100644 third_party/spirv-tools/source/opt/opextinst_forward_ref_fixup_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/opextinst_forward_ref_fixup_pass.h create mode 100644 third_party/spirv-tools/source/opt/optimizer.cpp create mode 100644 third_party/spirv-tools/source/opt/pass.cpp create mode 100644 third_party/spirv-tools/source/opt/pass.h create mode 100644 third_party/spirv-tools/source/opt/pass_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/pass_manager.h create mode 100644 third_party/spirv-tools/source/opt/passes.h create mode 100644 third_party/spirv-tools/source/opt/private_to_local_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/private_to_local_pass.h create mode 100644 third_party/spirv-tools/source/opt/propagator.cpp create mode 100644 third_party/spirv-tools/source/opt/propagator.h create mode 100644 third_party/spirv-tools/source/opt/reduce_load_size.cpp create mode 100644 third_party/spirv-tools/source/opt/reduce_load_size.h create mode 100644 third_party/spirv-tools/source/opt/redundancy_elimination.cpp create mode 100644 third_party/spirv-tools/source/opt/redundancy_elimination.h create mode 100644 third_party/spirv-tools/source/opt/reflect.h create mode 100644 third_party/spirv-tools/source/opt/register_pressure.cpp create mode 100644 third_party/spirv-tools/source/opt/register_pressure.h create mode 100644 third_party/spirv-tools/source/opt/relax_float_ops_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/relax_float_ops_pass.h create mode 100644 third_party/spirv-tools/source/opt/remove_dontinline_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/remove_dontinline_pass.h create mode 100644 third_party/spirv-tools/source/opt/remove_duplicates_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/remove_duplicates_pass.h create mode 100644 third_party/spirv-tools/source/opt/remove_unused_interface_variables_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/remove_unused_interface_variables_pass.h create mode 100644 third_party/spirv-tools/source/opt/replace_desc_array_access_using_var_index.cpp create mode 100644 third_party/spirv-tools/source/opt/replace_desc_array_access_using_var_index.h create mode 100644 third_party/spirv-tools/source/opt/replace_invalid_opc.cpp create mode 100644 third_party/spirv-tools/source/opt/replace_invalid_opc.h create mode 100644 third_party/spirv-tools/source/opt/resolve_binding_conflicts_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/resolve_binding_conflicts_pass.h create mode 100644 third_party/spirv-tools/source/opt/scalar_analysis.cpp create mode 100644 third_party/spirv-tools/source/opt/scalar_analysis.h create mode 100644 third_party/spirv-tools/source/opt/scalar_analysis_nodes.h create mode 100644 third_party/spirv-tools/source/opt/scalar_analysis_simplification.cpp create mode 100644 third_party/spirv-tools/source/opt/scalar_replacement_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/scalar_replacement_pass.h create mode 100644 third_party/spirv-tools/source/opt/set_spec_constant_default_value_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/set_spec_constant_default_value_pass.h create mode 100644 third_party/spirv-tools/source/opt/simplification_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/simplification_pass.h create mode 100644 third_party/spirv-tools/source/opt/split_combined_image_sampler_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/split_combined_image_sampler_pass.h create mode 100644 third_party/spirv-tools/source/opt/spread_volatile_semantics.cpp create mode 100644 third_party/spirv-tools/source/opt/spread_volatile_semantics.h create mode 100644 third_party/spirv-tools/source/opt/ssa_rewrite_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/ssa_rewrite_pass.h create mode 100644 third_party/spirv-tools/source/opt/strength_reduction_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/strength_reduction_pass.h create mode 100644 third_party/spirv-tools/source/opt/strip_debug_info_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/strip_debug_info_pass.h create mode 100644 third_party/spirv-tools/source/opt/strip_nonsemantic_info_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/strip_nonsemantic_info_pass.h create mode 100644 third_party/spirv-tools/source/opt/struct_cfg_analysis.cpp create mode 100644 third_party/spirv-tools/source/opt/struct_cfg_analysis.h create mode 100644 third_party/spirv-tools/source/opt/struct_packing_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/struct_packing_pass.h create mode 100644 third_party/spirv-tools/source/opt/switch_descriptorset_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/switch_descriptorset_pass.h create mode 100644 third_party/spirv-tools/source/opt/tree_iterator.h create mode 100644 third_party/spirv-tools/source/opt/trim_capabilities_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/trim_capabilities_pass.h create mode 100644 third_party/spirv-tools/source/opt/type_manager.cpp create mode 100644 third_party/spirv-tools/source/opt/type_manager.h create mode 100644 third_party/spirv-tools/source/opt/types.cpp create mode 100644 third_party/spirv-tools/source/opt/types.h create mode 100644 third_party/spirv-tools/source/opt/unify_const_pass.cpp create mode 100644 third_party/spirv-tools/source/opt/unify_const_pass.h create mode 100644 third_party/spirv-tools/source/opt/upgrade_memory_model.cpp create mode 100644 third_party/spirv-tools/source/opt/upgrade_memory_model.h create mode 100644 third_party/spirv-tools/source/opt/value_number_table.cpp create mode 100644 third_party/spirv-tools/source/opt/value_number_table.h create mode 100644 third_party/spirv-tools/source/opt/vector_dce.cpp create mode 100644 third_party/spirv-tools/source/opt/vector_dce.h create mode 100644 third_party/spirv-tools/source/opt/workaround1209.cpp create mode 100644 third_party/spirv-tools/source/opt/workaround1209.h create mode 100644 third_party/spirv-tools/source/opt/wrap_opkill.cpp create mode 100644 third_party/spirv-tools/source/opt/wrap_opkill.h create mode 100644 third_party/spirv-tools/source/parsed_operand.cpp create mode 100644 third_party/spirv-tools/source/parsed_operand.h create mode 100644 third_party/spirv-tools/source/print.cpp create mode 100644 third_party/spirv-tools/source/print.h create mode 100644 third_party/spirv-tools/source/software_version.cpp create mode 100644 third_party/spirv-tools/source/spirv_constant.h create mode 100644 third_party/spirv-tools/source/spirv_definition.h create mode 100644 third_party/spirv-tools/source/spirv_endian.cpp create mode 100644 third_party/spirv-tools/source/spirv_endian.h create mode 100644 third_party/spirv-tools/source/spirv_optimizer_options.cpp create mode 100644 third_party/spirv-tools/source/spirv_optimizer_options.h create mode 100644 third_party/spirv-tools/source/spirv_target_env.cpp create mode 100644 third_party/spirv-tools/source/spirv_target_env.h create mode 100644 third_party/spirv-tools/source/spirv_validator_options.cpp create mode 100644 third_party/spirv-tools/source/spirv_validator_options.h create mode 100644 third_party/spirv-tools/source/table.cpp create mode 100644 third_party/spirv-tools/source/table.h create mode 100644 third_party/spirv-tools/source/table2.cpp create mode 100644 third_party/spirv-tools/source/table2.h create mode 100644 third_party/spirv-tools/source/text.cpp create mode 100644 third_party/spirv-tools/source/text.h create mode 100644 third_party/spirv-tools/source/text_handler.cpp create mode 100644 third_party/spirv-tools/source/text_handler.h create mode 100644 third_party/spirv-tools/source/to_string.cpp create mode 100644 third_party/spirv-tools/source/to_string.h create mode 100644 third_party/spirv-tools/source/util/bit_vector.cpp create mode 100644 third_party/spirv-tools/source/util/bit_vector.h create mode 100644 third_party/spirv-tools/source/util/bitutils.h create mode 100644 third_party/spirv-tools/source/util/hash_combine.h create mode 100644 third_party/spirv-tools/source/util/hex_float.h create mode 100644 third_party/spirv-tools/source/util/ilist.h create mode 100644 third_party/spirv-tools/source/util/ilist_node.h create mode 100644 third_party/spirv-tools/source/util/index_range.h create mode 100644 third_party/spirv-tools/source/util/make_unique.h create mode 100644 third_party/spirv-tools/source/util/parse_number.cpp create mode 100644 third_party/spirv-tools/source/util/parse_number.h create mode 100644 third_party/spirv-tools/source/util/small_vector.h create mode 100644 third_party/spirv-tools/source/util/span.h create mode 100644 third_party/spirv-tools/source/util/status.h create mode 100644 third_party/spirv-tools/source/util/string_utils.cpp create mode 100644 third_party/spirv-tools/source/util/string_utils.h create mode 100644 third_party/spirv-tools/source/util/timer.cpp create mode 100644 third_party/spirv-tools/source/util/timer.h create mode 100644 third_party/spirv-tools/source/val/basic_block.cpp create mode 100644 third_party/spirv-tools/source/val/basic_block.h create mode 100644 third_party/spirv-tools/source/val/construct.cpp create mode 100644 third_party/spirv-tools/source/val/construct.h create mode 100644 third_party/spirv-tools/source/val/decoration.h create mode 100644 third_party/spirv-tools/source/val/function.cpp create mode 100644 third_party/spirv-tools/source/val/function.h create mode 100644 third_party/spirv-tools/source/val/instruction.cpp create mode 100644 third_party/spirv-tools/source/val/instruction.h create mode 100644 third_party/spirv-tools/source/val/validate.cpp create mode 100644 third_party/spirv-tools/source/val/validate.h create mode 100644 third_party/spirv-tools/source/val/validate_adjacency.cpp create mode 100644 third_party/spirv-tools/source/val/validate_annotation.cpp create mode 100644 third_party/spirv-tools/source/val/validate_arithmetics.cpp create mode 100644 third_party/spirv-tools/source/val/validate_atomics.cpp create mode 100644 third_party/spirv-tools/source/val/validate_barriers.cpp create mode 100644 third_party/spirv-tools/source/val/validate_bitwise.cpp create mode 100644 third_party/spirv-tools/source/val/validate_builtins.cpp create mode 100644 third_party/spirv-tools/source/val/validate_capability.cpp create mode 100644 third_party/spirv-tools/source/val/validate_cfg.cpp create mode 100644 third_party/spirv-tools/source/val/validate_composites.cpp create mode 100644 third_party/spirv-tools/source/val/validate_constants.cpp create mode 100644 third_party/spirv-tools/source/val/validate_conversion.cpp create mode 100644 third_party/spirv-tools/source/val/validate_debug.cpp create mode 100644 third_party/spirv-tools/source/val/validate_decorations.cpp create mode 100644 third_party/spirv-tools/source/val/validate_derivatives.cpp create mode 100644 third_party/spirv-tools/source/val/validate_dot_product.cpp create mode 100644 third_party/spirv-tools/source/val/validate_execution_limitations.cpp create mode 100644 third_party/spirv-tools/source/val/validate_explicit_layout.cpp create mode 100644 third_party/spirv-tools/source/val/validate_extensions.cpp create mode 100644 third_party/spirv-tools/source/val/validate_function.cpp create mode 100644 third_party/spirv-tools/source/val/validate_graph.cpp create mode 100644 third_party/spirv-tools/source/val/validate_group.cpp create mode 100644 third_party/spirv-tools/source/val/validate_id.cpp create mode 100644 third_party/spirv-tools/source/val/validate_image.cpp create mode 100644 third_party/spirv-tools/source/val/validate_instruction.cpp create mode 100644 third_party/spirv-tools/source/val/validate_interfaces.cpp create mode 100644 third_party/spirv-tools/source/val/validate_invalid_type.cpp create mode 100644 third_party/spirv-tools/source/val/validate_layout.cpp create mode 100644 third_party/spirv-tools/source/val/validate_literals.cpp create mode 100644 third_party/spirv-tools/source/val/validate_logical_pointers.cpp create mode 100644 third_party/spirv-tools/source/val/validate_logicals.cpp create mode 100644 third_party/spirv-tools/source/val/validate_memory.cpp create mode 100644 third_party/spirv-tools/source/val/validate_memory_semantics.cpp create mode 100644 third_party/spirv-tools/source/val/validate_memory_semantics.h create mode 100644 third_party/spirv-tools/source/val/validate_mesh_shading.cpp create mode 100644 third_party/spirv-tools/source/val/validate_misc.cpp create mode 100644 third_party/spirv-tools/source/val/validate_mode_setting.cpp create mode 100644 third_party/spirv-tools/source/val/validate_non_uniform.cpp create mode 100644 third_party/spirv-tools/source/val/validate_pipe.cpp create mode 100644 third_party/spirv-tools/source/val/validate_primitives.cpp create mode 100644 third_party/spirv-tools/source/val/validate_ray_query.cpp create mode 100644 third_party/spirv-tools/source/val/validate_ray_tracing.cpp create mode 100644 third_party/spirv-tools/source/val/validate_ray_tracing_reorder.cpp create mode 100644 third_party/spirv-tools/source/val/validate_scopes.cpp create mode 100644 third_party/spirv-tools/source/val/validate_scopes.h create mode 100644 third_party/spirv-tools/source/val/validate_small_type_uses.cpp create mode 100644 third_party/spirv-tools/source/val/validate_tensor.cpp create mode 100644 third_party/spirv-tools/source/val/validate_tensor_layout.cpp create mode 100644 third_party/spirv-tools/source/val/validate_type.cpp create mode 100644 third_party/spirv-tools/source/val/validation_state.cpp create mode 100644 third_party/spirv-tools/source/val/validation_state.h create mode 100644 third_party/zstd/LICENSE create mode 100644 third_party/zstd/common/allocations.h create mode 100644 third_party/zstd/common/bits.h create mode 100644 third_party/zstd/common/bitstream.h create mode 100644 third_party/zstd/common/compiler.h create mode 100644 third_party/zstd/common/cpu.h create mode 100644 third_party/zstd/common/debug.c create mode 100644 third_party/zstd/common/debug.h create mode 100644 third_party/zstd/common/entropy_common.c create mode 100644 third_party/zstd/common/error_private.c create mode 100644 third_party/zstd/common/error_private.h create mode 100644 third_party/zstd/common/fse.h create mode 100644 third_party/zstd/common/fse_decompress.c create mode 100644 third_party/zstd/common/huf.h create mode 100644 third_party/zstd/common/mem.h create mode 100644 third_party/zstd/common/pool.h create mode 100644 third_party/zstd/common/portability_macros.h create mode 100644 third_party/zstd/common/threading.h create mode 100644 third_party/zstd/common/xxhash.c create mode 100644 third_party/zstd/common/xxhash.h create mode 100644 third_party/zstd/common/zstd_common.c create mode 100644 third_party/zstd/common/zstd_deps.h create mode 100644 third_party/zstd/common/zstd_internal.h create mode 100644 third_party/zstd/common/zstd_trace.h create mode 100644 third_party/zstd/decompress/huf_decompress.c create mode 100644 third_party/zstd/decompress/zstd_ddict.c create mode 100644 third_party/zstd/decompress/zstd_ddict.h create mode 100644 third_party/zstd/decompress/zstd_decompress.c create mode 100644 third_party/zstd/decompress/zstd_decompress_block.c create mode 100644 third_party/zstd/decompress/zstd_decompress_block.h create mode 100644 third_party/zstd/decompress/zstd_decompress_internal.h create mode 100644 third_party/zstd/zstd.h create mode 100644 third_party/zstd/zstd_errors.h create mode 100644 tools/gen_spv_ops.py create mode 100644 tools/splat/gen_splat_spirv.sh create mode 100644 tools/splat/splat.frag create mode 100644 tools/splat/splat.vert diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f04d89..1318395 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,8 @@ jobs: steps: - name: Check out source uses: actions/checkout@v4 + with: + fetch-depth: 0 # all of main: the version shown is VERSION + its commit count - name: Restore compiler cache uses: actions/cache@v5 with: @@ -90,6 +92,8 @@ jobs: steps: - name: Check out source uses: actions/checkout@v4 + with: + fetch-depth: 0 # all of main: the version shown is VERSION + its commit count - name: Download Frame Home payload uses: actions/download-artifact@v4 with: @@ -131,6 +135,8 @@ jobs: steps: - name: Check out source uses: actions/checkout@v4 + with: + fetch-depth: 0 # all of main: the version shown is VERSION + its commit count - name: Restore compiler cache uses: actions/cache@v5 with: @@ -182,8 +188,9 @@ jobs: uses: actions/cache@v5 with: path: .apt-cache/archives - key: apt-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }} - restore-keys: apt-ubuntu-24.04-arm64- + # (its own key: the Linux arm64 Studio job caches other packages, and the first to save a key keeps it) + key: apt-frame-home-ubuntu-24.04-arm64-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: apt-frame-home-ubuntu-24.04-arm64- - name: Install Frame Home build dependencies run: | set -euo pipefail @@ -194,6 +201,11 @@ jobs: sudo chown -R "$(id -u):$(id -g)" "$APT_CACHE_DIR/archives" - name: Build payload and installer run: sh scripts/build-frame-home.sh --ci + - name: Tests (tests_loco, ctest) + run: | + cmake -S . -B build/frame-home-cmake-ci -DFRAME_TESTS=ON + cmake --build build/frame-home-cmake-ci --target frame-tests --parallel + ctest --test-dir build/frame-home-cmake-ci --output-on-failure - name: Show compiler cache statistics run: ccache --show-stats - name: Upload Frame Home payload @@ -225,7 +237,8 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} steps: - # Every push to main is also a numbered release: VERSION (major.minor) and the number of commits on main. + # Every push to main is also a numbered release: VERSION (major.minor) and the commits since VERSION last + # changed, so the commit that bumps it is .0 (cmake/frame_version.cmake works out the same for the apps). - name: Check out history (for the version number) if: github.ref == 'refs/heads/main' uses: actions/checkout@v4 @@ -235,7 +248,8 @@ jobs: - name: Version if: github.ref == 'refs/heads/main' run: | - version="v$(tr -d ' \r\n' < src/VERSION).$(git -C src rev-list --count HEAD)" + bump=$(git -C src log -1 --format=%H -- VERSION) + version="v$(tr -d ' \r\n' < src/VERSION).$(git -C src rev-list --count "$bump..HEAD")" echo "VERSION_TAG=$version" >> "$GITHUB_ENV" echo "$version" - name: Download Studio build artifacts @@ -302,9 +316,12 @@ jobs: # and the files in this release. - name: latest.json run: | + set -euo pipefail # (a payload file missing must fail here, not hash what's left) home=$(cd "$RUNNER_TEMP/frame-home-payload" && sha256sum bin/frame-home bin/frame-switcher bin/fenv-tool layer/libframe_home_layer.so layer/env_ps.spv | cut -c1-64 | sha256sum | cut -c1-64) files=$(ls dist | python3 -c 'import json, sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))') - printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" > dist/latest.json + # Each file's SHA-256 as "sha256-": Frame Studio checks its download against it before installing. + hashes=$(cd dist && for f in *; do if [ -f "$f" ] && [ "$f" != latest.json ]; then printf ', "sha256-%s": "%s"' "$f" "$(sha256sum "$f" | cut -c1-64)"; fi; done) + printf '{"build": "%s", "home": "%s", "date": "%s", "files": %s%s}\n' "$GITHUB_SHA" "$home" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$files" "$hashes" > dist/latest.json cat dist/latest.json - name: Attest Studio release archives uses: actions/attest-build-provenance@v2 @@ -316,21 +333,53 @@ jobs: subject-path: dist/FrameHome-installer.zip - name: Version release (tag v*) if: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(needs.*.result, 'failure') }} - run: gh release create "${{ github.ref_name }}" dist/* --repo "$REPO" --title "Frame Studio ${{ github.ref_name }}" --generate-notes + run: | + # A version release has every platform: this job doesn't wait for macOS (see above), so without its build + # here it stops; run this job again once the macOS build has finished. + if ! ls dist/FrameStudio-macos-*.zip > /dev/null 2>&1; then + echo "::error::the macOS build isn't here yet: re-run this job once it has finished"; exit 1 + fi + gh release create "${{ github.ref_name }}" dist/* --repo "$REPO" --title "Frame Studio ${{ github.ref_name }}" --generate-notes # The numbered release (push to main): GitHub's "Latest release", with the changes since the previous numbered # one. Only when no build failed; a re-run of the same commit finds it there and leaves it. - name: Numbered release (push to main) if: ${{ github.ref == 'refs/heads/main' && !contains(needs.*.result, 'failure') }} run: | - if gh release view "$VERSION_TAG" --repo "$REPO" > /dev/null 2>&1; then echo "$VERSION_TAG is already released"; exit 0; fi + if gh release view "$VERSION_TAG" --repo "$REPO" > /dev/null 2>&1; then + # A re-run of this commit: done already. The tag on another commit (pushed by hand, the same name as this + # number): say so rather than skip, or this push would never be released. + if [ "$(git -C src rev-list -n 1 "$VERSION_TAG" 2>/dev/null)" = "$GITHUB_SHA" ]; then echo "$VERSION_TAG is already released"; exit 0; fi + echo "::error::$VERSION_TAG already exists on another commit (a tag made by hand?): bump VERSION or remove that tag"; exit 1 + fi previous=$(gh release list --repo "$REPO" --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // empty') + # The notes open with CHANGELOG.md's top section when the changelog changed since the previous release + # (whatever number this push gets), then GitHub's list of changes. They're also what the Discord's #releases + # channel gets (the repository's release webhook). + : > "$RUNNER_TEMP/notes.md" + if [ -z "$previous" ] || ! git -C src diff --quiet "$previous" HEAD -- CHANGELOG.md 2>/dev/null; then + awk '/^## / { if (on) exit; on = 1; next } on' src/CHANGELOG.md > "$RUNNER_TEMP/notes.md" + fi + cat "$RUNNER_TEMP/notes.md" gh release create "$VERSION_TAG" dist/* --repo "$REPO" --target "${{ github.sha }}" --latest \ - --title "Frame Studio $VERSION_TAG" --generate-notes ${previous:+--notes-start-tag "$previous"} + --title "Frame Studio $VERSION_TAG" --notes-file "$RUNNER_TEMP/notes.md" --generate-notes ${previous:+--notes-start-tag "$previous"} # The rolling "latest" pre-release: where installed Studios and headsets look for updates (latest.json). + # Updated in place, never deleted first: a run cancelled midway (a newer push) or an update check meanwhile + # always finds a whole release. The files go up first and latest.json last, so it never names files that aren't + # there yet; then what this build no longer makes is removed and the tag moved to this commit. - name: Latest build (push to main) if: github.ref == 'refs/heads/main' run: | - gh release delete latest --repo "$REPO" --yes --cleanup-tag 2>/dev/null || true - gh release create latest dist/* --repo "$REPO" --prerelease --target "${{ github.sha }}" \ - --title "Latest build (${GITHUB_SHA::7})" \ - --notes "Built automatically from ${{ github.sha }} on $(date -u '+%Y-%m-%d %H:%M UTC'). Replaced by every push to main." + set -euo pipefail + title="Latest build (${GITHUB_SHA::7})" + notes="Built automatically from ${{ github.sha }} on $(date -u '+%Y-%m-%d %H:%M UTC'). Replaced by every push to main." + if ! gh release view latest --repo "$REPO" > /dev/null 2>&1; then + gh release create latest dist/* --repo "$REPO" --prerelease --target "${{ github.sha }}" --title "$title" --notes "$notes" + exit 0 + fi + find dist -maxdepth 1 -type f ! -name latest.json -print0 | xargs -0 gh release upload latest --repo "$REPO" --clobber + gh release upload latest dist/latest.json --repo "$REPO" --clobber + for asset in $(gh release view latest --repo "$REPO" --json assets --jq '.assets[].name'); do + [ -e "dist/$asset" ] || gh release delete-asset latest "$asset" --repo "$REPO" --yes + done + gh api -X PATCH "repos/$REPO/git/refs/tags/latest" -f sha="$GITHUB_SHA" -F force=true > /dev/null + gh release edit latest --repo "$REPO" --title "$title" --notes "$notes" diff --git a/.gitignore b/.gitignore index 5763715..50d3b22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,65 @@ # build output /build*/ /dist/ +/out/ /frame/payload/bin/ /frame/payload/layer/libframe_home_layer.so /frame/payload/layer/env_ps.spv /frame/payload/SOURCE studio/build*/ tools/*/build*/ +CMakeUserPresets.json +CMakeCache.txt +CMakeFiles/ +compile_commands.json +.cache/ +.ccache/ +.apt-cache/ +# compiler and linker leftovers outside a build folder +*.o +*.obj +*.a +*.lib +*.so +*.dll +*.exe +*.pdb +*.ilk +*.exp +*.dSYM/ # editor / OS clutter .vscode/ +.vs/ .idea/ +.fleet/ *.swp +*.swo +*~ .DS_Store Thumbs.db -# never commit homes, environments or keys +desktop.ini +# scripts' scratch +__pycache__/ +*.pyc +.venv/ +venv/ +node_modules/ +*.log +*.tmp +*.part +*.orig +*.rej +# never commit homes, environments, bundles or keys *.apk *.fenv +*.fbundle *.pem +*.key +*.p12 +*.pfx id_rsa* id_ed25519* +.env *Zone.Identifier # astcenc's own test framework (not built here) third_party/astcenc/Source/GoogleTest/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..174a2d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,155 @@ +# Changelog + +Newest first. When this file changed since the last release, the next release's notes start with the top section, +and the notes post to the Discord's #releases channel: add a section at the top in the push you want announced. +The version: VERSION (major.minor) and the commits since VERSION changed, so the commit that bumps VERSION is .0. + +## v0.2.0 + +### Frame Studio +- Gaussian splats: open or add 3DGS captures (.ply, .splat, .spz from Polycam, Luma, Scaniverse, Postshot...) as splat objects: listed in the Outliner, picked in the view, moved, rotated and scaled with the gizmo (undoable), hidden or deleted, with a headset budget in Properties (over 400,000 may need Half frame rate; over a million is asked about on Save and Send). Saved into the .fenv as .spz files. +- Newer .spz splat files (version 4, the kind Niantic's current spz tools save) open too. +- A saved environment records which Frame Studio made it, shown in Properties > Environment, on the start screen and by fenv-tool info. +- The version is shown at the bottom left (and in the Help menu), the same number as the release. +- Properties tabs are labelled: each shows its icon and name across the top, the open one underlined. +- The open tab of an area (Properties, Outliner) and the open workspace are underlined, so you can tell which is which. +- Buttons in the World, Environment, Bundle and Headset tabs sit in rows at their own size, as in the Object tab, + instead of stretching across the panel. +- Moving things: click to select, then drag the selected item to move it. Clicking something no longer nudges it + (or a large mesh behind it) when the mouse moves a little. +- Outliner folders have colours, one for each level of nesting, instead of plain grey. +- Removed "Stop at Meta's invisible walls" from the World tab. +- Help › Join the Discord, and a Discord button on the start screen: https://discord.gg/yJDn59ErgK +- New app icon (a roof over a headset) on Windows, macOS and Linux. +- Opens STL (3D printing, CAD) and PLY (scans, photogrammetry) models, alongside glTF, FBX and OBJ. +- PLY meshes show their own vertex colours (with see-through alpha when the file has it), in Studio and on the + headset. glTF models' vertex colours show too. Models without colours look the same as before. +- Environment tab › Sky: pick a 360° panorama (.hdr, .jpg or .png, twice as wide as it is tall) to use as the sky + instead of the home's own. Remove puts the home's sky back. It's saved in the .fenv, so Frame Home shows it as it is. +- Pictures can also be TGA, GIF (its first frame) and PSD (the flattened image). +- Meta homes look right in more places (Studio and Frame Home): + - Textures stored uncompressed (8-bit or half-float) load properly. They came out as streaky noise or pixelated + (Focused) or black (Horror's lanterns and bats). + - A vista's meshes whose shaders ship in its footprint draw when the footprint is in the same folder (Horror's + front garden, a Calming boulder). + - Meshes with no material in the scene use the one they were exported with (Horror's sky and candle flame, + Oceanarium's light rays and surface waves). + - Vista (Cascadia)'s floors are matte again instead of mirror-like: its reflection map keeps the blurred levels Meta + made for rough surfaces. Convert an environment made from it before this again to get the fix. + - Things placed inside another placed group sit where they belong (Haven2025's teleport spots by each seat were + all in the middle of the room). +- Environments can say which newer features they need; a Frame Studio or Frame Home too old for one says so instead of + showing it wrong. +- Fixed a crash in the Bundle tab: taking an environment out of a bundle (✕) could close Frame Studio, because its + picture was freed while it was still being drawn. +- Opening an environment that already has a saved walkable area no longer marks it as changed: the area is shown + exactly as saved until you edit something, so just opening and closing it doesn't ask you to save. +- Dragging the gizmo can no longer leave an object, a splat or a world item broken (gone from view) when the gizmo + gives a bad result for a moment, such as a rotate ring dragged edge-on. +- Placed surfaces far from the middle of the world join up as reliably as ones near it. +- Updates are checked before they're installed: a download that was cut off, damaged or not the real file is refused + and nothing is changed. The files an update sets aside are cleaned up even when the old copy was still running. +- An environment named "." or ".." (typed, or in a .fenv someone sent) could make the 5-minute auto-save delete + environments in My Environments. Names like that, and Windows device names (CON, NUL...), are now changed into + safe file names. +- Saving an environment opened from inside a bundle saves it into My Environments; before, Save wrote into a scratch + copy that was emptied the next time a bundle member was opened. +- Object edits are found again when the environment's name, or your Windows user name, has letters like é or ü + (they were silently missing, and saving then dropped them), and the edits file is written whole or not at all. +- An environment whose splat placement or a carried-over setting became "infinity" or "not a number" could never be + opened again after saving: those values are now saved as safe ones. Typing such a number into a Location, Rotation + or Scale field leaves the value as it was. +- Saving a bundle over an existing one no longer loses both when the old file can't be replaced, and a bundle can't + be saved over one of its own environments. +- A bundle with many environments no longer leaves the 3D view blank after a resize. +- Running out of memory opening a huge model, or saving a huge home, shows a message instead of closing Frame Studio. +- An OBJ model whose .mtl file is missing opens (without its materials) instead of not at all. +- Damaged or hostile files are handled safely: .spz files that unpack to gigabytes, PLY files with impossible + counts or indices, glTF files with a broken index count, oversized pictures in a .fenv or bundle. +- No more freeze after an edit: the walkable area is worked out in the background after you move, delete or undo + something. Big homes used to stop the window for up to 1.5 seconds each time (Hand Tutorial 4). +- The 3D view follows the mouse a frame sooner: orbiting, panning, flying and gizmo drags show in the same frame, + instead of one refresh behind the rest of the window. +- Frames come at an even pace. On Windows they're timed to the display's refreshes, which cuts the wait between + your input and the screen. +- Selecting a big mesh no longer holds up a frame for as long (its outline builds about twice as fast). +- The Bundle tab costs a third as much time per frame. +- Two Frame Studio windows can no longer install an update at the same time (the second says another is installing + it), and an update cut off halfway, by a crash or a power cut, is finished or undone the next time Frame Studio + starts instead of leaving a mix of old and new files. +- Closing an environment in the same moment the 3D view is resized no longer shows a frame of garbage in the view. +- An update stopped at the worst moment (a crash or power cut while Frame Studio swaps in the new program) can no + longer leave no program to start: the old program is replaced by the new one in a single step. +- On Windows, homes, models and environments in folders over 260 characters deep open, import and save (where + Windows' long paths setting is on). +- With letters like é or ü in your Windows user name, compiled shaders are now kept between runs instead of being + compiled again every time Frame Studio starts. +- glTF models whose index data doesn't start on an even byte are read safely. +- Copy and paste: Ctrl+C, Ctrl+X (cut), Ctrl+V and Ctrl+D (duplicate), also in the Edit menu and the right-click + menus, for objects, splat objects, spawns, seats, teleport hotspots, surfaces, invisible walls and gravity zones. + Paste puts the copy where the view is looking (at the same height over the floor), or right beside the original if + you're looking at it; Duplicate always puts it beside. It works across environments opened in the same Frame Studio, + and world items also paste into another Frame Studio window. One Ctrl+Z undoes it. Shortcuts do nothing while + you're typing in a text box. +- A new seat's "stand up here" spot is now in front of it (the way its arrow points) instead of behind it. + +### Frame Home +- Sitting down turns you to face the way the seat faces (its arrow in Frame Studio), wherever you were looking when + you pointed at it or teleported onto it. Before, you kept facing the way you were, so you could end up sitting + sideways or backwards. +- Gaussian splats: environments with splat captures draw them (sorted back to front as you move, both eyes in one pass); a .fenv that has splats needs this Frame Home (older ones refuse it rather than show it without them). The Environments panel shows which Frame Studio made each environment. +- The Debug page scrolls (mouse wheel and drag). +- Settings shows Frame Home's version at the bottom. +- The dashboard tab has the new icon. +- An environment made by a newer Frame Studio says "Needs a newer Frame Home" on its card. +- Smoother movement at any frame rate: walking and turning step by exactly one display frame's time per frame, so the + speed no longer wobbles with the frame timing. +- Half frame rate: frames are timed to the display (every other refresh, not a loose sleep), the frame shown twice is + turned on with your smooth turn instead of standing still, and while you walk, turn, dash or blink every frame is + drawn (when the home is light enough for that). +- A frame that comes late, or is shown twice, follows your smooth turning and (approximately) your walking, instead of + lagging behind for a refresh. +- Blink teleports fade to black and back in the system environment (they were an unfaded freeze, then a jump). +- Teleporting near chairs and stools goes where you aim: the arc takes a seat only when it comes down onto it, not when + it passes a stool on the way or lands beside a desk (Haven2025 kept sitting you in the desk chair). Every spot of + Haven2025's floor can be teleported to. +- Sitting down no longer throws you straight back out: the stick has to come back to the middle before pushing it + stands you up. +- While a game is on screen the home stops completely (no drawing, no sound, nothing read from the controllers), so + the GPU and CPU are all the game's. It keeps going while a game loads, and is back the moment the game closes. +- The Hand Tutorial homes draw faster: their hundreds of look-alike plants (see-through ones too) and other matching + parts are drawn together instead of one at a time. They look exactly the same. Hand Tutorial 7 takes about 20% less + GPU time a frame, the other Hand Tutorials 4 to 9% less. +- Updates are checked before they're installed: the downloaded installer must match the SHA-256 the release lists, so + a download cut short or damaged (or one the release lists no SHA-256 for) is never installed, and a failed update no longer says + "Frame Home is updated" the next time the panel starts. The installer is removed from the headset once unpacked. +- Installing never leaves new Frame Home programs beside an old compositor layer: everything is copied first, and a + full disk changes nothing. Installing from a computer reports an environment that didn't copy, and doesn't make a + missing one the environment shown. +- A damaged environment file can no longer hang or crash Frame Home while loading: walls and floors with absurd or + broken coordinates, shaders with types that contain themselves, texture data shorter than it claims and settings + that aren't numbers are caught, and a loader that runs out of memory fails that home instead of closing Frame Home. +- The system environment: no more single-frame flashes of Aurora (or of the home over passthrough) from a clock race + in the compositor layer, a frame carried on during a fast dash no longer swings sideways, and the layer refuses a + damaged hand-over instead of crashing the compositor. +- A tracking glitch that reports a broken (not-a-number) head pose is skipped for that frame instead of being fed to + walking and collisions. +- The system environment, while the home changes size (a resolution or foveation setting): the compositor no longer + shows a frame turned to the wrong angle (or half of one frame's angle and half of the next), and no longer redraws + with pictures it had already let go of. An older Frame Home with this compositor layer, or the other way round, + still works. +- A home with damaged shaders leaves out the parts that use them (and says so) instead of handing broken code to the + graphics driver. The check takes about 5 ms for all 172 of Haven's shaders. +- A home shown for more than 30 days is no longer deleted from the cache while it's on screen when another home is + opened (Frame Home and Frame Studio): a home still in use anywhere is skipped. +- Lower latency in the system environment: the head pose is read again just before each frame goes to the GPU (after + its drawing commands are made and the frame before is finished), so what you see is up to a millisecond or more + newer, and several milliseconds newer when the GPU is busy. +- Desert Terrace and other homes with cut-out foliage draw it a little faster (about 2% less GPU time in Desert + Terrace): the leaves' second pass tests depth before shading. They look exactly the same. +- The system environment: each frame Frame Home draws is now handed over to the compositor properly before the + compositor reads it (Vulkan's hand-over between programs), so a GPU driver that keeps image data in caches or + compressed can't show a stale or garbled frame. It costs no measurable GPU time, and an older Frame Home with this + compositor layer, or the other way round, still works. +- With foveation, the pass that smooths the low-detail edges now waits until its picture is ready to be drawn into + (a missing GPU wait that could, in principle, leave stray pixels). diff --git a/CMakeLists.txt b/CMakeLists.txt index ae18578..3fbf786 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,15 @@ add_library(lzma STATIC "${TP}/lzma/LzmaEnc.c" "${TP}/lzma/LzmaDec.c" "${TP}/lzm "${TP}/lzma/LzFindMt.c" "${TP}/lzma/Threads.c" "${TP}/lzma/CpuArch.c") target_include_directories(lzma PUBLIC "${TP}/lzma") +# zstd 1.5.6 (BSD), decompression only: version 4 .spz splat files (src/splat_import.cpp). Single-threaded, plain C +# (no amd64 assembly), so it builds the same everywhere. +add_library(zstd STATIC "${TP}/zstd/common/debug.c" "${TP}/zstd/common/entropy_common.c" + "${TP}/zstd/common/error_private.c" "${TP}/zstd/common/fse_decompress.c" "${TP}/zstd/common/xxhash.c" + "${TP}/zstd/common/zstd_common.c" "${TP}/zstd/decompress/huf_decompress.c" "${TP}/zstd/decompress/zstd_ddict.c" + "${TP}/zstd/decompress/zstd_decompress.c" "${TP}/zstd/decompress/zstd_decompress_block.c") +target_include_directories(zstd PUBLIC "${TP}/zstd") +target_compile_definitions(zstd PRIVATE ZSTD_DISABLE_ASM) + # The home loader: HSL / Clay / glTF homes, .fenv environments (baked scenes), Blender / Unity models. add_library(frame-scene STATIC src/hsl_scene.cpp @@ -77,6 +86,8 @@ add_library(frame-scene STATIC src/anim_bake.cpp src/fenv_lzma.cpp src/model_import.cpp + src/splat_import.cpp + src/spv_opt.cpp "${TP}/ufbx/ufbx.c" "${TP}/qhe/cook/hzanim_acl.cpp" "${TP}/stb/stb_image_impl.c") @@ -93,7 +104,15 @@ target_include_directories(frame-scene PUBLIC if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set_source_files_properties("${TP}/qhe/cook/hzanim_acl.cpp" PROPERTIES COMPILE_OPTIONS "-flax-vector-conversions") endif() -target_link_libraries(frame-scene PUBLIC miniz lzma smolv ${ASTCENC_LIB} pthread) +target_link_libraries(frame-scene PUBLIC miniz lzma zstd smolv ${ASTCENC_LIB} pthread) +# The SPIRV-Tools optimizer for src/spv_opt.h (FH_SPV_OPT=1 at run time). Off: on the Frame it made no shader any +# cheaper (Turnip's compiler already does what it does), and it is a large library to build everywhere. +option(FRAME_SPV_OPT "Build in the SPIRV-Tools shader optimizer (third_party/spirv-tools)" OFF) +if(FRAME_SPV_OPT) + add_subdirectory("${TP}/spirv-tools" spirv-tools EXCLUDE_FROM_ALL) + target_link_libraries(frame-scene PUBLIC spirv-tools-opt) + target_compile_definitions(frame-scene PUBLIC FH_HAVE_SPV_OPT) +endif() # libdeflate when the system has it (the Frame, CI): APK scene.zips inflate several times faster than with miniz. Its # static library first, so a release build doesn't need it installed on the headset. find_library(LIBDEFLATE_LIB NAMES libdeflate.a deflate) @@ -122,6 +141,7 @@ target_link_libraries(opusfile PUBLIC opus ogg) add_executable(frame-home src/main.cpp src/hsl_renderer.cpp + src/hsl_renderer_splat.cpp src/panorama.cpp "${TP}/stb/stb_vorbis_impl.c" "${TP}/miniaudio/miniaudio_impl.c") @@ -149,6 +169,11 @@ target_link_libraries(fenv-tool PRIVATE frame-scene) # frame-switcher: the SteamVR dashboard panel that switches the environment. add_executable(frame-switcher src/switcher.cpp) target_include_directories(frame-switcher PRIVATE src "${TP}/stb" "${TP}/qhe" "${TP}/miniz" "${TP}/openvr") +# Its Settings page shows the version: major.minor only (with the installed files' hash as the build), so a commit +# that doesn't touch Frame Home leaves its binaries, and the hash updates go by, the same. +file(READ "${CMAKE_SOURCE_DIR}/VERSION" FRAME_VERSION_BASE) +string(STRIP "${FRAME_VERSION_BASE}" FRAME_VERSION_BASE) +target_compile_definitions(frame-switcher PRIVATE FRAME_VERSION_BASE="${FRAME_VERSION_BASE}") if(FRAME_HOME_PC) target_include_directories(frame-switcher PRIVATE "${TP}/vulkan-headers/include") endif() @@ -160,3 +185,33 @@ endif() if(NOT FRAME_HOME_PC) set_target_properties(frame-switcher PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}" INSTALL_RPATH "${OPENVR_LIB_DIR}") endif() + +# Tests (tests_loco/): -DFRAME_TESTS=ON, then build frame-tests and run ctest. Off by default: release builds skip them. +option(FRAME_TESTS "Build the tests in tests_loco (cmake --build --target frame-tests; ctest --test-dir )" OFF) +if(FRAME_TESTS) + enable_testing() + add_executable(loco_test tests_loco/loco_test.cpp) # locomotion: floors, seats, stairs, gravity + add_executable(clip_test tests_loco/clip_test.cpp) # locomotion never puts eyes in walls or feet through floors + foreach(t loco_test clip_test) + target_include_directories(${t} PRIVATE "${TP}/openvr") + target_link_libraries(${t} PRIVATE frame-scene ${FH_OPENVR}) + if(NOT FRAME_HOME_PC) + set_target_properties(${t} PROPERTIES BUILD_RPATH "${OPENVR_LIB_DIR}") + endif() + endforeach() + add_executable(spv_check_test tests_loco/spv_check_test.cpp) # the shader checker, on every shader Frame Home ships + target_include_directories(spv_check_test PRIVATE src) + add_test(NAME loco COMMAND loco_test) + add_test(NAME clip COMMAND clip_test) + add_test(NAME spv_check COMMAND spv_check_test) + set(FRAME_TEST_TARGETS loco_test clip_test spv_check_test) + if(NOT WIN32) # the Frame Home <-> compositor layer shared state (Linux: memfd, the layer itself) + add_executable(env_share_test tests_loco/env_share_test.cpp) + target_include_directories(env_share_test PRIVATE src "${TP}/vulkan-headers/include") + target_link_libraries(env_share_test PRIVATE pthread) + add_test(NAME env_share_thread COMMAND env_share_test 2 thread) + add_test(NAME env_share_process COMMAND env_share_test 2 process) + list(APPEND FRAME_TEST_TARGETS env_share_test) + endif() + add_custom_target(frame-tests DEPENDS ${FRAME_TEST_TARGETS}) +endif() diff --git a/README.md b/README.md index 1f376ca..0529aa1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ behind the Steam menu and while games load. **Frame Studio** is the desktop edit macOS. It opens Meta homes, `.fenv` environments and models (glTF, FBX, OBJ), shows them with the same renderer the headset runs, and lets you edit, save and send them to the headset. +**Discord:** [join the Frame Studio server](https://discord.gg/yJDn59ErgK) for help, bug reports, feature requests, +new releases and environments people have made. What changed in each version: [CHANGELOG.md](CHANGELOG.md). + ## Quick start 1. **Download** Frame Studio for your computer from the Releases page (**Latest build**, or a version release) and diff --git a/VERSION b/VERSION index 49d5957..3b04cfb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1 +0.2 diff --git a/art/icon/frame-glyph-256.png b/art/icon/frame-glyph-256.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef7bb0a9304fabdb0c8444ca4b86287d5cb45e5 GIT binary patch literal 4417 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 B()a)X literal 0 HcmV?d00001 diff --git a/art/icon/frame-glyph-64.png b/art/icon/frame-glyph-64.png new file mode 100644 index 0000000000000000000000000000000000000000..377dd496ec5c9f2d72ba7a8d9b4621397e89f26d GIT binary patch literal 1182 zcmV;P1Y!G$P)>zUx10S<^aaMc7?x`2b?C5zo*_7|Ol3pj27H?)!)`~+&^U-H(c z5Em5!opaMv1BY?63s}7Juks2Zp97MynMD&y6+T0JL#-}YD;~ii95X;iybxkR{==$r z{4&821MG-5@XG|-1~?(!zz-8_-2^R)e_P-O)ii12n{!OMwcp zo@RoU0d9&n@X7?sT|lOs+u|i&bq7lsAWtAqr1&{)@u|3-3CafeA>P1E6O_7upW-Dt z-GQggwFak7jRFXru&AUexsu<_1d56zRE0z)j2{dt|R5GVGmW!sT3t}`=8_lq_ zAhMx@cI77VMzO(C;1#}dEoLD_?Q}IlXogySUt_CqXt@biur&;>N}yd?{2waTchJ^! zgsh`ORIiWW|y9@AB;sSyKVIG?3-vbC! zHeam$;CcO!++j2K^PhFlgzo|0p^3HuNr4gF{=>rWjyH(9)MLf1^ni`1h6a2chM|e} zJ%A19hYp0#%f(Ch*m+q74d!$444P;gFnLi7bAt!0e1_)^i8vwc+Z#5uR>r_eo&1RI z+rto_{JN!hpn0-U45cfds2A^L46N0812(W_z<8sO4?~SAv24~L3%r&oZUVT;qCOxq zu_RD6(I7s-2Daw`WA&mHN$lZ9KTj9K;$MqA9ES?^B9@FUz6CN{)ac}v?`A{0U=v5b zB(<@PcIEnT_O000I_L_t&o06EA% diff --git a/art/icon/frame-studio-1024.png b/art/icon/frame-studio-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..a2bdabea26c55bd842c326eda1016cd8fef08384 GIT binary patch literal 27895 zcmeFZc|4Wd7eBs_Ig%8aLQxSiL@Coz8A_Q-qeLVsLxUm2IptO|q%G_B&$-~_U@IxE zD2@=4+_ceZD?&o>zd}e%1pY%)){aJKF4|sru)(t3#NDYGTp}x#5vYuY)0zrS9F|PMA_a0Hb{x@Rwdb zAO0eSFcuB@Qt%tJ#}fagFPMw}^MB8)(<4Z0e|Y#bd(Nuy@%4ZT6D<*c8r zt$JS8C0SeWfEc?Jv^O=XWVP~_wgv-ksFDC;+->b$RThl@iLM#kM6|BzU2`+49Pj+}FRNV}k0d=YG&C$0kbk8^-!E@ER<~iO@L=^F^+xyj&@HsZZ3fDT zTWD)_m4`0zk}iNY)2GJnbYH>$f?gMfZn@_%a!c(^SxZ46(}-)_ibU)X-J-B#fSX)i zUOss89m{>;bg$maGOuaJfYg+TDoMnOdhlg)n91JD`}EvSZO|-SBYb9a!;!4G#v|FA z%PT7Mj>}TAQxIC}-<;Q5H+Hijy3yT9Y7293_`>8U^92ZH?}??9JQk>NOnlMMrBvco z(84Xx_iqUi%JmdLI`2My{P?@E<1}^k#E=X^@o&!_iX#=HLP*)zSGfBxC4z_J%ZbDu4f z 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 A9smFU literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-16.png b/art/icon/frame-studio-16.png new file mode 100644 index 0000000000000000000000000000000000000000..43e5a2c18ac31feb922abacd7fbc36b18f474d3d GIT binary patch literal 512 zcmV+b0{{JqP) zqJNN~M1qE)_ukG7Dv7s9KbSc)bIy0>oSAcE#9=m@tt2XG6yk3LX*>i10T16WK3Oc5 zD-un}4A*|Yzmb_trUprpoX8HfGGnalH?6I!N65YXZC$;lY1J~xB0XhCdlwewmc=?2 zi{bESU(c*a);qP^dIi~idjpCP%Zn?JWmz|pGzZ=GKFrN5!sT|tF*Yd9HrBU=LwYOh z>~5ym8;nL{J>62DAkWV(iJM182cRelR8__4*)ht?DlqDpfbZ=UUhi{)?~#(BP>2Kl z!#KY|p4 zeZ33Y1f-w&ghC;p$__?kk)L%!kU3ak3A9QZ!r^er8>Bek(3EI$t-iRR(9qMRrI0jw zJcL?GRZZU)+j7tqGMM84h(<_vkmi7wavOcm39>^oZzcnE;yrz34Kl+WHItRaH+ke_ zl4Slr$sg3tYa}Dz00030|1d%tga7~l21!IgR09AzQDi)v$_krg39q?Qp` z%QlACLMRe9fg(ocI1`LEISun(=iK>`GjGQCr1>6~d)~R{eBZtA-uq4gGGMpc-Bzo$ zN(iwD>wu5YakH#s3d%C`66c|xZ0#m&E|Hy1Kb-WmToKM*drLM3`*@6F)`%kw58wq)TKG9y1cr2{bjHfTzqO?aQ4PvXUpk zV;!9rrOi3Vo0@eox;^N@QzXjkQ{)V#q5;aeSGqW5s}C{n9$;oXD^G3#YY|MtR+4il=S9<&^gp!N=tIOl zWZCpk_B+7HxdLtO6V&N(C{_F7 zHJYUmN?jvGIH8U`hTxOKex(3PLo4v-7t}8!DSuk7rm2gIi<_k|)q_FmT9981Q9Wpt zvyMiiXQ`V({xFl^guFc~gm5_AXS3PpONm|NTE`Je!@R1h-SlZr6Xg;+vh595xmqej z#y|U2AYwmNxyelbUjP6A|NkdA;;jGx00v1!K~w_(mNNg>f;C?400000NkvXXu0mjf Dc5Q3d literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-256.png b/art/icon/frame-studio-256.png new file mode 100644 index 0000000000000000000000000000000000000000..2a120207c5ea508f148a2395cd4355c9a39c9864 GIT binary patch literal 6034 zcmZu#XIv9cwB5vnk{~7Y9uSZc5V3#=5+Nc@njoQsZlx+9C6v$tqDU1*q=-_a7nLeR zK!~6;0i_eA{iQ`f2oT5%zHjgMKJA?|yLV>Jow+;rJTN!Cd=w@E0|4Ntp#jDc06PPi6$J&?)5`*F9@4>q=}vY!+_4x(x6;`&XPqQ^tG)21c>JDL@B~y(*cuS^uBWh! zdfrzLUW^UL?_8Jh=F^)`)+K-b4Yi%$RQ4Mm8`E@VI|j?EuC8uqY}~a$Y(>u|HFb|| zrod&B(<7BikGnUN*VS!5zTT6e96SG(GnHd)*x|rxr{VFh{u~qO%27vKxz9^m2w|RP z_A9q-0R&yL`?voN=y8f9a@#7odiKmR*@Grw>x8D|9;k0 z0N*b|yVE`;yWT#^f%&f*%L0u^zWs4O-r@z+`~)Qz^-=OjetIh6nTo8csYC^u!&L!b zZyAfwv6kS3^1K-!MOs2mAYCndj#SCekCW}b{>{3GNzgpK-}?LaZ_VrDq*L+c>Cj~1 z)3^E|qipwTVw|2PCSmN%Gn41KEWueu>|fGb@9{(y7XrgIJ`s&&+fW{UnQxK8YT~q5 zEr;0TSVw?~6PlN%kFT7CVP+qulThRDBi{S}H+Y@Rq8N(QJGdfC_q*D%qJNX5{JKkSKnTd<}k^FA}KmX zE&a6@F)lO<9By*K5*(R*P3%Xq?)`1Atgkn6)8Yb<`Bi1flhNnj)z!5U0m!?fdDpi7 zmY##PoAFhI1i$(q4!W+KtUy>^&qC@k!q6BCA=FjSJ8fqcMXNu2==uFuvh-RsCgq;k z&0JaI3@sweN)u|%V2E&A%+4lmt64pxBoqScs}aqiJZ={;DQU~^$W}8wMy66KcvycR zs4sT?(&?ypS4yMzWizwB3~_OBFL}Muc2;)Ae;hg%%~mWPckc9F&g>Qek*_flum63F zdWX!f59l#)zGZJeULI!!7#ghjLs6;ke5b~SSa zz3wMx2Y$N3>!Zhbez2p%@ceS(?w9H~xA|y(CzpiHlVg2pbQ6&KuFp|c*|sr?P0c8( zqM07DT(<_w>!3yE9_LpkL>^)FmMRTN{xiZK&Si|WoxOsOZsFaADcS*q7^HW@NqwwRVNVc;9|?nnK?JCC*}b+k7xrg;eT$Q3Ail|D z^zIuOxEeD?FpPB;?8a1`sBBEm$6C1sxhwz-PnzCrejX^UErS^d;=N!z-9|Ba06kO;mEz;$;h`vfoiJ( z>Exk*{JVl;Q2+hgBOPEi3%kP*=Phfw3U(yQ&TzqzGPmKa3z;`_b+25xa?nv&Ukyoc z1H~ZADcf3qW*ssJQdj_Fj6bCNR22a3phk!rqa#oRf+~Hf6VWdUM*@wpjK^>4D6?11 zVpN#1bp=@&bWs_7hQD5lW)O0)0KYTMm>E?rc%BJIL@*W{qHqmpR-|gG(w{WYQa3h= zq?Ym;8%5L|bl6|rpFXnLrvb*o+a({J^SkLnB7lAz#I^I(olb&iK;=R0lBf(c9SdAI zNvAlocRYm79BG$S$v}r2(YW=91Q7jz5^przNa-7MtaMt-dn(>2)vO8h)zC@Pe{y6Vtj>NrUeiRKgHYx z`yRVRg%AV|;ApkQf(MK}L8|l{uIw146ex+Bl_<-d-N6sE=W?JTZ^@JZ{&2>TQ)@SA zN+blElPe6}MV0hhrf@{WV!;$63}{g#yz4Id$yM3B-t?i1gGMA8*qQe2Vz%5@1Y6Dk z(_sZoCnT?{au-=k zAf1WY5IME=C}iyo9b$Q%1kua=L0ZTHWB=}DUmGVA2|S&LL%2qvrFJrqr)bpgpl~G% zEq#J5KfM1Axg0Dc$hr5Id)t5JbJ0Adsy!llYRd5$`9~t*ZU)d^`Lx~$9m`18<2rkm zo-oh^`hW=9td8U6|JxuGzK_9y9clP$e@X70l5DS|f;ueX^ew@^{fhfOQI`y#5Hxob zJq_7gQ{(j_y~a5|K$@wgeV2AEk2}TMf(>DvD! z2xrR1}AUnDUN>d9iIZV1i^b3eA@PBt+A6%(i|DrDU}fo2p!Q4 zW2ZhdBbvRvJ^Y5&A7~y;Ig-?_<9iirzrx~?w&Ff2Fp|91RwBw~r&ILmD9ux9!px~h zFsk;JO4;K+9!RTf^6S8G0kotwxNgG#{C(kuaZQFcnQA|N02ztbOWuFqYQf4sj|zqK ze&eOI>}V)jmq8pIs9ND+{e8TrmT!W}UKTdWMXYX($>zQ16$|vmVGUteEpRg9D&I=O(#qHVxaIZ+91jI?j& z?-u^#Esk=%fZy!pmw##l)+2Hly3_atNFmHro!`_H6+AUlZV^D1!G5*Z73}TV{uuo0 zZoRQidfjJ&8Ma1E?cJ!@Nb7R+av&E$pdOl&)0UO^iEW5Jc^!he*4akN-Y3~RkkEwHjT6Y1}Gkd=kcwBmjXL*RV<#6Y>gAs4~rY%t3*>Z zQ7*mVmuVYU`PY+l$`S6J4jjhw2V)Oh7MD~Q9F3nn&ml%~eqB7M2109yE3wO*th$#o zeU=Huwpl)0;@0~#_j(bpXT#!fQqi*62||S$J{#F|<}BJ>#4dSq53{J)G;NEFU?x3m z&A4&VWSB>-$a9M`;b+TMBA}CUbk z*~Z#QH+akA~KMv|cBfAYhFz*w2)?eNI&hKQt-qdYUPh*)RbBHZ2nbWvV z*0z+CM}BJxzJeDVsKM{I=_^}$_rF}KI{O16=Vyb8El7Ag@~{iUxADhVHB)=d?#I)| zD3OCR4V7OpYq|Pu!QL>F`A{0TSXBVcv&uT(5`XVhZ1u}qoUAx<^W&tdp?9~%PUU&k_3C?pbP2g?r)Id77@Wkq@R?mTR(sEA z=36Y=i{`8`kcmS6Ory#~(eWteeSp?mSjGhatmhB4fImA~zEj^LqEl;U!-Ojsl5Ib= zLQ^=(b%?7I-zA|LVPM5xGtef%-84+Wf5AjsWrFwFtlN0t%^Xc*xt;Y_{R6~ac_>BY8SkFyg(#M#x2KoXOH1sKU2YUT#7B??0t|s*f`Fm)$icsC6orYL{+`lJI#EKPT?7y{KD| zGTrjJY|zye^fl`K=!(YNt&Y#Z;fgPsekxte-udi30Iovyl)0f_r7m4uWmZzn{wIFs zanc4{$V)1rY+JnFr8u|BdR#>B$GE1`-M{zo5ujBP8R>+uB?@_xo?CjNiic;R{G9KN zoFh1m?QE6tBW=^ro#BN&SDui{AA-Hn3z4vrX5e1K1U@fn;=2T9H8p#EVXEnIqUyQz z3ZH4Z{bFa1hCpxrIkvR1Dd#>du7=j--m76@)HIxqjd3@#gsM!XU<9ljz6p?*LuAy> zCq@rf>O}o45oG8IwuP;SETU?=?vhvWno+MSmR}VLub$$KTv%=HUNZLbH(ETnT@US6*rYWMkoUMNF-1&TdKsy;Y}d35}S`x?ow ze>4~TDW)o9x{x|dS@pzyP<9zD^0l8P?j87%1bsf<%JO9Ocqs02bjqk@A#r{Bl!(3a zFjr~;`|5fhrF%PzF*W?FK(Y;o!rM&caw+EWk`o3p0y7mVf3eF^Q^F;K1#!8E=4J zzp-+@w8iK7V1K`AeFWn`^7DH_I8s{KkcP*Hgb@Ax{b^&@XYr8!(a|@Y0a@(QgQF&` z6dR)tax*K;D)}?pOrd3t(_k3ybu)ByXq1_|NI#qO;8)n_u8#T0xmL!@*F1z!swq3e z8YwM~i$t_lria@43WFTvO1-&LOs(V?35X{S0F%L7BlZcf^dPbO^`m~{mJl0Tb(WOd zq}(~)jMGENeBWY}kz7yX-AlGmUK=%9AN^Y}n6NXv|#=1b>Xv8=7W>8h?;kAvlXH@2n9ok6( z)yz4C$Q3l^Wh(VVI<-`NB7uus>JOXw@4naq+BPsUzHLYrN;QV~pZ0csy1Tl%>doAL z*2iIqAf8y=sar|d8h1nIx)|7|9m5g~e+@aCjt~M#!_s3IYV3?pZ00lSd&!6(>{%G+ z69i2nUY=WRITuCY3Qe*%7X^LKc#;^EWWc`6E?p8I!|U-#Ioj=E=@5 zyS}d&U+jrEXx+Z9ke+2qJOd`DSr9ZYIOk}I!K4+3f9B-mNGdzEXuTDcKJ2{uos&Fw z@4UgoKNY$dS@Z3!S}**bYI|jjCKwy7d2>cZ_^22lt$4%E!(&OqD(G3Iq{Za=`0DT9 z&&LAER8C}mKq@Xwy_>wF&UU-E^?fl6eYXIz`V?W#3SUu%|6s@w;eHFRu$+)OO$Y2TQ`F)rQG?*UT{csSA|rwrHi#EyHco|SPUd%U-CtWB#9sdpMV zv-c`502Cx5Dw@TVu!gM7IWw-??0gjY6L7WBU=e4lEmf*edaeBV$iTn=%U5ARnRnUf z^@wCoCG(pGi4Y6A)#6%8*v>ciaE26z>+bp6*?eu@OjS2sOG364@@^$h1Vgb1{26>L zqv1{pNryeUGB&gR<=bV`3A<|^DtdE*!!k=OJ}IfUzyo%(0r|bnOPd10*~L01GA>QR z{ZiZshrWtRo7$xt4{y_mz4ua0AMg+4vy$jm_Li4_GsEP|k-7gQ1z^dqt;$g)!F zhe1D#{#1sUC}l_wOjCEu$NE55P3KPMjE>B@t23jxelUCPJ@?G{zI$fp-a9fz7-4-K z(b3T^GAWWIEhW2{u5lO|T2xgnBKwi-_4@kyYU7|}>^PmyI9Zm{h&&3z020th=0IIt z-ID+TH1h-^&Oiu2LbmH7q#p7qWWJJsi3kBmfS1fd65*D6eEUL0&LA!)nPhIuJ&t7< z1orGo#@;>qF&OBa`dhFVHo@d)&s@~nst~w${|;j8!XFqq(^W zMa6|SvS6Ig=Q}6ECXy0&`jP8kACK9EEde`{cH`28tG#{C9}g-kfA{uzjAQH|%xWOd z?!x)Yy?wXGjkx#?T084m4qQ`QTAO2pD)2DvrA3wjt3TDrmgY3+g zh>GgoR24wY7j~qM5n+6&h8W6W_gJp6C$Fg9)mvbCVITS#y zBWMM5#BJGzl9JCTt*C}$?FmS8S3`=P0ZDd1)K>tFJxM1{gY2A6OR@sBt_*5vE|lC< z%%2&HdGi(^EBk3sa<0ID0(3YM??PU*(a)#6x(HG6se2Z&OA)s2l;5~0$bgc0c%Zb_ z1gKP`QI|b5YkRA<=OZI{n9+JV0lgX?jbwRSO;k_>||zp}tSkuC5m+1IIr+ zfE8#5l8A_i^|WeD>n})@N5ztb3i%cs*2Om`+jT_n{Xm4+?2@elk&sDb zM`LJcLHCDg7TH94?)k<+2_uaE00030|3M#xzW@LL21!IgR09A+W{+J*kGfj`0000< KMNUMnLSTXkq06}d literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-48.png b/art/icon/frame-studio-48.png new file mode 100644 index 0000000000000000000000000000000000000000..87507091105adc26f8c96bbb9210e99142e93bbf GIT binary patch literal 1470 zcmV;v1ws0WP)XN}?bU3ZzII5(QdmBx*`+OIr%nATb04wdJsgM`J{z zjUmLMHrgm8U_^S@rnQGCQA*T$6i=w>A(*yXy1O&}ev?k!duOK8xcxNKdGqGYd*APy zH}9BP5mTHnEs3I|qmv>cA{I-Mv``R)2uvcfEZam;ykN80z8oAJ>~&c43B z8z?9#f~nV;KN;dP)^9*&Mi$J5v|32>JPxY^*#el%y0w{jrQ&r^rvT~#aq9F*m<((F zUUWGy4Ho~}tCgS!j%!N^XKmOBGogIv@Ln7{m=2R*p)8n%yA+0smyVciB98xQnhR-# z1+q|A@hYkb=EILZfLWOK;6D6NwqNlostHbO7N!`;$G?I1Q)G z&Djhyn6Lq^&4ULHd&Q5Aj$(7+CgkQnqJ%x!zweMKCMINn1#oS8!h#Ema?se+fY!Da zB|KRsl&J&_&8c*2Q}tZP1+xH8T*a&W^6MpB==>V@-?v`rT3uPSDh)%!L-^|aIq0N` zrW1{Z3=|b^L+P^zp+!72m{_?Y)rbjW23Y*ld&;0kJT#h+>94(`6_aLQ$M&7rwR10w zh=&FfOYccxEog3Of(G;%*!xr|9xHjmE8gL7D7}pq@$fLgQx2Xhxw3fC60Ih*8({HU zib~*1{FbeGX!xuSdSMgaes@v16TVEu#mA$uu^zz;1novw-<$4JJkKy$d~MyQFbZE= zye(hJ%agoE3y_|+CTJc(4b(T(;=StQZgE`mMa8)j60dT$_KcDj=jjTUsU?3+leyh{ zoh3qoe9LwqERIrnsCyfcI^se+5}lr^5Q%z-#CV_urOuqz-4Md z^KA6-?Zx0l+liAkN`W%1MlH+oA+FgCAtDw=kcY3rdFDAt?Qf5pcp&pZr7W!|voBf? zm(fF93LS`R_rtQ%cR`L5yCAn6b5EaA082)>&-Bi^3P@+4haPBMypPt!WyaP6vu^h* zo)Pcb2szs77AGu7^Phh84oK%~fWDuh7efYwgkd85hsUou?q3tM;2-8u%S@ zSHOvzUlarM&b?jNRo92$sNM?M_K(*o*>(-mCkMRZofO9-uUC2=jjL%)v5!Cxs%B** zrmr$L(e4U#BFEfFBWxjctH}_e5+M)#2I=#+fXkh}FPOa$;_@O0aW_LA>;w9{AvIS5 zL;WzoO!{utGyWonO!Uxh2Brpok$A53|DFMwTfB)Wf~!{7S83risq3I&{Z527-%#z} zO?x{Zxb{MJ5}(gsKMZ8<^^s^Ay;`}^N@i}RI` zKXg;#D%$2F1fZJlqAiESf8lv9&`%AfQ_u7wU05OndS^a~j5X(%Y2m_)EuRBLP+?>u znlhS49&V$ldZBOibP4-LfH~Ac1iXnyHX*flr1q+*;;I6$&xi>7a^gM7CS|@ z?AgmQ_GQM*Ju`j3_mBHL_dd^ke*bXJnfHF)=lwa$>#?D}_8w*)W&psR^Ew*F0H9zK z1sHe1f2+Pfw*g=S=QUJKeUs;gm|h<8dPP|>@aj1BB4JSav(P6-dc&IhN2(hl%p4sZ^BEWz{DWF=2mMTx_pq~Yb#*;C5#Jy0UGDhX4QnS1bPzFA zWWP%}Uq0!xka)V(E8;~ z_+A^ghS`9CfL_M*Oc3O}FjzJ4uDraq9}0Phl}AA0b)ivx#aGIwPcOgt^y!m!u%RJH zv{+bJ2z>m?pxnOG!irE__K1-V#&CeIemMnN(76wCersIqNu3IWS(@Hh0KQrIw!IA2(f@{Wnj?3Iu|LZ zX3L2KTqN!OS^xExcNzfKT5`0cM}P6p{Cus=OAfRHGYgk^UsiefnhXm$lO8mRi=pDH zi-<*JGP%5dUrcb*^#L;qw&LBpWcM-3ZUmfe8+nmsDq0)Ro~#f%HZ~UfyXO=3GQC4t zdwVjm*m{PmMdz6kK_$VRu9caT`uS67t^ zP-4rRn5VXD*a8~~dj~utkqHdiwD8>Hk2<=lMU$jGTDRe=EKak1PgzzmC}%!@`nXMUZZ|975? zEdQ9m0F-WWtMJNsY?~hlK7d>*arl(c)6*mE_Hz0F0t`bhGTB_QT>5rYk7?o~xRmLi z{Yv7xyb7q3DSUgB@L_m(SVgR1`93$!ag1_Akyz<34%c2+Szm<7uYm!E(2$UuhWe9U z^l4w~u~lda6AGlM%5mI^3#U(ZlN4)CP~rku#SxbN27bV{OAJw|#VpYfh34q%rtyLYTr!~)=JC`4G zIboRT){hKL|AEx1qm(wWE+ju(8A%x~`rw zB2L1K0&u!6H7`ZlNq><*AY9aaYdV7d4r_!sQFYisJ6m2H1vsBh`#E|a>g?_H$ut%% zM?0juxV!J<*V;hl(&-c!;GM2*LfTI|%SxE_BK7 zHIM>R;$xt*^USU+y+7GNu7gNq-E`Rxz`^*QHIOR`T8VVPQ`GA(VqCrp=LZ9zpLO#b z=0*>YT4V){Ri=duI$t^Ket&%$AJZ~vw(eO0Kx6Tb8|5Dg@OW$?N8A$;oN?}|J3ATR zNJ+H{D;)8@$jscF(zg(@y$~$rbsb8)TY(82ad(lI?kZs(Ot8|wHNtrak3}zPa3kJc zUID+D>wdsY#E26=z%0!_uPiI|Rud;qq63^Z87!!MNhpUgevmfGVaxVX2cfn>lLVt) z1}Gu00$kg3E4Bmv$jqNV_j2~*gy5HhEL?XuP)_vvZEhTwqVjP7r24hXMFbu9xgsT1 zlotiFZjt~HB;&#rHxK3>_-YB}hwC)JPwZBC!I@KHV(itCl?H zk(q^s;f^OQ3ov?ZeZ3u~ngY*J4(ePdSOAbYnWqK%hu>8;?yweg?`#zB$K7R7bX2#dt0SpI?MdDKg8kE7 z99UQ}8%Vl2Bv$-Ejh{rQF2DSF7ow9>(u)Lf^*}MRK0V7Us}YrWBGcHS|I$;tJeCf8 z>qvt-=oFdr0+)BKUq(gguBWH3v{zjURl<|)j{47$@9xZzT0{`c2fpm6Aan4N!qC~o zN|{tqHegVh_OsrpuAVCu69PJKrOMgkQ}C zuhkBW*{d^w&J$*6%rgLT*{;*5ex?K9Yr{$V%~;6H1aJZYRihB>9%xE<7PD#>tRio_ zDo{~jXX^1G9N=|*#QNdn;^VTmz{=V>o*8rW_ihT0E{GDRm}APy5`W%B2uhJ-SOMgG z5KTzj-wzkMUrY4>w-ey4?rD;%FoN-{f4`&rj2$P3gk9vJi9IIxY|i}yh|$doMzKAW z38u1!*&pcV{l*B_L6|DfXR7iqV$bIGIPRg2TuS^mBD#edJ+l`M7;@1BeSw0mF~cy5 z|C3=S%7s|5j7b0ilqJnjwyFshP>G<%T0Ch1(0RKHG$tw0j1AKg?Ezuc0QY@Dgl6b5 zW4MW7!E`MjpiRTwi%uQ~&;avd`4FvdD-1C8PuOVfbFlsJ2`8=12is~-{_*(1fFhcE z|HX(Iuu0cbZo?T>fWulrlsFiT(~w$^k+Kb&B-q@|NGXK-l_j{bWfvsnL_)Vi;m#|J zkgA{y*jS+m_If;3#62hi6@*=ZeD5G%BO`?Xr+o(Qwbo2j)VwR#m* zH~{T+2)Qwa9eu#S)RaLF!ZQJo#%$(5DbBmm;N|gpm0Dq>uGI+EApues)YA%w`!V>{Tm7SbMpEe^5gb&s8!}Y{@5DkLWHpiQU5n)kC_=uo=4vw(R3Txv$5)srXU&UhjN65Vw z9U@3$ITxbvB^F!qH?UI8!=IQWBPW;L(_?vd`C2hsigUcJ_p1X;XV%|ayveE5#tciL zaDzukkG96J%htnb(b4QxMfpB8E1X)WKMu!$pB92CVk3XLF=n4@nnGCH2pD~}-Cc}5 z;+8ANfnmZ78-gg_AZ*FKvBec_)bQ3qr7`5YEU+Ag!r2Ir9-*lVK;VE7@KZ-HQ|#nV z+52E};v8bY1QQx0Xl*hiou{sVheoI=G!r^o75E=f}h1EUrb zrpE|GDL`#f)K$jzi~}d02?R_SDGryZwSua~QWL>khxTSL!d%Bl>4H*dh#oXV^WcB< zUfqKRx!RiyPX$S?H-vCb40_>EPC5|ihH`F)V8iWE!z&+s3{ij5=mo^!vFaqWfMZPM zpir7J6o*6MOplTt3Gq?I!PTOp$PA1_39NB()H?W6n+cN$jTD7OzN5NM<>`Ea!u^CM zWXV#O^BVM2ixZYYZEc1zet~(aZ*L+waPOcAw_&p1>Cp_r@>w!b?GK0nuVI)T=LJ&& zi3^f$BCh|NIt>V%f=UXgj?Vy_^)%n)ey{8 z7&*;7|1tQ>0~AgN8uH}7hJ-;w(xD;jWCIPFr@~OEXuZmTF%?;wiyFZw+;_+UrO-ke zhJp+*UhL#97@A^pRK;yeAzK*q(Olw}q_MWrqrn3j(P^52 zQnsAnCyc!y>4hdO2D4C#F4Y?qJ{m!ZEGKY#Kw#C{%weI;=R-q4pr;YMq7fLvDoLTa z*cDEj36pf-0ITLRLdPzeN+noMf+*&&sHxF3GzkMVOmjg}*<%{LEGG^{bEaR5`a9r9 zr=`&>RHh{Z4rPd;sk5i4dlHT^l%n-h|Mj1u3iwyG zw4B;fLmJOe=QdL79|Hqg;4_UOQI#5wC{|9~Gq|qV$&8QeQSHi3RV6~Ym~|s?I*d9g zf_fBk6RQbzuCMZ<1srLd85kNTyaLz_3n)7|jDtFJ7&*xFkjAu^rn+ApX#_OYXJ~$2 zWkCx}(gZyJCxDL=$4Q$Ro&N|h1jbKj0`|~i)ftJ3QlrL>--R}{Q8zhpF*J=jg4C(U zt!9ANjMP-nPNZpg4Wl7M(@=_|MI%5Mh|r=DEkrYERt?eVtMOa8JFF1TM)tA^Xp6nK zB2s*U6fF|SwAr-G&twfVUWl7=@AtE-Mu1}?lv3qnUr@X8OS|tmKbiVbF)cJlUv0zQ zpBj(Bo%QYuWU%!mU;A~ldOmmpULnyEHJSk`BXJO5D~I2A{N8$gKU#f=I>d~520!w3rHNN9DpL1X{K-zb z`F$nTM_Ap);J5u&>UM;D%Zd`B`S!}!?mu&LogZP9Razhud;gg7G94N`BkVTx{rmUn zpY&SbO6l!!ty=OIN3WF;vZz~qiOQ2_v$zw>pDBIgcIlz~ewV^vPHDy98sK?SC@q9w zunT$@1M2x-7R(6Y2nKu6a0@1Z1gVr{WyHaRu;j!AalWNzBVz(I#gL6*f7Y>7DQ38R zGet3C9sWKsMln+Erh#JxjF6ilTXIn-jf;G)_MBQNoXx0d4$LNBd@aFv0Co zI4a6GkjeKmZ)A%p>_3>fS_rM1)kbvQMn^}tQe6qNGuH)Use51owy05QHec65ntoNJ z5hoK*C9t=;00~aQ)uiJ{C75_k6+&GDQ8KZRK*!FCD&Zm|IIm76Sk?JJCD23~(~dz_ z*4GBc(DDvBW{j4Q5!K|{-!N!c>QWSMr#d~$Z2|dKJ{i4$cbeyp7`eNn=I8SQBe`?m4UCD-FWVruZ(G=&=xE0I6N_ID%j%ZFjm}uAKKz8F_j5%3 zAuQfcOy038>z{%*<>SMBR(k)l|XH9pxh>PrHQjrJPC6$RXcaVo?J2|nA5%cM%7OKbNQtJK90V(WO zn~IT34jnybxPL=;9%GkUoAU-{Cu~M@Z6PBB1pWyiXfB6#FKJuV9(C!?0;2 zlHcp%pyMz$wYICbPfB0TJ9Q;Bf7Gqa?Mg>Tr*}2W+7_jvoB!3Zt%1EKoIgE8Tj2w1 zqfWw2a>Po(zV78;9*ia{A-yfNzgs*h1Z;DSxfmS-UUQOrpk`xk@8t^%Gf{(+IgQmT zm8k0v;U@TY3T1Ekp&k4BpEcSR`JUgHo@2XHPYhM(kt$Zqtlnixl{YU}Hdn8mM!LG) z5MidU^Rr+11xa+iyuG=e+qbt^u=X7{%&@+sFWnN^=E_TV6^M8(j1Al}2CtMF^h*)6 zgSPXyu`=cUO`-L=xD7F8hjaS5uj6{L_U#Xfx!D+)!O`hCY_gTzW?;JVMmoNwdeV6E z6~5@x03poKzGZ17rfjtzl>tv2@4WjiOpbq)pkKL341Q9-(-5mx(#eZG7C9uveu^1n zbp6PUt4tQmDZj`#wRRjYZDXUvJC7;ptdS^B_N=Heeu{fz2-p&qc>@KU$QAWHej@j- z*i3}% z+hs{#bTRO~RDgbOyC@$SK`z5XifQ}J(A;+&Kk8d!?bn1RZD;$6nWTG{J0mZ!^lP$@ zK~qe#;se#3_52ns-rSw>kFm%SR4FXouOXAYs-}goNiYMo@wvbt-3#m7JQjgpolMVe zvoy!{^NH53TZ{6O?b2^gPS-SPAt^@%#Fch4riiYQI=zfZ`}1@kJCTN_>h)?=;#VFZ)8?8` z*l8!pjhl(}yDFsy4+vel{`luR|VB{{zDhC^j5pHOTn(=Ig|-w#nKeycO; z1gURnz35{AdsaLsclx%ub%H*x@+N*^1U%-yZrdwdJ9{jOp0dLr^9Y6e<~1)4nwET7 zGnk;t*2~fy0!Bi^kDTF&V-LJbX!1N8&+N>(@4d4PEJQk-XmAmpI&QQ0aN_7s{yLAz zgl4~#&bI~6$M1@BeHVr&jH%W{LBNz)hN1l!W+BRgDNR;jLCe6yL&%wvWoQ9QyMg;i z_-{Fk%oni^4&2q)>y<4p1k>1|0bee|OCog6qXtywDLo|Z)ZQU>9rW;1XEwaT|Kw@$ z|LYy{|3-{aule&H9P7w`#^kK=6sPr!l?B60u+w-1V-RL?a6j&?C6RXP#8W7>9i5d`1Zp7(kAN-T=-}o-oC|6VWsj# z*^G$VE~s-K)Oo;ye|CC!=R_5yMr(LTzLD2ggJ&jM`mEIC$l9-|mV|+mZ6oGcuTsUF zDnFZ9ZHphNg-LKQBJtlK^m!z*+;k{kyZzjk+Zb(w}p!>kIAYz!cH$9ngf=)~d0GB+BeO?T!i zC)_^D7R-7tQam;@(Ul~dB5dot@oX@^@vMT!^RCkubmIIMb&FNKiQGpl$x_%g z*N#|XO9w@aBzUS%Y@^d4&-2&V^5OSpI&nTDsc32rXA_RZ6f|E;HCiJU$MmSY=~Mck z8|!g6{m+v{#-_2_(CbFvJEzCIeekB=cxGPm)^R^PkzeR5|Lr$6n@9W(x8R=F&G06| zdy+ec$)PpT$Pv44e(O!jPOomF&$xlvxs4&UnwkgN{q&3%GvSeYn9qV`Dc5G{nx7BZ z++}QMO)Y8fW)U0z#-atIGZ$PTcRVOrY^*+Qi<7oHURmML`li~yk!nhV1HK*UelW}1 z%Gp6jA@h%AQOCR#igK3OTFr(YN&cwSa9>GHcP|r7%F5#YjfL$;Y$tm|$mJ4WJM2p+ zavRMQu8c|Y$_<6?+ejO%k(|Txc#e$mVTycJZ#JbuapV5r*j@lweWhB|E)B*Q+8J=Rtb;tR}JIjUv7Fd<*Q#wW`6K!=fEt zljJc9eGt66ASm6-8I%>@wcBJe+2@+)r+CUpa+b?e@muZ-BbJmXTnYRZiE1JBCOhK^ z#&MPYwZvU!ly^3aq5db6P9Cwj%n8f`$LvY-E<@$=EN!>tmCnAc((f^oq|ANUcsTVQ zOSxFlB9QU@?s|w3IC`HfT-sPW*QItRk2?@PCGf_2kq@))8)4y@>e#W6pghB!a5?Lr zBmq*U!x>+Gsh&3QTzs$V{ES$cdcnsgcd%qJtGUp1&kFf|O7d_uy&jj=ddMjh2C3u9 z!;^|1i*dI;ldnGAN$H=bYP>Gh86N8?0*+w2&&v6jqG-)btk z^IGPeewB1zrl^U!w0SwfM@~uHfw{>5ITN9d)WziCw;v9Mk|p#E*4p>W4f=mvZnpN@ zYp^>Rp*T(2Z3G}DOoINFbht*nL6(oCLZu@&c}OQ}vbN4+FDLk3Ned`uoZ53P`;TQ# zGrN>*%kTsX?h+kNpw{(x51qhE6)WSg^kFYovz<1JAcxpHBSL#_5s&>TV+TDzV5jCU zHtkQUpFZXsxoc%`B(*4mApi9D;|FVI#R?aHOYpd9iY}lW_JHindt`sJ+VLA>k85sR z`+cKWN4+*;TK|3rc_ZRP-tgUsL3^xGUC-+)dvJe0zzudRCEb3fsmXKEQNFBc(Dh?< z;fbB#uOFS@E|@WL#fO-0M?jD+c4}17b>~E1a^;-PYi5m%Cw^C<_oq|w%o!BJ=VdGx z-yPDnU|KJ`6i}PzlyX&2H3jE#aA(AEvgm`p=A;K9BlM=lSm93oVE)JK3}}Cm zsMANIh?aYdt$GLg2ks)it(W#$N;(|>rfqs|=S-EneMZJo@n+3Pph@4C)Lpk1Ys@uG zVC|Z;BPfUL{0P$~&6&NZzeG3c$O-o3rtGY@b?kYQ$7oaidrjW=(>^m2IsZ4bLl-7d- z?_D(BY#g3U7{I2Ajs)h`So@tl%Hx#aAF0Nsi#YJX2fL|z;i=x2ZgMkx2-vWytJP|k z@@eatRFP^7)EW-aj?wP6mb94U4ecL2$sg~me<`7b6M{J4q7xknsQQMzccZ9?{zTC> zdD^+c`&EROt1NjZb8_#hs3bh2l`YPidB)>4+h@{nY(dYf(%mpU{ry$ixu(CTZU!~` z4d;sDX9PXZa*5=VPLRwx{EL$x+_&r2%D$7^eB!=AR?gu!1)juS{nizKtk|%cg+_}O zNEzF+-ItAmw!@0_)>g7FSN z%CR0B-^yM$EO4{Q@{%dpdnlpH-@~L>>Q0#h2`)B4r_@Zdv!lKWiu~d^?kT^gB5uaq ztTJ7==+puWkJ@VTP-N7dsEaJ~0Wr@<3$e~u76W}I754cQgWIJj!VNKyE-ubg2wIcm zi|?$@^+%qK*w^z4UDtY70v?Qubq)Hp+n?`D+?9M$=Aand#5eb1!CGW-L8YmBLyKjH zH#lhXuX^0!s|{t;x#-mI*j-DnKN#LwZ8aQ#jrOOEye`K^%dIo%JamHKjo(t<$ZGKR z0nZ*pz{_f2j{A;9uB%g$dSIfAQ@TvPZQ{`7>4?W=CMmLOl=+!7B}z~28f&xv%;NF_ z-tV`pae-_6#YvBWb8~rDtraVx$0>XDKIK2tb(DKJ9{GEd(nJ3H$MA_DA4#cJO{? zqc>;=H@7(-In7VOT=3Csd76^n{f8Mo9HXQlj%)f%dm((|PJioGD^&0mE1O>2$Lb$} z0=SH)N3GQ*L633YCJ%ge(~K@mAKrC|I5M}e@Ug4C8Y0$Y{uZUA99A(=ul*$S(xaq!Vt`Mk)X7w zr+KUtWhfdeu_`aY2EfZxD(c!=TJ7c*Cq{SzQ~ zFAXu;OCcUT2|j=R%sn74B>_PV?t$LhCbLfrU!xty;qIw#HXeE=y>uZ-m*12M4TH5i#S8hK8o9@I6IjR+HUV4k842Y6KXmUN6U%<~}VzqS(tZ-wF*!&B5n>0=;)S ziMx7R#gJWT%R}^SIB@Q&|0W4v?e!+}pNNnGZbyJkD>u$jJ6?33DgX`-Z3T)Uy2h!m z<;fL}X+~TJ<=aOHZjtKm-S4vkmA z)Uxf}fzOFJ{4#~^i2^C$JSSM&*r5F4>F9v3x(~K2XPh!PqI3{~<+xulhqDsI={yLg z)J=)&X;?BHJbZWj>XQjQW0czDn-W}zIv z;KuAwW<)$vV*p2VXd@rMo8sa{4nP z4haIVJei`Kh^~MEfQd$J zw_(cBEWNxQpx4vg?G+n~6QlinA8IdQ=qnVFsS z>zmTkFI}I2SgPGiWi1iCIaK@sSZZ5X#M`6xa>F`QM!;88R&GqLd+I1e&5Gbo^v`(IIZ!au(BNCP z4hL6^Cy$W=ayUid>PvVA$$6`q|j{5WQ{@_U$E^>}2-2ky&W0<*)UJS9YV(N1i z8ii=5G=z&p{(N8g>KH{~(olj0*P&a-57BrxD>-hx<|{)(XrTMYw#C;UKYo;sG4Zpa zKa&N)px3;&6Zn+$4}cOg2s`{0Jbs=AymPGLL*BfN#sw<2&&|xd^uXSSaIZIuigFX! zhm%@U9xDThgONb5!guNSxNcVG!JfG(IKypntXudj2qIP6uaVA-ixfdX-HX8+4X(?| z6O^mLVj!svf)mdvriueh^edN76bVJve1|7i;5Y=qq_~-&0@+^*!k=~zL{X}@x3}9( zRrh}Yn{LM(6qATs+2dy#Sb!lt_ik2Jywj~)1f%_z##k`n;k#(*7y`-T8e<;d;t z(>FVE&)>gzzc4W9ydsENtgwIHVa@(h5@4qKowj!=2J}3n3^ez9e{4$wdJkL>(5Q?R zvnL+1zxjEG|K#>9im5O_UUC7P+j1yz{!A5I;s*6G!ob?1;^?i6Se!xM+fV>pyr1&f z&&|#*$*<-zdl`Z3;-7G(#k^9vra3A=MZXrUdz6kp_5O-|ECp}ycfkbQ%*rxul0=0v zf~W&5J0n#^#IF##YR@9AhB&K1(CEs1{NlJdeFO&(kX!HfuL@WX{2H=d%*~^QFn-F= ze7b(q#bxW+j@n^Ye3TGGe>kPSEeiN%XPRrOjK4Yb!8$nG5U9O#`1DY-u&}V9zvac; znJlX*d9T|F5TSS48G#Z(rhgHZPcGO&+nFY3!}&#G3Lbi#=;V| zQEpDWB8DO_-6vWlfwY%e;;Y4#&0Wtzd$;a6JGXd}1JIX@u6va6 z4mK~n+^O{^Me+0UT7NYQfRB+>Ai8%U-TxS{{(oK+rBPS`pis_7%Tz%fyQoKSI&fuq V*e}PUld2V**VNZ2Qge9tzW~kF?yUd- literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-64.png b/art/icon/frame-studio-64.png new file mode 100644 index 0000000000000000000000000000000000000000..eeaed89e1fb77c733a040608435dadc18ebe615e GIT binary patch literal 1701 zcmV;W23q-vP)3`O?b@!`R@$vn1tSqdg2t%)zz8-Ph&S+uA_fJy7XtA=ih^6M#UL>#H=zk2 z5irC6id?K1f1rZluB0Res&XxMFSNB?x4SdP?<}2hx6I7W?w*~rTc2bzXU_R%cHeiu zbLM(v^mAnNa|-MRmX?+dbi3WpQgOT_Nh4@Eh(5QYcW{+H&5W@|T7DgkM)$S0wubC% zD5VPM7QTdv3rP4)=mU@uU9?z5M%1Pof#`#f5nOz&-|t^V%PylB zm)XV@6%`9Fr3@8Am2W$b=tm zXzy7;;>Xbj-_R$oju6xxRAPKE7~G#&pJV{_`C<}&j*1n?PyQk!hS8YNu2m%c%Ow6y zv{%I-=C20;yGMnSy|awZCy@im<)%R zIeiXh&zuXVak*T&*2Gic2jL*or_Dl*uP$DWDKUJyc>|n+y1^q_SvP<(;8r*U7yoX} z`}(?8cFKfAty&i-AXm!Z06oQP@H*b+5V`?c2E@n}H*$=bcnt?Lp-?N>z8ub$RN^(9 z?hcL^U{}0`lP2WcfON%cIAuakFL*K)ufeySTfG3eBxeT9m^M45_;u@7qqfdx8S~a6 zUrtTPNf+S%{5v&EEn~R&_ms+WATHE^iH4u${Lu9~!0X_)8PTKbrf9&`XbaZy0 z>aoX7x8ZF(hR2Gav$He)1B4ooiFggCO%QHCHsUp$G(o5Ve30@jskMwrC0@fR6NDMS z#aj(v9J}H*oG?L%0l5&bk!usOHo%Vf4_7S12g|JwhgRg$gscp(Bc7k&tX}gGvL#n0 zWNd(4@gh%Ja$!P7227nY9W`&)S;j0|UWc`xtb}N|ut-+~V@6k*#&8n?0Y6UtauTV~ z3`px*;J|kcxNz~jX*>VtSFK)wT(M^D$ClR)SFT(}!-4%shqMf6ZoYzvlb$m)L3G-i z!N0@E1TLQcHvSvaL4PnpPlK7=jRp;>#Fsnvp|bLh__~pJCKo`y>*4q^8zeOb_53kf zDoOQ;W05BQ;-w3AcZ-&mmbmte4B!kNJOp2C-;E6$*C&fF9}0PNHRO?#AbCn50@isA z>hbMR53U13|B2)#%%bNCFTOGWscJr4<9)Dts;(R(AxE|v zw#lQW!S&D!rrR>g0%hkyu%9>Ub;ZTSXm4*vx@ZPum7j^?arY<`Oh|pk$A0@BX!^;x zZQu~V!T?Ej!S(zy#Lge3yl;%xWgFD{r^>22!M5U!f^zBrY0XkSsw8;Tw|(j zZ_A<)u#Q0L_k3+2!~lN2&VD}(Y1kNB@yel1PX9w>;{1O=-Q&Blo(y53OR$4_cr!$iXuwrufqG&G)TTq0G4XTxZS#!#FV)4i zR>xy3zvs-iV2$Zr^BbUR)eT^bT|*W~V2Ev;0=2Qhustq*%Us~X3G1*okRpFW*)$bO<$aLut%6i}fBe)}CDDuzcqZ2XiLg_< z5c4laZiXCVmvo|!(7dcEGsWWqM|11Q5@_WS*x^RnIr6h+y` z%yKV}VAo$8->a?b282SPR>}Zz!)q^vYYYa1`i+dFa|yb;;w>yJY|7VnhWU%qkw|1H z-9!m!bxG#|)ZnL}89YmnXK+niTcUW*4O3>q;qV!c$J529nIMlK@rwe1Kz+}8!#P8I zZVj364;9Du^?5^l&?5&;?ZRy*L` z!R>a}%d*VhBOXacSjP7jx+hMqj7Fmq>F8$ppwM=Dch4YwhKdt<`PA7xBz&*1sk8Y` vzrQ8+F8}}l|Nl~MUPl!;tI>}CO>Bu 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 literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-square-512.png b/art/icon/frame-studio-square-512.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0fcded42861c77e2a7a16b2aa7c80b9cd06b0f GIT binary patch literal 7862 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 UcC{#FIamX1t?Vs}54c7CKhJ}hI{*Lx literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio-square.svg b/art/icon/frame-studio-square.svg new file mode 100644 index 0000000..73d8f14 --- /dev/null +++ b/art/icon/frame-studio-square.svg @@ -0,0 +1 @@ + diff --git a/art/icon/frame-studio.icns b/art/icon/frame-studio.icns new file mode 100644 index 0000000000000000000000000000000000000000..d5626b59ac0d2fb3e1c2706667d9a6ccb5961bba GIT binary patch literal 52502 zcmaI72|QHs_b_~C%wX)s8e#1FlCl&s_9c5{-y%y$*>_`?>{|&LDob`*vI|9#rDRLC z6j>)^9p<_8`@Qe;KF|O2em=$bd+yoK`JQ|3xtFWMJ#PT`#OUg9{|W#=Ngo>MX;6}} zkO2TdsimoE1pcNu`%g*?{^Zyp*aHBhK}%K1I3O?Y=8YT5O7y3{f}W?AqF;uo&?}|f znc@;4BTM)^OK(nl62a&Ztkm$(g+Vmv%cEzhu5aS~7#51cQwvoiV+;3ZhbV*|Hfbt8 zxq8`q$p2Sfj<;j4y>xCRkB}?h@DigmreN<>(X>Yo+J) zQ#D9BRIJNXR+ek@x1V@ew->v9mHfb^a7QDUiSbwVyK9V(7H}@M!xft2at+^t;8UT& zLOVR>E(&J4rDnY{Q>-UN$SrgSA6uui3poWLV z9_DJOlVAnNj7{GCp1SmLKuBWs31(}9!Dh=law{#p&>)`Zq(`<>RyUn)-m`01Ol){$ zPKL+*i_yG#e&el>Ur$lG3Y;Za<^<;~A+^ZDdlxN6;y(OlTncR`{|0+ea2T!3iTy~W z?wD5KtDJh(%pOs%&Dy8tOa`yn}*v zL8P>QdC+U>rb%x|f9f>OemB#DrCv=_e??7^D-z2^wyHthKZySQkO8hB5{WHhGDb4& za@Ps;*BPzFDJdqN8pq}c+|i`gx#>U9cZ4?8UDjJpOdf10zx@17m_+cV!v5f4N3hfK ztem7evwbMr=;)}Jq+mLaC~xbNst3$07RDx`T*qnae~*ri?*70!uFK{R$(m<>iI*qq z0gxk_ztGm!-!lNnl^qz@5SkRN+_=W|f_+=iu_GPxkQAbcc|53$T*wX%IaXF1LW%L` zINlpjNWQ}i+gsOuKdW$jiFqaO$g+l6`t6eQu+`Y~E1Ea{1S)ssA3ivJM@Pq3`Ep51 zi6+9P&zqYOYVTsC>+JG|m*x0-_|lR7M`>$z94S*Vq7b=szaO7t%G*PSIjDt%!}s?B z6CKu3gLhHGo85;4?V(M5NpB7)E#%hBsjO~VS>!rI3*jVAB3`y-3w15M&zTq-cUdwq z$(nxWvG;p^@}Tlt6ltFy)oYHzn0Zp`}~Z}gT{R%S)s!VslglxA`=ahudUO@H3*4vIV=6X1eOREfG}d7Qq3La^lJE?o<%b?K1(FoB`1PXJ^Ym`i zK^v+l?z)O)&KM6CTzC|?!-x_-b~W&->cT*MWRY9SHua;!C2cLFhJVU}8EbFQHg0Cl zy%Ffgqz49I>&IcG#xD#XD_n64E^l1HDP3hFzYO_h-oro1=iKkWh5g*_Bw?MvJNodg z^6HT@i$e#KPrdS$2{gOn)No~EP+?m~aRZy15lZdaNnfr!+Bw%X=thGHIXnCybGfgu zL;p)v6%#*EpQ1d5!G$C}K0f|${axKPam~!1hD!o>NlPiSXl%W(9hdVkr(s0zN0wDg zOQfq(?f(3VCFFEcyM*?a5Ph&dF%HRvt*el7M!(S#UZ>rl9We@gq$8|M!eN3+Vx8u8 z@${@lP*eNgWRT{DVW;?m+h=ENAc>8Qjkk})M|yf$kdll&!08q4Z62=hisOs#fuoz5 z2E3m~+ckXBQneg499&$K_TyX`r&NPB58o>xxDyHB0VNQWIAS`%#edGYs#A)!MoNn% z2>IX7xHO1R`9GX-G>C>iKjW`lAIySC7XQxpdaj#0y$N&dT+YUH)#R<|N+UQotxDwk zQhXPwG)ue;E{=;B-pmC3qnHqBS@{liNkFC$fufb@1rn?(n8UG=3so<8h+lb$`ia*! zSU0s^?%NW{f6z2IIJEgJzuvOI@v`&Kp_AX7f2a4aAje&A@E_Saadvh#l~3#UVWS6a zJX+i?SL3fYJ@>z#b^TLft-3`RTax;ILt|s?y1T$jvK7XT=mc`j1Kky(R9fqLGd1TQfwzsz@Tph%&-(mW_p*e9ugHN?Tn&Y0!mbubm-zf3i2!C}l%@~{U?GOp( znF6x{N8s<_ZDd$-0z<$4DHnI_=#!Bu6P}X+o>SeW>VP4S?<~H^%UFygN)z&~-$8(A zT9D{t8S~GzAYy~Lg4C_Wt5y!?=2K=K)_N{aDB^OihEh2V=aH8@Xtrt@@4dM|vVi(h z@<}dq%Uf;>a~X`}{4H4+6XD*cT_1&eP-M^ZFNVJePY7|3}qYFZqtH?d~9C=_Vw$_r$o^# zsSg7vOLS@Op`}4Rt|*m4LQ=Oy%Ni23cx`$F>7p-my|bjWyVwd|lu(L?G}Bb4xzW{0 zcO@TCn~L<13{+Cb-CFt_T|rU9{j6ZV!e)oIj@OjW(H&#`hfCkW%U)TXSSMeSP0Dk^ z{Jn^!Oy;=lMy7D8HoA3CJ*oi4s4_w#=QX7-!C zM2dL|c6pes`&<$w>jeKJaSpOTAE~9Z3{;>tCv-fJa`n%K0d*CTGU4G8-(b-)nRtil zqWCn^@P}zYPfH6iJw3haTXy%l)sN7cpjTBFxj8i&SsFh)FMj)XguM8hc@7{s+Mx(`r*fFc%;o^gR0eT(H(D!{|$LuagAKrM(({yzMT7GZ;;w5{BI9|4S zCQjVpy6#$F!I?p!*(>GAv=q#)d!et}!|t&@YonI`b)ak|+qf0uwJKa{`3uDw?TAVK z=5Rw^V>=UFcfl=I9*-!ciFy(oDI<4Gm1S&9@r!ncM?Vn)AL>-#UnTvrHAjC!Ffk{^ z`1*D~fQ6Pkd5+eg(RrxkH8q#7?CiUm6B}U8{^yF7!z&NZfg;+PWc2EOhzF_{21X6WVZbCu~65*UwnMx>uZwV1{Sc1Qd19);UTu@-rkF(F-efsG||09d^r01kJj2} z6!wKTYNCPyE~}=wbeT9t1l=YXD^|(A^vCdXAV250G}*4|L~Ue z)H5fGq;iF;OkH8n{u!FOSs7^@7>*w^Rob(EC&!W+@jx+p3l;{I{MKlbp^=QK@6f$ypa7hkPq$k==d_JaJ4 zROkD;R+dEpLzFk2x)*R4Qu@BUE6G8np#D69MxaB7gLYl5x*e+VN_^r0$tGI*pvcKNOeuw>qEWHPToCn@J>S@7~NZ9 z02lwcS;c)3@%!L2$^}CHx0{uO3;>WH{SP-Q42Xs~zghKMw;uqJO8#@R$|t{1|Cg<= zv$dH<^96!4PF~f*lBSR*KfBP(fJQX2t^S?RWzGES^p#}eIezXpc!j6~84PSBB_O08KX^Jgi*7hO@}g)8K?JKYX&SqW;TzGNyZV)tuwP5o8Q9;`vh6J;O1`%L&srz zgt?Ow|9xZ4&nyh-_3C7)zceAAFLGG%iHld?P*;CXYGUH~Qc-SXtYb8j2=P3&IKl1a z)!em~Fx4;hf4_eH%D=!xmdF2PYk&VTg@ddDd$E8vwI(vQqtEDwYT`=|Wdz!e>64Uf zdkU2D{XD7rC8omUkVA&=PC?gRWV12Qr)b%T|gD}S0ewTMZ|?gVf$ zQ*@BzPBLJ5V>%`fuB$_9SK2EtX&BVl{ zBo#og5r4j=nAkl>a^X5PeVvFvhLR?RqL*ZC>yb;xlcy){ngJ1aE=kMDwH7xv`kBfL zzvl&F6mufO)A(elIC!P8n#NDV&`1HEx6n$KXFxstk5Wvp_oS{Ddi}3k54wr;tjH?{+?FNtr81){^%d=p<(+thBYs-lQ5e- z2$brMq<*1tpcsPVbU*Bi3kcuwOAHJUQH{H!Jr&FLHHMOS)aCQA>(rLAo!LQn-pMtc z;jLDGQX!#_4@#GAk`6Il(X220LzkvU?nxct%F^+n{LAu{LDR}pau}b;w|XSX;(*T; zrF29_BOF5ambc&SNc9P@3rFx{zT$LY*9=>4E$b6)B;_0r zn;%6CL)X0nzM>nb7l2-jCs!S;8!_(#0<=+?z&#L91nZvR?A^fWK7lD-*uNPcNdv8U zo97tqx$QOTW6y&NVe0CZ$^GTn&12VgSn?Am`HT@|vXC2(ig1Q&`X2DvJnqUk1{~NQ z@ZotqMY@D<^7n}8ME1Qup|I)o7De}DD}KtsmFNf`t!~W`{3O?LMx$4lcMB#gB5N*3 zdU<*AMcvH1yO$f==|9I`T2}einuxfYYM-Ko$+=v}(5j82&B)Tya_qo!m~{Tu0pD+~ z2!8l|xsVK>)Glu*59p6kUR);dfoe|G5h2fwe$_2QYXa3LOApwXv)hmcM zjF_Yv-Td2xM%bAX(qS`XpjB(De)q41(DC!yZS~BGGu#&hwia?_c_EgHCdm8z7}IW< zKP@mT#C!uzw@m~h0}YT0p{v&JiUDk>=XxI@UswT#Aj(siAOpG{MB!2T2%|Ye(M&}` z^2ox6W-dIr>c$fAjvUEj4U}{{^LvV!$x^rVw8FF)x<26M%vxw{BAgLV$!PpUv*xyB zz|QU7+CyE|rf#w7p}fV1qKZiUT$E1C>q7w)x5JT zf!hpD6>uN0ZR7csVsn4snL?{a6U@DH`Fn$Bk6`*Vd|k0d#vai&zOE4^HRn$HBnkR{ zpj&O{l3VhLSp6lfIfNr}%=T3u=G5QPMk5oaJ9!Y^hOEJr3j-5&iqoN(riaE`EoaND<2pp5F_XjU+GPN0T=wMU@J! zhl<8jQ#}QI*Ch{*5+kO|WHM){3_^0|;qNA(+BO=cG7QS17vT+Cvdk^SdDE=E`R~6& z$U9f=9RGz1^HkC}#>gH2?HjV@Pz(L;63QLo@L2ie{k|Pjdj+gi&e_9$)nY7J!JcVj zMS7+D{Y{w%(yNlS9YiIr7Npu`z^C*HM2GD?_sQuEyRrPftiye!GQ6$TTA{V+!L#qs zmoe=HI?~WS8jJgi9ziDa5yjB^fpIj?P1r+HIz8kdT$z!U^RT-Ut(fr*8gb%`t?aEHM5bOlmMV-&zT1`wEf<(W*Vv`C zORmIBQu^%P-}^}xsK3dh242%$T2(sjM5Wi0j6c_t&u_9Tt@^V^d?~FY$3XN^UnRS*;Lu2DX(;3CGXx=v^GTMrjHakU8L#Z)ST-7vc-4^x~8c zG{ThDq`z7b5cTS!>&Dw)crLvFT$AZhpVFW&t{P#k#8n&NeqM`De$dP)|JIbZrWje| zU_}whe{dI)E`qUn@W#HXaVz=7g9VC!NO8#Ba+eip^mG5stmwbPjVj;dH(GZ7DtCym zyxvMmJmznOoY+?MPa|LkY{JIWhoNT@5&?#Tx-f;66 z$ye?y)sNCfUna?)iB9n#@ zjlsN&YTpPl(Z8smAE_dkxbCq2UL28EGgOaa60^p(FZ*Mes?~b#P&DUb9n}=e)d_9P zg;^;*2LbTBZh`Lao_$ z=>-J@DHD@&EhxOr>&wQjl4r5Rv~_zxj|BGS&@BFWp^X-$-yw3DcrjB#@#@P5 zr;p0zDu43MGfW>%!4;d|+ToOcEOWb3d{rLM%0aeflWHV29(upxmk%rfTg6CoS}&*X{a=BC`8?>r=Xp zLnNsI`JY4vT*Rg2SV~=|w@`nsWG?J{YM-qDz%ECwl);SrA%EfetuNzq?hm+r9Rg$F z^17bAx|b|(Q+{0ce)=q>A@Ai~TN@i2dnYG8Io;S{IX11QMoeTeM@Hmf662BCL!_^& zs>F&r953=b!&q$8-s*OC?BeqE^?iB8pw|L(ntGpH>R2r+D=S;StWE@oQcrJ8g>fEf z5I;~b%Z#mg%bu`AZHx;PeD)Vfmi8)|`!|QB8uj5DOYf&DQ?HNb6toxmLdRXZEo)Z*pW#1zt>((<6KtkDjoa;5!4Lr(h&o?16K?$R!0z1NCXcDBAG_w8>V zeb@zLZ(3MhOSGkHV>D0AX%8AHS0j1StJ-w*Ubcl><9oNt?pvn}(tOSbQ9pcSPw7qe zrF%GMCNqXnv`3>MvL7TO9akV)-6Q^H+OPLlf@f$*VSXbHlvImHg%?d|TfqTnqJc@4 zFWtH{maXd3RPThs7bB8Z$2Ucz#(MAaxa%9LOSopV-o(pi>w7Of3x3KC*YpOf#L#1~ zZ6^@)uNoA9i~n@~K>i;?quYOOf{_2c^OpqxrlqXnSUx{rlrPB_d3WdNtUNpRIM>QM}Df>53RZsxcH+| zKuCa$y)@qcjXbxIO#o_kpCU@4hoZ;FWP{*VOo3df+N}C<{AzY?XY3~B74N;{z5TB_ z5Zafwh*dK!M1UfC2MEneW^#2il~pfa-pQ=+H+_VM`e)i& zDAL5J+BlRiNlDwX57l}E>jeiN%LmTDRW6tStoPn55Kp=L$po&%gaseo;kiez@+(!j z|HCTW{MW9i_qXZkYj&jEWZ2r;+UAy)BU9vF)KW_8$LZZPa^BR;2;p*m$L7kWrv2nQ z1KFZ6zv`*dDYm{^of;oDC;u3Ero&Y|Wn<3wVQmk2fwm2y>uL@l@f;sleGVbXX&k71 zpr=zccr%#_s4>(FPedwvZ`0Hl+t}Zp7T(b8ocUe5?(|%=Y z-@4;XdIVu3{g{n8Oq<26VQat*UkVQIbxchsUT;rG-)0OR;W@b5Gt0Z(Gex2LyVi^t z9-iv@_9Wgg4{jj7P$2S_gQzGo9r;9zS3-}y3Qcia5I}THhbx+}Q^9G=M!O=6VB)CT zhMq(V+#VBd{q_uexa4$4M!w76v(~ zKl_ZJx!Q=q%uX-bh|*%#05#VgBW?MnA8{>0EA=O*5>#(q{&p5 z9%Xikd}|DE^!fey4yj=&9Hnxq%X>gQP{$TuYFzE(@4xkRYHBJWHTo7Hx#XG37#zF6 z|Ej;dZhZzE-aMp<_RR}TiYA1`7L?weOB4Anyv7oZN`xA;9&KjgQiy3ybn5C$9JSeG z=nmo)aNi$n$JmJ4;@+Z)x-^SpzfMfZ>~C;U=wRWf1x4+Sk=IJ9_6LUGE01L(Xhi<4 zi<7C!e)}G|pPULZ;(Sp>H5IAfqvRTznmS_vSOX>Y*52Rpt7JX;^i@HD&)eA`cSKVK zG3(p8C>25|TGfzQ z=LV*Z0wkHkxU9#tA$ie~nex#2R>idJmf+yvxDY|@L;atL!275L{?ED|CMI7GkOX~K zZcN8F?@oSJcH+qRdaf^+m(5o928`4pyh#8L{#X*P8kFvSPv8QZ^+$rBFBy&l#pR)e zxM=iw=j1wgYHH2*WWu-#upIH4HzX-nted@lI_2l*+a3gVY|&z(!!P#ezmUlqJv})l z%xvx4$bsVco~aTG{ETf4Bga}?Um>MQLyYMLgjlOhnnJF~$fT%hKIY{s>iYY5aC3e6 zp$X3UihyA()pxQu4!vyf**7oZm)7`R=9SNE1Z;>qyv%6EMT{r6P#Q5g@OJsa~!XpU6i zV>sMUB$5>5Wha&cr4NPBivm;mT~G^{eOhE@mWwb1IM zm(O^Z;}d6v+ph|^q7rbyW>9s(qhFt(5>}ZD`vJT)`!hg&NrHIXM|25SiR(I_3Ji-S+(yR2CkHs&tdiu=C0^-m_5X0=*2nA z4&pM{dkzCvAD7NnM-C7d0mPZ!yYHbRI_YP2uRMZd(|BS*?~Krqqv^D$KvS_Cj2Pn2 zG@y^r;v$$QF9pv`xo!qr(snpd5P>Mv0JqbBoiY650O_?mz;W0wQfAy_JM*cH2qwM^ zHy1QuJT^WaamxYG8waN;Wb>avWO$DAWpuUK$i5deDcuQzNVERd2!b|2hicjA zhh_lnXHk3x-(2+N;p+@PKbOJA+DXl!FC8GNU^oi=-V=(9L?Ewg0j|`sbN}ClM2q}B z&yijzsiDPLh_UPPx0!mAc;~6fQ9Q2Xoy$4S1-7~9(rW6eGs zVOZ-&A!|0~nHj)fcW?uYDm6J)hax=C3>qYO3uqIVOv^QWULes z&;90zc86u5X5>8_iC4Xy?ZFowlc#Y#v`tr1V6t!lNDqfdHX3gRT)%{0utW4dg3lB6 zaEN82L(Bj*3H&Gsih>&eJshIh=v^~ltzbmIf3Z%mh*vd7n0QWHW|7P z$Kk!cKoK5e23^nw=VdHmeGkxCH+iu`nPW>OH`;gcJx)U>tIOjipAyL{*!v^}7@y>J zBxBhlHzULr)5gE{H7^iLoUG**bf(9v zM0k9oYKOJq%7@jCjQDM1%%NIjo#{_9BDzH&v2z#irs$aQrq<(xA6{dVO+?xqe4=~lvmjA<$_8120#5q8B;oneQq?TPFSCDqGu?5DAecF*nm z-SHlFIO!VVQ1bpg6``s2``>0E9RcJ+%bwjQOD1ODZMi72?<`b@HzO5UcW&4Y>uVo? z<2G_9`9E;%foMcmkK+BCW|kYoZW$YnQ;g%OTivf%Ni7sho>LyUbI$174lqT&b`h&c z9;StL@}|D<4`W1gm_VCmeB}HtG=IBBQ0T{5emjMY$E&2CG0q<5lcE-Utc= z=zVLw|I3=l^dlDe3Rcx}8@e~QHP^AXFN7yaI*GX!MWyu?YBm5e=(hbmWi{@ki1u6e zl`J6e350Bxkf*i&y+7qwL;CZhqvV_I?X^y!2%cD5dq_o;DRlN(WI{I~vpPx1hN+9=o$FES{?r(;JZf9j&EmQK*luxys@Pa!A3r}XutwUuuqUiYw|{rCS{|@A6E*7B(2KMD0tiu(W9he{YyQ2~HN@_)d|C0iN2Sf=o3VP5tkf!B z3o~|ZGHj$8#N1 zYM$#!&1Q0P;P{5cy=Hl8C>17Mu@Mfx;t?@Ial}`#KvKAR(ARLig!4E*bf?v|Om>ramos=!$FY%_ zN;2+bQ1t%hDfj2(R*3M|I))xk>>zbGCoAkOI;Iv#=r6UGy3qHkY?z}#^K5Uhypxer zA?godxCOURyrBH*@=3qWc#}GY><1~9#+RW$MDDZkxO&eLgUv4SWZ8SDK-b+PzJ9C2 zZT9|0uj#v?0xy2EqIxK2F4l*MV4xz;texb)>&<+o9~Duiwah$>*+L`h zVTew~(*Ar1Uh+)9r79xlx8EhS?tL^otd^75uJ!WAv-8#3wviiOB<~_w{(z8(Q8?C~ z{}Mx**OogC(Ge_~ZxNd*;g1CSfrFvq3Tgl#kvp3O{5j0MKRXc~mHv7u^g06jp$ zO!tUox2}_0Pwi}BjyJ!M=C-;dkMFoSzxA2VFhvKdy|%0e9IwCgzI|NkuPD<**7Cr# zz9;7aU(w=Bu+Yrr&yl2We|guB^@u8e@wbA#uLz*Oc6;*jQ0%>J@uRAj@rfZ~Y*9{g zr*hcW47*uqGR#ek)Ej+W%ue@}mF_?{=^5tOrOh4xF3@o+%4`)&9e5O=4=V*PL0qg0bO?GhU>5P0O5m zdmb8^eSHC+BK@W|WPiH!eh3T`%4=N|R?0v8aBmb^BQj9ofPS97rnCt@NwxWiFJh?< zM(z98#6n2-=q5O+_9_PKS^c7KXW|_vhUqcz8usi=Oz7G||95;rxp)mN?Q-SS`%ce@ ztY!D}#l8`B&%qDBE+5~f4XU1G8j4zuAbZsYcs0)iVv^S760Ta%L{u5MR( z&fzUr-anIN94fj>nlU|VH!M%x+_^qd3FM%s&lCWXd+k!CJqZ(*5FdKXKPp4-b#$j}(SIA$g+k`;4XT<2CJep>Dma}^hy0GbP-7acBRdU~QF827;yNk*5txK*uiQ6MKd&*huj^(87xyvk; zc3-K}Um`ZQhZjEX=MrYW{&>mJZ7mXPI$J<3R6yT#)d+BA3Vt>RWfp}?kgGbv1ThLP z0H-sMFyBOdeSPWkgvi`6q@h_5_~9pdTutBVPp|@p-IYF6KZd(V>QihAPWYu2jpMa= z&ERdP4Y~3T&!?Y9MkL;Z6HYlkG-AR~T%uYB!NEa6u|7UN2h(?!f?*?5Q)N`Xxd^V$ zQ#zdsrrPa%^Bdq(@(0rSVxwoapF`>H=%b@TBJ~|vMo76neoavCFWbBgJ3Wp&lU$kf!7cjGb$Cr1$Ab=FnuqXAldkO~!FZ||J}BWMpe%1Fm` z3N8@XL!KSH7R#DtGYegOj#hmji)4tlEjzdtF3Av}UkuId7qF9!7eIoOjmoxT+lOQ( z*WkFZkH+&?1tT`ls)gw|@l0H~%uE7xf4L9Y{BOUD%V=}|i1_X?UO4Us%;(BIyT?bH zo16E*{4?PvOM=kGD9^g2nAJII*)>Iz?OG6wpyV%L(wRtR2p3sqG(i$Uct>h5FLRuV z3^0=>qsl@au*VC~NUj%%EKrA}SQ@ZGCbF}VB2zRG>jZ`p%Qp%+&IHL_LiILuk~3hY5wZhrav*{i5RKjS5p z>W7y@PudM4tE(1SkENu11u^nd(lyQQtZ$6fVI+|PfyEy>PUsw~2((uUO&|e|77|w4 z^Yl3!Nol^NB*L^YS~fk&ZLuW-Jv%?G32H9t15lhMEky>$tk$;5MSizUXuw? z{&azfry(D`9iHkgY~ZY!1T(}NuWY4-9)58QBXCmOK9Y0Ir|-T8PIWWo*?Bu*4I5Y% z)dfTU#lTzH&3Dtf6cJI?F*$Dv>(}*WEN;1psr+R6%Cko7nU?MX9kp2UEgEWD+hq*Q zFI5!JR{KuworXXD1|Bg_P9@pLeA<^bZ*%{fPXEu%$|=X$WS-2*X8Zr0%wJ_yykcag zXZ?Rn=C1;P_~QRPnTLXC(0`Np-GH&9|LT`&OE5kQ^&F#}u`d})Bi z2!Km^8aVjf5VQzf8ldUHpJef%+GvO&xcvV|7FFi^lrPs5JYHcwaItUdYx2vzmocZ0 zqo|I9AHe{K{II2S$=k|{DN}Q;eaR*c4i4ocBqYJngAby|p2~RIn7X;Si7q{!dhFNW zu;?CcBLHYZgrO-@t_^JE_#Sa$zY2B^4qGV+i4p!6NH|KZrM30(RPHZ{;J{M0IP-o* zH6Yb~FUPMl|JLO46d9|iS#8+Kk(9EK0f1DUClX*GQwlNwZjVh(9r}2X9jTo3*m&{- z0up3OUIGAZu44G<;?!&B&QY6=Ps!~tfYd9Ott2Hm`P8Q;mhwc|5yYu2KjE$zeRj9+ z(q7*b6d3F+O`@`L@B1AZ8ahE*_#B9K*_>(qR^QOjJ_Ry~43~ibPn~$@_2lxeOG<8M zynXvtBgVi0cxnn3ys(G)`VF?9O)YS*o1lPd*L4Tsy$ivv4HgRRot*^jVdTy_%?j-*Sw+ufax_uym9rWJ!n1|WhhR9N`zKQ zkAUZ$oK)ot}8LZ)|8Q0rd-PmZidV+-8k&!csOulSr_UK$IvhYsak&01Jk z$XXnK8*UG?dp$HX#567bfiOK_4YGMICC!5KyT>;30tdJIv-s|Y$U&!*=$`-x;5A$f zA8HuJQ}6BW%2z;zx9QQ3tpA2rSc%!%b&?(<00Yv!oX*Etn42CJcK3JcF(pu;B~rA% z|Gw36AEDy}na2a5?adkSV|DZS(kv3Ck%Cr`8cu@3#;duxO9{e<-)bh&dw?d_icwTVH_3+oN~ua!6M+<`|^4+`R4Su zZzOTCvG)x0e)__&@4CaA;RFN}z$!^2?`J2$^4!IRKjz3Y0^xFVBZ+Y>xaQ4;{7w_^YN z`NPd|`D26?JrJ%)@M>OHoUu2B05tiQLRlY$Cn_!d3nayM;MmsIU{lto&9UF`0Pvt@ zuz6T=SxrGfp*+(*XBKgPv`8)Z+wc8oMF1t1eN}GichCSf6uiWC?v$a|PM=yvZT!dp z%)#HJq$GvllcT?GnMye#061KgGb1|AZ=W#-0G_@%DIC1Ly`5UfLDCcu!7oq!PPql_ zfo0}H>nH6l?@AbjfJ$8>G4@mP4MaGAPFTOw$2*;An3MJrYEV~BAQ!F>S#=~kjXaV52pFwbN zs{^lHjFH%1cvm-JP6Xd61OSp<1+Si6cGla%;c&XzwZ?OBu&Fx~IaZpr(cP7zRpqXsaa(k*D1gCSs(8C1f6;RWv;rvhEi@E(icM@PU4Pf%o5b z9E5>!cbhwNGm;PhBWk9;`OEp`ZKNfn#&dlRyj86xTo!f^Jj=7VDVe|SAWjRuV)ydR zOYtv(`+uo{uHD|OoHTLi8a{7K{;eZn0HlEizSkr9ySG4gmlL^E#PF<}Q|&lsGqV(M*wIVhFjU;Q+w$!?|>= zrB9H{P!F6cQv+x^?UzQ5<}6x}mCeoB;e>%r(0iXICx(sHQ0Y)R6-It?0JtLhN++oZ zsGm-Kw+0&Wv@wtcwJYUY$b@63#!~+HZ-8CNI3*$mE^A&j4uTxvlf`VdKkf)H? z7i?+C&$R9S>0EAY$5BKR!!{m=007kmi5P(~DPWL5G&M3iTNS!DTi?`wLS6CoWWSmL z<3ldzpkhhXg;z|aEI8^7U;SDgR@wMF41e#taCM6^2OihjVBbRw(JZN%fJ8HngKU2L zk8hLjCO35+BNn!T|2(pJMLyg)IM=5Fd=^~y1?-9U?4haJdxeENL(OJ!@|~wPT*2$7 zJ}2w=0YM15g{}DPHS!Yz$>$H$grADn^+^<$11(QlTi5G^c>&E@iPzE%Za>*j|Br%^!k^E6y_Aq@DvfHVE5<-lCKGCxw8l09K=1IBYky&$7IWS|>G{_(I8VuVZI-6~K%v37#v;V+1xaYtCu0Gi9)ga=?muYzFpXhJsV4M`ANhm>#xF7e=! zf|O7R_A7tD#+H^FUy=fv9S3$^@wkGk+5inT$bz=J^Nd9h$O0N2z5~+LgLM6*1RNOl z6|mQ$p*>vyS&U)A83Hwq8~wlD-Urg)Vu&GH5_n(E^sJO9(rJjWg#V}2!<69 z0QyB0@!>~5b`XSXx2oCa{sm+dUfck z9S;zw4nbGZoW8xgU|VMV4EJ|GbUy#+YjrrMdzmy6jX=*D0I96e;Wa@ETRR%i*~85y z@THcWJ!(51im^iBqu^%;0L9D;1gb#Lzi3b27Sn;riLuKBmJo0tNBw{#SL^HqL>fX< zndkvW#s!Xw^NIp{4nt9cmhyBfnq59C45aD^^td6AdKHu=adygR5|J2@=K$&^DZ$SE ztX7mWM}6l2VVkK$!JV+WoVO*t~a zC=N%RoqbjZ$21V=r=XHTppx}xrk~M#PJm*@Kowp}ogL@Dprz{cU@6qlAX(r*ZJgOw znu5gCgDN}#lihnuWYxBmwl3!pB6LMqM-KNkWj zrV1Kf$Npc%Sl~b{H$Lho?|-H8kO8;V4oC(p&m6uA`d5%1=#2$cpZKrpNC6-ozQ7@} z^3;~*}p#6qt4maT71s)UuC?imA(SMCwRED4(LC>jC{zu{72q;DqR3zcQio}D8 z6oQJ-p6aWfTPgrD73x-GUQm=eH&HbPiWvbZKrZJljfa8+pucEOzksf(z6EXk<|hZS z#{yW_=x~YdrPtTN)BEo$y>O>Bg6a|FtN2w>b?&k;tqZ*;{d%IQxcd4VJ> zU}piU8XYcweK&0%Om{5Iu2=w=JU59${G4WGo^lZ9pHdtvF93IKTie>)Bt@h z&h}hfLK4(K;(TB5Z~q8tfI8pL{o6l;8ql2WJDLCOD-Dne~_G&U5Ne8PYF%7TXl{ ze*`3ORL?oVQ>8O^q*Bvk62Ws#drBH*3mv-d++4$}jovc^B;(J5!mUF%{Xl*5EY^;! zaFoM2WfA(E5*z_gfCZHHG#+^tIkYrL6nRcWbuM~J1=0_kiO zF5v~}4c>DJb(nK+gbDzH=iW%;Jy+?sGDLH-EpW$Y_UdDrQ(vplPgy}bg4O)^G{G>$ z!G~=Qge92qyiA0k#Y9I#0J#)LXm+-(Xy2dKm`vw5J-ewGSKfjK*oWPRT z0N&p_sk%7=&+9|#MKW|Ec$4os9j0$oJ>VWbcS5=nBpz{YBP|%f@+^!;OB3xP2>j4@ z{YbF8Gx@AhE@}I#+RYI&{}{t@auC;@4GRBCI4!10se9^Z+E~EQxLTZhZ)B&yUqHo5 zf6?}^;3!#UU`HOJR(t#1*VXm)&mCZumESx)o>(Vb6>$GPGBUC}22%%a*FE^5 z-hSHa;JY(-D&*c>qnMER8^gcdLzw(wQy9k)XciCVlzJwn4}6~F#m3_Fi9zc!fo_f` z6)U_LoIVvCY{A4J(TX)Mi?YFlFsH{v)7QdiPSK%i!jS#hVCsbe2{N#KGlr6e+pQ%S zK}iWdSb(R36r?687T@B=z<7ZDrX40nk{DtYLI>`4@gS=Lf;6~ zd{~+ctIZ=5{_=fgPn#Qxe z^&KXeDM#w#xDOwS{i?Y?X2<^e>{;U+n{(}2Plte{RH(aeJf9T z%!`eVW(7rwW)`&f%8&j%LArFw`lMXLC#wHq&eN_PhWS8rdJ|1x33<}C5`L&Q9uO19 zJoUUOs3xmm)Q#h=y6VY^?_s~X0=~ST-Yvf42(CLB7NCd+RJEJ5@7rwpubJGv(z8^4 zv^a-aaCw|{uOoW!Q|Yi`sd?Z58Hz zjDIMfRNU2~np$OKSQ)e9nI~gujtt{SI0u#7yp`n4N5NK?|K(SlPV~--i`3`H`y}CG zF}5T9Ba_e6xAId_Ze;W7)ymrsCgv!24Ano|>i*UH^|>!pdcJqI zOLp$BY3<^oh~c#)a0EvlZq$4@hwJIUC(5FZWzeYe=SkV`j8YzX(Lf86?oPuW7Puol zfUVOp(o221t_ZGYY!BIjJ|BhiwC=Uh8R?SQe7pc-H-B$cF;VwQoJxGw|TDIT%+2!h`cPTB2a-F4FjKV3);{_=yuRi@P`cyizjnv%Y zLfX7EiC;HcUCwD5YK^7*H8*yhUcv=bb|q_1T2r1VZ*$-0?o&5jCXu@@++(<2d)2(1 zdmr%FiMr9l^U>L}3AdV*8jhInEIqu;F}eTx_zw$mbCfHo5O#G#pK1b*)yK`hosOOj zcw8o_0i&nzMBK_rFb1 zhxps{g?a&sMe4(#Jx)fx1hxYW11YI{gG%G4@ z9aQCLp}D~SDY{KkzI@h3UYd34$)*0mvX2I&1etY;+RJ#KY@y9*z*+ICm_Y$;`MI0Y zwf;S;edn0R#78XC6pzbn-9=6vO?=YyCjEo6?Pj$czjvm;PQ8EdqxE_^Nk$~jAvml5 zEB|#Tx()Y*AytPx`_m2EN0s)S8KKQS*k0-N$*BV6)(P)8~W4kGFew2?bI6C=&5T=&fkojV%~`?UGc1G zyRBT(F=3=u72ERUuhf2CazLC;lyIXWF#5xOKdx}CI|V?MUMa6^Tn3Nwr`~X^&>M6v zn`>N)OZ?o+d*J%sd4qecJV;>59lO%HAbQ~XyJ?A-_q<5l>c=J9T@IQXZgXKw9<^&6 z)N0RBMf7pfM=*IQH2L}T_Lb|*H+Efff*pqdoF#m`toe}7NqAMU(+LPf2ZXtiG3+`n z@p}tXxwif|NKOIRkG4NMiSG7!D6VeY3_Fa0&oQ!SX-qzuco||X&ri;a+r`hV2AhYQ zJGbU@uRC_CJ%AMPQ=P2#UZG(n^=$OHqS|}1NkRbN{b@KP;!eL-!;zwDL`qU!-YIl| zyDQ9T1r4r5|Nmbt{(riM{C^WB?=AaXj5ahp3t#HAFN|ssF33;jn>e-a8rUG@$Qlvq zS>G25!C!W&L2Og|EGfB?$`vswwXY9euiYC(&a2)6|8WGjm#z$$TDLq z=Ep$o!n{OxeP@;bCUMcZFZAv|UYS7-!mft6G|yn=Ni`2d|2V`Vctb z746k}@5#IXO0?$-A8S2(0%(-vf}19`O+4p zH&#hs$~Zw^?<-YHy<0gX5d-HWSEN|MWp_yHPqj$f&@twB!x-;AQ%jw>&OK#iSNFGZ z@fxSVmb**Rntw9$#H53-H_OVompQX1b~*jU3hB9VYhI_-Fhj1mW2q|yz3ayNPcl7I zBLMx!@;^~~v^&~;8lL!qm)E@#LJiOPzWkLxla+w!rdrOF%^5K?WR7>W=9aa5UrK11 z8W))xTe!Aj>-U?iS4ve4wz-)4+$U^xazAV8{H~Tuq&kR^!d;`ZmC1cwm`2I>G^|i- zE})}#IYNToc$QvKw|x{f#J7mKhb*=k@#=f_m#ivETOYuh4%I755Q*mRYPfH0;J$@1bu}@9_PV_$i#nJ^xGS_|o2ItjT2(EXR26x4TJXrw%TS z)Q<`|b1#hQpIK9;97;OUrQ{nt`&qph*L66%n9%l1@7STZWZ9AaIB$o`ZysXZajDK> ziWgnSx{qPEskyKg$s3T~9%1%9KK!7_zkG3tC01~PH`sq`?AG-sP75Qepk7=l&Uhd@AF@+I7i|9)*DvP6U6DnTOJ zlO-jzs+(VYpn++vgoRV(oHhMpl~BT91n;BwrGuuI$BPY}!WWAY4|$)+?8qHY{q+2T zz!WPWA+D>G%V@R1>`he;GO2QP7LAc}OF{H=xJKFTmN31vvP;VIbGOC?w)aIuWn8X$ z$Yr!%gqykrxeWJGK9sX~-hQ@FW?{=$K6W;3DigtV-O-VZ=_mV2_ zXVDh^=?G>i8JW)1i>P<_SN#2C_0UZNIJ4q*8dnM^@C9de+A>Y_IfvQ zhFn?d{6Wg;$3FH+fjPVb0y=5GkEMSSQrS`4^^2c+l#{xy{KBShoa-Lx9X}lMq|5Vy zC9}@@x?S{@?u`N$7dQOQ7ee3AI%e4)O49E{UlXzpt9h!UJMl&GghtrUTUTc+b9Nd3 zR1$a5mmlM>6F_Mxek^~>^1k!Ex5~~t{5+p?U~hTkph-XjYc5hQtIH#@!69ml{)f| zR;O>v`Xm_jaIsO3%b!#4a(agXEi3tNjz{Ny>!q{#M+3{V923sU?oOaOpV#z%SIpnD zoYed)+{LUPW;etKk;k-yhi68-Gln*e@lAwE`mv&Ew&7PF^UC_ad&B{!NCu3RX3Xw0 z{V!gd==Zx%BnP`$_daJn>v&d?mJIk`$U9b;N7nlBel`+oYxf}ed_KDBn6llb>irfv z%$+3~w#mtpIrOsbpd+pC6PH{XFSF7=0nF-9< zx(0!aEZ!5PKW8+2-mJ1@a3^ygllbX=-QpO_s)S}wS*ErrJ9EyAkJ`5rrvB%XldhH~ zknI++tAmrWa;}&1|lo5A)>>ssf zg(1mK5-#ip{2~&)j=G8Rybhc@URiEXui{&Ly$q&&HeZHx7a9K_e%t~^=y@X z=wx#Aajh^UaloXL?+z@frudzI@q$b4#R6;4sn9Dv((}R&7Bi)P`Lw(;?9uYahb=EZ zr0LfWB~e#gzgzOeh5W6ptz>_u#m@n^>z{nPGUaJQvX?biZ+XU$V^}u$=fqtNIMrg1 zb|v$ZT!3k6x>QE?<;CwNpQrpO3SE1w_thGpgs$)3zk4j@y};9S8EzFb>JHuKzMknB z8RrQ;?asC&p-anso_|vINsp79JseXa*rhu;0+)%(dnO95S?aQEe2exz9l>vpV zWeGd7PA#4+{f%X=&>d{_em31vlvUugKlKL9?#nBR`&@PY)0#2S6+0wYSegJ9{I@Yg zg}+kO3Ms8#A?ui!VyW5Vvr9`_G2J!c=Rl;}eYX;eF=NMC2t0eH<2s{mU%73}KN@)d zZqIXyll^#*cmJ+c-Z|(}9>>Ia1@uHwap`mPIaVX*3jESON;7iRw|MT#&mKy!G0rhG zZQEaem-)*>`Ch)A){T#nBlpH^wv3PIeJq_b;AcwQxK97G_kq;entXgLnoJs&m2;VN zL^PEi?+T>c@yEKKzM|YwpIv(3A4f>u_^IMWn!d3hcKI7=o#*lP5z#BwnHL;m_XfqN zIX+Q)b~2{(^kC$z{38iFX0XwrByH?l`HbKv|Do}}V>I8NI}T@Gcxc@3-mWu}b=F3! z@J=7L-001-aKkg||MW%uq+{P$f8Lz(E%!b))cz&^IQi3cb!_~Cd5DXX{S9MIxFnJ4 zlZ{>7*VfBd^1Za0BS4_^kL*K6xMj#&sTa2E;W+z-2p|09*UrUd#9Lke&5hM?Y4&#z zd)D%s4=#>5R3MLQaA*B*{h7c9d4zI!?m0K zo##)wmKv*q?!s6aFC~@=m4OZ)v4FvQ946 zLskXv-VKk%Azy;ai<=A9{U>8AjbmpQXG>qyZiZww;|c9?BmE6|9A@&-O4AxjD-e35 zs<(H)fkD01J{bk1ezsTdV4o&r02rLm*twIQA+vHO#sBdV1Xp?yZhAF!v^;fK((?URRM4W%vc^ni)Lt5sBlj2Z?$Gs^JJzeyg!AwgRy1$=_lsC8(h3Ez+zcb-FkxKiEafa?< z1RfMdGjns;5AlWuxYfNne{9GxdFAZ`q={pFzBwzMv>{LD>J6-NK+$O5v88LIq`F|$ z@A|p3{xsyB*kz*t=;SJYd$*NgNN!4$>qXwm+iF`odl=jTlet><=)0qD8Sii4?^$l)QEB+@5MaG;OLs2o!wn?=@gjg@~0Op zz4Nl&G;_WU<-8N{g}74Xyyj;Bx1PQigAApb%^MG^a+__})n%>L>Xb$3Z-0Ve>gBw{ z8!WkdRL@+I&B=eDhAXPv=Gu`r>Z8|pUncV?r%D@S5piboAoT0QewQvxCetCom~w|)NkhZKzO4gBCV8{JAeU;{ zWPWYr_jFwT>vPg07at$`7<6H!;4%($_Vb|Y5N4EmG>f0=4jDz73Z&9wOIzVfrcdKe z&L={~SrC?bSn*XDN_?6M32pWYr2UZXb02)D<860$Pxl=s!t)CeA<1u;*4O2P%E=jOV{UXR_G6;xS=ko_;mD2K*FdE!ueHL|s}?KY0= zTWhCm&5x))*)J6^CX?UX;pn+^v?*50drTe9O&=axv-1FCr@Vi`u#OB5FAKPMF?au( zhhZGK$M#!WKeXjoE(&ode}YyBUv}u9y?kKT?cps{xwawqN;`}^2!Jo1v|BTExPJp9 zFq5*na+xKh=_#FecOTEaL&aZjYP-?zyqc9R&)L3Mzpsn0l1qVPeSB%F)5adic1j|x zX0YA`=$`GxcJ-Sn@lzWNE2SYD&#h?X#ix?_+>jdRT2VLtw4$P7`~4%n$GLW z*TQ2-Z4)t+YYAzGKW_9LBogn=lt_{?Uf!VwY1NMm4Lx$F1VFl1#<-UHFUmjt1_SO> z?J*ru$f(d~@@Jo6YSWr;BLgtPb?Soiv{XnU-6OK^2_H3T3kg-;>o{L?;jiv5?CcE% z6k7+$i8@+|iimtC-uVrtq$T*wF7!Cs1W7OnE=M?mgdWMlt7YxCu~PW>M7_msk=Kas zvcXO(c5yzfZ)XiZGUF0mCMZaAym)cKT;ymkKRF^|Nm-*|Wzne5O@vC$i28fC4tM+c z`{#&kMy5^nvK-@uws#wBgdQm)@<5y8!V;~(Z~tKJpW3`C>XMMrRc8|Vpi5D(U0iGH z_KqpNb>Sjru^EvbtwvOrzrl;6J4;}So4&oj8QEAD%3Ms2rkb|C3`WS=D@@XMWO#T| zV>*-1(;TJce1(~IKVHv4zh@nu{rY0oJ+Cj!tBP_<1y1mnfCJJ~QxAVs<_P9Rx5fCG z-6by;s~~mNmVf&mlG7eY8l4(_INoQ)6}b|vQ=e_~FA1Cts=B$5BPza!#QP>$KWx^` z*?B&kxku^(?e<2<{%}lu`696Tltre6F3qj;watw*Gqk6`?#(~?@SBF3w%U6mJKqPH z&(Cv^S21knN2ay3>cyp{X?Bny`RBUX3NxW&Wj>R4FPAXREq3bMDdjGb>JtqiZ0?oS zqP+qKYcgQ*cq~eKWS}^mQ`Bd;s9lx??tX4DM&bQG9JjO8W95BzXIjK;=9=Bq8tlo5 z{+^%uR#wzHEML0fb<@pRX7Jx|-dpQj4xfdD=1!PZ`&(n*DRU|3oslDvZ?+A#1qSuB z54J_i%F0>`E?+*&n@NTn72l}=PxwBwQoct03opfd<(B1G13x#~Vn@Or#ap$%{G`{O zXx;YGFCZYGrLvNobK4MZOyCr|wXnQA@UcM3yhBgsY$~_cX93NT@vSQnS8mU&d%i@+ zr^!dma(~8cX$c9ND$78)jKmYsy`FRaA1AE;KM#tMF!+HSh8?`4RswDO`|Tb3zg@oR z?oD{B|J%2BR8GZ4cXO18J3+iY_# zm->&0zjEu#DzP2nDF!K<3?fvk-#_{r=1kk5caLIZzw+LrbBdb{c5hrayJv(?US^N5 zaH_1zcAHiQuZQve^_V};rhgP`KR>oH@zs@%ioGH_G5O)lIj^}2+rf@2S|k9Vc1jNZ zO|6E6S>P-{f#82mB>W9JPR9RDT@9S)|E5=0Cz1SVKfi^f8#iv$_4W0|PHA^H4ST4I z>c8Aon^)B*&`|Ugo4)OFA~C33UGjTZlPc3&0-|zU=kSgyPy8RAonr^F&K<)$9|o1b znmf=?l=fXUjAoMKP?+{z-KP0M>~r|!W+{hQi6Zs1nAUXT-tppdbuqHdw&C7Jl=WSz zl2JyKT}qPUw^^~bz?y0E(=mg0@qawZioK1V*iAl?c~jM1rE1;dTg51Xzv+nNa)(N6fSjStcR>X3arlvMtVquw+szo}26 z{7g|hv%1i=-HR*No&zbq`}+0kXmjsXJ@wgfaQ5)GH!cMV?^MwLr>bvcWJH9t_BKL` z_{5cM>V4#K6BBZt)|1hpnE7Qxs%e&!yI+886*cFVFDn|p_M%+>`SXc2mBzei#mh|s zEFR?u=CtVkaUbAjW{#pW|B(Go&P97L{(|PBCcSpiMnLeq06w1duGw};!vWL8m`GEj znvdXbFAsASZz>;KwD4PasokKbtD)iTORnWb1ywPbnVI|3^rjPI5-4|ZC;|*S#@s4S>kjIRQOG=VAn;eu2Gci={Y3_Z_ zp>`jUn~hxB^7=mx3yvomHzeMPl;A|@!Im4;(cvAxGKYPd0IS^31&L*QWO>ooQ<}9D zvjxtDH5%cpu8pi!FjJo0KXqozC^!mCa1~5o`uX$c5xxvn67~3x0_Wx@<+TgVLlhG~ z#y)z8E<}tA?tC1HLnqhV{-=_s2);%un{M@$l7+dty8P)*yR%GC zQr0&UW8;~w=5DQa?7A9@nazRBj%`8Hx(2y(tFIQNu9)bZX1u0`s%B-}uZz{_R&Zrn zduYas-iR87h?y|(SDWG;N^);|?fIvOQ+^LhTG`abs!4x$p}B9kewGt8a)Gijfr%&6 z^hNkMQ^S(*M^CCYKX1!(sGVx=qPY5LL+DF-a+rUof)2~g&28-cbZsC>b{$gj2$u32 zN_%zTd&M>;iT+!S;Vz9(4YDH2}b57?FEGw%Ow^)HRJV6OcNhgQy`d8^@q8RB`lX4@MR6UiG4^~ zik5{#?l!8;7v^aCGl2aGMN9Ni(zsjkS8V1qofB1tKK=fMe|X3A#cZkDkH%cl1pS;%G8`Ug* zfOaSH4~&S|{qo_-G+{25y_c7lBYEi=lEU|ceOSLUJ@KxT-kqS)Y&{ratMMIkDb0Vz zADWn$xWy(W>S-}w^ByHxqg)whx}FmA$&}#mFcaDri_Q;A_NfD39OQU#5ZN7Dt~1@J zm2d)RDZ^rchf!0HYj? zYKN?FGv_Ucp`vqY2TUx(${-Lh()`mc!6@&=vLW7|->6SLg?w6|K|xNp_vz^3nPcKu z&Vd@iv`LJL3&@o}pzYTz3$VAh_X-MPWm6@JIjH*hkR3{$a2*y5s{vGvyu&+KqN`W0 zK2;XDI2(5D+ZAqfp@~j8shRMo8jFXHIfYM)V;^AN*e1lqmEoE$3`>jbzEHxERF#&N zCUuuZ_JLp1c0-fm&o_tWc$aoQbZajhjZ2C?L25KPruhepQDb%AOOTd=y>4IXcdTgK zLz7l;`rS>*Qtxrr@kY%T$mub3er-ZfA8or%f*8mti0EHB?#KAq(b3Vr`+~^X*H?uj#Y#Em^G;mUm6&wi;Y?;u8*80)=Ny{x)z z3m4-AA8C;1RsUAB<3Tlcq}l!L_IGsHw=Vg^8lG$cBDVYM~wLI*~g}yDAtbQpE?&Y6Rs+1?vooB7*G~Vg6zT=YUfg?!X4d6AH^kGw{;TI}M;)7(%Wp%zm)Fpd9KNvOW+M0XXZEmd7RzUtB%E$oyv|$fTN`eUqHr_lAas#MKs&$V>-jhx#*>3NbrkBVH4iV&j{5(e-5~bZ zL}gp7hI_@km>m~%-SLhKcpLrDP1G7ny0)C(pjrgUZqv~T{GPm-r4@l`!DJ=#KNXlR z2fuv^aqZ;FW$_Bg!0!C{-(JH!m_?s7%+-4ZSI5-V)n~t46V&xNq~gI*d0X=GhxM_| zy#>?PC=CF>>2bl0v2xhsMBPH~2DhTUYS4z6<`mX8&58Bk0xt_9DjqC1jf;=(T(b>B>)vyr$%_V{4g9hHLbaJ zRf2zst2UFI7#EaFfkP-%e5)XOOjlL)QqlbUykMH`z?({5bgcyfu3od?G#($(Vzhd5 zpnC|COSfJ9hRlmNaoRzHSfLEl3@y(PC8@i#=3at9mlj;I2Z`PG7{H>P88le4(a{IO zq{zpQ9u=R5`1EIY_q3}0E@qW-ny%?zr;I>o;g;0CjghA7GSTocAsaS~?T$t$M1DVPloo;ig;MD295aC$Grz;7)+^7lc&!8bwA9+vxu z+<{+~F4#{E)CI%J9JL1G5_g_vqb%PH3X$mQZ`6EU6LHt*}RO#HrWl&dsy=y_csgaRUT|?)amQ z(CKR)Ww=GmPL|g_kgDP4`Lz}CsO;D=6FVF{WQm)*rPIOQ{%01rVGhqC1kNHm+zr-9 zNN8>6roO@@Bu|t=fD3^+7Qd<^ya-m=Wd2F9wu_hkRZ-yp+xVPW6^YzSh4`nIRxLePV zz8_>(f2a?e00l{fO@*b3uBHA!l1KPf@2U-y&0N7OA$mh`ZkPPh?(+t_%v~wa)Q0h- z?U{Zxz4vDYFH{t`t$s>FQc71#%l}*tA5|heRXV(g@W;AK@2VH)E^;#TD|kPG@h>z9 zRv*GAgTXwl@6VgvyUrdNRx2#CRS)xy0iDbUX*w`gg~jpL#S43cORoQ_Xn*O~hgg{Pkt zY(?QfI%#-ZIzD>SwKYrOos9?rz7hm`On;zP==rzyo8=K=Tbp8HQJVY6n_&qCbJ{}_ z-mJ4vYai{xDgF0-`~?GiR5cc+5>7!)waPL7ISW82=&T>`3Blxz$1lM(mT~MDxH_wI z+5=x-E2Bm_i`=`v;CslO;x9z35zJ-mExt1M419ZjEPtROnOmH~U0EF=F`md#`MA8n z51$isRef{Cr*%FJu>SHVzX1udI7T^#uMHOuHJw{)D%8`E(kmXwO&%Y?VA-fx1C=hK zEl79^m$uKja|78<83wkbMy*A#5W1+)L~l6ENx$cmTYZYuGXkbY3SXw5A#s0hYCZ;~ z?0wBS98U1(N$Oe9gqgv;|557r_g${IaXDYoCFn>#M#GhxH3?Xg7^l>XQeL1|*jDro zFM62!g+mX~{reo1dlCk{e?X*l__8A6HAhnOFyl4u$d%`33j14ITc4%u7Qez>`6O8C zwR2s}j&9TLr^Yb#&@&6+D!A=z(|Vdd^#w(gaM-~%${K8s(85O7&vi$R9LWi}=>*HF zY4;<8lQH;o-N^9rQ~UXRAc7ZVe$bZVroMx|poPt@b+Uf3+r^W%SR=+ccv5MnW`E_I zE^_HAn}1|+RtgX3DyY1!tv$i~$}2$>Z&K5SH?$;cX?y-RX-=PsH$0|55XV9-&V0jt zYuN#F;YGL)yh49*vh-YX+IbiUaZGs|@JS?A&^3DkqAq_*^?6Cuq|pPXiV$7R<^+zn zBNZJTCe@=DS6+Fs1G~BP_@P$t<_-%PO_x#Uf2*Lx9m?gg?Xf;&7o%? z@}l!kKiPO`XE_Hxa1azkN$v@Zr|IwCN%(}kQKX=er?UNmYW$SQH2*Wz%bmw3C zFl`y##lP-`aiXKIc<_Ul?eb}N-AO|pnqXO0__p@j3 z@>>motnDOp|MEpX=W-U+Oal0!0ynUH?oqcFCR6@Y9%O)wf$PAOzOBjSj~O zFOB|qHQtf$*f5)M`x7_y+H%byxn|KnyGSVe0IeEZ2}b_}yDG=eK>Z~|4ws-@*lIpZ zM?IUUz+jkC*r9!JT#&vwwAOpPi#eHbTmBSj;-*KjD;z;=%b29?<3iNKdB6{f3~pF# zMIJz_x*tUXhb*?d#;$zyv0|-yDr(evb8?@)g*?KKr-r8W{Xj#}`J;W5TYyv{&kMde zA~(`i?yoMNd>HpA%3TS>QTbeAmToJ<0sRZeG}Pe8K8}3MHMKQiORAQ|ZroupQ5fk; zh?r+06;;*A->|C^f-}YlKZddp2~HMX7?OH8&4n!a5haGB(gGokBhl=dHm^2uQ#JPM zTWmp3nR=7e!YB_MF!?+i5>LS{ZYs^VCeWM|y%fRLBj{p!^L#%pk;Rr+{I2yz2L_EL zOnMQ{mtlks*xpZ1zXM08tAX~J@?wB*gY*3qUM@ESH@@4^nFotxlL$8!zwy11m^ z3JsRD^c0RP5AI5ZKydmKIc{wI8kWLUC-{)uK5jr*rX&g5;Ks|Il0-%N9gk#xcRqxz z7BPEFbo}P@jm$v>Ni=ltbriNy2G_GWQ~)Zjmrq(rvOBX4Hac=7wDsG4cJHg;8w2=( zi}dUURr_dO#3LPWal9PMS%#c4(;^5BPkVLLz3fGF`Eo%4j8Dx|`M(S}LO=hT7?VaX z-vo+1>@WtaLM&yFk%fGLRQ6=<3oi)?e4iz{mSh~7+^&uk8qKP)tG596 z?u0@2_HzF@m+;09oaC%^a|cQI`Xz9dp&Z2vyUsW@`14lg_{&O&1gcw*wj3nFhGWs- zj!wm#}9VEVpuhI`zW4(C?;cb zsPZUSDok~ctRRA|j2RG`6iy^wNgo#!Le#W_aup46@TWICKyeqxMBMZZp96?ow+3Uv z1#RO7eRBq=la3V1+Jk(2`Oq9M%24g2sDN(qUyIP}I%t-JkHA_H*wI&Gb~7@SKVDtv zJ7mk+gUAte@L7oy1m;6rK;1vlr6YrNrp2J;*QG~MF^i8X%Yn{U)1{T*QB0{kH?^o5 zHy(IIg(mK{X0p|o6Lj*crRHR(KR=8duVpVEwcE%xBok_=u=a!Eq_h_+(WXs7z)2!V zXonrc`Wl$CQ&|6{EKJiHUdw(y1ZPp7wzHn;gY>x<#-tu&)jgKQJe7Z%dXo^958fPv zH%d*n#>>I+RiRhFX3Q7%`0U^&JFLN4Ho?I$kG{DFSbi_`=LMoSdx#05jCFl9DKN%) zyBvtZ^9!JA8|x3=$fvvw+*G|Np{!4EQe(EInxX?9VA1NshbXIMHvkQYQf2)y20>V2 z0UMtxD|R=OLWxqS3M&>@a-RJTN;o=0q9nAP6^qxCXP36I+VOgP?2_-qHEHo0X>WV* zzV%MJU{v!}2rO@qq`~UG!r%$V9)K^2Z6uL_5Pb;8yx`nkOoS+fgj4*WCkDIWx3x%Y zWnZEM{!X2E#~V(Wg~i+M5zz{1J8RbpP<>hmSH0u|-EkLGyveS(qAxLe1yoccDxz@8 zC%m))FC{gEQ>^xa8Z$zT?l#si52)F_0#~!}o}QivY8H&~_J=t85_dzzA-F>Yq|xD& zCAbA_iHepgtTjuZVh~Z$P;;O+D%_hk6Y{jU(o_TnF8exK zhZFs9N|q6HpoGA|slv+S1RVVY4!5h5rt#pZ9pVIJy>QC!16-(wOo&RKVH|M;twsCr z-W3b=C2oS=wMyfnJxi+fjA5ag=p#f`Sk9Y3sRTZ5)Nk8a&fB205${vu`D#7abucYu zacIvS7|okNCGo}(-fZAN1+h5Hg2+iz{K9RN=)!Y*2F!*=D?(1ZT8{zid!sH<;;Yu{ zh=Y<6QSyNjw3VcT!$|rPPAT@3L1Zo5D&#Y3u-pJBP2mn$TnHc*0kW)z}y z2ue`T%$+D{RqKUAJu_RPqyQzTXJ$&2B&zi+AX&jok0|j&3D=@blzgG|5K7WS$r(zx zPC=psr^sLNdVxggAe8WWK&b)eE-2ykP7%owR-^2%YS;VBE zBFx{Lrnnc~gne!h44(>Nc$JIQdKVADC&+s7SxLRRjdj%qV(YSCqGC8y#C7@*I#*Qd z6BU3}t5m9%z>G_;YT0hx{VOpH-G?2~<*Kddned+I{dJW-x6p}R%m2(?w%Y_nPYek%94~jUEViev?hfH5$ zo|Ps}WAL^+H1YV*stgZ5MdG~50qo1O32#FK0?N1ZHNe8Dc`)cU?1!frN2~~HPCR51 zAp~>4AO2S|mUyUNt>++c9_P86Vf`$IPXd9%vbMN3;{1{D2JlCSG~mMy0vG-(mwBZ!6}P+PV9r{N@61c4Uq1mpZBcp+{vSKRr? z*VbV7EsJnQm!v3!oH%ia;`pGv{*M*O%>+a;Kx_&8PxoR7h&OSFTmB2t0-rHzI7H+B zLfk|^Tn`Z0d=`hOMnDw6AvXUPBCJ{9m%9KWo4Vo-oW{x)sga_{ea6m$gpglL)U5NQtjL>#~ zEoRIXBN)(4wl<)8co$9++A0Y#AbW(i)p(=8g>Zu;{3}LiBeTW+)ke^wC2VbL*kZd0 zg8(-IY{2>mZNj*=1-7=w|B4Z#V9FL_Ya7xa#Ms&-*ddP87~ z|5uFAwt*OCAc6P{wK{}*!%Cv&Fxx?__TrC3AXe4V2dmjN^Kb__O85~@3URob2{8w@ z7?~{=P2gBVXgju?@GZNIc-3MN@aQ#I_H`o8)-%cYnZHKC~SFdlV)xg7zOz&{~wkhSn|P;irI!g3SQ>L^YG8-c}v zJ-=Qh>{$>7k&@g8(FMZ39MP*rDO_0uL~T$e`L8k@uNoV#CtFz^zIHVpAe5=Gm8s!$ zk_`L~lr?W)D@a@_jP7!DOSS~wP*sa8Uc5dfQz$PSUuQ$@D&@b1RL&l9PU>(Tvs;S862(|Fhl4& zi~&%<8GH>i19k{cCS(a+Y>xPfu%n=c9AUJX4Y(K(MG(7KOE_y%5I)G~i3uX@7Ed7i zN+9bekiGlMh7pLvuwV~-b{t#m8g3#`1t!|-%Wjk)k#I(o&i7aNk^na3X_gP8j=C-zIGXYeKhX@3fwc16fX7N z&B}12R7cnK= zU9J)fQ}6;YU)j7?Y%?CQC>hdhvE_squ^I+15R;So2A{%WylgQ&wwNL=R>9Ubz}EH# zUtWe32r(xqw%9U4jIC{et&N3i8{%h+v9S#)<6@Rj+~2qz#tn}}{*4I&l}D#0`9w(KU7*sfKF_d<~f zdpt|ntCbsk;19uCrPe1cwwh>^E(HBb>SrrE%~tjYuZUAjz!4rw;K0$sZkKoxLp*WC zJ5_L-u#uSvF`Wt?1K38`S3y6?Ed&UHUXsJu_$>)@kR=H9z>`6}1$(Bu;!(7K%2p=I zR(1_YH%8}m94^&39yl={i_T|rp89rxSFjDM`m_~t*o7`Oc+;&6G~9#`mZt^*$y_c z0=66Vn5m1tO$>!zQ$>WJlnsM))IsMn+$G{!Jfy|7J_Fa{f23PpPT{B z;29Dm#IkTl%Mc@`6qQBHM&hUpKPkh-#FnweaHg9P60AGjJ6T^u(}3F&*pi8!^f;{z zyFV2cX$fosvQKdxE%kB@7_PPD&A_R45(nf((fdqJWWUc{326bpA3x)yz5y(MP7CS) z@@+UCIA|jpQero>2O1Jv&&KnGnD)e>8&&9EJX=A`Pbg_{dV9rM6+Re}N?!K(T3iF4 zjoLY2{&Mbh`Q)pdL?IOy{Pk^}36fwarp@9cLMx|57%ms|^^86;AR0^VqEi}5O@A{x zxU;WM3Yr-c9uWQyc!0_V!UGVTOTw(I%*B0YFYG!~ByPUSkhxj?T( z|85PFfblJC_-Z(O8FWns+-%z1osl?^aIdfDn&9%IOZ%jg*d$i$x;YQkgnUg{U+CBa zqEn40%D*q6&D+pcOS3*?!ry8d_2L9Fo@CA%Y#*$iO29t1mRxbHyex*Hy0{3|T?C_m z|Jwh17|LNqScjnO4;b@acT`{3Od%D{&78o=`r)Sdhit&FLl_C=ZPW!B2*^%yeTlG_b&?cH z(t(Th2Zy+vMEep8;mJm!qpnM}Kd>g*WrV7GpeQz)qdlq}Kb%FyVOhGFp%pm3VT6Nrmp+QWn>sW(K%X4&%4-EhKV= z@$UI5Sc0Zy$nF*1F&Zqa*1v2lZJJSDzbufbG+%0GW0q_O~_6yoN^Ry9<2^=i6wN=v14ExageB` z2Vdqwn|E`GmEi|2(R#~J2B(nS&Tz_RQ$)SHkNO6$uOMWX3-u+T{%)=zJ*fM76=}&k zdT`4sI9uLFWr2_rpHLDloC3=>S|D>B)d8V2A<_Z}!Q!4|x{vx7@WmK%9hK~pUN{85 zR9H!}s)tSt@1tIZYI;!3i0h~>R4ZITvZ|&BzX4_h!)5~}XjGS}{40j3?1b~U5Z;Tm zpy`Jp*G`o_=_R0vw36h8YZ~20Wo~Dko&fryBGj`Vgo+b*FCKxj&tS-KSjPdw27G|} zXAP#J4T=uN%2FZEV6h0sXR56^IU<|_v0vbP&Z{g#HfPxxZ>PxuD8Iq!y^%rZTiJ)$KWLIGLFKZJb))(-{Vgzf2-{P zSva#q;YaV`3J~+lQ(iM+I29CGkI39=gsMhc;}BFniP_Mm{s76_w^~+RN9QD0?$DUG zmZt7RgQuc6)-RloyyL1ZlV|K5a-cz0XMFAD*grL_Cq?|!1p7siu&YVM z16i93Pl~^sx%&WT>1FB=m=DeO$bXYkEoZFUfFg>@K$l(wUjHtJo<&bw~<)t&mXSlC;ZO+ z5~b+K==?oI6Kd-^`Nqyt2L|J!9jqj%C?gAoRyz0H*n?`;-jpj}-^nzo##UT7`*`jr zXN%P@Ut#Nb(W)xju%F2}O1k!cs&-?R_)uQ_gVWFD8RzlZ)UL&;@tOM{#%7B@j4?ta z+$wm?DDy8$HdkP&n=2N(?b75L_%B%G1b=le*!}9(-u3hD_G_07Bw-AWuE9;_AYsX+!m6!}^z%78w>Uhc zJ>l3q)V&sUoTwPh+kBY!LxYKGPw16v(Q9zI)BSkwtj@MV!^*w6rTT-r8^T6NdaL5= zyqBQ|%Cx^_o9i{V{Bl&1vZisQJXYE9;q@t}q|QV!Ej4C1#qI?B*?0$=ihCS*C@c7}nNlIqfc3?hkjP1oqEH;QCzqN=Zi4_x?3yaJNI<8VFn~ur66S9Xtqn>< zNeh3+1LRkmFjGDZC@l**3wcMA)l>zT6L47s-6h*TR|BjX)@uV|#VFN(Ql_Ym+-Rnm z*+JXK`bZ0HEz&CPGQ?vcab?lyU5igAW0ClmL)rt;s zZPJ%~hv-Q0owDi6)SEBhWO+l7B?g|{G4sMZOfZiA1TrKs?<@@#<#T-Gt(T58Rn4Lj ze>j|&=e7+QJ4lmM^)_GeW!U4VEjI63B!Y@FMm2#+`1-C>S;bIz)e&XrH`ZgY4j zj%ge%``CStqESuh7X~(TRnt4rD4aRqC{sGV{U70df3<^VzMonxy-45hHai|pae{a) zKEk6%4-#K6r5)rdrvD84+1B72GugepWMj20izRl5wC)eRZ$(n3=ATVx7*+0J^`;o) z4m`hAXcLOkgNA-#Oh>p#x>f{v1&W=k>=tKTC(8!)wx2uam^>31a3GvEG5nI%_9E9XxJhv;9l4_%@iLvJn|I0a zD*BO8>~%r0Y)VvCNmDjtCw>px+(5sKuHmzya)oEYqM+(*qbOIl)k{Z5;X%w>*gEcB z_4~q*&i0f?rZqBx3!DG!Q-8U|{$2HlO&a=Mbf%2f!Y$p*YZXU#&P1iqf6{o^oK=Mj zB6t=3P?u!9mi9Wk9j_fLltW?OhK>g+iN%i$$?cm>8mwyNOq9$O@63r`F~3NWWnV~B zcmJ>U-aHvdks^K|>?;K{XQgOcGX z%7IYzdf6t9a{ysFQRs^3S`q&adgkmyMqD9FMR?V6GV2PC7?N zh0`b*;q8A#ciXB2RXR!dO}B@y9}R2m=I1D2v-Y96J;_9#U4KO?)OP@fzh};iq!HH~ zYEO~e)=ypD)t~mgV({ATg{tY6I`R(tsar!Fx?2X3-t*o;V>rSMqx#C9*omFb8WBOYDYIO_JF zV{%^L(c0jp$Apt|B@Pn&7mlCrb+{P#Ah00nR`5IJnUsS#_j7l86zj<2(n85`0$|-B zl=J0`gFm3Ws;F?9Qna;aJ}Q2^N21lDWHz^f!Fq{vU-9L6T_fcp+pLyPlE_j@9#rM|xE_eWh#i3{|Mw z6_RLF`nDMhRK{5qC|29i*dy9|zDQROqBSeVbP(y~9!lHi<|VUp#C$$6kVH(~P}+u0 z?+6Uzu_@L;Mob_;^6K!SnhHk_73o~Q!F5KCz|1^ayqNl3FhzPo)J@4hVH*UD5i_rT zr6Z*2^Tw~wF7+%$%$o8iWi2-C<6eGxrFoyx;66uDBycZen74)e6wd%H%QIJ7aw>dn zS4g06yU&K9J55`h#5?J68?db~YOIdOdSR-mr{=!25h97>Qc*}oV&5qa{x)%$2n6oz7oa*zDT6C{!(o0 zbPmUbwo~{H{;qMtB`vLV1~3lKDkpEGXaG06@Z|T@wE`yxf4_+MI3aQ9w1!?cu~|*v z*kBNI?hKNC2EsF2i;SpJ2NKU7a84l-R6QRxRgq<#?`g!5jcfi&bEj{FCNq&Y1fvRb z*d$uII~OKd5K_tW{#t=2E{OlHMP2GrV*IOQ0sI@VScLuB>v=Z#HK#>1jZcUud!|$p z6#Gcc0+ie>2<2oW*c13l#zC&z?qfNe-JZ9nb5<_UUPq&IX-0V|>nIR4qMRJnT50CG z{Q}gi6tRInJk;);Z-Yzl2K=M3d)@jA&z&}|Hv9Z}0zIh6aNoU^(M-&73lIGqOF9;P z_f9meqmU|5TXBOM^AMyLX_$sM&}7c_d%3a`IS2cKm>Wm-Xo55@^abnx+{TYD@&350 zC4}wEqiY4s+v6^X-Wobh-|uW$G|yX~)K03~Q>A)eo1?12$rUsx3DToIkJkQcEwwQT zh5;gR(tqu(Ts--&lm1}VK7DVYXR@Ou%;-X=mCMnDc|mSN1-oJ$jX074Vi}K>PQbL$ z_n6(mo$ER#VHz>bSn&^lbx}xMP*B!&M6o5L*UpXoHZ7(Jz)j3VFk8SiM$Viv^2xOZ z%66lm#;(u_)eo4MqETdJXMBW3!&GO`B^%+vYlmG~?(a9z|4PKuNB6gsGcBY~kxkBZ zHaLBzkpfMZRvM`65~QpUQrzP?wR@me>;8`U*LZT<9qy5gj?#UuB%*L-snMK7h{T+E z&Xz(Kw{s7YrC}OVDjGbLq9>dPT8NK#E6!O9k$pDXkjUG5VsfS0Z|IWrDM8AfD)G*s z$?0bivpPEvt6m92*L_Y$^GId$4=yArefFLxk1iwz>beI=uzvxVA1`I^(67;bnKtC( zh~+Jy%I$-DkV7YwF2-pOmE$0#4KV`{#*|DD0B$5{H?d)s8_RD>->f0HvIJRbw$a@d z!F&{1SXWNsiL~am2rc=Z{Qvm^4w(;Dn+mSpZK+%dlj;t*3g-j4Gs6rK%j-})V_(lN zjfmx8(xpbs*2b5znj_m1}7-pwo_@D?Ortq{kF>NBpLF zmhnR8otGwskZ&jR!?_J>L=f84rD`{rJ}`J0AcK(K8Yoxp{F}*zbjf!6|JSJiqwXWz zmz`zqRF>)iD}nK}6khVu`{Pp~FEt*_BSrgmWEQ78x>UId4>$^V9T?s>|0h)b>q3_W z5$Bp2tT5Hjn|kLd1I?qsDj-73WK`*s&f-K zjgU8ICm%rb)9^ocDx4RQ`pIH#!Zx`0T<;G=2^`H1lHNd2dc~YXk#C{mK+Z+@Ulu8a zq>mTfC|MmFWIK54tc8I|a<3D3B8MR9%>9VLqUCnyGOX>pH3%X#8X%P%AcCdWAj;>s zAp1qwhX)!H5#*!QAMnVW)7i$0hlAPRpDsV7WWeWfK{Lp|qF_G}g`L>X|Nj;RBg6kI zHIyZgj}L>nk>inC$n8%Uulb+`^7+sNKgGo#gwKBI7@R}&1QMWtRg~TtS6*amITs`+ z0`c{%0vvD%qW$D$A7vL5RV!OMjv=&pm~H~e(DO6DM+NP3RU(0+8)^gLabS^k>UbV=c(MgAzb=Eq*wm>)xUdZGk7T_-NTc|R%LQ1 z(jgy2kB+SkFi{24N}=l@xMY~V1^HOAGJ{amz?j>Ld<1(%&)U24P~^lhL|0sdSP=civ;WOiKn z+DC2F7~VsxxbfZXfa`copR}Fkw$5y6DK*J$Fw4Kse&imay8dP1R=;p_6=Xj;Rx`U~ z6t$#X7kJyz>a$vo4`X>H@U{MEC$?yJ(4{L`g)QZS7W%j$UNSG2v=kIKtBlqPK5Ww@ zJ|?ra#HW$Z*1*Q;=;zR*PnnAg_0oiaKy&YMi|8FIH&-18zxwau^8N*enS&t{-#}g@ zsBESA4_K3xW)3n$YddrFy{irz?m6Y5IIBcekkYOg-78($ZULLYw7X+I^@eKIdntC- z?5XbmMlF_@vV(SAM3aPHvfZbkBV|zhdAMrjcb=fWx^`qBuhMNwVLj*KxgWuN&S+#9 zQ7T_U3Cny%4BZ$L4TFlYO^>9x%U<*)z06?27L)xF-PTEBJaN+Q>`5{IcRIf#xNf>7 z)bo`AQMz%O~(V$zM_~?!QY{^1A{!Y=K);K`jK+))`jm`YVQ%}_q}@jPa3JeZ?T|K ze$sHdEx7&zWBV3#lK$klFM=^s`oTC|`#|Lc2s1ePZ+>QZ)P6`#6RRz0{X794SSD=2_pr;T_DsIr^k|CRf+7xwWhMI(PAXB6rtL>I{2`b43ij zBD$u%5u8!{DEp1fKltlvGK$zjaTYo}+pXxJCAZ%AjD4j-MSa!tC&5cjSuk5{uCkhq z3QHZB$I`=Q_)X)=nl>n3ZvlHQ-m7-Uw+pYNCc#-#tp(n(_X>|!NC9A|rL8}fIlx!} zl@9fY(e~5YILhA<(xR!9;^MK(&b3Q9228gCLC@SiVjBN{7DHo0-9ys^?vejAb0zI241UC!ty#61U|yID z$R1`Uq-DP-Cq2(_*WL=5>XqsLy1MOf(EJ*D3KwV{AnMv%W6e9@KHS0*Td00y795p` zPIhlWtagHd7@szsPpN1m%*Mrk1kI+#|Ud$9hl;if)uETpnPsX2i?remPA$0 zJZtIF9{0V$86qvmL^)YJf7!D^A?gZ246)jUb)_&C!ybbZb1wE)6-4G$DhG&#mZ}oh@Yqv*tcEYpJBiRyZ9|?ECJ^6ZWV&+d8^2^6I6VENcOi?W{oV7@#2>p9hl)Ub=X4#FHoZrS628sjmM(Li-GGY_ks z@XBDlC#<_IjsBigB7J>2QcXEk>dhNyEpL;t&T13}c zI*_^;yt*Q=oltdHOlVNjmP^l_k-WBNJ@X~LqR(s6mffXiH6WTQV?`5fjFzy6(IQPzQlcC@Y>>XoOyWUFlnSf8a&g{$CtYX)7UR5 zx%L1xZLS5Lz1K97#VJ+1PxSJ&TG(pHW}wyyq0+sGAS~%p9Inq>Nx2 zmqzMBn)w%B!RO(au=4?58g8_Vgb7v*6I)oRx*~rxJ*3e(7FYUb;Pt{%?D=A5CRf_OJwh<;r?HfsTJ`#%?x}rY?@+GQVl}(}hG+B`htMd;T~PeQ7d(2E)42 z0NJIC3vpgku@6n7xHN*WR7i3jq<3L%MVWlMnDXhWWV0Z`;vJlssMR_IRP3X{$Saww z`7PYH_oKf@r7#yuNe|QW=j4Rf1g*`3G{yFR1Ff%XTY>yEb`^rX0|`vbytn ze%CieY^jv6OU?#uvM1fb zQB;MhfIMo{Mm`uL*AwKbXJQ+a7LU<(1`_7T(m`K&=I=XPaStFYqg-n*Opm04hYKQJ z$T}v1oV2+*`Esig_2$tAC7pwj1rn027Clinhl%TMn0@h$o4_1iBT$Ot48Q_r;J+D4 z9cgNub1J;-Aph@FZhCcST73^oVk`X?g)~|{sV>!BC5@cKFD<+}hLjyZPRdl1j{O-- z+4{|tZn|Q8ZeqlNST*Uw{6Q9nE0_W~y^jaWbt~~r1#walIdzGx*BDs_> zXuv5~=lyz*{MR^&VKHMw_bvigh48Ao?(1&Ch@~d zsZ&NtyDEE$XN3)D7lrv*u9#ve3ARQaTHP8SU5})56c@uQNyQhJL>Zg?YRiv)d^UN3 zlU2Gl&ycxNaJOtM8TVQ&ABm`q!v(;OqAju+Jq2NdJ?YVyC!E};bmRPI!ospl5L-Dt z+9&cxt*zubN?Y!S1rfk`vpGE7hPmx%Cj);pn~crAcoUOQU0S7C`tjs|=rxs!9F89{ zcLUi3dz(4apH+$rRV!=aC;#1GlRwdJ5>Ylam)?naA1gdkgkRe){i-dTYI2H@|E0$@ zBnY*iD}-;uu>WP8(=3Sfi9~J{m&@x_dnr~(1_c!;%m{L>n{IHijnuB(iYog3Af%_| zz78{zK+c~(4{I3}l;GA_a%Z0dy;m37lFor@RRGDUkX+8Ba1XEfbE|a4J~Te`C^C1- zT(~`Jyi(QB`wwns=YwjzV;F8FuqP*jF;vBrT*zGftSJSW=cwjf&-yo0ai+0l zbK*E$-ZT?_*=3< z$mZi8*kjx()lqjHY{NAcWr{|VNYl6D57qajt*NKVV@69+K4I{B@5}E$Uws!r2G`Ic zgq@b-Y!*w?VqNPKjvfR9Y9`_(RrIw~V%Wj0ChWK1{{?c3O*`kOgb#fAK0PB_ILM&D5WsSdm-|87z}Jam-KE@qjU#(GbD zfS_oGO!YPqm8~L$P;@*GKp6u#%|wXXlbA|H%2We@ayb->B~sT>|d8LKtN`cUKvwaF4VGoh-+XOX9x zBPY~*ME#P9`W_=DN8irrRJrt~Q1O0EFL2yob((uu$`=t=&P^U@NfL!4v%ujJI&J@Vy`-%6W_SOu%IK%SpMD0 z8i?&FI;V#+dCL0!jUejOw&{wrux79JE+!gIXO$YlGKt8JJo~Sx`mG$2{f{l#1hH(h z8Z_bzlp5J|CAq-CYuL*IUBobvSX5=nTFw7ZC?W6QJs>9vIqL#ZTCjc+_pYEzyi)_PW!j%* zT7TCKJMA6Ww1sp~Bkt;jb6!8!-1xz=Dd|b!L>)90z-LKmV@};@?fJOxldSpozvp~D zJSH!sNuhmqQs85th&@Sr@?l73KHPPt2m?0_}f6t;_Qf2x>{yq2ad+oGCoq>4ib zAt<0^`?iPem4zOppC{teNcJ@eJtFH{?NSF(Wp#CCviYtAqJk5+A?kmu`bOM=ubOBcxEwWZhINzTV>j~TRteDKOFUfTG6Wrmn&kSrvoJJNhD9=#~6K^7RLyN)8nQ* zt>*E8-zR51j0UF*?lxVD+qh6r*7vJ{8Zy(j4ejtVfCFTz!&nnP3z z*_HUTF#mJIM|3T+ic!&=V${f>*75*y+SoYdzI*3E?(_{4?B@*=B<}zSS`7W_#kK5( zGzP=zd8R&79AbdX1xlU9+(MK(-#8ZM?+nm?bAt3HSa*U6S!!1 zp>~@fo1KgMt!{xVvH%UtmHr;EW7LW&b<|Q+;P(qn1B++Wb(v)|n&$Rb4TVKhgqE08 z;m)R)TxUH-6yxc`j~ik`7yAki23j66h#xe!_cQs!VA<>Y&gC#hK{Ftp$#WHhZ=qUw zKj2NQf@Ay-tfI@uO~c}9J+FaUSn1HOzeBZg+ed$Kddzui-*#t!@`;DtV#KLx34_o@ z8KsW68`)BtGcH2GSDgc5Pu72&luQZSVjc)~t-1X?T_(S}*RO49|3sK-kv523cVH#m z$Qy%Jdj2?i&&S^Rr6{%H*sQjF^G7rL zO4Z0YmL^Q#NQZ+_)ONS-tazo^mC1^5-rtYLA-2)ks%cnjxgec@L8LVojrHPmk6h5L zdH*B+)hQL{YxFn?H!o?3$VApUAQ|3r?`uzq0JD*B0G1%1LJGecH9M18N<&%9v=rwT>znWir<0G`P9E=|KVRtxJX z<0k-z1$>=|>gA8OUyTi>omo;qs+4-_x#8Ny^bzrr?>ImJT6IOY9&1zgZu6Z>GB!4L zcjZPcj^7HHh0HeHQv(&X!8&9EjHZHI0x`sR^%&E=noZo!PFp%QY2e z*C4*wr>09?41}BX9 ziy>=t?a^BtUARo!1gpzrtXM3XTCtWg6jX_Bg{#W?wvXC^qJF9&tFtHIuqIq|KHQZ0 zo~U58D#hEF;dB^XY)bVh-T_|HdTc z&Wj+GLE}j$?!>K*)bYON>0ANYM$ zZTB{2{M(N)W_f30QRyLfHuKP4WDb<1;BZUCse^C>pVCwEWtN?maa2qM0YuB{2OIZ6 z7zW8}I&s0xQDVdQ=g8$7RFkZ1Oul6*R-E}8E_vx zj6NQk2C+g1S9$*#j52h$*za|5IGet>3+6GcTYF&H*Zs zQbW<$J-CxT4`6F4faF%*xC(-`2GGxRO((^Zs>{h37}=&M+;7yYadGkj#KzQZ)Kygj zNsru(FeEDbw&2hN~t%S6x`Z54XA|m>CBB)SOGe&!p5;&Pk2!7~c6C z6Ac#GP=m#&FzzUkL$<;i-@e+@4^{_t?yB({GdXVB&sT7o5Myc!Fp*LcG75mP+W=oq z0MQOfMNKrmV5GUEl4!D)s&n?D{Mm~D?$VAypa7~)Dn_P5jv7L2FEC+6GqTQxfV?Fq zB`u$hMC!2@#UwPb$s`arzZTb5Vf?h1cL&Zcv;fc*a_N2@8aP}t*_b(SmbI0#2asK^ zh^u+q9480>FbV7__W{brNe|RC=8@Nm9pa;`29z)aM8l2PP3ai47yv;%L%U)g&2FWP z&XYEgvX~+FfcCo5b83znEvzDCj_6y!ygCIN0CE{@CcX~e>3<9lr!V}2Ean6(pk>bS zrO2-a4EuJN_mAd&uI(7yRO^|pAjYZgiKDOtRE8V$a9x(Jl8n}Bc6vlIo53D`6q>B4 z?dZ;CJj5^x>6O7`vcJE-ly+DE+%?l2{(?FIm~Hi*GCp5i2lXvjp-!gtEQ}g~{yrMB z4A#xk(eUJ{t(28Y9}xCi!&fh14~n*DD=@>AIm|}*+V`p2#QfC9k2{jwJAYt|`79UN zr;4HYdA@dnA<01s0U>@P0UNO888I|1H6!Ys-QPSb3edjx+qT~WRLPHQHoJ7+-n~AF$7p)6NKiflRv}~X z?_Z8enbidD^|dX48s`CMu5s|J^PlgF1Yb}!%y{zb`&TntDFex-1be~*2~F(L_>oJ+ zJ%;@KO{E=G$$wIHAJV)OsYMaQwITy z^Az^-r$d)s`;>MQ?MkMd!U13pCRn}+ulm360(@-G!txV9q%2nhn=k;8u=D{fsN_`0Peao2FTD2$jm-+ zs^nPTe7z5{@3wY;*Jt&w1gx1=%BbfK)-@TNT;>zxE z9YA?*#KvoNqI+aUo4BAQ9gS8CAB@6Z(gNVTp4WYO!wshpAO&! zmx2y3S-wseg$8-YeNSc8H)mf8H2G%j!*v+Jz@Xp%`Ezda71AsK4IU}Ppow2WF>jws zL6mpG7c))^IscF1zt3b%qaN5-DR1JUAzACD7#O1Or=qc4@JTdt*vLJ)J7+X zigEKPE1wQlU@}jcR9mRV{Ro^~;xK;^aXY;@==QH@?Bk}rXHwdc{FE`lW%@KPrxcXh zy(_k}lA^~=PbZHu|8_a#4TQiP%I<2i`F*H zOAY|$HN<;zd;!m#{Nu}))B6gue{m@zu&Q4$Iwf$+iaUV<=RJ(8R6#}r5}h_$oN6>nxEh2A#BG=lUHN|O>1D`gK=9m+jg6Xi zuYR6T36oK|jNQJ|cgN3lf%^pTMOra5`BbsQz6+c?w;>zi%d4A6;$C2;(A)29Dk1yI z-KXX!1(IuhN{KC?vYc#WgthRSpws;=>@?A@sF7=zselNM2Jpvc86nCwp;bMS(mZ9F*^{w5Y2#w>$!3u+IZ-M)B41I{EqbuT8S1`0 zOE;I_suZ!w8JoO#sg}jbSUFU3BtRo^-|Zx*{nuZ2+43f04Zl3G23)`m7z7{uO2UVh zB*aCG@rn@g+iS~IfFc4mKat;|qtxeB&gC*TmzzPr^`HyFw#P=7c4^)RGhT)Bytp4A#9?_&n+Q4jYBm zL3D)4ngkbDR{~zn2Zz_A`;>0!AA{mt-{`o4E2p4`*Gh7B9CFXaw>pP3HM>oscf|BQ ze%W@h-T)<=MC)Ng-4aoUR^!vWOS?6>3X!%V-^&q#v?>!$9*TcY2>S$M+znSO1ID$bETNV-iyJ5_8KZNCZw3@wV^ ztQnx@N4?|_VwV1qg6Xd1?e9M`1tx8Z(2p%J3zZe~ZES27*`WWc?*7_>OdWoEcD%pV zb^6WYpFj%b`+bop_pM{Sf6f<&xP*&HAocxop;gJJvc7k!l+@K**B;e74#SX^5<>@M zJYY7bw)&K!@xN6L1#7)9%$)vvnDphF;zAbOGtRtB z+0J`^vZmu9jP~i~7&H~1QzwFs?IK^{OTB4_bR^a~76cm?+Gx!Jp801V{;OHtKz-+_ zjN#_gA@WbqmnUG!uh#k-#g`gx80&EkzE4Dmhb@1A0;w?wKEjhzFac~bH8n*KzW=cs zai+Fje9PP#RV!IP*8s!HyX(pJjf?VQc&a6nk538kAy?J*ipA6o_V1iqIlRsasPJ0W z^(+0ZDsz9!A}Xb`uw{(58Ts95GZs{-{;+Z2Aglok74dGa525Mh^syMfvs4D^ob#hx zb+Cku(hi64&5B{V)`T z*E>bTCBWFU9I9a6IW>L#x}w|Pe0?v3fm%oB^)jxVf&~d-&0X%7oHu%PX7oDGOL#ul zj*qKay#Juk+N*|eJ1-)etn=Ac#iV7ixQhqLd$NuzRg||($z7A{ohndU%Pu4rRmx12 zh(TBQeo`PPVZ-n|q$4{2QY9AFjC+zy{U2zt=W=sIi28f6&4}{xth29z{wcI!pZ|gh zm-97;(z3FvXW>J41NGO!2+W?Jd07y*^S75unA1};nLR*%Kff%p%Fhzf0zf9)hgZfS zdhY*|Kp2OPh#S~il$HmV#tB3)1M9&~WbVap=0a{;m7jf8{nAFBr14jC3~XWN$FJhF-}xwYUjnb9O1wt@7HYtF)slu`z#yz>V4r9!J7L1%tye> zy!)Puq8awv=0DinacuQggORClA%>~SK)kl8j%@OU3&Z!}%m6x{;bVkSCt#Fzim9ZQKOG3KmTygshU1%W5rr$x_>z!Bk_`>+7(dC9C8#ZcPhchg! z8wX~@i6j)wX}~UQFB#J=JRG`Zb}=4%D;4bn_LytFTyJ{%tifzie*1bJOf4zYL87 zbuQCOQ_qz^3fWG1@thrA&m7DrWBjbI1$z9PGRh=-eIfE4cIH7|*X6d2@WX+3y?)`E z@+Lll?TZ@8l4A4s@Xhd1{k`wKYeEM(&uuf)RF^7?x+a;y;4m6LNlOAG)J?hVqjzfV zPM52M1#F)g(lSY!Uo5?m`6%;ITHank1%Oa-S}?0xva$F=Fi45%Iw@z&`FwS!_V+0Q z-g-*%IL!>(b|AVNubN>y);|>Z0Y^aUuKqf*6Z=Kkj?GeN0tyrsd|sH)Fs-;@`2x%7 xzr(lwP_RG#Klm65<}HdQY+HN;!~5UyXAX8kzAlHQo?s(BVPK-4f6O`j{{f$xS8xCT literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio.ico b/art/icon/frame-studio.ico new file mode 100644 index 0000000000000000000000000000000000000000..b8eaeee7dc3cee1b803f4c6da29d18863d1bb489 GIT binary patch literal 14654 zcmaib1yEc~)9&u#4vPm{+$DGdgvH$H$ZUM zyLsQQ>c4gCukUVc)#-V9X3k9abf2D{a{vG!00Tfs2tZyT000O8@CE{I!(+Pq2|No|2-hHu8!b0e}z_*&aZ5j<7^lZ&s3(()G>B(NR&6mLhrF z^)F5;hkgu}A(2Y7S!Q6v#(p=mPGU&(5K878Al3ZVo|M~vHa0KGp)JOnbgL*NsZcg7 zs_<%k0+-`$tDio;p}QA4hNELS5yaaR1lD0gYShQR|Y3oZ3K5Q zxVJYsOSKeTDJ5qFznR~v>PV!zx>8}J`@z|?yVPNx|0{!pjY0r9*>1*{=VY;4aC`H~ zD#Zn{=I{P!%RvDs?+C|YIg@M*MN$MY)-GRs8|Ui(>D~WSm~BFMp*2#*pY!O*rF4lZ zPToplO*PzQ;)FIc;tw zT9SVgAx&Q}iZ5@SobM%_^vu8#t4W!vf6Hx@szh{iD$U-#x~HvRYHIf5$0R|2@dzVj zJ+%$;@9tBi{Fy%hG4hH&NxQK3?o>gOSPe1)t}G~C+Y&)Gc>Du2LaFIrMErKFQ>-5) zztYW3I5IJAAk~LJ@JT<3KoaPG6^H^5(@8lZ_3J-EA^;^hb=ewei?IJi|Hu^wp!i4s zn?4fP$d%aful}p_CRKE`NFGn9oIm6=?J7JUFMkWRRNOBr(yL}rW}L-zpt>N?wKme$ zihXTWkW5N1RgytuI3^smB+UjE31yJq6(x=neuj5$wzqndEZ{nHRdPf7K486lFL0y# zbp1{p$Zyg5eeGpsvodBq)*-CaVM~#Tzq_mJ$o!aGskp6eNor8zt9TejqiGQqO4LeXSi_wT(OWJ?3?S_0CxWMi*GkGMKJ_&L?|pIl7KlXfU%a zD1;uEFN^5&u)mm_JCfCKZB>W1nbd@PHgDMB+f%*M^bN)>VhUECTgDn(H;bM8^!s^4 zE6)kyS@cpa3e;0hLH%k|b3%t{^iXF9_hS)ov|YbL)TWZ)=_##+tvIKK3(%2Bb>L-+ z2iu<9Y{rFT)6%}o;0x>J3(ZThzL9p2^9k|XuQzOZciTI}GMeVCjL)o8WMT#9Ym29P zf*$qT`THIjR`v;s z2p~u{Zq(S>IT1qnRQ8y}!%|?+xRmW#+_!g?Kwx_-qm+-BlQlPjk zm@nW&;_BN?Pk`;-x){Gag=G-U^z<|jKYI!jH*;rZ%^Qj*M!I_340p*#e{OGYU;iku zJ`&BH5H-x0jS! z!xOGyZEgK2S1f)fPLET+xV;@q?n|U_gGJ8do*3%@OA6TMHu=zkE^#@M8z`3I{bMGw zvO8#!j(~$R;%NbJxM-$cj6DBCK61J3sl`mrm6 zqi<&@YjJMDen(F)edUYu<*&`9>+0{-CBo!=Z*LhFiBx3|(y20JM{IX;6fvqzo@n{q z)WQrde^U(e?Mt*WNeeUX$v(eXh=`Z^8qXkFS{pN--Et9-m?6Q!A}{pZw5DPtz!-Cd zviXH`VT{I71f68>0JiPqhw!6s?_=H_tm~4>eLf>q+4R0U9xSh}P7iwtLrJ6-8_7n0 z^5h9&t8=)GJNj^gMbCQ=YPDx1&>BX2G|xfj<*1aDNw(_pD63;EMGt+McX>c1Z|Iyp zAYMX=RXzMDM_r`>)Leu6+JPr+RrgyyWvt&h8HDrBLDRja4+ioS1)oZrHBC>Jb#-8B z{jLZgt5+dvIZ4}SV;g~``1uwzEufa`mT0|~5OfTvY}toPJ)^-90PY$Nur9IKjaV@h z%yirGnZ9G5e*0Pa;MVSmRS&smleFL>lvZ+ivbx2;u&bxEnZ{8YBKQ1ZI9nWQm+j!M z(yWI=i*}v1H(c1GFo{WB(a7~iM@7*XwRxg2l&`24|SJMaGz--hb8BF?!HYLs@qGO^dZNFGm zPH7A}JxDy&3ZuQNYb_Q5fsYQUFe7TgGHXEh`l=Z)uBD~r)h+MTz~B=wKiL4_QIPSB zi6Nxwj(P}itCOb5JTu*`;F+AHWUXLjZ!dKf?Lf9H>wj`HB!$Hohd>%A0wF~g*^4Cp zW81Q}iKYsP9mYuHzu7i2WCHO0W83|Cj|}nM*grU!d(l1oyq4=Psk5pg8BTa?uWNDJd!aY?4&2Atro2n3`wudT6%X zL1?zVB+soA1q%yHrDE5Ztv-e@;sc39)C_h9arNt*$|=RzFYe;HUj(Y+1z&3MX~$E! zO24f9gUPI@u%AjuN$EgCOZ#FR<25mO1jd-=xIy|_d7`JMhl-7jJ*;9eOOkS1V8In? zD^K<w+4TpYg=Wf_$Dt)n^1_v5ygW*OiB^yc6~DHGT6(aiH1A08QEr82z$Bf(Jx z%g%WTQ=l+#4{k;+mWiKgQOK1U*+~-h?tMb0GeZKx;RB9CBwB-}{%?*?Si1v>FJOk5 zmzyu{HCxR8BI=H0r4%DyCkD1;NX@p7y~X6^mvIF$>wq=m;TZS?CeSwMMy07o+h!guvo*;+?@UU!*JfYPc6;C7Fx)d#SK}~Dp2vpD?A-_{+NuQ zH3BqDzEs|y9WtBj-#@w9Xf^v~WNO&VfhRK{bA53CY`^4fDRF!hE(xiN%&$g_LOF54 zOoMiY`4}uKbc6fZfYAa4alsEsziPXSNmFethA(rMnmu^qVxrNBRZf*Gx$RvX!q6`$ zm+o}gX*vD+IoG$`*t+A%e#7}b>)_{cwm0}rgR5DZs4RXPzf;~+xzDKfv%jvw_qQb8 z2=>uxC+<-l{4!izUr+lP7xh?PesV@+0!~t13yVFi z6yBW3+;;cD50^J4=W<`#iNUq*EthdAaRO&f6B-^2|P0 zjLF3soG8*_rvBhIQ{a7!ZM}kOsC~9A&bc8PRaeJnbKGb1TI$^C;`i~FO}X!HQ-zd6 z`K#r*$Ij?$mhtEr$mElvYy16npD7-@CT9CW&ZRzbRkJnn8WFsomNCJmiE+FdXqfSl zN+9lpw9z;i&gP7kNV5#_2 z$t;x?hn^ojV9E2flpKB1m)8mr67ZJl2qpMrYr|Ap%P%s0g6+vLysF6P zMDJDkm9Pj*PjHX^>%0{#`v5$Plf1 zG3A0@iXM{eTXTWP1AZ{>ddnVG`EpU;VN0;$u)y*`JwkXx^LRroK#Z>KQra7wKWQ#W zSOiyiVU)OIp(_64(g?PYDWRpMbq{oYCQbNhll=DXZo@0~JUvO$Q)ioFWf$qUKb#hE zM-PT<`SDC?;Fao3UND#%FrfO|y?7fZwD;a&t3Q@IA4N{n(wgA|{|5zHm*N3yY^rSG z*Ab)b>YXkedkWZy%~KKZk6%x}+#t7~2*fLGd6NI}-m?SRIQT-1X8#}W4Mp;y|9J0D z4sX_xzTfCy?|qc*i1gkRQ5#vuD>X|mSE{wq7>Q)UhRW~zFomDQh`^&6(9zn-LBA#6 zg&9|WfjngsDa3*h@eKmrnN(4P6ohTnlrW)xa_9EuZE7}c?GzY3<;s22`t94qNnUP~ zalW;H-NcQp_l8fe$F9Hig$MGFeT1Hok%8~oxi+c&ri)35(f(P?i`HTvxAYg|arN>> z!8GymSIsRgQAbW}AF=nzdLrK8C|;}Wqa+cTHW|pt&z17`j{*`G4};<}T$A9T`{7AM zCV|x(r{;2FJjE36mxoA6NpT#s6t*Syi#-vStP->u3Zm4H(`-Xiu(PwXMF%UM7dGU- zjujUv6YUj_0}tmYf!TLb>wyaER&ydxdacw}IV zn$Dc6(PMfTV|rBEsr8+3UVGvN7AS!6Lll8uMy%LSR@hNSD=2;*`lD-ZAo0Z!{(OCxumfm>J~%=u;vSdfi-#dl zsL8q1Lp_n9yDgM+f}cQPq2Cpi%CfTx^Zeo{Y9p=A>&s7C`?)pOxEs*UWQYDOQ&{Zd zwb!A3am$1q<@o}Gxw_hj68R?LUQ9@Z+p6vQDq~^)(4?Hw==<%h5Mg|totcO#+&ad* z{LLz}bD{=jeHLpcnCWi@4I_6;X?b+jTz(oM*G0o2E@QLk=KQJ%`?fkoeN-!X{j4E3 zVh~VL{lOhvF@>vmXtdeqgV*NvyURfgv8P{c7HU#&dpw-6h_7E`B0muNpcvhRYwyw% z_BJ(7Kv2qaCHr&lzz8Z&9zyt$YE~+QPg)~9Yb_of@6+IWvCG1SvxMu8WM39?XwTu+ zH9r-T5c;3{r$!t~f8JUpvpjBp#PRltMXv0&%Q)OkTv~BdbEl1XJgP<3SYA48KTn~Z z2pDCQu@BE`6n?32ee33)1FJHbOqeO)cdvxpc{I}b0uGmOa z6@!L`gYRhVZ@;!VZxZ(P1C+c!|GDQMoS^&I96~+;|s46=4la_->^lu5gwHZJ>0F62HePCk)RF2on*z z!%x@M#oZ-3XVQoRqD}NlupVH}o^FslAoL6|VP0PSZ%~8EGK--KgsN|)+?O|UrR}^9 zvZ8zqX@3ivSbcJK9V;TLOMo6+1v*0p0l_^J!IHQROBBYxM+?<7eRvlZ=NmD8X&Rv} zmg_q^PfpN83=UFbM#cjVlDRLp??a$#KRWC4a4ic}>cahf1!R-$`}C|s{a<0smC9s1 z{q1q%d>(hlJO8ES<**f3@7}joi}+ur=Trrb^LQ3weAKuILCtt9 zb=xJqeK`Kgna*=)O&lGFL{wq8U#YyW#@0+^gmmS=vglBub$HkNaVk5FnBj8g=YGb2g?kzwnmjF>9=?f_R$ZtpM_u-ZFU z*N;!uLJ(I7QL%vb?-CPt!Iy}QhXf6Bb_HS`~TuAxu3z z*N>87Q*%AjX((95QKj!3b)ID(b_C1LHvO5OpJ&}-z|LWvJ-xaTz_k*UpeXK71|x4t;%5HlcBGAk@3nMDMjj#;=sQTIbI6wJo-S-A4hkPIRz86rsP zK?dy?V*1>dlaqr;LS@d#Xl)?WJI59f5HOsb!3OhcD$M5UiasZF{G2HkT5a6QQ;>`M z@@>jiK=ez`n6?BPawG#}ZDZ4|KCW!1D2_H3XG8AMzH3$z+{=o$Lmf8Qs2N{CK95;s zyX1re-I|OVoYlONsBB*>#)cXrG(X^l4j$sRXV}OVVHM0QND_nT3%KlL1GtL-f>qz~ z+^sd3{IyEg7`q9p(R=~m>6lChS>QgQG&Y1{njcRL_u}s1xFSX~1OLxs&Bg&bhwG1# z=>bBT(YXS{G7CO~^=m(&{xE_PnQO^FIK9)&aI|m8xp$nOFPChzjq-97&3q&t`n3Jb zq{H&5w1vTSNY2A^)ydONA50F8(KqEgI+zpWf{IOrzloF8aa;*P9iH@jt(@Hx{H9<1 zh=a<)^}PuUF}h}PfJoh96LJO;e`X$Wyp_!aT%cjG!sg*>s2P3QI7w)sF(QG)M9c-J zkk(%wHRe>A%3!<=CK_+tNYXx+YnCDvNC`ka(v_hBwI6HK6A36o(f}?}>>yK?n$>aY&*sD=#>J>Z%RdT>JrG9<^d;AN_uw+E*i2ygx) zxc0Ol#YO(2QCOWon7g|>OSn$X>&xt@UY`xt@`~!urYPwB_*b|cTAv5~VnJn(jV9!Si}Y;iiU&P(dX4mjP) z7G(w+OX`8$SYi78BELIOO|Ukb;c8u6KoO9yP#|%wa$h9Cj9_Ch7F=Qi&_Y)EfXYq^ zZ9-OIk{U%N{98pa2@K4^4w+b`%zWnf#POuLC@Zew{8ozAVOPQ80;&8`FNDJy74l64}M zSX~Q83gJQhfr>yR#R9lUJP z7fn4iWdOQBVwC=OdfUl;)o@C7PE^BW=p0`>j|3wgYWG(&&fY%ZK^=(|}N`*+;D!yi5)+XAQMUb+YLb(X3G&eO` z^QkCB2YSv5l~?Z28W5*<-{$TQh?A+B&^l7=?$7XqDV<9*@%${`(*H^K+}^*tzM4n~;D6@c-SMXVlk7pJ9_V~e@m;q;p; zsfv4Zlfyz&{p-9n(8tK`d{tr4Frkr~q_e-?W@stM%`cj;SdTU^vDaBFFv{%g>UzK~ z=LT2)7t4~|?-1f1^C}?!&Hb@6584RDB|}=6@h0l)#ZK75F(EH{>S@O509KlMcL9Dd zzIeYmIaJIqi@2pv=>kWJ0;4?7<&Rsu5#&$8w5Iz?pHhu#H%j@7c-@&MoXrzjsRJL{ z(An~892igoGS63WQ5_iXk z@TQ$i{IfoDT`9Q8$^aZ%@(e27%%X3rTxAzwAkk)J>t(~ zi_CZN7_=-VLST4%D1` z)Rm&-sTOXSG2e}ExESyP5sWD^m3R!RcThh1DNH6M*@NR^LG7`J7{_15vN$qF88}$( ze7TX@;&xn1%*9(EP2dg8#fFIrAN+Zt~80qVZ4?;q_8`?1_gSkK+$)XNt@t4ZU&^tqD@ zISVsq=VLGVC{v1*{CtFvo?fLA2(8O~_V_}|wUlqgwA*)puR!O0=xjbHopqDrxI^k! zpjg^Hk3ORG;NzRe*h;bLpUj)2E4RyNlI@=@;L<<#7^6%yHQ`TxZjgg53+@qWTha6F z^K3Z1IBzv&#{`X-HL)SIL;~?ElR6@9VI847Nxrlzw~3nPbZ@?i9T>oB4u8-Sfkxe5 zH7yolyImeFt65KAB>Co!bB)>a3X44{cUU=v{1!~xIv?*|uL2;W9Ir9IZvOnES)|I6`-F>FMbi8Wjy( zfN=SgmLydATLttt5(a5eb)RY9?GWg~{n+#VfU%Q5MKJ!NGnON`X*2ftAhXQd??gy?oUQ1mNbnDstTsej7HK)La1KVX#*6V-dl3J-t#wvbwn zs1tcy6{o3}?nQhgTtHJZzC5@nF{_QyY28*dt*ibehq0--wv1s_sVzo4L&IYyFW>_s znxY5NOH8~+hV2N1#xu=)B=Mh!ACUW7OZ(Mt9VGH!B7Pu}3k3WV@te4p+x~rv;y)4p z8dfIB&s=nPYezKYt~9l+rj`{0hv9ey1rY&!Wzb4whrcqZs^*Zi7(8Bc6;$)oZ~I3z z8wVRUZF!7On>ZtfnJ;Ah3OAf@0C&Js?-=1&iVNPaIjQ+^_i26Od`<^X(Bty%@@hT{ zNc8a~x@?*e7odp53P|{o0td$S#Q-47J_Ld&KP3Zbz0;$&{E(g9TF>Rk>DT79!9VA< z#$qVq8+#u>9J9bso9b=oAcCK?oE5bkEuLWhT3H_Lzb_HmtabwE-W<%Ljd@mZuG$O2AGgc2f5Ns zs^1Yh8gaa)H8kgvg9{R!~l!w2CpYku;t1NGFqIw==w1T10+9-o{N%0IFpB&Po1^~iu;NkfYq}SBB@7vbmzEiVPkH@Jp$R9&GFkuuGOQu5PWM~a(7CPkV7kT|UCaD(+&7+ZEH zHm#5AdrL@*=dT$XOs#S>h|HrJ^EJU(15Hf1Zmp+}&*}W~^76Z+h?fBV9oHnXfT%6j zPa~BLN2|zW&0E3O%s85La4Q~ z6Mx08o4NI_6mWJD(iKQ#B_*4jdel6kyE&$;PR|p7J>>(Oj69PQ42yZS)#0L`p)r|3 zLqp@tCbK+%fra>mE2-6`i|+XP^@Kv!=o8?`OGLcQp9HRENKu>Dn4*QfvGMmZHZ4O& z!1ApxKSeNu>6|h!CxSms9JJXfnViuP5D*X@$gX^&@iPuE6u!keqvouqH-8OAXgDy! zdcJ$~vQp8ahs888%)&mlo4AZ=%LAF?0ZjLI{P(i{DNZhIlr*NJ2zD=QI30w+SqY0; zd*=M%1=4Ej4KGE48S)G0ViZ;Q@%ByI-GAEV=H{AT`}Ld>!6HJa2Q*4V(x)FE?hvb| z=PFqsI7^-^I@`~v&S0DZqZj*_gvnTQYQBM{a!dL^K@pKeS;hCvEJb~P-hVqe+Iy=9 zw-aR3io##Rj;8Y#{8U-_bSuiS4*mzq^HkxMf_v=fmBuSz$<#=$&lfdfR~f{I#~Vq6 z(>LBX-|C~ScEd@{E#F-{U71Y1R|Q(%y5eCl_pfZ}H!O42blv+OwVnbC2CqZ&SxLEJ zp(q$G^p*ZezZXb@2~;44J8A*pJ;Ybo97cdF)!pxqAEem5(acF)HHT#5JU9@v`-qJJ zuwvRMPTwR3eNa01ZzD<&B@l*YQRUu$YKfIOIk~^40VG*Vg5Fjz-P2~kesgSKvWo;NBKxx7$pVoC0ZF(4%ej3ZKEga`Le&%?kgaL?C?Jrl9Jy02eSlH^hNxGSpw}RO8&tt zfcD=8|G|*d=+0m2)(U9PhV^&c7ek;a)s(;U-j78qNM?#cN@t1~2TH7jkDO`&9T^OL zh7c(uC@&W*^CSD;sk|@%JXN;3mf!+P)B#?l?EdZWw-s>E;u=n8xs8{qL-SXrW?ZnC z3iyWqnC{%dLg-5?tifnB!a^FKRjgFk1=f3FmoBosoPg(^1s+C1Hn9?g zJw1<3nqRtn9y^{(^oN8;AXd*O`eJeNh8up4@%hi?O?`g6dk`}-zUEl>^LbkCZfa_} zM{?^!21WIxx~zofGj3h&>w>aWqnHUHWYBM(1icFBeWQ8lOU{d0F;prLQ7#xo7C3nF z{wBl@iwPG8!crRfcSVqqL2*^_dKHGj2JjgiU&O-|^rIzE_S*$Kz>_l+xJ5cJUsD2r4_zV{HKNSh1TSPx)Gn)^rouYeI; zzy4VxgCI;TqrWOHI1+zFJ{ZruNq_@ka=_``%d*RtR#Q`Z94u|CM}21njDS>cU5WqR zG0A|@8vuYR0e{6UPzL}F!b50Opdnl^7~lQj5O|6T2LkAbL?l$SZtdu3MDQSW`wFP5 zI1EQjD*Y&=%7En=07xyaRghXGz$sA04e>Jo`Ln+SbaB9+^WCSnU#Fa(m(d>_P~Jw^`@wF)MpilVUr2k5vmp!WuV7f+-m^r zWz>j2<3DKe8XLB?nA%Yq>cY(DzGs?DR>1~{GI#Ar??ZYL6*VlT9sotK0CEvLINp|( zqJm8UxV?r3plKB+Am6?1D=b;gWVAkbc04m-?jR{(ARm`2)Sl@Rz!L|7B5-PV&DjUW zG=GH+9fs3+9Btu-L>hp$l#zL|C&9ySpy@iy1rup=QZveZ*C31QS*cu>aWH1yG_u1o z92JP~5E-&Vb*@kgN<#o{NJl+tn3Xl~{l8BNOWU9lsm33tl-XcdN~69%%?t!tPDUObzYl0!D36zfM#&U`}d+bH05}BN_*` zxE?2##}dC5p?sogW4(##8nb>pff7sD8STkq9&9K?34QU5UQk|=q zPiiP%Bhzd+w&K6h3a+_B`vxT4bPSA-(H&Edt(VCbl1}?SQDGWMmKEb&JJGLdn2(W% zHQMu3B}@{b_A)1x_ym(d>GVLYtDa)sl}Fi3Z~0#PSs ziMM)gL^<@TP|Zx>;=w^vlf(i?cEGB_(hio8lgkz6f)-+G|D~nTesk04Z#;J^5V}&u&@Q5l;I?I{&8FOuEYE}YR%tvc6Qjy5y}*0ZV9Xk*9WcVg}v)Xq|RzK z@BJ>=Una=Gv6~=edyjkpAPQH>`kIui&mZZki#j{Q{MI+CwY)}{491P6$tT9HKKuQ6 z)21Ss*7_BuVbH+O-@Ht{(0deqA2ad)jwL zr?!w;KFp4hH4p7C9#*|oYl-snz&$9r%i*h?niOrLdejTAFW8XjRt$o0F$*hQ8X?n4 z`1kYE7_R$wVpp%1YQ+Jj2He^s3hL3uvj7e}oC1jZZ#k2?YyR9fzO^I)_f--m+9}VH6%*4>!Lkqrb}yI`4F-5IsS^pbz+_ zYTZJC&maBpjoaZlv5&h;sN-#Ux#`;@`67 z-g#8!_kB}UWMWA!F>!c zaSD>w(;5dhtsMxKEuPl~8l1-imOzXCfbYTmLXqqE4 z1W8Qllh~Wber}S!v@XNs(BZvGgk|5*jo9O^YpAfNi1q7(B$A1gtG=<0DmvsXE(w?f zO@}c0lau2-PTAeaUzouCTCVPoFJ-*H$<+xjp|W`Bb47l9m#`2!3?w=Kt-_ZjacZ>m zK7s4WqfmS8xa?`Ze7~Oyw(4%+HK?4A3`%r@vD*{lE-(@1fuOWGo@jbm4U;(b-J$IQ z45d`ylOko@?|1$tb0qje!)?zjUb(%IC;VY4-;!bPkHSAn^xI=^;}o+1Up0zsqEZ+s zq`wVRP2IWL47}(6dxNJnD0ZLM_>^2mwQg#~_|EFxeeP*l>dm`wbg6OrF!sJ;@kL79 zDR0h3V&-&ZuK-oqi#9uWR?Aq!knzCvL}?ZBBMc0&zqx?lH`#9Miy`4DjXObE1Mfn&OSehRxgnZ?ZRv`<(#ceMNLxtXdD^^H#4os*`Lk)J`FSy_D=P0~N5w&x2?$d`9+`cqT)lScEihW0pd{E3J z)ZA(#823nITT*(;beVlNg_w{$;NW-VBwt+C!;!V0sxP1*WP8;eGpBWr zUj5QpL2-E4cRb8{`B?O){oq%>V2+&5ZBD7&o3Ad@pgOLxDl2Gl%AwQ=awpa0$-7;E ze?4tEa>x^ic}cuTkAGP;W=Z88@ha`!dU28jhE_+Lkp>Go+!|TC&#&aIBO=LRV|NlNb7-g;+3Jm0gk z8rO|`4mXPBug>lTyZ1ns>?6FNab@KjSteU(w<7OFO7OO{O6WR`5?$e)Q{ zEIxd`Rx)h+6FEYLyg%s7f1j&y_|3>k!1J(@nxo=U$`b+NP|_~VN{!s=>T0Tyj!i?$ z9-j9sXz)0aK6MD<^+Bj*0OR??)4G=B;-j&56;`LYH#(c*LB;pV-z`8seM2L0;<(E+ z1Yz@{6hAdFL@JJPkiLE|vA<+y1ikN>v$Jz?ti+@%Fozq>jjY^hp|0B^$Z_xh$4!~- z?6W5MHZ>oG`d-TX)`^p8H@D?-nVa!O)ZL>;AGzyi`)nr+$5s^P>r&_463T zfHT`;vC1CT4>MC!d~G3!N4l>quwV!Sx6*Y$fWLo~r>E!jip@>{>eTXb1-@4{7Q@W4 zYVVf5ayQH7F>;su6?3yxJI{P3h}cF08Xg#?Vf|zZlVN5Yd-+yUbKzMpqOgPr76@0z zLg+ylXiP)F{k3Usl4Z}txkjI{Oj^_hqegRbsB@9?P93x8&NQ6gn7mF^ALVgUyV{w_6VDWXbc+|nc1lBx-)-^t@j)a zJ~yhn`HVgE7%E$s0_RPGSBk8@BN(al#NPbnO}z&-^a+jWpJPUYtDt%cx>&rwJvljX zL9RbNR_u5nQKa;`eWF2wwUR|^1onjyLK}orf-0N_rT{Wvr$r$6u@GM{H8(}>lEA(O z!r1uf;A`3#HbVZRe6B5mz(iwBD&S&9dVE--BGwTW!>5=Cglog}srgUK&8u9T*NmGh zPFM&Hn+J}VawqU(@0AUETDE$W5NIS-3-e{HUr1}c3xf{9k+RpcoP-FU#89? z{>V>vF~!*5HPW)onpc;N&H;CP1GN#)K?dQ^?KXKR@u&d|9JWS|j)$VUzL~XjT5D(D zPYw<~toV$;@gYTCDW*Xpqa!yWm<|)YE#=tIHy=??-h(wUaE`e{cJbt69t(}59)`cx z^av0YBC~7evvYGz&Mc`}qaGMBXXg)#bA=bj$HzEC^#<4l8u3o#nGj|^d|7tptzP-4 z`;eq)PNHy89EgUpMF>04tK!WGM~R@HLWYcu)rQF++a+jUXLe@By{Jkf^&`IQ*N+pK z-I`&wHQQ8oLPB2bF!5#Kx^^e3HeGpCj4(F8(yu)a#MU(kBEdpEps%$NpNaA&Nfupv zvR639RN2_T`A<`XoW1-Q2(Czkn@YE?bS6X1>Ubi|WHsX0qHoriTJPUx zz94j0T;K)teL_W*jZ9(9>1pC;*y!JUe)8K(r$cey)KG%HlD+a}^@j!IHzw%gl;lj! zxzMwaBqvTyJH>cZt$W@5)8wF=S?gc~J?^VpF^61|eivj`H%*+DxfiwhxZsKGmX^=9 z-^+~l*U5d7SmCviS#5<)N9wCaFCBSgev;2K9iqD?r`Ut0jdr|>Cb|wU$oz84C3!RC z)^I$M@9(}NP0W>EO7uO8^3#)7jQ?h*|My_!k?wCs9^1t1?0+)y&!{8?wGGrw|L=_a F{{j1u?sNbE literal 0 HcmV?d00001 diff --git a/art/icon/frame-studio.svg b/art/icon/frame-studio.svg new file mode 100644 index 0000000..9233b10 --- /dev/null +++ b/art/icon/frame-studio.svg @@ -0,0 +1 @@ + diff --git a/cmake/frame_version.cmake b/cmake/frame_version.cmake new file mode 100644 index 0000000..cfd9b9b --- /dev/null +++ b/cmake/frame_version.cmake @@ -0,0 +1,32 @@ +# FRAME_VERSION: what the apps show as their version, the same name the release gets (.github/workflows/build.yml): +# "v" + VERSION (major.minor) + "." + the commits since VERSION last changed, so the commit that bumps VERSION is +# v.0. $FRAME_VERSION overrides it. Without the whole history (a shallow checkout, a copy that isn't a +# git repository) the count is unknown: "v0.2 (local)"; VERSION edited but not committed yet counts from 0. +file(READ "${CMAKE_CURRENT_LIST_DIR}/../VERSION" FRAME_VERSION_BASE) +string(STRIP "${FRAME_VERSION_BASE}" FRAME_VERSION_BASE) +set(FRAME_VERSION "$ENV{FRAME_VERSION}") +if(NOT FRAME_VERSION) + set(FRAME_VERSION "v${FRAME_VERSION_BASE} (local)") + set(FRAME_SRC "${CMAKE_CURRENT_LIST_DIR}/..") + find_package(Git QUIET) + if(GIT_FOUND) + execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse --is-shallow-repository WORKING_DIRECTORY "${FRAME_SRC}" + OUTPUT_VARIABLE FRAME_SHALLOW RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(FRAME_GIT_RC EQUAL 0 AND FRAME_SHALLOW STREQUAL "false") + execute_process(COMMAND "${GIT_EXECUTABLE}" diff --quiet HEAD -- VERSION WORKING_DIRECTORY "${FRAME_SRC}" + RESULT_VARIABLE FRAME_VERSION_EDITED ERROR_QUIET) + execute_process(COMMAND "${GIT_EXECUTABLE}" log -1 --format=%H -- VERSION WORKING_DIRECTORY "${FRAME_SRC}" + OUTPUT_VARIABLE FRAME_BUMP RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(FRAME_VERSION_EDITED EQUAL 1) + set(FRAME_VERSION "v${FRAME_VERSION_BASE}.0 (local)") + elseif(FRAME_GIT_RC EQUAL 0 AND FRAME_BUMP) + execute_process(COMMAND "${GIT_EXECUTABLE}" rev-list --count "${FRAME_BUMP}..HEAD" WORKING_DIRECTORY "${FRAME_SRC}" + OUTPUT_VARIABLE FRAME_COMMITS RESULT_VARIABLE FRAME_GIT_RC OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(FRAME_GIT_RC EQUAL 0 AND FRAME_COMMITS MATCHES "^[0-9]+$") + set(FRAME_VERSION "v${FRAME_VERSION_BASE}.${FRAME_COMMITS}") + endif() + endif() + endif() + endif() +endif() +message(STATUS "Version: ${FRAME_VERSION}") diff --git a/docs/studio-guide.md b/docs/studio-guide.md index 091b23a..2177896 100644 --- a/docs/studio-guide.md +++ b/docs/studio-guide.md @@ -111,7 +111,7 @@ edges, grey are floors you can't reach. **W** shows or hides it. | Item | What it does on the headset | |---|---| | **Spawn Point** | where you start, facing the way it faces | -| **Seat** | a place to sit: point at it and pull the trigger; its "stand up here" ring is where you get up | +| **Seat** | a place to sit: point at it and pull the trigger; you sit facing the way its arrow points; its "stand up here" ring is where you get up | | **Teleport Hotspot** | a spot the teleport arc snaps to | | **Surface** | a box you can scale into a floor, a step or a wall | | **Invisible Wall** | stops you; never drawn on the headset. Scale and turn it to fit; where it stands is no longer walkable | diff --git a/frame/install-frame-home.ps1 b/frame/install-frame-home.ps1 index 63703d0..0d6d2d3 100755 --- a/frame/install-frame-home.ps1 +++ b/frame/install-frame-home.ps1 @@ -33,9 +33,22 @@ $scp = Find-Tool "scp" if ($HostName -notmatch "@") { $HostName = "steamos@$HostName" } # The name an environment gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them). +# When that drops characters (accents, other scripts) a short hash of the real name is added, so two such names don't +# become the same file, and an all-non-Latin one doesn't become a bare ".fenv". function Frame-Name($path) { - $n = ([IO.Path]::GetFileName($path)).ToLowerInvariant() -replace " ", "-" - return ($n -replace "[^a-z0-9._-]", "") + $base = [IO.Path]::GetFileName($path) + $low = $base.ToLowerInvariant() -replace " ", "-" + $n = $low -replace "[^a-z0-9._-]", "" + $dot = $n.LastIndexOf(".") + $stem = if ($dot -ge 0) { $n.Substring(0, $dot) } else { $n } + if ($n -ne $low -or $stem -eq "") { + $sha = [Security.Cryptography.SHA1]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($base)) + $h = -join ($sha[0..3] | ForEach-Object { $_.ToString("x2") }) + $ext = if ($dot -ge 0) { $n.Substring($dot) } else { "" } + if ($stem -eq "") { $stem = "environment" } + $n = "$stem-$h$ext" + } + return $n } $payload = Join-Path $PSScriptRoot "payload" @@ -62,6 +75,11 @@ if ($Uninstall) { Say "copying $([IO.Path]::GetFileName($f)) -> $name" Push-Location (Split-Path -Parent (Resolve-Path $f)) try { & $scp -q ([IO.Path]::GetFileName($f)) "${HostName}:frame-home/envs/$name.part" } finally { Pop-Location } + if ($LASTEXITCODE -ne 0) { # (no half-copied file left behind, and the one shown stays as it was) + & $ssh $HostName "rm -f frame-home/envs/$name.part" + Say "couldn't copy $([IO.Path]::GetFileName($f)) (the headset's disk is full, or the connection dropped)" + continue + } & $ssh $HostName "mv -f frame-home/envs/$name.part frame-home/envs/$name" } & $ssh $HostName "sed -i 's/\r`$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh" @@ -70,7 +88,7 @@ if ($Uninstall) { $first = $Environments | Where-Object { $_ -match "\.fenv$" } | Select-Object -First 1 if ($first -and (Test-Path $first)) { $name = Frame-Name $first - & $ssh $HostName "printf '%s\n' '$name' > frame-home/current" + & $ssh $HostName "if [ -f 'frame-home/envs/$name' ]; then printf '%s\n' '$name' > frame-home/current; fi" } } Say "done" diff --git a/frame/install-frame-home.sh b/frame/install-frame-home.sh index 22ec6d5..bb27140 100755 --- a/frame/install-frame-home.sh +++ b/frame/install-frame-home.sh @@ -26,6 +26,20 @@ $1"; shift ;; done case "$HOST" in *@*) ;; *) HOST="steamos@$HOST" ;; esac say() { echo "frame-home: $*"; } +# The name a file gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them). When +# that drops characters (accents, other scripts) a short hash of the real name is added, so two such names don't +# become the same file, and an all-non-Latin one doesn't become a bare ".fenv". +frame_name() { + base=$(basename "$1") + low=$(printf '%s' "$base" | tr 'A-Z ' 'a-z-') + n=$(printf '%s' "$low" | tr -cd 'a-z0-9._-') + if [ "$n" != "$low" ] || [ -z "${n%.*}" ]; then + h=$(printf '%s' "$base" | cksum | cut -d' ' -f1) + stem=${n%.*} + n="${stem:-environment}-$h.${n##*.}" + fi + printf '%s' "$n" +} [ -f "$PAYLOAD/install.sh" ] || { say "the headset files are missing: expected $PAYLOAD/install.sh"; exit 1; } say "connecting to $HOST" @@ -40,17 +54,18 @@ if [ "$MODE" = install ]; then printf '%s\n' "$ENVS" | while IFS= read -r f; do [ -n "$f" ] || continue [ -f "$f" ] || { say "no such file: $f"; continue; } - # the name it gets on the headset: lowercase, spaces as dashes, only [a-z0-9._-] (as Studio names them) - name=$(basename "$f" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-') + name=$(frame_name "$f") say "copying $(basename "$f") -> $name" - scp -q "$f" "$HOST:frame-home/envs/$name.part" && ssh "$HOST" "mv -f frame-home/envs/$name.part frame-home/envs/$name" + scp -q "$f" "$HOST:frame-home/envs/$name.part" && ssh "$HOST" "mv -f frame-home/envs/$name.part frame-home/envs/$name" || { + ssh "$HOST" "rm -f frame-home/envs/$name.part" || true; say "couldn't copy $(basename "$f") (the headset's disk is full, or the connection dropped)"; } done ssh "$HOST" "sed -i 's/\r\$//' frame-home/.install/frame/install.sh; sh frame-home/.install/frame/install.sh" # the first environment given is the one shown (a bundle's are only known once the headset unpacks it: skipped) FIRST=$(printf '%s\n' "$ENVS" | grep -i '\.fenv$' | sed -n '1p') if [ -n "$FIRST" ] && [ -f "$FIRST" ]; then - name=$(basename "$FIRST" | tr 'A-Z ' 'a-z-' | tr -cd 'a-z0-9._-') - ssh "$HOST" "printf '%s\n' '$name' > frame-home/current" + name=$(frame_name "$FIRST") + # (only when it got there: a copy that failed leaves the one shown as it was) + ssh "$HOST" "if [ -f 'frame-home/envs/$name' ]; then printf '%s\n' '$name' > frame-home/current; fi" fi else # (never installed from a copy like this one: Frame Home itself puts SteamVR's environment back) diff --git a/frame/payload/install.sh b/frame/payload/install.sh index b4f8e32..0456425 100755 --- a/frame/payload/install.sh +++ b/frame/payload/install.sh @@ -63,10 +63,23 @@ if ! LD_LIBRARY_PATH="$P/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$P/bin/frame say "Frame Home can't run on this headset: ${missing:-unknown error} (nothing was changed)"; exit 1 fi mkdir -p "$H/bin" "$H/envs" || exit 1 -# New binaries beside the old ones, then renamed over them: a running copy keeps its file until it restarts. +# New binaries beside the old ones, then renamed over them: a running copy keeps its file until it restarts. All of +# them copied before any is renamed, so a copy that fails (a full disk) leaves the old set whole, never a mix. for x in frame-home frame-switcher fenv-tool; do [ -f "$P/bin/$x" ] || continue - cp -f "$P/bin/$x" "$H/bin/$x.new" && chmod +x "$H/bin/$x.new" && mv -f "$H/bin/$x.new" "$H/bin/$x" || { say "couldn't install $x"; exit 1; } + cp -f "$P/bin/$x" "$H/bin/$x.new" && chmod +x "$H/bin/$x.new" || { rm -f "$H"/bin/*.new; say "couldn't install $x (nothing was changed)"; exit 1; } +done +# The compositor layer and its shader are copied in the same round: new programs never go in beside an old layer +# because the layer's copy failed (Frame Home and the layer share the frames). +if [ -f "$P/layer/libframe_home_layer.so" ]; then + mkdir -p "$H/layer" || { rm -f "$H"/bin/*.new; say "couldn't install the compositor layer (nothing was changed)"; exit 1; } + for f in libframe_home_layer.so env_ps.spv; do + cp -f "$P/layer/$f" "$H/layer/.$f.new" || { rm -f "$H"/bin/*.new "$H"/layer/.*.new; say "couldn't install the compositor layer (nothing was changed)"; exit 1; } + done +fi +for x in frame-home frame-switcher fenv-tool; do + [ -f "$H/bin/$x.new" ] || continue + mv -f "$H/bin/$x.new" "$H/bin/$x" || { say "couldn't install $x"; exit 1; } done # The compositor layer and its shader (renamed over the old ones: a running compositor keeps its copy). layerChanged=0 @@ -74,7 +87,8 @@ if [ -f "$P/layer/libframe_home_layer.so" ]; then mkdir -p "$H/layer" "$(dirname "$LAYERJSON")" cmp -s "$P/layer/libframe_home_layer.so" "$H/layer/libframe_home_layer.so" || layerChanged=1 cmp -s "$P/layer/env_ps.spv" "$H/layer/env_ps.spv" || layerChanged=1 - for f in libframe_home_layer.so env_ps.spv; do cp -f "$P/layer/$f" "$H/layer/.$f.new" && mv -f "$H/layer/.$f.new" "$H/layer/$f"; done + # (the library and its shader go together: both copied above, then both renamed) + for f in libframe_home_layer.so env_ps.spv; do mv -f "$H/layer/.$f.new" "$H/layer/$f" || { say "couldn't install the compositor layer"; exit 1; }; done rm -f "$H/layer/off" "$H/layer/starting" "$H/layer/crashes" [ -f "$LAYERJSON" ] || layerChanged=1 cat > "$LAYERJSON" < 0.0) dir = normalize(dir * homeFrame.walk.w + homeFrame.walk.xyz); const vec4 c = homeFrame.viewProj[k] * vec4(dir, 0.0); vec3 rgb = vec3(0.0); if (c.w > 1e-5 && homeFrame.params.y > 0.5) { diff --git a/layer/frame_home_layer.cpp b/layer/frame_home_layer.cpp index 2b9da83..52046bf 100644 --- a/layer/frame_home_layer.cpp +++ b/layer/frame_home_layer.cpp @@ -26,8 +26,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -138,13 +140,16 @@ struct InstanceData { X(DestroyImage) X(CreateImageView) X(DestroyImageView) X(GetImageMemoryRequirements) X(AllocateMemory) X(FreeMemory) \ X(BindImageMemory) X(CreateBuffer) X(DestroyBuffer) X(GetBufferMemoryRequirements) X(BindBufferMemory) X(MapMemory) \ X(CmdBindPipeline) X(CmdBindDescriptorSets) X(CmdDraw) X(CmdDrawIndexed) X(QueueSubmit) X(QueueSubmit2) \ - X(QueueSubmit2KHR) X(QueueWaitIdle) X(DeviceWaitIdle) + X(QueueSubmit2KHR) X(QueueWaitIdle) X(DeviceWaitIdle) X(DestroyDescriptorPool) X(DestroyDescriptorSetLayout) X(DestroySampler) \ + X(AllocateCommandBuffers) X(FreeCommandBuffers) X(BeginCommandBuffer) X(ResetCommandBuffer) X(ResetCommandPool) \ + X(DestroyCommandPool) X(CreateCommandPool) X(EndCommandBuffer) X(CmdPipelineBarrier) X(GetDeviceQueue2) struct DeviceData { VkDevice device = VK_NULL_HANDLE; VkPhysicalDevice physical = VK_NULL_HANDLE; InstanceData* inst = nullptr; PFN_vkGetDeviceProcAddr gdpa = nullptr; + PFN_vkSetDeviceLoaderData setLoaderData = nullptr; // our own command buffers get the loader's dispatch with it bool samplesSubsampled = false; // VK_EXT_fragment_density_map is on: Frame Home's foveated frames read as made #define FH_DECL(n) PFN_vk##n n = nullptr; FH_DEVICE_FUNCS(FH_DECL) @@ -155,6 +160,7 @@ std::mutex g_mutex; std::unordered_map g_instances; std::unordered_map g_physToInstance; // physical device (dispatch key) -> instance data std::unordered_map g_devices; +std::unordered_map g_queueFamily; // (g_mutex) each queue the compositor got, its family DeviceData* dev(const void* dispatchable) { std::lock_guard l(g_mutex); @@ -176,6 +182,12 @@ std::unordered_map g_twins; // Aurora pipeline -> std::unordered_set g_skip; // other Aurora pieces: not drawn while the home shows std::unordered_set g_camera; // passthrough (the cameras' reprojection) std::atomic g_cameraNs{0}; // when passthrough was last drawn +// Passthrough drawn in the last 150 ms. Signed: another thread's command buffer may stamp g_cameraNs between our +// clock read and its load, and that is "just now", not 584 years ago (unsigned, it flashed the home over the cameras). +bool cameraRecent() { + const uint64_t c = g_cameraNs.load(); + return c && static_cast(envshare::nowNs() - c) < 150000000ll; +} // Aurora's stars are a render model (rendermodels/stars), drawn with the generic model pipeline the controllers use // too: they're told apart by their index count, read from the model itself. uint32_t g_starsIndices = 0; @@ -197,33 +209,77 @@ std::unordered_map g_bound; // graphics pipeline std::unordered_map g_names; // vertex + fragment shader names (trace) std::atomic g_traceDraws{0}; // ~/frame-home/layer/trace: log this many draws std::atomic g_off{false}; +std::atomic g_startedOff{false}; // off as this compositor started: none of our pipelines exist // Our objects on the compositor's device. +// +// Each Hello's images are a generation (Gen): the images, their views, and descriptor sets of their own, written +// once when the generation is made and never again. A descriptor set bound in a recorded command buffer must not be +// rewritten or freed until that command buffer is done with (re-begun, reset or freed: the spec forbids any of those +// while it's still pending on the GPU, so by then the GPU is done with it too). So a new generation never touches the +// old one's sets; the old one is retired, and destroyed once no command buffer that drew with it is left. +// +// The images are external memory written on Frame Home's queue: each frame Frame Home finishes is released to +// VK_QUEUE_FAMILY_EXTERNAL (src/hsl_renderer.cpp), and the compositor's queue must acquire it before sampling it, or +// what it reads is undefined (on some GPUs, caches or compression state another process left behind). The acquire is +// one pipeline barrier per slot, recorded once per generation and queue family (acquire[]), and submitted just ahead of +// the first compositor command buffer that draws a frame newer than the slot last acquired (acquiredFrame[]): once per +// Frame Home frame, never per draw. GENERAL to GENERAL on both sides (no layout transition), so an older Frame Home +// that doesn't release pairs with this layer as before, and an older layer that doesn't acquire with a newer Frame Home. +struct Use { uint32_t slots = 0; uint64_t frame[envshare::kSlots] = {}; }; // a command buffer's draws: which slots, which frames +struct Gen { + uint32_t generation = 0; + uint32_t layers = 1; + bool subsampled = false; // drawn with the subsampled twins (and their set layout) + VkImage image[envshare::kSlots] = {}; + VkDeviceMemory mem[envshare::kSlots] = {}; + VkImageView view[envshare::kSlots] = {}; + VkDescriptorPool pool = VK_NULL_HANDLE; // this generation's own (its sets go with it) + VkDescriptorSet set[envshare::kSlots] = {}; + std::unordered_map users; // recorded with these sets and not yet re-begun/reset/freed + VkCommandBuffer acquire[envshare::kSlots] = {}; // the slot's acquire barrier (for acquireFamily[k]) + uint32_t acquireFamily[envshare::kSlots] = {}; + uint64_t acquiredFrame[envshare::kSlots] = {}; // the Frame Home frame last acquired in the slot (0: none) +}; +constexpr size_t kMaxRetired = 2; // a compositor that never re-records a command buffer can't pile generations up + struct Ours { DeviceData* d = nullptr; VkShaderModule fragment = VK_NULL_HANDLE; VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; VkDescriptorSetLayout setLayoutSubsampled = VK_NULL_HANDLE; // (null: this device can't sample subsampled images) - VkDescriptorPool pool = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE, subsampledSampler = VK_NULL_HANDLE; - // The imported frames (one generation per Hello). - struct Slot { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; - VkBuffer ubo = VK_NULL_HANDLE; VkDeviceMemory uboMem = VK_NULL_HANDLE; void* uboMapped = nullptr; - VkDescriptorSet set = VK_NULL_HANDLE, setSubsampled = VK_NULL_HANDLE; uint64_t uboFrame = ~0ull; }; - Slot slot[envshare::kSlots]; - bool imported = false; - bool subsampled = false; // the imported images are subsampled (drawn with the subsampled twins) + // Each slot's uniforms are a ring of kUboRing blocks, one per compositor frame (a dynamic offset picks it): what a + // frame is drawn with changes every compositor frame (the turn carried on, the fade), and the block the last one is + // still drawing with on the GPU mustn't change under it. Shared by every generation (only the offset is bound). + struct Ubo { VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* mapped = nullptr; }; + Ubo ubo[envshare::kSlots]; + Gen* cur = nullptr; // the generation drawn now + std::vector retired; // older ones, until no command buffer uses them uint32_t generation = 0; // of the imported images int lastGood = -1; // newest slot of this generation drawn (shown while Frame Home resizes) + // What each slot was rendered with, copied (consistently: env_share.h readSlot) when the layer last saw it newest + // in this generation. Drawn from this copy, never from the shared slot: after a resize Frame Home rewrites the + // slot the layer is still showing from the old images with the new generation's pose. + envshare::Slot shown[envshare::kSlots]; + envshare::Motion shownMotion[envshare::kSlots]; envshare::State* state = nullptr; size_t stateSize = 0; + bool hasMotion = false; // the State says how the view moves (a v6 Frame Home; Motion) + bool seqlocked = false; // the State's slots are written under sequence counts (a v7 State) uint64_t healthyFrames = 0; bool baseReady = false, baseFailed = false; // ensureBase(): all made / gave up (the rest of the session is Aurora's) + std::unordered_map acquirePools; // per queue family: the generations' acquire[] command buffers + uint64_t acquires = 0; // frames acquired (logged as the device goes) } g_ours; std::mutex g_oursMutex; +std::atomic g_imported{false}; // g_ours.cur is set (read unlocked by the draw hooks as a quick "nothing to do") int g_latchSlot = -1; // frameSlot(): this compositor frame's pick, when it was made, and whether the -uint64_t g_latchNs = 0; // home was drawn with it (sampledNs stamped) +uint64_t g_latchNs = 0; // home was drawn with it (sampledNs stamped, its uniforms filled in) bool g_latchStamped = false; +constexpr uint32_t kUboRing = 8, kUboStride = 256; // uniform blocks per slot (compositor frames in flight are fewer) +uint32_t g_latchRing = 0; // this compositor frame's block +std::unordered_map g_cbPool; // (g_oursMutex) which pool each command buffer is from // A Hello from Frame Home waiting to be imported on the compositor's thread. struct Pending { bool have = false; envshare::Hello hello; int fds[envshare::kSlots] = {-1, -1, -1, -1}; int stateFd = -1; } g_pending; @@ -238,23 +294,95 @@ uint32_t memoryTypeFor(DeviceData* d, uint32_t bits, VkMemoryPropertyFlags want) return UINT32_MAX; } -void releaseImports() { +// A descriptor pool with room for one generation's sets of the given kind, and the sets. A subsampled combined image +// sampler takes half again a plain one's room, and Turnip sizes a pool for that only when the device enables the +// fragmentDensityMap feature (left off: it would cost the compositor elsewhere). So the count asks for the room +// (2 * kSlots plain-sized ones covers kSlots subsampled). +bool makeSets(DeviceData* d, VkDescriptorSetLayout layout, VkDescriptorPool& pool, VkDescriptorSet* sets) { + VkDescriptorPoolSize ps[2] = {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 * envshare::kSlots}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, envshare::kSlots}}; + VkDescriptorPoolCreateInfo pi = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pi.maxSets = envshare::kSlots; + pi.poolSizeCount = 2; + pi.pPoolSizes = ps; + pool = VK_NULL_HANDLE; + if (d->CreateDescriptorPool(d->device, &pi, nullptr, &pool) != VK_SUCCESS) { pool = VK_NULL_HANDLE; return false; } + VkDescriptorSetLayout layouts[envshare::kSlots]; + for (auto& l : layouts) l = layout; + VkDescriptorSetAllocateInfo dai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + dai.descriptorPool = pool; + dai.descriptorSetCount = envshare::kSlots; + dai.pSetLayouts = layouts; + if (d->AllocateDescriptorSets(d->device, &dai, sets) != VK_SUCCESS) { + d->DestroyDescriptorPool(d->device, pool, nullptr); + pool = VK_NULL_HANDLE; + for (uint32_t i = 0; i < envshare::kSlots; ++i) sets[i] = VK_NULL_HANDLE; + return false; + } + return true; +} + +// (g_oursMutex held, once it has acquire command buffers) None of those is pending by now: each was submitted ahead +// of a command buffer among the generation's users, and those are done. +void destroyGen(DeviceData* d, Gen* g) { + if (!g) return; + for (uint32_t i = 0; i < envshare::kSlots; ++i) + if (g->acquire[i]) { + auto p = g_ours.acquirePools.find(g->acquireFamily[i]); + if (p != g_ours.acquirePools.end()) d->FreeCommandBuffers(d->device, p->second, 1, &g->acquire[i]); + } + if (g->pool) d->DestroyDescriptorPool(d->device, g->pool, nullptr); // (its sets with it) + for (uint32_t i = 0; i < envshare::kSlots; ++i) { + if (g->view[i]) d->DestroyImageView(d->device, g->view[i], nullptr); + if (g->image[i]) d->DestroyImage(d->device, g->image[i], nullptr); + if (g->mem[i]) d->FreeMemory(d->device, g->mem[i], nullptr); + } + delete g; +} + +// (g_oursMutex held) Destroys the retired generations no command buffer uses any more. More than kMaxRetired still +// in use (command buffers recorded once and never re-recorded): the oldest goes after the device idles, as the layer +// always did before generations had their own sets. +void collectRetired() { DeviceData* d = g_ours.d; if (!d) return; - for (auto& s : g_ours.slot) { - if (s.view) d->DestroyImageView(d->device, s.view, nullptr); - if (s.image) d->DestroyImage(d->device, s.image, nullptr); - if (s.mem) d->FreeMemory(d->device, s.mem, nullptr); - s.view = VK_NULL_HANDLE; s.image = VK_NULL_HANDLE; s.mem = VK_NULL_HANDLE; - s.uboFrame = ~0ull; + auto& r = g_ours.retired; + for (size_t i = 0; i < r.size();) { + if (r[i]->users.empty()) { destroyGen(d, r[i]); r.erase(r.begin() + static_cast(i)); } + else ++i; } + if (r.size() > kMaxRetired) { + logf("generation %u: %zu command buffers still hold it; waiting for the GPU to free it", r.front()->generation, r.front()->users.size()); + d->DeviceWaitIdle(d->device); + destroyGen(d, r.front()); + r.erase(r.begin()); + } +} + +// A command buffer re-begun, reset or freed (none of which may happen while it's pending): no generation is held by it. +void forgetCommandBuffer(VkCommandBuffer cb) { + std::lock_guard ol(g_oursMutex); + if (g_ours.cur) g_ours.cur->users.erase(cb); + for (Gen* g : g_ours.retired) g->users.erase(cb); +} +void forgetPool(VkCommandPool pool, bool destroyed) { + std::lock_guard ol(g_oursMutex); + auto drop = [&](Gen* g) { for (auto it = g->users.begin(); it != g->users.end();) { auto p = g_cbPool.find(it->first); if (p != g_cbPool.end() && p->second == pool) it = g->users.erase(it); else ++it; } }; + if (g_ours.cur) drop(g_ours.cur); + for (Gen* g : g_ours.retired) drop(g); + if (destroyed) for (auto it = g_cbPool.begin(); it != g_cbPool.end();) it = it->second == pool ? g_cbPool.erase(it) : std::next(it); +} + +// (g_oursMutex held) The current generation stops being drawn: it waits among the retired for its command buffers. +void retireCurrent() { + if (g_ours.cur) g_ours.retired.push_back(g_ours.cur); + g_ours.cur = nullptr; + g_imported = false; if (g_ours.state) ::munmap(g_ours.state, g_ours.stateSize); g_ours.state = nullptr; - g_ours.imported = false; g_ours.lastGood = -1; } -// One-time objects (shader, set layout, pool, sampler, per-slot UBOs and sets) on the compositor's device. +// One-time objects (shader, set layouts, sampler, per-slot UBOs) on the compositor's device. bool ensureBase(DeviceData* d) { if (g_ours.baseReady) return g_ours.d == d; // made on the compositor's device: only that one draws if (g_ours.baseFailed) return false; @@ -268,7 +396,7 @@ bool ensureBase(DeviceData* d) { if (d->CreateShaderModule(d->device, &smi, nullptr, &g_ours.fragment) != VK_SUCCESS) return false; VkDescriptorSetLayoutBinding b[2] = {}; b[0].binding = 0; b[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; b[0].descriptorCount = 1; b[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - b[1].binding = 1; b[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; b[1].descriptorCount = 1; b[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + b[1].binding = 1; b[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; b[1].descriptorCount = 1; b[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo li = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; li.bindingCount = 2; li.pBindings = b; @@ -287,47 +415,34 @@ bool ensureBase(DeviceData* d) { if (d->CreateDescriptorSetLayout(d->device, &sli, nullptr, &g_ours.setLayoutSubsampled) != VK_SUCCESS) g_ours.setLayoutSubsampled = VK_NULL_HANDLE; } + // Can a generation's worth of that kind really be allocated here? (no: no twins of that kind are made) + VkDescriptorPool probe = VK_NULL_HANDLE; + VkDescriptorSet probeSets[envshare::kSlots]; + if (g_ours.setLayoutSubsampled && !makeSets(d, g_ours.setLayoutSubsampled, probe, probeSets)) { + d->DestroyDescriptorSetLayout(d->device, g_ours.setLayoutSubsampled, nullptr); + g_ours.setLayoutSubsampled = VK_NULL_HANDLE; + logf("subsampled frames: no descriptor set (plain frames only)"); + } + if (probe) d->DestroyDescriptorPool(d->device, probe, nullptr); logf(g_ours.setLayoutSubsampled ? "subsampled frames: readable" : "subsampled frames: no sampler/layout (plain frames only)"); } - // A subsampled combined image sampler takes half again a plain one's room, and Turnip sizes a pool for that only - // when the device enables the fragmentDensityMap feature (left off: it would cost the compositor elsewhere). So the - // count asks for the room: kSlots plain + kSlots subsampled = 2.5 * kSlots plain-sized ones (3 * kSlots to spare). - VkDescriptorPoolSize ps[2] = {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3 * envshare::kSlots}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2 * envshare::kSlots}}; - VkDescriptorPoolCreateInfo pi = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - pi.maxSets = 2 * envshare::kSlots; // a set of each kind per slot - pi.poolSizeCount = 2; - pi.pPoolSizes = ps; - if (d->CreateDescriptorPool(d->device, &pi, nullptr, &g_ours.pool) != VK_SUCCESS) return false; VkSamplerCreateInfo si = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; si.magFilter = si.minFilter = VK_FILTER_LINEAR; si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; if (d->CreateSampler(d->device, &si, nullptr, &g_ours.sampler) != VK_SUCCESS) return false; - for (auto& s : g_ours.slot) { + for (auto& u : g_ours.ubo) { VkBufferCreateInfo bi = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - bi.size = 256; + bi.size = kUboRing * kUboStride; // (256: no device needs uniform offsets aligned to more) bi.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; - if (d->CreateBuffer(d->device, &bi, nullptr, &s.ubo) != VK_SUCCESS) return false; + if (d->CreateBuffer(d->device, &bi, nullptr, &u.buffer) != VK_SUCCESS) return false; VkMemoryRequirements req; - d->GetBufferMemoryRequirements(d->device, s.ubo, &req); + d->GetBufferMemoryRequirements(d->device, u.buffer, &req); VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.allocationSize = req.size; ai.memoryTypeIndex = memoryTypeFor(d, req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - if (ai.memoryTypeIndex == UINT32_MAX || d->AllocateMemory(d->device, &ai, nullptr, &s.uboMem) != VK_SUCCESS) return false; - d->BindBufferMemory(d->device, s.ubo, s.uboMem, 0); - d->MapMemory(d->device, s.uboMem, 0, VK_WHOLE_SIZE, 0, &s.uboMapped); - VkDescriptorSetAllocateInfo dai = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - dai.descriptorPool = g_ours.pool; - dai.descriptorSetCount = 1; - dai.pSetLayouts = &g_ours.setLayout; - if (d->AllocateDescriptorSets(d->device, &dai, &s.set) != VK_SUCCESS) return false; - if (g_ours.setLayoutSubsampled) { - dai.pSetLayouts = &g_ours.setLayoutSubsampled; - if (d->AllocateDescriptorSets(d->device, &dai, &s.setSubsampled) != VK_SUCCESS) { - s.setSubsampled = VK_NULL_HANDLE; - g_ours.setLayoutSubsampled = VK_NULL_HANDLE; // (no twins of that kind are made: plain frames only) - logf("subsampled frames: no descriptor set (plain frames only)"); - } - } + if (ai.memoryTypeIndex == UINT32_MAX || d->AllocateMemory(d->device, &ai, nullptr, &u.mem) != VK_SUCCESS) return false; + d->BindBufferMemory(d->device, u.buffer, u.mem, 0); + if (d->MapMemory(d->device, u.mem, 0, VK_WHOLE_SIZE, 0, &u.mapped) != VK_SUCCESS) return false; } g_ours.baseFailed = false; g_ours.baseReady = true; @@ -335,7 +450,7 @@ bool ensureBase(DeviceData* d) { return true; } -// Imports a Hello's images (compositor thread, inside QueueSubmit: nothing of ours is in flight once the queue idles). +// Imports a Hello's images (compositor thread, inside QueueSubmit) as a new generation beside the one showing. void armHealth(); void importPending(DeviceData* d, VkQueue) { @@ -356,35 +471,44 @@ void importPending(DeviceData* d, VkQueue) { d->inst->GetPhysicalDeviceProperties2(d->physical, &pp); if (std::memcmp(idp.deviceUUID, p.hello.deviceUUID, 16) != 0) { logf("frames from another GPU: ignored"); closeAll(); return; } // The new frames are made beside the ones showing, which stay until all of them import: a failed import keeps - // the last ones (the sets in this very submit never point at destroyed images). - const size_t stateSize = sizeof(envshare::State); + // the last ones. + // How much State there is: a v5 Frame Home's ends before Motion, a v6 one's before the sequence counts (v7). + struct stat sst = {}; + const bool statOk = ::fstat(p.stateFd, &sst) == 0 && sst.st_size >= 0; + const envshare::StateLayout layout = envshare::stateLayout(p.hello.version, statOk ? static_cast(sst.st_size) : 0); + const bool hasMotion = layout.motion; + const size_t stateSize = layout.size; + // The State must really be that large: touching a mapping past the end of its file is SIGBUS, which would take + // the compositor down. And the images must be ones Vulkan can make (a memory type bit, layers to draw). + if (!statOk || !stateSize || p.hello.memoryType >= 32 || p.hello.layers < 1 || + p.hello.layers > 2 || !p.hello.width || !p.hello.height) { + logf("bad hello from Frame Home (state %lld bytes, memory type %u, %u layers, %ux%u): ignored", static_cast(sst.st_size), + p.hello.memoryType, p.hello.layers, p.hello.width, p.hello.height); + closeAll(); + return; + } void* st = ::mmap(nullptr, stateSize, PROT_READ | PROT_WRITE, MAP_SHARED, p.stateFd, 0); ::close(p.stateFd); p.stateFd = -1; if (st == MAP_FAILED) { logf("cannot map the shared state"); closeAll(); return; } + // Sequence counts only when the State is v7 throughout (an older Frame Home's slots are copied twice and compared). + const bool seqlocked = envshare::seqlocked(static_cast(st), stateSize); // Subsampled frames need this device to sample them (the extension, the sampler and the twins made for them): // otherwise Frame Home is told, and sends plain ones. const bool subsampled = (p.hello.flags & VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) != 0; - bool subsampledSets = g_ours.setLayoutSubsampled != VK_NULL_HANDLE; - for (const auto& s : g_ours.slot) subsampledSets &= s.setSubsampled != VK_NULL_HANDLE; - if (p.hello.flags & ~static_cast(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !subsampledSets)) { + if (p.hello.flags & ~static_cast(VK_IMAGE_CREATE_SUBSAMPLED_BIT_EXT) || (subsampled && !g_ours.setLayoutSubsampled)) { static_cast(st)->refusedFlags.store(p.hello.flags); logf("frames with create flags 0x%x: can't read them here (Frame Home will send plain ones)", p.hello.flags); ::munmap(st, stateSize); closeAll(); return; } - struct Made { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; } made[envshare::kSlots]; - auto dropMade = [&] { - for (Made& m : made) { - if (m.view) d->DestroyImageView(d->device, m.view, nullptr); - if (m.image) d->DestroyImage(d->device, m.image, nullptr); - if (m.mem) d->FreeMemory(d->device, m.mem, nullptr); - } - ::munmap(st, stateSize); - }; + Gen* g = new Gen(); + g->generation = p.hello.generation; + g->layers = p.hello.layers; + g->subsampled = subsampled; + auto dropMade = [&] { destroyGen(d, g); ::munmap(st, stateSize); }; for (uint32_t i = 0; i < envshare::kSlots; ++i) { - Made& s = made[i]; VkExternalMemoryImageCreateInfo ext = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO}; ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; VkImageCreateInfo ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; @@ -399,59 +523,70 @@ void importPending(DeviceData* d, VkQueue) { ci.usage = p.hello.usage; // exactly as Frame Home created them (the import must match) ci.flags = p.hello.flags; ci.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - bool ok = d->CreateImage(d->device, &ci, nullptr, &s.image) == VK_SUCCESS; + bool ok = d->CreateImage(d->device, &ci, nullptr, &g->image[i]) == VK_SUCCESS; + if (!ok) g->image[i] = VK_NULL_HANDLE; if (ok) { VkMemoryRequirements req; - d->GetImageMemoryRequirements(d->device, s.image, &req); + d->GetImageMemoryRequirements(d->device, g->image[i], &req); VkImportMemoryFdInfoKHR imp = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR}; imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT; imp.fd = p.fds[i]; VkMemoryDedicatedAllocateInfo ded = {VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO}; - ded.image = s.image; + ded.image = g->image[i]; if (p.hello.dedicated) imp.pNext = &ded; VkMemoryAllocateInfo ai = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; ai.pNext = &imp; ai.allocationSize = p.hello.memorySize[i]; ai.memoryTypeIndex = p.hello.memoryType; ok = (req.memoryTypeBits & (1u << p.hello.memoryType)) && req.size <= p.hello.memorySize[i] && - d->AllocateMemory(d->device, &ai, nullptr, &s.mem) == VK_SUCCESS; + d->AllocateMemory(d->device, &ai, nullptr, &g->mem[i]) == VK_SUCCESS; if (ok) p.fds[i] = -1; // the driver owns it now - ok = ok && d->BindImageMemory(d->device, s.image, s.mem, 0) == VK_SUCCESS; + else g->mem[i] = VK_NULL_HANDLE; + ok = ok && d->BindImageMemory(d->device, g->image[i], g->mem[i], 0) == VK_SUCCESS; } if (ok) { VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - vi.image = s.image; + vi.image = g->image[i]; vi.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; vi.format = ci.format; vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, p.hello.layers}; - ok = d->CreateImageView(d->device, &vi, nullptr, &s.view) == VK_SUCCESS; + ok = d->CreateImageView(d->device, &vi, nullptr, &g->view[i]) == VK_SUCCESS; + if (!ok) g->view[i] = VK_NULL_HANDLE; } if (!ok) { logf("importing frame slot %u failed: keeping the last frames", i); dropMade(); closeAll(); return; } } closeAll(); - std::lock_guard ol(g_oursMutex); - // Swap: nothing of the compositor's (any queue) may still be reading the old frames. - if (g_ours.imported) { d->DeviceWaitIdle(d->device); releaseImports(); } - g_ours.state = static_cast(st); - g_ours.stateSize = stateSize; + // This generation's own sets, written now, before any command buffer can bind them (and never written again). + if (!makeSets(d, subsampled ? g_ours.setLayoutSubsampled : g_ours.setLayout, g->pool, g->set)) { + logf("no descriptor sets for the new frames: keeping the last frames"); + dropMade(); + return; + } for (uint32_t i = 0; i < envshare::kSlots; ++i) { - auto& s = g_ours.slot[i]; - s.image = made[i].image; s.mem = made[i].mem; s.view = made[i].view; - VkDescriptorImageInfo ii = {g_ours.sampler, s.view, VK_IMAGE_LAYOUT_GENERAL}; // (the subsampled kind's sampler is in its layout) - VkDescriptorBufferInfo bi = {s.ubo, 0, 256}; - const VkDescriptorSet set = subsampled ? s.setSubsampled : s.set; + VkDescriptorImageInfo ii = {g_ours.sampler, g->view[i], VK_IMAGE_LAYOUT_GENERAL}; // (the subsampled kind's sampler is in its layout) + VkDescriptorBufferInfo bi = {g_ours.ubo[i].buffer, 0, kUboStride}; VkWriteDescriptorSet w[2] = {{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}}; - w[0].dstSet = set; w[0].dstBinding = 0; w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[0].pImageInfo = ⅈ - w[1].dstSet = set; w[1].dstBinding = 1; w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w[1].pBufferInfo = &bi; + w[0].dstSet = g->set[i]; w[0].dstBinding = 0; w[0].descriptorCount = 1; w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w[0].pImageInfo = ⅈ + w[1].dstSet = g->set[i]; w[1].dstBinding = 1; w[1].descriptorCount = 1; w[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; w[1].pBufferInfo = &bi; d->UpdateDescriptorSets(d->device, 2, w, 0, nullptr); } - g_ours.imported = true; - g_ours.subsampled = subsampled; - logf("imported Frame Home's frames: %ux%u, usage 0x%x, flags 0x%x (generation %u)", p.hello.width, p.hello.height, p.hello.usage, - p.hello.flags, p.hello.generation); + std::lock_guard ol(g_oursMutex); + // Swap: the old generation stays alive (images, views, sets) until the command buffers drawn with it are done. + retireCurrent(); + g_ours.cur = g; + g_ours.state = static_cast(st); + g_ours.stateSize = stateSize; + g_ours.hasMotion = hasMotion; + g_ours.seqlocked = seqlocked; g_ours.generation = p.hello.generation; g_ours.lastGood = -1; + for (auto& s : g_ours.shown) s = envshare::Slot(); + for (auto& m : g_ours.shownMotion) m = envshare::Motion(); g_latchNs = 0; + g_imported = true; + collectRetired(); + logf("imported Frame Home's frames: %ux%u, usage 0x%x, flags 0x%x (generation %u%s)", p.hello.width, p.hello.height, p.hello.usage, + p.hello.flags, p.hello.generation, seqlocked ? "" : ", an older Frame Home: slots without sequence counts"); armHealth(); logf("Frame Home connected: %ux%u frames, %u slots", p.hello.width, p.hello.height, envshare::kSlots); } @@ -459,16 +594,27 @@ void importPending(DeviceData* d, VkQueue) { // The slot to draw now, or -1 for Aurora. While Frame Home is connected the home stays up: a late frame, a home // loading or new images after a resize keep the last good frame on screen (Aurora flashing in looked broken). // Only Frame Home gone, or frozen for 5 s (hung), brings Aurora back. -std::chrono::steady_clock::time_point g_disconnectedAt; -int liveSlot() { - if (g_off || !g_ours.imported || !g_ours.state) return -1; +// (written by the socket thread, read by the compositor's: steady_clock ticks, atomic) +std::atomic g_disconnectedAt{0}; +int liveSlot() { // (g_oursMutex held) + if (g_off || !g_ours.cur || !g_ours.state) return -1; if (!g_connected) // Frame Home restarting (a setting that rebuilds the renderer): hold its last frame a moment - return std::chrono::steady_clock::now() - g_disconnectedAt < std::chrono::seconds(3) ? g_ours.lastGood : -1; + return std::chrono::steady_clock::now() - std::chrono::steady_clock::time_point(std::chrono::steady_clock::duration(g_disconnectedAt.load())) < + std::chrono::seconds(3) ? g_ours.lastGood : -1; envshare::State* s = g_ours.state; const uint64_t now = envshare::nowNs(); - if (now - s->publishNs.load() > 5000000000ull && now > s->busyUntilNs.load()) return -1; + // (signed: a frame published between reading the clock and reading publishNs is newer than `now`, not 584 years old) + if (static_cast(now - s->publishNs.load()) > 5000000000ll && now > s->busyUntilNs.load()) return -1; const uint32_t k = s->latest.load(std::memory_order_acquire); - if (k < envshare::kSlots && s->slot[k].generation == g_ours.generation) g_ours.lastGood = static_cast(k); + // The newest slot, if it's of the images we have: copied whole (a slot being rewritten keeps the last copy). + envshare::Slot sl; + envshare::Motion m; + if (k < envshare::kSlots && envshare::readSlot(s, k, g_ours.seqlocked, g_ours.hasMotion, sl, m) && sl.frame && + sl.generation == g_ours.generation) { + g_ours.shown[k] = sl; + g_ours.shownMotion[k] = m; + g_ours.lastGood = static_cast(k); + } return g_ours.lastGood; } @@ -482,28 +628,45 @@ int frameSlot() { g_latchSlot = liveSlot(); g_latchNs = now; g_latchStamped = false; + g_latchRing = (g_latchRing + 1) % kUboRing; return g_latchSlot; } -void fillUbo(int k) { - auto& slot = g_ours.slot[k]; +// Slot k's uniforms for this compositor frame (block `ring`): the frame's eye rotations, the brightness now, and how +// far to carry the frame on: by the rig's turn (exactly) and its walking (roughly) since the sample it was made for. +// Frame Home late or stopped: carried on 40 ms at most, so a frame left behind never keeps turning. +// From the layer's own copy of the slot (liveSlot), which matches the images drawn. +void fillUbo(int k, uint32_t ring) { envshare::State* s = g_ours.state; - const envshare::Slot& src = s->slot[k]; - if (slot.uboFrame == src.frame) return; - float data[16 * 2 + 4 * 2 + 4]; + const envshare::Slot& src = g_ours.shown[k]; + float data[16 * 2 + 4 * 2 + 4 + 4 + 4] = {}; std::memcpy(data, src.viewProj, sizeof src.viewProj); std::memcpy(data + 32, src.eyePos, sizeof src.eyePos); data[40] = s->fade.load() / 1000.0f; data[41] = s->show.load() ? 1.0f : 0.0f; - data[42] = data[43] = 0; - std::memcpy(slot.uboMapped, data, sizeof data); - slot.uboFrame = src.frame; + float turn = 0, depth = 0, dt = 0; + const envshare::Motion m = g_ours.shownMotion[k]; + if (m.targetNs) { + dt = std::max(-0.02f, std::min(0.04f, static_cast(static_cast(g_latchNs - m.targetNs) / 1e9))); + turn = m.yawRate * dt; + depth = m.depth; + } + data[44] = std::cos(turn); + data[45] = std::sin(turn); + for (int c = 0; c < 3; ++c) data[48 + c] = depth > 0 ? m.velocity[c] * dt : 0.0f; + // The walk carried on stays well short of the depth it's taken against (a dash is metres a second): the shader + // looks along dir * depth + walk, which turns sideways, then backwards, as the walk nears the depth. + const float walked = std::sqrt(data[48] * data[48] + data[49] * data[49] + data[50] * data[50]); + if (!(walked <= 0.5f * depth)) for (int c = 0; c < 3; ++c) data[48 + c] = walked > 0 && depth > 0 ? data[48 + c] * (0.5f * depth / walked) : 0.0f; + if (!std::isfinite(data[44]) || !std::isfinite(data[45])) { data[44] = 1; data[45] = 0; } // (a NaN turn rate: no turn) + data[51] = depth; + std::memcpy(static_cast(g_ours.ubo[k].mapped) + ring * kUboStride, data, sizeof data); } // ── health: off switch and crash guard ────────────────────────────────────────────────────────── void startHealth() { - if (exists(dir() + "/off")) { g_off = true; logf("off: Aurora as usual (delete %s/off to turn Frame Home back on)", dir().c_str()); return; } + if (exists(dir() + "/off")) { g_off = g_startedOff = true; logf("off: Aurora as usual (delete %s/off and restart SteamVR to turn Frame Home back on)", dir().c_str()); return; } // `starting` left over = the last compositor died after the layer took Frame Home's frames in (armHealth) and // before it got healthy showing them. A compositor that never got that far, or closed cleanly, doesn't count. int crashes = 0; @@ -515,7 +678,7 @@ void startHealth() { if (crashes >= 2) { if (FILE* f = std::fopen((dir() + "/off").c_str(), "w")) std::fclose(f); ::unlink((dir() + "/starting").c_str()); - g_off = true; + g_off = g_startedOff = true; logf("the compositor stopped twice before getting healthy: turned myself off (Aurora as usual)"); return; } @@ -566,11 +729,11 @@ void serve() { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { const size_t cnt = (cm->cmsg_len - CMSG_LEN(0)) / sizeof(int); const int* p = reinterpret_cast(CMSG_DATA(cm)); - fds.assign(p, p + cnt); + fds.insert(fds.end(), p, p + cnt); // (all of them: each is closed below if unused) } - // v5: the images' create flags ride along too; v4: their usage (Frame Home draws straight into them), no - // flags; v3 (an older Frame Home): made for copying into. - const bool v5 = n == static_cast(sizeof h) && h.version == envshare::kVersion; + // v6: the State carries Motion (the Hello is v5's); v5: the images' create flags ride along too; v4: their + // usage (Frame Home draws straight into them), no flags; v3 (an older Frame Home): made for copying into. + const bool v5 = n == static_cast(sizeof h) && (h.version == envshare::kVersion || h.version == 5); const bool v4 = n == static_cast(envshare::kHelloV4Size) && h.version == 4; const bool v3 = n == static_cast(envshare::kHelloV3Size) && h.version == 3; if (v3) h.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; @@ -588,7 +751,7 @@ void serve() { for (uint32_t i = 0; i < envshare::kSlots; ++i) g_pending.fds[i] = fds[i]; g_pending.stateFd = fds[envshare::kSlots]; } - g_disconnectedAt = std::chrono::steady_clock::now(); + g_disconnectedAt = std::chrono::steady_clock::now().time_since_epoch().count(); g_connected = false; ::close(c); logf("Frame Home disconnected: Aurora until it's back"); @@ -708,6 +871,7 @@ VKAPI_ATTR void VKAPI_CALL DestroyPipeline(VkDevice d, VkPipeline p, const VkAll if (it != g_twins.end()) { t = it->second; g_twins.erase(it); } g_skip.erase(p); g_camera.erase(p); + g_names.erase(p); // (else every pipeline the compositor ever made keeps its name string) } for (const TwinPipeline& k : {t.plain, t.subsampled}) { if (k.pipeline) dd->DestroyPipeline(d, k.pipeline, nullptr); @@ -724,9 +888,10 @@ VKAPI_ATTR void VKAPI_CALL CmdBindPipeline(VkCommandBuffer cb, VkPipelineBindPoi dev(cb)->CmdBindPipeline(cb, bp, p); } -// What to do with a draw: 0 as is, 1 replace with the twin, 2 skip. -int drawAction(VkCommandBuffer cb, Twin& t, int& slot) { - if (g_off || !g_ours.imported) return 0; +// What to do with a draw: 0 as is, 1 replace with the twin (with `set`, of the kind `subsampled`), 2 skip. A twin +// drawn holds the generation whose set it binds until the command buffer is re-begun, reset or freed (Gen::users). +int drawAction(VkCommandBuffer cb, Twin& t, VkDescriptorSet& set, bool& subsampled, uint32_t& uboOffset) { + if (g_off || !g_imported) return 0; VkPipeline p; bool skip; { @@ -741,29 +906,37 @@ int drawAction(VkCommandBuffer cb, Twin& t, int& slot) { } if (!t.plain.pipeline && !skip) return 0; std::lock_guard ol(g_oursMutex); - slot = frameSlot(); - if (slot < 0) return 0; + const int slot = frameSlot(); + if (slot < 0 || !g_ours.cur) return 0; // Passthrough on: the compositor would blend the home half over the cameras. Step aside for the real view. - if (envshare::nowNs() - g_cameraNs.load() < 150000000ull) return 2; + if (cameraRecent()) return 2; if (!t.plain.pipeline) return 2; - if (g_ours.subsampled && !t.subsampled.pipeline) return 0; // (never imported so: import checks the twins can) - fillUbo(slot); - if (!g_latchStamped) { g_latchStamped = true; g_ours.state->sampledNs.store(g_latchNs); } // the moment it took the frame + Gen* g = g_ours.cur; + if (g->subsampled && !t.subsampled.pipeline) return 0; // (never imported so: import checks the twins can) + if (!g_latchStamped) { // the moment it took the frame + g_latchStamped = true; + g_ours.state->sampledNs.store(g_latchNs); + fillUbo(slot, g_latchRing); + } + uboOffset = g_latchRing * kUboStride; + set = g->set[slot]; + subsampled = g->subsampled; + Use& u = g->users[cb]; + u.slots |= 1u << slot; + u.frame[slot] = g_ours.shown[slot].frame; // (the frame it shows: acquired ahead of this command buffer) return 1; } -void drawTwin(VkCommandBuffer cb, const Twin& t, int slot, const std::function& draw) { +void drawTwin(VkCommandBuffer cb, const Twin& t, VkDescriptorSet set, bool subsampled, uint32_t uboOffset, const std::function& draw) { DeviceData* dd = dev(cb); VkPipeline original; { std::lock_guard l(g_mutex); original = g_bound[cb]; } - const bool subsampled = g_ours.subsampled; const TwinPipeline& k = subsampled ? t.subsampled : t.plain; - const VkDescriptorSet set = subsampled ? g_ours.slot[slot].setSubsampled : g_ours.slot[slot].set; dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.pipeline); - dd->CmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.layout, 1, 1, &set, 0, nullptr); + dd->CmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, k.layout, 1, 1, &set, 1, &uboOffset); draw(); dd->CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, original); } @@ -782,11 +955,13 @@ void traceDraw(VkCommandBuffer cb, const char* kind, uint32_t n, int action) { VKAPI_ATTR void VKAPI_CALL CmdDraw(VkCommandBuffer cb, uint32_t vc, uint32_t ic, uint32_t fv, uint32_t fi) { Twin t; - int slot = -1; - const int act = drawAction(cb, t, slot); + VkDescriptorSet set = VK_NULL_HANDLE; + bool subsampled = false; + uint32_t off = 0; + const int act = drawAction(cb, t, set, subsampled, off); traceDraw(cb, "draw", vc, act); switch (act) { - case 1: drawTwin(cb, t, slot, [&] { dev(cb)->CmdDraw(cb, vc, ic, fv, fi); }); return; + case 1: drawTwin(cb, t, set, subsampled, off, [&] { dev(cb)->CmdDraw(cb, vc, ic, fv, fi); }); return; case 2: return; default: dev(cb)->CmdDraw(cb, vc, ic, fv, fi); } @@ -794,15 +969,17 @@ VKAPI_ATTR void VKAPI_CALL CmdDraw(VkCommandBuffer cb, uint32_t vc, uint32_t ic, VKAPI_ATTR void VKAPI_CALL CmdDrawIndexed(VkCommandBuffer cb, uint32_t n, uint32_t ic, uint32_t fi, int32_t vo, uint32_t fin) { Twin t; - int slot = -1; - int act = drawAction(cb, t, slot); - if (act == 0 && g_starsIndices && n == g_starsIndices && g_ours.imported) { // the stars, while the home shows + VkDescriptorSet set = VK_NULL_HANDLE; + bool subsampled = false; + uint32_t off = 0; + int act = drawAction(cb, t, set, subsampled, off); + if (act == 0 && g_starsIndices && n == g_starsIndices && g_imported) { // the stars, while the home shows std::lock_guard ol(g_oursMutex); - if (liveSlot() >= 0 && envshare::nowNs() - g_cameraNs.load() > 150000000ull) act = 2; + if (liveSlot() >= 0 && !cameraRecent()) act = 2; } traceDraw(cb, "drawIndexed", n, act); switch (act) { - case 1: drawTwin(cb, t, slot, [&] { dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin); }); return; + case 1: drawTwin(cb, t, set, subsampled, off, [&] { dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin); }); return; case 2: return; default: dev(cb)->CmdDrawIndexed(cb, n, ic, fi, vo, fin); } @@ -817,23 +994,217 @@ void onSubmit(VkQueue q) { if (now - g_lastOffCheck > std::chrono::seconds(1)) { g_lastOffCheck = now; const bool off = exists(dir() + "/off"); - if (off != g_off) { g_off = off; logf(off ? "turned off: Aurora as usual" : "turned back on"); } + if (off != g_off) { + g_off = off; + // Off since the compositor started: no pipelines of ours were made, so nothing changes until it restarts. + logf(off ? "turned off: Aurora as usual" : g_startedOff ? "turned back on: shows after SteamVR restarts (off since the compositor started)" : "turned back on"); + } if (exists(dir() + "/trace")) { ::unlink((dir() + "/trace").c_str()); logf("=== trace: the next 1500 draws ==="); g_traceDraws = 1500; } } + if (!g_off) importPending(dd, q); + std::lock_guard ol(g_oursMutex); // (liveSlot updates lastGood, as the draw hooks do on their threads) + if (g_ours.d == dd) collectRetired(); // old generations no command buffer holds any more if (g_off) return; - importPending(dd, q); - if (g_ours.imported && liveSlot() >= 0 && ++g_ours.healthyFrames == 900) markHealthy(); // ~10 s showing the home + if (g_ours.cur && liveSlot() >= 0 && ++g_ours.healthyFrames == 900) markHealthy(); // ~10 s showing the home +} + +// ── command buffers: when a generation's sets are no longer used ──────────────────────────────── + +VKAPI_ATTR VkResult VKAPI_CALL AllocateCommandBuffers(VkDevice d, const VkCommandBufferAllocateInfo* ai, VkCommandBuffer* out) { + VkResult r = dev(d)->AllocateCommandBuffers(d, ai, out); + if (r == VK_SUCCESS) { + std::lock_guard ol(g_oursMutex); + for (uint32_t i = 0; i < ai->commandBufferCount; ++i) g_cbPool[out[i]] = ai->commandPool; + } + return r; +} + +VKAPI_ATTR void VKAPI_CALL FreeCommandBuffers(VkDevice d, VkCommandPool pool, uint32_t n, const VkCommandBuffer* cbs) { + for (uint32_t i = 0; i < n; ++i) { + if (!cbs[i]) continue; + forgetCommandBuffer(cbs[i]); + { + std::lock_guard ol(g_oursMutex); + g_cbPool.erase(cbs[i]); + } + std::lock_guard l(g_mutex); + g_bound.erase(cbs[i]); // (a handle the driver hands out again is a new command buffer) + } + dev(d)->FreeCommandBuffers(d, pool, n, cbs); +} + +VKAPI_ATTR VkResult VKAPI_CALL BeginCommandBuffer(VkCommandBuffer cb, const VkCommandBufferBeginInfo* bi) { + forgetCommandBuffer(cb); // (begin resets a recorded one: what it drew before is gone) + { + std::lock_guard l(g_mutex); + g_bound.erase(cb); + } + return dev(cb)->BeginCommandBuffer(cb, bi); +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandBuffer(VkCommandBuffer cb, VkCommandBufferResetFlags f) { + forgetCommandBuffer(cb); + return dev(cb)->ResetCommandBuffer(cb, f); +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandPool(VkDevice d, VkCommandPool pool, VkCommandPoolResetFlags f) { + forgetPool(pool, false); + return dev(d)->ResetCommandPool(d, pool, f); +} + +VKAPI_ATTR void VKAPI_CALL DestroyCommandPool(VkDevice d, VkCommandPool pool, const VkAllocationCallbacks* a) { + forgetPool(pool, true); + dev(d)->DestroyCommandPool(d, pool, a); +} + +// ── acquiring Frame Home's frames (see Gen) ───────────────────────────────────────────────────── + +// (g_oursMutex held) Slot k's acquire command buffer for queue family `family`, recorded once; null if it can't be +// made (or the slot's was made for another family: one compositor queue family draws the home). +VkCommandBuffer acquireCommandBuffer(DeviceData* d, Gen* g, uint32_t k, uint32_t family) { + if (g->acquire[k]) return g->acquireFamily[k] == family ? g->acquire[k] : VK_NULL_HANDLE; + auto pit = g_ours.acquirePools.find(family); + if (pit == g_ours.acquirePools.end()) { + VkCommandPoolCreateInfo pci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pci.queueFamilyIndex = family; + VkCommandPool pool = VK_NULL_HANDLE; + if (!d->CreateCommandPool || d->CreateCommandPool(d->device, &pci, nullptr, &pool) != VK_SUCCESS) return VK_NULL_HANDLE; + pit = g_ours.acquirePools.emplace(family, pool).first; + } + VkCommandBufferAllocateInfo ai = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + ai.commandPool = pit->second; + ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ai.commandBufferCount = 1; + VkCommandBuffer cb = VK_NULL_HANDLE; + if (d->AllocateCommandBuffers(d->device, &ai, &cb) != VK_SUCCESS) return VK_NULL_HANDLE; + // A command buffer a layer makes itself carries the loader's dispatch like the device's (layers below look it up). + if (!d->setLoaderData) *reinterpret_cast(cb) = *reinterpret_cast(d->device); // (an old loader) + else if (d->setLoaderData(d->device, cb) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; } + VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + bi.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT; // (submitted again with each new frame in the slot) + VkImageMemoryBarrier b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_GENERAL; // as Frame Home released it (and as an older one left it) + b.srcAccessMask = 0; // (an acquire: the release made Frame Home's writes available) + b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL; + b.dstQueueFamilyIndex = family; + b.image = g->image[k]; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, g->layers}; + if (d->BeginCommandBuffer(cb, &bi) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; } + d->CmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); + if (d->EndCommandBuffer(cb) != VK_SUCCESS) { d->FreeCommandBuffers(d->device, pit->second, 1, &cb); return VK_NULL_HANDLE; } + g->acquire[k] = cb; + g->acquireFamily[k] = family; + static bool said = false; + if (!said) { said = true; logf("Frame Home's frames are acquired from its queue before they're sampled (queue family %u)", family); } + return cb; +} + +// The acquires to submit just ahead of a batch of the compositor's command buffers: one per slot whose frame one of +// them draws and that isn't acquired yet. None (the usual case: a frame is acquired once) appends nothing. +template +void acquiresFor(DeviceData* d, uint32_t family, uint32_t n, CommandBufferAt at, std::vector& out) { + auto visit = [&](Gen* g) { + if (g->users.empty()) return; + for (uint32_t i = 0; i < n; ++i) { + auto u = g->users.find(at(i)); + if (u == g->users.end()) continue; + for (uint32_t k = 0; k < envshare::kSlots; ++k) { + if (!(u->second.slots & (1u << k)) || !u->second.frame[k] || g->acquiredFrame[k] == u->second.frame[k]) continue; + if (VkCommandBuffer a = acquireCommandBuffer(d, g, k, family)) { out.push_back(a); ++g_ours.acquires; } + g->acquiredFrame[k] = u->second.frame[k]; + } + } + }; + for (Gen* g : g_ours.retired) visit(g); + if (g_ours.cur) visit(g_ours.cur); +} + +uint32_t queueFamilyOf(VkQueue q) { + std::lock_guard l(g_mutex); + auto it = g_queueFamily.find(q); + return it == g_queueFamily.end() ? VK_QUEUE_FAMILY_IGNORED : it->second; } VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t n, const VkSubmitInfo* si, VkFence f) { onSubmit(q); - return dev(q)->QueueSubmit(q, n, si, f); + DeviceData* dd = dev(q); + if (!g_imported) return dd->QueueSubmit(q, n, si, f); // (nothing of ours drawn: no frames imported) + const uint32_t family = queueFamilyOf(q); + std::vector> acq(n); + bool any = false; + if (family != VK_QUEUE_FAMILY_IGNORED) { + std::lock_guard ol(g_oursMutex); + if (g_ours.d == dd) + for (uint32_t i = 0; i < n; ++i) { + acquiresFor(dd, family, si[i].commandBufferCount, [&](uint32_t c) { return si[i].pCommandBuffers[c]; }, acq[i]); + any |= !acq[i].empty(); + } + } + if (!any) return dd->QueueSubmit(q, n, si, f); + // Each batch that needs some gets a batch of its acquires just before it (no semaphores: they wait on nothing, + // and the barriers order the batch's sampling after them in submission order). + std::vector batches; + batches.reserve(2 * n); + for (uint32_t i = 0; i < n; ++i) { + if (!acq[i].empty()) { + VkSubmitInfo a = {VK_STRUCTURE_TYPE_SUBMIT_INFO}; + a.commandBufferCount = static_cast(acq[i].size()); + a.pCommandBuffers = acq[i].data(); + batches.push_back(a); + } + batches.push_back(si[i]); + } + return dd->QueueSubmit(q, static_cast(batches.size()), batches.data(), f); } VKAPI_ATTR VkResult VKAPI_CALL QueueSubmit2(VkQueue q, uint32_t n, const VkSubmitInfo2* si, VkFence f) { onSubmit(q); DeviceData* dd = dev(q); - return (dd->QueueSubmit2 ? dd->QueueSubmit2 : dd->QueueSubmit2KHR)(q, n, si, f); + auto submit = dd->QueueSubmit2 ? dd->QueueSubmit2 : dd->QueueSubmit2KHR; + if (!g_imported) return submit(q, n, si, f); + const uint32_t family = queueFamilyOf(q); + std::vector> acq(n); + bool any = false; + if (family != VK_QUEUE_FAMILY_IGNORED) { + std::lock_guard ol(g_oursMutex); + if (g_ours.d == dd) + for (uint32_t i = 0; i < n; ++i) { + acquiresFor(dd, family, si[i].commandBufferInfoCount, [&](uint32_t c) { return si[i].pCommandBufferInfos[c].commandBuffer; }, acq[i]); + any |= !acq[i].empty(); + } + } + if (!any) return submit(q, n, si, f); + std::vector> infos(n); + std::vector batches; + batches.reserve(2 * n); + for (uint32_t i = 0; i < n; ++i) { + if (!acq[i].empty()) { + for (VkCommandBuffer c : acq[i]) { + VkCommandBufferSubmitInfo ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO}; + ci.commandBuffer = c; + infos[i].push_back(ci); + } + VkSubmitInfo2 a = {VK_STRUCTURE_TYPE_SUBMIT_INFO_2}; + a.commandBufferInfoCount = static_cast(infos[i].size()); + a.pCommandBufferInfos = infos[i].data(); + batches.push_back(a); + } + batches.push_back(si[i]); + } + return submit(q, static_cast(batches.size()), batches.data(), f); +} + +VKAPI_ATTR void VKAPI_CALL GetDeviceQueue(VkDevice d, uint32_t family, uint32_t index, VkQueue* q) { + dev(d)->GetDeviceQueue(d, family, index, q); + std::lock_guard l(g_mutex); + if (*q) g_queueFamily[*q] = family; +} + +VKAPI_ATTR void VKAPI_CALL GetDeviceQueue2(VkDevice d, const VkDeviceQueueInfo2* info, VkQueue* q) { + dev(d)->GetDeviceQueue2(d, info, q); + std::lock_guard l(g_mutex); + if (*q) g_queueFamily[*q] = info->queueFamilyIndex; } // ── instance / device plumbing ────────────────────────────────────────────────────────────────── @@ -883,6 +1254,9 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr; PFN_vkGetDeviceProcAddr gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr; link->u.pLayerInfo = link->u.pLayerInfo->pNext; + PFN_vkSetDeviceLoaderData setLoaderData = nullptr; + for (auto* l = static_cast(ci->pNext); l; l = static_cast(l->pNext)) + if (l->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && l->function == VK_LOADER_DATA_CALLBACK) setLoaderData = l->u.pfnSetDeviceLoaderData; auto create = reinterpret_cast(gipa(VK_NULL_HANDLE, "vkCreateDevice")); // The compositor's device gets VK_EXT_fragment_density_map when the GPU has it, so it can read Frame Home's // foveated frames as they were drawn (subsampled). If the device won't create with it, it's made as asked. @@ -924,6 +1298,7 @@ VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceC dd.device = *out; dd.physical = pd; dd.gdpa = gdpa; + dd.setLoaderData = setLoaderData; #define FH_LOAD(n) dd.n = reinterpret_cast(gdpa(*out, "vk" #n)); FH_DEVICE_FUNCS(FH_LOAD) #undef FH_LOAD @@ -955,8 +1330,27 @@ VKAPI_ATTR void VKAPI_CALL DestroyDevice(VkDevice d, const VkAllocationCallbacks { std::lock_guard ol(g_oursMutex); if (g_ours.d && g_ours.d->device == d) { - releaseImports(); + // (the device's work is all done by now: the spec requires it of vkDestroyDevice's caller) + retireCurrent(); + for (Gen* g : g_ours.retired) destroyGen(g_ours.d, g); + g_ours.retired.clear(); + g_cbPool.clear(); + for (auto& p : g_ours.acquirePools) g_ours.d->DestroyCommandPool(d, p.second, nullptr); // (after the generations' command buffers) + if (g_ours.acquires) logf("%llu Frame Home frames acquired", static_cast(g_ours.acquires)); + // ensureBase()'s objects go with the device (a compositor that makes a new device gets them made again). + DeviceData* od = g_ours.d; + for (auto& u : g_ours.ubo) { + if (u.buffer) od->DestroyBuffer(d, u.buffer, nullptr); + if (u.mem) od->FreeMemory(d, u.mem, nullptr); // (unmapped by freeing) + } + if (g_ours.setLayout) od->DestroyDescriptorSetLayout(d, g_ours.setLayout, nullptr); + if (g_ours.setLayoutSubsampled) od->DestroyDescriptorSetLayout(d, g_ours.setLayoutSubsampled, nullptr); + if (g_ours.sampler) od->DestroySampler(d, g_ours.sampler, nullptr); + if (g_ours.subsampledSampler) od->DestroySampler(d, g_ours.subsampledSampler, nullptr); + if (g_ours.fragment) od->DestroyShaderModule(d, g_ours.fragment, nullptr); g_ours = Ours(); + g_latchNs = 0; + g_latchSlot = -1; // a clean close (SteamVR quitting or restarting the compositor): not a crash if (g_armed.exchange(false)) ::unlink((dir() + "/starting").c_str()); } @@ -975,6 +1369,8 @@ PFN_vkVoidFunction intercept(const char* name) { if (!isCompositor()) return nullptr; FH_HOOK(CreateShaderModule) FH_HOOK(CreatePipelineLayout) FH_HOOK(CreateGraphicsPipelines) FH_HOOK(DestroyPipeline) FH_HOOK(CmdBindPipeline) FH_HOOK(CmdDraw) FH_HOOK(CmdDrawIndexed) FH_HOOK(QueueSubmit) + FH_HOOK(AllocateCommandBuffers) FH_HOOK(FreeCommandBuffers) FH_HOOK(BeginCommandBuffer) FH_HOOK(ResetCommandBuffer) + FH_HOOK(ResetCommandPool) FH_HOOK(DestroyCommandPool) FH_HOOK(GetDeviceQueue) FH_HOOK(GetDeviceQueue2) #undef FH_HOOK if (!std::strcmp(name, "vkQueueSubmit2") || !std::strcmp(name, "vkQueueSubmit2KHR")) return reinterpret_cast(QueueSubmit2); return nullptr; diff --git a/src/clay_spirv.h b/src/clay_spirv.h index 90d328a..2df7eff 100644 --- a/src/clay_spirv.h +++ b/src/clay_spirv.h @@ -270,3 +270,79 @@ static const uint32_t kClaySkinnedVertSpirv[] = { 0x00000027,0x0000003e,0x0004003d,0x00000007,0x0000002b,0x00000029,0x0003003e,0x0000002a, 0x0000002b,0x000100fd,0x00010038, }; + +static const uint32_t kClayVColorVertSpirv[] = { + 0x07230203,0x00010000,0x00000000,0x0000004d,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00001157,0x0006000a,0x5f565053,0x5f52484b,0x746c756d,0x65697669,0x00000077,0x0003000e, + 0x00000000,0x00000001,0x0010000f,0x00000000,0x00000001,0x6e69616d,0x00000000,0x00000020, + 0x00000021,0x00000022,0x00000023,0x00000024,0x00000029,0x00000025,0x00000026,0x00000027, + 0x0000002a,0x00000028,0x00040005,0x00000001,0x6e69616d,0x00000000,0x00060005,0x0000000c, + 0x626f6c47,0x6e556c61,0x726f6669,0x0000736d,0x00060005,0x0000001e,0x626f6c67,0x6e556c61, + 0x726f6669,0x0000736d,0x00060005,0x0000000d,0x68737550,0x736e6f43,0x746e6174,0x00000073, + 0x00060005,0x0000001f,0x68737570,0x736e6f43,0x746e6174,0x00000073,0x00050005,0x00000020, + 0x69736f70,0x6e6f6974,0x00000000,0x00030005,0x00000021,0x00007675,0x00040005,0x00000022, + 0x6f6c6f63,0x00000072,0x00040005,0x00000023,0x6d726f6e,0x00006c61,0x00030005,0x00000025, + 0x00565576,0x00040005,0x00000026,0x6c6f4376,0x0000726f,0x00040005,0x00000027,0x726f4e76, + 0x006c616d,0x00050005,0x00000029,0x6867696c,0x70616d74,0x00007655,0x00040005,0x0000002a, + 0x31565576,0x00000000,0x00070006,0x0000000c,0x00000000,0x70696c63,0x6d6f7246,0x6c726f57, + 0x00000064,0x00070006,0x0000000d,0x00000000,0x6c726f77,0x6f724664,0x646f4d6d,0x00006c65, + 0x00050006,0x0000000d,0x00000001,0x6f6c6f63,0x00000072,0x00070006,0x0000000d,0x00000002, + 0x72547675,0x66736e61,0x556d726f,0x00000000,0x00070006,0x0000000d,0x00000003,0x72547675, + 0x66736e61,0x566d726f,0x00000000,0x00040047,0x00000020,0x0000001e,0x00000000,0x00040047, + 0x00000021,0x0000001e,0x00000001,0x00040047,0x00000022,0x0000001e,0x00000002,0x00040047, + 0x00000023,0x0000001e,0x00000003,0x00040047,0x00000029,0x0000001e,0x00000004,0x00040047, + 0x00000025,0x0000001e,0x00000000,0x00040047,0x00000026,0x0000001e,0x00000001,0x00040047, + 0x00000027,0x0000001e,0x00000002,0x00040047,0x0000002a,0x0000001e,0x00000003,0x00040047, + 0x00000028,0x0000000b,0x00000000,0x00040047,0x00000024,0x0000000b,0x00001158,0x00040047, + 0x0000000b,0x00000006,0x00000040,0x00050048,0x0000000c,0x00000000,0x00000023,0x00000000, + 0x00040048,0x0000000c,0x00000000,0x00000005,0x00050048,0x0000000c,0x00000000,0x00000007, + 0x00000010,0x00030047,0x0000000c,0x00000002,0x00040047,0x0000001e,0x00000022,0x00000000, + 0x00040047,0x0000001e,0x00000021,0x00000000,0x00050048,0x0000000d,0x00000000,0x00000023, + 0x00000000,0x00040048,0x0000000d,0x00000000,0x00000005,0x00050048,0x0000000d,0x00000000, + 0x00000007,0x00000010,0x00050048,0x0000000d,0x00000001,0x00000023,0x00000040,0x00050048, + 0x0000000d,0x00000002,0x00000023,0x00000050,0x00050048,0x0000000d,0x00000003,0x00000023, + 0x00000060,0x00030047,0x0000000d,0x00000002,0x00020013,0x00000002,0x00030021,0x00000003, + 0x00000002,0x00030016,0x00000004,0x00000020,0x00040015,0x00000005,0x00000020,0x00000001, + 0x00040015,0x00000006,0x00000020,0x00000000,0x00040017,0x00000007,0x00000004,0x00000002, + 0x00040017,0x00000008,0x00000004,0x00000003,0x00040017,0x00000009,0x00000004,0x00000004, + 0x00040018,0x0000000a,0x00000009,0x00000004,0x0004002b,0x00000006,0x0000001d,0x00000002, + 0x0004001c,0x0000000b,0x0000000a,0x0000001d,0x0003001e,0x0000000c,0x0000000b,0x0006001e, + 0x0000000d,0x0000000a,0x00000009,0x00000009,0x00000009,0x00040020,0x0000000e,0x00000002, + 0x0000000c,0x00040020,0x0000000f,0x00000002,0x0000000a,0x00040020,0x00000010,0x00000009, + 0x0000000d,0x00040020,0x00000011,0x00000009,0x0000000a,0x00040020,0x00000012,0x00000009, + 0x00000009,0x00040020,0x00000013,0x00000001,0x00000008,0x00040020,0x00000014,0x00000001, + 0x00000007,0x00040020,0x00000015,0x00000001,0x00000009,0x00040020,0x00000016,0x00000001, + 0x00000005,0x00040020,0x00000017,0x00000003,0x00000007,0x00040020,0x00000018,0x00000003, + 0x00000009,0x00040020,0x00000019,0x00000003,0x00000008,0x0004002b,0x00000004,0x0000001a, + 0x3f800000,0x0004002b,0x00000005,0x0000001b,0x00000000,0x0004002b,0x00000005,0x0000001c, + 0x00000001,0x0004002b,0x00000005,0x0000003f,0x00000002,0x0004002b,0x00000005,0x00000040, + 0x00000003,0x0004003b,0x0000000e,0x0000001e,0x00000002,0x0004003b,0x00000010,0x0000001f, + 0x00000009,0x0004003b,0x00000013,0x00000020,0x00000001,0x0004003b,0x00000014,0x00000021, + 0x00000001,0x0004003b,0x00000015,0x00000022,0x00000001,0x0004003b,0x00000013,0x00000023, + 0x00000001,0x0004003b,0x00000016,0x00000024,0x00000001,0x0004003b,0x00000017,0x00000025, + 0x00000003,0x0004003b,0x00000018,0x00000026,0x00000003,0x0004003b,0x00000019,0x00000027, + 0x00000003,0x0004003b,0x00000018,0x00000028,0x00000003,0x0004003b,0x00000014,0x00000029, + 0x00000001,0x0004003b,0x00000017,0x0000002a,0x00000003,0x00050036,0x00000002,0x00000001, + 0x00000000,0x00000003,0x000200f8,0x0000002c,0x0004003d,0x00000005,0x0000002d,0x00000024, + 0x00060041,0x0000000f,0x0000002e,0x0000001e,0x0000001b,0x0000002d,0x0004003d,0x0000000a, + 0x0000002f,0x0000002e,0x00050041,0x00000011,0x00000030,0x0000001f,0x0000001b,0x0004003d, + 0x0000000a,0x00000031,0x00000030,0x00050041,0x00000012,0x00000032,0x0000001f,0x0000001c, + 0x0004003d,0x00000009,0x00000033,0x00000032,0x0004003d,0x00000008,0x00000034,0x00000020, + 0x00050051,0x00000004,0x00000035,0x00000034,0x00000000,0x00050051,0x00000004,0x00000036, + 0x00000034,0x00000001,0x00050051,0x00000004,0x00000037,0x00000034,0x00000002,0x00070050, + 0x00000009,0x00000038,0x00000035,0x00000036,0x00000037,0x0000001a,0x00050091,0x00000009, + 0x00000039,0x00000031,0x00000038,0x00050091,0x00000009,0x0000003a,0x0000002f,0x00000039, + 0x0003003e,0x00000028,0x0000003a,0x0004003d,0x00000007,0x0000003b,0x00000021,0x00050041, + 0x00000012,0x00000041,0x0000001f,0x0000003f,0x0004003d,0x00000009,0x00000042,0x00000041, + 0x00050041,0x00000012,0x00000043,0x0000001f,0x00000040,0x0004003d,0x00000009,0x00000044, + 0x00000043,0x0008004f,0x00000008,0x00000045,0x00000042,0x00000042,0x00000000,0x00000001, + 0x00000002,0x0008004f,0x00000008,0x00000046,0x00000044,0x00000044,0x00000000,0x00000001, + 0x00000002,0x00050051,0x00000004,0x00000047,0x0000003b,0x00000000,0x00050051,0x00000004, + 0x00000048,0x0000003b,0x00000001,0x00060050,0x00000008,0x00000049,0x00000047,0x00000048, + 0x0000001a,0x00050094,0x00000004,0x0000004a,0x00000045,0x00000049,0x00050094,0x00000004, + 0x0000004b,0x00000046,0x00000049,0x00050050,0x00000007,0x0000004c,0x0000004a,0x0000004b, + 0x0003003e,0x00000025,0x0000004c,0x0004003d,0x00000009,0x0000003c,0x00000022,0x00050085, + 0x00000009,0x0000003d,0x00000033,0x0000003c,0x0003003e,0x00000026,0x0000003d,0x0004003d, + 0x00000008,0x0000003e,0x00000023,0x0003003e,0x00000027,0x0000003e,0x0004003d,0x00000007, + 0x0000002b,0x00000029,0x0003003e,0x0000002a,0x0000002b,0x000100fd,0x00010038, +}; diff --git a/src/dashboard_icon.h b/src/dashboard_icon.h new file mode 100644 index 0000000..ab72250 --- /dev/null +++ b/src/dashboard_icon.h @@ -0,0 +1,72 @@ +// The dashboard tab icon: art/icon/frame-glyph.svg (the app icon's roof and headset, no tile) at 64 x 64, RGBA. +// Regenerate from art/icon/frame-glyph-64.png if the icon changes. +#pragma once +#include + +static constexpr int kDashIconSize = 64; +static const uint8_t kDashIcon[64 * 64 * 4] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,244,255,69,248,244,255,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,251,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,85,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,188,247,244,255,181,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,91,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,191,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,247,244,255,186,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,4,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,254,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,247,244,255,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,243,255,87,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,254,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,252,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,186,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,255,247,244,255,252,246,243,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,249,245,255,80,247,244,255,251,247,244,255,255,247,244,255,255,246,244,255,180,255,255,255,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,170,255,9,24,158,255,42,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,159,255,64,24,158,255,42,28,170,255,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,255,247,244,255,255,247,244,255,252,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,245,255,74,247,244,255,251,246,244,255,180,255,255,255,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,158,255,76,25,159,255,185,25,159,255,231,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,230,25,158,255,182,26,158,255,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,3,246,244,255,180,247,244,255,251,249,245,255,80,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,246,255,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,160,255,35,26,159,255,189,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,230,26,158,255,129,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,159,255,128,26,158,255,129,25,159,255,233,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,180,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,246,246,255,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,161,255,38,25,159,255,233,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,110,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,160,255,115,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,226,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,198,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,190,19,161,255,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,29,25,159,255,193,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,174,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,160,255,51,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,22,155,255,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,160,255,115,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,23,158,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,150,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,132,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,161,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,160,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,163,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,159,255,157,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,139,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,239,25,158,255,192,25,158,255,192,26,159,255,239,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,159,255,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,106,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,161,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,158,255,166,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,22,160,255,35,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,158,255,216,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,216,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,18,164,255,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,158,255,187,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,159,255,252,22,157,255,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,53,25,159,255,252,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,176,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,157,255,52,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,159,255,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,159,255,136,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,24,158,255,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,159,255,98,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,203,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,8,25,159,255,204,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,247,24,159,255,85,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,157,255,39,25,159,255,183,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,25,158,255,192,26,166,255,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,162,255,22,26,159,255,196,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,255,26,159,255,167,26,161,255,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,159,255,37,26,159,255,120,26,159,255,128,26,159,255,128,26,159,255,128,26,158,255,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,159,255,72,26,159,255,128,26,159,255,128,26,159,255,128,25,159,255,114,26,161,255,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +}; diff --git a/src/env_export.h b/src/env_export.h index 2ac539f..75790b3 100644 --- a/src/env_export.h +++ b/src/env_export.h @@ -76,17 +76,20 @@ public: int beginFrame() { const int k = static_cast(next_); next_ = (next_ + 1) % envshare::kSlots; return k; } - void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) { - envshare::Slot& s = state_->slot[k]; + void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) { + envshare::Slot s; s.frame = ++frame_; s.generation = generation_; std::memcpy(s.viewProj, viewProj, sizeof s.viewProj); for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; } + envshare::writeSlot(state_, static_cast(k), s, motion); // (under its sequence count: env_share.h) state_->published.store(frame_); state_->latest.store(static_cast(k), std::memory_order_release); state_->publishNs.store(envshare::nowNs()); } + void still() { const uint32_t k = state_->latest.load(); if (k < envshare::kSlots) envshare::writeMotion(state_, k, envshare::Motion()); } + uint32_t refusedFlags() const { return 0; } // (Windows frames carry no Vulkan create flags for the layer to refuse) double sinceSampled() const { @@ -282,17 +285,23 @@ public: int beginFrame() { const int k = static_cast(next_); next_ = (next_ + 1) % envshare::kSlots; return k; } // The frame in slot k is done on the GPU: hand it to the layer with the eye poses it was rendered from. - void publish(int k, const float viewProj[2][16], const float eyePos[2][3]) { - envshare::Slot& s = state_->slot[k]; + void publish(int k, const float viewProj[2][16], const float eyePos[2][3], const envshare::Motion& motion) { + envshare::Slot s; s.frame = ++frame_; s.generation = generation_; std::memcpy(s.viewProj, viewProj, sizeof s.viewProj); - for (int e = 0; e < 2; ++e) { for (int k = 0; k < 3; ++k) s.eyePos[e][k] = eyePos[e][k]; s.eyePos[e][3] = 1; } + for (int e = 0; e < 2; ++e) { for (int c = 0; c < 3; ++c) s.eyePos[e][c] = eyePos[e][c]; s.eyePos[e][3] = 1; } + // Under the slot's sequence count (env_share.h): after a resize this rewrites the slot the layer may still be + // showing from the old images, and it must never read half of this pose and half of the old one. + envshare::writeSlot(state_, static_cast(k), s, motion); state_->published.store(frame_); state_->latest.store(static_cast(k), std::memory_order_release); state_->publishNs.store(envshare::nowNs()); } + // The newest frame stays as it is (the home stops for a game): no turning or walking carried on from it. + void still() { const uint32_t k = state_->latest.load(); if (k < envshare::kSlots) envshare::writeMotion(state_, k, envshare::Motion()); } + // The layer couldn't import the images with these create flags (0: none refused). uint32_t refusedFlags() const { return state_->refusedFlags.load() & flags_; } diff --git a/src/env_share.h b/src/env_share.h index b7612ed..fb0445d 100644 --- a/src/env_share.h +++ b/src/env_share.h @@ -23,7 +23,11 @@ #include #include #include +#include #include +#ifdef _MSC_VER +#include +#endif namespace envshare { @@ -32,8 +36,9 @@ constexpr const char* kPipeName = "\\\\.\\pipe\\framehome-env"; // Win constexpr const char* kStateMapName = "Local\\framehome-env-state"; // Windows: the shared State mapping (+ a // generation suffix, so a new one is distinct) constexpr uint32_t kMagic = 0x46484556; // "FHEV" -constexpr uint32_t kVersion = 5; // 4: Hello says the images' usage (Frame Home draws straight into them) +constexpr uint32_t kVersion = 6; // 4: Hello says the images' usage (Frame Home draws straight into them) // 5: ...and their create flags (subsampled: the compositor smooths foveation) + // 6: the State says how the view was moving with each frame (Motion) // Four: the compositor reads the newest while Frame Home publishes the one before it finished and renders the next. constexpr uint32_t kSlots = 4; @@ -85,8 +90,20 @@ struct Slot { float eyePos[2][4] = {}; // per eye: where it was, tracking space (xyz) }; +// v6: how the view was moving when a frame was made, so the layer can carry a frame it shows again (Half rate, or a +// frame that came late) on to where you've got to since. The rig's smooth turn is a rotation about the head, so it's +// carried on exactly; walking moves the eye, which needs depth the layer hasn't got: everything is taken as `depth` +// metres away (what's in front of you), so walls and furniture keep up and only the far distance is a little off. +struct Motion { + uint64_t targetNs = 0; // the compositor sample the frame was made for (CLOCK_MONOTONIC; 0: as is) + float yawRate = 0; // the rig turning (smooth turn), rad/s, + = left, about tracking-space up + float depth = 0; // what walking parallax takes everything's distance as (m; 0: no correction) + float velocity[4] = {}; // the rig walking, tracking space of the frame (m/s, xyz) +}; + +constexpr uint32_t kStateVersion = 7; // State::version once Frame Home writes slots under `seq` (v6 wrote kVersion) struct State { - uint32_t magic = kMagic, version = kVersion; + uint32_t magic = kMagic, version = kStateVersion; std::atomic latest{0xFFFFFFFFu}; // newest finished slot (0xFFFFFFFF none) std::atomic published{0}; // frame number of `latest` std::atomic publishNs{0}; // CLOCK_MONOTONIC when it was published @@ -97,7 +114,106 @@ struct State { Slot slot[kSlots]; std::atomic refusedFlags{0}; // the layer: Hello flags it can't import with (Frame Home then sends // images without them) + Motion motion[kSlots]; // v6: with each slot's frame (written before `latest`, as Slot is) + // State v7: a sequence count per slot (a seqlock over slot[k] and motion[k]). Odd while Frame Home rewrites them; + // the layer copies a slot only between two equal even counts, so it never takes half of one frame's pose and half + // of the next's (a resize rewrites the slot the layer is still showing from the old images). See writeSlot(). + std::atomic seq[kSlots] = {}; }; +// An older Frame Home's State ends before the fields added since (a v5 Hello: before `motion`; a v6 State before +// `seq`). The layer maps only as much as it was sent: reading past the end of the shared memory would crash the +// compositor. The Hello stays v6 (its layout didn't change): `version` in the State says v7, and the memfd's size +// says whether `seq` is there at all, so an old layer with a new Frame Home (and the reverse) still pair up. +constexpr size_t kStateV5Size = offsetof(State, motion); +constexpr size_t kStateV6Size = offsetof(State, seq); + +// The layer: how much of the State a Frame Home sent may be mapped (0: less than its Hello's version promises, so +// none), and whether it says how the view moves (v6 Motion). Whether its slots are under sequence counts is known +// once it's mapped: seqlocked(). +struct StateLayout { size_t size = 0; bool motion = false; }; +inline StateLayout stateLayout(uint32_t helloVersion, uint64_t fileSize) { + StateLayout l; + l.motion = helloVersion >= 6; + const size_t need = l.motion ? kStateV6Size : kStateV5Size; + if (fileSize >= need) l.size = l.motion && fileSize >= sizeof(State) ? sizeof(State) : need; + return l; +} +// A State mapped `mapped` bytes long has `seq` and Frame Home writes the slots under it: v7 in size and in what it +// says (a bigger State from something else is still read the v6 way). +inline bool seqlocked(const State* st, size_t mapped) { + return mapped >= sizeof(State) && st->magic == kMagic && st->version >= kStateVersion; +} + +// Word-by-word relaxed atomic copies of the seqlocked slot data (plain memcpy racing the other process is a data +// race; these are the C++ memory model's way to say "may tear, the sequence count catches it"). +inline void storeWords(void* dst, const void* src, size_t bytes) { + uint32_t* d = static_cast(dst); + const uint32_t* s = static_cast(src); + for (size_t i = 0; i < bytes / 4; ++i) { +#ifdef _MSC_VER + __iso_volatile_store32(reinterpret_cast(d + i), static_cast<__int32>(s[i])); +#else + __atomic_store_n(d + i, s[i], __ATOMIC_RELAXED); +#endif + } +} +inline void loadWords(void* dst, const void* src, size_t bytes) { + uint32_t* d = static_cast(dst); + const uint32_t* s = static_cast(src); + for (size_t i = 0; i < bytes / 4; ++i) { +#ifdef _MSC_VER + d[i] = static_cast(__iso_volatile_load32(reinterpret_cast(s + i))); +#else + d[i] = __atomic_load_n(s + i, __ATOMIC_RELAXED); +#endif + } +} +static_assert(sizeof(Slot) % 4 == 0 && sizeof(Motion) % 4 == 0, "slot data is copied as 32-bit words"); + +// Frame Home: rewrites slot k (and its Motion) under its sequence count. Only Frame Home writes, from one thread. +inline void writeSlot(State* st, uint32_t k, const Slot& slot, const Motion& motion) { + const uint32_t s = st->seq[k].load(std::memory_order_relaxed); + st->seq[k].store(s | 1u, std::memory_order_relaxed); // odd: being written + std::atomic_thread_fence(std::memory_order_release); // (the odd count is seen before any new word) + storeWords(&st->slot[k], &slot, sizeof slot); + storeWords(&st->motion[k], &motion, sizeof motion); + st->seq[k].store((s | 1u) + 1u, std::memory_order_release); // even again: whole +} +// Frame Home: only the Motion of slot k (still()). +inline void writeMotion(State* st, uint32_t k, const Motion& motion) { + Slot cur; + loadWords(&cur, &st->slot[k], sizeof cur); // (Frame Home is the only writer: this is what's there) + writeSlot(st, k, cur, motion); +} + +// The layer: a consistent copy of slot k, or false (being rewritten right now, every try). `seqlocked`: the State is +// v7 (has `seq`); an older Frame Home has no count, so two copies that agree are the best that can be had. +inline bool readSlot(const State* st, uint32_t k, bool seqlocked, bool withMotion, Slot& slot, Motion& motion) { + motion = Motion(); + for (int tries = 0; tries < 8; ++tries) { + if (seqlocked) { + const uint32_t s1 = st->seq[k].load(std::memory_order_acquire); + if (s1 & 1u) continue; + loadWords(&slot, &st->slot[k], sizeof slot); + if (withMotion) loadWords(&motion, &st->motion[k], sizeof motion); + std::atomic_thread_fence(std::memory_order_acquire); // (the words are read before the second count) + if (st->seq[k].load(std::memory_order_relaxed) == s1) return true; + } else { + Slot a, b; + Motion ma, mb; + loadWords(&a, &st->slot[k], sizeof a); + if (withMotion) loadWords(&ma, &st->motion[k], sizeof ma); + loadWords(&b, &st->slot[k], sizeof b); + if (withMotion) loadWords(&mb, &st->motion[k], sizeof mb); + if (std::memcmp(&a, &b, sizeof a) == 0 && (!withMotion || std::memcmp(&ma, &mb, sizeof ma) == 0)) { + slot = a; + motion = ma; + return true; + } + } + } + return false; +} inline uint64_t nowNs() { #ifdef _WIN32 diff --git a/src/fenv.h b/src/fenv.h index bc33f81..f941497 100644 --- a/src/fenv.h +++ b/src/fenv.h @@ -21,12 +21,15 @@ #else #include #include +#include #include #endif #include "fenv_lzma.h" #include "miniz.h" #include +#include +#include #include #include #include @@ -35,6 +38,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -43,14 +49,31 @@ struct FenvSource { std::string file, role, format, package, version; }; +// A Gaussian splat object (splat_import.h): its .spz member, its name, how many splats and where it's placed. +struct FenvSplats { + std::string file, name; + uint64_t count = 0; + float xform[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; // world from the splats' own frame, column-major +}; + struct FenvManifest { std::string name; std::vector sources; // footprint first, then the vista (or one whole home) std::vector models; // imported model files (member paths) + std::vector splats; // Gaussian splat objects (written with needs = {"splats"}) std::string edits, thumbnail; // member paths, empty when absent std::string scene; // the baked scene (scene_bake.h): when present it IS the home, sources are info std::string world; // world.json (core/world.h): spawns, seats, walls, walkable areas + std::string studioVersion; // "studio": the Frame Studio that saved it ("v0.2.3"; "" for files from before v0.2) WorldSettings settings; // the world's own render settings (world_settings.h): none set = the headset's + // What it needs a reader to understand ("splats", ...): a reader that doesn't know one refuses the file with "needs + // a newer Frame Home" instead of showing it wrong. Written only when something is needed (older readers ignore it, + // so content they can't show must also change something they do check, e.g. the baked scene's version). + std::vector needs; + // What a newer Frame Studio wrote that this one doesn't know: its version and its other top-level keys (as JSON + // ",\n "key": value" pairs), written back as they were when this one rewrites the manifest. + int64_t version = 1; + std::string unknown; }; namespace fenv_detail { @@ -65,8 +88,33 @@ inline std::string quote(const std::string& s) { return o + "\""; } +// A parsed value written out again (the keys this version doesn't know, kept as they were). +inline std::string toJson(const tinyjson::Value& v) { + char b[40]; + switch (v.type) { + case tinyjson::Type::Bool: return v.boolVal ? "true" : "false"; + case tinyjson::Type::Int: std::snprintf(b, sizeof b, "%lld", static_cast(v.intVal)); return b; + // (inf / nan aren't JSON: written so, the manifest would never parse again and the file wouldn't open) + case tinyjson::Type::Float: if (!std::isfinite(v.floatVal)) return "null"; std::snprintf(b, sizeof b, "%.17g", v.floatVal); return b; + case tinyjson::Type::String: return quote(v.strVal); + case tinyjson::Type::Array: { + std::string o = "["; + for (size_t i = 0; i < v.arrVal.size(); ++i) o += (i ? ", " : "") + toJson(v.arrVal[i]); + return o + "]"; + } + case tinyjson::Type::Object: { + std::string o = "{"; + bool first = true; + for (const auto& kv : v.objVal) { o += (first ? "" : ", ") + quote(kv.first) + ": " + toJson(kv.second); first = false; } + return o + "}"; + } + default: return "null"; + } +} + inline std::string manifestJson(const FenvManifest& m) { - std::string j = "{\n \"format\": \"fenv\",\n \"version\": 1,\n \"name\": " + quote(m.name) + ",\n \"sources\": ["; + std::string j = "{\n \"format\": \"fenv\",\n \"version\": " + std::to_string(std::max(1, m.version)) + ",\n \"name\": " + + quote(m.name) + ",\n \"sources\": ["; for (size_t i = 0; i < m.sources.size(); ++i) { const FenvSource& s = m.sources[i]; j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"role\": " + quote(s.role) + ", \"format\": " + @@ -75,19 +123,42 @@ inline std::string manifestJson(const FenvManifest& m) { j += "\n ],\n \"models\": ["; for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }"; j += "\n ]"; + if (!m.splats.empty()) { + j += ",\n \"splats\": ["; + for (size_t i = 0; i < m.splats.size(); ++i) { + const FenvSplats& s = m.splats[i]; + j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"name\": " + quote(s.name) + ", \"count\": " + + std::to_string(s.count) + ", \"xform\": ["; + // A placement that isn't all finite numbers ("nan" / "inf" aren't JSON: the file would never open again) + // is written as where the splats were captured. + static const float kIdentity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; + const float* x = std::all_of(std::begin(s.xform), std::end(s.xform), [](float v) { return std::isfinite(v); }) ? s.xform : kIdentity; + for (int k = 0; k < 16; ++k) { char b[24]; std::snprintf(b, sizeof b, "%s%.9g", k ? ", " : "", x[k]); j += b; } + j += "] }"; + } + j += "\n ]"; + } if (!m.edits.empty()) j += ",\n \"edits\": " + quote(m.edits); if (!m.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail); if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene); if (!m.world.empty()) j += ",\n \"world\": " + quote(m.world); + if (!m.studioVersion.empty()) j += ",\n \"studio\": " + quote(m.studioVersion); if (!m.settings.empty() || !m.settings.tuned.empty()) j += ",\n \"settings\": " + m.settings.toJson(); - return j + "\n}\n"; + if (!m.needs.empty()) { + j += ",\n \"requires\": ["; + for (size_t i = 0; i < m.needs.size(); ++i) j += (i ? ", " : "") + quote(m.needs[i]); + j += "]"; + } + return j + m.unknown + "\n}\n"; } // Everything one .fenv may unpack to (its members, LZMA ones as unpacked): a damaged or hostile file must not // fill the disk or memory. The largest homes are a few hundred MB. constexpr uint64_t kExtractBudget = uint64_t(4) << 30; -// Empty when missing or larger than `limit` (the zip's declared size, which miniz holds the inflate to). +// Empty when missing, larger than `limit` (the zip's declared size, which miniz holds the inflate to) or damaged. +// The CRC is checked here: miniz is built without its own checks (APKs are read in place, and quickly), but a .fenv's +// members are copied out and kept, so a flipped bit (a bad card, a cut-off copy) must not pass as the real thing. inline std::vector readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) { std::vector out; const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0); @@ -96,11 +167,51 @@ inline std::vector readMember(mz_zip_archive& z, const char* name, uint size_t n = 0; void* p = mz_zip_reader_extract_to_heap(&z, static_cast(i), &n, 0); if (!p) return out; - try { out.assign(static_cast(p), static_cast(p) + n); } catch (const std::exception&) { out = {}; } + if (mz_crc32(MZ_CRC32_INIT, static_cast(p), n) == st.m_crc32) + try { out.assign(static_cast(p), static_cast(p) + n); } catch (const std::exception&) { out = {}; } mz_free(p); return out; } +// `tmp` (a finished file) in place of `out`. Where a plain rename can't (Windows, `out` open in another program), +// the old one is set aside first and put back if `tmp` still can't take its place: the user's only copy is never +// removed before the new one is there. A `tmp` that couldn't go in is removed (no stray .part left behind). +inline bool replaceWith(const std::filesystem::path& tmp, const std::filesystem::path& out, std::error_code& ec) { + namespace fs = std::filesystem; + fs::rename(tmp, out, ec); + if (ec) { + std::error_code e2; + fs::path old = out; + old += ".old"; + fs::remove(old, e2); + fs::rename(out, old, e2); + if (!e2) { + fs::rename(tmp, out, ec); + if (ec) fs::rename(old, out, e2); + else fs::remove(old, e2); + } + } + if (ec) { std::error_code e2; fs::remove(tmp, e2); return false; } + return true; +} + +// Member `i` into the file `out`, its CRC checked as it's written (false: unreadable, damaged, or the disk). +inline bool extractMember(mz_zip_archive& z, mz_uint i, const mz_zip_archive_file_stat& st, const std::filesystem::path& out) { + struct Sink { std::ofstream f; mz_ulong crc = MZ_CRC32_INIT; }; + Sink s; + s.f.open(out, std::ios::binary); + if (!s.f) return false; + const auto write = [](void* o, mz_uint64, const void* p, size_t n) -> size_t { + Sink& k = *static_cast(o); + k.crc = mz_crc32(k.crc, static_cast(p), n); + k.f.write(static_cast(p), static_cast(n)); + return k.f ? n : 0; + }; + const bool ok = mz_zip_reader_extract_to_callback(&z, i, write, &s, 0) && s.crc == st.m_crc32; + s.f.close(); + return ok && !s.f.fail(); +} + // A short tag for a file's full path: two "haven.fenv" in different folders get different cache folders. inline std::string pathTag(const std::filesystem::path& p) { std::error_code ec; @@ -138,6 +249,80 @@ struct ExtractLock { ExtractLock& operator=(const ExtractLock&) = delete; }; +// "In use": a lock on /.inuse. Every process with the folder open holds it shared; whoever removes an +// old folder (a stale version, or one not opened for 30 days) must take it exclusively without waiting, and leaves +// the folder alone when it can't. So a Frame Home that has shown one home for over a month (or Studio, or the +// switcher's unpacker in another process) never has its files pulled from under it. +struct UseLock { +#ifdef _WIN32 + HANDLE h = INVALID_HANDLE_VALUE; + OVERLAPPED ov = {}; + // shared: waits (only a remover holds it exclusively, briefly); exclusive: doesn't (false: in use). + bool take(const std::filesystem::path& folder, bool exclusive) { + h = CreateFileW((folder / L".inuse").wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (h == INVALID_HANDLE_VALUE) return false; + if (!LockFileEx(h, exclusive ? LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY : 0, 0, MAXDWORD, MAXDWORD, &ov)) { CloseHandle(h); h = INVALID_HANDLE_VALUE; return false; } + return true; + } + void release() { if (h != INVALID_HANDLE_VALUE) { UnlockFileEx(h, 0, MAXDWORD, MAXDWORD, &ov); CloseHandle(h); h = INVALID_HANDLE_VALUE; } } +#else + int fd = -1; + bool take(const std::filesystem::path& folder, bool exclusive) { + const std::filesystem::path p = folder / ".inuse"; + fd = open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600); + if (fd < 0) return false; + if (flock(fd, exclusive ? LOCK_EX | LOCK_NB : LOCK_SH) != 0) { close(fd); fd = -1; return false; } + // A remover may have had it just before us and removed the folder meanwhile: then this lock is on a file + // that's gone (or one a new unpack made since), and protects nothing. + struct stat a, b; + if (fstat(fd, &a) != 0 || ::stat(p.c_str(), &b) != 0 || a.st_ino != b.st_ino || a.st_dev != b.st_dev) { release(); return false; } + return true; + } + void release() { if (fd >= 0) { flock(fd, LOCK_UN); close(fd); fd = -1; } } +#endif + UseLock() = default; + ~UseLock() { release(); } + UseLock(const UseLock&) = delete; + UseLock& operator=(const UseLock&) = delete; +}; + +// The folders this process has open, newest first: a shared UseLock on each of the last kHeld it unpacked or opened +// (Frame Home shows one home at a time and loads the next beside it; older ones are released and may be pruned). +// Held until the process exits, or fenvRelease(). +struct HeldFolders { + static constexpr size_t kHeld = 4; + std::mutex m; + std::vector>> held; + static HeldFolders& get() { static HeldFolders h; return h; } + void hold(const std::string& folder, std::unique_ptr l) { + if (!l) return; + std::lock_guard g(m); + for (auto it = held.begin(); it != held.end(); ++it) if (it->first == folder) { held.erase(it); break; } + held.insert(held.begin(), {folder, std::move(l)}); + if (held.size() > kHeld) held.resize(kHeld); + } + void releaseUnder(const std::string& root) { + std::lock_guard g(m); + held.erase(std::remove_if(held.begin(), held.end(), [&](const auto& h) { return h.first.compare(0, root.size(), root) == 0; }), held.end()); + } +}; + +// Removes a cache folder unless something has it open (UseLock), and only if `doomed()` still says so with the +// lock held (it may have been opened, and its .complete touched, just before). True when it went. +inline bool removeUnused(const std::filesystem::path& folder, const std::function& doomed = {}) { + std::error_code ec; + UseLock l; + if (!l.take(folder, true)) return false; + if (doomed && !doomed()) return false; + std::filesystem::remove_all(folder, ec); +#ifdef _WIN32 + l.release(); // (Windows keeps the folder while its .inuse is open) + std::filesystem::remove_all(folder, ec); +#endif + return !std::filesystem::exists(folder, ec); +} + inline long processId() { #ifdef _WIN32 return static_cast(_getpid()); @@ -146,9 +331,27 @@ inline long processId() { #endif } -// A member path is safe to extract: relative, no "..", no drive letters. +// Well-formed UTF-8 (fs::u8path throws on anything else on Windows: a damaged name must not take Studio down). +inline bool validUtf8(const std::string& s) { + for (size_t i = 0; i < s.size();) { + const unsigned char c = static_cast(s[i]); + const size_t n = c < 0x80 ? 1 : (c >> 5) == 6 ? 2 : (c >> 4) == 14 ? 3 : (c >> 3) == 30 ? 4 : 0; + if (!n || i + n > s.size() || (n == 2 && c < 0xC2)) return false; + uint32_t cp = n == 1 ? c : c & (0x7F >> n); + for (size_t k = 1; k < n; ++k) { + const unsigned char d = static_cast(s[i + k]); + if ((d & 0xC0) != 0x80) return false; + cp = cp << 6 | (d & 0x3F); + } + if ((n == 3 && (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))) || (n == 4 && (cp < 0x10000 || cp > 0x10FFFF))) return false; + i += n; + } + return true; +} + +// A member path is safe to extract: relative, no "..", no drive letters, UTF-8, no NUL. inline bool safeMember(const std::string& m) { - if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos) return false; + if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos || m.find('\0') != std::string::npos || !validUtf8(m)) return false; for (size_t a = 0; a <= m.size();) { size_t b = m.find_first_of("/\\", a); if (b == std::string::npos) b = m.size(); @@ -161,11 +364,14 @@ inline bool safeMember(const std::string& m) { } // namespace fenv_detail // Where opened .fenv files are unpacked: FRAME_FENV_CACHE, else %LOCALAPPDATA%\FrameStudio\cache (Windows) or -// $XDG_CACHE_HOME / ~/.cache /frame-home/fenv. +// $XDG_CACHE_HOME / ~/.cache /frame-home/fenv. UTF-8: on Windows read wide (the narrow getenv is the ANSI code +// page, which u8path throws on for a user name like "José"). inline std::string fenvCacheDir() { - if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e; #ifdef _WIN32 - if (const char* l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\FrameStudio\\cache"; + if (const wchar_t* e = _wgetenv(L"FRAME_FENV_CACHE")) return std::filesystem::path(e).u8string(); + if (const wchar_t* l = _wgetenv(L"LOCALAPPDATA")) return (std::filesystem::path(l) / L"FrameStudio" / L"cache").u8string(); +#else + if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e; #endif if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv"; const char* h = std::getenv("HOME"); @@ -193,17 +399,48 @@ inline bool fenvParseManifest(const std::string& json, FenvManifest& m) { const std::string f = doc["models"][i]["file"].asString(); if (fenv_detail::safeMember(f)) m.models.push_back(f); } + for (size_t i = 0; i < doc["splats"].size(); ++i) { + const auto& e = doc["splats"][i]; + FenvSplats s; + s.file = e["file"].asString(); s.name = e["name"].asString(); + s.count = static_cast(std::max(0, e["count"].asInt())); + if (e["xform"].size() == 16) + for (size_t k = 0; k < 16; ++k) s.xform[k] = static_cast(e["xform"][k].asFloat()); + if (fenv_detail::safeMember(s.file)) m.splats.push_back(s); + } if (doc.has("edits") && fenv_detail::safeMember(doc["edits"].asString())) m.edits = doc["edits"].asString(); if (doc.has("thumbnail") && fenv_detail::safeMember(doc["thumbnail"].asString())) m.thumbnail = doc["thumbnail"].asString(); if (doc.has("scene") && fenv_detail::safeMember(doc["scene"].asString())) m.scene = doc["scene"].asString(); if (doc.has("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString(); if (doc.has("settings")) m.settings.fromValue(doc["settings"]); - return !m.sources.empty() || !m.models.empty() || !m.scene.empty(); + m.studioVersion = doc["studio"].asString(); + for (size_t i = 0; i < doc["requires"].size(); ++i) m.needs.push_back(doc["requires"][i].asString()); + m.version = std::max(1, doc["version"].asInt()); + static const char* kKnown[] = {"format", "version", "name", "sources", "models", "splats", "edits", "thumbnail", "scene", "world", + "settings", "requires", "studio"}; + for (const auto& kv : doc.objVal) + if (std::none_of(std::begin(kKnown), std::end(kKnown), [&](const char* k) { return kv.first == k; })) + m.unknown += ",\n " + fenv_detail::quote(kv.first) + ": " + fenv_detail::toJson(kv.second); + return !m.sources.empty() || !m.models.empty() || !m.scene.empty() || !m.splats.empty(); } catch (...) { return false; } } +// The features this build reads (FenvManifest::needs), and the ones a manifest asks for that it doesn't ("" none). +inline const std::vector& fenvFeatures() { + static const std::vector known = {"splats"}; // (sky panoramas, ... join as they're written) + return known; +} +// Who made it, as shown: "Frame Studio v0.2.3", or "made before v0.2" (the version wasn't written then). +inline std::string fenvMadeWith(const FenvManifest& m) { return m.studioVersion.empty() ? "made before v0.2" : "Frame Studio " + m.studioVersion; } +inline std::string fenvMissingFeatures(const FenvManifest& m) { + std::string missing; + for (const std::string& r : m.needs) + if (std::find(fenvFeatures().begin(), fenvFeatures().end(), r) == fenvFeatures().end()) missing += (missing.empty() ? "" : ", ") + r; + return missing; +} + // Reads fenv.json (and, if asked, the thumbnail bytes) without extracting anything else. inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector* thumb = nullptr) { mz_zip_archive z = {}; @@ -216,7 +453,8 @@ inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vect } // Extracts every member into /---