From 79d82f7b643a7b054070801b2e445508865e0e3c Mon Sep 17 00:00:00 2001 From: seedds Date: Wed, 21 Oct 2026 02:09:09 +0800 Subject: [PATCH] Remove the D2R code and the legacy frame step D2R (Diablo II Resurrected) has no profile and its code could not run here: the host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a missing dm/PRODUCT, and the host files its hooks called were never in this repository. Removed with it: - rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4 rule set and private gates, the boot list, the cut scope column and the KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no params.tsv any more); dm/tools/product.sh - host: D2R save edits, add-on content, render-scale controller, time() takeover and Terror Zone clock, crash decoding at D2R offsets, host-call registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped archives, boot profiling hooks with no implementation; D2R headers, tests and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate - graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE, the eco-fps setting, the loading-screen gate; shader_map.py, nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the Breath of the Wild and Tears of the Kingdom stubs lose their mode test. Breath of the Wild's interval-controller cuts stay: the framework keeps the controller's interval while the game's VFR override holds. patch_nso.py output is byte-identical for all 15 games on this machine except the two frame-step stubs; all 18 profiles build; the host tests pass (their Makefile now builds the general tests only). --- AGENTS.md | 4 +- README.md | 2 +- dm/games/01007EF00011E000/game.tsv | 4 +- dm/games/0100F2C0115B6000/game.tsv | 4 +- dm/games/README.md | 1 - dm/games/game.py | 3 - dm/gfx/README.md | 11 +- dm/gfx/nvn/hle_nvn.c | 21 +- dm/gfx/nvn/nvn_dirty.c | 2 +- dm/gfx/nvn/nvn_mtl.h | 2 - dm/gfx/nvn/nvn_mtl.m | 554 +-- dm/gfx/nvn/nvn_rtex.inc | 155 - dm/gfx/nvn/nvn_slots.tsv | 525 --- dm/gfx/nvn_metal_mapping.md | 4 +- dm/gfx/tests/t_dirty_race.c | 2 +- dm/gfx/tex/README.md | 19 +- dm/gfx/tex/tests/Makefile | 11 +- dm/gfx/tex/tests/fetch_samples.py | 26 - dm/gfx/tex/tests/t_tex.c | 146 +- dm/gfx/tools/nometal_run.py | 46 - dm/gfx/tools/nvn_slots.py | 70 - dm/gfx/tools/shader_map.py | 144 - dm/host/Makefile | 20 +- dm/host/dm.h | 24 +- dm/host/dm_actions.h | 66 - dm/host/dm_bossrush.h | 9 - dm/host/dm_cursor.h | 88 - dm/host/dm_cursor_item.h | 22 - dm/host/dm_custom.h | 15 - dm/host/dm_doorspot.h | 42 - dm/host/dm_elysium.h | 7 - dm/host/dm_hud.h | 30 - dm/host/dm_i18n.c | 71 - dm/host/dm_i18n.h | 10 - dm/host/dm_i18n_tab.h | 302 -- dm/host/dm_mode_items.h | 12 - dm/host/dm_modetx.h | 32 - dm/host/dm_padmap.h | 27 - dm/host/dm_rift.h | 27 - dm/host/dm_textbox_core.h | 10 - dm/host/dm_uiscreen.h | 17 - dm/host/dm_unit.h | 31 - dm/host/dm_widget.h | 31 - dm/host/dm_world.h | 11 - dm/host/front.h | 5 +- dm/host/front_mac.m | 11 - dm/host/hle.c | 3 - dm/host/hle.h | 27 +- dm/host/hle_fdstat.c | 4 +- dm/host/hle_fs.c | 224 +- dm/host/hle_guestthread.c | 2 +- dm/host/hle_lifecycle.c | 6 +- dm/host/hle_misc.c | 20 +- dm/host/hle_oe.c | 132 +- dm/host/hle_os.c | 15 +- dm/host/hle_sdk16.c | 2 +- dm/host/hle_svc.c | 4 +- dm/host/hle_swkbd.c | 5 +- dm/host/hle_thread.c | 49 +- dm/host/hle_time.c | 50 +- dm/host/host_stubs.c | 27 - dm/host/loader.c | 59 +- dm/host/main.c | 19 +- dm/host/probe.c | 2 +- dm/host/tests/Makefile | 112 +- dm/host/tests/client_sta_gate.py | 20 - dm/host/tests/mode_scan.py | 63 - dm/host/tests/move_codec.py | 45 - dm/host/tests/movie_cut.py | 89 - dm/host/tests/native_write_scan.py | 75 - dm/host/tests/oe_notify.py | 57 - dm/host/tests/run_manifest.py | 51 - dm/host/tests/run_rule.py | 88 - dm/host/tests/server42_rule.py | 28 - dm/host/tests/server_walk_request.py | 21 - dm/host/tests/t_altstack_stub.c | 6 +- dm/host/tests/t_cloudsync.c | 181 - dm/host/tests/t_condvar_stub.c | 1 - dm/host/tests/t_doorspot.c | 52 - dm/host/tests/t_fs.c | 48 +- dm/host/tests/t_fs_case.c | 4 +- dm/host/tests/t_fs_stub.c | 8 +- dm/host/tests/t_hid_receipt.c | 58 - dm/host/tests/t_hid_script.c | 144 - dm/host/tests/t_hud_dead.c | 27 - dm/host/tests/t_i18n.c | 40 - dm/host/tests/t_modetx.c | 35 - dm/host/tests/t_movie.c | 61 - dm/host/tests/t_namegen.c | 20 - dm/host/tests/t_oe.c | 73 - dm/host/tests/t_padmap.c | 23 - dm/host/tests/t_romfs_validate.c | 40 - dm/host/tests/t_swkbd.c | 1 - dm/host/tests/t_target_rank.c | 39 - dm/host/tests/t_textbox.c | 40 - dm/host/tests/t_thread_leak.c | 4 +- dm/host/tests/t_touch_mac.c | 112 - dm/host/tests/t_touch_tick_stub.c | 91 - dm/host/tests/t_unit_level.c | 54 - dm/host/tests/t_widget.c | 29 - dm/host/thread.c | 17 +- dm/host/touch_mac.h | 7 - dm/ios/Sources/dm_touch_controls.c | 645 --- dm/ios/Sources/dm_touch_controls.h | 182 - dm/ios/Sources/front_ios.m | 3 +- dm/ios/project.yml | 2 +- dm/ios/tools/build_ipa.sh | 2 +- dm/third_party/README.md | 3 +- dm/third_party/libdeflate/COPYING | 22 - dm/third_party/libdeflate/README.md | 244 - dm/third_party/libdeflate/VERSION.md | 6 - dm/third_party/libdeflate/common_defs.h | 735 --- dm/third_party/libdeflate/lib/adler32.c | 162 - .../libdeflate/lib/arm/adler32_impl.h | 365 -- .../libdeflate/lib/arm/cpu_features.c | 292 -- .../libdeflate/lib/arm/cpu_features.h | 214 - .../libdeflate/lib/arm/crc32_impl.h | 594 --- .../libdeflate/lib/arm/crc32_pmull_helpers.h | 156 - .../libdeflate/lib/arm/crc32_pmull_wide.h | 226 - .../libdeflate/lib/arm/matchfinder_impl.h | 78 - .../libdeflate/lib/bt_matchfinder.h | 342 -- .../libdeflate/lib/cpu_features_common.h | 93 - dm/third_party/libdeflate/lib/crc32.c | 262 -- .../libdeflate/lib/crc32_multipliers.h | 375 -- dm/third_party/libdeflate/lib/crc32_tables.h | 587 --- .../libdeflate/lib/decompress_template.h | 777 ---- .../libdeflate/lib/deflate_compress.c | 4135 ----------------- .../libdeflate/lib/deflate_compress.h | 15 - .../libdeflate/lib/deflate_constants.h | 56 - .../libdeflate/lib/deflate_decompress.c | 1208 ----- dm/third_party/libdeflate/lib/gzip_compress.c | 90 - .../libdeflate/lib/gzip_constants.h | 45 - .../libdeflate/lib/gzip_decompress.c | 144 - .../libdeflate/lib/hc_matchfinder.h | 401 -- .../libdeflate/lib/ht_matchfinder.h | 234 - dm/third_party/libdeflate/lib/lib_common.h | 106 - .../libdeflate/lib/matchfinder_common.h | 224 - .../libdeflate/lib/riscv/matchfinder_impl.h | 97 - dm/third_party/libdeflate/lib/utils.c | 141 - .../libdeflate/lib/x86/adler32_impl.h | 136 - .../libdeflate/lib/x86/adler32_template.h | 518 --- .../libdeflate/lib/x86/cpu_features.c | 213 - .../libdeflate/lib/x86/cpu_features.h | 170 - .../libdeflate/lib/x86/crc32_impl.h | 160 - .../lib/x86/crc32_pclmul_template.h | 424 -- .../libdeflate/lib/x86/decompress_impl.h | 57 - .../libdeflate/lib/x86/matchfinder_impl.h | 122 - dm/third_party/libdeflate/lib/zlib_compress.c | 82 - .../libdeflate/lib/zlib_constants.h | 21 - .../libdeflate/lib/zlib_decompress.c | 104 - dm/third_party/libdeflate/libdeflate.h | 412 -- dm/tools/patch_nso.py | 480 +- dm/tools/product.sh | 23 - docs/agents/02-survey.md | 4 +- docs/agents/03-bringup.md | 2 +- docs/agents/README.md | 2 +- docs/development.md | 10 +- scripts/build_ipa_from_nsp.py | 11 +- 158 files changed, 237 insertions(+), 21167 deletions(-) delete mode 100644 dm/gfx/nvn/nvn_rtex.inc delete mode 100644 dm/gfx/nvn/nvn_slots.tsv delete mode 100644 dm/gfx/tex/tests/fetch_samples.py delete mode 100644 dm/gfx/tools/nometal_run.py delete mode 100644 dm/gfx/tools/nvn_slots.py delete mode 100644 dm/gfx/tools/shader_map.py delete mode 100644 dm/host/dm_actions.h delete mode 100644 dm/host/dm_bossrush.h delete mode 100644 dm/host/dm_cursor.h delete mode 100644 dm/host/dm_cursor_item.h delete mode 100644 dm/host/dm_custom.h delete mode 100644 dm/host/dm_doorspot.h delete mode 100644 dm/host/dm_elysium.h delete mode 100644 dm/host/dm_hud.h delete mode 100644 dm/host/dm_i18n.c delete mode 100644 dm/host/dm_i18n.h delete mode 100644 dm/host/dm_i18n_tab.h delete mode 100644 dm/host/dm_mode_items.h delete mode 100644 dm/host/dm_modetx.h delete mode 100644 dm/host/dm_padmap.h delete mode 100644 dm/host/dm_rift.h delete mode 100644 dm/host/dm_textbox_core.h delete mode 100644 dm/host/dm_uiscreen.h delete mode 100644 dm/host/dm_unit.h delete mode 100644 dm/host/dm_widget.h delete mode 100644 dm/host/dm_world.h delete mode 100644 dm/host/host_stubs.c delete mode 100644 dm/host/tests/client_sta_gate.py delete mode 100644 dm/host/tests/mode_scan.py delete mode 100644 dm/host/tests/move_codec.py delete mode 100644 dm/host/tests/movie_cut.py delete mode 100644 dm/host/tests/native_write_scan.py delete mode 100644 dm/host/tests/oe_notify.py delete mode 100644 dm/host/tests/run_manifest.py delete mode 100644 dm/host/tests/run_rule.py delete mode 100644 dm/host/tests/server42_rule.py delete mode 100644 dm/host/tests/server_walk_request.py delete mode 100644 dm/host/tests/t_cloudsync.c delete mode 100644 dm/host/tests/t_doorspot.c delete mode 100644 dm/host/tests/t_hid_receipt.c delete mode 100644 dm/host/tests/t_hid_script.c delete mode 100644 dm/host/tests/t_hud_dead.c delete mode 100644 dm/host/tests/t_i18n.c delete mode 100644 dm/host/tests/t_modetx.c delete mode 100644 dm/host/tests/t_movie.c delete mode 100644 dm/host/tests/t_namegen.c delete mode 100644 dm/host/tests/t_padmap.c delete mode 100644 dm/host/tests/t_romfs_validate.c delete mode 100644 dm/host/tests/t_target_rank.c delete mode 100644 dm/host/tests/t_textbox.c delete mode 100644 dm/host/tests/t_touch_mac.c delete mode 100644 dm/host/tests/t_touch_tick_stub.c delete mode 100644 dm/host/tests/t_unit_level.c delete mode 100644 dm/host/tests/t_widget.c delete mode 100644 dm/host/touch_mac.h delete mode 100644 dm/ios/Sources/dm_touch_controls.c delete mode 100644 dm/ios/Sources/dm_touch_controls.h delete mode 100644 dm/third_party/libdeflate/COPYING delete mode 100644 dm/third_party/libdeflate/README.md delete mode 100644 dm/third_party/libdeflate/VERSION.md delete mode 100644 dm/third_party/libdeflate/common_defs.h delete mode 100644 dm/third_party/libdeflate/lib/adler32.c delete mode 100644 dm/third_party/libdeflate/lib/arm/adler32_impl.h delete mode 100644 dm/third_party/libdeflate/lib/arm/cpu_features.c delete mode 100644 dm/third_party/libdeflate/lib/arm/cpu_features.h delete mode 100644 dm/third_party/libdeflate/lib/arm/crc32_impl.h delete mode 100644 dm/third_party/libdeflate/lib/arm/crc32_pmull_helpers.h delete mode 100644 dm/third_party/libdeflate/lib/arm/crc32_pmull_wide.h delete mode 100644 dm/third_party/libdeflate/lib/arm/matchfinder_impl.h delete mode 100644 dm/third_party/libdeflate/lib/bt_matchfinder.h delete mode 100644 dm/third_party/libdeflate/lib/cpu_features_common.h delete mode 100644 dm/third_party/libdeflate/lib/crc32.c delete mode 100644 dm/third_party/libdeflate/lib/crc32_multipliers.h delete mode 100644 dm/third_party/libdeflate/lib/crc32_tables.h delete mode 100644 dm/third_party/libdeflate/lib/decompress_template.h delete mode 100644 dm/third_party/libdeflate/lib/deflate_compress.c delete mode 100644 dm/third_party/libdeflate/lib/deflate_compress.h delete mode 100644 dm/third_party/libdeflate/lib/deflate_constants.h delete mode 100644 dm/third_party/libdeflate/lib/deflate_decompress.c delete mode 100644 dm/third_party/libdeflate/lib/gzip_compress.c delete mode 100644 dm/third_party/libdeflate/lib/gzip_constants.h delete mode 100644 dm/third_party/libdeflate/lib/gzip_decompress.c delete mode 100644 dm/third_party/libdeflate/lib/hc_matchfinder.h delete mode 100644 dm/third_party/libdeflate/lib/ht_matchfinder.h delete mode 100644 dm/third_party/libdeflate/lib/lib_common.h delete mode 100644 dm/third_party/libdeflate/lib/matchfinder_common.h delete mode 100644 dm/third_party/libdeflate/lib/riscv/matchfinder_impl.h delete mode 100644 dm/third_party/libdeflate/lib/utils.c delete mode 100644 dm/third_party/libdeflate/lib/x86/adler32_impl.h delete mode 100644 dm/third_party/libdeflate/lib/x86/adler32_template.h delete mode 100644 dm/third_party/libdeflate/lib/x86/cpu_features.c delete mode 100644 dm/third_party/libdeflate/lib/x86/cpu_features.h delete mode 100644 dm/third_party/libdeflate/lib/x86/crc32_impl.h delete mode 100644 dm/third_party/libdeflate/lib/x86/crc32_pclmul_template.h delete mode 100644 dm/third_party/libdeflate/lib/x86/decompress_impl.h delete mode 100644 dm/third_party/libdeflate/lib/x86/matchfinder_impl.h delete mode 100644 dm/third_party/libdeflate/lib/zlib_compress.c delete mode 100644 dm/third_party/libdeflate/lib/zlib_constants.h delete mode 100644 dm/third_party/libdeflate/lib/zlib_decompress.c delete mode 100644 dm/third_party/libdeflate/libdeflate.h delete mode 100755 dm/tools/product.sh diff --git a/AGENTS.md b/AGENTS.md index d5035e1..cb3674d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # Notes for agents - This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Nineteen games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`) and Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`, profile key `graphics vulkan`). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (profile key `shader_glslc_capture`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`patch_nso.py` `FRAME_STEP`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout. Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (profile key `shader_vk_capture`); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (profile key `nvn_tex_slots`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (profile key `shader_ue_decode`, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (`patch_nso.py` cut, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`). [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and per-game notes. -- Each game's constants and shader tables form a game profile in `dm/games//`, where `` is the base game's title ID: `game.tsv` plus the tables and `dmsh_chain.h`. Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)). -- Per-build knowledge is keyed by the NSO build ID: rewriter cuts in `dm/tools/patch_nso.py`, the profile's `build` rows (the builds the NSP script accepts, and their display versions), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`, `DM_D2R_BUILD` for code inherited from D2R). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in. +- Each game's constants and shader tables form a game profile in `dm/games//`, where `` is the base game's title ID: `game.tsv` plus the tables and `dmsh_chain.h`. Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)). Prefer a rule that holds for every game (Horizon / NVN semantics, or what the binary, its SDK version, its imports or its files show) to a per-game key, cut or build-ID gate, and use those only when no general rule can do it. +- Per-build knowledge is keyed by the NSO build ID: rewriter cuts in `dm/tools/patch_nso.py`, the profile's `build` rows (the builds the NSP script accepts, and their display versions), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in. - To analyse or port **another** Switch title, first run `scripts/build_ipa_from_nsp.py` on its NSP folder. With no matching profile, it writes a survey to `/_survey/survey.md`, then a provisional profile guessed from the game files (`dm/games/profile_guess.py`; `<NSP folder>/<title ID>_build/profile/<title ID>/`, found by the tools through `DM_GAMES_DIR`), with a short heap check on the Mac when the game has virtual address memory, and a best-effort IPA that may not boot. Then follow [docs/agents](docs/agents/README.md) (extraction, binary survey, host bring-up failure catalogue, graphics specifics, debugging recipes) and add a profile. - Scripts the user runs live in `scripts/` at the repository root (now `build_ipa_from_nsp.py`). Each one runs both in Spyder (F5, configured by the settings block at its top) and from a terminal (command-line options override those settings; non-zero exit on error). Tools that only agents or other scripts call stay under `dm/`. - Game files, keys and anything extracted from them live in the game's NSP folder, never in this repository and never in the home folder (it is nearly full): the work folder `<NSP folder>/<id>-work` for extraction, runs and analysis, beside the NSP script's `<id>_build/`. Tool venvs and other large scratch data go on that drive too. Do not print key values. diff --git a/README.md b/README.md index 4088efe..c1a26b4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This project is built on other people's work: - **[LibHac](https://github.com/Thealexbarney/LibHac)**: reads NSPs and layers updates over the base game in `dm/tools/nx_extract`. **hactool** did the first extractions. - **[SPIRV-Cross](https://github.com/KhronosGroup/SPIRV-Cross)**: SPIR-V to Metal Shading Language. - **deko3d** (zlib): its texture layout size algorithm is ported in `dm/gfx/nvn/nvn_tex.c`. -- **libvpx** (VP9 movies), **stb_vorbis** (movie sound), **libdeflate**, the **Vulkan headers** and **NVIDIA FLIP** ship in `dm/third_party` with their licenses ([list](dm/third_party/README.md)). +- **libvpx** (VP9 movies), **stb_vorbis** (movie sound), the **Vulkan headers** and **NVIDIA FLIP** ship in `dm/third_party` with their licenses ([list](dm/third_party/README.md)). - **Capstone**, **lz4**, **fontTools** and **XcodeGen** are used by the build. ## Games diff --git a/dm/games/01007EF00011E000/game.tsv b/dm/games/01007EF00011E000/game.tsv index ad1c4a7..f31ad5b 100644 --- a/dm/games/01007EF00011E000/game.tsv +++ b/dm/games/01007EF00011E000/game.tsv @@ -19,8 +19,8 @@ pad_layout full # The frame-interval controller (60 fps cuts in patch_nso.py) and dynamic resolution read the GPU time between the frame's two timestamp reports: each report takes its segment's GPU start / end gpu_timestamps segment # 60 fps (30 on the Switch): the host decides each frame's step in whole vsyncs and holds the frame that long (rewriter stubs at setWaitVsyncInterval and VFR::updateInterval, -# patch_nso.py FRAME_STEP); 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power; DM_FRAME_STEP=legacy runs the game's own -# interval controller with the 60 fps cuts instead +# patch_nso.py FRAME_STEP); 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power; 0 runs the game's own +# interval controller (with the 60 fps cuts, which also serve while the game's VFR override is set) fps_target 60 # ---- Versions ---- diff --git a/dm/games/0100F2C0115B6000/game.tsv b/dm/games/0100F2C0115B6000/game.tsv index 2d34f09..a9ef727 100644 --- a/dm/games/0100F2C0115B6000/game.tsv +++ b/dm/games/0100F2C0115B6000/game.tsv @@ -41,8 +41,8 @@ tex_flush_upload on render_cond on tex_dirty_upload on # 60 fps (30 on the Switch): the host decides each frame's step in whole vsyncs and holds the frame that long (rewriter stubs at FPSLocker's sites, patch_nso.py FRAME_STEP); -# 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power, 0 = the game's own step; DM_FRAME_STEP=legacy steps by the measured -# frame time instead (the method before, iPhone-tested at 60) +# 30 when 60 does not hold, game time = real time. DM_FPS_TARGET=30 (Documents/env.txt on the iPhone) saves power, 0 = the game's own step. Until the host has decided a step +# (the first frames) the stub steps by the measured frame time fps_target 60 # ---- NVN driver constant segments (Breath of the Wild's values, the same NVN build) ---- diff --git a/dm/games/README.md b/dm/games/README.md index fef07bd..b3a64da 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -43,7 +43,6 @@ None of them has a default game: without one they stop and list the profiles. To | `game_args` | Arguments the game gets when the command line gives none (the iOS app, the NSP script's Mac runs), split at spaces; `dmhost <dir> -- …` overrides them. The SDK hands them to the game before `nninitStartup`, as on hardware (`main.c`). Octopath Traveler 0 caps the address region Unreal's `nninitStartup` reserves (62 GB by default) with `-SwitchVirtualAddressSpaceSize=2048` (MB) | | `present_interval` | `game` (the default): frames are paced by the present interval the game sets, as on the Switch (a frame presented with interval 2 stays on screen for two display ticks; a game that keeps asking for 2 for a second gets the 30 fps target, and on iOS the display link then runs at 30 Hz; Mario 1.2.1 switches to 2 for single frames after a slow one; Engage asks for 2 on the Switch, and a rewriter cut on its 2.0.0 `main` makes it ask for 1, see `0100A6301214E000/game.tsv`). `display`: one frame per display tick whatever the game asks, which is only right for games whose logic does not count frames (D2R's runs on its own 25 Hz clock). `DM_PRESENT_INTERVAL=game` / `display` overrides it, also from `Documents/env.txt` | | `fps_target` | Frame rate cap of the rewriter's frame step, for builds that have one (`patch_nso.py` `FRAME_STEP`: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0), or the engine's own frame limit the host writes into game memory (`hle_fpslock.c`: Monster Hunter Rise 16.0.2, which also needs `present_interval display`). `0` (the default): the game's own step. `30` / `60`: the host decides each frame's step in whole vsyncs and keeps the frame on screen that long (`hle_nvn.c` `fstep_tick`, `nvn_fstep.h`): 1 at a 60 cap, 2 when 60 does not hold or at a 30 cap (and while a prerendered movie plays), plus one vsync while the game is behind real time after a late frame. Presents follow that hold instead of the interval the game sets. `DM_FPS_TARGET` overrides it, also from `Documents/env.txt`; the log line `frame step: game time … of real time` checks the game's speed | -| `frame_step` | `ticks` (the default): the frame step above. `legacy`: each game's method before it, for the iPhone A/B test: Tears of the Kingdom steps by the measured frame time capped at `fps_target`, Breath of the Wild runs its own interval controller with the 60 fps cuts. `DM_FRAME_STEP=ticks` / `legacy` overrides it, also from `Documents/env.txt` | | `core_preempt` | `on` / `off` (the default): approximate Horizon's per-core preemption. When an SVC wakes a higher-priority thread pinned to the caller's core, the caller waits (at most 5 ms) until such threads block again (`hle_thread.c`). Metroid Dread's movie player relies on it; Mario's loading gets slower with it (first frame 0.6 → 2.9 s), so it is off unless a game needs it. `DM_PREEMPT=0` / `1` overrides | | `alloc_big_align` | `on` / `off` (the default): `nn::mem::StandardAllocator::Allocate` aligns blocks of 64 KB or more to 64 KB (`hle_os.c`). Vanillaware's engine (13 Sentinels: Aegis Rim, Unicorn Overlord) takes a memory pool's storage from malloc and cuts ring heaps out of it at 64 KB alignment relative to the pool's GPU address. On the Switch that address is big-page aligned; the host's is the CPU address, only 4 KB aligned, so the last ring heap does not fit and keeps base 0, and the game writes through a null pointer (13 Sentinels entering its first battle, Unicorn Overlord past the title screen) | | `shared_font` | What the shared-font address points at: `bfttf` (the default) = the 8-byte BFTTF header (with the "already decoded" marker) followed by the TTF, which Mario passes to `ScalableFontEngineHelper::Decode`; `ttf` = the TTF itself, which Super Mario 3D World + Bowser's Fury (SDK 10) hands straight to `ScalableFontEngine::LoadFont` (with the header the engine fails with error 403 and the game crashes looking up a glyph) | diff --git a/dm/games/game.py b/dm/games/game.py index 9762336..af3f31b 100644 --- a/dm/games/game.py +++ b/dm/games/game.py @@ -96,8 +96,6 @@ def header(g): if pi not in ('game', 'display'): sys.exit(f'{g.id}: present_interval must be game or display: {pi}') ft = g.get('fps_target', '0') # frame rate cap of the rewriter's frame step (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0) or of the engine's own frame limit (hle_fpslock.c): 0 = the game's own step, 30 / 60 if ft not in ('0', '30', '60'): sys.exit(f'{g.id}: fps_target must be 0, 30 or 60: {ft}') - fs = g.get('frame_step', 'ticks') # the frame step's method: ticks = whole vsyncs decided by the host (nvn_fstep.h), legacy = each game's earlier method (for the iPhone A/B test) - if fs not in ('ticks', 'legacy'): sys.exit(f'{g.id}: frame_step must be ticks or legacy: {fs}') cp = g.get('core_preempt', 'off') # same-core preemption approximation (hle_thread.c): on / off if cp not in ('on', 'off'): sys.exit(f'{g.id}: core_preempt must be on or off: {cp}') aba = g.get('alloc_big_align', 'off') # nn::mem::StandardAllocator::Allocate aligns blocks of 64 KB or more to 64 KB (hle_os.c; Vanillaware's memory pool storage): on / off @@ -155,7 +153,6 @@ def header(g): f'#define DM_GAME_CORE_PREEMPT {int(cp == "on")}', f'#define DM_GAME_ALLOC_BIG_ALIGN {int(aba == "on")}', f'#define DM_GAME_FPS_TARGET {int(ft)}', - f'#define DM_GAME_FRAME_STEP_LEGACY {int(fs == "legacy")}', f'#define DM_GAME_SHARED_FONT_TTF {int(sf == "ttf")}', f'#define DM_GAME_PAD_FULL {int(pl == "full")}', f'#define DM_GAME_OPMODE {int(om == "console")}', diff --git a/dm/gfx/README.md b/dm/gfx/README.md index 5570ec2..4077032 100644 --- a/dm/gfx/README.md +++ b/dm/gfx/README.md @@ -10,7 +10,7 @@ For the build steps of the whole project see `README.md` and `setup.sh` in the r - The game's main program imports only one NVN symbol, `nvnBootstrapLoader`; all other NVN functions are obtained through it by name. The host forcibly takes over this import (`nvn/hle_nvn.c`), and not a single instruction of the original graphics driver is executed. - The game fetches the addresses of 523 names in total (`nvn/names.txt`). Each name has a thunk (`nvn/nvn_thunks.S`, a generated file): it first increments that function's call count and then jumps to the implementation. -- Of these, the 216 that have call sites in the game's main program (`nvn/nvn_slots.tsv`) all have implementations: 203 are implemented according to NVN semantics, and 13 are empty implementations (`g_nops` in `hle_nvn.c`). The remaining 307 are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log. +- The functions games call have implementations, according to NVN semantics or empty (`g_nops` in `hle_nvn.c`); the rest are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log. - Every implementation gets a compile-time signature check against `nvn/nvn_protos.h` (a generated file). The calling conventions of Apple arm64 and the Switch differ for integer parameters narrower than 32 bits, so on the host side `NVNboolean` parameters are always received as `uint32_t` and only the low 8 bits are examined. ### Two layers: record and replay @@ -36,8 +36,6 @@ Original Switch shader package → xcrun metal / metallib compiles into Metal shader libraries ``` -Without Apple's Metal compiler (Windows, Linux, a Mac without Xcode) the last step cannot be done. `tools/nometal_run.py` lets the same chain run its first two steps as usual and puts the translated MSL source into the library files as is; a package made this way compiles the shaders once on the phone, and the system caches the compiled result. This route is used by `dm/tools/d2rm_pack.py`; see `docs/NO_XCODE.md` in the repository root. - The whole chain is driven by `tools/dmsh_build.py`, and `setup.sh` runs it for you. Key points: - **Key**: each shader is keyed by the first 16 hexadecimal digits of the sha256 of its control section (the first 0x880 bytes, or the section's own length at control +0x20 when that is shorter, as in SDK 10's 0x850-byte sections) followed by its code area (many of Mario's shaders share a control section and differ in code). At run time the game hands both to `nvnProgramSetShaders`, and the replay layer computes the same key to find the library. @@ -77,7 +75,7 @@ Binding conventions followed during translation (file header of `tools/maxwell_r | `nvn/nvn_hmap.h`, `nvn/nvn_cover.h`, `nvn/nvn_pace.h`, `nvn/nvn_pso_skey.h` | Hash table, geometric proof of full-coverage draws, presentation pacer, pipeline manifest key | | `nvn/nvn_builtin.h` | Hand-written MSL for the 4 compute shaders embedded in the main program | | `nvn/include/nvn.h` | NVN interface declarations (third party; for the license see the file header) | -| `nvn/names.txt`, `nvn/nvn_slots.tsv` | The NVN API name list and D2R's slot evidence. The per-game tables described above and `dmsh_chain.h` live in the game profile `dm/games/<id>/` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) | +| `nvn/names.txt` | The NVN API name list. The per-game tables described above and `dmsh_chain.h` live in the game profile `dm/games/<id>/` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) | | `tex/` | Texture format mapping, deswizzling, BC decompression, see [tex/README.md](tex/README.md) | | `metal/` | A standalone minimal Metal offscreen example, not compiled into the host, see [metal/README.md](metal/README.md) | | `tools/` | Offline tools, see below | @@ -95,11 +93,10 @@ The scripts in `tools/`; usage is written in each file's header: | `dmsh_fetch.py` | Generates the "readable tile" versions of fragment shaders | | `c1_guard.py` | Generates `c1_full.tsv` | | `dmsh_dryrun.py` | Dry-run conversion of the whole library, measuring only "can it be converted" | -| `shader_map.py`, `maxwell_refs.py` | Generates the shader map; reads resource references from the machine code | -| `nvn_slots.py`, `gen_protos.py` | Generate `nvn_slots.tsv` / `names.txt`, and `nvn_protos.h` / the thunks | +| `maxwell_refs.py` | Reads resource references from the machine code | +| `gen_protos.py` | Generates `nvn_protos.h` / the thunks | | `tbdr_budget.py`, `tbdr_load_dead.py`, `tbdr_store_dead.py`, `tbdr_pass_merge.py` | Read the `DM_NVN_PASSDUMP` log and analyze the load / store volume of each render pass's attachments per frame | | `passgpu_top.py` | Reads the `DM_NVN_PASSGPU` output and lists GPU time by pass | -| `nometal_run.py` | Runs this translation chain on a machine without Apple's Metal compiler: shaders go into the libraries as source and are compiled on the phone | ## Building diff --git a/dm/gfx/nvn/hle_nvn.c b/dm/gfx/nvn/hle_nvn.c index 2bc7e3e..48c2526 100644 --- a/dm/gfx/nvn/hle_nvn.c +++ b/dm/gfx/nvn/hle_nvn.c @@ -1201,7 +1201,7 @@ uint64_t dm_nvn_sub_cpu_ns(void) { return atomic_load(&g_cum_sub_cpu_ns); } // __attribute__((weak)) int dm_thermal_now(void) { return -1; } // Default (Mac); overridden by iOS front_ios.m // default (Mac); the strong definition in the iOS front end front_ios.m overrides it // Per-frame ring for the long-frame log (GPU time only arrives 8 frames late, so store first and judge later) #define LFN 16 -typedef struct { uint64_t f; double fr, nv, t_s, make_ms, up_mb, up_ms; unsigned pso, lib, up, rup; int ls; } lf_rec; // ls: whether the loading screen was covering the picture when this frame was presented +typedef struct { uint64_t f; double fr, nv, t_s, make_ms, up_mb, up_ms; unsigned pso, lib, up, rup; } lf_rec; static lf_rec g_lf[LFN]; __attribute__((weak)) void dm_longframe_note(const char *what) { LOG("[longframe] %s", what); } // Default (graphics unit tests); the strong definition in the host's diag.c adds scene context and writes perf.txt extern void mtl_create_ns(uint64_t *lib_ns, uint64_t *pso_ns); extern uint64_t mtl_upload_bytes(void); extern void mtl_pkg_stats(uint64_t *uploads, uint64_t *reuploads); @@ -1366,8 +1366,7 @@ static _Atomic uint64_t g_itv2_since; // start of the game's current run of in // The one-second hold: Mario 1.2.1 picks the interval from the previous frame's time (main+0xcfd24), setting 2 for the frame after a slow one and then 1 again (123 times in two minutes on an iPhone). Following that frame by frame would flap the display-link mode; // a single interval-2 frame is instead kept on screen for its own period by the display-link callback, and the next acquire waits as many ticks (mtl_link_wait_tick, nvn_mtl.m), without changing the mode // Frame step (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3, Breath of the Wild 1.9.0): the game no longer picks an interval. The host hands it each frame's step in whole vsyncs -// (fstep_tick, nvn_fstep.h) and presents follow the hold k it decides with it; in legacy mode (state +0x28, DM_FRAME_STEP=legacy: TOTK's elapsed-time step) presents follow the cap -// (30 while a prerendered movie plays) +// (fstep_tick, nvn_fstep.h) and presents follow the hold k it decides with it __attribute__((weak)) uint8_t *dm_fps_state; // loader.c; graphics unit tests have no loader __attribute__((weak)) void dm_fps_lock_tick(void) {} // hle_fpslock.c: engine frame limits in game memory (Monster Hunter Rise); graphics unit tests have no host static int fps_step_cap(void) { @@ -1375,7 +1374,7 @@ static int fps_step_cap(void) { uint32_t b = __atomic_load_n((uint32_t *)dm_fps_state, __ATOMIC_RELAXED); float t; memcpy(&t, &b, sizeof t); return t <= 0 ? 0 : __atomic_load_n((uint32_t *)(dm_fps_state + 4), __ATOMIC_RELAXED) || t <= 30 ? 30 : 60; } -static int fstep_on(void) { return fps_step_cap() && !__atomic_load_n((uint32_t *)(dm_fps_state + 0x28), __ATOMIC_RELAXED); } // whole vsyncs, not legacy +static int fstep_on(void) { return fps_step_cap() != 0; } // the host decides the steps in whole vsyncs static nvn_fstep g_fs; // present thread only static _Atomic int g_fs_k; // the hold presentation follows (0 = not decided yet) // The display link runs at 30 Hz at a 30 cap, except while the game fixes its own step at 1 (BotW's VFR override) @@ -1443,20 +1442,12 @@ static void fstep_tick(const win_t *o) { t0 = t; v0 = vs; memset(held, 0, sizeof held); } } -int (*g_nvn_loadscreen_up)(void); // host registration: whether the loading screen is up right now (filled in when diag.c subscribes to UI messages; null in graphics unit tests = no gate) NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) { (void)q; win_t *o = (win_t *)w; o->presents++; dm_fps_lock_tick(); if (dm_fps_state) { static int was; int c = fps_step_cap(); if (c != was) { LOG("[nvn] frame step cap %d → %d fps at present %llu%s", was, c, (unsigned long long)o->presents, c == 30 && was == 60 ? " (prerendered movie)" : ""); was = c; } } if (fstep_on()) fstep_tick(o); - else if (dm_fps_state && o->presents % 1800 == 0 && fps_step_cap()) { // legacy, the elapsed-time frame step: game time against real time (1.00 = normal speed; below 1 when frames take longer than 1/15 s) - static uint64_t t0; static double g0; double g; memcpy(&g, dm_fps_state + 0x18, sizeof g); uint64_t t = now_ns(); - if (t0) LOG("[nvn] frame step: game time %.2f s in %.2f s (%.3f of real time), cap %d fps", g - g0, (t - t0) / 1e9, (g - g0) / ((t - t0) / 1e9), fps_step_cap()); - t0 = t; g0 = g; } static uint64_t prev_in, prev_out, prev_pass, prev_draw; - // Loading screen on first entering a run: if the background pipeline warm-up has not finished, wait for it here (the loading screen stays a bit longer), so that after entering the run no pipelines are compiled on the fly on the submit thread. When warm-up finished long ago this passes with a single check - { static int gate; - if (!gate && g_nvn_loadscreen_up && g_nvn_loadscreen_up()) { gate = 1; int ms = mtl_warm_wait(15000); if (ms) LOG("[nvn] the loading screen into the run waited %d ms for pipeline prewarm", ms); } } present_cfg(o); { // Test switch DM_TEST_PRESENT_LAG=every N presents,milliseconds: sleep that long before the Nth, 2Nth… present, imitating an occasional slow frame on a phone (Mario 1.2.1 then sets present interval 2; checks that the display link holds frames by interval). Not set in the product @@ -1520,7 +1511,7 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) { trow(f, "@lib_new", (double)(lib_ - plib)); trow(f, "@pso_new", (double)(pso_ - ppso)); { // long-frame log: this frame's items are first recorded into the ring; whether to print is decided 8 frames later, once the GPU time has arrived (see below) static uint64_t plns, ppns, pup, prup, pub; uint64_t lns, pns, up, rup, ub; mtl_create_ns(&lns, &pns); mtl_pkg_stats(&up, &rup); ub = mtl_upload_bytes(); - lf_rec *r = &g_lf[f % LFN]; r->f = f; r->fr = fr; r->nv = nv; r->ls = g_nvn_loadscreen_up ? g_nvn_loadscreen_up() : 0; r->t_s = (now_ns() - g_t0_ns) / 1e9; r->pso = (unsigned)(pso_ - ppso); r->lib = (unsigned)(lib_ - plib); + lf_rec *r = &g_lf[f % LFN]; r->f = f; r->fr = fr; r->nv = nv; r->t_s = (now_ns() - g_t0_ns) / 1e9; r->pso = (unsigned)(pso_ - ppso); r->lib = (unsigned)(lib_ - plib); r->make_ms = ((lns - plns) + (pns - ppns)) / 1e6; r->up = (unsigned)(up - pup); r->rup = (unsigned)(rup - prup); r->up_mb = (ub - pub) / 1048576.0; r->up_ms = up_ms; plns = lns; ppns = pns; pup = up; prup = rup; pub = ub; } @@ -1540,8 +1531,6 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) { snprintf(b, sizeof b, "frame %llu (%.2f s) whole frame %.1f ms, submit+present %.1f ms, GPU %.1f ms; new pipelines %u + shader libraries %u, %.1f ms in all; texture uploads %u first + %u re-uploads, %.1f MB / %.1f ms", (unsigned long long)r->f, r->t_s, r->fr, r->nv, g, r->pso, r->lib, r->make_ms, r->up, r->rup, r->up_mb, r->up_ms); // Visibility: during the time a long frame "comes out late", what stays on screen is the previous frame; if the previous frame was a loading screen (or this frame itself is one), all the player sees is the loading screen staying a bit longer, with no motion interrupted - const lf_rec *pv = &g_lf[(f - 9) % LFN]; int hidden = r->ls || (f >= 9 && pv->f == f - 9 && pv->ls); - if (hidden) strlcat(b, "; next to a loading screen, not visible", sizeof b); dm_longframe_note(b); } } uint64_t g0, g1; if (frame_rec() && mtl_gpu_span(f - 8, &g0, &g1)) { trow(f - 8, "@gpu_beg_ms", (g0 - g_t0_ns) / 1e6); trow(f - 8, "@gpu_end_ms", (g1 - g_t0_ns) / 1e6); } } prev_in = t0; prev_pass = ps; prev_draw = dr; @@ -1568,7 +1557,7 @@ NVN(nvnQueuePresentTexture, void, NVNqueue *q, NVNwindow *w, int idx) { prev_out = now_ns(); } -// ---------- State-only interfaces used by newer-SDK games (SMBW) that D2R did not use: buffer objects, pool / sampler / texture queries, views, color state, device and window ---------- +// ---------- State-only interfaces used by newer-SDK games (SMBW): buffer objects, pool / sampler / texture queries, views, color state, device and window ---------- // They only read and write the object itself and record no commands. Where the host has no matching capability (texture channel swizzle, depth / stencil sampling mode, sampler reduction, virtual mapping) the first occurrence logs one line and the call returns normally. #define ONCE_LOG(...) do { static int once_; if (!once_++) LOG("[nvn] " __VA_ARGS__); } while (0) diff --git a/dm/gfx/nvn/nvn_dirty.c b/dm/gfx/nvn/nvn_dirty.c index 51dfb4b..da0778a 100644 --- a/dm/gfx/nvn/nvn_dirty.c +++ b/dm/gfx/nvn/nvn_dirty.c @@ -11,7 +11,7 @@ #include <mach/mach_time.h> #include <sched.h> -// Order of page state and mprotect (from d2r-mobile ef9b1ff13): the side changing protection first sets the state to BUSY and sets the final state only after mprotect has landed; other threads wait when they see BUSY. +// Order of page state and mprotect: the side changing protection first sets the state to BUSY and sets the final state only after mprotect has landed; other threads wait when they see BUSY. // set read-only: NONE -> BUSY -> mprotect(read-only) -> PROT; unprotect: PROT -> BUSY -> mprotect(writable) -> NONE. // Invariant: a page whose state is NONE is always writable, one whose state is PROT is always read-only. Setting read-only used to be "record PROT, then mprotect"; in between, another thread's unprotect-before-write // (the movie decoder thread writing a frame into texture storage, a disk read into a guest buffer) could set the state back to NONE, then the mprotect landed => the page read-only with state NONE: a game thread writing it got only diff --git a/dm/gfx/nvn/nvn_mtl.h b/dm/gfx/nvn/nvn_mtl.h index ae55084..5734486 100644 --- a/dm/gfx/nvn/nvn_mtl.h +++ b/dm/gfx/nvn/nvn_mtl.h @@ -44,5 +44,3 @@ void *mtl_device(void); uint64_t mtl_ext_tex(void *tex); uint64_t mtl_present_ext(uint64_t serial, double period, void *ev, uint64_t ev_val, const int crop[4]); // crop: x, y, w, h of the texture to show (NULL = all of it) void mtl_present_wait(uint64_t ticket); // as in nvn_obj.h: wait until that present has been handed to the GPU -void mtl_ui_ghost(int on, float px, float py, const float grid[4], float cx, float cy); // Drag-follow image for shop items: when on=1, the icon of the item that covers screen point (px,py) and sits on the shop shelf grid `grid` (top-left x,y, width and height of one cell) is translated to center (cx,cy) and drawn once more at the end of the UI pass (normalized, top-left origin); callable from any thread -int mtl_ui_ghost_rect(float r[4]); // Screen rectangle (normalized) of the item recognized above; returns 0 if not recognized yet diff --git a/dm/gfx/nvn/nvn_mtl.m b/dm/gfx/nvn/nvn_mtl.m index 33c0ce0..99f5792 100644 --- a/dm/gfx/nvn/nvn_mtl.m +++ b/dm/gfx/nvn/nvn_mtl.m @@ -71,16 +71,6 @@ static id<MTL4CommandBuffer> g_present_cb[3]; static id<MTL4ArgumentTable> g_vt, g_ft, g_ct, g_up_ct; static id<MTL4ArgumentTable> g_present_args; static id<MTLBuffer> g_present_uv[3]; -int g_bloom_q = -1; // Bloom 1/4 starting point (non-static: real-device same-process alternation sets it to 0 / 1 directly) -static id<MTL4ArgumentTable> g_bloomq_args; -static id<MTLBuffer> g_bloomq_const[3]; -static id<MTLSamplerState> g_bloomq_samp; -static id<MTLRenderPipelineState> g_bloomq_pso; -static MTLPixelFormat g_bloomq_fmt; -static id<MTLTexture> g_bloomq_view; -static int g_bloomq_i; -static struct { int state; uint64_t s_ser, b0_ser, b1_ser, s_h, samp_h, b1_h; const tex_t *s_tex; float threshold, coeff_g, coeff_b; } g_bloomq; -static uint64_t g_bloomq_last_b0, g_bloomq_last_b1h, g_bloomq_last_b1s; // Most recent bloom 1/4 overlay read redirection: B0 serial → B1 handle (kept across frames, see the 2e2d6700a2569795 branch) static uint64_t g_present_alloc_fence[3]; static pthread_mutex_t g_pq_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_pq_cv = PTHREAD_COND_INITIALIZER; @@ -323,15 +313,10 @@ void mtl_init(void) { pad.maxTextureBindCount = 1; pad.maxSamplerStateBindCount = 1; pad.maxBufferBindCount = 1; g_present_args = [g_dev newArgumentTableWithDescriptor:pad error:&ate]; if (!g_present_args) MDIE("failed to create the present argument table: %s", ate.localizedDescription.UTF8String); - g_bloomq_args = [g_dev newArgumentTableWithDescriptor:pad error:&ate]; - if (!g_bloomq_args) MDIE("failed to create the bloom 1/4 argument table: %s", ate.localizedDescription.UTF8String); for (int i = 0; i < 3; i++) { g_present_uv[i] = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared]; if (!g_present_uv[i]) MDIE("failed to create present UV buffer[%d]", i); res_add(g_present_uv[i]); - g_bloomq_const[i] = [g_dev newBufferWithLength:sizeof(float) * 8 options:MTLResourceStorageModeShared]; - if (!g_bloomq_const[i]) MDIE("failed to create bloom 1/4 constant buffer[%d]", i); - res_add(g_bloomq_const[i]); } g_ev = [g_dev newSharedEvent]; g_keep = [NSMutableArray array]; @@ -376,7 +361,7 @@ static void pc_put(int i) { // caller holds g_mu, pool i is resident *c = (pc_ent){g_pools[i].lo, g_pools[i].hi, g_pools[i].base, g_pools[i].buf, g_pools[i].gpu}; } static _Atomic uint64_t g_reg_gen = 1; // Change generation of memory pool / texture / sampler registration: one of the invalidation conditions for argument buffer reuse -// On-demand residency of memory pools (from d2r-mobile e2a0c9a9b): putting a zero-copy buffer into the residency set makes iOS read the whole pool into memory and pin it as wired, even a pool the game never uses +// On-demand residency of memory pools: putting a zero-copy buffer into the residency set makes iOS read the whole pool into memory and pin it as wired, even a pool the game never uses // (they measured on a 17 Pro Max a 512 MB render-target pool addressed 0 times in a run, with about 530 MB more device wired memory; a 4 GB A14 was ended by the system within seconds of launch because of it). // So a pool is not put into the residency set when created; it is put in, and the residency set committed on the spot, the first time the GPU gets its address through mtl_addr / mtl_gpu_addr -- these are the only two ways the GPU gets pool addresses, // and committing on the spot is earlier than any command buffer commit that uses it. CPU-side reads of pool contents through mtl_addr trigger it too (conservative, never misses). Caller holds g_mu @@ -578,13 +563,11 @@ static post_op *g_post; static int g_npost, g_postcap; // (command buffers, encoders, pass descriptors, etc.) are never released unless put in a pool. Measured on the character select screen of a real iOS device: the host heap grows by about 47 objects per frame, 0.22 MiB/s. void *objc_autoreleasePoolPush(void); void objc_autoreleasePoolPop(void *); static void *g_sub_pool; -static int g_ruler_truth = -1, g_ruler_pending; static int tbdr_on(void); static uint64_t g_sub_serial; // Submission index (used by the parallel precompile's 'predict only once per submission') void mtl_sub_begin(void) { mtl_init(); g_sub_serial++; g_seg = 0; seg_clk_put(g_sclk); g_sclk = NULL; g_rc_npend = 0; g_rc_skip = 0; - if (tbdr_on() == 0 && g_ruler_pending) { g_ruler_truth = 0; g_ruler_pending = 0; MLOG("DM_NVN_RULER: switching to the mode under test"); } defer_drain(); defer_reset(); warm_claim(); @@ -621,7 +604,6 @@ void mtl_sub_begin(void) { } static void rp_end(void); -extern int g_frz_on; static int g_frz_state; // World freeze: the state latched for this frame (0 normal / 1 full frame / 2 frozen frame), see the note above frz_op // DM_NVN_PASSGPU_SERIAL=1 (for measurement): before creating each encoder on the main command buffer, encode 'signal n+1 → wait for signal n+1' - the signal is only raised once all prior work has completed, // so the following encoder starts only after the previous one has fully finished; on TBDR passes no longer overlap in the pipeline, and the per-pass start-to-end span of DM_NVN_PASSGPU is that pass's exclusive time (the whole-frame total is inflated; look only at relative cost) static void ser_gate(void) { @@ -864,7 +846,6 @@ static _Atomic uint64_t g_cur_frame; // @set:<global variable name>=<value> on entering the phase set this int global variable to the value, on leaving restore it (address found by symbol name; switches to be used on a real device must have a line added to the registration table below) // <fragment shader key prefix> or * the phase drops all draws of these fragment shaders (the picture will be wrong; it only measures 'how much this kind of draw is worth'; a nearly idle category needs a round of its own) // Readings: each phase discards its first 45 frames (transition + present lag); for the remaining frames, the mean of the actual present interval (same source as perf.txt) and the frame counts in the three buckets 16.7 / 25 / ≥33 ms, one log line "UB present". -// Table output: $DM_ROOT/run/ruler/ub_table.py; real-device script: $DM_ROOT/run/ruler/ub_alt.sh. static int g_ub_n = -1, g_ub_N = 180; static _Atomic int g_ub_cur = -1; static char g_ub_name[16][16], g_ub_keys[16][16][48]; static int g_ub_nk[16]; static _Atomic int g_ubp_on, g_ubp_c[4]; static _Atomic uint64_t g_ubp_sum_us; void mtl_ub_present(double iv) { // Called once per frame by the present thread: actual present interval in ms (−1 = dropped, 0 = no reading) @@ -875,8 +856,8 @@ void mtl_ub_present(double iv) { // Called once per frame by the present threa // Registered runtime switches: name → address. Main-program symbols of an iOS release build are not in the dynamic symbol table and dlsym cannot find them (in the first real-device round all four categories were "global variable not found" and the whole round was wasted), // so a switch to be measured on a real device must have a line added here; on Mac / in unit tests unregistered ones are still looked up by symbol name. static int *ub_var(const char *nm) { - extern int g_bloom_q, g_dm_present_link; - static const struct { const char *n; int *p; } t[] = { {"g_bloom_q", &g_bloom_q}, {"g_dm_present_link", &g_dm_present_link}, {"g_frz_on", &g_frz_on} }; + extern int g_dm_present_link; + static const struct { const char *n; int *p; } t[] = { {"g_dm_present_link", &g_dm_present_link} }; for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) if (!strcmp(nm, t[i].n)) return t[i].p; return dlsym(RTLD_DEFAULT, nm); } @@ -1706,7 +1687,7 @@ void mtl_mem_stat(unsigned long long frame, unsigned long long rss_mb) { unsigned long dead_n = 0; unsigned long long dead_mb = 0; // Objects read from a weak table come back retained + autoreleased: the frame hook runs on a guest thread with no autorelease pool that drains, so without this wrapper every buffer counted would be pinned for good (the readout creating the leak itself) int pres = 0; @autoreleasepool { pthread_mutex_lock(&g_mu); pres = g_pool_res_n; for (id<MTLBuffer> b in g_dead_pools) { dead_n++; dead_mb += b.length; } pthread_mutex_unlock(&g_mu); } dead_mb >>= 20; // host objects held by serial (textures / packaged textures / programs): if it climbs along with GPU allocated, textures are not being released - // os_proc_available_memory: how far this process is from being terminated by the system for memory, iOS only (from d2r-mobile 704147218); the Mac writes "none". On a phone, a crash with no log is most likely this number reaching 0 + // os_proc_available_memory: how far this process is from being terminated by the system for memory, iOS only; the Mac writes "none". On a phone, a crash with no log is most likely this number reaching 0 char avail[32] = "none"; #if TARGET_OS_IPHONE snprintf(avail, sizeof avail, "%llu MB", (unsigned long long)(os_proc_available_memory() >> 20)); @@ -1737,11 +1718,10 @@ static struct { // ---------- Zero-source skip ---------- // Shader fact table (statically proven keys): 'the textures it samples are all 0 and the coefficient is finite ⇒ the output is all 0'. Coefficient = component comp of vector vec of fragment-stage constant buffer register ubo_slot. -// 4a819818b8e489c5 ps_outline_blur_p: output = (sum of several samples of the same texture along a row / column) × fp_c7._m0[1].x (fp_c7 = cbuf reg 4), homogeneous linear in the sampled values, no constant term -// (source of the proof: dmsh_b/<chain version>/4a819818b8e489c5_b.metal, read line by line). Add other shaders here once they are proven. +// No shader is in the table yet: a proof comes from reading the shader's translation line by line (dmsh_b/<chain version>/<key>_b.metal); unit tests add their own (zskip_test_add). typedef struct { char key[17]; int ubo_slot, vec, comp; } zskip_fact; -static zskip_fact g_zskip_facts[16] = {{"4a819818b8e489c5", 4, 1, 0}}; -static int g_zskip_nf = 1; +static zskip_fact g_zskip_facts[16]; +static int g_zskip_nf; void zskip_test_add(const char *key, int ubo_slot, int vec, int comp) { if (!key || g_zskip_nf >= 16) return; snprintf(g_zskip_facts[g_zskip_nf].key, sizeof g_zskip_facts[0].key, "%s", key); @@ -1789,45 +1769,9 @@ static int passdump(void) { } #define PD(...) do { if (passdump()) { fprintf(stderr, "[pass] F%llu ", (unsigned long long)atomic_load(&g_cur_frame)); fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); } } while (0) static void blit_note(void) { if (g_enc) PD("E %llu copy / buffer update ends the render pass", g_stat_pass); } -// DM_GFXUSE=<file>: once the character select screen is stable (starting 1 second after the first OpenPanel MainMenuButtonRibbon, requires DM_BOOTPROF=1), over 30 consecutive frames, -// the ranges in guest memory of the resources the GPU actually reads (texture storage, UBO / SSBO, vertex / index buffers), deduplicated by (kind, address, length), one line recorded on first sight. -// For intersecting with the inflate output ranges (which data decompressed before character select is really used by the character select screen). Columns: kind guest_addr len first_frame tex_id fmt w×h -__attribute__((weak)) uint64_t dm_prof_ribbon_ns(void) { return 0; } // diag.c provides the strong definition; when not merged it is always 0 → the time window does not open, and a failure is reported at the end -__attribute__((weak)) void dm_shot_written(const char *path, int ok) { (void)path; (void)ok; } __attribute__((weak)) void dm_guest_frame_tick(void) {} // hle_os.c provides the strong definition (DM_DETCLOCK deterministic guest clock); a no-op when graphics unit tests do not link the host // diag.c provides the strong definition (DM_TICK_HOLD=E:SHOT unfreeze); a no-op when graphics unit tests do not link the host __attribute__((weak)) int dm_detclock_enabled(void) { return 0; } // Same as above: the host's hle_os.c provides the strong definition; when graphics unit tests do not link the host, the deterministic clock is treated as off __attribute__((weak)) uint64_t guest_ns(void) { return 0; } // Same as above: read only when the deterministic clock is on; never reached in graphics unit tests -enum { USE_OFF, USE_WAIT, USE_REC, USE_DONE }; -static int g_use = -1; static FILE *g_use_f; static long g_use_f0, g_use_rows; static nvn_hmap g_use_seen = {.klen = 24}; -#define USE_FRAMES 30 -static void use_exit(void) { - if (g_use == USE_WAIT) fprintf(stderr, "[dm] [RED] DM_GFXUSE window never opened: dm_prof_ribbon_ns() = %llu (needs DM_BOOTPROF=1 and a host with panel probes)\n", (unsigned long long)dm_prof_ribbon_ns()); - else if (g_use == USE_REC) fprintf(stderr, "[dm] [RED] DM_GFXUSE recorded only %ld frames (fewer than %d) before exit; the file is incomplete\n", (long)atomic_load(&g_cur_frame) - g_use_f0, USE_FRAMES); - if (g_use_f) fflush(g_use_f); -} -static void use_tick(void) { // Called on every present - if (g_use < 0) { const char *e = getenv("DM_GFXUSE"); g_use = USE_OFF; - if (e) { g_use_f = fopen(e, "w"); if (!g_use_f) { MLOG("[RED] DM_GFXUSE cannot open %s", e); return; } - fprintf(g_use_f, "kind\tguest_addr\tlen\tfirst_frame\ttex_id\tfmt\twxh\n"); fflush(g_use_f); g_use = USE_WAIT; atexit(use_exit); - MLOG("[gfxuse] waiting for the panel event: records %d frames starting 1 s after the first OpenPanel MainMenuButtonRibbon → %s", USE_FRAMES, e); } } - long f = (long)atomic_load(&g_cur_frame); - if (g_use == USE_WAIT) { - uint64_t t = dm_prof_ribbon_ns(); - if (t && now_ns() - t >= 1000000000ull) { g_use = USE_REC; g_use_f0 = f; MLOG("[gfxuse] recording started: from frame %ld, %d frames", f, USE_FRAMES); } - } else if (g_use == USE_REC && f - g_use_f0 >= USE_FRAMES) { - g_use = USE_DONE; fclose(g_use_f); g_use_f = NULL; - MLOG("[gfxuse] recording done: frames %ld–%ld, %ld ranges after dedup", g_use_f0, f - 1, g_use_rows); - } -} -static __attribute__((always_inline)) inline void use_rec(const char *kind, uint64_t addr, uint64_t len, int tid, const tex_t *t) { - if (g_use != USE_REC || !addr || !len) return; - uint64_t k[3] = {addr, len, (uint64_t)(uint8_t)kind[0]}; - if (nvn_hm_get(&g_use_seen, k)) return; - nvn_hm_put(&g_use_seen, k, (void *)1); - g_use_rows++; - if (t) fprintf(g_use_f, "%s\t%#llx\t%llu\t%lld\t%d\t%#x\t%ux%u\n", kind, (unsigned long long)addr, (unsigned long long)len, (long long)atomic_load(&g_cur_frame), tid, t->d.format, t->d.width, t->d.height); - else fprintf(g_use_f, "%s\t%#llx\t%llu\t%lld\t\t\t\n", kind, (unsigned long long)addr, (unsigned long long)len, (long long)atomic_load(&g_cur_frame)); -} static uint64_t att_bytes(id<MTLTexture> t, NSUInteger level) { if (!t) return 0; NSUInteger w = MAX(t.width >> level, 1), h = MAX(t.height >> level, 1); @@ -1985,8 +1929,7 @@ static int udc_on(void) { // Only for equivalence comparison (the same replay run on / off, comparing the picture byte by byte), not a product configuration // Mario: off by default (only DM_NVN_TBDR=1 turns it on). These optimizations infer read / write relations from "which textures are bound in texture slots", while 38% of this game's shaders use bindless textures (handles in uniform buffers, NVN_RB_TEXU), // so the inference would miss readers - e.g. deciding an attachment is never read afterwards and not storing it. Turn them back on once they recognize bindless readers. -static int tbdr_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_TBDR"), *r = getenv("DM_NVN_RULER"); v = e && !strcmp(e, "1"); g_ruler_truth = r && !strcmp(r, "1"); if (!v) MLOG("TBDR optimizations off by default (this game uses bindless textures; DM_NVN_TBDR=1 turns them on)"); } return v && !g_ruler_truth; } -int dm_nvn_ruler_truth(void) { tbdr_on(); return g_ruler_truth; } // Visual ruler ground-truth phase (host-side settings under test yield accordingly, e.g. the 3D render scale) +static int tbdr_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_TBDR"); v = e && !strcmp(e, "1"); if (!v) MLOG("TBDR optimizations off by default (this game uses bindless textures; DM_NVN_TBDR=1 turns them on)"); } return v; } static int rt_range_on(void) { static int v = -1; if (v < 0) { const char *e=getenv("DM_NVN_RT_RANGE"); v=e && !strcmp(e,"1"); } return v; } // Narrow candidate, off by default static uint64_t g_sd_dead_bytes, g_sd_n_dead, g_sd_n_all; // ---------- Cross-submission attachment validity ledger ---------- @@ -1996,7 +1939,7 @@ static uint64_t g_sd_dead_bytes, g_sd_n_dead, g_sd_n_all; // as an attachment with load DontCare / Clear → the pending clear is voided, with load Load → changed to a Clear load; depth-stencil is recorded per plane. // Violation = report a failure + that stable key (format / width / height / level) permanently disabled (in memory + persisted to DM_XV_FILE, read at the next launch) + all entries of the same texture deleted. The fallback can only detect, not undo (in that frame the attachment holds old content). // Its first use: color attachments 'scanned to the end undecided' at the end of a submission are not written back (sd_finish) - conditions: its first touch in this submission is a full clear / DontCare load (reset every frame before use), it is not a window texture, and the key is not disabled. -// Switch DM_NVN_XV=0 turns it off; has no effect when tbdr_on()==0 (visual ruler ground-truth mode). +// Switch DM_NVN_XV=0 turns it off; has no effect when tbdr_on()==0. // Trial period (XV_PROBE): for a key's first XV_TRIAL times it is only 'shadow-accounted' - written back as usual, but its next touch is tracked all the same; only after XV_TRIAL consecutive compliant touches does the write-back really start being skipped. // On a read / Load during the trial period: only disable that key (within this process, not persisted, no failure), the picture is intact - in practice there are readers as early as the launch / menu screens (upscale target 36), and not every fresh install should get one wrong frame. enum { XV_INVALID = 1, XV_PENDING_CLEAR = 2, XV_PROBE = 3 }; @@ -2081,10 +2024,8 @@ static void defer_fallback(void); // the encoder track and the GPU interval table carry this name directly, and shader_map.tsv maps it back to the shader; off by default (one string allocation per pass) static int g_el_w, g_el_h, g_el_draws; static char g_el_key[12]; static int enc_label_on(void) { static int v = -1; if (v < 0) { const char *e = getenv("DM_NVN_ENC_LABEL"); v = e && atoi(e); } return v; } -static void ghost_flush(void); static int g_nbcrt; static void bcrt_flush(void); // shadows of block-compressed render targets used by the open pass (see bcrt_attach) static void rp_end(void) { if (g_enc) { AP_B; - ghost_flush(); if (enc_label_on()) { static uint64_t lf = ~0ull, base; uint64_t f = atomic_load(&g_cur_frame); if (f != lf) { lf = f; base = g_stat_pass - 1; } // Intra-frame serial: the same pass has the same name every frame, so the table can aggregate by name [(id)g_enc setLabel:[NSString stringWithFormat:@"p%02llu %dx%d %s d%d", (unsigned long long)(g_stat_pass - base), g_el_w, g_el_h, g_el_key[0] ? g_el_key : "-", g_el_draws]]; } #ifdef NVN_RT_RANGE_TEST @@ -2565,18 +2506,6 @@ static id<MTLLibrary> lib_from_file(NSString *path, NSError **err) { if (fd < 0 || fstat(fd, &st) || st.st_size <= 0) { if (fd >= 0) close(fd); if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"cannot open %@", path]}]; return nil; } size_t len = (size_t)st.st_size; void *m = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); if (m == MAP_FAILED) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"mmap failed %@", path]}]; return nil; } -#ifdef DM_SHELL - // Shell: when the user's computer has no Apple shader compiler (the no-compiler mode of tools/d2rm_pack.py, gfx/tools/nometal_run.py), the "library files" contain - // shader source rather than compiled libraries (a compiled library starts with MTLB). They are compiled here on the spot; the system caches the compiled result, and from the second launch on the same shader no longer costs this time. - // Measured on a real device (cold): about 80 ms each on average, about 450 ms for the longest single one; one session uses several hundred. - if (len < 4 || memcmp(m, "MTLB", 4)) { NSString *src = [[NSString alloc] initWithBytes:m length:len encoding:NSUTF8StringEncoding]; munmap(m, len); - if (!src) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:EILSEQ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"neither a compiled library nor source %@", path]}]; return nil; } - static _Atomic int n; static _Atomic long long us; CFAbsoluteTime t0 = CFAbsoluteTimeGetCurrent(); - id<MTLLibrary> l = [g_dev newLibraryWithSource:src options:nil error:err]; long long dt = (long long)((CFAbsoluteTimeGetCurrent() - t0) * 1e6); - int k = atomic_fetch_add(&n, 1) + 1; long long tot = atomic_fetch_add(&us, dt) + dt; - if (k == 1 || k % 100 == 0 || dt >= 300000) MLOG("runtime shader compile: #%d %.0f ms, %.1f s in total", k, dt / 1e3, tot / 1e6); - return l; } -#endif dispatch_data_t d = dispatch_data_create(m, len, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ munmap(m, len); }); return [g_dev newLibraryWithData:d error:err]; } @@ -3549,10 +3478,9 @@ static void warm_start(void) { atomic_store(&g_warm_running, 1); dispatch_async(q, ^{ warm_worker(paths); }); } -static int g_rt_ovr; // Stencil override for runtime extrapolation: 0 no override; 1 pass only on the unit / effect / extrapolation-hole bits (0xD0), no stencil write; 3 depth test as usual, write the effect bit 0x40 where it passes static id<MTLDepthStencilState> dss_get(int has_depth, int has_stencil) { - struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs, ovr; } k; - memset(&k, 0, sizeof k); k.ds = S.ds; memcpy(k.vm, S.st_vmask, 8); memcpy(k.wm, S.st_wmask, 8); k.hd = has_depth; k.hs = has_stencil; k.ovr = has_stencil ? g_rt_ovr : 0; + struct { ds_st ds; uint32_t vm[2], wm[2]; int hd, hs; } k; + memset(&k, 0, sizeof k); k.ds = S.ds; memcpy(k.vm, S.st_vmask, 8); memcpy(k.wm, S.st_wmask, 8); k.hd = has_depth; k.hs = has_stencil; if (!g_dss.klen) g_dss.klen = sizeof k; id<MTLDepthStencilState> s = hm_get_id(&g_dss, &k); if (s) return s; @@ -3568,12 +3496,6 @@ static id<MTLDepthStencilState> dss_get(int has_depth, int has_stencil) { b.readMask = S.st_vmask[1]; b.writeMask = S.st_wmask[1]; d.frontFaceStencil = f; d.backFaceStencil = b; } - if (has_stencil && k.ovr) { - MTLStencilDescriptor *f = [MTLStencilDescriptor new]; - if (k.ovr == 1) { f.stencilCompareFunction = MTLCompareFunctionNotEqual; f.readMask = 0xD0; f.writeMask = 0; } // Reference value 0: passes only if (s & 0xD0) != 0 (unit 0x10, effect 0x40, extrapolation hole / screen edge 0x80) - else { f.stencilCompareFunction = MTLCompareFunctionAlways; f.depthStencilPassOperation = MTLStencilOperationReplace; f.readMask = 0; f.writeMask = 0x40; } // Reference value 0x40 - d.frontFaceStencil = f; d.backFaceStencil = f; - } s = [g_dev newDepthStencilStateWithDescriptor:d]; hm_put_id(&g_dss, &k, s); return s; @@ -3650,22 +3572,6 @@ static id<MTLBuffer> support_buf(void) { static uint64_t support_gpu(void) { static uint64_t gpu; if (!gpu) gpu = support_buf().gpuAddress; return gpu; } static id<MTLBuffer> zero_bank(void) { static __strong id<MTLBuffer> b; if (!b) { b = [g_dev newBufferWithLength:65536 options:MTLResourceStorageModeShared]; res_add(b); } return b; } static uint64_t zero_bank_gpu(void) { static uint64_t gpu; if (!gpu) gpu = zero_bank().gpuAddress; return gpu; } -// Shift all UI draws down as a whole (iPad true fullscreen, the UI rect does not touch the top edge of the picture): add a clip-space vertical offset to the projection matrix at the start of the vertex stage's cb0, and bind a modified copy instead. -// Moving the viewport is not used: the viewport's top edge is the edge of the clip volume, so top-anchored widgets that the host lifts back to the real top of the picture (UI coordinate y negative) would be clipped away entirely. -// g_ui_moff is set per draw by draw() (0 = no change); the copies live in a ring buffer, and the same address in the same pass is copied only once. -static double g_ui_moff; -static int ui_mat_ok(void) { const p_bind_buf *u = &S.ubo[0][0]; void *mb; uint64_t mo; return u->addr && u->size >= 64 && u->size <= 4096 && !pend_hits(u->addr, u->size) && mtl_addr(u->addr, u->size, &mb, &mo); } -static uint64_t ui_mat_patch(const p_bind_buf *u, void *mb, uint64_t mo) { - enum { RING = 4 << 20 }; static __strong id<MTLBuffer> ring; static uint64_t ring_gpu, la, ls, lp, lg; static size_t at; static double lo; - if (!ring) { ring = [g_dev newBufferWithLength:RING options:MTLResourceStorageModeShared]; res_add(ring); ring_gpu = ring.gpuAddress; } - if (la == u->addr && ls == u->size && lp == g_pass.serial && lo == g_ui_moff) return lg; - const uint8_t *src = [(__bridge id<MTLBuffer>)mb contents]; if (!src || u->size < 64 || u->size > 4096) return 0; - size_t n = (u->size + 255) & ~(size_t)255; if (at + n > RING) at = 0; - uint8_t *dst = (uint8_t *)ring.contents + at; memcpy(dst, src + mo, u->size); - float m[16], o = (float)g_ui_moff; memcpy(m, dst, 64); m[1] += o * m[3]; m[5] += o * m[7]; m[9] += o * m[11]; m[13] += o * m[15]; memcpy(dst, m, 64); - la = u->addr; ls = u->size; lp = g_pass.serial; lo = g_ui_moff; lg = ring_gpu + at; at += n; - return lg; -} // Bindless handle (TEXU / SAMPU): the 32-bit word at byte offset off of uniform slot reg = the low 32 bits of the NVN handle the game wrote there (low 20 bits texture ID, high 12 bits sampler ID). // Read on the CPU side at replay time (the memory at the same moment a real GPU would read it at execution); slot unbound / out of range / unreadable = 0 (empty slot: dummy texture / dummy sampler). // A location with a GPU-side write queued in this submit (pend_hits) is still read at its current value; the first case is reported red. @@ -3711,11 +3617,9 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i const p_bind_buf *u = reg < 16 ? &S.ubo[nvn_stage][reg] : NULL; void *mb; uint64_t mo, gpu; if (!u || !u->addr || !mtl_gpu_addr(u->addr, 1, &mb, &mo, &gpu)) { RED_ONCE(16, "stage %d cb%d has no uniform buffer bound", nvn_stage, reg); continue; } - use_rec("buf", u->addr, u->size, 0, NULL); if (g_nubd && ubd_hits(u->addr, u->addr + u->size)) { // inline updates deferred in this pass (see mtl_sub_update); ubd_conflict made sure they cover the binding if (ubd_covers(u->addr, u->addr + u->size)) gpu = ubd_shadow(u->addr, u->addr + u->size); else RED_ONCE(92, "a uniform update deferred inside the pass covers only part of the binding (stage %d cb%d): the content before the update is read", nvn_stage, reg); } - if (nvn_stage == 0 && reg == 0 && g_ui_moff != 0) { uint64_t pg = ui_mat_patch(u, mb, mo); if (pg) gpu = pg; } if (bl) { PD("B %llu st%d cb reg%d addr=%#llx size=%llu pool=%#llx+%#llx f=%g,%g,%g,%g", g_stat_pass, nvn_stage, reg, (unsigned long long)u->addr, (unsigned long long)u->size, (unsigned long long)(u->addr - mo), (unsigned long long)mo, ((float *)u->addr)[0], ((float *)u->addr)[1], ((float *)u->addr)[2], ((float *)u->addr)[3]); @@ -3742,7 +3646,6 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i int tid = b->kind == NVN_RB_TEXU ? (int)(ubo_handle(nvn_stage, reg, b->off) & 0xfffff) : reg < NTEX ? (int)(S.tex[nvn_stage][reg] & 0xfffff) : 0; void *tv = NULL; uint64_t trid = tid ? tex_fast_rid(tid, &tv) : 0; __unsafe_unretained id<MTLTexture> t = (__bridge id<MTLTexture>)tv; if (t && g_fb_n && g_fb_seq == g_draw_seq && nvn_stage == 1) for (int q = 0; q < g_fb_n; q++) if (t == g_fb_src[q]) { t = g_fb_use[q]; trid = g_fb_rid[q]; break; } // same-pass feedback sampling: read the snapshot instead (see fb_prepare) - if (t && tid < g_ntreg) use_rec("tex", (uint64_t)g_treg[tid].t.mem, g_treg[tid].t.L.size, tid, &g_treg[tid].t); if (t && passdump() && tid < g_ntreg) PD("R %llu st%d ser=%llu", g_stat_pass, nvn_stage, g_treg[tid].t.serial); if (bl) PD("B %llu st%d tex reg%d tid=%d %s ser=%llu fmt=%lu %lux%lux%lu view_lv=%lu+%lu view_sl=%lu array=%lu target=%lu swz=%d%d%d%d mtl=%p", g_stat_pass, nvn_stage, reg, tid, t ? "real" : "dummy", t && tid < g_ntreg ? g_treg[tid].t.serial : 0ull, (unsigned long)t.pixelFormat, (unsigned long)t.width, (unsigned long)t.height, (unsigned long)t.depth, @@ -3795,7 +3698,6 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i if (t && t.textureType != (MTLTextureType)b->dtt) { uint64_t r2 = 0; id<MTLTexture> v = tex_fit(tid, t, (MTLTextureType)b->dtt, &r2); t = v; trid = v ? r2 : 0; } if (!t) { t = dummy_tex(b->dtt, b->dpf); trid = g_dummy_tex_rid[b->dtt & 7][b->dpf & 3]; } } - if (tv && tid < g_ntreg) use_rec("tex", (uint64_t)g_treg[tid].t.mem, g_treg[tid].t.L.size, tid, &g_treg[tid].t); if (bl && tid) PD("B %llu st%d texd [%d] tid=%d %s", g_stat_pass, nvn_stage, k, tid, tv ? "real" : "dummy"); if (tv && trid && trid != g_dummy_tbuf_rid[b->dpf & 3] && trid != g_dummy_tex_rid[b->dtt & 7][b->dpf & 3]) real |= 1ull << k; slots[aid + k] = trid; @@ -3814,13 +3716,13 @@ static int encode_args_(id<MTLCommandEncoder> en, NvShader *sh, int nvn_stage, i } else if (b->kind == NVN_RB_SBUFP || b->kind == NVN_RB_UBOP) { // B: storage buffers / compute-stage uniforms are device pointers; the address is taken by NVN slot const p_bind_buf *u = reg < 16 ? (b->kind == NVN_RB_UBOP ? &S.ubo[nvn_stage][reg] : &S.ssbo[nvn_stage][reg]) : NULL; void *mb; uint64_t mo, gpu; - if (u && u->addr && mtl_gpu_addr(u->addr, u->size ? u->size : 1, &mb, &mo, &gpu)) { use_rec("buf", u->addr, u->size, 0, NULL); slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); } + if (u && u->addr && mtl_gpu_addr(u->addr, u->size ? u->size : 1, &mb, &mo, &gpu)) { slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); } else { slots[aid] = dummy_buf_gpu(); USE(dummy_buf(), MTLResourceUsageRead); } if (bl) PD("B %llu st%d sbufp reg%d %s addr=%#llx size=%llu %s", g_stat_pass, nvn_stage, reg, u && u->addr ? "real" : "dummy", u ? (unsigned long long)u->addr : 0ull, u ? (unsigned long long)u->size : 0ull, b->rw ? "rw" : "ro"); } else if (b->kind == NVN_RB_SBUFU) { // B: a storage buffer whose GPU address the game keeps in its own uniform buffer (Monster Hunter Rise's compute shaders: c3 + 0x38); read like a bindless handle uint64_t addr; ubo_read(nvn_stage, reg, b->off, 8, &addr); void *mb; uint64_t mo, gpu; - if (addr && mtl_gpu_addr(addr, 1, &mb, &mo, &gpu)) { use_rec("buf", addr, 1, 0, NULL); slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); } + if (addr && mtl_gpu_addr(addr, 1, &mb, &mo, &gpu)) { slots[aid] = gpu; USE((__bridge id<MTLBuffer>)mb, b->rw ? MTLResourceUsageRead | MTLResourceUsageWrite : MTLResourceUsageRead); } else { slots[aid] = dummy_buf_gpu(); USE(dummy_buf(), MTLResourceUsageRead); } if (bl) PD("B %llu st%d sbufu reg%d+%#x addr=%#llx %s", g_stat_pass, nvn_stage, reg, b->off, (unsigned long long)addr, b->rw ? "rw" : "ro"); } else if (b->kind == NVN_RB_C1) { // B: the shader's own constant segment, built when the shader is fetched @@ -5133,121 +5035,6 @@ static int fz_p2_draw(NvProg *pr, const p_draw *dw, const p_draw_idx *di, uint8_ if (fz_check_on()) fz_snap_take(dw, di); g_fz_draws++; return 1; } -static void bloomq_reset(void) { memset(&g_bloomq, 0, sizeof g_bloomq); } -static int bloomq_enabled(void) { - if (g_bloom_q < 0) { const char *e = getenv("DM_NVN_BLOOM_Q"); g_bloom_q = !e || strcmp(e, "0"); } - return g_bloom_q; -} -static int bloomq_target(uint64_t *ser, int *lv, int *sl) { - if (!R.has_rt || R.rt.n != 1 || !R.rt.color[0]) return 0; - rt_sub(&R.rt, 0, ser, lv, sl); return *ser != 0; -} -static int bloomq_make_pipeline(MTLPixelFormat fmt) { - if (g_bloomq_pso && g_bloomq_fmt == fmt) return 1; - static const char *src = - "#include <metal_stdlib>\nusing namespace metal;\n" - "struct V { float4 p [[position]]; float2 uv; };\n" - "vertex V vs(uint id [[vertex_id]]) { float2 uv=float2((id<<1)&2,id&2); V o; o.p=float4(uv*float2(2,-2)+float2(-1,1),0,1); o.uv=uv; return o; }\n" - "struct P { float threshold; float coeff_g; float coeff_b; float dm; float sh; };\n" - "fragment float4 fs(V in [[stage_in]], texture2d<float> t [[texture(0)]], sampler s [[sampler(0)]], constant P& p [[buffer(0)]]) { float2 d=float2(1.0/p.dm,1.0/p.sh); float3 c=0; for (int y=-1;y<=1;y+=2) for (int x=-1;x<=1;x+=2) { float3 v=t.sample(s,in.uv+float2(x,y)*d).rgb; float l=v.r*0.2126+v.g*p.coeff_g+v.b*p.coeff_b; if(l>=p.threshold) c+=v/(1.0+l); } return float4(c*0.25,1); }\n"; - NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(src) options:nil error:&e]; - if (!lib) { MLOG("[RED] bloom 1/4 shader compile failed: %s", e.localizedDescription.UTF8String); return 0; } - MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new]; pd.vertexFunction = [lib newFunctionWithName:@"vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"fs"]; pd.colorAttachments[0].pixelFormat = fmt; - g_bloomq_pso = [g_dev newRenderPipelineStateWithDescriptor:pd error:&e]; - if (!g_bloomq_pso) { MLOG("[RED] bloom 1/4 pipeline failed: %s", e.localizedDescription.UTF8String); return 0; } - g_bloomq_fmt = fmt; - MTLSamplerDescriptor *sd = [MTLSamplerDescriptor new]; sd.minFilter = sd.magFilter = MTLSamplerMinMagFilterLinear; sd.sAddressMode = sd.tAddressMode = MTLSamplerAddressModeClampToEdge; sd.supportArgumentBuffers = YES; - g_bloomq_samp = [g_dev newSamplerStateWithDescriptor:sd]; return g_bloomq_samp != nil; -} -static int bloomq_capture(void) { - uint64_t ser; int lv, sl; if (!bloomq_target(&ser, &lv, &sl) || !g_bloomq.s_tex) return 0; - id<MTLTexture> base = tex_mtl(R.rt.color[0]), src = tex_mtl(g_bloomq.s_tex); if (!base || !src) return 0; - id<MTLTexture> dst = base; if (lv || sl) { static id<MTLTexture> vb; static int vl, vs; if (vb != base || vl != lv || vs != sl) { g_bloomq_view = [base newTextureViewWithPixelFormat:base.pixelFormat textureType:base.textureType levels:NSMakeRange(lv, 1) slices:NSMakeRange(sl, 1)]; if (g_bloomq_view) res_add(g_bloomq_view); vb = base; vl = lv; vs = sl; } dst = g_bloomq_view; } - if (!dst || !bloomq_make_pipeline(dst.pixelFormat)) return 0; - rp_end(); - MTL4RenderPassDescriptor *rp = [MTL4RenderPassDescriptor new]; rp.colorAttachments[0].texture = dst; rp.colorAttachments[0].loadAction = MTLLoadActionDontCare; rp.colorAttachments[0].storeAction = MTLStoreActionStore; - id<MTL4RenderCommandEncoder> en = [g_cb renderCommandEncoderWithDescriptor:rp]; if (!en) return 0; - [en barrierAfterQueueStages:MTLStageAll beforeStages:MTLStageAll visibilityOptions:MTL4VisibilityOptionDevice]; [en setArgumentTable:g_bloomq_args atStages:MTLRenderStageFragment]; - [g_bloomq_args setTexture:src.gpuResourceID atIndex:0]; [g_bloomq_args setSamplerState:g_bloomq_samp.gpuResourceID atIndex:0]; - float *p = g_bloomq_const[g_bloomq_i].contents; p[0] = g_bloomq.threshold; p[1] = g_bloomq.coeff_g; p[2] = g_bloomq.coeff_b; p[3] = src.width; p[4] = src.height; [g_bloomq_args setAddress:g_bloomq_const[g_bloomq_i].gpuAddress atIndex:0]; - [en setViewport:(MTLViewport){0,0,dst.width,dst.height,0,1}]; [en setRenderPipelineState:g_bloomq_pso]; [en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; m4_end(en); - g_bloomq_i = (g_bloomq_i + 1) % 3; args_dirty_all(); g_pso_dirty = g_dss_dirty = 1; return 1; -} -// ---------- Drag-follow image for shop items: items on a vendor's shelf have no "picked up" state, so during a drag the replay layer takes "the draw that draws this item's icon", translates it to the finger at the end of the same pass and draws it once more ---------- -// In the UI pass each rectangle is one 4-vertex draw (20-byte vertices: position 3 floats + texture coordinates 2 floats; the position goes to clip coordinates through the matrix in vertex constant 0). -// The front end (front_ios.m) gives the point where the finger pressed, the shelf grid and the center of the drag-follow image (normalized, top-left origin); here, in the UI pass, find the rectangle that "covers this point, has all four edges on grid lines, and has the smallest area" = that item -// (identifying by the point alone gets it wrong: about 0.1 seconds after the press the game draws a one-cell-sized rectangle centered on the press point, not on grid lines -- on a real device it was twice taken for the item) -// (does not depend on the game's selected state: selection has to wait about 0.1 seconds for the controller cursor to land on the item, and a quick drag that starts on press cannot wait for that). The same rectangle has two draws, cell background and icon: -// the cell background uses a common texture (many rectangles in this pass use it), while each item has its own icon texture -- take the draw with the texture "used the fewest times in this pass". The recognized rectangle is reported back to the front end (used to cover the original shelf position with a dark color). -// Vertical position and height of the UI rectangle in the real picture (both as a fraction of the picture height: y = 0 touches the top, h = 1 is the whole picture); registered by the front end and read fresh on every draw (changes when switching between docked / handheld mode). -// The game draws the UI into the upper h portion of the UI texture (the viewport is the whole texture, and the projection matrix carries the scaling). -static void (*g_ui_rect_fn)(double *y, double *h); -void mtl_set_ui_rect_fn(void (*fn)(double *y, double *h)) { g_ui_rect_fn = fn; } -static double ui_hfrac(void) { double y = 0, h = 1; if (g_ui_rect_fn) g_ui_rect_fn(&y, &h); return h > 0 && h < 1 ? h : 1; } -static _Atomic int g_ghost_on; static _Atomic uint64_t g_ghost_pt, g_ghost_at, g_ghost_found, g_ghost_grid; -void mtl_ui_ghost(int on, float px, float py, const float grid[4], float cx, float cy) { - if (on) { uint64_t gv = 0; for (int i = 0; i < 4; i++) gv |= (uint64_t)(uint16_t)(fminf(fmaxf(grid[i], 0), 1) * 65535 + .5f) << (16 * i); atomic_store(&g_ghost_grid, gv); - atomic_store(&g_ghost_pt, (uint64_t)(uint32_t)lroundf(fminf(fmaxf(px, 0), 1) * 65535) | (uint64_t)(uint32_t)lroundf(fminf(fmaxf(py, 0), 1) * 65535) << 32); - atomic_store(&g_ghost_at, (uint64_t)(uint32_t)(int32_t)lroundf(cx * 65535) | (uint64_t)(uint32_t)(int32_t)lroundf(cy * 65535) << 32); } - else atomic_store(&g_ghost_found, 0); - atomic_store(&g_ghost_on, on); -} -int mtl_ui_ghost_rect(float r[4]) { uint64_t v = atomic_load(&g_ghost_found); if (!v) return 0; for (int i = 0; i < 4; i++) r[i] = (float)((v >> (16 * i)) & 0xffff) / 65535.f; return 1; } -static struct { uint64_t pass; int n, nt, busy; float area, r[4]; struct { void *S_; p_draw dw; uint64_t tex; } c[4]; uint64_t tid[128]; uint16_t tn[128]; } g_gh; -static uint64_t ghost_tex(void) { // The texture actually sampled by this draw's fragment shader (slot taken from shader reflection; the other slots in S.tex are left over from earlier draws) - __unsafe_unretained NvProg *pr = S.prog ? prog_get(S.prog) : nil; NvShader *f = pr ? pr->sh[1] : nil; if (!f) return 0; - for (int i = 0; i < f->rf.nb; i++) if ((f->rf.b[i].kind == NVN_RB_TEX || f->rf.b[i].kind == NVN_RB_SAMP) && f->rf.b[i].reg < NTEX && S.tex[1][f->rf.b[i].reg]) return S.tex[1][f->rf.b[i].reg]; - return 0; } -// Whether the current UI draw is "untextured and exactly covering the whole UI rectangle" (the UI rectangle occupies the upper hf of the texture) -static int ui_cover_all(double hf) { - if (S.vbuf[0].size != 80 || ghost_tex()) return 0; - const p_bind_buf *u = &S.ubo[0][0]; void *vb, *mb; uint64_t vo, mo; const uint8_t *vp_, *mp_; - if (!u->addr || u->size < 64 || pend_hits(u->addr, 64) || pend_hits(S.vbuf[0].address, 80)) return 0; - if (!mtl_addr(S.vbuf[0].address, 80, &vb, &vo) || !(vp_ = [(__bridge id<MTLBuffer>)vb contents]) || !mtl_addr(u->addr, 64, &mb, &mo) || !(mp_ = [(__bridge id<MTLBuffer>)mb contents])) return 0; - float m[16], x0 = 9, y0 = 9, x1 = -9, y1 = -9; memcpy(m, mp_ + mo, 64); - for (int i = 0; i < 4; i++) { float f[2]; memcpy(f, vp_ + vo + 20 * i, 8); float nx = (m[0] * f[0] + m[4] * f[1] + m[12] + 1) / 2, ny = (m[1] * f[0] + m[5] * f[1] + m[13] + 1) / 2; - x0 = fminf(x0, nx); x1 = fmaxf(x1, nx); y0 = fminf(y0, ny); y1 = fmaxf(y1, ny); } - return x0 < 0.002f && x1 > 0.998f && y0 < 0.002f && fabsf(y1 - (float)hf) < 0.002f; -} -static void ghost_note(const p_draw *dw) { - if (g_gh.busy || dw->count != 4 || S.nattr != 2 || S.stream[0].stride != 20 || S.vbuf[0].size != 80) return; - if (g_gh.pass != g_stat_pass) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = g_gh.nt = 0; g_gh.pass = g_stat_pass; } - uint64_t t = ghost_tex(); int k = 0; for (; k < g_gh.nt; k++) if (g_gh.tid[k] == t) break; - if (k == g_gh.nt && g_gh.nt < 128) { g_gh.tid[k] = t; g_gh.tn[k] = 0; g_gh.nt++; } if (k < g_gh.nt) g_gh.tn[k]++; - const p_bind_buf *u = &S.ubo[0][0]; void *vb, *mb; uint64_t vo, mo; const uint8_t *vp_, *mp_; - if (!u->addr || u->size < 64 || pend_hits(u->addr, 64) || pend_hits(S.vbuf[0].address, 80)) return; - if (!mtl_addr(S.vbuf[0].address, 80, &vb, &vo) || !(vp_ = [(__bridge id<MTLBuffer>)vb contents]) || !mtl_addr(u->addr, 64, &mb, &mo) || !(mp_ = [(__bridge id<MTLBuffer>)mb contents])) return; - float m[16], x0 = 9, y0 = 9, x1 = -9, y1 = -9; memcpy(m, mp_ + mo, 64); - for (int i = 0; i < 4; i++) { float f[2]; memcpy(f, vp_ + vo + 20 * i, 8); float nx = (m[0] * f[0] + m[4] * f[1] + m[12] + 1) / 2, ny = (m[1] * f[0] + m[5] * f[1] + m[13] + 1) / 2; - x0 = fminf(x0, nx); x1 = fmaxf(x1, nx); y0 = fminf(y0, ny); y1 = fmaxf(y1, ny); } - { float hf = (float)ui_hfrac(); y0 /= hf; y1 /= hf; } // The points given by the front end are normalized coordinates within the UI rectangle; when the UI occupies only the upper hf portion of the texture, convert to the same convention (unchanged when hf = 1) - uint64_t pv = atomic_load(&g_ghost_pt); float qx = (float)(uint32_t)pv / 65535.f, qy = (float)(pv >> 32) / 65535.f; - if (qx < x0 || qx > x1 || qy < y0 || qy > y1) return; - uint64_t gv = atomic_load(&g_ghost_grid); float gx = (float)(gv & 0xffff) / 65535.f, gy = (float)((gv >> 16) & 0xffff) / 65535.f, gw = (float)((gv >> 32) & 0xffff) / 65535.f, gh = (float)(gv >> 48) / 65535.f; - if (gw <= 0 || gh <= 0) return; - float w = x1 - x0, h = y1 - y0, area = w * h, c0 = (x0 - gx) / gw, r0 = (y0 - gy) / gh, cn = w / gw, rn = h / gh; // Which cell the top-left corner is in, and how many cells it occupies - if (fabsf(c0 - roundf(c0)) > 0.15f || fabsf(r0 - roundf(r0)) > 0.15f || fabsf(cn - roundf(cn)) > 0.15f || fabsf(rn - roundf(rn)) > 0.15f || cn < 0.85f || rn < 0.85f || cn > 2.15f || rn > 4.15f) return; // One item is at most 2x4 cells - if (g_gh.n && area > g_gh.area * 1.03f) return; // A smaller one already exists - if (g_gh.n && area < g_gh.area * 0.97f) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0; } // Found a smaller one: the earlier ones are invalidated - if (g_gh.n == 4) { free(g_gh.c[0].S_); memmove(&g_gh.c[0], &g_gh.c[1], 3 * sizeof g_gh.c[0]); g_gh.n = 3; } - g_gh.area = area; g_gh.r[0] = x0; g_gh.r[1] = y0; g_gh.r[2] = x1; g_gh.r[3] = y1; - typeof(g_gh.c[0]) *c = &g_gh.c[g_gh.n++]; c->S_ = malloc(sizeof S); memcpy(c->S_, &S, sizeof S); c->dw = *dw; c->tex = t; -} -static void ghost_flush(void) { // Before the render pass ends: translate the recognized icon draw to the finger and draw it once more (moves the viewport, does not change vertices; the scissor is opened up) - if (g_gh.busy || !g_gh.n) return; - if (g_gh.pass != g_stat_pass || !atomic_load(&g_ghost_on)) { for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0; return; } - int best = -1, bn = 1 << 30; for (int i = 0; i < g_gh.n; i++) { int cnt = 0; for (int k = 0; k < g_gh.nt; k++) if (g_gh.tid[k] == g_gh.c[i].tex) cnt = g_gh.tn[k]; if (cnt < bn) { bn = cnt; best = i; } } - typeof(g_gh.c[0]) *c = &g_gh.c[best]; uint64_t at = atomic_load(&g_ghost_at); float ax = (float)(int32_t)(uint32_t)at / 65535.f, ay = (float)(int32_t)(uint32_t)(at >> 32) / 65535.f; - float W = (float)g_pass.tw, H = (float)g_pass.th, ccx = (g_gh.r[0] + g_gh.r[2]) / 2, ccy = (g_gh.r[1] + g_gh.r[3]) / 2; - { uint64_t v = 0; for (int i = 0; i < 4; i++) v |= (uint64_t)(uint16_t)(fminf(fmaxf(g_gh.r[i], 0), 1) * 65535 + .5f) << (16 * i); atomic_store(&g_ghost_found, v ? v : 1); } - g_gh.busy = 1; typeof(S) save; memcpy(&save, &S, sizeof S); p_scissor sc = R.sc; int has_sc = R.has_sc; - memcpy(&S, c->S_, sizeof S); if (!S.has_vp) { S.vp[0][0] = S.vp[0][1] = 0; S.vp[0][2] = W; S.vp[0][3] = H; S.has_vp = 1; } - S.vp[0][0] += (ax - ccx) * W; S.vp[0][1] += (ay - ccy) * H * (float)ui_hfrac(); R.has_sc = 0; - args_dirty_all(); g_pso_dirty = g_dss_dirty = 1; draw(&c->dw, NULL); - { static int once; if (!once++) MLOG("drag ghost image: first case pass %llu, %d candidates, took the one whose texture is used %d times in this pass", g_stat_pass, g_gh.n, bn); } - memcpy(&S, &save, sizeof S); args_dirty_all(); R.sc = sc; R.has_sc = has_sc; g_pso_dirty = g_dss_dirty = 1; g_gh.busy = 0; - for (int i = 0; i < g_gh.n; i++) free(g_gh.c[i].S_); g_gh.n = 0; -} // Diagnostic DM_NVN_KEYDUMP=<fragment shader key prefix>[:<count, default 6>[:<frame interval>]]: watch only this class of draws (for real devices; no frame number to guess). The first N matching draws force PASSDUMP's per-binding lines on (K / B / d: // textures with format / view / channel swizzle, samplers with LOD clamps), plus [kd] lines: vertex attribute formats, each vertex stream (first 4 vertices of per-vertex streams, all instances of per-instance streams) and every value of each uniform buffer of both stages. // Run the same switch once on the Mac and once on the device and compare item by item to see which input differs on the device (Super Mario 3D World + Bowser's Fury's magenta squares in 1-1: particle 5e4de7561c670fee, iPhone only, not reproducible on the Mac) @@ -5686,10 +5473,6 @@ static void draw(const p_draw *dw, const p_draw_idx *di) { if (!S.prog) { RED_ONCE(18, "no program bound before the draw"); PD("Z %llu draw skipped: no program bound", g_stat_pass + !g_enc); return; } __unsafe_unretained NvProg *pr = prog_get(S.prog); if (g_nubd && g_enc && ubd_conflict(pr, dw, di)) { g_ubd_conflicts++; rp_end(); } // land the deferred inline updates before this draw (see ubd_conflict) - if (g_ruler_truth == 0) { // Ruler negative control (DM_NVN_RULER_NEG=<fragment key>): in the mode under test drop this shader's draws, and the ruler must report non-equivalence - static const char *neg; static int ng = -1; if (ng < 0) { neg = getenv("DM_NVN_RULER_NEG"); ng = neg && *neg; } - if (ng && pr) for (int st = 0; st < 5; st++) { __unsafe_unretained NvShader *sh = pr->sh[st]; if (sh && sh->stage == 1 && !strncmp(sh->key, neg, strlen(neg))) { static int once; if (!once++) MLOG("DM_NVN_RULER_NEG: the mode under test drops the draws of shader %s", sh->key); return; } } - } if (zskip_can_draw(pr)) { // Zero-source skip: the output is always 0, writing 0 over 0, so this draw changes no result (see zskip_can_draw) g_zskip_count++; { static int once; if (!once++) MLOG("zero-source skip: first case pass %llu", g_stat_pass + 1); } PD("N %llu zero-source draw skipped", g_stat_pass + 1); return; } @@ -5711,51 +5494,6 @@ static void draw(const p_draw *dw, const p_draw_idx *di) { { static int small = -1; if (small < 0) { const char *e = getenv("DM_NVN_SKIP_SMALL"); small = e ? atoi(e) : 0; if (small) MLOG("DM_NVN_SKIP_SMALL: dropping draws whose target width is ≤ %d (diagnostic; the picture is wrong)", small); } if (small > 0 && R.has_rt && !g_defer_replaying) { int k = R.rt.n && R.rt.color[0] ? 0 : R.rt.depth ? 8 : -1; if (k >= 0) { uint64_t s_; int lv, sl; rt_sub(&R.rt, k, &s_, &lv, &sl); const tex_t *t = k == 8 ? R.rt.depth : R.rt.color[0]; if ((int)MAX(t->d.width >> lv, 1u) <= small) return; } } } - // Experimental switch DM_NVN_NOFXAA=1 (off by default; keep or drop decided by the visual ruler + looking at the picture on a real machine): skips the FXAA full-screen draw, and the later sharpening / upscale reads FXAA's input instead (the tone mapping result) - { static int nofxaa = -1; static uint64_t fx_from, fx_to; if (nofxaa < 0) { const char *e = getenv("DM_NVN_NOFXAA"); nofxaa = e && !strcmp(e, "1"); if (nofxaa) MLOG("DM_NVN_NOFXAA=1: skipping FXAA"); } - if (nofxaa && tbdr_on() && !g_defer_replaying && pr->sh[1]) { NvShader *f = pr->sh[1]; - if (!strncmp(f->key, "ebcfcd1fa0a77dbc", 16) && !g_enc && R.rt.n == 1 && R.rt.color[0] && !R.clear_c[0]) { - uint64_t h = 0; for (int i = 0; i < f->rf.nb && !h; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX) h = S.tex[1][f->rf.b[i].reg]; - if (h) { fx_from = R.rt.color[0]->serial; fx_to = h; PD("N %llu FXAA skipped: readers of ser=%llu read ser=%llu instead", g_stat_pass, (unsigned long long)fx_from, (unsigned long long)h_ser(h)); return; } } - else if (fx_from && !strncmp(f->key, "52aac1f2306b8fc5", 16)) { for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == fx_from) S.tex[1][r] = fx_to; fx_from = 0; } } } - { static uint64_t bloomq_frame; if (bloomq_frame != atomic_load(&g_cur_frame)) { bloomq_frame = atomic_load(&g_cur_frame); bloomq_reset(); } - if (bloomq_enabled() && tbdr_on() && !g_defer_replaying && pr->sh[1]) { - NvShader *f = pr->sh[1]; uint64_t tgt = 0; int lv, sl; - if (!strncmp(f->key, "2938a5b71b8e21b2", 16)) { - if (g_bloomq.state) bloomq_reset(); - if (bloomq_target(&tgt, &lv, &sl) && R.rt.color[0]) { - uint64_t h = 0; const tex_t *st = NULL; int cbuf = 0; - for (int i = 0; i < f->rf.nb; i++) { nvn_rbind *b = &f->rf.b[i]; - if (b->kind == NVN_RB_TEX && b->reg < NTEX && !h) { h = S.tex[1][b->reg]; int tid = (int)(h & 0xfffff); if (tid > 0 && tid < g_ntreg && g_treg[tid].ok) st = &g_treg[tid].t; } - if (b->kind == NVN_RB_SAMPT && b->reg < NTEX) g_bloomq.samp_h = S.tex[1][b->reg] >> 20; - if (b->kind == NVN_RB_SAMP && b->reg < 128) g_bloomq.samp_h = S.samp[1][b->reg] >> 20; - if (b->kind == NVN_RB_CBUF && !cbuf) cbuf = ubo_float(1, (uint16_t)(b->reg << 8), &g_bloomq.threshold); // c7[0].x - } - if (h && st && cbuf && f->rf.c1_len >= sizeof(float) * 2) { - float c1[2]; memcpy(c1, f->rf.c1, sizeof c1); g_bloomq.s_tex = st; g_bloomq.s_h = h; g_bloomq.s_ser = st->serial; g_bloomq.b0_ser = tgt; g_bloomq.coeff_g = c1[0]; g_bloomq.coeff_b = c1[1]; g_bloomq.state = 1; - { static int once; if (!once++) MLOG("bloom 1/4 start: active (S ser=%llu B0 ser=%llu)", (unsigned long long)g_bloomq.s_ser, (unsigned long long)g_bloomq.b0_ser); } PD("A %llu bloom 1/4 start S=%llu B0=%llu", g_stat_pass, (unsigned long long)g_bloomq.s_ser, (unsigned long long)g_bloomq.b0_ser); R.clear_c[0] = 0; // The whole B0 level is no longer needed: drop the pending clear too, otherwise a clear-only render pass would be opened just for it - return; - } - } - } else if (g_bloomq.state == 1 && !strncmp(f->key, "019ce2f82edbda84", 16)) { - uint64_t src = 0; for (int i = 0; i < f->rf.nb; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX) { src = h_ser(S.tex[1][f->rf.b[i].reg]); break; } - if (src == g_bloomq.b0_ser && bloomq_target(&tgt, &lv, &sl) && bloomq_capture()) { R.clear_c[0] = 0; g_bloomq.b1_ser = tgt; g_bloomq.state = 2; PD("B %llu bloom 1/4 native capture B0=%llu B1=%llu", g_stat_pass, (unsigned long long)g_bloomq.b0_ser, (unsigned long long)g_bloomq.b1_ser); return; } - RED_ONCE(62, "bloom 1/4 start: the capture was skipped but the native capture did not draw; this frame lacks bloom"); bloomq_reset(); - } else if (g_bloomq.state == 2 && !strncmp(f->key, "019ce2f82edbda84", 16)) { - for (int i = 0; i < f->rf.nb; i++) if (f->rf.b[i].kind == NVN_RB_TEX && f->rf.b[i].reg < NTEX && h_ser(S.tex[1][f->rf.b[i].reg]) == g_bloomq.b1_ser) { g_bloomq.b1_h = S.tex[1][f->rf.b[i].reg]; break; } - } else if (g_bloomq.state == 2 && !strncmp(f->key, "cc264583f2000c81", 16)) { - if (bloomq_target(&tgt, &lv, &sl) && tgt == g_bloomq.b0_ser) { g_bloomq.state = 3; PD("C %llu bloom 1/4 skips the B0 upsample", g_stat_pass); return; } - } else if (g_bloomq.state == 3 && !strncmp(f->key, "2e2d6700a2569795", 16)) { - if (g_bloomq.b1_h) { for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == g_bloomq.b0_ser) S.tex[1][r] = g_bloomq.b1_h; PD("D %llu bloom 1/4 composite reads B1 instead", g_stat_pass); g_bloomq_last_b0 = g_bloomq.b0_ser; g_bloomq_last_b1h = g_bloomq.b1_h; g_bloomq_last_b1s = g_bloomq.b1_ser; bloomq_reset(); } - else { RED_ONCE(63, "bloom 1/4 state lacks the B1 handle"); bloomq_reset(); } - } else if (!g_bloomq.state && g_bloomq_last_b1h && h_ser(g_bloomq_last_b1h) == g_bloomq_last_b1s && !strncmp(f->key, "2e2d6700a2569795", 16)) { // The B1 handle still points to the same texture (not recognized after a rebuild on resolution change) - // The bloom overlay is reached without the bloom chain having run this frame (the extrapolated frame of runtime extrapolation skipped it: outside the region the input 469 is the previous frame's LDR, so it cannot run): redirect the read to B1 as last time, overlaying the previous frame's bloom. - // Without the redirect it would read B0 -- with bloom 1/4 on, B0's upsample is always skipped and never written, so what is read is uninitialized video memory (on M4 Pro the units in the region turn entirely green; elsewhere it is mostly 0, i.e. missing bloom) - int hit = 0; for (int r = 0; r < NTEX; r++) if (S.tex[1][r] && h_ser(S.tex[1][r]) == g_bloomq_last_b0) { S.tex[1][r] = g_bloomq_last_b1h; hit = 1; } - if (hit) { PD("D %llu bloom chain did not run: composite reuses the previous frame's B1", g_stat_pass); static uint64_t n; if (n++ % 600 == 0) MLOG("bloom 1/4: a frame without the bloom chain; the composite reuses the previous frame's B1 (#%llu)", (unsigned long long)n); } - } else if (g_bloomq.state) { RED_ONCE(61, "bloom 1/4 start: another draw came in the middle of the chain; this frame's bloom is incomplete"); bloomq_reset(); } - } - } if (S.poly.cull == NVN_FACE_FRONT_AND_BACK) { PD("Z %llu draw skipped (before the pass begins): both faces culled", g_stat_pass + !g_enc); return; } // The continued render pass may have been opened with fewer depth / stencil attachments (open modes 2 / 3 / 4): if the plane this draw needs is not attached, split before it (on reopen, attach per the new lookahead) if (g_rt_merged && g_enc_mode != 0 && (((S.ds.dt || S.ds.dw) && !g_pass.hd) || (S.ds.st && !g_pass.hs))) { @@ -5779,12 +5517,10 @@ static void draw(const p_draw *dw, const p_draw_idx *di) { if (!g_enc) rr_begin(); #endif int defer_q_draw = !g_enc && !g_defer_replaying && g_defer_natq > 0; // The draw that opens Q's render pass: after the render pass is opened as usual through hoist_try / fuse_try, replay P first and then draw this one - if (g_frz_state) goto frz_plain; // Full frame / frozen frame of world freeze: no hoisting, no deferral, no merging (the pass-skipping state machine must see every set-target) if (!g_enc && tbdr_on()) hoist_try(); // First hoist and execute the pure-clear render passes sandwiched between this render pass and its continuation (before rp_begin_ex, not occupying this render pass) // The deferral decision comes after hoist_try: hoisted pure-clear render passes are skipped in the lookahead, and only then can P's same-target continuation segment be recognized if (!g_enc && tbdr_on() && !defer_has_in_p() && !g_defer_replaying && !g_fz.active && !g_fz_checking && g_exec_next) { defer_try(dw, di); if (g_defer_cur->active && g_defer_cur->in_p) return; } if (!g_enc && tbdr_on() && !g_fz_checking) fuse_try(); // Readable tile render pass merge is on by default; when the structure / same-pixel proof does not hold it still falls back to a normal render pass, and it is not re-entered during the CHECK redraw -frz_plain:; int need = g_enc || !tbdr_on() ? 3 : pass_ds_need(), mode = defer_q_draw ? 0 : need == 0 ? 2 : need == 2 ? 3 : need == 1 ? 4 : 0; // 2 attach neither, 3 attach stencil only, 4 attach depth only int skip_d = mode != 0; if (!g_enc) { g_fd_dw = dw; g_fd_di = di; } @@ -5810,26 +5546,16 @@ frz_plain:; unsigned df0 = (unsigned)S.ds.df; if (ovr) S.ds.df = 8; cur_ds = dss_get(g_pass.hd, g_pass.hs); S.ds.df = df0; cur_ds_pass = g_pass.serial; g_dss_dirty = 0; } else if (g_dss_dirty || cur_ds_pass != g_pass.serial) { cur_ds = dss_get(g_pass.hd, g_pass.hs); cur_ds_pass = g_pass.serial; g_dss_dirty = 0; } if (cur_ds != g_enc_dss) { enc_r_dss(en, cur_ds); g_enc_dss = cur_ds; } - { uint32_t sr = g_rt_ovr && g_pass.hs ? (g_rt_ovr == 3 ? 0x40 : 0) : S.st_ref[0]; // enc_r_stencil_ref sends the front value only + { uint32_t sr = S.st_ref[0]; // enc_r_stencil_ref sends the front value only if (!(g_ed.ok & ED_SREF) || g_ed.sref != sr) { enc_r_stencil_ref(en, sr, sr); g_ed.sref = sr; g_ed.ok |= ED_SREF; } } // Viewport / scissor (clamped within the target size) NSUInteger tw = g_pass.tw, th = g_pass.th; - // When the UI rectangle does not touch the top edge of the picture (iPad true full screen: the UI is laid out at 16:9 at the bottom of the picture, see dm/host/dm_uiscreen.c), UI draws are shifted down as a whole: a vertical offset is added to the projection matrix (ui_mat_patch), and the scissor moves down with it. - // A UI draw = 4 vertices, 2 attributes, 20-byte vertices, in a pass of its own and composited at end of frame (measured: startup loading, character select, character creation, entering-game loading, in-game, open panel, and full-screen map never mix in other draws). - // When the matrix cannot be read this time (there are still unlanded writes in this submit), fall back to moving the viewport: same position, except that widgets lifted to the top get clipped by the viewport's top edge this one time. - // A scissor starting from the top edge (y <= 0) does not have its top edge moved down: when the host lifts top-anchored widgets (minimap, area name etc.) back to the real top of the picture, they are drawn above the UI rectangle and must not be clipped. - // Exception: the untextured block that exactly covers the whole UI rectangle (the dimming when a panel opens, the black curtain of a transition) is changed to cover the whole picture -- the viewport is scaled vertically by 1/h and not moved down, otherwise the 3D outside the UI rectangle is left with a light/dark dividing line. - // When nobody registered the entry or the UI rectangle is the whole picture (iPhone, Mac default), vo = so = 0, vs = 1, the matrix is not changed, and what follows is word-for-word the same as before. - long vo = 0, so = 0; double vs = 1, moff = 0; - if (g_ui_rect_fn && dw && dw->count == 4 && S.nattr == 2 && S.stream[0].stride == 20) { double uy = 0, uh = 1; g_ui_rect_fn(&uy, &uh); - if (uh > 0 && uh < 1) { if (ui_cover_all(uh)) vs = 1 / uh; else if (uy > 0) { so = lround(uy * th); if (ui_mat_ok()) moff = 2 * uy; else { vo = so; static int n; if (n++ < 4) MLOG("UI shift down: the projection matrix could not be read this time; moving the viewport instead (pass %llu)", g_stat_pass); } } } } - if (moff != g_ui_moff) { g_ui_moff = moff; g_args_dirty[0] = 1; } - { MTLViewport v = S.has_vp ? (MTLViewport){S.vp[0][0], S.vp[0][1] + vo, S.vp[0][2], S.vp[0][3] * vs, S.dr[0], S.dr[1]} : (MTLViewport){0, vo, tw, th * vs, 0, 1}; + { MTLViewport v = S.has_vp ? (MTLViewport){S.vp[0][0], S.vp[0][1], S.vp[0][2], S.vp[0][3], S.dr[0], S.dr[1]} : (MTLViewport){0, 0, tw, th, 0, 1}; if (atomic_load_explicit(&g_origin_ll, memory_order_relaxed)) { v.originY += v.height; v.height = -v.height; } // lower-left origin (see g_origin_ll) if (!(g_ed.ok & ED_VP) || memcmp(&g_ed.vp, &v, sizeof v)) { enc_r_viewport(en, v); g_ed.vp = v; g_ed.ok |= ED_VP; } } MTLScissorRect sc = {0, 0, tw, th}; if (R.has_sc) { - long x0 = R.sc.x < 0 ? 0 : R.sc.x, y0 = R.sc.y <= 0 ? 0 : R.sc.y + so, x1 = (long)R.sc.x + R.sc.w, y1 = vs != 1 ? (long)th : (long)R.sc.y + R.sc.h + so; + long x0 = R.sc.x < 0 ? 0 : R.sc.x, y0 = R.sc.y <= 0 ? 0 : R.sc.y, x1 = (long)R.sc.x + R.sc.w, y1 = (long)R.sc.y + R.sc.h; if (x1 > (long)tw) x1 = tw; if (y1 > (long)th) y1 = th; if (x1 <= x0 || y1 <= y0) { PD("Z %llu draw skipped: empty scissor (%d,%d %dx%d, target %lux%lu)", g_stat_pass, R.sc.x, R.sc.y, R.sc.w, R.sc.h, (unsigned long)tw, (unsigned long)th); return; } sc = (MTLScissorRect){(NSUInteger)x0, (NSUInteger)y0, (NSUInteger)(x1 - x0), (NSUInteger)(y1 - y0)}; @@ -5850,7 +5576,7 @@ frz_plain:; if (!S.vbuf[s_].address) continue; void *mb; uint64_t mo; uint64_t gpu; - if (mtl_gpu_addr(S.vbuf[s_].address, 1, &mb, &mo, &gpu)) { enc_r_vbuf(en, VB_SLOT(s_), gpu); use_rec("buf", S.vbuf[s_].address, S.vbuf[s_].size, 0, NULL); } + if (mtl_gpu_addr(S.vbuf[s_].address, 1, &mb, &mo, &gpu)) enc_r_vbuf(en, VB_SLOT(s_), gpu); } static id<MTLBuffer> zero; if (!zero) { zero = [g_dev newBufferWithLength:64 options:MTLResourceStorageModeShared]; res_add(zero); } static uint64_t zero_gpu; if (!zero_gpu) zero_gpu = zero.gpuAddress; @@ -5881,9 +5607,6 @@ frz_plain:; } } g_stat_draw++; - { static int tg; if (!tg) { tg = 1; const char *e = getenv("DM_TEST_GHOST"); float v[8]; // Test switch DM_TEST_GHOST=px,py,gx,gy,gw,gh,cx,cy (normalized; press point, grid, center of the drag-follow image): turns on the drag-follow image directly without going through the front end (for Mac host screenshot verification) - if (e && sscanf(e, "%f,%f,%f,%f,%f,%f,%f,%f", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7]) == 8) mtl_ui_ghost(1, v[0], v[1], &v[2], v[6], v[7]); } } - if (dw && atomic_load(&g_ghost_on)) ghost_note(dw); if (enc_label_on() && !g_el_draws++ && pr->sh[1]) { memcpy(g_el_key, pr->sh[1]->key, 8); g_el_key[8] = 0; } if (passdump()) { // Characteristics of a render pass's first draw (to judge whether the load can be omitted: full screen, no blending, writes all channels, does not depend on depth); with DM_NVN_PASSDUMP_ALL every draw is recorded static uint64_t last_pass = ~0ull; static int all = -1; if (all < 0) all = getenv("DM_NVN_PASSDUMP_ALL") != NULL; @@ -5972,7 +5695,6 @@ frz_plain:; else { uint32_t q = (t >> 1) * 4, h = t & 1; d[3 * t] = IX(q); d[3 * t + 1] = IX(q + 1 + h); d[3 * t + 2] = IX(q + 2 + h); } } #undef IX - use_rec("buf", di->ibuf, (uint64_t)di->count * isz, 0, NULL); enc_r_draw_indexed(en, mp, nt * 3, MTLIndexTypeUInt32, g_cur_arena->gpu + off, (NSUInteger)nt * 12, di->instances ? di->instances : 1, di->base_vertex, di->base_instance); if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; } return; @@ -5982,7 +5704,6 @@ frz_plain:; if (!di->count) return; const wide_ent *w = wide_wanted(di) ? wide_find(di) : NULL; if (w && w->gpu) { // written earlier in this submission: widened on the GPU before this pass (wide_run) - use_rec("buf", di->ibuf, (uint64_t)di->count, 0, NULL); enc_r_draw_indexed(en, mp, di->count, MTLIndexTypeUInt16, w->gpu, (NSUInteger)di->count * 2, di->instances ? di->instances : 1, di->base_vertex, di->base_instance); if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; } return; @@ -5993,7 +5714,6 @@ frz_plain:; id<MTLBuffer> ab; size_t off = arena_alloc(((size_t)di->count * 2 + 15) & ~(size_t)15, &ab); uint16_t *d = (uint16_t *)((uint8_t *)ab.contents + off); src += mo; for (int32_t i = 0; i < di->count; i++) d[i] = src[i]; - use_rec("buf", di->ibuf, (uint64_t)di->count, 0, NULL); enc_r_draw_indexed(en, mp, di->count, MTLIndexTypeUInt16, g_cur_arena->gpu + off, (NSUInteger)di->count * 2, di->instances ? di->instances : 1, di->base_vertex, di->base_instance); if (g_rt_range_pending) { PD("j rt_level_range pass=%llu vc=%u", g_stat_pass, (unsigned)di->count); g_rt_range_pending = 0; } return; @@ -6001,7 +5721,6 @@ frz_plain:; void *mb; uint64_t mo; uint64_t gpu; if (!mtl_gpu_addr(di->ibuf, 1, &mb, &mo, &gpu)) { RED_ONCE(22, "index buffer is in no memory pool"); return; } - use_rec("buf", di->ibuf, (uint64_t)di->count * (di->itype == 1 ? 2 : 4), 0, NULL); if (di->count) enc_r_draw_indexed(en, mp, di->count, di->itype == 1 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32, gpu, ((__bridge id<MTLBuffer>)mb).length - mo, di->instances ? di->instances : 1, di->base_vertex, di->base_instance); } @@ -6033,47 +5752,6 @@ static void warm_claim(void) { } } } -// ---------- 3D picture brightening ---------- -// Tone mapping (fragment a1b1aab66933fa8f) = PQ-encode the composited HDR x0.008, then look up a 32^3 color lookup table (recomputed per region every frame by compute shader 552f45c348a61b86, read only by tone mapping). -// Multiplying HDR by exposure k first is equivalent to replacing the table with L'(u) = L(PQ(k * PQ^-1(u))): after the dispatch in which the game finishes computing the table, make a copy and rewrite it in place accordingly (one small 32^3 dispatch); the table is recomputed every frame, so it does not get brighter with each multiplication. -// Only the 3D world is touched: UI, text and the health orbs are composited after tone mapping; highlights are compressed by PQ and do not blur into a sheet of white as a final whole-screen multiply would. The normal / readable tile merge / freeze paths all just read this table, so it takes effect in one place. -// k is given every frame by the host's dm_bright_k() (dm/host/dm_bright.c: 1 out of game, 4 for maze-type areas, 4 where the original game's picture does not follow day and night, the rest derived from the original game's ambient brightness, with smooth transitions); no rewrite when k ~= 1. -__attribute__((weak)) float dm_bright_k(void) { return 1.0f; } // The host's dm_bright.c provides the strong definition; no brightening when graphics unit tests do not link the host -static const char k_lut_msl[] = -"#include <metal_stdlib>\nusing namespace metal;\n" -"constant float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;\n" // Bit-identical to the PQ constants in the tone mapping shader -"static float3 pq(float3 v) { float3 p = pow(abs(v), m1); return pow((c1 + c2 * p) / (1.0 + c3 * p), m2); }\n" -"static float3 pq_inv(float3 u) { float3 q = pow(u, 1.0 / m2); return pow(max(q - c1, 0.0) / max(c2 - c3 * q, 1e-6), 1.0 / m1); }\n" -"kernel void lutk(texture3d<float> src [[texture(0)]], texture3d<float, access::write> dst [[texture(1)]], constant float &k [[buffer(0)]], uint3 g [[thread_position_in_grid]]) {\n" -" uint3 n = uint3(dst.get_width(), dst.get_height(), dst.get_depth()); if (any(g >= n)) return;\n" -" constexpr sampler ls(filter::linear, address::clamp_to_edge, coord::normalized);\n" -" float3 u = (float3(g) + 0.5) / float3(n); float3 u2 = clamp(pq(k * pq_inv(u)), 0.0, 1.0);\n" -" float4 b = src.sample(ls, u2), o = src.read(g);\n" // b: output after brightening by exposure k; o: this cell in the original game -" float s = smoothstep(0.65, 1.0, dot(b.rgb, float3(0.2126, 0.7152, 0.0722)));\n" // Highlight protection: after brightening, the closer to white, the more it is pulled back to the original value (snow and skylight do not blur into a sheet of white); in dark areas s = 0 and brightening proceeds as usual -" dst.write(float4(max(mix(b.rgb, o.rgb, s), o.rgb), b.a), g); }\n"; -static void bright_lut(NvShader *cs) { - static int failed; static id<MTLComputePipelineState> ps; static id<MTL4ArgumentTable> at; static id<MTLTexture> tmp; static id<MTLBuffer> kb[3]; static int ki; - if (failed || strncmp(cs->key, "552f45c348a61b86", 16)) return; - float k = dm_bright_k(); if (!(k > 1.005f)) return; // k ~= 1 (out of game / noon): the table stays as in the original game - if (k > 4.0f) k = 4.0f; - int tid = 0; for (int i = 0; i < cs->rf.nb && !tid; i++) if (cs->rf.b[i].kind == NVN_RB_IMG && cs->rf.b[i].reg < 32) tid = (int)(S.img[5][cs->rf.b[i].reg] & 0xfffff); - pthread_mutex_lock(&g_rmu); id<MTLTexture> lut = tid ? tex_by_id(tid) : nil; pthread_mutex_unlock(&g_rmu); - if (!lut || lut.textureType != MTLTextureType3D || !(lut.usage & MTLTextureUsageShaderWrite)) { static int n; if (!n++) MLOG("[RED] 3D brightening: color LUT not recognized (texture ID %d); not brightening", tid); return; } - if (!ps) { NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(k_lut_msl) options:nil error:&e]; id<MTLFunction> f = lib ? [lib newFunctionWithName:@"lutk"] : nil; - ps = f ? [g_dev newComputePipelineStateWithFunction:f error:&e] : nil; - MTL4ArgumentTableDescriptor *ad = [MTL4ArgumentTableDescriptor new]; ad.maxTextureBindCount = 2; ad.maxBufferBindCount = 1; at = [g_dev newArgumentTableWithDescriptor:ad error:&e]; - for (int i = 0; i < 3; i++) { kb[i] = [g_dev newBufferWithLength:16 options:MTLResourceStorageModeShared]; if (kb[i]) res_add(kb[i]); } - if (!ps || !at || !kb[0] || !kb[1] || !kb[2]) { failed = 1; MLOG("[RED] 3D brightening: cannot create the compute pipeline %s (back to the original brightness)", e.localizedDescription.UTF8String); return; } - MLOG("3D brightening: ready (color LUT %lux%lux%lu, exposure supplied each frame by dm_bright_k)", (unsigned long)lut.width, (unsigned long)lut.height, (unsigned long)lut.depth); } - if (!tmp || tmp.width != lut.width || tmp.height != lut.height || tmp.depth != lut.depth || tmp.pixelFormat != lut.pixelFormat) { - MTLTextureDescriptor *d = [MTLTextureDescriptor new]; d.textureType = MTLTextureType3D; d.pixelFormat = lut.pixelFormat; d.width = lut.width; d.height = lut.height; d.depth = lut.depth; - d.usage = MTLTextureUsageShaderRead; d.storageMode = MTLStorageModePrivate; if (tmp) defer_obj_release(tmp); tmp = [g_dev newTextureWithDescriptor:d]; if (!tmp) { failed = 1; return; } res_add(tmp); } - id<MTLBuffer> b0 = kb[ki]; ki = (ki + 1) % 3; *(float *)b0.contents = k; // Three copies in rotation: in-flight frames are still reading the previous copy - id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, lut, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(lut.width, lut.height, lut.depth), tmp, 0, 0, MTLOriginMake(0, 0, 0)); enc_b_end(g_blit); g_blit = nil; - id<MTL4ComputeCommandEncoder> ce = (id<MTL4ComputeCommandEncoder>)enc_c_begin(NULL); [ce setArgumentTable:at]; [at setTexture:tmp.gpuResourceID atIndex:0]; [at setTexture:lut.gpuResourceID atIndex:1]; [at setAddress:b0.gpuAddress atIndex:0]; - enc_c_pso(ce, ps); enc_c_dispatch(ce, MTLSizeMake((lut.width + 3) / 4, (lut.height + 3) / 4, (lut.depth + 3) / 4), MTLSizeMake(4, 4, 4)); enc_c_end(ce); - args_dirty_all(); -} static void dispatch(const p_dispatch *dp) { zskip_clear(); g_wr_epoch++; @@ -6125,7 +5803,6 @@ static void dispatch(const p_dispatch *dp) { enc_c_dispatch_indirect(ce, ig, MTLSizeMake(tg[0], tg[1], tg[2])); } else enc_c_dispatch(ce, MTLSizeMake(dp->x, dp->y, dp->z), MTLSizeMake(tg[0], tg[1], tg[2])); enc_c_end(ce); - bright_lut(cs); // 3D picture brightening: rewrite after the game finishes computing the color lookup table } // GPU: texture -> buffer (row stride taken from SetCopyRowStride, layer stride from SetCopyImageStride, 0 = tightly packed) static void copy_t2b(const p_copy_b2t *c) { @@ -6419,201 +6096,15 @@ static void clear_tex(const p_clear_tex *c) { static const char *cctx_snap(void) { static char b[320]; static uint64_t p_pass, p_up, p_reup, p_ser; uint64_t up = 0, reup = 0; mtl_pkg_stats(&up, &reup); pthread_mutex_lock(&g_deferred_mu); unsigned long nd = (unsigned long)g_deferred.count; pthread_mutex_unlock(&g_deferred_mu); - snprintf(b, sizeof b, "frame %llu; since the last submission: passes +%llu, first texture uploads +%llu, re-uploads +%llu, serial table changes +%llu; deferred-release queue %lu; world freeze state %d", + snprintf(b, sizeof b, "frame %llu; since the last submission: passes +%llu, first texture uploads +%llu, re-uploads +%llu, serial table changes +%llu; deferred-release queue %lu", (unsigned long long)atomic_load(&g_cur_frame), (unsigned long long)(g_stat_pass - p_pass), (unsigned long long)(up - p_up), (unsigned long long)(reup - p_reup), - (unsigned long long)(g_ser_gen - p_ser), nd, g_frz_state); + (unsigned long long)(g_ser_gen - p_ser), nd); p_pass = g_stat_pass; p_up = up; p_reup = reup; p_ser = g_ser_gen; return b; } -// ---------- Do not draw the world while the world is frozen (the whole game can be frozen while a panel is open) ---------- -// The "world frozen" status bit g_frz_on is set by mtl_world_frozen() (the host's freeze handshake calls it; in tests DM_NVN_TEST_FRZ=1 can stand in, or it can be switched in-process via @set:g_frz_on of DM_NVN_UB). -// This code does not judge by itself whether a panel is open. Structure of a frame (3.3.94012 in-game, per-pass dump): UI pass -> [UI stencil pass] -> about 40 passes of shadows / depth / lighting / transparency / post-processing -> final compositing pass (writes the window texture). -// The first frame after the bit is set = full frame: draw everything as usual, dropping only the draws of the UI stencil pass (stencil all 0 = no pixels blocked by the panel) -- post-processing normally does not write pixels blocked by the panel, so a shrinking panel would expose old content; -// from the second frame on = frozen frame: from the first render pass without a color attachment up to before the render pass that writes the window texture, set-target / clear / draw / texture-to-texture copy / compute are all not executed, the scene result stays as the full frame's copy, and the UI pass and the final compositing pass are drawn as usual. -// Frame boundaries are identified by render pass: the first set-target without the window texture after the window texture has been written = start of a new frame, and the status bit is latched here. Unrecognized structure (no render pass without color in a frame) = skip nothing. -// State constants / buffer updates / events / counters / texture uploads execute as usual (the UI pass needs them, the game is waiting). -int g_frz_on = -1; -static int g_frz_phase = 2, g_frz_drop, g_frz_seen_ds; static uint64_t g_frz_full_n, g_frz_skip_n; -void mtl_world_frozen(int on) { g_frz_on = on != 0; } -static int frz_wanted(void) { if (g_frz_on < 0) { const char *e = getenv("DM_NVN_TEST_FRZ"); g_frz_on = e && atoi(e); if (g_frz_on) MLOG("DM_NVN_TEST_FRZ: the world freeze state bit is replaced by the test switch (always on)"); } tbdr_on(); return g_frz_on > 0 && tbdr_on() && g_ruler_truth <= 0; } -// ---------- Runtime extrapolation (power-saving rendering method; no switch for the player; it caused a stutter defect in released builds, now off by default, to be re-enabled once there is evidence of no stutter on A17) ---------- -// 3D every other frame: a fully drawn frame draws as usual and, before the attachments are repurposed, copies the opaque HDR (468), the main depth-stencil, the post-tone-mapping 468 and the post-FXAA 469; -// extrapolated frame: the UI stencil pass runs as usual; shadow map / depth prepass / main pass keep only the draws of moving units (skinned); before the first skinned draw of the depth prepass run one extrapolation pass (nvn_rtex.inc: by cb3 +464 -> +336 -// move the previous frame's HDR and depth to this frame, treat old unit positions 0x10 / UI cutout 0x2 as holes to fill, and export stencil 0x80 for holes and pixels newly exposed at the screen edge); translucents are drawn as usual and write effect bit 0x40; -// compositing / add bloom / add fog / tone mapping / FXAA really run only in the 0xD0 region, and outside the region the previous frame's LDR result is copied in with a whole-image shift (orthographic camera, rounded); full-screen items such as the bloom chain are skipped; sharpening upscale and UI compositing run as usual. -// Fallback to the original rendering method: version file render_safe = true (dm_update.m), the extrapolation pipeline cannot be built (whole run), test DM_RT_AB=0. When a panel is open and the world is frozen, yield to the freeze path. -// On a camera jump (orientation change or translation over 64 pixels) and for 4 frames before and after switching the 3D render scale, whole frames are fully drawn. Unit shadows on the ground keep using the previous fully drawn frame. -static int rt_is_skin(void) { // Moving units = the 10 whose vertex shader reads constants by bone index (the key table is taken from the Mac shader cache b-424a072084e6, identified by the dynamic vp_c5 index in the source) - __unsafe_unretained NvProg *pr = S.prog ? prog_get(S.prog) : nil; if (!pr || !pr->sh[0]) return 0; - static const char *k[] = { "0fa4eb7559f1637a", "1d97d33ab66abcba", "3deaf68fd65b5298", "4dffa355f2fbcdaf", "54c93fa22abff9ee", - "64dd7d022f3fae39", "67e2ebc28c830f44", "add86ee8e20f89b9", "b3ae9be5cc670f35", "fbf6dc1873a449e9" }; - for (unsigned i = 0; i < sizeof k / sizeof *k; i++) if (!strncmp(pr->sh[0]->key, k[i], 16)) return 1; - return 0; -} -int g_dm_rt_ab = 0; // Default is the original rendering method (the new method stutters in-game on A17 iPhone 15 Pro and ghosts characters on the character select page, so off for now); DM_RT_AB=1 turns on the new method for testing. The version file render_safe can only set it to 0, no effect -static struct { int env, frame, filt, first, hdrp, mainp_seen, skipall, oit_seen, tm_seen, fx_state, comp_seen; uint64_t ldr_ser; float vpc[16], vpp[16]; id<MTLTexture> ldr, pl468, pl469; int ui_seen, main_seen, copied, ex_done, shadow, have_prev, have_ldr, sdx, sdy, cam_checked, force_full, disabled, was_on; uint64_t n_jump; uint64_t ds_ser, hdr_ser, n_ex, n_full, n_noex; - id<MTLTexture> ds, hdr, pds, phdr; struct { uint64_t key; int skin, other; } sh[16]; int nsh, cur_sh; } g_rt = { .env = -1 }; -static int rt_want(void); -static void rt_toggle_poll(void) { static int tg = -1; if (tg < 0) { const char *e = getenv("DM_RT_AB_TOGGLE"); tg = e ? atoi(e) : 0; (void)rt_want(); } // On first call also read DM_RT_AB (on a real device the world-freeze bit is set to 0 very early, so frz_op is not necessarily called) // test: switch back and forth between A / B every tg seconds by wall clock - if (tg > 0) { int v = (int)((time(NULL) / tg) & 1); if (v != g_dm_rt_ab) { g_dm_rt_ab = v; MLOG("DM_RT_AB_TOGGLE: rendering method switched to %s", v ? "B" : "A"); } } } -void mtl_rt_force_full(int n) { g_rt.force_full = n; } // Called by the host: before an overall picture change such as switching the 3D render scale, the following n frames are all fully drawn -// ---------- Power-saving mode (30 frames) ---------- -// On: the display link object changes to 30 Hz (frame rate range 30–30, a ProMotion screen lowers its refresh rate accordingly; the frame rate hint in front_ios.m is lowered together), the game presents one frame per display-link tick; runtime extrapolation off (really draws every frame). -// Game logic already runs at 25 ticks / second and unit positions are interpolated by time, so there are no repeated frames at 30 frames (measured on a real device: coefficient of variation of per-frame camera displacement 0.292 at 30 frames, 0.296 at 60 frames). -// The switch is on the "Custom" page (dm_custom.c eco_fps, off by default, takes effect on apply); the test switch DM_FPS=30/60 pins it. -__attribute__((weak)) int dm_fps_setting(void) { return 60; } // The host's dm_custom.c provides the strong definition; always 60 when graphics unit tests do not link the host // When the game itself asks for 30 fps (pacing by the game's present interval, interval 2, hle_nvn.c) the display link runs at 30 Hz too: games whose logic counts frames, such as Engage, are not pulled up to 60 fps by the display link on iOS __attribute__((weak)) int nvn_game_fps(void) { return 60; } // hle_nvn.c provides the strong definition int mtl_fps_target(void) { static int env = -2; if (env == -2) { const char *e = getenv("DM_FPS"); env = e ? atoi(e) : -1; if (env != 30 && env != 60) env = -1; else MLOG("DM_FPS=%d: frame rate pinned by the test switch", env); } - return env > 0 ? env : dm_fps_setting() == 30 || nvn_game_fps() == 30 ? 30 : 60; } -// Whenever the extrapolation enabled state changes (power-saving mode toggled, world freeze takes over / hands back): first let the "no store across frames" ledger relearn (during the trial period it stores as usual), and the extrapolation really switches one frame later -- -// guaranteeing the last submit before the switch is a full store. Otherwise attachments cleared first every frame at 30 frames are learned as no-store, and the first extrapolated frame after switching back to 60 frames reads old content and flashes for one frame (measured on a test build; only reverted after the xv self-check reported a failure) -static void xv_relearn(const char *why) { g_xv_n = 0; g_xv_ntrial = 0; g_xv_ntouch = 0; MLOG("cross-frame store skipping: %s; the ledger is cleared and relearned (stores happen as usual during probation)", why); } -static int rt_want_raw(void); -static int rt_want(void) { - static int eff = -1; static uint64_t pend; int want = rt_want_raw(); uint64_t f = atomic_load(&g_cur_frame); - if (eff < 0) eff = want; - if (want == eff) { pend = 0; return eff; } - if (!pend) { pend = f + 1; xv_relearn(want ? "extrapolation will be enabled" : "extrapolation will be disabled"); return eff; } - if (f > pend) { eff = want; pend = 0; } - return eff; -} -static int rt_want_raw(void) { static int forced; if (g_rt.env < 0) { const char *e = getenv("DM_RT_AB"); g_rt.env = e ? atoi(e) : 0; forced = e != NULL; if (e) MLOG("DM_RT_AB=%d: rendering method pinned by the environment variable (for tests; the Custom page switch no longer has an effect)", g_rt.env); } - if (forced) g_dm_rt_ab = g_rt.env; - return g_dm_rt_ab == 1 && !g_rt.disabled && g_frz_on <= 0 && tbdr_on() && g_ruler_truth <= 0 && mtl_fps_target() == 60; } // When a panel is open and the world is frozen, yield to the freeze path (saves more); power-saving mode (30 frames) really draws every frame -#include "nvn_rtex.inc" -static uint64_t rt_key(const p_set_rt *r) { uint64_t s_; int lv, sl; rt_sub(r, 8, &s_, &lv, &sl); return s_ * 64 + (uint64_t)lv * 8 + (uint64_t)sl; } -static int rt_is_shadow(const p_set_rt *r) { return !r->n && r->depth && r->depth->d.width == r->depth->d.height; } -static id<MTLTexture> rt_cp(id<MTLTexture> src, id<MTLTexture> dst0) { - id<MTLTexture> dst = dst0; - if (!dst || dst.width != src.width || dst.height != src.height || dst.pixelFormat != src.pixelFormat) { - MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:src.pixelFormat width:src.width height:src.height mipmapped:NO]; - d.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget | MTLTextureUsagePixelFormatView; d.storageMode = MTLStorageModePrivate; - if (dst0) defer_obj_release(dst0); // Size change: the old copy may still be read by an in-flight command buffer (MTL4 command buffers do not retain resources), so release it deferred - dst = [g_dev newTextureWithDescriptor:d]; if (!dst) return nil; res_add(dst); } - id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, src, 0, 0, MTLOriginMake(0, 0, 0), MTLSizeMake(src.width, src.height, 1), dst, 0, 0, MTLOriginMake(0, 0, 0)); return dst; } -static void rt_blit_shift(id<MTLTexture> src, id<MTLTexture> dst, int dx, int dy) { // Integer-offset copy: dst(x + dx, y + dy) = src(x, y); the strip at the screen edge that cannot be moved is left for the region recompute (the extrapolation pass has already marked it 0x80) - if (!src || !dst || src.width != dst.width || src.height != dst.height) return; - int W = (int)src.width, H = (int)src.height, w = W - abs(dx), h = H - abs(dy); if (w <= 0 || h <= 0) return; - id<MTL4ComputeCommandEncoder> b = blit(); enc_b_copy_texture(b, src, 0, 0, MTLOriginMake(MAX(0, -dx), MAX(0, -dy), 0), MTLSizeMake(w, h, 1), dst, 0, 0, MTLOriginMake(MAX(0, dx), MAX(0, dy), 0)); } -static void rt_copy_prev(void) { // Fully drawn frame: copy the opaque HDR and the main depth-stencil once each (in order within the GPU, in this command buffer) - if (g_rt.copied || !g_rt.main_seen || !g_rt.hdr || !g_rt.ds) return; g_rt.copied = 1; // Do not copy when the main pass was not recognized this frame (foreground / loading screen): the textures recognized last time may already have been released by the game and not be in the residency set - if (!rx_setup(g_rt.hdr.pixelFormat, g_rt.ds.pixelFormat)) { if (!g_rt.disabled) { g_rt.disabled = 1; MLOG("[RED] runtime extrapolation: the extrapolation pipelines cannot be created on this device; the whole run falls back to the original rendering"); } return; } // Cannot be built (old GPU does not support stencil export etc.): fall back for the whole run - g_rt.phdr = rt_cp(g_rt.hdr, g_rt.phdr); g_rt.pds = rt_cp(g_rt.ds, g_rt.pds); g_rt.have_prev = g_rt.phdr && g_rt.pds; } -static void rt_frame_begin(void) { g_rt.filt = g_rt.ui_seen = g_rt.main_seen = g_rt.copied = g_rt.ex_done = g_rt.mainp_seen = 0; g_rt.cur_sh = -1; - g_rt.oit_seen = g_rt.tm_seen = g_rt.fx_state = g_rt.comp_seen = g_rt.skipall = g_rt.cam_checked = 0; g_rt_ovr = 0; g_dss_dirty = 1; - if (g_frz_state == 0) { g_rt.have_prev = g_rt.have_ldr = 0; for (int i = 0; i < g_rt.nsh; i++) g_rt.sh[i].skin = g_rt.sh[i].other = 0; } } // Fully drawn frame: the next frame may extrapolate only if this frame's copies succeeded -static void rt_set_rt(const p_set_rt *r, int win) { // Every set-target (both fully drawn and extrapolated frames go through here): identify roles, find the copy points in fully drawn frames, and in extrapolated frames decide whether this render pass keeps only skinned draws - g_rt.cur_sh = -1; g_rt.shadow = 0; g_rt.first = 1; g_rt.hdrp = 0; - if (win) { if (g_frz_state == 0) { rt_copy_prev(); if (g_rt.fx_state == 2 && g_rt.ldr) { g_rt.pl469 = rt_cp(g_rt.ldr, g_rt.pl469); g_rt.have_ldr = g_rt.pl469 != nil; g_rt.fx_state = 3; } } - g_rt.filt = 0; g_rt.skipall = 0; if (g_rt_ovr) { g_rt_ovr = 0; g_dss_dirty = 1; } return; } - id<MTLTexture> c0 = r->n > 0 && r->color[0] ? tex_mtl(r->color[0]) : nil, dt = r->depth ? tex_mtl(r->depth) : nil; - int hdr_here = 0; for (int i = 0; i < r->n && i < 8; i++) if (r->color[i] && g_rt.hdr_ser && r->color[i]->serial == g_rt.hdr_ser) hdr_here = 1; - if (g_frz_state == 0) { - if (!g_rt.main_seen && r->n == 1 && c0 && c0.pixelFormat == MTLPixelFormatRG11B10Float && dt && dt.pixelFormat == MTLPixelFormatDepth32Float_Stencil8 && dt.width >= 500 && dt.width != dt.height) { - { static uint64_t lh, ld; if (lh != r->color[0]->serial || ld != r->depth->serial) { lh = r->color[0]->serial; ld = r->depth->serial; MLOG("runtime extrapolation: main pass recognized: opaque HDR serial %llu %lux%lu, main depth-stencil serial %llu %lux%lu (pass %llu)", (unsigned long long)lh, (unsigned long)c0.width, (unsigned long)c0.height, (unsigned long long)ld, (unsigned long)dt.width, (unsigned long)dt.height, g_stat_pass + 1); } } - g_rt.main_seen = 1; g_rt.hdr_ser = r->color[0]->serial; g_rt.ds_ser = r->depth->serial; g_rt.hdr = c0; g_rt.ds = dt; } - else if (g_rt.main_seen && hdr_here && !g_rt.copied) rt_copy_prev(); // The first set-target after the main pass that attaches the opaque HDR again (the translucent pass; 468 occupies slot 0 but is not written): before it, copy the HDR and the main depth-stencil - else if (g_rt.main_seen && hdr_here && r->n == 1 && r->depth && r->depth->serial == g_rt.ds_ser) g_rt.fx_state = 1; // Only 468 + main depth attached = tone mapping (repurposes 468 as the LDR output) - else if (g_rt.fx_state == 1) { // The first render pass after tone mapping = FXAA (reads 468, writes 469): before it, copy the post-tone-mapping 468 (LDR) and note down 469 - if (r->n >= 1 && r->color[0] && c0) { g_rt.ldr_ser = r->color[0]->serial; g_rt.ldr = c0; g_rt.pl468 = rt_cp(g_rt.hdr, g_rt.pl468); g_rt.fx_state = 2; } else g_rt.fx_state = 0; } - else if (g_rt.fx_state == 2 && g_rt.ldr) { g_rt.pl469 = rt_cp(g_rt.ldr, g_rt.pl469); g_rt.have_ldr = g_rt.pl468 && g_rt.pl469; g_rt.fx_state = 3; } // Before the first render pass after FXAA: copy the post-FXAA 469 - } - if (rt_is_shadow(r)) { uint64_t k = rt_key(r); int i; for (i = 0; i < g_rt.nsh && g_rt.sh[i].key != k; i++) {} - if (i == g_rt.nsh && i < 16) { g_rt.sh[i].key = k; g_rt.sh[i].skin = g_rt.sh[i].other = 0; g_rt.nsh++; } g_rt.cur_sh = i < 16 ? i : -1; g_rt.shadow = 1; } - int ds_here = r->depth && g_rt.ds_ser && r->depth->serial == g_rt.ds_ser; - if (ds_here && !r->n && !g_rt.ui_seen) { g_rt.ui_seen = 1; g_rt.filt = 0; return; } // The first render pass in this frame with the main depth and no color = the UI stencil pass: run as usual - g_rt.hdrp = hdr_here && ds_here && r->n == 1 && !g_rt.mainp_seen; if (g_rt.hdrp) g_rt.mainp_seen = 1; // The first render pass in this frame with "only opaque HDR + main depth attached" = the main pass - g_rt.filt = g_rt.shadow || (ds_here && !r->n) || g_rt.hdrp; - if (g_frz_state == 2 && g_rt.mainp_seen && !g_rt.hdrp) { // Extrapolated frame, after the main pass: post-processing runs only in the unit / effect region (plan B) - int c0ldr = r->n >= 1 && r->color[0] && g_rt.ldr_ser && r->color[0]->serial == g_rt.ldr_ser, ovr = 0; g_rt.skipall = 0; - if (g_rt.fx_state == 2) {} // After FXAA (sharpening upscale + vignette, UI compositing): run everything - else if (g_rt.fx_state == 1) g_rt.fx_state = 2; - else if (!g_rt.oit_seen && ds_here && r->n >= 2) { g_rt.oit_seen = 1; ovr = 3; } // Translucent (weighted blended, two targets): draw as usual, writing effect bit 0x40 where it passes - else if (!g_rt.comp_seen) { if (ds_here && c0ldr) { g_rt.comp_seen = 1; ovr = 1; rt_blit_shift(g_rt.pl469, g_rt.ldr, g_rt.sdx, g_rt.sdy); } else g_rt.skipall = 1; } // Before compositing: the previous frame's post-FXAA 469 is copied in with an integer shift (then recomputed in the region) // the other passes before compositing (mip chain, 511 refraction layer etc.) are skipped - else if (ds_here && c0ldr && g_rt.tm_seen) { // The FXAA pass itself (recomputed in the region; the 469 outside the region was already shift-copied before compositing) - g_rt.fx_state = 1; ovr = 1; } - else if (ds_here) { if (hdr_here && r->n == 1) { g_rt.tm_seen = 1; rt_blit_shift(g_rt.pl468, g_rt.hdr, g_rt.sdx, g_rt.sdy); } ovr = 1; } // Before tone mapping: the previous frame's post-tone-mapping 468 (LDR) is copied in with an integer shift // add bloom, add fog, tone mapping: run only in the region - else g_rt.skipall = 1; // Those without the main depth attached, such as the bloom chain: skip (the region keeps using the previous frame's bloom) - if (ovr != g_rt_ovr) { g_rt_ovr = ovr; g_dss_dirty = 1; } - } -} -static int rt_op(unsigned op) { // Non-set-target operations in an extrapolated frame: returns 1 = not executed - if (g_frz_state == 2 && g_rt_ovr == 1 && op == OP_CLEAR_COLOR) return 1; // The region post-processing passes do not clear: outside the region is the previous frame's result copied in with a shift - if (g_rt.skipall && (op == OP_DRAW || op == OP_DRAW_IDX || op == OP_CLEAR_COLOR || op == OP_CLEAR_DS || op == OP_COPY_T2T || op == OP_CLEAR_TEX || op == OP_DISPATCH)) return 1; - if (op == OP_DRAW || op == OP_DRAW_IDX) { - if (g_frz_state == 2 && !g_rt.cam_checked) { // First draw of an extrapolated frame (UI stencil pass, before shadows / prepass): check this frame's camera, and on a jump switch the whole frame to fully drawn - g_rt.cam_checked = 1; const p_bind_buf *u = &S.ubo[0][3]; void *mb; uint64_t mo; const uint8_t *base; int ok = 0; - if (u->addr && u->size >= 528 && mtl_addr(u->addr, u->size, &mb, &mo) && (base = [(__bridge id<MTLBuffer>)mb contents])) { - const float *cur = (const float *)(base + mo + 336), *prev = (const float *)(base + mo + 464); int dx, dy; float md = 0; - for (int r = 0; r < 3; r++) for (int c = 0; c < 4; c++) md = MAX(md, fabsf(cur[r * 4 + c] - prev[r * 4 + c])); // The first three rows (orientation / scale) should be unchanged: an orthographic camera only translates - rt_shift_px(prev, cur, g_rt.hdr.width, g_rt.hdr.height, &dx, &dy); - ok = md < 1e-4f && abs(dx) <= 64 && abs(dy) <= 64; if (!ok) { static int n; if (n++ < 20) MLOG("runtime extrapolation: camera jump (shift %d,%d pixels, orientation difference %g); this frame is fully rendered", dx, dy, md); } } - if (!ok) { g_frz_state = 0; g_rt.n_jump++; rt_frame_begin(); g_rt.cam_checked = 1; return 0; } - } - if (g_frz_state == 0) { if (g_rt.cur_sh >= 0) { if (rt_is_skin()) g_rt.sh[g_rt.cur_sh].skin++; else g_rt.sh[g_rt.cur_sh].other++; } return 0; } - if (!g_rt.filt) return 0; - if (!rt_is_skin()) return 1; - if (!g_rt.ex_done && !g_rt.shadow) { // Before the first skinned draw of the depth prepass: the extrapolation pass (reads the cb3 bound by this draw: +336 this frame's, +464 the previous frame's view-projection) - g_rt.ex_done = 1; const p_bind_buf *u = &S.ubo[0][3]; void *mb; uint64_t mo; const uint8_t *base; - if (u->addr && u->size >= 528 && mtl_addr(u->addr, u->size, &mb, &mo) && (base = [(__bridge id<MTLBuffer>)mb contents])) { - float cur[16], prev[16]; memcpy(cur, base + mo + 336, 64); memcpy(prev, base + mo + 464, 64); memcpy(g_rt.vpc, cur, 64); memcpy(g_rt.vpp, prev, 64); rt_shift_px(prev, cur, g_rt.hdr.width, g_rt.hdr.height, &g_rt.sdx, &g_rt.sdy); - rp_end(); if (rt_ex_encode(g_rt.hdr, g_rt.ds, g_rt.phdr, g_rt.pds, prev, cur, 0x10, 0x2)) g_rt.n_ex++; else g_rt.n_noex++; - } else g_rt.n_noex++; - } - return 0; - } - if (g_frz_state == 2 && g_rt.filt) { - if (op == OP_CLEAR_COLOR || op == OP_CLEAR_DS) { - if (g_rt.shadow && g_rt.cur_sh >= 0 && !g_rt.sh[g_rt.cur_sh].other) return 0; // A shadow level that had only skinned draws in the previous fully drawn frame (character-only level): clear and draw as usual - return 1; } - } - return 0; -} -static int frz_op(unsigned op, const void *a) { // Returns 1 = this operation is not executed - if (op == OP_SET_RT) { - const p_set_rt *r = a; int win = 0; - for (int i = 0; i < r->n; i++) { uint64_t s_; int lv, sl; rt_sub(r, i, &s_, &lv, &sl); if (s_ && xv_present(s_)) win = 1; } - if (g_frz_phase == 2 && !win && rt_want()) { // Runtime extrapolation: new frame, fully drawn frames and extrapolated frames alternate - int was = g_frz_state; g_frz_phase = 0; g_rt.was_on = 1; - { static int dbg = -1, n; if (dbg < 0) dbg = getenv("DM_RT_DBG") != NULL; if (dbg && n < 4000 && g_rt.ds_ser) { n++; MLOG("[rtdbg] frame boundary pass %llu prev_state %d main_seen %d copied %d extrapolated %d fx %d ldr %d hoist %d fz %d defer %d/%d", g_stat_pass, was, g_rt.main_seen, g_rt.copied, g_rt.ex_done, g_rt.fx_state, g_rt.have_ldr, g_hoist_n, g_fz.active, defer_has_active(), g_defer_replaying); } } - if (g_rt.force_full > 0) g_rt.force_full--; - g_frz_state = !was && g_rt.have_prev && g_rt.have_ldr && !g_rt.force_full && !g_hoist_n && !g_fz.active && !defer_has_active() && !g_defer_replaying ? 2 : 0; - if (g_frz_state == 2) g_rt.frame++; else g_rt.n_full++; - if (g_frz_state != was && (g_rt.frame + g_rt.n_full) % 600 < 2) MLOG("runtime extrapolation: full frames %llu, extrapolated frames %d (extrapolation pass encoded %llu times, not encoded %llu times; full render after a camera jump %llu times)", g_rt.n_full, g_rt.frame, g_rt.n_ex, g_rt.n_noex, g_rt.n_jump); - rt_frame_begin(); - } - if (g_frz_phase != 2 && rt_want()) { if (win) g_frz_phase = 2; rt_set_rt(r, win); return g_rt.skipall; } - if (g_frz_phase == 2 && win && rt_want()) { rt_set_rt(r, win); return 0; } - { int on = rt_want(); if (g_rt.was_on && !on) { // The new rendering method was just turned off (version file fallback / panel freeze takes over / fallback for the whole run): reset state to zero and hand back to the original path - g_frz_state = 0; g_frz_phase = 2; g_rt.have_prev = g_rt.have_ldr = 0; if (g_rt_ovr) { g_rt_ovr = 0; g_dss_dirty = 1; } g_rt.skipall = 0; - MLOG("runtime extrapolation: new rendering method disabled (method %d, world freeze %d, whole-run fallback %d)", g_dm_rt_ab, g_frz_on, g_rt.disabled); } - g_rt.was_on = on; } - if (g_frz_phase == 2 && !win) { // New frame - int on = frz_wanted(), was = g_frz_state; g_frz_phase = 0; g_frz_seen_ds = 0; - { static uint64_t fr, p0, k0; if (was == 2 && (++fr == 4 || fr % 600 == 0)) MLOG("world freeze: frozen frame %llu opened %llu passes, skipped %llu set-target commands", fr, g_stat_pass - p0, g_frz_skip_n - k0); if (was != 2) fr = 0; p0 = g_stat_pass; k0 = g_frz_skip_n; } - if (!on) g_frz_state = 0; - else if (was) g_frz_state = 2; - else if (!g_hoist_n && !g_fz.active && !defer_has_active() && !g_defer_replaying) g_frz_state = 1; // The premises / merges / deferrals left by the previous frame have not finished yet: enter next frame - if (g_frz_state != was) MLOG("world freeze: from pass %llu, %s (full frames so far %llu, passes skipped %llu)", g_stat_pass + 1, g_frz_state == 1 ? "full frame (without the UI stencil)" : g_frz_state == 2 ? "frozen frame (only the UI and the final composite are drawn)" : "back to normal", g_frz_full_n += g_frz_state == 1, g_frz_skip_n); - } - g_frz_drop = 0; - if (win) { g_frz_phase = 2; return 0; } - if (!g_frz_state || g_frz_phase == 2) return 0; - if (g_frz_phase == 0 && !r->n && r->depth && !g_frz_seen_ds) { // The first render pass without color in this frame - g_frz_seen_ds = 1; - if (g_frz_state == 2) g_frz_phase = 1; - else { uint64_t s_; int lv, sl; rt_sub(r, 8, &s_, &lv, &sl); static int neg = -1; if (neg < 0) neg = getenv("DM_NVN_TEST_FRZ_NEG") != NULL; // Negative control: the full frame is drawn with the UI stencil as usual, and old pixels must be exposed when the panel shrinks - g_frz_drop = !neg && MAX(r->depth->d.width >> lv, 1u) != MAX(r->depth->d.height >> lv, 1u); } // Full frame: the one that is not square (shadow map) is the UI stencil pass - } - if (g_frz_phase == 1) { g_frz_skip_n++; return 1; } - return 0; - } - if (rt_want() && g_frz_phase != 2) return rt_op(op); - if (g_frz_drop) return op == OP_DRAW || op == OP_DRAW_IDX; - if (g_frz_state == 2 && g_frz_phase == 1) return op == OP_CLEAR_COLOR || op == OP_CLEAR_DS || op == OP_DRAW || op == OP_DRAW_IDX || op == OP_COPY_T2T || op == OP_CLEAR_TEX || op == OP_DISPATCH; - return 0; -} - + return env > 0 ? env : nvn_game_fps() == 30 ? 30 : 60; } // Hand a submission to the GPU in segments (DM_NVN_CUT=draw count, 0 = off; default 64): when the game holds back the next frame's submit until this frame's GPU work is done (The Legend of Zelda: Breath of the Wild: the main thread's nvnSyncWait on the fence of the frame just submitted), // the GPU sits idle for the milliseconds the host spends translating inside the submit (in the shrine each frame is GPU 13 ms + translation 7 ms back to back). Each time this many draws have accumulated, the command buffer encoded so far is committed between two passes: the GPU runs the earlier passes while the CPU translates the later ones. // Cut only when no encoder is open and no pass is deferred / merged / hoisted; commit this batch's uploads first (later draws use them). The fence signal and completion work stay on the last segment only: the queue executes in commit order and every encoder starts with a queue barrier @@ -6779,8 +6270,6 @@ void mtl_exec(const uint8_t *p) { g_exec_cur = p; // Scan start point of the store lookahead: the operation that ends the render pass (inclusive) if (p == g_defer_pend) { for (int gi = 0; gi < 4; gi++) if (g_defer_slots[gi].active && g_defer_slots[gi].p_end_op == p) g_defer_slots[gi].in_p = 0; g_defer_pend = NULL; } sdx_seq(p); - if (h.op == OP_SET_RT) rt_toggle_poll(); - if ((g_frz_on || g_frz_state || g_dm_rt_ab == 1) && frz_op(h.op, p + 8)) { p += h.size; continue; } if (trace) PD("o cmd=%p pass=%llu op=%u size=%u", p, g_stat_pass, (unsigned)h.op, (unsigned)h.size); // Diagnostic DM_NVN_ALLOCPROF=1: growth of the GPU allocated size is charged to the previous operation's type (rough: allocations on other threads are counted too); the memory report line appends the top 4 if (g_allocprof < 0) g_allocprof = getenv("DM_NVN_ALLOCPROF") != NULL; @@ -7015,9 +6504,7 @@ static void shot(id<MTLTexture> src, const char *path, int screen, const int cro if (!ctx || !img || !dst) MLOG("[RED] screenshot %s failed: %s", path, !ctx ? "cannot create the bitmap context" : !img ? "cannot get the image" : "cannot write (directory missing or not writable)"); else { CGImageDestinationAddImage(dst, img, NULL); ok = CGImageDestinationFinalize(dst); } if (dst) CFRelease(dst); if (url) CFRelease(url); CGImageRelease(img); CGContextRelease(ctx); CGColorSpaceRelease(cs); - dm_shot_written(path, ok); // Let the host write the "frozen tick" line first, then write the screenshot line ending in "ok": the screenshot script stops the host as soon as it sees "ok", and with the order reversed the log would lack the "frozen tick" line under full load (false failure in the gate check's require_log) MLOG("screenshot %zux%zu format %lu → %s %s", w, h, (unsigned long)read.pixelFormat, path, ok ? "ok" : "failed"); - if (g_ruler_truth && getenv("DM_NVN_RULER") && !strcmp(getenv("DM_NVN_RULER"), "1") && ok) { static int shots; if (++shots == 2) g_ruler_pending = 1; } if (g_defer_ab >= 0 && getenv("DM_NVN_DEFER_AB") && !strcmp(getenv("DM_NVN_DEFER_AB"), "1") && ok) { static int shots; if (++shots == 2) { g_defer_ab_on = 1; MLOG("DM_NVN_DEFER_AB: second screenshot written; deferred pass merging on"); } } if (g_udc_ab >= 0 && getenv("DM_NVN_UDC_AB") && !strcmp(getenv("DM_NVN_UDC_AB"), "1") && ok) { static int shots; @@ -7288,7 +6775,6 @@ static double g_link_it_tin; // Enqueue time of the frame the display-link cal static int present_now(const tex_t *t, double period, uint64_t frame, uint64_t ticket, const int crop[4], id<CAMetalDrawable> supplied, double target_ts) { if (g_present_stall_ns) { struct timespec ts = {0, (long)g_present_stall_ns}; nanosleep(&ts, NULL); } g_present_n++; - if (g_use != USE_OFF && g_use != USE_DONE) use_tick(); if (bootprof() && (g_present_n == 1 || g_present_n % 120 == 0)) { if (g_present_n == 1) g_bp_first_present = bp_now() - g_bp_t0; MLOG("[bootprof] t=%.3f s present #%llu: metallibs loaded %llu / %.1f ms; pipelines created %llu / %.1f ms (all synchronous on the submit thread); prewarmed and used: shaders %llu / render pipelines %llu / compute pipelines %llu; " diff --git a/dm/gfx/nvn/nvn_rtex.inc b/dm/gfx/nvn/nvn_rtex.inc deleted file mode 100644 index ab45b7e..0000000 --- a/dm/gfx/nvn/nvn_rtex.inc +++ /dev/null @@ -1,155 +0,0 @@ -// -// int rt_ex_encode(dst_hdr, dst_ds, src_hdr, src_ds, vp_prev[16], vp_cur[16], unit_bits, ui_bits) -// nvn_rtex.inc - shaders for runtime extrapolation (#included by nvn_mtl.m after the definitions of rp_end / m4_end / res_add / args_dirty_all / blit etc.) -// int rt_ex_encode(dst_hdr, dst_ds, src_hdr, src_ds, vp_prev[16], vp_cur[16], unit_bits, ui_bits) -// Encodes one render pass (a single fullscreen triangle) on the current MTL4 command buffer g_cb, extrapolating the opaque HDR and depth of the previous fully drawn frame into dst_hdr / dst_ds by this frame's camera: -// Backward mapping: destination pixel q → source pixel p, with 2 fixed-point iterations using the source depth; M = inv(vp_prev) · vp_cur is computed on the CPU in double precision (with translations on the order of 1e4 a single-precision inverse matrix would be off by 0.01 pixel). -// Non-hole: five-tap Catmull-Rom, clamped to the min / max of the 2×2 core texels against ringing, then sharpening compensation by the fractional phase of the displacement (strongest at phase 0.5, 0 at integer phase, strength k = 0.5). -// Hole (the stencil of the source 2×2 core contains unit_bits | ui_bits: old unit position / UI cut-out): along 8 directions search outward every 4 pixels for the first pixel that is neither a hole nor pure black (at most 24 steps), -// weighted average by 1/distance²; depth takes the nearest of them. Holes and pixels newly exposed at the screen edge (source out of bounds) get stencil 0x80 exported by the fragment, and the later regional post-processing is recomputed accordingly. -// Pixels covered by this frame's UI (the UI stencil pass already wrote 0x2) are skipped by the stencil test. -// Returns 1 = encoded; 0 = size mismatch / pipeline cannot be built (the caller rt_copy_prev prechecks with rx_setup first, and if it cannot be built the whole run falls back to the original drawing method). -// Before and after the call: this function first calls rp_end() to close the current render pass and copy encoder; after encoding it sets args_dirty_all() / g_pso_dirty / g_dss_dirty, and the game's next draw reopens its pass with 'load'. -// Resources: src_hdr / src_ds are created and res_add'ed by the caller; src_ds must be able to open an X32_Stencil8 view (created with MTLTextureUsagePixelFormatView). -static id<MTLRenderPipelineState> g_rx_pso1; static MTLPixelFormat g_rx_fmt_c, g_rx_fmt_ds; -static id<MTLDepthStencilState> g_rx_dss; static id<MTL4ArgumentTable> g_rx_args; -static id<MTLBuffer> g_rx_const[3]; static int g_rx_ci; -static id<MTLTexture> g_rx_stv; static __unsafe_unretained id<MTLTexture> g_rx_stv_src; -static const char *k_rx_msl = -"#include <metal_stdlib>\n" -"using namespace metal;\n" -"struct VO { float4 p [[position]]; };\n" -"vertex VO rx_vs(uint id [[vertex_id]]) { float2 uv = float2((id << 1) & 2, id & 2); VO o; o.p = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); return o; }\n" -"struct K { float4x4 M; uint hole; float amount; float2 pad; };\n" -"static float4 cr_w(float t) { return float4(((-0.5 * t + 1.0) * t - 0.5) * t, (1.5 * t - 2.5) * t * t + 1.0, ((-1.5 * t + 2.0) * t + 0.5) * t, (0.5 * t - 0.5) * t * t); }\n" -"static uint stv(texture2d<uint> s, int2 p) { uint4 v = s.read(uint2(p)); return v.r | v.g; }\n" // X32_Stencil8 view: which component holds the stencil value is implementation-dependent, take both r | g (the other one is 0) -"static float zproj(float4x4 M, float2 p, float z, int W, int H) { float4 c = M * float4((p.x + 0.5) / W * 2 - 1, 1 - (p.y + 0.5) / H * 2, z, 1); return c.z / c.w; }\n" -"struct O1 { float4 c [[color(0)]]; float d [[depth(any)]]; uint s [[stencil]]; };\n" -"fragment O1 rx_fs1(VO in [[stage_in]], texture2d<float> hdr [[texture(0)]], depth2d<float> dep [[texture(1)]], texture2d<uint> stc [[texture(2)]], constant K& k [[buffer(0)]]) {\n" -" constexpr sampler ls(filter::linear, address::clamp_to_edge, coord::pixel);\n" -" int W = hdr.get_width(), H = hdr.get_height(); int2 hi = int2(W - 1, H - 1);\n" -" float2 q = floor(in.p.xy); float2 p = q;\n" -" for (int it = 0; it < 2; it++) {\n" -" int2 pi = clamp(int2(rint(p)), int2(0), hi); float z = dep.read(uint2(pi));\n" -" float4 c = k.M * float4((p.x + 0.5) / W * 2 - 1, 1 - (p.y + 0.5) / H * 2, z, 1);\n" -" float2 qq = float2((c.x / c.w + 1) * 0.5 * W - 0.5, (1 - c.y / c.w) * 0.5 * H - 0.5); p += q - qq; }\n" -" bool oob = p.x < -0.5 || p.y < -0.5 || p.x > float(hi.x) + 0.5 || p.y > float(hi.y) + 0.5;\n" -" p = clamp(p, float2(0), float2(hi));\n" -" int2 b0 = clamp(int2(floor(p)), int2(0), hi - 1);\n" -" uint s4 = stv(stc, b0) | stv(stc, b0 + int2(1, 0)) | stv(stc, b0 + int2(0, 1)) | stv(stc, b0 + int2(1, 1));\n" -" int2 cp = clamp(int2(rint(p)), int2(0), hi);\n" -" O1 o; o.s = ((s4 & k.hole) || oob) ? 0x80u : 0u;\n" -" if (!(s4 & k.hole)) {\n" -" float3 t00 = hdr.read(uint2(b0)).rgb, t10 = hdr.read(uint2(b0 + int2(1, 0))).rgb, t01 = hdr.read(uint2(b0 + int2(0, 1))).rgb, t11 = hdr.read(uint2(b0 + int2(1, 1))).rgb;\n" -" float3 lo = min(min(t00, t10), min(t01, t11)), hc = max(max(t00, t10), max(t01, t11)), avg = (t00 + t10 + t01 + t11) * 0.25;\n" -" float2 sp = p + 0.5; float2 tp1 = floor(sp - 0.5) + 0.5; float2 f = sp - tp1;\n" // Five-tap Catmull-Rom (bilinear sampling merges the two middle taps, the four corners are dropped) -" float2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f)), w1 = 1.0 + f * f * (-2.5 + 1.5 * f), w2 = f * (0.5 + f * (2.0 - 1.5 * f)), w3 = f * f * (-0.5 + 0.5 * f);\n" -" float2 w12 = w1 + w2, t12 = tp1 + w2 / w12, t0 = tp1 - 1.0, t3 = tp1 + 2.0;\n" -" float3 acc = hdr.sample(ls, float2(t12.x, t0.y)).rgb * (w12.x * w0.y) + hdr.sample(ls, float2(t0.x, t12.y)).rgb * (w0.x * w12.y) + hdr.sample(ls, t12).rgb * (w12.x * w12.y)\n" -" + hdr.sample(ls, float2(t3.x, t12.y)).rgb * (w3.x * w12.y) + hdr.sample(ls, float2(t12.x, t3.y)).rgb * (w12.x * w3.y);\n" -" float wt = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y; float3 cr = clamp(acc / wt, lo, hc);\n" -" o.c = float4(max(cr + k.amount * (cr - avg), 0.0), 1); o.d = zproj(k.M, p, dep.read(uint2(cp)), W, H); return o; }\n" // Sharpening compensation by fractional phase is folded into the same pass (the neighborhood takes the mean of the 2×2 core) -" const int2 dirs[8] = { int2(1, 0), int2(-1, 0), int2(0, 1), int2(0, -1), int2(1, 1), int2(-1, 1), int2(1, -1), int2(-1, -1) };\n" -" float3 sum = 0; float wsum = 0; int bestr = 1000; int2 bests = cp;\n" -" for (int d = 0; d < 8; d++) for (int r = 1; r <= 24; r++) { int2 s = cp + dirs[d] * (4 * r);\n" -" if (s.x < 0 || s.y < 0 || s.x > hi.x || s.y > hi.y) break;\n" -" if (stv(stc, s) & k.hole) continue;\n" -" float3 v = hdr.read(uint2(s)).rgb; if (max(v.r, max(v.g, v.b)) <= 0) continue;\n" -" float w = 1.0 / float(r * r); sum += v * w; wsum += w; if (r < bestr) { bestr = r; bests = s; } break; }\n" -" if (wsum > 0) { o.c = float4(sum / wsum, 1); o.d = zproj(k.M, float2(bests), dep.read(uint2(bests)), W, H); return o; }\n" -" o.c = float4(hdr.read(uint2(cp)).rgb, 1); o.d = zproj(k.M, p, dep.read(uint2(cp)), W, H); return o;\n" -"}\n"; -static int rx_setup(MTLPixelFormat fc, MTLPixelFormat fds) { - if (getenv("DM_RT_TEST_NOPSO")) return 0; // Test: pretend this device cannot build the extrapolation pipeline (verifies the whole run falls back to the original drawing method) - if (g_rx_pso1 && g_rx_fmt_c == fc && g_rx_fmt_ds == fds) return 1; - static int failed; if (failed) return 0; - NSError *e = nil; id<MTLLibrary> lib = [g_dev newLibraryWithSource:@(k_rx_msl) options:nil error:&e]; - if (!lib) { MLOG("[RED] runtime extrapolation: shader compile failed %s", e.localizedDescription.UTF8String); failed = 1; return 0; } - MTLRenderPipelineDescriptor *pd = [MTLRenderPipelineDescriptor new]; pd.vertexFunction = [lib newFunctionWithName:@"rx_vs"]; pd.fragmentFunction = [lib newFunctionWithName:@"rx_fs1"]; - pd.colorAttachments[0].pixelFormat = fc; pd.depthAttachmentPixelFormat = fds; - if (fds == MTLPixelFormatDepth32Float_Stencil8) pd.stencilAttachmentPixelFormat = fds; - g_rx_pso1 = [g_dev newRenderPipelineStateWithDescriptor:pd error:&e]; - if (!g_rx_pso1) { MLOG("[RED] runtime extrapolation: cannot build the pipeline %s", e.localizedDescription.UTF8String); g_rx_pso1 = nil; failed = 1; return 0; } - MTLDepthStencilDescriptor *dd = [MTLDepthStencilDescriptor new]; dd.depthCompareFunction = MTLCompareFunctionAlways; dd.depthWriteEnabled = YES; - if (fds == MTLPixelFormatDepth32Float_Stencil8) { MTLStencilDescriptor *sd = [MTLStencilDescriptor new]; sd.stencilCompareFunction = MTLCompareFunctionEqual; sd.readMask = 0x2; sd.writeMask = 0x80; sd.depthStencilPassOperation = MTLStencilOperationReplace; // Hole pixels / pixels newly entering from off-screen get 0x80 exported by the fragment, and the later LDR region is recomputed accordingly // Pixels covered by this frame's UI (the UI stencil pass already wrote 0x2) do not count - dd.frontFaceStencil = sd; dd.backFaceStencil = sd; } - g_rx_dss = [g_dev newDepthStencilStateWithDescriptor:dd]; - if (!g_rx_args) { MTL4ArgumentTableDescriptor *ad = [MTL4ArgumentTableDescriptor new]; ad.maxTextureBindCount = 3; ad.maxBufferBindCount = 1; - g_rx_args = [g_dev newArgumentTableWithDescriptor:ad error:&e]; - for (int i = 0; i < 3; i++) { g_rx_const[i] = [g_dev newBufferWithLength:256 options:MTLResourceStorageModeShared]; res_add(g_rx_const[i]); } } - if (!g_rx_args || !g_rx_dss) { MLOG("[RED] runtime extrapolation: cannot build the argument table / depth state"); g_rx_pso1 = nil; failed = 1; return 0; } - g_rx_fmt_c = fc; g_rx_fmt_ds = fds; MLOG("runtime extrapolation: shaders built (color format %lu, depth format %lu)", (unsigned long)fc, (unsigned long)fds); - return 1; -} -static void rx_inv4(const double *m, double *o) { // 4×4 inverse (adjugate method, double precision) - double inv[16]; - inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10]; - inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10]; - inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9]; - inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9]; - inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]; - inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]; - inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9]; - inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9]; - inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]; - inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]; - inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]; - inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]; - inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]; - inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]; - inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]; - inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]; - double det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]; det = det != 0 ? 1.0 / det : 0; - for (int i = 0; i < 16; i++) o[i] = inv[i] * det; -} -int rt_ex_encode(id<MTLTexture> dst_hdr, id<MTLTexture> dst_ds, id<MTLTexture> src_hdr, id<MTLTexture> src_ds, - const float vp_prev[16], const float vp_cur[16], uint32_t unit_bits, uint32_t ui_bits) { - if (!dst_hdr || !dst_ds || !src_hdr || !src_ds || !vp_prev || !vp_cur || !g_cb) return 0; - if (src_hdr.width != dst_hdr.width || src_hdr.height != dst_hdr.height || src_ds.width != src_hdr.width || src_ds.height != src_hdr.height - || dst_ds.width != dst_hdr.width || dst_ds.height != dst_hdr.height) return 0; // Resolution changed (loading screen / dynamic scale): no extrapolation this frame - if (!rx_setup(dst_hdr.pixelFormat, dst_ds.pixelFormat)) return 0; - if (g_rx_stv_src != src_ds) { - if (g_rx_stv) defer_obj_release(g_rx_stv); - g_rx_stv = [src_ds newTextureViewWithPixelFormat:MTLPixelFormatX32_Stencil8]; - if (!g_rx_stv) { static int n; if (!n++) MLOG("[RED] runtime extrapolation: cannot create the stencil view (src_ds needs the PixelFormatView usage)"); return 0; } - res_add(g_rx_stv); g_rx_stv_src = src_ds; - } - // Constant: M = inv(vp_prev) · vp_cur (row vectors: clip_cur = (ndc_prev, z, 1) · M); Metal's float4x4 is stored by column, and row i of the row-vector matrix serves exactly as column i ⇒ write M * v in the shader - double a[16], b[16], ia[16], m[16]; - for (int i = 0; i < 16; i++) { a[i] = vp_prev[i]; b[i] = vp_cur[i]; } - rx_inv4(a, ia); - for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) { double s = 0; for (int k = 0; k < 4; k++) s += ia[r * 4 + k] * b[k * 4 + c]; m[r * 4 + c] = s; } - // Fractional phase: pixel displacement on screen between the two frames of the world origin (any point works; orthographic + translation is uniform across the screen), d = destination − source; the source of the backward mapping = destination − d - double W = dst_hdr.width, H = dst_hdr.height; - double c0x = m[12] / m[15], c0y = m[13] / m[15]; // Source NDC origin (0, 0, 0, 1) projected into this frame - double dx = c0x / 2 * W, dy = -c0y / 2 * H, sx = -dx, sy = -dy; // Offset of the source relative to the destination (pixels) - double fx = sx - floor(sx), fy = sy - floor(sy), amount = 0.5 * 2 * (fx * (1 - fx) + fy * (1 - fy)); - float *k = g_rx_const[g_rx_ci].contents; - for (int i = 0; i < 16; i++) k[i] = (float)m[i]; - ((uint32_t *)k)[16] = unit_bits | ui_bits; k[17] = (float)amount; - id<MTLBuffer> cbuf = g_rx_const[g_rx_ci]; g_rx_ci = (g_rx_ci + 1) % 3; - rp_end(); if (g_blit) { enc_b_end(g_blit); g_blit = nil; } - // Single pass: extrapolation + hole filling + sharpening compensation by fractional phase → dst_hdr; depth is written into dst_ds; pixels covered by this frame's UI are skipped by stencil (stencil not written) - MTL4RenderPassDescriptor *rp = [MTL4RenderPassDescriptor new]; - rp.colorAttachments[0].texture = dst_hdr; rp.colorAttachments[0].loadAction = MTLLoadActionLoad; rp.colorAttachments[0].storeAction = MTLStoreActionStore; - rp.depthAttachment.texture = dst_ds; rp.depthAttachment.loadAction = MTLLoadActionLoad; rp.depthAttachment.storeAction = MTLStoreActionStore; - if (dst_ds.pixelFormat == MTLPixelFormatDepth32Float_Stencil8) { rp.stencilAttachment.texture = dst_ds; rp.stencilAttachment.loadAction = MTLLoadActionLoad; rp.stencilAttachment.storeAction = MTLStoreActionStore; } - id<MTL4RenderCommandEncoder> en = [g_cb renderCommandEncoderWithDescriptor:rp]; if (!en) return 0; - [en barrierAfterQueueStages:MTLStageAll beforeStages:MTLStageAll visibilityOptions:MTL4VisibilityOptionDevice]; - [en setArgumentTable:g_rx_args atStages:MTLRenderStageFragment]; - [g_rx_args setTexture:src_hdr.gpuResourceID atIndex:0]; [g_rx_args setTexture:src_ds.gpuResourceID atIndex:1]; [g_rx_args setTexture:g_rx_stv.gpuResourceID atIndex:2]; - [g_rx_args setAddress:cbuf.gpuAddress atIndex:0]; - [en setViewport:(MTLViewport){0, 0, W, H, 0, 1}]; [en setRenderPipelineState:g_rx_pso1]; [en setDepthStencilState:g_rx_dss]; [en setStencilReferenceValue:0]; - [en drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; m4_end(en); - args_dirty_all(); g_pso_dirty = g_dss_dirty = 1; - return 1; -} - -// Whole-image translation in pixels between two frames under an orthographic camera (rounded): destination = source + (dx, dy) -static void rt_shift_px(const float vp_prev[16], const float vp_cur[16], double W, double H, int *dx, int *dy) { - double a[16], b[16], ia[16], m[16]; - for (int i = 0; i < 16; i++) { a[i] = vp_prev[i]; b[i] = vp_cur[i]; } - rx_inv4(a, ia); - for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) { double s = 0; for (int k = 0; k < 4; k++) s += ia[r * 4 + k] * b[k * 4 + c]; m[r * 4 + c] = s; } - *dx = (int)lround(m[12] / m[15] / 2 * W); *dy = (int)lround(-m[13] / m[15] / 2 * H); -} diff --git a/dm/gfx/nvn/nvn_slots.tsv b/dm/gfx/nvn/nvn_slots.tsv deleted file mode 100644 index daf9cb6..0000000 --- a/dm/gfx/nvn/nvn_slots.tsv +++ /dev/null @@ -1,525 +0,0 @@ -# kind: evidence main NVN fetch slots and static use sites (nvn_slots.py) -name slot nuses first_use -nvnDeviceBuilderSetDefaults 320e910 1 173cb6c -nvnDeviceBuilderSetFlags 320e918 1 173cb7c -nvnDeviceBuilderGetFlags 320e920 0 - -nvnDeviceInitialize 320e928 1 173cb90 -nvnDeviceFinalize 320e930 1 173e31c -nvnDeviceSetDebugLabel 320e938 0 - -nvnDeviceGetProcAddress 320e940 1 173cba4 -nvnDeviceGetInteger 320e948 2 173cbd8 -nvnDeviceGetCurrentTimestampInNanoseconds 320e950 0 - -nvnDeviceSetIntermediateShaderCache 320e958 0 - -nvnDeviceGetTextureHandle 320e960 0 - -nvnDeviceGetTexelFetchHandle 320e968 0 - -nvnDeviceGetImageHandle 320e970 4 173ceb8 -nvnDeviceInstallDebugCallback 320e978 1 173cbb8 -nvnDeviceGenerateDebugDomainId 320e980 0 - -nvnDeviceSetWindowOriginMode 320e988 1 173cc04 -nvnDeviceSetDepthMode 320e990 1 173cc18 -nvnDeviceRegisterFastClearColor 320e998 0 - -nvnDeviceRegisterFastClearColori 320e9a0 0 - -nvnDeviceRegisterFastClearColorui 320e9a8 0 - -nvnDeviceRegisterFastClearDepth 320e9b0 0 - -nvnDeviceGetWindowOriginMode 320e9b8 0 - -nvnDeviceGetDepthMode 320e9c0 0 - -nvnDeviceGetTimestampInNanoseconds 320e9c8 0 - -nvnDeviceApplyDeferredFinalizes 320e9d0 0 - -nvnDeviceFinalizeCommandHandle 320e9d8 1 17443fc -nvnDeviceWalkDebugDatabase 320e9e0 0 - -nvnDeviceGetSeparateTextureHandle 320e9e8 4 173ced0 -nvnDeviceGetSeparateSamplerHandle 320e9f0 7 173cee8 -nvnDeviceIsExternalDebuggerAttached 320e9f8 0 - -nvnQueueGetError 320ea00 3 1744464 -nvnQueueGetTotalCommandMemoryUsed 320ea08 0 - -nvnQueueGetTotalControlMemoryUsed 320ea10 0 - -nvnQueueGetTotalComputeMemoryUsed 320ea18 0 - -nvnQueueResetMemoryUsageCounts 320ea20 0 - -nvnQueueBuilderSetDevice 320ea28 1 173d3e8 -nvnQueueBuilderSetDefaults 320ea30 1 173d3c4 -nvnQueueBuilderSetFlags 320ea38 1 173d3d4 -nvnQueueBuilderSetCommandMemorySize 320ea40 1 173d410 -nvnQueueBuilderSetComputeMemorySize 320ea48 1 173d3fc -nvnQueueBuilderSetControlMemorySize 320ea50 1 173d424 -nvnQueueBuilderGetQueueMemorySize 320ea58 1 173d44c -nvnQueueBuilderSetQueueMemory 320ea60 1 173d47c -nvnQueueBuilderSetCommandFlushThreshold 320ea68 1 173d438 -nvnQueueBuilderSetQueuePriority 320ea70 0 - -nvnQueueBuilderGetQueuePriority 320ea78 0 - -nvnQueueBuilderGetDevice 320ea80 0 - -nvnQueueBuilderGetFlags 320ea88 0 - -nvnQueueBuilderGetCommandMemorySize 320ea90 0 - -nvnQueueBuilderGetComputeMemorySize 320ea98 0 - -nvnQueueBuilderGetControlMemorySize 320eaa0 0 - -nvnQueueBuilderGetCommandFlushThreshold 320eaa8 0 - -nvnQueueBuilderGetMemorySize 320eab0 0 - -nvnQueueBuilderGetMemory 320eab8 0 - -nvnQueueInitialize 320eac0 1 173d494 -nvnQueueFinalize 320eac8 1 1743ea4 -nvnQueueSetDebugLabel 320ead0 0 - -nvnQueueSubmitCommands 320ead8 1 17443d4 -nvnQueueFlush 320eae0 1 1744448 -nvnQueueFinish 320eae8 1 17444e0 -nvnQueuePresentTexture 320eaf0 1 1744978 -nvnQueueAcquireTexture 320eaf8 0 - -nvnWindowBuilderSetDevice 320eb00 1 17414e8 -nvnWindowBuilderSetDefaults 320eb08 1 17414fc -nvnWindowBuilderSetNativeWindow 320eb10 1 1741514 -nvnWindowBuilderSetTextures 320eb18 1 1741528 -nvnWindowBuilderSetPresentInterval 320eb20 1 1741540 -nvnWindowBuilderSetNumActiveTextures 320eb28 0 - -nvnWindowBuilderGetDevice 320eb30 0 - -nvnWindowBuilderGetNumTextures 320eb38 0 - -nvnWindowBuilderGetTexture 320eb40 0 - -nvnWindowBuilderGetNativeWindow 320eb48 0 - -nvnWindowBuilderGetPresentInterval 320eb50 0 - -nvnWindowBuilderGetNumActiveTextures 320eb58 0 - -nvnWindowInitialize 320eb60 1 174154c -nvnWindowFinalize 320eb68 1 1742db4 -nvnWindowSetDebugLabel 320eb70 0 - -nvnWindowAcquireTexture 320eb78 1 1743b38 -nvnWindowGetNativeWindow 320eb80 0 - -nvnWindowGetPresentInterval 320eb88 0 - -nvnWindowSetPresentInterval 320eb90 1 1743af8 -nvnWindowSetCrop 320eb98 1 1743ad0 -nvnWindowGetCrop 320eba0 0 - -nvnWindowSetNumActiveTextures 320eba8 0 - -nvnWindowGetNumActiveTextures 320ebb0 0 - -nvnWindowGetNumTextures 320ebb8 0 - -nvnProgramInitialize 320ebc0 7 173f0d8 -nvnProgramFinalize 320ebc8 7 17403c4 -nvnProgramSetDebugLabel 320ebd0 0 - -nvnProgramSetShaders 320ebd8 7 173f100 -nvnProgramSetShadersExt 320ebe0 0 - -nvnProgramSetSampleShading 320ebe8 0 - -nvnMemoryPoolBuilderSetDevice 320ebf0 2 174947c -nvnMemoryPoolBuilderSetDefaults 320ebf8 2 174946c -nvnMemoryPoolBuilderSetStorage 320ec00 2 1749498 -nvnMemoryPoolBuilderSetFlags 320ec08 2 17494c4 -nvnMemoryPoolBuilderGetDevice 320ec10 0 - -nvnMemoryPoolBuilderGetMemory 320ec18 0 - -nvnMemoryPoolBuilderGetSize 320ec20 0 - -nvnMemoryPoolBuilderGetFlags 320ec28 0 - -nvnMemoryPoolInitialize 320ec30 2 17494d4 -nvnMemoryPoolSetDebugLabel 320ec38 0 - -nvnMemoryPoolFinalize 320ec40 8 173e088 -nvnMemoryPoolMap 320ec48 0 - -nvnMemoryPoolFlushMappedRange 320ec50 2 173d9a4 -nvnMemoryPoolInvalidateMappedRange 320ec58 0 - -nvnMemoryPoolGetBufferAddress 320ec60 12 173cd40 -nvnMemoryPoolMapVirtual 320ec68 0 - -nvnMemoryPoolGetSize 320ec70 0 - -nvnMemoryPoolGetFlags 320ec78 0 - -nvnTexturePoolInitialize 320ec80 1 173cdec -nvnTexturePoolSetDebugLabel 320ec88 0 - -nvnTexturePoolFinalize 320ec90 1 173de88 -nvnTexturePoolRegisterTexture 320ec98 5 173ec3c -nvnTexturePoolRegisterImage 320eca0 0 - -nvnTexturePoolGetMemoryPool 320eca8 0 - -nvnTexturePoolGetMemoryOffset 320ecb0 0 - -nvnTexturePoolGetSize 320ecb8 0 - -nvnSamplerPoolInitialize 320ecc0 1 173ce88 -nvnSamplerPoolSetDebugLabel 320ecc8 0 - -nvnSamplerPoolFinalize 320ecd0 1 173de9c -nvnSamplerPoolRegisterSampler 320ecd8 1 1740d6c -nvnSamplerPoolRegisterSamplerBuilder 320ece0 0 - -nvnSamplerPoolGetMemoryPool 320ece8 0 - -nvnSamplerPoolGetMemoryOffset 320ecf0 0 - -nvnSamplerPoolGetSize 320ecf8 0 - -nvnBufferBuilderSetDevice 320ed00 0 - -nvnBufferBuilderSetDefaults 320ed08 0 - -nvnBufferBuilderSetStorage 320ed10 0 - -nvnBufferBuilderGetDevice 320ed18 0 - -nvnBufferBuilderGetMemoryPool 320ed20 0 - -nvnBufferBuilderGetMemoryOffset 320ed28 0 - -nvnBufferBuilderGetSize 320ed30 0 - -nvnBufferInitialize 320ed38 0 - -nvnBufferSetDebugLabel 320ed40 0 - -nvnBufferFinalize 320ed48 0 - -nvnBufferMap 320ed50 0 - -nvnBufferGetAddress 320ed58 0 - -nvnBufferFlushMappedRange 320ed60 0 - -nvnBufferInvalidateMappedRange 320ed68 0 - -nvnBufferGetMemoryPool 320ed70 0 - -nvnBufferGetMemoryOffset 320ed78 0 - -nvnBufferGetSize 320ed80 0 - -nvnBufferGetDebugID 320ed88 0 - -nvnTextureBuilderSetDevice 320ed90 2 173ead8 -nvnTextureBuilderSetDefaults 320ed98 2 173eaec -nvnTextureBuilderSetFlags 320eda0 4 173eb04 -nvnTextureBuilderSetTarget 320eda8 2 173eb48 -nvnTextureBuilderSetWidth 320edb0 1 1741be4 -nvnTextureBuilderSetHeight 320edb8 1 1741c40 -nvnTextureBuilderSetDepth 320edc0 1 1741c50 -nvnTextureBuilderSetSize1D 320edc8 1 173eb3c -nvnTextureBuilderSetSize2D 320edd0 0 - -nvnTextureBuilderSetSize3D 320edd8 0 - -nvnTextureBuilderSetLevels 320ede0 1 1741c6c -nvnTextureBuilderSetFormat 320ede8 2 173eb24 -nvnTextureBuilderSetSamples 320edf0 1 1741c98 -nvnTextureBuilderSetSwizzle 320edf8 0 - -nvnTextureBuilderSetDepthStencilMode 320ee00 0 - -nvnTextureBuilderGetStorageSize 320ee08 3 1741760 -nvnTextureBuilderGetStorageAlignment 320ee10 2 1741780 -nvnTextureBuilderSetStorage 320ee18 3 173eb70 -nvnTextureBuilderSetPackagedTextureData 320ee20 0 - -nvnTextureBuilderSetPackagedTextureLayout 320ee28 1 1741b24 -nvnTextureBuilderSetStride 320ee30 1 1741b78 -nvnTextureBuilderSetGLTextureName 320ee38 0 - -nvnTextureBuilderGetStorageClass 320ee40 0 - -nvnTextureBuilderGetDevice 320ee48 0 - -nvnTextureBuilderGetFlags 320ee50 2 1741728 -nvnTextureBuilderGetTarget 320ee58 0 - -nvnTextureBuilderGetWidth 320ee60 0 - -nvnTextureBuilderGetHeight 320ee68 0 - -nvnTextureBuilderGetDepth 320ee70 0 - -nvnTextureBuilderGetLevels 320ee78 0 - -nvnTextureBuilderGetFormat 320ee80 0 - -nvnTextureBuilderGetSamples 320ee88 0 - -nvnTextureBuilderGetSwizzle 320ee90 0 - -nvnTextureBuilderGetDepthStencilMode 320ee98 0 - -nvnTextureBuilderGetPackagedTextureData 320eea0 0 - -nvnTextureBuilderGetPackagedTextureLayout 320eea8 0 - -nvnTextureBuilderGetStride 320eeb0 0 - -nvnTextureBuilderGetSparseTileLayout 320eeb8 0 - -nvnTextureBuilderGetGLTextureName 320eec0 0 - -nvnTextureBuilderGetZCullStorageSize 320eec8 0 - -nvnTextureBuilderGetMemoryPool 320eed0 0 - -nvnTextureBuilderGetMemoryOffset 320eed8 0 - -nvnTextureBuilderGetRawStorageClass 320eee0 0 - -nvnTextureViewSetDefaults 320eee8 8 174071c -nvnTextureViewSetLevels 320eef0 10 1740734 -nvnTextureViewSetLayers 320eef8 9 1740754 -nvnTextureViewSetFormat 320ef00 3 1740788 -nvnTextureViewSetSwizzle 320ef08 0 - -nvnTextureViewSetDepthStencilMode 320ef10 0 - -nvnTextureViewSetTarget 320ef18 3 17407b0 -nvnTextureViewGetLevels 320ef20 0 - -nvnTextureViewGetLayers 320ef28 2 1745844 -nvnTextureViewGetFormat 320ef30 0 - -nvnTextureViewGetSwizzle 320ef38 0 - -nvnTextureViewGetDepthStencilMode 320ef40 0 - -nvnTextureViewGetTarget 320ef48 0 - -nvnTextureViewCompare 320ef50 0 - -nvnTextureInitialize 320ef58 3 173eb84 -nvnTextureGetZCullStorageSize 320ef60 0 - -nvnTextureFinalize 320ef68 4 174214c -nvnTextureSetDebugLabel 320ef70 0 - -nvnTextureGetStorageClass 320ef78 0 - -nvnTextureGetViewOffset 320ef80 2 17433b4 -nvnTextureGetFlags 320ef88 2 1742ee4 -nvnTextureGetTarget 320ef90 0 - -nvnTextureGetWidth 320ef98 7 174564c -nvnTextureGetHeight 320efa0 7 1745668 -nvnTextureGetDepth 320efa8 7 174567c -nvnTextureGetLevels 320efb0 0 - -nvnTextureGetFormat 320efb8 0 - -nvnTextureGetSamples 320efc0 0 - -nvnTextureGetSwizzle 320efc8 0 - -nvnTextureGetDepthStencilMode 320efd0 0 - -nvnTextureGetStride 320efd8 0 - -nvnTextureGetTextureAddress 320efe0 0 - -nvnTextureGetSparseTileLayout 320efe8 0 - -nvnTextureWriteTexels 320eff0 0 - -nvnTextureWriteTexelsStrided 320eff8 0 - -nvnTextureReadTexels 320f000 16 1843924 -nvnTextureReadTexelsStrided 320f008 1 18ca2d8 -nvnTextureFlushTexels 320f010 0 - -nvnTextureInvalidateTexels 320f018 1 1883124 -nvnTextureGetMemoryPool 320f020 0 - -nvnTextureGetMemoryOffset 320f028 0 - -nvnTextureGetStorageSize 320f030 3 1743430 -nvnTextureCompare 320f038 2 18d6fc8 -nvnTextureGetDebugID 320f040 0 - -nvnTextureGetRawStorageClass 320f048 8 174899c -nvnSamplerBuilderSetDevice 320f050 1 1740bc8 -nvnSamplerBuilderSetDefaults 320f058 1 1740be8 -nvnSamplerBuilderSetMinMagFilter 320f060 2 1740c18 -nvnSamplerBuilderSetWrapMode 320f068 1 1740c80 -nvnSamplerBuilderSetLodClamp 320f070 1 1883364 -nvnSamplerBuilderSetLodBias 320f078 1 1883328 -nvnSamplerBuilderSetCompare 320f080 1 1740ca4 -nvnSamplerBuilderSetBorderColor 320f088 2 1740cdc -nvnSamplerBuilderSetBorderColori 320f090 0 - -nvnSamplerBuilderSetBorderColorui 320f098 0 - -nvnSamplerBuilderSetMaxAnisotropy 320f0a0 1 1740c08 -nvnSamplerBuilderSetReductionFilter 320f0a8 0 - -nvnSamplerBuilderSetLodSnap 320f0b0 1 1740cc0 -nvnSamplerBuilderGetDevice 320f0b8 0 - -nvnSamplerBuilderGetMinMagFilter 320f0c0 1 18832ec -nvnSamplerBuilderGetWrapMode 320f0c8 3 18ca2d0 -nvnSamplerBuilderGetLodClamp 320f0d0 0 - -nvnSamplerBuilderGetLodBias 320f0d8 0 - -nvnSamplerBuilderGetCompare 320f0e0 0 - -nvnSamplerBuilderGetBorderColor 320f0e8 0 - -nvnSamplerBuilderGetBorderColori 320f0f0 0 - -nvnSamplerBuilderGetBorderColorui 320f0f8 0 - -nvnSamplerBuilderGetMaxAnisotropy 320f100 0 - -nvnSamplerBuilderGetReductionFilter 320f108 0 - -nvnSamplerBuilderGetLodSnap 320f110 0 - -nvnSamplerInitialize 320f118 1 1740cec -nvnSamplerFinalize 320f120 2 1742920 -nvnSamplerSetDebugLabel 320f128 0 - -nvnSamplerGetMinMagFilter 320f130 0 - -nvnSamplerGetWrapMode 320f138 0 - -nvnSamplerGetLodClamp 320f140 0 - -nvnSamplerGetLodBias 320f148 0 - -nvnSamplerGetCompare 320f150 0 - -nvnSamplerGetBorderColor 320f158 0 - -nvnSamplerGetBorderColori 320f160 0 - -nvnSamplerGetBorderColorui 320f168 0 - -nvnSamplerGetMaxAnisotropy 320f170 0 - -nvnSamplerGetReductionFilter 320f178 0 - -nvnSamplerCompare 320f180 0 - -nvnSamplerGetDebugID 320f188 0 - -nvnBlendStateSetDefaults 320f190 1 173fd44 -nvnBlendStateSetBlendTarget 320f198 1 173fd54 -nvnBlendStateSetBlendFunc 320f1a0 1 173fd8c -nvnBlendStateSetBlendEquation 320f1a8 1 173fdac -nvnBlendStateSetAdvancedMode 320f1b0 0 - -nvnBlendStateSetAdvancedOverlap 320f1b8 0 - -nvnBlendStateSetAdvancedPremultipliedSrc 320f1c0 0 - -nvnBlendStateSetAdvancedNormalizedDst 320f1c8 0 - -nvnBlendStateGetBlendTarget 320f1d0 0 - -nvnBlendStateGetBlendFunc 320f1d8 0 - -nvnBlendStateGetBlendEquation 320f1e0 0 - -nvnBlendStateGetAdvancedMode 320f1e8 0 - -nvnBlendStateGetAdvancedOverlap 320f1f0 0 - -nvnBlendStateGetAdvancedPremultipliedSrc 320f1f8 0 - -nvnBlendStateGetAdvancedNormalizedDst 320f200 0 - -nvnColorStateSetDefaults 320f208 1 173fce4 -nvnColorStateSetBlendEnable 320f210 1 173fd2c -nvnColorStateSetLogicOp 320f218 0 - -nvnColorStateSetAlphaTest 320f220 0 - -nvnColorStateGetBlendEnable 320f228 0 - -nvnColorStateGetLogicOp 320f230 0 - -nvnColorStateGetAlphaTest 320f238 0 - -nvnChannelMaskStateSetDefaults 320f240 1 173fcf0 -nvnChannelMaskStateSetChannelMask 320f248 1 173fdc8 -nvnChannelMaskStateGetChannelMask 320f250 0 - -nvnMultisampleStateSetDefaults 320f258 1 1740000 -nvnMultisampleStateSetMultisampleEnable 320f260 1 1740020 -nvnMultisampleStateSetSamples 320f268 1 1740044 -nvnMultisampleStateSetAlphaToCoverageEnable 320f270 1 1740054 -nvnMultisampleStateSetAlphaToCoverageDither 320f278 0 - -nvnMultisampleStateGetMultisampleEnable 320f280 0 - -nvnMultisampleStateGetSamples 320f288 0 - -nvnMultisampleStateGetAlphaToCoverageEnable 320f290 0 - -nvnMultisampleStateGetAlphaToCoverageDither 320f298 0 - -nvnMultisampleStateSetRasterSamples 320f2a0 0 - -nvnMultisampleStateGetRasterSamples 320f2a8 0 - -nvnMultisampleStateSetCoverageModulationMode 320f2b0 0 - -nvnMultisampleStateGetCoverageModulationMode 320f2b8 0 - -nvnMultisampleStateSetCoverageToColorEnable 320f2c0 0 - -nvnMultisampleStateGetCoverageToColorEnable 320f2c8 0 - -nvnMultisampleStateSetCoverageToColorOutput 320f2d0 0 - -nvnMultisampleStateGetCoverageToColorOutput 320f2d8 0 - -nvnMultisampleStateSetSampleLocationsEnable 320f2e0 1 17466d8 -nvnMultisampleStateGetSampleLocationsEnable 320f2e8 0 - -nvnMultisampleStateGetSampleLocationsGrid 320f2f0 0 - -nvnMultisampleStateSetSampleLocationsGridEnable 320f2f8 0 - -nvnMultisampleStateGetSampleLocationsGridEnable 320f300 0 - -nvnMultisampleStateSetSampleLocations 320f308 1 17466c4 -nvnPolygonStateSetDefaults 320f310 1 173ff54 -nvnPolygonStateSetCullFace 320f318 1 173ff94 -nvnPolygonStateSetFrontFace 320f320 1 173ffac -nvnPolygonStateSetPolygonMode 320f328 1 173ff70 -nvnPolygonStateSetPolygonOffsetEnables 320f330 1 173ffc8 -nvnPolygonStateGetCullFace 320f338 0 - -nvnPolygonStateGetFrontFace 320f340 0 - -nvnPolygonStateGetPolygonMode 320f348 0 - -nvnPolygonStateGetPolygonOffsetEnables 320f350 0 - -nvnDepthStencilStateSetDefaults 320f358 1 173fe28 -nvnDepthStencilStateSetDepthTestEnable 320f360 1 173fe6c -nvnDepthStencilStateSetDepthWriteEnable 320f368 1 173fe58 -nvnDepthStencilStateSetDepthFunc 320f370 1 173fea4 -nvnDepthStencilStateSetStencilTestEnable 320f378 1 173fe80 -nvnDepthStencilStateSetStencilFunc 320f380 2 173febc -nvnDepthStencilStateSetStencilOp 320f388 2 173ff04 -nvnDepthStencilStateGetDepthTestEnable 320f390 0 - -nvnDepthStencilStateGetDepthWriteEnable 320f398 0 - -nvnDepthStencilStateGetDepthFunc 320f3a0 0 - -nvnDepthStencilStateGetStencilTestEnable 320f3a8 0 - -nvnDepthStencilStateGetStencilFunc 320f3b0 0 - -nvnDepthStencilStateGetStencilOp 320f3b8 0 - -nvnVertexAttribStateSetDefaults 320f3c0 1 174027c -nvnVertexAttribStateSetFormat 320f3c8 1 1740348 -nvnVertexAttribStateSetStreamIndex 320f3d0 1 174035c -nvnVertexAttribStateGetFormat 320f3d8 0 - -nvnVertexAttribStateGetStreamIndex 320f3e0 0 - -nvnVertexStreamStateSetDefaults 320f3e8 1 1742014 -nvnVertexStreamStateSetStride 320f3f0 1 1742030 -nvnVertexStreamStateSetDivisor 320f3f8 1 1742044 -nvnVertexStreamStateGetStride 320f400 0 - -nvnVertexStreamStateGetDivisor 320f408 0 - -nvnCommandBufferInitialize 320f410 4 173d4c8 -nvnCommandBufferFinalize 320f418 7 17423e8 -nvnCommandBufferSetDebugLabel 320f420 0 - -nvnCommandBufferSetMemoryCallback 320f428 3 173ef60 -nvnCommandBufferSetMemoryCallbackData 320f430 3 173ef78 -nvnCommandBufferSetCommandMemoryCallbackEnabled 320f438 0 - -nvnCommandBufferAddCommandMemory 320f440 2 173d5ac -nvnCommandBufferAddControlMemory 320f448 2 173d674 -nvnCommandBufferGetCommandMemorySize 320f450 0 - -nvnCommandBufferGetCommandMemoryUsed 320f458 0 - -nvnCommandBufferGetCommandMemoryFree 320f460 0 - -nvnCommandBufferGetControlMemorySize 320f468 0 - -nvnCommandBufferGetControlMemoryUsed 320f470 0 - -nvnCommandBufferGetControlMemoryFree 320f478 0 - -nvnCommandBufferBeginRecording 320f480 3 173d684 -nvnCommandBufferEndRecording 320f488 3 173d6ec -nvnCommandBufferCallCommands 320f490 0 - -nvnCommandBufferCopyCommands 320f498 1 17e2f88 -nvnCommandBufferBindBlendState 320f4a0 1 1746618 -nvnCommandBufferBindChannelMaskState 320f4a8 1 1746644 -nvnCommandBufferBindColorState 320f4b0 1 174665c -nvnCommandBufferBindMultisampleState 320f4b8 2 17466ec -nvnCommandBufferBindPolygonState 320f4c0 1 1746674 -nvnCommandBufferBindDepthStencilState 320f4c8 1 1746578 -nvnCommandBufferBindVertexAttribState 320f4d0 1 1746740 -nvnCommandBufferBindVertexStreamState 320f4d8 1 174675c -nvnCommandBufferBindProgram 320f4e0 4 174671c -nvnCommandBufferBindVertexBuffer 320f4e8 0 - -nvnCommandBufferBindVertexBuffers 320f4f0 2 17472a8 -nvnCommandBufferBindUniformBuffer 320f4f8 9 174690c -nvnCommandBufferBindUniformBuffers 320f500 3 1746898 -nvnCommandBufferBindTransformFeedbackBuffer 320f508 0 - -nvnCommandBufferBindTransformFeedbackBuffers 320f510 0 - -nvnCommandBufferBindStorageBuffer 320f518 2 174a160 -nvnCommandBufferBindStorageBuffers 320f520 3 1746868 -nvnCommandBufferBindTexture 320f528 0 - -nvnCommandBufferBindTextures 320f530 0 - -nvnCommandBufferBindImage 320f538 0 - -nvnCommandBufferBindImages 320f540 3 1746814 -nvnCommandBufferSetPatchSize 320f548 1 1746780 -nvnCommandBufferSetInnerTessellationLevels 320f550 0 - -nvnCommandBufferSetOuterTessellationLevels 320f558 0 - -nvnCommandBufferSetPrimitiveRestart 320f560 0 - -nvnCommandBufferBeginTransformFeedback 320f568 0 - -nvnCommandBufferEndTransformFeedback 320f570 0 - -nvnCommandBufferPauseTransformFeedback 320f578 0 - -nvnCommandBufferResumeTransformFeedback 320f580 0 - -nvnCommandBufferDrawTransformFeedback 320f588 0 - -nvnCommandBufferDrawArrays 320f590 2 17473a0 -nvnCommandBufferDrawElements 320f598 0 - -nvnCommandBufferDrawElementsBaseVertex 320f5a0 0 - -nvnCommandBufferDrawArraysInstanced 320f5a8 0 - -nvnCommandBufferDrawElementsInstanced 320f5b0 2 17474d4 -nvnCommandBufferDrawArraysIndirect 320f5b8 0 - -nvnCommandBufferDrawElementsIndirect 320f5c0 0 - -nvnCommandBufferMultiDrawArraysIndirectCount 320f5c8 2 1747614 -nvnCommandBufferMultiDrawElementsIndirectCount 320f5d0 2 1747784 -nvnCommandBufferClearColor 320f5d8 1 1745c98 -nvnCommandBufferClearColori 320f5e0 0 - -nvnCommandBufferClearColorui 320f5e8 0 - -nvnCommandBufferClearDepthStencil 320f5f0 1 1745d00 -nvnCommandBufferDispatchCompute 320f5f8 3 1747f7c -nvnCommandBufferDispatchComputeIndirect 320f600 2 174809c -nvnCommandBufferSetViewport 320f608 0 - -nvnCommandBufferSetViewports 320f610 2 1747894 -nvnCommandBufferSetViewportSwizzles 320f618 0 - -nvnCommandBufferSetScissor 320f620 4 1745c3c -nvnCommandBufferSetScissors 320f628 0 - -nvnCommandBufferSetDepthRange 320f630 2 17478bc -nvnCommandBufferSetDepthBounds 320f638 3 17465ec -nvnCommandBufferSetDepthRanges 320f640 0 - -nvnCommandBufferSetTiledCacheAction 320f648 0 - -nvnCommandBufferSetTiledCacheTileSize 320f650 0 - -nvnCommandBufferBindSeparateTexture 320f658 0 - -nvnCommandBufferBindSeparateSampler 320f660 4 1746464 -nvnCommandBufferBindSeparateTextures 320f668 3 1746880 -nvnCommandBufferBindSeparateSamplers 320f670 2 17468b0 -nvnCommandBufferSetStencilValueMask 320f678 1 17465a4 -nvnCommandBufferSetStencilMask 320f680 1 17465c0 -nvnCommandBufferSetStencilRef 320f688 2 17479fc -nvnCommandBufferSetBlendColor 320f690 2 1747a2c -nvnCommandBufferSetPointSize 320f698 0 - -nvnCommandBufferSetLineWidth 320f6a0 0 - -nvnCommandBufferSetPolygonOffsetClamp 320f6a8 1 1746694 -nvnCommandBufferSetAlphaRef 320f6b0 0 - -nvnCommandBufferSetSampleMask 320f6b8 0 - -nvnCommandBufferSetRasterizerDiscard 320f6c0 0 - -nvnCommandBufferSetDepthClamp 320f6c8 0 - -nvnCommandBufferSetConservativeRasterEnable 320f6d0 0 - -nvnCommandBufferSetConservativeRasterDilate 320f6d8 0 - -nvnCommandBufferSetSubpixelPrecisionBias 320f6e0 0 - -nvnCommandBufferCopyBufferToTexture 320f6e8 1 1745390 -nvnCommandBufferCopyTextureToBuffer 320f6f0 1 17454cc -nvnCommandBufferCopyTextureToTexture 320f6f8 3 1745238 -nvnCommandBufferCopyBufferToBuffer 320f700 4 1745050 -nvnCommandBufferClearBuffer 320f708 0 - -nvnCommandBufferClearTexture 320f710 2 1745894 -nvnCommandBufferClearTexturei 320f718 0 - -nvnCommandBufferClearTextureui 320f720 3 17460c0 -nvnCommandBufferUpdateUniformBuffer 320f728 1 174a140 -nvnCommandBufferReportCounter 320f730 8 1748f24 -nvnCommandBufferResetCounter 320f738 2 1744d60 -nvnCommandBufferReportValue 320f740 0 - -nvnCommandBufferSetRenderEnable 320f748 0 - -nvnCommandBufferSetRenderEnableConditional 320f750 0 - -nvnCommandBufferSetRenderTargets 320f758 1 1745bdc -nvnCommandBufferDiscardColor 320f760 0 - -nvnCommandBufferDiscardDepthStencil 320f768 0 - -nvnCommandBufferDownsample 320f770 0 - -nvnCommandBufferTiledDownsample 320f778 0 - -nvnCommandBufferDownsampleTextureView 320f780 2 1745e54 -nvnCommandBufferTiledDownsampleTextureView 320f788 0 - -nvnCommandBufferBarrier 320f790 33 1744e74 -nvnCommandBufferWaitSync 320f798 4 1748280 -nvnCommandBufferFenceSync 320f7a0 0 - -nvnCommandBufferSetTexturePool 320f7a8 1 173d6c0 -nvnCommandBufferSetSamplerPool 320f7b0 1 173d6dc -nvnCommandBufferSetShaderScratchMemory 320f7b8 1 173d6a8 -nvnCommandBufferSaveZCullData 320f7c0 0 - -nvnCommandBufferRestoreZCullData 320f7c8 0 - -nvnCommandBufferSetCopyRowStride 320f7d0 2 1745368 -nvnCommandBufferSetCopyImageStride 320f7d8 0 - -nvnCommandBufferGetCopyRowStride 320f7e0 0 - -nvnCommandBufferGetCopyImageStride 320f7e8 0 - -nvnCommandBufferDrawTexture 320f7f0 0 - -nvnProgramSetSubroutineLinkage 320f7f8 0 - -nvnCommandBufferSetProgramSubroutines 320f800 0 - -nvnCommandBufferBindCoverageModulationTable 320f808 0 - -nvnCommandBufferResolveDepthBuffer 320f810 0 - -nvnCommandBufferSetColorReductionEnable 320f818 0 - -nvnCommandBufferSetColorReductionThresholds 320f820 0 - -nvnCommandBufferPushDebugGroupStatic 320f828 0 - -nvnCommandBufferPushDebugGroupDynamic 320f830 2 1748c88 -nvnCommandBufferPushDebugGroup 320f838 0 - -nvnCommandBufferPopDebugGroup 320f840 2 1748d68 -nvnCommandBufferPopDebugGroupId 320f848 0 - -nvnCommandBufferInsertDebugMarkerStatic 320f850 0 - -nvnCommandBufferInsertDebugMarkerDynamic 320f858 2 1748e18 -nvnCommandBufferInsertDebugMarker 320f860 0 - -nvnCommandBufferGetMemoryCallback 320f868 0 - -nvnCommandBufferGetMemoryCallbackData 320f870 0 - -nvnCommandBufferIsRecording 320f878 2 1744f14 -nvnSyncInitialize 320f880 2 173cf14 -nvnSyncFinalize 320f888 2 173dffc -nvnSyncSetDebugLabel 320f890 0 - -nvnQueueFenceSync 320f898 1 1744420 -nvnSyncWait 320f8a0 2 17439fc -nvnQueueWaitSync 320f8a8 0 - -nvnSyncInitializeFromFencedGLSync 320f8b0 0 - -nvnSyncCreateGLSync 320f8b8 0 - -nvnEventBuilderSetDefaults 320f8c0 2 173f7a4 -nvnEventBuilderSetStorage 320f8c8 2 173f7b8 -nvnEventBuilderGetStorage 320f8d0 0 - -nvnEventBuilderGetMemoryPool 320f8d8 0 - -nvnEventBuilderGetMemoryOffset 320f8e0 0 - -nvnEventInitialize 320f8e8 2 173f7c8 -nvnEventFinalize 320f8f0 2 17424fc -nvnEventGetValue 320f8f8 1 1743a78 -nvnEventSignal 320f900 2 173f7e0 -nvnEventGetMemoryPool 320f908 0 - -nvnEventGetMemoryOffset 320f910 0 - -nvnCommandBufferWaitEvent 320f918 4 1744130 -nvnCommandBufferSignalEvent 320f920 8 17440b8 -nvnCommandBufferSetStencilCullCriteria 320f928 0 - -nvnCommandBufferInsertCheckpointStatic 320f930 0 - -nvnCommandBufferInsertCheckpointDynamic 320f938 0 - -nvnDeviceSetAutomaticCheckpointsEnable 320f940 0 - -nvnDeviceSetAutomaticCheckpointsSamplingInterval 320f948 0 - -nvnDeviceSetAutomaticCheckpointsByDebugGroup 320f950 0 - -nvnDeviceSetCheckpointCallStacksEnable 320f958 0 - -nvnDeviceSetCheckpointMaxCallStacksDepth 320f960 0 - diff --git a/dm/gfx/nvn_metal_mapping.md b/dm/gfx/nvn_metal_mapping.md index 814cf99..bd670f2 100644 --- a/dm/gfx/nvn_metal_mapping.md +++ b/dm/gfx/nvn_metal_mapping.md @@ -1,8 +1,8 @@ # NVN → Metal object mapping -This document lists what each kind of NVN (the Switch's native graphics interface) object and command lands on in Metal within D2R Mobile's graphics layer. For the overall structure of the graphics layer see [README.md](README.md). +This document lists what each kind of NVN (the Switch's native graphics interface) object and command lands on in Metal in the host's graphics layer. For the overall structure of the graphics layer see [README.md](README.md). -Scope: the 216 NVN functions that have call sites in the game's main program (listed in `nvn/nvn_slots.tsv`). The remaining 307 are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log. +Scope: the NVN functions games call. The others are connected to a red-reporting stub that, when called, prints `[RED] NVN unimplemented function called: <name>` in the log. Two conventions that run through the whole table: diff --git a/dm/gfx/tests/t_dirty_race.c b/dm/gfx/tests/t_dirty_race.c index 3b91eeb..6cf9e2e 100644 --- a/dm/gfx/tests/t_dirty_race.c +++ b/dm/gfx/tests/t_dirty_race.c @@ -1,4 +1,4 @@ -// Ordering race between "set read-only" and "unprotect before write" in dirty-page tracking (from d2r-mobile ef9b1ff13's t_dirty_race.c, stubs adapted to this repository's tracking table). +// Ordering race between "set read-only" and "unprotect before write" in dirty-page tracking (originally a test of the host this one started as, stubs adapted to this repository's tracking table). // Scene: thread A, in nvn_dirty_protect, has already recorded the page state as "protected" while its mprotect has not landed yet; thread B now arrives with prep_write (the unprotect done before the host writes into guest memory, // for example the movie decoder thread writing a frame into texture storage, or a disk read into a guest buffer). Old implementation: B sets the state back to "none", then A's mprotect lands => the page is read-only while its state is "none" -- // when the host reads a file into it again: prep_write sees state "none" and skips, the system call does not go through the page-fault handler and fails with EFAULT; when a game thread writes directly: the page-fault handler only returns "re-execute" and spins forever. diff --git a/dm/gfx/tex/README.md b/dm/gfx/tex/README.md index 641a071..bd405d6 100644 --- a/dm/gfx/tex/README.md +++ b/dm/gfx/tex/README.md @@ -26,7 +26,6 @@ Only GPUs with BC compressed textures are supported (`supportsBCTextureCompressi ## Tests ```sh -make -C dm/gfx/tex/tests samples # fetch real-data samples (see below) make -C dm/gfx/tex/tests run # run t_fmt_mtl and t_tex ``` @@ -38,25 +37,11 @@ make -C dm/gfx/tex/tests run # run t_fmt_mtl and t_tex | A round trip | Writes random data into each level and layer and reads it back out; must be byte-for-byte equal. Covers 2D, 2D array and 3D, several bytes-per-block values and non-power-of-two sizes | | B address formula | The result of `nvn_bl_offset()` equals the address computed by hand from the public layout rules | | E hand-made 3D | Places markers at hand-computed addresses, checks where they land after deswizzling, and checks block height, block depth and storage size | - | C real data | The Switch version (block linear) of a texture, after deswizzling, is compared with the PC version (linear) of the same texture: those at the same resolution must be byte-for-byte equal; those whose Switch version was reduced in resolution were recompressed, so the comparison is instead that the PSNR after decoding (peak signal-to-noise ratio, a measure of how alike two images are) is not below 22 dB. It also checks that the per-level offsets in the file agree with `nvn_tex_level_offset` | - | D negative control | Deliberately deswizzles with the block height off by one step; the result must no longer be byte-for-byte equal and the PSNR must be below 22 dB, proving that the criterion of group C can tell a wrong layout apart | - The A round trip can only prove "one-to-one and within bounds"; it cannot prove that the layout formula is right. What really backs it up is the hand-computed addresses of group B and the byte-for-byte equality of the same-resolution samples of group C. - -**About the real-data samples**: groups C and D need one copy each of the Switch version and the PC version of the same texture. `tests/fetch_samples.py` takes samples from the game files in your workspace: the Switch version is read from `$DM_ROOT/data/switch/3.3.94012/Update/RomFS/data`, the PC version from `$DM_ROOT/data/assets-src/raw`, and the output goes to `$DM_ROOT/build/cache/dm/tex_samples`. When there are no samples, the real-data items of `t_tex` count as failed and are not skipped; groups A, B and E need no game files. The samples do not go into the repository. - -## Layout-related fields in the texture file header - -Facts used by `t_tex.c` when parsing the game's `.texture` files: - -- The file begins with `<DE(`; the Switch version and the PC version use the same container. -- In files that went through the Switch conversion, the four bytes starting at offset 0x14 are `00 00 00 07` (0 in the PC version), which indicates that the pixel area is in block-linear layout; offset 0x13 is the log2 of the level 0 block height, with the same meaning as byte 0 of the layout parameters the game passes to `SetPackagedTextureLayout` at run time. -- Files without this marker are stored linearly and are byte-for-byte identical to the PC version. + The A round trip can only prove "one-to-one and within bounds"; it cannot prove that the layout formula is right. What backs it up is the hand-computed addresses of groups B and E, and the games that render right. ## Limitations - `nvn_deswizzle()` does not support multisampled textures, textures with the LINEAR flag (which are linear to begin with) or the BUFFER target; it returns -1 when it encounters them. -- Cubes and cube arrays are handled as layered textures and have not been separately checked with real data. -- 3D textures have only the round-trip and hand-computed address checks; there are no real-data samples. +- Cubes and cube arrays are handled as layered textures. - The component bit order of the 16-bit packed formats (565 / 5551 / 4444) is mapped according to the format names and has not been checked with real data; they are marked as "substitute" in the format table. -- The real-data sample set is small, covering the four formats RGBA8, BC1, BC3 and BC4. diff --git a/dm/gfx/tex/tests/Makefile b/dm/gfx/tex/tests/Makefile index 7b1e677..0231c60 100644 --- a/dm/gfx/tex/tests/Makefile +++ b/dm/gfx/tex/tests/Makefile @@ -1,14 +1,9 @@ -DM_ROOT ?= $(HOME)/dm-work -# Texture sub-module unit tests (seconds): make run. Real-data samples: make samples fetches them into $(SAMPLES) (build cache, not in the repo) -SAMPLES ?= $(DM_ROOT)/build/cache/dm/tex_samples -PY ?= $(DM_ROOT)/build/cache/dm/venv/bin/python +# Texture sub-module unit tests (seconds): make run CFLAGS = -O2 -g -Wall -Wextra t_tex: t_tex.c ../nvn_deswizzle.c ../nvn_deswizzle.h ../../nvn/nvn_tex.c ../../nvn/nvn_tex.h clang $(CFLAGS) -o $@ t_tex.c ../nvn_deswizzle.c ../../nvn/nvn_tex.c -lm t_fmt_mtl: t_fmt_mtl.m ../nvn_fmt_mtl.c ../nvn_fmt_mtl.h clang $(CFLAGS) -fobjc-arc -o $@ t_fmt_mtl.m ../nvn_fmt_mtl.c -framework Metal -framework Foundation -samples: - $(PY) fetch_samples.py $(SAMPLES) run: t_tex t_fmt_mtl - ./t_fmt_mtl && ./t_tex $(SAMPLES) -.PHONY: samples run + ./t_fmt_mtl && ./t_tex +.PHONY: run diff --git a/dm/gfx/tex/tests/fetch_samples.py b/dm/gfx/tex/tests/fetch_samples.py deleted file mode 100644 index 0a66fa1..0000000 --- a/dm/gfx/tex/tests/fetch_samples.py +++ /dev/null @@ -1,26 +0,0 @@ -# Fetch real-data samples for cross-checking: one copy each of the same-named .texture from the Switch package (RomFS, block-linear) and the PC package (extracted originals, linear). -# Usage: fetch_samples.py <output dir> → <output dir>/{dm,pc}/<index>.texture + list.tsv (index, path, note) -# Sample selection: covers RGBA8 / BC1 / BC3 / BC4, sizes from 32² to 2048², including non-square; same-resolution ones are compared byte for byte, reduced-resolution ones by decoded PSNR. -import os, sys -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../tools')) -DM_ROOT = os.environ.get('DM_ROOT') or os.path.expanduser('~/dm-work') # Workspace root (game files, build outputs and run artifacts are all under it) -os.environ.setdefault('DMX_ROOT', os.path.join(DM_ROOT, 'data/switch/3.3.94012/Update/RomFS/data')) -import dmx -PC = os.path.join(DM_ROOT, 'data/assets-src/raw') -SAMPLES = [ - ('hd/vfx/textures/gradient/T_FX_perlin_wipe_03.texture', 'RGBA8 256² same resolution'), - ('hd/vfx/textures/gradient/T_FX_portal_gradient.texture', 'RGBA8 256×16 non-square same resolution'), - ('hd/env/texture/terrain/path/066_PATH.texture', 'BC4 32² same resolution'), - ('hd/debug/wasted_mip_size_2048.texture', 'BC3 sRGB 2048² 12 levels same resolution'), - ('hd/items/armor/armor/plate_mail/textures/armor_plate_mail_alb.texture', 'BC3 sRGB 256² (PC 1024²) reduced resolution'), - ('hd/env/texture/global/flow/act4/lava/lava_lavaNS_FLOW.texture', 'BC1 64² (PC 256²) reduced resolution'), - ('hd/character/enemy/fingermage1/Textures/fingermage_KTINT.texture', 'BC3 256² (PC 1024²) reduced resolution'), -] -out = sys.argv[1] -os.makedirs(f'{out}/dm', exist_ok=True); os.makedirs(f'{out}/pc', exist_ok=True) -with open(f'{out}/list.tsv', 'w') as f: - for i, (p, why) in enumerate(SAMPLES): - open(f'{out}/dm/{i}.texture', 'wb').write(dmx.get('data:data/' + p)) - open(f'{out}/pc/{i}.texture', 'wb').write(open(os.path.join(PC, p), 'rb').read()) - f.write(f'{i}\t{p}\t{why}\n') -print(len(SAMPLES), 'sample pairs →', out) diff --git a/dm/gfx/tex/tests/t_tex.c b/dm/gfx/tex/tests/t_tex.c index 6b18070..9a348ec 100644 --- a/dm/gfx/tex/tests/t_tex.c +++ b/dm/gfx/tex/tests/t_tex.c @@ -1,5 +1,5 @@ -// Block-linear deswizzle unit tests (seconds). A round trip; B hand-calculated addresses; C real-data cross-check (Switch block-linear vs PC linear, including per-level offset verification); D wrong-block-height negative control. -// Usage: t_tex <sample dir> (produced by fetch_samples.py). Missing samples count as red, not skipped. +// Block-linear deswizzle unit tests (seconds). A round trip; B hand-calculated addresses; E hand-built 3D. +// Usage: t_tex #include "../nvn_deswizzle.h" #include <math.h> #include <stdio.h> @@ -45,136 +45,7 @@ static void roundtrip(const char *tag, int target, int fmt, uint32_t w, uint32_t free(st); } -// ---------- Real data ---------- -static uint8_t *slurp(const char *p, size_t *n) { - FILE *f = fopen(p, "rb"); if (!f) return NULL; - fseek(f, 0, SEEK_END); *n = (size_t)ftell(f); fseek(f, 0, SEEK_SET); - uint8_t *b = malloc(*n); if (fread(b, 1, *n, f) != *n) { free(b); b = NULL; } fclose(f); return b; -} -// Files converted for Switch: the four bytes at 0x14 = 00 00 00 07 (0 on PC), 0x13 = log2 of the level-0 block height (same meaning as byte 0 of the game's SetPackagedTextureLayout). -// Unconverted ones (e.g. those under hd/debug) are byte-identical to the PC files and stored linearly. -typedef struct { int code, nvn, bl, th_hdr; uint32_t w, h, mips, hdr; uint32_t size[16]; uint64_t off[16]; const uint8_t *px; } tex_t; -static int d2r_nvn(int code) { // D2R .texture format code → NVNformat - switch (code) { case 0x1F: return 0x25; case 0x39: return 0x42; case 0x3A: return 0x46; case 0x3D: return 0x44; - case 0x3E: return 0x48; case 0x3F: return 0x49; case 0x41: return 0x4B; } - return 0; -} -static int parse(const uint8_t *b, size_t n, tex_t *t) { - if (n < 0x24 || memcmp(b, "<DE(", 4)) return -1; - t->code = b[4]; t->nvn = d2r_nvn(t->code); t->bl = b[0x14] == 0 && b[0x15] == 0 && b[0x16] == 0 && b[0x17] == 7; t->th_hdr = b[0x13]; - memcpy(&t->w, b + 8, 4); memcpy(&t->h, b + 12, 4); memcpy(&t->mips, b + 0x1C, 4); - if (!t->nvn || t->mips == 0 || t->mips > 16) return -1; - t->hdr = 36 + 8 * t->mips; uint64_t o = 0; - for (unsigned i = 0; i < t->mips; i++) { memcpy(&t->size[i], b + 0x24 + 8 * i, 4); t->off[i] = o; o += t->size[i]; } - if (t->hdr + o != n) return -1; - t->px = b + t->hdr; return 0; -} - -// Decode to 8-bit RGB (BC1 / BC3 take the color block, BC4 takes the single channel replicated three times, RGBA8 takes RGB). Row = block row, expanded as 4×4 -static void bc1(const uint8_t *b, uint8_t out[16][3]) { - unsigned c0 = b[0] | b[1] << 8, c1 = b[2] | b[3] << 8; uint32_t bits = b[4] | b[5] << 8 | b[6] << 16 | (uint32_t)b[7] << 24; - int p[4][3]; - for (int k = 0; k < 2; k++) { unsigned c = k ? c1 : c0; p[k][0] = (c >> 11 & 31) * 255 / 31; p[k][1] = (c >> 5 & 63) * 255 / 63; p[k][2] = (c & 31) * 255 / 31; } - for (int j = 0; j < 3; j++) { - if (c0 > c1) { p[2][j] = (2 * p[0][j] + p[1][j]) / 3; p[3][j] = (p[0][j] + 2 * p[1][j]) / 3; } - else { p[2][j] = (p[0][j] + p[1][j]) / 2; p[3][j] = 0; } - } - for (int i = 0; i < 16; i++) for (int j = 0; j < 3; j++) out[i][j] = (uint8_t)p[bits >> (2 * i) & 3][j]; -} -static void bc4(const uint8_t *b, uint8_t out[16][3]) { - int a0 = b[0], a1 = b[1], v[8] = {a0, a1}; - for (int k = 2; k < 8; k++) v[k] = a0 > a1 ? ((8 - k) * a0 + (k - 1) * a1) / 7 : k < 6 ? ((6 - k) * a0 + (k - 1) * a1) / 5 : (k == 6 ? 0 : 255); - uint64_t bits = 0; for (int k = 0; k < 6; k++) bits |= (uint64_t)b[2 + k] << (8 * k); - for (int i = 0; i < 16; i++) out[i][0] = out[i][1] = out[i][2] = (uint8_t)v[bits >> (3 * i) & 7]; -} -static uint8_t *decode(int nvn, const uint8_t *lin, uint32_t w, uint32_t h) { // → w×h×3 - uint8_t *img = calloc((size_t)w * h, 3); - if (nvn == 0x25) { for (size_t i = 0; i < (size_t)w * h; i++) memcpy(img + 3 * i, lin + 4 * i, 3); return img; } - uint32_t bwn = (w + 3) / 4, bhn = (h + 3) / 4, bpb = (nvn == 0x42 || nvn == 0x46 || nvn == 0x49) ? 8 : 16; - for (uint32_t by = 0; by < bhn; by++) for (uint32_t bx = 0; bx < bwn; bx++) { - const uint8_t *b = lin + ((size_t)by * bwn + bx) * bpb; uint8_t px[16][3]; - if (nvn == 0x49) bc4(b, px); else bc1(bpb == 16 ? b + 8 : b, px); - for (int i = 0; i < 16; i++) { uint32_t x = bx * 4 + i % 4, y = by * 4 + i / 4; if (x < w && y < h) memcpy(img + 3 * ((size_t)y * w + x), px[i], 3); } - } - return img; -} -static double psnr(const uint8_t *a, const uint8_t *b, size_t n) { - double e = 0; for (size_t i = 0; i < n; i++) { double d = (double)a[i] - b[i]; e += d * d; } - return e == 0 ? 99 : 10 * log10(255.0 * 255.0 / (e / n)); -} -// Decode one level with a specified block height (for the negative control; addresses beyond the storage read as 0) -static void deswz_tile(const uint8_t *st, uint64_t st_size, uint64_t base, uint32_t rb, uint32_t rows, unsigned th, uint8_t *out) { - for (uint32_t y = 0; y < rows; y++) for (uint32_t x = 0; x < rb; x++) { - uint64_t a = base + nvn_bl_offset(x, y, 0, rb, rows, th, 0); - out[(size_t)y * rb + x] = a < st_size ? st[a] : 0; - } -} - -// Criterion threshold: the correct block height must be ≥ this value and a wrong block height must be < it. Measured: correct 28.45–38.90 dB (the 3 reduced-resolution recompressed ones; same-resolution ones are byte-identical), -// block height off by one step 6.94–16.64 dB ⇒ take the midpoint of the two sides, 22 dB, leaving about 6 dB on each side -#define PSNR_PASS 22.0 - -static void real_pair(const char *dir, int i, const char *why) { - char p[1024]; size_t ns, np; - snprintf(p, sizeof p, "%s/dm/%d.texture", dir, i); uint8_t *sb = slurp(p, &ns); - snprintf(p, sizeof p, "%s/pc/%d.texture", dir, i); uint8_t *pb = slurp(p, &np); - tex_t s, c; - CHECK(sb && pb && !parse(sb, ns, &s) && !parse(pb, np, &c), "sample %d read / parse failed (%s)", i, why); - if (!sb || !pb || parse(sb, ns, &s) || parse(pb, np, &c)) return; - if (!s.bl) { // not converted for Switch: linear, should be byte-identical to the PC original - CHECK(ns == np && !memcmp(sb, pb, ns), "sample %d has no block-linear marker but differs from the PC original", i); - printf(" C %d %-30s no Switch block-linear marker (0x14 = 0) → stored linearly, %s the PC original (%zu bytes)\n", i, why, ns == np && !memcmp(sb, pb, ns) ? "byte-identical to" : "differs from", ns); - free(sb); free(pb); return; - } - nvn_texdesc d = {1, s.nvn, 0, 0, s.w, s.h, 1, s.mips, 0, s.th_hdr + 1}; // block height follows the file header (same as the packaged layout parameters the game passes in) - unsigned th_auto, td_auto; { nvn_texdesc a = d; a.pkg_block_h = 0; nvn_tex_level_tiles(&a, 0, &th_auto, &td_auto); } - // ③ Per-level offsets in the file (cumulative mip table) vs nvn_tex_level_offset - int off_ok = 1; - for (unsigned l = 0; l < s.mips; l++) - if (s.off[l] != nvn_tex_level_offset(&d, l)) { off_ok = 0; printf(" ✗ sample %d level %u file offset %llu ≠ nvn_tex %llu\n", i, l, (unsigned long long)s.off[l], (unsigned long long)nvn_tex_level_offset(&d, l)); } - nvn_texlayout L; nvn_tex_layout(&d, &L); - uint64_t file_total = s.off[s.mips - 1] + s.size[s.mips - 1]; - checks++; if (!off_ok) fails++; - CHECK(file_total == L.size, "sample %d file pixel area %llu ≠ nvn_tex total size %llu", i, (unsigned long long)file_total, (unsigned long long)L.size); - // Corresponding PC level: PC side length = Switch × 2^k - unsigned k = 0; while ((s.w << k) < c.w) k++; - if ((s.w << k) != c.w || (s.h << k) != c.h) { CHECK(0, "sample %d size ratio is not a power of 2", i); return; } - unsigned nl = s.mips < c.mips - k ? s.mips : c.mips - k; - uint64_t eq = 0, tot = 0; double ps = 0, ps_bad = 99; unsigned th0, td0; - nvn_tex_level_tiles(&d, 0, &th0, &td0); - for (unsigned l = 0; l < nl; l++) { - uint32_t rb, rows, sl; dims(&d, l, &rb, &rows, &sl); - uint8_t *lin = malloc((size_t)rb * rows); - CHECK(!nvn_deswizzle(&d, l, 0, s.px, lin, rb), "sample %d level %u deswizzle failed", i, l); - const uint8_t *pcl = c.px + c.off[l + k]; - CHECK(c.size[l + k] >= (uint64_t)rb * rows, "sample %d level %u PC level too small", i, l); - if (!k) { for (size_t j = 0; j < (size_t)rb * rows; j++) eq += lin[j] == pcl[j]; tot += (uint64_t)rb * rows; } - if (l == 0) { - uint32_t w = s.w, h = s.h; - uint8_t *a = decode(s.nvn, lin, w, h), *b = decode(c.nvn, pcl, w, h); ps = psnr(a, b, (size_t)w * h * 3); - for (int dt = -1; dt <= 1; dt += 2) { // D: block height off by one step (when only 1 GOB high, block height does not affect the layout; not applicable) - if ((int)th0 + dt < 0 || rows <= 8) continue; - uint8_t *bad = malloc((size_t)rb * rows); deswz_tile(s.px, ns - s.hdr, 0, rb, rows, th0 + dt, bad); - uint8_t *e = decode(s.nvn, bad, w, h); double q = psnr(e, b, (size_t)w * h * 3); if (q < ps_bad) ps_bad = q; - if (!k) CHECK(memcmp(bad, pcl, (size_t)rb * rows), "sample %d block height %+d still byte-identical (negative control ineffective)", i, dt); - free(bad); free(e); - } - free(a); free(b); - } - free(lin); - } - if (!k) CHECK(eq == tot, "sample %d identical bytes %llu/%llu", i, (unsigned long long)eq, (unsigned long long)tot); - CHECK(ps >= PSNR_PASS, "sample %d correct block height PSNR %.2f < %.0f", i, ps, PSNR_PASS); - uint32_t rows0 = (s.h + 3) / 4; if (s.nvn == 0x25) rows0 = s.h; - if (rows0 > 8) CHECK(ps_bad < PSNR_PASS, "sample %d wrong block height PSNR %.2f ≥ %.0f (negative control ineffective)", i, ps_bad, PSNR_PASS); - char neg[48]; if (rows0 > 8) snprintf(neg, sizeof neg, "%.2f", ps_bad); else snprintf(neg, sizeof neg, "n/a (1 GOB high)"); - printf(" C %d %-30s %4u×%-4u→PC level %u header block height %d (auto %u) offsets %s %s PSNR correct %.2f / wrong block height %s dB\n", i, why, s.w, s.h, k, - s.th_hdr, th_auto, off_ok ? "✓" : "✗", k ? "(reduced resolution, bytes not compared)" : (eq == tot ? "byte-identical" : "bytes differ"), ps, neg); - if (!k) printf(" identical bytes %llu/%llu\n", (unsigned long long)eq, (unsigned long long)tot); - free(sb); free(pb); -} - -int main(int argc, char **argv) { +int main(void) { printf("A round trip (random data, non-power-of-2 sizes)\n"); roundtrip("2D 100×37 R8 4 levels", 1, 0x01, 100, 37, 1, 4); roundtrip("2D 100×37 RG8 4 levels", 1, 0x0D, 100, 37, 1, 4); @@ -237,16 +108,9 @@ int main(int argc, char **argv) { // Shape of the real color lookup table on the character select screen: 32³ RGBA16F (ser 17 / 16). Block depth 32 + 16 = 48 ≥ 16 → 16 slices; row 256 bytes = 4 GOBs wide, 32 rows = 4 GOBs high, 2 block layers → 4·4·2·8192 = 262144 nvn_texdesc lut = {2, 0x29, 0, 0, 32, 32, 32, 1, 0, 0}; nvn_tex_layout(&lut, &L); CHECK(L.tileH == 0 && L.tileD == 4 && L.size == 4 * 4 * 2 * 8192ull, "E 32³ RGBA16F block height/depth %u/%u storage %llu, expected 0/4, 262144", L.tileH, L.tileD, (unsigned long long)L.size); - printf(" E 32³ RGBA16F (shape of the character select color lookup table) block height/depth %u/%u, storage %llu (expected 0/4, 262144)\n", L.tileH, L.tileD, (unsigned long long)L.size); + printf(" E 32³ RGBA16F (the shape of a 32³ color lookup table) block height/depth %u/%u, storage %llu (expected 0/4, 262144)\n", L.tileH, L.tileD, (unsigned long long)L.size); } - printf("C/D real-data cross-check (Switch block-linear vs PC linear) + wrong-block-height negative control, threshold %.0f dB\n", PSNR_PASS); - const char *dir = argc > 1 ? argv[1] : getenv("DM_TEX_SAMPLES"); - char lp[1024]; snprintf(lp, sizeof lp, "%s/list.tsv", dir ? dir : "."); FILE *f = dir ? fopen(lp, "r") : NULL; - CHECK(f, "sample directory missing (run make samples first)"); int npair = 0; - if (f) { char line[1024]; while (fgets(line, sizeof line, f)) { int i; char path[512], why[256]; - if (sscanf(line, "%d\t%511[^\t]\t%255[^\n]", &i, path, why) == 3) { real_pair(dir, i, why); npair++; } } fclose(f); } - CHECK(npair >= 3, "fewer than 3 real-data sample pairs (%d)", npair); - printf("%s: %d/%d checks passed (%d real-data pairs)\n", fails ? "RED" : "all passed", checks - fails, checks, npair); + printf("%s: %d/%d checks passed\n", fails ? "RED" : "all passed", checks - fails, checks); return fails ? 1 : 0; } diff --git a/dm/gfx/tools/nometal_run.py b/dm/gfx/tools/nometal_run.py deleted file mode 100644 index 44812e1..0000000 --- a/dm/gfx/tools/nometal_run.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# Runs the shader build chain on a machine without Apple's Metal compiler (Windows / Linux / a Mac without Xcode; for use by tools/d2rm_pack.py). -# Usage: nometal_run.py <script.py> [args...] Example: nometal_run.py dmsh_build.py <package dir> <cache dir> -# How: not a single file of the build chain is modified (their contents feed into the translation chain version, and any change would force a whole new generation of the beta shader libraries); only, within this process, the one entry through which they call external commands -# (subprocess.run) is taken over: the commands below are recognized and answered here, and everything else is passed through unchanged: -# xcrun -sdk <x> metal --version → answers "none" (the compiler is recorded as unused in the tool fingerprint → the chain version forms its own branch, and its cache never mixes with normal mode's) -# xcrun -sdk <x> metal -c <source> -o <intermediate file> → copies the source as the intermediate file (line endings normalized to LF) -# xcrun -sdk <x> metallib <intermediate file> -o <library> → copies the intermediate file verbatim as the library -# which <name> → looked up by Python itself -# cp -c <a> <b> → ordinary copy -# Result: the "library files" (*_b.metallib / *_bf.metallib) contain shader source. When the graphics layer of the shell (DM_SHELL build) finds that a library is not a compiled one, it compiles it on the device -# (gfx/nvn/nvn_mtl.m); the system caches the compiled result, and from the second launch on nothing is compiled again. -# The one check that is lost: in normal mode Apple's compiler also verifies along the way that every source compiles; here that is not verified, and one that fails to compile only reports a failure when it is used on the device. -import os, runpy, shutil, subprocess, sys - -_real = subprocess.run - - -def _done(args, out=''): - return subprocess.CompletedProcess(args, 0, stdout=out, stderr='') - - -def run(args, *a, **k): - text = k.get('text') or k.get('universal_newlines') - enc = (lambda s: s) if text else (lambda s: s.encode()) - if isinstance(args, (list, tuple)) and args: - c = [str(x) for x in args] - if c[0] == 'xcrun' and 'metal' in c and '--version' in c: return _done(args, enc('none\n')) - if c[0] == 'xcrun' and 'metal' in c and '-c' in c and '-o' in c: # source = the first argument after metal that is neither an option nor the value of -o (the iOS command also carries a version option after -c) - o = c.index('-o'); src = [x for i, x in enumerate(c[c.index('metal') + 1:], c.index('metal') + 1) if not x.startswith('-') and i != o + 1] - if len(src) != 1: raise RuntimeError('nometal_run: cannot tell the source argument in %r' % c) - with open(src[0], 'rb') as f: b = f.read() - with open(c[o + 1], 'wb') as f: f.write(b.replace(b'\r\n', b'\n')) # On Windows spirv-cross writes sources with CRLF line endings: normalize before they go into the install package, so the same input gives the same package on every system - return _done(args, enc('')) - if c[0] == 'xcrun' and 'metallib' in c and '-o' in c: shutil.copyfile(c[c.index('metallib') + 1], c[c.index('-o') + 1]); return _done(args, enc('')) - if c[0] == 'xcrun': raise RuntimeError('nometal_run: unrecognized xcrun command %r' % c) - if c[0] == 'which' and len(c) == 2: return _done(args, enc((shutil.which(c[1]) or '') + '\n')) - if c[0] == 'cp' and len(c) == 4 and c[1] == '-c': shutil.copyfile(c[2], c[3]); return _done(args, enc('')) - return _real(args, *a, **k) - - -if __name__ == '__main__': - if len(sys.argv) < 2: sys.exit(__doc__ or 'usage: nometal_run.py <script.py> [args...]') - subprocess.run = run - script = os.path.abspath(sys.argv[1]); sys.argv = sys.argv[1:]; sys.path.insert(0, os.path.dirname(script)) - runpy.run_path(script, run_name='__main__') diff --git a/dm/gfx/tools/nvn_slots.py b/dm/gfx/tools/nvn_slots.py deleted file mode 100644 index ad3f989..0000000 --- a/dm/gfx/tools/nvn_slots.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# main's NVN address-lookup table and static call surface (3.3.94012). -# 1) Parse the NVN loader function in main (looks up addresses by name and stores them one by one into global pointer slots) → name → slot; -# 2) Scan all of .text for "ldr xT,[xN,#imm]" slot reads: search backward from the read for the previous write to xN; it only counts as a call site if that write is an adrp to the slot's page. -# (Forward register tracking treats stale values as slot addresses, or misses an adrp hoisted out of a loop; spot checks found both kinds of error, hence the backward trace.) -# Usage: nvn_slots.py <main raw image .bin (extracted by dm/tools/nso.py)> <output tsv> [name table output] -# Output columns: name, slot address, call site count, first call site; the name table is ordered by slot address (= load order). -import struct, sys -LOADER = 0x1739290 # NVN loader function inside main (3.3.94012; re-locate it first when changing versions) -img, out = sys.argv[1], sys.argv[2] -d = open(img, 'rb').read() -TEXT_END = 0x1b786a0 # end of main .text (NSO segment header) -N = TEXT_END // 4 -w = struct.unpack_from('<%dI' % N, d, 0) - -def adrp(x, pc): - imm = ((x >> 29) & 3) | (((x >> 5) & 0x7ffff) << 2) - if imm & (1 << 20): imm -= 1 << 21 - return (pc & ~0xfff) + (imm << 12) -def cstr(a): return d[a:d.index(b'\0', a)].decode() - -# 1) Loader function: adrp/add x1 = name, str x0,[xn,#imm] = slot -i, reg, cur, slot = LOADER // 4, {}, None, {} -while w[i] != 0xd65f03c0: - x, pc = w[i], i * 4 - if x & 0x9f000000 == 0x90000000: reg[x & 31] = adrp(x, pc) - elif x & 0xffc00000 == 0x91000000 and (x & 31) == 1: cur = cstr(reg[(x >> 5) & 31] + ((x >> 10) & 0xfff)) - elif x & 0xffc00000 == 0xf9000000 and (x & 31) == 0: slot[reg[(x >> 5) & 31] + ((x >> 10) & 0xfff) * 8] = cur - i += 1 -offs = {a & 0xfff for a in slot} - -def writes(x, r): - """Whether x writes general-purpose register r (approximate, conservative)""" - op0 = (x >> 25) & 0xf - if x & 0xfc000000 == 0x94000000 or x & 0xfffffc1f == 0xd63f0000: return r <= 18 # bl / blr - if op0 in (0b1000, 0b1001, 0b0101, 0b1101): return (x & 31) == r # data processing - if op0 & 0b0101 == 0b0100: # memory load/store - L = (x >> 22) & 1 - if (x >> 27) & 7 == 0b101: - wb = (x >> 23) & 3 in (1, 3) - return (L and ((x & 31) == r or ((x >> 10) & 31) == r)) or (wb and ((x >> 5) & 31) == r) - if (x >> 27) & 7 == 0b111 and (x >> 24) & 3 == 0: - wb = (x >> 21) & 1 == 0 and (x >> 10) & 3 in (1, 3) - return (L and (x & 31) == r) or (wb and ((x >> 5) & 31) == r) - if (x >> 27) & 7 == 0b111: return bool(L) and (x & 31) == r - if (x >> 27) & 7 == 0b011: return (x & 31) == r - return False - -# 2) Attribute each slot read by tracing backward -uses = {a: [] for a in slot} -for i in range(N): - x = w[i] - if x & 0xffc00000 != 0xf9400000: continue - off = ((x >> 10) & 0xfff) * 8 - rn = (x >> 5) & 31 - if off not in offs or rn == 31: continue - for j in range(i - 1, max(i - 4000, -1), -1): - y = w[j] - if y == 0xd65f03c0: break - if writes(y, rn): - if y & 0x9f000000 == 0x90000000 and (y & 31) == rn and adrp(y, j * 4) + off in uses: - uses[adrp(y, j * 4) + off].append(i * 4) - break -with open(out, 'w') as f: - f.write('# kind: evidence main NVN fetch slots and static use sites (nvn_slots.py)\nname\tslot\tnuses\tfirst_use\n') - for a in sorted(slot): - u = uses[a]; f.write('%s\t%x\t%d\t%s\n' % (slot[a], a, len(u), '%x' % u[0] if u else '-')) -if len(sys.argv) > 3: open(sys.argv[3], 'w').write(''.join(slot[a] + '\n' for a in sorted(slot))) -used = sum(1 for a in slot if uses[a]) -print('slots %d, with use sites %d, use sites %d' % (len(slot), used, sum(len(v) for v in uses.values()))) diff --git a/dm/gfx/tools/shader_map.py b/dm/gfx/tools/shader_map.py deleted file mode 100644 index 758cd7d..0000000 --- a/dm/gfx/tools/shader_map.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 -# Mapping table: Switch shader → PC shader (DXBC) → PC-chain MSL (metallib). -# Basis: data/hd/shaders on both sides holds 162 SHAD packages with the same names, one BTCD record per variant, and record counts are equal package by package (10847 in total); -# apart from sizes, record headers differ only in the platform number (PC 3 / Switch 5), and there is no permutation key ⇒ map one-to-one by index within the package. -# Evidence of order alignment: compute shader thread group sizes are equal record by record, 22/22; for the 22 packages with ≥16 records, code size is rank-correlated, 0.81–1.00 when aligned and lower in every case when shifted by one record. -# Switch record: after the header = GPU code segment (starts with 0x12345678), followed by = control segment (starts with 0x98761234, length in the record header). -# At runtime nvnProgramSetShaders receives the control segment pointer ⇒ key = sha256(control segment)[:16]. -# PC record: contains one DXBC; PC-chain cache file name = sha256(DXBC)[:16].metallib (platform/shell/mtl12/shaders/build_cache.py). -# Binding mapping (header comment of maxwell_refs.py): NVN slot number = D3D register number; verified record by record against what the Switch machine code actually references that "used by Switch ⊆ declared by PC". -# On a key conflict (same Switch binary, different PC DXBC) pick one: the one with the smallest difference from the output count / cb / t / s actually used by Switch; -# on a tie take the simplest (fewer outputs, fewer resources), on a further tie take the smallest DXBC hash (deterministic). The choice and the reason are written to <output>.conflicts.tsv. -# Shaders embedded in the executable itself (not in packages): the Switch main raw image has 10 control segments; the first 6 are graphics shaders and map one-to-one, in the same order, to the 6 DXBC embedded in PC D2R.exe -# (each side has a pair of identical duplicates at the same position); the last 4 are compute shaders with no PC counterpart, handled separately. -# Usage: shader_map.py <PC package dir> <Switch package dir> <PC metallib cache dir> <output tsv> [<main raw image> <PC D2R.exe>] -import hashlib, json, os, re, struct, sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from maxwell_refs import bindings, vs_inputs, sph_stage - -pc_dir, dm_dir, cache_dir, out = sys.argv[1:5] - -def records(d): - r = [m.start() for m in re.finditer(b'BTCDBTCD', d)] - return [d[x:(r[k + 1] if k + 1 < len(r) else len(d))] for k, x in enumerate(r)] - -def pc_dxbc(rec): - i = rec.find(b'DXBC') - return rec[i:i + struct.unpack_from('<I', rec, i + 24)[0]] - -def dx_outputs(dx): - j = dx.find(b'OSGN') - if j < 0: return -1 - n = struct.unpack_from('<I', dx, j + 8)[0] - return sum(1 for q in range(n) if struct.unpack_from('<I', dx, j + 16 + q * 24 + 8)[0] == 0) - -def dm_parts(rec): - i = rec.find(bytes.fromhex('34127698')) # control segment magic (little-endian 0x98761234) - csz = struct.unpack_from('<I', rec, 0x28)[0] # record header: platform number 5, control segment length... - return rec[i:i + csz], i - -STAGE = {'vs': 'vertex', 'ps': 'pixel', 'cs': 'compute', 'hs': 'hull', 'ds': 'domain'} -cache = set(os.listdir(cache_dir)) -rows, keys, cand, dxs, locs = [], {}, {}, {}, {} -for name in sorted(os.listdir(dm_dir)): - A = records(open(os.path.join(pc_dir, name), 'rb').read()) - B = records(open(os.path.join(dm_dir, name), 'rb').read()) - if len(A) != len(B): sys.exit(f'[RED] {name}: record counts differ, PC {len(A)} / Switch {len(B)}') - for k, (a, b) in enumerate(zip(A, B)): - dx = pc_dxbc(a); ctl, _ = dm_parts(b) - if len(ctl) < 0x100: sys.exit(f'[RED] {name}#{k}: bad control section') - swk = hashlib.sha256(ctl).hexdigest()[:16]; pck = hashlib.sha256(dx).hexdigest()[:16] - ml = pck + '.metallib' in cache - dm_out = struct.unpack_from('<I', ctl, 0x734)[0] if name.startswith('ps_') else -1 - pc_out = dx_outputs(dx) if name.startswith('ps_') else -1 - refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else [] - pcb = {x['reg'] for x in refl if x['type'] == 'cbuffer'} - pt = {x['reg'] for x in refl if x['type'].startswith('texture') or x['type'].startswith('buffer')} - ps_ = {x['reg'] for x in refl if x['type'] == 'sampler'} - _, ci = dm_parts(b); sb = bindings(b[0x38:ci], name[:2]) - pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')} - ok = int(sb['ubo'] <= pcb) | int(sb['tex'] <= pt) << 1 | int(sb['samp'] <= ps_) << 2 | int(sb['ssbo'] == pbuf) << 3 - fmt = lambda x: ','.join(map(str, sorted(x))) or '-' - rows.append((name, k, STAGE.get(name[:2], name[:2]), swk, pck, int(ml), pc_out, dm_out, - fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok)) - keys.setdefault(swk, set()).add(pck) - cand.setdefault(swk, {})[pck] = (pc_out, dm_out, pcb, pt, ps_, sb) - dxs[pck] = dx; locs.setdefault(swk, []).append(f'{name}#{k}') - -if len(sys.argv) > 6: - mi = open(sys.argv[5], 'rb').read(); pe = open(sys.argv[6], 'rb').read() - dm_ctl = [m.start() for m in re.finditer(bytes.fromhex('34127698'), mi)] - pc_dx = [] - for m in re.finditer(b'DXBC', pe): - o = m.start(); sz = struct.unpack_from('<I', pe, o + 24)[0] if o + 32 <= len(pe) else 0 - if 64 < sz < 1 << 20 and o + sz <= len(pe) and struct.unpack_from('<I', pe, o + 20)[0] == 1: pc_dx.append(pe[o:o + sz]) - gfx = [o for o in dm_ctl if mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o) > 0] - # Pair by features (not by order: disassembly showed the embedded order differs between the two sides): stage is taken from SPH; set difference = symmetric difference of cb/t/s and (vertex) input slots, take the unique minimum - def pc_feat(dx): - h = hashlib.sha256(dx).hexdigest()[:16] - j = json.load(open(os.path.join(cache_dir, h + '.json'))) - bs = j['bindings'] - return h, j['stage'], ({x['reg'] for x in bs if x['type'] == 'cbuffer'}, {x['reg'] for x in bs if x['type'].startswith('texture')}, - {x['reg'] for x in bs if x['type'] == 'sampler'}, {x['location'] for x in j['vertex_inputs']}) - pcs = [pc_feat(dx) for dx in pc_dx] - for k, o in enumerate(gfx): - g = mi.rfind(bytes.fromhex('78563412'), o - 0x1000, o) - code = mi[g:o]; stg = sph_stage(code); sb = bindings(code, stg) - dm = (sb['ubo'], sb['tex'], sb['samp'], vs_inputs(code) if stg == 'vs' else set()) - cands = sorted((sum(len(a ^ b) for a, b in zip(dm, f)), h) for h, st, f in pcs if st == stg) - if not cands or (len(cands) > 1 and cands[0][0] == cands[1][0] and cands[0][1] != cands[1][1]): - sys.exit(f'[RED] embedded shader @{o:#x} ({stg}): no unique PC counterpart: {cands}') - pck = cands[0][1]; swk = hashlib.sha256(mi[o:o + 0x880]).hexdigest()[:16] - ml = pck + '.metallib' in cache - refl = json.load(open(os.path.join(cache_dir, pck + '.json')))['bindings'] if ml else [] - pbuf = {x['reg'] for x in refl if x['type'] in ('buffer_raw', 'buffer_structured', 'atomic_alias')} - f = pcs[[h for h, _, _ in pcs].index(pck)][2] - ok = int(dm[0] <= f[0]) | int(dm[1] <= f[1]) << 1 | int(dm[2] <= f[2]) << 2 | int(sb['ssbo'] == pbuf) << 3 - fmt = lambda x: ','.join(map(str, sorted(x))) or '-' - rows.append(('main.rodata@%x' % o, k, STAGE.get(stg, stg), swk, pck, int(ml), -1, -1, fmt(sb['ubo']), fmt(sb['tex']), fmt(sb['samp']), fmt(sb['ssbo']), ok)) - keys.setdefault(swk, set()).add(pck) - print(f' embedded @{o:#x} {stg} uses cb{fmt(dm[0])} t{fmt(dm[1])} s{fmt(dm[2])} inputs {fmt(dm[3])} → PC {pck} (difference {cands[0][0]}, runner-up {cands[1][0] if len(cands) > 1 else "-"})') - print(f'embedded graphics shaders {len(gfx)} (with metallib {sum(1 for r in rows[-len(gfx):] if r[5])}), embedded compute shaders not mapped {len(dm_ctl) - len(gfx)}') - -amb = {k: v for k, v in keys.items() if len(v) > 1} -with open(out, 'w') as f: - f.write('# kind: evidence Switch shader variants → PC DXBC → PC-chain metallib (dm/gfx/tools/shader_map.py)\n') - f.write('# key conflicts (one Switch control section, several PC DXBC): %d\n' % len(amb)) - f.write('package\tindex\tstage\tdm_ctl_sha16\tpc_dxbc_sha16\tmetallib\tpc_outputs\tdm_outputs\tdm_ubo\tdm_tex\tdm_samp\tdm_ssbo\tsubset_ok(cb|t<<1|s<<2|ssbo_equal<<3)\n') - for r in rows: f.write('\t'.join(map(str, r)) + '\n') - -# Key conflict resolution -def dx_chunks(d): - out = {} - for i in range(struct.unpack_from('<I', d, 28)[0]): - o = struct.unpack_from('<I', d, 32 + 4 * i)[0] - out[d[o:o + 4]] = d[o + 8:o + 8 + struct.unpack_from('<I', d, o + 4)[0]] - return out -def klass(pcs): - ch = [dx_chunks(dxs[p]) for p in pcs] - code = [c.get(b'SHEX', c.get(b'SHDR')) for c in ch] - if len(set(code)) == 1: return 'same instruction block, only the input signature differs (picked by the vertex shader outputs when the pipeline is built)' - if len({len(x) for x in code}) == 1: return 'instruction blocks of equal length, input signatures differ (probably renumbered input registers, also picked by pairing)' - return 'different instruction blocks (real code differences Switch compiled into one binary; needs the package and index located at run time, or a visual check)' -def score(c): - po, so, pcb, pt, ps_, sb = c - diff = (po != so and po >= 0) + len(pcb ^ sb['ubo']) + len(pt ^ sb['tex']) + len(ps_ ^ sb['samp']) - return (diff, max(po, 0) + len(pcb) + len(pt) + len(ps_)) -with open(out + '.conflicts.tsv', 'w') as f: - f.write('# kind: evidence PC candidate rulings for Switch control section key conflicts (smallest difference → simplest → smallest hash)\n') - f.write('dm_ctl_sha16\tchosen_pc\tclass\tcandidates(pc:diff:size)\trecords\n') - for k in sorted(amb): - cs = sorted(cand[k].items(), key=lambda kv: (score(kv[1]), kv[0])) - f.write('%s\t%s\t%s\t%s\t%s\n' % (k, cs[0][0], klass([p for p, _ in cs]), ' '.join('%s:%d:%d' % (p, *score(c)) for p, c in cs), ' '.join(locs[k]))) - -n = len(rows); ml = sum(r[5] for r in rows) -okc = [sum(1 for r in rows if r[12] >> b & 1) for b in range(4)] -print(f'binding subset holds: cb {okc[0]}/{len(rows)}, t {okc[1]}/{len(rows)}, s {okc[2]}/{len(rows)}; storage buffers equal {okc[3]}/{len(rows)}') -uniq_dm = len(keys); uniq_pc = len({r[4] for r in rows}); uniq_pc_ml = len({r[4] for r in rows if r[5]}) -outd = [r for r in rows if r[6] >= 0 and r[6] != r[7]] -print(f'records {n}; distinct Switch control sections {uniq_dm}; distinct PC DXBC {uniq_pc}; key conflicts {len(amb)}') -print(f'records with a PC metallib {ml}/{n}; distinct PC DXBC with a metallib {uniq_pc_ml}/{uniq_pc}') -print(f'pixel shaders whose output counts differ (PC has more targets, Switch writes fewer) {len(outd)}') -by = {} -for r in rows: by.setdefault(r[2], [0, 0]); by[r[2]][0] += 1; by[r[2]][1] += r[5] -for s, (t, m) in by.items(): print(f' {s}: records {t}, with metallib {m}') diff --git a/dm/host/Makefile b/dm/host/Makefile index 0e0ec7d..4fcf25c 100644 --- a/dm/host/Makefile +++ b/dm/host/Makefile @@ -1,4 +1,3 @@ -DM_ROOT ?= $(HOME)/dm-work # Game profile (dm/games/<id>/): the host is built for it (make GAME=base game title ID, required: there is no default game). game_profile.h is generated from game.tsv into gen/<id>/, dmsh_chain.h lives in the profile folder # without GAME only clean and a bare make (whose default goal only builds libvpx) pass; anything else stops and lists the profiles ifeq ($(strip $(GAME)),) @@ -17,13 +16,11 @@ endif GAME_H = gen/$(GAME)/game_profile.h CFLAGS=-O1 -g -Wall -Wno-unused-function # Service-layer takeover hle_*.c files are included by wildcard: adding a file does not touch this line (avoids merge conflicts) -SRC=main.c loader.c hle.c host_stubs.c crash.c probe.c thread.c memstat.m ../ios/Sources/dm_touch_controls.c $(sort $(wildcard hle_*.c)) front_mac.m entry.S dm_i18n.c +SRC=main.c loader.c hle.c crash.c probe.c thread.c memstat.m $(sort $(wildcard hle_*.c)) front_mac.m entry.S # NVN null device + nn::vi + the Vulkan layer (dm/gfx/) # Sources under dm/gfx, dm/gfx/nvn, dm/gfx/tex and dm/gfx/vk are included by wildcard (tests/ and metal/ do not go into the host): adding a file does not touch this line GFX=$(sort $(wildcard ../gfx/*.c ../gfx/nvn/*.c ../gfx/nvn/*.m ../gfx/nvn/*.S ../gfx/tex/*.c ../gfx/tex/*.m ../gfx/vk/*.m)) GFX_H=$(sort $(wildcard ../gfx/nvn/*.h ../gfx/nvn/include/*.h ../gfx/tex/*.h)) -# Third party: only the decompression-related sources of libdeflate are built (TACT BLTE zlib blocks, hle_tact.c) -LIBDEFLATE=../third_party/libdeflate/lib/deflate_decompress.c ../third_party/libdeflate/lib/zlib_decompress.c ../third_party/libdeflate/lib/adler32.c ../third_party/libdeflate/lib/utils.c ../third_party/libdeflate/lib/arm/cpu_features.c # Third party: libvpx ships only the VP9 decoder's sources (movie decoding, hle_movie.c), with its configuration headers in config/ (arm64, shared by Mac and iOS). Built separately into a static library, file by file, so host changes do not recompile it VPX=../third_party/libvpx VPX_SRC=$(sort $(shell find $(VPX) -name '*.c')) @@ -35,19 +32,6 @@ gen/vpx/%.o: $(VPX)/%.c $(wildcard $(VPX)/config/*.h) $(VPX_LIB): $(VPX_OBJ) rm -f $@; ar rcs $@ $^ .DEFAULT_GOAL := dmhost # The header generation rules are written first below, so the default goal must be stated explicitly (otherwise make only generates the headers and does not build the host) -# Headers generated from the original game's tables: not checked into the repository, generated at build time from the player's own game files (Update/RomFS of Switch version 3.3.94012); the path is given by the environment variable DM_ROMFS_SRC -ROMFS?=$(or $(DM_ROMFS_SRC),$(DM_ROOT)/data/switch/3.3.94012/Update/RomFS) -GENH=dm_vault_tab.h dm_hud_exp.h dm_bright_levels.inc -NEED_ROMFS=@test -f "$(ROMFS)/data/build_manifest.psv" || { echo "🔴 game files not found: $(ROMFS)/data/build_manifest.psv. Put the Update/RomFS of Switch version 3.3.94012 there, or point the environment variable DM_ROMFS_SRC at it (see dm/README.md, 'Generating resources from your game files')"; exit 1; } -dm_vault_tab.h: ../ios/tools/gen_vault_table.py - $(NEED_ROMFS) - python3 ../ios/tools/gen_vault_table.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@ -dm_hud_exp.h: ../ios/tools/gen_hud_exp.py ../ios/tools/romfs_art.py - $(NEED_ROMFS) - python3 ../ios/tools/gen_hud_exp.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@ -dm_bright_levels.inc: ../tools/gen_bright_levels.py ../ios/tools/touch_layout.py - $(NEED_ROMFS) - python3 ../tools/gen_bright_levels.py "$(ROMFS)" > $@.tmp && mv $@.tmp $@ .PHONY: game_profile game_profile: $(GAME_H) # Regenerated on every make (FORCE): the same GAME can come from another profile folder (DM_GAMES_FIRST), whose game.tsv may be older than the header; game.py writes it only when it changes @@ -59,5 +43,5 @@ $(GAME_STAMP): FORCE @mkdir -p gen; echo $(GAME) | cmp -s - $@ || echo $(GAME) > $@ .PHONY: FORCE FORCE: -dmhost: $(SRC) $(GFX) dm.h hle.h dm_input.h front.h touch_mac.h ../ios/Sources/dm_touch_controls.[ch] $(GFX_H) $(GAME_H) $(GAME_DIR)/dmsh_chain.h $(GAME_STAMP) $(VPX_LIB) +dmhost: $(SRC) $(GFX) dm.h hle.h dm_input.h front.h $(GFX_H) $(GAME_H) $(GAME_DIR)/dmsh_chain.h $(GAME_STAMP) $(VPX_LIB) clang $(CFLAGS) -I../gfx/nvn/include -I../third_party/vulkan-headers/include -I$(dir $(GAME_H)) -I$(GAME_DIR) -I$(VPX) -I../third_party/stb_vorbis -fobjc-arc -o $@ $(SRC) $(GFX) $(VPX_LIB) -framework Cocoa -framework QuartzCore -framework Metal -framework AudioToolbox -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework ImageIO -framework UniformTypeIdentifiers diff --git a/dm/host/dm.h b/dm/host/dm.h index 521e696..2b34030 100644 --- a/dm/host/dm.h +++ b/dm/host/dm.h @@ -14,7 +14,6 @@ typedef struct { uint32_t st_name; uint8_t st_info, st_other; uint16_t st_shndx; uint64_t st_value, st_size; } Elf64_Sym; typedef struct { int64_t d_tag; union { uint64_t d_val, d_ptr; } d_un; } Elf64_Dyn; typedef struct { uint64_t r_offset, r_info; int64_t r_addend; } Elf64_Rela; -typedef struct { uint8_t *d; size_t n; } dm_blob; // One save file of the original game read whole into memory (dm_pages.c dm_save_blobs) #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) enum { DT_INIT = 12, DT_RELA = 7, DT_RELASZ = 8, DT_JMPREL = 23, DT_PLTRELSZ = 2, DT_SYMTAB = 6, DT_STRTAB = 5, @@ -31,11 +30,10 @@ typedef struct { size_t size; uint64_t text_end, ro_off, data_off, key_page_off, key_page_size; // From the rewriter fixups header uint64_t fps_state_off; // frame step state page (patch_nso.py FRAME_STEP: Tears of the Kingdom 1.4.3 and Breath of the Wild 1.9.0 main); 0 = none - int fps_legacy_elapsed; // its legacy mode is an elapsed-time step (TOTK); 0 = legacy is the game's own interval logic (BotW) int64_t patch_off; // patch area relative to the base; negative = before the base (image too large, see patch_nso.py FAR), and size does not include it uint64_t image_size, file_image_len, patch_len; // only with the patch area before the base: whole image (with bss and key page), length of the image part of the file, patch area length char build_id[33]; // First 16 bytes of the NSO build ID (hex); empty for older build outputs - unsigned key_idx[8]; + unsigned key_idx[4]; // key page index of each thread slot (K18, KTLS, KTLR, KGATE) const Elf64_Dyn *dyn; const Elf64_Sym *symtab; const char *strtab; @@ -46,12 +44,10 @@ typedef struct { extern module_t g_mod[MAX_MOD]; extern int g_nmod; -// main's build ID (empty if none). DM_D2R_BUILD = the D2R build that the code inherited from d2r-mobile belongs to: code with fixed D2R addresses or that edits D2R saves runs only on it -#define DM_D2R_BUILD "db0507f36b84200ac05f75d76ed5026f" +// main's build ID (empty if none): code with fixed addresses in a game's module runs only on the build they were found in static inline const char *dm_main_build(void) { for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) return g_mod[i].build_id; return ""; } // Four thread-local slots: K18 = the Switch's x18, KTLS = tpidr_el0, KTLR = tpidrro_el0 (ThreadLocalRegion), KGATE = host gate entry; the actual key numbers are written into the key page by the loader extern pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate; -extern pthread_key_t g_keyres[2], g_keyaspect, g_keyaspectui; void load_modules(const char *dir, const char **names, int n); void run_module_inits(void); @@ -61,24 +57,18 @@ module_t *module_of(const void *p); // Gate dispatch: rewritten svc / timer reads come in through the common entry, ret_addr = the return point of that gate block, regs points to the saved x0..x30 uint64_t gate_dispatch(uint64_t ret_addr, uint64_t *regs); // Observation gates (rewriter OBS table; numbering matches patch_nso.py): read-only callbacks, called on a gate hit once attached -enum { OBS_UIMSG = 1, OBS_TICK = 2, OBS_C2S = 3, OBS_C2S_SYS = 4, OBS_SETLANG = 5, OBS_TARGET_RANK = 7, OBS_UI_PROJECT = 8, OBS_FREEZE_WORLD = 10, OBS_FREEZE_CLIENT = 11, OBS_FREEZE_ANIM = 12, OBS_FREEZE_RENDER = 13, OBS_TEXTBOX = 14, OBS_EVENT_MSG = 15, OBS_LEVEL_WARP = 16, OBS_QUEST_KILL = 17, OBS_NOTICE_INPUT = 18, OBS_MOVIE_RING_LINK = 19, OBS_MOVIE_RING_UNLINK = 20, OBS_MAX = 21 }; // 6 = iOS binding read (dm_touch_game_bridge.c) +enum { OBS_MOVIE_RING_LINK = 19, OBS_MOVIE_RING_UNLINK = 20, OBS_MAX = 21 }; // Metroid Dread 2.1.0's movie request ring (hle_movie.c) void gate_obs_set(int id, void (*fn)(const uint64_t *regs)); int gate_obs_present(int id); -void dm_quiet_difficulty_msg(int on); // In a rift run, block the original game's "Game difficulty set to N" message (observation gate 15, diag.c) -int gate_svc_present(int num); // Whether the build output has a gate for this svc number (for host call numbers introduced by cuts) // whether the build output has this observation gate (registered in gates.tsv) uint64_t now_ns(void); -uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while DM_TICK_HOLD is frozen) +uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while frozen, guest_clock_freeze) int dm_detclock_enabled(void); void dm_guest_clock_advanced(void); void guest_clock_freeze(int on); void guest_clock_freeze_for(int reason, int on); // Freeze by reason (1 background, 2 system keyboard); see hle_os.c void dm_guest_frame_tick(void); // Called on every present (hle_nvn.c): when DM_DETCLOCK is on, advances the guest clock by one fixed step -int dm_nvn_ruler_truth(void); // 1 during the ground-truth phase of the visual ruler (the first two screenshots with DM_NVN_RULER=1) uint64_t dm_nvn_frame_count(void); uint64_t dm_nvn_sub_cpu_ns(void); // Cumulative CPU nanoseconds of submit (excluding present) -int dm_ingame_now(void); // Whether we are in a run (diag.c) -void dm_3d_scale_feed(double iv_ms); // The present thread reports the actual present interval every frame (−1 discard / 0 no reading), driving the dynamic 3D scale -float dm_3d_scale_cur(void); // The 3D render scale currently in effect (hle_oe.c scale3d_tick) // current cumulative count of presented frames (for DM_TICKS alignment observation) void dm_backtrace(uint64_t fp); void crash_watch_progress(uint32_t tick, const char *stage); void crash_watch_frame(void); @@ -88,12 +78,6 @@ void crash_dump_now(const char *why); // Dump state on demand (threads + graph // Diagnostic switches: read once from environment variables when main starts (getenv takes the libc global environment lock, and on a hot path it would make all guest threads queue up) extern uint64_t g_t0_ns; // Host start time (now_ns), used for relative times in the log extern struct dm_flags { int thr, svc, fs, movie, dbg, imports, bootprof; } g_flag; -// Boot timeline (DM_BOOTPROF=1, diag.c): segment marks + counters (nanoseconds / bytes, accumulated over all threads); PROF_GFX_* can be used for instrumentation -enum { PROF_FS_READ, PROF_WAIT_LOCK, PROF_WAIT_CV, PROF_WAIT_ADDR, PROF_WAIT_SYNC, PROF_SLEEP, PROF_MAIN_WAIT, - PROF_GFX_PRESENT, PROF_GFX_SHADER, PROF_TACT, PROF_N }; // PROF_TACT: time and output bytes of TACT block zlib decode (svc #0x80, host libdeflate) -void dm_prof_add(int id, uint64_t ns, uint64_t bytes); -void dm_prof_mark(const char *seg); -uint64_t dm_prof_ribbon_ns(void); // The now_ns time at which OpenPanel MainMenuButtonRibbon first appeared (0 = not yet; needs DM_BOOTPROF=1) void *objmap_get(const void *guest); void objmap_put(const void *guest, void *host); void *objmap_del(const void *guest); void hle_signal_system_event(void *g); // hle_thread.c: signal a service-layer SystemEvent (any thread) void hle_bind_system_event(void *g, int autoclear); // On a guest thread, bind a guest SystemEvent to a kernel event diff --git a/dm/host/dm_actions.h b/dm/host/dm_actions.h deleted file mode 100644 index 655cf99..0000000 --- a/dm/host/dm_actions.h +++ /dev/null @@ -1,66 +0,0 @@ -// Host entry points for "select first, then confirm": vendor buying/selling, stat / skill point allocation, setting the quantity box value. For implementation and rationale see dm_actions.c, dm_textinput.c. -// Actions: callable from any thread, executed on the game main thread (where the controller is fetched). Readings: the main thread publishes a snapshot every frame, readable from any thread. -#pragma once -#include <stdint.h> -typedef struct { - int open; // the vendor trade page is open - int gold, stash_gold; // gold carried, gold in the stash (purchases can deduct from both, game rule) - int sel_valid; // the cursor has selected a piece of goods - uint32_t sel_id; char sel_code[4]; char sel_name[64]; int buy_price; - int sel_gx, sel_gy, sel_gw, sel_gh; // top-left cell and footprint of the selected goods on the shelf (in cells; meaningless when sel_valid is 0). Converted to a screen rectangle for the drag-goods follow image - int can_buy; // enough gold (buy price ≤ gold carried + gold in the stash); whether the inventory has room is judged by the game at purchase time - int held_valid; // an item is held in hand (being dragged) - uint32_t held_id; char held_name[64]; int sell_price; // sell_price = 0: this item cannot be sold -} dm_shop_info; -void dm_shop_get(dm_shop_info *out); -int dm_shop_open(void); -void dm_shop_buy(void); // buy the selected item (the game's own purchase path; not enough gold / no room in the inventory is rejected by the game with the original game's prompt, not blocked here) -void dm_shop_sell_cursor(void); // sell the item held in hand - -typedef struct { - int stat_points, skill_points; // remaining stat points / skill points - int tree_open; // the skill tree page is open - int char_open; // the character page is open (and the advanced stats page is not covering it); the phone layer draws the stat "confirm +1 / +5 / all" based on this - int sel_skill; // skill id currently selected on the skill tree page, -1 = none - int sel_can_add; // whether a point can still be added (= sel_block == 0) - int sel_block; // reason a point cannot be added: 0 can add, 1 no skill points, 2 level too low, 3 prerequisite skill not learned, 4 other (already at the cap, etc.) - char sel_name[64]; -} dm_points_info; -void dm_points_get(dm_points_info *out); -void dm_stat_add(int stat, int n); // stat: 0 strength 1 dexterity 2 vitality 3 energy; n: 1 / 5 / -1 all (valid while the character page is open) -void dm_skill_add_selected(void); // add 1 point to the skill currently selected on the skill tree page -void dm_belt_put(uint32_t item_id); // put this inventory item into the belt (the game's own "move to belt" path; does nothing if it is not a potion / the belt is full). item_id uses the id from dm_cursor_item -void dm_item_use(uint32_t item_id); // use this item immediately (the same function as the original game's "use": the endpoint of controller hold-confirm / keyboard-and-mouse right click; does not wait for the long-press threshold). Drinks potions, reads scrolls, uses tomes, opens the cube. item_id uses the id from dm_cursor_item -int dm_ui_hover_now(int x, int y); // put the original game's controller cursor at (x, y) (the same coordinate pair dm_cursor.c writes) and have the game recompute hover / focus / selected cell on the spot; returns 1 = refreshed, 0 = not in controller cursor mode right now. May only be called on the game main thread (where the controller is fetched) -void dm_item_compare(int on); // equipment comparison on / off: while on, the item worn in the same slot is shown side by side next to the hovered / selected equipment's tooltip (the same state bit as the original game's "hold the compare button", not going through the controller button, - // so it also works on the vendor page and the stash page, where the compare button is taken up by tab switching). Callable from any thread; while on, the host maintains it every frame. The original game does not support comparison on the gambling page, so nothing shows there even when on -void dm_belt_expand(int on); // expand / collapse the upper rows of the belt (the same panel toggled by the original game's keyboard-and-mouse "show belt" action, panel id 0x1a). Callable from any thread -void dm_belt_use(int col, int row); // drink the bottle in row `row` (0 = bottom row), column `col` (0–3) of the belt; does nothing if that cell is empty. Goes through the belt branch of dm_item_use. Callable from any thread -void dm_belt_use_merc(int col, int row); // same as above, but feeds it to the mercenary (belt double-tap); does nothing if the mercenary is absent / dead / this bottle cannot be fed -int dm_belt_rows(int *expanded); // belt row count (1–4, depending on the belt worn); when expanded is non-null, writes 1 = currently expanded. Reads the snapshot the main thread publishes every frame -void dm_held_equip(int bodyloc); // put the held item into this body slot (the same function as the original game's equipment slot "press confirm while holding an item", main+0x3bb24c; empty slot = equip, occupied = swap; nothing happens for what the original game rejects). Callable from any thread -void dm_inv_sort(int page); // sort (auto-arrange): page 0 inventory, 3 Horadric Cube, 4 current page of the personal stash. The endpoint function of the original game's "hold to sort", without waiting for the long press. Callable from any thread -void dm_name_random(void); // character creation naming page: fill the name box with a random name (the same message as the original game's "Random Name" button). Valid while the name panel is open. Callable from any thread -int dm_name_taken(const char *name); // returns 1 if a character with this name already exists (a same-named directory exists under the save working directory) -int dm_name_gen(char *out, int cap, int (*taken)(const char *)); // random name used when the naming page is left blank (dm_namegen.c): syllable concatenation, draws again if taken is non-zero, at most 64 times; returns 1 on success -void dm_item_equip(uint32_t item_id); // immediately equip this item from the inventory (or stash / cube) into the corresponding slot (the endpoint of the original game's "hold confirm = equip"; the slot is chosen by the game, and the game swaps by itself when something is already equipped; when the mercenary panel is open and he can wear it, it goes to the mercenary, same as the original game). - // When it cannot be worn (level / stats too low, unidentified, not equipment) only the original game's error sound is played and no packet is sent -void dm_item_unequip(uint32_t item_id); // immediately unequip this worn item into the inventory (the endpoint of the original game's equipment panel "hold confirm = unequip"; when the inventory cannot hold it the game plays the error sound and does nothing). Not executed while the vendor page is open (in the vendor page that original path would turn into selling it) -void dm_item_identify(uint32_t target_id); // in the "used an identify scroll / identify tome, waiting to pick a target" state (dm_item_targeting() non-zero), complete the use on this item (the endpoint of the original game's item cell / equipment panel click handling in that state: sends the "use item on item" packet; scroll consumption / tome charge deduction is done by the server as in the original game). - // Not called when not in that state, when the target is not an item on oneself / in the inventory / stash / cube, or when the target has an operation pending confirmation (the log writes the reason) -void dm_item_repair(uint32_t item_id); // repair this one item in repair mode on the vendor page (the same endpoint as clicking an item in the original game's repair mode); not called when not in repair mode / something is in hand / the original game judges no repair is needed (the log writes the reason) -void dm_belt_fill(uint64_t belt_widget); // the belt widget in the inventory panel: fill the empty slots in one go with potions from the inventory that can go into the belt (the same code as the original controller "hold R with focus on the belt"); does nothing if the conditions are not met. Callable from any thread -void dm_item_target_cancel(void); // leave the "waiting to pick a target" state without consuming the scroll (the same code the original game runs when clicking on empty space / an invalid target: clears the pending item, resets the cursor state). Does nothing when not in that state -void dm_stash_take(int count); // from the stack the cursor is pointing at right now in the advanced stash (gems / materials / runes pages), take count pieces into the inventory (command 0x63, destination = inventory; 1 piece goes through the grid widget method main+0x39d824, multiple pieces go through main+0x39d9e0 with a piece count). - // When count exceeds the stack count, takes all that remain. Does nothing when there is no item under the cursor / it is not a stacked item / the stash is not open; the log line "take multiple:" gives the reason. When the inventory cannot hold them, the server decides how many to take (not verified) -void dm_stash_take_id(uint32_t item_id, int count); // same as above, specified by item id (for testing) -void dm_num_set(int n); // set the quantity box value: ≥0 that number, -1 all, -2 half (dm_textinput.c) -void dm_actions_pump(void); // called every frame where the controller is fetched (main thread) -void dm_held_stow(void); // put the item in hand (on the cursor) into the inventory without closing the panel: the step the original game does for "press back while holding an item" (main+0x3cf4d0: finds a place in the inventory for the item and sends 0x18). Callable from any thread, executed on the game main thread next frame -int dm_held_stow_result(void); // result of the most recent dm_held_stow: -1 no result yet (if it was not stowed it retries every frame for about 1.5 seconds: an item just swapped into the hand has to wait for the server's acknowledgement); 1 put into the inventory (packet sent); 0 not stowed, the held item is untouched -void dm_item_identify_free(uint32_t item_id); // identify this item for free (double-tap on an unidentified item): calls the original game's identify function main+0x78acf0 in the server tick, without consuming a scroll / tome -// One-press casting of off-hand set skills (that section of dm_actions.c) -uint64_t dm_swapskill_filter(uint64_t buttons); // where the controller state is fetched (hle_hid.c): hold back / let through the button that needs a weapon swap first -void dm_swapskill_srv_wset(int w); // the server tick (diag.c hud_tick) publishes the current weapon set 0 / 1 -int dm_swapskill_slot(int slot, int *charges); // skill button display: 1 = the skill is in the other set ("II" badge), 2 = charged skill (*charges = remaining charges) -int dm_swapskill_equipped(int32_t item_id); // this item is worn on the body (including the weapon-swap set) diff --git a/dm/host/dm_bossrush.h b/dm/host/dm_bossrush.h deleted file mode 100644 index 66a5937..0000000 --- a/dm/host/dm_bossrush.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -// Boss rush: the door in Harrogath → 5 stages at the Arreat Summit → a door into the Worldstone Chamber, 5 stages with the terror zone forced → 2 more stages in Hell. For implementation and rationale see dm_bossrush.c -#include <stdint.h> -void dm_bossrush_tick(uint64_t game, uint64_t player, uint8_t *main_base); // called every server tick (where diag.c reads the player state) -void dm_bossrush_c2s(uint64_t game, uint64_t player, const uint8_t *pkt, uint32_t len); // called per packet at the client command packet dispatch entry (observation gate OBS_C2S) -void dm_game_notice(uint8_t *main_base, const char *text); // original-game system notification (one chat bar line + the system notification box), called from the server thread -int dm_bossrush_owns(uint32_t id); // this monster unit was spawned by the current boss rush stage -void dm_kills_tick(uint64_t G, uint64_t player, uint8_t *main_base); // kill board (dm_kills.c), called in the server tick -void dm_door_label(uint8_t *main_base, uint64_t game, int cls, const char *key, const char *zh, const char *en, int in_town); // name label of the doors we create: in town the string key of this object class is replaced with key (attached to the original game's string table), and written back on leaving town diff --git a/dm/host/dm_cursor.h b/dm/host/dm_cursor.h deleted file mode 100644 index 6fd85f4..0000000 --- a/dm/host/dm_cursor.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once -#include <stdint.h> - -void dm_cursor_set(int x, int y); -// The front end registers a 'pending write to the original game's cursor' (screen point coordinates), and the host applies it when the game reads the controller (hle_hid fill): -// it reaches the game in the same sample as the keys. If the front-end main thread writes the cursor global directly at an arbitrary time, the game writes it back from its own controller cursor state before processing A, and A lands at the old cursor -void dm_cursor_set_screen(float x, float y, float screen_w, float screen_h); -void dm_cursor_clear(void); -// Stick closed loop (interim approach, used until the original game's mouse mode switch is found): on a real device the original game's per-frame mouse move path does not run, and a directly written cursor gets overwritten by the controller cursor logic. -// Instead: on every controller read, read the original game's cursor (main+0x2d128f8/fc), and if it is > DM_CURSOR_NEAR (9) pixels from the target push the left stick toward the target (force reduced by distance within 60 pixels); -// an A pressed before it is in position is queued and held back together with the target position at that time, and released only when the cursor reaches that position (one each for the start / end of a drag-and-drop); if not in position after 1.5 seconds in the queue, report a failure and drop it. -// Called where the host reads the controller (hle_hid fill), reaching the game in the same sample as the keys -void dm_cursor_apply(uint64_t *buttons, int32_t *lx, int32_t *ly); -int dm_cursor_pending(void); // How many A presses are still held back and not delivered to the game (the cursor is not in position yet); the front end must wait for it to return to zero before ending the pulse -// Native click (UI mouse message injection, see dm_cursor.c): the front end hands over the touch points, and the host sends them frame by frame to the UI root widget where the game reads the controller -void dm_ui_send_queue(const char *spec); // 'category:action[:argument]'; sent to the UI root widget on the next controller read (used by the MSG step of key scripts) -int dm_ui_fe_drag_ok(void); // Whether a vertical drag in the front end can count as a scroll right now (not while the delete confirmation / mode page / naming page / a confirmation box is open) -void dm_ui_touch(int phase, float nx, float ny); // phase: 0 down, 1 move, 2 up, 3 cancelled (turned into a scroll); coordinates = normalized to the rendered picture [0,1] -void dm_ui_mouse_pump(void); -// Generic pointer events: HOVER starts a held hover (pins the cursor every frame), MOVE updates the position, END ends the hold; the rest are queued, one per frame. arg: SCROLL = wheel amount (120 per notch), KEY = VK key code -enum { DM_UP_HOVER = 0, DM_UP_MOVE = 1, DM_UP_END = 2, DM_UP_LDOWN = 3, DM_UP_LUP = 4, DM_UP_RDOWN = 5, DM_UP_RUP = 6, DM_UP_SCROLL = 7, DM_UP_KEYDOWN = 8, DM_UP_KEYUP = 9 }; -unsigned dm_ui_item_query(float nx, float ny); // Ask 'is this point an item area'; returns the question sequence number; the main thread answers on the next frame -int dm_ui_item_result(unsigned seq); // 1 item area / 2 tab / 0 other widget inside a panel / 3 outside the panels / 6 pressing on a button covered by an upper panel (not clicked) / 7 goods on the vendor shelf / 8 stacked cell / 10 another item while the single-item slot is occupied (left alone) / -1 not answered yet -unsigned dm_ui_drop_query(float nx, float ny_lifted, float ny_finger); // How to handle a drag-and-drop landing point: result 5 put at the position given by dm_ui_drop_point (item area; for an equipment slot the slot center) / 3 drop / 0 put back where it was (fetched with dm_ui_item_result as well) -int dm_ui_drop_at_now(float nx, float ny_lifted, float ny_finger); // Synchronous version, callable only from the game main thread -int dm_ui_origin_point(float *nx, float *ny); // Center of the original position of the most recently 'picked up' item (used to put it back where it was); 0 = none -int dm_ui_drop_last(void); // Class of the most recently answered drag-and-drop landing point (used on release if this one has not been answered yet; -1 = none) -int dm_ui_drop_point(float *nx, float *ny); // Put-down position when the landing verdict is 5 (normalized) -int dm_ui_drop_bag(void); // When the landing verdict is 5, whether the landing point is a player inventory cell (shelf drag-to-buy / dragging out of a stacked cell only complete here) -int dm_fe_bliss(void); // Whether "Elysium" on the character-creation mode page is checked (1 = checked; the last value is kept after the player leaves the mode page and is read at character creation). Readable from any thread -int dm_ui_shelf_grid(float g[4]); // Vendor shelf grid (normalized: top-left x,y, width and height of one cell); 0 = not probed yet -int dm_ui_item_rect(unsigned seq, float r[4]); // Screen rectangle of the goods being held down on the vendor shelf (normalized; used for the finger-following image when dragging goods); 0 = none / not ready yet, ask again next frame -int dm_ui_stack_left(void); // How many items are in the stack of the stacked cell pressed this time; -1 = not read yet -void dm_ui_world_tap(float ux, float uy, float wx, float wy); // Tap on the world in the game state: a tap on the mercenary portrait = open the mercenary page, otherwise forwarded to dm_world_tap(wx, wy). u = normalized to the view, w = normalized to the rendered picture -void dm_ui_stack_take_at(float nx, float ny); // A stack in the advanced stash dragged to the inventory and released: put the cursor back on that stack and take 1 item into the inventory -void dm_ui_stack_cube_at(float nx, float ny); // A stack in the advanced stash dragged onto the Horadric Cube cell at the bottom of this page and released (landing class 11): put the cursor back on that stack and press the original game's X = 1 item into the Horadric Cube -void dm_ui_shop_buy_at(float nx, float ny); // Goods on the vendor shelf dragged to the inventory and released: put the controller cursor back on those goods (it was moved away when the drag started) and buy them -void dm_ui_item_tap(void); // Single tap in an item area: when in the 'identify scroll / tome used, waiting for target selection' state = identify the item under the cursor; does nothing when not in that state -int dm_ui_targeting(void); // Whether that state is in effect right now (published by the host every frame, read from any thread) -int dm_ui_panel_resident(const char *name); // In-run always-on layers (the HUD layers, mercenary portrait bar, save indicator, button legend...): being open does not count as 'a panel is open' -enum { DM_PS_BANK = 1, DM_PS_VENDOR = 2, DM_PS_DEAD = 4, DM_PS_TEXT = 8, DM_PS_IMBUE = 16, DM_PS_CUBE = 32, DM_PS_VAULT = 64 }; // VAULT = the stash is open on the sixth page "Chronicle" (dm_vault_ui.c) // CUBE = the Horadric Cube page is open (HoradricCubeLayout) // IMBUE = the single-item task slot panel is open (ImbueItemsPanel, shared by imbue / socketing / personalize / tomb pedestal) -int dm_ui_panel_state(void); -void dm_ui_lifecycle(int background); /* Called when the front end enters / leaves the background: position guard self-check (dm_cursor.c mv_audit) */ // Panel state bits (published by reading the game's panel tree every frame; on change the synthetic UI message DM:PanelState is sent) -int dm_ui_panel_shown(const char *name); // This panel is visible in the game's panel tree right now (called from the game main thread) -int dm_ui_tap_box(void); // Whether the most recently single-tapped item is the Horadric Cube -uint32_t dm_ui_cur_stack_id(void); // While the stash is open, if the cursor is pointing at an item in the inventory right now = its unit id, otherwise 0 -int dm_ui_free_cursor(void); // Whether the current page has a free cursor -void dm_ui_use_item(void); // Double-tap in an item area: a vendor's goods under the cursor -> buy; equippable -> the original game's 'hold the confirm button' path (auto-equip); everything else -> use on the spot (drink a potion / read a scroll / open the Horadric Cube) -void dm_ui_long_press(void); // Long-press in an item area = refill the belt: if the controller cursor points at a potion in the inventory that can go into the belt and the belt has room, put it into the belt; anything else is left alone -void dm_ui_tab_go(void); // The most recent hit was a tab and this time it is a tap: press ZL / ZR (L / R at the top) enough times to switch to it -int dm_ui_item_at_now(float nx, float ny); // Synchronous version of the above; may only be called from the game main thread -void dm_ui_watch(const char *spec); // Test probe: watch the UI widget 'name[@panel name]' (NULL cancels); the main thread publishes its center every frame -int dm_ui_watch_pos(float *nx, float *ny); // Any thread: returns 1 if the watched widget is visible and gives its coordinates normalized to the game view -uint64_t dm_ui_taps_base(void); // Test: presented frame at which DM_UI_TAPS_AT hit (0 = not hit yet) -void dm_ui_ptr(int kind, float nx, float ny, int arg); -int dm_ui_touch_active(void); -// Front-end path streamlining (see dm_cursor.c): UI message callback registration, execution where the controller is read, auto key mask -void dm_ui_on_uimsg(const char *cat, const char *act, const char *arg); -void dm_ui_frontend_pump(void); -uint64_t dm_ui_auto_buttons(void); -void dm_ui_key_pulse(uint64_t mask, int frames); // Tap a controller key on the player's behalf (held for frames guest frames) -// Continuous UI gestures (called by the front end from any thread, executed every frame on the main thread): kind 1 = vertical drag -> scroll, 2 = horizontal drag -> slider; coordinates = normalized to the rendered picture [0,1]; tol_ny = how much vertical slack to allow when identifying a slider (finger thickness) -void dm_ui_drag_begin(int kind, float ox, float oy, float tol_ny); -void dm_ui_drag_move(float x, float y); -void dm_ui_drag_end(void); -void dm_ui_drag_lift(float ny); // Before a drag starts, report how far the item image is lifted above the finger (as a fraction of screen height): dragging a chronicle cell back to the inventory decides the landing point by the item image position (dm_vault_ui.c) -// Rectangle of the top-right minimap (the game's AutoMap panel) in the rendered picture, normalized [0,1]: r = left, top, right, bottom. Returns 1 = has been read (the most recent one is given while the panel is closed); callable from any thread -int dm_ui_minimap_rect(float r[4]); -// Full-screen map ↔ minimap (switched at runtime, not written to disk): dm_ui_map_full(1) switches to full screen, (0) returns to the minimap level set by the player; dm_ui_map_is_full() = whether the game is currently on the full-screen map (callable from any thread, refreshed every frame on the main thread) -// Touch flow for skill binding (called by the front end from any thread, advanced every frame on the main thread): dm_ui_bind_begin(key) = choose a skill for this controller key (may carry ZL = second set) -- the host opens the skill tree, -// the player taps a skill (while the flow is in progress the front end turns the tap into 'cursor to the point + press Y') -> the original game's "Press button to rebind skill" box pops up -> the host presses the target key for the player -> the panel closes. dm_ui_bind_active() = flow in progress (0 none / 1 opening the skill tree / 2 waiting for the player to tap a skill / 3 wrapping up) -void dm_ui_bind_begin(uint64_t key_mask); -int dm_ui_bind_slot(void); /* Binding target slot 1..8 (F1–F8), 0 when not in the binding state */ -void dm_ui_safe_left(int units); /* Left safe area, in layout units (2160 high); the mercenary / party member portrait bar in the top-left corner is moved inward by it */ -void dm_ui_safe_right(int units); /* Right safe area, in layout units (2160 high); the minimap and info bar in the top-right corner are moved inward by it */ -uint32_t dm_ui_hit_id(void); /* The most recently hit point-spending button: attribute '+' 1..4 (strength / dexterity / vitality / energy), skill icon = an identifier with the top bit set to 1; 0 if nothing was hit */ -int dm_ui_bind_active(void); uint64_t dm_ui_auto_release(void); /* Used where the controller is read: the keys to release from the player's input right now */ int dm_ui_bind_skill_hit(float *nx, float *ny); /* The most recent hit (after snapping) landed on a point-spending button: 1 = a skill icon in the skill tree, 2 = a '+' on the attributes page; comes with normalized coordinates */ -void dm_ui_bind_cancel(void); -void dm_ui_map_full(int on); -int dm_ui_map_is_full(void); -void dm_ui_text_entered(const char *text); // Called when the system keyboard returns: text = final text written back to the game (UTF-8); pass NULL for cancel / empty string -// Latency readout (diagnostic, DM_LAT_LOG=1): at key moments print a line "LAT <millisecond timestamp> <event>", to measure the latency from finger down / release to the game taking effect -int dm_lat_on(void); -#define DM_LAT(fmt, ...) do { if (dm_lat_on()) LOG("LAT %llu " fmt, (unsigned long long)(now_ns() / 1000000), ##__VA_ARGS__); } while (0) -void dm_ui_hand_idle(int idle); // Reported by the touch front end every tick: no fingers, no keys or cursor requests being sent / queued = 1 (used by the held-item guard, dm_cursor.c hand_guard) -uint32_t dm_ui_hover_unid(void); // id of our own unidentified item the cursor is pointing at right now (inventory / stash / cube / equipped; none = 0). Published by the host every frame, read from any thread -void dm_ui_identify_hovered(void); // Touch layer double-tap on an unidentified item: on the next frame the host calls free identify on it (dm_item_identify_free) -int dm_ui_map_geom(float *cell); // Map geometry: returns 0 not open / 1 minimap / 2 full screen, *cell = pixels per cell (real picture pixels, any thread) diff --git a/dm/host/dm_cursor_item.h b/dm/host/dm_cursor_item.h deleted file mode 100644 index 3ad497e..0000000 --- a/dm/host/dm_cursor_item.h +++ /dev/null @@ -1,22 +0,0 @@ -// 'The item the controller cursor / pointer is currently pointing at' in an item panel: taken from the game's own hover object (the function used by the bottom key hints), not back-computed from cell coordinates. -// May only be called from the game main thread (where the controller is read / UI message callbacks): it reads client UI widgets and the client unit table, synchronously, read-only, without locking. -#pragma once -#include <stdint.h> -enum { DM_ITEM_AT_NONE, DM_ITEM_AT_INVENTORY, DM_ITEM_AT_BELT, DM_ITEM_AT_EQUIP, DM_ITEM_AT_STASH, DM_ITEM_AT_CUBE, DM_ITEM_AT_SHOP, DM_ITEM_AT_TRADE }; -typedef struct { - uint32_t id, cls; // Item unit id, item table row number - char code[4]; // Item code (trailing spaces removed, e.g. "hp1", "box"), 0-terminated - int container; // DM_ITEM_AT_* - int potion, scroll, book; // Item type (the game's type membership function): potion / scroll / book - int beltable; // Beltable by type (the belt flag in the item type table) - int belt_room; // Beltable and the belt has room right now (the same check that precedes the game's "Move to Belt" hint) - int equippable; // Can be equipped right now (the same check that precedes the game's "Equip" hint) - int stack; // Stack count of this cell in the advanced stash (gem / material / rune pages); not a stacked item = 0 (item data +0x9c, the same byte the original game checks before a long-press takes multiple items) - int unid; // Unidentified: bit 0x10 of item data ([item+0x10]) +0x18 is 0 (the original game's 'identified' flag; verified at both ends by flipping it in the test code dm_actions.c) - int gx, gy, gw, gh; // Top-left cell and cells occupied of the item in its container (inventory / stash / cube / shop / trade); for the belt gives (cell number, 0, 1, 1), for an equipment slot gives (0, 0, 1, 1) -} dm_cursor_item_info; -int dm_cursor_item(dm_cursor_item_info *out); // 1 = pointing at an item and out has been filled; 0 = none (panel not open / pointing at an empty cell / focus not on an item cell) -int dm_held_size(int *w, int *h); // Cells occupied by the item held in hand (width, height); returns 0 if nothing is held. The 'identify scroll used, waiting for a target' case does not count as held -int dm_held_pending(void); // Whether the item in hand has an operation pending confirmation right now (the previous command has not been answered yet); returns 0 if nothing is held -int dm_vendor_repair_mode(void); // Whether the vendor page is in the 'repair' cursor mode right now (repair button tapped, hammer cursor). May only be called from the game main thread -int dm_item_targeting(void); // Whether the 'an item was used, waiting to select a target item' state is in effect right now: 0 none; non-zero = the target type of that item in the item table + 1 (identify scroll / identify tome both go through this state) diff --git a/dm/host/dm_custom.h b/dm/host/dm_custom.h deleted file mode 100644 index 6289c35..0000000 --- a/dm/host/dm_custom.h +++ /dev/null @@ -1,15 +0,0 @@ -// "Custom" settings: on/off switches for the features we added ourselves, all read and written here. Each module only calls dm_custom_get and does not read files / environment variables as settings on its own. -#pragma once - -// Effective value 0 / 1: changes only after the player taps "Apply" on the options page, unchanged by "Discard", "Default" = each key's default (power-saving mode off, the rest on). Returns 1 for an unrecognized key. -// Callable from any thread: the first call reads the file once, and after that it is just an atomic read + a few string comparisons, lock-free. On iOS it can be read already during startup (before the save is mounted). -// Keys: auto_mana auto-drink potions (both red and blue; the key name is kept) / drop_light light beam on high-value drops / map_mark minimap markers for entrances and exits, waypoints and quest goals / item_notes item notes and old translated names / icloud iCloud save sync (cloud_ios.m, read at startup) / pot_pick potion auto-pickup (dm_potpick.c) / drop_haptic haptics on high-value drops (dm_drophaptic.c) / gem_pick auto-pickup of runes and gems into the advanced stash (pouch) / store_btn the inventory "Deposit" button (dm_pouch.c) / eco_fps power-saving mode 30 fps (nvn_mtl.m, off by default) -int dm_custom_get(const char *key); -int dm_custom_lang_explicit(void); // Value of the language switch (key lang_en) as written in the settings file: -1 never written (original settings left alone) / 0 Simplified Chinese / 1 English; read by hle_fs.c at save mount -int dm_fps_setting(void); // Power-saving mode (key eco_fps, off by default): on = 30 fps, off = 60; read by mtl_fps_target in nvn_mtl.m - -void dm_custom_init(const char *dir); // Directory holding the settings file (the iOS shell passes Documents at startup); if not called, the parent of the save commit directory is used (Mac host) - -// The following are only for the UI wiring of the custom page -void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg); // Options page apply / default / discard (forwarded from the UI message callback in dm_cursor.c) -void dm_custom_ui_pump(unsigned long long root); // Game main thread, every frame: while the options page is open, read the checkbox of each row (widget name TouchCustom_<key>) diff --git a/dm/host/dm_doorspot.h b/dm/host/dm_doorspot.h deleted file mode 100644 index bb1dab1..0000000 --- a/dm/host/dm_doorspot.h +++ /dev/null @@ -1,42 +0,0 @@ -// Landing points of the two doors at the rift completion (pure algorithm, does not touch game memory; used by dm_rift.c, tested by tests/t_doorspot.c). -// What must be guaranteed: the doors land on cells the player "can walk to". Previously "the player walks 10 cells with their back to the guardian, offset 5 cells to the left and to the right" was handed directly to the original game's free-ground search main+0x7160d0 (mask 0x400 = objects only), -// the wall bit (0x1) is not in the mask and there is no reachability check at all -- in narrow maps such as the Cave / the Pit the player stands right against a wall, and 10 cells away is inside the wall or outside the map (figure: completion of rift level 26 on Pit Level 1, both doors in the black area). -// Approach: take a window centered on the player, blk[] = whether this cell blocks the player (the caller fills it by the player's own collision mask; everything outside the window counts as blocked); from the cell at the player's feet do a breadth-first search with the "player occupies a cross" way of walking, -// and only the cells the search reaches are candidates = necessarily reachable. Among the candidates take the one nearest to the ideal point, in three tiers: -// tier 0: the 2 cells around it (5 × 5) are all reachable cells -- the door body does not touch a wall and the player can walk around it; tier 1: the 1 cell around it (3 × 3); tier 2: reachable is enough, switched to nearest to the player (when too narrow to fit, fall back to the nearest walkable cell by the feet). -// If none holds (the player is boxed into a single cell), land on the cell at the player's feet. -#pragma once -#include <stdint.h> -#include <stdlib.h> -#include <string.h> - -enum { DS_R = 24, DS_W = 2 * DS_R + 1, DS_N = DS_W * DS_W }; // window radius 24 cells: the ideal point is 10 cells out and then offset by another 5 cells, leaving a margin of as much again -enum { DS_FAR = 36 }; // doors of tier 0 / 1 are at most this many steps of walking from the player (about 11 cells in a straight line; ones reached only by going around a wall are not wanted) -enum { DS_SELF_GAP = 3 }; // doors of tier 0 / 1 are at least 3 cells from the player (not on top of the player and the first-clear reward at the player's feet) -struct ds_grid { uint8_t blk[DS_N]; int16_t dist[DS_N]; int reach; }; // blk is filled by the caller; dist / reach are computed by ds_flood (-1 = unreachable) - -static inline int ds_blk(const struct ds_grid *g, int ix, int iy) { return ix < 0 || iy < 0 || ix >= DS_W || iy >= DS_W || g->blk[iy * DS_W + ix]; } -// The player occupies a cross (center + four neighbors), same convention as ground-tap pathfinding (dm_world.c wp_free) -static inline int ds_free(const struct ds_grid *g, int ix, int iy) { return !(ds_blk(g, ix, iy) || ds_blk(g, ix + 1, iy) || ds_blk(g, ix - 1, iy) || ds_blk(g, ix, iy + 1) || ds_blk(g, ix, iy - 1)); } -static inline int ds_dist(const struct ds_grid *g, int ix, int iy) { return ix < 0 || iy < 0 || ix >= DS_W || iy >= DS_W ? -1 : g->dist[iy * DS_W + ix]; } -// Breadth-first search from the window center (the player's feet). The start cell is not required to have its whole cross free: the player is standing right there at this moment -static inline void ds_flood(struct ds_grid *g) { - static uint16_t q[DS_N]; int h = 0, t = 0; memset(g->dist, 0xff, sizeof g->dist); g->reach = 0; - g->dist[DS_R * DS_W + DS_R] = 0; q[t++] = (uint16_t)(DS_R * DS_W + DS_R); - while (h < t) { int c = q[h++], cx = c % DS_W, cy = c / DS_W; g->reach++; - static const int d4[4][2] = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }; - for (int k = 0; k < 4; k++) { int nx = cx + d4[k][0], ny = cy + d4[k][1]; if (ds_dist(g, nx, ny) != -1 || nx < 0 || ny < 0 || nx >= DS_W || ny >= DS_W || !ds_free(g, nx, ny)) continue; - g->dist[ny * DS_W + nx] = (int16_t)(g->dist[c] + 1); q[t++] = (uint16_t)(ny * DS_W + nx); } } -} -static inline int ds_clear(const struct ds_grid *g, int ix, int iy, int k) { for (int dy = -k; dy <= k; dy++) for (int dx = -k; dx <= k; dx++) if (ds_dist(g, ix + dx, iy + dy) < 0) return 0; return 1; } -// Pick the landing point of one door. All coordinates are cell offsets relative to the player (player = 0,0). (wx, wy) is the ideal point; when have_o, (ox, oy) is the other door already decided, and the new door is at least gap cells away from it (tier 2 lowers this to 2 cells). -// Returns which tier was used (0 / 1 / 2; 3 = landed at the player's feet); the landing point is written to *rx / *ry -static inline int ds_pick(const struct ds_grid *g, int wx, int wy, int have_o, int ox, int oy, int gap, int *rx, int *ry) { - for (int tier = 0; tier < 3; tier++) { long best = -1; int bx = 0, by = 0, tx = tier == 2 ? 0 : wx, ty = tier == 2 ? 0 : wy; - for (int y = -DS_R; y <= DS_R; y++) for (int x = -DS_R; x <= DS_R; x++) { int d = ds_dist(g, x + DS_R, y + DS_R); if (d < 0) continue; - int far = abs(x) > abs(y) ? abs(x) : abs(y), og = !have_o ? 1 << 20 : abs(x - ox) > abs(y - oy) ? abs(x - ox) : abs(y - oy); - if (tier < 2 ? (far < DS_SELF_GAP || d > DS_FAR || og < gap || !ds_clear(g, x + DS_R, y + DS_R, 2 - tier)) : (far < 1 || og < 2)) continue; - long s = (long)(x - tx) * (x - tx) + (long)(y - ty) * (y - ty); if (best >= 0 && s >= best) continue; best = s; bx = x; by = y; } - if (best >= 0) { *rx = bx; *ry = by; return tier; } } - *rx = 0; *ry = 0; return 3; -} diff --git a/dm/host/dm_elysium.h b/dm/host/dm_elysium.h deleted file mode 100644 index 8d9fee5..0000000 --- a/dm/host/dm_elysium.h +++ /dev/null @@ -1,7 +0,0 @@ -#include <stdint.h> -// "Elysium": a per-character switch checked at character creation and unchangeable afterwards. While that character is in a run: unique drop rate ×5, sets ×3 (quality ratio table). For the implementation and its basis see dm_elysium.c -#pragma once -void dm_elysium_arm(int on); // Called by the checkbox on the character creation page: 1 = the character created this time is an Elysium character, 0 = it is not. Applies only to the immediately following character creation and is cleared once used -int dm_elysium_active(void); // Whether the current run is the run of an Elysium character (tables changed) -void dm_elysium_pump(void); // Called every frame on the game main thread (dm_actions_pump) -void dm_elysium_stash_step(uint64_t game, uint64_t player, uint8_t *main_base); // Called every tick on the game server side (where diag.c reads the player state): tops up the shared stash after a new Elysium character is created diff --git a/dm/host/dm_hud.h b/dm/host/dm_hud.h deleted file mode 100644 index a6e7adf..0000000 --- a/dm/host/dm_hud.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -// HUD numbers for bottom UI v4 (read by the host once per server tick; the front end UIKit draws 'Lv · %', badges and the difficulty number) and difficulty level requests. -// Difficulty goes through the same chain as the original game's /players handler (main+0x415db0): main+0x3102c0 gets the players setting object -> main+0x1199c50(object, N); -// it is applied at the next server tick entry (game thread); the original game's rules apply to monsters spawned from then on (measured: level 8 stat 100 = 8, max life floor(×4.5)). -typedef struct { - int valid; // In a run and the controlled player has been read - int level; // stat 12 - int exp_permille; // Experience progress within the current level ‰ (thresholds from the original game's experience.txt) - int belt[4]; // How many items remain in each of the four belt columns (belt item x % 4 = column) - int portal; // Town portal count: quantity in the Tome of Town Portal tbk (stat 70) + number of Scroll of Town Portal tsc - int players; // Current players setting (players setting object +0xc); 0 = not read - int hp, hp_max, mp, mp_max; // Life / mana (stat 6–9 ÷ 256, rounded to an integer) - int area, x, y; // Current area number (levels.txt Id; 0 = not read) and player coordinates (cells) - int points; // Unassigned attribute points stat 4 + skill points stat 5 (lit state of the red square cross) - int holding; // An item is held on the cursor (a drag-and-drop did not complete and the item is still in hand): the front end uses this to treat 'tap on empty space' as cancel - int quest_unread; // A new quest is waiting to be read: the original game's HUD quest hint is lit (client UI state byte main+0x2d5ff58); cleared after the quest panel is opened (the other condition for the red state of the bottom bar cross) - int dead; // Player dead (mode 0 / 17): the front end hides the UIKit bottom bar so it does not block the death screen -} dm_hud_info; -// Player 'dead' test = the game's own definition (main+0x6e5c48 / 0x6e5cc8 client command dispatch: a player unit whose [+0xc] mode is 0 or 0x11 is handled as dead; -// D2 player mode 0 = DEATH, in the death animation, 17 = DEAD, already dead; 0 -> 17 when the death animation ends, and it becomes town standing 5 after reviving back in town) -static inline int dm_hud_player_mode_dead(unsigned mode) { return mode == 0 || mode == 17; } -// UIKit bottom bar visibility: shown only in the normal in-run state -- it gets out of the way when the player has not been read, on death, in navigation states (panel / loading screen / cutscene), and when blocked by the panel stack -// Area numbers of the five act towns (levels.txt Id): Rogue Encampment 1, Lut Gholein 40, Kurast Docks 75, Pandemonium Fortress 103, Harrogath 109 -static inline int dm_hud_area_is_town(int area) { return area == 1 || area == 40 || area == 75 || area == 103 || area == 109; } -static inline int dm_hud_bar_visible(const dm_hud_info *h, int navigation, int blocked) { return h->valid && !h->dead && !navigation && !blocked; } -void dm_hud_get(dm_hud_info *out); -void dm_hud_request_players(int n); // 1..8; callable from any thread -// Host internal (diag.c tick entry): take the latest request, publish this tick's readings -int dm_hud_requested_players(void); -void dm_hud_publish(const dm_hud_info *in); diff --git a/dm/host/dm_i18n.c b/dm/host/dm_i18n.c deleted file mode 100644 index 3de957a..0000000 --- a/dm/host/dm_i18n.c +++ /dev/null @@ -1,71 +0,0 @@ -// String lookup for the UI text we added ourselves. Our own UI text is English only, whatever language the game is set to. -// Table: dm_i18n_tab.h (key, English), kept by hand and compiled into this file. -// - dm_str: text drawn inside the game (layouts write "@key") goes through the original string lookup at main+0x7f5630 first and falls back to the -// table when the game's string tables lack the key. -// - dm_ui: places the game cannot draw (UIKit alerts, the asset download page, shared file names, bottom-bar glyphs) read the table directly. -// The game's own language (dm_lang_index, a separate matter from our text): once the game has set it (SetLanguage observation gate), that value wins; -// before that (asset download page, before entering the front end) read "Language" from Settings/Settings.json in the save data; -// no settings file = first install, use the iOS system language (dm_sys_lang). -#include "dm_i18n.h" -#include "dm_i18n_tab.h" -#include <TargetConditionals.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -_Atomic int g_dm_lang = -1; - -#if TARGET_OS_IPHONE -#include <CoreFoundation/CoreFoundation.h> -#endif -// Language tag (BCP 47, e.g. en-US, zh-Hans-CN, zh-Hant-TW, yue-Hant-HK) → the original game's language index. Only two outcomes: a language code that -// is Chinese (zh) or a Chinese variety -// (yue Cantonese, wuu Wu, hak Hakka, nan Min Nan, cmn Mandarin, gan Gan, hsn Xiang, lzh Literary Chinese) → 8 Simplified Chinese, regardless of script -// and region; everything else → 0 English, because nobody has verified the original game's other languages such as German / Japanese. Empty tag = English -int dm_lang_from_tag(const char *t) { - if (!t || !*t) return 0; - char l[4] = { 0 }; int i = 0; while (i < 3 && t[i] && t[i] != '-' && t[i] != '_') { l[i] = (char)(t[i] | 0x20); i++; } - static const char *const zh[] = { "zh", "yue", "wuu", "hak", "nan", "cmn", "gan", "hsn", "lzh" }; - for (unsigned k = 0; k < sizeof zh / sizeof *zh; k++) if (!strcmp(l, zh[k])) return 8; - return 0; -} -// Language on first install (no settings file yet): by the first entry of the iOS preferred languages (CFLocaleCopyPreferredLanguages, through dm_lang_from_tag: Chinese and Sinitic varieties → Simplified Chinese, everything else → English). -// It used to be always Simplified Chinese (back then the touch version had no way to change the language, and a Chinese player whose system was in English got the English version on a fresh install and could not change it back); the Custom page title row now has a language switch (dm_custom.c lang_en), so this is opened up. -// DM_LANG (host / test simulation, same as hle_oe.c) takes priority; a non-iOS host has no system language to ask and uses English, the default game language -int dm_sys_lang(void) { - const char *e = getenv("DM_LANG"); if (e && *e) return dm_lang_from_tag(e); -#if TARGET_OS_IPHONE - int r = 0; CFArrayRef a = CFLocaleCopyPreferredLanguages(); - if (a) { if (CFArrayGetCount(a) > 0) { char t[64]; if (CFStringGetCString((CFStringRef)CFArrayGetValueAtIndex(a, 0), t, sizeof t, kCFStringEncodingUTF8)) r = dm_lang_from_tag(t); } CFRelease(a); } - return r; -#else - return 0; -#endif -} - -static int settings_lang(void) { // "Language" in the settings file; file missing = first install, use the system language (hle_fs.c writes the same value when it first creates the settings file); file present but key missing = -1 - const char *sv = getenv("DM_SAVE"); if (!sv) return dm_sys_lang(); - char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", sv); - FILE *f = fopen(path, "rb"); if (!f) return dm_sys_lang(); - char buf[8192]; size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = 0; - const char *k = strstr(buf, "\"Language\""); if (!k) return -1; - k = strchr(k + 10, ':'); return k ? atoi(k + 1) : -1; -} - -int dm_lang_index(void) { - int l = atomic_load(&g_dm_lang); if (l >= 0) return l; - static _Atomic int file = -2; int f = atomic_load(&file); if (f == -2) { f = settings_lang(); atomic_store(&file, f); } - return f < 0 ? 0 : f; // settings file exists but has no language entry = the game default (the host reports en-US) -} - -const char *dm_ui(const char *key) { // our own UI text is English only; a key missing from the table comes back as it is - for (unsigned i = 0; i < sizeof k_dm_i18n / sizeof *k_dm_i18n; i++) if (!strcmp(k_dm_i18n[i].k, key)) return k_dm_i18n[i].en; - return key; -} - -const char *dm_str(uint8_t *b, const char *key) { - if (!b) return dm_ui(key); - struct { const char *k; uint64_t n; } s = { key, strlen(key) }; - const char *r = ((const char *(*)(void *))(b + 0x7f5630))(&s); - return r && *r && strcmp(r, key) ? r : dm_ui(key); // if the game's string tables lack this key, fall back to the compile-time table -} diff --git a/dm/host/dm_i18n.h b/dm/host/dm_i18n.h deleted file mode 100644 index db4f789..0000000 --- a/dm/host/dm_i18n.h +++ /dev/null @@ -1,10 +0,0 @@ -// String lookup for the UI text we added ourselves (English only; table = dm_i18n_tab.h, kept by hand), and the original game's language index -#pragma once -#include <stdatomic.h> -#include <stdint.h> -extern _Atomic int g_dm_lang; // the original game's current language index (0 enUS ... 8 zhCN), written by the SetLanguage observation gate (diag.c); -1 = the game has not set it yet -int dm_lang_index(void); // current language index of the original game (set by the game > settings file > system language on first install) -int dm_lang_from_tag(const char *tag); // language tag (en-US / zh-Hant-TW / es-419 ...) → the original game's language index: Chinese / Chinese varieties = 8 zhCN, everything else = 0 enUS -int dm_sys_lang(void); // system language → the original game's language index (DM_LANG > iOS preferred language > English) -const char *dm_str(uint8_t *main_base, const char *key); // text drawn inside the game: the original string lookup, falling back to dm_ui -const char *dm_ui(const char *key); // text drawn outside the game (UIKit / asset download page / bottom-bar glyphs): the English text from the compile-time table diff --git a/dm/host/dm_i18n_tab.h b/dm/host/dm_i18n_tab.h deleted file mode 100644 index e56dce3..0000000 --- a/dm/host/dm_i18n_tab.h +++ /dev/null @@ -1,302 +0,0 @@ -// UI text we added ourselves (key, English); a hand-kept table, edit it directly. Read by dm_ui() / dm_str() in dm_i18n.c -#pragma once -static const struct { const char *k, *en; } k_dm_i18n[] = { - { "dmPauseRestart", "Quick Restart" }, - { "dmUpdBuild", "Build %d" }, - { "dmUpdNew", "New version %d · %s" }, - { "dmUpdNoNet", "Can't check for updates · Allow Wireless Data for this app in iOS Settings" }, - { "dmUpdFail", "Update check failed" }, - { "dmUpdFailSite", "Update check failed · Open %s in a browser to update" }, - { "dmUpdNewUrgent", "New version %d · %s" }, - { "dmUpdUrgentTitle", "Update Recommended" }, - { "dmUpdButton", "Update" }, - { "dmVaultStored", "Stored" }, - { "dmVaultSeen", "Not stored" }, - { "dmVaultNone", "Not found" }, - { "dmVaultStoredTip", "Stored · Double-tap to withdraw" }, - { "dmVaultStats", "%d · %d stored · %d seen" }, - { "dmVaultSetStats", "Sets: %d · %d / %d pieces stored" }, - { "dmVaultSetFull", "%d / %d stored · complete" }, - { "dmVaultSetHave", "%d / %d stored" }, - { "dmVaultSearch", "%s: %d items (%d unique · %d set)" }, - { "dmStoreHead", "Deposited: " }, - { "dmStoreVault", "Chronicle %d" }, - { "dmStoreStash", "%sStash %d" }, - { "dmStorePouch", "%sPouch %d" }, - { "dmStoreSep", ", " }, - { "dmStoreNone", "No items to deposit" }, - { "dmStoreLeft", ", %d left in inventory" }, - { "dmStoreFull", "That stack is full. Item left where it is." }, - { "dmPouchTitle", "Pouch" }, - { "dmPouchEmpty", "(empty)" }, - { "dmRwTitle", "%s · Socketed (%d) · Runewords" }, - { "dmRwLevel", "Required Level: %d" }, - { "dmKbJoin3", "%s, %s, %s" }, - { "dmKbKilledNth", "%s slain! (kill #%d)" }, - { "dmKbKilledTotal", "%s slain %d times in total!" }, - { "dmKbKills", "Kills %d" }, - { "dmKbFirst", "First %m-%d %H:%M" }, - { "dmKbDrops", "Unique %d · Set %d" }, - { "dmKbTimes", "Best %s · Last %s · Avg %s" }, - { "dmKbTotal", "Total kills %s" }, - { "dmKbAct1", "Act I" }, - { "dmKbAct2", "Act II" }, - { "dmKbAct3", "Act III" }, - { "dmKbAct4", "Act IV" }, - { "dmKbAct5", "Act V" }, - { "dmKbRift", "Rift" }, - { "dmKbSortAct", "By Act" }, - { "dmKbSortCount", "By Kills" }, - { "dmKbSortFast", "By Best Time" }, - { "dmBrPrefix", "Boss Rush %s" }, - { "dmBrDoneThisGame", "Already completed this game. Try again next game" }, - { "dmBrToChamber", "First 5 stages done. Enter the portal to the Worldstone Chamber" }, - { "dmBrAllDone", "All stages complete" }, - { "dmBrStage", "Stage %d / %d: %s%s" }, - { "dmBrTerror", " (Terror Zone)" }, - { "dmBrAndariel", "Andariel" }, - { "dmBrDuriel", "Duriel" }, - { "dmBrMephisto", "Mephisto" }, - { "dmBrDiablo", "Diablo" }, - { "dmBrBaal", "Baal" }, - { "dmBrDiabloClone", "Diablo Clone" }, - { "dmBrUbers", "Uber Mephisto, Uber Diablo, Uber Baal" }, - { "dmBlissTitle", "Elysium" }, - { "dmBlissDesc", "Unique ×5, Set ×3, Runes ×5; bosses always drop as on first kill." }, - { "dmRwHint", "Move out of shared stash first" }, - { "dmRwGo", "Craft" }, - { "dmPauseShareLog", "Share Diagnostics" }, - { "dmCustomAutoMana", "Auto-drink (Life 50%, Mana 30%)" }, - { "dmCustomMercPot", "Auto-feed Merc at Life <50%" }, - { "dmCustomDropLight", "Light beam on valuable drops" }, - { "dmCustomMapMark", "Automap: exits, waypoints, quests" }, - { "dmCustomItemNotes", "Item notes (Chinese only, restart)" }, - { "dmCustomIcloud", "iCloud save sync (after restart)" }, - { "dmCustomPotPick", "Auto-pick potions into belt" }, - { "dmCustomDropHaptic", "Vibrate on valuable drops" }, - { "dmCustomGemPick", "Auto-stash runes & gems" }, - { "dmCustomStoreBtn", "Inventory Deposit button" }, - { "dmCustomEcoFps", "Power saver (30 fps)" }, - { "dmCustomLootFilter", "Loot Filter (auto, Nightmare+)" }, - { "dmCustomAutoBuff", "Auto-recast buffs when idle" }, - { "dmCustomLangEn", "Language: English" }, - { "dmCustomLangHint", "Restart to apply" }, - { "dmTabGameSettings", "Settings" }, - { "dmVaultPart0", "Helms" }, - { "dmVaultPart1", "Armor" }, - { "dmVaultPart2", "Shields\n& Gloves" }, - { "dmVaultPart3", "Boots\n& Belts" }, - { "dmVaultPart4", "Jewelry" }, - { "dmVaultPart5", "Swords\n& Daggers" }, - { "dmVaultPart6", "Axes\n& Claws" }, - { "dmVaultPart7", "Maces\n& Clubs" }, - { "dmVaultPart8", "Polearms\n& Spears" }, - { "dmVaultPart9", "Bows &\nCrossbows" }, - { "dmVaultPart10", "Staves\n& Casters" }, - { "dmVaultSearchHint", "Search name" }, - { "dmRvBytes", "%lld bytes" }, - { "dmRvItem", "%s%s (%s)" }, - { "dmRvSep", ", " }, - { "dmRvItem2", "%s%s (%s / %s)" }, - { "dmRvHolesBad", "bad hole table" }, - { "dmRvAllOk", "All %d files present" }, - { "dmRvReport", "%d / %d files present. %s%s%s%s%s%s" }, - { "dmRvMissing", "Missing: " }, - { "dmRvPeriod", ". " }, - { "dmRvPartial", "Incomplete: " }, - { "dmRvNoFile", "Missing %s" }, - { "dmRvHoles", "%s.holes has a bad hole table" }, - { "dmRvShort", "%s is incomplete (%lld / %lld bytes)" }, - { "dmDlAppFolder", "D2R Mobile" }, - { "dmDlQuoted", "%@" }, - { "dmDlIncomplete", "Resources in %s are incomplete: %s" }, - { "dmDlNoData", "No game resources found. \"D2R Mobile\" now contains: %s%s. Copy in the whole resource folder" }, - { "dmDlTidyFail", "Could not organize the game resources (%s)" }, - { "dmDlUnknown", "unknown reason" }, - { "dmDlPackBad", "The downloaded resource pack is incomplete: %s" }, - { "dmDlNone", "No game resources yet" }, - { "dmDlNameSep", ", " }, - { "dmDlEtc", " etc." }, - { "dmDlRetry", "Retry" }, - { "dmDlNoAssets", "No game resources yet\nDownload the \"D2R Mobile Resources\" folder from \"Download resource pack\" on the beta page,\nthen in Files copy the whole folder to \"On My iPhone → D2R Mobile\" (do not rename or nest it), then tap Retry" }, - { "dmDlTestProgress", "Downloading game resources\n0.00 / 5.50 GB (0%) 0.0 MB/s" }, - { "dmDlConnecting", "Connecting to the resource server…" }, - { "dmDlDownloading", "Downloading game resources…" }, - { "dmDlSpaceMsg", "%.2f GB more space needed.\nGame resources still need %.2f GB (plus 0.2 GB headroom); this %@ has %.2f GB free.\nFree up space in Settings → General → %@ Storage, then tap Retry." }, - { "dmDlSpaceTitle", "%@ Storage Full" }, - { "dmOk", "OK" }, - { "dmDlProgressNote", "Downloading game resources\n%.2f / %.2f GB (%.0f%%) %.1f MB/s%@" }, - { "dmDlCellTitle", "Using Cellular Data" }, - { "dmDlCellMsg", "%.2f GB of game resources left to download over cellular data. Wi-Fi is recommended." }, - { "dmDlCellLater", "Not Now" }, - { "dmDlCellWait", "No game resources yet\nConnect to Wi-Fi, then tap Retry" }, - { "dmDlCellGo", "Use Cellular Data" }, - { "dmDlProgress", "Downloading game resources\n%.2f / %.2f GB (%.0f%%) %.1f MB/s" }, - { "dmDlPaused", "Download paused (check your network). It resumes automatically." }, - { "dmDlPackWait", "The system is still preparing the resource pack. It continues automatically." }, - { "dmDlNotReady", "Game resources are not ready\n%@\nFix this, then tap Retry" }, - { "dmDlWill", "Downloading game resources: %@ remaining (%.1f GB total)" }, - { "dmDlNotStarted", "%@The resource download could not start: %@\n%@\nYou can also download the resource pack from the beta page and put it in \"On My iPhone → D2R Mobile\"" }, - { "dmDlNetDown", "this device is offline (on first launch the system asks whether to allow network access; if you declined, turn it on in Settings → D2R Mobile)" }, - { "dmDlNoServer", "cannot reach the resource server" }, - { "dmDlNoBase", "this build has no download address configured" }, - { "dmDlNextAuto", "It resumes automatically once you are back online, or tap Retry. Anything already downloaded is kept." }, - { "dmDlNextRetry", "Tap Retry to try again. Anything already downloaded is kept." }, - { "dmDlStopped", "The resource download stopped: %@\nDownloaded %.2f / %.2f GB. Tap Retry to resume (it will not start over)" }, - { "dmDlCopyIn", "No game resources yet\nCopy the game resource folder into \"On My iPhone → D2R Mobile\" in the Files app\nAny folder name or nesting works; then tap Retry" }, - { "dmDlCopyInShort", "Copy the missing files into the same folder, then tap Retry" }, - { "dmOldNoSupportPkg", "This build has no support for older devices (romfs_overlay_astc/pack.json is missing)" }, - { "dmOldPackSize", "The pack in \"%s\" has the wrong size (%.2f / %.2f GB); the copy looks incomplete" }, - { "dmOldPackBad", "The pack in \"%s\" does not match this version or is corrupted; please download it again" }, - { "dmOldPackNone", "Supplementary pack for older devices not found" }, - { "dmOldUnsupTitle", "Device Not Supported" }, - { "dmOldUnsupMsg", "This device (iPhone 14 or earlier, A16 chip or older) is not supported yet: its GPU cannot read the game's texture format. iPhone 15 Pro and later can run the game." }, - { "dmOldNeedTitle", "Supplementary Pack Required" }, - { "dmOldNeedMsg", "This device needs the supplementary pack for older devices (about %.1f GB).\nIn the Files app, copy the whole \"Legacy Device Pack\" folder into \"On My iPhone → D2R Mobile\", then open D2R Mobile again.%@" }, - { "dmOldWhySuffix", "\n(%s)" }, - { "dmOldDl", "Download" }, - { "dmOldCopied", "Already Copied" }, - { "dmOldQuit", "Quit" }, - { "dmOldConnecting", "Connecting to the download server…" }, - { "dmOldDlFail", "Download failed: %s" }, - { "dmOldProgress", "Downloading the supplementary pack\n%.2f / %.2f GB (%.0f%%)%@" }, - { "dmA11ySurface", "Game touch area" }, - { "dmInputTitle", "Input" }, - { "dmNameRule", "Letters or Chinese characters: 2–15 letters, or 1–5 Chinese characters (can be mixed)" }, - { "dmNameTitle", "Character Name" }, - { "dmQtyTitle", "Quantity" }, - { "dmNameBlankRandom", "Leave blank for a random name" }, - { "dmNamePickIme", "Finish choosing characters on the keyboard first" }, - { "dmNameTooShort", "At least 2 letters, or 1 Chinese character" }, - { "dmNameEdge", "Cannot start or end with - or _" }, - { "dmCancel", "Cancel" }, - { "dmConfirm", "OK" }, - { "dmSaveNoSpace", "Not enough storage. Progress was not saved" }, - { "dmHudLife", "Life: %d / %d" }, - { "dmHudMana", "Mana: %d / %d" }, - { "dmDlVerifying", "Verifying %@" }, - { "dmDlWriteFail", "Failed to write %@: %@" }, - { "dmDlBad3", "%@ failed verification 3 times" }, - { "dmDlNetRetry", "Network lost. Retrying in %d s" }, - { "dmDlReconnect", "Unstable connection. Reconnected (attempt %d)" }, - { "dmShareLogFile", "D2RMobile_diagnostics_%@_%@.txt" }, - { "dmVaultTierN", "Norm" }, - { "dmVaultTierX", "Exc" }, - { "dmVaultTierE", "Elite" }, - { "dmStashVaultTab", "Vault" }, - { "dmTroveTab", "Trove" }, - { "dmRiftSayPrefix", "Rift %s" }, - { "dmRiftSayFirst", "Level %d first clear reward" }, - { "dmRiftSayDoneBoth", "Rift complete! Portals opened" }, - { "dmRiftSayLateBoth", "Not cleared. Portals opened" }, - { "dmRiftSayLateNext", "Not cleared. Portal opened" }, - { "dmRiftSayLateTown", "Not cleared. Town Portal open" }, - { "dmRiftSayLate", "Level not cleared" }, - { "dmRiftSayDoneNext", "Rift complete! Portal opened" }, - { "dmRiftSayDoneTown", "Complete! Town Portal open" }, - { "dmRiftSayDone", "Rift complete" }, - { "dmRiftSayErr", "The Rift could not be opened" }, - { "dmRiftSayStart", "Level %d: 15 minute time limit" }, - { "dmRiftSayGift", "A Rainbow Facet has dropped" }, - { "dmRiftSayTimeout", "Time's up: level not cleared" }, - { "dmRiftSayGuardian", "The Rift Guardian appears!" }, - { "dmElyToaGround", "Inventory full: Token of Absolution dropped" }, - { "dmMercSlotLocked", "Clear Greater Rift Level 10 to unlock" }, - { "dmRiftPtsGot", "Level %d: +%d Skill Points, +%d Stat Points" }, - { "dmSkillCapLocked", "Clear any Rift level to unlock the skill level cap" }, - { "dmRiftPtsHeld", "Spend your points to get the rest" }, - { "dmRiftTierN", "Level %d" }, - { "dmRiftCapResel", "Choose another level" }, - { "dmRiftCapGuard", "Return to Guardian" }, - { "dmRiftCapVoid", "Run will be lost" }, - { "dmRiftCapVoidPad", "Hold X to quit level" }, - { "dmRiftTitleDone", "Rift Complete" }, - { "dmRiftTitleLate", "Time's Up" }, - { "dmRiftTitleRun", "Rift in Progress" }, - { "dmRiftGuardDown", "Guardian slain" }, - { "dmRiftNextOpen", "Next portal open" }, - { "dmRiftRetryOpen", "Portal reopened" }, - { "dmRiftSaved", "Progress saved" }, - { "dmRiftNotCounted", "Not cleared" }, - { "dmRiftTimeLeft", "Time Left" }, - { "dmRiftProgress", "Progress" }, - { "dmRiftLvlBase", "Unchanged" }, - { "dmRiftGamUni", "Unique" }, - { "dmRiftGamSet", "Set" }, - { "dmRiftGamUniAfter", "Unique after" }, - { "dmRiftGamSetAfter", "Set after" }, - { "dmRiftMsGot", "Claimed" }, - { "dmRiftMsNext", "Next" }, - { "dmRiftMsNo", "Locked" }, - { "dmKbRiftName", "Greater Rift" }, - { "dmKbRiftBest", "Best Level: %d" }, - { "dmKbRiftNever", "No Greater Rifts yet" }, - { "dmKbRiftClears", "Cleared %d · Deaths %d" }, - { "dmKbRiftUnlock", "Max: %d" }, - { "dmKbRiftMs", "First Clear Rewards" }, - { "dmKbRiftMsNext", "Next at Level %d" }, - { "dmKbRiftMsAll", "All claimed" }, - { "dmKbRiftFast", "Best Time %d:%02d" }, - { "dmKbRiftMsGot", "First clear reward claimed" }, - { "dmKbRiftTotal", "Best Level: %d" }, - { "dmCloudConflict", "iCloud shared stash mismatch: kept this device. Please contact us" }, - { "dmRiftHudTitle", "Rift" }, - { "dmRiftHudDone", "Level %d complete" }, - { "dmRiftHudLate", "Level %d overtime" }, - { "dmRiftHudGuard", "Rift Guardian" }, - { "dmRiftHudNext", "Next: Level %d" }, - { "dmRiftHudSame", "Next: same level" }, - { "dmRiftHudKill", "Slay the Rift Guardian" }, - { "dmRiftHudGoal", "Slay the monsters %d%%" }, - { "dmRiftMsItem10", "Sunder Charm" }, - { "dmRiftMsSub10", "One at random" }, - { "dmRiftMsTier10", "10" }, - { "dmRiftMsItem20", "Annihilus" }, - { "dmRiftMsTier20", "20" }, - { "dmRiftMsItem30", "Hellfire Torch" }, - { "dmRiftMsTier30", "30" }, - { "dmRiftMsItem40", "Ethereal Elite" }, - { "dmRiftMsSub40", "Socketed item" }, - { "dmRiftMsTier40", "40" }, - { "dmRiftMsItem50", "Colossal Jewel" }, - { "dmRiftMsSub50", "One at random" }, - { "dmRiftMsTier50", "50" }, - { "dmRiftMsItem60", "Death's Fathom" }, - { "dmRiftMsTier60", "60" }, - { "dmRiftMsItem70", "Griffon's Eye" }, - { "dmRiftMsTier70", "70" }, - { "dmRiftMsItem80", "Tyrael's Might" }, - { "dmRiftMsTier80", "80" }, - { "dmRiftMsItem90", "Sunder Charm" }, - { "dmRiftMsSub90", "One you are missing" }, - { "dmRiftMsTier90", "90" }, - { "dmRiftMsItem100", "6 High Runes" }, - { "dmRiftMsSub100", "Lo to Zod, one each" }, - { "dmRiftMsTier100", "100" }, - { "dmRiftSelT", "Select Level" }, - { "dmRiftInfT", "This Level" }, - { "dmRiftRange", "Unlocked" }, - { "dmRiftBest", "Highest" }, - { "dmRiftStr", "Monster Power" }, - { "dmRiftLvl", "Monster Level" }, - { "dmRiftTime", "Time Limit" }, - { "dmRiftGamT", "Gamble at Anya" }, - { "dmRiftBtnStart", "Enter Rift" }, - { "dmRiftBtnAbandon", "Quit" }, - { "dmRiftBtnResel", "Select Level" }, - { "dmRiftCapCont", "Resume your run" }, - { "dmRiftBtnCont", "Continue" }, - { "dmRiftMsT", "First Clear" }, - { "dmRiftMsN", "+Rune, Token, Socket" }, - { "dmRiftPanelTitle", "Greater Rift" }, - { "dmPadLegDiff", "Difficulty" }, - { "dmPadLegSort", "Sort" }, - { "dmPadLegView", "View / Withdraw" }, - { "dmPadLegPrev", "Previous Page" }, - { "dmPadLegNext", "Next Page" }, - { "dmPadHintDeposit", "Deposit" }, - { "dmPadLegIdentify", "Pick Up (Hold to Identify)" }, - { "dmPadDiffLine", "Game Difficulty: %d" }, - { "dmRiftGamAfterEn", "After This Level" }, - { "dmRiftMsNEn", "+ Rune, Token of Absolution, Sockets" }, -}; diff --git a/dm/host/dm_mode_items.h b/dm/host/dm_mode_items.h deleted file mode 100644 index 8c20904..0000000 --- a/dm/host/dm_mode_items.h +++ /dev/null @@ -1,12 +0,0 @@ -// Registry of touch-only / controller-only behaviors: the mode switch transaction (dm_padmode.c) calls the "leave" of each item on the old side before changing the layout, and the "enter" of each item on the new side after the rebuild completes. -// Leave = the item stands down by itself: void in-flight key-press / message requests made on the player's behalf, write moved original widgets back, clear cached widget addresses (after a rebuild the old addresses are all dangling). -// Side: 0 = acts only in touch mode, 1 = acts only in controller mode, 2 = present on both sides but holding things that become invalid with a rebuild (leaves once and enters once on every switch). -// Register a new mode-specific behavior with one row here; do not check the mode separately elsewhere, and do not have the switch code call some module by name. -// X(name, side, enter, leave) enter / leave: void (*)(void); write dm_mode_none (an empty function) when there is none -#define DM_MODE_ITEMS(X) \ - X("runeword list", 2, dm_mode_none, dm_runeword_ui_rebuild) \ - X("front end path straightening and key presses for the player", 0, dm_mode_none, dm_ui_auto_mode_leave) \ - X("UI touch cache and widget move table", 2, dm_mode_none, dm_ui_cache_mode_leave) \ - X("main menu build number and new version notice", 2, dm_update_mode_enter, dm_mode_none) \ - X("death popup reopened for the current mode", 2, dm_dead_modal_mode_enter, dm_mode_none) \ - X("cube side by side", 0, dm_mode_none, dm_cube_side_mode_leave) diff --git a/dm/host/dm_modetx.h b/dm/host/dm_modetx.h deleted file mode 100644 index 1cedeeb..0000000 --- a/dm/host/dm_modetx.h +++ /dev/null @@ -1,32 +0,0 @@ -// Transaction for switching between controller / touch modes (pure state machine, does not touch game memory; used by dm_padmode.c, tested by tests/t_modetx.c). -// One switch = freeze input → registered items of the old side stand down → change the layout prefix and report the other operation mode to the game (the game rebuilds the whole UI) → read back that the rebuild is complete → report the original operation mode again (rebuild once more) → -// read back that the rebuild is complete → registered items of the new side take their places → release input. "Rebuild complete" does not count frames: the caller supplies a readback signal count tips (the game came to read the operation mode, the set of panels under the UI root was replaced; each counts once), -// and it is complete when the count has moved and then stays unchanged for SETTLE consecutive frames; if that has not happened after TIMEOUT frames a timeout is recorded and it proceeds anyway (input cannot be kept frozen forever), and the caller reports a failure. -// New requests arriving during a transaction do not interrupt it: the caller keeps only the last wanted mode and compares once more after the transaction completes and returns to idle. -#ifndef DM_MODETX_H -#define DM_MODETX_H -#include <stdint.h> -enum { DM_MODETX_SETTLE = 4, DM_MODETX_TIMEOUT = 240 }; -enum { DM_TX_IDLE, DM_TX_REBUILD1, DM_TX_REBUILD2 }; -enum { DM_TXA_NONE, DM_TXA_BEGIN, DM_TXA_REPORT_BACK, DM_TXA_DONE }; // What the caller should do this frame -typedef struct { int st, timeouts; uint64_t at, moved_at; uint32_t tips; } dm_modetx; -static inline int dm_modetx_step(dm_modetx *t, uint64_t frame, int want, int cur, uint32_t tips) { - if (t->st == DM_TX_IDLE) { - if (want < 0 || want == cur) return DM_TXA_NONE; - t->st = DM_TX_REBUILD1; t->at = frame; t->moved_at = 0; t->tips = tips; t->timeouts = 0; return DM_TXA_BEGIN; } - if (tips != t->tips) { t->tips = tips; t->moved_at = frame; } - int done = t->moved_at && frame - t->moved_at >= DM_MODETX_SETTLE, late = frame - t->at >= DM_MODETX_TIMEOUT; - if (!done && !late) return DM_TXA_NONE; - if (!done) t->timeouts++; - t->at = frame; t->moved_at = 0; - if (t->st == DM_TX_REBUILD1) { t->st = DM_TX_REBUILD2; return DM_TXA_REPORT_BACK; } - t->st = DM_TX_IDLE; return DM_TXA_DONE; -} -// Input gate: during the freeze all keys handed to the game are 0, and the keys held at any point during it are recorded; after release these keys must be let go once before they are handed to the game again -// (the press that triggers the switch only does the switch: pressing A switches the UI to the controller version, and that A must not land on the freshly built UI). Returns 1 = the sticks are zeroed too -typedef struct { uint64_t hold; } dm_modegate; -static inline int dm_modegate_apply(dm_modegate *g, int frozen, uint64_t *buttons) { - if (frozen) { g->hold |= *buttons; *buttons = 0; return 1; } - g->hold &= *buttons; *buttons &= ~g->hold; return 0; -} -#endif diff --git a/dm/host/dm_padmap.h b/dm/host/dm_padmap.h deleted file mode 100644 index eb82327..0000000 --- a/dm/host/dm_padmap.h +++ /dev/null @@ -1,27 +0,0 @@ -// The four face buttons of a physical controller -> game keys: mapped by the letter printed on the button glyph (press whichever letter the -// button button legend shows). Pure function, used by the front end dm_gamepad.m, checked by the host unit test tests/t_padmap.c. -// The four face buttons given by the system (GameController) are defined by position, not by the letter on the button glyph: -// buttonA bottom, buttonB right, buttonX left, buttonY top. -// - Nintendo controllers (Switch Pro, Joy-Con): the glyphs are right A, bottom B, top X, left Y -- identical to the Switch as is. -// - Xbox controllers: the glyphs are bottom A, right B, left X, top Y. -// - Controllers with no letters on the glyphs (PS etc.) and unrecognized types: laid out by the Xbox positions (bottom button = confirm). -// The type is recognized by whether the two strings given by the system, product category and vendor name (case-insensitive), contain -// switch / nintendo / joy-con; neither = laid out as Xbox. -// (The vendor name of the Switch Pro is "Pro Controller", which has none of these words, so it must be recognized by the product category -// "Switch Pro Controller"; that is why both strings are checked.) -#pragma once -#include <stdint.h> -#include <string.h> -#include <strings.h> -#include "dm_input.h" -enum { DM_PADKIND_XBOX = 0, DM_PADKIND_NINTENDO = 1 }; -static inline int dm_padmap_has(const char *s, const char *w) { size_t n = strlen(w); for (; s && *s; s++) if (!strncasecmp(s, w, n)) return 1; return 0; } -static inline int dm_padmap_kind(const char *category, const char *vendor) { - static const char *const k[] = { "switch", "nintendo", "joy-con" }; - for (unsigned i = 0; i < sizeof k / sizeof *k; i++) if (dm_padmap_has(category, k[i]) || dm_padmap_has(vendor, k[i])) return DM_PADKIND_NINTENDO; - return DM_PADKIND_XBOX; -} -static inline uint64_t dm_padmap_face(int kind, int south, int east, int west, int north) { // The four parameters = whether the bottom / right / left / top button is held - if (kind == DM_PADKIND_NINTENDO) return (east ? DM_BTN_A : 0) | (south ? DM_BTN_B : 0) | (north ? DM_BTN_X : 0) | (west ? DM_BTN_Y : 0); - return (south ? DM_BTN_A : 0) | (east ? DM_BTN_B : 0) | (west ? DM_BTN_X : 0) | (north ? DM_BTN_Y : 0); -} diff --git a/dm/host/dm_rift.h b/dm/host/dm_rift.h deleted file mode 100644 index aa8cd48..0000000 --- a/dm/host/dm_rift.h +++ /dev/null @@ -1,27 +0,0 @@ -// Rift tower climb (dm_rift.c) -#pragma once -#include <stdint.h> -typedef struct { int tier; float progress; int secs_left, done, guardian, inc; } dm_rift_hud_t; -void dm_rift_tick(uint64_t G, uint64_t player, uint8_t *main_base); // Server tick (diag.c) -void dm_rift_c2s(uint64_t G, const uint8_t *pkt, uint32_t len); // Client command (door tap) -void dm_rift_client_pump(uint8_t *main_base); // At the controller sampling point (dm_world.c): recognize kills and newly spawned monsters -int dm_rift_hud(dm_rift_hud_t *out); // Front end every frame: returns 1 while a rift is in progress / just completed -int dm_rift_active(void); -void dm_rift_gamble_vals(int best, uint32_t *uniq, uint32_t *set); // Gambling bonus: highest cleared level -> Hell unique / set values (in units of 1/100000); best < 1 = 0 (no bonus) -typedef struct { int menu, best, unlock, def; unsigned ms; int clvl, tier, done, secs_left; float progress; int inc; } dm_rift_menu_t; // menu 1 = level selection page, 2 = continue / quit; inc = 0 with done set = Guardian defeated after time ran out (not passed) -int dm_rift_cleared_any(void); // has this character cleared any Greater Rift level (only clears within the time limit count: highest cleared level ≥ 1): the unlock condition for the hard skill point cap (dm_actions.c) -int dm_rift_merc_unlocked(void); // Whether the new mercenary equipment slots are unlocked (highest rift level cleared ≥ 10) -void dm_rift_merc_locked_hint(void); // Hint when not unlocked: "Clear Greater Rift Level 10 to unlock" -int dm_rift_tier_level(int tier, int clvl); // this level's monster level (below level 9, 0 = unchanged) -int dm_rift_tier_hp_x10(int tier, int clvl); // total life multiplier of this level's monsters ×10 (relative to a native Hell /players 1 monster) -int dm_rift_menu(dm_rift_menu_t *out); // Front end every frame: the page to show (0 = none) -void dm_rift_menu_close(void); // Close (the X at the top right) -void dm_rift_menu_start(int tier); // Level selection page "Enter Rift" -void dm_rift_menu_continue(void); // "Continue" -void dm_rift_menu_abandon(void); -int dm_rift_menu_pending(void); int dm_rift_menu_seq(void); int dm_rift_swept(uint32_t id); // "Quit" (after abandoning, the server opens the level selection page again) -void dm_rift_menu_peek(dm_rift_menu_t *out); // Fetch the page readout even when there is no page to open (chronicle "Rift" page) -void dm_rift_ui_pump(void); // dm_rift_ui.c: every frame on the game main thread (dm_actions.c) -void dm_rift_ui_on_uimsg(const char *cat, const char *act, const char *arg); // UI messages (diag.c) -typedef struct { int best, clears, deaths, tier, unlock; unsigned ms; int fast[101]; } dm_rift_stats_t; -int dm_rift_stats(dm_rift_stats_t *out); // Kill board "Rift" page readout (sidecar file); returns 0 if there is none diff --git a/dm/host/dm_textbox_core.h b/dm/host/dm_textbox_core.h deleted file mode 100644 index 1a4cdb1..0000000 --- a/dm/host/dm_textbox_core.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -// Text box color code mapping table (implementation and notes in dm_textbox_core.c) -#include <stddef.h> -int dm_textbox_load(const char *path); // Read the table, return the entry count; return -1 if it cannot be opened (table cleared) -void dm_textbox_reset(void); -int dm_textbox_count(void); -// Rewrite by the table: 0 = unchanged (*out is NULL); 1 = whole-string hit; 2 = annotated substrings inside were replaced. *out is freed by the caller -int dm_textbox_rewrite(const char *s, size_t n, char **out, size_t *on); -// Read the mapping table (read only once, does not hook the gate; implemented in dm_textbox.c): returns 1 = table present, 0 = none (item annotations off / no overlay) -int dm_textbox_ensure_loaded(void); diff --git a/dm/host/dm_uiscreen.h b/dm/host/dm_uiscreen.h deleted file mode 100644 index 5903c26..0000000 --- a/dm/host/dm_uiscreen.h +++ /dev/null @@ -1,17 +0,0 @@ -// On screens narrower than 16:9 (iPad 4:3), lay out the game UI at 16:9 while 3D still fills the whole screen. Off by default: when not turned on nothing is written, and the conversions below are all identity. Implementation and basis in dm_uiscreen.c -#pragma once -enum { DM_UISCREEN_OFF, DM_UISCREEN_TOP, DM_UISCREEN_CENTER, DM_UISCREEN_BOTTOM }; -void dm_uiscreen_enable(int mode); // Called by the front end when "the window is narrower than 16:9": DM_UISCREEN_BOTTOM etc. = turn on, with the UI rectangle (width = frame width, height = width÷16×9) top-aligned / centered / bottom-aligned; DM_UISCREEN_OFF = turn off and write the game's values back. Callable from any thread, takes effect next frame -void dm_uiscreen_pump(void); // Called every frame on the game main thread: on finding that the game rewrote the "screen width / height" globals (boot, switching docked / handheld mode), record the real values and rewrite them to the values the UI uses - this one place only, regardless of occasion -int dm_screen_real(int *w, int *h); // Real frame width and height (the one used by 3D rendering and hit boxes). Returns 0 = not read yet -void dm_ui_rect(int *x, int *y, int *w, int *h); // Position of the UI rectangle in the real frame (pixels); when off = the whole frame. The graphics layer shifting the UI draw and the front end placing the game view both read it; it is the only source for the vertical position -float dm_ui_ny(float ny_real); // Normalized y in the real frame → normalized y within the UI rectangle (horizontal unchanged) -float dm_ui_ny_back(float ny_ui); // Reverse -int dm_ui_proj_dy(void); // Number of vertical pixels subtracted from the result of the game's "world point → screen" (main+0x1237c60 / 0x90d110) (the UI rectangle's y; not subtracting = 0). Add it back when the host itself calls these two functions and needs real frame coordinates -int dm_uiscreen_ingame(void); // 1 = in a run (true fullscreen); 0 = out of a run (front end pages and loading screen: 16:9 centered with margins). The single signal, readable from any thread -void dm_uiscreen_set_ingame(int on); // May only be called from the per-frame UI site (dm_cursor.c) -void dm_uiscreen_set_panel(int on); // A large panel open in a run (function pages / stash / vendor / cube..., = the touch front end's panel mode): the UI rectangle moves to the center; when closed it drops back to the placement the front end chose. Called by the touch front end in the UI message callback -int dm_uiscreen_panel(void); // The state above: a large panel open in a run = 1 -#include <stdint.h> -uint64_t dm_game_res(int docked); // The value the game's render resolution parameter slot should give right now (packed {u32 width, u32 height}): the front end's native value, changed to 16:9 of the screen width when true fullscreen is on and not in a run -void dm_uiscreen_res_reread(void); // May only be called by hle_oe.c when the game takes the "performance mode changed" notification diff --git a/dm/host/dm_unit.h b/dm/host/dm_unit.h deleted file mode 100644 index 7f1d917..0000000 --- a/dm/host/dm_unit.h +++ /dev/null @@ -1,31 +0,0 @@ -// Unit → the level it is in (the host reads game memory directly; server units and client units share the same offsets): -// unit +0x38 path → path +0x20 room (for object / item class units the "path" is a different struct, with the room at +0) → room +0x18 → +0x90 level → level +0x1f8 level id. -// Every step can be NULL and must be null-checked one by one: during the act-change loading screen the client player has a path but no room. -// Previously various places read four levels in one go, rd(rd(rd(rd(u + 0x38) + 0x20) + 0x18) + 0x90); rd only guards address 0, so with a NULL room it went on to read address 0x18 → -// sig=11 addr=0x18 on both real device and Mac (in a rift, tapping "Continue" after dying in another act's map, during those few act-change loading frames). Unit test tests/t_unit_level.c. -#ifndef DM_UNIT_H -#define DM_UNIT_H -#include <stdint.h> -static inline uint64_t dm_unit_rd64(uint64_t a) { return a ? *(volatile uint64_t *)(uintptr_t)a : 0; } -// Level struct pointer; returns 0 if any step is NULL -static inline uint64_t dm_unit_level_ptr(uint64_t u) { - uint64_t path = u ? dm_unit_rd64(u + 0x38) : 0, room = path ? dm_unit_rd64(path + 0x20) : 0, ex = room ? dm_unit_rd64(room + 0x18) : 0; - return ex ? dm_unit_rd64(ex + 0x90) : 0; -} -// Level id; returns 0 = unknown level when it cannot be read (unit / path / room NULL at any step) -static inline int dm_unit_level(uint64_t u) { uint64_t lv = dm_unit_level_ptr(u); return lv ? *(volatile int32_t *)(uintptr_t)(lv + 0x1f8) : 0; } -// Head of a level's room chain (level +0x10, next room = room +0x48); returns 0 when the level pointer is NULL - anything that walks rooms from the result of dm_unit_level_ptr must go through here, -// so that "level could not be read" never turns into reading address 0x10 -static inline uint64_t dm_level_rooms(uint64_t lv) { return lv ? dm_unit_rd64(lv + 0x10) : 0; } -// Fixed-point teleport: put the unit in a free spot next to (target room, x, y) and send the client a relocation = the original main+0x7cc530(game, -// unit, target room, x, y, 0, 0). -// The target room must be given: the original change-level-by-number 0x634000 is itself "0x5596c0 gets the landing point and room → 0x7cc530(..., room, -// x, y, 0, 0)" (0x634168–0x634188). -// Passing 0 only works when the target is near the unit's current room; a target in another cluster of rooms returns failure (after finishing a Rift and -// returning to town, tapping "Continue" landed on that map's waypoint). -// A room of 0 (the target room is not loaded / not in this map) returns 0 directly without taking chances -static inline int dm_teleport(const uint8_t *main_base, uint64_t G, uint64_t unit, uint64_t room, int x, int y) { - if (!main_base || !G || !unit || !room) return 0; - return ((int (*)(uint64_t, uint64_t, uint64_t, int, int, int, int))(uintptr_t)(main_base + 0x7cc530))(G, unit, room, x, y, 0, 0); -} -#endif diff --git a/dm/host/dm_widget.h b/dm/host/dm_widget.h deleted file mode 100644 index 3a0b84b..0000000 --- a/dm/host/dm_widget.h +++ /dev/null @@ -1,31 +0,0 @@ -// Before reading or writing "class-specific fields" of a native UI widget by offset, check that it is the expected class (vtable). -// Why: a widget found by name is not guaranteed to be the class the offsets were measured on (layout changed, a same-named widget changed class, the game swapped the slot for another widget), and writing by offset then scribbles into some other object. -// Base-class fields shared by all widgets (name +8, visible +0x50 / +0x51, children +0x58 / +0x60, rect +0x70) need no check. -// Vtable values are offsets within the main module (3.3.94012), read on the host: -#pragma once -#include <stdint.h> - -enum { - DM_VT_TEXT = 0x20ba4c8, // text box: string +0x88 / length +0x90, text color +0xbc - DM_VT_INPUT = 0x20b8718, // input box: committed text string +0x558 - DM_VT_NUMINPUT = 0x20b91c0, // numeric input box - DM_VT_TOGGLE = 0x20bc110, // checkbox / sub-tab (ToggleButtonWidget): checked +0xa88, four frame ids at +0x6e8.., current frame +0x610 - DM_VT_TABBAR = 0x20ba3a8, // tab bar (TabBarWidget) - DM_VT_SLOT = 0x2089fa0, // item slot: body location +0x5d8, owner +0x5c8 - DM_VT_SLOT_B = 0x2089ab8, // item slot (another kind, recognized by dm_actions.c / dm_runeword.c) - DM_VT_SLOT_C = 0x2088fc8, // item slot (the third kind, recognized by dm_cursor_item.c) - DM_VT_IMAGE = 0x20b8650, // image widget: current frame +0x90 (read on the host: Img on the Chronicle page) - DM_VT_BUTTON = 0x20b7b00, // button: four frame ids at +0x6e8.., current frame +0x610 (read on the host: TouchRF_M1 on the Greater Rift level select page) -}; - -static inline uint64_t dm_widget_vt(const uint8_t *base, uint64_t w) { return w && base ? *(const uint64_t *)(uintptr_t)w - (uint64_t)(uintptr_t)base : 0; } -static inline int dm_widget_is(const uint8_t *base, uint64_t w, uint64_t vt) { return w && base && dm_widget_vt(base, w) == vt; } -static inline int dm_widget_is_slot(const uint8_t *base, uint64_t w) { uint64_t v = dm_widget_vt(base, w); return v == DM_VT_SLOT || v == DM_VT_SLOT_B || v == DM_VT_SLOT_C; } - -// For call sites: expected class → 1; otherwise → 0 and log one failure (at most 5 per file, no log flooding). A file using it needs LOG (hle.h) and one DM_WIDGET_RED_DEF in the file -#define DM_WIDGET_OK(base, w, vt, who) (dm_widget_is((base), (w), (vt)) ? 1 : dm_widget_red_((base), (w), (vt), (who))) -#define DM_WIDGET_RED_DEF \ - static int dm_widget_red_(const uint8_t *base, uint64_t w, uint64_t vt, const char *who) { static int n; \ - if (w && base && n++ < 5) { const char *nm = *(const char **)(uintptr_t)(w + 8); \ - LOG("[RED] %s: widget '%s' is not of the expected class (vtable main+%#llx, should be %#llx), offsets not read or written", who, nm ? nm : "?", (unsigned long long)dm_widget_vt(base, w), (unsigned long long)vt); } \ - return 0; } diff --git a/dm/host/dm_world.h b/dm/host/dm_world.h deleted file mode 100644 index f407842..0000000 --- a/dm/host/dm_world.h +++ /dev/null @@ -1,11 +0,0 @@ -// Tapping a target in the game world = interacting with it (implementation and rationale in dm_world.c) -#pragma once -unsigned dm_world_tap(float nx, float ny); // any thread: queue one tap (coordinates normalized to the rendered picture, [0,1]); the main thread resolves the target and sends the command at the controller-poll point, and sends nothing when there is no target. Returns the sequence number -int dm_world_tap_result(unsigned seq); // -1 not executed yet; 0 no target; 1 item (pick up) 2 NPC (talk) 3 object (use) 4 exit 5 corpse (retrieve) -int dm_world_has_target(void); // any thread: whether a target that the primary button would interact with exists (the game's own controller-version selection) -void dm_world_pump(void); // called every frame where the controller is fetched (main thread) -void dm_world_watch(int type, uint32_t cls); // test probe: watch one kind of unit (type < 0 cancels); the main thread publishes the screen position of its body box center every frame -int dm_world_watch_pos(float *nx, float *ny); // any thread: returns 1 and gives normalized coordinates when the watched unit is in the picture -// Minimap quest goals (dm_world.c): published by the main thread every frame, fetched by the front end from any thread. x / y = world cell; art >= 64 = frame art-64 of the Tal Rasha symbols, otherwise = nameplate number (tools/gen_map_goal_art.py) -typedef struct { int n, px, py; float sx, sy; int x[12], y[12]; unsigned char art[12]; } dm_map_goal_view; // px / py player world cell; sx / sy position of the player's feet on screen (normalized) -int dm_map_goals(dm_map_goal_view *v); // returns the number of goals (map marking switch off, no goal in this level, scan not finished = 0) diff --git a/dm/host/front.h b/dm/host/front.h index 2b0e63d..a548d0b 100644 --- a/dm/host/front.h +++ b/dm/host/front.h @@ -4,10 +4,7 @@ void front_run(void (*guest)(void)); // The CAMetalLayer the graphics backend presents frames to (NULL when there is no window) void *front_metal_layer(void); -// Game output resolution (the rewriter's PARAMS resolution parameter slot): docked = 1 docked mode / 0 handheld mode, returns (u64)height << 32 | width. -// The original game's constants are 1600x900 / 1280x720 (16:9); front ends with a different screen aspect ratio widen at the same height, and the game natively renders at the new width and re-lays out the UI (width ≤ 4096). #include <stdint.h> -uint64_t front_game_res(int docked); // Text input box (for the software keyboard swkbd): the main thread shows a native input box and the calling thread blocks until the user confirms / cancels. // All text is UTF-8; max_chars is the maximum character count (0 = unlimited, only a hint for the front end; truncation is done uniformly by the caller hle_swkbd.c in UTF-16 units). // Confirm returns 1 and writes the result into out (including the terminating 0); cancel returns 0; @@ -20,7 +17,7 @@ void dm_host_preload_start(int argc, char **argv); // Touchscreen (implemented by the host's hle_touch.c; called by the iOS / Mac front end): whenever the set of touch points changes (down / move / up / cancel) the whole set is pushed once; n = 0 = all lifted. // id = finger slot number 0–15 (the lowest free slot on down, unchanged until up; used directly as finger_id); x / y = normalized coordinates [0,1) on the output frame, origin at top left. -// The host converts to the panel coordinates the game reads, 1280x720 (x·1280, y·720, the way main 0x1b65260 reads them), and latches short taps (a down and up between two reads is reported at least once). May be called from any thread. +// The host converts to the panel coordinates the game reads, 1280x720 (x·1280, y·720), and latches short taps (a down and up between two reads is reported at least once). May be called from any thread. typedef struct { uint32_t id; float x, y; } dm_touch; void dm_touch_set(const dm_touch *t, int n); diff --git a/dm/host/front_mac.m b/dm/host/front_mac.m index b2017ec..9c87408 100644 --- a/dm/host/front_mac.m +++ b/dm/host/front_mac.m @@ -6,10 +6,6 @@ #import <Metal/Metal.h> #include "dm.h" #include "dm_input.h" -#include "dm_uiscreen.h" -void mtl_set_ui_rect_fn(void (*fn)(double *y, double *h)); -static void ui_rect_frac(double *fy, double *fh) { int x, y, w, h, rw = 0, rh = 0; dm_ui_rect(&x, &y, &w, &h); if (dm_screen_real(&rw, &rh) && rh > 0) { *fy = (double)y / rh; *fh = (double)h / rh; } } // Same as front_ios.m: vertical position and height of the UI rectangle (as fractions of the frame height) -__attribute__((constructor)) static void ui_shift_reg(void) { mtl_set_ui_rect_fn(ui_rect_frac); } // Must also take effect when running without a window (screenshots / gate checks), so it is not placed where the layer is created #include "front.h" #include "hle.h" #include <signal.h> @@ -19,13 +15,6 @@ static uint64_t g_btn; static int g_w, g_a, g_s, g_d; void *front_metal_layer(void) { return (__bridge void *)g_layer; } -// The original game's 16:9 constants, behavior unchanged; DM_GAME_RES=<width>x<height> (docked mode; handheld scaled by 720/900) is only for side-by-side comparison with iOS widescreen -uint64_t front_game_res(int docked) { - unsigned w, h; const char *e = getenv("DM_GAME_RES"); - if (e && sscanf(e, "%ux%u", &w, &h) == 2) return docked ? (uint64_t)h << 32 | w : (uint64_t)(h * 4 / 5) << 32 | (w * 4 / 5 + 3) / 4 * 4; - return docked ? 900ull << 32 | 1600 : 720ull << 32 | 1280; -} - static void push(void) { int32_t lx = (g_d - g_a) * 32767, ly = (g_w - g_s) * 32767; dm_input_set(g_btn, lx, ly, 0, 0); diff --git a/dm/host/hle.c b/dm/host/hle.c index 4e44c8a..0b52502 100644 --- a/dm/host/hle.c +++ b/dm/host/hle.c @@ -61,8 +61,6 @@ enum { G_SVC, G_CNTPCT, G_CNTVCT, G_OBS }; static void (*g_obs_fn[OBS_MAX])(const uint64_t *regs); static int g_obs_have[OBS_MAX]; void gate_obs_set(int id, void (*fn)(const uint64_t *regs)) { if (id <= 0 || id >= OBS_MAX) die("observation gate number out of range %d", id); g_obs_fn[id] = fn; } int gate_obs_present(int id) { return id > 0 && id < OBS_MAX && g_obs_have[id]; } -static uint8_t g_svc_have[256]; // svc numbers registered in the build output (used to tell whether the host call numbers from 0x80 up, introduced by cuts, are present) -int gate_svc_present(int num) { return num >= 0 && num < 256 && g_svc_have[num]; } typedef struct { uint64_t ret; int kind, arg; uint64_t orig; } gate_t; #define GATE_BITS 12 // 4096 slots, gate count ≤ 1024, load factor ≤ 1/4 static gate_t g_gate[1u << GATE_BITS]; @@ -73,7 +71,6 @@ void gate_register(uint64_t ret_abs, const char *kind, int arg, uint64_t orig) { int k = !strcmp(kind, "svc") ? G_SVC : !strcmp(kind, "cntpct") ? G_CNTPCT : !strcmp(kind, "cntvct") ? G_CNTVCT : !strcmp(kind, "obs") ? G_OBS : -1; if (k < 0) die("unknown gate kind %s", kind); if (k == G_OBS) { if (arg <= 0 || arg >= OBS_MAX) die("observation gate number out of range %d", arg); g_obs_have[arg] = 1; } - if (k == G_SVC && arg >= 0 && arg < 256) g_svc_have[arg] = 1; if (++g_ngate > (1 << GATE_BITS) / 4) die("gate overflow"); unsigned i = gate_h(ret_abs); while (g_gate[i].ret) { if (g_gate[i].ret == ret_abs) die("gate registered twice %#llx", ret_abs); i = (i + 1) & ((1u << GATE_BITS) - 1); } diff --git a/dm/host/hle.h b/dm/host/hle.h index 48462fb..7bd373e 100644 --- a/dm/host/hle.h +++ b/dm/host/hle.h @@ -20,11 +20,10 @@ void *objmap_del(const void *guest); uint64_t now_ns(void); // This thread is doing the host's own heavy work (NVN submit / present: encoding Metal commands; on hardware that is the GPU's business and does not occupy a guest core). The same-core preemption approximation (hle_thread.c) does not count it as "running" extern __thread _Atomic int dm_host_work; -uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while DM_TICK_HOLD is frozen) +uint64_t guest_ns(void); // Guest clock (hle_os.c; does not advance while frozen, guest_clock_freeze) int dm_detclock_enabled(void); void dm_guest_clock_advanced(void); uint64_t dm_guest_frame(void); // Incremented each time the host presents a frame; unaffected by machine load -uint64_t dm_server_tick(void); // In-run server tick; returns UINT64_MAX when not in a run or while DM_TICK_HOLD is frozen void guest_clock_freeze(int on); void guest_clock_freeze_for(int reason, int on); // Freeze by reason (1 background, 2 system keyboard); see hle_os.c @@ -48,10 +47,6 @@ typedef struct { const char *name; void *fn; const char *why; } hle_force_ent; // Operation mode (0 handheld / 1 docked): owned by hle_oe.c, and the hid controller style follows it; the front end can switch it at runtime int dm_opmode(void); -void dm_3d_scale_hot(int hot); // When overheating, lower the 3D render scale to 0.70 (UI untouched); 0 = restore -ssize_t dm_data_pread(int fd, void *buf, size_t n, off_t off); // Resource archive reads: zero-stripped archives with a ".holes" extent table are translated through the table (hle_fs.c) -void dm_data_note_fd(int fd, const char *hostpath); -int dm_overlay_file(const char *rel, char *out, size_t n); // File in the overlay: DM_ROMFS_OVERLAY may have several layers (colon-separated, earlier ones take priority); look for <layer>/<rel> layer by layer, and on a hit write it to out and return 1 (hle_fs.c) void dm_set_opmode(int m); // On change, push notification 30 / 31 according to the switches the game has turned on // Focus (1 foreground / 2 focus lost / 3 background): called by the front end on lifecycle changes; a change pushes notification 15 void dm_focus_set(int st); @@ -60,23 +55,3 @@ extern uint8_t *dm_fps_state; // frame step state page (loader.c install_fps_s void dm_audio_suspend(int on); uint64_t dm_audio_render_calls(void); // Cumulative sound card callback count (the silent sink is not counted) void dm_audio_reset(void); // iOS media services reset: recreate the AudioUnits (hle_audio.c) - -// ---------- Host call number registry (a cut replaces a game-internal function entry with `svc #number; ret`, and the rewriter turns it into a host gate) ---------- -// Horizon's SVC numbers only go up to 0x7F; 0x80 onward is reserved for host calls. New ones must be registered here and numbers must not repeat; dispatch is in hle_svc.c → svc_host_call. -// No. Purpose File Cut (main offset) -// 0x80 TACT BLTE block zlib decode → libdeflate hle_tact.c 0x1b0ee60 (DecodeZlib entry) -// 0x81 Client command packet delivery (receive pump → server receive) diag.c 0x5d3a88 (bl 0x632510 call site; forwarded as is by default, dropped under DM_C2S_REPLAY) -// 0x82 System-class client message delivery (receive pump → 0x6310b0) diag.c 0x5d39c8 (bl 0x6310b0 call site; forwarded as is by default, dropped from the file's first tick on under DM_C2S_REPLAY) -// 0x83 Granny section per-block decode (section read 0x11f0110) hle_granny.c 0x11f02dc (bl 0x11f4af0 call site; by default replaced with the pre-decoded candidate, forwarded as is if it does not check out) -// 0x84 Granny fixup table per-block decode (section read 0x11f07d0) hle_granny.c 0x11f0914 (bl 0x11f4af0 call site; same as above) -// 0x85 Private skill HUD self-draw query (read-only, writes x8) hle_tact.c 0x3e443c (branch gate: x8≠0 jumps to 0x3e4460, otherwise executes the original instruction and returns to 0x3e4440; touch-only private gate) -// 0x86 Private skill ground direction (forwards to the original 0x2b4ef8, then redirects by the right stick) hle_private_aim.c 0x2faf04 (bl 0x2b4ef8 call site; touch-only private gate) -// 0x87 Ground item appearance path (forwards to the original 0xa14dc4, then repoints by rule to the lit variant) dm_dropfilter.c 0x8fa590 (bl 0xa14dc4 call site; only items in mode 3 / 5 are changed, output in the x8 string) -// 0x88 Item landing drop sound (forwards to the original 0x470c20, then swaps tier-one items to a notification sound) dm_dropfilter.c 0x468f2c (bl 0x470c20 call site; writes x0 = sound id) -// 0x89 Rift exit rerouting (in a rift run the destination is replaced with the next map, writes x2 / x3 / x8) dm_rift.c 0x7cc1e0 (branch gate: x8≠0 jumps to 0x634000, otherwise executes the original instruction and returns to 0x7cc1e4) -// 0x8a Spawn point for the player on run entry (forwards to the original 0x5596c0; replaced with a point next to the waypoint when entering in town) dm_townwp.c 0x5d8040 (bl 0x5596c0 call site; in the player join function 0x5d7f70) -// 0x8b Drop point for the corpse on run entry (same as above, the same point as the player gets) dm_townwp.c 0x5d8400 (bl 0x5596c0 call site; in the same function) -// 0x8c Current act saved while in a rift (when the character is in a rift map w9 is replaced with 4 = Act V, writes x9; x8 always 0) dm_rift.c 0x6fa9d0 (branch gate: x8≠0 jumps to 0x6fa9d4, otherwise executes the original instruction and returns to 0x6fa9d4) -// 0x8d Extra mercenary equipment slots (mercenary + rift level ≥10 + belt / boots / gloves / ring / amulet: w0 is replaced with 1, writes x0; x8 always 0) dm_rift.c 0x64a34c (branch gate: x8≠0 jumps to 0x64a350, otherwise executes the original instruction and returns to 0x64a350) -// 0x8e Client-side check for the extra mercenary equipment slots (rift level ≥10 + the item is a belt / boots / gloves / ring / amulet: w1 is replaced with the item's own type number, writes x1; x8 always 0) dm_rift.c 0x3beb58 (branch gate: x8≠0 jumps to 0x3beb5c, otherwise executes the original instruction and returns to 0x3beb5c) -enum { SVC_HOST_TACT_ZLIB = 0x80, SVC_HOST_C2S_DELIVER = 0x81, SVC_HOST_C2S_SYS = 0x82, SVC_HOST_GRANNY_A = 0x83, SVC_HOST_GRANNY_B = 0x84, SVC_HOST_PRIVATE_SKILL_RENDER = 0x85, SVC_HOST_PRIVATE_SKILL_AIM = 0x86, SVC_HOST_GROUND_ASSET = 0x87, SVC_HOST_DROP_SOUND = 0x88, SVC_HOST_RIFT_EXIT = 0x89, SVC_HOST_JOIN_SPAWN = 0x8a, SVC_HOST_JOIN_CORPSE = 0x8b, SVC_HOST_SAVE_ACT = 0x8c, SVC_HOST_MERC_SLOT = 0x8d, SVC_HOST_MERC_SLOT_CL = 0x8e }; // 0x87: fetch the model path for ground items (dm_dropfilter.c drop light) // 0x83/0x84: Granny per-block decode call sites (hle_granny.c) diff --git a/dm/host/hle_fdstat.c b/dm/host/hle_fdstat.c index 49fa6cf..24cabe1 100644 --- a/dm/host/hle_fdstat.c +++ b/dm/host/hle_fdstat.c @@ -21,8 +21,8 @@ static void *fdstat_main(void *arg) { if (S_ISSOCK(st.st_mode)) { sock++; continue; } if (S_ISFIFO(st.st_mode)) { pipe_++; continue; } if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) { dev++; continue; } char p[MAXPATHLEN] = ""; if (fcntl(fd, F_GETPATH, p) || !p[0]) { if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode)) other++; else kq++; continue; } if (S_ISDIR(st.st_mode)) dir++; - if (strstr(p, "romfs") || strstr(p, "RomFS") || strstr(p, "/data/hd/") || strstr(p, ".casc") || strstr(p, "/Data/data/")) romfs++; - else if (strstr(p, "/save") || strstr(p, ".d2s") || strstr(p, ".d2i") || strstr(p, "/user/")) save++; + if (strstr(p, "romfs") || strstr(p, "RomFS")) romfs++; + else if (strstr(p, "/save")) save++; else if (strstr(p, ".log") || strstr(p, "boot.txt") || strstr(p, ".jsonl") || strstr(p, ".tsv")) logf++; else if (strstr(p, "/Caches/") || strstr(p, "/cache/") || strstr(p, ".metallib") || strstr(p, "/tmp/") || strstr(p, "/var/folders/")) cache++; else { other++; if (sk < 300) { const char *b = strrchr(p, '/'); sk += snprintf(sample + sk, sizeof sample - sk, " %s", b ? b + 1 : p); } } } diff --git a/dm/host/hle_fs.c b/dm/host/hle_fs.c index dc06d4d..b90ec68 100644 --- a/dm/host/hle_fs.c +++ b/dm/host/hle_fs.c @@ -1,5 +1,5 @@ // nn::fs: on the real device it goes through the fsp-srv service; the host connects directly to the Mac file system. Mount name → host directory: -// rom → $DM_ROMFS (default $DM_ROOT/data/switch/3.3.94012/Update/RomFS, read-only; the game parses the storage blocks in it itself) +// rom → $DM_ROMFS (default $DM_ROOT/romfs, read-only; the game parses the storage blocks in it itself) // save → $DM_SAVE (must be set, pointing to this run's copy of the standard save save_ref; if unset, report a failure and abort) // temp → $DM_TEMP (default $DM_ROOT/run/platform/temp) // Paths look like "rom:/data/x". FileHandle / DirectoryHandle are 8 bytes passed by value, here they store the host fd / DIR*. @@ -46,12 +46,11 @@ static uint8_t g_fsave[65536]; // fd → whether it is on a transac static _Atomic int g_save_writes; // number of writes on the save mount (shared by the injection point and the commit log) static _Atomic uint64_t g_txn_first_ns; // time of this transaction's first save write (used by the commit log to report timing; cleared to 0 at commit) // Write failures are refused per transaction: on the real device save space is reserved when the save is created, overwrites cannot fail, and the game does not check the return values of WriteFile / SetFileSize either, it Commits as usual after a failure -// (after injecting a failure on the 1st write, the committed joe.d2s is 0 bytes = character lost). When a host write fails (disk full etc., really possible on iOS): +// (after injecting a failure on the 1st write, the committed save file was 0 bytes). When a host write fails (disk full etc., really possible on iOS): // if any save write / set-size failed between the last Commit and this Commit, this Commit is refused -- the committed version is unchanged, the working directory is discarded and re-cloned from the committed version, // and the front end is notified (front_save_failed); the next transaction (the game's next full save rewrite) is attempted as usual and committed if it succeeds. g_save_poison = errno of the first failure in this transaction (0 = none). static _Atomic int g_save_poison; static void save_poison(int e) { int z = 0; atomic_compare_exchange_strong(&g_save_poison, &z, e ? e : EIO); } -void dm_fs_save_poison(int e) { save_poison(e); } // for the pre-commit hook: a side file (Chronicle / pouch) that failed to write also counts as a write failure of this transaction -- if the commit went ahead as usual, the items would no longer be in the character save and would not be in the side file either // The default weak implementation only logs (Mac); the iOS front end provides a strong implementation of the same name that shows a native alert. Called on the game thread, the front end switches to the main thread itself __attribute__((weak)) void front_save_failed(int err) { LOG("front_save_failed(errno=%d): no front end attached (default weak implementation)", err); } // DM_FSLOG=<file>: each ReadFile logs one TSV line "seconds since boot\tguest path\toffset\tlength\tguest buffer address" (which bytes boot read, for offline reverse lookup against the manifest) @@ -64,7 +63,7 @@ static const char *envdir(const char *var, const char *rel, char *buf, size_t n) snprintf(buf, n, "%s%s/%s", getenv("DM_ROOT") ?: getenv("HOME"), getenv("DM_ROOT") ? "" : "/dm-work", rel); return buf; } -// Steps that end in a failed write (from d2r-mobile b94670c79): test injection DM_TEST_FS_FAILAT=<point>[:<count>][,...] -- in a save transaction, mountclone (clone the work directory at mount), +// Steps that end in a failed write: test injection DM_TEST_FS_FAILAT=<point>[:<count>][,...] -- in a save transaction, mountclone (clone the work directory at mount), // reclone (reclone after a refused commit), guestname (write the guest name after a rename), renameback (rename rollback) fail with ENOSPC for the first <count> times (default 1), // to check "no exit, committed version unchanged, next commit works" (tests/t_fs.c 6). Without the variable it costs one getenv static int dm_fs_test_fail(const char *pt) { @@ -116,7 +115,7 @@ static Result mount(const char *name, const char *dir, int ro) { return mount_ex // APFS is case-insensitive by default, Horizon paths are case-sensitive: every component is looked up exactly by guest name. // Ordinary host names are unchanged; only when a name collides on the host is a separate physical name chosen, with the guest name stored in an xattr. // clonefile preserves xattr, so save Commit/remount needs no separate mapping table; enumeration must not leak physical aliases. -#define FS_GUEST_NAME "org.d2rmobile.guest-name" +#define FS_GUEST_NAME "org.d2rmobile.guest-name" // the name this host has always used: existing saves carry it, so it is kept static int guest_name(const char *parent, const char *physical, char *out, size_t n) { // Only the physical names chosen on a case collision (.dm-case-N) carry the xattr (set_guest_name writes it only when the physical name differs from the guest name); any other physical name is the guest name, // so no per-entry getxattr is needed -- that was one system call per entry when listing, seconds for the 216,000 entries Engage walks in RomFS at boot @@ -214,20 +213,6 @@ static int root_path(const char *root, const char *rest, char *out, size_t n) { } return 1; } -// Files in the overlay that the host itself reads (marker files, sidecar tables, data archives) are all looked up here. DM_ROMFS_OVERLAY may have -// several layers, colon-separated, earlier ones taking priority; old devices have two layers, -// "in-bundle romfs_overlay_astc:Legacy Device Pack directory"; treating the whole string as one directory when building file names finds nothing at all -// on old devices. -int dm_overlay_file(const char *rel, char *out, size_t n) { - const char *ov = getenv("DM_ROMFS_OVERLAY"); - for (const char *p = ov ? ov : ""; *p;) { - size_t l = strcspn(p, ":"); - if (l && snprintf(out, n, "%.*s/%s", (int)l, p, rel) < (int)n && !access(out, R_OK)) return 1; - p += l; if (*p == ':') p++; - } - if (n) out[0] = 0; - return 0; -} static int rom_overlay_path(const mount_t *m, const char *rest, char *out, size_t n) { char *roots = strdup(m->overlay); if (!roots) die("rom overlay path allocation failed"); @@ -321,7 +306,7 @@ static Result err_res(const char *op, const char *path) { HLE_FORCE("_ZN2nn2fs22QueryMountRomCacheSizeEPm", "fsp-srv; rom reads the host directory directly, the cache gets only the minimum size", Result, h_fs_QueryRomCache, size_t *out) { *out = 0x1000; return RES_OK; } HLE_FORCE("_ZN2nn2fs8MountRomEPKcPvm", "fsp-srv; rom → host RomFS directory", Result, h_fs_MountRom, const char *name, void *cache, size_t sz) { (void)cache; (void)sz; char b[1024]; - Result r = mount(name, envdir("DM_ROMFS", "data/switch/3.3.94012/Update/RomFS", b, sizeof b), 1); + Result r = mount(name, envdir("DM_ROMFS", "romfs", b, sizeof b), 1); if (r == RES_OK && getenv("DM_ROMFS_OVERLAY")) { PATH_LOCK(); for (int i = 0; i < g_nmnt; i++) if (!strcmp(g_mnt[i].name, name)) { @@ -410,100 +395,6 @@ HLE_FORCE("_ZN2nn2fs14ExtendSaveDataERKNS_7account3UidEll", "fsp-srv; extend sav (void)uid; LOG("fs extend save data to %lld bytes (journal %lld): nothing to do", (long long)size, (long long)journal); return RES_OK; } HLE_FORCE("_ZN2nn2fs14EnsureSaveDataERKNS_7account3UidE", "fsp-srv; the save directory is created at mount time", Result, h_fs_EnsureSave, const void *uid) { (void)uid; return RES_OK; } -// Settings defaults for the touch product (dropped item names shown permanently by default, can be turned off in settings): Settings/Settings.json in the save only records keys the player has changed, unrecorded ones use the game default. -// Before mounting the save, fill in the touch product defaults for the two keys "the player has not touched yet"; existing keys are never changed (if the player changed it in the options page, the player wins): -// "Item Name Display": 1 -- options page "Show Items" = toggle style (game default 0 = hold; touch has no key that can be held). Companion: the front end taps the "Show Items" key once for the player the first time this process reaches the normal in-run state (front_ios.m), -// in toggle style one tap keeps it shown, and it persists into the next run after exit and re-entry; a player who wants it off changes this item back to "Hold". -// "Item Tooltip Hotkey Appender": 0 -- item tooltips carry no controller key hints (game default 1). -// Minimap (placed top right: mini, semi-transparent): -// "AutoMapMode": 2 = mini map on the right (0 full screen / 1 left / 2 right, the game default is also 2, stated here explicitly); opacity (AutoMapFade) is not filled in, the game default 0 = no fade is used -- with "All" (3) the minimap lines fade until almost only the text remains; the map is lines overlaid on the picture anyway, so it shows through without fading; -// "AutoMap Open On Load": 1 = open automatically on entering a run (game default 0). Tapping the minimap = the map toggle key, see front_ios.m. -// Done by default in the iOS app; not done by default on the Mac host (gate checks compare images against Switch ground truth), DM_TOUCH_DEFAULTS=1 on / =0 off. -// Shared stash seed: when the save directory has no ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i, write the seed file (stash_seed.h); an existing one is not changed by a single byte. -// Done before mounting the save and cloning the working directory, so it is what the game reads the first time. On by default on iOS, DM_STASH_SEED=0 turns it off; off by default on the Mac host, DM_STASH_SEED=1 turns it on. -// Only covers the DLC type (standard + hardcore): SharedStash{Soft,Hard}CoreV2.d2i used by the Lord of Destruction type is not seeded -- the touch character creation page cannot create that kind of character -// The seed file stash_seed.h holds the bytes of a save the game itself generated, and the public repository does not ship it: without the header nothing is seeded (the game then creates an empty shared stash of its own the first time one is needed, as the original does) -#if __has_include("stash_seed.h") -#include "stash_seed.h" -static void stash_seed_one(const char *save_dir, const char *name) { - char dir[1100], path[1200], tmp[1210]; snprintf(dir, sizeof dir, "%s/%s", save_dir, name); snprintf(path, sizeof path, "%s/%s.d2i", dir, name); - struct stat st; if (!stat(path, &st)) return; - if (mkdir(dir, 0755) && errno != EEXIST) { LOG("shared stash seed: creating directory %s failed (%s), not seeding", dir, strerror(errno)); return; } - snprintf(tmp, sizeof tmp, "%s.tmp", path); FILE *f = fopen(tmp, "wb"); if (!f) { LOG("shared stash seed: writing %s failed (%s), not seeding", tmp, strerror(errno)); return; } - int ok = fwrite(g_stash_seed, 1, sizeof g_stash_seed, f) == sizeof g_stash_seed; ok = fclose(f) == 0 && ok; - if (ok && !rename(tmp, path)) LOG("shared stash seed: %s did not exist, wrote the seed file (%zu bytes: 2.5 million gold + 2 Tokens of Absolution)", path, sizeof g_stash_seed); else { remove(tmp); LOG("shared stash seed: write failed, not seeding"); } -} -// One copy each for standard and hardcore, each judged on its own (the game picks the file name by character type, main+0x2ce1e8: DLC type standard = ModernSharedStashSoftCoreV2, hardcore = ModernSharedStashHardCoreV2; same file format) -static void stash_seed(const char *save_dir) { - const char *e = getenv("DM_STASH_SEED"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return; // on by default on iOS; off by default on the Mac host (gate checks such as lockstep comparison / save transaction compare the save directory byte by byte, one extra file turns them red), set DM_STASH_SEED=1 to verify - stash_seed_one(save_dir, "ModernSharedStashSoftCoreV2"); stash_seed_one(save_dir, "ModernSharedStashHardCoreV2"); -} -#else -static void stash_seed(const char *save_dir) { (void)save_dir; } -#endif -// Fallback for "Large Font" (settings key "Low Vision Mode"): if it is 1 in the settings file, change it back to 0. It switches the UI to the original game's low-vision layout profile, which our touch UI was never built for; it hangs after being turned on in the settings page. -// This item in the settings page is separately hidden by the UI overlay; this here guarantees that devices where it was already turned on recover on restart. Same switch as settings_seed (on by default on iOS, on the Mac host only with DM_TOUCH_DEFAULTS=1) -static void low_vision_off(const char *save_dir) { - const char *e = getenv("DM_TOUCH_DEFAULTS"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return; - char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", save_dir); static char buf[65536]; FILE *f = fopen(path, "rb"); if (!f) return; - size_t n = fread(buf, 1, sizeof buf - 1, f); fclose(f); if (n == sizeof buf - 1) return; buf[n] = 0; - char *k = strstr(buf, "\"Low Vision Mode\""); if (!k) return; char *v = strchr(k + 17, ':'); if (!v) return; v++; while (*v == ' ' || *v == '\t') v++; - if (*v != '1' || (v[1] >= '0' && v[1] <= '9')) return; *v = '0'; - char tmp[1210]; snprintf(tmp, sizeof tmp, "%s.tmp", path); f = fopen(tmp, "wb"); if (!f) return; int ok = fwrite(buf, 1, n, f) == n; ok = fclose(f) == 0 && ok; - if (ok && !rename(tmp, path)) LOG("settings fallback: 'Large Font' (Low Vision Mode) was on, turned it back off"); else remove(tmp); -} -static void settings_seed(const char *save_dir) { - const char *e = getenv("DM_TOUCH_DEFAULTS"); int on = e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE; if (!on) return; - static const char *kv[][2] = {{"\"Item Name Display\"", "1"}, {"\"Item Tooltip Hotkey Appender\"", "0"}, - {"\"AutoMapMode\"", "2"}, {"\"AutoMap Open On Load\"", "1"}}; - char dir[1100], path[1200]; snprintf(dir, sizeof dir, "%s/Settings", save_dir); snprintf(path, sizeof path, "%s/Settings.json", dir); - static char buf[65536], out[66000]; size_t n = 0; FILE *f = fopen(path, "rb"); // static: the game thread that mounts the save has a small stack (two 64 KB blocks on the stack overflow it outright, measured crash); mounting the save is done on one thread only - int first = f == NULL; // no settings file = first install - if (f) { n = fread(buf, 1, sizeof buf - 1, f); fclose(f); if (n == sizeof buf - 1) { LOG("settings defaults: %s is over 64 KB, left alone", path); return; } } - buf[n] = 0; - char *brace = strchr(buf, '{'); int had = brace != NULL; size_t o = 0; int added = 0; - o += (size_t)snprintf(out + o, sizeof out - o, "{"); - for (size_t i = 0; i < sizeof kv / sizeof kv[0]; i++) if (!strstr(buf, kv[i][0])) { o += (size_t)snprintf(out + o, sizeof out - o, "\n %s: %s,", kv[i][0], kv[i][1]); added++; } - // On first install the original game's text and voice languages are set from the system language (dm_sys_lang(), see dm_i18n.c; only when there is no settings file). An existing settings file does not get these two keys added (existing players stay as they are); afterwards they are changed by the language switch on the Custom page (settings_lang_apply) - if (first) { int dm_sys_lang(void); o += (size_t)snprintf(out + o, sizeof out - o, "\n \"Language\": %d,\n \"VoiceLanguage\": %d,", dm_sys_lang(), dm_sys_lang()); added += 2; } - if (!added) return; - if (had) { const char *rest = brace + 1; const char *q = rest; while (*q == ' ' || *q == '\n' || *q == '\r' || *q == '\t') q++; - if (*q == '}') { out[o - 1] = 0; o--; } // the original file is an empty object: remove the last comma that was added - o += (size_t)snprintf(out + o, sizeof out - o, "%s", rest); } - else { out[o - 1] = 0; o--; o += (size_t)snprintf(out + o, sizeof out - o, "\n}\n"); } - mkdir(dir, 0755); char tmp[1300]; snprintf(tmp, sizeof tmp, "%s.seed", path); - f = fopen(tmp, "wb"); if (!f || fwrite(out, 1, o, f) != o) { if (f) fclose(f); LOG("[RED] settings defaults: writing %s failed", tmp); return; } - fclose(f); if (rename(tmp, path)) { LOG("[RED] settings defaults: renaming to %s failed", path); return; } - LOG("settings defaults: %s got %d keys added (Show Items = toggle / tooltip hotkeys = off / minimap on the right, opens on entering a run%s; existing keys untouched)", path, added, first ? " / first install: Language written from the system language" : ""); -} -// Language switch (the cell in the title row of the Custom page, lang_en in dm_custom.c): once the player has tapped "Apply" the settings file holds an explicit value, and on every save mount (before the game reads its settings) the original game's settings file has its -// Language (text) and VoiceLanguage (voice; the voice switches along with it) changed to 0 English / 8 Simplified Chinese accordingly. It is changed only when it "does not match the switch": switch = English but text is not 0; switch = Chinese but text is 0. -// So with the switch off, if the player has chosen another language such as Traditional Chinese in the original game, it is not changed back to Simplified Chinese. Whichever of the two keys is missing is added. Apply never tapped (-1) = nothing is touched -static int json_int_set(char *buf, size_t cap, const char *key, int val) { // Replace the N of "key": N with val (if the key is absent, insert it after the first {); returns 1 = changed - char *k = strstr(buf, key); char num[16]; snprintf(num, sizeof num, "%d", val); - if (!k) { char *b = strchr(buf, '{'); char ins[96]; int m = snprintf(ins, sizeof ins, "\n %s: %s,", key, num); if (!b || strlen(buf) + (size_t)m + 1 >= cap) return 0; - memmove(b + 1 + m, b + 1, strlen(b + 1) + 1); memcpy(b + 1, ins, (size_t)m); return 1; } - char *v = strchr(k + strlen(key), ':'); if (!v) return 0; v++; while (*v == ' ' || *v == '\t') v++; char *e = v; if (*e == '-') e++; while (*e >= '0' && *e <= '9') e++; - if (e == v || atoi(v) == val) return 0; size_t nl = strlen(num); if (strlen(buf) + nl + 1 >= cap) return 0; - memmove(v + nl, e, strlen(e) + 1); memcpy(v, num, nl); return 1; } -static void settings_lang_apply(const char *save_dir) { - int dm_custom_lang_explicit(void); int en = dm_custom_lang_explicit(); if (en < 0) return; - char path[1200]; snprintf(path, sizeof path, "%s/Settings/Settings.json", save_dir); static char buf[66000]; FILE *f = fopen(path, "rb"); if (!f) return; - size_t n = fread(buf, 1, 65535, f); fclose(f); if (n == 65535) return; buf[n] = 0; - const char *k = strstr(buf, "\"Language\""), *c = k ? strchr(k + 10, ':') : NULL; int cur = c ? atoi(c + 1) : 0; // Not written = game default (the host reports en-US) - if ((en && cur == 0) || (!en && cur != 0)) { const char *vk = strstr(buf, "\"VoiceLanguage\""), *vc = vk ? strchr(vk + 15, ':') : NULL; int vcur = vc ? atoi(vc + 1) : cur; - if ((en && vcur == 0) || (!en && vcur != 0)) return; } // Text and voice already match the switch - int want = en ? 0 : 8, ch = json_int_set(buf, sizeof buf, "\"Language\"", want) | json_int_set(buf, sizeof buf, "\"VoiceLanguage\"", want); if (!ch) return; - char tmp[1300]; snprintf(tmp, sizeof tmp, "%s.lang", path); f = fopen(tmp, "wb"); size_t m = strlen(buf); - if (!f || fwrite(buf, 1, m, f) != m) { if (f) fclose(f); LOG("[RED] language switch: writing %s failed", tmp); return; } - fclose(f); if (rename(tmp, path)) { LOG("[RED] language switch: renaming to %s failed", path); return; } - LOG("language switch: set the settings file's Language / VoiceLanguage to %d (%s; previous text language %d)", want, en ? "English" : "Simplified Chinese", cur); -} -// The current working directory of the transactional save (the copy the game reads and writes; the host's own side files must be written here to enter the committed version with Commit). Returns NULL when no save is mounted -const char *dm_save_workdir(void) { for (int i = 0; i < g_nmnt; i++) if (g_mnt[i].commit[0]) return g_mnt[i].dir; return NULL; } -// The committed directory of the transactional save (the copy on disk, the version from the last Commit). Returns NULL when no save is mounted -const char *dm_save_commitdir(void) { for (int i = 0; i < g_nmnt; i++) if (g_mnt[i].commit[0]) return g_mnt[i].commit; return NULL; } -void (*g_dm_mount_seed_hook)(const char *save_dir); // called before mounting the save (save dir): adds the files the host needs to the save // Another application's save data (Tears of the Kingdom asks by application ID and would mount it read-only): the host keeps only this game's own save, which exists once mounted HLE_FORCE("_ZN2nn2fs18IsSaveDataExistingENS_13ApplicationIdERKNS_7account3UidE", "fsp-srv; only this game's own save data exists", int, h_fs_SaveExisting, uint64_t app, const void *uid) { (void)uid; LOG("fs asks whether save data exists: program %016llx → %s", (unsigned long long)app, app == DM_GAME_PROGRAM_ID ? "this game, yes" : "another program, no"); return app == DM_GAME_PROGRAM_ID; @@ -517,25 +408,15 @@ HLE_FORCE("_ZN2nn2fs13MountSaveDataEPKcRKNS_7account3UidE", "fsp-srv; save data const char *d = getenv("DM_SAVE"); if (!d) die("DM_SAVE is not set: point it at a writable copy of a save directory"); char dd[1024]; strlcpy(dd, d, sizeof dd); size_t L = strlen(dd); while (L > 1 && dd[L - 1] == '/') dd[--L] = 0; // strip trailing slashes (.work / .new are appended at the same level) - // The items below all edit D2R's save (Settings/Settings.json, shared stash): only on the D2R build, other games' files of the same name are left alone - int d2r = !strcmp(dm_main_build(), DM_D2R_BUILD); - if (d2r) { stash_seed(dd); low_vision_off(dd); settings_seed(dd); settings_lang_apply(dd); } - // Rule files for the loot filter (the game loads them at startup; dm_lootfilter.c registers this hook from a constructor; it is null in unit tests that link only hle_fs.c). Same switch as settings_seed: - // written by default on iOS; not written under the Mac host's parity gate (saves must match the device ground truth file by file) - { const char *e = getenv("DM_TOUCH_DEFAULTS"); if (d2r && g_dm_mount_seed_hook && (e ? strcmp(e, "0") != 0 : TARGET_OS_IPHONE)) g_dm_mount_seed_hook(dd); } return mount_ex(name, dd, 0, 1); } -void (*g_dm_pre_commit_hook[4])(const char *workdir); // called before commit (working directory): host side files are checked against the save contents in the working directory before being written ([0] personal stash pages dm_pages.c, [2] pouch dm_pouch.c, [1] chronicle vault dm_vault.c, [3] rift progress dm_rift.c) -void (*g_dm_save_commit_hook)(const char *commit_dir); // called after a save commit succeeds (committed directory); the iOS iCloud sync sets it, NULL on the host static _Atomic uint64_t g_save_commits; // number of successful commits (used by the front end when going to background to wait for "the game's current save has been committed") uint64_t dm_fs_commit_count(void) { return atomic_load(&g_save_commits); } HLE_FORCE("_ZN2nn2fs6CommitEPKc", "fsp-srv; transactional save: the whole work directory tree is cloned, then swapped atomically with the committed directory (renamex_np RENAME_SWAP)", Result, h_fs_Commit, const char *name) { PATH_LOCK(); pthread_mutex_lock(&g_lock); int i = 0; while (i < g_nmnt && strcmp(g_mnt[i].name, name)) i++; if (i == g_nmnt || !g_mnt[i].commit[0]) { pthread_mutex_unlock(&g_lock); return RES_OK; } // non-transactional mount: nothing to commit - uint64_t tc0 = clock_gettime_nsec_np(CLOCK_MONOTONIC), tw0 = atomic_exchange(&g_txn_first_ns, 0); - for (int k = 0; k < 4; k++) if (g_dm_pre_commit_hook[k]) g_dm_pre_commit_hook[k](g_mnt[i].dir); - uint64_t tc1 = clock_gettime_nsec_np(CLOCK_MONOTONIC); // side files written into the working directory are swapped in atomically together with this commit + uint64_t tc1 = clock_gettime_nsec_np(CLOCK_MONOTONIC), tw0 = atomic_exchange(&g_txn_first_ns, 0); int perr = atomic_exchange(&g_save_poison, 0); if (perr) { // a write failed in this transaction: refuse the commit, discard the working directory char torn[512] = ""; size_t tl = 0; DIR *d0 = opendir(g_mnt[i].dir); struct dirent *e0; // record files truncated to 0 bytes (the damage that would enter the save if committed as usual) @@ -568,11 +449,11 @@ HLE_FORCE("_ZN2nn2fs6CommitEPKc", "fsp-srv; transactional save: the whole work d if (clonefile(g_mnt[i].dir, neu, 0)) { LOG("[RED] save commit: clonefile %s failed errno=%d (committed version unchanged)", neu, errno); r = RES_FAIL; } else if (renamex_np(neu, g_mnt[i].commit, RENAME_SWAP)) { LOG("[RED] save commit: atomic swap failed errno=%d (committed version unchanged)", errno); rm_tree(neu); r = RES_FAIL; } else rm_tree(neu); // the old version that was swapped out - // Timing readouts (integer microseconds, no floating point): how long the pre-commit hook and the clone + atomic swap each took; how long in total from this transaction's first save write to the end of the commit (the game writing files + the commit; 0 = no save write in this transaction) + // Timing readouts (integer microseconds, no floating point): how long the clone + atomic swap took; how long in total from this transaction's first save write to the end of the commit (the game writing files + the commit; 0 = no save write in this transaction) static int n; if (r == RES_OK) { uint64_t tc2 = clock_gettime_nsec_np(CLOCK_MONOTONIC); - LOG("save commit #%d: %s swapped atomically into %s (%d save writes so far) (hook %llu us, clone + swap %llu us, since this transaction's first write %llu us)", ++n, g_mnt[i].dir, g_mnt[i].commit, atomic_load(&g_save_writes), - (unsigned long long)((tc1 - tc0) / 1000), (unsigned long long)((tc2 - tc1) / 1000), (unsigned long long)(tw0 ? (tc2 - tw0) / 1000 : 0)); atomic_fetch_add(&g_save_commits, 1); - if (g_dm_save_commit_hook) g_dm_save_commit_hook(g_mnt[i].commit); } // iCloud sync (cloud_ios.m): called with the lock held, that side only takes a clonefile snapshot + LOG("save commit #%d: %s swapped atomically into %s (%d save writes so far) (clone + swap %llu us, since this transaction's first write %llu us)", ++n, g_mnt[i].dir, g_mnt[i].commit, atomic_load(&g_save_writes), + (unsigned long long)((tc2 - tc1) / 1000), (unsigned long long)(tw0 ? (tc2 - tw0) / 1000 : 0)); atomic_fetch_add(&g_save_commits, 1); + } pthread_mutex_unlock(&g_lock); return r; } @@ -581,75 +462,6 @@ HLE_FORCE("_ZN2nn2fs14CommitSaveDataEPKc", "fsp-srv; same as Commit (SDK 11's ol HLE_FORCE("_ZN2nn2fs7UnmountEPKc", "fsp-srv; the mount table is permanent, unmounting is a no-op", void, h_fs_Unmount, const char *name) { (void)name; } // ---------- Files ---------- -// Zero-stripped archives: large zero-filled ranges in the resource pack (cutscene videos, old-version music) are not distributed with the package -- these zero-filled ranges are stripped from the archive file, and an extent table is placed beside it, -// "<archive name>.holes" (text: first line `size <original byte count>`, then one line `<zero-filled range start> <zero-filled range length>` each, ascending by start, non-overlapping). -// When reading such archives the host translates offsets through the table: bytes falling in a zero-filled range return 0 and the file size is reported as the original value; the game and the startup prefetch still see the original layout, and not a single word of the index needs changing. -// There are only two read entry points: ReadFile in this file and the startup prefetch in hle_tact.c, both going through dm_data_pread. Files without .holes take the original path with zero extra overhead. -typedef struct { int64_t size; int n; int64_t *log, *len, *phys; char name[40]; } holemap; // Kept segment: original start / length / start in the zero-stripped file -static _Atomic uint64_t g_hole_reads; // Number of reads that hit a zero-filled range: should normally be 0 (zero-filled ranges are stripped cutscene videos / old-version music the game should not come to read); non-zero = stripped wrongly -static holemap *g_hm[4096]; -static holemap *hm_load(const char *hostpath) { - char hp2[1100]; if (snprintf(hp2, sizeof hp2, "%s.holes", hostpath) >= (int)sizeof hp2) return NULL; - FILE *f = fopen(hp2, "r"); if (!f) return NULL; - long long size = -1, o, l; if (fscanf(f, "size %lld", &size) != 1 || size < 0) die("bad hole table format: %s", hp2); - holemap *m = calloc(1, sizeof *m); int cap = 64; m->size = size; m->log = malloc(cap * sizeof *m->log); m->len = malloc(cap * sizeof *m->len); m->phys = malloc(cap * sizeof *m->phys); - int64_t pos = 0, phys = 0; - while (fscanf(f, "%lld %lld", &o, &l) == 2) { - if (o < pos || l <= 0 || o + l > size) die("hole table zero range out of bounds or out of order: %s (%lld %lld)", hp2, o, l); - if (o > pos) { if (m->n == cap) { cap *= 2; m->log = realloc(m->log, cap * sizeof *m->log); m->len = realloc(m->len, cap * sizeof *m->len); m->phys = realloc(m->phys, cap * sizeof *m->phys); } - m->log[m->n] = pos; m->len[m->n] = o - pos; m->phys[m->n++] = phys; phys += o - pos; } - pos = o + l; - } - fclose(f); - if (pos < size) { if (m->n == cap) { cap++; m->log = realloc(m->log, cap * sizeof *m->log); m->len = realloc(m->len, cap * sizeof *m->len); m->phys = realloc(m->phys, cap * sizeof *m->phys); } - m->log[m->n] = pos; m->len[m->n] = size - pos; m->phys[m->n++] = phys; phys += size - pos; } - struct stat st; if (stat(hostpath, &st) || st.st_size != phys) die("zero-stripped archive length does not match its hole table: %s is %lld bytes, the table says %lld", hostpath, (long long)st.st_size, (long long)phys); - const char *bn = strrchr(hostpath, '/'); strlcpy(m->name, bn ? bn + 1 : hostpath, sizeof m->name); - { static _Atomic int said[64]; unsigned h = 0; for (const char *c = m->name; *c; c++) h = h * 31 + (unsigned char)*c; // Print a line for each archive only on first registration - if (!atomic_exchange(&said[h % 64], 1)) LOG("zero-stripped archive %s: original %lld bytes, stored %lld bytes, %d kept segments", m->name, (long long)size, (long long)phys, m->n); } - return m; -} -void dm_data_note_fd(int fd, const char *hostpath) { // Called after opening a resource archive: register it if there is an extent table - if (fd < 0 || fd >= 4096) return; - g_hm[fd] = hm_load(hostpath); -} -// Same as dm_data_logical_size, but opened relative to the directory fd: listing does not resolve the long path from the root again for every entry -static int64_t data_logical_size_at(int dfd, const char *name, int64_t phys_size) { - char hn[0x320]; if (snprintf(hn, sizeof hn, "%s.holes", name) >= (int)sizeof hn) return phys_size; - int fd = openat(dfd, hn, O_RDONLY); if (fd < 0) return phys_size; - FILE *f = fdopen(fd, "r"); if (!f) { close(fd); return phys_size; } - long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size; -} -int64_t dm_data_logical_size(const char *hostpath, int64_t phys_size) { // For directory listing / size queries: report the original size when there is an extent table - char hp2[1100]; if (snprintf(hp2, sizeof hp2, "%s.holes", hostpath) >= (int)sizeof hp2) return phys_size; - FILE *f = fopen(hp2, "r"); if (!f) return phys_size; long long size = -1; int ok = fscanf(f, "size %lld", &size) == 1; fclose(f); return ok && size >= 0 ? size : phys_size; -} -ssize_t dm_data_pread(int fd, void *buf, size_t n, off_t off) { // Same semantics as pread (may short-read); zero-stripped archives are translated through the extent table - holemap *m = fd >= 0 && fd < 4096 ? g_hm[fd] : NULL; - if (!m) return pread(fd, buf, n, off); - if (off >= m->size) return 0; - if ((int64_t)n > m->size - off) n = (size_t)(m->size - off); - int lo = 0, hi = m->n; // The first kept segment whose end point > off - while (lo < hi) { int mid = (lo + hi) / 2; if (m->log[mid] + m->len[mid] <= off) lo = mid + 1; else hi = mid; } - size_t done = 0; - while (done < n) { - int64_t cur = off + (int64_t)done; - if (lo < m->n && cur >= m->log[lo]) { // In a kept segment: read directly into the destination buffer - int64_t in = cur - m->log[lo]; size_t want = n - done; if ((int64_t)want > m->len[lo] - in) want = (size_t)(m->len[lo] - in); - size_t got = 0; while (got < want) { ssize_t r = pread(fd, (uint8_t *)buf + done + got, want - got, m->phys[lo] + in + (int64_t)got); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return (ssize_t)(done + got); got += (size_t)r; } - done += want; if (in + (int64_t)want == m->len[lo]) lo++; - } else { // In a zero-filled range: fill 0 up to the start of the next kept segment - int64_t end = lo < m->n ? m->log[lo] : m->size; size_t z = n - done; if ((int64_t)z > end - cur) z = (size_t)(end - cur); - memset((uint8_t *)buf + done, 0, z); done += z; - // The game reads whole blocks aligned to 256 KB: when what it wants is a kept file next to a zero-filled range, the whole block incidentally overlaps the edge of the zero-filled range (harmless, the kept file's bytes are returned as usual). - // Only a read that lies entirely inside a zero-filled range is really reading stripped content → report a failure (the first 20 one by one, then one line per 1000); boundary-crossing reads are only noted the first 3 times - if (z == n) { uint64_t hr = atomic_fetch_add(&g_hole_reads, 1); - if (hr < 20 || hr % 1000 == 0) LOG("[RED] read inside a stripped zero range %s offset %lld length %zu (#%llu; the whole block lies in the stripped cutscene videos / old-version music: the stripping was wrong or there is a new reader)", m->name, (long long)off, n, (unsigned long long)hr + 1); - } else { static _Atomic uint64_t edge; if (atomic_fetch_add(&edge, 1) < 3) LOG("block read overlaps the edge of a stripped zero range (harmless): %s read %lld length %zu, %zu bytes of it in the zero range", m->name, (long long)off, n, z); } - } - } - return (ssize_t)done; -} HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Result, h_fs_OpenFile, uint64_t *out, const char *path, int mode) { HP(path); if (mode & (MODE_WRITE | MODE_APPEND)) RW(); @@ -659,7 +471,6 @@ HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Re if (S_ISDIR(st.st_mode)) { close(fd); return RES_PATH_NOT_FOUND; } if (fd >= (int)sizeof g_fmode) die("fd beyond the table"); g_fmode[fd] = (uint8_t)mode; *out = (uint64_t)fd; - g_hm[fd] = NULL; if (!(mode & (MODE_WRITE | MODE_APPEND))) dm_data_note_fd(fd, hp); { const char *c = strchr(path, ':'); size_t ln = c ? (size_t)(c - path) : 0; g_fsave[fd] = 0; for (int i = 0; c && i < g_nmnt; i++) if (strlen(g_mnt[i].name) == ln && !strncmp(g_mnt[i].name, path, ln)) g_fsave[fd] = g_mnt[i].commit[0] != 0; } fslog_init(); @@ -669,16 +480,15 @@ HLE_FORCE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", "fsp-srv → open(2)", Re } HLE_FORCE("_ZN2nn2fs8ReadFileEPmNS0_10FileHandleElPvm", "fsp-srv → pread(2)", Result, h_fs_ReadFile, size_t *out, uint64_t h, int64_t off, void *buf, size_t sz) { if (!h) { *out = 0; return RES_FAIL; } // a file handle is an fd; 0 is stdin and never a file the game opened: the null handle the game keeps using after a failed open - size_t got = 0; uint64_t t0 = g_flag.bootprof ? now_ns() : 0; + size_t got = 0; dm_guest_write_prepare(buf, sz); // The destination may be a texture pool page write-protected by the graphics layer: a kernel write to a read-only page returns EFAULT instead of page-faulting, so unprotect and mark dirty first while (got < sz) { - ssize_t r = dm_data_pread((int)h, (uint8_t *)buf + got, sz - got, off + got); + ssize_t r = pread((int)h, (uint8_t *)buf + got, sz - got, off + got); if (r < 0) { if (errno == EINTR) continue; return err_res("pread", "fd"); } if (r == 0) break; got += r; } *out = got; - if (g_flag.bootprof) dm_prof_add(PROF_FS_READ, now_ns() - t0, got); if (g_fslog) { pthread_mutex_lock(&g_fslog_lock); static int hash_on = -1; if (hash_on < 0) hash_on = getenv("DM_FSLOG_HASH") != NULL; // DM_FSLOG_HASH=1: append a column with the FNV-1a 64 of the content read (to compare whether the bytes read from two resource packs match) uint64_t fh = 0xcbf29ce484222325ull; if (hash_on) for (size_t i = 0; i < got; i++) fh = (fh ^ ((uint8_t *)buf)[i]) * 0x100000001b3ull; @@ -718,7 +528,7 @@ HLE_FORCE("_ZN2nn2fs9WriteFileENS0_10FileHandleElPKvmRKNS0_11WriteOptionE", "fsp HLE_FORCE("_ZN2nn2fs11GetFileSizeEPlNS0_10FileHandleE", "fsp-srv → fstat(2)", Result, h_fs_GetFileSize, int64_t *out, uint64_t h) { if (!h) return RES_FAIL; struct stat st; if (fstat((int)h, &st)) return err_res("fstat", "fd"); - *out = (int)h < 4096 && g_hm[(int)h] ? g_hm[(int)h]->size : st.st_size; return RES_OK; + *out = st.st_size; return RES_OK; } HLE_FORCE("_ZN2nn2fs11SetFileSizeENS0_10FileHandleEl", "fsp-srv → ftruncate(2)", Result, h_fs_SetFileSize, uint64_t h, int64_t sz) { if (!h) return RES_FAIL; @@ -730,7 +540,6 @@ HLE_FORCE("_ZN2nn2fs9FlushFileENS0_10FileHandleE", "fsp-srv; host writes are alr HLE_FORCE("_ZN2nn2fs9CloseFileENS0_10FileHandleE", "fsp-srv → close(2)", void, h_fs_CloseFile, uint64_t h) { if (!h) return; // the null handle after a failed open (see ReadFile): do not touch fd 0 g_fmode[(int)h & 0xffff] = 0; g_fsave[(int)h & 0xffff] = 0; - if ((int)h >= 0 && (int)h < 4096 && g_hm[(int)h]) { holemap *m = g_hm[(int)h]; g_hm[(int)h] = NULL; free(m->log); free(m->len); free(m->phys); free(m); } close((int)h); } HLE_FORCE("_ZN2nn2fs15GetFileOpenModeENS0_10FileHandleE", "fsp-srv; the open mode is recorded by the host", int, h_fs_GetFileOpenMode, uint64_t h) { return g_fmode[(int)h & 0xffff]; } @@ -817,7 +626,6 @@ static void dir_snapshot(dirh_t *h, const mount_t *m, const char *rest) { struct dirent *e; while ((e = readdir(x))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; - { size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } // The extent table is not shown to the game if (!guest_name(parent, e->d_name, guest, 0x301)) continue; int dup = 0; for (int j = 0; j < nn; j++) if (!strcmp(names[j], guest)) { dup = 1; break; } if (!dup && nn < 2048) strlcpy(names[nn++], guest, sizeof names[0]); @@ -831,7 +639,7 @@ static void dir_snapshot(dirh_t *h, const mount_t *m, const char *rest) { snprintf(rel, 2048, "%s/%s", rest, names[j]); if (!rom_overlay_path(m, rel, resolved, 1024)) continue; struct stat st; - if (!stat(resolved, &st)) { h->snap[j].type = S_ISDIR(st.st_mode) ? ENT_DIR : ENT_FILE; h->snap[j].size = S_ISDIR(st.st_mode) ? 0 : dm_data_logical_size(resolved, st.st_size); } + if (!stat(resolved, &st)) { h->snap[j].type = S_ISDIR(st.st_mode) ? ENT_DIR : ENT_FILE; h->snap[j].size = S_ISDIR(st.st_mode) ? 0 : st.st_size; } } free(resolved); free(rel); free(guest); free(parent); free(all); free(names); free(roots); } @@ -852,7 +660,6 @@ HLE_FORCE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandl int dfd = dirfd(h->d); while (n < max && (e = readdir(h->d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; - { size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } // The extent table is not shown to the game // a directory's type comes from d_type without stat; files need a size, taken with fstatat relative to the directory fd (following links, like the old stat) struct stat st; int isdir; if (e->d_type == DT_DIR) isdir = 1; @@ -860,7 +667,7 @@ HLE_FORCE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandl if (!(h->mode & (isdir ? DIRMODE_DIR : DIRMODE_FILE))) continue; DirectoryEntry *o = &ents[n++]; memset(o, 0, sizeof *o); if (!guest_name(h->path, e->d_name, o->name, sizeof o->name)) { char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name); *count = n - 1; return err_res("directory name", c); } - o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : data_logical_size_at(dfd, e->d_name, st.st_size); + o->type = isdir ? ENT_DIR : ENT_FILE; o->size = isdir ? 0 : st.st_size; } *count = n; return RES_OK; } @@ -887,7 +694,6 @@ HLE_FORCE("_ZN2nn2fs22GetDirectoryEntryCountEPlNS0_15DirectoryHandleE", "fsp-srv struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; - { size_t dl = strlen(e->d_name); if (dl > 6 && !strcmp(e->d_name + dl - 6, ".holes")) continue; } char c[1280]; snprintf(c, sizeof c, "%s/%s", h->path, e->d_name); struct stat st; if (stat(c, &st)) continue; if (h->mode & (S_ISDIR(st.st_mode) ? DIRMODE_DIR : DIRMODE_FILE)) n++; diff --git a/dm/host/hle_guestthread.c b/dm/host/hle_guestthread.c index 9f7c34c..601c241 100644 --- a/dm/host/hle_guestthread.c +++ b/dm/host/hle_guestthread.c @@ -1,4 +1,4 @@ -// Host-initiated guest threads (host background work such as Granny pre-decoding needs to call the game's own allocator / locks and needs a complete SDK thread context). +// Host-initiated guest threads (host background work that calls the game's own allocator / locks, such as the movie player's, needs a complete SDK thread context). // Approach: on a guest thread (e.g. before guest_main enters nnMain) call the SDK's nn::os::CreateThread / StartThread directly, with a host function as the entry -- // the thread is created by the SDK, so ThreadType, thread handle and TLS slots are all in place, and the game's malloc / nn::os locks work as usual (the lock tag word holds this thread's guest handle). // Constraints: must be called on a guest thread (SDK thread creation goes through the SVC gate); not exposed to the game. diff --git a/dm/host/hle_lifecycle.c b/dm/host/hle_lifecycle.c index 5609aa9..e70fd9b 100644 --- a/dm/host/hle_lifecycle.c +++ b/dm/host/hle_lifecycle.c @@ -1,21 +1,17 @@ // Host lifecycle: graceful exit (SIGTERM; on iOS the App termination callback calls dm_host_shutdown). -// Stop background threads (boot block prefetch), pause audio, suspend graphics (wait for in-flight NVN calls to end and drain GPU work), then _exit(0); a 1.5 s fallback goes straight to _exit(0). +// Pause audio, suspend graphics (wait for in-flight NVN calls to end and drain GPU work), then _exit(0); a 1.5 s fallback goes straight to _exit(0). // The default action also exits in about 0.08 s (exit code 143); the value of this function is stopping background threads in order, exit code 0, and giving the iOS lifecycle callback an entry. #include "hle.h" #include <dispatch/dispatch.h> #include <stdatomic.h> #include <signal.h> #include <unistd.h> -void tact_prefetch_stop(void); -void granny_prof_dump(void); void hlecount_flush(void); void dm_host_shutdown(const char *why) { static _Atomic int once; if (atomic_exchange(&once, 1)) return; uint64_t t0 = now_ns(); LOG("received %s: graceful exit", why); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1500 * NSEC_PER_MSEC), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ LOG("[RED] graceful exit not done in 1.5 seconds, exiting at once"); _exit(0); }); - tact_prefetch_stop(); - granny_prof_dump(); hlecount_flush(); // DM_HLECOUNT: _exit does not run atexit, write a copy here dm_audio_suspend(1); dm_gfx_suspend(1); diff --git a/dm/host/hle_misc.c b/dm/host/hle_misc.c index 1fe91e0..d3771ce 100644 --- a/dm/host/hle_misc.c +++ b/dm/host/hle_misc.c @@ -3,23 +3,18 @@ #include "hle.h" #include <dirent.h> -// ---------- aoc add-on content (D2R's Reign of the Warlock; other games by the DLC NSPs in the player's folder) / ec shop (excluded) ---------- -// Reign of the Warlock is the only add-on content in the Switch package: AOC 0100726014353001 = add-on content index 1 of the base 0100726014352000. -// This DLC is only an unlock ticket of about 100 KB, the content itself is already in the main RomFS; main does not import MountAddOnContent and only judges by "is index 1 present". -// The reimplementation scope requires keeping Reign of the Warlock, so index 1 is reported as owned; what is excluded is the shop (ec), not the DLC itself. -enum { AOC_WARLOCK = 1 }; +// ---------- aoc add-on content (the DLC NSPs in the player's folder) / ec shop (excluded) ---------- // Owned add-on content indices (ascending), fixed at the first query: // numeric subdirectories of DM_AOC_DIR: each is one DLC's RomFS, which the packing script extracts only from DLC NSPs really in the player's folder (title ID = base + 0x1000 + index). // Engage: F001 → 1 (E0), F002 → 2 (H0); the content itself is in the 2.0.0 update's RomFS and each DLC package holds one empty marker file; the game calls MountAddOnContent for each index ListAddOnContent returns (hle_fs.c). -// No DM_AOC_DIR: D2R (main build db0507f3…) reports {1} (the Warlock expansion, content in the main RomFS, main does not import MountAddOnContent); other builds none -- -// on a Switch 2 Mario 1.2.1 uses it to tell whether the upgrade pack is owned, and the user has no Mario add-on content files. -// DM_AOC_OWNED=0: always none (test switch, for lockstep comparison against ground truth recorded without the DLC installed); DM_AOC_OWNED=1 without DM_AOC_DIR: reports {1}. +// No DM_AOC_DIR: none (on a Switch 2 Mario 1.2.1 uses it to tell whether the upgrade pack is owned, and the user has no Mario add-on content files). +// DM_AOC_OWNED=0: always none (test switch, for runs without the DLC). static int g_aoc[64], g_naoc = -1; static int aoc_int_cmp(const void *a, const void *b) { return *(const int *)a - *(const int *)b; } static void aoc_load(void) { if (g_naoc >= 0) return; g_naoc = 0; - const char *e = getenv("DM_AOC_OWNED"), *dir = getenv("DM_AOC_DIR"); const char *why = "by main build ID"; + const char *e = getenv("DM_AOC_OWNED"), *dir = getenv("DM_AOC_DIR"); const char *why = "no DM_AOC_DIR"; if (e && !strcmp(e, "0")) why = "DM_AOC_OWNED=0"; else if (dir && *dir) { why = dir; DIR *d = opendir(dir); struct dirent *ent; @@ -29,16 +24,11 @@ static void aoc_load(void) { } if (d) closedir(d); qsort(g_aoc, g_naoc, sizeof *g_aoc, aoc_int_cmp); - } else { - int d2r = 0; - for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) d2r = !strcmp(g_mod[i].build_id, "db0507f36b84200ac05f75d76ed5026f"); - if (e) why = "DM_AOC_OWNED"; - if (d2r || e) g_aoc[g_naoc++] = AOC_WARLOCK; } char list[256] = ""; for (int i = 0; i < g_naoc; i++) snprintf(list + strlen(list), sizeof list - strlen(list), "%s%d", i ? "," : "", g_aoc[i]); LOG("aoc: reporting %d add-on contents {%s} (%s)", g_naoc, list, why); } -HLE_FORCE("_ZN2nn3aoc17CountAddOnContentEv", "add-on content count: length of the owned index list (D2R Reign of the Warlock / DM_AOC_DIR)", int, h_aoc_Count, void) { aoc_load(); return g_naoc; } +HLE_FORCE("_ZN2nn3aoc17CountAddOnContentEv", "add-on content count: length of the owned index list (DM_AOC_DIR)", int, h_aoc_Count, void) { aoc_load(); return g_naoc; } // ListAddOnContent(out, start index, max count): writes out from the owned index list starting at the start index, returns the number written HLE_FORCE("_ZN2nn3aoc16ListAddOnContentEPiii", "add-on content list: the owned index list", int, h_aoc_List, int *out, int off, int n) { int w = 0; aoc_load(); diff --git a/dm/host/hle_oe.c b/dm/host/hle_oe.c index 799da1f..c035a7f 100644 --- a/dm/host/hle_oe.c +++ b/dm/host/hle_oe.c @@ -2,7 +2,6 @@ // oe::Initialize is in hle_sdkinit.c, not here. // Environment switches: DM_OPMODE=0 handheld / 1 console (default: the profile's op_mode, console unless set; console corresponds to the console-mode render profile); DM_LANG=language code (default en-US). #include "hle.h" -#include "dm_uiscreen.h" // The operation mode is runtime state (the front end can change it: iOS overheat downgrade switches to handheld): the single source is here, taken once from DM_OPMODE at startup, the environment variable is not read again afterwards #include <stdatomic.h> @@ -13,7 +12,7 @@ int dm_opmode(void) { return m; } // ---------- System notification queue (AppletMessage): front end state changes → fetched by the game every frame with TryPopNotificationMessage ---------- -// Message numbers (those actually handled by the game at main 0xe10d30): 15 focus change, 30 operation mode change, 31 performance mode change +// Message numbers the host sends: 15 focus change, 30 operation mode change, 31 performance mode change enum { MSG_FOCUS = 15, MSG_OPMODE = 30, MSG_PERFMODE = 31 }; #define MQ 32 static pthread_mutex_t g_mq_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_mq_cond = PTHREAD_COND_INITIALIZER; // cond: wakes the blocking PopNotificationMessage @@ -38,7 +37,7 @@ static void mq_push(uint32_t m) { mq_push_(m); mq_ev_sync(); } static void mq_push_(uint32_t m) { pthread_mutex_lock(&g_mq_lock); // 15/30/31 are invalidation notifications meaning "query the current state", not events carrying historical values. - // While one of the same type has not been taken yet, the first queue position is kept: the D2R consumer loop only reads the latest state, repeated notifications write back the same thing, and with the same size there is no further resize. + // While one of the same type has not been taken yet, the first queue position is kept: a consumer loop only reads the latest state, repeated notifications write back the same thing, and with the same size there is no further resize. // Only these three state notifications are merged; future per-occurrence events (such as Resume/Exit) must not use this rule. // Legitimate producers only send these three, so at most three items are pending, and any number of focus losses/restores no longer hits the 32-slot limit; after a pop, new changes are enqueued as usual. if (m == MSG_FOCUS || m == MSG_OPMODE || m == MSG_PERFMODE) @@ -49,16 +48,6 @@ static void mq_push_(uint32_t m) { pthread_cond_broadcast(&g_mq_cond); pthread_mutex_unlock(&g_mq_lock); } -void dm_oe_res_changed(void) { if (getenv("DM_TEST_OE_FOCUS_FIRST")) mq_push(MSG_FOCUS); /* Test: queue 31 behind a focus notification (the order at iOS app startup) */ - if (g_ntf_perfmode) mq_push(MSG_PERFMODE); else LOG("[RED] the resolution needs to change, but the game has not enabled performance mode change notifications"); } // the render resolution should change (dm_uiscreen.c): make the game re-read the parameter bits -// Operation mode reported to GetOperationMode only (used to make the game reload layouts when switching between controller / touch layouts, dm_padmode.c): -// m ≥ 0 reports m and pushes one operation-mode notification; m < 0 reports the true value again and pushes one more. -// The controller style (hle_hid.c connected_id) and performance mode keep their true values, so no "Reconnect controller" prompt appears and the render -// resolution does not change -static _Atomic int g_ui_opmode = -1; -static _Atomic int g_ui_armed; static _Atomic uint32_t g_ui_reads; // Counts once when the game first comes to read the operation mode after a notification has been pushed: the switch transaction uses it to read back "the game received it" -uint32_t dm_oe_ui_opmode_reads(void) { return atomic_load(&g_ui_reads); } -void dm_oe_ui_opmode(int m) { g_ui_opmode = m < 0 ? -1 : m; atomic_store(&g_ui_armed, 1); if (g_ntf_opmode) mq_push(MSG_OPMODE); } void dm_set_opmode(int m) { // called by the front end (overheat downgrade etc.): change the state and push 30 / 31 according to the switches the game has turned on int old = dm_opmode(); if (old == m) return; g_opmode = m; @@ -110,7 +99,7 @@ HLE_FORCE("_ZN2nn2oe17SetCopyrightImageEPKvmiiiiNS0_16WindowOriginModeE", "copyr HLE_FORCE("_ZN2nn2oe22SetCopyrightVisibilityEb", "copyright overlay visibility; the host has no system screenshot", void, h_oe_CopyrightVisible, int on) { (void)on; } // OperationMode: 0 Handheld, 1 Console; PerformanceMode: 0 Normal, 1 Boost (one-to-one with the operation mode) -HLE_FORCE("_ZN2nn2oe16GetOperationModeEv", "operation mode query; a fixed value from DM_OPMODE", int, h_oe_GetOpMode, void) { int u = g_ui_opmode; if (atomic_exchange(&g_ui_armed, 0)) atomic_fetch_add(&g_ui_reads, 1); return u >= 0 ? u : opmode(); } +HLE_FORCE("_ZN2nn2oe16GetOperationModeEv", "operation mode query; a fixed value from DM_OPMODE", int, h_oe_GetOpMode, void) { return opmode(); } // Profile perf_mode boost: Boost whatever the operation mode, a combination a Switch 1 never reports, so a profile choice and not a default. Rhythm Heaven Groove keeps one flag per mode // (main+0xa764 / main+0xa784): console means a TV, which gets a latency calibration (main+0x20d0, scene script main+0x4c0820), and Boost alone picks its 1920x1080 draw size // (table main+0xada6d0, else 1280x720). Handheld with Boost: no TV calibration on a phone, full-size drawing @@ -132,10 +121,8 @@ HLE_FORCE("_ZN2nn2oe27SetPerformanceConfigurationENS0_15PerformanceModeEi", "set } // Scripted state changes (verify lifecycle / overheat downgrade without a window): DM_OE_SCRIPT="focus=3@10,focus=1@20,opmode=0@30" (second N); bg=1 / bg=0 run the whole background round trip (see script_bg) -// Time counts from the game's first notification fetch; "@ingame+N" = N seconds after the loading screen is taken down for the K-th time (K = number of preceding @ingame items + 1, the first one is run entry) (event-based, not affected by load). Coexists with front end calls. +// Time counts from the game's first notification fetch. Coexists with front end calls. #include <unistd.h> -static _Atomic int g_ls_unloads; // number of times the loading screen was taken down (the UIMSG handler in diag.c calls dm_note_loadscreen_unload) -void dm_note_loadscreen_unload(void) { atomic_fetch_add(&g_ls_unloads, 1); } // bg=1 / bg=0: the whole "to background / back to foreground" sequence, in the same order as the scene lifecycle in front_ios.m (resign active -> enter background; enter foreground -> become active), minus the audio session reset iOS does on returning. // Used to reproduce "after going to the background and back" problems on the Mac (audio take-over and resume, guest clock freeze, graphics suspend); to imitate the process being suspended as well, send dmhost SIGSTOP / SIGCONT between the two items static void script_bg(int on) { @@ -146,15 +133,9 @@ static void *oe_script(void *arg) { char *buf = arg; uint64_t t0 = now_ns(); for (char *save, *tok = strtok_r(buf, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { char key[16]; int v; double at; - if (sscanf(tok, "%15[a-z]=%d@ingame+%lf", key, &v, &at) == 3) { - static int need; - need++; while (atomic_load(&g_ls_unloads) < need) usleep(20000); - usleep((useconds_t)(at * 1e6)); - } else { - if (sscanf(tok, "%15[a-z]=%d@%lf", key, &v, &at) != 3) die("DM_OE_SCRIPT entry '%s' should be key=value@seconds or key=value@ingame+seconds", tok); - uint64_t due = t0 + (uint64_t)(at * 1e9), now = now_ns(); - if (due > now) usleep((useconds_t)((due - now) / 1000)); - } + if (sscanf(tok, "%15[a-z]=%d@%lf", key, &v, &at) != 3) die("DM_OE_SCRIPT entry '%s' should be key=value@seconds", tok); + uint64_t due = t0 + (uint64_t)(at * 1e9), now = now_ns(); + if (due > now) usleep((useconds_t)((due - now) / 1000)); if (!strcmp(key, "focus")) dm_focus_set(v); else if (!strcmp(key, "opmode")) dm_set_opmode(v); else if (!strcmp(key, "bg")) script_bg(v); @@ -173,111 +154,12 @@ HLE_FORCE("_ZN2nn2oe20GetCurrentFocusStateEv", "foreground / background state; d HLE_FORCE("_ZN2nn2oe20SetFocusHandlingModeENS0_17FocusHandlingModeE", "focus handling mode; the game sets 1 (focus notifications), the host implements the notification semantics", void, h_oe_SetFocusMode, int m) { LOG("oe::SetFocusHandlingMode %d", m); } // Notification: returns bool (w0), the host has no system notifications -// ---------- 3D internal render scale ---------- -// The game has a static 3D render scale built in: renderer object (global pointer [main+0x31e0848]) member +0x66a4 (float, always 1.0 in the original game). -// The resolution change function main+0x8582f0(output width, output height) → main+0x1627ef0 rebuilds the 3D target as "scale × output width/height" (+0xee78 / +0xee7c), -// the UI layer and Final Result are still built at the output size (+0xee88 / +0xee8c), and at the end the game's own upscale shader ps_upsample_p brings it back to the output size. -// The boot flow does not call 0x8582f0, so it has to be called once actively; the call site must be where the game main thread fetches system notifications -- the same context in which the game itself handles mode switches -// (calling it in the present HLE self-deadlocks: the render lock is already held by the present path). -// Default: iOS 0.75, Mac host 1.0 (the screenshot gate compares against Switch ground truth); DM_3D_SCALE=0.5–1.0 overrides. -// The visual ruler ground truth phase (A / B of DM_NVN_RULER=1) forces 1.0, it only takes effect after switching to the mode under test. -// Dynamic scale (system throttling looks at utilization, the thermal state is only a coarse signal → adjust by actual dropped frames): the controller consumes the actual present intervals reported by the present thread -// (same source as perf.txt), three levels configured value (0.75) → 0.72 → 0.70; lower limit 0.70 (0.65 is visibly different). For the rules see scale3d_ctl_step. -// Switches: DM_3D_SCALE_DYN=0 off; DM_THERMAL_NOFALLBACK=1 (power / frame time measurement) also turns it off -- the scale must be pinned during measurement. -// DM_3D_SCALE_DYN_INJECT=start frame-end frame: diagnostic, within these frames every interval fed to the controller is taken as 25 ms (the Mac has no present timestamps, this is used to verify the whole lower / raise chain). -#include <TargetConditionals.h> -#define SCALE3D_MIN 0.70f -#define SCALE3D_MID 0.72f -// Pure-function controller (unit test t_oe.c feeds sequences directly): level 0 = configured value, larger is lower. Safety valve: only acts on perceptible dropped frames. -// Window: one window per 2 seconds of accumulated present intervals, frame rate in the window = presented frames ÷ sum of intervals (frames dropped by the system are not counted, that time falls into the interval of the next presented frame). -// Lower: frame rate of two consecutive windows both < target × 0.92 (55.2 at 60 fps) → lower one level. -// Raise: frame rate of 10 consecutive windows (20 seconds) all ≥ target × 0.97 (58.2 at 60 fps) → raise one level; if knocked back within 10 seconds of going up, the next raise needs 30 consecutive windows (60 seconds). -// Judging is suspended for 2 seconds after a level change (rebuilding the 3D target itself causes a hitch). Target frame rate = 1000 ÷ period_ms (0 = 60 fps). -// The old rule (lower as soon as slow frames > 5% in a 2-second window, raise needs 60 consecutive seconds with zero slow frames, knock-back ×3) lowered too easily and almost never came back up: on the test iPhone, of 582 run entries 33 were lowered and 0 came back up, -// and two real-device logs were also lowered to 0.70 at thermal state 2 with frame rate 59.8–60; the slow frame count is kept only in the diagnostic log and no longer takes part in the decision. -typedef struct { int level, max_level; double period_ms; uint64_t hold_until, last_up; double win_ms, last_fps; unsigned win_n; int bad_run, good_run, need_good; } scale3d_ctl; -#define S3_SEC 1000000000ull -static void scale3d_ctl_hold(scale3d_ctl *c, uint64_t now, uint64_t ns) { // suspend judging for a while (after entering a run / returning to foreground / changing level): the window and the consecutive counts are cleared - if (now + ns > c->hold_until) c->hold_until = now + ns; - c->win_ms = 0; c->win_n = 0; c->bad_run = c->good_run = 0; -} -static int scale3d_ctl_step(scale3d_ctl *c, double iv_ms, uint64_t now) { // returns the current level - if (now < c->hold_until || iv_ms <= 0) return c->level; // judging suspended / no reading (0) / dropped (−1) - c->win_ms += iv_ms; c->win_n++; - if (c->win_ms < 2000) return c->level; - double tgt = 1000.0 / (c->period_ms > 0 ? c->period_ms : 1000.0 / 60), fps = c->win_n * 1000.0 / c->win_ms; - c->last_fps = fps; c->win_ms = 0; c->win_n = 0; - if (fps < tgt * 0.92) { - c->good_run = 0; - if (++c->bad_run >= 2 && c->level < c->max_level) { - if (c->last_up && now - c->last_up < 10 * S3_SEC) c->need_good = 30; // dropped again within 10 seconds of going up: the next raise needs 60 seconds - c->level++; scale3d_ctl_hold(c, now, 2 * S3_SEC); - } - } else { - c->bad_run = 0; - if (fps >= tgt * 0.97) { if (++c->good_run >= (c->need_good ? c->need_good : 10) && c->level > 0) { c->level--; c->last_up = now; c->need_good = 0; scale3d_ctl_hold(c, now, 2 * S3_SEC); } } - else c->good_run = 0; - } - return c->level; -} -static _Atomic int g_scale3d_level; -static _Atomic int g_scale3d_resumed; -void dm_3d_scale_resumed(void) { atomic_store(&g_scale3d_resumed, 1); } // front end: just returned to foreground. Slow frames in the first few seconds back are recovery (surface rebuild, caches gone cold), not GPU pressure: suspend judging for 3 seconds -void dm_3d_scale_feed(double iv_ms) { // called by the present thread every frame (forwarded from diag.c dm_perf_present): actual present interval in ms, −1 = dropped, 0 = no reading - static scale3d_ctl c = {.max_level = 2}; static int on = -1; static long inj0 = -1, inj1 = -1; - if (on < 0) { const char *e = getenv("DM_3D_SCALE_DYN"), *j = getenv("DM_3D_SCALE_DYN_INJECT"); on = !(e && !strcmp(e, "0")) && !getenv("DM_THERMAL_NOFALLBACK"); - if (j) { inj0 = atol(j); inj1 = strchr(j, '-') ? atol(strchr(j, '-') + 1) : inj0; } } - if (!on) return; - if (inj0 >= 0) { long f = (long)dm_nvn_frame_count(); iv_ms = f >= inj0 && f <= inj1 ? 25.0 : 16.7; } - uint64_t now = now_ns(); - if (atomic_exchange(&g_scale3d_resumed, 0)) scale3d_ctl_hold(&c, now, 3 * S3_SEC); - { static int was; int in = dm_ingame_now(); // not judged outside a run (menu / loading); not judged in the first 20 seconds of a run either (slow frames in the first ten-odd seconds of a run are loading, not GPU pressure) - if (!in) { was = 0; scale3d_ctl_hold(&c, now, S3_SEC); return; } - if (!was) { was = 1; if (inj0 < 0) scale3d_ctl_hold(&c, now, 20 * S3_SEC); } } - { int mtl_fps_target(void); double p = mtl_fps_target() == 30 ? 1000.0 / 30 : 0; if (p != c.period_ms) { c.period_ms = p; scale3d_ctl_hold(&c, now, 2 * S3_SEC); } } // power-saving mode (30 fps): the target period changes with it, judging is suspended for 2 seconds after the switch - int was = c.level, lv = scale3d_ctl_step(&c, iv_ms, now); - if (lv != was) LOG("3D scale control: window frame rate %.1f (target %.0f) → %s to level %d%s", c.last_fps, c.period_ms > 0 ? 1000.0 / c.period_ms : 60.0, lv > was ? "down" : "up", lv, c.need_good ? " (dropped right after going up: the next step up needs 60 s)" : ""); - atomic_store(&g_scale3d_level, lv); -} -void dm_3d_scale_hot(int hot) { (void)hot; } // overheating no longer lowers the level directly (now by actual dropped frames); the symbol is kept for the front end -static _Atomic float g_scale3d_cur = 1.0f; -float dm_3d_scale_cur(void) { return atomic_load(&g_scale3d_cur); } -static void scale3d_tick(void) { - static float want_cfg = -1, cur = 1.0f; static module_t *main; - if (want_cfg < 0) { - const char *e = getenv("DM_3D_SCALE"); want_cfg = TARGET_OS_IPHONE ? 0.75f : 1.0f; - if (e) { char *end; float v = strtof(e, &end); if (end == e || *end || !(v >= 0.5f && v <= 1.0f)) die("DM_3D_SCALE invalid: %s (must be 0.5–1.0)", e); want_cfg = v; } - for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) main = &g_mod[i]; - // The addresses above (renderer global pointer, resolution change function, member offsets) all belong to D2R build db0507f3…; in other builds (every Mario version) the same addresses are unrelated data and code - // (Mario v0's main+0x8582f0 is in the middle of a static initializer), so writing and calling them would corrupt memory. Only enabled on that build; elsewhere it says so only when DM_3D_SCALE asks for this scale - // (Breath of the Wild and Tears of the Kingdom scale through DM_RENDER_SCALE, and the line read like a failure in their logs) - if (main && strcmp(main->build_id, "db0507f36b84200ac05f75d76ed5026f")) { if (e) LOG("3D render scale: main build %s has no known addresses, left alone (only D2R db0507f3… is known)", main->build_id[0] ? main->build_id : "(output has no build ID)"); main = NULL; } - } - int lv = atomic_load(&g_scale3d_level); // dynamic level: 0 = configured value, 1 = 0.72, 2 = 0.70 (none higher than the configured value) - float dyn = lv == 0 ? want_cfg : lv == 1 ? SCALE3D_MID : SCALE3D_MIN; if (dyn > want_cfg) dyn = want_cfg; - float want = dm_nvn_ruler_truth() ? 1.0f : dyn; - if (want == cur || !main || dm_nvn_frame_count() < 30) return; - uintptr_t renderer = *(uintptr_t *)(main->base + 0x31e0848); if (!renderer) return; - uint32_t ow = *(uint32_t *)(renderer + 0xee88), oh = *(uint32_t *)(renderer + 0xee8c); if (!ow || !oh) return; - { extern void mtl_rt_force_full(int) __attribute__((weak)); if (mtl_rt_force_full) mtl_rt_force_full(4); } // runtime extrapolation: the few frames before and after a scale change are all drawn in full (the frame where the main pass changes size cannot be extrapolated) - float *scale = (float *)(renderer + 0x66a4); dm_guest_write_prepare(scale, sizeof *scale); *scale = want; - ((void (*)(uint32_t, uint32_t))(main->base + 0x8582f0))(ow, oh); - cur = want; atomic_store(&g_scale3d_cur, want); - LOG("3D render scale %.2f: 3D target %ux%u, output %ux%u (frame %llu)", want, *(uint32_t *)(renderer + 0xee78), *(uint32_t *)(renderer + 0xee7c), - *(uint32_t *)(renderer + 0xee88), *(uint32_t *)(renderer + 0xee8c), (unsigned long long)dm_nvn_frame_count()); -} HLE_FORCE("_ZN2nn2oe25TryPopNotificationMessageEPj", "system notification queue; front end state changes are queued", unsigned, h_oe_TryPopMsg, uint32_t *msg) { - scale3d_tick(); pthread_once(&g_oe_once, oe_script_start); pthread_mutex_lock(&g_mq_lock); int ok = g_mqn > 0; if (ok) { *msg = g_mq[g_mqh]; g_mqh = (g_mqh + 1) % MQ; g_mqn--; } pthread_mutex_unlock(&g_mq_lock); - // The resolution parameter bits live in the thread slot, and the game reads them on the thread that fetches notifications to change the render size (on receiving 31 "performance mode changed", from main+0xe10ddc; 30 only updates a flag). - // When they are read: each time the handler is entered, once after the first notification is fetched (0xe10d5c / 0xe10d60), and notifications fetched later in the loop still use the values from that read. So every time one is taken, the value due at this moment is written into the current thread's slot -- - // writing only when 31 is taken is not enough: when 31 is queued behind another notification (e.g. focus 15), the parameter bits have already been read after fetching 15 (iOS app: the front end did not switch to 16:9, in the Mac host run 31 happened to be queued first). - if (ok) for (int i = 0; i < 2; i++) if (g_keyres[i]) pthread_setspecific(g_keyres[i], (void *)dm_game_res(i)); - if (ok && *msg == MSG_PERFMODE) dm_uiscreen_res_reread(); if (ok) LOG("oe notification popped by the game: %u", *msg); // state changes are rare, printing each one does not flood the log mq_ev_sync(); // signalled again while notifications remain, cleared when the queue is empty (also a signal left behind by a pop that found it empty) return ok; diff --git a/dm/host/hle_os.c b/dm/host/hle_os.c index faae819..10d3ce4 100644 --- a/dm/host/hle_os.c +++ b/dm/host/hle_os.c @@ -55,26 +55,13 @@ static int detclock_cfg(void) { static uint64_t wall_guest_ns(void) { return atomic_load(&g_gfz) ? atomic_load(&g_gfrozen) : now_ns() - atomic_load(&g_glag); } uint64_t guest_ns(void) { return atomic_load(&g_gdet_on) ? atomic_load(&g_gdet) : wall_guest_ns(); } int dm_detclock_enabled(void) { return detclock_cfg() && atomic_load(&g_gdet_on); } -// Run entry alignment: the number of presented frames in the front end / character select phase varies with real time (loading advances on a background thread by wall clock), so the guest clock value accumulated since the first frame differs every run -- -// pictures that depend on absolute time (such as the liquid level of the life / mana orbs) still differ at the same tick (measured: at tick E + 2 frames two passes differ by about 18,000 pixels, concentrated at the two orb positions). -// On seeing the UI message FrontEndNavigation:ToGame (before the run is created and the server tick deadline is initialized) the guest clock is set to the fixed value 2^52 ns (forward jumps only), -// from then on it advances purely by presents: which frame tick E lands on and the absolute time at that moment are determined by the step alone. -#define DET_TOGAME_NS (1ull << 52) -static void det_on_uimsg(const char *cat, const char *act, const char *arg) { - (void)arg; if (strcmp(cat, "FrontEndNavigation") || strcmp(act, "ToGame")) return; - uint64_t cur = atomic_load(&g_gdet); - if (cur >= DET_TOGAME_NS) { LOG("[RED] DM_DETCLOCK: guest clock %llu ≥ alignment value 2^52 on entering a run, not turned back (alignment not applied)", (unsigned long long)cur); return; } - atomic_store(&g_gdet, DET_TOGAME_NS); - LOG("DM_DETCLOCK: on entering a run (ToGame) the guest clock is aligned to 2^52 ns (jumped forward %.3f s)", (DET_TOGAME_NS - cur) / 1e9); -} void dm_guest_frame_tick(void) { crash_watch_frame(); atomic_fetch_add(&g_guest_frame, 1); if (!detclock_cfg()) return; if (!atomic_load(&g_gdet_on)) { atomic_store(&g_gdet, wall_guest_ns()); atomic_store(&g_gdet_on, 1); - void uimsg_subscribe(void (*)(const char *, const char *, const char *)); uimsg_subscribe(det_on_uimsg); - LOG("DM_DETCLOCK: from the first present on, the guest clock advances by presents, step %llu ns; aligned to 2^52 ns on entering a run", (unsigned long long)g_gdet_step); return; + LOG("DM_DETCLOCK: from the first present on, the guest clock advances by presents, step %llu ns", (unsigned long long)g_gdet_step); return; } if (!atomic_load(&g_gfz)) { atomic_fetch_add(&g_gdet, g_gdet_step); dm_guest_clock_advanced(); } } diff --git a/dm/host/hle_sdk16.c b/dm/host/hle_sdk16.c index 22fee8f..ab8aee7 100644 --- a/dm/host/hle_sdk16.c +++ b/dm/host/hle_sdk16.c @@ -1,4 +1,4 @@ -// System service interfaces used by games on newer SDKs (16.x) that the d2r-mobile title did not use: oe / time / pl (shared font) / vi vsync / diag. +// System service interfaces used by games on newer SDKs (16.x): oe / time / pl (shared font) / vi vsync / diag. // On the real device all of these go through Horizon system services (IPC); the host gives values directly or makes them "unavailable". #include "hle.h" #include <fcntl.h> diff --git a/dm/host/hle_svc.c b/dm/host/hle_svc.c index d9fc1c8..443b113 100644 --- a/dm/host/hle_svc.c +++ b/dm/host/hle_svc.c @@ -15,7 +15,7 @@ // about 4.2 GB in total; on iOS without the "extended virtual addressing" entitlement a single reservation can be at most 6 GB // (read on a real device: 6 GB succeeds, 7 GB fails), so it cannot be made any wider -- // with an 8 GB reservation, builds from developer accounts that cannot sign that entitlement (free accounts) would exit at this step. -#define RESERVE DM_GAME_MEM_RESERVE // SMBW: the heap needs only 1.47 GB, so the block stays at 5 GB like D2R (the total proven on a real device; a single 6 GB block can be reserved, but Metal etc. still need address space) +#define RESERVE DM_GAME_MEM_RESERVE // SMBW: the heap needs only 1.47 GB, so the block stays at 5 GB (the total proven on a real device; a single 6 GB block can be reserved, but Metal etc. still need address space) static uint8_t *g_as; // ASLR region base = the whole block static uint8_t *g_heap; static size_t g_heap_cur, g_heap_cap; // The address range lies entirely within the currently opened (readable and writable) part of the guest heap: such reads need no kernel trap to check, memcpy directly (used where guest memory is read every tick, such as diag.c tk_rd) @@ -120,7 +120,6 @@ uint64_t svc_dispatch(int num, uint64_t *regs, uint64_t orig_addr) { { void hlecount_svc(int); hlecount_svc(num); } // DM_HLECOUNT: svc counts by number mem_init(); if (svc_thread(num, regs)) return regs[0]; - { uint64_t svc_host_call(int, uint64_t *); if (num >= 0x80 && svc_host_call(num, regs)) return regs[0]; } // from 0x80: host calls by cut convention (hle_tact.c) switch (num) { case 0x01: { // SetHeapSize(out_addr*, size) → x1 = heap base size_t size = regs[1]; @@ -204,7 +203,6 @@ uint64_t svc_dispatch(int num, uint64_t *regs, uint64_t orig_addr) { // The address lies in "the current host thread's stack": report the whole stack (state 0xB = stack). When the SDK initializes the main thread record it queries here with the sp at that moment and takes the reported range as the main thread stack; // later nn::os::GetCurrentStackInfo asserts that sp is still inside that range and aborts if not. Previously every address outside the guest address space was reported as "the 4 KiB page containing it", // so the main thread stack was recorded as one page. Whether the sp at init and a later sp share a page depends on the absolute position of the host stack top, which moves with the total length of environment variables / launch arguments (a different environment occasionally aborts at startup). - // This Diablo build does not call GetCurrentStackInfo (main / subsdk0 do not import it and the SDK has no internal call site; the host reaches the main menu with 12 different environment padding lengths), so it was never hit; reporting "one page" is wrong in itself, so fix it anyway. uint64_t top = (uint64_t)pthread_get_stackaddr_np(pthread_self()), ssz = pthread_get_stacksize_np(pthread_self()); if (addr < top && addr >= top - ssz) { base = top - ssz; size = ssz; state = 0xB/*Stack*/; perm = 3; } // Other host memory: report "free, no permissions" (the 4 KiB page around it) as Horizon does for unmapped addresses. It used to report Code+RX, and GetExceptionInfo walking back from main merged on through host memory down to 0 diff --git a/dm/host/hle_swkbd.c b/dm/host/hle_swkbd.c index e556d1b..e4071ec 100644 --- a/dm/host/hle_swkbd.c +++ b/dm/host/hle_swkbd.c @@ -1,13 +1,13 @@ // swkbd software keyboard (system applet) → host native input box. MakePreset / SetInitialTextUtf8 / GetRequired*Size are left to run as is in the SDK, // here we only read the ShowKeyboardArg they write and write back String per the output convention of the real swkbd applet. -// Layout sources (offsets in the original sdk 3.3.94012 image): +// Layout sources (offsets in the sdk image the host was first written against): // ShowKeyboard 0x3a6770 moves KeyboardConfig field by field into the applet arguments: +0x24 title u16[65], +0xa6 subtitle u16[129], +0x1a8 hint u16[257], // +0x3ac maximum length, +0x3b0 minimum length, +0x3bd isUseUtf8, +0x3c0 offset of the initial text within the work buffer, +0x3c4 UTF-16 length of the initial text; // work buffer pointer / size at +0x4d0 / +0x4d8 (handed to the applet together with CreateLargeStorage) // SetInitialTextUtf8 0x3a74c0: UTF-8 → UTF-16 written to work buffer+offset (offset 0x14, at most 500 characters + terminating 0) // ReadCloseResultAndString 0x3a63f0: output storage = u32 CloseResult + text area; the text area is copied as is into String{+0 pointer, +8 byte count} // ShowKeyboard wrap-up 0x3a6acc: CloseResult == 0 (OK) → Result 0, otherwise (cancel) → 0x29F; GetRequiredStringBufferSize = 0x7D4 -// Game side (main 0xc6deac) sets isUseUtf8 = 1 after MakePreset and reads back as a C string (0x1b66640 strlen); UTF-16 output is supported as well per the original convention. +// A game that sets isUseUtf8 = 1 after MakePreset reads the result back as a C string; UTF-16 output is supported as well per the original convention. // No window and DM_KBD unset: return as cancel (no popup, no hang). DM_KBD="text": no popup, return that text directly as OK (windowless testing / lockstep comparison). #include "hle.h" #include <stddef.h> @@ -90,7 +90,6 @@ Result swkbd_show(void *out, const void *arg_, swkbd_input_fn input) { uint16_t u16[1003]; size_t n = swkbd_u8_to_u16(text, maxlen > 0 ? (size_t)maxlen : 1002, u16, 1003); // truncate to the character limit (counted in UTF-16 units) if (arg[ARG_UTF8]) swkbd_u16_to_u8(u16, n, (char *)buf, cap); else memcpy(buf, u16, (n + 1) * 2 <= cap ? n * 2 : (cap / 2 - 1) * 2); - { char fin[64]; swkbd_u16_to_u8(u16, n, fin, sizeof fin); void dm_ui_text_entered(const char *text); dm_ui_text_entered(n > 0 ? fin : NULL); } { const char *lag = getenv("DM_TEST_SWKBD_LAG_MS"); if (lag) usleep((useconds_t)atoi(lag) * 1000); } // test: slow down "keyboard handed back → the game's thread gets the result" to verify that auto-accept does not jump the gun // front end path cleanup (dm_cursor.c): auto-accept after the name is typed on the naming page return RES_OK; } diff --git a/dm/host/hle_thread.c b/dm/host/hle_thread.c index 21ea94b..80ef114 100644 --- a/dm/host/hle_thread.c +++ b/dm/host/hle_thread.c @@ -165,16 +165,7 @@ static void wake(thr_t *t, int64_t res) { if (self && self != t && !t->host_owned && t->prio < self->prio && t->mask == 1ull << (self->core & 63)) self->yield_pending = 1; } // Suspend until woken or timed out (ns<0 infinite, 0 immediate timeout); returns wres or R_TIMEOUT -static int64_t block_inner(thr_t *t, int kind, int64_t ns); -static int64_t block(thr_t *t, int kind, int64_t ns) { // Boot timeline: accumulated by wait kind (all threads) + the main thread listed separately - if (!g_flag.bootprof) return block_inner(t, kind, ns); - uint64_t t0 = now_ns(); int64_t r = block_inner(t, kind, ns); uint64_t d = now_ns() - t0; - static const int id[] = {0, PROF_WAIT_LOCK, PROF_WAIT_CV, PROF_WAIT_ADDR, PROF_WAIT_SYNC}; - dm_prof_add(id[kind & 7], d, 0); - if (t->handle == 0x1001) dm_prof_add(PROF_MAIN_WAIT, d, 0); - return r; -} -static int64_t block_inner(thr_t *t, int kind, int64_t ns) { +static int64_t block(thr_t *t, int kind, int64_t ns) { t->wkind = kind; t->woke = 0; t->next = g_wait; g_wait = t; t->wseq = ++g_wseq; if (ns == 0) { wait_unlink(t); t->wkind = 0; return R_TIMEOUT; } struct timespec dl; @@ -445,7 +436,7 @@ static void thr_bt(thr_t *t) { arm_thread_state64_t st; mach_msg_type_number_t n = ARM_THREAD_STATE64_COUNT; if (thread_get_state(t->mport, ARM_THREAD_STATE64, (thread_state_t)&st, &n) == KERN_SUCCESS) { // Threads not waiting in a system call (running guest code, possibly spinning on a condition): print the callee-saved registers x19–x28, plus 128 bytes of the memory pointed to by each of x19 / x20. - // The state seen in the occasional "no frames after entering a run": the main thread spins in the job pool's "help while waiting" (main+0x14f08d8), x19 = wait object, x20 = job pool -- only with these can one tell which batch it is waiting for and what the unfinished count is + // A thread spinning on a condition is told apart by its callee-saved registers (often the object it waits on) and the memory they point to if (t->wkind == 0) { char rl[400]; int q = snprintf(rl, sizeof rl, "[dm] %#x registers pc=", t->handle); uint64_t pc = __darwin_arm_thread_state64_get_pc(st); module_t *pm = module_of((void *)pc); q += pm ? snprintf(rl + q, sizeof rl - q, "%s+%#llx", pm->name, pc - (uint64_t)pm->base) : snprintf(rl + q, sizeof rl - q, "%#llx", pc); for (int i = 19; i <= 28; i++) q += snprintf(rl + q, sizeof rl - q, " x%d=%#llx", i, st.__x[i]); @@ -461,43 +452,15 @@ static void thr_bt(thr_t *t) { if (sp > 0x100000000ull && !vm_read_overwrite(mach_task_self(), sp, sizeof stk, (vm_address_t)stk, &g) && g == sizeof stk) for (int r = 0; r < 128; r += 8) fprintf(stderr, "[dm] %#x stack sp+%#05x: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", t->handle, r * 8, stk[r], stk[r + 1], stk[r + 2], stk[r + 3], stk[r + 4], stk[r + 5], stk[r + 6], stk[r + 7]); } - char line[1024]; int p = snprintf(line, sizeof line, "[dm] %#x backtrace:", t->handle), nf = 0; uint64_t wait_fp = 0; + char line[1024]; int p = snprintf(line, sizeof line, "[dm] %#x backtrace:", t->handle), nf = 0; for (int k = 0; k < 64 && fp && nf < 20 && p < (int)sizeof line - 40; k++) { module_t *m = module_of((void *)lr); if (m) { p += snprintf(line + p, sizeof line - p, " %s+%#llx", m->name, lr - (uint64_t)m->base); nf++; } uint64_t fr[2]; vm_size_t g2 = 0; if (vm_read_overwrite(mach_task_self(), fp, 16, (vm_address_t)fr, &g2) || g2 != 16 || fr[0] <= fp) break; - if (m && !strcmp(m->name, "main") && lr - (uint64_t)m->base == 0x132db30) wait_fp = fp; // The return point is inside the resource wait function main+0x132d988 (after it calls the job pool's "help while waiting"): the frame pointer at this moment is its own fp = fr[0]; lr = fr[1]; } fprintf(stderr, "%s\n", line); - // The resource wait function main+0x132d988 (prologue sub sp,#0x50 … add x29,sp,#0x40) places the wait object {vtable, pointer to the event pointer} at the bottom of its own stack (0x132db14 stp x8,x23,[sp]), - // so the wait object = its frame pointer −0x40; what that points to, −0x10 = the resource wait object {+0 in-flight job, +8 mutex pointer, +0x10 event pointer}. It is located by the return point 0x132db30, regardless of which instruction of the wait loop the thread is stopped on right now. - // The old code took "the previous frame" based on 0x14f0934 and was off by one level, reading the resource wait object as the wait object; "in-flight job null" and "event / lock unreadable" were both misreads - if (wait_fp) { uint64_t wo[2] = {0, 0}, res[3] = {0, 0, 0}, wjob = 0; vm_size_t g = 0; - #define RD(addr, buf) ((addr) > 0x100000000ull && !vm_read_overwrite(mach_task_self(), (addr), sizeof(buf), (vm_address_t)(buf), &g) && g == sizeof(buf)) - #define HEX(tag, addr, nbytes) { uint32_t raw[32]; vm_size_t gg = 0; if ((addr) > 0x100000000ull && !vm_read_overwrite(mach_task_self(), (addr), (nbytes), (vm_address_t)raw, &gg) && gg == (nbytes)) { \ - char hx[300]; int h = 0; for (unsigned q = 0; q < (nbytes) / 4; q++) h += snprintf(hx + h, sizeof hx - h, "%08x ", raw[q]); fprintf(stderr, "[dm] %#x job pool wait: %s %#llx: %s\n", t->handle, tag, (unsigned long long)(addr), hx); } \ - else fprintf(stderr, "[dm] %#x job pool wait: %s %#llx unreadable\n", t->handle, tag, (unsigned long long)(addr)); } - if (RD(wait_fp - 0x40, wo)) { module_t *vm_ = module_of((void *)wo[0]); - fprintf(stderr, "[dm] %#x job pool wait: wait object %#llx vtable %s+%#llx (should be main+0x20f2408), points to %#llx\n", t->handle, (unsigned long long)(wait_fp - 0x40), vm_ ? vm_->name : "?", vm_ ? wo[0] - (uint64_t)vm_->base : wo[0], wo[1]); - if (RD(wo[1] - 0x10, res)) { - fprintf(stderr, "[dm] %#x job pool wait: resource wait object %#llx: in-flight job %#llx (%s), mutex %#llx, event %#llx. Reading: in-flight job null = the signal was consumed by some wait; non-null and event not set = the completion signal never came; event set = a problem in the wait loop\n", - t->handle, (unsigned long long)(wo[1] - 0x10), res[0], res[0] ? "non-null" : "null", res[1], res[2]); - HEX("event body 64 bytes", res[2], 64); - HEX("mutex body 32 bytes (+8 recursion count, +0x10 owner thread object, +0x18 lock word = owner handle)", res[1], 32); - // Job object (filled by the submit function main+0x147f154): +0x10 flags (filtered by a per-thread-class mask when taking a job, 0xeda190 / 0xdf8fac), +0x20 next in the queue, dependency table from +0x30 (low 31 bits = count), +0x60 resource, +0x68 pointer to the event pointer - if (res[0]) { HEX("in-flight job object, 128 bytes from +0", res[0], 128); HEX("in-flight job object, 128 bytes from +0x80", res[0] + 0x80, 128); } wjob = res[0]; } } - // Job pool: when the thread is stopped on an instruction of the wait loop main+0x14f08d8 itself, x20 is the pool; when it is stopped in a function called from it, x20 cannot be trusted and is not printed - { uint64_t pc = __darwin_arm_thread_state64_get_pc(st); module_t *pm = module_of((void *)pc); uint64_t off = pm && !strcmp(pm->name, "main") ? pc - (uint64_t)pm->base : 0; - // Queue (0xeda190): {+0 tail, +8 head, +0x10 count, +0x18 lock}, node +0x10 flags, +0x20 next. Walk from the head for at most 8 nodes to see whether the awaited job is in this pool - if (off >= 0x14f0900 && off < 0x14f09b8) { uint64_t qa = st.__x[20] + 0x410, nd = 0; HEX("job queue head (pool = x20, 32 bytes from +0x410)", qa, 32); int in = 0, cnt = 0; - if (RD(qa + 8, &nd)) for (; nd && cnt < 8; cnt++) { uint64_t nb[5] = {0}; if (!RD(nd, nb)) break; fprintf(stderr, "[dm] %#x job pool wait: queue entry %d %#llx flags %#x%s\n", t->handle, cnt, (unsigned long long)nd, (uint32_t)nb[2], nd == wjob ? " (the waited-for job)" : ""); in |= nd == wjob; nd = nb[4]; } - fprintf(stderr, "[dm] %#x job pool wait: the waited-for job %s among the first %d in the queue of pool x20\n", t->handle, in ? "is" : "is not", cnt); } - else fprintf(stderr, "[dm] %#x job pool wait: the thread is not on the wait loop's own instructions, job pool not read\n", t->handle); } - #undef RD - #undef HEX - } } thread_resume(t->mport); } @@ -530,7 +493,7 @@ void svc_thread_dump(void) { fprintf(stderr, "[dm] %#x waited-for object, 64 bytes from %#llx: %s\n", t->handle, t->waddr & ~0xfull, hx); } thr_bt(t); } - // Threads created by the host itself (prefetch, Granny background, graphics present, audio, etc.): not in the guest thread table, enumerated as mach threads, the first 12 frames symbolized with dladdr + // Threads created by the host itself (graphics present, audio, movie decoding, etc.): not in the guest thread table, enumerated as mach threads, the first 12 frames symbolized with dladdr thread_act_array_t ths; mach_msg_type_number_t nth = 0; if (task_threads(mach_task_self(), &ths, &nth) == KERN_SUCCESS) { for (mach_msg_type_number_t i = 0; i < nth; i++) { @@ -668,14 +631,13 @@ static int svc_thread_impl(int num, uint64_t *regs) { case 0x0A: thread_exit(cur()); return 1; case 0x0B: { // SleepThread(x0 ns); 0/-1/-2 = the various kinds of yield int64_t ns = (int64_t)regs[0]; - uint64_t t0 = g_flag.bootprof ? now_ns() : 0; if (ns > 0 && dm_detclock_enabled()) { uint64_t deadline = guest_ns() + (uint64_t)ns; while (guest_ns() < deadline) { struct timespec ts = {0, 1000000}; nanosleep(&ts, 0); } } else if (ns >= 20000) { thr_t *me = pthread_getspecific(g_keythr); if (me) me->sleeping = 1; sleep_precise(now_ns() + (uint64_t)ns); if (me) me->sleeping = 0; } // Yield (0 / -1 / -2), and sleeps shorter than 20 µs, which sleep_precise would return from at once: Super Mario Galaxy's Hagi polls with SleepThread(100 ns) (main+0x517c), // which kept a core at 100% (1.4 billion calls in 200 s); as yields they get the back-off below. - // Count and time of the main thread's yields are recorded: the game's render task graph "wait for everything to finish" is pure polling + yield (main+0x15ee898); measure how much of a frame it takes + // Count and time of the main thread's yields are recorded: a render task graph's "wait for everything to finish" is often pure polling + yield; measure how much of a frame it takes else { thr_t *me = pthread_getspecific(g_keythr); uint64_t y0 = me ? now_ns() : 0; @@ -685,7 +647,6 @@ static int svc_thread_impl(int num, uint64_t *regs) { else sched_yield(); if (me && me->handle == 0x1001) { g_main_yield_ns += now_ns() - y0; g_main_yield_n++; } } - if (g_flag.bootprof) { uint64_t d = now_ns() - t0; dm_prof_add(PROF_SLEEP, d, 0); thr_t *me = pthread_getspecific(g_keythr); if (me && me->handle == 0x1001) dm_prof_add(PROF_MAIN_WAIT, d, 0); } return 1; } case 0x0C: { thr_t *t = h_get((uint32_t)regs[1], O_THREAD); regs[0] = t ? 0 : R_BAD_HANDLE; regs[1] = t ? (uint64_t)t->prio : 0; return 1; } diff --git a/dm/host/hle_time.c b/dm/host/hle_time.c index 1fde3da..7af257e 100644 --- a/dm/host/hle_time.c +++ b/dm/host/hle_time.c @@ -8,37 +8,13 @@ typedef struct { int16_t year; int8_t month, day, hour, minute, second; int8_t pad; } CalendarTime; typedef struct { uint32_t wday, yday; char tzname[8]; uint32_t dst; int32_t offset; } CalendarAdditionalInfo; -// DM_FIXED_TIME=seconds (for deterministic comparison; the ground-truth side counterpart is dm/verify/ryu/drive.py --fix-time): -// ① the user clock (source of the save's d2s +0x20 last-played time and .meta last_modified_time) always returns this value; -// ② the one time() at the game-creation random seed (main 0x3509a0 bl time, return point 0x3509a4, same point as drive.py) returns this value. -// This SDK's libc time() is uptime (clock_gettime(0) → __nnmusl_ClockGetTime → GetSystemTick), not wall clock; -// the other 17 time() call sites in main are left alone and forwarded unchanged to the SDK's own time(). +// DM_FIXED_TIME=seconds (for deterministic comparison): the user clock always returns this value (and main.c pins the host time zone to UTC) static int64_t fixed_time(void) { static int64_t v = -2; if (v == -2) { const char *e = getenv("DM_FIXED_TIME"); v = e ? strtoll(e, 0, 0) : -1; - if (v >= 0) LOG("DM_FIXED_TIME=%lld: user clock and the run-setup time() (main 0x3509a4) return a fixed value", (long long)v); } + if (v >= 0) LOG("DM_FIXED_TIME=%lld: the user clock returns a fixed value", (long long)v); } return v; } -HLE_FORCE("time", "recognizes the run-setup seed call by its return address (DM_FIXED_TIME); everything else passes straight to the SDK's time()", int64_t, h_time_time, int64_t *t) { - uint64_t lr = (uint64_t)__builtin_return_address(0); - static int64_t (*sdk_time)(int64_t *); static uint64_t seed_ret; - if (!sdk_time) { - module_t *ms = NULL, *mm = NULL; - for (int i = 0; i < g_nmod; i++) { if (!strcmp(g_mod[i].name, "sdk")) ms = &g_mod[i]; if (!strcmp(g_mod[i].name, "main")) mm = &g_mod[i]; } - void *f = ms ? module_sym(ms, "time") : NULL; if (!f) die("time not found in the SDK"); - uint32_t w = mm && !strcmp(mm->build_id, DM_D2R_BUILD) ? *(uint32_t *)(mm->base + 0x3509a0) : 0; // read only on the D2R build (elsewhere this is unrelated code, and a main smaller than 3.3 MB would be read past its end) - if ((w & 0xfc000000) == 0x94000000) seed_ret = (uint64_t)mm->base + 0x3509a4; // bl time (build db0507f3…; the rewriter changes the branch target, so only the bl itself is matched) - else if (fixed_time() >= 0) LOG("[RED] DM_FIXED_TIME: main 0x3509a0 is not the expected bl time (%#x), run-setup time() not fixed", w); - sdk_time = (int64_t (*)(int64_t *))f; - } - int64_t ft = fixed_time(); - if (ft >= 0 && lr == seed_ret) { - static int once; if (!once++) LOG("DM_FIXED_TIME: run-setup time() returns %lld", (long long)ft); - if (t) *t = ft; return ft; - } - return sdk_time(t); -} - HLE_FORCE("_ZN2nn4time10InitializeEv", "connect to the time service; clocks come from the host system time instead", Result, h_time_Initialize, void) { return RES_OK; } HLE_FORCE("_ZN2nn4time13IsInitializedEv", "the time service is taken over, always initialized", int, h_time_IsInitialized, void) { return 1; } // Steady clock (seconds since boot + a clock source ID, a 16-byte UUID): the host's monotonic seconds with a fixed source ID. GetSpanBetween runs as is in sdk: it subtracts only when the source IDs match @@ -78,23 +54,7 @@ HLE_FORCE("_ZN2nn4time23StandardUserSystemClock21GetSystemClockContextEPNS0_18Sy pthread_mutex_unlock(&mu); return RES_OK; } -// Forced Terror Zone for stages 6–10 of the boss rush (set by dm_bossrush.c): when g_dm_tz_time is non-zero and not expired, only the clock reads at the "Terror Zone" sites get it; save timestamps and all other calls are unchanged. -// Which sites: the return point falls in the cluster starting at the zone function 0x58f1e0 (0x58f1e0–0x591440), or the zone schedule 0x58f310 is called within 8 instructions after the return (server side 0x636b38 applying the zone, 0x694b48 and so on, 9 sites). -// Expiry: dm_bossrush renews it by 1 second every tick; once this run stops ticking (exit / crash) it no longer applies, and the next run gets the real clock when the game is created. -_Atomic int64_t g_dm_tz_time; -_Atomic uint64_t g_dm_tz_live_ns; -static int tz_site(uint64_t lr) { // Checked only while forcing is active, reading the 8 instructions after the return point each time, no caching (several threads read the clock) - static _Atomic uint64_t base; uint64_t b = atomic_load(&base); - if (!b) { for (int i = 0; i < g_nmod; i++) if (!strcmp(g_mod[i].name, "main")) b = (uint64_t)g_mod[i].base; atomic_store(&base, b); } - if (!b || lr < b) return 0; - uint64_t off = lr - b; if (off > 0x58f1e0 && off < 0x591440) return 1; - for (int k = 0; k < 8; k++) { uint32_t w = *(const uint32_t *)(uintptr_t)(lr + 4 * k); - if ((w & 0xfc000000) == 0x94000000) { int64_t imm = w & 0x3ffffff; if (imm & 0x2000000) imm -= 1 << 26; if (off + 4 * k + imm * 4 == 0x58f310) return 1; } } - return 0; -} -HLE_FORCE("_ZN2nn4time23StandardUserSystemClock3nowEv", "user clock → host time(); while the boss rush forces a zone, the Terror Zone call sites get a different value", int64_t, h_time_now, void) { - int64_t tz = atomic_load(&g_dm_tz_time); - if (tz && (uint64_t)clock_gettime_nsec_np(CLOCK_MONOTONIC) < atomic_load(&g_dm_tz_live_ns) && tz_site((uint64_t)__builtin_return_address(0))) return tz; +HLE_FORCE("_ZN2nn4time23StandardUserSystemClock3nowEv", "user clock → host time()", int64_t, h_time_now, void) { int64_t ft = fixed_time(); return ft >= 0 ? ft : time(NULL); } HLE_FORCE("_ZN2nn4time23StandardUserSystemClock9to_time_tERKNSt3__16chrono10time_pointINS0_17SystemClockTraitsENS3_8durationIlNS2_5ratioILl1ELl1EEEEEEE", "the SDK implementation pairs with now(), taken over with it", int64_t, h_time_to_time_t, const int64_t *tp) { return *tp; } @@ -142,8 +102,8 @@ HLE_FORCE("_ZN2nn4time18ToPosixTimeFromUtcERKNS0_12CalendarTimeE", "UTC calendar struct tm tm = {.tm_year = ct->year - 1900, .tm_mon = ct->month - 1, .tm_mday = ct->day, .tm_hour = ct->hour, .tm_min = ct->minute, .tm_sec = ct->second}; return (int64_t)timegm(&tm); } -// Local time conversion is forced onto the host libc: the SDK's own localtime / localtime_r / mktime have no time zone data and always use UTC (measured on the host: the chronicle's "First Discovered" is produced by -// main+0xb04d34 calling localtime and then strftime, and was off by 8 hours). The SDK on a real Switch converts by the system time zone, so only following the host system time zone here is consistent; +// Local time conversion is forced onto the host libc: the SDK's own localtime / localtime_r / mktime have no time zone data and always use UTC +// (a game's local timestamps were off by the time zone's offset). The SDK on a real Switch converts by the system time zone, so only following the host system time zone here is consistent; // the host TZ normally = the system time zone, and is pinned to UTC only for deterministic comparison with DM_FIXED_TIME (main.c). The struct tm layout is the same on both sides (9 ints, long offset, time zone name pointer). // gmtime / gmtime_r / timegm are UTC anyway and are not taken over. The same-named pass-through entries in hle_table.c only take effect when the symbol has no definition (the SDK defines them), hence HLE_FORCE HLE_FORCE("localtime", "local time → host localtime (follows the system time zone)", struct tm *, h_localtime, const int64_t *t) { time_t v = (time_t)*t; return localtime(&v); } diff --git a/dm/host/host_stubs.c b/dm/host/host_stubs.c deleted file mode 100644 index 8dc0b8c..0000000 --- a/dm/host/host_stubs.c +++ /dev/null @@ -1,27 +0,0 @@ -// Hook points left behind by the game-specific modules split out of d2r-mobile (diagnostic tick gates, TACT/Granny, UI screen, touch UI interaction, ...): -// the generic host still calls them, and they get empty implementations here. When a feature is redone for this game, replace the matching stub with the real thing. -#include "dm.h" -#include "front.h" - -void crash_sample_self(void) {} -void inflate_bench_maybe(void) {} -void ticks_install(void) {} -void target_rank_install(void) {} -void dm_padmode_boot(void) {} -void dm_prof_mark(const char *seg) { (void)seg; } -void dm_prof_add(int id, uint64_t ns, uint64_t bytes) { (void)id; (void)ns; (void)bytes; } -void tact_chk_init(void) {} -void tact_prefetch_start(const char *outdir) { (void)outdir; } -void tact_prefetch_stop(void) {} -void granny_prof_dump(void) {} -void uimsg_subscribe(void (*fn)(const char *cat, const char *act, const char *arg)) { (void)fn; } -void dm_ui_text_entered(const char *text) { (void)text; } -int dm_custom_lang_explicit(void) { return 0; } -int dm_ingame_now(void) { return 0; } -void dm_uiscreen_res_reread(void) {} -// Game resolution: whatever the front end gives (Mac window / iOS screen), without D2R's UI-screen rewriting -uint64_t dm_game_res(int docked) { return front_game_res(docked); } -int dm_screen_real(int *w, int *h) { *w = *h = 0; return 0; } -void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; } -// svc #0x80.. host gates: only D2R's rewrite rules create them; they should not appear in this game's build output -uint64_t svc_host_call(int num, uint64_t *regs) { (void)regs; die("svc_host_call: this game has no host gate svc #%#x", num); } diff --git a/dm/host/loader.c b/dm/host/loader.c index 4c08604..1afea6e 100644 --- a/dm/host/loader.c +++ b/dm/host/loader.c @@ -13,8 +13,7 @@ module_t g_mod[MAX_MOD]; int g_nmod; pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate; -pthread_key_t g_keyres[2], g_keyaspect, g_keyaspectui; // g_keyaspect: aspect ratio upper-limit parameter slot (KASPECT) // resolution parameter slots (rewriter PARAMS, KRES0 handheld / KRES1 docked); 0 and not allocated if the build output lacks them -enum { KEY_K18, KEY_KTLS, KEY_KTLR, KEY_KGATE, KEY_KRES0, KEY_KRES1, KEY_KASPECT, KEY_KASPECTUI, KEY_N }; +enum { KEY_K18, KEY_KTLS, KEY_KTLR, KEY_KGATE, KEY_N }; uint64_t resolve_import(module_t *m, const char *name); int hle_implements(const char *name); @@ -23,18 +22,13 @@ void gate_register(uint64_t ret_addr, const char *kind, int arg, uint64_t orig); static pthread_key_t key_value(unsigned i) { switch (i) { case KEY_K18: return g_key18; case KEY_KTLS: return g_keytls; case KEY_KTLR: return g_keytlr; - case KEY_KGATE: return g_keygate; case KEY_KRES0: return g_keyres[0]; case KEY_KRES1: return g_keyres[1]; - case KEY_KASPECT: return g_keyaspect; default: return g_keyaspectui; + default: return g_keygate; } } static void create_keys(void) { if (pthread_key_create(&g_key18, NULL) || pthread_key_create(&g_keytls, NULL) || pthread_key_create(&g_keytlr, NULL) || pthread_key_create(&g_keygate, NULL)) die("cannot create the host key"); - // Parameter slot keys are always created (not decided by whether the build output uses them: an iOS app has no params.tsv, so deciding by file would miss creating them and the key page would be written as 0 → the game reads TSD slot 0) - if (pthread_key_create(&g_keyres[0], NULL) || pthread_key_create(&g_keyres[1], NULL) || - pthread_key_create(&g_keyaspect, NULL) || pthread_key_create(&g_keyaspectui, NULL)) die("cannot create the parameter-bit key"); - LOG("key numbers K18=%lu KTLS=%lu KTLR=%lu KGATE=%lu KRES0=%lu KRES1=%lu KASPECT=%lu KASPECTUI=%lu", - g_key18, g_keytls, g_keytlr, g_keygate, g_keyres[0], g_keyres[1], g_keyaspect, g_keyaspectui); + LOG("key numbers K18=%lu KTLS=%lu KTLR=%lu KGATE=%lu", g_key18, g_keytls, g_keytlr, g_keygate); } // K slot check: the pthread TSD layout is fixed, slot = tpidrro_el0 + 8*key @@ -150,10 +144,10 @@ static void read_layout(module_t *m, const char *dir) { char path[1024]; snprintf(path, sizeof path, "%s/%s.fixups.tsv", dir, m->name); FILE *f = fopen(path, "r"); if (!f) die("open %s", path); - static const char *kn[8] = {"K18", "KTLS", "KTLR", "KGATE", "KRES0", "KRES1", "KASPECT", "KASPECTUI"}; + static const char *kn[KEY_N] = {"K18", "KTLS", "KTLR", "KGATE"}; char line[256], key[32]; unsigned long v; unsigned seen = 0; while (fgets(line, sizeof line, f)) { - if (sscanf(line, "# %31s %lx", key, &v) != 2) die("%s: the output comes from an old rewriter; replace it with output of the current product name (dm/PRODUCT)", path); + if (sscanf(line, "# %31s %lx", key, &v) != 2) die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path); if (!strcmp(key, "PATCH_OFF")) m->patch_off = strtoll(line + 2 + strlen(key), NULL, 16); // may be negative (patch area before the base) else if (!strcmp(key, "IMAGE_SIZE")) m->image_size = v; else if (!strcmp(key, "FILE_IMAGE_LEN")) m->file_image_len = v; @@ -163,9 +157,8 @@ static void read_layout(module_t *m, const char *dir) { else if (!strcmp(key, "RO_OFF")) m->ro_off = v; else if (!strcmp(key, "DATA_OFF")) m->data_off = v; else if (!strcmp(key, "FPS_STATE_OFF")) m->fps_state_off = v; - else if (!strcmp(key, "FPS_LEGACY_ELAPSED")) m->fps_legacy_elapsed = (int)v; else if (!strcmp(key, "BUILD_ID")) sscanf(line, "# %*s %32s", m->build_id); - else for (int k = 0; k < 8; k++) { + else for (int k = 0; k < KEY_N; k++) { size_t n = strlen(kn[k]); if (!strncmp(key, "KEY_", 4) && !strncmp(key + 4, kn[k], n) && !strcmp(key + 4 + n, "_IDX")) { m->key_idx[k] = (unsigned)strtoul(strrchr(line, '\t') + 1, 0, 10); seen |= 1u << k; @@ -173,44 +166,35 @@ static void read_layout(module_t *m, const char *dir) { } } fclose(f); - if (!m->patch_off || m->key_page_off % 0x4000 || m->key_page_size != 0x4000 || seen != 0xff) - die("%s: the output comes from an old rewriter; replace it with output of the current product name (dm/PRODUCT)", path); + if (!m->patch_off || m->key_page_off % 0x4000 || m->key_page_size != 0x4000 || seen != (1u << KEY_N) - 1) + die("%s: the output comes from an old rewriter; rewrite the modules again with dm/tools/patch_nso.py", path); if (m->patch_off < 0 && (!m->image_size || !m->file_image_len || m->file_image_len % 0x4000 || m->patch_off % 0x4000 || m->key_page_off + m->key_page_size > m->image_size)) die("%s: patch area before the base, but IMAGE_SIZE / FILE_IMAGE_LEN missing or wrong", path); } // Frame step state (the page after the key page, read-write; layout in patch_nso.py FRAME_STEP): the rewriter's stubs step the game by the host's whole vsyncs capped at this // target, 0 = the game's own interval logic. The profile's fps_target, overridden by DM_FPS_TARGET (0, 30 or 60); the graphics layer decides the steps and paces presents -// (hle_nvn.c fstep_tick). Mode +0x28: the profile's frame_step, overridden by DM_FRAME_STEP (ticks / legacy); legacy is each game's earlier method: Tears of the Kingdom's -// elapsed-time step, or for a build whose legacy is not one (FPS_LEGACY_ELAPSED 0, Breath of the Wild) target 0, so its own interval controller runs +// (hle_nvn.c fstep_tick) uint8_t *dm_fps_state; static void install_fps_state(module_t *m) { if (m->fps_state_off + 0x40 > m->size || m->fps_state_off < m->key_page_off + m->key_page_size) die("%s: frame step state page out of range", m->name); const char *e = getenv("DM_FPS_TARGET"); int t = e ? atoi(e) : DM_GAME_FPS_TARGET; if (t != 0 && t != 30 && t != 60) { LOG("[RED] DM_FPS_TARGET=%s: must be 0, 30 or 60; using the game's own frame step", e); t = 0; } - const char *fe = getenv("DM_FRAME_STEP"); int legacy = DM_GAME_FRAME_STEP_LEGACY; - if (fe) { if (!strcmp(fe, "legacy")) legacy = 1; else if (!strcmp(fe, "ticks")) legacy = 0; else LOG("[RED] DM_FRAME_STEP=%s: must be ticks or legacy; keeping the profile's frame_step", fe); } - if (legacy && !m->fps_legacy_elapsed) t = 0; float f = (float)t; dm_fps_state = m->base + m->fps_state_off; memcpy(dm_fps_state, &f, sizeof f); - uint32_t mode = (uint32_t)legacy; memcpy(dm_fps_state + 0x28, &mode, sizeof mode); // Render scale of the 3D views (the stubs at Tears of the Kingdom's main+0xd5cc0, Breath of the Wild's main+0x1266880): 1.0 keeps the game's own size and dynamic resolution; - // another factor fixes the size at that fraction (DM_RENDER_SCALE, 0.5 to 1.0). Independent of the frame step: written in legacy mode and at target 0 too + // another factor fixes the size at that fraction (DM_RENDER_SCALE, 0.5 to 1.0). Independent of the frame step: written at target 0 too const char *rs = getenv("DM_RENDER_SCALE"); float sc = 1.0f; if (rs) { char *end; sc = strtof(rs, &end); if (end == rs || *end || !(sc >= 0.5f && sc <= 1.0f)) { LOG("[RED] DM_RENDER_SCALE=%s: must be 0.5 to 1.0; keeping the game's size", rs); sc = 1.0f; } else LOG("%s: 3D views rendered at %.2f of the game's size (DM_RENDER_SCALE)", m->name, sc); } memcpy(dm_fps_state + 0x10, &sc, sizeof sc); char cap[32]; snprintf(cap, sizeof cap, ", capped at %d fps", t); - if (legacy && !m->fps_legacy_elapsed) LOG("%s: frame step as the game sets it (legacy: %s)", m->name, fe ? "DM_FRAME_STEP" : "game profile frame_step"); - else LOG("%s: frame step %s%s (%s; %s)", m->name, !t ? "as the game sets it" : legacy ? "by elapsed time (legacy)" : "in whole vsyncs decided by the host", t ? cap : "", - e ? "DM_FPS_TARGET" : "game profile fps_target", fe ? "DM_FRAME_STEP" : "game profile frame_step"); + LOG("%s: frame step %s%s (%s)", m->name, !t ? "as the game sets it" : "in whole vsyncs decided by the host", t ? cap : "", e ? "DM_FPS_TARGET" : "game profile fps_target"); } static void install_key_page(module_t *m) { if (m->key_page_off + m->key_page_size > m->size) die("%s: key page out of range", m->name); uint64_t *page = (uint64_t *)(m->base + m->key_page_off); memset(page, 0, m->key_page_size); - for (unsigned i = 0; i < 4; i++) page[m->key_idx[i]] = (uint64_t)key_value(i) * 8; - page[m->key_idx[KEY_KRES0]] = (uint64_t)g_keyres[0] * 8; page[m->key_idx[KEY_KRES1]] = (uint64_t)g_keyres[1] * 8; - page[m->key_idx[KEY_KASPECT]] = (uint64_t)g_keyaspect * 8; page[m->key_idx[KEY_KASPECTUI]] = (uint64_t)g_keyaspectui * 8; - for (int k = 0; k < 8; k++) if (!page[m->key_idx[k]]) die("%s: key page entry %d is 0 (host key not created) -- the game would read TSD slot 0", m->name, k); + for (unsigned i = 0; i < KEY_N; i++) page[m->key_idx[i]] = (uint64_t)key_value(i) * 8; + for (int k = 0; k < KEY_N; k++) if (!page[m->key_idx[k]]) die("%s: key page entry %d is 0 (host key not created) -- the game would read TSD slot 0", m->name, k); if (mprotect(m->base + m->key_page_off, m->key_page_size, PROT_READ)) die("%s key page mprotect", m->name); if (m->fps_state_off) install_fps_state(m); } @@ -346,24 +330,7 @@ static void remap_at(module_t *m, vm_address_t to, const void *from, unsigned lo if (kr != KERN_SUCCESS || a != to) die("%s vm_remap %p → %#lx (%#lx bytes) failed: %d", m->name, from, (unsigned long)to, size, kr); if ((cur & need) != need) die("%s permissions after vm_remap %#x, missing %#x", m->name, cur, need); } -#ifdef DM_SHELL -// Shell (the App built by ios/tools/shell_build.sh, containing no game content at all): the module images are not in the main executable but in the -// Frameworks/Guest.framework/Guest that users build themselves from their game files -- a dynamic library that holds only segments (written by dm/tools/guest_dylib.py), with the same section names as the embedded approach; below only the image header to fetch from is swapped. -#include <dlfcn.h> -#include <mach-o/dyld.h> -static const struct mach_header_64 *image_mh(void) { - static const struct mach_header_64 *mh; if (mh) return mh; - char exe[4096]; uint32_t n = sizeof exe; if (_NSGetExecutablePath(exe, &n)) die("cannot get the executable path"); - char *sl = strrchr(exe, '/'); if (!sl || (size_t)(sl - exe) + 40 > sizeof exe) die("unexpected executable path: %s", exe); strcpy(sl, "/Frameworks/Guest.framework/Guest"); - if (!dlopen(exe, RTLD_NOW)) die("this install package has no game program yet (%s): first put your own game files into the package with dm/tools/d2rm_pack.py, then install it. The system says: %s", exe, dlerror()); - for (uint32_t i = 0; i < _dyld_image_count(); i++) if (strstr(_dyld_get_image_name(i), "/Guest.framework/Guest")) mh = (const struct mach_header_64 *)_dyld_get_image_header(i); - if (!mh) die("Guest dynamic library is loaded but its image header cannot be found"); - fprintf(stderr, "boot: module image taken from Guest dynamic library %s\n", exe); return mh; -} -#define IMAGE_MH image_mh() -#else #define IMAGE_MH (&_mh_execute_header) -#endif static void map_image(module_t *m, const char *dir) { (void)dir; char sx0[17], sx1[17], dm[17]; snprintf(sx0, sizeof sx0, "__dm%sx0", m->name); snprintf(sx1, sizeof sx1, "__dm%sx1", m->name); snprintf(dm, sizeof dm, "__DM%sW", m->name); diff --git a/dm/host/main.c b/dm/host/main.c index 543ea1b..5fc9f21 100644 --- a/dm/host/main.c +++ b/dm/host/main.c @@ -6,18 +6,15 @@ #include <sys/sysctl.h> #include <sys/time.h> #include <unistd.h> -void probe_install(void); void ticks_install(void); void hid_keys_init(void); void tact_prefetch_start(const char *outdir); void tact_chk_init(void); void stubs_report(void); void crash_install(void); void guest_thread_init(void); uint32_t svc_thread_main_init(void); +void probe_install(void); void hid_keys_init(void); void stubs_report(void); void crash_install(void); void guest_thread_init(void); uint32_t svc_thread_main_init(void); typedef void (*fn0)(void); typedef void (*fn2)(uint64_t, uint64_t); typedef void (*fnp)(void *); static void *req(const char *n){ void *p=guest_sym(n); if(!p) die("missing symbol %s", n); return p; } // Game arguments: argv[0] is fixed to "game", and whatever follows "--" is appended as is (on a real device nn::init takes them from the process argument area and hands them to the SDK through SetHostArgc/Argv) static char *g_argv[64] = {"game"}; static int g_argc = 1; -void crash_sample_self(void); static void guest_main(void) { guest_thread_init(); - { void inflate_bench_maybe(void); inflate_bench_maybe(); } // DM_INFLATE_BENCH test mode (for diagnostics, exits when done) - crash_sample_self(); // DM_SAMPLE sampling (for diagnostics) uint32_t hmain = svc_thread_main_init(); // Main thread handle: x0 of nnosInitialize (stored in an sdk global, used by ThreadManagerImplByHorizon); x1 = pointer to the argument area // Copy Start step by step: _init_libc1 = __nnmusl_init_locks (initializes the stdio etc. locks as recursive locks; skipping it self-deadlocks on nested locking in putc) + __init_ssp. // The three callbacks passed in by rtld: A only sets an rtld flag (skipped), B = each module's DT_INIT (before nnMain), C = destructors (not done) @@ -31,11 +28,8 @@ static void guest_main(void) { ((fnp)req("_ZN2nn2os11SetHostArgvEPPc"))(g_argv); LOG(">>> nninitStartup"); ((fn0)req("nninitStartup"))(); ((fn0)req("_init_libc2"))(); - dm_prof_mark("② SDK init (nnosInitialize → nninitStartup)"); run_module_inits(); // rtld callback B: each module's DT_INIT (global constructors) - dm_prof_mark("③ module init_arrays"); { void dm_guest_spawn_selftest(void); dm_guest_spawn_selftest(); } // DM_GUEST_SPAWN_TEST self-test of host-created guest threads (hle_guestthread.c) - { void dm_padmode_boot(void); dm_padmode_boot(); } // Touch overlay present: repoint the layout prefixes to the touch version (dm_padmode.c); must happen before the game builds its panels { void dm_oodle_job_maybe(void); dm_oodle_job_maybe(); } // DM_OODLE_JOB: decode a job file with the game's own Oodle and exit (hle_oodle.c, for the offline shader tools) LOG(">>> nnMain"); ((fn0)req("nnMain"))(); LOG("nnMain returned"); @@ -66,7 +60,6 @@ static void *keyrace_thread(void *unused); static void host_preload(int argc, char **argv) { g_t0_ns = proc_start_mono_ns(); flags_init(); - dm_prof_mark("⓪ process creation → host main (dyld)"); if (argc < 2) die("usage: dmhost <dir> [mod...] [-- game arguments...]"); // Time zone: the game converts absolute time to local time along two paths, both ending in the host libc -- nn::time::ToCalendarTime (hle_time.c, localtime_r) and the guest libc's // localtime_r / mktime (hle_table.c wires them straight to the host). So only when the host follows the system time zone do the original game's screens (chronicle discovery time etc.) show this machine's time. @@ -101,16 +94,10 @@ static void host_preload(int argc, char **argv) { load_modules(argv[1], names, n); uint64_t ta = now_ns(); probe_install(); // DM_PROBE probes (for diagnostics) - ticks_install(); // DM_TICKS per-tick state hash (tick-entry observation gate) - { void target_rank_install(void); target_rank_install(); } // Touch version auto-targeting Boss > elite > normal (hle_target.c) - hid_keys_init(); // DM_KEYS scripted input (steps driven by UI messages must subscribe before the message appears) + hid_keys_init(); // DM_KEYS scripted input uint64_t tb = now_ns(); - tact_chk_init(); // DM_TACTCHK per-block log (logging stops at character select) - tact_prefetch_start(argv[1]); // Background pre-decode of boot blocks (on by default: bootlist.tsv in the build output directory; DM_TACT_PREFETCH=0 turns it off) - uint64_t tc = now_ns(); stubs_report(); - if (g_flag.bootprof) LOG("① breakdown (after loading): probes / tick gates / keys %.1f ms, prefetch start (reading the list) %.1f ms, stub report %.1f ms", (tb - ta) / 1e6, (tc - tb) / 1e6, (now_ns() - tc) / 1e6); - dm_prof_mark("① host loading (read NSO / relocate / forced takeover)"); + if (g_flag.bootprof) LOG("① breakdown (after loading): probes / keys %.1f ms, stub report %.1f ms", (tb - ta) / 1e6, (now_ns() - tb) / 1e6); { void loader_hash_log(void); loader_hash_log(); } // DM_LOADHASH=1: address-independent hash of the load result (loader.c) { void dm_ue_malloc_install(void); dm_ue_malloc_install(); } // Unreal's allocator on a host malloc zone, for the builds whose constructor call is cut (hle_uemalloc.c) } diff --git a/dm/host/probe.c b/dm/host/probe.c index d1ce753..6e1e42b 100644 --- a/dm/host/probe.c +++ b/dm/host/probe.c @@ -1,4 +1,4 @@ -// Probes DM_PROBE (taken from d2r-mobile dm/host/diag.c, keeping only the probe itself): when the debugger needs a system authorization prompt on the Mac, use this to print registers and a backtrace at a guest address. +// Probes DM_PROBE: when the debugger needs a system authorization prompt on the Mac, use this to print registers and a backtrace at a guest address. #include "dm.h" #include <TargetConditionals.h> #include <signal.h> diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index b67d533..ec87eb2 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -1,109 +1,49 @@ -DM_ROOT ?= $(HOME)/dm-work -# dm/host unit tests in one command: make -C dm/host/tests run (takes seconds; build output goes to build/ and is not committed) -PY ?= $(DM_ROOT)/build/cache/dm/venv/bin/python -EXEFS ?= $(DM_ROOT)/data/switch/3.3.94012/Update/ExeFS +# dm/host unit tests in one command: make -C dm/host/tests GAME=<base game title ID> run (takes seconds; build output goes to build/ and is not committed) B = build # game profile header (dm.h needs game_profile.h, generated by the host Makefile into ../gen/<profile>/) # tests that use the game profile need GAME=<base game title ID> (there is no default game); GINC only expands when used, so tests without the profile build as before GINC = $(if $(strip $(GAME)),-I../gen/$(GAME) -I../../games/$(GAME),$(error GAME is not set: make GAME=<base game title ID>: $(shell python3 ../../games/game.py list | awk -F'\t' '{printf "%s (%s) ", $$1, $$3}'))) -run: $(B)/t_textbox $(B)/t_padmap $(B)/t_touch $(B)/t_hid_receipt $(B)/t_hid_script $(B)/t_touch_mac $(B)/t_oe $(B)/t_fs $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_romfs_validate $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_movie $(B)/t_condvar $(B)/t_condvar_old $(B)/t_target_rank $(B)/t_hud_dead $(B)/t_namegen $(B)/t_modetx $(B)/t_unit_level $(B)/t_widget $(B)/t_doorspot $(B)/t_cloudsync $(B)/t_i18n $(B)/main.orig.bin $(B)/subsdk0.orig.bin - python3 native_write_scan.py --selftest .. - python3 mode_scan.py ../.. - python3 ../../ios/tools/dm_i18n.py --check - python3 ../../i18n/scan_literals.py - $(B)/t_textbox +GAME_H = ../gen/$(GAME)/game_profile.h +run: $(B)/t_touch $(B)/t_oe $(B)/t_fs $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old $(B)/t_touch - $(B)/t_hid_script 2>/dev/null - $(B)/t_hid_receipt 2>/dev/null - $(B)/t_touch_mac $(B)/t_oe 2>/dev/null $(B)/t_fs 2>/dev/null $(B)/t_thread_leak TMPDIR="$(abspath $(B))" $(B)/t_fs_case 2>/dev/null - DM_ROMFS="$(DM_ROOT)/data/switch/3.3.94012/Update/RomFS" $(B)/t_romfs_validate $(B)/t_audio $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack - $(B)/t_movie $(B)/t_condvar $(B)/t_condvar_old - $(B)/t_target_rank 2>/dev/null - $(B)/t_hud_dead - $(B)/t_namegen - $(B)/t_modetx - $(B)/t_padmap - $(B)/t_unit_level - $(B)/t_widget - $(B)/t_doorspot - TMPDIR="$(abspath $(B))" $(B)/t_cloudsync 2>/dev/null - for m in en zh none sysen systw tags game; do TMPDIR="$(abspath $(B))" $(B)/t_i18n $$m || exit 1; done - $(PY) movie_cut.py $(B)/main.orig.bin $(B)/subsdk0.orig.bin - $(PY) oe_notify.py $(B)/main.orig.bin -$(B)/t_i18n: t_i18n.c ../dm_i18n.c ../dm_i18n.h ../dm_i18n_tab.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_i18n.c ../dm_i18n.c -$(B)/t_hud_dead: t_hud_dead.c ../dm_hud.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_hud_dead.c -$(B)/t_textbox: t_textbox.c ../dm_textbox_core.c ../dm_textbox_core.h | $(B) - clang -O1 -Wall -Werror -o $@ t_textbox.c ../dm_textbox_core.c -$(B)/t_doorspot: t_doorspot.c ../dm_doorspot.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_doorspot.c -$(B)/t_widget: t_widget.c ../dm_widget.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_widget.c -$(B)/t_unit_level: t_unit_level.c ../dm_unit.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_unit_level.c -$(B)/t_padmap: t_padmap.c ../dm_padmap.h ../dm_input.h | $(B) - clang -O1 -Wall -Werror -o $@ t_padmap.c -$(B)/t_modetx: t_modetx.c ../dm_modetx.h | $(B) - clang -O1 -Wall -Werror -o $@ t_modetx.c -$(B)/t_namegen: t_namegen.c ../dm_namegen.c | $(B) - clang -O1 -Wall -Werror -o $@ t_namegen.c ../dm_namegen.c -$(B)/t_cloudsync: t_cloudsync.c ../dm_cloudsync.c ../dm.h | $(B) - clang -O1 -Wall -Werror -I.. -o $@ t_cloudsync.c ../dm_cloudsync.c -$(B)/t_touch: t_touch.c ../hle_touch.c ../front.h ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_touch.c ../hle_touch.c -$(B)/t_thread_leak: t_thread_leak.c ../thread.c ../dm.h | $(B) +$(GAME_H): + $(MAKE) -C .. game_profile GAME=$(GAME) +$(B)/t_touch: t_touch.c ../hle_touch.c ../front.h ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_touch.c ../hle_touch.c +$(B)/t_thread_leak: t_thread_leak.c ../thread.c ../dm.h $(GAME_H) | $(B) clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_thread_leak.c -$(B)/t_fs: t_fs.c t_fs_stub.c ../hle_fs.c ../hle.h ../dm.h ../dm_i18n.c | $(B) - clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs.c t_fs_stub.c ../dm_i18n.c -$(B)/t_audio: t_audio.c ../hle_audio.c ../hle.h ../dm.h | $(B) +$(B)/t_fs: t_fs.c t_fs_stub.c ../hle_fs.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs.c t_fs_stub.c ../hle_bigstack.c +$(B)/t_fs_case: t_fs_case.c ../hle_fs.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs_case.c ../hle_bigstack.c +$(B)/t_audio: t_audio.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B) clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_audio.c -framework AudioToolbox -$(B)/t_audio_spsc: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h | $(B) +$(B)/t_audio_spsc: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B) clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_audio_spsc.c -framework AudioToolbox -$(B)/t_audio_spsc_tsan: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h | $(B) - clang -O1 -g -O1 -fsanitize=thread -Wno-unused-function -I.. -o $@ t_audio_spsc.c -framework AudioToolbox -$(B)/t_swkbd: t_swkbd.c ../hle_swkbd.c ../hle.h ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_swkbd.c ../hle_swkbd.c -$(B)/t_movie: t_movie.c ../hle_movie.c ../hle.h ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_movie.c ../hle_movie.c -$(B)/t_altstack: t_altstack.c t_altstack_stub.c ../crash.c ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_altstack.c t_altstack_stub.c ../crash.c -$(B)/%.orig.bin: $(EXEFS)/% | $(B) - $(PY) ../../tools/nso.py $< $@ > /dev/null +$(B)/t_audio_spsc_tsan: t_audio_spsc.c ../hle_audio.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -g -O1 -fsanitize=thread -Wno-unused-function -I.. $(GINC) -o $@ t_audio_spsc.c -framework AudioToolbox +$(B)/t_swkbd: t_swkbd.c ../hle_swkbd.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_swkbd.c ../hle_swkbd.c +$(B)/t_altstack: t_altstack.c t_altstack_stub.c ../crash.c ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_altstack.c t_altstack_stub.c ../crash.c +$(B)/t_oe: t_oe.c ../hle_oe.c ../hle.h ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_oe.c +$(B)/t_condvar: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -I.. $(GINC) -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c +$(B)/t_condvar_old: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h $(GAME_H) | $(B) + clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -DDM_TEST_CV_OLD_ORDER -I.. $(GINC) -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c $(B): mkdir -p $@ .PHONY: run tsan tsan: $(B)/t_audio_spsc_tsan $(B)/t_audio_spsc_tsan -$(B)/t_condvar: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h | $(B) - clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -I.. -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c -$(B)/t_condvar_old: t_condvar.c t_condvar_stub.c ../hle_thread.c ../dm.h | $(B) - clang -O1 -Wno-unused-function -DDM_TEST_HOOKS -DDM_TEST_CV_OLD_ORDER -I.. -o $@ t_condvar.c t_condvar_stub.c ../hle_thread.c - -$(B)/t_fs_case: t_fs_case.c ../hle_fs.c ../hle.h ../dm.h ../dm_i18n.c | $(B) - clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_fs_case.c ../dm_i18n.c -$(B)/t_romfs_validate: t_romfs_validate.c ../romfs_validate.c ../romfs_validate.h ../dm_i18n.c ../dm_i18n_tab.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_romfs_validate.c ../romfs_validate.c ../dm_i18n.c - -$(B)/t_hid_script: t_hid_script.c t_touch_tick_stub.c ../hle_hid.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../touch_mac.c ../../ios/Sources/dm_touch_controls.c ../hle.h ../dm.h ../dm_input.h ../front.h | $(B) - clang -O1 -Wno-unused-function -I.. -I../../ios/Sources -o $@ t_hid_script.c t_touch_tick_stub.c ../dm_cursor.c ../dm_hud.c ../touch_mac.c ../../ios/Sources/dm_touch_controls.c -lm - -$(B)/t_oe: t_oe.c ../hle_oe.c ../hle.h ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_oe.c - -$(B)/t_hid_receipt: t_hid_receipt.c t_touch_tick_stub.c ../hle_hid.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../touch_mac.c ../../ios/Sources/dm_touch_controls.c ../hle.h ../dm.h ../dm_input.h ../front.h | $(B) - clang -O1 -Wno-unused-function -I.. -I../../ios/Sources -o $@ t_hid_receipt.c t_touch_tick_stub.c ../dm_cursor.c ../dm_hud.c ../touch_mac.c ../../ios/Sources/dm_touch_controls.c -lm -$(B)/t_touch_mac: t_touch_mac.c ../touch_mac.c ../dm_cursor.c ../dm_hud.c ../dm_hud.h ../../ios/Sources/dm_touch_controls.c ../../ios/Sources/dm_touch_controls.h ../../host/dm_input.h | $(B) # the test #includes touch_mac.c and dm_touch_controls.c, which must be listed as dependencies (omitting them means source changes do not trigger a rebuild) - clang -std=c11 -Wall -Wextra -Werror -I.. -I../../ios/Sources -o $@ t_touch_mac.c ../dm_cursor.c ../dm_hud.c -lm -$(B)/t_target_rank: t_target_rank.c ../hle_target.c ../dm.h | $(B) - clang -O1 -Wno-unused-function -I.. -o $@ t_target_rank.c ../hle_target.c diff --git a/dm/host/tests/client_sta_gate.py b/dm/host/tests/client_sta_gate.py deleted file mode 100644 index 5c629ed..0000000 --- a/dm/host/tests/client_sta_gate.py +++ /dev/null @@ -1,20 +0,0 @@ -# Executes only the client STA gate/rule trampoline after the original GetStat; the end uses an explicit STOP stand-in, and neither the original getter nor the real Move speed is verified. -import sys,struct -from pathlib import Path -from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE -from unicorn.arm64_const import * -b=Path(sys.argv[1]).read_bytes();u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x10000) -UNIT=0x7001000;ctx={} -def hook(u,a,n,d): - if a in [0x4ac36c,0x4ac3f4]:ctx['target']=a;u.emu_stop() -u.hook_add(UC_HOOK_CODE,hook);bad=0;n=0 -for kind in [0,1,2,3,4,5,6,99]: - for sta in [0,1,22016,0xffffffff]: - ctx.clear();u.mem_write(UNIT,struct.pack('<I',kind));u.reg_write(UC_ARM64_REG_X19,UNIT);u.reg_write(UC_ARM64_REG_W0,sta);u.reg_write(UC_ARM64_REG_SP,0x700f000);u.reg_write(UC_ARM64_REG_NZCV,0xa0000000) - u.emu_start(0x4ac368,0x4000000,count=100) - target=0x4ac3f4 if kind==0 or sta else 0x4ac36c - ok=ctx.get('target')==target and u.reg_read(UC_ARM64_REG_W0)==sta and u.reg_read(UC_ARM64_REG_X19)==UNIT and u.reg_read(UC_ARM64_REG_SP)==0x700f000 and u.reg_read(UC_ARM64_REG_NZCV)==0xa0000000 - n+=1 - if not ok:bad+=1;print('FAIL type',kind,'STA',sta,'expected',hex(target),'→ got',ctx) -print(f'CLIENT_STA_GATE {n} cases, {bad} failed; only type0 skips the STA auto Stop, non-player keeps the original zero / non-zero branch, original W0/X19/SP/NZCV unchanged; the end is a stand-in') -sys.exit(bool(bad)) diff --git a/dm/host/tests/mode_scan.py b/dm/host/tests/mode_scan.py deleted file mode 100644 index f9275a2..0000000 --- a/dm/host/tests/mode_scan.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""Registration gate for controller / touch modes (run in the host unit tests, takes seconds). Reads source only, does not run the game. -Three rules: -1. Every pump on the long per-frame line in dm_cursor.c either carries "if (tm) " (touch-only: not run in controller mode or while a switch is in progress) or is on the list in the comment above it headed "Runs in both modes:"; - every *_pump defined in the file must also appear on this line or on that list -- a newly added pump registered in neither fails. -2. The number of places that check the mode ad hoc (dm_padmode_current()) and that press keys / send messages on the player's behalf (fe_key / ui_auto_key / fe_send / dm_ui_key_pulse) is recorded per file in the two tables below, and may only decrease, never increase. - To add one: first think through whether in the other mode it should stop, carry on as usual, or work differently, register it in dm_mode_items.h (for things to be cleaned up on a switch) or on the list above, then change the number here and state the reason in the commit message. -3. Every enter / leave function registered in dm_mode_items.h must have a definition. -Usage: mode_scan.py <dm directory> -""" -import re, sys, os, glob -root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), '..', '..') -red = [] -def src(rel): return open(os.path.join(root, rel), encoding='utf-8').read() -def strip_comments(s): return re.sub(r'//[^\n]*', '', re.sub(r'/\*.*?\*/', '', s, flags=re.S)) - -# ---- 1 ---- -cur = src('host/dm_cursor.c') -m = re.search(r'^.*int tm = ui_touch_mode\(\);.*$', cur, re.M) -if not m: red.append('cannot find the per-frame line in dm_cursor.c (int tm = ui_touch_mode();)') -else: - line = m.group(0) - both_txt = re.search(r'Runs in both modes:(.*?)\nstatic int ui_touch_mode', cur, re.S) - both = set(re.findall(r'\b(\w+_pump|ui_map_center|ui_panel_heal)\b', both_txt.group(1))) if both_txt else set() - if not both: red.append('cannot find the "Runs in both modes:" list in dm_cursor.c') - on_line = {} - for mm in re.finditer(r'(if \(tm\) )?\b(\w+_pump|ui_map_center)\(', line): on_line[mm.group(2)] = bool(mm.group(1)) - for name, gated in sorted(on_line.items()): - if not gated and name not in both: red.append('%s on the per-frame line neither carries if (tm) nor is on the "Runs in both modes" list' % name) - if gated and name in both: red.append('%s carries if (tm) and is also on the "Runs in both modes" list; pick one' % name) - for name in sorted(set(re.findall(r'^static void (\w+_pump)\(', cur, re.M))): - if name not in on_line and name not in both: red.append('dm_cursor.c defines %s, but it is neither on the per-frame line nor on the "Runs in both modes" list' % name) - -# ---- 2 ---- -PADMODE = {'host/dm_cursor.c': 8, 'host/dm_custom.c': 2, 'host/dm_kills.c': 2, 'host/dm_rift_ui.c': 4, 'host/dm_runeword.c': 3, 'host/dm_vault_ui.c': 1, 'host/dm_update.c': 2, 'host/hle_tact.c': 1} -AUTOKEY = {'host/dm_cursor.c': 38, 'host/dm_rift_ui.c': 6, 'host/hle_hid.c': 2, 'ios/Sources/front_ios.m': 4} -def count(pat, skip_defs): - out = {} - for f in sorted(glob.glob(os.path.join(root, 'host', '*.c')) + glob.glob(os.path.join(root, 'ios', 'Sources', '*.m')) + glob.glob(os.path.join(root, 'ios', 'Sources', '*.c'))): - rel = os.path.relpath(f, root); s = strip_comments(open(f, encoding='utf-8').read()); n = 0 - for mm in re.finditer(pat, s): - ls = s.rfind('\n', 0, mm.start()) + 1; head = s[ls:mm.start()] - if skip_defs and re.search(r'(^|\s)(static\s+)?(void|int|uint64_t)\s+$', head): continue # A function definition / declaration itself does not count as a call - n += 1 - if n: out[rel] = n - return out -for label, table, got in (('ad-hoc mode check dm_padmode_current()', PADMODE, count(r'\bdm_padmode_current\(\)', True)), - ('key presses / messages for the player', AUTOKEY, count(r'\b(fe_key|ui_auto_key|fe_send|dm_ui_key_pulse|dm_ui_key_pulse_alone|dm_ui_send|dm_ui_send_queue)\(', True))): - if label.startswith('ad-hoc') and 'host/dm_padmode.c' in got: got.pop('host/dm_padmode.c') - for rel in sorted(set(got) | set(table)): - g, t = got.get(rel, 0), table.get(rel, 0) - if g > t: red.append('%s: %s has %d now, the table records %d -- register the new ones first (see the top of this file), then change the table' % (label, rel, g, t)) - elif g < t: red.append('%s: %s has %d now, fewer than the %d in the table -- lower the number in the table (it may only go down)' % (label, rel, g, t)) - -# ---- 3 ---- -items = src('host/dm_mode_items.h'); allsrc = ''.join(open(f, encoding='utf-8').read() for f in glob.glob(os.path.join(root, 'host', '*.c'))) -for name, side, enter, leave in re.findall(r'X\("([^"]+)",\s*(\d),\s*(\w+),\s*(\w+)\)', items): - for fn in (enter, leave): - if not re.search(r'\bvoid %s\(void\)\s*\{' % fn, allsrc): red.append('dm_mode_items.h entry "%s": function %s is not defined' % (name, fn)) - -if red: - print('mode registration gate [RED]: %d' % len(red)); [print(' ' + r) for r in red]; sys.exit(1) -print('✓ mode registration gate: all %d per-frame pumps are classified, ad-hoc mode checks and key presses for the player are within the registered counts, all entry functions are defined' % (len(on_line) if m else 0)) diff --git a/dm/host/tests/move_codec.py b/dm/host/tests/move_codec.py deleted file mode 100644 index 22be30a..0000000 --- a/dm/host/tests/move_codec.py +++ /dev/null @@ -1,45 +0,0 @@ -# Executes the original ARM codec; only libm calls use explicit f32 stand-ins. Not a criterion for speed or for the game stopping. -import math, struct, hashlib, sys -from pathlib import Path -from unicorn import Uc, UC_ARCH_ARM64, UC_MODE_ARM, UC_HOOK_CODE -from unicorn.arm64_const import * -P=Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).with_name('build')/'main.orig.bin' -b=P.read_bytes(); u=Uc(UC_ARCH_ARM64,UC_MODE_ARM) -u.mem_map(0,0x4000000); u.mem_write(0,b) -u.mem_map(0x7000000,0x20000) -STOP,VEC,OUT,AMP,FLAG=0x7000000,0x7001000,0x7002000,0x7002010,0x7002020 -bits=lambda v:struct.unpack('<I',struct.pack('<f',v))[0] -flt=lambda v:struct.unpack('<f',struct.pack('<I',v))[0] -def hook(u,a,n,d): - if a==STOP: u.emu_stop(); return - fn={0x1b77270:lambda:math.atan2(flt(u.reg_read(UC_ARM64_REG_S0)),flt(u.reg_read(UC_ARM64_REG_S1))), - 0x1b77220:lambda:math.fmod(flt(u.reg_read(UC_ARM64_REG_S0)),flt(u.reg_read(UC_ARM64_REG_S1))), - 0x1b77130:lambda:math.cos(flt(u.reg_read(UC_ARM64_REG_S0))), - 0x1b77120:lambda:math.sin(flt(u.reg_read(UC_ARM64_REG_S0)))}.get(a) - if fn: - u.reg_write(UC_ARM64_REG_S0,bits(fn())); u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR)) -u.hook_add(UC_HOOK_CODE,hook) -def call(pc): - u.reg_write(UC_ARM64_REG_SP,0x701fff0);u.reg_write(UC_ARM64_REG_LR,STOP) - u.emu_start(pc,STOP+4,count=3000) - assert u.reg_read(UC_ARM64_REG_PC)==STOP -n=0 -# Expectations hard-coded first: four-axis direction 10bit=512/768/0/256; magnitude 5bit=0/0/0/7/15/31/31. -for dx,dy,direction in [(1,0,512),(0,1,768),(-1,0,0),(0,-1,256)]: - packets=[] - for amp,mag in [(-1,0),(0,0),(1/64,0),(.25,7),(.5,15),(1,31),(2,31)]: - pair=[] - for run in [0,1]: - u.mem_write(VEC,struct.pack('<ff',dx,dy));u.reg_write(UC_ARM64_REG_X0,VEC);u.reg_write(UC_ARM64_REG_X1,run);u.reg_write(UC_ARM64_REG_S0,bits(amp));call(0x4cd4b0) - packet=u.reg_read(UC_ARM64_REG_W0);want=direction|(mag<<10)|(run<<15) - assert packet==want,('encode',dx,dy,amp,run,hex(packet),hex(want)) - pair.append(packet) - u.mem_write(OUT,b'\xff'*0x30);u.reg_write(UC_ARM64_REG_X0,packet);u.reg_write(UC_ARM64_REG_X1,OUT);u.reg_write(UC_ARM64_REG_X2,AMP);u.reg_write(UC_ARM64_REG_X3,FLAG);call(0x4cd5f0) - x,y=struct.unpack('<ff',u.mem_read(OUT,8));a=struct.unpack('<f',u.mem_read(AMP,4))[0];f=u.mem_read(FLAG,1)[0] - assert abs(x-dx)<1e-6 and abs(y-dy)<1e-6,('direction',dx,dy,x,y) - assert abs(a-mag/31)<1e-7 and f==run,('decode',amp,mag,a,f,run) - n+=1 - assert pair[0]^pair[1]==0x8000 - print(f'axis ({dx},{dy}) direction code {direction}: 7 magnitudes × 2 walk/run bits, encode and decode all match') -print('original image SHA256',hashlib.sha256(b).hexdigest()) -print(f'ORIGINAL_MOVE_CODEC OK {n}/{n}; the 28 pairs differ only in bit15, zero/tiny magnitudes quantize to 0; speed / stick release / real movement not tested') diff --git a/dm/host/tests/movie_cut.py b/dm/host/tests/movie_cut.py deleted file mode 100644 index 3a5c9cc..0000000 --- a/dm/host/tests/movie_cut.py +++ /dev/null @@ -1,89 +0,0 @@ -# Static check of cutscene no-op playback (the game-side protocol in the header comment of hle_movie.c): check the key instructions and vtable slot names one by one on the original image; any mismatch is a failure. -# Usage: movie_cut.py <main.orig.bin> <subsdk0.orig.bin> (.orig.bin is generated from ExeFS by dm/tools/nso.py) -import struct, sys -from capstone import * -main, sub = open(sys.argv[1], 'rb').read(), open(sys.argv[2], 'rb').read() -md = Cs(CS_ARCH_ARM64, CS_MODE_ARM) -def ins(a): i = next(md.disasm(main[a:a + 4], a)); return f'{i.mnemonic} {i.op_str}' - -# (offset, expected disassembly, meaning of this instruction in the chain) -- the game-side protocol that cutscene "no-op playback for the real duration" (plays nothing but lasts the real duration) depends on (header comment of hle_movie.c) -CHAIN = [ - (0x898ed0, 'mov w1, #0x10', 'Extractor object 0x10 bytes: the cut stores its state pointer in the second word'), - (0x899960, 'mov w1, #0x10', 'Decoder object 0x10 bytes: same as above'), - (0x899780, 'ldr x8, [x8, #0x20]', 'Setup calls Extractor slot 4 GetTrackCount'), - (0x89979c, 'b.lt #0x899928', 'track count < 1 skips track selection (the cut reports 0 tracks)'), - (0x898f70, 'stp x24, x23, [x19, #0x1b8]', 'DecoderEvents[0..1] = {P+0x18, P+0x40}'), - (0x899044, 'mov w8, #1', 'wait item kind at P+0x18 is 1 = input available'), - (0x8990b8, 'mov w8, #2', 'wait item kind at P+0x40 is 2 = output available'), - (0x8999e4, 'ldr x8, [x8, #0x28]', 'Setup calls Decoder slot 5 Start'), - (0x899f54, 'cmp w21, #0', 'Open returns (Start status == 0): Start in the cut succeeds ⇒ playing'), - (0x899f5c, 'cset w0, eq', ''), - (0xbbfe40, 'bl #0x898230', 'the per-frame update calls the player update'), - (0x89832c, 'ldrb w8, [x8, #0x489]', 'with the frame queue empty returns !P[+0x489] (finished flag) …'), - (0x898334, 'cset w0, eq', '… not finished counts as playing'), - (0x8992e0, 'cmp w21, #1', 'decoder thread: kind 1 takes the input path'), - (0x8992e8, 'cmp w21, #2', 'decoder thread: kind 2 takes the output path'), - (0x899508, 'ldr x8, [x8, #0x48]', 'input: Decoder slot 9 AcquireInputBufferIndex'), - (0x899534, 'ldr x8, [x8, #0x50]', 'input: Decoder slot 10 GetInputBuffer'), - (0x8995a8, 'ldr x8, [x8, #0x50]', 'input: Extractor slot 10 Read'), - (0x8995b8, 'cmp w23, #2, lsl #12', 'Read returning 0x2000 = end of stream …'), - (0x8995c0, 'ldrb w8, [x19, #0x12]', '… loop flag …'), - (0x8995d4, 'ldr x8, [x8, #0x40]', '… if looping, Extractor slot 8 SeekTo (the cut re-arms its timer)'), - (0x899620, 'mov w26, #4', '… otherwise sends flag 4 (end of stream)'), - (0x899634, 'ldr x8, [x8, #0x60]', 'input: Extractor slot 12 GetSampleTime'), - (0x899670, 'ldr x8, [x8, #0x58]', 'input: Decoder slot 11 SendInputBufferForDecode'), - (0x89968c, 'ldr x8, [x8, #0x58]', 'input: Extractor slot 11 Advance'), - (0x89932c, 'ldr x8, [x8, #0x60]', 'output: Decoder slot 12 AcquireOutputBufferIndex'), - (0x899338, 'tst w0, #0xffffdfff', 'only status 0 / 0x2000 enters the frame queue (the cut returns 0x1008 with no output)'), - (0x899cb4, 'cmp x9, #2', 'frames are dropped only with a queue ≥2 (a single end-of-stream item is never dropped)'), - (0x89a650, 'ldr x8, [x8, #0x90]', 'frame fetch: Decoder slot 18 GetOutputBufferFormat (the cut returns non-zero)'), - (0x89a65c, 'cmp w27, #4', 'flag 4 …'), - (0x89a670, 'strb w8, [x25, #0x489]', '… sets finished ⇒ the game takes the normal finished path'), - (0x899fdc, 'ldr x8, [x8, #0x30]', 'teardown calls Decoder slot 6 Stop (the cut cancels its timer)'), - (0x899ff0, 'ldr x8, [x8, #8]', 'teardown calls Decoder slot 1 deleting destructor'), - (0x89a004, 'ldr x8, [x8, #8]', 'teardown calls Extractor slot 1 deleting destructor'), -] -bad = 0 -for a, want, why in CHAIN: - got = ins(a); ok = got == want; bad += not ok - print(f'{"✓" if ok else "✗"} {a:#x} {got:28s} {"" if ok else "expected " + want} {why}') - -# Vtable slot names (subsdk0 dynamic symbols + RELATIVE relocations): the cut vtable is ordered by these slot numbers -m0 = struct.unpack_from('<I', sub, 4)[0]; dyn = m0 + struct.unpack_from('<i', sub, m0 + 4)[0] -D = {}; o = dyn -while True: - t, v = struct.unpack_from('<qQ', sub, o); o += 16 - if t == 0: break - D.setdefault(t, v) -rel = {} -for k in range(D[8] // 24): - off, info, add = struct.unpack_from('<QQq', sub, D[7] + 24 * k) - rel[off] = add if info & 0xffffffff == 1027 else ('sym', info >> 32) # RELATIVE gives an address; ABS64 / GLOB_DAT give a symbol number -names, sym, symn = {}, {}, {} -for k in range(1, (D[5] - D[6]) // 24): - nm, info, oth, shn, val, sz = struct.unpack_from('<IBBHQQ', sub, D[6] + 24 * k) - s = sub[D[5] + nm:sub.index(b'\0', D[5] + nm)].decode(); symn[k] = s - if shn and val: names.setdefault(val, s); sym[s] = val -def slot_name(r): return symn.get(r[1], '?') if isinstance(r, tuple) else names.get(r, '?') -VT = [('_ZTVN5movie9ExtractorE', 4, '_ZN5movie9Extractor13GetTrackCountEPi'), - ('_ZTVN5movie9ExtractorE', 15, '_ZN5movie9Extractor13SetDataSourceEPNS_18ClientStreamReaderEPKc'), - ('_ZTVN5movie7DecoderE', 2, '_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE'), - ('_ZTVN5movie7DecoderE', 3, '_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE'), - ('_ZTVN5movie7DecoderE', 5, '_ZN5movie7Decoder5StartEv'), - ('_ZTVN5movie7DecoderE', 6, '_ZN5movie7Decoder4StopEv'), - ('_ZTVN5movie9ExtractorE', 8, '_ZN5movie9Extractor6SeekToEl'), - ('_ZTVN5movie9ExtractorE', 10, '_ZN5movie9Extractor4ReadEPNS_6BufferE'), - ('_ZTVN5movie9ExtractorE', 11, '_ZN5movie9Extractor7AdvanceEv'), - ('_ZTVN5movie9ExtractorE', 12, '_ZN5movie9Extractor13GetSampleTimeEPl'), - ('_ZTVN5movie7DecoderE', 9, '_ZN5movie7Decoder23AcquireInputBufferIndexEPiS1_'), - ('_ZTVN5movie7DecoderE', 10, '_ZN5movie7Decoder14GetInputBufferEiPNS_6BufferE'), - ('_ZTVN5movie7DecoderE', 11, '_ZN5movie7Decoder24SendInputBufferForDecodeEiiilj'), - ('_ZTVN5movie7DecoderE', 12, '_ZN5movie7Decoder24AcquireOutputBufferIndexEPiPlPjS1_'), - ('_ZTVN5movie7DecoderE', 15, '_ZN5movie7Decoder24ReleaseOutputBufferIndexEi'), - ('_ZTVN5movie7DecoderE', 18, '_ZN5movie7Decoder21GetOutputBufferFormatEiPNS_9MediaDataE')] -for vt, slot, want in VT: - got = slot_name(rel.get(sym[vt] + 16 + 8 * slot, 0)); ok = got == want; bad += not ok - print(f'{"✓" if ok else "✗"} {vt} slot {slot} {got}{"" if ok else " expected " + want}') -n = len(CHAIN) + len(VT) -print(f'{"all passed" if not bad else "[RED]"}: {n - bad}/{n}') -sys.exit(1 if bad else 0) diff --git a/dm/host/tests/native_write_scan.py b/dm/host/tests/native_write_scan.py deleted file mode 100644 index 97f6a27..0000000 --- a/dm/host/tests/native_write_scan.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -# Our own features must not write the original game's stash widgets (the chronicle vault is a separate page; lost-equipment incident: the chronicle page set the original grid invisible and the game never resets it, so the personal / shared pages looked completely empty). -# Static scan: the chronicle page (all of dm_vault_ui.c) and the personal stash page numbers (ui_pages_pump in dm_cursor.c) must not contain original stash widget names -- these two places should only touch widgets under TouchVault / TouchPgPage. -# There is another check at run time: dm_vault_ui.c vu_guard (before writing, walk the parent chain to check whether it is our widget; if not, report a failure and do not write). -# Second check (do not write original-game widgets): the whole host (dm/host/*.c) must not write a widget's visible flag +0x50 / +0x51 directly, only inside guarded functions -- -# dm_cursor.c ui_show_ours, dm_vault_ui.c vu_show, dm_kills.c kb_show (a name not starting with Touch is reported as a failure and not written). To change visibility elsewhere, call them -# Usage: native_write_scan.py <dm/host directory>; --selftest first verifies with a deliberately violating piece of text that the scan itself reports a failure -import os, re, sys - -NATIVE = ['grid', 'gold_amount', 'gold_withdraw', 'gold_max', 'basicstash_container', 'advancedstash_gems', 'advancedstash_materials', 'advancedstash_runes', - 'advancedstash_horadriccube', 'SharedStashTabContainer', 'StashNavigation', 'CurrentSharedStashText', 'background', 'PreviousSeasonToggleDisplay'] - -def scan(text, where): - bad = [] - for no, line in enumerate(text.split('\n'), 1): - code = line.split('//', 1)[0] - for nm in NATIVE: - if re.search(r'"%s"' % re.escape(nm), code): bad.append('%s:%d uses the original-game widget name "%s": %s' % (where, no, nm, line.strip()[:120])) - return bad - -VIS = re.compile(r'\+\s*0x5[01]\)\s*=(?!=)') -GUARDED = ('ui_show_ours', 'vu_show', 'kb_show') -def vis_scan(text, where): # lines that write the visible flag directly; those inside a guarded function (its definition line or its body) do not count - bad, inside = [], None - for no, line in enumerate(text.split('\n'), 1): - m = re.match(r'static\s+\w+\s+(\w+)\(', line) - if m: inside = m.group(1) if m.group(1) in GUARDED else None - code = line.split('//', 1)[0] - if VIS.search(code) and not inside: bad.append('%s:%d writes the visible flag directly (go through %s): %s' % (where, no, ' / '.join(GUARDED), line.strip()[:120])) - if line.startswith('}'): inside = None - return bad - -# Third check (position guard): in dm_cursor.c, writing a widget rectangle (+0x70 x, +0x74 y, +0x78 width, +0x7c height) is allowed only in mv_offset (original-game widgets, allow list + record original value + must write back) -# and in ui_move_ours (only names starting with Touch). Two forms are recognized: a direct *(int32_t *)(... + 0x7?) = ...; or, within the same function, first taking int32_t *name = (int32_t *)(... + 0x7?) and then writing name[k] = / *name = -RECT_OK = ('mv_offset', 'ui_move_ours') -RECT_PTR = re.compile(r'int32_t\s*\*\s*(\w+)\s*=\s*\(int32_t\s*\*\)\s*\([^;]*\+\s*0x7[0-9a-c]\b') -RECT_DIRECT = re.compile(r'\*\s*\(int32_t\s*\*\)\s*\([^)]*\+\s*0x7[0-9a-c]\)\s*=(?!=)') -def rect_scan(text, where): - bad, fn, ptrs = [], None, set() - for no, line in enumerate(text.split('\n'), 1): - m = re.match(r'(?:static\s+)?\w+\s+\*?(\w+)\(', line) - if m and not line.startswith(' '): fn, ptrs = m.group(1), set() - code = line.split('//', 1)[0] - for pm in RECT_PTR.finditer(code): ptrs.add(pm.group(1)) - code = RECT_PTR.sub('', code) # the declaration itself (int32_t *name = ...) does not count as a write - hit = RECT_DIRECT.search(code) or any(re.search(r'(?<![\w.>])(?:\*\s*%s|%s\s*\[\s*\d\s*\])\s*=(?!=)' % (q, q), code) for q in ptrs) - if hit and fn not in RECT_OK: bad.append('%s:%d writes a widget rectangle directly in %s (original-game widgets go through mv_offset, ours through ui_move_ours): %s' % (where, no, fn, line.strip()[:120])) - return bad - -def pages_pump(src): - i = src.find('static void ui_pages_pump(') - if i < 0: raise SystemExit('native_write_scan: ui_pages_pump not found in dm_cursor.c (if it was renamed, update this script)') - j = src.find('\n}\n', i) - return src[i:j] - -def main(): - if '--selftest' in sys.argv: - if not scan(' vu_show(vu_find(bk, "grid", 2), 0);', 'selftest') or scan(' vu_show(vu_find(vt, "TouchV_Back", 0), 1); // "grid" only in a comment', 'selftest') \ - or not vis_scan('static void f(uint64_t w) {\n *(uint8_t *)(w + 0x50) = 0;\n}', 'selftest') or vis_scan('static void ui_show_ours(uint64_t w, int on) {\n *(uint8_t *)(w + 0x50) = 1;\n}\n if (*(uint8_t *)(w + 0x50) == 1) x();', 'selftest') \ - or not rect_scan('static void f(uint64_t w) {\n int32_t *r = (int32_t *)(w + 0x70); r[0] = 5;\n}', 'selftest') or not rect_scan('static void g(uint64_t w) {\n *(int32_t *)(w + 0x74) = 3;\n}', 'selftest') \ - or rect_scan('static void k(uint64_t w) {\n int32_t *rx = (int32_t *)(w + 0x70), ww = *(int32_t *)(w + 0x78); if (*rx) x();\n}\nstatic void mv_offset(uint64_t w) {\n int32_t *v = (int32_t *)(w + 0x70); *v = 1;\n}\nstatic int h(float *r) {\n r[0] = 1.0f; int x = *(int32_t *)(w + 0x70); return x;\n}', 'selftest'): - print('✗ native_write_scan selftest failed: a violation was not reported, or a clean line was'); return 1 - print('✓ native_write_scan selftest: violating lines are reported, names in comments do not count') - host = next(a for a in sys.argv[1:] if not a.startswith('--')) - bad = scan(open(os.path.join(host, 'dm_vault_ui.c'), encoding='utf-8').read(), 'dm_vault_ui.c') - bad += scan(pages_pump(open(os.path.join(host, 'dm_cursor.c'), encoding='utf-8').read()), 'dm_cursor.c ui_pages_pump') - srcs = sorted(f for f in os.listdir(host) if f.endswith('.c')) - for f in srcs: bad += vis_scan(open(os.path.join(host, f), encoding='utf-8', errors='replace').read(), f) - bad += rect_scan(open(os.path.join(host, 'dm_cursor.c'), encoding='utf-8').read(), 'dm_cursor.c') - if bad: - print('✗ our features touched original-game widgets (the Chronicle page / personal stash page numbers may only write widgets under TouchVault / TouchPgPage; the visible flag only through guarded functions):'); [print(' ' + b) for b in bad]; return 1 - print('✓ the Chronicle page and personal stash page numbers do not touch the original stash widgets (%d names scanned); none of the %d host source files writes the visible flag around the guards; dm_cursor.c writes widget rectangles only in mv_offset / ui_move_ours' % (len(NATIVE), len(srcs))); return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/dm/host/tests/oe_notify.py b/dm/host/tests/oe_notify.py deleted file mode 100644 index d46e9d3..0000000 --- a/dm/host/tests/oe_notify.py +++ /dev/null @@ -1,57 +0,0 @@ -# Runs the original image main+e10d30 directly; only the system queries and the resize callback are replaced, the consumer function is not rewritten. -# Hand-written expectations: at the end of the message loop focus/op/perf each hold the current value; the target size corresponds to the performance mode; the same target is resized only once. -import struct,itertools,hashlib,sys -from pathlib import Path -from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE -from unicorn.arm64_const import * -P=Path(sys.argv[1]);b=P.read_bytes() -u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x2000);u.mem_map(0x7100000,0x10000) -if sys.argv[2:] == ['--negative']: - u.mem_write(0xe10e24,bytes.fromhex('1f2003d5')) # only in the negative control is the same-size skip removed, so repeated notifications resize repeatedly - print('NEGATIVE: local copy of the original function, e10e24 b.eq→nop, the source image is unchanged') -u.mem_write(0x3211718,struct.pack('<Q',0x7001001));u.mem_write(0x7001000,struct.pack('<Q',0x7001100));u.mem_write(0x7001110,struct.pack('<Q',0x7001200)) -state={} -def hook(u,a,n,d): - if a==0x7001800:u.emu_stop();return - if a==0x1b77890: - q=state['q'];out=u.reg_read(UC_ARM64_REG_X0) - if q:u.mem_write(out,struct.pack('<I',q.pop(0)));u.reg_write(UC_ARM64_REG_X0,1) - else:u.reg_write(UC_ARM64_REG_X0,0) - elif a==0x1b778a0:u.reg_write(UC_ARM64_REG_X0,state['mode']) - elif a==0x1b778b0:u.reg_write(UC_ARM64_REG_X0,state['focus']) - elif a==0x1b778c0:u.reg_write(UC_ARM64_REG_X0,state['mode']) - elif a==0x7001200: - w=struct.unpack('<Q',u.mem_read(u.reg_read(UC_ARM64_REG_X3),8))[0];h=struct.unpack('<Q',u.mem_read(u.reg_read(UC_ARM64_REG_X4),8))[0] - state['resize'].append((w,h));u.reg_write(UC_ARM64_REG_X0,0) - else:return - u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR)) -u.hook_add(UC_HOOK_CODE,hook) -def run(q,mode,focus,canvas=(1600,900),target=(0,0)): - state.update(q=list(q),mode=mode,focus=focus,resize=[]) - u.mem_write(0x211da30,struct.pack('<II',*canvas));u.mem_write(0x211da38,struct.pack('<II',*target)) - for a in [0x315e628,0x315e630,0x32137e8,0x32137ec]:u.mem_write(a,b'\x00'*8) - u.reg_write(UC_ARM64_REG_SP,0x710fff0);u.reg_write(UC_ARM64_REG_LR,0x7001800) - try:u.emu_start(0xe10d30,0x7001804,count=20000) - except Exception: - print("FAIL PC",hex(u.reg_read(UC_ARM64_REG_PC)),"LR",hex(u.reg_read(UC_ARM64_REG_LR)),"SP",hex(u.reg_read(UC_ARM64_REG_SP)));raise - assert u.reg_read(UC_ARM64_REG_PC)==0x7001800,'did not return' - return (struct.unpack('<I',u.mem_read(0x315e628,4))[0],u.mem_read(0x315e630,2),u.mem_read(0x32137e8,1),u.mem_read(0x32137ec,1),struct.unpack('<II',u.mem_read(0x211da38,8)),list(state['resize'])) -checks=0 -print('original image SHA256',hashlib.sha256(b).hexdigest()) -for focus,mode in [(1,0),(3,1)]: - for order in itertools.permutations([15,30,31]): - full=[m for m in order for _ in range(11)] # 33 messages, really exceeding the old capacity of 32; each message still carries no historical value - a=run(full,mode,focus);c=run(order,mode,focus);checks+=1 - size=(1280,720) if mode==0 else (1600,900) - want=(focus,bytes([mode,mode]),bytes([mode]),bytes([mode]),size,[size]) - ok=a==c==want - print(('✓' if ok else '✗')+f' order {order} focus={focus} mode={mode}: 33 raw notifications / 3 state notifications → same write-back and exactly 1 resize {c}') - if not ok:raise SystemExit(1) -for mode in [0,1]: - size=(1280,720) if mode==0 else (1600,900) - for name,canvas,target,out,resizes in [('target already matches',(1600,900),size,size,[]),('capacity too small',(1,1),(777,333),(777,333),[]),('width matches, height differs',(1600,900),(size[0],333),size,[size])]: - a=run([15]*11+[30]*11+[31]*11,mode,2,canvas,target);c=run([15,30,31],mode,2,canvas,target);checks+=1 - want=(2,bytes([mode,mode]),bytes([mode]),bytes([mode]),out,resizes) - ok=a==c==want;print(('✓' if ok else '✗')+f' {name} mode={mode} focus=2: 33/3 notifications write back as hand-written expectations, resize={len(c[-1])}') - if not ok:raise SystemExit(1) -print(f'CONSUMER_EQUIV OK {checks}/{checks}: original ARM consumer function, 33→3 notifications, hand-written state/size/resize') diff --git a/dm/host/tests/run_manifest.py b/dm/host/tests/run_manifest.py deleted file mode 100644 index 853b2b9..0000000 --- a/dm/host/tests/run_manifest.py +++ /dev/null @@ -1,51 +0,0 @@ -# Independent registration check of the new rule's build output; does not execute the game and does not trust the rewriter's own digest. -import sys,struct,hashlib -from pathlib import Path -D=Path(sys.argv[1]); b=(D/'main.patched.bin').read_bytes(); orig=Path(sys.argv[2]).read_bytes() -headers={};blocks=[] -try: - for line in (D/'main.rules.tsv').read_text().splitlines(): - if line.startswith('# '):k,v=line[2:].split('\t',1);headers[k]=v - else:blocks.append(line.split('\t')) - assert headers['RULESET']=='touch-run-v4' - assert headers['BUILD_ID']=='db0507f36b84200ac05f75d76ed5026f' - assert headers['ORIGINAL_SHA256']==hashlib.sha256(orig).hexdigest()=='6843ab6909f96e1558b4b73b0fa807f3873409120cf3e1ef8a86dbebb275ee27' - assert headers['IMAGE_SHA256']==hashlib.sha256(b).hexdigest() - expected={0x2c620c:0x52800029,0x2c6210:0xb90e5109,0x369a90:None,0x369980:0xd503201f,0x2b2c74:0xd503201f,0x2b2dc0:0xd503201f,0x2b30fc:0x52800021,0x2b3104:0x52800021,0x6dc2c4:0x52800062,0x6dce90:0xd503201f,0x6dcec0:None,0x6c6648:None,0x6c6290:None,0x4ac368:None,0x6c6580:None} - cuts={} - for line in (D/'main.cuts.tsv').read_text().splitlines(): - if line.startswith('#'):continue - off,old,new,scope,why=line.split('\t',4) - if why.startswith('touch-run-v4: '):cuts[int(off,16)]=(old,new,scope) - assert set(cuts)==set(expected),('cut set',set(cuts)^set(expected)) - def w(at):return struct.unpack_from('<I',b,at)[0] - def branch(at): - v=w(at);assert v&0xfc000000==0x14000000;imm=v&0x3ffffff - return at+4*((imm^0x2000000)-0x2000000) - for off,newword in expected.items(): - old,new,scope=cuts[off];assert scope=='all';assert orig[off:off+4]==bytes.fromhex(old);assert b[off:off+4]==bytes.fromhex(new) - if newword is not None:assert w(off)==newword - assert branch(0x369a90)==0x369900 and branch(0x6dcec0)==0x6dce50 - assert len(blocks)==4 - fixheaders={} - for line in (D/'main.fixups.tsv').read_text().splitlines(): - if line.startswith('# '):k,v=line[2:].split('\t');fixheaders[k]=int(v,0) - po=fixheaders['PATCH_OFF'];seen=set() - for off,raw,sha,why in blocks: - off=int(off,16);body=bytes.fromhex(raw);assert po<=off<len(b) and off+len(body)<=len(b);assert b[off:off+len(body)]==body;assert hashlib.sha256(body).hexdigest()==sha - src=next(s for s in [0x6c6648,0x6c6290,0x4ac368,0x6c6580] if branch(s)==off);assert src not in seen;seen.add(src) - if src==0x6c6580: - assert len(body)==24 and w(off)==0x35000088 and w(off+4)==0x51000849 and w(off+8)==0x35000049 and w(off+12)==0x52800062 and w(off+16)==0x51000849 and branch(off+20)==0x6c6584 - continue - assert w(off)==0xb9400268 - assert w(off+4)==(0x34000048 if src==0x4ac368 else 0x35000048) - if src==0x6c6648: - assert len(body)==24 and branch(off+8)==0x6c664c and w(off+12)==0x34000040 and branch(off+16)==0x6c664c and branch(off+20)==0x6c6654 - elif src==0x6c6290: - assert len(body)==20 and branch(off+8)==0x6c62ec and w(off+12)==0x710006df and branch(off+16)==0x6c6294 - else: - assert len(body)==28 and branch(off+8)==off+16 and branch(off+12)==0x4ac3f4 and w(off+16)==0x34000040 and branch(off+20)==0x4ac3f4 and branch(off+24)==0x4ac36c - assert seen=={0x6c6648,0x6c6290,0x4ac368,0x6c6580} - print('RUN_MANIFEST OK original/image hash, buildid, 15-cut whitelist, 4 trampolines hash/range/structure/entry B') -except (AssertionError,KeyError,OSError,ValueError,StopIteration) as e: - print('RUN_MANIFEST FAIL',type(e).__name__,str(e));sys.exit(1) diff --git a/dm/host/tests/run_rule.py b/dm/host/tests/run_rule.py deleted file mode 100644 index 6f5508d..0000000 --- a/dm/host/tests/run_rule.py +++ /dev/null @@ -1,88 +0,0 @@ -# Local original-ARM contract of the new rule; does not replace the speed/real action matrix. -import sys,struct,hashlib -from pathlib import Path -from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE -from unicorn.arm64_const import * -orig=Path(sys.argv[1]).read_bytes(); candidate=Path(sys.argv[2]).read_bytes() -U,P,R,A,L=0x7001000,0x7002000,0x7003000,0x7004000,0x7005000 -fails=[]; total=0 -class Machine: - def __init__(self,blob): - self.u=u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,blob);u.mem_map(0x7000000,0x20000) - for addr,v in [(U+0x38,P),(P,R),(P+0x20,R),(R+0x18,A),(A+0x90,L)]:u.mem_write(addr,struct.pack('<Q',v)) - self.sta=0;self.calls=[];self.stop=0 - u.hook_add(UC_HOOK_CODE,self.hook) - def hook(self,u,a,n,d): - if a==self.stop:u.emu_stop();return - if a==0x5b9290:self.calls.append(('get',10));u.reg_write(UC_ARM64_REG_X0,self.sta) - elif a==0x5a13a0:u.reg_write(UC_ARM64_REG_X0,0) - elif a==0x5bc5a0: - assert u.reg_read(UC_ARM64_REG_W1)==10 - v=u.reg_read(UC_ARM64_REG_W2);v=(v^0x80000000)-0x80000000 - self.calls.append(('add',v));self.sta+=v - elif a==0x5bc1f0: - assert u.reg_read(UC_ARM64_REG_W1)==10 - self.calls.append(('set',u.reg_read(UC_ARM64_REG_W2)));self.sta=u.reg_read(UC_ARM64_REG_W2) - elif a==0x6c6550 and self.stop==0x6c62ec:self.calls.append(('downgrade',u.reg_read(UC_ARM64_REG_W2))) - elif a==0x45f9d0:u.reg_write(UC_ARM64_REG_X0,U) - elif a==0x460fd0: - v=u.reg_read(UC_ARM64_REG_W1);self.calls.append(('mode',v));u.mem_write(U+0xc,struct.pack('<I',v));u.reg_write(UC_ARM64_REG_X0,0) - elif a in [0x4bbc50,0x5ca8a0]: - v=u.reg_read(UC_ARM64_REG_W1);self.calls.append(('mode',v));u.mem_write(U+0xc,struct.pack('<I',v)) - elif a==0x2ceab0:self.calls.append(('packet',u.mem_read(u.reg_read(UC_ARM64_REG_X0),1)[0])) - elif a==0x5a7510:pass - else:return - u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR)) - def go(self,start,stop): - self.stop=stop;self.u.reg_write(UC_ARM64_REG_SP,0x701fff0) - self.u.emu_start(start,stop+4,count=3000) - assert self.u.reg_read(UC_ARM64_REG_PC)==stop,hex(self.u.reg_read(UC_ARM64_REG_PC)) - def selector(self,typ,town,sta,mode): - u=self.u;self.sta=sta;self.calls=[];u.mem_write(U,struct.pack('<I',typ));u.mem_write(L+0x1f8,struct.pack('<I',1 if town else 2)) - u.reg_write(UC_ARM64_REG_X0,0x7006000);u.reg_write(UC_ARM64_REG_X1,U);u.reg_write(UC_ARM64_REG_X2,mode) - self.go(0x6c6550,0x6c6678);return u.reg_read(UC_ARM64_REG_W22) - def drain(self,typ,sta): - self.sta=sta;self.calls=[];u=self.u;u.mem_write(U,struct.pack('<I',typ));u.reg_write(UC_ARM64_REG_X19,U);u.reg_write(UC_ARM64_REG_X20,0x7006000);u.reg_write(UC_ARM64_REG_W22,2) - self.go(0x6c6290,0x6c62ec);return self.sta,list(self.calls) -def check(label,actual,want): - global total - total+=1 - if actual!=want:fails.append((label,actual,want)) -a,b=Machine(orig),Machine(candidate) -for typ in [0,1,2,3,4,6]: - for town in [False,True]: - for sta in [0,1,22272]: - for mode in [1,2,3,6,7,10,17,19]: - old=a.selector(typ,town,sta,mode) - want=3 if typ==0 and mode in (2,3) else old - check(f'select type={typ} town={town} sta={sta} mode={mode}',b.selector(typ,town,sta,mode),want) -for typ in [0,1,2,3,4,6]: - for sta in [0,1,22272]: - old=a.drain(typ,sta);want=(sta,[]) if typ==0 else old - check(f'cost type={typ} sta={sta}',b.drain(typ,sta),want) -for pref in [0,1]: - for hyst in [0,1]: - for latch in [0,1]: - u=b.u;u.reg_write(UC_ARM64_REG_W10,pref);u.reg_write(UC_ARM64_REG_W8,hyst);u.reg_write(UC_ARM64_REG_W9,latch) - b.go(0x2b30e8,0x2b3108);check(f'client select {pref}/{hyst}/{latch}',u.reg_read(UC_ARM64_REG_W1),1) -u=b.u;b.go(0x2c6208,0x2c6214);check('initial run preference',struct.unpack('<I',u.mem_read(0x2d20e50,4))[0],1) -# The build cut entry redirection check does not claim full On execution. -for start,target in [(0x369a90,0x369900),(0x6dcec0,0x6dce50)]: - u=b.u;u.reg_write(UC_ARM64_REG_SP,0x701fff0);u.emu_start(start,0x4000000,count=1);check(f'off redirect {start:x}',u.reg_read(UC_ARM64_REG_PC),target) -# The real On/Off functions are executed in full; lookup/stat/animation switch/packet send are explicit peripheral stand-ins. -for typ in [0,1]: - for mode in [0,1,2,3,4,5,6,7,10,17,19]: - for sta in [0,1,22272]: - for entry in [0x369900,0x369a90,0x6dce50,0x6dcec0]: - u=b.u;b.sta=sta;b.calls=[];u.mem_write(U,struct.pack('<I',typ));u.mem_write(U+0xc,struct.pack('<I',mode)) - u.mem_write(0x2d20ba1,b'\0');u.mem_write(0x2d20e50,struct.pack('<II',0,0));u.reg_write(UC_ARM64_REG_X1,U);u.reg_write(UC_ARM64_REG_W3,1);u.reg_write(UC_ARM64_REG_LR,0x7000100) - b.go(entry,0x7000100);want=3 if typ==0 and mode==2 else mode - actual=struct.unpack('<I',u.mem_read(U+0xc,4))[0] - check(f'toggle entry={entry:x} type={typ} mode={mode} sta={sta}',actual,want) - packets=[v for k,v in b.calls if k=='packet'] - wp=[0x49] if entry<0x600000 and typ==0 and mode==2 else [] - check(f'toggle packets entry={entry:x} type={typ} mode={mode} sta={sta}',packets,wp) -print('ORIGINAL_SHA256',hashlib.sha256(orig).hexdigest());print('CANDIDATE_SHA256',hashlib.sha256(candidate).hexdigest()) -for label,actual,want in fails[:30]:print(f'FAIL {label}: expected {want} → got {actual}') -print(f'RUN_RULE_LOCAL {total} cases, {len(fails)} failed; selector / deduction window / encoding choice / preference / entry redirect; not a full action / speed criterion') -sys.exit(bool(fails)) diff --git a/dm/host/tests/server42_rule.py b/dm/host/tests/server42_rule.py deleted file mode 100644 index 1af2a83..0000000 --- a/dm/host/tests/server42_rule.py +++ /dev/null @@ -1,28 +0,0 @@ -# disabled_bit_guard only tests in-place eligibility; a hand-made local memory fixture, which does not prove the legitimate source of the control lock of a living player. -# Executes the input validity checks and dispatch of the original full 42 handler; the decoder/downstream movement/fatal log are explicit peripheral stand-ins. -import sys,struct -from pathlib import Path -from unicorn import * -from unicorn.arm64_const import * -b=Path(sys.argv[1]).read_bytes();u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,b);u.mem_map(0x7000000,0x20000) -G,U,D,C,P,PK,STOP=0x7001000,0x7002000,0x7003000,0x7004000,0x7005000,0x7006000,0x701f000 -ctx={} -def hook(u,a,n,d): - if a==STOP:ctx['returned']=True;u.emu_stop();return - if a==0x4cd5f0: - v=u.reg_read(UC_ARM64_REG_W0);u.mem_write(u.reg_read(UC_ARM64_REG_X1),struct.pack('<ff',1,0));u.mem_write(u.reg_read(UC_ARM64_REG_X2),struct.pack('<f',1));u.mem_write(u.reg_read(UC_ARM64_REG_X3),bytes([v>>15]));u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR)) - elif a==0x6cd670:ctx['move']=u.reg_read(UC_ARM64_REG_W2);u.reg_write(UC_ARM64_REG_W0,0);u.reg_write(UC_ARM64_REG_PC,u.reg_read(UC_ARM64_REG_LR)) - elif a==0xde1850:ctx['fatal']=True;u.emu_stop() -u.hook_add(UC_HOOK_CODE,hook);bad=0;n=0 -for bit in [0,1]: - for case in ['valid','badlen','dead0','dead17','disabled_bit_guard','far','nonplayer']: - ctx.clear();u.mem_write(G,b'\0'*0x1000);u.mem_write(U,b'\0'*0x400);u.mem_write(D,b'\0'*0x200);u.mem_write(C,b'\0'*0x1000);u.mem_write(P,b'\0'*0x300) - u.mem_write(U,struct.pack('<I',int(case=='nonplayer')));u.mem_write(U+12,struct.pack('<I',0 if case=='dead0'else 17 if case=='dead17'else 1));u.mem_write(U+0x10,struct.pack('<Q',D));u.mem_write(D+0xf0,struct.pack('<Q',C));u.mem_write(U+0x38,struct.pack('<Q',P));u.mem_write(P+2,struct.pack('<H',5826));u.mem_write(P+6,struct.pack('<H',4520));u.mem_write(U+0x126,bytes([int(case=='disabled_bit_guard')]));u.mem_write(G+0x170,struct.pack('<I',421));u.mem_write(PK,struct.pack('<BHHH',0x42,512|(31<<10)|(bit<<15),5926 if case=='far'else 5826,4520)) - for r,v in [(UC_ARM64_REG_X0,G),(UC_ARM64_REG_X1,U),(UC_ARM64_REG_X2,PK),(UC_ARM64_REG_X3,6 if case=='badlen'else 7),(UC_ARM64_REG_SP,0x701fff0),(UC_ARM64_REG_LR,STOP)]:u.reg_write(r,v) - u.emu_start(0x6dc270,STOP+4,count=5000) - wantmove=3 if case=='valid'else None;wantstatus=3 if case=='badlen'else 1 if case in ['dead0','dead17','disabled_bit_guard','far']else 0 - ok=(ctx.get('fatal',False) or ctx.get('returned',False)) and ctx.get('move')==wantmove and (ctx.get('fatal',False)==(case=='nonplayer')) and (case=='nonplayer'or u.reg_read(UC_ARM64_REG_W0)==wantstatus) - n+=1 - if not ok:bad+=1;print('FAIL',bit,case,'expected move',wantmove,'→ got',ctx,'status',u.reg_read(UC_ARM64_REG_W0)) -print(f'SERVER42_RULE {n} cases, {bad} failed; real RET to STOP / original fatal-halt criterion; bit0/1 legal requests Run3, length / death / disable bit / coordinates / non-player original checks; real downstream speed not included') -sys.exit(bool(bad)) diff --git a/dm/host/tests/server_walk_request.py b/dm/host/tests/server_walk_request.py deleted file mode 100644 index a19829f..0000000 --- a/dm/host/tests/server_walk_request.py +++ /dev/null @@ -1,21 +0,0 @@ -# Independent rule test of the generic movement dispatch index point; the entry has already passed the original null/type5 gate, the end point is an explicit stand-in, and the real path is not verified. -import sys,struct -from pathlib import Path -from unicorn import Uc,UC_ARCH_ARM64,UC_MODE_ARM,UC_HOOK_CODE -from unicorn.arm64_const import * -u=Uc(UC_ARCH_ARM64,UC_MODE_ARM);u.mem_map(0,0x4000000);u.mem_write(0,Path(sys.argv[1]).read_bytes());u.mem_map(0x7000000,0x10000);ctx={};bad=0;n=0 -G,UNIT,SP=0x7001000,0x7002000,0x700f000 -def hook(u,a,n,d): - if a==0x6c6584:ctx['arrived']=True;u.emu_stop() -u.hook_add(UC_HOOK_CODE,hook) -for kind in [0,1,2,3,4,5,6,99]: - for mode in [0,1,2,3,5,6,10,0xffffffff]: - ctx.clear();u.mem_write(UNIT,struct.pack('<I',kind)) - for r,v in [(UC_ARM64_REG_X0,G),(UC_ARM64_REG_X19,UNIT),(UC_ARM64_REG_W8,kind),(UC_ARM64_REG_W2,mode),(UC_ARM64_REG_SP,SP),(UC_ARM64_REG_NZCV,0xa0000000)]:u.reg_write(r,v) - u.emu_start(0x6c6580,0x4000000,count=100) - want=3 if kind==0 and mode==2 else mode - ok=ctx.get('arrived',False) and u.reg_read(UC_ARM64_REG_W2)==want and u.reg_read(UC_ARM64_REG_W9)==((want-2)&0xffffffff) and u.reg_read(UC_ARM64_REG_W8)==kind and u.reg_read(UC_ARM64_REG_X0)==G and u.reg_read(UC_ARM64_REG_X19)==UNIT and u.reg_read(UC_ARM64_REG_SP)==SP and u.reg_read(UC_ARM64_REG_NZCV)==0xa0000000 - n+=1 - if not ok:bad+=1;print('FAIL type',kind,'request',mode,'expected normalized',want,'→ got',u.reg_read(UC_ARM64_REG_W2),'arrived',ctx.get('arrived',False)) -print(f'SERVER_WALK_REQUEST {n} cases, {bad} failed; only type0 request 2 is normalized to 3 and the index recomputed, other requests / types and W0/X19/W8/SP/NZCV unchanged; stand-in at the end') -sys.exit(bool(bad)) diff --git a/dm/host/tests/t_altstack_stub.c b/dm/host/tests/t_altstack_stub.c index c17c5ef..efa6211 100644 --- a/dm/host/tests/t_altstack_stub.c +++ b/dm/host/tests/t_altstack_stub.c @@ -1,3 +1,5 @@ -#include <stdint.h> -typedef struct module_t module_t; +// the loader's module table (loader.c), which crash.c's report walks: the unit test has no modules +#include "../dm.h" +module_t g_mod[MAX_MOD]; +int g_nmod; module_t *module_of(const void *p) { (void)p; return 0; } diff --git a/dm/host/tests/t_cloudsync.c b/dm/host/tests/t_cloudsync.c deleted file mode 100644 index 8887f9a..0000000 --- a/dm/host/tests/t_cloudsync.c +++ /dev/null @@ -1,181 +0,0 @@ -// iCloud sync core unit test (takes seconds): dm_cloudsync.c. Two local directories act as two devices and one local directory as the cloud; all expectations are derived by hand from "later write overwrites earlier write", not obtained by running the implementation under test. -#include <dirent.h> -#include <stdint.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> -#include <sys/time.h> -#include <unistd.h> -int dm_cloud_sync(const char *save, const char *cloud, const char *bak, const char *statef, int allow_down, uint64_t now_ms, int (*ready)(const char *unit)); -int dm_cloud_conflict_take(void); -#if __has_include("../stash_seed.h") // the public repository does not ship this seed (it is the bytes of a save the game generated): when it is absent, the two groups that use it, 18 and 19, are skipped -#include "../stash_seed.h" -#define T_HAVE_STASH_SEED 1 -#endif - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) -static char R[512]; -static void path(char *o, const char *a, const char *b) { snprintf(o, 1024, "%s/%s/%s", R, a, b); } -static void put(const char *dev, const char *rel, const char *txt, long sec) { // write a file and set its modification time to sec seconds - char p[1024]; path(p, dev, rel); char d[1024]; strcpy(d, p); for (char *s = d + strlen(R) + 1; (s = strchr(s, '/')); s++) { *s = 0; mkdir(d, 0755); *s = '/'; } - FILE *f = fopen(p, "w"); fputs(txt, f); fclose(f); struct timeval tv[2] = { { sec, 0 }, { sec, 0 } }; utimes(p, tv); -} -static const char *get(const char *dev, const char *rel) { static char b[256]; char p[1024]; path(p, dev, rel); FILE *f = fopen(p, "r"); if (!f) return NULL; size_t n = fread(b, 1, 255, f); b[n] = 0; fclose(f); return b; } -static long mtime(const char *dev, const char *rel) { char p[1024]; struct stat st; path(p, dev, rel); return stat(p, &st) ? -1 : st.st_mtimespec.tv_sec; } -static int dsync(const char *dev, int down, uint64_t now) { char s[1024], c[1024], b[1024], t[1024]; path(s, dev, "save"); snprintf(c, sizeof c, "%s/cloud", R); path(b, dev, "bak"); path(t, dev, "state"); return dm_cloud_sync(s, c, b, t, down, now, NULL); } -static int count_bak(const char *dev, const char *pre) { char d[1024]; path(d, dev, "bak"); DIR *dp = opendir(d); struct dirent *e; int n = 0; while (dp && (e = readdir(dp))) n += !strncmp(e->d_name, pre, strlen(pre)); if (dp) closedir(dp); return n; } -static int no_skip(const char *u) { return strcmp(u, "Alice"); } -// Shared stash file: one page of items (items pieces, only the page header and item count are written) + one chronicle section (chron bytes); side file: n pieces. Only the structure is built, as the sync side only looks at structure (dm_cloudsync.c rich_of) -static void put_bin(const char *dev, const char *rel, const uint8_t *d, size_t n, long sec) { - char p[1024]; path(p, dev, rel); char dd[1024]; strcpy(dd, p); for (char *x = dd + strlen(R) + 1; (x = strchr(x, '/')); x++) { *x = 0; mkdir(dd, 0755); *x = '/'; } - FILE *f = fopen(p, "wb"); fwrite(d, 1, n, f); fclose(f); struct timeval tv[2] = { { sec, 0 }, { sec, 0 } }; utimes(p, tv); -} -static void put_d2i(const char *dev, int items, int chron, long sec) { - static uint8_t b[8192]; memset(b, 0, sizeof b); uint32_t l1 = 68, l2 = (uint32_t)chron; - memcpy(b, "\x55\xaa\x55\xaa", 4); memcpy(b + 16, &l1, 4); memcpy(b + 64, "JM", 2); b[66] = (uint8_t)items; - memcpy(b + 68, "\x55\xaa\x55\xaa", 4); memcpy(b + 68 + 16, &l2, 4); b[68 + 64] = 0xc0; - put_bin(dev, "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i", b, 68 + l2, sec); -} -static void put_vault(const char *dev, int n, long sec) { uint8_t b[8] = { 'S', 'W', 'V', 'T', 1, 0, (uint8_t)n, 0 }; put_bin(dev, "save/ModernSharedStashSoftCoreV2.swvault", b, 8, sec); } -static int d2i_items(const char *dev, const char *rel) { char p[1024]; path(p, dev, rel); FILE *f = fopen(p, "rb"); if (!f) return -1; uint8_t b[70]; size_t n = fread(b, 1, 70, f); fclose(f); return n >= 67 ? b[66] : -1; } - -int main(void) { - snprintf(R, sizeof R, "%s/t_cloudsync.%d", getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", getpid()); mkdir(R, 0755); - char c[1024]; snprintf(c, sizeof c, "%s/cloud", R); mkdir(c, 0755); - // (1) A has character Alice (timestamp 1000 seconds), the shared stash and Settings; B is empty -> A uploads, B gets Alice and the shared stash at startup, Settings is not synced, and modification times follow A's - put("A", "save/Alice/Alice.d2s", "a1", 1000); put("A", "save/Alice/Alice.key", "k", 900); - put("A", "save/SharedStashSoftCoreV2.d2i", "s1", 1100); put("A", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i", "m1", 1100); put("A", "save/Settings/Settings.json", "{}", 1100); - put("B", "save/Settings/Settings.json", "{b}", 1200); - CHECK(dsync("A", 0, 0) == 2, "① A should upload 2 units (Alice, _stash)"); - CHECK(dsync("B", 1, 0) == 2, "① B should download 2 units"); - CHECK(get("B", "save/Alice/Alice.d2s") && !strcmp(get("B", "save/Alice/Alice.d2s"), "a1"), "① B's Alice should be a1"); - CHECK(mtime("B", "save/Alice/Alice.d2s") == 1000, "① modification time should be restored to 1000, got %ld", mtime("B", "save/Alice/Alice.d2s")); - CHECK(get("B", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i") && !strcmp(get("B", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i"), "m1"), "① B's shared stash should be m1"); - CHECK(!strcmp(get("B", "save/Settings/Settings.json"), "{b}"), "① B's Settings should not be touched"); - CHECK(dsync("A", 1, 0) == 0 && dsync("B", 1, 0) == 0, "① syncing once more should do nothing on either side"); - // (2) Only B changed (2000) -> B uploads while running; A gets it at startup, and A's former copy goes to backup - put("B", "save/Alice/Alice.d2s", "b2", 2000); - CHECK(dsync("B", 0, 0) == 1, "② B should upload 1"); - CHECK(dsync("A", 0, 0) == 0 && !strcmp(get("A", "save/Alice/Alice.d2s"), "a1"), "② A should not change local files while running"); - CHECK(dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/Alice/Alice.d2s"), "b2"), "② A should have b2 after startup"); - CHECK(get("A", "bak/Alice-1000000/Alice/Alice.d2s") && !strcmp(get("A", "bak/Alice-1000000/Alice/Alice.d2s"), "a1"), "② A's overwritten a1 should be in backup Alice-1000000"); - // (3) Both sides changed, B wrote later (A's 3000 uploaded first, B 3500) -> B still uploads while running (last write wins), the cloud's former a3 goes to B's backup; A gets b3 at startup - put("A", "save/Alice/Alice.d2s", "a3", 3000); put("B", "save/Alice/Alice.d2s", "b3", 3500); - CHECK(dsync("A", 0, 0) == 1, "③ A uploads first"); - CHECK(dsync("B", 0, 0) == 1, "③ B wrote later and uploads too"); - CHECK(get("B", "bak/Alice-3000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-3000000/Alice/Alice.d2s"), "a3"), "③ the overwritten cloud a3 should be in B's backup"); - CHECK(dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/Alice/Alice.d2s"), "b3"), "③ A should have b3 after startup"); - // (4) Both sides changed, A wrote later (A's 5000 uploaded; B's 4000 not uploaded before restarting) -> B gets A's a4 at startup, b4 goes to backup Alice-4000000 - put("A", "save/Alice/Alice.d2s", "a4", 5000); CHECK(dsync("A", 0, 0) == 1, "④ A uploads"); - put("B", "save/Alice/Alice.d2s", "b4", 4000); - CHECK(dsync("B", 1, 0) == 1 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a4"), "④ B should have a4 after startup (cloud 5000 > local 4000)"); - CHECK(get("B", "bak/Alice-4000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-4000000/Alice/Alice.d2s"), "b4"), "④ B's overwritten b4 should be in backup"); - // (5) The cloud package has not finished downloading (ready returns 0) -> skip, local files untouched - put("A", "save/Alice/Alice.d2s", "a5", 6000); CHECK(dsync("A", 0, 0) == 1, "⑤ A uploads"); - { char s[1024], b[1024], t[1024]; path(s, "B", "save"); path(b, "B", "bak"); path(t, "B", "state"); CHECK(dm_cloud_sync(s, c, b, t, 1, 0, no_skip) == 0 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a4"), "⑤ B should stay untouched while the download is unfinished"); } - CHECK(dsync("B", 1, 0) == 1 && !strcmp(get("B", "save/Alice/Alice.d2s"), "a5"), "⑤ B should have a5 once downloaded"); - // (6) A deletes Alice -> deletion marker in the cloud; B also deletes at startup and B's copy goes to backup; B's backup keeps only 3 copies (before this there were 1000000 not on B, 3000000, 4000000, plus 6000000 = 3 copies) - { char p[1024]; path(p, "A", "save/Alice/Alice.d2s"); unlink(p); path(p, "A", "save/Alice/Alice.key"); unlink(p); path(p, "A", "save/Alice"); rmdir(p); } - CHECK(dsync("A", 0, 7000000) == 1, "⑥ A should write a deletion marker"); - CHECK(dsync("B", 1, 0) == 1 && !get("B", "save/Alice/Alice.d2s"), "⑥ B's Alice should be deleted after startup"); - CHECK(get("B", "bak/Alice-6000000/Alice/Alice.d2s") && !strcmp(get("B", "bak/Alice-6000000/Alice/Alice.d2s"), "a5"), "⑥ B's deleted a5 should be in backup"); - CHECK(count_bak("B", "Alice-") == 3, "⑥ B should keep 3 backups of Alice, got %d", count_bak("B", "Alice-")); - for (long t = 8; t <= 9; t++) { char v[8]; snprintf(v, sizeof v, "x%ld", t); put("B", "save/Bob/Bob.d2s", v, t * 1000); } // unrelated characters are not affected - CHECK(dsync("B", 1, 0) == 1, "⑥ B's new character Bob uploads"); - // (7) After the deletion A created a character with the same name again (9500) -> upload (revive); B gets it at startup - put("A", "save/Alice/Alice.d2s", "a7", 9500); - CHECK(dsync("A", 1, 0) == 2 && !strcmp(get("A", "save/Bob/Bob.d2s"), "x9"), "⑦ A: Alice uploads + Bob downloads"); - CHECK(dsync("B", 1, 0) == 1 && get("B", "save/Alice/Alice.d2s") && !strcmp(get("B", "save/Alice/Alice.d2s"), "a7"), "⑦ B should have the recreated Alice a7 after startup"); - // (8) Shared stash: B changes it (10000) -> A gets it at startup - put("B", "save/SharedStashSoftCoreV2.d2i", "s8", 10000); - CHECK(dsync("B", 0, 0) == 1 && dsync("A", 1, 0) == 1 && !strcmp(get("A", "save/SharedStashSoftCoreV2.d2i"), "s8"), "⑧ A's shared stash should be s8"); - CHECK(!strcmp(get("A", "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i"), "m1"), "⑧ the shared stash's other file should keep m1"); - // (9) The save directory was emptied by something other than the game (the state file is still there) -> at startup it does not count as a deletion, everything is fetched back from the cloud, and no deletion marker appears in the cloud - { char p[1024]; path(p, "A", "save"); char cmd[1200]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", p); system(cmd); mkdir(p, 0755); } - CHECK(dsync("A", 1, 0) == 3, "⑨ A should download 3 at startup (Alice, Bob, _stash)"); - CHECK(get("A", "save/Alice/Alice.d2s") && !strcmp(get("A", "save/Alice/Alice.d2s"), "a7") && get("A", "save/Bob/Bob.d2s") && !strcmp(get("A", "save/SharedStashSoftCoreV2.d2i"), "s8"), "⑨ A should get back Alice a7, Bob, shared stash s8"); - CHECK(dsync("B", 1, 0) == 0 && get("B", "save/Alice/Alice.d2s"), "⑨ B is unaffected (no deletion marker came over)"); - // (10) The backup directory cannot be written (here it is blocked with a regular file of the same name) -> neither overwrite nor delete touches local files; after unblocking it proceeds as usual (previously it still overwrote / deleted when the backup write failed) - { char s[1024], c2[1024], b[1024], t2[1024], bf[1024]; snprintf(c2, sizeof c2, "%s/cloud2", R); mkdir(c2, 0755); - #define SY(dev, down, now) (path(s, dev, "save"), path(b, dev, "bak"), path(t2, dev, "state"), dm_cloud_sync(s, c2, b, t2, down, now, NULL)) - put("F", "save/Settings/Settings.json", "{}", 1); // F is a device that already has a save directory - put("E", "save/Eve/Eve.d2s", "e1", 1000); CHECK(SY("E", 0, 0) == 1 && SY("F", 1, 0) == 1 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e1"), "⑩ setup: F gets Eve e1"); - put("E", "save/Eve/Eve.d2s", "e2", 2000); CHECK(SY("E", 0, 0) == 1, "⑩ E uploads e2"); - path(bf, "F", "bak"); FILE *f = fopen(bf, "w"); fclose(f); // F's backup directory location is a regular file: the directory cannot be created - CHECK(SY("F", 1, 0) == 0 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e1"), "⑩ F should not overwrite while the backup cannot be written, still e1"); - unlink(bf); - CHECK(SY("F", 1, 0) == 1 && get("F", "save/Eve/Eve.d2s") && !strcmp(get("F", "save/Eve/Eve.d2s"), "e2") && get("F", "bak/Eve-1000000/Eve/Eve.d2s") && !strcmp(get("F", "bak/Eve-1000000/Eve/Eve.d2s"), "e1"), "⑩ once unblocked F switches to e2, the whole e1 is in the backup"); - { char p2[1024]; path(p2, "E", "save/Eve/Eve.d2s"); unlink(p2); path(p2, "E", "save/Eve"); rmdir(p2); } - CHECK(SY("E", 0, 3000000) == 1, "⑩ E deleted Eve while running, writes a deletion marker"); - char bk[1100]; snprintf(bk, sizeof bk, "%s.keep", bf); rename(bf, bk); f = fopen(bf, "w"); fclose(f); // move the existing backup directory aside first, then block it with a regular file - CHECK(SY("F", 1, 0) == 0 && get("F", "save/Eve/Eve.d2s"), "⑩ F should not delete Eve while the backup cannot be written"); - unlink(bf); rename(bk, bf); - CHECK(SY("F", 1, 0) == 1 && !get("F", "save/Eve/Eve.d2s") && get("F", "bak/Eve-2000000/Eve/Eve.d2s"), "⑩ once unblocked F deletes Eve, the whole e2 is in the backup"); - #undef SY - } - // (11)-(15) Chronicle replaced wholesale by an empty one: a new device's first sync takes the cloud as authoritative; the shared stash must not be overwritten by a copy with "clearly less" (another cloud directory cloud3, devices G / H) - { char s3[1024], c3[1024], b3[1024], t3[1024]; snprintf(c3, sizeof c3, "%s/cloud3", R); mkdir(c3, 0755); - #define S3(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, c3, b3, t3, down, 0, NULL)) - #define D2I "save/ModernSharedStashSoftCoreV2/ModernSharedStashSoftCoreV2.d2i" - put_d2i("G", 5, 300, 1000); put_vault("G", 3, 1000); - CHECK(S3("G", 0) == 1, "⑫ cloud empty: G's first sync uploads the shared stash"); - dm_cloud_conflict_take(); - put_d2i("H", 0, 148, 5000); // H freshly installed: the empty shared stash the game just created, with a timestamp newer than the cloud's - CHECK(S3("H", 0) == 0 && d2i_items("H", D2I) == 0, "⑪ new device's first sync while running: no upload, no change (an empty stash must not overwrite the cloud)"); - CHECK(S3("H", 1) == 1 && d2i_items("H", D2I) == 5 && get("H", "save/ModernSharedStashSoftCoreV2.swvault"), "⑪ new device's first sync at startup: the cloud wins, gets the 5 items and the Chronicle vault"); - CHECK(count_bak("H", "_stash-") == 1, "⑪ the new device's own empty stash goes to backup"); - CHECK(!dm_cloud_conflict_take(), "⑪ a normal first sync is not a conflict"); - put_d2i("G", 6, 400, 6000); CHECK(S3("G", 0) == 1, "⑭ G's Chronicle grew (400), uploads as usual"); - CHECK(S3("H", 1) == 1 && d2i_items("H", D2I) == 6, "⑭ H gets G's new version at startup"); - put_d2i("H", 0, 148, 7000); // the shared stash on H somehow became empty (chronicle 148, 0 items), with a newer timestamp - CHECK(S3("H", 0) == 0, "⑬ H wants to upload the emptied copy while running: blocked"); - CHECK(dm_cloud_conflict_take() == 1, "⑬ the block records a conflict (a notice pops up on entering a run)"); - CHECK(S3("G", 1) == 0 && d2i_items("G", D2I) == 6, "⑬ the cloud was not overwritten: G still has 6 items"); - put_d2i("H", 7, 500, 7500); CHECK(S3("H", 0) == 1, "⑮ H plays normally: Chronicle 500 uploads"); - put_d2i("G", 6, 450, 7600); // G wrote 450 itself without having synced H's 500 (both sides changed, G wrote later) - CHECK(S3("G", 0) == 0, "⑮ G wants to overwrite the cloud's Chronicle 500 with 450 while running: blocked"); - CHECK(S3("G", 1) == 0 && d2i_items("G", D2I) == 6, "⑮ G at startup: the cloud's 500 is older than the local 450 but its Chronicle is longer -- last write wins would upload, it is blocked; local files untouched"); - CHECK(dm_cloud_conflict_take() == 1, "⑮ conflict recorded"); - { char p3[1024]; path(p3, "H", "save/ModernSharedStashSoftCoreV2.swvault"); unlink(p3); } put_d2i("H", 7, 500, 8000); - CHECK(S3("H", 0) == 0 && dm_cloud_conflict_take() == 1, "⑬ the Chronicle vault side file is gone (the cloud has 3 items): blocked"); - put_vault("H", 0, 8100); put_d2i("H", 0, 500, 8100); - CHECK(S3("H", 0) == 1 && !dm_cloud_conflict_take(), "⑯ normal play: shared stash items taken out, Chronicle vault emptied (file still there, 0 items), Chronicle not shorter -- uploads as usual"); - // (17) That rf3 shape: cloud hardcore stash has 2 items and the chronicle is still the freshly created 148; the local copy was replaced by the empty file the game freshly creates (0 items, 148), the state record is still there and the cloud is unchanged = takes the "only local changed" upload - { char cd[1024]; snprintf(cd, sizeof cd, "%s/cloud4", R); mkdir(cd, 0755); - #define S4(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, cd, b3, t3, down, 0, NULL)) - put_d2i("J", 2, 148, 1000); CHECK(S4("J", 1) == 1, "⑰ setup: J uploads a stash with 2 items and Chronicle 148"); - put_d2i("J", 0, 148, 2000); dm_cloud_conflict_take(); - CHECK(S4("J", 0) == 0 && dm_cloud_conflict_take() == 1, "⑰ wants to overwrite the 2-item copy with a freshly created empty file (0 items, 148) while running: blocked"); - CHECK(S4("J", 1) == 0, "⑰ blocked at startup too (no upload)"); - put_d2i("J", 0, 300, 3000); CHECK(S4("J", 0) == 1, "⑰ a stash emptied with a Chronicle longer than the fresh size: uploads as usual"); - // (18) The real rf3 shape: the cloud hardcore stash is another device's preset (stash_seed.h), and the local device has no hardcore stash file yet (startup sync runs before the preset is written) -> not blocked, no prompt; uploading after the local device writes the preset is not blocked either -#ifdef T_HAVE_STASH_SEED - { char cd5[1024]; snprintf(cd5, sizeof cd5, "%s/cloud5", R); mkdir(cd5, 0755); - #define S5(dev, down) (path(s3, dev, "save"), path(b3, dev, "bak"), path(t3, dev, "state"), dm_cloud_sync(s3, cd5, b3, t3, down, 0, NULL)) - #define HC "save/ModernSharedStashHardCoreV2/ModernSharedStashHardCoreV2.d2i" - put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 1000); put_d2i("K", 3, 400, 1000); CHECK(S5("K", 1) == 1, "⑱ setup: K uploads (the hardcore stash is the preset)"); - { char p5[1024]; path(p5, "K", HC); unlink(p5); } put_d2i("K", 3, 400, 2000); dm_cloud_conflict_take(); - S5("K", 0); CHECK(!dm_cloud_conflict_take(), "⑱ no local hardcore stash file yet, the cloud holds only the preset: nothing is lost, no notice"); - put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 3000); S5("K", 0); CHECK(!dm_cloud_conflict_take(), "⑱ the local device wrote the preset and uploads: not blocked"); - // (19) The reverse: the cloud has real content and the local copy is the preset -> still blocked - put_d2i("K", 5, 600, 4000); S5("K", 0); dm_cloud_conflict_take(); - { uint8_t big[700]; memset(big, 0, sizeof big); uint32_t l1 = 68, l2 = 300; memcpy(big, "\x55\xaa\x55\xaa", 4); memcpy(big + 16, &l1, 4); memcpy(big + 64, "JM", 2); big[66] = 9; memcpy(big + 68, "\x55\xaa\x55\xaa", 4); memcpy(big + 84, &l2, 4); big[132] = 0xc0; - put_bin("K", HC, big, 68 + 300, 5000); CHECK(S5("K", 0) == 1, "⑲ setup: hardcore stash with 9 items, Chronicle 300 uploads"); } - put_bin("K", HC, g_stash_seed, sizeof g_stash_seed, 6000); - CHECK(S5("K", 0) == 0 && dm_cloud_conflict_take() == 1, "⑲ the local copy reverted to the preset wants to overwrite the 9-item copy: blocked"); - #undef HC - #undef S5 - } -#else - printf("skipping ⑱ ⑲: no shared stash seed stash_seed.h (the public repository does not ship it)\n"); -#endif - #undef S4 - } - #undef D2I - #undef S3 - } - printf("t_cloudsync: %d checks, %d failed\n", checks, fails); - return fails != 0; -} diff --git a/dm/host/tests/t_condvar_stub.c b/dm/host/tests/t_condvar_stub.c index 48f0a9a..cbfe171 100644 --- a/dm/host/tests/t_condvar_stub.c +++ b/dm/host/tests/t_condvar_stub.c @@ -7,7 +7,6 @@ void guest_thread_init(void) {} void dm_thread_enter(uint64_t entry, uint64_t arg, uint64_t sp) { (void)entry; (void)arg; (void)sp; abort(); } void *guest_sym(const char *name) { (void)name; return NULL; } module_t *module_of(const void *p) { (void)p; return NULL; } -void dm_prof_add(int id, uint64_t ns, uint64_t bytes) { (void)id; (void)ns; (void)bytes; } #include <mach/mach_time.h> uint64_t now_ns(void) { static mach_timebase_info_data_t tb; if (!tb.denom) mach_timebase_info(&tb); return mach_absolute_time() * tb.numer / tb.denom; } uint64_t guest_ns(void) { return now_ns(); } diff --git a/dm/host/tests/t_doorspot.c b/dm/host/tests/t_doorspot.c deleted file mode 100644 index 9843eab..0000000 --- a/dm/host/tests/t_doorspot.c +++ /dev/null @@ -1,52 +0,0 @@ -// Unit test of the rift completion door landing points (dm_doorspot.h, takes seconds). The expected values are all computed by hand from the maps, without running the implementation under test. -// Situation: narrow maps of the Cave / Pit kind, the player standing against a wall, and the old rule's ideal point (10 cells with the back to the guardian, offset 5 cells left / right) lands inside a wall. -// Fail → pass: on the same map, the old rule's landing point is a blocked cell (old_spot_blocked); the new rule's landing point must be a reachable cell. -#include "dm_doorspot.h" -#include <stdio.h> - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) - -static struct ds_grid g; -static void fill(int v) { memset(g.blk, v, sizeof g.blk); } -static void rect(int x0, int y0, int x1, int y1, int v) { for (int y = y0; y <= y1; y++) for (int x = x0; x <= x1; x++) if (x >= -DS_R && x <= DS_R && y >= -DS_R && y <= DS_R) g.blk[(y + DS_R) * DS_W + x + DS_R] = (uint8_t)v; } -static int blk(int x, int y) { return ds_blk(&g, x + DS_R, y + DS_R); } -static int dist(int x, int y) { return ds_dist(&g, x + DS_R, y + DS_R); } - -int main(void) { - int x, y, t; - // ① Open ground: the ideal point itself qualifies → taken as is (wilderness maps do not degrade) - fill(0); ds_flood(&g); - t = ds_pick(&g, 12, -3, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && x == 12 && y == -3, "open ground should take the ideal point 12,-3 tier 0, got %d,%d tier %d", x, y, t); - t = ds_pick(&g, 2, 12, 1, 12, -3, 6, &x, &y); CHECK(t == 0 && x == 2 && y == 12, "open ground, second door should take the ideal point 2,12, got %d,%d tier %d", x, y, t); - CHECK(dist(12, -3) == 15, "open ground, 12,-3 is 15 steps away, computed %d", dist(12, -3)); - - // ② Horizontal corridor (y −4..4 walkable, the rest is wall), the player in the middle, the ideal point inside the wall straight below - fill(1); rect(-DS_R, -4, DS_R, 4, 0); ds_flood(&g); - CHECK(blk(5, 10) && blk(-5, 10), "control: the old rule's two landing points ±5,10 are inside the wall"); - // Cross footprint → the reachable y is −3..3; the 2 cells around all reachable → y −1..1. The nearest to (5,10) is (5,1) - t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && x == 5 && y == 1, "in a corridor should take 5,1 tier 0, got %d,%d tier %d", x, y, t); - CHECK(dist(x, y) >= 0 && !blk(x, y), "landing point must be reachable and not blocked"); - int x2, y2; t = ds_pick(&g, -5, 10, 1, x, y, 6, &x2, &y2); CHECK(t == 0 && x2 == -5 && y2 == 1, "in a corridor, second door should take -5,1, got %d,%d tier %d", x2, y2, t); - - // ③ There is a patch of open ground beyond the wall but it is not connected to the player: it must not be chosen - fill(1); rect(-DS_R, -4, DS_R, 4, 0); rect(-DS_R, 8, DS_R, 16, 0); ds_flood(&g); - CHECK(!blk(5, 10) && dist(5, 10) < 0, "control: 5,10 is not blocked but cannot be reached from the player"); - t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(dist(x, y) >= 0 && y <= 1, "open ground behind a wall must not be chosen, should stay on the player's side, got %d,%d", x, y); - - // ④ Narrow passage (3 cells wide: y −1..1): neither 5 × 5 nor 3 × 3 fits → tier 2, take the reachable cell nearest to the player (with the cross footprint only the row y = 0 is reachable, 1 cell from the player) - fill(1); rect(-DS_R, -1, DS_R, 1, 0); ds_flood(&g); - t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 2 && y == 0 && abs(x) == 1, "in a narrow passage should fall back to the nearest reachable cell at the feet (±1,0) tier 2, got %d,%d tier %d", x, y, t); - t = ds_pick(&g, -5, 10, 1, x, y, 6, &x2, &y2); CHECK(t == 2 && y2 == 0 && abs(x2 - x) >= 2 && dist(x2, y2) >= 0, "in a narrow passage, second door at least 2 cells from the first and reachable, got %d,%d tier %d", x2, y2, t); - - // ⑤ The player is boxed into a single cell: land at the feet - fill(1); rect(0, 0, 0, 0, 0); ds_flood(&g); - t = ds_pick(&g, 5, 10, 0, 0, 0, 6, &x, &y); CHECK(t == 3 && x == 0 && y == 0 && g.reach == 1, "when walled in, lands at the feet, tier 3, got %d,%d tier %d (%d cells reachable)", x, y, t, g.reach); - - // ⑥ Cells reached only by a long detour are not chosen: the ideal point is behind a long wall, and going around takes more than 36 steps - fill(0); rect(-20, 6, 20, 7, 1); ds_flood(&g); - CHECK(dist(0, 12) > DS_FAR, "control: 0,12 behind the wall takes %d steps (> %d)", dist(0, 12), DS_FAR); - t = ds_pick(&g, 0, 12, 0, 0, 0, 6, &x, &y); CHECK(t == 0 && abs(x) == 3 && y == 2, "in front of a long wall should take ±3,2 on this side of the wall (y = 2 is the row nearest the wall with all cells within 2 reachable; at least 3 cells from the player, so x = ±3), got %d,%d tier %d", x, y, t); - - printf("t_doorspot: %d checks, %d failed\n", checks, fails); return fails != 0; -} diff --git a/dm/host/tests/t_fs.c b/dm/host/tests/t_fs.c index 226070f..267a955 100644 --- a/dm/host/tests/t_fs.c +++ b/dm/host/tests/t_fs.c @@ -17,9 +17,8 @@ static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off); ssize_t pwrite(int, const void *, size_t, off_t); struct dm_flags g_flag; uint64_t g_t0_ns; uint64_t now_ns(void) { return 0; } // stubs for log timestamps and the read path (not used by the write path) -void dm_prof_add(int id, uint64_t ns, uint64_t b) { (void)id; (void)ns; (void)b; } void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; } +void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; } extern int g_nfail, g_lasterr; // front-end notification count (the strong implementation in t_fs_stub.c replaces the weak one in hle_fs.c) -void dm_fs_save_poison(int e); static int g_mode, g_calls; // 0 write normally; 1 always return 0; 2 write only 3 bytes the first time static ssize_t fake_pwrite(int fd, const void *buf, size_t n, off_t off) { g_calls++; @@ -41,29 +40,7 @@ static Result write_save(const char *path, const char *s) { // open -> } static int read_fs(const char *path, char *out, size_t n) { uint64_t h; size_t got = 0; if (h_fs_OpenFile(&h, path, MODE_READ)) return 0; Result r = h_fs_ReadFile(&got, h, 0, out, n - 1); h_fs_CloseFile(h); out[got] = 0; return !r; } -// Offset translation for zero-stripped archives (dm_data_pread): original content = a fixed byte sequence computed from position, with 0 filled in at zero-run positions; zero-stripped file = zero runs removed. -// Expected values are taken directly from the "original sequence", not through the implementation under test. Covers: crossing kept-run / zero-run boundaries, a whole read falling in a zero run, start and end, reading past end of file. -static int test_holes(void) { - enum { N = 100000 }; static uint8_t orig[N], got[N]; const long hole[3][2] = {{0, 1000}, {30000, 25000}, {99000, 1000}}; // one zero run each at the start, middle and end - for (long i = 0; i < N; i++) orig[i] = (uint8_t)(i * 131 + (i >> 8) * 7 + 1); - for (int h = 0; h < 3; h++) memset(orig + hole[h][0], 0, (size_t)hole[h][1]); - char path[] = "/tmp/dm-holes.XXXXXX"; int tfd = mkstemp(path); if (tfd < 0) return 1; char hp[64]; snprintf(hp, sizeof hp, "%s.holes", path); - long pos = 0; for (int h = 0; h <= 3; h++) { long end = h < 3 ? hole[h][0] : N; if (end > pos && write(tfd, orig + pos, (size_t)(end - pos)) != end - pos) return 1; if (h < 3) pos = hole[h][0] + hole[h][1]; } - close(tfd); FILE *f = fopen(hp, "w"); fprintf(f, "size %d\n", N); for (int h = 0; h < 3; h++) fprintf(f, "%ld %ld\n", hole[h][0], hole[h][1]); fclose(f); - int fd = open(path, O_RDONLY); dm_data_note_fd(fd, path); int bad = 0; - const long q[][2] = {{0, 500}, {500, 1000}, {999, 2}, {1000, 29000}, {29990, 20}, {30000, 25000}, {40000, 100}, {54990, 20}, {55000, 44000}, {98990, 1010}, {99500, 500}, {99990, 100}, {0, N}, {100000, 10}, {123456, 10}}; - for (size_t k = 0; k < sizeof q / sizeof *q; k++) { - long off = q[k][0], n = q[k][1], want = off >= N ? 0 : (off + n > N ? N - off : n); - memset(got, 0xAA, sizeof got); ssize_t r = dm_data_pread(fd, got, (size_t)n, off); - if (r != want || (want && memcmp(got, orig + off, (size_t)want))) { fprintf(stderr, "FAIL: zero-stripped read off=%ld n=%ld returned %zd (expected %ld)\n", off, n, r, want); bad++; } - } - if (dm_data_logical_size(path, 49000) != N) { fprintf(stderr, "FAIL: original size of the zero-stripped archive\n"); bad++; } - close(fd); g_hm[fd] = NULL; unlink(path); unlink(hp); - printf("%s zero-stripped archive offset translation: 15 kinds of read equal the original content byte for byte\n", bad ? "✗" : "✓"); - return bad; -} int main(void) { - if (test_holes()) return 1; char root[1024], save[1100], work[1200], p[1200]; const char *sp = getenv("TMPDIR"); snprintf(root, sizeof root, "%s/t_fs.XXXXXX", sp ? sp : "/private/tmp"); if (!mkdtemp(root)) { perror("mkdtemp"); return 1; } @@ -74,15 +51,6 @@ int main(void) { setenv("DM_ROMFS", rom, 1); setenv("DM_ROMFS_OVERLAY", overlay, 1); CHECK(h_fs_MountRom("rom", NULL, 0) == RES_OK, "mount RomFS with two layers (base + overlay)"); CHECK(read_fs("rom:/data/same", got, sizeof got) && !strcmp(got, "overlay"), "the overlay file wins"); CHECK(read_fs("rom:/data/base", got, sizeof got) && !strcmp(got, "only-base"), "falls back to the base when the overlay lacks the file"); uint64_t rh; CHECK(h_fs_OpenFile(&rh, "rom:/data/same", MODE_WRITE) == RES_FAIL, "the two-layer mount refuses writes"); - { // Find a file in the overlay (dm_overlay_file): two layers joined by a colon (old devices = in-bundle overlay + Legacy Device Pack), searched layer by layer with earlier ones taking priority; treating the whole string as one directory finds nothing - char l2[1100], two[2300], want[1300]; snprintf(l2, sizeof l2, "%s/pack", root); mkdir(l2, 0755); snprintf(two, sizeof two, "%s:%s", overlay, l2); setenv("DM_ROMFS_OVERLAY", two, 1); - snprintf(fn, sizeof fn, "%s/mark", overlay); put(fn, "1"); snprintf(fn, sizeof fn, "%s/only2", l2); put(fn, "2"); snprintf(fn, sizeof fn, "%s/mark", l2); put(fn, "2"); - snprintf(want, sizeof want, "%s/mark", overlay); CHECK(dm_overlay_file("mark", p, sizeof p) == 1 && !strcmp(p, want), "in both layers: the first layer's is taken"); - snprintf(want, sizeof want, "%s/only2", l2); CHECK(dm_overlay_file("only2", p, sizeof p) == 1 && !strcmp(p, want), "only in the second layer: the second layer's is taken"); - snprintf(want, sizeof want, "%s/data/over", overlay); CHECK(dm_overlay_file("data/over", p, sizeof p) == 1 && !strcmp(p, want), "relative path with a subdirectory"); - CHECK(dm_overlay_file("none", p, sizeof p) == 0 && !p[0], "in no layer: returns 0, output cleared"); - setenv("DM_ROMFS_OVERLAY", overlay, 1); snprintf(want, sizeof want, "%s/mark", overlay); CHECK(dm_overlay_file("mark", p, sizeof p) == 1 && !strcmp(p, want), "a single layer (newer devices) works as before"); - unsetenv("DM_ROMFS_OVERLAY"); CHECK(dm_overlay_file("mark", p, sizeof p) == 0, "no overlay set: returns 0"); setenv("DM_ROMFS_OVERLAY", overlay, 1); } g_nmnt = 0; snprintf(save, sizeof save, "%s/save", root); snprintf(work, sizeof work, "%s.work", save); mkdir(save, 0755); snprintf(p, sizeof p, "%s/a", save); put(p, "old"); @@ -138,7 +106,7 @@ int main(void) { // 6a Recloning after a refused commit fails: do not exit, the work directory is untouched and the committed version unchanged; the next commit is refused again and the reclone succeeds; the transaction after that commits normally setenv("DM_TEST_FS_FAILAT", "reclone:1", 1); CHECK(write_save("save:/a", "bad") == RES_OK, "write a=bad"); - dm_fs_save_poison(ENOSPC); int nf0 = g_nfail; + save_poison(ENOSPC); int nf0 = g_nfail; // a save write of this transaction failed CHECK(h_fs_Commit("save") == RES_FAIL && g_nfail == nf0 + 1, "this transaction is refused (injected reclone failure), the process is still alive, front end notified once"); snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "new"), "committed directory a is still new"); snprintf(p, sizeof p, "%s/a", work); CHECK(same(p, "bad"), "work directory not deleted (a is still bad)"); @@ -169,18 +137,6 @@ int main(void) { snprintf(p, sizeof p, "%s/a", save); CHECK(same(p, "ok3"), "committed a = ok3"); - // Language switch rewriting the settings file (settings_lang_apply): expected values computed by hand from the rule -- only when it "does not match the switch" are Language / VoiceLanguage changed to 0 (English) or 8 (Simplified Chinese); other keys and other languages are untouched - { extern int g_t_lang; char sd[1200], sf[1300], gotj[512]; snprintf(sd, sizeof sd, "%s/langt", root); mkdir(sd, 0755); snprintf(sf, sizeof sf, "%s/Settings", sd); mkdir(sf, 0755); snprintf(sf, sizeof sf, "%s/Settings/Settings.json", sd); - #define LANGCASE(dm, in, want, what) do { g_t_lang = (dm); put(sf, (in)); settings_lang_apply(sd); FILE *lf = fopen(sf, "rb"); size_t ln_ = lf ? fread(gotj, 1, sizeof gotj - 1, lf) : 0; if (lf) fclose(lf); gotj[ln_] = 0; CHECK(!strcmp(gotj, (want)), "language switch: %s", (what)); } while (0) - LANGCASE(-1, "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "Apply never tapped: not a byte changes"); - LANGCASE(1, "{ \"A\": 1, \"Language\": 8, \"VoiceLanguage\": 8, \"B\": 2 }", "{ \"A\": 1, \"Language\": 0, \"VoiceLanguage\": 0, \"B\": 2 }", "switch = English, file is Simplified Chinese: text and voice both become 0, other keys untouched"); - LANGCASE(0, "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "{ \"Language\": 8, \"VoiceLanguage\": 8 }", "switch = Chinese, file is English: both become 8"); - LANGCASE(0, "{ \"Language\": 9, \"VoiceLanguage\": 9 }", "{ \"Language\": 9, \"VoiceLanguage\": 9 }", "switch = Chinese, the player chose Traditional Chinese in the original game: not changed back to Simplified Chinese"); - LANGCASE(1, "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "switch = English, file already English: untouched"); - LANGCASE(1, "{ \"Language\": 0, \"VoiceLanguage\": 8 }", "{ \"Language\": 0, \"VoiceLanguage\": 0 }", "switch = English, text already English but voice still Chinese: the voice follows"); - LANGCASE(1, "{ \"Language\": 12 }", "{\n \"VoiceLanguage\": 0, \"Language\": 0 }", "switch = English, two-digit language number and no voice key: rewritten and the voice key added"); - g_t_lang = -1; } - char cmd[1200]; snprintf(cmd, sizeof cmd, "rm -rf '%s'", root); system(cmd); printf("== %d checks, %d failed ==\n", checks, fails); return fails != 0; diff --git a/dm/host/tests/t_fs_case.c b/dm/host/tests/t_fs_case.c index 5447801..d40a543 100644 --- a/dm/host/tests/t_fs_case.c +++ b/dm/host/tests/t_fs_case.c @@ -15,7 +15,6 @@ static int fake_setxattr(const char *p, const char *name, const void *v, size_t #undef setxattr struct dm_flags g_flag; uint64_t g_t0_ns; uint64_t now_ns(void) { return 0; } -void dm_prof_add(int id, uint64_t ns, uint64_t b) { (void)id; (void)ns; (void)b; } void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; } static int checks, fails; #define CHECK(c, text) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", text); } while (0) @@ -42,7 +41,6 @@ static int entry_count(void) { } static void *create_race(void *p) { return (void *)(uintptr_t)h_fs_CreateFile(p, 0); } module_t g_mod[MAX_MOD]; int g_nmod; // Module table (MountSave picks save fixes by main build ID): this unit test loads no modules -int dm_custom_lang_explicit(void) { return -1; } // Language switch (dm_custom.c): not involved in this unit test, treated as the player never having tapped it int main(void) { char root[1024], save[1024], p[1100]; const char *tmp = getenv("TMPDIR"); @@ -113,3 +111,5 @@ int main(void) { rm_tree(root); printf("== case: %d checks, %d failed ==\n", checks, fails); return fails != 0; } +// the guest address space (hle_svc.c): the unit test has none, so every stack is a host stack (hle_bigstack.c) +int dm_guest_as_contains(uintptr_t a) { (void)a; return 0; } diff --git a/dm/host/tests/t_fs_stub.c b/dm/host/tests/t_fs_stub.c index 26fc98e..7eb1bfa 100644 --- a/dm/host/tests/t_fs_stub.c +++ b/dm/host/tests/t_fs_stub.c @@ -2,10 +2,10 @@ int g_nfail, g_lasterr; void front_save_failed(int err) { g_nfail++; g_lasterr = err; } -int g_t_lang = -1; // Value of the language switch (lang_en in dm_custom.c) in the settings file, changed by the cases in t_fs.c: -1 never tapped / 0 Simplified Chinese / 1 English -int dm_custom_lang_explicit(void) { return g_t_lang; } - -// the loader's module table: h_fs_MountSave picks the save directory by main's build ID (dm.h dm_main_build); the unit test has no modules, so the table is empty +// the loader's module table (dm.h): the unit test has no modules, so the table is empty #include "../dm.h" module_t g_mod[MAX_MOD]; int g_nmod; +// the guest address space (hle_svc.c): the unit test has none, so every stack is a host stack and the big-stack takeovers run on it directly (hle_bigstack.c) +#include <stdint.h> +int dm_guest_as_contains(uintptr_t a) { (void)a; return 0; } diff --git a/dm/host/tests/t_hid_receipt.c b/dm/host/tests/t_hid_receipt.c deleted file mode 100644 index 3be847c..0000000 --- a/dm/host/tests/t_hid_receipt.c +++ /dev/null @@ -1,58 +0,0 @@ -// The actual dm_input_set -> GetNpadState return takes the sample; the fake clock only fixes the timestamp and does not replace the input/return layout. -#ifndef HLE_HID_SOURCE -#define HLE_HID_SOURCE "../hle_hid.c" -#endif -#include HLE_HID_SOURCE -module_t g_mod[MAX_MOD]; int g_nmod; -#include <unistd.h> -#include <sys/wait.h> -static uint64_t clock_ns=1000000000ull; -uint64_t dm_guest_frame(void) { return 0; } -uint64_t dm_server_tick(void) { return UINT64_MAX; } -int dm_loadscreen_up(void) { return 0; } -void dm_hold_for_loadscreen(void) {} -uint64_t now_ns(void) { return clock_ns; } -uint64_t guest_ns(void) { return clock_ns; } -static int opmode=1; -int dm_opmode(void) { return opmode; } -int touch_fill(void *p, uint64_t n) { (void)p;(void)n;return 0; } -void dm_touch_set(const dm_touch *p,int n) { (void)p;(void)n; } -void uimsg_subscribe(void (*f)(const char *,const char *,const char *)) { (void)f; } -static int checks,fails; -#define CK(c) do { checks++; if (!(c)) { fails++;printf("FAIL line=%d %s\n",__LINE__,#c); } } while(0) -typedef struct { long long sampling; unsigned long long start,time,buttons,script; unsigned id,style,attrs; int lx,ly,rx,ry; } Row; -static int load(const char *path,Row *rows,int *limit) { - FILE *f=fopen(path,"r");if(!f)return -1;char s[512];int n=0;*limit=0; - while(fgets(s,sizeof s,f)) { if(strstr(s,"limit_reached")){(*limit)++;continue;} Row r={0}; - int k=sscanf(s,"{\"event\":\"npad_return\",\"read_start_ns\":%llu,\"time_ns\":%llu,\"sampling\":%lld,\"id\":%u,\"style\":%u,\"buttons\":%llu,\"lx\":%d,\"ly\":%d,\"rx\":%d,\"ry\":%d,\"attributes\":%u,\"script_mask\":%llu}",&r.start,&r.time,&r.sampling,&r.id,&r.style,&r.buttons,&r.lx,&r.ly,&r.rx,&r.ry,&r.attrs,&r.script); - CK(k==12);CK(r.start<=r.time);if(n<1024)rows[n]=r;n++; - }fclose(f);return n; -} -static void exercise(int which) { - char path[256];snprintf(path,sizeof path,"%s/hid-receipt-%d.jsonl",getenv("TMPDIR")?getenv("TMPDIR"):"/tmp",getpid());unlink(path); - unsetenv("DM_KEYS");unsetenv("DM_HID_RECEIPT_LOG");if(which)setenv("DM_HID_RECEIPT_LOG",path,1);if(which==2)setenv("DM_KEYS","A@0+10",1); - NpadState s;uint32_t id=0;Row *rows=calloc(1024,sizeof *rows);int limit=0,n; - if(which==4) { - dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);h_hid_GetFullKey(&s,&id); - clock_ns+=1000000000ull;h_hid_GetFullKey(&s,&id); - n=load(path,rows,&limit);CK(n==2);if(n==2){CK(rows[0].sampling==1&&rows[1].sampling==3);CK(rows[1].time-rows[0].time==1000000000ull);CK(!rows[0].buttons&&!rows[1].buttons);}CK(limit==0); - } else if(which==3) { - for(int i=0;i<1030;i++){dm_input_set(0,(i&1)?32767:-32767,0,0,0);h_hid_GetFullKey(&s,&id);CK(s.lx==((i&1)?32767:-32767));} - n=load(path,rows,&limit);CK(n==1024);CK(limit==1); - } else if(which==2) { - dm_input_set(DM_BTN_B,123,-456,7,-8);h_hid_GetFullKey(&s,&id);CK(s.buttons==(DM_BTN_A|DM_BTN_B));CK(s.lx==123&&s.ly==-456); - n=load(path,rows,&limit);CK(n==1);if(n==1){CK(rows[0].buttons==s.buttons);CK(rows[0].script==DM_BTN_A);CK(rows[0].lx==123&&rows[0].ly==-456&&rows[0].rx==7&&rows[0].ry==-8);}CK(limit==0); - } else { - dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);CK(s.sampling==1&&s.attributes==1); - dm_input_set(DM_BTN_B,32767,-32767,11,-12);h_hid_GetFullKey(&s,&id);CK(s.buttons==DM_BTN_B&&s.lx==32767&&s.ly==-32767&&s.rx==11&&s.ry==-12); - h_hid_GetFullKey(&s,&id);CK(s.sampling==3); - dm_input_set(0,0,0,0,0);h_hid_GetFullKey(&s,&id);CK(!s.buttons&&!s.lx&&!s.ly); - opmode=0;id=0x20;dm_input_set(DM_BTN_Y,-32767,0,0,0);h_hid_GetHandheld(&s,&id);CK(s.attributes==1&&s.buttons==DM_BTN_Y&&s.lx==-32767); - id=0;h_hid_GetFullKey(&s,&id);CK(s.attributes==0&&!s.buttons&&!s.lx&&s.sampling==6); - n=load(path,rows,&limit); - if(!which)CK(n==-1); - else {CK(n==5);CK(limit==0);if(n==5){CK(rows[0].sampling==1&&rows[0].time==clock_ns&&rows[0].buttons==0);CK(rows[1].sampling==2&&rows[1].buttons==DM_BTN_B&&rows[1].lx==32767&&rows[1].ry==-12);CK(rows[2].sampling==4&&rows[2].buttons==0&&rows[2].lx==0);CK(rows[3].id==0x20&&rows[3].style==STYLE_HANDHELD&&rows[3].attrs==1&&rows[3].lx==-32767);CK(rows[4].id==0&&rows[4].style==STYLE_FULLKEY&&rows[4].attrs==0&&rows[4].sampling==6);}} - } - free(rows);unlink(path);printf("HID_RECEIPT case=%d checks=%d failures=%d\n",which,checks,fails);fflush(stdout);_exit(fails?1:0); -} -int main(void){int fail=0;for(int i=0;i<5;i++){pid_t p=fork();if(!p)exercise(i);int s;waitpid(p,&s,0);fail+=!WIFEXITED(s)||WEXITSTATUS(s)!=0;}printf("HID_RECEIPT %s cases=5 failures=%d\n",fail?"FAIL":"PASS",fail);return !!fail;} diff --git a/dm/host/tests/t_hid_script.c b/dm/host/tests/t_hid_script.c deleted file mode 100644 index c8e5dab..0000000 --- a/dm/host/tests/t_hid_script.c +++ /dev/null @@ -1,144 +0,0 @@ -// HID script unit test: a fake clock advances frame by frame, with real DM_KEYS parsing and the real GetNpad path; no sleeping, no skipping steps by duration. -// Begin is action start/repeat trigger and End marks action end: two Begins need only one End of the same action. -#ifndef HLE_HID_SOURCE -#define HLE_HID_SOURCE "../hle_hid.c" -#endif -#include <signal.h> -#include <sys/wait.h> -#include <unistd.h> -#include HLE_HID_SOURCE -module_t g_mod[MAX_MOD]; int g_nmod; -static uint64_t clock_ns = 1000000000ull; -static uint64_t guest_frame; -static uint64_t server_tick; -static int server_available; -uint64_t now_ns(void) { return clock_ns; } -uint64_t guest_ns(void) { return clock_ns; } -uint64_t dm_guest_frame(void) { return guest_frame; } -uint64_t dm_server_tick(void) { return server_available ? server_tick : UINT64_MAX; } -int touch_fill(void *out, uint64_t max) { (void)out; (void)max; return 0; } -static int test_loadscreen_up, hold_requests; -int dm_loadscreen_up(void) { return test_loadscreen_up; } // the real check is in diag.c; the unit test sets it by hand -void dm_hold_for_loadscreen(void) { hold_requests++; } // the real implementation freezes the guest clock; the unit test only counts requests -int dm_opmode(void) { return 1; } -void dm_touch_set(const dm_touch *t, int n) { (void)t; (void)n; } -static void (*ui_cb)(const char *, const char *, const char *); -void uimsg_subscribe(void (*f)(const char *, const char *, const char *)) { ui_cb = f; } -static int checks, fails; -#define CHECK(c, s) do { checks++; int ok = !!(c); fails += !ok; printf("%s %s\n", ok ? "✓" : "✗", s); } while (0) -static void setup(const char *script) { - memset(g_steps, 0, sizeof g_steps); memset(g_keys, 0, sizeof g_keys); - g_nsteps = g_nkeys = g_kcur = g_ktries = g_kbeg = g_kend = 0; g_kstate = KS_PRE; g_krel = 1; - clock_ns = 1000000000ull; guest_frame = 0; server_tick = 0; server_available = 1; setenv("DM_KEYS", script, 1); keys_parse(); -} -static void setup_pregame(const char *script) { - memset(g_steps, 0, sizeof g_steps); memset(g_keys, 0, sizeof g_keys); - g_nsteps = g_nkeys = g_kcur = g_ktries = g_kbeg = g_kend = 0; g_kstate = KS_PRE; g_krel = 1; - clock_ns = 1000000000ull; guest_frame = 0; server_tick = 0; server_available = 0; setenv("DM_KEYS", script, 1); keys_parse(); -} -static void ui(const char *act) { ui_cb("InputMessage", act, ""); } -static NpadState read_state(void) { NpadState s; fill(&s, 0, STYLE_FULLKEY); return s; } -static int neutral(NpadState s) { return !s.buttons && !s.lx && !s.ly; } -static void release(void) { guest_frame += ACTION_HOLD_TICKS; server_tick += ACTION_HOLD_TICKS; read_state(); } -static void frames(uint64_t n) { guest_frame += n; server_tick += n; read_state(); } -int main(int argc, char **argv) { - unsetenv("DM_KEYS"); hid_keys_init(); - setup("LSL@+0.05,LSU@+0.05"); - NpadState s = read_state(); CHECK(s.lx == -32767 && s.ly == 0 && s.buttons == 0, "first step LSL goes into the real controller layout, not into the button bits"); - ui("ControllerActionBegin"); ui("ControllerActionBegin"); release(); - CHECK(neutral(read_state()), "with two Begins and no End yet, the next step is still held back"); - ui("ControllerActionEnd"); s = read_state(); - CHECK(s.ly == 32767 && s.lx == 0, "after two repeated Begins / one End, the next step LSU goes ahead"); - - setup("A@+0.05,B@+0.05"); CHECK(read_state().buttons == DM_BTN_A, "plain A pressed"); - ui("ControllerActionBegin"); release(); - CHECK(neutral(read_state()), "a single Begin without End holds back the next key"); - ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "B is sent only after End, no key swallowed"); - - setup("A@+0.6"); CHECK(read_state().buttons == DM_BTN_A, "hold duration test: A pressed first"); - clock_ns += 60000000000ull; guest_frame += 15 - 1; server_tick += 15 - 1; // +0.6 seconds = 0.6*25 = 15 ticks - CHECK(read_state().buttons == DM_BTN_A, "wall clock jumps but the guest frame is not due: A still held"); - guest_frame++; server_tick++; CHECK(neutral(read_state()), "A released only once the server tick is due"); - - setup("LSL@+0.05,LSU@+0.05"); read_state(); release(); - CHECK(read_state().ly == 32767, "no Begin in the run: idle on release, no wait for an End that never comes"); - - setup("A@+0.05,B@+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionEnd"); ui("ControllerActionBegin"); release(); - CHECK(neutral(read_state()), "a new Begin after End: busy again, the old End does not release the new action"); - ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "goes ahead after the new action's own End"); - - setup("A@+0.05,B@+0.05"); read_state(); ui("ControllerActionEnd"); ui("ControllerActionEnd"); ui("ControllerActionBegin"); release(); - CHECK(neutral(read_state()), "surplus Ends that came first do not cancel a later Begin"); - ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "continues once the later Begin has ended"); - - setup("A@>OpenPanel:Done,B@+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionBegin"); - ui_cb("PanelManager", "OpenPanel", "Done"); - CHECK(neutral(read_state()), "a confirm while held releases, but with the action not ended it still waits for idle"); - ui("ControllerActionEnd"); CHECK(read_state().buttons == DM_BTN_B, "repeated Begins after the confirm need only one End, then B goes"); - - setup("A@+0.05,B@%0.2+0.05"); read_state(); ui("ControllerActionBegin"); ui("ControllerActionBegin"); release(); ui("ControllerActionEnd"); - CHECK(neutral(read_state()), "End does not skip the script's own delay"); - frames(4); CHECK(neutral(read_state()), "server delay at 4 of 5 ticks: next key held back"); - frames(1); CHECK(read_state().buttons == DM_BTN_B, "next key only once the server delay reaches 5 ticks and the action has ended"); - release(); CHECK(neutral(read_state()), "no extra key press after the last step is released"); - - setup("A@>Never"); read_state(); release(); - frames(OK_WAIT_TICKS - ACTION_HOLD_TICKS - 1); CHECK(g_ktries == 1 && neutral(read_state()), "no re-press before the confirm wait reaches 150 server ticks"); - frames(1); CHECK(g_ktries == 2 && read_state().buttons == DM_BTN_A, "re-press only after a confirm wait of 150 server ticks"); - - setup_pregame("A@%0.2"); read_state(); frames(11); CHECK(neutral(read_state()), "main menu presented-frame delay at 11/12 frames: no early press"); - frames(1); CHECK(read_state().buttons == DM_BTN_A, "main menu presented-frame delay: press only after 12 frames"); - - pid_t short_hold = fork(); - if (!short_hold) { setup("A@+0.01"); _exit(read_state().buttons == 0 ? 0 : 99); } - int short_status; waitpid(short_hold, &short_status, 0); - CHECK(WIFEXITED(short_status) && WEXITSTATUS(short_status) == 0, "counterexample: held less than 1 server tick, the real reading counts as not sampled"); - - pid_t child = fork(); - if (!child) { setup("A@>Never"); read_state(); release(); clock_ns += 60000000000ull; read_state(); _exit(99); } - int status; waitpid(child, &status, 0); - CHECK(WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT, "guest frames stopped but 60 s of wall clock: only the fallback prints a [RED] line"); - - setup("A@Start>OpenPanel:Done,B@>OpenPanel:Next"); - ui_cb("PanelManager", "OpenPanel", "Done"); ui_cb("PanelManager", "OpenPanel", "Start"); - CHECK(read_state().buttons == DM_BTN_A && g_kcur == 0, "an old confirm from before the press does not complete the current step"); - ui("ControllerActionBegin"); ui("ControllerActionBegin"); ui_cb("PanelManager", "OpenPanel", "Done"); - ui_cb("PanelManager", "OpenPanel", "Next"); - CHECK(neutral(read_state()), "an old confirm before the next step is pressed cannot skip a step, still waits for the previous action's End"); - ui("ControllerActionEnd"); - CHECK(read_state().buttons == DM_BTN_B && g_kcur == 1, "after the previous End the next step really presses B, the old Next confirm does not count as completion"); - ui_cb("PanelManager", "OpenPanel", "Next"); - CHECK(neutral(read_state()) && g_kcur == 2, "only a fresh confirm after the next step is pressed completes it and releases"); - if (argc > 1) { - FILE *f = fopen(argv[1], "r"); if (!f) { perror("trace"); return 2; } - setup("LSL@+8,LSU@+8"); read_state(); char line[2048]; int entered = 0, nb = 0, ne = 0; - while (fgets(line, sizeof line, f)) { - if (strstr(line, "hid script step 3 pressed")) entered = 1; - if (!entered) continue; - double t; - if (sscanf(line, "[dm] UIMSG [%lfs]", &t) != 1) continue; - int beg = strstr(line, "InputMessage ControllerActionBegin") != NULL; - int end = strstr(line, "InputMessage ControllerActionEnd") != NULL; - if (!beg && !end) continue; - clock_ns = 1000000000ull + (uint64_t)((t - 16.13) * 1e9); read_state(); - ui(beg ? "ControllerActionBegin" : "ControllerActionEnd"); nb += beg; ne += end; - if (end) break; - } - fclose(f); s = read_state(); - CHECK(nb == 73 && ne == 1 && s.ly == 32767, "iOS original step 3 trace, 73 Begin / 1 End replayed, then enters LSU (fake clock advanced by message times)"); - } - // During a tick freeze (DM_TICK_HOLD) the tick number does not advance: the precondition was seen under the in-game tick clock, then the host reports "no tick", and %1 must be finished by presented frames instead, 60 frames (host_savecmp waited 451 s dead) - setup("A@OpenPanel:PauseLayout%1>PausePanelMessage:ExitGame"); server_tick = 401; guest_frame = 1000; - ui_cb("PanelManager", "OpenPanel", "PauseLayout"); CHECK(neutral(read_state()), "pre seen before the freeze, delay not elapsed: no press"); - server_available = 0; read_state(); - guest_frame += 59; CHECK(neutral(read_state()), "counted in frames after the freeze: 59 frames, under 1 second, no press"); - guest_frame += 1; CHECK(read_state().buttons == DM_BTN_A, "counted in frames after the freeze: A pressed at 60 frames (1 second), no endless wait on the stopped tick number"); - // An in-game key press that comes due while the loading screen is still up: do not press, request a tick freeze; press only after it is down (screenshot gate NPC page: the A at tick 84 was swallowed by the loading screen) - setup("A@DM:Tick:84#>OpenPanel:NpcDialogPanel"); server_tick = 84; test_loadscreen_up = 1; hold_requests = 0; - ui_cb("DM", "Tick", "84#"); CHECK(neutral(read_state()) && hold_requests == 1, "loading screen up: no A even though the pre has arrived, and a tick freeze is requested"); - frames(30); CHECK(neutral(read_state()) && hold_requests >= 2, "loading screen still up: still no press"); - test_loadscreen_up = 0; hold_requests = 0; CHECK(read_state().buttons == DM_BTN_A && hold_requests == 0, "loading screen down: A pressed, no more freeze requests"); - setup("A@DM:Tick:84#>OpenPanel:NpcDialogPanel"); server_tick = 84; test_loadscreen_up = 0; hold_requests = 0; - ui_cb("DM", "Tick", "84#"); CHECK(read_state().buttons == DM_BTN_A && hold_requests == 0, "loading screen already down: press when due, no freeze request (timing unchanged)"); - printf("== HID script %d checks, %d failed ==\n", checks, fails); return fails != 0; -} diff --git a/dm/host/tests/t_hud_dead.c b/dm/host/tests/t_hud_dead.c deleted file mode 100644 index 961c8e7..0000000 --- a/dm/host/tests/t_hud_dead.c +++ /dev/null @@ -1,27 +0,0 @@ -// Unit test for hiding the bottom bar on death: mode -> dead, bottom bar show/hide decision (dm_hud.h). Expected values are hand-written from the game code: main+0x6e5c48 player mode 0 / 0x11 means dead -#include <stdio.h> -#include "dm_hud.h" -static int fails, cases; -static void expect(const char *what, int got, int want) { - cases++; if (got == want) printf("✓ %s\n", what); else { fails++; printf("✗ %s: got %d, expected %d\n", what, got, want); } -} -int main(void) { - char buf[64]; - for (unsigned m = 0; m <= 19; m++) { // D2 player modes 0..19 (DEATH ... KNOCKBACK) - snprintf(buf, sizeof buf, "mode %u %s", m, (m == 0 || m == 17) ? "→ dead" : "→ alive"); - expect(buf, dm_hud_player_mode_dead(m), m == 0 || m == 17); - } - expect("mode 0xffffffff (out of range) → alive", dm_hud_player_mode_dead(0xffffffffu), 0); - dm_hud_info h = {0}; h.valid = 1; - for (unsigned m = 0; m <= 19; m++) { - h.dead = dm_hud_player_mode_dead(m); - snprintf(buf, sizeof buf, "normal in-game state, mode %u → bottom bar %s", m, (m == 0 || m == 17) ? "hidden" : "shown"); - expect(buf, dm_hud_bar_visible(&h, 0, 0), !(m == 0 || m == 17)); - } - h.dead = 0; - expect("alive but in navigation state → hidden", dm_hud_bar_visible(&h, 1, 0), 0); - expect("alive but covered by a panel → hidden", dm_hud_bar_visible(&h, 0, 1), 0); - h.valid = 0; expect("player not read → hidden", dm_hud_bar_visible(&h, 0, 0), 0); - printf("%s t_hud_dead %d/%d\n", fails ? "FAIL" : "OK", cases - fails, cases); - return fails != 0; -} diff --git a/dm/host/tests/t_i18n.c b/dm/host/tests/t_i18n.c deleted file mode 100644 index 25b47a2..0000000 --- a/dm/host/tests/t_i18n.c +++ /dev/null @@ -1,40 +0,0 @@ -// Language source of dm_i18n.c: once the game has set the language (the SetLanguage observation gate writes g_dm_lang), that value -// wins; if not, read "Language" from Settings/Settings.json in the save data; -// no settings file = first install, use the system language (simulated in tests with DM_LANG). Our own UI text (dm_ui, for UIKit text) is English -// in every language. -// Expected values come from the rule itself (for the language index table see the name table at main+0x20ae478 in diag.c), without running the game. -// Each case needs its own process (the file is read once and then cached), so this program runs one case per argv and the Makefile invokes them one -// by one. -#include "../dm_i18n.h" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/stat.h> -static int fails; -static void expect(const char *what, const char *got, const char *want) { int ok = !strcmp(got, want); if (!ok) fails++; printf("%s %s: expected '%s' → got '%s'\n", ok ? "✓" : "✗", what, want, got); } -int main(int argc, char **argv) { - const char *mode = argc > 1 ? argv[1] : ""; - char dir[512]; snprintf(dir, sizeof dir, "%s/i18n_%s", getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", mode); - char sd[600], f[700]; snprintf(sd, sizeof sd, "%s/Settings", dir); snprintf(f, sizeof f, "%s/Settings.json", sd); - mkdir(dir, 0755); mkdir(sd, 0755); remove(f); setenv("DM_SAVE", dir, 1); - if (!strcmp(mode, "en") || !strcmp(mode, "game")) { FILE *o = fopen(f, "w"); fputs("{\n \"Item Name Display\": 1,\n \"Language\": 0,\n \"AutoMapMode\": 2\n}\n", o); fclose(o); } - if (!strcmp(mode, "zh")) { FILE *o = fopen(f, "w"); fputs("{ \"Language\": 8 }", o); fclose(o); } - if (!strcmp(mode, "en")) expect("settings file Language 0, game has not set it", dm_ui("dmConfirm"), "OK"); - else if (!strcmp(mode, "zh")) expect("settings file Language 8 (our text stays English)", dm_ui("dmConfirm"), "OK"); - else if (!strcmp(mode, "none")) { unsetenv("DM_LANG"); expect("no settings file, no system language given (Mac default English)", dm_ui("dmConfirm"), "OK"); } - else if (!strcmp(mode, "sysen")) { setenv("DM_LANG", "en-US", 1); expect("no settings file (first install), system language en-US", dm_ui("dmConfirm"), "OK"); } - else if (!strcmp(mode, "systw")) { setenv("DM_LANG", "zh-Hant-TW", 1); expect("no settings file, system language zh-Hant-TW (our text stays English)", dm_ui("dmConfirm"), "OK"); } - else if (!strcmp(mode, "tags")) { // language tag → the original game's language index - static const struct { const char *t; int want; } c[] = { { "en-US", 0 }, { "en-GB", 0 }, { "zh-Hans-CN", 8 }, { "zh-Hans", 8 }, { "zh", 8 }, { "zh-Hans-HK", 8 }, { "zh-Hans-TW", 8 }, { "zh-Hans-MO", 8 }, - { "zh-Hant-TW", 8 }, { "zh-Hant-HK", 8 }, { "zh-Hant", 8 }, { "zh-HK", 8 }, { "zh_TW", 8 }, { "yue-Hant-HK", 8 }, { "yue-Hans-CN", 8 }, { "yue", 8 }, { "wuu-Hans-CN", 8 }, - { "de-DE", 0 }, { "es-ES", 0 }, { "es-419", 0 }, { "fr-CA", 0 }, { "it-IT", 0 }, { "ko-KR", 0 }, { "pl-PL", 0 }, { "ru-RU", 0 }, { "ja-JP", 0 }, { "pt-BR", 0 }, { "nl-NL", 0 }, { "ar", 0 }, { "zu-ZA", 0 } }; - for (unsigned i = 0; i < sizeof c / sizeof *c; i++) { char w[8], g[8]; snprintf(w, sizeof w, "%d", c[i].want); snprintf(g, sizeof g, "%d", dm_lang_from_tag(c[i].t)); expect(c[i].t, g, w); } } - else if (!strcmp(mode, "game")) { // the settings file says English, then the game switches to Simplified Chinese, zhTW (9) and deDE: our text stays English throughout - expect("before the game sets it, the settings file decides", dm_ui("dmConfirm"), "OK"); - atomic_store(&g_dm_lang, 8); expect("game switches to zhCN (our text stays English)", dm_ui("dmConfirm"), "OK"); - atomic_store(&g_dm_lang, 9); expect("game switches to zhTW (our text stays English)", dm_ui("dmConfirm"), "OK"); - atomic_store(&g_dm_lang, 1); expect("game switches to deDE (English)", dm_ui("dmConfirm"), "OK"); - expect("a key missing from the table comes back as it is", dm_ui("dmNoSuchKey"), "dmNoSuchKey"); } - else { fprintf(stderr, "usage: t_i18n en|zh|none|sysen|systw|tags|game\n"); return 2; } - return fails ? 1 : 0; -} diff --git a/dm/host/tests/t_modetx.c b/dm/host/tests/t_modetx.c deleted file mode 100644 index 849425b..0000000 --- a/dm/host/tests/t_modetx.c +++ /dev/null @@ -1,35 +0,0 @@ -// Unit test of the controller / touch switch transaction (takes seconds): the state machine and input gate of dm_modetx.h, without touching the game. Expected values are all computed by hand from the rules: -// rebuild complete = SETTLE consecutive frames without movement after the hint panel reopen count has moved; if that has not happened within TIMEOUT frames, proceed as a timeout; keys that were held during the freeze are passed to the game only after they have been released once following the unfreeze. -#include <stdio.h> -#include "../dm_modetx.h" -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) -int main(void) { - { dm_modetx t = {0}; uint64_t f = 100; // One normal switch: touch (0) → controller (1) - CHECK(dm_modetx_step(&t, f, -1, 0, 7) == DM_TXA_NONE, "no request: must not move"); - CHECK(dm_modetx_step(&t, f, 0, 0, 7) == DM_TXA_NONE, "requested mode is the current one: must not move"); - CHECK(dm_modetx_step(&t, f, 1, 0, 7) == DM_TXA_BEGIN && t.st == DM_TX_REBUILD1, "request for the other mode should begin"); - for (int i = 1; i <= 10; i++) CHECK(dm_modetx_step(&t, f + i, 1, 0, 7) == DM_TXA_NONE, "must not proceed before the hint panel reopens (frame %d)", i); - CHECK(dm_modetx_step(&t, f + 11, 1, 0, 8) == DM_TXA_NONE, "the frame of the reopen does not count as done"); - CHECK(dm_modetx_step(&t, f + 13, 1, 0, 9) == DM_TXA_NONE, "reopened again: restart the settle count"); - CHECK(dm_modetx_step(&t, f + 16, 1, 0, 9) == DM_TXA_NONE, "3 settled frames are not enough"); - CHECK(dm_modetx_step(&t, f + 17, 1, 0, 9) == DM_TXA_REPORT_BACK && t.st == DM_TX_REBUILD2, "4 settled frames → report back the original run mode"); - CHECK(dm_modetx_step(&t, f + 30, 0, 1, 9) == DM_TXA_NONE, "a reverse request during the transaction does not interrupt it"); - CHECK(dm_modetx_step(&t, f + 31, 0, 1, 10) == DM_TXA_NONE, "second rebuild just started"); - CHECK(dm_modetx_step(&t, f + 35, 0, 1, 10) == DM_TXA_DONE && t.st == DM_TX_IDLE && !t.timeouts, "second rebuild settled → done, no timeout"); - CHECK(dm_modetx_step(&t, f + 36, 0, 1, 10) == DM_TXA_BEGIN, "the frame after completion handles the queued reverse request"); } - { dm_modetx t = {0}; // Timeout: the hint panel never reopens - CHECK(dm_modetx_step(&t, 0, 1, 0, 0) == DM_TXA_BEGIN, "begin"); - CHECK(dm_modetx_step(&t, DM_MODETX_TIMEOUT - 1, 1, 0, 0) == DM_TXA_NONE, "1 frame short: no timeout"); - CHECK(dm_modetx_step(&t, DM_MODETX_TIMEOUT, 1, 0, 0) == DM_TXA_REPORT_BACK && t.timeouts == 1, "at the deadline proceed as a timeout and count it"); - CHECK(dm_modetx_step(&t, 2 * DM_MODETX_TIMEOUT - 1, 1, 0, 0) == DM_TXA_NONE, "second stage restarts the timer"); - CHECK(dm_modetx_step(&t, 2 * DM_MODETX_TIMEOUT, 1, 0, 0) == DM_TXA_DONE && t.timeouts == 2 && t.st == DM_TX_IDLE, "second stage times out too → done, 2 counted"); } - { dm_modegate g = {0}; uint64_t b; // Input gate: A=0x1 triggers the switch and stays held, B=0x2 is also pressed along the way, and after releasing B it is pressed again - b = 0x1; CHECK(dm_modegate_apply(&g, 1, &b) == 1 && b == 0, "frozen: keys cleared, sticks cleared too"); - b = 0x3; CHECK(dm_modegate_apply(&g, 1, &b) == 1 && b == 0, "keys newly pressed while frozen are not passed either"); - b = 0x3; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0, "A and B still held after the unfreeze are not passed"); - b = 0x5; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0x4, "B released, A still held: newly pressed X=0x4 is passed as usual, A still not"); - b = 0x0; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0, "all released"); - b = 0x3; CHECK(dm_modegate_apply(&g, 0, &b) == 0 && b == 0x3, "pressed again after a release: passed as usual"); } - printf("t_modetx: %d checks, %d failed\n", checks, fails); return fails != 0; -} diff --git a/dm/host/tests/t_movie.c b/dm/host/tests/t_movie.c deleted file mode 100644 index d7f94c8..0000000 --- a/dm/host/tests/t_movie.c +++ /dev/null @@ -1,61 +0,0 @@ -// Unit test for WebM duration parsing of cutscene no-op playback (takes seconds): a few container headers are assembled by hand per EBML, and all expected values are computed by hand without calling the implementation under test. -// Build and run: make -C dm/host/tests run -#include "../hle.h" -#include <stdio.h> - -void *guest_sym(const char *n) { (void)n; return NULL; } // stub for the guest call in the file under test (not used by the parsing path) -struct dm_flags g_flag; uint64_t g_t0_ns; -uint64_t now_ns(void) { return 0; } // log timestamp stub (not used by the parsing path) -typedef int64_t (*mv_read_fn)(void *ctx, int64_t off, void *buf, size_t n); -int64_t mv_webm_duration_ns(mv_read_fn rd, void *ctx); - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } else { printf(" ✓ "); printf(__VA_ARGS__); printf("\n"); } } while (0) - -typedef struct { const uint8_t *p; size_t n; } mem; -static int64_t rd_mem(void *c_, int64_t off, void *buf, size_t n) { - mem *c = c_; if (off < 0 || (size_t)off >= c->n) return 0; - size_t k = c->n - off < n ? c->n - off : n; memcpy(buf, c->p + off, k); return (int64_t)k; -} -static int64_t dur(const uint8_t *p, size_t n) { mem m = {p, n}; return mv_webm_duration_ns(rd_mem, &m); } - -int main(void) { - // (1) Normal: EBML header (empty body) + Segment (known length) { SeekHead (empty) , Info { TimecodeScale=1000000, Duration=float64 6633.0 } } - // IEEE754 double of 6633.0 = 0x40B9E90000000000; expected 6633.0 * 1000000 ns = 6,633,000,000 ns (= the 6.633 s ffprobe measures for logoloop) - static const uint8_t a[] = { - 0x1A,0x45,0xDF,0xA3, 0x80, // EBML, length 0 - 0x18,0x53,0x80,0x67, 0x9C, // Segment, length 28 = 5 + 5 + 18 - 0x11,0x4D,0x9B,0x74, 0x80, // SeekHead, length 0 - 0x15,0x49,0xA9,0x66, 0x92, // Info, length 18 = 7 + 11 - 0x2A,0xD7,0xB1, 0x83, 0x0F,0x42,0x40, // TimecodeScale = 0x0F4240 = 1000000 - 0x44,0x89, 0x88, 0x40,0xB9,0xE9,0x00,0x00,0x00,0x00,0x00 // Duration float64 6633.0 - }; - CHECK(dur(a, sizeof a) == 6633000000ll, "float64 duration 6633 × 1ms = 6.633 s (read %lld)", (long long)dur(a, sizeof a)); - // (2) float32 + non-default TimecodeScale: Duration=float32 2000.0 (0x44FA0000), Scale=500000 => 1,000,000,000 ns; Segment of unknown length (0x01FFFFFFFFFFFFFF) - static const uint8_t b[] = { - 0x1A,0x45,0xDF,0xA3, 0x80, - 0x18,0x53,0x80,0x67, 0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0x15,0x49,0xA9,0x66, 0x8E, // Info, length 14 = 7 + 7 - 0x2A,0xD7,0xB1, 0x83, 0x07,0xA1,0x20, // 0x07A120 = 500000 - 0x44,0x89, 0x84, 0x44,0xFA,0x00,0x00 // float32 2000.0 - }; - CHECK(dur(b, sizeof b) == 1000000000ll, "float32 2000 × 0.5ms + unknown-size Segment = 1.000 s (read %lld)", (long long)dur(b, sizeof b)); - // (3) Default TimecodeScale (absent from Info): Duration=float64 10033.0 (0x40C3988000000000) => 10,033,000,000 ns - static const uint8_t c[] = { - 0x1A,0x45,0xDF,0xA3, 0x80, - 0x18,0x53,0x80,0x67, 0x90, - 0x15,0x49,0xA9,0x66, 0x8B, - 0x44,0x89, 0x88, 0x40,0xC3,0x98,0x80,0x00,0x00,0x00,0x00 - }; - CHECK(dur(c, sizeof c) == 10033000000ll, "default TimecodeScale = 1ms, 10033 ⇒ 10.033 s (read %lld)", (long long)dur(c, sizeof c)); - // (4) Cluster encountered first, no Info => -1 (the caller reports a failure on this and gives no default) - static const uint8_t d[] = { 0x1A,0x45,0xDF,0xA3, 0x80, 0x18,0x53,0x80,0x67, 0x85, 0x1F,0x43,0xB6,0x75, 0x80 }; - CHECK(dur(d, sizeof d) == -1, "no Info, Cluster comes first ⇒ -1 (read %lld)", (long long)dur(d, sizeof d)); - // (5) No Duration in Info => -1 - static const uint8_t e[] = { 0x1A,0x45,0xDF,0xA3, 0x80, 0x18,0x53,0x80,0x67, 0x8C, 0x15,0x49,0xA9,0x66, 0x87, 0x2A,0xD7,0xB1, 0x83, 0x0F,0x42,0x40 }; - CHECK(dur(e, sizeof e) == -1, "no Duration ⇒ -1 (read %lld)", (long long)dur(e, sizeof e)); - // (6) Truncated (no data after the Segment header) => -1 - CHECK(dur(a, 10) == -1, "truncated ⇒ -1"); - printf(fails ? "t_movie failed %d/%d\n" : "t_movie all passed: %d/%d\n", fails ? fails : checks, checks); - return fails != 0; -} diff --git a/dm/host/tests/t_namegen.c b/dm/host/tests/t_namegen.c deleted file mode 100644 index 1baecdf..0000000 --- a/dm/host/tests/t_namegen.c +++ /dev/null @@ -1,20 +0,0 @@ -// Unit test for random names at character naming (takes seconds): dm_namegen.c. Criterion: create a character 20 times in a row with the name left blank, and the names are all different and all valid. -// "Characters already created" are simulated with the taken stub: each name drawn is recorded in the existing list, and drawing it the next time counts as a duplicate. Valid = the game's naming rule (main+0x7fed78): 2–15 bytes, first and last are letters, at most one of - _; our names should contain only English letters. -#include <stdio.h> -#include <string.h> -int dm_name_gen(char *out, int cap, int (*taken)(const char *)); - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) -static char have[20][32]; static int nhave; -static int taken(const char *n) { for (int i = 0; i < nhave; i++) if (!strcasecmp(have[i], n)) return 1; return 0; } // the save directory is on a case-insensitive file system -static int all_taken(const char *n) { (void)n; return 1; } -static int legal(const char *n) { size_t l = strlen(n); if (l < 2 || l > 15) return 0; for (size_t i = 0; i < l; i++) if (!((n[i] >= 'A' && n[i] <= 'Z') || (n[i] >= 'a' && n[i] <= 'z'))) return 0; return 1; } - -int main(void) { - for (int i = 0; i < 20; i++) { char nm[32]; int ok = dm_name_gen(nm, sizeof nm, taken); CHECK(ok, "draw %d: no name", i + 1); CHECK(legal(nm), "draw %d: '%s' is not valid", i + 1, nm); CHECK(!taken(nm), "draw %d: '%s' duplicates an existing character", i + 1, nm); strcpy(have[nhave++], nm); } - printf(" 20 draws: %s %s %s … %s\n", have[0], have[1], have[2], have[19]); - int bad = 0; for (int i = 0; i < 100000; i++) { char nm[32]; dm_name_gen(nm, sizeof nm, NULL); bad += !legal(nm); } CHECK(!bad, "%d invalid out of 100000", bad); - char nm[32]; CHECK(!dm_name_gen(nm, sizeof nm, all_taken), "should return 0 when every name is taken"); - printf("t_namegen: %d checks, %d failed\n", checks, fails); return fails != 0; -} diff --git a/dm/host/tests/t_oe.c b/dm/host/tests/t_oe.c index f7904b9..eef5948 100644 --- a/dm/host/tests/t_oe.c +++ b/dm/host/tests/t_oe.c @@ -7,16 +7,6 @@ #include <signal.h> #include <sys/wait.h> uint64_t now_ns(void) { return 0; } -// External dependencies of the 3D render scale (scale3d_tick): the unit test has no module table or graphics layer, so they are stubbed as "no main module" -> the function returns immediately -module_t g_mod[MAX_MOD]; int g_nmod; -void dm_guest_write_prepare(void *a, size_t n) { (void)a; (void)n; } -uint64_t dm_nvn_frame_count(void) { return 0; } -int dm_nvn_ruler_truth(void) { return 0; } -int dm_ingame_now(void) { return 1; } -int mtl_fps_target(void) { return 60; } // target frame rate (nvn_mtl.m): always 60 in the unit test, and the 30 fps mode cases pass period_ms directly -pthread_key_t g_keyres[2]; // stub for the resolution parameter slot (loader.c): no key created, so taking 31 does not write the thread slot -uint64_t dm_game_res(int docked) { (void)docked; return 0; } // stub for the render resolution (dm_uiscreen.c) -void dm_uiscreen_res_reread(void) {} void dm_audio_suspend(int on) { (void)on; } // stubs for the bg item of DM_OE_SCRIPT (hle_audio.c / hle_os.c / hle_nvn.c) void guest_clock_freeze_for(int reason, int on) { (void)reason; (void)on; } void dm_gfx_suspend(int on) { (void)on; } @@ -66,70 +56,7 @@ static int child_burst(int kind) { int st; if (waitpid(p, &st, 0) != p) return 0; return WIFEXITED(st) && WEXITSTATUS(st) == 0; } -// Dynamic 3D scale controller (scale3d_ctl_step, a pure function): expected values are computed by hand from the rules, not generated by the implementation (rules in hle_oe.c: frame rate over 2-second windows; two consecutive windows < target *0.92 steps down; 10 consecutive windows >= *0.97 steps up; a drop within 10 seconds of stepping up -> the next step-up needs 30 windows) -static uint64_t s3_now; static scale3d_ctl s3; static int s3_dm; // s3_dm: cumulative number of level changes -static int s3_one(double iv) { int b = s3.level; s3_now += (uint64_t)((iv > 0 ? iv : 16.6667) * 1e6); int lv = scale3d_ctl_step(&s3, iv, s3_now); s3_dm += lv != b; return lv; } -static int s3_n(int n, double iv) { int lv = s3.level; for (int i = 0; i < n; i++) lv = s3_one(iv); return lv; } -#define F60 16.6667 // 60 fps: 120 frames per window (120 * 16.6667 = 2000.004 ms) -// Frame counts of the "25 ms bucket / >=33 ms bucket" for each 10-second window of the second section of real play on a real device (copied by hand from perf lines) -static const int s3_rows[18][2] = {{0,1},{2,4},{3,3},{0,0},{10,12},{0,0},{0,0},{0,0},{2,1},{0,0},{6,8},{0,0},{0,0},{0,0},{2,3},{5,4},{0,0},{0,0}}; -static int s3_replay(int clustered) { // clustered = 1: the slow frames of each 10 seconds are packed consecutively at the start (worst case); 0: spread evenly. Returns the number of level changes - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 5000 * S3_SEC; s3_dm = 0; - for (int r = 0; r < 18; r++) { int ns = s3_rows[r][0] + s3_rows[r][1], every = ns ? 600 / ns : 0; - for (int i = 0; i < 600; i++) { int slow = clustered ? i < ns : (ns && i % every == 0 && i / every < ns); s3_one(slow ? (i % 2 ? 25.0 : 40.0) : F60); } } - return s3_dm; -} -static void test_scale3d(void) { - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 1000 * S3_SEC; - CHECK(s3_n(1200, F60) == 0, "dynamic scale: steady 60 fps for 20 s → stays at level 0"); - CHECK(s3_n(199, 20.0) == 0, "dynamic scale: 50 fps (< 55.2) in window 1 + window 2 short by 1 frame → not lowered yet"); - CHECK(s3_n(1, 20.0) == 1, "dynamic scale: two 2 s windows in a row at 50 fps → lowered to level 1"); - CHECK(s3_n(100, 20.0) == 1, "dynamic scale: frames within 2 s of a level change do not count (50 fps still does not lower)"); - CHECK(s3_n(200, 20.0) == 2, "dynamic scale: after the hold ends, two more windows in a row at 50 fps → lowered to level 2"); - CHECK(s3_n(1000, 25.0) == 2, "dynamic scale: at the floor, further drops stay at level 2"); - s3 = (scale3d_ctl){.max_level = 2, .level = 2}; s3_now = 1500 * S3_SEC; // test stepping up from a clean level 2 (without the half bad window the previous section left in the window) - CHECK(s3_n(1199, F60) == 2, "dynamic scale: 60 fps for a little over 9 windows in a row → not raised yet"); - CHECK(s3_n(1, F60) == 1, "dynamic scale: 10 windows in a row (20 s) ≥ 58.2 → raised to level 1"); - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 2000 * S3_SEC; - for (int k = 0; k < 10; k++) { s3_n(100, 20.0); s3_n(120, F60); } - CHECK(s3.level == 0, "dynamic scale: bad and good windows alternating (never two bad in a row) → not lowered"); - s3 = (scale3d_ctl){.max_level = 2, .level = 1}; s3_now = 2500 * S3_SEC; - CHECK(s3_n(1200 * 3, 1000.0 / 57) == 1, "dynamic scale: 57 fps (between 55.2 and 58.2) for 60 s → neither lowered nor raised"); - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 3000 * S3_SEC; - s3_n(90, F60); s3_one(500.0); s3_n(120 * 3, F60); - CHECK(s3.level == 0, "dynamic scale: one 500 ms stall in a window (about 45 frames there), next window normal → not lowered (needs two in a row)"); - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 3500 * S3_SEC; int lv = 0; - for (int i = 0; i < 2000; i++) lv = s3_one(i % 4 == 0 ? -1 : F60); - CHECK(lv == 0, "dynamic scale: frames dropped by the system (−1) not counted, the rest at 60 fps → not lowered"); - for (int i = 0; i < 2000; i++) lv = s3_one(0); - CHECK(lv == 0, "dynamic scale: all frames without a reading (0) → no change"); - // Debounce: knocked back down within 10 seconds of stepping up -> the next step-up needs 30 windows (60 seconds) - s3 = (scale3d_ctl){.max_level = 2, .level = 1}; s3_now = 4000 * S3_SEC; - CHECK(s3_n(1200, F60) == 0, "debounce: 20 s of good windows → raised to level 0"); - s3_n(120, F60); // judging is suspended for 2 seconds after a level change - CHECK(s3_n(200, 20.0) == 1, "debounce: two 50 fps windows in a row about 6 s after going up → back to level 1"); - s3_n(120, F60); - CHECK(s3_n(2400, F60) == 1, "debounce: dropped right after going up, then 40 s of good windows → not raised (needs 60 s)"); - CHECK(s3_n(1200, F60) == 0, "debounce: a full 60 s of good windows → raised to level 0"); - s3_n(120, F60); s3_n(120 * 6, F60); // drops only 12 seconds (> 10 seconds) after stepping up - CHECK(s3_n(200, 20.0) == 1, "debounce: dropped only 12 s after going up → lowered to level 1"); - s3_n(120, F60); - CHECK(s3_n(1200, F60) == 0, "debounce: not a drop right after going up, 20 s of good windows raise it back"); - // 30 fps mode: target 30, step-down line 27.6, step-up line 29.1 - s3 = (scale3d_ctl){.max_level = 2, .period_ms = 1000.0 / 30}; s3_now = 6000 * S3_SEC; - CHECK(s3_n(600, 33.34) == 0, "30 fps tier: 33.34 ms (60 frames per window) steady for 20 s → stays at level 0"); - CHECK(s3_n(100, 40.0) == 1, "30 fps tier: 25 fps (< 27.6) two windows in a row → lowered to level 1"); - // Injection: the rds run treats the interval of a stretch of frames as 25 ms (40 fps) - s3 = (scale3d_ctl){.max_level = 2}; s3_now = 7000 * S3_SEC; - CHECK(s3_n(160, 25.0) == 1 && s3_n(80, 25.0) == 1 && s3_n(160, 25.0) == 2, "inject 25 ms: 4 s to level 1, 2 s hold, then 4 s more to level 2"); - s3_n(120, F60); - CHECK(s3_n(1200, F60) == 1 && (s3_n(120, F60), s3_n(1200, F60)) == 0, "injection over: up one level every 20 s, back to level 0"); - int a = s3_replay(0), b = s3_replay(1); - printf(" (replay of real play, part two: slow frames spread evenly changed level %d times, bunched together %d times; the old controller did 12 times on the device)\n", a, b); - CHECK(a == 0 && b == 0, "dynamic scale: the slow-frame sequence from real play part two (59+ fps) changes no level, spread out or bunched together"); -} int main(void) { - test_scale3d(); unsetenv("DM_OE_SCRIPT"); reset(); CHECK(empty(), "empty queue at start: returns false and leaves the output alone"); dm_focus_set(1); dm_set_opmode(1); CHECK(empty(), "same state sends no notification"); diff --git a/dm/host/tests/t_padmap.c b/dm/host/tests/t_padmap.c deleted file mode 100644 index 7189833..0000000 --- a/dm/host/tests/t_padmap.c +++ /dev/null @@ -1,23 +0,0 @@ -// Physical controller face button mapping (dm_padmap.h): each type x four face buttons -> game keys. Expected values are hand-written from -// the rule: whatever letter is printed is that game key; Nintendo glyphs are right A, bottom B, top X, left Y, Xbox glyphs are bottom A, -// right B, left X, top Y; everything else is laid out as Xbox. -#include <stdio.h> -#include "../dm_padmap.h" -static int checks, fails; -#define CHECK(c, ...) do { checks++; if (c) printf(" ✓ "); else { fails++; printf(" ✗ "); } printf(__VA_ARGS__); printf("\n"); } while (0) -int main(void) { - struct { const char *cat, *ven; int kind; } id[] = { - { "Switch Pro Controller", "Pro Controller", DM_PADKIND_NINTENDO }, { "Nintendo Switch Joy-Con (L/R)", "Joy-Con (L/R)", DM_PADKIND_NINTENDO }, - { "Nintendo Switch Joy-Con (L)", "Joy-Con (L)", DM_PADKIND_NINTENDO }, { "MFi", "NINTENDO compatible pad", DM_PADKIND_NINTENDO }, - { "Xbox One", "Xbox Wireless Controller", DM_PADKIND_XBOX }, { "DualSense", "DualSense Wireless Controller", DM_PADKIND_XBOX }, - { "DualShock 4", "DUALSHOCK 4 Wireless Controller", DM_PADKIND_XBOX }, { "MFi", "SteelSeries Nimbus", DM_PADKIND_XBOX }, { NULL, NULL, DM_PADKIND_XBOX }, { "", "", DM_PADKIND_XBOX } }; - for (unsigned i = 0; i < sizeof id / sizeof *id; i++) CHECK(dm_padmap_kind(id[i].cat, id[i].ven) == id[i].kind, "category '%s' vendor name '%s' → %s layout", id[i].cat ? id[i].cat : "(empty)", id[i].ven ? id[i].ven : "(empty)", id[i].kind ? "Nintendo" : "Xbox"); - // bottom right left top - CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 1, 0, 0, 0) == DM_BTN_B, "Nintendo: bottom = B"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 1, 0, 0) == DM_BTN_A, "Nintendo: right = A"); - CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 1, 0) == DM_BTN_Y, "Nintendo: left = Y"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 0, 1) == DM_BTN_X, "Nintendo: top = X"); - CHECK(dm_padmap_face(DM_PADKIND_XBOX, 1, 0, 0, 0) == DM_BTN_A, "Xbox: bottom = A"); CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 1, 0, 0) == DM_BTN_B, "Xbox: right = B"); - CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 0, 1, 0) == DM_BTN_X, "Xbox: left = X"); CHECK(dm_padmap_face(DM_PADKIND_XBOX, 0, 0, 0, 1) == DM_BTN_Y, "Xbox: top = Y"); - CHECK(dm_padmap_face(DM_PADKIND_XBOX, 1, 1, 0, 0) == (DM_BTN_A | DM_BTN_B), "two pressed at once: each counted on its own"); CHECK(dm_padmap_face(DM_PADKIND_NINTENDO, 0, 0, 0, 0) == 0, "none pressed = 0"); - printf("== %d checks, %d failed ==\n", checks, fails); - return fails != 0; -} diff --git a/dm/host/tests/t_romfs_validate.c b/dm/host/tests/t_romfs_validate.c deleted file mode 100644 index ca535f7..0000000 --- a/dm/host/tests/t_romfs_validate.c +++ /dev/null @@ -1,40 +0,0 @@ -#include "../romfs_validate.h" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -static int check(int ok, const char *s) { if (!ok) fprintf(stderr, "FAILED: %s\n", s); return ok; } -int main(void) { - const char *src = getenv("DM_ROMFS"); if (!src) src = getenv("HOME"); - if (!src) return 1; - char cmd[4096], why[512], rep[2048], sd[4096]; snprintf(sd, sizeof sd, "%s/data", src); - if (!check(dm_romfs_validate(sd, why, sizeof why), "original RomFS passes")) { fprintf(stderr, "%s\n", why); return 1; } - if (!check(dm_romfs_report(sd, rep, sizeof rep) == 0, "full list: original RomFS has nothing missing")) return 1; - { // Empty directory: none of the 18 archives is present. Expected values come from the need / size tables in romfs_validate.c: 00-00000001.data 1073741824 bytes = 1.00 GB, 17-00000002.data 4496514 bytes = 4 MB - char r1[] = "/tmp/dm-romfs-test.XXXXXX"; if (!mkdtemp(r1)) return 1; - if (!check(!dm_romfs_validate(r1, why, sizeof why) && !strcmp(why, "Missing 00-00000001.data"), "empty directory rejected, first archive reported")) { fprintf(stderr, "%s\n", why); return 1; } - int bad = dm_romfs_report(r1, rep, sizeof rep); - if (!check(bad == 18 && strstr(rep, "0 / 18 files present") && strstr(rep, "00-00000001.data (1.00 GB)") && strstr(rep, "17-00000002.data (4 MB)") && !strstr(rep, "Incomplete"), "full list: all 18 missing listed one by one")) { fprintf(stderr, "%s\n", rep); return 1; } - // Small files such as the config, key and manifests are outside the scope of the check: put in a copy with wrong contents, and neither the verdict nor - // the list shown to the player changes, and their names do not appear in the list - snprintf(cmd, sizeof cmd, "printf bad > '%s/.build.config' && printf x > '%s/build_key.txt' && printf x > '%s/archive_manifest.psv'", r1, r1, r1); if (system(cmd)) return 1; - bad = dm_romfs_report(r1, rep, sizeof rep); - if (!check(bad == 18 && !strstr(rep, "build") && !strstr(rep, "psv"), "full list: small files not mentioned")) { fprintf(stderr, "%s\n", rep); return 1; } - snprintf(cmd, sizeof cmd, "rm -rf '%s'", r1); system(cmd); - } - { // Archive files alone (no config, key or any manifest) are enough to pass; then check byte counts and zero-stripped archives: build a copy with symlinks, then truncate one small archive / replace it with "zero-stripped + extent table" - char r2[] = "/tmp/dm-romfs-size.XXXXXX"; if (!mkdtemp(r2)) return 1; - snprintf(cmd, sizeof cmd, "for f in '%s'/data/*.data; do ln -s \"$f\" '%s/' || exit 1; done", src, r2); if (system(cmd)) return 1; - if (!check(dm_romfs_validate(r2, why, sizeof why), "directory with only the archive files passes")) { fprintf(stderr, "%s\n", why); return 1; } - snprintf(cmd, sizeof cmd, "rm '%s/04-00000002.data' && head -c 1000000 '%s/data/04-00000002.data' > '%s/04-00000002.data'", r2, src, r2); if (system(cmd)) return 1; - if (!check(!dm_romfs_validate(r2, why, sizeof why) && strstr(why, "is incomplete"), "half-copied archive rejected")) { fprintf(stderr, "%s\n", why); return 1; } - int bad = dm_romfs_report(r2, rep, sizeof rep); - if (!check(bad == 1 && strstr(rep, "17 / 18 files present") && strstr(rep, "Incomplete: 04-00000002.data (1 MB / 9 MB)"), "full list: incomplete archives listed separately with present / expected size (1000000 bytes = 1 MB, 8915855 bytes = 9 MB, divided by 1048576 and rounded)")) { fprintf(stderr, "%s\n", rep); return 1; } - snprintf(cmd, sizeof cmd, "printf 'size 8915855\\n1000000 7915855\\n' > '%s/04-00000002.data.holes'", r2); if (system(cmd)) return 1; - if (!check(dm_romfs_validate(r2, why, sizeof why), "zero-stripped archive (first 1000000 bytes + extent table) passes")) { fprintf(stderr, "%s\n", why); return 1; } - snprintf(cmd, sizeof cmd, "printf 'size 8915855\\n1000000 100\\n' > '%s/04-00000002.data.holes'", r2); if (system(cmd)) return 1; - if (!check(!dm_romfs_validate(r2, why, sizeof why) && strstr(why, "is incomplete"), "extent table that does not match the actual length rejected")) { fprintf(stderr, "%s\n", why); return 1; } - snprintf(cmd, sizeof cmd, "rm -rf '%s'", r2); system(cmd); - } - return 0; -} diff --git a/dm/host/tests/t_swkbd.c b/dm/host/tests/t_swkbd.c index e2dc5db..8fce046 100644 --- a/dm/host/tests/t_swkbd.c +++ b/dm/host/tests/t_swkbd.c @@ -4,7 +4,6 @@ #include <stdio.h> Result swkbd_show(void *out, const void *arg, int (*input)(const char *, const char *, const char *, const char *, int, char *, int)); -void dm_ui_text_entered(const char *text) { (void)text; } // the real implementation is in dm_cursor.c (the naming page auto-accepts once input is finished) int front_text_input(const char *t, const char *s, const char *g, const char *i, int m, char *o, int c) { (void)t; (void)s; (void)g; (void)i; (void)m; (void)o; (void)c; return -1; } static int fails, checks; diff --git a/dm/host/tests/t_target_rank.c b/dm/host/tests/t_target_rank.c deleted file mode 100644 index 1dedd7e..0000000 --- a/dm/host/tests/t_target_rank.c +++ /dev/null @@ -1,39 +0,0 @@ -// Automatic monster targeting unit test: lay out fake units / fake monster data in this process's memory, call the observation gate callback, and check the score adjustment (expected values computed by hand, not by calling the implementation under test). -// Layout: unit +0 type, +4 class number, +0x10 -> monster data (+0x1A TypeFlag: 0x02 super unique / 0x04 champion / 0x08 unique / 0x10 minion). -#include <stdio.h> -#include <string.h> -#include <stdint.h> -int gate_obs_present(int id) { (void)id; return 1; } -static void (*g_fn)(const uint64_t *); -void gate_obs_set(int id, void (*fn)(const uint64_t *)) { (void)id; g_fn = fn; } -void target_rank_install(void); -static int fails, cases; -static float run(const void *unit, float score) { - uint64_t frame[100] = {0}; frame[1] = (uint64_t)(uintptr_t)unit; frame[2] = 1; - memcpy(&frame[32], &score, 4); g_fn(frame); - float out; memcpy(&out, &frame[32], 4); return out; -} -static void expect(const char *what, float got, float want) { - cases++; if (got == want) printf("✓ %s (%g)\n", what, got); else { fails++; printf("✗ %s: got %g, expected %g\n", what, got, want); } -} -int main(void) { - target_rank_install(); - uint8_t data[0x40] = {0}; - struct { uint32_t type, cls, id, mode; uint64_t data; } unit = {1, 5, 77, 0, (uint64_t)(uintptr_t)data}; - expect("normal zombie (class 5, TypeFlag 0): score unchanged", run(&unit, 0.5f), 0.5f); - data[0x1a] = 0x10; expect("minion (0x10) counts as normal", run(&unit, 0.5f), 0.5f); - data[0x1a] = 0x04; expect("champion (0x04) +1000", run(&unit, 0.5f), 1000.5f); - data[0x1a] = 0x08; expect("unique (0x08) +1000", run(&unit, 0.25f), 1000.25f); - data[0x1a] = 0x02; expect("super unique (0x02) +1000", run(&unit, 0.75f), 1000.75f); - data[0x1a] = 0; unit.cls = 156; expect("Andariel (class 156, boss=1) +2000", run(&unit, 0.5f), 2000.5f); - unit.cls = 745; expect("new 3.3 Boss Colossus (class 745) +2000", run(&unit, 0.125f), 2000.125f); - unit.cls = 5; unit.type = 0; data[0x1a] = 0x08; expect("player unit (type 0) untouched", run(&unit, 0.5f), 0.5f); - unit.type = 1; unit.data = 0; data[0x1a] = 0; expect("null monster data pointer: nothing to read, untouched", run(&unit, 0.5f), 0.5f); - expect("null candidate unit untouched", run(NULL, 0.5f), 0.5f); - // Across tiers: the worst elite score (0.01+1000) is still higher than the best normal score (0.99); the worst Boss score is higher than the best elite score - unit.data = (uint64_t)(uintptr_t)data; data[0x1a] = 0x04; float elite = run(&unit, 0.01f); - data[0x1a] = 0; unit.cls = 211; float boss = run(&unit, 0.01f); - cases++; if (elite > 0.99f && boss > 1000.99f) printf("✓ priority across tiers: elite %g > normal 0.99, Boss %g > elite 1000.99\n", elite, boss); else { fails++; printf("✗ priority across tiers\n"); } - printf("%s: %d/%d\n", fails ? "[RED] auto-targeting" : "auto-targeting all green", cases - fails, cases); - return fails != 0; -} diff --git a/dm/host/tests/t_textbox.c b/dm/host/tests/t_textbox.c deleted file mode 100644 index 6fb2353..0000000 --- a/dm/host/tests/t_textbox.c +++ /dev/null @@ -1,40 +0,0 @@ -// dm_textbox_core unit test: all expectations are hand-written. U+E07E = EE 81 BE. -// Criterion: whole strings / substrings of the annotated version found in the table are replaced by the text box version; strings outside the table (including the original game's own coded strings and strings without codes) are untouched byte for byte; with no table nothing is touched. -#include "../dm_textbox_core.h" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -#define C "\xee\x81\xbe" -static int fails, checks; -static void expect(const char *what, const char *in, int want_k, const char *want) { - char *o; size_t on; int k = dm_textbox_rewrite(in, strlen(in), &o, &on); checks++; - int ok = k == want_k && (want ? (o && on == strlen(want) && !memcmp(o, want, on)) : !o); - if (!ok) { fails++; printf("✗ %s: got k=%d '%.*s'\n", what, k, o ? (int)on : 0, o ? o : ""); } else printf("✓ %s\n", what); - free(o); -} -int main(void) { - // The table rows and inputs below are game text from the Chinese D2R client (Hongye mod), written as UTF-8 escapes so the bytes stay identical: - // an "enhanced damage 60-70%" range line, two unique item names, "Found Chronicle item: ...!", "and", - // the game's whisper line "You whisper to <someone>: hello", and a two-line text "line one" / "line two". - char path[] = "/tmp/t_textbox_XXXXXX"; int fd = mkstemp(path); FILE *f = fdopen(fd, "w"); - fprintf(f, "# annotated version\ttext box version\n"); - fprintf(f, "%s\t%s\n", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "60-70%]\\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2"); // range line + name - fprintf(f, "%s\t%s\n", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a"); // Hongye mod name color - fclose(f); - expect("no table: coded string untouched", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 0, NULL); - if (dm_textbox_load(path) != 2) { printf("✗ table entry count is not 2\n"); return 1; } - expect("whole annotated string in the table → text box version", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "60-70%]\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 1, "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2"); - expect("whole Hongye name-color string → plain name", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 1, "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a"); - expect("annotated text inside a longer sentence: only that part is replaced", "\xe5\x8f\x91\xe7\x8e\xb0\xe7\xbc\x96\xe5\xb9\xb4\xe5\x8f\xb2\xe7\x89\xa9\xe5\x93\x81\xef\xbc\x9a" C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a" "\xef\xbc\x81", 2, "\xe5\x8f\x91\xe7\x8e\xb0\xe7\xbc\x96\xe5\xb9\xb4\xe5\x8f\xb2\xe7\x89\xa9\xe5\x93\x81\xef\xbc\x9a\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a\xef\xbc\x81"); - expect("two occurrences in one sentence: both replaced", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a" "\xe5\x92\x8c" C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 2, "\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a\xe5\x92\x8c\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a"); - expect("the original game's own coded string (whisper) is not in the table: untouched", C "0\xe4\xbd\xa0\xe6\x82\x84\xe6\x82\x84\xe5\x9c\xb0\xe5\xaf\xb9" C "1\xe6\x9f\x90\xe4\xba\xba" C "0\xe8\xaf\xb4: \xe4\xbd\xa0\xe5\xa5\xbd", 0, NULL); - expect("string without codes: untouched", "\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 0, NULL); - expect("multi-line original text (no codes): untouched", "\xe7\xac\xac\xe4\xb8\x80\xe8\xa1\x8c\n\xe7\xac\xac\xe4\xba\x8c\xe8\xa1\x8c", 0, NULL); - expect("looks annotated but is not in the table: untouched", C "5[\xe5\xbc\xba\xe4\xbc\xa4" "1-2%]\n" C "4\xe6\xad\xbb\xe4\xba\xa1\xe4\xb9\x8b\xe9\x93\xb2", 0, NULL); - unlink(path); dm_textbox_reset(); - expect("after clearing the table: untouched", C "1\xe6\x80\x80\xe7\x89\xb9\xe4\xb9\x8b\xe8\x84\x9a", 0, NULL); - printf("TEXTBOX %s checks=%d failures=%d\n", fails ? "FAIL" : "PASS", checks, fails); - return fails != 0; -} diff --git a/dm/host/tests/t_thread_leak.c b/dm/host/tests/t_thread_leak.c index 5c01893..8fc3524 100644 --- a/dm/host/tests/t_thread_leak.c +++ b/dm/host/tests/t_thread_leak.c @@ -18,10 +18,8 @@ static void t_free(void *p) { if (!p) return; atomic_fetch_sub(&g_live_blocks, 1 #undef calloc #undef free -pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate, g_keyres[2], g_keyaspect, g_keyaspectui; +pthread_key_t g_key18, g_keytls, g_keytlr, g_keygate; void dm_gate(void) {} -uint64_t dm_game_res(int i) { (void)i; return 0; } -uint64_t front_game_res(int i) { (void)i; return 0; } static int fails, checks; #define EXPECT(name, want, got) do { long w_ = (long)(want), g_ = (long)(got); checks++; printf("%-46s expected %-8ld → got %-8ld %s\n", name, w_, g_, w_ == g_ ? "✓" : "✗"); if (w_ != g_) fails++; } while (0) diff --git a/dm/host/tests/t_touch_mac.c b/dm/host/tests/t_touch_mac.c deleted file mode 100644 index 42bb481..0000000 --- a/dm/host/tests/t_touch_mac.c +++ /dev/null @@ -1,112 +0,0 @@ -#include <stdio.h> -#include <stdlib.h> -#include "../dm.h" -#include "../touch_mac.c" -#include "../../ios/Sources/dm_touch_controls.c" - -uint64_t now_ns(void) { return 0; } -void (*dm_pad_hook)(uint64_t, uint64_t *, int32_t *, int32_t *, int32_t *, int32_t *); -uint64_t guest_ns(void) { return 0; } -uint64_t dm_guest_frame(void) { return 0; } // the UI mouse / front-end paths run on the presented-frame cadence; this unit test does not test them -int dm_world_has_target(void) { return 1; } // stub for world tapping (dm_world.c): has a target = the primary button sends its key as usual -unsigned dm_world_tap(float nx, float ny) { (void)nx; (void)ny; return 0; } -int dm_shop_open(void) { return 0; } // stub for the vendor page (dm_actions.c): not open -int dm_padmode_current(void) { return 0; } // Stub for controller / touch mode (dm_padmode.c): touch -uint64_t dm_padmode_page_keys(uint64_t m) { (void)m; return 0; } void dm_padmode_legend_hide(const char *l, int a) { (void)l; (void)a; } int dm_padmode_busy(void) { return 0; } uint64_t dm_padmode_page_held(void) { return 0; } void dm_padmode_page_pulse(uint64_t m) { (void)m; } void dm_padmode_ident_hint(int on) { (void)on; } void dm_padmode_legend(int a, const char *t) { (void)a; (void)t; } const char *dm_str(uint8_t *b, const char *k) { (void)b; return k; } // Stubs for controller page keys / button legend / string fetch (used by controller paging of the personal stash, not reached under touch) -int dm_swapskill_wset(void) { return -1; } -int dm_pages_cur(void) { return 0; } int dm_pages_count(void) { return 5; } void dm_pages_request_flip(int d) { (void)d; } // stubs for the multi-page personal stash (dm_pages.c) -void dm_vault_ui_flip(int dir) { (void)dir; } -int dm_vault_ui_pager_hit(int x, int y) { (void)x; (void)y; return 0; } -void dm_vault_request_deposit(uint32_t id) { (void)id; } -uint32_t dm_vault_ui_held(void) { return 0; } -int dm_vault_ui_over(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_tip_gate(int x, int y) { (void)x; (void)y; return 0; } -int dm_pouch_tip_gate(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_blocks(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_tap(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_drag(int phase, int x, int y) { (void)phase; (void)x; (void)y; return 0; } -uint32_t dm_swapskill_itemsig(void) { return 0; } // stub for the current weapon set of the swap-set skill (dm_actions.c): cannot be read -_Atomic int g_dm_frz_want, g_dm_frozen; void dm_freeze_frame(int want) { (void)want; } // stub for the panel freeze (diag.c) -void dm_elysium_arm(int on) { (void)on; } // stub for the Elysium checkbox (dm_elysium.c) -void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } // stub for the "Custom" settings (dm_custom.c) -void dm_belt_put(unsigned id) { (void)id; } -void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; } -int dm_screen_real(int *w, int *h) { (void)w; (void)h; return 0; } -int dm_uiscreen_ingame(void) { return 0; } -void dm_uiscreen_set_ingame(int on) { (void)on; } // stub for the UI rectangle (dm_uiscreen.c): true full screen not enabled -void dm_inv_sort(int page) { (void)page; } -int dm_diag_difficulty(void) { return -1; } -uint64_t dm_fs_commit_count(void) { return 0; } -void dm_held_equip(int bodyloc) { (void)bodyloc; } -int dm_rift_merc_unlocked(void) { return 1; } -int dm_rift_merc_slot_empty(int loc) { (void)loc; return 0; } -void dm_rift_merc_locked_hint(void) {} -void dm_stash_take(int count) { (void)count; } -void dm_shop_buy(void) {} -int dm_cursor_item(void *out) { (void)out; return 0; } -void dm_shop_sell_cursor(void) {} -module_t g_mod[MAX_MOD]; int g_nmod; -static void (*ui_cb)(const char *, const char *, const char *); -static uint32_t test_tick; -uint32_t dm_cur_tick(void) { return test_tick; } -static int test_loadscreen_down; -int dm_loadscreen_down(void) { return test_loadscreen_down; } // the real check is in diag.c; the unit test sets it by hand following the loading screen messages -int dm_loadscreen_up(void) { return 0; } // in the unit test the loading screen does not block starting a run: the freeze is not triggered -void dm_hold_for_loadscreen(void) {} -void uimsg_subscribe(void (*f)(const char *, const char *, const char *)) { ui_cb = f; } -static int checks, fails; -#define CHECK(c, s) do { checks++; if (!(c)) { fails++; printf("FAIL %s\n", s); } else printf("PASS %s\n", s); } while (0) - -int main(void) { - const char *path = "build/touch_host.script"; - FILE *f = fopen(path, "w"); if (!f) return 2; - fputs("0 down 0 90,1220\n0.1 move 0 150,1220\n2.1 up 0\n", f); fclose(f); - setenv("DM_TOUCH_SCRIPT", path, 1); unsetenv("DM_TOUCH_TEST_DROP"); - uint64_t b = 0; int32_t lx = 0, ly = 0, rx = 0, ry = 0; - test_tick = 100; - dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry); - ui_cb("FrontEndNavigation", "ToGame", ""); - ui_cb("PanelManager", "UnloadPanel", "LoadScreenPanel"); test_loadscreen_down = 1; - dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry); - CHECK(lx == 0 && ly == 0, "down only sets the stick origin"); - for (test_tick = 101; test_tick <= 162; test_tick++) { - b = 0; lx = ly = rx = ry = 0; - dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry); - } - CHECK(lx > 0 && ly == 0, "move writes the host left stick"); - test_tick = 162; - b = 0; lx = ly = rx = ry = 0; dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry); - CHECK(lx > 0 && ly == 0, "repeated sampling in the same tick does not advance script time"); - test_tick = 212; - b = 0; lx = ly = rx = ry = 0; dm_touch_host_step(0, &b, &lx, &ly, &rx, &ry); - CHECK(lx == 0 && ly == 0, "up clears the host left stick"); - // Cleanup of the mode switch transaction (the "leave" registered in dm_mode_items.h): key presses on the player's behalf queued by touch are voided on a switch; ones the host sends by itself, such as the single "Show Items" press on entering a run, are not among them - { uint64_t dm_ui_auto_buttons(void); void dm_ui_key_pulse(uint64_t, int), dm_ui_auto_mode_leave(void), dm_ui_cache_mode_leave(void); - dm_ui_key_pulse(0x40, 1000); CHECK(dm_ui_auto_buttons() == 0x40, "touch button key pulse in flight: the key is among the keys pressed on the player's behalf"); - dm_ui_auto_mode_leave(); CHECK(dm_ui_auto_buttons() == 0, "key pulses in flight are voided by the switch cleanup"); - dm_ui_cache_mode_leave(); CHECK(1, "UI cache cleanup does not crash without a UI root"); } - printf("== touch host %d checks, %d failed ==\n", checks, fails); - return fails != 0; -} -void dm_shop_get(dm_shop_info *out) { memset(out, 0, sizeof *out); } // vendor readout (dm_actions.c): all 0 -void dm_item_use(unsigned id) { (void)id; } -int dm_ui_hover_now(int x, int y) { (void)x; (void)y; return 0; } -void dm_item_equip(unsigned id) { (void)id; } -void dm_item_unequip(unsigned id) { (void)id; } -int dm_held_size(int *w, int *h) { (void)w; (void)h; return 0; } -int dm_item_targeting(void) { return 0; } -int dm_vendor_repair_mode(void) { return 0; } -void dm_item_repair(uint32_t id) { (void)id; } -void dm_belt_fill(uint64_t w) { (void)w; } -void dm_item_identify(unsigned id) { (void)id; } -void dm_item_target_cancel(void) {} -void dm_item_compare(int on) { (void)on; } -void dm_held_stow(void) {} -int dm_held_stow_result(void) { return -1; } -int dm_held_pending(void) { return 0; } -void uimsg_emit(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } -void dm_item_identify_free(unsigned id) { (void)id; } // stub for free identify (dm_actions.c) -int dm_runeword_long_press(const void *ci) { (void)ci; return 0; } // stub for the runeword helper (dm_runeword.c) -int dm_runeword_list_open(void) { return 0; } -void dm_runeword_button(const char *name) { (void)name; } -void dm_runeword_list_close(void) {} diff --git a/dm/host/tests/t_touch_tick_stub.c b/dm/host/tests/t_touch_tick_stub.c deleted file mode 100644 index e1edfc9..0000000 --- a/dm/host/tests/t_touch_tick_stub.c +++ /dev/null @@ -1,91 +0,0 @@ -#include <stdint.h> - -uint32_t dm_cur_tick(void) { return 0; } -int dm_loadscreen_down(void) { return 1; } // the loading screen check is in diag.c; the touch point unit test treats it as already down -// World tapping (dm_world.c): these unit tests carry no game image, so empty stubs are given -void dm_world_pump(void) {} -int dm_world_has_target(void) { return 1; } -unsigned dm_world_tap(float nx, float ny) { (void)nx; (void)ny; return 0; } -void dm_textinput_pump(void) {} // text box hookup (dm_textinput.c): same as above, an empty stub -void dm_update_pump(void) {} // build number and new-version notice on the character select page (dm_update.c): same as above, an empty stub -void dm_num_set(int n) { (void)n; } -void dm_actions_pump(void) {} -uint64_t dm_swapskill_filter(uint64_t b) { return b; } // swap-set skill (dm_actions.c): does not suppress keys -uint64_t dm_runeword_pad_filter(uint64_t b) { return b; } // Runeword list in controller mode (dm_runeword.c): swallow no buttons -uint64_t dm_padmode_page_filter(uint64_t b) { return b; } // Our own pages in controller mode (dm_padmode.c): swallow no buttons -int dm_padmode_input_gate(uint64_t *b) { (void)b; return 0; } // Input gate of the controller / touch switch transaction (dm_padmode.c): not switching -int dm_padmode_current(void) { return 0; } // Controller / touch mode (dm_padmode.c): touch -uint64_t dm_padmode_page_keys(uint64_t m) { (void)m; return 0; } void dm_padmode_legend_hide(const char *l, int a) { (void)l; (void)a; } int dm_padmode_busy(void) { return 0; } uint64_t dm_padmode_page_held(void) { return 0; } void dm_padmode_page_pulse(uint64_t m) { (void)m; } void dm_padmode_ident_hint(int on) { (void)on; } void dm_padmode_legend(int a, const char *t) { (void)a; (void)t; } const char *dm_str(uint8_t *b, const char *k) { (void)b; return k; } // Stubs for controller page keys / button legend / string fetch (used by controller paging of the personal stash, not reached under touch) -int dm_swapskill_wset(void) { return -1; } -int dm_pages_cur(void) { return 0; } int dm_pages_count(void) { return 5; } void dm_pages_request_flip(int d) { (void)d; } // stubs for the multi-page personal stash (dm_pages.c) -void dm_vault_ui_flip(int dir) { (void)dir; } -int dm_vault_ui_pager_hit(int x, int y) { (void)x; (void)y; return 0; } -void dm_vault_request_deposit(uint32_t id) { (void)id; } -uint32_t dm_vault_ui_held(void) { return 0; } -int dm_vault_ui_over(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_tip_gate(int x, int y) { (void)x; (void)y; return 0; } -int dm_pouch_tip_gate(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_blocks(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_tap(int x, int y) { (void)x; (void)y; return 0; } -int dm_vault_ui_drag(int phase, int x, int y) { (void)phase; (void)x; (void)y; return 0; } -uint32_t dm_swapskill_itemsig(void) { return 0; } // same as above: the current weapon set cannot be read -void dm_shop_buy(void) {} -void dm_shop_sell_cursor(void) {} -void dm_stat_add(int s, int n) { (void)s; (void)n; } -void dm_skill_add_selected(void) {} -void dm_test_hold_first_sellable(void) {} -void dm_belt_put(unsigned id) { (void)id; } -void dm_test_sell_first(void) {} -int dm_shop_open(void) { return 0; } -_Atomic int g_dm_frz_want, g_dm_frozen; void dm_freeze_frame(int want) { (void)want; } // stub for the panel freeze (diag.c) -int dm_cursor_item(void *out) { (void)out; return 0; } // item under the cursor (dm_cursor_item.c): none -void dm_test_belt_first_potion(void) {} -void dm_shop_get(void *out) { __builtin_memset(out, 0, 320); } // vendor readout (dm_actions.c): all 0 (dm_shop_info is under 320 bytes) -void dm_test_use_first(int kind) { (void)kind; } -void dm_test_hover_at(float nx, float ny) { (void)nx; (void)ny; } -int dm_ui_hover_now(int x, int y) { (void)x; (void)y; return 0; } -void dm_item_compare(int on) { (void)on; } -void dm_belt_expand(int on) { (void)on; } -void dm_inv_sort(int page) { (void)page; } -int dm_diag_difficulty(void) { return -1; } -uint64_t dm_fs_commit_count(void) { return 0; } -void dm_held_equip(int bodyloc) { (void)bodyloc; } -int dm_rift_merc_unlocked(void) { return 1; } -int dm_rift_merc_slot_empty(int loc) { (void)loc; return 0; } -void dm_rift_merc_locked_hint(void) {} -void dm_name_random(void) {} -void dm_item_use(unsigned id) { (void)id; } -void dm_test_equip(int what) { (void)what; } -void dm_item_equip(unsigned id) { (void)id; } -void dm_item_unequip(unsigned id) { (void)id; } -int dm_held_size(int *w, int *h) { (void)w; (void)h; return 0; } -int dm_item_targeting(void) { return 0; } -int dm_vendor_repair_mode(void) { return 0; } -void dm_item_repair(uint32_t id) { (void)id; } -void dm_belt_fill(uint64_t w) { (void)w; } -void dm_test_identify(int what) { (void)what; } -uint64_t dm_diag_srv_item(uint32_t id) { (void)id; return 0; } -void dm_belt_use(int col, int row) { (void)col; (void)row; } -void dm_aim_cone_pump(void) {} -void dm_item_identify(unsigned id) { (void)id; } -void dm_item_target_cancel(void) {} -void dm_elysium_arm(int on) { (void)on; } // Elysium checkbox (dm_elysium.c): empty stub in the unit test -void dm_custom_on_uimsg(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } // stub for the "Custom" settings (dm_custom.c) -void dm_test_stash_take(int n) { (void)n; } -void dm_stash_take(int n) { (void)n; } -float dm_ui_ny(float ny) { return ny; } -int dm_screen_real(int *w, int *h) { (void)w; (void)h; return 0; } -void dm_uiscreen_enable(int m) { (void)m; } -void dm_ui_rect(int *x, int *y, int *w, int *h) { *x = *y = *w = *h = 0; } -int dm_uiscreen_ingame(void) { return 0; } -void dm_uiscreen_set_ingame(int on) { (void)on; } -void dm_held_stow(void) {} -int dm_held_stow_result(void) { return -1; } -void dm_test_swap_held(void) {} -int dm_held_pending(void) { return 0; } -void uimsg_emit(const char *cat, const char *act, const char *arg) { (void)cat; (void)act; (void)arg; } -void dm_item_identify_free(unsigned id) { (void)id; } // stub for free identify (dm_actions.c) -int dm_runeword_long_press(const void *ci) { (void)ci; return 0; } // stub for the runeword helper (dm_runeword.c) -int dm_runeword_list_open(void) { return 0; } -void dm_runeword_button(const char *name) { (void)name; } -void dm_runeword_list_close(void) {} diff --git a/dm/host/tests/t_unit_level.c b/dm/host/tests/t_unit_level.c deleted file mode 100644 index 3d3d137..0000000 --- a/dm/host/tests/t_unit_level.c +++ /dev/null @@ -1,54 +0,0 @@ -// Unit test for reading "unit -> the level it is in" (dm_unit.h, takes seconds). -// Situation: during the loading screen of an act change the client player "has a path but no room". The old code read four levels in one go, and with a null room went on to read address 0x18 -> sig=11 addr=0x18. -// Fail -> pass: with the same set of inputs, the old code (old_level, copied verbatim from the pre-fix line) runs in a child process and must die of SIGSEGV; the new code returns 0 (unknown level). -#include "dm_unit.h" -#include <signal.h> -#include <stdio.h> -#include <string.h> -#include <sys/wait.h> -#include <unistd.h> - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) - -static uint64_t rd(uint64_t a) { return a ? *(volatile uint64_t *)(uintptr_t)a : 0; } -static int old_level(uint64_t u) { uint64_t r = rd(rd(rd(rd(u + 0x38) + 0x20) + 0x18) + 0x90); return r ? *(int32_t *)(uintptr_t)(r + 0x1f8) : 0; } // exactly as it was before the fix -// Run the old code in a child process: returns which signal it died of (0 = returned normally) -static int old_dies(uint64_t u) { - fflush(stdout); pid_t p = fork(); if (p == 0) { signal(SIGSEGV, SIG_DFL); signal(SIGBUS, SIG_DFL); volatile int v = old_level(u); (void)v; _exit(0); } - int st = 0; waitpid(p, &st, 0); return WIFSIGNALED(st) ? WTERMSIG(st) : 0; -} - -int main(void) { - static uint8_t unit[0x200], path[0x100], room[0x100], ex[0x100], level[0x300]; - // A complete chain: level id 59 - *(uint64_t *)(unit + 0x38) = (uint64_t)(uintptr_t)path; *(uint64_t *)(path + 0x20) = (uint64_t)(uintptr_t)room; - *(uint64_t *)(room + 0x18) = (uint64_t)(uintptr_t)ex; *(uint64_t *)(ex + 0x90) = (uint64_t)(uintptr_t)level; *(int32_t *)(level + 0x1f8) = 59; - uint64_t u = (uint64_t)(uintptr_t)unit; - CHECK(dm_unit_level(u) == 59, "complete chain should read 59, read %d", dm_unit_level(u)); - CHECK(dm_unit_level_ptr(u) == (uint64_t)(uintptr_t)level, "complete chain should return the level struct pointer"); - CHECK(old_level(u) == 59, "old code should also read 59 on the complete chain (control)"); - - // (1) Has a path, room is null (the client player during an act-change loading) - *(uint64_t *)(path + 0x20) = 0; - int sg = old_dies(u); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'path present, room null', actual signal %d", sg); - CHECK(dm_unit_level(u) == 0 && dm_unit_level_ptr(u) == 0, "new code should return 0 on 'path present, room null'"); - // (2) Path is null - *(uint64_t *)(path + 0x20) = (uint64_t)(uintptr_t)room; *(uint64_t *)(unit + 0x38) = 0; - sg = old_dies(u); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'path null', actual signal %d", sg); - CHECK(dm_unit_level(u) == 0 && dm_unit_level_ptr(u) == 0, "new code should return 0 on 'path null'"); - // (3) Unit is null - sg = old_dies(0); CHECK(sg == SIGSEGV || sg == SIGBUS, "old code should crash (red) on 'unit null', actual signal %d", sg); - CHECK(dm_unit_level(0) == 0 && dm_unit_level_ptr(0) == 0, "new code should return 0 on 'unit null'"); - // (4) Room present, room +0x18 null; (5) +0x90 null (the last two steps of the chain) - *(uint64_t *)(unit + 0x38) = (uint64_t)(uintptr_t)path; *(uint64_t *)(room + 0x18) = 0; - CHECK(dm_unit_level(u) == 0, "room +0x18 null should return 0"); - *(uint64_t *)(room + 0x18) = (uint64_t)(uintptr_t)ex; *(uint64_t *)(ex + 0x90) = 0; - CHECK(dm_unit_level(u) == 0, "null level pointer should return 0"); - *(uint64_t *)(ex + 0x90) = (uint64_t)(uintptr_t)level; - CHECK(dm_unit_level(u) == 59, "should read 59 again after relinking"); - // Taking the room chain head when the level pointer is null: returns 0 (previously each place did rd(lv + 0x10) directly and read address 0x10) - CHECK(dm_level_rooms(0) == 0, "room list head should be 0 for a null level pointer"); - *(uint64_t *)(level + 0x10) = (uint64_t)(uintptr_t)room; CHECK(dm_level_rooms((uint64_t)(uintptr_t)level) == (uint64_t)(uintptr_t)room, "non-null level should read the room at +0x10"); - printf("t_unit_level: %d checks, %d failed\n", checks, fails); return fails != 0; -} diff --git a/dm/host/tests/t_widget.c b/dm/host/tests/t_widget.c deleted file mode 100644 index e956532..0000000 --- a/dm/host/tests/t_widget.c +++ /dev/null @@ -1,29 +0,0 @@ -// Unit test for the widget type check (dm_widget.h), runs in seconds. The fake "main module" is a block of memory; a widget's first word = base + vtable offset. -#include "dm_widget.h" -#include <stdio.h> - -static int fails, checks; -#define CHECK(c, ...) do { checks++; if (!(c)) { fails++; printf(" ✗ "); printf(__VA_ARGS__); printf("\n"); } } while (0) - -int main(void) { - static uint8_t base[16]; static uint64_t w_toggle[4], w_text[4], w_slot[4], w_slotb[4], w_slotc[4], w_other[4]; - w_toggle[0] = (uint64_t)(uintptr_t)base + DM_VT_TOGGLE; w_text[0] = (uint64_t)(uintptr_t)base + DM_VT_TEXT; w_other[0] = (uint64_t)(uintptr_t)base + 0x1234; - w_slot[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT; w_slotb[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT_B; w_slotc[0] = (uint64_t)(uintptr_t)base + DM_VT_SLOT_C; - uint64_t tg = (uint64_t)(uintptr_t)w_toggle, tx = (uint64_t)(uintptr_t)w_text, ot = (uint64_t)(uintptr_t)w_other; - - CHECK(dm_widget_vt(base, tg) == DM_VT_TOGGLE, "toggle vtable offset should read %#x, read %#llx", DM_VT_TOGGLE, (unsigned long long)dm_widget_vt(base, tg)); - CHECK(dm_widget_is(base, tg, DM_VT_TOGGLE), "toggle should be recognized as a toggle"); - CHECK(!dm_widget_is(base, tx, DM_VT_TOGGLE), "text box must not be recognized as a toggle (this happens when a widget of the same name changes class)"); - CHECK(dm_widget_is(base, tx, DM_VT_TEXT), "text box should be recognized as a text box"); - CHECK(!dm_widget_is(base, ot, DM_VT_TOGGLE) && !dm_widget_is(base, ot, DM_VT_TEXT), "unknown classes are never recognized"); - CHECK(!dm_widget_is(base, 0, DM_VT_TOGGLE) && dm_widget_vt(base, 0) == 0, "null widget: not recognized, vtable reads 0"); - CHECK(!dm_widget_is(NULL, tg, DM_VT_TOGGLE) && dm_widget_vt(NULL, tg) == 0, "no base address: not recognized, vtable reads 0"); - CHECK(dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slot) && dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slotb) && dm_widget_is_slot(base, (uint64_t)(uintptr_t)w_slotc), "all three item slot kinds recognized as item slots"); - CHECK(!dm_widget_is_slot(base, tg) && !dm_widget_is_slot(base, 0), "toggle / null widget is not an item slot"); - // Vtable values of the classes are pairwise distinct (one mistyped number would make two classes match each other) - { const uint64_t v[] = { DM_VT_TEXT, DM_VT_INPUT, DM_VT_NUMINPUT, DM_VT_TOGGLE, DM_VT_TABBAR, DM_VT_SLOT, DM_VT_SLOT_B, DM_VT_SLOT_C, DM_VT_IMAGE, DM_VT_BUTTON }; int dup = 0; - for (unsigned i = 0; i < sizeof v / sizeof *v; i++) for (unsigned j = i + 1; j < sizeof v / sizeof *v; j++) dup += v[i] == v[j]; - CHECK(dup == 0, "%d duplicate pairs among the vtable constants", dup); } - - printf("t_widget: %d checks, %d failed\n", checks, fails); return fails != 0; -} diff --git a/dm/host/thread.c b/dm/host/thread.c index cdf394e..339a5ed 100644 --- a/dm/host/thread.c +++ b/dm/host/thread.c @@ -9,25 +9,10 @@ static pthread_key_t g_keyown; // raw pointers of the TLS block / thread-local static pthread_once_t g_keyown_once = PTHREAD_ONCE_INIT; static void own_free(void *p) { void **own = p; free(own[0]); free(own[1]); free(own); } static void own_key_init(void) { if (pthread_key_create(&g_keyown, own_free)) die("cannot create the free key for thread-local blocks"); } -#include "front.h" -#include "dm_uiscreen.h" void guest_thread_init(void) { pthread_setspecific(g_keygate, (void *)dm_gate); - for (int i = 0; i < 2; i++) if (g_keyres[i]) pthread_setspecific(g_keyres[i], (void *)dm_game_res(i)); // resolution parameter slot: {u32 width, u32 height} packed - if (g_keyaspect || g_keyaspectui) { // aspect ratio limit parameter slot: max(19/9, output width / height of the two modes), float bit pattern in the low 32 bits - float a = 19.0f / 9.0f; int wide = 0; - for (int i = 0; i < 2; i++) { uint64_t r = front_game_res(i); float w = (float)(uint32_t)r, h = (float)(uint32_t)(r >> 32); if (h > 0 && w / h > a) { a = w / h; wide = 1; } } - // Limit of the UI side-pillar shader: when the screen is wider than 19:9 add another 0.15, pushing the pillars' inward soft edge off the screen; 16:9 is unchanged. - // Derivation: pillar inner edge x = screen height * limit / 2, so for every delta added to the limit the inner edge moves outward by screen height * delta / 2 on each side. Measured on Mac 1952x900, the soft edge is <= 30 pixels wide - // (with limit = screen aspect ratio, columns 0–20 at the right edge fade from black to bright, and it disappears once the limit is pushed to 2.4) => need delta >= 2 * 30 / 900 ~ 0.067; 0.15 is taken (about 67 pixels outward per side), leaving more than double the margin. - // Not a magic number: when the screen height grows the margin stays proportional (delta is relative to screen height) - float ui = wide ? a + 0.15f : a; - uint32_t bits; - if (g_keyaspect) { memcpy(&bits, &a, 4); pthread_setspecific(g_keyaspect, (void *)(uintptr_t)bits); } - if (g_keyaspectui) { memcpy(&bits, &ui, 4); pthread_setspecific(g_keyaspectui, (void *)(uintptr_t)bits); } - } pthread_setspecific(g_key18, 0); - // The TLS block and the thread-local region are freed when the thread exits (from d2r-mobile 2eb575877). The guest overwrites the slots of the TLS / thread-local-region keys (it sets its own thread pointer), so they cannot be freed by the current value in the key: + // The TLS block and the thread-local region are freed when the thread exits. The guest overwrites the slots of the TLS / thread-local-region keys (it sets its own thread pointer), so they cannot be freed by the current value in the key: // the raw pointers that were allocated are recorded separately on this file's own key g_keyown and freed by its destructor (key destructors run at pthread_exit, like crash.c's signal alternate stack; the destructor only calls free, which is safe on a guest thread's small stack too). // They used not to be freed: every short-lived thread the game started leaked 8192 + 0x200 + 0x400 = 9728 bytes. The thread-local region and the current thread object are one block (the SDK may also overwrite the TLR[0x1f8] slot) pthread_once(&g_keyown_once, own_key_init); diff --git a/dm/host/touch_mac.h b/dm/host/touch_mac.h deleted file mode 100644 index c3c288d..0000000 --- a/dm/host/touch_mac.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include <stdint.h> - -int dm_touch_host_enabled(void); -void dm_touch_host_game_start(uint64_t now); -void dm_touch_host_step(uint64_t now, uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry); diff --git a/dm/ios/Sources/dm_touch_controls.c b/dm/ios/Sources/dm_touch_controls.c deleted file mode 100644 index afaaf4c..0000000 --- a/dm/ios/Sources/dm_touch_controls.c +++ /dev/null @@ -1,645 +0,0 @@ -#include "dm_touch_controls.h" -#include "../../host/dm_input.h" -#include <math.h> -#include <string.h> - -#include <stdatomic.h> -#define DM_TC_POINTER_MOVE_PT 10.0f -#define DM_TC_POINTER_HOLD_S 0.5 -#define DM_TC_POINTER_TAP_S 0.35 - -/*Widget scale. Phone (picture height ≤ 440 points; the largest iPhone in landscape is exactly 440): min(1, width/667, height/375), identical to before to the letter. - Pictures taller than the phone's (the centered 16:9 block on iPad; 13-inch is 1366×768 points): scale up proportionally by picture height (height/440), otherwise the stick and skill buttons keep the phone's point size - and are just a small cluster in the large picture (as seen when rendering at iPad size on Mac), while the bottom bar and the game UI both scale by picture height. - This enlargement takes effect only when the front end turns it on explicitly (dm_tc_set_tall_scale, turned on when front_ios.m creates the view): the Mac host's touch test-case path (dm/host/touch_mac.c) passes pixel counts - (2868×1320) in as points, and the coordinates of the 13 test cases are written for "scale = 1"; without this switch, 1320 > 440 would enlarge their widgets 3x and move the main button (skill_main red). */ -static int g_tall_scale; -void dm_tc_set_tall_scale(int on) { g_tall_scale = !!on; } -float dm_tc_scale_for_size(float width, float height) { float k = fminf(1.0f, fminf(width / 667.0f, height / 375.0f)); return g_tall_scale && k >= 1.0f && height > 440.0f ? height / 440.0f : k; } -static float scale_for(const dm_tc_layout *l) { return dm_tc_scale_for_size(l->width, l->height); } - -/*Safe area (in iPhone landscape there is a strip on each of the left and right yielded to the Dynamic Island / rounded corners; read as 62 points each on a 17 Pro Max): the whole right-side skill key group and the left stick are translated by the minimum displacement so that they lie entirely inside the safe area, - with the relative arrangement within the group unchanged. Which side the island is on depends on how the player holds the device, so both sides are inset. Default 0 (Mac host and unit tests do not set it = original layout). */ -static float g_safe_l, g_safe_r; -/*The strip along the bottom edge of the screen that belongs to the system (points): a swipe up starting here is taken by the system as a bottom-edge gesture (on iPad two in a row pull out the Dock), and movement is exactly a repeated push upward starting from the same place. - A touch starting in this strip is not treated as the stick, and its release is not treated as a ground tap; a stick drag that started above and moves in keeps its hold as usual. Only the iPad front end passes a non-zero value; on phones it is always 0. */ -static float g_edge_b; -void dm_tc_set_edge_bottom(float bottom) { g_edge_b = isfinite(bottom) && bottom > 0 ? bottom : 0; } -void dm_tc_set_safe_insets(float left, float right) { g_safe_l = isfinite(left) && left > 0 ? left : 0; g_safe_r = isfinite(right) && right > 0 ? right : 0; } - -void dm_tc_layout_make(dm_tc_layout *l, float width, float height) { - memset(l, 0, sizeof(*l)); - l->width = isfinite(width) && width > 0 ? width : 0; - l->height = isfinite(height) && height > 0 ? height : 0; - float k = scale_for(l); - float main_x = l->width - 56 * k, main_y = l->height - 82 * k; - { float over = main_x + 40*k - (l->width - g_safe_r); if (over > 0) main_x -= over; } /* The rightmost one in the right-side group is the main skill key (radius 40) */ - l->target[DM_TC_MAIN] = (dm_tc_target){ main_x, main_y, 40*k, DM_BTN_A }; - /* Unit vectors of the quarter arc: 180°, 150°, 120°, 90°. */ - const float dx[4] = {-1, -0.8660254038f, -0.5f, 0}; - const float dy[4] = {0, -0.5f, -0.8660254038f, -1}; - const uint64_t skill[4] = {DM_BTN_B, DM_BTN_X, DM_BTN_Y, DM_BTN_R}; - for (int i = 0; i < 4; ++i) - l->target[DM_TC_SKILL1+i] = (dm_tc_target){main_x+112*k*dx[i], main_y+112*k*dy[i], 26*k, skill[i]}; - /*Bottom bar = the six cells of bottom UI v4 (NativeHUD/v4; the game HUD scales by screen height and is centered on the screen centerline, so convert by screen height): - in the 2868×1320 mock, cell center x = 1090 + 120×cell index, y = 1241.5, cell width 114; cell 0 difficulty, 1–4 the four belt columns, 5 the function entry (PLUS). - Town portal is not in the bottom bar (too close to the potions, easy to hit by mistake): it is instead a round button directly above the topmost one on the skill arc (skill four), same radius, gap 12; on tap the host casts the original game's town portal skill (dm_freetp.c), with no key press. */ - /* The four belt columns map to the D-pad: left = column 1, up = column 2, down = column 3, right = column 4 (measured in the original game: pressing "right" decrements column 4, pressing "down" decrements column 3; the first version had the last two swapped, which was only hit on a real device) */ - const uint64_t bottom[5] = {DM_BTN_LEFT, DM_BTN_UP, DM_BTN_DOWN, DM_BTN_RIGHT, DM_BTN_PLUS}; - const int cell[5] = {1, 2, 3, 4, 5}; - float u = l->height / 1320.0f; - for (int i = 0; i < 5; ++i) - l->target[DM_TC_BELT1+i] = (dm_tc_target){l->width/2+(1090+120*cell[i]-1434)*u, 1241.5f*u, 57*u, bottom[i]}; - { const dm_tc_target *s4 = &l->target[DM_TC_SKILL4]; l->target[DM_TC_PORTAL] = (dm_tc_target){s4->x, s4->y-s4->r-12*k-26*k, 26*k, 0}; } - /* Cell 0 difficulty and the selector bar: the selector bar has 8 cells of the same cell width, cell pitch 120, centerline aligned with the centerline between the two orbs (mock 1390), cell center y = 1051.5 (above the seven cells, clear of the value text) */ - l->diff = (dm_tc_target){l->width/2+(1090-1434)*u, 1241.5f*u, 57*u, 0}; - for (int i = 0; i < 8; ++i) l->pick[i] = (dm_tc_target){l->width/2+(1390+(i-3.5f)*120-1434)*u, 1051.5f*u, 57*u, 0}; - l->target[DM_TC_MAP] = (dm_tc_target){l->width-30*k, 30*k, 22*k, DM_BTN_MINUS}; - /* Skill set switch: lower-left of the main skill, below skill one, clear of the main skill and the four keys on the arc */ - /* Position and size: directly below skill one, radius 22 (diameter 44 points ≈ 7.3 mm); top edge 4 points from the bottom edge of skill one, bottom edge 10 points from the bottom of the screen (956×440) */ - /*On pictures narrower than the phone's (16:9: iPad letterboxed / true full-screen game view, 667×375) the bottom bar scales by screen height and the right-end statue wing tip (mock x 2075) would extend under the set-switch key: - the key moves right to at least 8 points away from the wing tip, size unchanged (measured: at 1366×768 the original position overlaps by 21 points; at 956×440 it is already 28 points apart and is not moved) */ - { float sx = main_x-112*k, bar_r = l->width/2+(2015-1434)*u+8*k+22*k; /* Right statue wing tip: 2075 with seven cells; with six cells the right end of the stone bar is pulled in by 60 */ if (sx < bar_r) sx = bar_r; - l->swap = (dm_tc_target){sx, main_y+52*k, 22*k, 0}; } - /* Outside the bottom bar are the life / mana orbs; the orbs are not input targets. */ - l->joy_x = fmaxf(90*k, g_safe_l + 60*k); /* The left edge of the stick base (radius 60) does not cross the left boundary of the safe area */ - l->joy_y = l->height - 100*k; - l->joy_radius = 60*k; -} - -int dm_tc_hit(const dm_tc_layout *l, float x, float y) { - if (!isfinite(x) || !isfinite(y) || x < 0 || y < 0 || x > l->width || y > l->height) - return DM_TC_NONE; - for (int i = 0; i < DM_TC_COUNT; ++i) { - const dm_tc_target *t = &l->target[i]; - float dx = x-t->x, dy = y-t->y; - if (t->r > 0 && dx*dx+dy*dy <= t->r*t->r) return i; - } - float sx = x-l->swap.x, sy = y-l->swap.y; - if (l->swap.r > 0 && sx*sx+sy*sy <= l->swap.r*l->swap.r) return DM_TC_SWAP; - return DM_TC_NONE; -} - -static int skill_target(int target) { - return target >= DM_TC_MAIN && target <= DM_TC_SKILL4; -} - -/* Drag direction (unit vector, screen coordinates): a drag that does not pass the 10-point dead zone = no direction */ -static int aim_dir(const dm_tc_state *s, const dm_tc_finger *f, float *ux, float *uy) { - float dx = f->x-f->aim_origin_x, dy = f->y-f->aim_origin_y, d = hypotf(dx, dy); - if (!(d > 10 * scale_for(&s->layout))) return 0; - *ux = dx/d; *uy = dy/d; return 1; -} - -static int inside_map_cancel(const dm_tc_state *s, const dm_tc_finger *f) { - const dm_tc_target *map = &s->layout.target[DM_TC_MAP]; - return map->r > 0 && fabsf(f->x-map->x) <= map->r && fabsf(f->y-map->y) <= map->r; -} - -static int g_iso_fix = 1; -void dm_tc_set_iso_fix(int on) { g_iso_fix = !!on; } -static void update_aim(dm_tc_state *s) { - s->rx = s->ry = 0; - s->aim_slot = DM_TC_NONE; - s->map_cancel_active = 0; - if (s->navigation_mode) return; - uint64_t latest = 0; - for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) { - const dm_tc_finger *f = &s->finger[i]; - if (!f->active || !skill_target(f->target)) continue; - if (inside_map_cancel(s, f)) s->map_cancel_active = 1; - if (!f->aim_cancelled && f->aim_move_sequence > latest) { - latest = f->aim_move_sequence; - s->aim_slot = i; - } - } - if (s->pulse_button) { s->rx = s->pulse_rx; s->ry = s->pulse_ry; return; } - float radius = 60 * scale_for(&s->layout); - if (s->aim_slot == DM_TC_NONE || radius <= 0) return; - const dm_tc_finger *f = &s->finger[s->aim_slot]; - if (f->target != DM_TC_MAIN && hypotf(f->x-f->aim_origin_x, f->y-f->aim_origin_y) <= 10 * scale_for(&s->layout)) return; /* Skill button: a drag of no more than 10 points = tap, no direction given, the original game auto-targets */ - float dx = (f->x-f->aim_origin_x)/radius; - float dy = (f->aim_origin_y-f->y)/radius; - s->rx = (int32_t)lroundf(fmaxf(-1, fminf(1, dx))*DM_STICK_MAX); - s->ry = (int32_t)lroundf(fmaxf(-1, fminf(1, dy))*DM_STICK_MAX); -} - -static void update(dm_tc_state *s) { - s->buttons = 0; - for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) { - dm_tc_finger *f = &s->finger[i]; - if (f->active && !f->aim_cancelled && f->target >= 0 && f->target < DM_TC_COUNT) { - uint64_t b = f->target==DM_TC_PORTAL && s->portal_pending ? DM_BTN_ZL : s->layout.target[f->target].button; - /*Drag to set direction (the core gesture). Skill button: after press, first wait DM_TC_AIM_SETTLE_S to see whether it is a tap or a drag -- once dragged past the dead zone, key down immediately with the right-axis direction, - and key down too if the time is reached without dragging (tap = the original game's auto target / facing); otherwise the key would go down first and the right axis deflect later, and the shot at press would aim at the old facing. - Main button: key down on press, no wait (it is the most frequent key and a tap should not wait longer; with press-then-drag the first shot uses the native facing, and from the second shot on it follows the finger). - Direction changes while held are re-read every tick by the host's aim adapter, so no "release-press" is done here; the decision time is not lengthened for "press, pause, then drag". */ - if (!s->navigation_mode && f->target >= DM_TC_SKILL1 && f->target <= DM_TC_SKILL4) { - float ux, uy; - if (!f->key_down && !aim_dir(s, f, &ux, &uy) && s->last_time_s - f->start_time_s < DM_TC_AIM_SETTLE_S) b = 0; - else if (!f->key_down) { f->key_down = 1; f->key_down_s = s->last_time_s; } - } - /*Multi-row belt: swiping up on a belt cell = expand, swiping down = collapse, so the key is not sent on press; always wait for the moment of release and decide by displacement (dm_tc_end_at). No "drink once held long enough" timer: - a slightly slow swipe up / down would first be judged by it as drinking a potion (hit once each on a real device and on the second Mac). With only 1 row, still drink on press as before */ - /* with only 1 row, also always drink at the moment of release (same path as multiple rows) */ - if (!s->navigation_mode && f->target >= DM_TC_BELT1 && f->target <= DM_TC_BELT4) b = 0; - s->buttons |= b; - } - } - s->buttons |= s->pulse_button; - /*Second skill set: ZL is the original game's set-switch modifier. It is added only at the moment a skill is used -- while a finger is on the main button / a skill button, or a skill pulse is being sent (including waiting to be sent); - it is not pressed the rest of the time: holding it all the time would turn the cross (+), the belt and town portal all into "ZL + key" combos (in state Ⅱ the cross could not open the panel). - ZL goes down first, and the main button's key goes down DM_TC_ZL_LEAD_S later (pressed in the same frame, the game only recognizes the main key = casts the first set's skill). */ - if (s->skill_set && !s->navigation_mode) { - /*The pulse being sent carries ZL only if it is a skill button (the make-up press of the main button / a skill button): a drink pulse made up on release of a multi-row belt, if it carried ZL, would become the original game's "ZL + belt key = feed the mercenary" - (actual run on the local machine: tapping the belt under the second set, 0x26 packet target byte = 1, mercenary life rose 642 → 762, player unchanged) */ - /* A skill waiting for a make-up send also needs ZL to lead -- but only when no other pulse is being sent: adding ZL while a belt pulse is being sent likewise becomes "feed the mercenary" (the unit-test probe read 0x1100) */ - int use = s->defer_button != 0 && !s->pulse_button; - for (int t = DM_TC_MAIN; t <= DM_TC_SKILL4 && !use; ++t) if (s->pulse_button && s->pulse_button == s->layout.target[t].button) use = 1; - /*The "drink for the player" belt pulse sent while a finger holds a skill of the second skill set: with ZL still held it becomes the original game's "ZL + belt key = feed the mercenary". ZL and the held skill key have to get out of the way together - (releasing only ZL and not the skill key = casting the first set's skill), and they have to get out of the way first, with the belt key pressed after a gap: with "release ZL + press belt key" in the same tick the game still counts ZL as held - (measured in a real run: what went out was still the feed-the-mercenary packet -- where the host fetches the controller it latches the press edge, and what was sampled this tick was both ZL and the belt key down). So there are three stages: - waiting to send (belt_gap_cell) → the pulse is being sent → after it is withdrawn ZL comes back first, and the skill key comes back once ZL has led by a full DM_TC_ZL_LEAD_S (pressed in the same frame, the game only recognizes the main key) */ - int belt_pulse = s->belt_gap_cell != 0; - for (int t = DM_TC_BELT1; t <= DM_TC_BELT4; ++t) if (s->pulse_button && s->pulse_button == s->layout.target[t].button) belt_pulse = 1; - if (belt_pulse) s->belt_pulse_s = s->last_time_s; - for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) { - const dm_tc_finger *f = &s->finger[i]; - if (!f->active || f->aim_cancelled || f->target < DM_TC_MAIN || f->target > DM_TC_SKILL4) continue; - if (!belt_pulse) use = 1; - if (belt_pulse || s->last_time_s - f->start_time_s < DM_TC_ZL_LEAD_S || (s->belt_pulse_s > 0 && s->last_time_s - s->belt_pulse_s < DM_TC_ZL_LEAD_S)) s->buttons &= ~s->layout.target[f->target].button; - } - if (use) s->buttons |= DM_BTN_ZL; - } - update_aim(s); - s->lx = s->ly = 0; - if (s->joy_slot < 0) return; - dm_tc_finger *f = &s->finger[s->joy_slot]; - float dx = f->x-s->joy_origin_x, dy = s->joy_origin_y-f->y; - /*Input travel does not follow the visual enlargement on large screens: the thumb does not grow with the screen. Full deflection 60 points, dead zone 6 points, same on phone and iPad - (it used to be multiplied by the visual factor, so a 12.9-inch iPad needed a 105-point ≈ 20 mm push for full deflection, twice the phone's). The size of the base and the stick head is still drawn by the visual factor. */ - float in_k = fminf(1.0f, scale_for(&s->layout)); - float distance = hypotf(dx, dy), radius = 60 * in_k; - float dead = 6 * in_k; - if (distance <= dead || radius <= dead) return; - float strength = fminf(1, (distance-dead)/(radius-dead)); - /*Diagonal pre-correction: the original game turns the stick into a world direction by a pure 45° rotation and does not compensate for the 2:1 of the isometric view, so the screen angle the character actually walks = atan(tan(stick angle) / 2) (push 45°, walk 26.6°). - Controller players do not see the mapping between stick and picture, but on a touchscreen what the finger draws is the screen direction, so before sending the value the vertical component is doubled and then scaled back to the original magnitude: screen angle = finger angle, push depth unchanged. - Only the movement stick in game state is affected (navigation state has no stick value; skill direction goes through the right axis and does not pass here) */ - float ex = dx/distance, ey = dy/distance; - if (g_iso_fix) { float n = hypotf(ex, 2*ey); ex /= n; ey = 2*ey/n; } - s->lx = (int32_t)lroundf(ex * strength * DM_STICK_MAX); - s->ly = (int32_t)lroundf(ey * strength * DM_STICK_MAX); -} - -static int pointer_finger(const dm_tc_state *s, const dm_tc_finger *f) { - return s->navigation_mode && f->active && s->pointer_slot >= 0 && - &s->finger[s->pointer_slot] == f; -} - -static void update_pointer(dm_tc_state *s) { - if (!s->pointer_valid || s->pointer_slot < 0 || s->pointer_slot >= DM_TC_MAX_TOUCH) return; - dm_tc_finger *f = &s->finger[s->pointer_slot]; - if (f->pointer_dragged && s->pointer_pickup_done && !f->shop_drag && !f->locked_drag) { - s->pointer_x = f->x; - s->pointer_y = f->y - DM_TC_POINTER_LIFT_PT * scale_for(&s->layout); - } -} - -/*Pointer pulse (kind 1 = pick-up of a drag-and-drop, 2 = put-down / tap): the pointer first lands at (x,y), then A is pressed. While the previous pulse has not been released (the game has not consumed it yet) the pointer is not moved and the request is only recorded, - and it starts only when the front end calls dm_tc_pulse_resume, after the previous one is released and the game has seen "A released" once -- otherwise two A presses merge into one, or the pick-up lands at the put-down position */ -static void pointer_pulse_btn(dm_tc_state *s, float x, float y, int kind, uint64_t button, double hold_s) { - if (!s->pulse_button) { - s->pointer_x = x; s->pointer_y = y; - s->pulse_button = button; - s->pulse_hold_s = hold_s; - s->pulse_rx = s->pulse_ry = 0; - s->pointer_pulse_active = kind; - ++s->pulse_epoch; - } else { - s->pointer_pulse_pending = kind; - s->pointer_pending_x = x; - s->pointer_pending_y = y; - s->pointer_pending_button = button; s->pointer_pending_hold = hold_s; - } -} -static void pointer_pulse_start(dm_tc_state *s, float x, float y, int kind) { pointer_pulse_btn(s, x, y, kind, DM_BTN_A, 0); } -void dm_tc_pointer_release(dm_tc_state *s, int kind) { s->pointer_release_item = kind == 1 || kind == 3 || kind == 4 || kind == 8 ? kind : 0; } -void dm_tc_pointer_release_at(dm_tc_state *s, float x, float y) { s->pointer_release_item = 5; s->release_x = x; s->release_y = y; } -void dm_tc_set_holding(dm_tc_state *s, int holding) { s->holding = !!holding; } -void dm_tc_pointer_item(dm_tc_state *s, int kind) { - if (kind == 7) { s->pointer_shop = 1; kind = 1; } - s->pointer_lock = kind == 10; if (kind == 10) kind = 1; - s->pointer_stack = kind == 8; if (kind == 8) kind = 1; /* 8 = stacked cell of the advanced stash: follows the item area, only double tap differs */ /* 7 = goods on the vendor shelf: everything else follows the item area, only drag start differs */ - s->pointer_item = (kind >= 1 && kind <= 3) || kind == 6 || kind == 11 ? kind : 0; /* 11 = this tap does not click (a covered button is underneath), but it may start a scroll gesture (text / empty space in the settings page list) */ - if (s->mouse_click_mode && s->pointer_slot >= 0 && s->pointer_slot < DM_TC_MAX_TOUCH && s->finger[s->pointer_slot].active && !s->finger[s->pointer_slot].pointer_dragged) { - if (kind == 1) { if (!s->pulse_button) { s->pointer_valid = 1; s->pointer_x = s->finger[s->pointer_slot].aim_origin_x; s->pointer_y = s->finger[s->pointer_slot].aim_origin_y; } } - else s->pointer_valid = 0; /* Not in the item area: release the controller cursor (based on this the front end clears the cursor request and stops pushing the stick) */ - } -} /* 1 item area, 2 tab (a tap sends neither a key nor a mouse click; the front end separately calls the host to switch pages) */ -static _Atomic int g_dead_modal; -static _Atomic int g_vendor_open; /* Vendor trade page open: ZL switches shelf tabs, so "tap the same spot again = compare" is not sent */ -static _Atomic int g_close_stop; /* A confirmation box popped up (a panel whose name ends in Modal opened) in the middle of "tap outside the panel = press back until the panel closes": stop pressing the remaining back presses */ -static _Atomic int g_tap_box; /* The most recently single-tapped item is the cube (synced by the front end from the host every tick) */ -void dm_tc_set_tap_box(int on) { atomic_store(&g_tap_box, on != 0); } -static _Atomic unsigned g_cur_stack; -void dm_tc_set_cur_stack(unsigned id) { atomic_store(&g_cur_stack, id); } -static _Atomic unsigned g_hover_unid; -void dm_tc_set_hover_unid(unsigned id) { atomic_store(&g_hover_unid, id); } -static _Atomic int g_imbue_open; /* Single-item quest slot panel open (imbue / socket / personalize / tomb pedestal, all the same ImbueItemsPanel) */ -static _Atomic int g_cube_open; /* Horadric Cube page open: double tap = hold Y (the original game's "move to the other side": inventory → cube / cube → inventory), not "use / equip" */ -static _Atomic int g_vault_open; /* Stash open on page six "Chronicle": double-tapping an inventory item = store it in the Chronicle vault (goes through use_seq; the host's ui_use_pump determines the page); do not send the original game's "hold Y" -- the game treats that page as the cube page */ -static _Atomic int g_bank_open; /* Stash open (set by the front end from the host's reading of the game panel tree, not from open/close messages): double tap = hold Y (move to the other side) */ -void dm_tc_set_panel_state(int bits) { atomic_store(&g_bank_open, (bits & 1) != 0); atomic_store(&g_vendor_open, (bits & 2) != 0); atomic_store(&g_dead_modal, (bits & 4) != 0); atomic_store(&g_imbue_open, (bits & 16) != 0); atomic_store(&g_cube_open, (bits & 32) != 0); atomic_store(&g_vault_open, (bits & 64) != 0); } /* Bits are the same as DM_PS_* in dm_cursor.h; obtained by the host reading the game state */ - -static void pointer_drag_begin(dm_tc_state *s, dm_tc_finger *f) { - f->pointer_dragged = 1; - if (s->pointer_lock) { f->locked_drag = 1; return; } /* Drag starting from another item while the single-item slot is occupied: do not pick up; this drag does nothing */ - if (s->pointer_shop || s->pointer_stack) { f->shop_drag = 1; s->pointer_x = 2; s->pointer_y = 2; update(s); return; } /* Drag starting from goods on the shelf: do not pick up; move the cursor to a screen corner so the tooltip on the goods closes (at purchase time the host puts the cursor back on that item) */ - s->pointer_pickup_done = 0; - pointer_pulse_start(s, f->aim_origin_x, f->aim_origin_y, 1); -} - -void dm_tc_cancel_all(dm_tc_state *s) { - ++s->gesture_epoch; s->portal_pending=s->portal_phase=0; s->diff_open=0; - memset(s->finger, 0, sizeof(s->finger)); - for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) s->finger[i].target = DM_TC_NONE; - s->joy_slot = DM_TC_NONE; - s->joy_origin_x = s->joy_origin_y = 0; - s->buttons = 0; - s->lx = s->ly = s->rx = s->ry = 0; - s->aim_slot = DM_TC_NONE; - s->aim_move_sequence = 0; - s->map_cancel_active = 0; - s->pulse_button = 0; s->pulse_rx = s->pulse_ry = 0; - s->belt_q = 0; s->belt_gap_cell = 0; /* queued and pending drinks are voided as well */ /* The belt cell waiting for single/double tap decision and the pending mercenary feed are voided together */ - s->defer_button = 0; /* The skill press waiting for a make-up send is voided as well: previously it was still cast 0.2 seconds after the cancel */ - s->pointer_slot = DM_TC_NONE; - s->pointer_valid = 0; s->pointer_x = s->pointer_y = 0; - s->pointer_pickup_done = 0; - s->pointer_pulse_active = s->pointer_pulse_pending = 0; - s->pointer_pending_x = s->pointer_pending_y = 0; -} - -int dm_tc_pulse_end(dm_tc_state *s, uint64_t epoch) { - if (!s->pulse_button || s->pulse_epoch != epoch) return 0; - int ended_pointer = s->pointer_pulse_active; - if (s->defer_button && s->defer_at_s < s->last_time_s + DM_TC_ZL_LEAD_S) s->defer_at_s = s->last_time_s + DM_TC_ZL_LEAD_S; /* Skill waiting for a make-up send: ZL can start leading only from this moment (see dm_tc_tick) */ - s->pulse_button = 0; s->pulse_rx = s->pulse_ry = 0; s->pulse_hold_s = 0; - s->pointer_pulse_active = 0; - if (ended_pointer == 1) { - s->pointer_pickup_done = 1; - update_pointer(s); - } - update(s); - return 1; -} - -int dm_tc_pulse_pending(const dm_tc_state *s) { return !s->pulse_button && s->pointer_pulse_pending; } - -int dm_tc_pulse_resume(dm_tc_state *s) { - if (s->pulse_button || !s->pointer_pulse_pending) return 0; - int kind = s->pointer_pulse_pending; - s->pointer_pulse_pending = 0; - pointer_pulse_btn(s, s->pointer_pending_x, s->pointer_pending_y, kind, s->pointer_pending_button ? s->pointer_pending_button : DM_BTN_A, s->pointer_pending_hold); - s->pointer_pending_button = 0; s->pointer_pending_hold = 0; - update(s); - return 1; -} - -void dm_tc_init(dm_tc_state *s, float width, float height) { - memset(s, 0, sizeof(*s)); - s->pointer_release_item = 1; - dm_tc_layout_make(&s->layout, width, height); - s->enabled_targets = (1u << DM_TC_COUNT) - 1; - dm_tc_cancel_all(s); -} - -void dm_tc_resize(dm_tc_state *s, float width, float height) { - uint64_t portal = s->layout.target[DM_TC_PORTAL].button; - dm_tc_cancel_all(s); - dm_tc_layout_make(&s->layout, width, height); - s->layout.target[DM_TC_PORTAL].button = portal; -} - -void dm_tc_enable_targets(dm_tc_state *s, uint32_t mask) { - mask &= (1u << DM_TC_COUNT) - 1; - if (mask == s->enabled_targets) return; - dm_tc_cancel_all(s); - s->enabled_targets = mask; -} - -void dm_tc_set_joy_block(dm_tc_state *s, int block) { s->joy_block = block != 0; } -void dm_tc_set_navigation_mode(dm_tc_state *s, int navigation_mode) { - navigation_mode = !!navigation_mode; - if (navigation_mode == s->navigation_mode) return; - dm_tc_cancel_all(s); - s->navigation_mode = navigation_mode; -} - -/* The system cancelled this finger (notification banner, system gesture): do not treat it as a release -- no make-up pulse, no world tap, no UI click; only let go of the keys, stick and cursor it was holding */ -void dm_tc_cancel_finger(dm_tc_state *s, int slot) { - if (slot < 0 || slot >= DM_TC_MAX_TOUCH) return; - ++s->gesture_epoch; - if (s->finger[slot].active && s->finger[slot].target == DM_TC_PORTAL) { dm_tc_cancel_portal(s); } - if (slot == s->joy_slot && s->finger[slot].active) { s->joy_lost_s = s->last_time_s; s->joy_lost_x = s->finger[slot].x; s->joy_lost_y = s->finger[slot].y; } /* the system cancelled the joystick finger: record it, and a finger landing again nearby right afterwards continues as the joystick */ - s->finger[slot] = (dm_tc_finger){.target=DM_TC_NONE}; - if (slot == s->joy_slot) { s->joy_slot = DM_TC_NONE; s->joy_origin_x = s->joy_origin_y = 0; } - if (slot == s->aim_slot) s->aim_slot = DM_TC_NONE; - if (slot == s->pointer_slot) { s->pointer_item = 0; s->pointer_shop = 0; s->pointer_stack = 0; s->pointer_lock = 0; s->pointer_slot = DM_TC_NONE; s->pointer_valid = 0; } - update(s); -} -void dm_tc_cancel_portal(dm_tc_state *s) { - ++s->gesture_epoch; s->portal_pending=s->portal_phase=0; - for (int i=0;i<DM_TC_MAX_TOUCH;++i) - if(s->finger[i].active && s->finger[i].target==DM_TC_PORTAL) - s->finger[i]=(dm_tc_finger){.target=DM_TC_NONE}; - update(s); -} -void dm_tc_set_portal_binding(dm_tc_state *s,uint64_t mask,uint64_t identity,uint64_t scene) { - if(s->layout.target[DM_TC_PORTAL].button==mask && s->portal_binding_identity==identity && s->portal_scene_epoch==scene)return; - dm_tc_cancel_portal(s); - s->portal_binding_identity=identity;s->portal_scene_epoch=scene; - s->layout.target[DM_TC_PORTAL].button=mask;update(s); -} -void dm_tc_set_portal_button(dm_tc_state *s,uint64_t mask) { - dm_tc_set_portal_binding(s,mask,s->portal_binding_identity,s->portal_scene_epoch); -} -int dm_tc_portal_ack(dm_tc_state *s,uint64_t epoch,uint64_t identity,uint64_t scene) { - if(!s->portal_pending || s->gesture_epoch!=epoch || s->portal_binding_identity!=identity || s->portal_scene_epoch!=scene)return 0; - int active=0;for(int i=0;i<DM_TC_MAX_TOUCH;i++)active|=s->finger[i].active && s->finger[i].target==DM_TC_PORTAL; - if(!active)return 0; - s->portal_pending=0;s->portal_phase=2;update(s);return 1; -} - -/* Belt expanded and this point is not on the belt (outside the four bottom-row cells and the expanded rows): based on this the front end collapses the belt and consumes this tap before its own gestures (minimap block, empty-slot cross) */ -int dm_tc_belt_outside(const dm_tc_state *s, float x, float y) { - if (s->navigation_mode || !s->belt_open || s->belt_rows <= 1) return 0; - int hit = dm_tc_hit(&s->layout, x, y); if (hit >= DM_TC_BELT1 && hit <= DM_TC_BELT4) return 0; - float u = s->layout.height / 1320.0f; - for (int r = 1; r < s->belt_rows && r < 4; ++r) for (int c = 0; c < 4; ++c) { - const dm_tc_target *t = &s->layout.target[DM_TC_BELT1+c]; - if (fabsf(x - t->x) <= 60*u && fabsf(y - (t->y - (165.0f + 162.0f*(r-1))*u)) <= 53.75f*u) return 0; - } - return 1; -} -/*One tap on the belt = the player drinks, several taps drink several (original feel, no double-tap detection). Bottom row = one belt key pulse (without ZL, also under the second set); upper rows have no controller key = belt_request asks the host to drink that cell. - Feeding the mercenary has no gesture on the belt: the game's own entry in the mercenary panel and the "auto-feed mercenary" setting are unchanged */ -static void belt_fire(dm_tc_state *s, int cell) { - if (cell >= 0x100) { s->belt_request = cell; return; } - uint64_t btn = s->layout.target[DM_TC_BELT1 + (cell & 0xf)].button; if (!btn) return; - if (s->pulse_button || s->defer_button || s->belt_gap_cell) { s->belt_q = cell; return; } /* A pulse is being sent / a make-up send is waiting: queue it (dm_tc_tick sends it when free) */ - if (s->skill_set && !s->navigation_mode) for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) { /* a skill of the second skill set is held: first release ZL and the skill key, and press the belt key after DM_TC_ZL_LEAD_S (see update) */ - const dm_tc_finger *f = &s->finger[i]; if (!f->active || f->aim_cancelled || f->target < DM_TC_MAIN || f->target > DM_TC_SKILL4) continue; - s->belt_gap_cell = 0x1000 | cell; s->belt_gap_at = s->last_time_s + DM_TC_ZL_LEAD_S; update(s); return; } - s->pulse_rx = s->pulse_ry = 0; s->pulse_button = btn; ++s->pulse_epoch; - update(s); -} -void dm_tc_begin_at(dm_tc_state *s, int slot, float x, float y, double now_s) { - if (slot < 0 || slot >= DM_TC_MAX_TOUCH || s->finger[slot].active || !isfinite(x) || !isfinite(y)) return; - s->last_time_s = now_s; - dm_tc_finger *f = &s->finger[slot]; - if (!s->navigation_mode) { /* Difficulty: tapping cell 0 opens / collapses the selector bar; when expanded, tapping a cell = select that level and collapse; tapping elsewhere only collapses and is then handled as usual */ - const dm_tc_target *d = &s->layout.diff; - int on_diff = d->r > 0 && hypotf(x-d->x, y-d->y) <= d->r, pick = -1; - for (int i = 0; s->diff_open && i < 8; ++i) if (hypotf(x-s->layout.pick[i].x, y-s->layout.pick[i].y) <= s->layout.pick[i].r) pick = i; - if (on_diff || pick >= 0) { - if (pick >= 0) s->diff_request = pick + 1; - s->diff_open = on_diff ? !s->diff_open : 0; - *f = (dm_tc_finger){.active=1, .target=DM_TC_DIFF, .x=x, .y=y}; update(s); return; - } - s->diff_open = 0; - } - int hit = dm_tc_hit(&s->layout, x, y); - if (!s->navigation_mode && s->belt_open && s->belt_rows > 1) { /* Belt expanded: tapping the upper rows = drink that cell; tapping outside the belt = only collapse */ - float u = s->layout.height / 1320.0f; /* The expanded rows are drawn by the game itself: columns align with the bottom row, row pitch 162, the first row is 165 above the bottom row (mock units; measured on a real device at 2868×1320; the row spacing is set by bpy in gen_hud_v4.py, and changing it there requires changing it here too) */ - for (int r = 1; r < s->belt_rows && r < 4; ++r) for (int c = 0; c < 4; ++c) { - const dm_tc_target *t = &s->layout.target[DM_TC_BELT1+c]; - if (fabsf(x - t->x) <= 60*u && fabsf(y - (t->y - (165.0f + 162.0f*(r-1))*u)) <= 53.75f*u) { - belt_fire(s, 0x100 | r << 4 | c); /* drink that cell on press */ - *f = (dm_tc_finger){.active=1, .target=DM_TC_DIFF, .x=x, .y=y}; update(s); return; - } - } - if (hit < DM_TC_BELT1 || hit > DM_TC_BELT4) { /* Tap elsewhere: only collapse the belt; this tap does nothing else (no walking, no world tap, no key press) */ - s->belt_request = 2; *f = (dm_tc_finger){.active=1, .target=DM_TC_DIFF, .x=x, .y=y}; update(s); return; - } - } - if (hit == DM_TC_SWAP) { - if (s->navigation_mode) hit = DM_TC_NONE; - else { s->skill_set = !s->skill_set; *f = (dm_tc_finger){.active=1, .target=DM_TC_SWAP, .x=x, .y=y}; update(s); return; } - } - int enabled = hit >= 0 && (s->enabled_targets & (1u << hit)); - /*Panel state: disabled targets (bottom-bar belt / function key / town portal, and the skill button positions after the round buttons are hidden) do not capture fingers -- the place they cover may be exactly a panel button - ("Apply" on the settings page is at the bottom of the screen and landed on a disabled bottom-bar target, getting neither classified nor clicked). In game state they still capture (preventing a hidden town-portal touch point from turning into the stick) */ - if (s->navigation_mode && hit >= 0 && !enabled) hit = DM_TC_NONE; - if(enabled && hit==DM_TC_PORTAL && s->layout.target[hit].button && s->portal_phase==0) { - ++s->gesture_epoch; - uint64_t mask=s->layout.target[hit].button; - s->portal_pending=(mask&DM_BTN_ZL) && (mask&~DM_BTN_ZL); - s->portal_phase=s->portal_pending?1:2; - } - *f = (dm_tc_finger){.active=1, .target=enabled ? hit : DM_TC_NONE, .x=x, .y=y, - .aim_origin_x=x, .aim_origin_y=y, - .start_time_s=now_s}; - /* A disabled circle still blocks falling through into the left stick area, preventing a hidden town-portal touch point from turning into movement. */ - if (s->navigation_mode && hit == DM_TC_NONE && s->pointer_slot == DM_TC_NONE) { - s->pointer_slot = slot; - s->pointer_item = 0; - /*In mouse click mode the controller cursor follows only "a finger that landed in the item area" (the front end calls dm_tc_pointer_item once it has the hit answer): - the cursor moves by pushing the stick, and on pages without a free cursor (options, pause menu, waypoint) pushing the stick = changing the current option / moving the focus (tapping a tab changed "Show Items" as a side effect) */ - if (!s->mouse_click_mode) { s->pointer_valid = 1; s->pointer_x = x; s->pointer_y = y; } - } else if (!s->navigation_mode && hit == DM_TC_NONE && g_edge_b > 0 && x >= 0 && x < s->layout.width*0.43f && y > s->layout.height - g_edge_b) { - f->target = DM_TC_EDGE; - } else if (!s->navigation_mode && !s->joy_block && hit == DM_TC_NONE && s->joy_slot == DM_TC_NONE && - x >= 0 && y <= s->layout.height && x < s->layout.width*0.43f && y > s->layout.height*0.25f && - (hypotf(x - s->layout.joy_x, y - s->layout.joy_y) <= DM_TC_JOY_GRAB_K * s->layout.joy_radius || - (s->joy_lost_s > 0 && now_s - s->joy_lost_s <= DM_TC_JOY_RESUME_S && hypotf(x - s->joy_lost_x, y - s->joy_lost_y) <= DM_TC_JOY_GRAB_K * s->layout.joy_radius))) { - /*The joystick base is fixed at its home position in the lower left: the joystick starts only when a finger lands near the base (within DM_TC_JOY_GRAB_K × radius), and direction and magnitude are computed as "finger relative to the base center"; - once held it is still recognized however far it is dragged; landing elsewhere on the left does not start the joystick (it is a ground tap as before). Previously it was a floating joystick: wherever in the left area the finger landed, the base appeared there, returning to its home position on release. - Exception: when the joystick finger has just been cancelled by the system (notification banner, system gesture; dm_tc_cancel_finger records the time and position), a finger landing again near the same spot within DM_TC_JOY_RESUME_S continues as the joystick -- - a joystick dragged far away should not be cut off for good by a brief system interruption; with the floating joystick this case showed up as the base restarting under the finger */ - s->joy_lost_s = 0; - f->target = DM_TC_JOYSTICK; - s->joy_slot = slot; - s->joy_origin_x = s->layout.joy_x; - s->joy_origin_y = s->layout.joy_y; - } - update(s); -} - -void dm_tc_move_at(dm_tc_state *s, int slot, float x, float y, double now_s) { - if (slot < 0 || slot >= DM_TC_MAX_TOUCH || !s->finger[slot].active || !isfinite(x) || !isfinite(y)) return; - s->last_time_s = now_s; - dm_tc_finger *f = &s->finger[slot]; - f->x = x; - f->y = y; - if (pointer_finger(s, f)) { - float distance = hypotf(f->x-f->aim_origin_x, f->y-f->aim_origin_y); - if (!f->pointer_dragged && distance > DM_TC_POINTER_MOVE_PT * scale_for(&s->layout) && (!s->mouse_click_mode || s->pointer_item == 1)) - pointer_drag_begin(s, f); /* Mouse click mode: only a gesture starting in the item area is a drag-and-drop; swipes elsewhere send no key. Holding still longer than the long-press duration and then dragging also counts as drag-and-drop (a real person "holds for a moment and then drags"; z4: a drag that started moving only after 0.5 seconds could not pick the item up) */ - /* A gesture starting outside the modal box / covered by an upper layer (class 6 = this tap does not click) does not start a scroll / slider gesture either: previously, with a modal box open, swiping up and down outside it still sent wheel events to what was underneath */ - else if (s->mouse_click_mode && s->pointer_item != 1 && s->pointer_item != 3 && s->pointer_item != 6 && !f->ui_drag && !f->pointer_dragged && distance > DM_TC_POINTER_MOVE_PT * scale_for(&s->layout)) - f->ui_drag = fabsf(f->x - f->aim_origin_x) > fabsf(f->y - f->aim_origin_y) ? 2 : 1; /* Continuous UI gesture: the type is fixed by direction at the moment the threshold is crossed (vertical = scroll, horizontal = slider) */ - update_pointer(s); - } else if (!s->navigation_mode && skill_target(f->target)) { - if (inside_map_cancel(s, f)) f->aim_cancelled = 1; - if (!f->aim_cancelled) f->aim_move_sequence = ++s->aim_move_sequence; - } - update(s); -} - -void dm_tc_end_at(dm_tc_state *s, int slot, double now_s) { - if (slot < 0 || slot >= DM_TC_MAX_TOUCH) return; - s->last_time_s = now_s; - if(s->finger[slot].active && s->finger[slot].target==DM_TC_PORTAL) { - s->finger[slot]=(dm_tc_finger){.target=DM_TC_NONE}; - for(int i=0;i<DM_TC_MAX_TOUCH;i++)if(s->finger[i].active && s->finger[i].target==DM_TC_PORTAL) {update(s);return;} - dm_tc_cancel_portal(s);return; - } - dm_tc_finger done = s->finger[slot]; - if (s->navigation_mode && slot == s->pointer_slot) { - float distance = hypotf(done.x-done.aim_origin_x, done.y-done.aim_origin_y); - double elapsed = now_s - done.start_time_s; - /* Long press on goods on the vendor shelf = the original game's "Hold Y to buy in bulk" (button legend text; host reading: about 1.3 seconds): when the inventory has a non-full scroll tome / stack of the same kind it buys until full, otherwise it is the same as buying 1; how many to buy and whether there is enough gold are decided by the game, and here we only press the key for the player */ - if (done.pointer_long && !done.pointer_dragged && !done.shop_drag && s->pointer_item == 1 && s->pointer_shop) pointer_pulse_btn(s, done.aim_origin_x, done.aim_origin_y, 2, DM_BTN_Y, 1.3); - else if (done.pointer_long && !done.pointer_dragged && !done.shop_drag && s->pointer_item == 1 && !s->pointer_lock) ++s->long_press_seq; /* Long press counts only at release (held without moving): if it counted as soon as the hold time was reached, a potion that is "held for a moment and then dragged" would first be put into the belt (2 extra occurrences in the potion-drag-to-belt script on the second Mac) */ - if (done.locked_drag) { } /* Dragging another item while the single-item slot is occupied: send no key */ - else if (done.shop_drag) { ++s->shop_drop_seq; } /* Whether to buy is decided by the front end from the drop point; no key is sent here */ - else if (done.pointer_dragged) { - if (s->pointer_release_item == 1) pointer_pulse_start(s, done.x, done.y - DM_TC_POINTER_LIFT_PT * scale_for(&s->layout), 2); /* Place at the lifted point (where the item is displayed) */ - else if (s->pointer_release_item == 5) pointer_pulse_start(s, s->release_x, s->release_y, 2); /* Drop position given by the host (equipment slot = slot center) */ - else if (s->pointer_release_item == 4) pointer_pulse_start(s, done.x, done.y, 2); /* Place at the finger point (the player aims with the finger) */ - else if (s->pointer_release_item == 3) pointer_pulse_btn(s, done.aim_origin_x, done.aim_origin_y, 2, DM_BTN_Y, 0.15); /* Finger outside the panel = discard: pointer returns to the original spot and presses Y once in place */ - else if (s->pointer_release_item == 8) { /* Released on the single-item quest slot: redo it the original game's way -- first put it back in its original cell, then hold Y on it (the original game's "put into slot"). Merely holding the item and hovering over the slot does not count as entering the slot: the slot shows it, but tapping confirm does not complete the transaction (measured on the local machine: no transaction packet) */ - pointer_pulse_start(s, done.aim_origin_x, done.aim_origin_y, 2); pointer_pulse_btn(s, done.aim_origin_x, done.aim_origin_y, 2, DM_BTN_Y, 1.3); } - else pointer_pulse_start(s, done.aim_origin_x, done.aim_origin_y, 2); /* Blank area inside the panel: put it back where it was */ - } else if (!done.ui_drag && !done.pointer_long && elapsed < DM_TC_POINTER_TAP_S && distance <= DM_TC_POINTER_MOVE_PT * scale_for(&s->layout)) { /* Dragging out and back to the original spot does not count as a tap */ - /* While the single-item quest slot panel is open, the held item is "the one in the slot": tapping an inventory cell still puts it back, tapping elsewhere is handled as if nothing were held -- the confirm / close buttons must be tappable, and tapping outside the panel = close the panel (not drop the slot's item on the ground) */ - if (s->pointer_lock) { s->pointer_x = done.aim_origin_x; s->pointer_y = done.aim_origin_y; } /* Tapping another item while the single-item slot is occupied: only move the cursor to show the tooltip; neither single nor double tap moves it */ - else if (s->holding && s->pointer_item != 0 && !(atomic_load(&g_imbue_open) && s->pointer_item != 1)) { if (s->pointer_item == 1) pointer_pulse_start(s, done.aim_origin_x, done.aim_origin_y, 2); - else if (s->pointer_item == 3) pointer_pulse_btn(s, s->pointer_x, s->pointer_y, 2, DM_BTN_Y, 0.15); } /*Still holding an item (the last drop did not succeed): tap the item area = put down; tap outside the panel / the black grid on either side = drop (Y); tapping tabs or elsewhere does nothing. - Tapping a button (class 0) is not swallowed here and sends a mouse click as usual: previously even the transmute button was swallowed, and tapping transmute with something in hand sent nothing; for the transmute button the host first stows the held item and then presses, see dm_cursor.c */ - else if (s->pointer_item == 2) { ++s->tab_tap_seq; } - else if (s->pointer_item == 6 || s->pointer_item == 11) { } /* Pressing on a button covered by an upper panel: no click, no key */ - else if (s->pointer_item == 3) { if (!atomic_load(&g_dead_modal)) { s->close_left = 3; s->close_next_s = now_s; atomic_store(&g_close_stop, 0); } } /* Outside the panel: do not send a mouse click (it would land in the game world = tap ground to walk); press B to close the panel -- in a submenu one B only goes back one level, so press until the panel is closed (at most 3 presses, see dm_tc_tick); do nothing while the death popup is open */ - else if (s->pointer_item) { /* Item area: a single tap only moves the cursor (the game shows the tooltip itself); another tap at the same spot right after = hold A / Y */ - float ox = done.aim_origin_x, oy = done.aim_origin_y; - int dbl = now_s - s->last_tap_s < DM_TC_DOUBLE_TAP_S && hypotf(ox - s->last_tap_x, oy - s->last_tap_y) <= 2 * DM_TC_POINTER_MOVE_PT * scale_for(&s->layout); - if (dbl) { if (atomic_load(&g_hover_unid) && !s->pointer_stack && !s->pointer_shop) ++s->ident_seq; /* Unidentified item: double tap = free identify (checked first on every page; goods on the vendor shelf are not our items, and the host does not publish them either) */ - else if (s->pointer_stack) ++s->stack_take_seq; /* Stacked cell: take 1 (short press Y in the original game; no key is pressed here, since holding for 0.6 seconds would become "move 3") */ - else if (atomic_load(&g_imbue_open)) pointer_pulse_btn(s, ox, oy, 2, DM_BTN_Y, 1.3); /* Single-item quest slot panel: in the original game holding Y on an inventory item = put it into the slot (host reading: about 1.3 seconds); it must not be treated as "use" -- otherwise double-tapping the staff in front of the tomb pedestal would equip it in hand */ - else if (atomic_load(&g_bank_open) && !atomic_load(&g_tap_box) && !atomic_load(&g_vault_open)) { - /*Item in the inventory: first send the original game's X (move to the stacking tab). Stackable ones (runes / gems / materials) are accepted by the game and go into a stack cell; for non-stackable ones the game does nothing on X, and if it is still under the cursor after 0.4 seconds, fall back to hold Y. - Do not hold Y directly: that means "to the stash's current page", and on the rune / gem page with a cube in the inventory it would go into the cube cell embedded in the page. Items on the stash side do not go through here; they are still retrieved by holding Y */ - if (atomic_load(&g_cur_stack)) { pointer_pulse_btn(s, ox, oy, 2, DM_BTN_X, 0.15); s->stash_x_check_s = now_s + 0.4; s->stash_x_x = ox; s->stash_x_y = oy; s->stash_x_id = atomic_load(&g_cur_stack); } - else pointer_pulse_btn(s, ox, oy, 2, DM_BTN_Y, DM_TC_HOLD_USE_S); } - else if (atomic_load(&g_cube_open) && !atomic_load(&g_bank_open)) pointer_pulse_btn(s, ox, oy, 2, DM_BTN_Y, DM_TC_HOLD_USE_S); /* Cube page (not the cell embedded in the stash): in the original game holding Y sends the item into / out of the cube in one step; same rule as double tap on the stash page */ - else ++s->use_seq; /* Stash open: hold Y = move to the other side (except the cube: in the original game holding A with the stash open still opens the cube, per the code); otherwise hand it to the host to use on the spot (it used to hold A for 0.6 seconds and took effect only after about 0.4 seconds) */ - s->last_tap_s = -1e9; } - else { /* Single tap: move the cursor. Tapping the same spot again after a while (not a double tap) = compare with the other equipped item (the original game cycles with ZL; tooltip text "Tap again to compare with the other one") */ - int again = now_s - s->last_tap_s < 8.0 && hypotf(ox - s->last_tap_x, oy - s->last_tap_y) <= 2 * DM_TC_POINTER_MOVE_PT * scale_for(&s->layout); - if (again && !atomic_load(&g_bank_open) && !atomic_load(&g_vendor_open)) pointer_pulse_btn(s, ox, oy, 2, DM_BTN_ZL, 0); /* With the stash / vendor page open ZL switches tabs, so do not send it (a second tap on goods flipped the shelf to the next page) */ - else if (!s->pulse_button) { s->pointer_x = ox; s->pointer_y = oy; } - ++s->item_tap_seq; s->last_tap_s = now_s; s->last_tap_x = ox; s->last_tap_y = oy; } - } else if (s->tap_button) { - s->pointer_valid = 1; pointer_pulse_btn(s, s->tap_x, s->tap_y, 2, s->tap_button, 0); /* Front-end rewrite: cursor to the point + key press (tapping a skill in skill binding) */ - } else if (s->tap_move) { - s->pointer_valid = 1; s->pointer_x = s->tap_x; s->pointer_y = s->tap_y; ++s->tap_move_seq; - } else if (s->mouse_click_mode) { - s->mouse_click_x = done.aim_origin_x; s->mouse_click_y = done.aim_origin_y; ++s->mouse_click_seq; - } else pointer_pulse_start(s, done.aim_origin_x, done.aim_origin_y, 2); - } - s->pointer_item = 0; s->pointer_release_item = 1; s->pointer_shop = 0; s->pointer_stack = 0; s->pointer_lock = 0; - s->pointer_slot = DM_TC_NONE; - s->finger[slot] = (dm_tc_finger){.target=DM_TC_NONE}; - update(s); - return; - } - /*Skill button: hold = continuous cast, drag sets the direction, release lifts the key; no longer "hold to aim, release to cast once". - The only thing to make up: the tap was too short and the finger lifted before the key could go down (shorter than the tap / drag decision time, or in the second set ZL has not led long enough) -- make it up as one pulse with direction; - in the second set, when ZL has not led long enough, this pulse is delayed until it has (dm_tc_tick). */ - if (done.active && done.target >= DM_TC_SKILL1 && done.target <= DM_TC_SKILL4 && !done.aim_cancelled && !s->navigation_mode - && (!done.key_down || now_s - done.key_down_s < DM_TC_KEY_MIN_S || (s->skill_set && now_s - done.start_time_s < DM_TC_ZL_LEAD_S))) { /* The key was never pressed, or was pressed for less than 2 frames (the game may miss it): make it up as a pulse */ - float ux = 0, uy = 0; int has = aim_dir(s, &done, &ux, &uy); - s->pulse_rx = has ? (int32_t)lroundf(ux*DM_STICK_MAX) : 0; s->pulse_ry = has ? (int32_t)lroundf(-uy*DM_STICK_MAX) : 0; - if (s->skill_set && now_s - done.start_time_s < DM_TC_ZL_LEAD_S) { s->defer_button = s->layout.target[done.target].button; s->defer_at_s = done.start_time_s + DM_TC_ZL_LEAD_S; } - else { s->pulse_button = s->layout.target[done.target].button; ++s->pulse_epoch; } - } - if (done.active && !s->navigation_mode && done.target >= DM_TC_BELT1 && done.target <= DM_TC_BELT4) { - /* decided when a belt cell is released: with multiple rows, moved up past the threshold = expand, down past the threshold = collapse; barely moved = a tap, drink right then; slid away sideways = do nothing */ - float th = DM_TC_BELT_SWIPE_PT * scale_for(&s->layout), up = done.aim_origin_y - done.y, dx = done.x - done.aim_origin_x; - if (s->belt_rows > 1 && up > th) s->belt_request = 1; - else if (s->belt_rows > 1 && up < -th) s->belt_request = 2; - else if (fabsf(dx) <= th && fabsf(up) <= th) belt_fire(s, 0x10 | (done.target - DM_TC_BELT1)); - } - if (done.active && !s->navigation_mode && done.target == DM_TC_NONE && now_s - done.start_time_s < DM_TC_POINTER_TAP_S && /* the tap that lands on the joystick base is the joystick (direction as soon as pressed) and no longer doubles as a ground tap */ - hypotf(done.x - done.aim_origin_x, done.y - done.aim_origin_y) <= DM_TC_POINTER_MOVE_PT * scale_for(&s->layout)) { - s->world_tap_x = done.aim_origin_x; s->world_tap_y = done.aim_origin_y; ++s->world_tap_seq; /* Tap the game world */ - } - s->finger[slot] = (dm_tc_finger){.target=DM_TC_NONE}; - if (slot == s->joy_slot) { - s->joy_slot = DM_TC_NONE; - s->joy_origin_x = s->joy_origin_y = 0; - } - update(s); -} - -void dm_tc_begin(dm_tc_state *s, int slot, float x, float y) { dm_tc_begin_at(s, slot, x, y, s->last_time_s); } -void dm_tc_move(dm_tc_state *s, int slot, float x, float y) { dm_tc_move_at(s, slot, x, y, s->last_time_s); } -void dm_tc_end(dm_tc_state *s, int slot) { dm_tc_end_at(s, slot, s->last_time_s); } - -void dm_tc_pointer_press(dm_tc_state *s, float x, float y, uint64_t button) { - if (s->pointer_slot >= 0 && s->pointer_slot < DM_TC_MAX_TOUCH) s->finger[s->pointer_slot].pointer_long = 1; - s->pointer_valid = 1; pointer_pulse_btn(s, x, y, 2, button, 0); update(s); -} -void dm_tc_tick(dm_tc_state *s, double now_s) { - s->last_time_s = now_s; - /* The make-up send must not overwrite a pulse being sent (belt potion, etc.): wait while one is being sent; when that pulse is released (dm_tc_pulse_end), push the make-up time to after ZL has again led for a full DM_TC_ZL_LEAD_S (it used to overwrite directly when due, and the belt press was lost) */ - if (s->belt_gap_cell && now_s >= s->belt_gap_at && !s->pulse_button) { uint64_t btn = s->layout.target[DM_TC_BELT1 + (s->belt_gap_cell & 0xf)].button; s->belt_gap_cell = 0; - if (btn && !s->navigation_mode) { s->pulse_rx = s->pulse_ry = 0; s->pulse_button = btn; ++s->pulse_epoch; } update(s); } - if (s->belt_q && !s->pulse_button && !s->defer_button && !s->belt_gap_cell) { int q = s->belt_q; s->belt_q = 0; if (!s->navigation_mode) belt_fire(s, q); } - if (s->defer_button && !s->pulse_button && now_s >= s->defer_at_s) { s->pulse_button = s->defer_button; s->defer_button = 0; ++s->pulse_epoch; update(s); } - else if (!s->navigation_mode) { /* A finger is on the main button / a skill button: the key values must be refreshed on time at two moments, the decision time and the ZL lead */ - for (int i = 0; i < DM_TC_MAX_TOUCH; ++i) if (s->finger[i].active && (s->finger[i].target >= DM_TC_MAIN && s->finger[i].target <= DM_TC_SKILL4)) { update(s); break; } - } - if (s->stash_x_check_s > 0 && now_s >= s->stash_x_check_s) { /* The item that X was sent for is still under the cursor (not stackable / this save has no stacking tab / that stack is full): fall back to hold Y, moving it to the stash's current page. The check is on the same item id; if X has already moved it away, do nothing */ - s->stash_x_check_s = 0; - if (atomic_load(&g_bank_open) && atomic_load(&g_cur_stack) == s->stash_x_id && !s->holding && !s->pulse_button) { pointer_pulse_btn(s, s->stash_x_x, s->stash_x_y, 2, DM_BTN_Y, DM_TC_HOLD_USE_S); update(s); } - } - if (s->close_left > 0 && atomic_exchange(&g_close_stop, 0)) s->close_left = 0; /* The back button brought up a confirmation box (the settings page has unapplied changes): stop and let the player answer -- one more press would choose "Discard" on their behalf (changes lost) */ - if (s->close_left > 0) { /* Tap outside the panel = close all the way: if the panel is still open and the previous B has been released, press once more after 0.3 seconds */ - if (!s->navigation_mode) s->close_left = 0; - else if (!s->pulse_button && now_s >= s->close_next_s) { s->pulse_button = DM_BTN_B; s->pulse_hold_s = 0; s->pulse_rx = s->pulse_ry = 0; s->pointer_pulse_active = 2; ++s->pulse_epoch; --s->close_left; s->close_next_s = now_s + 0.3; update(s); } - } - if (s->pointer_slot < 0 || s->pointer_slot >= DM_TC_MAX_TOUCH) return; - dm_tc_finger *f = &s->finger[s->pointer_slot]; - if (!f->active || f->pointer_dragged || f->pointer_long) return; - if (s->mouse_click_mode && s->pointer_item != 1) return; - if (now_s - f->start_time_s >= DM_TC_POINTER_HOLD_S && - hypotf(f->x-f->aim_origin_x, f->y-f->aim_origin_y) <= DM_TC_POINTER_MOVE_PT * scale_for(&s->layout)) { - f->pointer_long = 1; - s->pointer_x = f->aim_origin_x; - s->pointer_y = f->aim_origin_y; - update(s); - } -} - -int dm_tc_navigation(dm_tc_scene *s, const char *cat, const char *act, const char *arg) { - static const char *names[]={"PlayerInventoryExpansionLayout","SkillsTreePanel","PauseLayout","PauseLayoutGarden","CharacterStatsPanel","QuestLogPanelExpansion","BankExpansionLayout","HoradricCubeLayout","WaypointsPanelExpansion","NpcDialogPanel","NpcTradePanel","ButtonBindingModal","SkillSelect","YouDiedModal"}; - if(!strcmp(cat,"FrontEndNavigation")&&!strcmp(act,"ToFrontEnd")) { s->game=0;s->panels=0; } - if(!strcmp(cat,"PanelManager")) { - if(!strcmp(arg,"LoadScreenPanel")&&!strcmp(act,"OpenPanel"))s->game=0; - if(!strcmp(arg,"LoadScreenPanel")&&!strcmp(act,"UnloadPanel"))s->game=1; - if(!strcmp(act,"UnloadAll"))s->panels=0; - { size_t al=strlen(arg); if((!strcmp(act,"OpenPanel")||!strcmp(act,"OpenExclusivePanel"))&&al>5&&!strcmp(arg+al-5,"Modal"))atomic_store(&g_close_stop,1); } - for(unsigned i=0;i<sizeof(names)/sizeof(*names);i++)if(!strcmp(arg,names[i])) { - if(!strcmp(act,"OpenPanel")||!strcmp(act,"OpenExclusivePanel"))s->panels|=1ull<<i; - if(!strcmp(act,"ClosePanel")||!strcmp(act,"UnloadPanel"))s->panels&=~(1ull<<i); - } - } - return !s->game||s->panels!=0; -} -/* Death popup open: also counts as navigation state (touch points go through UI clicks, so "Continue" can be tapped), but the five controller round buttons are not offered -- after death a tap at a skill button position triggers resurrection, which is easy to hit by mistake */ -int dm_tc_scene_dead(void) { return atomic_load(&g_dead_modal); } -int dm_tc_scene_dialog_only(const dm_tc_scene *s) { return s->game && s->panels == (1ull << 9); } /* Entry 9 of the panel table = NpcDialogPanel (see names in dm_tc_navigation) */ - diff --git a/dm/ios/Sources/dm_touch_controls.h b/dm/ios/Sources/dm_touch_controls.h deleted file mode 100644 index 570ac7f..0000000 --- a/dm/ios/Sources/dm_touch_controls.h +++ /dev/null @@ -1,182 +0,0 @@ -#pragma once -#include <stdint.h> - -#ifdef __cplusplus -extern "C" { -#endif - -enum { DM_TC_MAX_TOUCH = 16, DM_TC_COUNT = 12 }; -enum { - DM_TC_NONE = -1, DM_TC_JOYSTICK = -2, DM_TC_SWAP = -3, DM_TC_DIFF = -4, /* DIFF: a finger captured by the difficulty cell / selector bar; sends no key */ - DM_TC_EDGE = -5, /* A finger that started in the strip along the bottom edge of the screen that belongs to the system: not treated as the stick, and its release is not treated as a ground tap */ - DM_TC_MAIN = 0, - DM_TC_SKILL1, DM_TC_SKILL2, DM_TC_SKILL3, DM_TC_SKILL4, - DM_TC_PORTAL, DM_TC_BELT1, DM_TC_BELT2, DM_TC_BELT3, DM_TC_BELT4, - DM_TC_MENU, DM_TC_MAP -}; -typedef struct { float x, y, r; uint64_t button; } dm_tc_target; -typedef struct { - float width, height; - dm_tc_target target[DM_TC_COUNT]; - float joy_x, joy_y, joy_radius; - dm_tc_target swap; /* Skill set switch key: a local toggle; does not send a key directly */ - dm_tc_target diff; /* Bottom UI v4 cell 0 difficulty: tap to open / collapse the 1–8 selector bar; sends no key */ - dm_tc_target pick[8]; /* The selector bar's 8 cells (cell i = player-count level i+1), above the seven cells */ -} dm_tc_layout; -typedef struct { - int active, target; - float x, y; - float aim_origin_x, aim_origin_y; - uint64_t aim_move_sequence; - int aim_cancelled; - int key_down; double key_down_s; /* Skill button: whether the key has gone down and when (after press, first wait the decision time, see update) */ - double start_time_s; - int pointer_dragged, pointer_long; - /*Continuous UI gesture (mouse click mode, started neither in the item area nor outside the panel; when displacement crosses the threshold the type is fixed by the initial direction and does not change afterwards): 1 = vertical drag (scroll), 2 = horizontal drag (slider). - The gesture layer only fixes the type; on seeing it the front end forwards the start point and current point to the host (dm_ui_drag_*), and the scroll / slider is done by the host according to the game's own hit testing */ - int ui_drag; - int shop_drag; /* Drag starting from goods on the vendor shelf (not picked up): on release, buy / do not buy by the drop point */ - int locked_drag; /* Drag starting from another item while the single-item quest slot is occupied: do not pick up, and send no key on release */ -} dm_tc_finger; -typedef struct { - dm_tc_layout layout; - dm_tc_finger finger[DM_TC_MAX_TOUCH]; - int joy_slot; - float joy_origin_x, joy_origin_y; - uint32_t enabled_targets; - uint64_t buttons; - int32_t lx, ly, rx, ry; - int aim_slot, navigation_mode, map_cancel_active; - uint64_t aim_move_sequence; - int portal_pending, portal_phase; // 0idle / 1modifier-pending / 2armed - uint64_t gesture_epoch, portal_binding_identity, portal_scene_epoch; - /* Short-press pulse for release-to-cast: the key and the right-axis direction are published together, and the front end calls dm_tc_pulse_end to release it when due */ - uint64_t pulse_button, pulse_epoch; - int32_t pulse_rx, pulse_ry; - uint64_t defer_button; double defer_at_s; /* The too-short tap in the second set: make it up once ZL has led long enough */ - /* Skill set: 0 = first set; 1 = second set (on the original Switch game, holding ZL uses the alt slots). It is a mode and is not cleared on cancel/size change */ - int skill_set; - /*Second skill set (ZL modifier): ZL is held only while a finger is on the main button / a skill button or a skill pulse is being sent, and goes down DM_TC_ZL_LEAD_S before the skill button. - When a release-to-cast press is too short, that pulse is delayed until ZL has led long enough (dm_tc_tick starts it when due). */ - int pointer_slot; - double last_time_s; - int pointer_pickup_done; - int pointer_pulse_active, pointer_pulse_pending; - float pointer_pending_x, pointer_pending_y; - int pointer_valid; - float pointer_x, pointer_y; - /*Item gestures: pointer_item = the current pointer finger landed in the item area (set by the front end before release from the host's hit result). A single tap in the item area only moves the cursor (shows the tooltip), - double tap = hold A (game hint: hold = open / use / equip; with the stash open, hold Y = move to the other side); a single tap outside the item area is as before = move the cursor + press A. - pulse_hold_s > 0 = this pulse must be held this long, and the front end waits it out in full in addition to "the game has consumed it" before releasing */ - int pointer_item; - /*Taps outside the item area go through mouse clicks (pause menu buttons and the close / repair buttons inserted back into the layout only respond to the mouse): mouse_click_mode is turned on by the front end; - each such tap increments mouse_click_seq, and on seeing a change the front end sends one UI mouse click at (mouse_click_x, mouse_click_y) and no longer presses A */ - /*Whether the drag-and-drop release spot (the drop point where the item is lifted above the finger) is the item area: set by the front end before release. If not = dragged outside the panel = discard (pointer returns to the original spot and presses Y once in place). - holding = the cursor is still holding an item (the last drop did not succeed): then tapping the item area = put down (A), tapping elsewhere does nothing */ - int pointer_release_item, holding; float release_x, release_y; - /*A tap in game state (no panel open) = tap the game world: the finger did not land on any widget target (including landing in the stick area without dragging), less than 0.35 seconds from press to lift, displacement within the tap threshold - → world_tap_seq is incremented, and on seeing a change the front end calls the host's dm_world_tap (tap an item name to pick up / tap an NPC to talk / tap a chest; tapping empty ground sends nothing). */ - unsigned world_tap_seq; float world_tap_x, world_tap_y; - int close_left; double close_next_s; /* After a tap outside the panel, how many more B presses remain and the earliest time of the next one (press until the panel is closed) */ - /*Vendor page: this press landed on goods on the shelf (set by the front end on host hit result 7). A drag starting from goods = intent to buy: do not send "pick up", and the cursor stays on that item (the host buys the item the cursor has selected); - on release shop_drop_seq is incremented and the front end decides by the drop point -- landing in an inventory-type item area = buy; dragged back to the shelf / outside the panel / elsewhere = do not buy */ - int pointer_shop; unsigned shop_drop_seq; - int pointer_lock; /* Pressing on another item while the single-item quest slot holds something (host hit result 10): move the cursor and show the tooltip as in the item area, but do not pick it up; double tap / long press / single tap do not move it, and the item in the slot is not placed here either */ - /* Stacked cell of the advanced stash (set by the front end on host hit result 8): everything else follows the item area, only double tap differs -- stack_take_seq is incremented and the front end calls the host's dm_stash_take(1) to take 1 into the inventory */ - int pointer_stack; unsigned stack_take_seq; - unsigned ident_seq; /* Number of double taps on an unidentified item in the item area (only when unidentified does double tap = free identify, and this rule is checked first on every page): on seeing a change the front end calls the host's dm_ui_identify_hovered */ - unsigned use_seq; /* Number of double taps in the item area (when no stash is open): on seeing a change the front end calls the host's dm_ui_use_item -- performs the native use / buy / equip on the item under the cursor without waiting for the "hold A" threshold */ - unsigned item_tap_seq; /* Number of single taps in the item area (on seeing a change the front end calls the host's dm_ui_item_tap: in identify state, single-tapping an item = identify it) */ - unsigned long_press_seq; /* Number of times the item area was held still for the full long-press duration (on seeing a change the front end calls the host's dm_ui_long_press: inventory potion → belt) */ - unsigned tab_tap_seq; /* Number of taps that landed on a tab (on seeing a change the front end calls the host's dm_ui_tab_go) */ - int mouse_click_mode; unsigned mouse_click_seq; float mouse_click_x, mouse_click_y; - /* Temporary front-end rewrite of "taps inside a panel outside the item area": non-zero = send no mouse click; instead the controller cursor walks to the touch point and presses this key on arrival (tapping a skill in the skill binding flow = cursor onto it and press Y). Tabs / outside the panel behave as before */ - uint64_t tap_button; float tap_x, tap_y; int tap_move; unsigned tap_move_seq; /* tap_move: this tap only moves the cursor to tap_x / tap_y and sends no click (skill icon / attribute "+": select first, then confirm) */ - double pulse_hold_s; - double last_tap_s; float last_tap_x, last_tap_y; - double stash_x_check_s; float stash_x_x, stash_x_y; unsigned stash_x_id; /* With the stash open, double-tapping an inventory item sent X first: if at this time the item is still under the cursor (the game did not accept that X), fall back to hold Y */ - uint64_t pointer_pending_button; double pointer_pending_hold; - /* Difficulty: diff_open = selector bar expanded; diff_request = the player-count level just selected (1–8), cleared after the front end hands it to the host's dm_hud_request_players */ - int diff_open, diff_request; - /*Belt expansion (the rows of the original game's keyboard-and-mouse "show belt"; the controller version has no entry for it): when the worn belt has more than 1 row, swiping up on a belt cell = expand, swiping down / tapping elsewhere = collapse, - and after expansion tapping a potion in the upper rows = drink. belt_rows / belt_open are written by the front end from host readings before each touch; belt_request is cleared after the front end takes it: - 1 expand, 2 collapse, 0x100 | row << 4 | column = drink that cell (row 1–3 = which row above the bottom row) */ - int belt_rows, belt_open, belt_request; - /* one tap on the belt = the player drinks, at the moment of release, several taps drink several; no double-tap detection, no mercenary feeding gesture on the belt */ - int belt_gap_cell; double belt_gap_at; /* to drink a potion while a skill of the second skill set is held: first release ZL and the skill key and leave a gap of DM_TC_ZL_LEAD_S, and only at belt_gap_at press the belt key (belt_gap_cell = the cell that is waiting, 0 = none) */ - double joy_lost_s; float joy_lost_x, joy_lost_y; /* time and position at which the joystick finger was cancelled by the system (0 = none): a finger landing again nearby right afterwards continues as the joystick */ - int joy_block; /* speech text box open on its own (the front end does not enter panel state): a finger on the base starts no stick and is still just one click (closes the speech box). Written by the front end from the panel stack */ - double belt_pulse_s; /* Time until which the most recent "player drinks" belt pulse is still being sent: during it the held second-set skill gives way together with ZL, and after the release ZL again leads by DM_TC_ZL_LEAD_S before the skill key comes back */ - int belt_q; /* another pulse is being sent / another resend is pending: queue and fire when free (they never overwrite each other). Value = cell */ -} dm_tc_state; - -/* Driven by the original game's UI messages; panels are not guessed from input keys. AutoMap is a combat overlay and does not switch to navigation. */ -typedef struct { int game; uint64_t panels; } dm_tc_scene; -int dm_tc_navigation(dm_tc_scene *scene, const char *cat, const char *act, const char *arg); -int dm_tc_scene_dialog_only(const dm_tc_scene *scene); /* Only the NPC dialog menu is open */ -int dm_tc_scene_dead(void); /* Death popup open (navigation state, but no controller round buttons) */ - -/* Coordinates use UIKit points; the layout itself does not draw the orbs and does not claim the game has bound town portal/belt. */ -/* Left/right safe-area insets (points); set before init / resize, and the layout afterwards uses them to move the right-side skill key group and the left stick into the safe area. Default 0. */ -void dm_tc_set_safe_insets(float left, float right); -void dm_tc_set_edge_bottom(float bottom); /* Height (points) of the strip along the bottom edge of the screen that belongs to the system; 0 = none. Constrains only where the movement stick may start */ -void dm_tc_layout_make(dm_tc_layout *layout, float width, float height); -int dm_tc_hit(const dm_tc_layout *layout, float x, float y); -#define DM_TC_POINTER_LIFT_PT 40.0f // Number of points the item is lifted above the finger while dragging on function pages (× widget scale) -void dm_tc_set_tall_scale(int on); // Switch for scaling up by height/440 when picture height > 440 points; default off (the Mac host touch test cases pass pixel counts and must not be scaled up); the iOS front end turns it on when creating the view -float dm_tc_scale_for_size(float width, float height); // Widget scale: phone min(1, width/667, height/375); when picture height > 440 points (iPad), scale up proportionally by height/440 -void dm_tc_init(dm_tc_state *state, float width, float height); -/* A size change cancels all touch points, and the caller then pushes the zeroed state. */ -void dm_tc_resize(dm_tc_state *state, float width, float height); -/* Widget switches do not change the layout; existing touch points are cancelled when the valid bits are toggled. */ -void dm_tc_enable_targets(dm_tc_state *state, uint32_t mask); -/*Widgets available in navigation mode (a panel / dialog / front-end page is open): the five virtual controller round buttons on the right (confirm / back / action / bind / next page = A B X Y R). - The "close" at the bottom bar's cross position is no longer offered: only one close entry for panels remains (the close button at the top-right corner of the frame; while the round buttons are present there is also "back"). - pad = 0: the five round buttons are not offered either (used once in-run fingers tapping the UI directly is hooked up; switch DM_TOUCH_NAV_PAD=0, default 1). Both front ends share this one place and do not each write their own. */ -static inline uint32_t dm_tc_nav_mask(int pad) { return pad ? ((1u<<DM_TC_MAIN)|(1u<<DM_TC_SKILL1)|(1u<<DM_TC_SKILL2)|(1u<<DM_TC_SKILL3)|(1u<<DM_TC_SKILL4)) : 0; } -/* A mode change releases all gestures; navigation mode only presses keys and does not enable skill dragging or the map cancel area. */ -void dm_tc_set_navigation_mode(dm_tc_state *state, int navigation_mode); -void dm_tc_set_joy_block(dm_tc_state *state, int block); /* speech text box / dialogue menu open on its own: a finger on the base starts no stick and is just one click */ -/* Cancel only the core touch point owned by the town-portal widget; the physical touch point is kept by the front end. */ -void dm_tc_set_portal_button(dm_tc_state *state, uint64_t mask); -void dm_tc_set_portal_binding(dm_tc_state *state, uint64_t mask, uint64_t binding_identity, uint64_t scene_epoch); -/* Feedback confirmation applies only to a pending gesture that is still held and whose identity/scene/request match; returns whether it was armed for the first time. */ -int dm_tc_portal_ack(dm_tc_state *state, uint64_t gesture_epoch, uint64_t binding_identity, uint64_t scene_epoch); -void dm_tc_cancel_portal(dm_tc_state *state); -void dm_tc_begin(dm_tc_state *state, int slot, float x, float y); -void dm_tc_move(dm_tc_state *state, int slot, float x, float y); -void dm_tc_end(dm_tc_state *state, int slot); -void dm_tc_begin_at(dm_tc_state *state, int slot, float x, float y, double now_s); -void dm_tc_move_at(dm_tc_state *state, int slot, float x, float y, double now_s); -void dm_tc_end_at(dm_tc_state *state, int slot, double now_s); -void dm_tc_tick(dm_tc_state *state, double now_s); -void dm_tc_cancel_all(dm_tc_state *state); -void dm_tc_cancel_finger(dm_tc_state *state, int slot); /* The system cancels this finger: do not treat it as a release, only let go of what it was holding */ -/* End the cast pulse corresponding to epoch; returns whether anything changed (the caller then pushes) */ -int dm_tc_pulse_end(dm_tc_state *state, uint64_t pulse_epoch); -// After release there is still a queued pointer pulse (the "put-down" of a drag-and-drop arrived before the "pick-up" was released): the front end waits until the game has seen a release once, then calls resume to start it -int dm_tc_pulse_pending(const dm_tc_state *state); -void dm_tc_pointer_press(dm_tc_state *state, float x, float y, uint64_t button); /* Cursor to (x, y), then press button once; the current pointer finger is marked "long press handled" and its release no longer counts as a tap */ -void dm_tc_pointer_item(dm_tc_state *state, int kind); /* Called as soon as the pointer finger's hit result is available (before release at the latest): 1 = item area, 2 = tab, 3 = outside the panel (tap = close the panel), anything else = elsewhere inside the panel (tap = mouse click) */ -void dm_tc_pointer_release(dm_tc_state *state, int kind); /* Called before a drag-and-drop release: the host's drop decision (1 place at the lifted point / 4 place at the finger point / 3 discard / 0 put back where it was) */ -void dm_tc_pointer_release_at(dm_tc_state *state, float x, float y); /* Called before a drag-and-drop release: place at this position (computed by the host; equipment slot = slot center) */ -void dm_tc_set_holding(dm_tc_state *state, int holding); -void dm_tc_set_cur_stack(unsigned id); /* id of the inventory item the cursor is pointing at right now while the stash is open (pointing at the stash side / pointing at nothing = 0) */ -void dm_tc_set_hover_unid(unsigned id); /* id of our unidentified item the cursor is pointing at right now (host dm_ui_hover_unid): an item-area double tap checks it first -- non-zero = identify (ident_seq incremented), skipping each page's original double tap */ -void dm_tc_set_tap_box(int on); /* Whether the most recently single-tapped item is the cube (with the stash open, double-tapping the cube = open) */ -void dm_tc_set_panel_state(int bits); /* State bits the host obtains by reading the game panel tree (1 stash open / 2 vendor page / 4 death popup); the front end calls this when the UI message DM:PanelState arrives */ -void dm_tc_set_iso_fix(int on); /* Switch for the movement stick's diagonal pre-correction (default on; used to measure before and after the change) */ -int dm_tc_belt_outside(const dm_tc_state *state, float x, float y); -#define DM_TC_AIM_SETTLE_S 0.08 /* After a skill button is pressed, wait this long to decide tap versus drag (dragging past the dead zone puts the key down early) */ -#define DM_TC_BELT_SWIPE_PT 24 /* On release over a belt cell, vertical displacement beyond this many points = expand / collapse; less = tap (drink) */ -#define DM_TC_KEY_MIN_S 0.035 /* A skill button is held at least this long (about 2 frames); a shorter one is made up as a pulse on release to guarantee the game reads it */ -#define DM_TC_JOY_RESUME_S 0.35 /* after the joystick finger is cancelled by the system, a finger landing again near the same spot within this long continues as the joystick (landing near the base is not required) */ -#define DM_TC_JOY_GRAB_K 1.5f /* the joystick starts only when a finger lands within this many radii of the joystick base center (the base is fixed and does not follow the touch-down point) */ -#define DM_TC_ZL_LEAD_S 0.17 /* Time the modifier key leads the main key (about 10 frames; pressed in the same frame, the game only recognizes the main key) */ -#define DM_TC_DOUBLE_TAP_S 0.40 /* Upper limit of the interval between two single taps */ -#define DM_TC_HOLD_USE_S 0.60 /* Hold duration of the "hold" action: measured on the Mac host, 0.25 s only picks up, 0.45 s opens the cube */ -int dm_tc_pulse_resume(dm_tc_state *state); - -#ifdef __cplusplus -} -#endif diff --git a/dm/ios/Sources/front_ios.m b/dm/ios/Sources/front_ios.m index 047cb61..36533a1 100644 --- a/dm/ios/Sources/front_ios.m +++ b/dm/ios/Sources/front_ios.m @@ -93,7 +93,6 @@ static void front_ios_power_watch(void) { } void *front_metal_layer(void) { return (__bridge void *)g_layer; } -uint64_t front_game_res(int docked) { return docked ? 900ull << 32 | 1600 : 720ull << 32 | 1280; } // Same as Mac: the original 16:9 constants void front_run(void (*guest)(void)) { guest(); } // Already on main.m's big-stack thread // Software keyboard: show a UIAlertController on the main thread, the calling thread waits for the result @@ -122,7 +121,7 @@ int front_text_input(const char *title, const char *sub, const char *guide, cons return 1; } -// Save write failed (most likely the phone's storage is full): show a notice on the main thread without blocking the game (as d2r-mobile does). hle_fs.c calls this when a save transaction is refused or a mount has to fall back to read-only; +// Save write failed (most likely the phone's storage is full): show a notice on the main thread without blocking the game. hle_fs.c calls this when a save transaction is refused or a mount has to fall back to read-only; // the iOS front end used not to implement it, so there was only a log line and the player never learned that progress was not being saved void front_save_failed(int err) { LOG("front end: save data write failed errno=%d, telling the player", err); diff --git a/dm/ios/project.yml b/dm/ios/project.yml index 6ecf41e..9059b45 100644 --- a/dm/ios/project.yml +++ b/dm/ios/project.yml @@ -14,7 +14,7 @@ targets: sources: - Sources - path: Gen/embed.S - - path: Gen/dm # Rewriter tables (*.fixups.tsv / gates / params / cuts / obs) + shader map / c1 list / warm-up list + - path: Gen/dm # Rewriter tables (*.fixups.tsv / gates / cuts / obs) + shader map / c1 list / warm-up list type: folder buildPhase: resources - path: Gen/Assets.xcassets # app icon (generated by build_ipa.sh from the game icon; an empty catalog when there is none, since xcodegen lists it in the project whether or not it exists) diff --git a/dm/ios/tools/build_ipa.sh b/dm/ios/tools/build_ipa.sh index 054cc21..9d3d057 100755 --- a/dm/ios/tools/build_ipa.sh +++ b/dm/ios/tools/build_ipa.sh @@ -39,7 +39,7 @@ rm -rf Gen && mkdir -p Gen/dm || die "mkdir Gen" python3 tools/gen_build.py ../host Gen > Gen/dm.xcconfig || die "gen_build" F=$(python3 tools/gen_embed.py "$PATCHED" Gen/embed.S $MODS) || die "gen_embed" echo "OTHER_LDFLAGS = \$(inherited) \$(DM_FRAMEWORKS) $F" >> Gen/dm.xcconfig -cp "$PATCHED"/*.fixups.tsv "$PATCHED"/*.gates.tsv "$PATCHED"/*.params.tsv "$PATCHED"/*.cuts.tsv "$PATCHED"/*.obs.tsv Gen/dm/ || die "copy rewriter tables" +cp "$PATCHED"/*.fixups.tsv "$PATCHED"/*.gates.tsv "$PATCHED"/*.cuts.tsv "$PATCHED"/*.obs.tsv Gen/dm/ || die "copy rewriter tables" if [ "$GFXAPI" = vulkan ]; then cp "$GD/shader_map.tsv" "$GD/pso_manifest.tsv" Gen/dm/ || die "copy shader tables" # empty NVN tables: the NVN layer is never used else cp "$GD/shader_map.tsv" "$GD/pso_manifest.tsv" "$GD/c1_full.tsv" Gen/dm/ || die "copy shader tables"; fi # Diagnostic build: DM_ENV_FILE=file → dm/env.txt in the bundle (main.m sets one environment variable per line at boot) diff --git a/dm/third_party/README.md b/dm/third_party/README.md index 1ba38b6..4dcd2fe 100644 --- a/dm/third_party/README.md +++ b/dm/third_party/README.md @@ -5,7 +5,6 @@ This is code written by others, shipped with the repository so there is nothing | Directory | What it is | Version and changes | Where it is used | License file | |---|---|---|---|---| | `flip/` | NVIDIA FLIP, an image difference metric that models human viewing. `FLIP.h` plus the source of the command-line tool in `tool/` | Not recorded in the directory | Verification: the image quality measurements in `dm/verify/gfxq/`; build `flip` first with `make -C dm/third_party/flip` | `flip/LICENSE` (BSD 3-Clause) | -| `libdeflate/` | Decompression library | Upstream commit `92e6a0d`, unmodified | Host: zlib decoding of the game's resource blocks (`dm/host/hle_tact.c`). Only the five decompression-related source files are built; they are listed in `dm/host/Makefile` | `libdeflate/COPYING` (MIT) | | `libvpx/` | The VP9 decoder from libvpx: the 71 source files a VP9-decoder-only build compiles, their headers, and the generated configuration in `config/` | v1.17.0 (upstream commit `6df3ec3`), unmodified | Host: decodes the games' VP9 movies (`dm/host/hle_movie.c`). `dm/host/Makefile` builds it into `gen/vpx/libvpx.a`; the iOS build compiles the same files | `libvpx/LICENSE` (BSD 3-Clause), with the patent grant in `libvpx/PATENTS` | | `stb_vorbis/` | Ogg Vorbis decoder, a single file `stb_vorbis.c` | 1.22 (upstream commit `1ee679c`), unmodified | Host: decodes the Vorbis sound tracks of the games' WebM movies (`dm/host/hle_movie.c`, which includes the file with its push-data API only) | `stb_vorbis/LICENSE` (MIT or public domain, your choice; the same text ends `stb_vorbis.c`) | | `ryujinx/` | The shader translation library from Ryujinx, three projects: `Ryujinx.Graphics.Shader`, `Ryujinx.Common`, `Spv.Generator` | Ryujinx 1.3.2, with the local changes listed in `ryujinx/README.md` | Referenced by the shader translation tool `dm/gfx/tools/dmsh_xlate`, which translates the original Switch shaders to SPIR-V | `ryujinx/LICENSE.txt` (MIT); `ryujinx/src/Spv.Generator/LICENSE` is that subproject's own license text | @@ -14,5 +13,5 @@ This is code written by others, shipped with the repository so there is nothing A few more notes: - `flip/tool/` also carries three single-header libraries used by the FLIP command-line tool: `stb_image.h` (2.23), `stb_image_write.h` (1.13) and `tinyexr.h`. Their license notices are in the files themselves. -- `libdeflate/VERSION.md`, `libvpx/README.md`, `stb_vorbis/README.md`, `ryujinx/README.md` and `vulkan-headers/README.md` record where each came from and what it is used for. +- `libvpx/README.md`, `stb_vorbis/README.md`, `ryujinx/README.md` and `vulkan-headers/README.md` record where each came from and what it is used for. - Four pieces of third-party code live outside this directory: `dm/gfx/nvn/include/nvn.h` (MIT, author named in the file header), the layout size algorithm in `dm/gfx/nvn/nvn_tex.c`, ported from deko3d (zlib license), and two adapted from MacNativeGames (MIT, license text in `macnativegames/LICENSE`): in `dm/gfx/nvn/nvn_mtl.m` the GPU widening of 8-bit indices (`wide_*`) and the in-pass clear triangles (`clear_color_tri` / `clear_ds_tri`), from its `widen_u8_gpu` and `clear_quad` / `clear_ds_quad`, and the Tears of the Kingdom visibility-counter lowering in `dm/gfx/tools/dmsh_xlate/Program.cs` (`VertexCounterLowering`). diff --git a/dm/third_party/libdeflate/COPYING b/dm/third_party/libdeflate/COPYING deleted file mode 100644 index 9b620b3..0000000 --- a/dm/third_party/libdeflate/COPYING +++ /dev/null @@ -1,22 +0,0 @@ -Copyright 2016 Eric Biggers -Copyright 2024 Google LLC - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/dm/third_party/libdeflate/README.md b/dm/third_party/libdeflate/README.md deleted file mode 100644 index bf68fad..0000000 --- a/dm/third_party/libdeflate/README.md +++ /dev/null @@ -1,244 +0,0 @@ -# Overview - -libdeflate is a library for fast, whole-buffer DEFLATE-based compression and -decompression. - -The supported formats are: - -- DEFLATE (raw) -- zlib (a.k.a. DEFLATE with a zlib wrapper) -- gzip (a.k.a. DEFLATE with a gzip wrapper) - -libdeflate is heavily optimized. It is significantly faster than the zlib -library, both for compression and decompression, and especially on x86 and ARM -processors. In addition, libdeflate provides optional high compression modes -that provide a better compression ratio than the zlib's "level 9". - -libdeflate itself is a library. The following command-line programs which use -this library are also included: - -* `libdeflate-gzip`, a program which can be a drop-in replacement for standard - `gzip` under some circumstances. Note that `libdeflate-gzip` has some - limitations; it is provided for convenience and is **not** meant to be the - main use case of libdeflate. It needs a lot of memory to process large files, - and it omits support for some infrequently-used options of GNU gzip. - -* `benchmark`, a test program that does round-trip compression and decompression - of the provided data, and measures the compression and decompression speed. - It can use libdeflate, zlib, or a combination of the two. - -* `checksum`, a test program that checksums the provided data with Adler-32 or - CRC-32, and optionally measures the speed. It can use libdeflate or zlib. - -For the release notes, see the [NEWS file](NEWS.md). - -## Table of Contents - -- [Building](#building) - - [Using CMake](#using-cmake) - - [Directly integrating the library sources](#directly-integrating-the-library-sources) - - [Supported compilers](#supported-compilers) -- [API](#api) -- [Bindings for other programming languages](#bindings-for-other-programming-languages) -- [DEFLATE vs. zlib vs. gzip](#deflate-vs-zlib-vs-gzip) -- [Compression levels](#compression-levels) -- [Motivation](#motivation) -- [License](#license) - -# Building - -## Using CMake - -libdeflate uses [CMake](https://cmake.org/). It can be built just like any -other CMake project, e.g. with: - - cmake -B build && cmake --build build - -By default the following targets are built: - -- The static library (normally called `libdeflate.a`) -- The shared library (normally called `libdeflate.so`) -- The `libdeflate-gzip` program, including its alias `libdeflate-gunzip` - -Besides the standard CMake build and installation options, there are some -libdeflate-specific build options. See `CMakeLists.txt` for the list of these -options. To set an option, add `-DOPTION=VALUE` to the `cmake` command. - -Prebuilt Windows binaries can be downloaded from -https://github.com/ebiggers/libdeflate/releases. - -## Directly integrating the library sources - -Although the official build system is CMake, care has been taken to keep the -library source files compilable directly, without a prerequisite configuration -step. Therefore, it is also fine to just add the library source files directly -to your application, without using CMake. - -You should compile both `lib/*.c` and `lib/*/*.c`. You don't need to worry -about excluding irrelevant architecture-specific code, as this is already -handled in the source files themselves using `#ifdef`s. - -If you are doing a freestanding build with `-ffreestanding`, you must add -`-DFREESTANDING` as well (matching what the `CMakeLists.txt` does). - -## Supported compilers - -- gcc: v4.9 and later -- clang: v3.9 and later (upstream), Xcode 8 and later (Apple) -- MSVC: Visual Studio 2019 version 16.8 and later -- Other compilers: any other C11-compatible compiler should work, though if your - compiler pretends to be gcc, clang, or MSVC, it needs to be sufficiently - compatible with the compiler it pretends to be. - -The above are the minimums, but using a newer compiler allows more of the -architecture-optimized code to be built. libdeflate is most heavily optimized -for gcc and clang, but MSVC is supported fairly well now too. - -The recommended optimization flag is `-O2`, and the `CMakeLists.txt` sets this -for release builds. `-O3` is fine too, but often `-O2` actually gives better -results. It's unnecessary to add flags such as `-mavx2` or `/arch:AVX2`, though -you can do so if you want to. Most of the relevant optimized functions are -built regardless of such flags, and appropriate ones are selected at runtime. -For the same reason, flags like `-mno-avx2` do *not* cause all code using the -corresponding instruction set extension to be omitted from the binary; this is -working as intended due to the use of runtime CPU feature detection. - -If using gcc, your gcc should always be paired with a binutils version that is -not much older than itself, to avoid problems where the compiler generates -instructions the assembler cannot assemble. Usually systems have their gcc and -binutils paired properly, but rarely a mismatch can arise in cases such as the -user installing a newer gcc version without a proper binutils alongside it. -Since libdeflate v1.22, the CMake-based build system will detect incompatible -binutils versions and disable some optimized code accordingly. In older -versions of libdeflate, or if CMake is not being used, a too-old binutils can -cause build errors like "no such instruction" from the assembler. - -# API - -libdeflate has a simple API that is not zlib-compatible. You can create -compressors and decompressors and use them to compress or decompress buffers. -See libdeflate.h for details. - -There is currently no support for streaming. This has been considered, but it -always significantly increases complexity and slows down fast paths. -Unfortunately, at this point it remains a future TODO. So: if your application -compresses data in "chunks", say, less than 1 MB in size, then libdeflate is a -great choice for you; that's what it's designed to do. This is perfect for -certain use cases such as transparent filesystem compression. But if your -application compresses large files as a single compressed stream, similarly to -the `gzip` program, then libdeflate isn't for you. - -Note that with chunk-based compression, you generally should have the -uncompressed size of each chunk stored outside of the compressed data itself. -This enables you to allocate an output buffer of the correct size without -guessing. However, libdeflate's decompression routines do optionally provide -the actual number of output bytes in case you need it. - -Windows developers: note that the calling convention of libdeflate.dll is -"cdecl". (libdeflate v1.4 through v1.12 used "stdcall" instead.) - -# Bindings for other programming languages - -The libdeflate project itself only provides a C library. If you need to use -libdeflate from a programming language other than C or C++, consider using the -following bindings: - -* C#: [LibDeflate.NET](https://github.com/jzebedee/LibDeflate.NET) -* Delphi: [libdeflate-pas](https://github.com/zedxxx/libdeflate-pas) -* Go: [go-libdeflate](https://github.com/4kills/go-libdeflate) -* Java: [jlibdeflate](https://github.com/fulcrumgenomics/jlibdeflate) -* Julia: [LibDeflate.jl](https://github.com/jakobnissen/LibDeflate.jl) -* Nim: [libdeflate-nim](https://github.com/gemesa/libdeflate-nim) -* Perl: [Gzip::Libdeflate](https://github.com/benkasminbullock/gzip-libdeflate) -* PHP: [ext-libdeflate](https://github.com/pmmp/ext-libdeflate) -* Python: [deflate](https://github.com/dcwatson/deflate) -* Ruby: [libdeflate-ruby](https://github.com/kaorimatz/libdeflate-ruby) -* Rust: [libdeflater](https://github.com/adamkewley/libdeflater) -* Swift: [SwiftDeflate](https://github.com/dcwatson/SwiftDeflate) - -Note: these are third-party projects which haven't necessarily been vetted by -the authors of libdeflate. Please direct all questions, bugs, and improvements -for these bindings to their authors. - -Also, unfortunately many of these bindings bundle or pin an old version of -libdeflate. To avoid known issues in old versions and to improve performance, -before using any of these bindings please ensure that the bundled or pinned -version of libdeflate has been upgraded to the latest release. - -# DEFLATE vs. zlib vs. gzip - -The DEFLATE format ([rfc1951](https://www.ietf.org/rfc/rfc1951.txt)), the zlib -format ([rfc1950](https://www.ietf.org/rfc/rfc1950.txt)), and the gzip format -([rfc1952](https://www.ietf.org/rfc/rfc1952.txt)) are commonly confused with -each other as well as with the [zlib software library](http://zlib.net), which -actually supports all three formats. libdeflate (this library) also supports -all three formats. - -Briefly, DEFLATE is a raw compressed stream, whereas zlib and gzip are different -wrappers for this stream. Both zlib and gzip include checksums, but gzip can -include extra information such as the original filename. Generally, you should -choose a format as follows: - -- If you are compressing whole files with no subdivisions, similar to the `gzip` - program, you probably should use the gzip format. -- Otherwise, if you don't need the features of the gzip header and footer but do - still want a checksum for corruption detection, you probably should use the - zlib format. -- Otherwise, you probably should use raw DEFLATE. This is ideal if you don't - need checksums, e.g. because they're simply not needed for your use case or - because you already compute your own checksums that are stored separately from - the compressed stream. - -Note that gzip and zlib streams can be distinguished from each other based on -their starting bytes, but this is not necessarily true of raw DEFLATE streams. - -# Compression levels - -An often-underappreciated fact of compression formats such as DEFLATE is that -there are an enormous number of different ways that a given input could be -compressed. Different algorithms and different amounts of computation time will -result in different compression ratios, while remaining equally compatible with -the decompressor. - -For this reason, the commonly used zlib library provides nine compression -levels. Level 1 is the fastest but provides the worst compression; level 9 -provides the best compression but is the slowest. It defaults to level 6. -libdeflate uses this same design but is designed to improve on both zlib's -performance *and* compression ratio at every compression level. In addition, -libdeflate's levels go [up to 12](https://xkcd.com/670/) to make room for a -minimum-cost-path based algorithm (sometimes called "optimal parsing") that can -significantly improve on zlib's compression ratio. - -If you are using DEFLATE (or zlib, or gzip) in your application, you should test -different levels to see which works best for your application. - -# Motivation - -Despite DEFLATE's widespread use mainly through the zlib library, in the -compression community this format from the early 1990s is often considered -obsolete. And in a few significant ways, it is. - -So why implement DEFLATE at all, instead of focusing entirely on -bzip2/LZMA/xz/LZ4/LZX/ZSTD/Brotli/LZHAM/LZFSE/[insert cool new format here]? - -To do something better, you need to understand what came before. And it turns -out that most ideas from DEFLATE are still relevant. Many of the newer formats -share a similar structure as DEFLATE, with different tweaks. The effects of -trivial but very useful tweaks, such as increasing the sliding window size, are -often confused with the effects of nontrivial but less useful tweaks. And -actually, many of these formats are similar enough that common algorithms and -optimizations (e.g. those dealing with LZ77 matchfinding) can be reused. - -In addition, comparing compressors fairly is difficult because the performance -of a compressor depends heavily on optimizations which are not intrinsic to the -compression format itself. In this respect, the zlib library sometimes compares -poorly to certain newer code because zlib is not well optimized for modern -processors. libdeflate addresses this by providing an optimized DEFLATE -implementation which can be used for benchmarking purposes. And, of course, -real applications can use it as well. - -# License - -libdeflate is [MIT-licensed](COPYING). - -I am not aware of any patents or patent applications relevant to libdeflate. diff --git a/dm/third_party/libdeflate/VERSION.md b/dm/third_party/libdeflate/VERSION.md deleted file mode 100644 index 9899b7c..0000000 --- a/dm/third_party/libdeflate/VERSION.md +++ /dev/null @@ -1,6 +0,0 @@ -# libdeflate (third party, MIT license, see COPYING) -- Upstream: https://github.com/ebiggers/libdeflate commit 92e6a0d (2026-08-22) -- Use: zlib decoding of the BLTE blocks in TACT storage (host hle_tact.c, taken over at the main 0x1b0ee60 entry through a rewriter cut) -- Offline cross-check on the 10-01 platform line: all 4,979 files actually decompressed before character select have MD5 == the manifest CKey; 2.42× faster than the game's own zlib 1.2.11 on the same machine -- Only the decompression sources are compiled: lib/deflate_decompress.c, lib/zlib_decompress.c, lib/adler32.c, lib/utils.c, lib/arm/cpu_features.c -- Upstream code unmodified diff --git a/dm/third_party/libdeflate/common_defs.h b/dm/third_party/libdeflate/common_defs.h deleted file mode 100644 index 80307b9..0000000 --- a/dm/third_party/libdeflate/common_defs.h +++ /dev/null @@ -1,735 +0,0 @@ -/* - * common_defs.h - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef COMMON_DEFS_H -#define COMMON_DEFS_H - -#include "libdeflate.h" - -#include <stdbool.h> -#include <stddef.h> /* for size_t */ -#include <stdint.h> -#ifdef _MSC_VER -# include <intrin.h> /* for _BitScan*() and other intrinsics */ -# include <stdlib.h> /* for _byteswap_*() */ - /* Disable MSVC warnings that are expected. */ - /* /W2 */ -# pragma warning(disable : 4146) /* unary minus on unsigned type */ - /* /W3 */ -# pragma warning(disable : 4018) /* signed/unsigned mismatch */ -# pragma warning(disable : 4244) /* possible loss of data */ -# pragma warning(disable : 4267) /* possible loss of precision */ -# pragma warning(disable : 4310) /* cast truncates constant value */ - /* /W4 */ -# pragma warning(disable : 4100) /* unreferenced formal parameter */ -# pragma warning(disable : 4127) /* conditional expression is constant */ -# pragma warning(disable : 4189) /* local variable initialized but not referenced */ -# pragma warning(disable : 4232) /* nonstandard extension used */ -# pragma warning(disable : 4245) /* conversion from 'int' to 'unsigned int' */ -# pragma warning(disable : 4295) /* array too small to include terminating null */ -#endif -#ifndef FREESTANDING -# include <string.h> /* for memcpy() */ -#endif - -/* ========================================================================== */ -/* Target architecture */ -/* ========================================================================== */ - -/* If possible, define a compiler-independent ARCH_* macro. */ -#undef ARCH_X86_64 -#undef ARCH_X86_32 -#undef ARCH_ARM64 -#undef ARCH_ARM32 -#undef ARCH_RISCV -#ifdef _MSC_VER - /* Way too many things are broken in ARM64EC to pretend that it is x86_64. */ -# if defined(_M_X64) && !defined(_M_ARM64EC) -# define ARCH_X86_64 -# elif defined(_M_IX86) -# define ARCH_X86_32 -# elif defined(_M_ARM64) -# define ARCH_ARM64 -# elif defined(_M_ARM) -# define ARCH_ARM32 -# endif -#else -# if defined(__x86_64__) -# define ARCH_X86_64 -# elif defined(__i386__) -# define ARCH_X86_32 -# elif defined(__aarch64__) -# define ARCH_ARM64 -# elif defined(__arm__) -# define ARCH_ARM32 -# elif defined(__riscv) -# define ARCH_RISCV -# endif -#endif - -/* ========================================================================== */ -/* Type definitions */ -/* ========================================================================== */ - -/* Fixed-width integer types */ -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -typedef int8_t s8; -typedef int16_t s16; -typedef int32_t s32; -typedef int64_t s64; - -/* ssize_t, if not available in <sys/types.h> */ -#ifdef _MSC_VER -# ifdef _WIN64 - typedef long long ssize_t; -# else - typedef long ssize_t; -# endif -#endif - -/* - * Word type of the target architecture. Use 'size_t' instead of - * 'unsigned long' to account for platforms such as Windows that use 32-bit - * 'unsigned long' on 64-bit architectures. - */ -typedef size_t machine_word_t; - -/* Number of bytes in a word */ -#define WORDBYTES ((int)sizeof(machine_word_t)) - -/* Number of bits in a word */ -#define WORDBITS (8 * WORDBYTES) - -/* ========================================================================== */ -/* Optional compiler features */ -/* ========================================================================== */ - -/* Compiler version checks. Only use when absolutely necessary. */ -#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) -# define GCC_PREREQ(major, minor) \ - (__GNUC__ > (major) || \ - (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor))) -# if !GCC_PREREQ(4, 9) -# error "gcc versions older than 4.9 are no longer supported" -# endif -#else -# define GCC_PREREQ(major, minor) 0 -#endif -#ifdef __clang__ -# ifdef __apple_build_version__ -# define CLANG_PREREQ(major, minor, apple_version) \ - (__apple_build_version__ >= (apple_version)) -# else -# define CLANG_PREREQ(major, minor, apple_version) \ - (__clang_major__ > (major) || \ - (__clang_major__ == (major) && __clang_minor__ >= (minor))) -# endif -# if !CLANG_PREREQ(3, 9, 8000000) -# error "clang versions older than 3.9 are no longer supported" -# endif -#else -# define CLANG_PREREQ(major, minor, apple_version) 0 -#endif -#ifdef _MSC_VER -# define MSVC_PREREQ(version) (_MSC_VER >= (version)) -# if !MSVC_PREREQ(1928) -# error "MSVC versions older than Visual Studio 2019 v16.8 are no longer supported" -# endif -#else -# define MSVC_PREREQ(version) 0 -#endif - -/* - * __has_attribute(attribute) - check whether the compiler supports the given - * attribute (and also supports doing the check in the first place). Mostly - * useful just for clang, since gcc didn't add this macro until gcc 5. - */ -#ifndef __has_attribute -# define __has_attribute(attribute) 0 -#endif - -/* - * __has_builtin(builtin) - check whether the compiler supports the given - * builtin (and also supports doing the check in the first place). Mostly - * useful just for clang, since gcc didn't add this macro until gcc 10. - */ -#ifndef __has_builtin -# define __has_builtin(builtin) 0 -#endif - -/* inline - suggest that a function be inlined */ -#ifdef _MSC_VER -# define inline __inline -#endif /* else assume 'inline' is usable as-is */ - -/* forceinline - force a function to be inlined, if possible */ -#if defined(__GNUC__) || __has_attribute(always_inline) -# define forceinline inline __attribute__((always_inline)) -#elif defined(_MSC_VER) -# define forceinline __forceinline -#else -# define forceinline inline -#endif - -/* MAYBE_UNUSED - mark a function or variable as maybe unused */ -#if defined(__GNUC__) || __has_attribute(unused) -# define MAYBE_UNUSED __attribute__((unused)) -#else -# define MAYBE_UNUSED -#endif - -/* NORETURN - mark a function as never returning, e.g. due to calling abort() */ -#if defined(__GNUC__) || __has_attribute(noreturn) -# define NORETURN __attribute__((noreturn)) -#else -# define NORETURN -#endif - -/* likely(expr) - hint that an expression is usually true */ -#if defined(__GNUC__) || __has_builtin(__builtin_expect) -# define likely(expr) __builtin_expect(!!(expr), 1) -#else -# define likely(expr) (expr) -#endif - -/* unlikely(expr) - hint that an expression is usually false */ -#if defined(__GNUC__) || __has_builtin(__builtin_expect) -# define unlikely(expr) __builtin_expect(!!(expr), 0) -#else -# define unlikely(expr) (expr) -#endif - -/* prefetchr(addr) - prefetch into L1 cache for read */ -#undef prefetchr -#if defined(__GNUC__) || __has_builtin(__builtin_prefetch) -# define prefetchr(addr) __builtin_prefetch((addr), 0) -#elif defined(_MSC_VER) -# if defined(ARCH_X86_32) || defined(ARCH_X86_64) -# define prefetchr(addr) _mm_prefetch((addr), _MM_HINT_T0) -# elif defined(ARCH_ARM64) -# define prefetchr(addr) __prefetch2((addr), 0x00 /* prfop=PLDL1KEEP */) -# elif defined(ARCH_ARM32) -# define prefetchr(addr) __prefetch(addr) -# endif -#endif -#ifndef prefetchr -# define prefetchr(addr) -#endif - -/* prefetchw(addr) - prefetch into L1 cache for write */ -#undef prefetchw -#if defined(__GNUC__) || __has_builtin(__builtin_prefetch) -# define prefetchw(addr) __builtin_prefetch((addr), 1) -#elif defined(_MSC_VER) -# if defined(ARCH_X86_32) || defined(ARCH_X86_64) -# define prefetchw(addr) _m_prefetchw(addr) -# elif defined(ARCH_ARM64) -# define prefetchw(addr) __prefetch2((addr), 0x10 /* prfop=PSTL1KEEP */) -# elif defined(ARCH_ARM32) -# define prefetchw(addr) __prefetchw(addr) -# endif -#endif -#ifndef prefetchw -# define prefetchw(addr) -#endif - -/* - * _aligned_attribute(n) - declare that the annotated variable, or variables of - * the annotated type, must be aligned on n-byte boundaries. - */ -#undef _aligned_attribute -#if defined(__GNUC__) || __has_attribute(aligned) -# define _aligned_attribute(n) __attribute__((aligned(n))) -#elif defined(_MSC_VER) -# define _aligned_attribute(n) __declspec(align(n)) -#endif - -/* - * _target_attribute(attrs) - override the compilation target for a function. - * - * This accepts one or more comma-separated suffixes to the -m prefix jointly - * forming the name of a machine-dependent option. On gcc-like compilers, this - * enables codegen for the given targets, including arbitrary compiler-generated - * code as well as the corresponding intrinsics. On other compilers this macro - * expands to nothing, though MSVC allows intrinsics to be used anywhere anyway. - */ -#if defined(__GNUC__) || __has_attribute(target) -# define _target_attribute(attrs) __attribute__((target(attrs))) -#else -# define _target_attribute(attrs) -#endif - -/* ========================================================================== */ -/* Miscellaneous macros */ -/* ========================================================================== */ - -#define ARRAY_LEN(A) (sizeof(A) / sizeof((A)[0])) -#define MIN(a, b) ((a) <= (b) ? (a) : (b)) -#define MAX(a, b) ((a) >= (b) ? (a) : (b)) -#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) -#define STATIC_ASSERT(expr) ((void)sizeof(char[1 - 2 * !(expr)])) -#define ALIGN(n, a) (((n) + (a) - 1) & ~((a) - 1)) -#define ROUND_UP(n, d) ((d) * DIV_ROUND_UP((n), (d))) - -/* ========================================================================== */ -/* Endianness handling */ -/* ========================================================================== */ - -/* - * CPU_IS_LITTLE_ENDIAN() - 1 if the CPU is little endian, or 0 if it is big - * endian. When possible this is a compile-time macro that can be used in - * preprocessor conditionals. As a fallback, a generic method is used that - * can't be used in preprocessor conditionals but should still be optimized out. - */ -#if defined(__BYTE_ORDER__) /* gcc v4.6+ and clang */ -# define CPU_IS_LITTLE_ENDIAN() (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) -#elif defined(_MSC_VER) -# define CPU_IS_LITTLE_ENDIAN() true -#else -static forceinline bool CPU_IS_LITTLE_ENDIAN(void) -{ - union { - u32 w; - u8 b; - } u; - - u.w = 1; - return u.b; -} -#endif - -/* bswap16(v) - swap the bytes of a 16-bit integer */ -static forceinline u16 bswap16(u16 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_bswap16) - return __builtin_bswap16(v); -#elif defined(_MSC_VER) - return _byteswap_ushort(v); -#else - return (v << 8) | (v >> 8); -#endif -} - -/* bswap32(v) - swap the bytes of a 32-bit integer */ -static forceinline u32 bswap32(u32 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_bswap32) - return __builtin_bswap32(v); -#elif defined(_MSC_VER) - return _byteswap_ulong(v); -#else - return ((v & 0x000000FF) << 24) | - ((v & 0x0000FF00) << 8) | - ((v & 0x00FF0000) >> 8) | - ((v & 0xFF000000) >> 24); -#endif -} - -/* bswap64(v) - swap the bytes of a 64-bit integer */ -static forceinline u64 bswap64(u64 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_bswap64) - return __builtin_bswap64(v); -#elif defined(_MSC_VER) - return _byteswap_uint64(v); -#else - return ((v & 0x00000000000000FF) << 56) | - ((v & 0x000000000000FF00) << 40) | - ((v & 0x0000000000FF0000) << 24) | - ((v & 0x00000000FF000000) << 8) | - ((v & 0x000000FF00000000) >> 8) | - ((v & 0x0000FF0000000000) >> 24) | - ((v & 0x00FF000000000000) >> 40) | - ((v & 0xFF00000000000000) >> 56); -#endif -} - -#define le16_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? (v) : bswap16(v)) -#define le32_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? (v) : bswap32(v)) -#define le64_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? (v) : bswap64(v)) -#define be16_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? bswap16(v) : (v)) -#define be32_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? bswap32(v) : (v)) -#define be64_bswap(v) (CPU_IS_LITTLE_ENDIAN() ? bswap64(v) : (v)) - -/* ========================================================================== */ -/* Unaligned memory accesses */ -/* ========================================================================== */ - -/* - * UNALIGNED_ACCESS_IS_FAST() - 1 if unaligned memory accesses can be performed - * efficiently on the target platform, otherwise 0. - */ -#if (defined(__GNUC__) || defined(__clang__)) && \ - (defined(ARCH_X86_64) || defined(ARCH_X86_32) || \ - defined(__ARM_FEATURE_UNALIGNED) || \ - defined(__powerpc64__) || defined(__powerpc__) || defined(__POWERPC__) || \ - defined(__riscv_misaligned_fast) || \ - /* - * For all compilation purposes, WebAssembly behaves like any other CPU - * instruction set. Even though WebAssembly engine might be running on - * top of different actual CPU architectures, the WebAssembly spec - * itself permits unaligned access and it will be fast on most of those - * platforms, and simulated at the engine level on others, so it's - * worth treating it as a CPU architecture with fast unaligned access. - */ defined(__wasm__)) -# define UNALIGNED_ACCESS_IS_FAST 1 -#elif defined(_MSC_VER) -# define UNALIGNED_ACCESS_IS_FAST 1 -#else -# define UNALIGNED_ACCESS_IS_FAST 0 -#endif - -/* - * Implementing unaligned memory accesses using memcpy() is portable, and it - * usually gets optimized appropriately by modern compilers. I.e., each - * memcpy() of 1, 2, 4, or WORDBYTES bytes gets compiled to a load or store - * instruction, not to an actual function call. - * - * We no longer use the "packed struct" approach to unaligned accesses, as that - * is nonstandard, has unclear semantics, and doesn't receive enough testing - * (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94994). - * - * arm32 with __ARM_FEATURE_UNALIGNED in gcc 5 and earlier is a known exception - * where memcpy() generates inefficient code - * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67366). However, we no longer - * consider that one case important enough to maintain different code for. - * If you run into it, please just use a newer version of gcc (or use clang). - */ - -#ifdef FREESTANDING -# define MEMCOPY __builtin_memcpy -#else -# define MEMCOPY memcpy -#endif - -/* Unaligned loads and stores without endianness conversion */ - -#define DEFINE_UNALIGNED_TYPE(type) \ -static forceinline type \ -load_##type##_unaligned(const void *p) \ -{ \ - type v; \ - \ - MEMCOPY(&v, p, sizeof(v)); \ - return v; \ -} \ - \ -static forceinline void \ -store_##type##_unaligned(type v, void *p) \ -{ \ - MEMCOPY(p, &v, sizeof(v)); \ -} - -DEFINE_UNALIGNED_TYPE(u16) -DEFINE_UNALIGNED_TYPE(u32) -DEFINE_UNALIGNED_TYPE(u64) -DEFINE_UNALIGNED_TYPE(machine_word_t) - -#undef MEMCOPY - -#define load_word_unaligned load_machine_word_t_unaligned -#define store_word_unaligned store_machine_word_t_unaligned - -/* Unaligned loads with endianness conversion */ - -static forceinline u16 -get_unaligned_le16(const u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) - return le16_bswap(load_u16_unaligned(p)); - else - return ((u16)p[1] << 8) | p[0]; -} - -static forceinline u16 -get_unaligned_be16(const u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) - return be16_bswap(load_u16_unaligned(p)); - else - return ((u16)p[0] << 8) | p[1]; -} - -static forceinline u32 -get_unaligned_le32(const u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) - return le32_bswap(load_u32_unaligned(p)); - else - return ((u32)p[3] << 24) | ((u32)p[2] << 16) | - ((u32)p[1] << 8) | p[0]; -} - -static forceinline u32 -get_unaligned_be32(const u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) - return be32_bswap(load_u32_unaligned(p)); - else - return ((u32)p[0] << 24) | ((u32)p[1] << 16) | - ((u32)p[2] << 8) | p[3]; -} - -static forceinline u64 -get_unaligned_le64(const u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) - return le64_bswap(load_u64_unaligned(p)); - else - return ((u64)p[7] << 56) | ((u64)p[6] << 48) | - ((u64)p[5] << 40) | ((u64)p[4] << 32) | - ((u64)p[3] << 24) | ((u64)p[2] << 16) | - ((u64)p[1] << 8) | p[0]; -} - -static forceinline machine_word_t -get_unaligned_leword(const u8 *p) -{ - STATIC_ASSERT(WORDBITS == 32 || WORDBITS == 64); - if (WORDBITS == 32) - return get_unaligned_le32(p); - else - return get_unaligned_le64(p); -} - -/* Unaligned stores with endianness conversion */ - -static forceinline void -put_unaligned_le16(u16 v, u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) { - store_u16_unaligned(le16_bswap(v), p); - } else { - p[0] = (u8)(v >> 0); - p[1] = (u8)(v >> 8); - } -} - -static forceinline void -put_unaligned_be16(u16 v, u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) { - store_u16_unaligned(be16_bswap(v), p); - } else { - p[0] = (u8)(v >> 8); - p[1] = (u8)(v >> 0); - } -} - -static forceinline void -put_unaligned_le32(u32 v, u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) { - store_u32_unaligned(le32_bswap(v), p); - } else { - p[0] = (u8)(v >> 0); - p[1] = (u8)(v >> 8); - p[2] = (u8)(v >> 16); - p[3] = (u8)(v >> 24); - } -} - -static forceinline void -put_unaligned_be32(u32 v, u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) { - store_u32_unaligned(be32_bswap(v), p); - } else { - p[0] = (u8)(v >> 24); - p[1] = (u8)(v >> 16); - p[2] = (u8)(v >> 8); - p[3] = (u8)(v >> 0); - } -} - -static forceinline void -put_unaligned_le64(u64 v, u8 *p) -{ - if (UNALIGNED_ACCESS_IS_FAST) { - store_u64_unaligned(le64_bswap(v), p); - } else { - p[0] = (u8)(v >> 0); - p[1] = (u8)(v >> 8); - p[2] = (u8)(v >> 16); - p[3] = (u8)(v >> 24); - p[4] = (u8)(v >> 32); - p[5] = (u8)(v >> 40); - p[6] = (u8)(v >> 48); - p[7] = (u8)(v >> 56); - } -} - -static forceinline void -put_unaligned_leword(machine_word_t v, u8 *p) -{ - STATIC_ASSERT(WORDBITS == 32 || WORDBITS == 64); - if (WORDBITS == 32) - put_unaligned_le32(v, p); - else - put_unaligned_le64(v, p); -} - -/* ========================================================================== */ -/* Bit manipulation functions */ -/* ========================================================================== */ - -/* - * Bit Scan Reverse (BSR) - find the 0-based index (relative to the least - * significant end) of the *most* significant 1 bit in the input value. The - * input value must be nonzero! - */ - -static forceinline unsigned -bsr32(u32 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_clz) - return 31 - __builtin_clz(v); -#elif defined(_MSC_VER) - unsigned long i; - - _BitScanReverse(&i, v); - return i; -#else - unsigned i = 0; - - while ((v >>= 1) != 0) - i++; - return i; -#endif -} - -static forceinline unsigned -bsr64(u64 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_clzll) - return 63 - __builtin_clzll(v); -#elif defined(_MSC_VER) && defined(_WIN64) - unsigned long i; - - _BitScanReverse64(&i, v); - return i; -#else - unsigned i = 0; - - while ((v >>= 1) != 0) - i++; - return i; -#endif -} - -static forceinline unsigned -bsrw(machine_word_t v) -{ - STATIC_ASSERT(WORDBITS == 32 || WORDBITS == 64); - if (WORDBITS == 32) - return bsr32(v); - else - return bsr64(v); -} - -/* - * Bit Scan Forward (BSF) - find the 0-based index (relative to the least - * significant end) of the *least* significant 1 bit in the input value. The - * input value must be nonzero! - */ - -static forceinline unsigned -bsf32(u32 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_ctz) - return __builtin_ctz(v); -#elif defined(_MSC_VER) - unsigned long i; - - _BitScanForward(&i, v); - return i; -#else - unsigned i = 0; - - for (; (v & 1) == 0; v >>= 1) - i++; - return i; -#endif -} - -static forceinline unsigned -bsf64(u64 v) -{ -#if defined(__GNUC__) || __has_builtin(__builtin_ctzll) - return __builtin_ctzll(v); -#elif defined(_MSC_VER) && defined(_WIN64) - unsigned long i; - - _BitScanForward64(&i, v); - return i; -#else - unsigned i = 0; - - for (; (v & 1) == 0; v >>= 1) - i++; - return i; -#endif -} - -static forceinline unsigned -bsfw(machine_word_t v) -{ - STATIC_ASSERT(WORDBITS == 32 || WORDBITS == 64); - if (WORDBITS == 32) - return bsf32(v); - else - return bsf64(v); -} - -/* - * rbit32(v): reverse the bits in a 32-bit integer. This doesn't have a - * fallback implementation; use '#ifdef rbit32' to check if this is available. - */ -#undef rbit32 -#if (defined(__GNUC__) || defined(__clang__)) && defined(ARCH_ARM32) && \ - (__ARM_ARCH >= 7 || (__ARM_ARCH == 6 && defined(__ARM_ARCH_6T2__))) -static forceinline u32 -rbit32(u32 v) -{ - __asm__("rbit %0, %1" : "=r" (v) : "r" (v)); - return v; -} -#define rbit32 rbit32 -#elif (defined(__GNUC__) || defined(__clang__)) && defined(ARCH_ARM64) -static forceinline u32 -rbit32(u32 v) -{ - __asm__("rbit %w0, %w1" : "=r" (v) : "r" (v)); - return v; -} -#define rbit32 rbit32 -#endif - -#endif /* COMMON_DEFS_H */ diff --git a/dm/third_party/libdeflate/lib/adler32.c b/dm/third_party/libdeflate/lib/adler32.c deleted file mode 100644 index d5f39d8..0000000 --- a/dm/third_party/libdeflate/lib/adler32.c +++ /dev/null @@ -1,162 +0,0 @@ -/* - * adler32.c - Adler-32 checksum algorithm - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "lib_common.h" - -/* The Adler-32 divisor, or "base", value */ -#define DIVISOR 65521 - -/* - * MAX_CHUNK_LEN is the most bytes that can be processed without the possibility - * of s2 overflowing when it is represented as an unsigned 32-bit integer. This - * value was computed using the following Python script: - * - * divisor = 65521 - * count = 0 - * s1 = divisor - 1 - * s2 = divisor - 1 - * while True: - * s1 += 0xFF - * s2 += s1 - * if s2 > 0xFFFFFFFF: - * break - * count += 1 - * print(count) - * - * Note that to get the correct worst-case value, we must assume that every byte - * has value 0xFF and that s1 and s2 started with the highest possible values - * modulo the divisor. - */ -#define MAX_CHUNK_LEN 5552 - -/* - * Update the Adler-32 values s1 and s2 using n bytes from p, update p to p + n, - * update n to 0, and reduce s1 and s2 mod DIVISOR. It is assumed that neither - * s1 nor s2 can overflow before the reduction at the end, i.e. n plus any bytes - * already processed after the last reduction must not exceed MAX_CHUNK_LEN. - * - * This uses only portable C code. This is used as a fallback when a vectorized - * implementation of Adler-32 (e.g. AVX2) is unavailable on the platform. - * - * Some of the vectorized implementations also use this to handle the end of the - * data when the data isn't evenly divisible by the length the vectorized code - * works on. To avoid compiler errors about target-specific option mismatches - * when this is used in that way, this is a macro rather than a function. - * - * Although this is unvectorized, this does include an optimization where the - * main loop processes four bytes at a time using a strategy similar to that - * used by vectorized implementations. This provides increased instruction- - * level parallelism compared to the traditional 's1 += *p++; s2 += s1;'. - */ -#define ADLER32_CHUNK(s1, s2, p, n) \ -do { \ - if (n >= 4) { \ - u32 s1_sum = 0; \ - u32 byte_0_sum = 0; \ - u32 byte_1_sum = 0; \ - u32 byte_2_sum = 0; \ - u32 byte_3_sum = 0; \ - \ - do { \ - s1_sum += s1; \ - s1 += p[0] + p[1] + p[2] + p[3]; \ - byte_0_sum += p[0]; \ - byte_1_sum += p[1]; \ - byte_2_sum += p[2]; \ - byte_3_sum += p[3]; \ - p += 4; \ - n -= 4; \ - } while (n >= 4); \ - s2 += (4 * (s1_sum + byte_0_sum)) + (3 * byte_1_sum) + \ - (2 * byte_2_sum) + byte_3_sum; \ - } \ - for (; n; n--, p++) { \ - s1 += *p; \ - s2 += s1; \ - } \ - s1 %= DIVISOR; \ - s2 %= DIVISOR; \ -} while (0) - -static u32 MAYBE_UNUSED -adler32_generic(u32 adler, const u8 *p, size_t len) -{ - u32 s1 = adler & 0xFFFF; - u32 s2 = adler >> 16; - - while (len) { - size_t n = MIN(len, MAX_CHUNK_LEN & ~3); - - len -= n; - ADLER32_CHUNK(s1, s2, p, n); - } - - return (s2 << 16) | s1; -} - -/* Include architecture-specific implementation(s) if available. */ -#undef DEFAULT_IMPL -#undef arch_select_adler32_func -typedef u32 (*adler32_func_t)(u32 adler, const u8 *p, size_t len); -#if defined(ARCH_ARM32) || defined(ARCH_ARM64) -# include "arm/adler32_impl.h" -#elif defined(ARCH_X86_32) || defined(ARCH_X86_64) -# include "x86/adler32_impl.h" -#endif - -#ifndef DEFAULT_IMPL -# define DEFAULT_IMPL adler32_generic -#endif - -#ifdef arch_select_adler32_func -static u32 dispatch_adler32(u32 adler, const u8 *p, size_t len); - -static volatile adler32_func_t adler32_impl = dispatch_adler32; - -/* Choose the best implementation at runtime. */ -static u32 dispatch_adler32(u32 adler, const u8 *p, size_t len) -{ - adler32_func_t f = arch_select_adler32_func(); - - if (f == NULL) - f = DEFAULT_IMPL; - - adler32_impl = f; - return f(adler, p, len); -} -#else -/* The best implementation is statically known, so call it directly. */ -#define adler32_impl DEFAULT_IMPL -#endif - -LIBDEFLATEAPI u32 -libdeflate_adler32(u32 adler, const void *buffer, size_t len) -{ - if (buffer == NULL) /* Return initial value. */ - return 1; - return adler32_impl(adler, buffer, len); -} diff --git a/dm/third_party/libdeflate/lib/arm/adler32_impl.h b/dm/third_party/libdeflate/lib/arm/adler32_impl.h deleted file mode 100644 index 66d2f2b..0000000 --- a/dm/third_party/libdeflate/lib/arm/adler32_impl.h +++ /dev/null @@ -1,365 +0,0 @@ -/* - * arm/adler32_impl.h - ARM implementations of Adler-32 checksum algorithm - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_ARM_ADLER32_IMPL_H -#define LIB_ARM_ADLER32_IMPL_H - -#include "cpu_features.h" - -/* Regular NEON implementation */ -#if HAVE_NEON_INTRIN && CPU_IS_LITTLE_ENDIAN() -# define adler32_arm_neon adler32_arm_neon -# if HAVE_NEON_NATIVE - /* - * Use no attributes if none are needed, to support old versions of clang - * that don't accept the simd target attribute. - */ -# define ATTRIBUTES -# elif defined(ARCH_ARM32) -# define ATTRIBUTES _target_attribute("fpu=neon") -# elif defined(__clang__) -# define ATTRIBUTES _target_attribute("simd") -# else -# define ATTRIBUTES _target_attribute("+simd") -# endif -static ATTRIBUTES MAYBE_UNUSED u32 -adler32_arm_neon(u32 adler, const u8 *p, size_t len) -{ - static const u16 _aligned_attribute(16) mults[64] = { - 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, - 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, - 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, - }; - const uint16x8_t mults_a = vld1q_u16(&mults[0]); - const uint16x8_t mults_b = vld1q_u16(&mults[8]); - const uint16x8_t mults_c = vld1q_u16(&mults[16]); - const uint16x8_t mults_d = vld1q_u16(&mults[24]); - const uint16x8_t mults_e = vld1q_u16(&mults[32]); - const uint16x8_t mults_f = vld1q_u16(&mults[40]); - const uint16x8_t mults_g = vld1q_u16(&mults[48]); - const uint16x8_t mults_h = vld1q_u16(&mults[56]); - u32 s1 = adler & 0xFFFF; - u32 s2 = adler >> 16; - - /* - * If the length is large and the pointer is misaligned, align it. - * For smaller lengths, just take the misaligned load penalty. - */ - if (unlikely(len > 32768 && ((uintptr_t)p & 15))) { - do { - s1 += *p++; - s2 += s1; - len--; - } while ((uintptr_t)p & 15); - s1 %= DIVISOR; - s2 %= DIVISOR; - } - - while (len) { - /* - * Calculate the length of the next data chunk such that s1 and - * s2 are guaranteed to not exceed UINT32_MAX. - */ - size_t n = MIN(len, MAX_CHUNK_LEN & ~63); - - len -= n; - - if (n >= 64) { - uint32x4_t v_s1 = vdupq_n_u32(0); - uint32x4_t v_s2 = vdupq_n_u32(0); - /* - * v_byte_sums_* contain the sum of the bytes at index i - * across all 64-byte segments, for each index 0..63. - */ - uint16x8_t v_byte_sums_a = vdupq_n_u16(0); - uint16x8_t v_byte_sums_b = vdupq_n_u16(0); - uint16x8_t v_byte_sums_c = vdupq_n_u16(0); - uint16x8_t v_byte_sums_d = vdupq_n_u16(0); - uint16x8_t v_byte_sums_e = vdupq_n_u16(0); - uint16x8_t v_byte_sums_f = vdupq_n_u16(0); - uint16x8_t v_byte_sums_g = vdupq_n_u16(0); - uint16x8_t v_byte_sums_h = vdupq_n_u16(0); - - s2 += s1 * (n & ~63); - - do { - /* Load the next 64 data bytes. */ - const uint8x16_t data_a = vld1q_u8(p + 0); - const uint8x16_t data_b = vld1q_u8(p + 16); - const uint8x16_t data_c = vld1q_u8(p + 32); - const uint8x16_t data_d = vld1q_u8(p + 48); - uint16x8_t tmp; - - /* - * Accumulate the previous s1 counters into the - * s2 counters. The needed multiplication by 64 - * is delayed to later. - */ - v_s2 = vaddq_u32(v_s2, v_s1); - - /* - * Add the 64 data bytes to their v_byte_sums - * counters, while also accumulating the sums of - * each adjacent set of 4 bytes into v_s1. - */ - tmp = vpaddlq_u8(data_a); - v_byte_sums_a = vaddw_u8(v_byte_sums_a, - vget_low_u8(data_a)); - v_byte_sums_b = vaddw_u8(v_byte_sums_b, - vget_high_u8(data_a)); - tmp = vpadalq_u8(tmp, data_b); - v_byte_sums_c = vaddw_u8(v_byte_sums_c, - vget_low_u8(data_b)); - v_byte_sums_d = vaddw_u8(v_byte_sums_d, - vget_high_u8(data_b)); - tmp = vpadalq_u8(tmp, data_c); - v_byte_sums_e = vaddw_u8(v_byte_sums_e, - vget_low_u8(data_c)); - v_byte_sums_f = vaddw_u8(v_byte_sums_f, - vget_high_u8(data_c)); - tmp = vpadalq_u8(tmp, data_d); - v_byte_sums_g = vaddw_u8(v_byte_sums_g, - vget_low_u8(data_d)); - v_byte_sums_h = vaddw_u8(v_byte_sums_h, - vget_high_u8(data_d)); - v_s1 = vpadalq_u16(v_s1, tmp); - - p += 64; - n -= 64; - } while (n >= 64); - - /* s2 = 64*s2 + (64*bytesum0 + 63*bytesum1 + ... + 1*bytesum63) */ - #ifdef ARCH_ARM32 - # define umlal2(a, b, c) vmlal_u16((a), vget_high_u16(b), vget_high_u16(c)) - #else - # define umlal2 vmlal_high_u16 - #endif - v_s2 = vqshlq_n_u32(v_s2, 6); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_a), - vget_low_u16(mults_a)); - v_s2 = umlal2(v_s2, v_byte_sums_a, mults_a); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_b), - vget_low_u16(mults_b)); - v_s2 = umlal2(v_s2, v_byte_sums_b, mults_b); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_c), - vget_low_u16(mults_c)); - v_s2 = umlal2(v_s2, v_byte_sums_c, mults_c); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_d), - vget_low_u16(mults_d)); - v_s2 = umlal2(v_s2, v_byte_sums_d, mults_d); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_e), - vget_low_u16(mults_e)); - v_s2 = umlal2(v_s2, v_byte_sums_e, mults_e); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_f), - vget_low_u16(mults_f)); - v_s2 = umlal2(v_s2, v_byte_sums_f, mults_f); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_g), - vget_low_u16(mults_g)); - v_s2 = umlal2(v_s2, v_byte_sums_g, mults_g); - v_s2 = vmlal_u16(v_s2, vget_low_u16(v_byte_sums_h), - vget_low_u16(mults_h)); - v_s2 = umlal2(v_s2, v_byte_sums_h, mults_h); - #undef umlal2 - - /* Horizontal sum to finish up */ - #ifdef ARCH_ARM32 - s1 += vgetq_lane_u32(v_s1, 0) + vgetq_lane_u32(v_s1, 1) + - vgetq_lane_u32(v_s1, 2) + vgetq_lane_u32(v_s1, 3); - s2 += vgetq_lane_u32(v_s2, 0) + vgetq_lane_u32(v_s2, 1) + - vgetq_lane_u32(v_s2, 2) + vgetq_lane_u32(v_s2, 3); - #else - s1 += vaddvq_u32(v_s1); - s2 += vaddvq_u32(v_s2); - #endif - } - /* - * Process the last 0 <= n < 64 bytes of the chunk using - * scalar instructions and reduce s1 and s2 mod DIVISOR. - */ - ADLER32_CHUNK(s1, s2, p, n); - } - return (s2 << 16) | s1; -} -#undef ATTRIBUTES -#endif /* Regular NEON implementation */ - -/* NEON+dotprod implementation */ -#if HAVE_DOTPROD_INTRIN && CPU_IS_LITTLE_ENDIAN() && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_DOTPROD) -# define adler32_arm_neon_dotprod adler32_arm_neon_dotprod -# ifdef __clang__ -# define ATTRIBUTES _target_attribute("dotprod") - /* - * Both gcc and binutils originally considered dotprod to depend on - * arch=armv8.2-a or later. This was fixed in gcc 13.2 by commit - * 9aac37ab8a7b ("aarch64: Remove architecture dependencies from intrinsics") - * and in binutils 2.41 by commit 205e4380c800 ("aarch64: Remove version - * dependencies from features"). Unfortunately, always using arch=armv8.2-a - * causes build errors with some compiler options because it may reduce the - * arch rather than increase it. Therefore we try to omit the arch whenever - * possible. If gcc is 14 or later, then both gcc and binutils are probably - * fixed, so we omit the arch. We also omit the arch if a feature that - * depends on armv8.2-a or later (in gcc 13.1 and earlier) is present. - */ -# elif GCC_PREREQ(14, 0) || defined(__ARM_FEATURE_JCVT) \ - || defined(__ARM_FEATURE_DOTPROD) -# define ATTRIBUTES _target_attribute("+dotprod") -# else -# define ATTRIBUTES _target_attribute("arch=armv8.2-a+dotprod") -# endif -static ATTRIBUTES u32 -adler32_arm_neon_dotprod(u32 adler, const u8 *p, size_t len) -{ - static const u8 _aligned_attribute(16) mults[64] = { - 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, - 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, - 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, - }; - const uint8x16_t mults_a = vld1q_u8(&mults[0]); - const uint8x16_t mults_b = vld1q_u8(&mults[16]); - const uint8x16_t mults_c = vld1q_u8(&mults[32]); - const uint8x16_t mults_d = vld1q_u8(&mults[48]); - const uint8x16_t ones = vdupq_n_u8(1); - u32 s1 = adler & 0xFFFF; - u32 s2 = adler >> 16; - - /* - * If the length is large and the pointer is misaligned, align it. - * For smaller lengths, just take the misaligned load penalty. - */ - if (unlikely(len > 32768 && ((uintptr_t)p & 15))) { - do { - s1 += *p++; - s2 += s1; - len--; - } while ((uintptr_t)p & 15); - s1 %= DIVISOR; - s2 %= DIVISOR; - } - - while (len) { - /* - * Calculate the length of the next data chunk such that s1 and - * s2 are guaranteed to not exceed UINT32_MAX. - */ - size_t n = MIN(len, MAX_CHUNK_LEN & ~63); - - len -= n; - - if (n >= 64) { - uint32x4_t v_s1_a = vdupq_n_u32(0); - uint32x4_t v_s1_b = vdupq_n_u32(0); - uint32x4_t v_s1_c = vdupq_n_u32(0); - uint32x4_t v_s1_d = vdupq_n_u32(0); - uint32x4_t v_s2_a = vdupq_n_u32(0); - uint32x4_t v_s2_b = vdupq_n_u32(0); - uint32x4_t v_s2_c = vdupq_n_u32(0); - uint32x4_t v_s2_d = vdupq_n_u32(0); - uint32x4_t v_s1_sums_a = vdupq_n_u32(0); - uint32x4_t v_s1_sums_b = vdupq_n_u32(0); - uint32x4_t v_s1_sums_c = vdupq_n_u32(0); - uint32x4_t v_s1_sums_d = vdupq_n_u32(0); - uint32x4_t v_s1; - uint32x4_t v_s2; - uint32x4_t v_s1_sums; - - s2 += s1 * (n & ~63); - - do { - uint8x16_t data_a = vld1q_u8(p + 0); - uint8x16_t data_b = vld1q_u8(p + 16); - uint8x16_t data_c = vld1q_u8(p + 32); - uint8x16_t data_d = vld1q_u8(p + 48); - - v_s1_sums_a = vaddq_u32(v_s1_sums_a, v_s1_a); - v_s1_a = vdotq_u32(v_s1_a, data_a, ones); - v_s2_a = vdotq_u32(v_s2_a, data_a, mults_a); - - v_s1_sums_b = vaddq_u32(v_s1_sums_b, v_s1_b); - v_s1_b = vdotq_u32(v_s1_b, data_b, ones); - v_s2_b = vdotq_u32(v_s2_b, data_b, mults_b); - - v_s1_sums_c = vaddq_u32(v_s1_sums_c, v_s1_c); - v_s1_c = vdotq_u32(v_s1_c, data_c, ones); - v_s2_c = vdotq_u32(v_s2_c, data_c, mults_c); - - v_s1_sums_d = vaddq_u32(v_s1_sums_d, v_s1_d); - v_s1_d = vdotq_u32(v_s1_d, data_d, ones); - v_s2_d = vdotq_u32(v_s2_d, data_d, mults_d); - - p += 64; - n -= 64; - } while (n >= 64); - - v_s1 = vaddq_u32(vaddq_u32(v_s1_a, v_s1_b), - vaddq_u32(v_s1_c, v_s1_d)); - v_s2 = vaddq_u32(vaddq_u32(v_s2_a, v_s2_b), - vaddq_u32(v_s2_c, v_s2_d)); - v_s1_sums = vaddq_u32(vaddq_u32(v_s1_sums_a, - v_s1_sums_b), - vaddq_u32(v_s1_sums_c, - v_s1_sums_d)); - v_s2 = vaddq_u32(v_s2, vqshlq_n_u32(v_s1_sums, 6)); - - s1 += vaddvq_u32(v_s1); - s2 += vaddvq_u32(v_s2); - } - /* - * Process the last 0 <= n < 64 bytes of the chunk using - * scalar instructions and reduce s1 and s2 mod DIVISOR. - */ - ADLER32_CHUNK(s1, s2, p, n); - } - return (s2 << 16) | s1; -} -#undef ATTRIBUTES -#endif /* NEON+dotprod implementation */ - -#if defined(adler32_arm_neon_dotprod) && defined(__ARM_FEATURE_DOTPROD) -#define DEFAULT_IMPL adler32_arm_neon_dotprod -#else -static inline adler32_func_t -arch_select_adler32_func(void) -{ - const u32 features MAYBE_UNUSED = get_arm_cpu_features(); - -#ifdef adler32_arm_neon_dotprod - if (HAVE_NEON(features) && HAVE_DOTPROD(features)) - return adler32_arm_neon_dotprod; -#endif -#ifdef adler32_arm_neon - if (HAVE_NEON(features)) - return adler32_arm_neon; -#endif - return NULL; -} -#define arch_select_adler32_func arch_select_adler32_func -#endif - -#endif /* LIB_ARM_ADLER32_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/arm/cpu_features.c b/dm/third_party/libdeflate/lib/arm/cpu_features.c deleted file mode 100644 index a4008ca..0000000 --- a/dm/third_party/libdeflate/lib/arm/cpu_features.c +++ /dev/null @@ -1,292 +0,0 @@ -/* - * arm/cpu_features.c - feature detection for ARM CPUs - * - * Copyright 2018 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * ARM CPUs don't have a standard way for unprivileged programs to detect CPU - * features. But an OS-specific way can be used when available. - */ - -#ifdef __APPLE__ -# undef _ANSI_SOURCE -# undef _DARWIN_C_SOURCE -# define _DARWIN_C_SOURCE /* for sysctlbyname() */ -#endif - -#include "../cpu_features_common.h" /* must be included first */ -#include "cpu_features.h" - -#ifdef ARM_CPU_FEATURES_KNOWN -/* Runtime ARM CPU feature detection is supported. */ - -#ifdef __linux__ -/* - * On Linux, arm32 and arm64 CPU features can be detected by reading the - * AT_HWCAP and AT_HWCAP2 values from /proc/self/auxv. - * - * Ideally we'd use the C library function getauxval(), but it's not guaranteed - * to be available: it was only added to glibc in 2.16, and in Android it was - * added to API level 18 for arm32 and level 21 for arm64. - */ - -#include <errno.h> -#include <fcntl.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -#define AT_HWCAP 16 -#define AT_HWCAP2 26 - -static void scan_auxv(unsigned long *hwcap, unsigned long *hwcap2) -{ - int fd; - unsigned long auxbuf[32]; - int filled = 0; - int i; - - fd = open("/proc/self/auxv", O_RDONLY | O_CLOEXEC); - if (fd < 0) - return; - - for (;;) { - do { - int ret = read(fd, &((char *)auxbuf)[filled], - sizeof(auxbuf) - filled); - if (ret <= 0) { - if (ret < 0 && errno == EINTR) - continue; - goto out; - } - filled += ret; - } while (filled < 2 * sizeof(long)); - - i = 0; - do { - unsigned long type = auxbuf[i]; - unsigned long value = auxbuf[i + 1]; - - if (type == AT_HWCAP) - *hwcap = value; - else if (type == AT_HWCAP2) - *hwcap2 = value; - i += 2; - filled -= 2 * sizeof(long); - } while (filled >= 2 * sizeof(long)); - - memmove(auxbuf, &auxbuf[i], filled); - } -out: - close(fd); -} - -static u32 query_arm_cpu_features(void) -{ - u32 features = 0; - unsigned long hwcap = 0; - unsigned long hwcap2 = 0; - - scan_auxv(&hwcap, &hwcap2); - -#ifdef ARCH_ARM32 - STATIC_ASSERT(sizeof(long) == 4); - if (hwcap & (1 << 12)) /* HWCAP_NEON */ - features |= ARM_CPU_FEATURE_NEON; -#else - STATIC_ASSERT(sizeof(long) == 8); - if (hwcap & (1 << 1)) /* HWCAP_ASIMD */ - features |= ARM_CPU_FEATURE_NEON; - if (hwcap & (1 << 4)) /* HWCAP_PMULL */ - features |= ARM_CPU_FEATURE_PMULL; - if (hwcap & (1 << 7)) /* HWCAP_CRC32 */ - features |= ARM_CPU_FEATURE_CRC32; - if (hwcap & (1 << 17)) /* HWCAP_SHA3 */ - features |= ARM_CPU_FEATURE_SHA3; - if (hwcap & (1 << 20)) /* HWCAP_ASIMDDP */ - features |= ARM_CPU_FEATURE_DOTPROD; -#endif - return features; -} - -#ifdef ARCH_ARM64 -/* - * Return whether cpu0's MIDR_EL1 identifies one of the Arm Neoverse - * V-class server cores (V1 / V2 / V3 / V3AE). MIDR_EL1 is exposed - * unprivileged via sysfs (added in Linux 4.7). Reading cpu0 only is fine - * in practice: no Neoverse V-class server SKU has shipped as part of a - * big.LITTLE cluster. Any failure (file missing, read error, parse - * failure, unrecognized CPU) returns false. - */ -static bool arm64_cpu_is_neoverse_v_class(void) -{ - int fd; - char buf[32]; - ssize_t n; - unsigned long midr; - u32 part; - - fd = open("/sys/devices/system/cpu/cpu0/regs/identification/midr_el1", - O_RDONLY | O_CLOEXEC); - if (fd < 0) - return false; - do { - n = read(fd, buf, sizeof(buf) - 1); - } while (n < 0 && errno == EINTR); - close(fd); - if (n <= 0) - return false; - buf[n] = '\0'; - midr = strtoul(buf, NULL, 0); /* sysfs prints "0x%016llx\n" */ - - /* MIDR_EL1: [31:24]=Implementer, [15:4]=PartNum. */ - if (((midr >> 24) & 0xff) != 0x41) /* Implementer must be Arm Ltd. */ - return false; - part = (midr >> 4) & 0xfff; - switch (part) { - case 0xd40: /* Neoverse V1 (e.g. AWS Graviton 3) */ - case 0xd4f: /* Neoverse V2 (e.g. AWS Graviton 4) */ - case 0xd83: /* Neoverse V3AE */ - case 0xd84: /* Neoverse V3 */ - return true; - } - return false; -} -#endif /* ARCH_ARM64 */ - -#elif defined(__APPLE__) -/* On Apple platforms, arm64 CPU features can be detected via sysctlbyname(). */ - -#include <sys/types.h> -#include <sys/sysctl.h> -#include <TargetConditionals.h> - -static const struct { - const char *name; - u32 feature; -} feature_sysctls[] = { - { "hw.optional.neon", ARM_CPU_FEATURE_NEON }, - { "hw.optional.AdvSIMD", ARM_CPU_FEATURE_NEON }, - { "hw.optional.arm.FEAT_PMULL", ARM_CPU_FEATURE_PMULL }, - { "hw.optional.armv8_crc32", ARM_CPU_FEATURE_CRC32 }, - { "hw.optional.armv8_2_sha3", ARM_CPU_FEATURE_SHA3 }, - { "hw.optional.arm.FEAT_SHA3", ARM_CPU_FEATURE_SHA3 }, - { "hw.optional.arm.FEAT_DotProd", ARM_CPU_FEATURE_DOTPROD }, -}; - -static u32 query_arm_cpu_features(void) -{ - u32 features = 0; - size_t i; - - for (i = 0; i < ARRAY_LEN(feature_sysctls); i++) { - const char *name = feature_sysctls[i].name; - u32 val = 0; - size_t valsize = sizeof(val); - - if (sysctlbyname(name, &val, &valsize, NULL, 0) == 0 && - valsize == sizeof(val) && val == 1) - features |= feature_sysctls[i].feature; - } - return features; -} -#elif defined(_WIN32) - -#include <windows.h> - -#ifndef PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE /* added in Windows SDK 20348 */ -# define PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE 43 -#endif - -static u32 query_arm_cpu_features(void) -{ - u32 features = ARM_CPU_FEATURE_NEON; - - if (IsProcessorFeaturePresent(PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE)) - features |= ARM_CPU_FEATURE_PMULL; - if (IsProcessorFeaturePresent(PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)) - features |= ARM_CPU_FEATURE_CRC32; - if (IsProcessorFeaturePresent(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE)) - features |= ARM_CPU_FEATURE_DOTPROD; - - /* FIXME: detect SHA3 support too. */ - - return features; -} -#else -#error "unhandled case" -#endif - -static const struct cpu_feature arm_cpu_feature_table[] = { - {ARM_CPU_FEATURE_NEON, "neon"}, - {ARM_CPU_FEATURE_PMULL, "pmull"}, - {ARM_CPU_FEATURE_PREFER_PMULL, "prefer_pmull"}, - {ARM_CPU_FEATURE_CRC32, "crc32"}, - {ARM_CPU_FEATURE_SHA3, "sha3"}, - {ARM_CPU_FEATURE_DOTPROD, "dotprod"}, -}; - -/* - * Whether to set ARM_CPU_FEATURE_PREFER_PMULL on this CPU. This is the - * right choice on CPUs whose pmull pipes have more aggregate throughput - * than the crc32 unit -- in measured cases by a wide margin: the Apple M - * series sustains ~68 GB/s on pmull vs ~25 GB/s on crc32 (M1), and the Arm - * Neoverse V class sustains ~40 GB/s vs ~22 GB/s (Graviton 4 / V2). - * - * We detect Apple at compile time, and Neoverse V-class cores at runtime - * via MIDR_EL1 on Linux. Elsewhere we leave this unset, and the dispatcher - * picks the crc32-instruction path which is the right default for most - * other modern ARM CPUs. - */ -static bool arm_cpu_prefers_pmull(void) -{ -#if defined(__APPLE__) && TARGET_OS_OSX - return true; -#elif defined(__linux__) && defined(ARCH_ARM64) - if (arm64_cpu_is_neoverse_v_class()) - return true; -#endif -#ifdef TEST_SUPPORT__DO_NOT_USE - return true; -#endif - return false; -} - -volatile u32 libdeflate_arm_cpu_features = 0; - -void libdeflate_init_arm_cpu_features(void) -{ - u32 features = query_arm_cpu_features(); - - if (arm_cpu_prefers_pmull()) - features |= ARM_CPU_FEATURE_PREFER_PMULL; - - disable_cpu_features_for_testing(&features, arm_cpu_feature_table, - ARRAY_LEN(arm_cpu_feature_table)); - - libdeflate_arm_cpu_features = features | ARM_CPU_FEATURES_KNOWN; -} - -#endif /* ARM_CPU_FEATURES_KNOWN */ diff --git a/dm/third_party/libdeflate/lib/arm/cpu_features.h b/dm/third_party/libdeflate/lib/arm/cpu_features.h deleted file mode 100644 index dc9ab8a..0000000 --- a/dm/third_party/libdeflate/lib/arm/cpu_features.h +++ /dev/null @@ -1,214 +0,0 @@ -/* - * arm/cpu_features.h - feature detection for ARM CPUs - * - * Copyright 2018 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_ARM_CPU_FEATURES_H -#define LIB_ARM_CPU_FEATURES_H - -#include "../lib_common.h" - -#if defined(ARCH_ARM32) || defined(ARCH_ARM64) - -#define ARM_CPU_FEATURE_NEON (1 << 0) -#define ARM_CPU_FEATURE_PMULL (1 << 1) -/* - * PREFER_PMULL indicates that the CPU has very high pmull throughput, and so - * the 12x wide pmull-based CRC-32 implementation is likely to be faster than an - * implementation based on the crc32 instructions. - */ -#define ARM_CPU_FEATURE_PREFER_PMULL (1 << 2) -#define ARM_CPU_FEATURE_CRC32 (1 << 3) -#define ARM_CPU_FEATURE_SHA3 (1 << 4) -#define ARM_CPU_FEATURE_DOTPROD (1 << 5) - -#if !defined(FREESTANDING) && \ - (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER)) && \ - (defined(__linux__) || \ - (defined(__APPLE__) && defined(ARCH_ARM64)) || \ - (defined(_WIN32) && defined(ARCH_ARM64))) -/* Runtime ARM CPU feature detection is supported. */ -# define ARM_CPU_FEATURES_KNOWN (1U << 31) -extern volatile u32 libdeflate_arm_cpu_features; - -void libdeflate_init_arm_cpu_features(void); - -static inline u32 get_arm_cpu_features(void) -{ - if (libdeflate_arm_cpu_features == 0) - libdeflate_init_arm_cpu_features(); - return libdeflate_arm_cpu_features; -} -#else -static inline u32 get_arm_cpu_features(void) { return 0; } -#endif - -/* NEON */ -#if defined(__ARM_NEON) || (defined(_MSC_VER) && defined(ARCH_ARM64)) -# define HAVE_NEON(features) 1 -# define HAVE_NEON_NATIVE 1 -#else -# define HAVE_NEON(features) ((features) & ARM_CPU_FEATURE_NEON) -# define HAVE_NEON_NATIVE 0 -#endif -/* - * With both gcc and clang, NEON intrinsics require that the main target has - * NEON enabled already. Exception: with gcc 6.1 and later (r230411 for arm32, - * r226563 for arm64), hardware floating point support is sufficient. - */ -#if (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER)) && \ - (HAVE_NEON_NATIVE || (GCC_PREREQ(6, 1) && defined(__ARM_FP))) -# define HAVE_NEON_INTRIN 1 -# include <arm_neon.h> -#else -# define HAVE_NEON_INTRIN 0 -#endif - -/* PMULL */ -#ifdef __ARM_FEATURE_CRYPTO -# define HAVE_PMULL(features) 1 -#else -# define HAVE_PMULL(features) ((features) & ARM_CPU_FEATURE_PMULL) -#endif -#if defined(ARCH_ARM64) && HAVE_NEON_INTRIN && \ - (GCC_PREREQ(7, 1) || defined(__clang__) || defined(_MSC_VER)) && \ - CPU_IS_LITTLE_ENDIAN() /* untested on big endian */ -# define HAVE_PMULL_INTRIN 1 - /* Work around MSVC's vmull_p64() taking poly64x1_t instead of poly64_t */ -# ifdef _MSC_VER -# define compat_vmull_p64(a, b) vmull_p64(vcreate_p64(a), vcreate_p64(b)) -# else -# define compat_vmull_p64(a, b) vmull_p64((a), (b)) -# endif -#else -# define HAVE_PMULL_INTRIN 0 -#endif - -/* CRC32 */ -#ifdef __ARM_FEATURE_CRC32 -# define HAVE_CRC32(features) 1 -#else -# define HAVE_CRC32(features) ((features) & ARM_CPU_FEATURE_CRC32) -#endif -#if defined(ARCH_ARM64) && \ - (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER)) -# define HAVE_CRC32_INTRIN 1 -# if defined(__GNUC__) || defined(__clang__) -# include <arm_acle.h> -# endif - /* - * Use an inline assembly fallback for clang 15 and earlier, which only - * defined the crc32 intrinsics when crc32 is enabled in the main target. - */ -# if defined(__clang__) && !CLANG_PREREQ(16, 0, 16000000) && \ - !defined(__ARM_FEATURE_CRC32) -# undef __crc32b -# define __crc32b(a, b) \ - ({ uint32_t res; \ - __asm__("crc32b %w0, %w1, %w2" \ - : "=r" (res) : "r" (a), "r" (b)); \ - res; }) -# undef __crc32h -# define __crc32h(a, b) \ - ({ uint32_t res; \ - __asm__("crc32h %w0, %w1, %w2" \ - : "=r" (res) : "r" (a), "r" (b)); \ - res; }) -# undef __crc32w -# define __crc32w(a, b) \ - ({ uint32_t res; \ - __asm__("crc32w %w0, %w1, %w2" \ - : "=r" (res) : "r" (a), "r" (b)); \ - res; }) -# undef __crc32d -# define __crc32d(a, b) \ - ({ uint32_t res; \ - __asm__("crc32x %w0, %w1, %2" \ - : "=r" (res) : "r" (a), "r" (b)); \ - res; }) -# pragma clang diagnostic ignored "-Wgnu-statement-expression" -# endif -#else -# define HAVE_CRC32_INTRIN 0 -#endif - -/* SHA3 (needed for the eor3 instruction) */ -#ifdef __ARM_FEATURE_SHA3 -# define HAVE_SHA3(features) 1 -#else -# define HAVE_SHA3(features) ((features) & ARM_CPU_FEATURE_SHA3) -#endif -#if defined(ARCH_ARM64) && HAVE_NEON_INTRIN && \ - (GCC_PREREQ(9, 1) /* r268049 */ || \ - CLANG_PREREQ(7, 0, 10010463) /* r338010 */) -# define HAVE_SHA3_INTRIN 1 - /* - * Use an inline assembly fallback for clang 15 and earlier, which only - * defined the sha3 intrinsics when sha3 is enabled in the main target. - */ -# if defined(__clang__) && !CLANG_PREREQ(16, 0, 16000000) && \ - !defined(__ARM_FEATURE_SHA3) -# undef veor3q_u8 -# define veor3q_u8(a, b, c) \ - ({ uint8x16_t res; \ - __asm__("eor3 %0.16b, %1.16b, %2.16b, %3.16b" \ - : "=w" (res) : "w" (a), "w" (b), "w" (c)); \ - res; }) -# pragma clang diagnostic ignored "-Wgnu-statement-expression" -# endif -#else -# define HAVE_SHA3_INTRIN 0 -#endif - -/* dotprod */ -#ifdef __ARM_FEATURE_DOTPROD -# define HAVE_DOTPROD(features) 1 -#else -# define HAVE_DOTPROD(features) ((features) & ARM_CPU_FEATURE_DOTPROD) -#endif -#if defined(ARCH_ARM64) && HAVE_NEON_INTRIN && \ - (GCC_PREREQ(8, 1) || CLANG_PREREQ(7, 0, 10010000) || defined(_MSC_VER)) -# define HAVE_DOTPROD_INTRIN 1 - /* - * Use an inline assembly fallback for clang 15 and earlier, which only - * defined the dotprod intrinsics when dotprod is enabled in the main target. - */ -# if defined(__clang__) && !CLANG_PREREQ(16, 0, 16000000) && \ - !defined(__ARM_FEATURE_DOTPROD) -# undef vdotq_u32 -# define vdotq_u32(a, b, c) \ - ({ uint32x4_t res = (a); \ - __asm__("udot %0.4s, %1.16b, %2.16b" \ - : "+w" (res) : "w" (b), "w" (c)); \ - res; }) -# pragma clang diagnostic ignored "-Wgnu-statement-expression" -# endif -#else -# define HAVE_DOTPROD_INTRIN 0 -#endif - -#endif /* ARCH_ARM32 || ARCH_ARM64 */ - -#endif /* LIB_ARM_CPU_FEATURES_H */ diff --git a/dm/third_party/libdeflate/lib/arm/crc32_impl.h b/dm/third_party/libdeflate/lib/arm/crc32_impl.h deleted file mode 100644 index 696af5a..0000000 --- a/dm/third_party/libdeflate/lib/arm/crc32_impl.h +++ /dev/null @@ -1,594 +0,0 @@ -/* - * arm/crc32_impl.h - ARM implementations of the gzip CRC-32 algorithm - * - * Copyright 2022 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_ARM_CRC32_IMPL_H -#define LIB_ARM_CRC32_IMPL_H - -#include "cpu_features.h" - -/* - * crc32_arm_crc() - implementation using crc32 instructions (only) - * - * In general this implementation is straightforward. However, naive use of the - * crc32 instructions is serial: one of the two inputs to each crc32 instruction - * is the output of the previous one. To take advantage of CPUs that can - * execute multiple crc32 instructions in parallel, when possible we interleave - * the checksumming of several adjacent chunks, then combine their CRCs. - * - * However, without pmull, combining CRCs is fairly slow. So in this pmull-less - * version, we only use a large chunk length, and thus we only do chunked - * processing if there is a lot of data to checksum. This also means that a - * variable chunk length wouldn't help much, so we just support a fixed length. - */ -#if HAVE_CRC32_INTRIN -# ifdef __clang__ -# define ATTRIBUTES _target_attribute("crc") -# else -# define ATTRIBUTES _target_attribute("+crc") -# endif - -/* - * Combine the CRCs for 4 adjacent chunks of length L = CRC32_FIXED_CHUNK_LEN - * bytes each by computing: - * - * [ crc0*x^(3*8*L) + crc1*x^(2*8*L) + crc2*x^(1*8*L) + crc3 ] mod G(x) - * - * This has been optimized in several ways: - * - * - The needed multipliers (x to some power, reduced mod G(x)) were - * precomputed. - * - * - The 3 multiplications are interleaved. - * - * - The reduction mod G(x) is delayed to the end and done using __crc32d. - * Note that the use of __crc32d introduces an extra factor of x^32. To - * cancel that out along with the extra factor of x^1 that gets introduced - * because of how the 63-bit products are aligned in their 64-bit integers, - * the multipliers are actually x^(j*8*L - 33) instead of x^(j*8*L). - */ -static forceinline ATTRIBUTES u32 -combine_crcs_slow(u32 crc0, u32 crc1, u32 crc2, u32 crc3) -{ - u64 res0 = 0, res1 = 0, res2 = 0; - int i; - - /* Multiply crc{0,1,2} by CRC32_FIXED_CHUNK_MULT_{3,2,1}. */ - for (i = 0; i < 32; i++) { - if (CRC32_FIXED_CHUNK_MULT_3 & (1U << i)) - res0 ^= (u64)crc0 << i; - if (CRC32_FIXED_CHUNK_MULT_2 & (1U << i)) - res1 ^= (u64)crc1 << i; - if (CRC32_FIXED_CHUNK_MULT_1 & (1U << i)) - res2 ^= (u64)crc2 << i; - } - /* Add the different parts and reduce mod G(x). */ - return __crc32d(0, res0 ^ res1 ^ res2) ^ crc3; -} - -#define crc32_arm_crc crc32_arm_crc -static ATTRIBUTES u32 -crc32_arm_crc(u32 crc, const u8 *p, size_t len) -{ - if (len >= 64) { - const size_t align = -(uintptr_t)p & 7; - - /* Align p to the next 8-byte boundary. */ - if (align) { - if (align & 1) - crc = __crc32b(crc, *p++); - if (align & 2) { - crc = __crc32h(crc, le16_bswap(*(u16 *)p)); - p += 2; - } - if (align & 4) { - crc = __crc32w(crc, le32_bswap(*(u32 *)p)); - p += 4; - } - len -= align; - } - /* - * Interleave the processing of multiple adjacent data chunks to - * take advantage of instruction-level parallelism. - * - * Some CPUs don't prefetch the data if it's being fetched in - * multiple interleaved streams, so do explicit prefetching. - */ - while (len >= CRC32_NUM_CHUNKS * CRC32_FIXED_CHUNK_LEN) { - const u64 *wp0 = (const u64 *)p; - const u64 * const wp0_end = - (const u64 *)(p + CRC32_FIXED_CHUNK_LEN); - u32 crc1 = 0, crc2 = 0, crc3 = 0; - - STATIC_ASSERT(CRC32_NUM_CHUNKS == 4); - STATIC_ASSERT(CRC32_FIXED_CHUNK_LEN % (4 * 8) == 0); - do { - prefetchr(&wp0[64 + 0*CRC32_FIXED_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 1*CRC32_FIXED_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 2*CRC32_FIXED_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 3*CRC32_FIXED_CHUNK_LEN/8]); - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_FIXED_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_FIXED_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_FIXED_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_FIXED_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_FIXED_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_FIXED_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_FIXED_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_FIXED_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_FIXED_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_FIXED_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_FIXED_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_FIXED_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_FIXED_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_FIXED_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_FIXED_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_FIXED_CHUNK_LEN/8])); - wp0++; - } while (wp0 != wp0_end); - crc = combine_crcs_slow(crc, crc1, crc2, crc3); - p += CRC32_NUM_CHUNKS * CRC32_FIXED_CHUNK_LEN; - len -= CRC32_NUM_CHUNKS * CRC32_FIXED_CHUNK_LEN; - } - /* - * Due to the large fixed chunk length used above, there might - * still be a lot of data left. So use a 64-byte loop here, - * instead of a loop that is less unrolled. - */ - while (len >= 64) { - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 0))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 8))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 16))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 24))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 32))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 40))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 48))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 56))); - p += 64; - len -= 64; - } - } - if (len & 32) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - crc = __crc32d(crc, get_unaligned_le64(p + 16)); - crc = __crc32d(crc, get_unaligned_le64(p + 24)); - p += 32; - } - if (len & 16) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - p += 16; - } - if (len & 8) { - crc = __crc32d(crc, get_unaligned_le64(p)); - p += 8; - } - if (len & 4) { - crc = __crc32w(crc, get_unaligned_le32(p)); - p += 4; - } - if (len & 2) { - crc = __crc32h(crc, get_unaligned_le16(p)); - p += 2; - } - if (len & 1) - crc = __crc32b(crc, *p); - return crc; -} -#undef ATTRIBUTES -#endif /* crc32_arm_crc() */ - -/* - * crc32_arm_crc_pmullcombine() - implementation using crc32 instructions, plus - * pmull instructions for CRC combining - * - * This is similar to crc32_arm_crc(), but it enables the use of pmull - * (carryless multiplication) instructions for the steps where the CRCs of - * adjacent data chunks are combined. As this greatly speeds up CRC - * combination, this implementation also differs from crc32_arm_crc() in that it - * uses a variable chunk length which can get fairly small. The precomputed - * multipliers needed for the selected chunk length are loaded from a table. - * - * Note that pmull is used here only for combining the CRCs of separately - * checksummed chunks, not for folding the data itself. See crc32_arm_pmull*() - * for implementations that use pmull for folding the data itself. - */ -#if HAVE_CRC32_INTRIN && HAVE_PMULL_INTRIN -# ifdef __clang__ -# define ATTRIBUTES _target_attribute("crc,aes") -# else -# define ATTRIBUTES _target_attribute("+crc,+crypto") -# endif - -/* Do carryless multiplication of two 32-bit values. */ -static forceinline ATTRIBUTES u64 -clmul_u32(u32 a, u32 b) -{ - uint64x2_t res = vreinterpretq_u64_p128( - compat_vmull_p64((poly64_t)a, (poly64_t)b)); - - return vgetq_lane_u64(res, 0); -} - -/* - * Like combine_crcs_slow(), but uses vmull_p64 to do the multiplications more - * quickly, and supports a variable chunk length. The chunk length is - * 'i * CRC32_MIN_VARIABLE_CHUNK_LEN' - * where 1 <= i < ARRAY_LEN(crc32_mults_for_chunklen). - */ -static forceinline ATTRIBUTES u32 -combine_crcs_fast(u32 crc0, u32 crc1, u32 crc2, u32 crc3, size_t i) -{ - u64 res0 = clmul_u32(crc0, crc32_mults_for_chunklen[i][0]); - u64 res1 = clmul_u32(crc1, crc32_mults_for_chunklen[i][1]); - u64 res2 = clmul_u32(crc2, crc32_mults_for_chunklen[i][2]); - - return __crc32d(0, res0 ^ res1 ^ res2) ^ crc3; -} - -#define crc32_arm_crc_pmullcombine crc32_arm_crc_pmullcombine -static ATTRIBUTES u32 -crc32_arm_crc_pmullcombine(u32 crc, const u8 *p, size_t len) -{ - const size_t align = -(uintptr_t)p & 7; - - if (len >= align + CRC32_NUM_CHUNKS * CRC32_MIN_VARIABLE_CHUNK_LEN) { - /* Align p to the next 8-byte boundary. */ - if (align) { - if (align & 1) - crc = __crc32b(crc, *p++); - if (align & 2) { - crc = __crc32h(crc, le16_bswap(*(u16 *)p)); - p += 2; - } - if (align & 4) { - crc = __crc32w(crc, le32_bswap(*(u32 *)p)); - p += 4; - } - len -= align; - } - /* - * Handle CRC32_MAX_VARIABLE_CHUNK_LEN specially, so that better - * code is generated for it. - */ - while (len >= CRC32_NUM_CHUNKS * CRC32_MAX_VARIABLE_CHUNK_LEN) { - const u64 *wp0 = (const u64 *)p; - const u64 * const wp0_end = - (const u64 *)(p + CRC32_MAX_VARIABLE_CHUNK_LEN); - u32 crc1 = 0, crc2 = 0, crc3 = 0; - - STATIC_ASSERT(CRC32_NUM_CHUNKS == 4); - STATIC_ASSERT(CRC32_MAX_VARIABLE_CHUNK_LEN % (4 * 8) == 0); - do { - prefetchr(&wp0[64 + 0*CRC32_MAX_VARIABLE_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 1*CRC32_MAX_VARIABLE_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 2*CRC32_MAX_VARIABLE_CHUNK_LEN/8]); - prefetchr(&wp0[64 + 3*CRC32_MAX_VARIABLE_CHUNK_LEN/8]); - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - wp0++; - crc = __crc32d(crc, le64_bswap(wp0[0*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc1 = __crc32d(crc1, le64_bswap(wp0[1*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc2 = __crc32d(crc2, le64_bswap(wp0[2*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - crc3 = __crc32d(crc3, le64_bswap(wp0[3*CRC32_MAX_VARIABLE_CHUNK_LEN/8])); - wp0++; - } while (wp0 != wp0_end); - crc = combine_crcs_fast(crc, crc1, crc2, crc3, - ARRAY_LEN(crc32_mults_for_chunklen) - 1); - p += CRC32_NUM_CHUNKS * CRC32_MAX_VARIABLE_CHUNK_LEN; - len -= CRC32_NUM_CHUNKS * CRC32_MAX_VARIABLE_CHUNK_LEN; - } - /* Handle up to one variable-length chunk. */ - if (len >= CRC32_NUM_CHUNKS * CRC32_MIN_VARIABLE_CHUNK_LEN) { - const size_t i = len / (CRC32_NUM_CHUNKS * - CRC32_MIN_VARIABLE_CHUNK_LEN); - const size_t chunk_len = - i * CRC32_MIN_VARIABLE_CHUNK_LEN; - const u64 *wp0 = (const u64 *)(p + 0*chunk_len); - const u64 *wp1 = (const u64 *)(p + 1*chunk_len); - const u64 *wp2 = (const u64 *)(p + 2*chunk_len); - const u64 *wp3 = (const u64 *)(p + 3*chunk_len); - const u64 * const wp0_end = wp1; - u32 crc1 = 0, crc2 = 0, crc3 = 0; - - STATIC_ASSERT(CRC32_NUM_CHUNKS == 4); - STATIC_ASSERT(CRC32_MIN_VARIABLE_CHUNK_LEN % (4 * 8) == 0); - do { - prefetchr(wp0 + 64); - prefetchr(wp1 + 64); - prefetchr(wp2 + 64); - prefetchr(wp3 + 64); - crc = __crc32d(crc, le64_bswap(*wp0++)); - crc1 = __crc32d(crc1, le64_bswap(*wp1++)); - crc2 = __crc32d(crc2, le64_bswap(*wp2++)); - crc3 = __crc32d(crc3, le64_bswap(*wp3++)); - crc = __crc32d(crc, le64_bswap(*wp0++)); - crc1 = __crc32d(crc1, le64_bswap(*wp1++)); - crc2 = __crc32d(crc2, le64_bswap(*wp2++)); - crc3 = __crc32d(crc3, le64_bswap(*wp3++)); - crc = __crc32d(crc, le64_bswap(*wp0++)); - crc1 = __crc32d(crc1, le64_bswap(*wp1++)); - crc2 = __crc32d(crc2, le64_bswap(*wp2++)); - crc3 = __crc32d(crc3, le64_bswap(*wp3++)); - crc = __crc32d(crc, le64_bswap(*wp0++)); - crc1 = __crc32d(crc1, le64_bswap(*wp1++)); - crc2 = __crc32d(crc2, le64_bswap(*wp2++)); - crc3 = __crc32d(crc3, le64_bswap(*wp3++)); - } while (wp0 != wp0_end); - crc = combine_crcs_fast(crc, crc1, crc2, crc3, i); - p += CRC32_NUM_CHUNKS * chunk_len; - len -= CRC32_NUM_CHUNKS * chunk_len; - } - - while (len >= 32) { - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 0))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 8))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 16))); - crc = __crc32d(crc, le64_bswap(*(u64 *)(p + 24))); - p += 32; - len -= 32; - } - } else { - while (len >= 32) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - crc = __crc32d(crc, get_unaligned_le64(p + 16)); - crc = __crc32d(crc, get_unaligned_le64(p + 24)); - p += 32; - len -= 32; - } - } - if (len & 16) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - p += 16; - } - if (len & 8) { - crc = __crc32d(crc, get_unaligned_le64(p)); - p += 8; - } - if (len & 4) { - crc = __crc32w(crc, get_unaligned_le32(p)); - p += 4; - } - if (len & 2) { - crc = __crc32h(crc, get_unaligned_le16(p)); - p += 2; - } - if (len & 1) - crc = __crc32b(crc, *p); - return crc; -} -#undef ATTRIBUTES -#endif /* crc32_arm_crc_pmullcombine() */ - -/* - * crc32_arm_pmullx4() - implementation using "folding" with pmull instructions - * - * This implementation is intended for CPUs that support pmull instructions but - * not crc32 instructions. - */ -#if HAVE_PMULL_INTRIN -# define crc32_arm_pmullx4 crc32_arm_pmullx4 -# define SUFFIX _pmullx4 -# ifdef __clang__ - /* - * This used to use "crypto", but that stopped working with clang 16. - * Now only "aes" works. "aes" works with older versions too, so use - * that. No "+" prefix; clang 15 and earlier doesn't accept that. - */ -# define ATTRIBUTES _target_attribute("aes") -# else - /* - * With gcc, only "+crypto" works. Both the "+" prefix and the - * "crypto" (not "aes") are essential... - */ -# define ATTRIBUTES _target_attribute("+crypto") -# endif -# define ENABLE_EOR3 0 -# include "crc32_pmull_helpers.h" - -static ATTRIBUTES u32 -crc32_arm_pmullx4(u32 crc, const u8 *p, size_t len) -{ - static const u64 _aligned_attribute(16) mults[3][2] = { - { CRC32_X159_MODG, CRC32_X95_MODG }, /* 1 vecs */ - { CRC32_X543_MODG, CRC32_X479_MODG }, /* 4 vecs */ - { CRC32_X287_MODG, CRC32_X223_MODG }, /* 2 vecs */ - }; - static const u64 _aligned_attribute(16) barrett_consts[3][2] = { - { CRC32_X95_MODG, }, - { CRC32_BARRETT_CONSTANT_1, }, - { CRC32_BARRETT_CONSTANT_2, }, - }; - const poly64x2_t multipliers_1 = load_multipliers(mults[0]); - uint8x16_t v0, v1, v2, v3; - - if (len < 64 + 15) { - if (len < 16) - return crc32_slice1(crc, p, len); - v0 = veorq_u8(vld1q_u8(p), u32_to_bytevec(crc)); - p += 16; - len -= 16; - while (len >= 16) { - v0 = fold_vec(v0, vld1q_u8(p), multipliers_1); - p += 16; - len -= 16; - } - } else { - const poly64x2_t multipliers_4 = load_multipliers(mults[1]); - const poly64x2_t multipliers_2 = load_multipliers(mults[2]); - const size_t align = -(uintptr_t)p & 15; - const uint8x16_t *vp; - - v0 = veorq_u8(vld1q_u8(p), u32_to_bytevec(crc)); - p += 16; - /* Align p to the next 16-byte boundary. */ - if (align) { - v0 = fold_partial_vec(v0, p, align, multipliers_1); - p += align; - len -= align; - } - vp = (const uint8x16_t *)p; - v1 = *vp++; - v2 = *vp++; - v3 = *vp++; - while (len >= 64 + 64) { - v0 = fold_vec(v0, *vp++, multipliers_4); - v1 = fold_vec(v1, *vp++, multipliers_4); - v2 = fold_vec(v2, *vp++, multipliers_4); - v3 = fold_vec(v3, *vp++, multipliers_4); - len -= 64; - } - v0 = fold_vec(v0, v2, multipliers_2); - v1 = fold_vec(v1, v3, multipliers_2); - if (len & 32) { - v0 = fold_vec(v0, *vp++, multipliers_2); - v1 = fold_vec(v1, *vp++, multipliers_2); - } - v0 = fold_vec(v0, v1, multipliers_1); - if (len & 16) - v0 = fold_vec(v0, *vp++, multipliers_1); - p = (const u8 *)vp; - len &= 15; - } - - /* Handle any remaining partial block now before reducing to 32 bits. */ - if (len) - v0 = fold_partial_vec(v0, p, len, multipliers_1); - - /* Reduce to 32 bits, following lib/x86/crc32_pclmul_template.h */ - v0 = veorq_u8(clmul_low(v0, load_multipliers(barrett_consts[0])), - vextq_u8(v0, vdupq_n_u8(0), 8)); - v1 = clmul_low(v0, load_multipliers(barrett_consts[1])); - v1 = clmul_low(v1, load_multipliers(barrett_consts[2])); - v0 = veorq_u8(v0, v1); - return vgetq_lane_u32(vreinterpretq_u32_u8(v0), 2); -} -#undef SUFFIX -#undef ATTRIBUTES -#undef ENABLE_EOR3 -#endif /* crc32_arm_pmullx4() */ - -/* - * crc32_arm_pmullx12_crc() - large-stride implementation using "folding" with - * pmull instructions, where crc32 instructions are also available - * - * See crc32_pmull_wide.h for explanation. - */ -#if HAVE_PMULL_INTRIN && HAVE_CRC32_INTRIN -# define crc32_arm_pmullx12_crc crc32_arm_pmullx12_crc -# define SUFFIX _pmullx12_crc -# ifdef __clang__ -# define ATTRIBUTES _target_attribute("aes,crc") -# else -# define ATTRIBUTES _target_attribute("+crypto,+crc") -# endif -# define ENABLE_EOR3 0 -# include "crc32_pmull_wide.h" -#endif - -/* - * crc32_arm_pmullx12_crc_eor3() - * - * This like crc32_arm_pmullx12_crc(), but it adds the eor3 instruction (from - * the sha3 extension) for even better performance. - */ -#if HAVE_PMULL_INTRIN && HAVE_CRC32_INTRIN && HAVE_SHA3_INTRIN && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_SHA3) -# define crc32_arm_pmullx12_crc_eor3 crc32_arm_pmullx12_crc_eor3 -# define SUFFIX _pmullx12_crc_eor3 -# ifdef __clang__ -# define ATTRIBUTES _target_attribute("aes,crc,sha3") - /* - * Both gcc and binutils originally considered sha3 to depend on - * arch=armv8.2-a or later. This was fixed in gcc 13.2 by commit - * 9aac37ab8a7b ("aarch64: Remove architecture dependencies from intrinsics") - * and in binutils 2.41 by commit 205e4380c800 ("aarch64: Remove version - * dependencies from features"). Unfortunately, always using arch=armv8.2-a - * causes build errors with some compiler options because it may reduce the - * arch rather than increase it. Therefore we try to omit the arch whenever - * possible. If gcc is 14 or later, then both gcc and binutils are probably - * fixed, so we omit the arch. We also omit the arch if a feature that - * depends on armv8.2-a or later (in gcc 13.1 and earlier) is present. - */ -# elif GCC_PREREQ(14, 0) || defined(__ARM_FEATURE_JCVT) \ - || defined(__ARM_FEATURE_DOTPROD) -# define ATTRIBUTES _target_attribute("+crypto,+crc,+sha3") -# else -# define ATTRIBUTES _target_attribute("arch=armv8.2-a+crypto+crc+sha3") -# endif -# define ENABLE_EOR3 1 -# include "crc32_pmull_wide.h" -#endif - -static inline crc32_func_t -arch_select_crc32_func(void) -{ - const u32 features MAYBE_UNUSED = get_arm_cpu_features(); - -#ifdef crc32_arm_pmullx12_crc_eor3 - if ((features & ARM_CPU_FEATURE_PREFER_PMULL) && - HAVE_PMULL(features) && HAVE_CRC32(features) && HAVE_SHA3(features)) - return crc32_arm_pmullx12_crc_eor3; -#endif -#ifdef crc32_arm_pmullx12_crc - if ((features & ARM_CPU_FEATURE_PREFER_PMULL) && - HAVE_PMULL(features) && HAVE_CRC32(features)) - return crc32_arm_pmullx12_crc; -#endif -#ifdef crc32_arm_crc_pmullcombine - if (HAVE_CRC32(features) && HAVE_PMULL(features)) - return crc32_arm_crc_pmullcombine; -#endif -#ifdef crc32_arm_crc - if (HAVE_CRC32(features)) - return crc32_arm_crc; -#endif -#ifdef crc32_arm_pmullx4 - if (HAVE_PMULL(features)) - return crc32_arm_pmullx4; -#endif - return NULL; -} -#define arch_select_crc32_func arch_select_crc32_func - -#endif /* LIB_ARM_CRC32_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/arm/crc32_pmull_helpers.h b/dm/third_party/libdeflate/lib/arm/crc32_pmull_helpers.h deleted file mode 100644 index 023b904..0000000 --- a/dm/third_party/libdeflate/lib/arm/crc32_pmull_helpers.h +++ /dev/null @@ -1,156 +0,0 @@ -/* - * arm/crc32_pmull_helpers.h - helper functions for CRC-32 folding with PMULL - * - * Copyright 2022 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * This file is a "template" for instantiating helper functions for CRC folding - * with pmull instructions. It accepts the following parameters: - * - * SUFFIX: - * Name suffix to append to all instantiated functions. - * ATTRIBUTES: - * Target function attributes to use. - * ENABLE_EOR3: - * Use the eor3 instruction (from the sha3 extension). - */ - -/* Create a vector with 'a' in the first 4 bytes, and the rest zeroed out. */ -#undef u32_to_bytevec -static forceinline ATTRIBUTES uint8x16_t -ADD_SUFFIX(u32_to_bytevec)(u32 a) -{ - return vreinterpretq_u8_u32(vsetq_lane_u32(a, vdupq_n_u32(0), 0)); -} -#define u32_to_bytevec ADD_SUFFIX(u32_to_bytevec) - -/* Load two 64-bit values into a vector. */ -#undef load_multipliers -static forceinline ATTRIBUTES poly64x2_t -ADD_SUFFIX(load_multipliers)(const u64 p[2]) -{ - return vreinterpretq_p64_u64(vld1q_u64(p)); -} -#define load_multipliers ADD_SUFFIX(load_multipliers) - -/* Do carryless multiplication of the low halves of two vectors. */ -#undef clmul_low -static forceinline ATTRIBUTES uint8x16_t -ADD_SUFFIX(clmul_low)(uint8x16_t a, poly64x2_t b) -{ - return vreinterpretq_u8_p128( - compat_vmull_p64(vgetq_lane_p64(vreinterpretq_p64_u8(a), 0), - vgetq_lane_p64(b, 0))); -} -#define clmul_low ADD_SUFFIX(clmul_low) - -/* Do carryless multiplication of the high halves of two vectors. */ -#undef clmul_high -static forceinline ATTRIBUTES uint8x16_t -ADD_SUFFIX(clmul_high)(uint8x16_t a, poly64x2_t b) -{ -#ifdef __clang__ - /* - * Use inline asm to ensure that pmull2 is really used. This works - * around clang bug https://github.com/llvm/llvm-project/issues/52868. - */ - uint8x16_t res; - - __asm__("pmull2 %0.1q, %1.2d, %2.2d" : "=w" (res) : "w" (a), "w" (b)); - return res; -#else - return vreinterpretq_u8_p128(vmull_high_p64(vreinterpretq_p64_u8(a), b)); -#endif -} -#define clmul_high ADD_SUFFIX(clmul_high) - -#undef eor3 -static forceinline ATTRIBUTES uint8x16_t -ADD_SUFFIX(eor3)(uint8x16_t a, uint8x16_t b, uint8x16_t c) -{ -#if ENABLE_EOR3 - return veor3q_u8(a, b, c); -#else - return veorq_u8(veorq_u8(a, b), c); -#endif -} -#define eor3 ADD_SUFFIX(eor3) - -#undef fold_vec -static forceinline ATTRIBUTES uint8x16_t -ADD_SUFFIX(fold_vec)(uint8x16_t src, uint8x16_t dst, poly64x2_t multipliers) -{ - uint8x16_t a = clmul_low(src, multipliers); - uint8x16_t b = clmul_high(src, multipliers); - - return eor3(a, b, dst); -} -#define fold_vec ADD_SUFFIX(fold_vec) - -/* - * Given v containing a 16-byte polynomial, and a pointer 'p' that points to the - * next '1 <= len <= 15' data bytes, rearrange the concatenation of v and the - * data into vectors x0 and x1 that contain 'len' bytes and 16 bytes, - * respectively. Then fold x0 into x1 and return the result. Assumes that - * 'p + len - 16' is in-bounds. - */ -#undef fold_partial_vec -static forceinline ATTRIBUTES MAYBE_UNUSED uint8x16_t -ADD_SUFFIX(fold_partial_vec)(uint8x16_t v, const u8 *p, size_t len, - poly64x2_t multipliers_1) -{ - /* - * vqtbl1q_u8(v, shift_tab[len..len+15]) left shifts v by 16-len bytes. - * vqtbl1q_u8(v, shift_tab[len+16..len+31]) right shifts v by len bytes. - */ - static const u8 shift_tab[48] = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - }; - const uint8x16_t lshift = vld1q_u8(&shift_tab[len]); - const uint8x16_t rshift = vld1q_u8(&shift_tab[len + 16]); - uint8x16_t x0, x1, bsl_mask; - - /* x0 = v left-shifted by '16 - len' bytes */ - x0 = vqtbl1q_u8(v, lshift); - - /* Create a vector of '16 - len' 0x00 bytes, then 'len' 0xff bytes. */ - bsl_mask = vreinterpretq_u8_s8( - vshrq_n_s8(vreinterpretq_s8_u8(rshift), 7)); - - /* - * x1 = the last '16 - len' bytes from v (i.e. v right-shifted by 'len' - * bytes) followed by the remaining data. - */ - x1 = vbslq_u8(bsl_mask /* 0 bits select from arg3, 1 bits from arg2 */, - vld1q_u8(p + len - 16), vqtbl1q_u8(v, rshift)); - - return fold_vec(x0, x1, multipliers_1); -} -#define fold_partial_vec ADD_SUFFIX(fold_partial_vec) diff --git a/dm/third_party/libdeflate/lib/arm/crc32_pmull_wide.h b/dm/third_party/libdeflate/lib/arm/crc32_pmull_wide.h deleted file mode 100644 index 300e282..0000000 --- a/dm/third_party/libdeflate/lib/arm/crc32_pmull_wide.h +++ /dev/null @@ -1,226 +0,0 @@ -/* - * arm/crc32_pmull_wide.h - gzip CRC-32 with PMULL (extra-wide version) - * - * Copyright 2022 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * This file is a "template" for instantiating PMULL-based crc32_arm functions. - * The "parameters" are: - * - * SUFFIX: - * Name suffix to append to all instantiated functions. - * ATTRIBUTES: - * Target function attributes to use. - * ENABLE_EOR3: - * Use the eor3 instruction (from the sha3 extension). - * - * This is the extra-wide version; it uses an unusually large stride length of - * 12, and it assumes that crc32 instructions are available too. It's intended - * for powerful CPUs that support both pmull and crc32 instructions, but where - * throughput of pmull and xor (given enough instructions issued in parallel) is - * significantly higher than that of crc32, thus making the crc32 instructions - * (counterintuitively) not actually the fastest way to compute the CRC-32. The - * Apple M1 processor is an example of such a CPU. - */ - -#include "crc32_pmull_helpers.h" - -static ATTRIBUTES u32 -ADD_SUFFIX(crc32_arm)(u32 crc, const u8 *p, size_t len) -{ - uint8x16_t v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11; - - if (len < 3 * 192) { - static const u64 _aligned_attribute(16) mults[3][2] = { - { CRC32_X543_MODG, CRC32_X479_MODG }, /* 4 vecs */ - { CRC32_X287_MODG, CRC32_X223_MODG }, /* 2 vecs */ - { CRC32_X159_MODG, CRC32_X95_MODG }, /* 1 vecs */ - }; - poly64x2_t multipliers_4, multipliers_2, multipliers_1; - - if (len < 64) - goto tail; - multipliers_4 = load_multipliers(mults[0]); - multipliers_2 = load_multipliers(mults[1]); - multipliers_1 = load_multipliers(mults[2]); - /* - * Short length; don't bother aligning the pointer, and fold - * 64 bytes (4 vectors) at a time, at most. - */ - v0 = veorq_u8(vld1q_u8(p + 0), u32_to_bytevec(crc)); - v1 = vld1q_u8(p + 16); - v2 = vld1q_u8(p + 32); - v3 = vld1q_u8(p + 48); - p += 64; - len -= 64; - while (len >= 64) { - v0 = fold_vec(v0, vld1q_u8(p + 0), multipliers_4); - v1 = fold_vec(v1, vld1q_u8(p + 16), multipliers_4); - v2 = fold_vec(v2, vld1q_u8(p + 32), multipliers_4); - v3 = fold_vec(v3, vld1q_u8(p + 48), multipliers_4); - p += 64; - len -= 64; - } - v0 = fold_vec(v0, v2, multipliers_2); - v1 = fold_vec(v1, v3, multipliers_2); - if (len >= 32) { - v0 = fold_vec(v0, vld1q_u8(p + 0), multipliers_2); - v1 = fold_vec(v1, vld1q_u8(p + 16), multipliers_2); - p += 32; - len -= 32; - } - v0 = fold_vec(v0, v1, multipliers_1); - } else { - static const u64 _aligned_attribute(16) mults[4][2] = { - { CRC32_X1567_MODG, CRC32_X1503_MODG }, /* 12 vecs */ - { CRC32_X799_MODG, CRC32_X735_MODG }, /* 6 vecs */ - { CRC32_X415_MODG, CRC32_X351_MODG }, /* 3 vecs */ - { CRC32_X159_MODG, CRC32_X95_MODG }, /* 1 vecs */ - }; - const poly64x2_t multipliers_12 = load_multipliers(mults[0]); - const poly64x2_t multipliers_6 = load_multipliers(mults[1]); - const poly64x2_t multipliers_3 = load_multipliers(mults[2]); - const poly64x2_t multipliers_1 = load_multipliers(mults[3]); - const size_t align = -(uintptr_t)p & 15; - const uint8x16_t *vp; - - /* Align p to the next 16-byte boundary. */ - if (align) { - if (align & 1) - crc = __crc32b(crc, *p++); - if (align & 2) { - crc = __crc32h(crc, le16_bswap(*(u16 *)p)); - p += 2; - } - if (align & 4) { - crc = __crc32w(crc, le32_bswap(*(u32 *)p)); - p += 4; - } - if (align & 8) { - crc = __crc32d(crc, le64_bswap(*(u64 *)p)); - p += 8; - } - len -= align; - } - vp = (const uint8x16_t *)p; - v0 = veorq_u8(*vp++, u32_to_bytevec(crc)); - v1 = *vp++; - v2 = *vp++; - v3 = *vp++; - v4 = *vp++; - v5 = *vp++; - v6 = *vp++; - v7 = *vp++; - v8 = *vp++; - v9 = *vp++; - v10 = *vp++; - v11 = *vp++; - len -= 192; - /* Fold 192 bytes (12 vectors) at a time. */ - do { - v0 = fold_vec(v0, *vp++, multipliers_12); - v1 = fold_vec(v1, *vp++, multipliers_12); - v2 = fold_vec(v2, *vp++, multipliers_12); - v3 = fold_vec(v3, *vp++, multipliers_12); - v4 = fold_vec(v4, *vp++, multipliers_12); - v5 = fold_vec(v5, *vp++, multipliers_12); - v6 = fold_vec(v6, *vp++, multipliers_12); - v7 = fold_vec(v7, *vp++, multipliers_12); - v8 = fold_vec(v8, *vp++, multipliers_12); - v9 = fold_vec(v9, *vp++, multipliers_12); - v10 = fold_vec(v10, *vp++, multipliers_12); - v11 = fold_vec(v11, *vp++, multipliers_12); - len -= 192; - } while (len >= 192); - - /* - * Fewer than 192 bytes left. Fold v0-v11 down to just v0, - * while processing up to 144 more bytes. - */ - v0 = fold_vec(v0, v6, multipliers_6); - v1 = fold_vec(v1, v7, multipliers_6); - v2 = fold_vec(v2, v8, multipliers_6); - v3 = fold_vec(v3, v9, multipliers_6); - v4 = fold_vec(v4, v10, multipliers_6); - v5 = fold_vec(v5, v11, multipliers_6); - if (len >= 96) { - v0 = fold_vec(v0, *vp++, multipliers_6); - v1 = fold_vec(v1, *vp++, multipliers_6); - v2 = fold_vec(v2, *vp++, multipliers_6); - v3 = fold_vec(v3, *vp++, multipliers_6); - v4 = fold_vec(v4, *vp++, multipliers_6); - v5 = fold_vec(v5, *vp++, multipliers_6); - len -= 96; - } - v0 = fold_vec(v0, v3, multipliers_3); - v1 = fold_vec(v1, v4, multipliers_3); - v2 = fold_vec(v2, v5, multipliers_3); - if (len >= 48) { - v0 = fold_vec(v0, *vp++, multipliers_3); - v1 = fold_vec(v1, *vp++, multipliers_3); - v2 = fold_vec(v2, *vp++, multipliers_3); - len -= 48; - } - v0 = fold_vec(v0, v1, multipliers_1); - v0 = fold_vec(v0, v2, multipliers_1); - p = (const u8 *)vp; - } - /* Reduce 128 to 32 bits using crc32 instructions. */ - crc = __crc32d(0, vgetq_lane_u64(vreinterpretq_u64_u8(v0), 0)); - crc = __crc32d(crc, vgetq_lane_u64(vreinterpretq_u64_u8(v0), 1)); -tail: - /* Finish up the remainder using crc32 instructions. */ - if (len & 32) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - crc = __crc32d(crc, get_unaligned_le64(p + 16)); - crc = __crc32d(crc, get_unaligned_le64(p + 24)); - p += 32; - } - if (len & 16) { - crc = __crc32d(crc, get_unaligned_le64(p + 0)); - crc = __crc32d(crc, get_unaligned_le64(p + 8)); - p += 16; - } - if (len & 8) { - crc = __crc32d(crc, get_unaligned_le64(p)); - p += 8; - } - if (len & 4) { - crc = __crc32w(crc, get_unaligned_le32(p)); - p += 4; - } - if (len & 2) { - crc = __crc32h(crc, get_unaligned_le16(p)); - p += 2; - } - if (len & 1) - crc = __crc32b(crc, *p); - return crc; -} - -#undef SUFFIX -#undef ATTRIBUTES -#undef ENABLE_EOR3 diff --git a/dm/third_party/libdeflate/lib/arm/matchfinder_impl.h b/dm/third_party/libdeflate/lib/arm/matchfinder_impl.h deleted file mode 100644 index 79c1dbc..0000000 --- a/dm/third_party/libdeflate/lib/arm/matchfinder_impl.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * arm/matchfinder_impl.h - ARM implementations of matchfinder functions - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_ARM_MATCHFINDER_IMPL_H -#define LIB_ARM_MATCHFINDER_IMPL_H - -#include "cpu_features.h" - -#if HAVE_NEON_NATIVE -static forceinline void -matchfinder_init_neon(mf_pos_t *data, size_t size) -{ - int16x8_t *p = (int16x8_t *)data; - int16x8_t v = vdupq_n_s16(MATCHFINDER_INITVAL); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - p[0] = v; - p[1] = v; - p[2] = v; - p[3] = v; - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_init matchfinder_init_neon - -static forceinline void -matchfinder_rebase_neon(mf_pos_t *data, size_t size) -{ - int16x8_t *p = (int16x8_t *)data; - int16x8_t v = vdupq_n_s16((u16)-MATCHFINDER_WINDOW_SIZE); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - p[0] = vqaddq_s16(p[0], v); - p[1] = vqaddq_s16(p[1], v); - p[2] = vqaddq_s16(p[2], v); - p[3] = vqaddq_s16(p[3], v); - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_rebase matchfinder_rebase_neon - -#endif /* HAVE_NEON_NATIVE */ - -#endif /* LIB_ARM_MATCHFINDER_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/bt_matchfinder.h b/dm/third_party/libdeflate/lib/bt_matchfinder.h deleted file mode 100644 index b247d4b..0000000 --- a/dm/third_party/libdeflate/lib/bt_matchfinder.h +++ /dev/null @@ -1,342 +0,0 @@ -/* - * bt_matchfinder.h - Lempel-Ziv matchfinding with a hash table of binary trees - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * ---------------------------------------------------------------------------- - * - * This is a Binary Trees (bt) based matchfinder. - * - * The main data structure is a hash table where each hash bucket contains a - * binary tree of sequences whose first 4 bytes share the same hash code. Each - * sequence is identified by its starting position in the input buffer. Each - * binary tree is always sorted such that each left child represents a sequence - * lexicographically lesser than its parent and each right child represents a - * sequence lexicographically greater than its parent. - * - * The algorithm processes the input buffer sequentially. At each byte - * position, the hash code of the first 4 bytes of the sequence beginning at - * that position (the sequence being matched against) is computed. This - * identifies the hash bucket to use for that position. Then, a new binary tree - * node is created to represent the current sequence. Then, in a single tree - * traversal, the hash bucket's binary tree is searched for matches and is - * re-rooted at the new node. - * - * Compared to the simpler algorithm that uses linked lists instead of binary - * trees (see hc_matchfinder.h), the binary tree version gains more information - * at each node visitation. Ideally, the binary tree version will examine only - * 'log(n)' nodes to find the same matches that the linked list version will - * find by examining 'n' nodes. In addition, the binary tree version can - * examine fewer bytes at each node by taking advantage of the common prefixes - * that result from the sort order, whereas the linked list version may have to - * examine up to the full length of the match at each node. - * - * However, it is not always best to use the binary tree version. It requires - * nearly twice as much memory as the linked list version, and it takes time to - * keep the binary trees sorted, even at positions where the compressor does not - * need matches. Generally, when doing fast compression on small buffers, - * binary trees are the wrong approach. They are best suited for thorough - * compression and/or large buffers. - * - * ---------------------------------------------------------------------------- - */ - -#ifndef LIB_BT_MATCHFINDER_H -#define LIB_BT_MATCHFINDER_H - -#include "matchfinder_common.h" - -#define BT_MATCHFINDER_HASH3_ORDER 16 -#define BT_MATCHFINDER_HASH3_WAYS 2 -#define BT_MATCHFINDER_HASH4_ORDER 16 - -#define BT_MATCHFINDER_TOTAL_HASH_SIZE \ - (((1UL << BT_MATCHFINDER_HASH3_ORDER) * BT_MATCHFINDER_HASH3_WAYS + \ - (1UL << BT_MATCHFINDER_HASH4_ORDER)) * sizeof(mf_pos_t)) - -/* Representation of a match found by the bt_matchfinder */ -struct lz_match { - - /* The number of bytes matched. */ - u16 length; - - /* The offset back from the current position that was matched. */ - u16 offset; -}; - -struct MATCHFINDER_ALIGNED bt_matchfinder { - - /* The hash table for finding length 3 matches */ - mf_pos_t hash3_tab[1UL << BT_MATCHFINDER_HASH3_ORDER][BT_MATCHFINDER_HASH3_WAYS]; - - /* The hash table which contains the roots of the binary trees for - * finding length 4+ matches */ - mf_pos_t hash4_tab[1UL << BT_MATCHFINDER_HASH4_ORDER]; - - /* The child node references for the binary trees. The left and right - * children of the node for the sequence with position 'pos' are - * 'child_tab[pos * 2]' and 'child_tab[pos * 2 + 1]', respectively. */ - mf_pos_t child_tab[2UL * MATCHFINDER_WINDOW_SIZE]; -}; - -/* Prepare the matchfinder for a new input buffer. */ -static forceinline void -bt_matchfinder_init(struct bt_matchfinder *mf) -{ - STATIC_ASSERT(BT_MATCHFINDER_TOTAL_HASH_SIZE % - MATCHFINDER_SIZE_ALIGNMENT == 0); - - matchfinder_init((mf_pos_t *)mf, BT_MATCHFINDER_TOTAL_HASH_SIZE); -} - -static forceinline void -bt_matchfinder_slide_window(struct bt_matchfinder *mf) -{ - STATIC_ASSERT(sizeof(*mf) % MATCHFINDER_SIZE_ALIGNMENT == 0); - - matchfinder_rebase((mf_pos_t *)mf, sizeof(*mf)); -} - -static forceinline mf_pos_t * -bt_left_child(struct bt_matchfinder *mf, s32 node) -{ - return &mf->child_tab[2 * (node & (MATCHFINDER_WINDOW_SIZE - 1)) + 0]; -} - -static forceinline mf_pos_t * -bt_right_child(struct bt_matchfinder *mf, s32 node) -{ - return &mf->child_tab[2 * (node & (MATCHFINDER_WINDOW_SIZE - 1)) + 1]; -} - -/* The minimum permissible value of 'max_len' for bt_matchfinder_get_matches() - * and bt_matchfinder_skip_byte(). There must be sufficiently many bytes - * remaining to load a 32-bit integer from the *next* position. */ -#define BT_MATCHFINDER_REQUIRED_NBYTES 5 - -/* Advance the binary tree matchfinder by one byte, optionally recording - * matches. @record_matches should be a compile-time constant. */ -static forceinline struct lz_match * -bt_matchfinder_advance_one_byte(struct bt_matchfinder * const mf, - const u8 * const in_base, - const ptrdiff_t cur_pos, - const u32 max_len, - const u32 nice_len, - const u32 max_search_depth, - u32 * const next_hashes, - struct lz_match *lz_matchptr, - const bool record_matches) -{ - const u8 *in_next = in_base + cur_pos; - u32 depth_remaining = max_search_depth; - const s32 cutoff = cur_pos - MATCHFINDER_WINDOW_SIZE; - u32 next_hashseq; - u32 hash3; - u32 hash4; - s32 cur_node; -#if BT_MATCHFINDER_HASH3_WAYS >= 2 - s32 cur_node_2; -#endif - const u8 *matchptr; - mf_pos_t *pending_lt_ptr, *pending_gt_ptr; - u32 best_lt_len, best_gt_len; - u32 len; - u32 best_len = 3; - - STATIC_ASSERT(BT_MATCHFINDER_HASH3_WAYS >= 1 && - BT_MATCHFINDER_HASH3_WAYS <= 2); - - next_hashseq = get_unaligned_le32(in_next + 1); - - hash3 = next_hashes[0]; - hash4 = next_hashes[1]; - - next_hashes[0] = lz_hash(next_hashseq & 0xFFFFFF, BT_MATCHFINDER_HASH3_ORDER); - next_hashes[1] = lz_hash(next_hashseq, BT_MATCHFINDER_HASH4_ORDER); - prefetchw(&mf->hash3_tab[next_hashes[0]]); - prefetchw(&mf->hash4_tab[next_hashes[1]]); - - cur_node = mf->hash3_tab[hash3][0]; - mf->hash3_tab[hash3][0] = cur_pos; -#if BT_MATCHFINDER_HASH3_WAYS >= 2 - cur_node_2 = mf->hash3_tab[hash3][1]; - mf->hash3_tab[hash3][1] = cur_node; -#endif - if (record_matches && cur_node > cutoff) { - u32 seq3 = load_u24_unaligned(in_next); - if (seq3 == load_u24_unaligned(&in_base[cur_node])) { - lz_matchptr->length = 3; - lz_matchptr->offset = in_next - &in_base[cur_node]; - lz_matchptr++; - } - #if BT_MATCHFINDER_HASH3_WAYS >= 2 - else if (cur_node_2 > cutoff && - seq3 == load_u24_unaligned(&in_base[cur_node_2])) - { - lz_matchptr->length = 3; - lz_matchptr->offset = in_next - &in_base[cur_node_2]; - lz_matchptr++; - } - #endif - } - - cur_node = mf->hash4_tab[hash4]; - mf->hash4_tab[hash4] = cur_pos; - - pending_lt_ptr = bt_left_child(mf, cur_pos); - pending_gt_ptr = bt_right_child(mf, cur_pos); - - if (cur_node <= cutoff) { - *pending_lt_ptr = MATCHFINDER_INITVAL; - *pending_gt_ptr = MATCHFINDER_INITVAL; - return lz_matchptr; - } - - best_lt_len = 0; - best_gt_len = 0; - len = 0; - - for (;;) { - matchptr = &in_base[cur_node]; - - if (matchptr[len] == in_next[len]) { - len = lz_extend(in_next, matchptr, len + 1, max_len); - if (!record_matches || len > best_len) { - if (record_matches) { - best_len = len; - lz_matchptr->length = len; - lz_matchptr->offset = in_next - matchptr; - lz_matchptr++; - } - if (len >= nice_len) { - *pending_lt_ptr = *bt_left_child(mf, cur_node); - *pending_gt_ptr = *bt_right_child(mf, cur_node); - return lz_matchptr; - } - } - } - - if (matchptr[len] < in_next[len]) { - *pending_lt_ptr = cur_node; - pending_lt_ptr = bt_right_child(mf, cur_node); - cur_node = *pending_lt_ptr; - best_lt_len = len; - if (best_gt_len < len) - len = best_gt_len; - } else { - *pending_gt_ptr = cur_node; - pending_gt_ptr = bt_left_child(mf, cur_node); - cur_node = *pending_gt_ptr; - best_gt_len = len; - if (best_lt_len < len) - len = best_lt_len; - } - - if (cur_node <= cutoff || !--depth_remaining) { - *pending_lt_ptr = MATCHFINDER_INITVAL; - *pending_gt_ptr = MATCHFINDER_INITVAL; - return lz_matchptr; - } - } -} - -/* - * Retrieve a list of matches with the current position. - * - * @mf - * The matchfinder structure. - * @in_base - * Pointer to the next byte in the input buffer to process _at the last - * time bt_matchfinder_init() or bt_matchfinder_slide_window() was called_. - * @cur_pos - * The current position in the input buffer relative to @in_base (the - * position of the sequence being matched against). - * @max_len - * The maximum permissible match length at this position. Must be >= - * BT_MATCHFINDER_REQUIRED_NBYTES. - * @nice_len - * Stop searching if a match of at least this length is found. - * Must be <= @max_len. - * @max_search_depth - * Limit on the number of potential matches to consider. Must be >= 1. - * @next_hashes - * The precomputed hash codes for the sequence beginning at @in_next. - * These will be used and then updated with the precomputed hashcodes for - * the sequence beginning at @in_next + 1. - * @lz_matchptr - * An array in which this function will record the matches. The recorded - * matches will be sorted by strictly increasing length and (non-strictly) - * increasing offset. The maximum number of matches that may be found is - * 'nice_len - 2'. - * - * The return value is a pointer to the next available slot in the @lz_matchptr - * array. (If no matches were found, this will be the same as @lz_matchptr.) - */ -static forceinline struct lz_match * -bt_matchfinder_get_matches(struct bt_matchfinder *mf, - const u8 *in_base, - ptrdiff_t cur_pos, - u32 max_len, - u32 nice_len, - u32 max_search_depth, - u32 next_hashes[2], - struct lz_match *lz_matchptr) -{ - return bt_matchfinder_advance_one_byte(mf, - in_base, - cur_pos, - max_len, - nice_len, - max_search_depth, - next_hashes, - lz_matchptr, - true); -} - -/* - * Advance the matchfinder, but don't record any matches. - * - * This is very similar to bt_matchfinder_get_matches() because both functions - * must do hashing and tree re-rooting. - */ -static forceinline void -bt_matchfinder_skip_byte(struct bt_matchfinder *mf, - const u8 *in_base, - ptrdiff_t cur_pos, - u32 nice_len, - u32 max_search_depth, - u32 next_hashes[2]) -{ - bt_matchfinder_advance_one_byte(mf, - in_base, - cur_pos, - nice_len, - nice_len, - max_search_depth, - next_hashes, - NULL, - false); -} - -#endif /* LIB_BT_MATCHFINDER_H */ diff --git a/dm/third_party/libdeflate/lib/cpu_features_common.h b/dm/third_party/libdeflate/lib/cpu_features_common.h deleted file mode 100644 index d0531d5..0000000 --- a/dm/third_party/libdeflate/lib/cpu_features_common.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * cpu_features_common.h - code shared by all lib/$arch/cpu_features.c - * - * Copyright 2020 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_CPU_FEATURES_COMMON_H -#define LIB_CPU_FEATURES_COMMON_H - -#if defined(TEST_SUPPORT__DO_NOT_USE) && !defined(FREESTANDING) - /* for strdup() and strtok_r() */ -# undef _ANSI_SOURCE -# ifndef __APPLE__ -# undef _GNU_SOURCE -# define _GNU_SOURCE -# endif -# include <stdio.h> -# include <stdlib.h> -# include <string.h> -#endif - -#include "lib_common.h" - -struct cpu_feature { - u32 bit; - const char *name; -}; - -#if defined(TEST_SUPPORT__DO_NOT_USE) && !defined(FREESTANDING) -/* Disable any features that are listed in $LIBDEFLATE_DISABLE_CPU_FEATURES. */ -static inline void -disable_cpu_features_for_testing(u32 *features, - const struct cpu_feature *feature_table, - size_t feature_table_length) -{ - char *env_value, *strbuf, *p, *saveptr = NULL; - size_t i; - - env_value = getenv("LIBDEFLATE_DISABLE_CPU_FEATURES"); - if (!env_value) - return; - strbuf = strdup(env_value); - if (!strbuf) - abort(); - p = strtok_r(strbuf, ",", &saveptr); - while (p) { - for (i = 0; i < feature_table_length; i++) { - if (strcmp(p, feature_table[i].name) == 0) { - *features &= ~feature_table[i].bit; - break; - } - } - if (i == feature_table_length) { - fprintf(stderr, - "unrecognized feature in LIBDEFLATE_DISABLE_CPU_FEATURES: \"%s\"\n", - p); - abort(); - } - p = strtok_r(NULL, ",", &saveptr); - } - free(strbuf); -} -#else /* TEST_SUPPORT__DO_NOT_USE */ -static inline void -disable_cpu_features_for_testing(u32 *features, - const struct cpu_feature *feature_table, - size_t feature_table_length) -{ -} -#endif /* !TEST_SUPPORT__DO_NOT_USE */ - -#endif /* LIB_CPU_FEATURES_COMMON_H */ diff --git a/dm/third_party/libdeflate/lib/crc32.c b/dm/third_party/libdeflate/lib/crc32.c deleted file mode 100644 index a0ec022..0000000 --- a/dm/third_party/libdeflate/lib/crc32.c +++ /dev/null @@ -1,262 +0,0 @@ -/* - * crc32.c - CRC-32 checksum algorithm for the gzip format - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * High-level description of CRC - * ============================= - * - * Consider a bit sequence 'bits[1...len]'. Interpret 'bits' as the "message" - * polynomial M(x) with coefficients in GF(2) (the field of integers modulo 2), - * where the coefficient of 'x^i' is 'bits[len - i]'. Then, compute: - * - * R(x) = M(x)*x^n mod G(x) - * - * where G(x) is a selected "generator" polynomial of degree 'n'. The remainder - * R(x) is a polynomial of max degree 'n - 1'. The CRC of 'bits' is R(x) - * interpreted as a bitstring of length 'n'. - * - * CRC used in gzip - * ================ - * - * In the gzip format (RFC 1952): - * - * - The bitstring to checksum is formed from the bytes of the uncompressed - * data by concatenating the bits from the bytes in order, proceeding - * from the low-order bit to the high-order bit within each byte. - * - * - The generator polynomial G(x) is: x^32 + x^26 + x^23 + x^22 + x^16 + - * x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1. - * Consequently, the CRC length is 32 bits ("CRC-32"). - * - * - The highest order 32 coefficients of M(x)*x^n are inverted. - * - * - All 32 coefficients of R(x) are inverted. - * - * The two inversions cause added leading and trailing zero bits to affect the - * resulting CRC, whereas with a regular CRC such bits would have no effect on - * the CRC. - * - * Computation and optimizations - * ============================= - * - * We can compute R(x) through "long division", maintaining only 32 bits of - * state at any given time. Multiplication by 'x' can be implemented as - * right-shifting by 1 (assuming the polynomial<=>bitstring mapping where the - * highest order bit represents the coefficient of x^0), and both addition and - * subtraction can be implemented as bitwise exclusive OR (since we are working - * in GF(2)). Here is an unoptimized implementation: - * - * static u32 crc32_gzip(const u8 *p, size_t len) - * { - * u32 crc = 0; - * const u32 divisor = 0xEDB88320; - * - * for (size_t i = 0; i < len * 8 + 32; i++) { - * int bit; - * u32 multiple; - * - * if (i < len * 8) - * bit = (p[i / 8] >> (i % 8)) & 1; - * else - * bit = 0; // one of the 32 appended 0 bits - * - * if (i < 32) // the first 32 bits are inverted - * bit ^= 1; - * - * if (crc & 1) - * multiple = divisor; - * else - * multiple = 0; - * - * crc >>= 1; - * crc |= (u32)bit << 31; - * crc ^= multiple; - * } - * - * return ~crc; - * } - * - * In this implementation, the 32-bit integer 'crc' maintains the remainder of - * the currently processed portion of the message (with 32 zero bits appended) - * when divided by the generator polynomial. 'crc' is the representation of - * R(x), and 'divisor' is the representation of G(x) excluding the x^32 - * coefficient. For each bit to process, we multiply R(x) by 'x^1', then add - * 'x^0' if the new bit is a 1. If this causes R(x) to gain a nonzero x^32 - * term, then we subtract G(x) from R(x). - * - * We can speed this up by taking advantage of the fact that XOR is commutative - * and associative, so the order in which we combine the inputs into 'crc' is - * unimportant. And since each message bit we add doesn't affect the choice of - * 'multiple' until 32 bits later, we need not actually add each message bit - * until that point: - * - * static u32 crc32_gzip(const u8 *p, size_t len) - * { - * u32 crc = ~0; - * const u32 divisor = 0xEDB88320; - * - * for (size_t i = 0; i < len * 8; i++) { - * int bit; - * u32 multiple; - * - * bit = (p[i / 8] >> (i % 8)) & 1; - * crc ^= bit; - * if (crc & 1) - * multiple = divisor; - * else - * multiple = 0; - * crc >>= 1; - * crc ^= multiple; - * } - * - * return ~crc; - * } - * - * With the above implementation we get the effect of 32 appended 0 bits for - * free; they never affect the choice of a divisor, nor would they change the - * value of 'crc' if they were to be actually XOR'ed in. And by starting with a - * remainder of all 1 bits, we get the effect of complementing the first 32 - * message bits. - * - * The next optimization is to process the input in multi-bit units. Suppose - * that we insert the next 'n' message bits into the remainder. Then we get an - * intermediate remainder of length '32 + n' bits, and the CRC of the extra 'n' - * bits is the amount by which the low 32 bits of the remainder will change as a - * result of cancelling out those 'n' bits. Taking n=8 (one byte) and - * precomputing a table containing the CRC of each possible byte, we get - * crc32_slice1() defined below. - * - * As a further optimization, we could increase the multi-bit unit size to 16. - * However, that is inefficient because the table size explodes from 256 entries - * (1024 bytes) to 65536 entries (262144 bytes), which wastes memory and won't - * fit in L1 cache on typical processors. - * - * However, we can actually process 4 bytes at a time using 4 different tables - * with 256 entries each. Logically, we form a 64-bit intermediate remainder - * and cancel out the high 32 bits in 8-bit chunks. Bits 32-39 are cancelled - * out by the CRC of those bits, whereas bits 40-47 are be cancelled out by the - * CRC of those bits with 8 zero bits appended, and so on. - * - * In crc32_slice8(), this method is extended to 8 bytes at a time. The - * intermediate remainder (which we never actually store explicitly) is 96 bits. - * - * On CPUs that support fast carryless multiplication, CRCs can be computed even - * more quickly via "folding". See e.g. the x86 PCLMUL implementations. - */ - -#include "lib_common.h" -#include "crc32_multipliers.h" -#include "crc32_tables.h" - -/* This is the default implementation. It uses the slice-by-8 method. */ -static u32 MAYBE_UNUSED -crc32_slice8(u32 crc, const u8 *p, size_t len) -{ - const u8 * const end = p + len; - const u8 *end64; - - for (; ((uintptr_t)p & 7) && p != end; p++) - crc = (crc >> 8) ^ crc32_slice8_table[(u8)crc ^ *p]; - - end64 = p + ((end - p) & ~7); - for (; p != end64; p += 8) { - u32 v1 = le32_bswap(*(const u32 *)(p + 0)); - u32 v2 = le32_bswap(*(const u32 *)(p + 4)); - - crc = crc32_slice8_table[0x700 + (u8)((crc ^ v1) >> 0)] ^ - crc32_slice8_table[0x600 + (u8)((crc ^ v1) >> 8)] ^ - crc32_slice8_table[0x500 + (u8)((crc ^ v1) >> 16)] ^ - crc32_slice8_table[0x400 + (u8)((crc ^ v1) >> 24)] ^ - crc32_slice8_table[0x300 + (u8)(v2 >> 0)] ^ - crc32_slice8_table[0x200 + (u8)(v2 >> 8)] ^ - crc32_slice8_table[0x100 + (u8)(v2 >> 16)] ^ - crc32_slice8_table[0x000 + (u8)(v2 >> 24)]; - } - - for (; p != end; p++) - crc = (crc >> 8) ^ crc32_slice8_table[(u8)crc ^ *p]; - - return crc; -} - -/* - * This is a more lightweight generic implementation, which can be used as a - * subroutine by architecture-specific implementations to process small amounts - * of unaligned data at the beginning and/or end of the buffer. - */ -static forceinline u32 MAYBE_UNUSED -crc32_slice1(u32 crc, const u8 *p, size_t len) -{ - size_t i; - - for (i = 0; i < len; i++) - crc = (crc >> 8) ^ crc32_slice1_table[(u8)crc ^ p[i]]; - return crc; -} - -/* Include architecture-specific implementation(s) if available. */ -#undef DEFAULT_IMPL -#undef arch_select_crc32_func -typedef u32 (*crc32_func_t)(u32 crc, const u8 *p, size_t len); -#if defined(ARCH_ARM32) || defined(ARCH_ARM64) -# include "arm/crc32_impl.h" -#elif defined(ARCH_X86_32) || defined(ARCH_X86_64) -# include "x86/crc32_impl.h" -#endif - -#ifndef DEFAULT_IMPL -# define DEFAULT_IMPL crc32_slice8 -#endif - -#ifdef arch_select_crc32_func -static u32 dispatch_crc32(u32 crc, const u8 *p, size_t len); - -static volatile crc32_func_t crc32_impl = dispatch_crc32; - -/* Choose the best implementation at runtime. */ -static u32 dispatch_crc32(u32 crc, const u8 *p, size_t len) -{ - crc32_func_t f = arch_select_crc32_func(); - - if (f == NULL) - f = DEFAULT_IMPL; - - crc32_impl = f; - return f(crc, p, len); -} -#else -/* The best implementation is statically known, so call it directly. */ -#define crc32_impl DEFAULT_IMPL -#endif - -LIBDEFLATEAPI u32 -libdeflate_crc32(u32 crc, const void *p, size_t len) -{ - if (p == NULL) /* Return initial value. */ - return 0; - return ~crc32_impl(~crc, p, len); -} diff --git a/dm/third_party/libdeflate/lib/crc32_multipliers.h b/dm/third_party/libdeflate/lib/crc32_multipliers.h deleted file mode 100644 index 9a08745..0000000 --- a/dm/third_party/libdeflate/lib/crc32_multipliers.h +++ /dev/null @@ -1,375 +0,0 @@ -/* - * crc32_multipliers.h - constants for CRC-32 folding - * - * THIS FILE WAS GENERATED BY gen-crc32-consts.py. DO NOT EDIT. - */ - -#define CRC32_X159_MODG 0xae689191 /* x^159 mod G(x) */ -#define CRC32_X95_MODG 0xccaa009e /* x^95 mod G(x) */ - -#define CRC32_X287_MODG 0xf1da05aa /* x^287 mod G(x) */ -#define CRC32_X223_MODG 0x81256527 /* x^223 mod G(x) */ - -#define CRC32_X415_MODG 0x3db1ecdc /* x^415 mod G(x) */ -#define CRC32_X351_MODG 0xaf449247 /* x^351 mod G(x) */ - -#define CRC32_X543_MODG 0x8f352d95 /* x^543 mod G(x) */ -#define CRC32_X479_MODG 0x1d9513d7 /* x^479 mod G(x) */ - -#define CRC32_X671_MODG 0x1c279815 /* x^671 mod G(x) */ -#define CRC32_X607_MODG 0xae0b5394 /* x^607 mod G(x) */ - -#define CRC32_X799_MODG 0xdf068dc2 /* x^799 mod G(x) */ -#define CRC32_X735_MODG 0x57c54819 /* x^735 mod G(x) */ - -#define CRC32_X927_MODG 0x31f8303f /* x^927 mod G(x) */ -#define CRC32_X863_MODG 0x0cbec0ed /* x^863 mod G(x) */ - -#define CRC32_X1055_MODG 0x33fff533 /* x^1055 mod G(x) */ -#define CRC32_X991_MODG 0x910eeec1 /* x^991 mod G(x) */ - -#define CRC32_X1183_MODG 0x26b70c3d /* x^1183 mod G(x) */ -#define CRC32_X1119_MODG 0x3f41287a /* x^1119 mod G(x) */ - -#define CRC32_X1311_MODG 0xe3543be0 /* x^1311 mod G(x) */ -#define CRC32_X1247_MODG 0x9026d5b1 /* x^1247 mod G(x) */ - -#define CRC32_X1439_MODG 0x5a1bb05d /* x^1439 mod G(x) */ -#define CRC32_X1375_MODG 0xd1df2327 /* x^1375 mod G(x) */ - -#define CRC32_X1567_MODG 0x596c8d81 /* x^1567 mod G(x) */ -#define CRC32_X1503_MODG 0xf5e48c85 /* x^1503 mod G(x) */ - -#define CRC32_X1695_MODG 0x682bdd4f /* x^1695 mod G(x) */ -#define CRC32_X1631_MODG 0x3c656ced /* x^1631 mod G(x) */ - -#define CRC32_X1823_MODG 0x4a28bd43 /* x^1823 mod G(x) */ -#define CRC32_X1759_MODG 0xfe807bbd /* x^1759 mod G(x) */ - -#define CRC32_X1951_MODG 0x0077f00d /* x^1951 mod G(x) */ -#define CRC32_X1887_MODG 0x1f0c2cdd /* x^1887 mod G(x) */ - -#define CRC32_X2079_MODG 0xce3371cb /* x^2079 mod G(x) */ -#define CRC32_X2015_MODG 0xe95c1271 /* x^2015 mod G(x) */ - -#define CRC32_X2207_MODG 0xa749e894 /* x^2207 mod G(x) */ -#define CRC32_X2143_MODG 0xb918a347 /* x^2143 mod G(x) */ - -#define CRC32_X2335_MODG 0x2c538639 /* x^2335 mod G(x) */ -#define CRC32_X2271_MODG 0x71d54a59 /* x^2271 mod G(x) */ - -#define CRC32_X2463_MODG 0x32b0733c /* x^2463 mod G(x) */ -#define CRC32_X2399_MODG 0xff6f2fc2 /* x^2399 mod G(x) */ - -#define CRC32_X2591_MODG 0x0e9bd5cc /* x^2591 mod G(x) */ -#define CRC32_X2527_MODG 0xcec97417 /* x^2527 mod G(x) */ - -#define CRC32_X2719_MODG 0x76278617 /* x^2719 mod G(x) */ -#define CRC32_X2655_MODG 0x1c63267b /* x^2655 mod G(x) */ - -#define CRC32_X2847_MODG 0xc51b93e3 /* x^2847 mod G(x) */ -#define CRC32_X2783_MODG 0xf183c71b /* x^2783 mod G(x) */ - -#define CRC32_X2975_MODG 0x7eaed122 /* x^2975 mod G(x) */ -#define CRC32_X2911_MODG 0x9b9bdbd0 /* x^2911 mod G(x) */ - -#define CRC32_X3103_MODG 0x2ce423f1 /* x^3103 mod G(x) */ -#define CRC32_X3039_MODG 0xd31343ea /* x^3039 mod G(x) */ - -#define CRC32_X3231_MODG 0x8b8d8645 /* x^3231 mod G(x) */ -#define CRC32_X3167_MODG 0x4470ac44 /* x^3167 mod G(x) */ - -#define CRC32_X3359_MODG 0x4b700aa8 /* x^3359 mod G(x) */ -#define CRC32_X3295_MODG 0xeea395c4 /* x^3295 mod G(x) */ - -#define CRC32_X3487_MODG 0xeff5e99d /* x^3487 mod G(x) */ -#define CRC32_X3423_MODG 0xf9d9c7ee /* x^3423 mod G(x) */ - -#define CRC32_X3615_MODG 0xad0d2bb2 /* x^3615 mod G(x) */ -#define CRC32_X3551_MODG 0xcd669a40 /* x^3551 mod G(x) */ - -#define CRC32_X3743_MODG 0x9fb66bd3 /* x^3743 mod G(x) */ -#define CRC32_X3679_MODG 0x6d40f445 /* x^3679 mod G(x) */ - -#define CRC32_X3871_MODG 0xc2dcc467 /* x^3871 mod G(x) */ -#define CRC32_X3807_MODG 0x9ee62949 /* x^3807 mod G(x) */ - -#define CRC32_X3999_MODG 0x398e2ff2 /* x^3999 mod G(x) */ -#define CRC32_X3935_MODG 0x145575d5 /* x^3935 mod G(x) */ - -#define CRC32_X4127_MODG 0x1072db28 /* x^4127 mod G(x) */ -#define CRC32_X4063_MODG 0x0c30f51d /* x^4063 mod G(x) */ - -#define CRC32_BARRETT_CONSTANT_1 0xb4e5b025f7011641ULL /* floor(x^95 / G(x)) */ -#define CRC32_BARRETT_CONSTANT_2 0x00000001db710641ULL /* G(x) */ - -#define CRC32_NUM_CHUNKS 4 -#define CRC32_MIN_VARIABLE_CHUNK_LEN 128UL -#define CRC32_MAX_VARIABLE_CHUNK_LEN 16384UL - -/* Multipliers for implementations that use a variable chunk length */ -static const u32 crc32_mults_for_chunklen[][CRC32_NUM_CHUNKS - 1] MAYBE_UNUSED = { - { 0 /* unused row */ }, - /* chunk_len=128 */ - { 0xd31343ea /* x^3039 mod G(x) */, 0xe95c1271 /* x^2015 mod G(x) */, 0x910eeec1 /* x^991 mod G(x) */, }, - /* chunk_len=256 */ - { 0x1d6708a0 /* x^6111 mod G(x) */, 0x0c30f51d /* x^4063 mod G(x) */, 0xe95c1271 /* x^2015 mod G(x) */, }, - /* chunk_len=384 */ - { 0xdb3839f3 /* x^9183 mod G(x) */, 0x1d6708a0 /* x^6111 mod G(x) */, 0xd31343ea /* x^3039 mod G(x) */, }, - /* chunk_len=512 */ - { 0x1753ab84 /* x^12255 mod G(x) */, 0xbbf2f6d6 /* x^8159 mod G(x) */, 0x0c30f51d /* x^4063 mod G(x) */, }, - /* chunk_len=640 */ - { 0x3796455c /* x^15327 mod G(x) */, 0xb8e0e4a8 /* x^10207 mod G(x) */, 0xc352f6de /* x^5087 mod G(x) */, }, - /* chunk_len=768 */ - { 0x3954de39 /* x^18399 mod G(x) */, 0x1753ab84 /* x^12255 mod G(x) */, 0x1d6708a0 /* x^6111 mod G(x) */, }, - /* chunk_len=896 */ - { 0x632d78c5 /* x^21471 mod G(x) */, 0x3fc33de4 /* x^14303 mod G(x) */, 0x9a1b53c8 /* x^7135 mod G(x) */, }, - /* chunk_len=1024 */ - { 0xa0decef3 /* x^24543 mod G(x) */, 0x7b4aa8b7 /* x^16351 mod G(x) */, 0xbbf2f6d6 /* x^8159 mod G(x) */, }, - /* chunk_len=1152 */ - { 0xe9c09bb0 /* x^27615 mod G(x) */, 0x3954de39 /* x^18399 mod G(x) */, 0xdb3839f3 /* x^9183 mod G(x) */, }, - /* chunk_len=1280 */ - { 0xd51917a4 /* x^30687 mod G(x) */, 0xcae68461 /* x^20447 mod G(x) */, 0xb8e0e4a8 /* x^10207 mod G(x) */, }, - /* chunk_len=1408 */ - { 0x154a8a62 /* x^33759 mod G(x) */, 0x41e7589c /* x^22495 mod G(x) */, 0x3e9a43cd /* x^11231 mod G(x) */, }, - /* chunk_len=1536 */ - { 0xf196555d /* x^36831 mod G(x) */, 0xa0decef3 /* x^24543 mod G(x) */, 0x1753ab84 /* x^12255 mod G(x) */, }, - /* chunk_len=1664 */ - { 0x8eec2999 /* x^39903 mod G(x) */, 0xefb0a128 /* x^26591 mod G(x) */, 0x6044fbb0 /* x^13279 mod G(x) */, }, - /* chunk_len=1792 */ - { 0x27892abf /* x^42975 mod G(x) */, 0x48d72bb1 /* x^28639 mod G(x) */, 0x3fc33de4 /* x^14303 mod G(x) */, }, - /* chunk_len=1920 */ - { 0x77bc2419 /* x^46047 mod G(x) */, 0xd51917a4 /* x^30687 mod G(x) */, 0x3796455c /* x^15327 mod G(x) */, }, - /* chunk_len=2048 */ - { 0xcea114a5 /* x^49119 mod G(x) */, 0x68c0a2c5 /* x^32735 mod G(x) */, 0x7b4aa8b7 /* x^16351 mod G(x) */, }, - /* chunk_len=2176 */ - { 0xa1077e85 /* x^52191 mod G(x) */, 0x188cc628 /* x^34783 mod G(x) */, 0x0c21f835 /* x^17375 mod G(x) */, }, - /* chunk_len=2304 */ - { 0xc5ed75e1 /* x^55263 mod G(x) */, 0xf196555d /* x^36831 mod G(x) */, 0x3954de39 /* x^18399 mod G(x) */, }, - /* chunk_len=2432 */ - { 0xca4fba3f /* x^58335 mod G(x) */, 0x0acfa26f /* x^38879 mod G(x) */, 0x6cb21510 /* x^19423 mod G(x) */, }, - /* chunk_len=2560 */ - { 0xcf5bcdc4 /* x^61407 mod G(x) */, 0x4fae7fc0 /* x^40927 mod G(x) */, 0xcae68461 /* x^20447 mod G(x) */, }, - /* chunk_len=2688 */ - { 0xf36b9d16 /* x^64479 mod G(x) */, 0x27892abf /* x^42975 mod G(x) */, 0x632d78c5 /* x^21471 mod G(x) */, }, - /* chunk_len=2816 */ - { 0xf76fd988 /* x^67551 mod G(x) */, 0xed5c39b1 /* x^45023 mod G(x) */, 0x41e7589c /* x^22495 mod G(x) */, }, - /* chunk_len=2944 */ - { 0x6c45d92e /* x^70623 mod G(x) */, 0xff809fcd /* x^47071 mod G(x) */, 0x0c46baec /* x^23519 mod G(x) */, }, - /* chunk_len=3072 */ - { 0x6116b82b /* x^73695 mod G(x) */, 0xcea114a5 /* x^49119 mod G(x) */, 0xa0decef3 /* x^24543 mod G(x) */, }, - /* chunk_len=3200 */ - { 0x4d9899bb /* x^76767 mod G(x) */, 0x9f9d8d9c /* x^51167 mod G(x) */, 0x53deb236 /* x^25567 mod G(x) */, }, - /* chunk_len=3328 */ - { 0x3e7c93b9 /* x^79839 mod G(x) */, 0x6666b805 /* x^53215 mod G(x) */, 0xefb0a128 /* x^26591 mod G(x) */, }, - /* chunk_len=3456 */ - { 0x388b20ac /* x^82911 mod G(x) */, 0xc5ed75e1 /* x^55263 mod G(x) */, 0xe9c09bb0 /* x^27615 mod G(x) */, }, - /* chunk_len=3584 */ - { 0x0956d953 /* x^85983 mod G(x) */, 0x97fbdb14 /* x^57311 mod G(x) */, 0x48d72bb1 /* x^28639 mod G(x) */, }, - /* chunk_len=3712 */ - { 0x55cb4dfe /* x^89055 mod G(x) */, 0x1b37c832 /* x^59359 mod G(x) */, 0xc07331b3 /* x^29663 mod G(x) */, }, - /* chunk_len=3840 */ - { 0x52222fea /* x^92127 mod G(x) */, 0xcf5bcdc4 /* x^61407 mod G(x) */, 0xd51917a4 /* x^30687 mod G(x) */, }, - /* chunk_len=3968 */ - { 0x0603989b /* x^95199 mod G(x) */, 0xb03c8112 /* x^63455 mod G(x) */, 0x5e04b9a5 /* x^31711 mod G(x) */, }, - /* chunk_len=4096 */ - { 0x4470c029 /* x^98271 mod G(x) */, 0x2339d155 /* x^65503 mod G(x) */, 0x68c0a2c5 /* x^32735 mod G(x) */, }, - /* chunk_len=4224 */ - { 0xb6f35093 /* x^101343 mod G(x) */, 0xf76fd988 /* x^67551 mod G(x) */, 0x154a8a62 /* x^33759 mod G(x) */, }, - /* chunk_len=4352 */ - { 0xc46805ba /* x^104415 mod G(x) */, 0x416f9449 /* x^69599 mod G(x) */, 0x188cc628 /* x^34783 mod G(x) */, }, - /* chunk_len=4480 */ - { 0xc3876592 /* x^107487 mod G(x) */, 0x4b809189 /* x^71647 mod G(x) */, 0xc35cf6e7 /* x^35807 mod G(x) */, }, - /* chunk_len=4608 */ - { 0x5b0c98b9 /* x^110559 mod G(x) */, 0x6116b82b /* x^73695 mod G(x) */, 0xf196555d /* x^36831 mod G(x) */, }, - /* chunk_len=4736 */ - { 0x30d13e5f /* x^113631 mod G(x) */, 0x4c5a315a /* x^75743 mod G(x) */, 0x8c224466 /* x^37855 mod G(x) */, }, - /* chunk_len=4864 */ - { 0x54afca53 /* x^116703 mod G(x) */, 0xbccfa2c1 /* x^77791 mod G(x) */, 0x0acfa26f /* x^38879 mod G(x) */, }, - /* chunk_len=4992 */ - { 0x93102436 /* x^119775 mod G(x) */, 0x3e7c93b9 /* x^79839 mod G(x) */, 0x8eec2999 /* x^39903 mod G(x) */, }, - /* chunk_len=5120 */ - { 0xbd2655a8 /* x^122847 mod G(x) */, 0x3e116c9d /* x^81887 mod G(x) */, 0x4fae7fc0 /* x^40927 mod G(x) */, }, - /* chunk_len=5248 */ - { 0x70cd7f26 /* x^125919 mod G(x) */, 0x408e57f2 /* x^83935 mod G(x) */, 0x1691be45 /* x^41951 mod G(x) */, }, - /* chunk_len=5376 */ - { 0x2d546c53 /* x^128991 mod G(x) */, 0x0956d953 /* x^85983 mod G(x) */, 0x27892abf /* x^42975 mod G(x) */, }, - /* chunk_len=5504 */ - { 0xb53410a8 /* x^132063 mod G(x) */, 0x42ebf0ad /* x^88031 mod G(x) */, 0x161f3c12 /* x^43999 mod G(x) */, }, - /* chunk_len=5632 */ - { 0x67a93f75 /* x^135135 mod G(x) */, 0xcf3233e4 /* x^90079 mod G(x) */, 0xed5c39b1 /* x^45023 mod G(x) */, }, - /* chunk_len=5760 */ - { 0x9830ac33 /* x^138207 mod G(x) */, 0x52222fea /* x^92127 mod G(x) */, 0x77bc2419 /* x^46047 mod G(x) */, }, - /* chunk_len=5888 */ - { 0xb0b6fc3e /* x^141279 mod G(x) */, 0x2fde73f8 /* x^94175 mod G(x) */, 0xff809fcd /* x^47071 mod G(x) */, }, - /* chunk_len=6016 */ - { 0x84170f16 /* x^144351 mod G(x) */, 0xced90d99 /* x^96223 mod G(x) */, 0x30de0f98 /* x^48095 mod G(x) */, }, - /* chunk_len=6144 */ - { 0xd7017a0c /* x^147423 mod G(x) */, 0x4470c029 /* x^98271 mod G(x) */, 0xcea114a5 /* x^49119 mod G(x) */, }, - /* chunk_len=6272 */ - { 0xadb25de6 /* x^150495 mod G(x) */, 0x84f40beb /* x^100319 mod G(x) */, 0x2b7e0e1b /* x^50143 mod G(x) */, }, - /* chunk_len=6400 */ - { 0x8282fddc /* x^153567 mod G(x) */, 0xec855937 /* x^102367 mod G(x) */, 0x9f9d8d9c /* x^51167 mod G(x) */, }, - /* chunk_len=6528 */ - { 0x46362bee /* x^156639 mod G(x) */, 0xc46805ba /* x^104415 mod G(x) */, 0xa1077e85 /* x^52191 mod G(x) */, }, - /* chunk_len=6656 */ - { 0xb9077a01 /* x^159711 mod G(x) */, 0xdf7a24ac /* x^106463 mod G(x) */, 0x6666b805 /* x^53215 mod G(x) */, }, - /* chunk_len=6784 */ - { 0xf51d9bc6 /* x^162783 mod G(x) */, 0x2b52dc39 /* x^108511 mod G(x) */, 0x7e774cf6 /* x^54239 mod G(x) */, }, - /* chunk_len=6912 */ - { 0x4ca19a29 /* x^165855 mod G(x) */, 0x5b0c98b9 /* x^110559 mod G(x) */, 0xc5ed75e1 /* x^55263 mod G(x) */, }, - /* chunk_len=7040 */ - { 0xdc0fc3fc /* x^168927 mod G(x) */, 0xb939fcdf /* x^112607 mod G(x) */, 0x3678fed2 /* x^56287 mod G(x) */, }, - /* chunk_len=7168 */ - { 0x63c3d167 /* x^171999 mod G(x) */, 0x70f9947d /* x^114655 mod G(x) */, 0x97fbdb14 /* x^57311 mod G(x) */, }, - /* chunk_len=7296 */ - { 0x5851d254 /* x^175071 mod G(x) */, 0x54afca53 /* x^116703 mod G(x) */, 0xca4fba3f /* x^58335 mod G(x) */, }, - /* chunk_len=7424 */ - { 0xfeacf2a1 /* x^178143 mod G(x) */, 0x7a3c0a6a /* x^118751 mod G(x) */, 0x1b37c832 /* x^59359 mod G(x) */, }, - /* chunk_len=7552 */ - { 0x93b7edc8 /* x^181215 mod G(x) */, 0x1fea4d2a /* x^120799 mod G(x) */, 0x58fa96ee /* x^60383 mod G(x) */, }, - /* chunk_len=7680 */ - { 0x5539e44a /* x^184287 mod G(x) */, 0xbd2655a8 /* x^122847 mod G(x) */, 0xcf5bcdc4 /* x^61407 mod G(x) */, }, - /* chunk_len=7808 */ - { 0xde32a3d2 /* x^187359 mod G(x) */, 0x4ff61aa1 /* x^124895 mod G(x) */, 0x6a6a3694 /* x^62431 mod G(x) */, }, - /* chunk_len=7936 */ - { 0xf0baeeb6 /* x^190431 mod G(x) */, 0x7ae2f6f4 /* x^126943 mod G(x) */, 0xb03c8112 /* x^63455 mod G(x) */, }, - /* chunk_len=8064 */ - { 0xbe15887f /* x^193503 mod G(x) */, 0x2d546c53 /* x^128991 mod G(x) */, 0xf36b9d16 /* x^64479 mod G(x) */, }, - /* chunk_len=8192 */ - { 0x64f34a05 /* x^196575 mod G(x) */, 0xe0ee5efe /* x^131039 mod G(x) */, 0x2339d155 /* x^65503 mod G(x) */, }, - /* chunk_len=8320 */ - { 0x1b6d1aea /* x^199647 mod G(x) */, 0xfeafb67c /* x^133087 mod G(x) */, 0x4fb001a8 /* x^66527 mod G(x) */, }, - /* chunk_len=8448 */ - { 0x82adb0b8 /* x^202719 mod G(x) */, 0x67a93f75 /* x^135135 mod G(x) */, 0xf76fd988 /* x^67551 mod G(x) */, }, - /* chunk_len=8576 */ - { 0x694587c7 /* x^205791 mod G(x) */, 0x3b34408b /* x^137183 mod G(x) */, 0xeccb2978 /* x^68575 mod G(x) */, }, - /* chunk_len=8704 */ - { 0xd2fc57c3 /* x^208863 mod G(x) */, 0x07fcf8c6 /* x^139231 mod G(x) */, 0x416f9449 /* x^69599 mod G(x) */, }, - /* chunk_len=8832 */ - { 0x9dd6837c /* x^211935 mod G(x) */, 0xb0b6fc3e /* x^141279 mod G(x) */, 0x6c45d92e /* x^70623 mod G(x) */, }, - /* chunk_len=8960 */ - { 0x3a9d1f97 /* x^215007 mod G(x) */, 0xefd033b2 /* x^143327 mod G(x) */, 0x4b809189 /* x^71647 mod G(x) */, }, - /* chunk_len=9088 */ - { 0x1eee1d2a /* x^218079 mod G(x) */, 0xf2a6e46e /* x^145375 mod G(x) */, 0x55b4c814 /* x^72671 mod G(x) */, }, - /* chunk_len=9216 */ - { 0xb57c7728 /* x^221151 mod G(x) */, 0xd7017a0c /* x^147423 mod G(x) */, 0x6116b82b /* x^73695 mod G(x) */, }, - /* chunk_len=9344 */ - { 0xf2fc5d61 /* x^224223 mod G(x) */, 0x242aac86 /* x^149471 mod G(x) */, 0x05245cf0 /* x^74719 mod G(x) */, }, - /* chunk_len=9472 */ - { 0x26387824 /* x^227295 mod G(x) */, 0xc15c4ca5 /* x^151519 mod G(x) */, 0x4c5a315a /* x^75743 mod G(x) */, }, - /* chunk_len=9600 */ - { 0x8c151e77 /* x^230367 mod G(x) */, 0x8282fddc /* x^153567 mod G(x) */, 0x4d9899bb /* x^76767 mod G(x) */, }, - /* chunk_len=9728 */ - { 0x8ea1f680 /* x^233439 mod G(x) */, 0xf5ff6cdd /* x^155615 mod G(x) */, 0xbccfa2c1 /* x^77791 mod G(x) */, }, - /* chunk_len=9856 */ - { 0xe8cf3d2a /* x^236511 mod G(x) */, 0x338b1fb1 /* x^157663 mod G(x) */, 0xeda61f70 /* x^78815 mod G(x) */, }, - /* chunk_len=9984 */ - { 0x21f15b59 /* x^239583 mod G(x) */, 0xb9077a01 /* x^159711 mod G(x) */, 0x3e7c93b9 /* x^79839 mod G(x) */, }, - /* chunk_len=10112 */ - { 0x6f68d64a /* x^242655 mod G(x) */, 0x901b0161 /* x^161759 mod G(x) */, 0xb9fd3537 /* x^80863 mod G(x) */, }, - /* chunk_len=10240 */ - { 0x71b74d95 /* x^245727 mod G(x) */, 0xf5ddd5ad /* x^163807 mod G(x) */, 0x3e116c9d /* x^81887 mod G(x) */, }, - /* chunk_len=10368 */ - { 0x4c2e7261 /* x^248799 mod G(x) */, 0x4ca19a29 /* x^165855 mod G(x) */, 0x388b20ac /* x^82911 mod G(x) */, }, - /* chunk_len=10496 */ - { 0x8a2d38e8 /* x^251871 mod G(x) */, 0xd27ee0a1 /* x^167903 mod G(x) */, 0x408e57f2 /* x^83935 mod G(x) */, }, - /* chunk_len=10624 */ - { 0x7e58ca17 /* x^254943 mod G(x) */, 0x69dfedd2 /* x^169951 mod G(x) */, 0x3a76805e /* x^84959 mod G(x) */, }, - /* chunk_len=10752 */ - { 0xf997967f /* x^258015 mod G(x) */, 0x63c3d167 /* x^171999 mod G(x) */, 0x0956d953 /* x^85983 mod G(x) */, }, - /* chunk_len=10880 */ - { 0x48215963 /* x^261087 mod G(x) */, 0x71e1dfe0 /* x^174047 mod G(x) */, 0x42a6d410 /* x^87007 mod G(x) */, }, - /* chunk_len=11008 */ - { 0xa704b94c /* x^264159 mod G(x) */, 0x679f198a /* x^176095 mod G(x) */, 0x42ebf0ad /* x^88031 mod G(x) */, }, - /* chunk_len=11136 */ - { 0x1d699056 /* x^267231 mod G(x) */, 0xfeacf2a1 /* x^178143 mod G(x) */, 0x55cb4dfe /* x^89055 mod G(x) */, }, - /* chunk_len=11264 */ - { 0x6800bcc5 /* x^270303 mod G(x) */, 0x16024f15 /* x^180191 mod G(x) */, 0xcf3233e4 /* x^90079 mod G(x) */, }, - /* chunk_len=11392 */ - { 0x2d48e4ca /* x^273375 mod G(x) */, 0xbe61582f /* x^182239 mod G(x) */, 0x46026283 /* x^91103 mod G(x) */, }, - /* chunk_len=11520 */ - { 0x4c4c2b55 /* x^276447 mod G(x) */, 0x5539e44a /* x^184287 mod G(x) */, 0x52222fea /* x^92127 mod G(x) */, }, - /* chunk_len=11648 */ - { 0xd8ce94cb /* x^279519 mod G(x) */, 0xbc613c26 /* x^186335 mod G(x) */, 0x33776b4b /* x^93151 mod G(x) */, }, - /* chunk_len=11776 */ - { 0xd0b5a02b /* x^282591 mod G(x) */, 0x490d3cc6 /* x^188383 mod G(x) */, 0x2fde73f8 /* x^94175 mod G(x) */, }, - /* chunk_len=11904 */ - { 0xa223f7ec /* x^285663 mod G(x) */, 0xf0baeeb6 /* x^190431 mod G(x) */, 0x0603989b /* x^95199 mod G(x) */, }, - /* chunk_len=12032 */ - { 0x58de337a /* x^288735 mod G(x) */, 0x3bf3d597 /* x^192479 mod G(x) */, 0xced90d99 /* x^96223 mod G(x) */, }, - /* chunk_len=12160 */ - { 0x37f5d8f4 /* x^291807 mod G(x) */, 0x4d5b699b /* x^194527 mod G(x) */, 0xd7262e5f /* x^97247 mod G(x) */, }, - /* chunk_len=12288 */ - { 0xfa8a435d /* x^294879 mod G(x) */, 0x64f34a05 /* x^196575 mod G(x) */, 0x4470c029 /* x^98271 mod G(x) */, }, - /* chunk_len=12416 */ - { 0x238709fe /* x^297951 mod G(x) */, 0x52e7458f /* x^198623 mod G(x) */, 0x9a174cd3 /* x^99295 mod G(x) */, }, - /* chunk_len=12544 */ - { 0x9e1ba6f5 /* x^301023 mod G(x) */, 0xef0272f7 /* x^200671 mod G(x) */, 0x84f40beb /* x^100319 mod G(x) */, }, - /* chunk_len=12672 */ - { 0xcd8b57fa /* x^304095 mod G(x) */, 0x82adb0b8 /* x^202719 mod G(x) */, 0xb6f35093 /* x^101343 mod G(x) */, }, - /* chunk_len=12800 */ - { 0x0aed142f /* x^307167 mod G(x) */, 0xb1650290 /* x^204767 mod G(x) */, 0xec855937 /* x^102367 mod G(x) */, }, - /* chunk_len=12928 */ - { 0xd1f064db /* x^310239 mod G(x) */, 0x6e7340d3 /* x^206815 mod G(x) */, 0x5c28cb52 /* x^103391 mod G(x) */, }, - /* chunk_len=13056 */ - { 0x464ac895 /* x^313311 mod G(x) */, 0xd2fc57c3 /* x^208863 mod G(x) */, 0xc46805ba /* x^104415 mod G(x) */, }, - /* chunk_len=13184 */ - { 0xa0e6beea /* x^316383 mod G(x) */, 0xcfeec3d0 /* x^210911 mod G(x) */, 0x0225d214 /* x^105439 mod G(x) */, }, - /* chunk_len=13312 */ - { 0x78703ce0 /* x^319455 mod G(x) */, 0xc60f6075 /* x^212959 mod G(x) */, 0xdf7a24ac /* x^106463 mod G(x) */, }, - /* chunk_len=13440 */ - { 0xfea48165 /* x^322527 mod G(x) */, 0x3a9d1f97 /* x^215007 mod G(x) */, 0xc3876592 /* x^107487 mod G(x) */, }, - /* chunk_len=13568 */ - { 0xdb89b8db /* x^325599 mod G(x) */, 0xa6172211 /* x^217055 mod G(x) */, 0x2b52dc39 /* x^108511 mod G(x) */, }, - /* chunk_len=13696 */ - { 0x7ca03731 /* x^328671 mod G(x) */, 0x1db42849 /* x^219103 mod G(x) */, 0xc5df246e /* x^109535 mod G(x) */, }, - /* chunk_len=13824 */ - { 0x8801d0aa /* x^331743 mod G(x) */, 0xb57c7728 /* x^221151 mod G(x) */, 0x5b0c98b9 /* x^110559 mod G(x) */, }, - /* chunk_len=13952 */ - { 0xf89cd7f0 /* x^334815 mod G(x) */, 0xcc396a0b /* x^223199 mod G(x) */, 0xdb799c51 /* x^111583 mod G(x) */, }, - /* chunk_len=14080 */ - { 0x1611a808 /* x^337887 mod G(x) */, 0xaeae6105 /* x^225247 mod G(x) */, 0xb939fcdf /* x^112607 mod G(x) */, }, - /* chunk_len=14208 */ - { 0xe3cdb888 /* x^340959 mod G(x) */, 0x26387824 /* x^227295 mod G(x) */, 0x30d13e5f /* x^113631 mod G(x) */, }, - /* chunk_len=14336 */ - { 0x552a4cf6 /* x^344031 mod G(x) */, 0xee2d04bb /* x^229343 mod G(x) */, 0x70f9947d /* x^114655 mod G(x) */, }, - /* chunk_len=14464 */ - { 0x85e248e9 /* x^347103 mod G(x) */, 0x0a79663f /* x^231391 mod G(x) */, 0x53339cf7 /* x^115679 mod G(x) */, }, - /* chunk_len=14592 */ - { 0x1c61c3e9 /* x^350175 mod G(x) */, 0x8ea1f680 /* x^233439 mod G(x) */, 0x54afca53 /* x^116703 mod G(x) */, }, - /* chunk_len=14720 */ - { 0xb14cfc2b /* x^353247 mod G(x) */, 0x2e073302 /* x^235487 mod G(x) */, 0x10897992 /* x^117727 mod G(x) */, }, - /* chunk_len=14848 */ - { 0x6ec444cc /* x^356319 mod G(x) */, 0x9e819f13 /* x^237535 mod G(x) */, 0x7a3c0a6a /* x^118751 mod G(x) */, }, - /* chunk_len=14976 */ - { 0xe2fa5f80 /* x^359391 mod G(x) */, 0x21f15b59 /* x^239583 mod G(x) */, 0x93102436 /* x^119775 mod G(x) */, }, - /* chunk_len=15104 */ - { 0x6d33f4c6 /* x^362463 mod G(x) */, 0x31a27455 /* x^241631 mod G(x) */, 0x1fea4d2a /* x^120799 mod G(x) */, }, - /* chunk_len=15232 */ - { 0xb6dec609 /* x^365535 mod G(x) */, 0x4d437056 /* x^243679 mod G(x) */, 0x42eb1e2a /* x^121823 mod G(x) */, }, - /* chunk_len=15360 */ - { 0x1846c518 /* x^368607 mod G(x) */, 0x71b74d95 /* x^245727 mod G(x) */, 0xbd2655a8 /* x^122847 mod G(x) */, }, - /* chunk_len=15488 */ - { 0x9f947f8a /* x^371679 mod G(x) */, 0x2b501619 /* x^247775 mod G(x) */, 0xa4924b0e /* x^123871 mod G(x) */, }, - /* chunk_len=15616 */ - { 0xb7442f4d /* x^374751 mod G(x) */, 0xba30a5d8 /* x^249823 mod G(x) */, 0x4ff61aa1 /* x^124895 mod G(x) */, }, - /* chunk_len=15744 */ - { 0xe2c93242 /* x^377823 mod G(x) */, 0x8a2d38e8 /* x^251871 mod G(x) */, 0x70cd7f26 /* x^125919 mod G(x) */, }, - /* chunk_len=15872 */ - { 0xcd6863df /* x^380895 mod G(x) */, 0x78fd88dc /* x^253919 mod G(x) */, 0x7ae2f6f4 /* x^126943 mod G(x) */, }, - /* chunk_len=16000 */ - { 0xd512001d /* x^383967 mod G(x) */, 0xe6612dff /* x^255967 mod G(x) */, 0x5c4d0ca9 /* x^127967 mod G(x) */, }, - /* chunk_len=16128 */ - { 0x4e8d6b6c /* x^387039 mod G(x) */, 0xf997967f /* x^258015 mod G(x) */, 0x2d546c53 /* x^128991 mod G(x) */, }, - /* chunk_len=16256 */ - { 0xfa653ba1 /* x^390111 mod G(x) */, 0xc99014d4 /* x^260063 mod G(x) */, 0xa0c9fd27 /* x^130015 mod G(x) */, }, - /* chunk_len=16384 */ - { 0x49893408 /* x^393183 mod G(x) */, 0x29c2448b /* x^262111 mod G(x) */, 0xe0ee5efe /* x^131039 mod G(x) */, }, -}; - -/* Multipliers for implementations that use a large fixed chunk length */ -#define CRC32_FIXED_CHUNK_LEN 32768UL -#define CRC32_FIXED_CHUNK_MULT_1 0x29c2448b /* x^262111 mod G(x) */ -#define CRC32_FIXED_CHUNK_MULT_2 0x4b912f53 /* x^524255 mod G(x) */ -#define CRC32_FIXED_CHUNK_MULT_3 0x454c93be /* x^786399 mod G(x) */ diff --git a/dm/third_party/libdeflate/lib/crc32_tables.h b/dm/third_party/libdeflate/lib/crc32_tables.h deleted file mode 100644 index d6aff73..0000000 --- a/dm/third_party/libdeflate/lib/crc32_tables.h +++ /dev/null @@ -1,587 +0,0 @@ -/* - * crc32_tables.h - data tables for CRC-32 computation - * - * THIS FILE WAS GENERATED BY gen-crc32-consts.py. DO NOT EDIT. - */ - -static const u32 crc32_slice1_table[] MAYBE_UNUSED = { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, - 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, - 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, - 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, - 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, - 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, - 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, - 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, - 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, - 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, - 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, - 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, - 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, - 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, - 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, - 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, - 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, - 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, - 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, - 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, - 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, - 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, - 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, - 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, - 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, - 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, - 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, - 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, - 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, - 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, - 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, - 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, - 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, - 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, - 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, - 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, - 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, - 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, - 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, - 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, -}; - -static const u32 crc32_slice8_table[] MAYBE_UNUSED = { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, - 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, - 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, - 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, - 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, - 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, - 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, - 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, - 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, - 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, - 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, - 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, - 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, - 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, - 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, - 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, - 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, - 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, - 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, - 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, - 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, - 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, - 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, - 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, - 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, - 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, - 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, - 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, - 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, - 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, - 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, - 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, - 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, - 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, - 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, - 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, - 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, - 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, - 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, - 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, - 0x00000000, 0x191b3141, 0x32366282, 0x2b2d53c3, - 0x646cc504, 0x7d77f445, 0x565aa786, 0x4f4196c7, - 0xc8d98a08, 0xd1c2bb49, 0xfaefe88a, 0xe3f4d9cb, - 0xacb54f0c, 0xb5ae7e4d, 0x9e832d8e, 0x87981ccf, - 0x4ac21251, 0x53d92310, 0x78f470d3, 0x61ef4192, - 0x2eaed755, 0x37b5e614, 0x1c98b5d7, 0x05838496, - 0x821b9859, 0x9b00a918, 0xb02dfadb, 0xa936cb9a, - 0xe6775d5d, 0xff6c6c1c, 0xd4413fdf, 0xcd5a0e9e, - 0x958424a2, 0x8c9f15e3, 0xa7b24620, 0xbea97761, - 0xf1e8e1a6, 0xe8f3d0e7, 0xc3de8324, 0xdac5b265, - 0x5d5daeaa, 0x44469feb, 0x6f6bcc28, 0x7670fd69, - 0x39316bae, 0x202a5aef, 0x0b07092c, 0x121c386d, - 0xdf4636f3, 0xc65d07b2, 0xed705471, 0xf46b6530, - 0xbb2af3f7, 0xa231c2b6, 0x891c9175, 0x9007a034, - 0x179fbcfb, 0x0e848dba, 0x25a9de79, 0x3cb2ef38, - 0x73f379ff, 0x6ae848be, 0x41c51b7d, 0x58de2a3c, - 0xf0794f05, 0xe9627e44, 0xc24f2d87, 0xdb541cc6, - 0x94158a01, 0x8d0ebb40, 0xa623e883, 0xbf38d9c2, - 0x38a0c50d, 0x21bbf44c, 0x0a96a78f, 0x138d96ce, - 0x5ccc0009, 0x45d73148, 0x6efa628b, 0x77e153ca, - 0xbabb5d54, 0xa3a06c15, 0x888d3fd6, 0x91960e97, - 0xded79850, 0xc7cca911, 0xece1fad2, 0xf5facb93, - 0x7262d75c, 0x6b79e61d, 0x4054b5de, 0x594f849f, - 0x160e1258, 0x0f152319, 0x243870da, 0x3d23419b, - 0x65fd6ba7, 0x7ce65ae6, 0x57cb0925, 0x4ed03864, - 0x0191aea3, 0x188a9fe2, 0x33a7cc21, 0x2abcfd60, - 0xad24e1af, 0xb43fd0ee, 0x9f12832d, 0x8609b26c, - 0xc94824ab, 0xd05315ea, 0xfb7e4629, 0xe2657768, - 0x2f3f79f6, 0x362448b7, 0x1d091b74, 0x04122a35, - 0x4b53bcf2, 0x52488db3, 0x7965de70, 0x607eef31, - 0xe7e6f3fe, 0xfefdc2bf, 0xd5d0917c, 0xcccba03d, - 0x838a36fa, 0x9a9107bb, 0xb1bc5478, 0xa8a76539, - 0x3b83984b, 0x2298a90a, 0x09b5fac9, 0x10aecb88, - 0x5fef5d4f, 0x46f46c0e, 0x6dd93fcd, 0x74c20e8c, - 0xf35a1243, 0xea412302, 0xc16c70c1, 0xd8774180, - 0x9736d747, 0x8e2de606, 0xa500b5c5, 0xbc1b8484, - 0x71418a1a, 0x685abb5b, 0x4377e898, 0x5a6cd9d9, - 0x152d4f1e, 0x0c367e5f, 0x271b2d9c, 0x3e001cdd, - 0xb9980012, 0xa0833153, 0x8bae6290, 0x92b553d1, - 0xddf4c516, 0xc4eff457, 0xefc2a794, 0xf6d996d5, - 0xae07bce9, 0xb71c8da8, 0x9c31de6b, 0x852aef2a, - 0xca6b79ed, 0xd37048ac, 0xf85d1b6f, 0xe1462a2e, - 0x66de36e1, 0x7fc507a0, 0x54e85463, 0x4df36522, - 0x02b2f3e5, 0x1ba9c2a4, 0x30849167, 0x299fa026, - 0xe4c5aeb8, 0xfdde9ff9, 0xd6f3cc3a, 0xcfe8fd7b, - 0x80a96bbc, 0x99b25afd, 0xb29f093e, 0xab84387f, - 0x2c1c24b0, 0x350715f1, 0x1e2a4632, 0x07317773, - 0x4870e1b4, 0x516bd0f5, 0x7a468336, 0x635db277, - 0xcbfad74e, 0xd2e1e60f, 0xf9ccb5cc, 0xe0d7848d, - 0xaf96124a, 0xb68d230b, 0x9da070c8, 0x84bb4189, - 0x03235d46, 0x1a386c07, 0x31153fc4, 0x280e0e85, - 0x674f9842, 0x7e54a903, 0x5579fac0, 0x4c62cb81, - 0x8138c51f, 0x9823f45e, 0xb30ea79d, 0xaa1596dc, - 0xe554001b, 0xfc4f315a, 0xd7626299, 0xce7953d8, - 0x49e14f17, 0x50fa7e56, 0x7bd72d95, 0x62cc1cd4, - 0x2d8d8a13, 0x3496bb52, 0x1fbbe891, 0x06a0d9d0, - 0x5e7ef3ec, 0x4765c2ad, 0x6c48916e, 0x7553a02f, - 0x3a1236e8, 0x230907a9, 0x0824546a, 0x113f652b, - 0x96a779e4, 0x8fbc48a5, 0xa4911b66, 0xbd8a2a27, - 0xf2cbbce0, 0xebd08da1, 0xc0fdde62, 0xd9e6ef23, - 0x14bce1bd, 0x0da7d0fc, 0x268a833f, 0x3f91b27e, - 0x70d024b9, 0x69cb15f8, 0x42e6463b, 0x5bfd777a, - 0xdc656bb5, 0xc57e5af4, 0xee530937, 0xf7483876, - 0xb809aeb1, 0xa1129ff0, 0x8a3fcc33, 0x9324fd72, - 0x00000000, 0x01c26a37, 0x0384d46e, 0x0246be59, - 0x0709a8dc, 0x06cbc2eb, 0x048d7cb2, 0x054f1685, - 0x0e1351b8, 0x0fd13b8f, 0x0d9785d6, 0x0c55efe1, - 0x091af964, 0x08d89353, 0x0a9e2d0a, 0x0b5c473d, - 0x1c26a370, 0x1de4c947, 0x1fa2771e, 0x1e601d29, - 0x1b2f0bac, 0x1aed619b, 0x18abdfc2, 0x1969b5f5, - 0x1235f2c8, 0x13f798ff, 0x11b126a6, 0x10734c91, - 0x153c5a14, 0x14fe3023, 0x16b88e7a, 0x177ae44d, - 0x384d46e0, 0x398f2cd7, 0x3bc9928e, 0x3a0bf8b9, - 0x3f44ee3c, 0x3e86840b, 0x3cc03a52, 0x3d025065, - 0x365e1758, 0x379c7d6f, 0x35dac336, 0x3418a901, - 0x3157bf84, 0x3095d5b3, 0x32d36bea, 0x331101dd, - 0x246be590, 0x25a98fa7, 0x27ef31fe, 0x262d5bc9, - 0x23624d4c, 0x22a0277b, 0x20e69922, 0x2124f315, - 0x2a78b428, 0x2bbade1f, 0x29fc6046, 0x283e0a71, - 0x2d711cf4, 0x2cb376c3, 0x2ef5c89a, 0x2f37a2ad, - 0x709a8dc0, 0x7158e7f7, 0x731e59ae, 0x72dc3399, - 0x7793251c, 0x76514f2b, 0x7417f172, 0x75d59b45, - 0x7e89dc78, 0x7f4bb64f, 0x7d0d0816, 0x7ccf6221, - 0x798074a4, 0x78421e93, 0x7a04a0ca, 0x7bc6cafd, - 0x6cbc2eb0, 0x6d7e4487, 0x6f38fade, 0x6efa90e9, - 0x6bb5866c, 0x6a77ec5b, 0x68315202, 0x69f33835, - 0x62af7f08, 0x636d153f, 0x612bab66, 0x60e9c151, - 0x65a6d7d4, 0x6464bde3, 0x662203ba, 0x67e0698d, - 0x48d7cb20, 0x4915a117, 0x4b531f4e, 0x4a917579, - 0x4fde63fc, 0x4e1c09cb, 0x4c5ab792, 0x4d98dda5, - 0x46c49a98, 0x4706f0af, 0x45404ef6, 0x448224c1, - 0x41cd3244, 0x400f5873, 0x4249e62a, 0x438b8c1d, - 0x54f16850, 0x55330267, 0x5775bc3e, 0x56b7d609, - 0x53f8c08c, 0x523aaabb, 0x507c14e2, 0x51be7ed5, - 0x5ae239e8, 0x5b2053df, 0x5966ed86, 0x58a487b1, - 0x5deb9134, 0x5c29fb03, 0x5e6f455a, 0x5fad2f6d, - 0xe1351b80, 0xe0f771b7, 0xe2b1cfee, 0xe373a5d9, - 0xe63cb35c, 0xe7fed96b, 0xe5b86732, 0xe47a0d05, - 0xef264a38, 0xeee4200f, 0xeca29e56, 0xed60f461, - 0xe82fe2e4, 0xe9ed88d3, 0xebab368a, 0xea695cbd, - 0xfd13b8f0, 0xfcd1d2c7, 0xfe976c9e, 0xff5506a9, - 0xfa1a102c, 0xfbd87a1b, 0xf99ec442, 0xf85cae75, - 0xf300e948, 0xf2c2837f, 0xf0843d26, 0xf1465711, - 0xf4094194, 0xf5cb2ba3, 0xf78d95fa, 0xf64fffcd, - 0xd9785d60, 0xd8ba3757, 0xdafc890e, 0xdb3ee339, - 0xde71f5bc, 0xdfb39f8b, 0xddf521d2, 0xdc374be5, - 0xd76b0cd8, 0xd6a966ef, 0xd4efd8b6, 0xd52db281, - 0xd062a404, 0xd1a0ce33, 0xd3e6706a, 0xd2241a5d, - 0xc55efe10, 0xc49c9427, 0xc6da2a7e, 0xc7184049, - 0xc25756cc, 0xc3953cfb, 0xc1d382a2, 0xc011e895, - 0xcb4dafa8, 0xca8fc59f, 0xc8c97bc6, 0xc90b11f1, - 0xcc440774, 0xcd866d43, 0xcfc0d31a, 0xce02b92d, - 0x91af9640, 0x906dfc77, 0x922b422e, 0x93e92819, - 0x96a63e9c, 0x976454ab, 0x9522eaf2, 0x94e080c5, - 0x9fbcc7f8, 0x9e7eadcf, 0x9c381396, 0x9dfa79a1, - 0x98b56f24, 0x99770513, 0x9b31bb4a, 0x9af3d17d, - 0x8d893530, 0x8c4b5f07, 0x8e0de15e, 0x8fcf8b69, - 0x8a809dec, 0x8b42f7db, 0x89044982, 0x88c623b5, - 0x839a6488, 0x82580ebf, 0x801eb0e6, 0x81dcdad1, - 0x8493cc54, 0x8551a663, 0x8717183a, 0x86d5720d, - 0xa9e2d0a0, 0xa820ba97, 0xaa6604ce, 0xaba46ef9, - 0xaeeb787c, 0xaf29124b, 0xad6fac12, 0xacadc625, - 0xa7f18118, 0xa633eb2f, 0xa4755576, 0xa5b73f41, - 0xa0f829c4, 0xa13a43f3, 0xa37cfdaa, 0xa2be979d, - 0xb5c473d0, 0xb40619e7, 0xb640a7be, 0xb782cd89, - 0xb2cddb0c, 0xb30fb13b, 0xb1490f62, 0xb08b6555, - 0xbbd72268, 0xba15485f, 0xb853f606, 0xb9919c31, - 0xbcde8ab4, 0xbd1ce083, 0xbf5a5eda, 0xbe9834ed, - 0x00000000, 0xb8bc6765, 0xaa09c88b, 0x12b5afee, - 0x8f629757, 0x37def032, 0x256b5fdc, 0x9dd738b9, - 0xc5b428ef, 0x7d084f8a, 0x6fbde064, 0xd7018701, - 0x4ad6bfb8, 0xf26ad8dd, 0xe0df7733, 0x58631056, - 0x5019579f, 0xe8a530fa, 0xfa109f14, 0x42acf871, - 0xdf7bc0c8, 0x67c7a7ad, 0x75720843, 0xcdce6f26, - 0x95ad7f70, 0x2d111815, 0x3fa4b7fb, 0x8718d09e, - 0x1acfe827, 0xa2738f42, 0xb0c620ac, 0x087a47c9, - 0xa032af3e, 0x188ec85b, 0x0a3b67b5, 0xb28700d0, - 0x2f503869, 0x97ec5f0c, 0x8559f0e2, 0x3de59787, - 0x658687d1, 0xdd3ae0b4, 0xcf8f4f5a, 0x7733283f, - 0xeae41086, 0x525877e3, 0x40edd80d, 0xf851bf68, - 0xf02bf8a1, 0x48979fc4, 0x5a22302a, 0xe29e574f, - 0x7f496ff6, 0xc7f50893, 0xd540a77d, 0x6dfcc018, - 0x359fd04e, 0x8d23b72b, 0x9f9618c5, 0x272a7fa0, - 0xbafd4719, 0x0241207c, 0x10f48f92, 0xa848e8f7, - 0x9b14583d, 0x23a83f58, 0x311d90b6, 0x89a1f7d3, - 0x1476cf6a, 0xaccaa80f, 0xbe7f07e1, 0x06c36084, - 0x5ea070d2, 0xe61c17b7, 0xf4a9b859, 0x4c15df3c, - 0xd1c2e785, 0x697e80e0, 0x7bcb2f0e, 0xc377486b, - 0xcb0d0fa2, 0x73b168c7, 0x6104c729, 0xd9b8a04c, - 0x446f98f5, 0xfcd3ff90, 0xee66507e, 0x56da371b, - 0x0eb9274d, 0xb6054028, 0xa4b0efc6, 0x1c0c88a3, - 0x81dbb01a, 0x3967d77f, 0x2bd27891, 0x936e1ff4, - 0x3b26f703, 0x839a9066, 0x912f3f88, 0x299358ed, - 0xb4446054, 0x0cf80731, 0x1e4da8df, 0xa6f1cfba, - 0xfe92dfec, 0x462eb889, 0x549b1767, 0xec277002, - 0x71f048bb, 0xc94c2fde, 0xdbf98030, 0x6345e755, - 0x6b3fa09c, 0xd383c7f9, 0xc1366817, 0x798a0f72, - 0xe45d37cb, 0x5ce150ae, 0x4e54ff40, 0xf6e89825, - 0xae8b8873, 0x1637ef16, 0x048240f8, 0xbc3e279d, - 0x21e91f24, 0x99557841, 0x8be0d7af, 0x335cb0ca, - 0xed59b63b, 0x55e5d15e, 0x47507eb0, 0xffec19d5, - 0x623b216c, 0xda874609, 0xc832e9e7, 0x708e8e82, - 0x28ed9ed4, 0x9051f9b1, 0x82e4565f, 0x3a58313a, - 0xa78f0983, 0x1f336ee6, 0x0d86c108, 0xb53aa66d, - 0xbd40e1a4, 0x05fc86c1, 0x1749292f, 0xaff54e4a, - 0x322276f3, 0x8a9e1196, 0x982bbe78, 0x2097d91d, - 0x78f4c94b, 0xc048ae2e, 0xd2fd01c0, 0x6a4166a5, - 0xf7965e1c, 0x4f2a3979, 0x5d9f9697, 0xe523f1f2, - 0x4d6b1905, 0xf5d77e60, 0xe762d18e, 0x5fdeb6eb, - 0xc2098e52, 0x7ab5e937, 0x680046d9, 0xd0bc21bc, - 0x88df31ea, 0x3063568f, 0x22d6f961, 0x9a6a9e04, - 0x07bda6bd, 0xbf01c1d8, 0xadb46e36, 0x15080953, - 0x1d724e9a, 0xa5ce29ff, 0xb77b8611, 0x0fc7e174, - 0x9210d9cd, 0x2aacbea8, 0x38191146, 0x80a57623, - 0xd8c66675, 0x607a0110, 0x72cfaefe, 0xca73c99b, - 0x57a4f122, 0xef189647, 0xfdad39a9, 0x45115ecc, - 0x764dee06, 0xcef18963, 0xdc44268d, 0x64f841e8, - 0xf92f7951, 0x41931e34, 0x5326b1da, 0xeb9ad6bf, - 0xb3f9c6e9, 0x0b45a18c, 0x19f00e62, 0xa14c6907, - 0x3c9b51be, 0x842736db, 0x96929935, 0x2e2efe50, - 0x2654b999, 0x9ee8defc, 0x8c5d7112, 0x34e11677, - 0xa9362ece, 0x118a49ab, 0x033fe645, 0xbb838120, - 0xe3e09176, 0x5b5cf613, 0x49e959fd, 0xf1553e98, - 0x6c820621, 0xd43e6144, 0xc68bceaa, 0x7e37a9cf, - 0xd67f4138, 0x6ec3265d, 0x7c7689b3, 0xc4caeed6, - 0x591dd66f, 0xe1a1b10a, 0xf3141ee4, 0x4ba87981, - 0x13cb69d7, 0xab770eb2, 0xb9c2a15c, 0x017ec639, - 0x9ca9fe80, 0x241599e5, 0x36a0360b, 0x8e1c516e, - 0x866616a7, 0x3eda71c2, 0x2c6fde2c, 0x94d3b949, - 0x090481f0, 0xb1b8e695, 0xa30d497b, 0x1bb12e1e, - 0x43d23e48, 0xfb6e592d, 0xe9dbf6c3, 0x516791a6, - 0xccb0a91f, 0x740cce7a, 0x66b96194, 0xde0506f1, - 0x00000000, 0x3d6029b0, 0x7ac05360, 0x47a07ad0, - 0xf580a6c0, 0xc8e08f70, 0x8f40f5a0, 0xb220dc10, - 0x30704bc1, 0x0d106271, 0x4ab018a1, 0x77d03111, - 0xc5f0ed01, 0xf890c4b1, 0xbf30be61, 0x825097d1, - 0x60e09782, 0x5d80be32, 0x1a20c4e2, 0x2740ed52, - 0x95603142, 0xa80018f2, 0xefa06222, 0xd2c04b92, - 0x5090dc43, 0x6df0f5f3, 0x2a508f23, 0x1730a693, - 0xa5107a83, 0x98705333, 0xdfd029e3, 0xe2b00053, - 0xc1c12f04, 0xfca106b4, 0xbb017c64, 0x866155d4, - 0x344189c4, 0x0921a074, 0x4e81daa4, 0x73e1f314, - 0xf1b164c5, 0xccd14d75, 0x8b7137a5, 0xb6111e15, - 0x0431c205, 0x3951ebb5, 0x7ef19165, 0x4391b8d5, - 0xa121b886, 0x9c419136, 0xdbe1ebe6, 0xe681c256, - 0x54a11e46, 0x69c137f6, 0x2e614d26, 0x13016496, - 0x9151f347, 0xac31daf7, 0xeb91a027, 0xd6f18997, - 0x64d15587, 0x59b17c37, 0x1e1106e7, 0x23712f57, - 0x58f35849, 0x659371f9, 0x22330b29, 0x1f532299, - 0xad73fe89, 0x9013d739, 0xd7b3ade9, 0xead38459, - 0x68831388, 0x55e33a38, 0x124340e8, 0x2f236958, - 0x9d03b548, 0xa0639cf8, 0xe7c3e628, 0xdaa3cf98, - 0x3813cfcb, 0x0573e67b, 0x42d39cab, 0x7fb3b51b, - 0xcd93690b, 0xf0f340bb, 0xb7533a6b, 0x8a3313db, - 0x0863840a, 0x3503adba, 0x72a3d76a, 0x4fc3feda, - 0xfde322ca, 0xc0830b7a, 0x872371aa, 0xba43581a, - 0x9932774d, 0xa4525efd, 0xe3f2242d, 0xde920d9d, - 0x6cb2d18d, 0x51d2f83d, 0x167282ed, 0x2b12ab5d, - 0xa9423c8c, 0x9422153c, 0xd3826fec, 0xeee2465c, - 0x5cc29a4c, 0x61a2b3fc, 0x2602c92c, 0x1b62e09c, - 0xf9d2e0cf, 0xc4b2c97f, 0x8312b3af, 0xbe729a1f, - 0x0c52460f, 0x31326fbf, 0x7692156f, 0x4bf23cdf, - 0xc9a2ab0e, 0xf4c282be, 0xb362f86e, 0x8e02d1de, - 0x3c220dce, 0x0142247e, 0x46e25eae, 0x7b82771e, - 0xb1e6b092, 0x8c869922, 0xcb26e3f2, 0xf646ca42, - 0x44661652, 0x79063fe2, 0x3ea64532, 0x03c66c82, - 0x8196fb53, 0xbcf6d2e3, 0xfb56a833, 0xc6368183, - 0x74165d93, 0x49767423, 0x0ed60ef3, 0x33b62743, - 0xd1062710, 0xec660ea0, 0xabc67470, 0x96a65dc0, - 0x248681d0, 0x19e6a860, 0x5e46d2b0, 0x6326fb00, - 0xe1766cd1, 0xdc164561, 0x9bb63fb1, 0xa6d61601, - 0x14f6ca11, 0x2996e3a1, 0x6e369971, 0x5356b0c1, - 0x70279f96, 0x4d47b626, 0x0ae7ccf6, 0x3787e546, - 0x85a73956, 0xb8c710e6, 0xff676a36, 0xc2074386, - 0x4057d457, 0x7d37fde7, 0x3a978737, 0x07f7ae87, - 0xb5d77297, 0x88b75b27, 0xcf1721f7, 0xf2770847, - 0x10c70814, 0x2da721a4, 0x6a075b74, 0x576772c4, - 0xe547aed4, 0xd8278764, 0x9f87fdb4, 0xa2e7d404, - 0x20b743d5, 0x1dd76a65, 0x5a7710b5, 0x67173905, - 0xd537e515, 0xe857cca5, 0xaff7b675, 0x92979fc5, - 0xe915e8db, 0xd475c16b, 0x93d5bbbb, 0xaeb5920b, - 0x1c954e1b, 0x21f567ab, 0x66551d7b, 0x5b3534cb, - 0xd965a31a, 0xe4058aaa, 0xa3a5f07a, 0x9ec5d9ca, - 0x2ce505da, 0x11852c6a, 0x562556ba, 0x6b457f0a, - 0x89f57f59, 0xb49556e9, 0xf3352c39, 0xce550589, - 0x7c75d999, 0x4115f029, 0x06b58af9, 0x3bd5a349, - 0xb9853498, 0x84e51d28, 0xc34567f8, 0xfe254e48, - 0x4c059258, 0x7165bbe8, 0x36c5c138, 0x0ba5e888, - 0x28d4c7df, 0x15b4ee6f, 0x521494bf, 0x6f74bd0f, - 0xdd54611f, 0xe03448af, 0xa794327f, 0x9af41bcf, - 0x18a48c1e, 0x25c4a5ae, 0x6264df7e, 0x5f04f6ce, - 0xed242ade, 0xd044036e, 0x97e479be, 0xaa84500e, - 0x4834505d, 0x755479ed, 0x32f4033d, 0x0f942a8d, - 0xbdb4f69d, 0x80d4df2d, 0xc774a5fd, 0xfa148c4d, - 0x78441b9c, 0x4524322c, 0x028448fc, 0x3fe4614c, - 0x8dc4bd5c, 0xb0a494ec, 0xf704ee3c, 0xca64c78c, - 0x00000000, 0xcb5cd3a5, 0x4dc8a10b, 0x869472ae, - 0x9b914216, 0x50cd91b3, 0xd659e31d, 0x1d0530b8, - 0xec53826d, 0x270f51c8, 0xa19b2366, 0x6ac7f0c3, - 0x77c2c07b, 0xbc9e13de, 0x3a0a6170, 0xf156b2d5, - 0x03d6029b, 0xc88ad13e, 0x4e1ea390, 0x85427035, - 0x9847408d, 0x531b9328, 0xd58fe186, 0x1ed33223, - 0xef8580f6, 0x24d95353, 0xa24d21fd, 0x6911f258, - 0x7414c2e0, 0xbf481145, 0x39dc63eb, 0xf280b04e, - 0x07ac0536, 0xccf0d693, 0x4a64a43d, 0x81387798, - 0x9c3d4720, 0x57619485, 0xd1f5e62b, 0x1aa9358e, - 0xebff875b, 0x20a354fe, 0xa6372650, 0x6d6bf5f5, - 0x706ec54d, 0xbb3216e8, 0x3da66446, 0xf6fab7e3, - 0x047a07ad, 0xcf26d408, 0x49b2a6a6, 0x82ee7503, - 0x9feb45bb, 0x54b7961e, 0xd223e4b0, 0x197f3715, - 0xe82985c0, 0x23755665, 0xa5e124cb, 0x6ebdf76e, - 0x73b8c7d6, 0xb8e41473, 0x3e7066dd, 0xf52cb578, - 0x0f580a6c, 0xc404d9c9, 0x4290ab67, 0x89cc78c2, - 0x94c9487a, 0x5f959bdf, 0xd901e971, 0x125d3ad4, - 0xe30b8801, 0x28575ba4, 0xaec3290a, 0x659ffaaf, - 0x789aca17, 0xb3c619b2, 0x35526b1c, 0xfe0eb8b9, - 0x0c8e08f7, 0xc7d2db52, 0x4146a9fc, 0x8a1a7a59, - 0x971f4ae1, 0x5c439944, 0xdad7ebea, 0x118b384f, - 0xe0dd8a9a, 0x2b81593f, 0xad152b91, 0x6649f834, - 0x7b4cc88c, 0xb0101b29, 0x36846987, 0xfdd8ba22, - 0x08f40f5a, 0xc3a8dcff, 0x453cae51, 0x8e607df4, - 0x93654d4c, 0x58399ee9, 0xdeadec47, 0x15f13fe2, - 0xe4a78d37, 0x2ffb5e92, 0xa96f2c3c, 0x6233ff99, - 0x7f36cf21, 0xb46a1c84, 0x32fe6e2a, 0xf9a2bd8f, - 0x0b220dc1, 0xc07ede64, 0x46eaacca, 0x8db67f6f, - 0x90b34fd7, 0x5bef9c72, 0xdd7beedc, 0x16273d79, - 0xe7718fac, 0x2c2d5c09, 0xaab92ea7, 0x61e5fd02, - 0x7ce0cdba, 0xb7bc1e1f, 0x31286cb1, 0xfa74bf14, - 0x1eb014d8, 0xd5ecc77d, 0x5378b5d3, 0x98246676, - 0x852156ce, 0x4e7d856b, 0xc8e9f7c5, 0x03b52460, - 0xf2e396b5, 0x39bf4510, 0xbf2b37be, 0x7477e41b, - 0x6972d4a3, 0xa22e0706, 0x24ba75a8, 0xefe6a60d, - 0x1d661643, 0xd63ac5e6, 0x50aeb748, 0x9bf264ed, - 0x86f75455, 0x4dab87f0, 0xcb3ff55e, 0x006326fb, - 0xf135942e, 0x3a69478b, 0xbcfd3525, 0x77a1e680, - 0x6aa4d638, 0xa1f8059d, 0x276c7733, 0xec30a496, - 0x191c11ee, 0xd240c24b, 0x54d4b0e5, 0x9f886340, - 0x828d53f8, 0x49d1805d, 0xcf45f2f3, 0x04192156, - 0xf54f9383, 0x3e134026, 0xb8873288, 0x73dbe12d, - 0x6eded195, 0xa5820230, 0x2316709e, 0xe84aa33b, - 0x1aca1375, 0xd196c0d0, 0x5702b27e, 0x9c5e61db, - 0x815b5163, 0x4a0782c6, 0xcc93f068, 0x07cf23cd, - 0xf6999118, 0x3dc542bd, 0xbb513013, 0x700de3b6, - 0x6d08d30e, 0xa65400ab, 0x20c07205, 0xeb9ca1a0, - 0x11e81eb4, 0xdab4cd11, 0x5c20bfbf, 0x977c6c1a, - 0x8a795ca2, 0x41258f07, 0xc7b1fda9, 0x0ced2e0c, - 0xfdbb9cd9, 0x36e74f7c, 0xb0733dd2, 0x7b2fee77, - 0x662adecf, 0xad760d6a, 0x2be27fc4, 0xe0beac61, - 0x123e1c2f, 0xd962cf8a, 0x5ff6bd24, 0x94aa6e81, - 0x89af5e39, 0x42f38d9c, 0xc467ff32, 0x0f3b2c97, - 0xfe6d9e42, 0x35314de7, 0xb3a53f49, 0x78f9ecec, - 0x65fcdc54, 0xaea00ff1, 0x28347d5f, 0xe368aefa, - 0x16441b82, 0xdd18c827, 0x5b8cba89, 0x90d0692c, - 0x8dd55994, 0x46898a31, 0xc01df89f, 0x0b412b3a, - 0xfa1799ef, 0x314b4a4a, 0xb7df38e4, 0x7c83eb41, - 0x6186dbf9, 0xaada085c, 0x2c4e7af2, 0xe712a957, - 0x15921919, 0xdececabc, 0x585ab812, 0x93066bb7, - 0x8e035b0f, 0x455f88aa, 0xc3cbfa04, 0x089729a1, - 0xf9c19b74, 0x329d48d1, 0xb4093a7f, 0x7f55e9da, - 0x6250d962, 0xa90c0ac7, 0x2f987869, 0xe4c4abcc, - 0x00000000, 0xa6770bb4, 0x979f1129, 0x31e81a9d, - 0xf44f2413, 0x52382fa7, 0x63d0353a, 0xc5a73e8e, - 0x33ef4e67, 0x959845d3, 0xa4705f4e, 0x020754fa, - 0xc7a06a74, 0x61d761c0, 0x503f7b5d, 0xf64870e9, - 0x67de9cce, 0xc1a9977a, 0xf0418de7, 0x56368653, - 0x9391b8dd, 0x35e6b369, 0x040ea9f4, 0xa279a240, - 0x5431d2a9, 0xf246d91d, 0xc3aec380, 0x65d9c834, - 0xa07ef6ba, 0x0609fd0e, 0x37e1e793, 0x9196ec27, - 0xcfbd399c, 0x69ca3228, 0x582228b5, 0xfe552301, - 0x3bf21d8f, 0x9d85163b, 0xac6d0ca6, 0x0a1a0712, - 0xfc5277fb, 0x5a257c4f, 0x6bcd66d2, 0xcdba6d66, - 0x081d53e8, 0xae6a585c, 0x9f8242c1, 0x39f54975, - 0xa863a552, 0x0e14aee6, 0x3ffcb47b, 0x998bbfcf, - 0x5c2c8141, 0xfa5b8af5, 0xcbb39068, 0x6dc49bdc, - 0x9b8ceb35, 0x3dfbe081, 0x0c13fa1c, 0xaa64f1a8, - 0x6fc3cf26, 0xc9b4c492, 0xf85cde0f, 0x5e2bd5bb, - 0x440b7579, 0xe27c7ecd, 0xd3946450, 0x75e36fe4, - 0xb044516a, 0x16335ade, 0x27db4043, 0x81ac4bf7, - 0x77e43b1e, 0xd19330aa, 0xe07b2a37, 0x460c2183, - 0x83ab1f0d, 0x25dc14b9, 0x14340e24, 0xb2430590, - 0x23d5e9b7, 0x85a2e203, 0xb44af89e, 0x123df32a, - 0xd79acda4, 0x71edc610, 0x4005dc8d, 0xe672d739, - 0x103aa7d0, 0xb64dac64, 0x87a5b6f9, 0x21d2bd4d, - 0xe47583c3, 0x42028877, 0x73ea92ea, 0xd59d995e, - 0x8bb64ce5, 0x2dc14751, 0x1c295dcc, 0xba5e5678, - 0x7ff968f6, 0xd98e6342, 0xe86679df, 0x4e11726b, - 0xb8590282, 0x1e2e0936, 0x2fc613ab, 0x89b1181f, - 0x4c162691, 0xea612d25, 0xdb8937b8, 0x7dfe3c0c, - 0xec68d02b, 0x4a1fdb9f, 0x7bf7c102, 0xdd80cab6, - 0x1827f438, 0xbe50ff8c, 0x8fb8e511, 0x29cfeea5, - 0xdf879e4c, 0x79f095f8, 0x48188f65, 0xee6f84d1, - 0x2bc8ba5f, 0x8dbfb1eb, 0xbc57ab76, 0x1a20a0c2, - 0x8816eaf2, 0x2e61e146, 0x1f89fbdb, 0xb9fef06f, - 0x7c59cee1, 0xda2ec555, 0xebc6dfc8, 0x4db1d47c, - 0xbbf9a495, 0x1d8eaf21, 0x2c66b5bc, 0x8a11be08, - 0x4fb68086, 0xe9c18b32, 0xd82991af, 0x7e5e9a1b, - 0xefc8763c, 0x49bf7d88, 0x78576715, 0xde206ca1, - 0x1b87522f, 0xbdf0599b, 0x8c184306, 0x2a6f48b2, - 0xdc27385b, 0x7a5033ef, 0x4bb82972, 0xedcf22c6, - 0x28681c48, 0x8e1f17fc, 0xbff70d61, 0x198006d5, - 0x47abd36e, 0xe1dcd8da, 0xd034c247, 0x7643c9f3, - 0xb3e4f77d, 0x1593fcc9, 0x247be654, 0x820cede0, - 0x74449d09, 0xd23396bd, 0xe3db8c20, 0x45ac8794, - 0x800bb91a, 0x267cb2ae, 0x1794a833, 0xb1e3a387, - 0x20754fa0, 0x86024414, 0xb7ea5e89, 0x119d553d, - 0xd43a6bb3, 0x724d6007, 0x43a57a9a, 0xe5d2712e, - 0x139a01c7, 0xb5ed0a73, 0x840510ee, 0x22721b5a, - 0xe7d525d4, 0x41a22e60, 0x704a34fd, 0xd63d3f49, - 0xcc1d9f8b, 0x6a6a943f, 0x5b828ea2, 0xfdf58516, - 0x3852bb98, 0x9e25b02c, 0xafcdaab1, 0x09baa105, - 0xfff2d1ec, 0x5985da58, 0x686dc0c5, 0xce1acb71, - 0x0bbdf5ff, 0xadcafe4b, 0x9c22e4d6, 0x3a55ef62, - 0xabc30345, 0x0db408f1, 0x3c5c126c, 0x9a2b19d8, - 0x5f8c2756, 0xf9fb2ce2, 0xc813367f, 0x6e643dcb, - 0x982c4d22, 0x3e5b4696, 0x0fb35c0b, 0xa9c457bf, - 0x6c636931, 0xca146285, 0xfbfc7818, 0x5d8b73ac, - 0x03a0a617, 0xa5d7ada3, 0x943fb73e, 0x3248bc8a, - 0xf7ef8204, 0x519889b0, 0x6070932d, 0xc6079899, - 0x304fe870, 0x9638e3c4, 0xa7d0f959, 0x01a7f2ed, - 0xc400cc63, 0x6277c7d7, 0x539fdd4a, 0xf5e8d6fe, - 0x647e3ad9, 0xc209316d, 0xf3e12bf0, 0x55962044, - 0x90311eca, 0x3646157e, 0x07ae0fe3, 0xa1d90457, - 0x579174be, 0xf1e67f0a, 0xc00e6597, 0x66796e23, - 0xa3de50ad, 0x05a95b19, 0x34414184, 0x92364a30, - 0x00000000, 0xccaa009e, 0x4225077d, 0x8e8f07e3, - 0x844a0efa, 0x48e00e64, 0xc66f0987, 0x0ac50919, - 0xd3e51bb5, 0x1f4f1b2b, 0x91c01cc8, 0x5d6a1c56, - 0x57af154f, 0x9b0515d1, 0x158a1232, 0xd92012ac, - 0x7cbb312b, 0xb01131b5, 0x3e9e3656, 0xf23436c8, - 0xf8f13fd1, 0x345b3f4f, 0xbad438ac, 0x767e3832, - 0xaf5e2a9e, 0x63f42a00, 0xed7b2de3, 0x21d12d7d, - 0x2b142464, 0xe7be24fa, 0x69312319, 0xa59b2387, - 0xf9766256, 0x35dc62c8, 0xbb53652b, 0x77f965b5, - 0x7d3c6cac, 0xb1966c32, 0x3f196bd1, 0xf3b36b4f, - 0x2a9379e3, 0xe639797d, 0x68b67e9e, 0xa41c7e00, - 0xaed97719, 0x62737787, 0xecfc7064, 0x205670fa, - 0x85cd537d, 0x496753e3, 0xc7e85400, 0x0b42549e, - 0x01875d87, 0xcd2d5d19, 0x43a25afa, 0x8f085a64, - 0x562848c8, 0x9a824856, 0x140d4fb5, 0xd8a74f2b, - 0xd2624632, 0x1ec846ac, 0x9047414f, 0x5ced41d1, - 0x299dc2ed, 0xe537c273, 0x6bb8c590, 0xa712c50e, - 0xadd7cc17, 0x617dcc89, 0xeff2cb6a, 0x2358cbf4, - 0xfa78d958, 0x36d2d9c6, 0xb85dde25, 0x74f7debb, - 0x7e32d7a2, 0xb298d73c, 0x3c17d0df, 0xf0bdd041, - 0x5526f3c6, 0x998cf358, 0x1703f4bb, 0xdba9f425, - 0xd16cfd3c, 0x1dc6fda2, 0x9349fa41, 0x5fe3fadf, - 0x86c3e873, 0x4a69e8ed, 0xc4e6ef0e, 0x084cef90, - 0x0289e689, 0xce23e617, 0x40ace1f4, 0x8c06e16a, - 0xd0eba0bb, 0x1c41a025, 0x92cea7c6, 0x5e64a758, - 0x54a1ae41, 0x980baedf, 0x1684a93c, 0xda2ea9a2, - 0x030ebb0e, 0xcfa4bb90, 0x412bbc73, 0x8d81bced, - 0x8744b5f4, 0x4beeb56a, 0xc561b289, 0x09cbb217, - 0xac509190, 0x60fa910e, 0xee7596ed, 0x22df9673, - 0x281a9f6a, 0xe4b09ff4, 0x6a3f9817, 0xa6959889, - 0x7fb58a25, 0xb31f8abb, 0x3d908d58, 0xf13a8dc6, - 0xfbff84df, 0x37558441, 0xb9da83a2, 0x7570833c, - 0x533b85da, 0x9f918544, 0x111e82a7, 0xddb48239, - 0xd7718b20, 0x1bdb8bbe, 0x95548c5d, 0x59fe8cc3, - 0x80de9e6f, 0x4c749ef1, 0xc2fb9912, 0x0e51998c, - 0x04949095, 0xc83e900b, 0x46b197e8, 0x8a1b9776, - 0x2f80b4f1, 0xe32ab46f, 0x6da5b38c, 0xa10fb312, - 0xabcaba0b, 0x6760ba95, 0xe9efbd76, 0x2545bde8, - 0xfc65af44, 0x30cfafda, 0xbe40a839, 0x72eaa8a7, - 0x782fa1be, 0xb485a120, 0x3a0aa6c3, 0xf6a0a65d, - 0xaa4de78c, 0x66e7e712, 0xe868e0f1, 0x24c2e06f, - 0x2e07e976, 0xe2ade9e8, 0x6c22ee0b, 0xa088ee95, - 0x79a8fc39, 0xb502fca7, 0x3b8dfb44, 0xf727fbda, - 0xfde2f2c3, 0x3148f25d, 0xbfc7f5be, 0x736df520, - 0xd6f6d6a7, 0x1a5cd639, 0x94d3d1da, 0x5879d144, - 0x52bcd85d, 0x9e16d8c3, 0x1099df20, 0xdc33dfbe, - 0x0513cd12, 0xc9b9cd8c, 0x4736ca6f, 0x8b9ccaf1, - 0x8159c3e8, 0x4df3c376, 0xc37cc495, 0x0fd6c40b, - 0x7aa64737, 0xb60c47a9, 0x3883404a, 0xf42940d4, - 0xfeec49cd, 0x32464953, 0xbcc94eb0, 0x70634e2e, - 0xa9435c82, 0x65e95c1c, 0xeb665bff, 0x27cc5b61, - 0x2d095278, 0xe1a352e6, 0x6f2c5505, 0xa386559b, - 0x061d761c, 0xcab77682, 0x44387161, 0x889271ff, - 0x825778e6, 0x4efd7878, 0xc0727f9b, 0x0cd87f05, - 0xd5f86da9, 0x19526d37, 0x97dd6ad4, 0x5b776a4a, - 0x51b26353, 0x9d1863cd, 0x1397642e, 0xdf3d64b0, - 0x83d02561, 0x4f7a25ff, 0xc1f5221c, 0x0d5f2282, - 0x079a2b9b, 0xcb302b05, 0x45bf2ce6, 0x89152c78, - 0x50353ed4, 0x9c9f3e4a, 0x121039a9, 0xdeba3937, - 0xd47f302e, 0x18d530b0, 0x965a3753, 0x5af037cd, - 0xff6b144a, 0x33c114d4, 0xbd4e1337, 0x71e413a9, - 0x7b211ab0, 0xb78b1a2e, 0x39041dcd, 0xf5ae1d53, - 0x2c8e0fff, 0xe0240f61, 0x6eab0882, 0xa201081c, - 0xa8c40105, 0x646e019b, 0xeae10678, 0x264b06e6, -}; diff --git a/dm/third_party/libdeflate/lib/decompress_template.h b/dm/third_party/libdeflate/lib/decompress_template.h deleted file mode 100644 index 8c874c3..0000000 --- a/dm/third_party/libdeflate/lib/decompress_template.h +++ /dev/null @@ -1,777 +0,0 @@ -/* - * decompress_template.h - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * This is the actual DEFLATE decompression routine, lifted out of - * deflate_decompress.c so that it can be compiled multiple times with different - * target instruction sets. - */ - -#ifndef ATTRIBUTES -# define ATTRIBUTES -#endif -#ifndef EXTRACT_VARBITS -# define EXTRACT_VARBITS(word, count) ((word) & BITMASK(count)) -#endif -#ifndef EXTRACT_VARBITS8 -# define EXTRACT_VARBITS8(word, count) ((word) & BITMASK((u8)(count))) -#endif - -static ATTRIBUTES MAYBE_UNUSED enum libdeflate_result -FUNCNAME(struct libdeflate_decompressor * restrict d, - const void * restrict in, size_t in_nbytes, - void * restrict out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, size_t *actual_out_nbytes_ret) -{ - u8 *out_next = out; - u8 * const out_end = out_next + out_nbytes_avail; - u8 * const out_fastloop_end = - out_end - MIN(out_nbytes_avail, FASTLOOP_MAX_BYTES_WRITTEN); - - /* Input bitstream state; see deflate_decompress.c for documentation */ - const u8 *in_next = in; - const u8 * const in_end = in_next + in_nbytes; - const u8 * const in_fastloop_end = - in_end - MIN(in_nbytes, FASTLOOP_MAX_BYTES_READ); - bitbuf_t bitbuf = 0; - bitbuf_t saved_bitbuf; - u32 bitsleft = 0; - size_t overread_count = 0; - - bool is_final_block; - unsigned block_type; - unsigned num_litlen_syms; - unsigned num_offset_syms; - bitbuf_t litlen_tablemask; - u32 entry; - -next_block: - /* Starting to read the next block */ - ; - - STATIC_ASSERT(CAN_CONSUME(1 + 2 + 5 + 5 + 4 + 3)); - REFILL_BITS(); - - /* BFINAL: 1 bit */ - is_final_block = bitbuf & BITMASK(1); - - /* BTYPE: 2 bits */ - block_type = (bitbuf >> 1) & BITMASK(2); - - if (block_type == DEFLATE_BLOCKTYPE_DYNAMIC_HUFFMAN) { - - /* Dynamic Huffman block */ - - /* The order in which precode lengths are stored */ - static const u8 deflate_precode_lens_permutation[DEFLATE_NUM_PRECODE_SYMS] = { - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 - }; - - unsigned num_explicit_precode_lens; - unsigned i; - - /* Read the codeword length counts. */ - - STATIC_ASSERT(DEFLATE_NUM_LITLEN_SYMS == 257 + BITMASK(5)); - num_litlen_syms = 257 + ((bitbuf >> 3) & BITMASK(5)); - - STATIC_ASSERT(DEFLATE_NUM_OFFSET_SYMS == 1 + BITMASK(5)); - num_offset_syms = 1 + ((bitbuf >> 8) & BITMASK(5)); - - STATIC_ASSERT(DEFLATE_NUM_PRECODE_SYMS == 4 + BITMASK(4)); - num_explicit_precode_lens = 4 + ((bitbuf >> 13) & BITMASK(4)); - - d->static_codes_loaded = false; - - /* - * Read the precode codeword lengths. - * - * A 64-bit bitbuffer is just one bit too small to hold the - * maximum number of precode lens, so to minimize branches we - * merge one len with the previous fields. - */ - STATIC_ASSERT(DEFLATE_MAX_PRE_CODEWORD_LEN == (1 << 3) - 1); - if (CAN_CONSUME(3 * (DEFLATE_NUM_PRECODE_SYMS - 1))) { - d->u.precode_lens[deflate_precode_lens_permutation[0]] = - (bitbuf >> 17) & BITMASK(3); - bitbuf >>= 20; - bitsleft -= 20; - REFILL_BITS(); - i = 1; - do { - d->u.precode_lens[deflate_precode_lens_permutation[i]] = - bitbuf & BITMASK(3); - bitbuf >>= 3; - bitsleft -= 3; - } while (++i < num_explicit_precode_lens); - } else { - bitbuf >>= 17; - bitsleft -= 17; - i = 0; - do { - if ((u8)bitsleft < 3) - REFILL_BITS(); - d->u.precode_lens[deflate_precode_lens_permutation[i]] = - bitbuf & BITMASK(3); - bitbuf >>= 3; - bitsleft -= 3; - } while (++i < num_explicit_precode_lens); - } - for (; i < DEFLATE_NUM_PRECODE_SYMS; i++) - d->u.precode_lens[deflate_precode_lens_permutation[i]] = 0; - - /* Build the decode table for the precode. */ - SAFETY_CHECK(build_precode_decode_table(d)); - - /* Decode the litlen and offset codeword lengths. */ - i = 0; - do { - unsigned presym; - u8 rep_val; - unsigned rep_count; - - if ((u8)bitsleft < DEFLATE_MAX_PRE_CODEWORD_LEN + 7) - REFILL_BITS(); - - /* - * The code below assumes that the precode decode table - * doesn't have any subtables. - */ - STATIC_ASSERT(PRECODE_TABLEBITS == DEFLATE_MAX_PRE_CODEWORD_LEN); - - /* Decode the next precode symbol. */ - entry = d->u.l.precode_decode_table[ - bitbuf & BITMASK(DEFLATE_MAX_PRE_CODEWORD_LEN)]; - bitbuf >>= (u8)entry; - bitsleft -= entry; /* optimization: subtract full entry */ - presym = entry >> 16; - - if (presym < 16) { - /* Explicit codeword length */ - d->u.l.lens[i++] = presym; - continue; - } - - /* Run-length encoded codeword lengths */ - - /* - * Note: we don't need to immediately verify that the - * repeat count doesn't overflow the number of elements, - * since we've sized the lens array to have enough extra - * space to allow for the worst-case overrun (138 zeroes - * when only 1 length was remaining). - * - * In the case of the small repeat counts (presyms 16 - * and 17), it is fastest to always write the maximum - * number of entries. That gets rid of branches that - * would otherwise be required. - * - * It is not just because of the numerical order that - * our checks go in the order 'presym < 16', 'presym == - * 16', and 'presym == 17'. For typical data this is - * ordered from most frequent to least frequent case. - */ - STATIC_ASSERT(DEFLATE_MAX_LENS_OVERRUN == 138 - 1); - - if (presym == 16) { - /* Repeat the previous length 3 - 6 times. */ - SAFETY_CHECK(i != 0); - rep_val = d->u.l.lens[i - 1]; - STATIC_ASSERT(3 + BITMASK(2) == 6); - rep_count = 3 + (bitbuf & BITMASK(2)); - bitbuf >>= 2; - bitsleft -= 2; - d->u.l.lens[i + 0] = rep_val; - d->u.l.lens[i + 1] = rep_val; - d->u.l.lens[i + 2] = rep_val; - d->u.l.lens[i + 3] = rep_val; - d->u.l.lens[i + 4] = rep_val; - d->u.l.lens[i + 5] = rep_val; - i += rep_count; - } else if (presym == 17) { - /* Repeat zero 3 - 10 times. */ - STATIC_ASSERT(3 + BITMASK(3) == 10); - rep_count = 3 + (bitbuf & BITMASK(3)); - bitbuf >>= 3; - bitsleft -= 3; - d->u.l.lens[i + 0] = 0; - d->u.l.lens[i + 1] = 0; - d->u.l.lens[i + 2] = 0; - d->u.l.lens[i + 3] = 0; - d->u.l.lens[i + 4] = 0; - d->u.l.lens[i + 5] = 0; - d->u.l.lens[i + 6] = 0; - d->u.l.lens[i + 7] = 0; - d->u.l.lens[i + 8] = 0; - d->u.l.lens[i + 9] = 0; - i += rep_count; - } else { - /* Repeat zero 11 - 138 times. */ - STATIC_ASSERT(11 + BITMASK(7) == 138); - rep_count = 11 + (bitbuf & BITMASK(7)); - bitbuf >>= 7; - bitsleft -= 7; - memset(&d->u.l.lens[i], 0, - rep_count * sizeof(d->u.l.lens[i])); - i += rep_count; - } - } while (i < num_litlen_syms + num_offset_syms); - - /* Unnecessary, but check this for consistency with zlib. */ - SAFETY_CHECK(i == num_litlen_syms + num_offset_syms); - - } else if (block_type == DEFLATE_BLOCKTYPE_UNCOMPRESSED) { - u16 len, nlen; - - /* - * Uncompressed block: copy 'len' bytes literally from the input - * buffer to the output buffer. - */ - - bitsleft -= 3; /* for BTYPE and BFINAL */ - - /* - * Align the bitstream to the next byte boundary. This means - * the next byte boundary as if we were reading a byte at a - * time. Therefore, we have to rewind 'in_next' by any bytes - * that have been refilled but not actually consumed yet (not - * counting overread bytes, which don't increment 'in_next'). - */ - bitsleft = (u8)bitsleft; - SAFETY_CHECK(overread_count <= (bitsleft >> 3)); - in_next -= (bitsleft >> 3) - overread_count; - overread_count = 0; - bitbuf = 0; - bitsleft = 0; - - SAFETY_CHECK(in_end - in_next >= 4); - len = get_unaligned_le16(in_next); - nlen = get_unaligned_le16(in_next + 2); - in_next += 4; - - SAFETY_CHECK(len == (u16)~nlen); - if (unlikely(len > out_end - out_next)) - return LIBDEFLATE_INSUFFICIENT_SPACE; - SAFETY_CHECK(len <= in_end - in_next); - - memcpy(out_next, in_next, len); - in_next += len; - out_next += len; - - goto block_done; - - } else { - unsigned i; - - SAFETY_CHECK(block_type == DEFLATE_BLOCKTYPE_STATIC_HUFFMAN); - - /* - * Static Huffman block: build the decode tables for the static - * codes. Skip doing so if the tables are already set up from - * an earlier static block; this speeds up decompression of - * degenerate input of many empty or very short static blocks. - * - * Afterwards, the remainder is the same as decompressing a - * dynamic Huffman block. - */ - - bitbuf >>= 3; /* for BTYPE and BFINAL */ - bitsleft -= 3; - - if (d->static_codes_loaded) - goto have_decode_tables; - - d->static_codes_loaded = true; - - STATIC_ASSERT(DEFLATE_NUM_LITLEN_SYMS == 288); - STATIC_ASSERT(DEFLATE_NUM_OFFSET_SYMS == 32); - - for (i = 0; i < 144; i++) - d->u.l.lens[i] = 8; - for (; i < 256; i++) - d->u.l.lens[i] = 9; - for (; i < 280; i++) - d->u.l.lens[i] = 7; - for (; i < 288; i++) - d->u.l.lens[i] = 8; - - for (; i < 288 + 32; i++) - d->u.l.lens[i] = 5; - - num_litlen_syms = 288; - num_offset_syms = 32; - } - - /* Decompressing a Huffman block (either dynamic or static) */ - - SAFETY_CHECK(build_offset_decode_table(d, num_litlen_syms, num_offset_syms)); - SAFETY_CHECK(build_litlen_decode_table(d, num_litlen_syms, num_offset_syms)); -have_decode_tables: - litlen_tablemask = BITMASK(d->litlen_tablebits); - - /* - * This is the "fastloop" for decoding literals and matches. It does - * bounds checks on in_next and out_next in the loop conditions so that - * additional bounds checks aren't needed inside the loop body. - * - * To reduce latency, the bitbuffer is refilled and the next litlen - * decode table entry is preloaded before each loop iteration. - */ - if (in_next >= in_fastloop_end || out_next >= out_fastloop_end) - goto generic_loop; - REFILL_BITS_IN_FASTLOOP(); - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - do { - u32 length, offset, lit; - const u8 *src; - u8 *dst; - - /* - * Consume the bits for the litlen decode table entry. Save the - * original bitbuf for later, in case the extra match length - * bits need to be extracted from it. - */ - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; /* optimization: subtract full entry */ - - /* - * Begin by checking for a "fast" literal, i.e. a literal that - * doesn't need a subtable. - */ - if (entry & HUFFDEC_LITERAL) { - /* - * On 64-bit platforms, we decode up to 2 extra fast - * literals in addition to the primary item, as this - * increases performance and still leaves enough bits - * remaining for what follows. We could actually do 3, - * assuming LITLEN_TABLEBITS=11, but that actually - * decreases performance slightly (perhaps by messing - * with the branch prediction of the conditional refill - * that happens later while decoding the match offset). - * - * Note: the definitions of FASTLOOP_MAX_BYTES_WRITTEN - * and FASTLOOP_MAX_BYTES_READ need to be updated if the - * number of extra literals decoded here is changed. - */ - if (/* enough bits for 2 fast literals + length + offset preload? */ - CAN_CONSUME_AND_THEN_PRELOAD(2 * LITLEN_TABLEBITS + - LENGTH_MAXBITS, - OFFSET_TABLEBITS) && - /* enough bits for 2 fast literals + slow literal + litlen preload? */ - CAN_CONSUME_AND_THEN_PRELOAD(2 * LITLEN_TABLEBITS + - DEFLATE_MAX_LITLEN_CODEWORD_LEN, - LITLEN_TABLEBITS)) { - /* 1st extra fast literal */ - lit = entry >> 16; - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; - *out_next++ = lit; - if (entry & HUFFDEC_LITERAL) { - /* 2nd extra fast literal */ - lit = entry >> 16; - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; - *out_next++ = lit; - if (entry & HUFFDEC_LITERAL) { - /* - * Another fast literal, but - * this one is in lieu of the - * primary item, so it doesn't - * count as one of the extras. - */ - lit = entry >> 16; - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - REFILL_BITS_IN_FASTLOOP(); - *out_next++ = lit; - continue; - } - } - } else { - /* - * Decode a literal. While doing so, preload - * the next litlen decode table entry and refill - * the bitbuffer. To reduce latency, we've - * arranged for there to be enough "preloadable" - * bits remaining to do the table preload - * independently of the refill. - */ - STATIC_ASSERT(CAN_CONSUME_AND_THEN_PRELOAD( - LITLEN_TABLEBITS, LITLEN_TABLEBITS)); - lit = entry >> 16; - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - REFILL_BITS_IN_FASTLOOP(); - *out_next++ = lit; - continue; - } - } - - /* - * It's not a literal entry, so it can be a length entry, a - * subtable pointer entry, or an end-of-block entry. Detect the - * two unlikely cases by testing the HUFFDEC_EXCEPTIONAL flag. - */ - if (unlikely(entry & HUFFDEC_EXCEPTIONAL)) { - /* Subtable pointer or end-of-block entry */ - - if (unlikely(entry & HUFFDEC_END_OF_BLOCK)) - goto block_done; - - /* - * A subtable is required. Load and consume the - * subtable entry. The subtable entry can be of any - * type: literal, length, or end-of-block. - */ - entry = d->u.litlen_decode_table[(entry >> 16) + - EXTRACT_VARBITS(bitbuf, (entry >> 8) & 0x3F)]; - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; - - /* - * 32-bit platforms that use the byte-at-a-time refill - * method have to do a refill here for there to always - * be enough bits to decode a literal that requires a - * subtable, then preload the next litlen decode table - * entry; or to decode a match length that requires a - * subtable, then preload the offset decode table entry. - */ - if (!CAN_CONSUME_AND_THEN_PRELOAD(DEFLATE_MAX_LITLEN_CODEWORD_LEN, - LITLEN_TABLEBITS) || - !CAN_CONSUME_AND_THEN_PRELOAD(LENGTH_MAXBITS, - OFFSET_TABLEBITS)) - REFILL_BITS_IN_FASTLOOP(); - if (entry & HUFFDEC_LITERAL) { - /* Decode a literal that required a subtable. */ - lit = entry >> 16; - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - REFILL_BITS_IN_FASTLOOP(); - *out_next++ = lit; - continue; - } - if (unlikely(entry & HUFFDEC_END_OF_BLOCK)) - goto block_done; - /* Else, it's a length that required a subtable. */ - } - - /* - * Decode the match length: the length base value associated - * with the litlen symbol (which we extract from the decode - * table entry), plus the extra length bits. We don't need to - * consume the extra length bits here, as they were included in - * the bits consumed by the entry earlier. We also don't need - * to check for too-long matches here, as this is inside the - * fastloop where it's already been verified that the output - * buffer has enough space remaining to copy a max-length match. - */ - length = entry >> 16; - length += EXTRACT_VARBITS8(saved_bitbuf, entry) >> (u8)(entry >> 8); - - /* - * Decode the match offset. There are enough "preloadable" bits - * remaining to preload the offset decode table entry, but a - * refill might be needed before consuming it. - */ - STATIC_ASSERT(CAN_CONSUME_AND_THEN_PRELOAD(LENGTH_MAXFASTBITS, - OFFSET_TABLEBITS)); - entry = d->offset_decode_table[bitbuf & BITMASK(OFFSET_TABLEBITS)]; - if (CAN_CONSUME_AND_THEN_PRELOAD(OFFSET_MAXBITS, - LITLEN_TABLEBITS)) { - /* - * Decoding a match offset on a 64-bit platform. We may - * need to refill once, but then we can decode the whole - * offset and preload the next litlen table entry. - */ - if (unlikely(entry & HUFFDEC_EXCEPTIONAL)) { - /* Offset codeword requires a subtable */ - if (unlikely((u8)bitsleft < OFFSET_MAXBITS + - LITLEN_TABLEBITS - PRELOAD_SLACK)) - REFILL_BITS_IN_FASTLOOP(); - bitbuf >>= OFFSET_TABLEBITS; - bitsleft -= OFFSET_TABLEBITS; - entry = d->offset_decode_table[(entry >> 16) + - EXTRACT_VARBITS(bitbuf, (entry >> 8) & 0x3F)]; - } else if (unlikely((u8)bitsleft < OFFSET_MAXFASTBITS + - LITLEN_TABLEBITS - PRELOAD_SLACK)) - REFILL_BITS_IN_FASTLOOP(); - } else { - /* Decoding a match offset on a 32-bit platform */ - REFILL_BITS_IN_FASTLOOP(); - if (unlikely(entry & HUFFDEC_EXCEPTIONAL)) { - /* Offset codeword requires a subtable */ - bitbuf >>= OFFSET_TABLEBITS; - bitsleft -= OFFSET_TABLEBITS; - entry = d->offset_decode_table[(entry >> 16) + - EXTRACT_VARBITS(bitbuf, (entry >> 8) & 0x3F)]; - REFILL_BITS_IN_FASTLOOP(); - /* No further refill needed before extra bits */ - STATIC_ASSERT(CAN_CONSUME( - OFFSET_MAXBITS - OFFSET_TABLEBITS)); - } else { - /* No refill needed before extra bits */ - STATIC_ASSERT(CAN_CONSUME(OFFSET_MAXFASTBITS)); - } - } - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; /* optimization: subtract full entry */ - offset = entry >> 16; - offset += EXTRACT_VARBITS8(saved_bitbuf, entry) >> (u8)(entry >> 8); - - /* Validate the match offset; needed even in the fastloop. */ - SAFETY_CHECK(offset <= out_next - (const u8 *)out); - src = out_next - offset; - dst = out_next; - out_next += length; - - /* - * Before starting to issue the instructions to copy the match, - * refill the bitbuffer and preload the litlen decode table - * entry for the next loop iteration. This can increase - * performance by allowing the latency of the match copy to - * overlap with these other operations. To further reduce - * latency, we've arranged for there to be enough bits remaining - * to do the table preload independently of the refill, except - * on 32-bit platforms using the byte-at-a-time refill method. - */ - if (!CAN_CONSUME_AND_THEN_PRELOAD( - MAX(OFFSET_MAXBITS - OFFSET_TABLEBITS, - OFFSET_MAXFASTBITS), - LITLEN_TABLEBITS) && - unlikely((u8)bitsleft < LITLEN_TABLEBITS - PRELOAD_SLACK)) - REFILL_BITS_IN_FASTLOOP(); - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - REFILL_BITS_IN_FASTLOOP(); - - /* - * Copy the match. On most CPUs the fastest method is a - * word-at-a-time copy, unconditionally copying about 5 words - * since this is enough for most matches without being too much. - * - * The normal word-at-a-time copy works for offset >= WORDBYTES, - * which is most cases. The case of offset == 1 is also common - * and is worth optimizing for, since it is just RLE encoding of - * the previous byte, which is the result of compressing long - * runs of the same byte. - * - * Writing past the match 'length' is allowed here, since it's - * been ensured there is enough output space left for a slight - * overrun. FASTLOOP_MAX_BYTES_WRITTEN needs to be updated if - * the maximum possible overrun here is changed. - */ - if (UNALIGNED_ACCESS_IS_FAST && offset >= WORDBYTES) { - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - while (dst < out_next) { - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - store_word_unaligned(load_word_unaligned(src), dst); - src += WORDBYTES; - dst += WORDBYTES; - } - } else if (UNALIGNED_ACCESS_IS_FAST && offset == 1) { - machine_word_t v; - - /* - * This part tends to get auto-vectorized, so keep it - * copying a multiple of 16 bytes at a time. - */ - v = (machine_word_t)0x0101010101010101 * src[0]; - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - while (dst < out_next) { - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - store_word_unaligned(v, dst); - dst += WORDBYTES; - } - } else if (UNALIGNED_ACCESS_IS_FAST) { - store_word_unaligned(load_word_unaligned(src), dst); - src += offset; - dst += offset; - store_word_unaligned(load_word_unaligned(src), dst); - src += offset; - dst += offset; - do { - store_word_unaligned(load_word_unaligned(src), dst); - src += offset; - dst += offset; - store_word_unaligned(load_word_unaligned(src), dst); - src += offset; - dst += offset; - } while (dst < out_next); - } else { - *dst++ = *src++; - *dst++ = *src++; - do { - *dst++ = *src++; - } while (dst < out_next); - } - } while (in_next < in_fastloop_end && out_next < out_fastloop_end); - - /* - * This is the generic loop for decoding literals and matches. This - * handles cases where in_next and out_next are close to the end of - * their respective buffers. Usually this loop isn't performance- - * critical, as most time is spent in the fastloop above instead. We - * therefore omit some optimizations here in favor of smaller code. - */ -generic_loop: - for (;;) { - u32 length, offset; - const u8 *src; - u8 *dst; - - REFILL_BITS(); - entry = d->u.litlen_decode_table[bitbuf & litlen_tablemask]; - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; - if (unlikely(entry & HUFFDEC_SUBTABLE_POINTER)) { - entry = d->u.litlen_decode_table[(entry >> 16) + - EXTRACT_VARBITS(bitbuf, (entry >> 8) & 0x3F)]; - saved_bitbuf = bitbuf; - bitbuf >>= (u8)entry; - bitsleft -= entry; - } - length = entry >> 16; - if (entry & HUFFDEC_LITERAL) { - if (unlikely(out_next == out_end)) - return LIBDEFLATE_INSUFFICIENT_SPACE; - *out_next++ = length; - continue; - } - if (unlikely(entry & HUFFDEC_END_OF_BLOCK)) - goto block_done; - length += EXTRACT_VARBITS8(saved_bitbuf, entry) >> (u8)(entry >> 8); - if (unlikely(length > out_end - out_next)) - return LIBDEFLATE_INSUFFICIENT_SPACE; - - if (!CAN_CONSUME(LENGTH_MAXBITS + OFFSET_MAXBITS)) - REFILL_BITS(); - entry = d->offset_decode_table[bitbuf & BITMASK(OFFSET_TABLEBITS)]; - if (unlikely(entry & HUFFDEC_EXCEPTIONAL)) { - bitbuf >>= OFFSET_TABLEBITS; - bitsleft -= OFFSET_TABLEBITS; - entry = d->offset_decode_table[(entry >> 16) + - EXTRACT_VARBITS(bitbuf, (entry >> 8) & 0x3F)]; - if (!CAN_CONSUME(OFFSET_MAXBITS)) - REFILL_BITS(); - } - offset = entry >> 16; - offset += EXTRACT_VARBITS8(bitbuf, entry) >> (u8)(entry >> 8); - bitbuf >>= (u8)entry; - bitsleft -= entry; - - SAFETY_CHECK(offset <= out_next - (const u8 *)out); - src = out_next - offset; - dst = out_next; - out_next += length; - - STATIC_ASSERT(DEFLATE_MIN_MATCH_LEN == 3); - *dst++ = *src++; - *dst++ = *src++; - do { - *dst++ = *src++; - } while (dst < out_next); - } - -block_done: - /* Finished decoding a block */ - - if (!is_final_block) - goto next_block; - - /* That was the last block. */ - - bitsleft = (u8)bitsleft; - - /* - * If any of the implicit appended zero bytes were consumed (not just - * refilled) before hitting end of stream, then the data is bad. - */ - SAFETY_CHECK(overread_count <= (bitsleft >> 3)); - - /* Optionally return the actual number of bytes consumed. */ - if (actual_in_nbytes_ret) { - /* Don't count bytes that were refilled but not consumed. */ - in_next -= (bitsleft >> 3) - overread_count; - - *actual_in_nbytes_ret = in_next - (u8 *)in; - } - - /* Optionally return the actual number of bytes written. */ - if (actual_out_nbytes_ret) { - *actual_out_nbytes_ret = out_next - (u8 *)out; - } else { - if (out_next != out_end) - return LIBDEFLATE_SHORT_OUTPUT; - } - return LIBDEFLATE_SUCCESS; -} - -#undef FUNCNAME -#undef ATTRIBUTES -#undef EXTRACT_VARBITS -#undef EXTRACT_VARBITS8 diff --git a/dm/third_party/libdeflate/lib/deflate_compress.c b/dm/third_party/libdeflate/lib/deflate_compress.c deleted file mode 100644 index b24087c..0000000 --- a/dm/third_party/libdeflate/lib/deflate_compress.c +++ /dev/null @@ -1,4135 +0,0 @@ -/* - * deflate_compress.c - a compressor for DEFLATE - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "deflate_compress.h" -#include "deflate_constants.h" - -/******************************************************************************/ - -/* - * The following parameters can be changed at build time to customize the - * compression algorithms slightly: - * - * (Note, not all customizable parameters are here. Some others can be found in - * libdeflate_alloc_compressor() and in *_matchfinder.h.) - */ - -/* - * If this parameter is defined to 1, then the near-optimal parsing algorithm - * will be included, and compression levels 10-12 will use it. This algorithm - * usually produces a compression ratio significantly better than the other - * algorithms. However, it is slow. If this parameter is defined to 0, then - * levels 10-12 will be the same as level 9 and will use the lazy2 algorithm. - */ -#define SUPPORT_NEAR_OPTIMAL_PARSING 1 - -/* - * This is the minimum block length that the compressor will use, in - * uncompressed bytes. This should be a value below which using shorter blocks - * is unlikely to be worthwhile, due to the per-block overhead. This value does - * not apply to the final block, which may be shorter than this (if the input is - * shorter, it will have to be), or to the final uncompressed block in a series - * of uncompressed blocks that cover more than UINT16_MAX bytes. - * - * This value is also approximately the amount by which what would otherwise be - * the second-to-last block is allowed to grow past the soft maximum length in - * order to avoid having to use a very short final block. - * - * Defining a fixed minimum block length is needed in order to guarantee a - * reasonable upper bound on the compressed size. It's also needed because our - * block splitting algorithm doesn't work well on very short blocks. - */ -#define MIN_BLOCK_LENGTH 5000 - -/* - * For the greedy, lazy, lazy2, and near-optimal compressors: This is the soft - * maximum block length, in uncompressed bytes. The compressor will try to end - * blocks at this length, but it may go slightly past it if there is a match - * that straddles this limit or if the input data ends soon after this limit. - * This parameter doesn't apply to uncompressed blocks, which the DEFLATE format - * limits to 65535 bytes. - * - * This should be a value above which it is very likely that splitting the block - * would produce a better compression ratio. For the near-optimal compressor, - * increasing/decreasing this parameter will increase/decrease per-compressor - * memory usage linearly. - */ -#define SOFT_MAX_BLOCK_LENGTH 300000 - -/* - * For the greedy, lazy, and lazy2 compressors: this is the length of the - * sequence store, which is an array where the compressor temporarily stores - * matches that it's going to use in the current block. This value is the - * maximum number of matches that can be used in a block. If the sequence store - * fills up, then the compressor will be forced to end the block early. This - * value should be large enough so that this rarely happens, due to the block - * being ended normally before then. Increasing/decreasing this value will - * increase/decrease per-compressor memory usage linearly. - */ -#define SEQ_STORE_LENGTH 50000 - -/* - * For deflate_compress_fastest(): This is the soft maximum block length. - * deflate_compress_fastest() doesn't use the regular block splitting algorithm; - * it only ends blocks when they reach FAST_SOFT_MAX_BLOCK_LENGTH bytes or - * FAST_SEQ_STORE_LENGTH matches. Therefore, this value should be lower than - * the regular SOFT_MAX_BLOCK_LENGTH. - */ -#define FAST_SOFT_MAX_BLOCK_LENGTH 65535 - -/* - * For deflate_compress_fastest(): this is the length of the sequence store. - * This is like SEQ_STORE_LENGTH, but this should be a lower value. - */ -#define FAST_SEQ_STORE_LENGTH 8192 - -/* - * These are the maximum codeword lengths, in bits, the compressor will use for - * each Huffman code. The DEFLATE format defines limits for these. However, - * further limiting litlen codewords to 14 bits is beneficial, since it has - * negligible effect on compression ratio but allows some optimizations when - * outputting bits. (It allows 4 literals to be written at once rather than 3.) - */ -#define MAX_LITLEN_CODEWORD_LEN 14 -#define MAX_OFFSET_CODEWORD_LEN DEFLATE_MAX_OFFSET_CODEWORD_LEN -#define MAX_PRE_CODEWORD_LEN DEFLATE_MAX_PRE_CODEWORD_LEN - -#if SUPPORT_NEAR_OPTIMAL_PARSING - -/* Parameters specific to the near-optimal parsing algorithm */ - -/* - * BIT_COST is a scaling factor that allows the near-optimal compressor to - * consider fractional bit costs when deciding which literal/match sequence to - * use. This is useful when the true symbol costs are unknown. For example, if - * the compressor thinks that a symbol has 6.5 bits of entropy, it can set its - * cost to 6.5 bits rather than have to use 6 or 7 bits. Although in the end - * each symbol will use a whole number of bits due to the Huffman coding, - * considering fractional bits can be helpful due to the limited information. - * - * BIT_COST should be a power of 2. A value of 8 or 16 works well. A higher - * value isn't very useful since the calculations are approximate anyway. - * - * BIT_COST doesn't apply to deflate_flush_block() and - * deflate_compute_true_cost(), which consider whole bits. - */ -#define BIT_COST 16 - -/* - * The NOSTAT_BITS value for a given alphabet is the number of bits assumed to - * be needed to output a symbol that was unused in the previous optimization - * pass. Assigning a default cost allows the symbol to be used in the next - * optimization pass. However, the cost should be relatively high because the - * symbol probably won't be used very many times (if at all). - */ -#define LITERAL_NOSTAT_BITS 13 -#define LENGTH_NOSTAT_BITS 13 -#define OFFSET_NOSTAT_BITS 10 - -/* - * This is (slightly less than) the maximum number of matches that the - * near-optimal compressor will cache per block. This behaves similarly to - * SEQ_STORE_LENGTH for the other compressors. - */ -#define MATCH_CACHE_LENGTH (SOFT_MAX_BLOCK_LENGTH * 5) - -#endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - -/******************************************************************************/ - -/* Include the needed matchfinders. */ -#define MATCHFINDER_WINDOW_ORDER DEFLATE_WINDOW_ORDER -#include "hc_matchfinder.h" -#include "ht_matchfinder.h" -#if SUPPORT_NEAR_OPTIMAL_PARSING -# include "bt_matchfinder.h" -/* - * This is the maximum number of matches the binary trees matchfinder can find - * at a single position. Since the matchfinder never finds more than one match - * for the same length, presuming one of each possible length is sufficient for - * an upper bound. (This says nothing about whether it is worthwhile to - * consider so many matches; this is just defining the worst case.) - */ -#define MAX_MATCHES_PER_POS \ - (DEFLATE_MAX_MATCH_LEN - DEFLATE_MIN_MATCH_LEN + 1) -#endif - -/* - * The largest block length we will ever use is when the final block is of - * length SOFT_MAX_BLOCK_LENGTH + MIN_BLOCK_LENGTH - 1, or when any block is of - * length SOFT_MAX_BLOCK_LENGTH + 1 + DEFLATE_MAX_MATCH_LEN. The latter case - * occurs when the lazy2 compressor chooses two literals and a maximum-length - * match, starting at SOFT_MAX_BLOCK_LENGTH - 1. - */ -#define MAX_BLOCK_LENGTH \ - MAX(SOFT_MAX_BLOCK_LENGTH + MIN_BLOCK_LENGTH - 1, \ - SOFT_MAX_BLOCK_LENGTH + 1 + DEFLATE_MAX_MATCH_LEN) - -static forceinline void -check_buildtime_parameters(void) -{ - /* - * Verify that MIN_BLOCK_LENGTH is being honored, as - * libdeflate_deflate_compress_bound() depends on it. - */ - STATIC_ASSERT(SOFT_MAX_BLOCK_LENGTH >= MIN_BLOCK_LENGTH); - STATIC_ASSERT(FAST_SOFT_MAX_BLOCK_LENGTH >= MIN_BLOCK_LENGTH); - STATIC_ASSERT(SEQ_STORE_LENGTH * DEFLATE_MIN_MATCH_LEN >= - MIN_BLOCK_LENGTH); - STATIC_ASSERT(FAST_SEQ_STORE_LENGTH * HT_MATCHFINDER_MIN_MATCH_LEN >= - MIN_BLOCK_LENGTH); -#if SUPPORT_NEAR_OPTIMAL_PARSING - STATIC_ASSERT(MIN_BLOCK_LENGTH * MAX_MATCHES_PER_POS <= - MATCH_CACHE_LENGTH); -#endif - - /* The definition of MAX_BLOCK_LENGTH assumes this. */ - STATIC_ASSERT(FAST_SOFT_MAX_BLOCK_LENGTH <= SOFT_MAX_BLOCK_LENGTH); - - /* Verify that the sequence stores aren't uselessly large. */ - STATIC_ASSERT(SEQ_STORE_LENGTH * DEFLATE_MIN_MATCH_LEN <= - SOFT_MAX_BLOCK_LENGTH + MIN_BLOCK_LENGTH); - STATIC_ASSERT(FAST_SEQ_STORE_LENGTH * HT_MATCHFINDER_MIN_MATCH_LEN <= - FAST_SOFT_MAX_BLOCK_LENGTH + MIN_BLOCK_LENGTH); - - /* Verify that the maximum codeword lengths are valid. */ - STATIC_ASSERT( - MAX_LITLEN_CODEWORD_LEN <= DEFLATE_MAX_LITLEN_CODEWORD_LEN); - STATIC_ASSERT( - MAX_OFFSET_CODEWORD_LEN <= DEFLATE_MAX_OFFSET_CODEWORD_LEN); - STATIC_ASSERT( - MAX_PRE_CODEWORD_LEN <= DEFLATE_MAX_PRE_CODEWORD_LEN); - STATIC_ASSERT( - (1U << MAX_LITLEN_CODEWORD_LEN) >= DEFLATE_NUM_LITLEN_SYMS); - STATIC_ASSERT( - (1U << MAX_OFFSET_CODEWORD_LEN) >= DEFLATE_NUM_OFFSET_SYMS); - STATIC_ASSERT( - (1U << MAX_PRE_CODEWORD_LEN) >= DEFLATE_NUM_PRECODE_SYMS); -} - -/******************************************************************************/ - -/* Table: length slot => length slot base value */ -static const u32 deflate_length_slot_base[] = { - 3, 4, 5, 6, 7, 8, 9, 10, - 11, 13, 15, 17, 19, 23, 27, 31, - 35, 43, 51, 59, 67, 83, 99, 115, - 131, 163, 195, 227, 258, -}; - -/* Table: length slot => number of extra length bits */ -static const u8 deflate_extra_length_bits[] = { - 0, 0, 0, 0, 0, 0, 0, 0, - 1, 1, 1, 1, 2, 2, 2, 2, - 3, 3, 3, 3, 4, 4, 4, 4, - 5, 5, 5, 5, 0, -}; - -/* Table: offset slot => offset slot base value */ -static const u32 deflate_offset_slot_base[] = { - 1, 2, 3, 4, 5, 7, 9, 13, - 17, 25, 33, 49, 65, 97, 129, 193, - 257, 385, 513, 769, 1025, 1537, 2049, 3073, - 4097, 6145, 8193, 12289, 16385, 24577, -}; - -/* Table: offset slot => number of extra offset bits */ -static const u8 deflate_extra_offset_bits[] = { - 0, 0, 0, 0, 1, 1, 2, 2, - 3, 3, 4, 4, 5, 5, 6, 6, - 7, 7, 8, 8, 9, 9, 10, 10, - 11, 11, 12, 12, 13, 13, -}; - -/* Table: length => length slot */ -static const u8 deflate_length_slot[DEFLATE_MAX_MATCH_LEN + 1] = { - 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, - 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, - 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, - 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, - 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, - 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, - 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, - 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, - 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, - 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, - 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, - 27, 27, 28, -}; - -/* - * Table: 'offset - 1 => offset_slot' for offset <= 256. - * This was generated by scripts/gen_offset_slot_map.py. - */ -static const u8 deflate_offset_slot[256] = { - 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, - 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, - 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, - 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, -}; - -/* The order in which precode codeword lengths are stored */ -static const u8 deflate_precode_lens_permutation[DEFLATE_NUM_PRECODE_SYMS] = { - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 -}; - -/* Table: precode symbol => number of extra bits */ -static const u8 deflate_extra_precode_bits[DEFLATE_NUM_PRECODE_SYMS] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7 -}; - -/* Codewords for the DEFLATE Huffman codes */ -struct deflate_codewords { - u32 litlen[DEFLATE_NUM_LITLEN_SYMS]; - u32 offset[DEFLATE_NUM_OFFSET_SYMS]; -}; - -/* - * Codeword lengths (in bits) for the DEFLATE Huffman codes. - * A zero length means the corresponding symbol had zero frequency. - */ -struct deflate_lens { - u8 litlen[DEFLATE_NUM_LITLEN_SYMS]; - u8 offset[DEFLATE_NUM_OFFSET_SYMS]; -}; - -/* Codewords and lengths for the DEFLATE Huffman codes */ -struct deflate_codes { - struct deflate_codewords codewords; - struct deflate_lens lens; -}; - -/* Symbol frequency counters for the DEFLATE Huffman codes */ -struct deflate_freqs { - u32 litlen[DEFLATE_NUM_LITLEN_SYMS]; - u32 offset[DEFLATE_NUM_OFFSET_SYMS]; -}; - -/* - * Represents a run of literals followed by a match or end-of-block. This - * struct is needed to temporarily store items chosen by the parser, since items - * cannot be written until all items for the block have been chosen and the - * block's Huffman codes have been computed. - */ -struct deflate_sequence { - - /* - * Bits 0..22: the number of literals in this run. This may be 0 and - * can be at most MAX_BLOCK_LENGTH. The literals are not stored - * explicitly in this structure; instead, they are read directly from - * the uncompressed data. - * - * Bits 23..31: the length of the match which follows the literals, or 0 - * if this literal run was the last in the block, so there is no match - * which follows it. - */ -#define SEQ_LENGTH_SHIFT 23 -#define SEQ_LITRUNLEN_MASK (((u32)1 << SEQ_LENGTH_SHIFT) - 1) - u32 litrunlen_and_length; - - /* - * If 'length' doesn't indicate end-of-block, then this is the offset of - * the match which follows the literals. - */ - u16 offset; - - /* - * If 'length' doesn't indicate end-of-block, then this is the offset - * slot of the match which follows the literals. - */ - u16 offset_slot; -}; - -#if SUPPORT_NEAR_OPTIMAL_PARSING - -/* Costs for the near-optimal parsing algorithm */ -struct deflate_costs { - - /* The cost to output each possible literal */ - u32 literal[DEFLATE_NUM_LITERALS]; - - /* The cost to output each possible match length */ - u32 length[DEFLATE_MAX_MATCH_LEN + 1]; - - /* The cost to output a match offset of each possible offset slot */ - u32 offset_slot[DEFLATE_NUM_OFFSET_SYMS]; -}; - -/* - * This structure represents a byte position in the input data and a node in the - * graph of possible match/literal choices for the current block. - * - * Logically, each incoming edge to this node is labeled with a literal or a - * match that can be taken to reach this position from an earlier position; and - * each outgoing edge from this node is labeled with a literal or a match that - * can be taken to advance from this position to a later position. - * - * But these "edges" are actually stored elsewhere (in 'match_cache'). Here we - * associate with each node just two pieces of information: - * - * 'cost_to_end' is the minimum cost to reach the end of the block from - * this position. - * - * 'item' represents the literal or match that must be chosen from here to - * reach the end of the block with the minimum cost. Equivalently, this - * can be interpreted as the label of the outgoing edge on the minimum-cost - * path to the "end of block" node from this node. - */ -struct deflate_optimum_node { - - u32 cost_to_end; - - /* - * Notes on the match/literal representation used here: - * - * The low bits of 'item' are the length: 1 if this is a literal, - * or the match length if this is a match. - * - * The high bits of 'item' are the actual literal byte if this is a - * literal, or the match offset if this is a match. - */ -#define OPTIMUM_OFFSET_SHIFT 9 -#define OPTIMUM_LEN_MASK (((u32)1 << OPTIMUM_OFFSET_SHIFT) - 1) - u32 item; - -}; - -#endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - -/* Block split statistics. See "Block splitting algorithm" below. */ -#define NUM_LITERAL_OBSERVATION_TYPES 8 -#define NUM_MATCH_OBSERVATION_TYPES 2 -#define NUM_OBSERVATION_TYPES (NUM_LITERAL_OBSERVATION_TYPES + \ - NUM_MATCH_OBSERVATION_TYPES) -#define NUM_OBSERVATIONS_PER_BLOCK_CHECK 512 -struct block_split_stats { - u32 new_observations[NUM_OBSERVATION_TYPES]; - u32 observations[NUM_OBSERVATION_TYPES]; - u32 num_new_observations; - u32 num_observations; -}; - -struct deflate_output_bitstream; - -/* The main DEFLATE compressor structure */ -struct libdeflate_compressor { - - /* Pointer to the compress() implementation chosen at allocation time */ - void (*impl)(struct libdeflate_compressor *restrict c, const u8 *in, - size_t in_nbytes, struct deflate_output_bitstream *os); - - /* The free() function for this struct, chosen at allocation time */ - free_func_t free_func; - - /* The compression level with which this compressor was created */ - unsigned compression_level; - - /* Anything of this size or less we won't bother trying to compress. */ - size_t max_passthrough_size; - - /* - * The maximum search depth: consider at most this many potential - * matches at each position - */ - u32 max_search_depth; - - /* - * The "nice" match length: if a match of this length is found, choose - * it immediately without further consideration - */ - u32 nice_match_length; - - /* Frequency counters for the current block */ - struct deflate_freqs freqs; - - /* Block split statistics for the current block */ - struct block_split_stats split_stats; - - /* Dynamic Huffman codes for the current block */ - struct deflate_codes codes; - - /* The static Huffman codes defined by the DEFLATE format */ - struct deflate_codes static_codes; - - /* Temporary space for block flushing */ - union { - /* Information about the precode */ - struct { - u32 freqs[DEFLATE_NUM_PRECODE_SYMS]; - u32 codewords[DEFLATE_NUM_PRECODE_SYMS]; - u8 lens[DEFLATE_NUM_PRECODE_SYMS]; - unsigned items[DEFLATE_NUM_LITLEN_SYMS + - DEFLATE_NUM_OFFSET_SYMS]; - unsigned num_litlen_syms; - unsigned num_offset_syms; - unsigned num_explicit_lens; - unsigned num_items; - } precode; - /* - * The "full" length codewords. Used only after the information - * in 'precode' is no longer needed. - */ - struct { - u32 codewords[DEFLATE_MAX_MATCH_LEN + 1]; - u8 lens[DEFLATE_MAX_MATCH_LEN + 1]; - } length; - } o; - - union { - /* Data for greedy or lazy parsing */ - struct { - /* Hash chains matchfinder */ - struct hc_matchfinder hc_mf; - - /* Matches and literals chosen for the current block */ - struct deflate_sequence sequences[SEQ_STORE_LENGTH + 1]; - - } g; /* (g)reedy */ - - /* Data for fastest parsing */ - struct { - /* Hash table matchfinder */ - struct ht_matchfinder ht_mf; - - /* Matches and literals chosen for the current block */ - struct deflate_sequence sequences[ - FAST_SEQ_STORE_LENGTH + 1]; - - } f; /* (f)astest */ - - #if SUPPORT_NEAR_OPTIMAL_PARSING - /* Data for near-optimal parsing */ - struct { - - /* Binary tree matchfinder */ - struct bt_matchfinder bt_mf; - - /* - * Cached matches for the current block. This array - * contains the matches that were found at each position - * in the block. Specifically, for each position, there - * is a list of matches found at that position, if any, - * sorted by strictly increasing length. In addition, - * following the matches for each position, there is a - * special 'struct lz_match' whose 'length' member - * contains the number of matches found at that - * position, and whose 'offset' member contains the - * literal at that position. - * - * Note: in rare cases, there will be a very high number - * of matches in the block and this array will overflow. - * If this happens, we force the end of the current - * block. MATCH_CACHE_LENGTH is the length at which we - * actually check for overflow. The extra slots beyond - * this are enough to absorb the worst case overflow, - * which occurs if starting at - * &match_cache[MATCH_CACHE_LENGTH - 1], we write - * MAX_MATCHES_PER_POS matches and a match count header, - * then skip searching for matches at - * 'DEFLATE_MAX_MATCH_LEN - 1' positions and write the - * match count header for each. - */ - struct lz_match match_cache[MATCH_CACHE_LENGTH + - MAX_MATCHES_PER_POS + - DEFLATE_MAX_MATCH_LEN - 1]; - - /* - * Array of nodes, one per position, for running the - * minimum-cost path algorithm. - * - * This array must be large enough to accommodate the - * worst-case number of nodes, which is MAX_BLOCK_LENGTH - * plus 1 for the end-of-block node. - */ - struct deflate_optimum_node optimum_nodes[ - MAX_BLOCK_LENGTH + 1]; - - /* The current cost model being used */ - struct deflate_costs costs; - - /* Saved cost model */ - struct deflate_costs costs_saved; - - /* - * A table that maps match offset to offset slot. This - * differs from deflate_offset_slot[] in that this is a - * full map, not a condensed one. The full map is more - * appropriate for the near-optimal parser, since the - * near-optimal parser does more offset => offset_slot - * translations, it doesn't intersperse them with - * matchfinding (so cache evictions are less of a - * concern), and it uses more memory anyway. - */ - u8 offset_slot_full[DEFLATE_MAX_MATCH_OFFSET + 1]; - - /* Literal/match statistics saved from previous block */ - u32 prev_observations[NUM_OBSERVATION_TYPES]; - u32 prev_num_observations; - - /* - * Approximate match length frequencies based on a - * greedy parse, gathered during matchfinding. This is - * used for setting the initial symbol costs. - */ - u32 new_match_len_freqs[DEFLATE_MAX_MATCH_LEN + 1]; - u32 match_len_freqs[DEFLATE_MAX_MATCH_LEN + 1]; - - /* - * The maximum number of optimization passes - * (min-cost path searches) per block. - * Larger values = more compression. - */ - unsigned max_optim_passes; - - /* - * If an optimization pass improves the cost by fewer - * than this number of bits, then optimization will stop - * early, before max_optim_passes has been reached. - * Smaller values = more compression. - */ - u32 min_improvement_to_continue; - - /* - * The minimum number of bits that would need to be - * saved for it to be considered worth the time to - * regenerate and use the min-cost path from a previous - * optimization pass, in the case where the final - * optimization pass actually increased the cost. - * Smaller values = more compression. - */ - u32 min_bits_to_use_nonfinal_path; - - /* - * The maximum block length, in uncompressed bytes, at - * which to find and consider the optimal match/literal - * list for the static Huffman codes. This strategy - * improves the compression ratio produced by static - * Huffman blocks and can discover more cases in which - * static blocks are worthwhile. This helps mostly with - * small blocks, hence why this parameter is a max_len. - * - * Above this block length, static Huffman blocks are - * only used opportunistically. I.e. a static Huffman - * block is only used if a static block using the same - * match/literal list as the optimized dynamic block - * happens to be cheaper than the dynamic block itself. - */ - u32 max_len_to_optimize_static_block; - - } n; /* (n)ear-optimal */ - #endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - - } p; /* (p)arser */ -}; - -/* - * The type for the bitbuffer variable, which temporarily holds bits that are - * being packed into bytes and written to the output buffer. For best - * performance, this should have size equal to a machine word. - */ -typedef machine_word_t bitbuf_t; - -/* - * The capacity of the bitbuffer, in bits. This is 1 less than the real size, - * in order to avoid undefined behavior when doing bitbuf >>= bitcount & ~7. - */ -#define BITBUF_NBITS (8 * sizeof(bitbuf_t) - 1) - -/* - * Can the specified number of bits always be added to 'bitbuf' after any - * pending bytes have been flushed? There can be up to 7 bits remaining after a - * flush, so the count must not exceed BITBUF_NBITS after adding 'n' more bits. - */ -#define CAN_BUFFER(n) (7 + (n) <= BITBUF_NBITS) - -/* - * Structure to keep track of the current state of sending bits to the - * compressed output buffer - */ -struct deflate_output_bitstream { - - /* Bits that haven't yet been written to the output buffer */ - bitbuf_t bitbuf; - - /* - * Number of bits currently held in @bitbuf. This can be between 0 and - * BITBUF_NBITS in general, or between 0 and 7 after a flush. - */ - unsigned bitcount; - - /* - * Pointer to the position in the output buffer at which the next byte - * should be written - */ - u8 *next; - - /* Pointer to the end of the output buffer */ - u8 *end; - - /* true if the output buffer ran out of space */ - bool overflow; -}; - -/* - * Add some bits to the bitbuffer variable of the output bitstream. The caller - * must ensure that 'bitcount + n <= BITBUF_NBITS', by calling FLUSH_BITS() - * frequently enough. - */ -#define ADD_BITS(bits, n) \ -do { \ - bitbuf |= (bitbuf_t)(bits) << bitcount; \ - bitcount += (n); \ - ASSERT(bitcount <= BITBUF_NBITS); \ -} while (0) - -/* - * Flush bits from the bitbuffer variable to the output buffer. After this, the - * bitbuffer will contain at most 7 bits (a partial byte). - * - * Since deflate_flush_block() verified ahead of time that there is enough space - * remaining before actually writing the block, it's guaranteed that out_next - * won't exceed os->end. However, there might not be enough space remaining to - * flush a whole word, even though that's fastest. Therefore, flush a whole - * word if there is space for it, otherwise flush a byte at a time. - */ -#define FLUSH_BITS() \ -do { \ - if (UNALIGNED_ACCESS_IS_FAST && likely(out_next < out_fast_end)) { \ - /* Flush a whole word (branchlessly). */ \ - put_unaligned_leword(bitbuf, out_next); \ - bitbuf >>= bitcount & ~7; \ - out_next += bitcount >> 3; \ - bitcount &= 7; \ - } else { \ - /* Flush a byte at a time. */ \ - while (bitcount >= 8) { \ - ASSERT(out_next < os->end); \ - *out_next++ = bitbuf; \ - bitcount -= 8; \ - bitbuf >>= 8; \ - } \ - } \ -} while (0) - -/* - * Given the binary tree node A[subtree_idx] whose children already satisfy the - * maxheap property, swap the node with its greater child until it is greater - * than or equal to both of its children, so that the maxheap property is - * satisfied in the subtree rooted at A[subtree_idx]. 'A' uses 1-based indices. - */ -static void -heapify_subtree(u32 A[], unsigned length, unsigned subtree_idx) -{ - unsigned parent_idx; - unsigned child_idx; - u32 v; - - v = A[subtree_idx]; - parent_idx = subtree_idx; - while ((child_idx = parent_idx * 2) <= length) { - if (child_idx < length && A[child_idx + 1] > A[child_idx]) - child_idx++; - if (v >= A[child_idx]) - break; - A[parent_idx] = A[child_idx]; - parent_idx = child_idx; - } - A[parent_idx] = v; -} - -/* - * Rearrange the array 'A' so that it satisfies the maxheap property. - * 'A' uses 1-based indices, so the children of A[i] are A[i*2] and A[i*2 + 1]. - */ -static void -heapify_array(u32 A[], unsigned length) -{ - unsigned subtree_idx; - - for (subtree_idx = length / 2; subtree_idx >= 1; subtree_idx--) - heapify_subtree(A, length, subtree_idx); -} - -/* - * Sort the array 'A', which contains 'length' unsigned 32-bit integers. - * - * Note: name this function heap_sort() instead of heapsort() to avoid colliding - * with heapsort() from stdlib.h on BSD-derived systems. - */ -static void -heap_sort(u32 A[], unsigned length) -{ - A--; /* Use 1-based indices */ - - heapify_array(A, length); - - while (length >= 2) { - u32 tmp = A[length]; - - A[length] = A[1]; - A[1] = tmp; - length--; - heapify_subtree(A, length, 1); - } -} - -#define NUM_SYMBOL_BITS 10 -#define NUM_FREQ_BITS (32 - NUM_SYMBOL_BITS) -#define SYMBOL_MASK ((1 << NUM_SYMBOL_BITS) - 1) -#define FREQ_MASK (~SYMBOL_MASK) - -#define GET_NUM_COUNTERS(num_syms) (num_syms) - -/* - * Sort the symbols primarily by frequency and secondarily by symbol value. - * Discard symbols with zero frequency and fill in an array with the remaining - * symbols, along with their frequencies. The low NUM_SYMBOL_BITS bits of each - * array entry will contain the symbol value, and the remaining bits will - * contain the frequency. - * - * @num_syms - * Number of symbols in the alphabet, at most 1 << NUM_SYMBOL_BITS. - * - * @freqs[num_syms] - * Frequency of each symbol, summing to at most (1 << NUM_FREQ_BITS) - 1. - * - * @lens[num_syms] - * An array that eventually will hold the length of each codeword. This - * function only fills in the codeword lengths for symbols that have zero - * frequency, which are not well defined per se but will be set to 0. - * - * @symout[num_syms] - * The output array, described above. - * - * Returns the number of entries in 'symout' that were filled. This is the - * number of symbols that have nonzero frequency. - */ -static unsigned -sort_symbols(unsigned num_syms, const u32 freqs[], u8 lens[], u32 symout[]) -{ - unsigned sym; - unsigned i; - unsigned num_used_syms; - unsigned num_counters; - unsigned counters[GET_NUM_COUNTERS(DEFLATE_MAX_NUM_SYMS)]; - - /* - * We use heapsort, but with an added optimization. Since often most - * symbol frequencies are low, we first do a count sort using a limited - * number of counters. High frequencies are counted in the last - * counter, and only they will be sorted with heapsort. - * - * Note: with more symbols, it is generally beneficial to have more - * counters. About 1 counter per symbol seems fastest. - */ - - num_counters = GET_NUM_COUNTERS(num_syms); - - memset(counters, 0, num_counters * sizeof(counters[0])); - - /* Count the frequencies. */ - for (sym = 0; sym < num_syms; sym++) - counters[MIN(freqs[sym], num_counters - 1)]++; - - /* - * Make the counters cumulative, ignoring the zero-th, which counted - * symbols with zero frequency. As a side effect, this calculates the - * number of symbols with nonzero frequency. - */ - num_used_syms = 0; - for (i = 1; i < num_counters; i++) { - unsigned count = counters[i]; - - counters[i] = num_used_syms; - num_used_syms += count; - } - - /* - * Sort nonzero-frequency symbols using the counters. At the same time, - * set the codeword lengths of zero-frequency symbols to 0. - */ - for (sym = 0; sym < num_syms; sym++) { - u32 freq = freqs[sym]; - - if (freq != 0) { - symout[counters[MIN(freq, num_counters - 1)]++] = - sym | (freq << NUM_SYMBOL_BITS); - } else { - lens[sym] = 0; - } - } - - /* Sort the symbols counted in the last counter. */ - heap_sort(symout + counters[num_counters - 2], - counters[num_counters - 1] - counters[num_counters - 2]); - - return num_used_syms; -} - -/* - * Build a Huffman tree. - * - * This is an optimized implementation that - * (a) takes advantage of the frequencies being already sorted; - * (b) only generates non-leaf nodes, since the non-leaf nodes of a Huffman - * tree are sufficient to generate a canonical code; - * (c) Only stores parent pointers, not child pointers; - * (d) Produces the nodes in the same memory used for input frequency - * information. - * - * Array 'A', which contains 'sym_count' entries, is used for both input and - * output. For this function, 'sym_count' must be at least 2. - * - * For input, the array must contain the frequencies of the symbols, sorted in - * increasing order. Specifically, each entry must contain a frequency left - * shifted by NUM_SYMBOL_BITS bits. Any data in the low NUM_SYMBOL_BITS bits of - * the entries will be ignored by this function. Although these bits will, in - * fact, contain the symbols that correspond to the frequencies, this function - * is concerned with frequencies only and keeps the symbols as-is. - * - * For output, this function will produce the non-leaf nodes of the Huffman - * tree. These nodes will be stored in the first (sym_count - 1) entries of the - * array. Entry A[sym_count - 2] will represent the root node. Each other node - * will contain the zero-based index of its parent node in 'A', left shifted by - * NUM_SYMBOL_BITS bits. The low NUM_SYMBOL_BITS bits of each entry in A will - * be kept as-is. Again, note that although these low bits will, in fact, - * contain a symbol value, this symbol will have *no relationship* with the - * Huffman tree node that happens to occupy the same slot. This is because this - * implementation only generates the non-leaf nodes of the tree. - */ -static void -build_tree(u32 A[], unsigned sym_count) -{ - const unsigned last_idx = sym_count - 1; - - /* Index of the next lowest frequency leaf that still needs a parent */ - unsigned i = 0; - - /* - * Index of the next lowest frequency non-leaf that still needs a - * parent, or 'e' if there is currently no such node - */ - unsigned b = 0; - - /* Index of the next spot for a non-leaf (will overwrite a leaf) */ - unsigned e = 0; - - do { - u32 new_freq; - - /* - * Select the next two lowest frequency nodes among the leaves - * A[i] and non-leaves A[b], and create a new node A[e] to be - * their parent. Set the new node's frequency to the sum of the - * frequencies of its two children. - * - * Usually the next two lowest frequency nodes are of the same - * type (leaf or non-leaf), so check those cases first. - */ - if (i + 1 <= last_idx && - (b == e || (A[i + 1] & FREQ_MASK) <= (A[b] & FREQ_MASK))) { - /* Two leaves */ - new_freq = (A[i] & FREQ_MASK) + (A[i + 1] & FREQ_MASK); - i += 2; - } else if (b + 2 <= e && - (i > last_idx || - (A[b + 1] & FREQ_MASK) < (A[i] & FREQ_MASK))) { - /* Two non-leaves */ - new_freq = (A[b] & FREQ_MASK) + (A[b + 1] & FREQ_MASK); - A[b] = (e << NUM_SYMBOL_BITS) | (A[b] & SYMBOL_MASK); - A[b + 1] = (e << NUM_SYMBOL_BITS) | - (A[b + 1] & SYMBOL_MASK); - b += 2; - } else { - /* One leaf and one non-leaf */ - new_freq = (A[i] & FREQ_MASK) + (A[b] & FREQ_MASK); - A[b] = (e << NUM_SYMBOL_BITS) | (A[b] & SYMBOL_MASK); - i++; - b++; - } - A[e] = new_freq | (A[e] & SYMBOL_MASK); - /* - * A binary tree with 'n' leaves has 'n - 1' non-leaves, so the - * tree is complete once we've created 'n - 1' non-leaves. - */ - } while (++e < last_idx); -} - -/* - * Given the stripped-down Huffman tree constructed by build_tree(), determine - * the number of codewords that should be assigned each possible length, taking - * into account the length-limited constraint. - * - * @A - * The array produced by build_tree(), containing parent index information - * for the non-leaf nodes of the Huffman tree. Each entry in this array is - * a node; a node's parent always has a greater index than that node - * itself. This function will overwrite the parent index information in - * this array, so essentially it will destroy the tree. However, the data - * in the low NUM_SYMBOL_BITS of each entry will be preserved. - * - * @root_idx - * The 0-based index of the root node in 'A', and consequently one less - * than the number of tree node entries in 'A'. (Or, really 2 less than - * the actual length of 'A'.) - * - * @len_counts - * An array of length ('max_codeword_len' + 1) in which the number of - * codewords having each length <= max_codeword_len will be returned. - * - * @max_codeword_len - * The maximum permissible codeword length. - */ -static void -compute_length_counts(u32 A[], unsigned root_idx, unsigned len_counts[], - unsigned max_codeword_len) -{ - unsigned len; - int node; - - /* - * The key observations are: - * - * (1) We can traverse the non-leaf nodes of the tree, always visiting a - * parent before its children, by simply iterating through the array - * in reverse order. Consequently, we can compute the depth of each - * node in one pass, overwriting the parent indices with depths. - * - * (2) We can initially assume that in the real Huffman tree, both - * children of the root are leaves. This corresponds to two - * codewords of length 1. Then, whenever we visit a (non-leaf) node - * during the traversal, we modify this assumption to account for - * the current node *not* being a leaf, but rather its two children - * being leaves. This causes the loss of one codeword for the - * current depth and the addition of two codewords for the current - * depth plus one. - * - * (3) We can handle the length-limited constraint fairly easily by - * simply using the largest length available when a depth exceeds - * max_codeword_len. - */ - - for (len = 0; len <= max_codeword_len; len++) - len_counts[len] = 0; - len_counts[1] = 2; - - /* Set the root node's depth to 0. */ - A[root_idx] &= SYMBOL_MASK; - - for (node = root_idx - 1; node >= 0; node--) { - - /* Calculate the depth of this node. */ - - unsigned parent = A[node] >> NUM_SYMBOL_BITS; - unsigned parent_depth = A[parent] >> NUM_SYMBOL_BITS; - unsigned depth = parent_depth + 1; - - /* - * Set the depth of this node so that it is available when its - * children (if any) are processed. - */ - A[node] = (A[node] & SYMBOL_MASK) | (depth << NUM_SYMBOL_BITS); - - /* - * If needed, decrease the length to meet the length-limited - * constraint. This is not the optimal method for generating - * length-limited Huffman codes! But it should be good enough. - */ - if (depth >= max_codeword_len) { - depth = max_codeword_len; - do { - depth--; - } while (len_counts[depth] == 0); - } - - /* - * Account for the fact that we have a non-leaf node at the - * current depth. - */ - len_counts[depth]--; - len_counts[depth + 1] += 2; - } -} - -/* - * DEFLATE uses bit-reversed codewords, so we must bit-reverse the codewords - * after generating them. All codewords have length <= 16 bits. If the CPU has - * a bit-reversal instruction, then that is the fastest method. Otherwise the - * fastest method is to reverse the bits in each of the two bytes using a table. - * The table method is slightly faster than using bitwise operations to flip - * adjacent 1, 2, 4, and then 8-bit fields, even if 2 to 4 codewords are packed - * into a machine word and processed together using that method. - */ - -#ifdef rbit32 -static forceinline u32 reverse_codeword(u32 codeword, u8 len) -{ - return rbit32(codeword) >> ((32 - len) & 31); -} -#else -/* Generated by scripts/gen_bitreverse_tab.py */ -static const u8 bitreverse_tab[256] = { - 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, - 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, - 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, - 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, - 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, - 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, - 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, - 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, - 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, - 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, - 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, - 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, - 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, - 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, - 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, - 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, - 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, - 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, - 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, - 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, - 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, - 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, - 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, - 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, - 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, - 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, - 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, - 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, - 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, - 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, - 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, - 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff, -}; - -static forceinline u32 reverse_codeword(u32 codeword, u8 len) -{ - STATIC_ASSERT(DEFLATE_MAX_CODEWORD_LEN <= 16); - codeword = ((u32)bitreverse_tab[codeword & 0xff] << 8) | - bitreverse_tab[codeword >> 8]; - return codeword >> (16 - len); -} -#endif /* !rbit32 */ - -/* - * Generate the codewords for a canonical Huffman code. - * - * @A - * The output array for codewords. In addition, initially this - * array must contain the symbols, sorted primarily by frequency and - * secondarily by symbol value, in the low NUM_SYMBOL_BITS bits of - * each entry. - * - * @len - * Output array for codeword lengths. - * - * @len_counts - * An array that provides the number of codewords that will have - * each possible length <= max_codeword_len. - * - * @max_codeword_len - * Maximum length, in bits, of each codeword. - * - * @num_syms - * Number of symbols in the alphabet, including symbols with zero - * frequency. This is the length of the 'A' and 'len' arrays. - */ -static void -gen_codewords(u32 A[], u8 lens[], const unsigned len_counts[], - unsigned max_codeword_len, unsigned num_syms) -{ - u32 next_codewords[DEFLATE_MAX_CODEWORD_LEN + 1]; - unsigned i; - unsigned len; - unsigned sym; - - /* - * Given the number of codewords that will have each length, assign - * codeword lengths to symbols. We do this by assigning the lengths in - * decreasing order to the symbols sorted primarily by increasing - * frequency and secondarily by increasing symbol value. - */ - for (i = 0, len = max_codeword_len; len >= 1; len--) { - unsigned count = len_counts[len]; - - while (count--) - lens[A[i++] & SYMBOL_MASK] = len; - } - - /* - * Generate the codewords themselves. We initialize the - * 'next_codewords' array to provide the lexicographically first - * codeword of each length, then assign codewords in symbol order. This - * produces a canonical code. - */ - next_codewords[0] = 0; - next_codewords[1] = 0; - for (len = 2; len <= max_codeword_len; len++) - next_codewords[len] = - (next_codewords[len - 1] + len_counts[len - 1]) << 1; - - for (sym = 0; sym < num_syms; sym++) { - /* DEFLATE requires bit-reversed codewords. */ - A[sym] = reverse_codeword(next_codewords[lens[sym]]++, - lens[sym]); - } -} - -/* - * --------------------------------------------------------------------- - * deflate_make_huffman_code() - * --------------------------------------------------------------------- - * - * Given an alphabet and the frequency of each symbol in it, construct a - * length-limited canonical Huffman code. - * - * @num_syms - * The number of symbols in the alphabet. The symbols are the integers in - * the range [0, num_syms - 1]. This parameter must be at least 2 and - * must not exceed (1 << NUM_SYMBOL_BITS). - * - * @max_codeword_len - * The maximum permissible codeword length. - * - * @freqs - * An array of length @num_syms that gives the frequency of each symbol. - * It is valid for some, none, or all of the frequencies to be 0. The sum - * of frequencies must not exceed (1 << NUM_FREQ_BITS) - 1. - * - * @lens - * An array of @num_syms entries in which this function will return the - * length, in bits, of the codeword assigned to each symbol. Symbols with - * 0 frequency will not have codewords per se, but their entries in this - * array will be set to 0. No lengths greater than @max_codeword_len will - * be assigned. - * - * @codewords - * An array of @num_syms entries in which this function will return the - * codeword for each symbol, right-justified and padded on the left with - * zeroes. Codewords for symbols with 0 frequency will be undefined. - * - * --------------------------------------------------------------------- - * - * This function builds a length-limited canonical Huffman code. - * - * A length-limited Huffman code contains no codewords longer than some - * specified length, and has exactly (with some algorithms) or approximately - * (with the algorithm used here) the minimum weighted path length from the - * root, given this constraint. - * - * A canonical Huffman code satisfies the properties that a longer codeword - * never lexicographically precedes a shorter codeword, and the lexicographic - * ordering of codewords of the same length is the same as the lexicographic - * ordering of the corresponding symbols. A canonical Huffman code, or more - * generally a canonical prefix code, can be reconstructed from only a list - * containing the codeword length of each symbol. - * - * The classic algorithm to generate a Huffman code creates a node for each - * symbol, then inserts these nodes into a min-heap keyed by symbol frequency. - * Then, repeatedly, the two lowest-frequency nodes are removed from the - * min-heap and added as the children of a new node having frequency equal to - * the sum of its two children, which is then inserted into the min-heap. When - * only a single node remains in the min-heap, it is the root of the Huffman - * tree. The codeword for each symbol is determined by the path needed to reach - * the corresponding node from the root. Descending to the left child appends a - * 0 bit, whereas descending to the right child appends a 1 bit. - * - * The classic algorithm is relatively easy to understand, but it is subject to - * a number of inefficiencies. In practice, it is fastest to first sort the - * symbols by frequency. (This itself can be subject to an optimization based - * on the fact that most frequencies tend to be low.) At the same time, we sort - * secondarily by symbol value, which aids the process of generating a canonical - * code. Then, during tree construction, no heap is necessary because both the - * leaf nodes and the unparented non-leaf nodes can be easily maintained in - * sorted order. Consequently, there can never be more than two possibilities - * for the next-lowest-frequency node. - * - * In addition, because we're generating a canonical code, we actually don't - * need the leaf nodes of the tree at all, only the non-leaf nodes. This is - * because for canonical code generation we don't need to know where the symbols - * are in the tree. Rather, we only need to know how many leaf nodes have each - * depth (codeword length). And this information can, in fact, be quickly - * generated from the tree of non-leaves only. - * - * Furthermore, we can build this stripped-down Huffman tree directly in the - * array in which the codewords are to be generated, provided that these array - * slots are large enough to hold a symbol and frequency value. - * - * Still furthermore, we don't even need to maintain explicit child pointers. - * We only need the parent pointers, and even those can be overwritten in-place - * with depth information as part of the process of extracting codeword lengths - * from the tree. So in summary, we do NOT need a big structure like: - * - * struct huffman_tree_node { - * unsigned int symbol; - * unsigned int frequency; - * unsigned int depth; - * struct huffman_tree_node *left_child; - * struct huffman_tree_node *right_child; - * }; - * - * - * ... which often gets used in "naive" implementations of Huffman code - * generation. - * - * Many of these optimizations are based on the implementation in 7-Zip (source - * file: C/HuffEnc.c), which was placed in the public domain by Igor Pavlov. - */ -static void -deflate_make_huffman_code(unsigned num_syms, unsigned max_codeword_len, - const u32 freqs[], u8 lens[], u32 codewords[]) -{ - u32 *A = codewords; - unsigned num_used_syms; - - STATIC_ASSERT(DEFLATE_MAX_NUM_SYMS <= 1 << NUM_SYMBOL_BITS); - STATIC_ASSERT(MAX_BLOCK_LENGTH <= ((u32)1 << NUM_FREQ_BITS) - 1); - - /* - * We begin by sorting the symbols primarily by frequency and - * secondarily by symbol value. As an optimization, the array used for - * this purpose ('A') shares storage with the space in which we will - * eventually return the codewords. - */ - num_used_syms = sort_symbols(num_syms, freqs, lens, A); - /* - * 'num_used_syms' is the number of symbols with nonzero frequency. - * This may be less than @num_syms. 'num_used_syms' is also the number - * of entries in 'A' that are valid. Each entry consists of a distinct - * symbol and a nonzero frequency packed into a 32-bit integer. - */ - - /* - * A complete Huffman code must contain at least 2 codewords. Yet, it's - * possible that fewer than 2 symbols were used. When this happens, - * it's usually for the offset code (0-1 symbols used). But it's also - * theoretically possible for the litlen and pre codes (1 symbol used). - * - * The DEFLATE RFC explicitly allows the offset code to contain just 1 - * codeword, or even be completely empty. But it's silent about the - * other codes. It also doesn't say whether, in the 1-codeword case, - * the codeword (which it says must be 1 bit) is '0' or '1'. - * - * In any case, some DEFLATE decompressors reject these cases. zlib - * generally allows them, but it does reject precodes that have just 1 - * codeword. More problematically, zlib v1.2.1 and earlier rejected - * empty offset codes, and this behavior can also be seen in Windows - * Explorer's ZIP unpacker (supposedly even still in Windows 11). - * - * Other DEFLATE compressors, including zlib, always send at least 2 - * codewords in order to make a complete Huffman code. Therefore, this - * is a case where practice does not entirely match the specification. - * We follow practice by generating 2 codewords of length 1: codeword - * '0' for symbol 0, and codeword '1' for another symbol -- the used - * symbol if it exists and is not symbol 0, otherwise symbol 1. This - * does worsen the compression ratio by having to send an unnecessary - * offset codeword length. But this only affects rare cases such as - * blocks containing all literals, and it only makes a tiny difference. - */ - if (unlikely(num_used_syms < 2)) { - unsigned sym = num_used_syms ? (A[0] & SYMBOL_MASK) : 0; - unsigned nonzero_idx = sym ? sym : 1; - - codewords[0] = 0; - lens[0] = 1; - codewords[nonzero_idx] = 1; - lens[nonzero_idx] = 1; - return; - } - - /* - * Build a stripped-down version of the Huffman tree, sharing the array - * 'A' with the symbol values. Then extract length counts from the tree - * and use them to generate the final codewords. - */ - - build_tree(A, num_used_syms); - - { - unsigned len_counts[DEFLATE_MAX_CODEWORD_LEN + 1]; - - compute_length_counts(A, num_used_syms - 2, - len_counts, max_codeword_len); - - gen_codewords(A, lens, len_counts, max_codeword_len, num_syms); - } -} - -/* - * Clear the Huffman symbol frequency counters. This must be called when - * starting a new DEFLATE block. - */ -static void -deflate_reset_symbol_frequencies(struct libdeflate_compressor *c) -{ - memset(&c->freqs, 0, sizeof(c->freqs)); -} - -/* - * Build the literal/length and offset Huffman codes for a DEFLATE block. - * - * This takes as input the frequency tables for each alphabet and produces as - * output a set of tables that map symbols to codewords and codeword lengths. - */ -static void -deflate_make_huffman_codes(const struct deflate_freqs *freqs, - struct deflate_codes *codes) -{ - deflate_make_huffman_code(DEFLATE_NUM_LITLEN_SYMS, - MAX_LITLEN_CODEWORD_LEN, - freqs->litlen, - codes->lens.litlen, - codes->codewords.litlen); - - deflate_make_huffman_code(DEFLATE_NUM_OFFSET_SYMS, - MAX_OFFSET_CODEWORD_LEN, - freqs->offset, - codes->lens.offset, - codes->codewords.offset); -} - -/* Initialize c->static_codes. */ -static void -deflate_init_static_codes(struct libdeflate_compressor *c) -{ - unsigned i; - - for (i = 0; i < 144; i++) - c->freqs.litlen[i] = 1 << (9 - 8); - for (; i < 256; i++) - c->freqs.litlen[i] = 1 << (9 - 9); - for (; i < 280; i++) - c->freqs.litlen[i] = 1 << (9 - 7); - for (; i < 288; i++) - c->freqs.litlen[i] = 1 << (9 - 8); - - for (i = 0; i < 32; i++) - c->freqs.offset[i] = 1 << (5 - 5); - - deflate_make_huffman_codes(&c->freqs, &c->static_codes); -} - -/* Return the offset slot for the given match offset, using the small map. */ -static forceinline unsigned -deflate_get_offset_slot(u32 offset) -{ - /* - * 1 <= offset <= 32768 here. For 1 <= offset <= 256, - * deflate_offset_slot[offset - 1] gives the slot. - * - * For 257 <= offset <= 32768, we take advantage of the fact that 257 is - * the beginning of slot 16, and each slot [16..30) is exactly 1 << 7 == - * 128 times larger than each slot [2..16) (since the number of extra - * bits increases by 1 every 2 slots). Thus, the slot is: - * - * deflate_offset_slot[2 + ((offset - 257) >> 7)] + (16 - 2) - * == deflate_offset_slot[((offset - 1) >> 7)] + 14 - * - * Define 'n = (offset <= 256) ? 0 : 7'. Then any offset is handled by: - * - * deflate_offset_slot[(offset - 1) >> n] + (n << 1) - * - * For better performance, replace 'n = (offset <= 256) ? 0 : 7' with - * the equivalent (for offset <= 536871168) 'n = (256 - offset) >> 29'. - */ - unsigned n = (256 - offset) >> 29; - - ASSERT(offset >= 1 && offset <= 32768); - - return deflate_offset_slot[(offset - 1) >> n] + (n << 1); -} - -static unsigned -deflate_compute_precode_items(const u8 lens[], const unsigned num_lens, - u32 precode_freqs[], unsigned precode_items[]) -{ - unsigned *itemptr; - unsigned run_start; - unsigned run_end; - unsigned extra_bits; - u8 len; - - memset(precode_freqs, 0, - DEFLATE_NUM_PRECODE_SYMS * sizeof(precode_freqs[0])); - - itemptr = precode_items; - run_start = 0; - do { - /* Find the next run of codeword lengths. */ - - /* len = the length being repeated */ - len = lens[run_start]; - - /* Extend the run. */ - run_end = run_start; - do { - run_end++; - } while (run_end != num_lens && len == lens[run_end]); - - if (len == 0) { - /* Run of zeroes. */ - - /* Symbol 18: RLE 11 to 138 zeroes at a time. */ - while ((run_end - run_start) >= 11) { - extra_bits = MIN((run_end - run_start) - 11, - 0x7F); - precode_freqs[18]++; - *itemptr++ = 18 | (extra_bits << 5); - run_start += 11 + extra_bits; - } - - /* Symbol 17: RLE 3 to 10 zeroes at a time. */ - if ((run_end - run_start) >= 3) { - extra_bits = MIN((run_end - run_start) - 3, - 0x7); - precode_freqs[17]++; - *itemptr++ = 17 | (extra_bits << 5); - run_start += 3 + extra_bits; - } - } else { - - /* A run of nonzero lengths. */ - - /* Symbol 16: RLE 3 to 6 of the previous length. */ - if ((run_end - run_start) >= 4) { - precode_freqs[len]++; - *itemptr++ = len; - run_start++; - do { - extra_bits = MIN((run_end - run_start) - - 3, 0x3); - precode_freqs[16]++; - *itemptr++ = 16 | (extra_bits << 5); - run_start += 3 + extra_bits; - } while ((run_end - run_start) >= 3); - } - } - - /* Output any remaining lengths without RLE. */ - while (run_start != run_end) { - precode_freqs[len]++; - *itemptr++ = len; - run_start++; - } - } while (run_start != num_lens); - - return itemptr - precode_items; -} - -/* - * Huffman codeword lengths for dynamic Huffman blocks are compressed using a - * separate Huffman code, the "precode", which contains a symbol for each - * possible codeword length in the larger code as well as several special - * symbols to represent repeated codeword lengths (a form of run-length - * encoding). The precode is itself constructed in canonical form, and its - * codeword lengths are represented literally in 19 3-bit fields that - * immediately precede the compressed codeword lengths of the larger code. - */ - -/* Precompute the information needed to output dynamic Huffman codes. */ -static void -deflate_precompute_huffman_header(struct libdeflate_compressor *c) -{ - /* Compute how many litlen and offset symbols are needed. */ - - for (c->o.precode.num_litlen_syms = DEFLATE_NUM_LITLEN_SYMS; - c->o.precode.num_litlen_syms > 257; - c->o.precode.num_litlen_syms--) - if (c->codes.lens.litlen[c->o.precode.num_litlen_syms - 1] != 0) - break; - - for (c->o.precode.num_offset_syms = DEFLATE_NUM_OFFSET_SYMS; - c->o.precode.num_offset_syms > 1; - c->o.precode.num_offset_syms--) - if (c->codes.lens.offset[c->o.precode.num_offset_syms - 1] != 0) - break; - - /* - * If we're not using the full set of literal/length codeword lengths, - * then temporarily move the offset codeword lengths over so that the - * literal/length and offset codeword lengths are contiguous. - */ - STATIC_ASSERT(offsetof(struct deflate_lens, offset) == - DEFLATE_NUM_LITLEN_SYMS); - if (c->o.precode.num_litlen_syms != DEFLATE_NUM_LITLEN_SYMS) { - memmove((u8 *)&c->codes.lens + c->o.precode.num_litlen_syms, - (u8 *)&c->codes.lens + DEFLATE_NUM_LITLEN_SYMS, - c->o.precode.num_offset_syms); - } - - /* - * Compute the "items" (RLE / literal tokens and extra bits) with which - * the codeword lengths in the larger code will be output. - */ - c->o.precode.num_items = - deflate_compute_precode_items((u8 *)&c->codes.lens, - c->o.precode.num_litlen_syms + - c->o.precode.num_offset_syms, - c->o.precode.freqs, - c->o.precode.items); - - /* Build the precode. */ - deflate_make_huffman_code(DEFLATE_NUM_PRECODE_SYMS, - MAX_PRE_CODEWORD_LEN, - c->o.precode.freqs, c->o.precode.lens, - c->o.precode.codewords); - - /* Count how many precode lengths we actually need to output. */ - for (c->o.precode.num_explicit_lens = DEFLATE_NUM_PRECODE_SYMS; - c->o.precode.num_explicit_lens > 4; - c->o.precode.num_explicit_lens--) - if (c->o.precode.lens[deflate_precode_lens_permutation[ - c->o.precode.num_explicit_lens - 1]] != 0) - break; - - /* Restore the offset codeword lengths if needed. */ - if (c->o.precode.num_litlen_syms != DEFLATE_NUM_LITLEN_SYMS) { - memmove((u8 *)&c->codes.lens + DEFLATE_NUM_LITLEN_SYMS, - (u8 *)&c->codes.lens + c->o.precode.num_litlen_syms, - c->o.precode.num_offset_syms); - } -} - -/* - * To make it faster to output matches, compute the "full" match length - * codewords, i.e. the concatenation of the litlen codeword and the extra bits - * for each possible match length. - */ -static void -deflate_compute_full_len_codewords(struct libdeflate_compressor *c, - const struct deflate_codes *codes) -{ - u32 len; - - STATIC_ASSERT(MAX_LITLEN_CODEWORD_LEN + - DEFLATE_MAX_EXTRA_LENGTH_BITS <= 32); - - for (len = DEFLATE_MIN_MATCH_LEN; len <= DEFLATE_MAX_MATCH_LEN; len++) { - unsigned slot = deflate_length_slot[len]; - unsigned litlen_sym = DEFLATE_FIRST_LEN_SYM + slot; - u32 extra_bits = len - deflate_length_slot_base[slot]; - - c->o.length.codewords[len] = - codes->codewords.litlen[litlen_sym] | - (extra_bits << codes->lens.litlen[litlen_sym]); - c->o.length.lens[len] = codes->lens.litlen[litlen_sym] + - deflate_extra_length_bits[slot]; - } -} - -/* Write a match to the output buffer. */ -#define WRITE_MATCH(c_, codes_, length_, offset_, offset_slot_) \ -do { \ - const struct libdeflate_compressor *c__ = (c_); \ - const struct deflate_codes *codes__ = (codes_); \ - u32 length__ = (length_); \ - u32 offset__ = (offset_); \ - unsigned offset_slot__ = (offset_slot_); \ - \ - /* Litlen symbol and extra length bits */ \ - STATIC_ASSERT(CAN_BUFFER(MAX_LITLEN_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_LENGTH_BITS)); \ - ADD_BITS(c__->o.length.codewords[length__], \ - c__->o.length.lens[length__]); \ - \ - if (!CAN_BUFFER(MAX_LITLEN_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_LENGTH_BITS + \ - MAX_OFFSET_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_OFFSET_BITS)) \ - FLUSH_BITS(); \ - \ - /* Offset symbol */ \ - ADD_BITS(codes__->codewords.offset[offset_slot__], \ - codes__->lens.offset[offset_slot__]); \ - \ - if (!CAN_BUFFER(MAX_OFFSET_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_OFFSET_BITS)) \ - FLUSH_BITS(); \ - \ - /* Extra offset bits */ \ - ADD_BITS(offset__ - deflate_offset_slot_base[offset_slot__], \ - deflate_extra_offset_bits[offset_slot__]); \ - \ - FLUSH_BITS(); \ -} while (0) - -/* - * Choose the best type of block to use (dynamic Huffman, static Huffman, or - * uncompressed), then output it. - * - * The uncompressed data of the block is @block_begin[0..@block_length-1]. The - * sequence of literals and matches that will be used to compress the block (if - * a compressed block is chosen) is given by @sequences if it's non-NULL, or - * else @c->p.n.optimum_nodes. @c->freqs and @c->codes must be already set - * according to the literals, matches, and end-of-block symbol. - */ -static void -deflate_flush_block(struct libdeflate_compressor *c, - struct deflate_output_bitstream *os, - const u8 *block_begin, u32 block_length, - const struct deflate_sequence *sequences, - bool is_final_block) -{ - /* - * It is hard to get compilers to understand that writes to 'os->next' - * don't alias 'os'. That hurts performance significantly, as - * everything in 'os' would keep getting re-loaded. ('restrict' - * *should* do the trick, but it's unreliable.) Therefore, we keep all - * the output bitstream state in local variables, and output bits using - * macros. This is similar to what the decompressor does. - */ - const u8 *in_next = block_begin; - const u8 * const in_end = block_begin + block_length; - bitbuf_t bitbuf = os->bitbuf; - unsigned bitcount = os->bitcount; - u8 *out_next = os->next; - u8 * const out_fast_end = - os->end - MIN(WORDBYTES - 1, os->end - out_next); - /* - * The cost for each block type, in bits. Start with the cost of the - * block header which is 3 bits. - */ - u32 dynamic_cost = 3; - u32 static_cost = 3; - u32 uncompressed_cost = 3; - u32 best_cost; - struct deflate_codes *codes; - unsigned sym; - - ASSERT(block_length >= MIN_BLOCK_LENGTH || - (is_final_block && block_length > 0)); - ASSERT(block_length <= MAX_BLOCK_LENGTH); - ASSERT(bitcount <= 7); - ASSERT((bitbuf & ~(((bitbuf_t)1 << bitcount) - 1)) == 0); - ASSERT(out_next <= os->end); - ASSERT(!os->overflow); - - /* Precompute the precode items and build the precode. */ - deflate_precompute_huffman_header(c); - - /* Account for the cost of encoding dynamic Huffman codes. */ - dynamic_cost += 5 + 5 + 4 + (3 * c->o.precode.num_explicit_lens); - for (sym = 0; sym < DEFLATE_NUM_PRECODE_SYMS; sym++) { - u32 extra = deflate_extra_precode_bits[sym]; - - dynamic_cost += c->o.precode.freqs[sym] * - (extra + c->o.precode.lens[sym]); - } - - /* Account for the cost of encoding literals. */ - for (sym = 0; sym < 144; sym++) { - dynamic_cost += c->freqs.litlen[sym] * - c->codes.lens.litlen[sym]; - static_cost += c->freqs.litlen[sym] * 8; - } - for (; sym < 256; sym++) { - dynamic_cost += c->freqs.litlen[sym] * - c->codes.lens.litlen[sym]; - static_cost += c->freqs.litlen[sym] * 9; - } - - /* Account for the cost of encoding the end-of-block symbol. */ - dynamic_cost += c->codes.lens.litlen[DEFLATE_END_OF_BLOCK]; - static_cost += 7; - - /* Account for the cost of encoding lengths. */ - for (sym = DEFLATE_FIRST_LEN_SYM; - sym < DEFLATE_FIRST_LEN_SYM + ARRAY_LEN(deflate_extra_length_bits); - sym++) { - u32 extra = deflate_extra_length_bits[ - sym - DEFLATE_FIRST_LEN_SYM]; - - dynamic_cost += c->freqs.litlen[sym] * - (extra + c->codes.lens.litlen[sym]); - static_cost += c->freqs.litlen[sym] * - (extra + c->static_codes.lens.litlen[sym]); - } - - /* Account for the cost of encoding offsets. */ - for (sym = 0; sym < ARRAY_LEN(deflate_extra_offset_bits); sym++) { - u32 extra = deflate_extra_offset_bits[sym]; - - dynamic_cost += c->freqs.offset[sym] * - (extra + c->codes.lens.offset[sym]); - static_cost += c->freqs.offset[sym] * (extra + 5); - } - - /* Compute the cost of using uncompressed blocks. */ - uncompressed_cost += (-(bitcount + 3) & 7) + 32 + - (40 * (DIV_ROUND_UP(block_length, - UINT16_MAX) - 1)) + - (8 * block_length); - - /* - * Choose and output the cheapest type of block. If there is a tie, - * prefer uncompressed, then static, then dynamic. - */ - - best_cost = MIN(dynamic_cost, MIN(static_cost, uncompressed_cost)); - - /* If the block isn't going to fit, then stop early. */ - if (DIV_ROUND_UP(bitcount + best_cost, 8) > os->end - out_next) { - os->overflow = true; - return; - } - /* - * Else, now we know that the block fits, so no further bounds checks on - * the output buffer are required until the next block. - */ - - if (best_cost == uncompressed_cost) { - /* - * Uncompressed block(s). DEFLATE limits the length of - * uncompressed blocks to UINT16_MAX bytes, so if the length of - * the "block" we're flushing is over UINT16_MAX, we actually - * output multiple blocks. - */ - do { - u8 bfinal = 0; - size_t len = UINT16_MAX; - - if (in_end - in_next <= UINT16_MAX) { - bfinal = is_final_block; - len = in_end - in_next; - } - /* It was already checked that there is enough space. */ - ASSERT(os->end - out_next >= - DIV_ROUND_UP(bitcount + 3, 8) + 4 + len); - /* - * Output BFINAL (1 bit) and BTYPE (2 bits), then align - * to a byte boundary. - */ - STATIC_ASSERT(DEFLATE_BLOCKTYPE_UNCOMPRESSED == 0); - *out_next++ = (bfinal << bitcount) | bitbuf; - if (bitcount > 5) - *out_next++ = 0; - bitbuf = 0; - bitcount = 0; - /* Output LEN and NLEN, then the data itself. */ - put_unaligned_le16(len, out_next); - out_next += 2; - put_unaligned_le16(~len, out_next); - out_next += 2; - memcpy(out_next, in_next, len); - out_next += len; - in_next += len; - } while (in_next != in_end); - /* Done outputting uncompressed block(s) */ - goto out; - } - - if (best_cost == static_cost) { - /* Static Huffman block */ - codes = &c->static_codes; - ADD_BITS(is_final_block, 1); - ADD_BITS(DEFLATE_BLOCKTYPE_STATIC_HUFFMAN, 2); - FLUSH_BITS(); - } else { - const unsigned num_explicit_lens = c->o.precode.num_explicit_lens; - const unsigned num_precode_items = c->o.precode.num_items; - unsigned precode_sym, precode_item; - unsigned i; - - /* Dynamic Huffman block */ - - codes = &c->codes; - STATIC_ASSERT(CAN_BUFFER(1 + 2 + 5 + 5 + 4 + 3)); - ADD_BITS(is_final_block, 1); - ADD_BITS(DEFLATE_BLOCKTYPE_DYNAMIC_HUFFMAN, 2); - ADD_BITS(c->o.precode.num_litlen_syms - 257, 5); - ADD_BITS(c->o.precode.num_offset_syms - 1, 5); - ADD_BITS(num_explicit_lens - 4, 4); - - /* Output the lengths of the codewords in the precode. */ - if (CAN_BUFFER(3 * (DEFLATE_NUM_PRECODE_SYMS - 1))) { - /* - * A 64-bit bitbuffer is just one bit too small to hold - * the maximum number of precode lens, so to minimize - * flushes we merge one len with the previous fields. - */ - precode_sym = deflate_precode_lens_permutation[0]; - ADD_BITS(c->o.precode.lens[precode_sym], 3); - FLUSH_BITS(); - i = 1; /* num_explicit_lens >= 4 */ - do { - precode_sym = - deflate_precode_lens_permutation[i]; - ADD_BITS(c->o.precode.lens[precode_sym], 3); - } while (++i < num_explicit_lens); - FLUSH_BITS(); - } else { - FLUSH_BITS(); - i = 0; - do { - precode_sym = - deflate_precode_lens_permutation[i]; - ADD_BITS(c->o.precode.lens[precode_sym], 3); - FLUSH_BITS(); - } while (++i < num_explicit_lens); - } - - /* - * Output the lengths of the codewords in the litlen and offset - * codes, encoded by the precode. - */ - i = 0; - do { - precode_item = c->o.precode.items[i]; - precode_sym = precode_item & 0x1F; - STATIC_ASSERT(CAN_BUFFER(MAX_PRE_CODEWORD_LEN + 7)); - ADD_BITS(c->o.precode.codewords[precode_sym], - c->o.precode.lens[precode_sym]); - ADD_BITS(precode_item >> 5, - deflate_extra_precode_bits[precode_sym]); - FLUSH_BITS(); - } while (++i < num_precode_items); - } - - /* Output the literals and matches for a dynamic or static block. */ - ASSERT(bitcount <= 7); - deflate_compute_full_len_codewords(c, codes); -#if SUPPORT_NEAR_OPTIMAL_PARSING - if (sequences == NULL) { - /* Output the literals and matches from the minimum-cost path */ - struct deflate_optimum_node *cur_node = - &c->p.n.optimum_nodes[0]; - struct deflate_optimum_node * const end_node = - &c->p.n.optimum_nodes[block_length]; - do { - u32 length = cur_node->item & OPTIMUM_LEN_MASK; - u32 offset = cur_node->item >> OPTIMUM_OFFSET_SHIFT; - - if (length == 1) { - /* Literal */ - ADD_BITS(codes->codewords.litlen[offset], - codes->lens.litlen[offset]); - FLUSH_BITS(); - } else { - /* Match */ - WRITE_MATCH(c, codes, length, offset, - c->p.n.offset_slot_full[offset]); - } - cur_node += length; - } while (cur_node != end_node); - } else -#endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - { - /* Output the literals and matches from the sequences list. */ - const struct deflate_sequence *seq; - - for (seq = sequences; ; seq++) { - u32 litrunlen = seq->litrunlen_and_length & - SEQ_LITRUNLEN_MASK; - u32 length = seq->litrunlen_and_length >> - SEQ_LENGTH_SHIFT; - unsigned lit; - - /* Output a run of literals. */ - if (CAN_BUFFER(4 * MAX_LITLEN_CODEWORD_LEN)) { - for (; litrunlen >= 4; litrunlen -= 4) { - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - FLUSH_BITS(); - } - if (litrunlen-- != 0) { - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - if (litrunlen-- != 0) { - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - if (litrunlen-- != 0) { - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - } - } - FLUSH_BITS(); - } - } else { - while (litrunlen--) { - lit = *in_next++; - ADD_BITS(codes->codewords.litlen[lit], - codes->lens.litlen[lit]); - FLUSH_BITS(); - } - } - - if (length == 0) { /* Last sequence? */ - ASSERT(in_next == in_end); - break; - } - - /* Output a match. */ - WRITE_MATCH(c, codes, length, seq->offset, - seq->offset_slot); - in_next += length; - } - } - - /* Output the end-of-block symbol. */ - ASSERT(bitcount <= 7); - ADD_BITS(codes->codewords.litlen[DEFLATE_END_OF_BLOCK], - codes->lens.litlen[DEFLATE_END_OF_BLOCK]); - FLUSH_BITS(); -out: - ASSERT(bitcount <= 7); - /* - * Assert that the block cost was computed correctly. This is relied on - * above for the bounds check on the output buffer. Also, - * libdeflate_deflate_compress_bound() relies on this via the assumption - * that uncompressed blocks will always be used when cheapest. - */ - ASSERT(8 * (out_next - os->next) + bitcount - os->bitcount == best_cost); - os->bitbuf = bitbuf; - os->bitcount = bitcount; - os->next = out_next; -} - -static void -deflate_finish_block(struct libdeflate_compressor *c, - struct deflate_output_bitstream *os, - const u8 *block_begin, u32 block_length, - const struct deflate_sequence *sequences, - bool is_final_block) -{ - c->freqs.litlen[DEFLATE_END_OF_BLOCK]++; - deflate_make_huffman_codes(&c->freqs, &c->codes); - deflate_flush_block(c, os, block_begin, block_length, sequences, - is_final_block); -} - -/******************************************************************************/ - -/* - * Block splitting algorithm. The problem is to decide when it is worthwhile to - * start a new block with new Huffman codes. There is a theoretically optimal - * solution: recursively consider every possible block split, considering the - * exact cost of each block, and choose the minimum cost approach. But this is - * far too slow. Instead, as an approximation, we can count symbols and after - * every N symbols, compare the expected distribution of symbols based on the - * previous data with the actual distribution. If they differ "by enough", then - * start a new block. - * - * As an optimization and heuristic, we don't distinguish between every symbol - * but rather we combine many symbols into a single "observation type". For - * literals we only look at the high bits and low bits, and for matches we only - * look at whether the match is long or not. The assumption is that for typical - * "real" data, places that are good block boundaries will tend to be noticeable - * based only on changes in these aggregate probabilities, without looking for - * subtle differences in individual symbols. For example, a change from ASCII - * bytes to non-ASCII bytes, or from few matches (generally less compressible) - * to many matches (generally more compressible), would be easily noticed based - * on the aggregates. - * - * For determining whether the probability distributions are "different enough" - * to start a new block, the simple heuristic of splitting when the sum of - * absolute differences exceeds a constant seems to be good enough. We also add - * a number proportional to the block length so that the algorithm is more - * likely to end long blocks than short blocks. This reflects the general - * expectation that it will become increasingly beneficial to start a new block - * as the current block grows longer. - * - * Finally, for an approximation, it is not strictly necessary that the exact - * symbols being used are considered. With "near-optimal parsing", for example, - * the actual symbols that will be used are unknown until after the block - * boundary is chosen and the block has been optimized. Since the final choices - * cannot be used, we can use preliminary "greedy" choices instead. - */ - -/* Initialize the block split statistics when starting a new block. */ -static void -init_block_split_stats(struct block_split_stats *stats) -{ - int i; - - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) { - stats->new_observations[i] = 0; - stats->observations[i] = 0; - } - stats->num_new_observations = 0; - stats->num_observations = 0; -} - -/* - * Literal observation. Heuristic: use the top 2 bits and low 1 bits of the - * literal, for 8 possible literal observation types. - */ -static forceinline void -observe_literal(struct block_split_stats *stats, u8 lit) -{ - stats->new_observations[((lit >> 5) & 0x6) | (lit & 1)]++; - stats->num_new_observations++; -} - -/* - * Match observation. Heuristic: use one observation type for "short match" and - * one observation type for "long match". - */ -static forceinline void -observe_match(struct block_split_stats *stats, u32 length) -{ - stats->new_observations[NUM_LITERAL_OBSERVATION_TYPES + - (length >= 9)]++; - stats->num_new_observations++; -} - -static void -merge_new_observations(struct block_split_stats *stats) -{ - int i; - - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) { - stats->observations[i] += stats->new_observations[i]; - stats->new_observations[i] = 0; - } - stats->num_observations += stats->num_new_observations; - stats->num_new_observations = 0; -} - -static bool -do_end_block_check(struct block_split_stats *stats, u32 block_length) -{ - if (stats->num_observations > 0) { - /* - * Compute the sum of absolute differences of probabilities. To - * avoid needing to use floating point arithmetic or do slow - * divisions, we do all arithmetic with the probabilities - * multiplied by num_observations * num_new_observations. E.g., - * for the "old" observations the probabilities would be - * (double)observations[i] / num_observations, but since we - * multiply by both num_observations and num_new_observations we - * really do observations[i] * num_new_observations. - */ - u32 total_delta = 0; - u32 num_items; - u32 cutoff; - int i; - - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) { - u32 expected = stats->observations[i] * - stats->num_new_observations; - u32 actual = stats->new_observations[i] * - stats->num_observations; - u32 delta = (actual > expected) ? actual - expected : - expected - actual; - - total_delta += delta; - } - - num_items = stats->num_observations + - stats->num_new_observations; - /* - * Heuristic: the cutoff is when the sum of absolute differences - * of probabilities becomes at least 200/512. As above, the - * probability is multiplied by both num_new_observations and - * num_observations. Be careful to avoid integer overflow. - */ - cutoff = stats->num_new_observations * 200 / 512 * - stats->num_observations; - /* - * Very short blocks have a lot of overhead for the Huffman - * codes, so only use them if it clearly seems worthwhile. - * (This is an additional penalty, which adds to the smaller - * penalty below which scales more slowly.) - */ - if (block_length < 10000 && num_items < 8192) - cutoff += (u64)cutoff * (8192 - num_items) / 8192; - - /* Ready to end the block? */ - if (total_delta + - (block_length / 4096) * stats->num_observations >= cutoff) - return true; - } - merge_new_observations(stats); - return false; -} - -static forceinline bool -ready_to_check_block(const struct block_split_stats *stats, - const u8 *in_block_begin, const u8 *in_next, - const u8 *in_end) -{ - return stats->num_new_observations >= NUM_OBSERVATIONS_PER_BLOCK_CHECK - && in_next - in_block_begin >= MIN_BLOCK_LENGTH - && in_end - in_next >= MIN_BLOCK_LENGTH; -} - -static forceinline bool -should_end_block(struct block_split_stats *stats, - const u8 *in_block_begin, const u8 *in_next, const u8 *in_end) -{ - /* Ready to try to end the block (again)? */ - if (!ready_to_check_block(stats, in_block_begin, in_next, in_end)) - return false; - - return do_end_block_check(stats, in_next - in_block_begin); -} - -/******************************************************************************/ - -static void -deflate_begin_sequences(struct libdeflate_compressor *c, - struct deflate_sequence *first_seq) -{ - deflate_reset_symbol_frequencies(c); - first_seq->litrunlen_and_length = 0; -} - -static forceinline void -deflate_choose_literal(struct libdeflate_compressor *c, unsigned literal, - bool gather_split_stats, struct deflate_sequence *seq) -{ - c->freqs.litlen[literal]++; - - if (gather_split_stats) - observe_literal(&c->split_stats, literal); - - STATIC_ASSERT(MAX_BLOCK_LENGTH <= SEQ_LITRUNLEN_MASK); - seq->litrunlen_and_length++; -} - -static forceinline void -deflate_choose_match(struct libdeflate_compressor *c, - u32 length, u32 offset, bool gather_split_stats, - struct deflate_sequence **seq_p) -{ - struct deflate_sequence *seq = *seq_p; - unsigned length_slot = deflate_length_slot[length]; - unsigned offset_slot = deflate_get_offset_slot(offset); - - c->freqs.litlen[DEFLATE_FIRST_LEN_SYM + length_slot]++; - c->freqs.offset[offset_slot]++; - if (gather_split_stats) - observe_match(&c->split_stats, length); - - seq->litrunlen_and_length |= length << SEQ_LENGTH_SHIFT; - seq->offset = offset; - seq->offset_slot = offset_slot; - - seq++; - seq->litrunlen_and_length = 0; - *seq_p = seq; -} - -/* - * Decrease the maximum and nice match lengths if we're approaching the end of - * the input buffer. - */ -static forceinline void -adjust_max_and_nice_len(u32 *max_len, u32 *nice_len, size_t remaining) -{ - if (unlikely(remaining < DEFLATE_MAX_MATCH_LEN)) { - *max_len = remaining; - *nice_len = MIN(*nice_len, *max_len); - } -} - -/* - * Choose the minimum match length for the greedy and lazy parsers. - * - * By default the minimum match length is 3, which is the smallest length the - * DEFLATE format allows. However, with greedy and lazy parsing, some data - * (e.g. DNA sequencing data) benefits greatly from a longer minimum length. - * Typically, this is because literals are very cheap. In general, the - * near-optimal parser handles this case naturally, but the greedy and lazy - * parsers need a heuristic to decide when to use short matches. - * - * The heuristic we use is to make the minimum match length depend on the number - * of different literals that exist in the data. If there are many different - * literals, then literals will probably be expensive, so short matches will - * probably be worthwhile. Conversely, if not many literals are used, then - * probably literals will be cheap and short matches won't be worthwhile. - */ -static u32 -choose_min_match_len(u32 num_used_literals, u32 max_search_depth) -{ - /* map from num_used_literals to min_len */ - static const u8 min_lens[] = { - 9, 9, 9, 9, 9, 9, 8, 8, 7, 7, 6, 6, 6, 6, 6, 6, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - /* The rest is implicitly 3. */ - }; - u32 min_len; - - STATIC_ASSERT(DEFLATE_MIN_MATCH_LEN <= 3); - STATIC_ASSERT(ARRAY_LEN(min_lens) <= DEFLATE_NUM_LITERALS + 1); - - if (num_used_literals >= ARRAY_LEN(min_lens)) - return 3; - min_len = min_lens[num_used_literals]; - /* - * With a low max_search_depth, it may be too hard to find long matches. - */ - if (max_search_depth < 16) { - if (max_search_depth < 5) - min_len = MIN(min_len, 4); - else if (max_search_depth < 10) - min_len = MIN(min_len, 5); - else - min_len = MIN(min_len, 7); - } - return min_len; -} - -static u32 -calculate_min_match_len(const u8 *data, size_t data_len, u32 max_search_depth) -{ - u8 used[256] = { 0 }; - u32 num_used_literals = 0; - size_t i; - - /* - * For very short inputs, the static Huffman code has a good chance of - * being best, in which case there is no reason to avoid short matches. - */ - if (data_len < 512) - return DEFLATE_MIN_MATCH_LEN; - - /* - * For an initial approximation, scan the first 4 KiB of data. The - * caller may use recalculate_min_match_len() to update min_len later. - */ - data_len = MIN(data_len, 4096); - for (i = 0; i < data_len; i++) - used[data[i]] = 1; - for (i = 0; i < 256; i++) - num_used_literals += used[i]; - return choose_min_match_len(num_used_literals, max_search_depth); -} - -/* - * Recalculate the minimum match length for a block, now that we know the - * distribution of literals that are actually being used (freqs->litlen). - */ -static u32 -recalculate_min_match_len(const struct deflate_freqs *freqs, - u32 max_search_depth) -{ - u32 literal_freq = 0; - u32 cutoff; - u32 num_used_literals = 0; - int i; - - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) - literal_freq += freqs->litlen[i]; - - cutoff = literal_freq >> 10; /* Ignore literals used very rarely. */ - - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) { - if (freqs->litlen[i] > cutoff) - num_used_literals++; - } - return choose_min_match_len(num_used_literals, max_search_depth); -} - -static forceinline const u8 * -choose_max_block_end(const u8 *in_block_begin, const u8 *in_end, - size_t soft_max_len) -{ - if (in_end - in_block_begin < soft_max_len + MIN_BLOCK_LENGTH) - return in_end; - return in_block_begin + soft_max_len; -} - -/* - * This is the level 0 "compressor". It always outputs uncompressed blocks. - */ -static size_t -deflate_compress_none(const u8 *in, size_t in_nbytes, - u8 *out, size_t out_nbytes_avail) -{ - const u8 *in_next = in; - const u8 * const in_end = in + in_nbytes; - u8 *out_next = out; - u8 * const out_end = out + out_nbytes_avail; - - /* - * If the input is zero-length, we still must output a block in order - * for the output to be a valid DEFLATE stream. Handle this case - * specially to avoid potentially passing NULL to memcpy() below. - */ - if (unlikely(in_nbytes == 0)) { - if (out_nbytes_avail < 5) - return 0; - /* BFINAL and BTYPE */ - *out_next++ = 1 | (DEFLATE_BLOCKTYPE_UNCOMPRESSED << 1); - /* LEN and NLEN */ - put_unaligned_le32(0xFFFF0000, out_next); - return 5; - } - - do { - u8 bfinal = 0; - size_t len = UINT16_MAX; - - if (in_end - in_next <= UINT16_MAX) { - bfinal = 1; - len = in_end - in_next; - } - if (out_end - out_next < 5 + len) - return 0; - /* - * Output BFINAL and BTYPE. The stream is already byte-aligned - * here, so this step always requires outputting exactly 1 byte. - */ - *out_next++ = bfinal | (DEFLATE_BLOCKTYPE_UNCOMPRESSED << 1); - - /* Output LEN and NLEN, then the data itself. */ - put_unaligned_le16(len, out_next); - out_next += 2; - put_unaligned_le16(~len, out_next); - out_next += 2; - memcpy(out_next, in_next, len); - out_next += len; - in_next += len; - } while (in_next != in_end); - - return out_next - out; -} - -/* - * This is a faster variant of deflate_compress_greedy(). It uses the - * ht_matchfinder rather than the hc_matchfinder. It also skips the block - * splitting algorithm and just uses fixed length blocks. c->max_search_depth - * has no effect with this algorithm, as it is hardcoded in ht_matchfinder.h. - */ -static void -deflate_compress_fastest(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os) -{ - const u8 *in_next = in; - const u8 *in_end = in_next + in_nbytes; - const u8 *in_cur_base = in_next; - u32 max_len = DEFLATE_MAX_MATCH_LEN; - u32 nice_len = MIN(c->nice_match_length, max_len); - u32 next_hash = 0; - - ht_matchfinder_init(&c->p.f.ht_mf); - - do { - /* Starting a new DEFLATE block */ - - const u8 * const in_block_begin = in_next; - const u8 * const in_max_block_end = choose_max_block_end( - in_next, in_end, FAST_SOFT_MAX_BLOCK_LENGTH); - struct deflate_sequence *seq = c->p.f.sequences; - - deflate_begin_sequences(c, seq); - - do { - u32 length; - u32 offset; - size_t remaining = in_end - in_next; - - if (unlikely(remaining < DEFLATE_MAX_MATCH_LEN)) { - max_len = remaining; - if (max_len < HT_MATCHFINDER_REQUIRED_NBYTES) { - do { - deflate_choose_literal(c, - *in_next++, false, seq); - } while (--max_len); - break; - } - nice_len = MIN(nice_len, max_len); - } - length = ht_matchfinder_longest_match(&c->p.f.ht_mf, - &in_cur_base, - in_next, - max_len, - nice_len, - &next_hash, - &offset); - if (length) { - /* Match found */ - deflate_choose_match(c, length, offset, false, - &seq); - ht_matchfinder_skip_bytes(&c->p.f.ht_mf, - &in_cur_base, - in_next + 1, - in_end, - length - 1, - &next_hash); - in_next += length; - } else { - /* No match found */ - deflate_choose_literal(c, *in_next++, false, - seq); - } - - /* Check if it's time to output another block. */ - } while (in_next < in_max_block_end && - seq < &c->p.f.sequences[FAST_SEQ_STORE_LENGTH]); - - deflate_finish_block(c, os, in_block_begin, - in_next - in_block_begin, - c->p.f.sequences, in_next == in_end); - } while (in_next != in_end && !os->overflow); -} - -/* - * This is the "greedy" DEFLATE compressor. It always chooses the longest match. - */ -static void -deflate_compress_greedy(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os) -{ - const u8 *in_next = in; - const u8 *in_end = in_next + in_nbytes; - const u8 *in_cur_base = in_next; - u32 max_len = DEFLATE_MAX_MATCH_LEN; - u32 nice_len = MIN(c->nice_match_length, max_len); - u32 next_hashes[2] = {0, 0}; - - hc_matchfinder_init(&c->p.g.hc_mf); - - do { - /* Starting a new DEFLATE block */ - - const u8 * const in_block_begin = in_next; - const u8 * const in_max_block_end = choose_max_block_end( - in_next, in_end, SOFT_MAX_BLOCK_LENGTH); - struct deflate_sequence *seq = c->p.g.sequences; - u32 min_len; - - init_block_split_stats(&c->split_stats); - deflate_begin_sequences(c, seq); - min_len = calculate_min_match_len(in_next, - in_max_block_end - in_next, - c->max_search_depth); - do { - u32 length; - u32 offset; - - adjust_max_and_nice_len(&max_len, &nice_len, - in_end - in_next); - length = hc_matchfinder_longest_match( - &c->p.g.hc_mf, - &in_cur_base, - in_next, - min_len - 1, - max_len, - nice_len, - c->max_search_depth, - next_hashes, - &offset); - - if (length >= min_len && - (length > DEFLATE_MIN_MATCH_LEN || - offset <= 4096)) { - /* Match found */ - deflate_choose_match(c, length, offset, true, - &seq); - hc_matchfinder_skip_bytes(&c->p.g.hc_mf, - &in_cur_base, - in_next + 1, - in_end, - length - 1, - next_hashes); - in_next += length; - } else { - /* No match found */ - deflate_choose_literal(c, *in_next++, true, - seq); - } - - /* Check if it's time to output another block. */ - } while (in_next < in_max_block_end && - seq < &c->p.g.sequences[SEQ_STORE_LENGTH] && - !should_end_block(&c->split_stats, - in_block_begin, in_next, in_end)); - - deflate_finish_block(c, os, in_block_begin, - in_next - in_block_begin, - c->p.g.sequences, in_next == in_end); - } while (in_next != in_end && !os->overflow); -} - -static forceinline void -deflate_compress_lazy_generic(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os, bool lazy2) -{ - const u8 *in_next = in; - const u8 *in_end = in_next + in_nbytes; - const u8 *in_cur_base = in_next; - u32 max_len = DEFLATE_MAX_MATCH_LEN; - u32 nice_len = MIN(c->nice_match_length, max_len); - u32 next_hashes[2] = {0, 0}; - - hc_matchfinder_init(&c->p.g.hc_mf); - - do { - /* Starting a new DEFLATE block */ - - const u8 * const in_block_begin = in_next; - const u8 * const in_max_block_end = choose_max_block_end( - in_next, in_end, SOFT_MAX_BLOCK_LENGTH); - const u8 *next_recalc_min_len = - in_next + MIN(in_end - in_next, 10000); - struct deflate_sequence *seq = c->p.g.sequences; - u32 min_len; - - init_block_split_stats(&c->split_stats); - deflate_begin_sequences(c, seq); - min_len = calculate_min_match_len(in_next, - in_max_block_end - in_next, - c->max_search_depth); - do { - u32 cur_len; - u32 cur_offset; - u32 next_len; - u32 next_offset; - - /* - * Recalculate the minimum match length if it hasn't - * been done recently. - */ - if (in_next >= next_recalc_min_len) { - min_len = recalculate_min_match_len( - &c->freqs, - c->max_search_depth); - next_recalc_min_len += - MIN(in_end - next_recalc_min_len, - in_next - in_block_begin); - } - - /* Find the longest match at the current position. */ - adjust_max_and_nice_len(&max_len, &nice_len, - in_end - in_next); - cur_len = hc_matchfinder_longest_match( - &c->p.g.hc_mf, - &in_cur_base, - in_next, - min_len - 1, - max_len, - nice_len, - c->max_search_depth, - next_hashes, - &cur_offset); - if (cur_len < min_len || - (cur_len == DEFLATE_MIN_MATCH_LEN && - cur_offset > 8192)) { - /* No match found. Choose a literal. */ - deflate_choose_literal(c, *in_next++, true, - seq); - continue; - } - in_next++; - -have_cur_match: - /* - * We have a match at the current position. - * If it's very long, choose it immediately. - */ - if (cur_len >= nice_len) { - deflate_choose_match(c, cur_len, cur_offset, - true, &seq); - hc_matchfinder_skip_bytes(&c->p.g.hc_mf, - &in_cur_base, - in_next, - in_end, - cur_len - 1, - next_hashes); - in_next += cur_len - 1; - continue; - } - - /* - * Try to find a better match at the next position. - * - * Note: since we already have a match at the *current* - * position, we use only half the 'max_search_depth' - * when checking the *next* position. This is a useful - * trade-off because it's more worthwhile to use a - * greater search depth on the initial match. - * - * Note: it's possible to structure the code such that - * there's only one call to longest_match(), which - * handles both the "find the initial match" and "try to - * find a better match" cases. However, it is faster to - * have two call sites, with longest_match() inlined at - * each. - */ - adjust_max_and_nice_len(&max_len, &nice_len, - in_end - in_next); - next_len = hc_matchfinder_longest_match( - &c->p.g.hc_mf, - &in_cur_base, - in_next++, - cur_len - 1, - max_len, - nice_len, - c->max_search_depth >> 1, - next_hashes, - &next_offset); - if (next_len >= cur_len && - 4 * (int)(next_len - cur_len) + - ((int)bsr32(cur_offset) - - (int)bsr32(next_offset)) > 2) { - /* - * Found a better match at the next position. - * Output a literal. Then the next match - * becomes the current match. - */ - deflate_choose_literal(c, *(in_next - 2), true, - seq); - cur_len = next_len; - cur_offset = next_offset; - goto have_cur_match; - } - - if (lazy2) { - /* In lazy2 mode, look ahead another position */ - adjust_max_and_nice_len(&max_len, &nice_len, - in_end - in_next); - next_len = hc_matchfinder_longest_match( - &c->p.g.hc_mf, - &in_cur_base, - in_next++, - cur_len - 1, - max_len, - nice_len, - c->max_search_depth >> 2, - next_hashes, - &next_offset); - if (next_len >= cur_len && - 4 * (int)(next_len - cur_len) + - ((int)bsr32(cur_offset) - - (int)bsr32(next_offset)) > 6) { - /* - * There's a much better match two - * positions ahead, so use two literals. - */ - deflate_choose_literal( - c, *(in_next - 3), true, seq); - deflate_choose_literal( - c, *(in_next - 2), true, seq); - cur_len = next_len; - cur_offset = next_offset; - goto have_cur_match; - } - /* - * No better match at either of the next 2 - * positions. Output the current match. - */ - deflate_choose_match(c, cur_len, cur_offset, - true, &seq); - if (cur_len > 3) { - hc_matchfinder_skip_bytes(&c->p.g.hc_mf, - &in_cur_base, - in_next, - in_end, - cur_len - 3, - next_hashes); - in_next += cur_len - 3; - } - } else { /* !lazy2 */ - /* - * No better match at the next position. Output - * the current match. - */ - deflate_choose_match(c, cur_len, cur_offset, - true, &seq); - hc_matchfinder_skip_bytes(&c->p.g.hc_mf, - &in_cur_base, - in_next, - in_end, - cur_len - 2, - next_hashes); - in_next += cur_len - 2; - } - /* Check if it's time to output another block. */ - } while (in_next < in_max_block_end && - seq < &c->p.g.sequences[SEQ_STORE_LENGTH] && - !should_end_block(&c->split_stats, - in_block_begin, in_next, in_end)); - - deflate_finish_block(c, os, in_block_begin, - in_next - in_block_begin, - c->p.g.sequences, in_next == in_end); - } while (in_next != in_end && !os->overflow); -} - -/* - * This is the "lazy" DEFLATE compressor. Before choosing a match, it checks to - * see if there's a better match at the next position. If yes, it outputs a - * literal and continues to the next position. If no, it outputs the match. - */ -static void -deflate_compress_lazy(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os) -{ - deflate_compress_lazy_generic(c, in, in_nbytes, os, false); -} - -/* - * The lazy2 compressor. This is similar to the regular lazy one, but it looks - * for a better match at the next 2 positions rather than the next 1. This - * makes it take slightly more time, but compress some inputs slightly more. - */ -static void -deflate_compress_lazy2(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os) -{ - deflate_compress_lazy_generic(c, in, in_nbytes, os, true); -} - -#if SUPPORT_NEAR_OPTIMAL_PARSING - -/* - * Follow the minimum-cost path in the graph of possible match/literal choices - * for the current block and compute the frequencies of the Huffman symbols that - * would be needed to output those matches and literals. - */ -static void -deflate_tally_item_list(struct libdeflate_compressor *c, u32 block_length) -{ - struct deflate_optimum_node *cur_node = &c->p.n.optimum_nodes[0]; - struct deflate_optimum_node *end_node = - &c->p.n.optimum_nodes[block_length]; - - do { - u32 length = cur_node->item & OPTIMUM_LEN_MASK; - u32 offset = cur_node->item >> OPTIMUM_OFFSET_SHIFT; - - if (length == 1) { - /* Literal */ - c->freqs.litlen[offset]++; - } else { - /* Match */ - c->freqs.litlen[DEFLATE_FIRST_LEN_SYM + - deflate_length_slot[length]]++; - c->freqs.offset[c->p.n.offset_slot_full[offset]]++; - } - cur_node += length; - } while (cur_node != end_node); - - /* Tally the end-of-block symbol. */ - c->freqs.litlen[DEFLATE_END_OF_BLOCK]++; -} - -static void -deflate_choose_all_literals(struct libdeflate_compressor *c, - const u8 *block, u32 block_length) -{ - u32 i; - - deflate_reset_symbol_frequencies(c); - for (i = 0; i < block_length; i++) - c->freqs.litlen[block[i]]++; - c->freqs.litlen[DEFLATE_END_OF_BLOCK]++; - - deflate_make_huffman_codes(&c->freqs, &c->codes); -} - -/* - * Compute the exact cost, in bits, that would be required to output the matches - * and literals described by @c->freqs as a dynamic Huffman block. The litlen - * and offset codes are assumed to have already been built in @c->codes. - */ -static u32 -deflate_compute_true_cost(struct libdeflate_compressor *c) -{ - u32 cost = 0; - unsigned sym; - - deflate_precompute_huffman_header(c); - - memset(&c->codes.lens.litlen[c->o.precode.num_litlen_syms], 0, - DEFLATE_NUM_LITLEN_SYMS - c->o.precode.num_litlen_syms); - - cost += 5 + 5 + 4 + (3 * c->o.precode.num_explicit_lens); - for (sym = 0; sym < DEFLATE_NUM_PRECODE_SYMS; sym++) { - cost += c->o.precode.freqs[sym] * - (c->o.precode.lens[sym] + - deflate_extra_precode_bits[sym]); - } - - for (sym = 0; sym < DEFLATE_FIRST_LEN_SYM; sym++) - cost += c->freqs.litlen[sym] * c->codes.lens.litlen[sym]; - - for (; sym < DEFLATE_FIRST_LEN_SYM + - ARRAY_LEN(deflate_extra_length_bits); sym++) - cost += c->freqs.litlen[sym] * - (c->codes.lens.litlen[sym] + - deflate_extra_length_bits[sym - DEFLATE_FIRST_LEN_SYM]); - - for (sym = 0; sym < ARRAY_LEN(deflate_extra_offset_bits); sym++) - cost += c->freqs.offset[sym] * - (c->codes.lens.offset[sym] + - deflate_extra_offset_bits[sym]); - return cost; -} - -/* Set the current cost model from the codeword lengths specified in @lens. */ -static void -deflate_set_costs_from_codes(struct libdeflate_compressor *c, - const struct deflate_lens *lens) -{ - unsigned i; - - /* Literals */ - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) { - u32 bits = (lens->litlen[i] ? - lens->litlen[i] : LITERAL_NOSTAT_BITS); - - c->p.n.costs.literal[i] = bits * BIT_COST; - } - - /* Lengths */ - for (i = DEFLATE_MIN_MATCH_LEN; i <= DEFLATE_MAX_MATCH_LEN; i++) { - unsigned length_slot = deflate_length_slot[i]; - unsigned litlen_sym = DEFLATE_FIRST_LEN_SYM + length_slot; - u32 bits = (lens->litlen[litlen_sym] ? - lens->litlen[litlen_sym] : LENGTH_NOSTAT_BITS); - - bits += deflate_extra_length_bits[length_slot]; - c->p.n.costs.length[i] = bits * BIT_COST; - } - - /* Offset slots */ - for (i = 0; i < ARRAY_LEN(deflate_offset_slot_base); i++) { - u32 bits = (lens->offset[i] ? - lens->offset[i] : OFFSET_NOSTAT_BITS); - - bits += deflate_extra_offset_bits[i]; - c->p.n.costs.offset_slot[i] = bits * BIT_COST; - } -} - -/* - * This lookup table gives the default cost of a literal symbol and of a length - * symbol, depending on the characteristics of the input data. It was generated - * by scripts/gen_default_litlen_costs.py. - * - * This table is indexed first by the estimated match probability: - * - * i=0: data doesn't contain many matches [match_prob=0.25] - * i=1: neutral [match_prob=0.50] - * i=2: data contains lots of matches [match_prob=0.75] - * - * This lookup produces a subtable which maps the number of distinct used - * literals to the default cost of a literal symbol, i.e.: - * - * int(-log2((1 - match_prob) / num_used_literals) * BIT_COST) - * - * ... for num_used_literals in [1, 256] (and 0, which is copied from 1). This - * accounts for literals usually getting cheaper as the number of distinct - * literals decreases, and as the proportion of literals to matches increases. - * - * The lookup also produces the cost of a length symbol, which is: - * - * int(-log2(match_prob/NUM_LEN_SLOTS) * BIT_COST) - * - * Note: we don't currently assign different costs to different literal symbols, - * or to different length symbols, as this is hard to do in a useful way. - */ -static const struct { - u8 used_lits_to_lit_cost[257]; - u8 len_sym_cost; -} default_litlen_costs[] = { - { /* match_prob = 0.25 */ - .used_lits_to_lit_cost = { - 6, 6, 22, 32, 38, 43, 48, 51, - 54, 57, 59, 61, 64, 65, 67, 69, - 70, 72, 73, 74, 75, 76, 77, 79, - 80, 80, 81, 82, 83, 84, 85, 85, - 86, 87, 88, 88, 89, 89, 90, 91, - 91, 92, 92, 93, 93, 94, 95, 95, - 96, 96, 96, 97, 97, 98, 98, 99, - 99, 99, 100, 100, 101, 101, 101, 102, - 102, 102, 103, 103, 104, 104, 104, 105, - 105, 105, 105, 106, 106, 106, 107, 107, - 107, 108, 108, 108, 108, 109, 109, 109, - 109, 110, 110, 110, 111, 111, 111, 111, - 112, 112, 112, 112, 112, 113, 113, 113, - 113, 114, 114, 114, 114, 114, 115, 115, - 115, 115, 115, 116, 116, 116, 116, 116, - 117, 117, 117, 117, 117, 118, 118, 118, - 118, 118, 118, 119, 119, 119, 119, 119, - 120, 120, 120, 120, 120, 120, 121, 121, - 121, 121, 121, 121, 121, 122, 122, 122, - 122, 122, 122, 123, 123, 123, 123, 123, - 123, 123, 124, 124, 124, 124, 124, 124, - 124, 125, 125, 125, 125, 125, 125, 125, - 125, 126, 126, 126, 126, 126, 126, 126, - 127, 127, 127, 127, 127, 127, 127, 127, - 128, 128, 128, 128, 128, 128, 128, 128, - 128, 129, 129, 129, 129, 129, 129, 129, - 129, 129, 130, 130, 130, 130, 130, 130, - 130, 130, 130, 131, 131, 131, 131, 131, - 131, 131, 131, 131, 131, 132, 132, 132, - 132, 132, 132, 132, 132, 132, 132, 133, - 133, 133, 133, 133, 133, 133, 133, 133, - 133, 134, 134, 134, 134, 134, 134, 134, - 134, - }, - .len_sym_cost = 109, - }, { /* match_prob = 0.5 */ - .used_lits_to_lit_cost = { - 16, 16, 32, 41, 48, 53, 57, 60, - 64, 66, 69, 71, 73, 75, 76, 78, - 80, 81, 82, 83, 85, 86, 87, 88, - 89, 90, 91, 92, 92, 93, 94, 95, - 96, 96, 97, 98, 98, 99, 99, 100, - 101, 101, 102, 102, 103, 103, 104, 104, - 105, 105, 106, 106, 107, 107, 108, 108, - 108, 109, 109, 110, 110, 110, 111, 111, - 112, 112, 112, 113, 113, 113, 114, 114, - 114, 115, 115, 115, 115, 116, 116, 116, - 117, 117, 117, 118, 118, 118, 118, 119, - 119, 119, 119, 120, 120, 120, 120, 121, - 121, 121, 121, 122, 122, 122, 122, 122, - 123, 123, 123, 123, 124, 124, 124, 124, - 124, 125, 125, 125, 125, 125, 126, 126, - 126, 126, 126, 127, 127, 127, 127, 127, - 128, 128, 128, 128, 128, 128, 129, 129, - 129, 129, 129, 129, 130, 130, 130, 130, - 130, 130, 131, 131, 131, 131, 131, 131, - 131, 132, 132, 132, 132, 132, 132, 133, - 133, 133, 133, 133, 133, 133, 134, 134, - 134, 134, 134, 134, 134, 134, 135, 135, - 135, 135, 135, 135, 135, 135, 136, 136, - 136, 136, 136, 136, 136, 136, 137, 137, - 137, 137, 137, 137, 137, 137, 138, 138, - 138, 138, 138, 138, 138, 138, 138, 139, - 139, 139, 139, 139, 139, 139, 139, 139, - 140, 140, 140, 140, 140, 140, 140, 140, - 140, 141, 141, 141, 141, 141, 141, 141, - 141, 141, 141, 142, 142, 142, 142, 142, - 142, 142, 142, 142, 142, 142, 143, 143, - 143, 143, 143, 143, 143, 143, 143, 143, - 144, - }, - .len_sym_cost = 93, - }, { /* match_prob = 0.75 */ - .used_lits_to_lit_cost = { - 32, 32, 48, 57, 64, 69, 73, 76, - 80, 82, 85, 87, 89, 91, 92, 94, - 96, 97, 98, 99, 101, 102, 103, 104, - 105, 106, 107, 108, 108, 109, 110, 111, - 112, 112, 113, 114, 114, 115, 115, 116, - 117, 117, 118, 118, 119, 119, 120, 120, - 121, 121, 122, 122, 123, 123, 124, 124, - 124, 125, 125, 126, 126, 126, 127, 127, - 128, 128, 128, 129, 129, 129, 130, 130, - 130, 131, 131, 131, 131, 132, 132, 132, - 133, 133, 133, 134, 134, 134, 134, 135, - 135, 135, 135, 136, 136, 136, 136, 137, - 137, 137, 137, 138, 138, 138, 138, 138, - 139, 139, 139, 139, 140, 140, 140, 140, - 140, 141, 141, 141, 141, 141, 142, 142, - 142, 142, 142, 143, 143, 143, 143, 143, - 144, 144, 144, 144, 144, 144, 145, 145, - 145, 145, 145, 145, 146, 146, 146, 146, - 146, 146, 147, 147, 147, 147, 147, 147, - 147, 148, 148, 148, 148, 148, 148, 149, - 149, 149, 149, 149, 149, 149, 150, 150, - 150, 150, 150, 150, 150, 150, 151, 151, - 151, 151, 151, 151, 151, 151, 152, 152, - 152, 152, 152, 152, 152, 152, 153, 153, - 153, 153, 153, 153, 153, 153, 154, 154, - 154, 154, 154, 154, 154, 154, 154, 155, - 155, 155, 155, 155, 155, 155, 155, 155, - 156, 156, 156, 156, 156, 156, 156, 156, - 156, 157, 157, 157, 157, 157, 157, 157, - 157, 157, 157, 158, 158, 158, 158, 158, - 158, 158, 158, 158, 158, 158, 159, 159, - 159, 159, 159, 159, 159, 159, 159, 159, - 160, - }, - .len_sym_cost = 84, - }, -}; - -/* - * Choose the default costs for literal and length symbols. These symbols are - * both part of the litlen alphabet. - */ -static void -deflate_choose_default_litlen_costs(struct libdeflate_compressor *c, - const u8 *block_begin, u32 block_length, - u32 *lit_cost, u32 *len_sym_cost) -{ - u32 num_used_literals = 0; - u32 literal_freq = block_length; - u32 match_freq = 0; - u32 cutoff; - u32 i; - - /* Calculate the number of distinct literals that exist in the data. */ - memset(c->freqs.litlen, 0, - DEFLATE_NUM_LITERALS * sizeof(c->freqs.litlen[0])); - cutoff = literal_freq >> 11; /* Ignore literals used very rarely. */ - for (i = 0; i < block_length; i++) - c->freqs.litlen[block_begin[i]]++; - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) { - if (c->freqs.litlen[i] > cutoff) - num_used_literals++; - } - if (num_used_literals == 0) - num_used_literals = 1; - - /* - * Estimate the relative frequency of literals and matches in the - * optimal parsing solution. We don't know the optimal solution, so - * this can only be a very rough estimate. Therefore, we basically use - * the match frequency from a greedy parse. We also apply the min_len - * heuristic used by the greedy and lazy parsers, to avoid counting too - * many matches when literals are cheaper than short matches. - */ - match_freq = 0; - i = choose_min_match_len(num_used_literals, c->max_search_depth); - for (; i < ARRAY_LEN(c->p.n.match_len_freqs); i++) { - match_freq += c->p.n.match_len_freqs[i]; - literal_freq -= i * c->p.n.match_len_freqs[i]; - } - if ((s32)literal_freq < 0) /* shouldn't happen */ - literal_freq = 0; - - if (match_freq > literal_freq) - i = 2; /* many matches */ - else if (match_freq * 4 > literal_freq) - i = 1; /* neutral */ - else - i = 0; /* few matches */ - - STATIC_ASSERT(BIT_COST == 16); - *lit_cost = default_litlen_costs[i].used_lits_to_lit_cost[ - num_used_literals]; - *len_sym_cost = default_litlen_costs[i].len_sym_cost; -} - -static forceinline u32 -deflate_default_length_cost(u32 len, u32 len_sym_cost) -{ - unsigned slot = deflate_length_slot[len]; - u32 num_extra_bits = deflate_extra_length_bits[slot]; - - return len_sym_cost + (num_extra_bits * BIT_COST); -} - -static forceinline u32 -deflate_default_offset_slot_cost(unsigned slot) -{ - u32 num_extra_bits = deflate_extra_offset_bits[slot]; - /* - * Assume that all offset symbols are equally probable. - * The resulting cost is 'int(-log2(1/30) * BIT_COST)', - * where 30 is the number of potentially-used offset symbols. - */ - u32 offset_sym_cost = 4*BIT_COST + (907*BIT_COST)/1000; - - return offset_sym_cost + (num_extra_bits * BIT_COST); -} - -/* Set default symbol costs for the first block's first optimization pass. */ -static void -deflate_set_default_costs(struct libdeflate_compressor *c, - u32 lit_cost, u32 len_sym_cost) -{ - u32 i; - - /* Literals */ - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) - c->p.n.costs.literal[i] = lit_cost; - - /* Lengths */ - for (i = DEFLATE_MIN_MATCH_LEN; i <= DEFLATE_MAX_MATCH_LEN; i++) - c->p.n.costs.length[i] = - deflate_default_length_cost(i, len_sym_cost); - - /* Offset slots */ - for (i = 0; i < ARRAY_LEN(deflate_offset_slot_base); i++) - c->p.n.costs.offset_slot[i] = - deflate_default_offset_slot_cost(i); -} - -static forceinline void -deflate_adjust_cost(u32 *cost_p, u32 default_cost, int change_amount) -{ - if (change_amount == 0) - /* Block is very similar to previous; prefer previous costs. */ - *cost_p = (default_cost + 3 * *cost_p) / 4; - else if (change_amount == 1) - *cost_p = (default_cost + *cost_p) / 2; - else if (change_amount == 2) - *cost_p = (5 * default_cost + 3 * *cost_p) / 8; - else - /* Block differs greatly from previous; prefer default costs. */ - *cost_p = (3 * default_cost + *cost_p) / 4; -} - -static forceinline void -deflate_adjust_costs_impl(struct libdeflate_compressor *c, - u32 lit_cost, u32 len_sym_cost, int change_amount) -{ - u32 i; - - /* Literals */ - for (i = 0; i < DEFLATE_NUM_LITERALS; i++) - deflate_adjust_cost(&c->p.n.costs.literal[i], lit_cost, - change_amount); - - /* Lengths */ - for (i = DEFLATE_MIN_MATCH_LEN; i <= DEFLATE_MAX_MATCH_LEN; i++) - deflate_adjust_cost(&c->p.n.costs.length[i], - deflate_default_length_cost(i, - len_sym_cost), - change_amount); - - /* Offset slots */ - for (i = 0; i < ARRAY_LEN(deflate_offset_slot_base); i++) - deflate_adjust_cost(&c->p.n.costs.offset_slot[i], - deflate_default_offset_slot_cost(i), - change_amount); -} - -/* - * Adjust the costs when beginning a new block. - * - * Since the current costs are optimized for the data already, it can be helpful - * to reuse them instead of starting over with the default costs. However, this - * depends on how similar the new block is to the previous block. Therefore, - * use a heuristic to decide how similar the blocks are, and mix together the - * current costs and the default costs accordingly. - */ -static void -deflate_adjust_costs(struct libdeflate_compressor *c, - u32 lit_cost, u32 len_sym_cost) -{ - u64 total_delta = 0; - u64 cutoff; - int i; - - /* - * Decide how different the current block is from the previous block, - * using the block splitting statistics from the current and previous - * blocks. The more different the current block is, the more we prefer - * the default costs rather than the previous block's costs. - * - * The algorithm here is similar to the end-of-block check one, but here - * we compare two entire blocks rather than a partial block with a small - * extra part, and therefore we need 64-bit numbers in some places. - */ - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) { - u64 prev = (u64)c->p.n.prev_observations[i] * - c->split_stats.num_observations; - u64 cur = (u64)c->split_stats.observations[i] * - c->p.n.prev_num_observations; - - total_delta += prev > cur ? prev - cur : cur - prev; - } - cutoff = ((u64)c->p.n.prev_num_observations * - c->split_stats.num_observations * 200) / 512; - - if (total_delta > 3 * cutoff) - /* Big change in the data; just use the default costs. */ - deflate_set_default_costs(c, lit_cost, len_sym_cost); - else if (4 * total_delta > 9 * cutoff) - deflate_adjust_costs_impl(c, lit_cost, len_sym_cost, 3); - else if (2 * total_delta > 3 * cutoff) - deflate_adjust_costs_impl(c, lit_cost, len_sym_cost, 2); - else if (2 * total_delta > cutoff) - deflate_adjust_costs_impl(c, lit_cost, len_sym_cost, 1); - else - deflate_adjust_costs_impl(c, lit_cost, len_sym_cost, 0); -} - -static void -deflate_set_initial_costs(struct libdeflate_compressor *c, - const u8 *block_begin, u32 block_length, - bool is_first_block) -{ - u32 lit_cost, len_sym_cost; - - deflate_choose_default_litlen_costs(c, block_begin, block_length, - &lit_cost, &len_sym_cost); - if (is_first_block) - deflate_set_default_costs(c, lit_cost, len_sym_cost); - else - deflate_adjust_costs(c, lit_cost, len_sym_cost); -} - -/* - * Find the minimum-cost path through the graph of possible match/literal - * choices for this block. - * - * We find the minimum cost path from 'c->p.n.optimum_nodes[0]', which - * represents the node at the beginning of the block, to - * 'c->p.n.optimum_nodes[block_length]', which represents the node at the end of - * the block. Edge costs are evaluated using the cost model 'c->p.n.costs'. - * - * The algorithm works backwards, starting at the end node and proceeding - * backwards one node at a time. At each node, the minimum cost to reach the - * end node is computed and the match/literal choice that begins that path is - * saved. - */ -static void -deflate_find_min_cost_path(struct libdeflate_compressor *c, - const u32 block_length, - const struct lz_match *cache_ptr) -{ - struct deflate_optimum_node *end_node = - &c->p.n.optimum_nodes[block_length]; - struct deflate_optimum_node *cur_node = end_node; - - cur_node->cost_to_end = 0; - do { - unsigned num_matches; - u32 literal; - u32 best_cost_to_end; - - cur_node--; - cache_ptr--; - - num_matches = cache_ptr->length; - literal = cache_ptr->offset; - - /* It's always possible to choose a literal. */ - best_cost_to_end = c->p.n.costs.literal[literal] + - (cur_node + 1)->cost_to_end; - cur_node->item = (literal << OPTIMUM_OFFSET_SHIFT) | 1; - - /* Also consider matches if there are any. */ - if (num_matches) { - const struct lz_match *match; - u32 len; - u32 offset; - u32 offset_slot; - u32 offset_cost; - u32 cost_to_end; - - /* - * Consider each length from the minimum - * (DEFLATE_MIN_MATCH_LEN) to the length of the longest - * match found at this position. For each length, we - * consider only the smallest offset for which that - * length is available. Although this is not guaranteed - * to be optimal due to the possibility of a larger - * offset costing less than a smaller offset to code, - * this is a very useful heuristic. - */ - match = cache_ptr - num_matches; - len = DEFLATE_MIN_MATCH_LEN; - do { - offset = match->offset; - offset_slot = c->p.n.offset_slot_full[offset]; - offset_cost = - c->p.n.costs.offset_slot[offset_slot]; - do { - cost_to_end = offset_cost + - c->p.n.costs.length[len] + - (cur_node + len)->cost_to_end; - if (cost_to_end < best_cost_to_end) { - best_cost_to_end = cost_to_end; - cur_node->item = len | - (offset << - OPTIMUM_OFFSET_SHIFT); - } - } while (++len <= match->length); - } while (++match != cache_ptr); - cache_ptr -= num_matches; - } - cur_node->cost_to_end = best_cost_to_end; - } while (cur_node != &c->p.n.optimum_nodes[0]); - - deflate_reset_symbol_frequencies(c); - deflate_tally_item_list(c, block_length); - deflate_make_huffman_codes(&c->freqs, &c->codes); -} - -/* - * Choose the literals and matches for the current block, then output the block. - * - * To choose the literal/match sequence, we find the minimum-cost path through - * the block's graph of literal/match choices, given a cost model. However, the - * true cost of each symbol is unknown until the Huffman codes have been built, - * but at the same time the Huffman codes depend on the frequencies of chosen - * symbols. Consequently, multiple passes must be used to try to approximate an - * optimal solution. The first pass uses default costs, mixed with the costs - * from the previous block when it seems appropriate. Later passes use the - * Huffman codeword lengths from the previous pass as the costs. - * - * As an alternate strategy, also consider using only literals. The boolean - * returned in *used_only_literals indicates whether that strategy was best. - */ -static void -deflate_optimize_and_flush_block(struct libdeflate_compressor *c, - struct deflate_output_bitstream *os, - const u8 *block_begin, u32 block_length, - const struct lz_match *cache_ptr, - bool is_first_block, bool is_final_block, - bool *used_only_literals) -{ - unsigned num_passes_remaining = c->p.n.max_optim_passes; - u32 best_true_cost = UINT32_MAX; - u32 true_cost; - u32 only_lits_cost; - u32 static_cost = UINT32_MAX; - struct deflate_sequence seq_; - struct deflate_sequence *seq = NULL; - u32 i; - - /* - * On some data, using only literals (no matches) ends up being better - * than what the iterative optimization algorithm produces. Therefore, - * consider using only literals. - */ - deflate_choose_all_literals(c, block_begin, block_length); - only_lits_cost = deflate_compute_true_cost(c); - - /* - * Force the block to really end at the desired length, even if some - * matches extend beyond it. - */ - for (i = block_length; - i <= MIN(block_length - 1 + DEFLATE_MAX_MATCH_LEN, - ARRAY_LEN(c->p.n.optimum_nodes) - 1); i++) - c->p.n.optimum_nodes[i].cost_to_end = 0x80000000; - - /* - * Sometimes a static Huffman block ends up being cheapest, particularly - * if the block is small. So, if the block is sufficiently small, find - * the optimal static block solution and remember its cost. - */ - if (block_length <= c->p.n.max_len_to_optimize_static_block) { - /* Save c->p.n.costs temporarily. */ - c->p.n.costs_saved = c->p.n.costs; - - deflate_set_costs_from_codes(c, &c->static_codes.lens); - deflate_find_min_cost_path(c, block_length, cache_ptr); - static_cost = c->p.n.optimum_nodes[0].cost_to_end / BIT_COST; - static_cost += 7; /* for the end-of-block symbol */ - - /* Restore c->p.n.costs. */ - c->p.n.costs = c->p.n.costs_saved; - } - - /* Initialize c->p.n.costs with default costs. */ - deflate_set_initial_costs(c, block_begin, block_length, is_first_block); - - do { - /* - * Find the minimum-cost path for this pass. - * Also set c->freqs and c->codes to match the path. - */ - deflate_find_min_cost_path(c, block_length, cache_ptr); - - /* - * Compute the exact cost of the block if the path were to be - * used. Note that this differs from - * c->p.n.optimum_nodes[0].cost_to_end in that true_cost uses - * the actual Huffman codes instead of c->p.n.costs. - */ - true_cost = deflate_compute_true_cost(c); - - /* - * If the cost didn't improve much from the previous pass, then - * doing more passes probably won't be helpful, so stop early. - */ - if (true_cost + c->p.n.min_improvement_to_continue > - best_true_cost) - break; - - best_true_cost = true_cost; - - /* Save the cost model that gave 'best_true_cost'. */ - c->p.n.costs_saved = c->p.n.costs; - - /* Update the cost model from the Huffman codes. */ - deflate_set_costs_from_codes(c, &c->codes.lens); - - } while (--num_passes_remaining); - - *used_only_literals = false; - if (MIN(only_lits_cost, static_cost) < best_true_cost) { - if (only_lits_cost < static_cost) { - /* Using only literals ended up being best! */ - deflate_choose_all_literals(c, block_begin, block_length); - deflate_set_costs_from_codes(c, &c->codes.lens); - seq_.litrunlen_and_length = block_length; - seq = &seq_; - *used_only_literals = true; - } else { - /* Static block ended up being best! */ - deflate_set_costs_from_codes(c, &c->static_codes.lens); - deflate_find_min_cost_path(c, block_length, cache_ptr); - } - } else if (true_cost >= - best_true_cost + c->p.n.min_bits_to_use_nonfinal_path) { - /* - * The best solution was actually from a non-final optimization - * pass, so recover and use the min-cost path from that pass. - */ - c->p.n.costs = c->p.n.costs_saved; - deflate_find_min_cost_path(c, block_length, cache_ptr); - deflate_set_costs_from_codes(c, &c->codes.lens); - } - deflate_flush_block(c, os, block_begin, block_length, seq, - is_final_block); -} - -static void -deflate_near_optimal_init_stats(struct libdeflate_compressor *c) -{ - init_block_split_stats(&c->split_stats); - memset(c->p.n.new_match_len_freqs, 0, - sizeof(c->p.n.new_match_len_freqs)); - memset(c->p.n.match_len_freqs, 0, sizeof(c->p.n.match_len_freqs)); -} - -static void -deflate_near_optimal_merge_stats(struct libdeflate_compressor *c) -{ - unsigned i; - - merge_new_observations(&c->split_stats); - for (i = 0; i < ARRAY_LEN(c->p.n.match_len_freqs); i++) { - c->p.n.match_len_freqs[i] += c->p.n.new_match_len_freqs[i]; - c->p.n.new_match_len_freqs[i] = 0; - } -} - -/* - * Save some literal/match statistics from the previous block so that - * deflate_adjust_costs() will be able to decide how much the current block - * differs from the previous one. - */ -static void -deflate_near_optimal_save_stats(struct libdeflate_compressor *c) -{ - int i; - - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) - c->p.n.prev_observations[i] = c->split_stats.observations[i]; - c->p.n.prev_num_observations = c->split_stats.num_observations; -} - -static void -deflate_near_optimal_clear_old_stats(struct libdeflate_compressor *c) -{ - int i; - - for (i = 0; i < NUM_OBSERVATION_TYPES; i++) - c->split_stats.observations[i] = 0; - c->split_stats.num_observations = 0; - memset(c->p.n.match_len_freqs, 0, sizeof(c->p.n.match_len_freqs)); -} - -/* - * This is the "near-optimal" DEFLATE compressor. It computes the optimal - * representation of each DEFLATE block using a minimum-cost path search over - * the graph of possible match/literal choices for that block, assuming a - * certain cost for each Huffman symbol. - * - * For several reasons, the end result is not guaranteed to be optimal: - * - * - Nonoptimal choice of blocks - * - Heuristic limitations on which matches are actually considered - * - Symbol costs are unknown until the symbols have already been chosen - * (so iterative optimization must be used) - */ -static void -deflate_compress_near_optimal(struct libdeflate_compressor * restrict c, - const u8 *in, size_t in_nbytes, - struct deflate_output_bitstream *os) -{ - const u8 *in_next = in; - const u8 *in_block_begin = in_next; - const u8 *in_end = in_next + in_nbytes; - const u8 *in_cur_base = in_next; - const u8 *in_next_slide = - in_next + MIN(in_end - in_next, MATCHFINDER_WINDOW_SIZE); - u32 max_len = DEFLATE_MAX_MATCH_LEN; - u32 nice_len = MIN(c->nice_match_length, max_len); - struct lz_match *cache_ptr = c->p.n.match_cache; - u32 next_hashes[2] = {0, 0}; - bool prev_block_used_only_literals = false; - - bt_matchfinder_init(&c->p.n.bt_mf); - deflate_near_optimal_init_stats(c); - - do { - /* Starting a new DEFLATE block */ - const u8 * const in_max_block_end = choose_max_block_end( - in_block_begin, in_end, SOFT_MAX_BLOCK_LENGTH); - const u8 *prev_end_block_check = NULL; - bool change_detected = false; - const u8 *next_observation = in_next; - u32 min_len; - - /* - * Use the minimum match length heuristic to improve the - * literal/match statistics gathered during matchfinding. - * However, the actual near-optimal parse won't respect min_len, - * as it can accurately assess the costs of different matches. - * - * If the "use only literals" strategy happened to be the best - * strategy on the previous block, then probably the - * min_match_len heuristic is still not aggressive enough for - * the data, so force gathering literal stats only. - */ - if (prev_block_used_only_literals) - min_len = DEFLATE_MAX_MATCH_LEN + 1; - else - min_len = calculate_min_match_len( - in_block_begin, - in_max_block_end - in_block_begin, - c->max_search_depth); - - /* - * Find matches until we decide to end the block. We end the - * block if any of the following is true: - * - * (1) Maximum block length has been reached - * (2) Match catch may overflow. - * (3) Block split heuristic says to split now. - */ - for (;;) { - struct lz_match *matches; - u32 best_len; - size_t remaining = in_end - in_next; - - /* Slide the window forward if needed. */ - if (in_next == in_next_slide) { - bt_matchfinder_slide_window(&c->p.n.bt_mf); - in_cur_base = in_next; - in_next_slide = in_next + - MIN(remaining, MATCHFINDER_WINDOW_SIZE); - } - - /* - * Find matches with the current position using the - * binary tree matchfinder and save them in match_cache. - * - * Note: the binary tree matchfinder is more suited for - * optimal parsing than the hash chain matchfinder. The - * reasons for this include: - * - * - The binary tree matchfinder can find more matches - * in the same number of steps. - * - One of the major advantages of hash chains is that - * skipping positions (not searching for matches at - * them) is faster; however, with optimal parsing we - * search for matches at almost all positions, so this - * advantage of hash chains is negated. - */ - matches = cache_ptr; - best_len = 0; - adjust_max_and_nice_len(&max_len, &nice_len, remaining); - if (likely(max_len >= BT_MATCHFINDER_REQUIRED_NBYTES)) { - cache_ptr = bt_matchfinder_get_matches( - &c->p.n.bt_mf, - in_cur_base, - in_next - in_cur_base, - max_len, - nice_len, - c->max_search_depth, - next_hashes, - matches); - if (cache_ptr > matches) - best_len = cache_ptr[-1].length; - } - if (in_next >= next_observation) { - if (best_len >= min_len) { - observe_match(&c->split_stats, - best_len); - next_observation = in_next + best_len; - c->p.n.new_match_len_freqs[best_len]++; - } else { - observe_literal(&c->split_stats, - *in_next); - next_observation = in_next + 1; - } - } - - cache_ptr->length = cache_ptr - matches; - cache_ptr->offset = *in_next; - in_next++; - cache_ptr++; - - /* - * If there was a very long match found, don't cache any - * matches for the bytes covered by that match. This - * avoids degenerate behavior when compressing highly - * redundant data, where the number of matches can be - * very large. - * - * This heuristic doesn't actually hurt the compression - * ratio very much. If there's a long match, then the - * data must be highly compressible, so it doesn't - * matter much what we do. - */ - if (best_len >= DEFLATE_MIN_MATCH_LEN && - best_len >= nice_len) { - --best_len; - do { - remaining = in_end - in_next; - if (in_next == in_next_slide) { - bt_matchfinder_slide_window( - &c->p.n.bt_mf); - in_cur_base = in_next; - in_next_slide = in_next + - MIN(remaining, - MATCHFINDER_WINDOW_SIZE); - } - adjust_max_and_nice_len(&max_len, - &nice_len, - remaining); - if (max_len >= - BT_MATCHFINDER_REQUIRED_NBYTES) { - bt_matchfinder_skip_byte( - &c->p.n.bt_mf, - in_cur_base, - in_next - in_cur_base, - nice_len, - c->max_search_depth, - next_hashes); - } - cache_ptr->length = 0; - cache_ptr->offset = *in_next; - in_next++; - cache_ptr++; - } while (--best_len); - } - /* Maximum block length or end of input reached? */ - if (in_next >= in_max_block_end) - break; - /* Match cache overflowed? */ - if (cache_ptr >= - &c->p.n.match_cache[MATCH_CACHE_LENGTH]) - break; - /* Not ready to try to end the block (again)? */ - if (!ready_to_check_block(&c->split_stats, - in_block_begin, in_next, - in_end)) - continue; - /* Check if it would be worthwhile to end the block. */ - if (do_end_block_check(&c->split_stats, - in_next - in_block_begin)) { - change_detected = true; - break; - } - /* Ending the block doesn't seem worthwhile here. */ - deflate_near_optimal_merge_stats(c); - prev_end_block_check = in_next; - } - /* - * All the matches for this block have been cached. Now choose - * the precise end of the block and the sequence of items to - * output to represent it, then flush the block. - */ - if (change_detected && prev_end_block_check != NULL) { - /* - * The block is being ended because a recent chunk of - * data differs from the rest of the block. We could - * end the block at 'in_next' like the greedy and lazy - * compressors do, but that's not ideal since it would - * include the differing chunk in the block. The - * near-optimal compressor has time to do a better job. - * Therefore, we rewind to just before the chunk, and - * output a block that only goes up to there. - * - * We then set things up to correctly start the next - * block, considering that some work has already been - * done on it (some matches found and stats gathered). - */ - struct lz_match *orig_cache_ptr = cache_ptr; - const u8 *in_block_end = prev_end_block_check; - u32 block_length = in_block_end - in_block_begin; - bool is_first = (in_block_begin == in); - bool is_final = false; - u32 num_bytes_to_rewind = in_next - in_block_end; - size_t cache_len_rewound; - - /* Rewind the match cache. */ - do { - cache_ptr--; - cache_ptr -= cache_ptr->length; - } while (--num_bytes_to_rewind); - cache_len_rewound = orig_cache_ptr - cache_ptr; - - deflate_optimize_and_flush_block( - c, os, in_block_begin, - block_length, cache_ptr, - is_first, is_final, - &prev_block_used_only_literals); - memmove(c->p.n.match_cache, cache_ptr, - cache_len_rewound * sizeof(*cache_ptr)); - cache_ptr = &c->p.n.match_cache[cache_len_rewound]; - deflate_near_optimal_save_stats(c); - /* - * Clear the stats for the just-flushed block, leaving - * just the stats for the beginning of the next block. - */ - deflate_near_optimal_clear_old_stats(c); - in_block_begin = in_block_end; - } else { - /* - * The block is being ended for a reason other than a - * differing data chunk being detected. Don't rewind at - * all; just end the block at the current position. - */ - u32 block_length = in_next - in_block_begin; - bool is_first = (in_block_begin == in); - bool is_final = (in_next == in_end); - - deflate_near_optimal_merge_stats(c); - deflate_optimize_and_flush_block( - c, os, in_block_begin, - block_length, cache_ptr, - is_first, is_final, - &prev_block_used_only_literals); - cache_ptr = &c->p.n.match_cache[0]; - deflate_near_optimal_save_stats(c); - deflate_near_optimal_init_stats(c); - in_block_begin = in_next; - } - } while (in_next != in_end && !os->overflow); -} - -/* Initialize c->p.n.offset_slot_full. */ -static void -deflate_init_offset_slot_full(struct libdeflate_compressor *c) -{ - u32 offset_slot; - u32 offset; - u32 offset_end; - - for (offset_slot = 0; offset_slot < ARRAY_LEN(deflate_offset_slot_base); - offset_slot++) { - offset = deflate_offset_slot_base[offset_slot]; - offset_end = offset + - (1 << deflate_extra_offset_bits[offset_slot]); - do { - c->p.n.offset_slot_full[offset] = offset_slot; - } while (++offset != offset_end); - } -} - -#endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - -LIBDEFLATEAPI struct libdeflate_compressor * -libdeflate_alloc_compressor_ex(int compression_level, - const struct libdeflate_options *options) -{ - struct libdeflate_compressor *c; - size_t size = offsetof(struct libdeflate_compressor, p); - - check_buildtime_parameters(); - - /* - * Note: if more fields are added to libdeflate_options, this code will - * need to be updated to support both the old and new structs. - */ - if (options->sizeof_options != sizeof(*options)) - return NULL; - - /* - * Note: For similarity with zlib's API, -1 is accepted as an alias for - * the default compression level. - */ - if (compression_level == -1) - compression_level = 6; - - if (compression_level < 0 || compression_level > 12) - return NULL; - -#if SUPPORT_NEAR_OPTIMAL_PARSING - if (compression_level >= 10) - size += sizeof(c->p.n); - else -#endif - { - if (compression_level >= 2) - size += sizeof(c->p.g); - else if (compression_level == 1) - size += sizeof(c->p.f); - } - - c = libdeflate_aligned_malloc(options->malloc_func ? - options->malloc_func : - libdeflate_default_malloc_func, - MATCHFINDER_MEM_ALIGNMENT, size); - if (!c) - return NULL; - c->free_func = options->free_func ? - options->free_func : libdeflate_default_free_func; - - c->compression_level = compression_level; - - /* - * The higher the compression level, the more we should bother trying to - * compress very small inputs. - */ - c->max_passthrough_size = 55 - (compression_level * 4); - - switch (compression_level) { - case 0: - c->max_passthrough_size = SIZE_MAX; - c->impl = NULL; /* not used */ - break; - case 1: - c->impl = deflate_compress_fastest; - /* max_search_depth is unused. */ - c->nice_match_length = 32; - break; - case 2: - c->impl = deflate_compress_greedy; - c->max_search_depth = 6; - c->nice_match_length = 10; - break; - case 3: - c->impl = deflate_compress_greedy; - c->max_search_depth = 12; - c->nice_match_length = 14; - break; - case 4: - c->impl = deflate_compress_greedy; - c->max_search_depth = 16; - c->nice_match_length = 30; - break; - case 5: - c->impl = deflate_compress_lazy; - c->max_search_depth = 16; - c->nice_match_length = 30; - break; - case 6: - c->impl = deflate_compress_lazy; - c->max_search_depth = 35; - c->nice_match_length = 65; - break; - case 7: - c->impl = deflate_compress_lazy; - c->max_search_depth = 100; - c->nice_match_length = 130; - break; - case 8: - c->impl = deflate_compress_lazy2; - c->max_search_depth = 300; - c->nice_match_length = DEFLATE_MAX_MATCH_LEN; - break; - case 9: -#if !SUPPORT_NEAR_OPTIMAL_PARSING - default: -#endif - c->impl = deflate_compress_lazy2; - c->max_search_depth = 600; - c->nice_match_length = DEFLATE_MAX_MATCH_LEN; - break; -#if SUPPORT_NEAR_OPTIMAL_PARSING - case 10: - c->impl = deflate_compress_near_optimal; - c->max_search_depth = 35; - c->nice_match_length = 75; - c->p.n.max_optim_passes = 2; - c->p.n.min_improvement_to_continue = 32; - c->p.n.min_bits_to_use_nonfinal_path = 32; - c->p.n.max_len_to_optimize_static_block = 0; - deflate_init_offset_slot_full(c); - break; - case 11: - c->impl = deflate_compress_near_optimal; - c->max_search_depth = 100; - c->nice_match_length = 150; - c->p.n.max_optim_passes = 4; - c->p.n.min_improvement_to_continue = 16; - c->p.n.min_bits_to_use_nonfinal_path = 16; - c->p.n.max_len_to_optimize_static_block = 1000; - deflate_init_offset_slot_full(c); - break; - case 12: - default: - c->impl = deflate_compress_near_optimal; - c->max_search_depth = 300; - c->nice_match_length = DEFLATE_MAX_MATCH_LEN; - c->p.n.max_optim_passes = 10; - c->p.n.min_improvement_to_continue = 1; - c->p.n.min_bits_to_use_nonfinal_path = 1; - c->p.n.max_len_to_optimize_static_block = 10000; - deflate_init_offset_slot_full(c); - break; -#endif /* SUPPORT_NEAR_OPTIMAL_PARSING */ - } - - deflate_init_static_codes(c); - - return c; -} - - -LIBDEFLATEAPI struct libdeflate_compressor * -libdeflate_alloc_compressor(int compression_level) -{ - static const struct libdeflate_options defaults = { - .sizeof_options = sizeof(defaults), - }; - return libdeflate_alloc_compressor_ex(compression_level, &defaults); -} - -LIBDEFLATEAPI size_t -libdeflate_deflate_compress(struct libdeflate_compressor *c, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail) -{ - struct deflate_output_bitstream os; - - /* - * For extremely short inputs, or for compression level 0, just output - * uncompressed blocks. - */ - if (unlikely(in_nbytes <= c->max_passthrough_size)) - return deflate_compress_none(in, in_nbytes, - out, out_nbytes_avail); - - /* Initialize the output bitstream structure. */ - os.bitbuf = 0; - os.bitcount = 0; - os.next = out; - os.end = os.next + out_nbytes_avail; - os.overflow = false; - - /* Call the actual compression function. */ - (*c->impl)(c, in, in_nbytes, &os); - - /* Return 0 if the output buffer is too small. */ - if (os.overflow) - return 0; - - /* - * Write the final byte if needed. This can't overflow the output - * buffer because deflate_flush_block() would have set the overflow flag - * if there wasn't enough space remaining for the full final block. - */ - ASSERT(os.bitcount <= 7); - if (os.bitcount) { - ASSERT(os.next < os.end); - *os.next++ = os.bitbuf; - } - - /* Return the compressed size in bytes. */ - return os.next - (u8 *)out; -} - -LIBDEFLATEAPI void -libdeflate_free_compressor(struct libdeflate_compressor *c) -{ - if (c) - libdeflate_aligned_free(c->free_func, c); -} - -unsigned int -libdeflate_get_compression_level(struct libdeflate_compressor *c) -{ - return c->compression_level; -} - -LIBDEFLATEAPI size_t -libdeflate_deflate_compress_bound(struct libdeflate_compressor *c, - size_t in_nbytes) -{ - size_t max_blocks; - - /* - * Since the compressor never uses a compressed block when an - * uncompressed block is cheaper, the worst case can be no worse than - * the case where only uncompressed blocks are used. - * - * This is true even though up to 7 bits are "wasted" to byte-align the - * bitstream when a compressed block is followed by an uncompressed - * block. This is because a compressed block wouldn't have been used if - * it wasn't cheaper than an uncompressed block, and uncompressed blocks - * always end on a byte boundary. So the alignment bits will, at worst, - * go up to the place where the uncompressed block would have ended. - */ - - /* - * Calculate the maximum number of uncompressed blocks that the - * compressor can use for 'in_nbytes' of data. - * - * The minimum length that is passed to deflate_flush_block() is - * MIN_BLOCK_LENGTH bytes, except for the final block if needed. If - * deflate_flush_block() decides to use an uncompressed block, it - * actually will (in general) output a series of uncompressed blocks in - * order to stay within the UINT16_MAX limit of DEFLATE. But this can - * be disregarded here as long as '2 * MIN_BLOCK_LENGTH <= UINT16_MAX', - * as in that case this behavior can't result in more blocks than the - * case where deflate_flush_block() is called with min-length inputs. - * - * So the number of uncompressed blocks needed would be bounded by - * DIV_ROUND_UP(in_nbytes, MIN_BLOCK_LENGTH). However, empty inputs - * need 1 (empty) block, which gives the final expression below. - */ - STATIC_ASSERT(2 * MIN_BLOCK_LENGTH <= UINT16_MAX); - max_blocks = MAX(DIV_ROUND_UP(in_nbytes, MIN_BLOCK_LENGTH), 1); - - /* - * Each uncompressed block has 5 bytes of overhead, for the BFINAL, - * BTYPE, LEN, and NLEN fields. (For the reason explained earlier, the - * alignment bits at the very start of the block can be disregarded; - * they would otherwise increase the overhead to 6 bytes per block.) - * Therefore, the maximum number of overhead bytes is '5 * max_blocks'. - * To get the final bound, add the number of uncompressed bytes. - */ - return (5 * max_blocks) + in_nbytes; -} diff --git a/dm/third_party/libdeflate/lib/deflate_compress.h b/dm/third_party/libdeflate/lib/deflate_compress.h deleted file mode 100644 index 9451d54..0000000 --- a/dm/third_party/libdeflate/lib/deflate_compress.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef LIB_DEFLATE_COMPRESS_H -#define LIB_DEFLATE_COMPRESS_H - -#include "lib_common.h" - -/* - * DEFLATE compression is private to deflate_compress.c, but we do need to be - * able to query the compression level for zlib and gzip header generation. - */ - -struct libdeflate_compressor; - -unsigned int libdeflate_get_compression_level(struct libdeflate_compressor *c); - -#endif /* LIB_DEFLATE_COMPRESS_H */ diff --git a/dm/third_party/libdeflate/lib/deflate_constants.h b/dm/third_party/libdeflate/lib/deflate_constants.h deleted file mode 100644 index 95c9e0a..0000000 --- a/dm/third_party/libdeflate/lib/deflate_constants.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * deflate_constants.h - constants for the DEFLATE compression format - */ - -#ifndef LIB_DEFLATE_CONSTANTS_H -#define LIB_DEFLATE_CONSTANTS_H - -/* Valid block types */ -#define DEFLATE_BLOCKTYPE_UNCOMPRESSED 0 -#define DEFLATE_BLOCKTYPE_STATIC_HUFFMAN 1 -#define DEFLATE_BLOCKTYPE_DYNAMIC_HUFFMAN 2 - -/* Minimum and maximum supported match lengths (in bytes) */ -#define DEFLATE_MIN_MATCH_LEN 3 -#define DEFLATE_MAX_MATCH_LEN 258 - -/* Maximum supported match offset (in bytes) */ -#define DEFLATE_MAX_MATCH_OFFSET 32768 - -/* log2 of DEFLATE_MAX_MATCH_OFFSET */ -#define DEFLATE_WINDOW_ORDER 15 - -/* Number of symbols in each Huffman code. Note: for the literal/length - * and offset codes, these are actually the maximum values; a given block - * might use fewer symbols. */ -#define DEFLATE_NUM_PRECODE_SYMS 19 -#define DEFLATE_NUM_LITLEN_SYMS 288 -#define DEFLATE_NUM_OFFSET_SYMS 32 - -/* The maximum number of symbols across all codes */ -#define DEFLATE_MAX_NUM_SYMS 288 - -/* Division of symbols in the literal/length code */ -#define DEFLATE_NUM_LITERALS 256 -#define DEFLATE_END_OF_BLOCK 256 -#define DEFLATE_FIRST_LEN_SYM 257 - -/* Maximum codeword length, in bits, within each Huffman code */ -#define DEFLATE_MAX_PRE_CODEWORD_LEN 7 -#define DEFLATE_MAX_LITLEN_CODEWORD_LEN 15 -#define DEFLATE_MAX_OFFSET_CODEWORD_LEN 15 - -/* The maximum codeword length across all codes */ -#define DEFLATE_MAX_CODEWORD_LEN 15 - -/* Maximum possible overrun when decoding codeword lengths */ -#define DEFLATE_MAX_LENS_OVERRUN 137 - -/* - * Maximum number of extra bits that may be required to represent a match - * length or offset. - */ -#define DEFLATE_MAX_EXTRA_LENGTH_BITS 5 -#define DEFLATE_MAX_EXTRA_OFFSET_BITS 13 - -#endif /* LIB_DEFLATE_CONSTANTS_H */ diff --git a/dm/third_party/libdeflate/lib/deflate_decompress.c b/dm/third_party/libdeflate/lib/deflate_decompress.c deleted file mode 100644 index d5af5eb..0000000 --- a/dm/third_party/libdeflate/lib/deflate_decompress.c +++ /dev/null @@ -1,1208 +0,0 @@ -/* - * deflate_decompress.c - a decompressor for DEFLATE - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * --------------------------------------------------------------------------- - * - * This is a highly optimized DEFLATE decompressor. It is much faster than - * vanilla zlib, typically well over twice as fast, though results vary by CPU. - * - * Why this is faster than vanilla zlib: - * - * - Word accesses rather than byte accesses when reading input - * - Word accesses rather than byte accesses when copying matches - * - Faster Huffman decoding combined with various DEFLATE-specific tricks - * - Larger bitbuffer variable that doesn't need to be refilled as often - * - Other optimizations to remove unnecessary branches - * - Only full-buffer decompression is supported, so the code doesn't need to - * support stopping and resuming decompression. - * - On x86_64, a version of the decompression routine is compiled with BMI2 - * instructions enabled and is used automatically at runtime when supported. - */ - -#include "lib_common.h" -#include "deflate_constants.h" - -/* - * If the expression passed to SAFETY_CHECK() evaluates to false, then the - * decompression routine immediately returns LIBDEFLATE_BAD_DATA, indicating the - * compressed data is invalid. - * - * Theoretically, these checks could be disabled for specialized applications - * where all input to the decompressor will be trusted. - */ -#if 0 -# pragma message("UNSAFE DECOMPRESSION IS ENABLED. THIS MUST ONLY BE USED IF THE DECOMPRESSOR INPUT WILL ALWAYS BE TRUSTED!") -# define SAFETY_CHECK(expr) (void)(expr) -#else -# define SAFETY_CHECK(expr) if (unlikely(!(expr))) return LIBDEFLATE_BAD_DATA -#endif - -/***************************************************************************** - * Input bitstream * - *****************************************************************************/ - -/* - * The state of the "input bitstream" consists of the following variables: - * - * - in_next: a pointer to the next unread byte in the input buffer - * - * - in_end: a pointer to just past the end of the input buffer - * - * - bitbuf: a word-sized variable containing bits that have been read from - * the input buffer or from the implicit appended zero bytes - * - * - bitsleft: the number of bits in 'bitbuf' available to be consumed. - * After REFILL_BITS_BRANCHLESS(), 'bitbuf' can actually - * contain more bits than this. However, only the bits counted - * by 'bitsleft' can actually be consumed; the rest can only be - * used for preloading. - * - * As a micro-optimization, we allow bits 8 and higher of - * 'bitsleft' to contain garbage. When consuming the bits - * associated with a decode table entry, this allows us to do - * 'bitsleft -= entry' instead of 'bitsleft -= (u8)entry'. - * On some CPUs, this helps reduce instruction dependencies. - * This does have the disadvantage that 'bitsleft' sometimes - * needs to be cast to 'u8', such as when it's used as a shift - * amount in REFILL_BITS_BRANCHLESS(). But that one happens - * for free since most CPUs ignore high bits in shift amounts. - * - * - overread_count: the total number of implicit appended zero bytes that - * have been loaded into the bitbuffer, including any - * counted by 'bitsleft' and any already consumed - */ - -/* - * The type for the bitbuffer variable ('bitbuf' described above). For best - * performance, this should have size equal to a machine word. - * - * 64-bit platforms have a significant advantage: they get a bigger bitbuffer - * which they don't have to refill as often. - */ -typedef machine_word_t bitbuf_t; -#define BITBUF_NBITS (8 * (int)sizeof(bitbuf_t)) - -/* BITMASK(n) returns a bitmask of length 'n'. */ -#define BITMASK(n) (((bitbuf_t)1 << (n)) - 1) - -/* - * MAX_BITSLEFT is the maximum number of consumable bits, i.e. the maximum value - * of '(u8)bitsleft'. This is the size of the bitbuffer variable, minus 1 if - * the branchless refill method is being used (see REFILL_BITS_BRANCHLESS()). - */ -#define MAX_BITSLEFT \ - (UNALIGNED_ACCESS_IS_FAST ? BITBUF_NBITS - 1 : BITBUF_NBITS) - -/* - * CONSUMABLE_NBITS is the minimum number of bits that are guaranteed to be - * consumable (counted in 'bitsleft') immediately after refilling the bitbuffer. - * Since only whole bytes can be added to 'bitsleft', the worst case is - * 'MAX_BITSLEFT - 7': the smallest amount where another byte doesn't fit. - */ -#define CONSUMABLE_NBITS (MAX_BITSLEFT - 7) - -/* - * FASTLOOP_PRELOADABLE_NBITS is the minimum number of bits that are guaranteed - * to be preloadable immediately after REFILL_BITS_IN_FASTLOOP(). (It is *not* - * guaranteed after REFILL_BITS(), since REFILL_BITS() falls back to a - * byte-at-a-time refill method near the end of input.) This may exceed the - * number of consumable bits (counted by 'bitsleft'). Any bits not counted in - * 'bitsleft' can only be used for precomputation and cannot be consumed. - */ -#define FASTLOOP_PRELOADABLE_NBITS \ - (UNALIGNED_ACCESS_IS_FAST ? BITBUF_NBITS : CONSUMABLE_NBITS) - -/* - * PRELOAD_SLACK is the minimum number of bits that are guaranteed to be - * preloadable but not consumable, following REFILL_BITS_IN_FASTLOOP() and any - * subsequent consumptions. This is 1 bit if the branchless refill method is - * being used, and 0 bits otherwise. - */ -#define PRELOAD_SLACK MAX(0, FASTLOOP_PRELOADABLE_NBITS - MAX_BITSLEFT) - -/* - * CAN_CONSUME(n) is true if it's guaranteed that if the bitbuffer has just been - * refilled, then it's always possible to consume 'n' bits from it. 'n' should - * be a compile-time constant, to enable compile-time evaluation. - */ -#define CAN_CONSUME(n) (CONSUMABLE_NBITS >= (n)) - -/* - * CAN_CONSUME_AND_THEN_PRELOAD(consume_nbits, preload_nbits) is true if it's - * guaranteed that after REFILL_BITS_IN_FASTLOOP(), it's always possible to - * consume 'consume_nbits' bits, then preload 'preload_nbits' bits. The - * arguments should be compile-time constants to enable compile-time evaluation. - */ -#define CAN_CONSUME_AND_THEN_PRELOAD(consume_nbits, preload_nbits) \ - (CONSUMABLE_NBITS >= (consume_nbits) && \ - FASTLOOP_PRELOADABLE_NBITS >= (consume_nbits) + (preload_nbits)) - -/* - * REFILL_BITS_BRANCHLESS() branchlessly refills the bitbuffer variable by - * reading the next word from the input buffer and updating 'in_next' and - * 'bitsleft' based on how many bits were refilled -- counting whole bytes only. - * This is much faster than reading a byte at a time, at least if the CPU is - * little endian and supports fast unaligned memory accesses. - * - * The simplest way of branchlessly updating 'bitsleft' would be: - * - * bitsleft += (MAX_BITSLEFT - bitsleft) & ~7; - * - * To make it faster, we define MAX_BITSLEFT to be 'WORDBITS - 1' rather than - * WORDBITS, so that in binary it looks like 111111 or 11111. Then, we update - * 'bitsleft' by just setting the bits above the low 3 bits: - * - * bitsleft |= MAX_BITSLEFT & ~7; - * - * That compiles down to a single instruction like 'or $0x38, %rbp'. Using - * 'MAX_BITSLEFT == WORDBITS - 1' also has the advantage that refills can be - * done when 'bitsleft == MAX_BITSLEFT' without invoking undefined behavior. - * - * The simplest way of branchlessly updating 'in_next' would be: - * - * in_next += (MAX_BITSLEFT - bitsleft) >> 3; - * - * With 'MAX_BITSLEFT == WORDBITS - 1' we could use an XOR instead, though this - * isn't really better: - * - * in_next += (MAX_BITSLEFT ^ bitsleft) >> 3; - * - * An alternative which can be marginally better is the following: - * - * in_next += sizeof(bitbuf_t) - 1; - * in_next -= (bitsleft >> 3) & 0x7; - * - * It seems this would increase the number of CPU instructions from 3 (sub, shr, - * add) to 4 (add, shr, and, sub). However, if the CPU has a bitfield - * extraction instruction (e.g. arm's ubfx), it stays at 3, and is potentially - * more efficient because the length of the longest dependency chain decreases - * from 3 to 2. This alternative also has the advantage that it ignores the - * high bits in 'bitsleft', so it is compatible with the micro-optimization we - * use where we let the high bits of 'bitsleft' contain garbage. - */ -#define REFILL_BITS_BRANCHLESS() \ -do { \ - bitbuf |= get_unaligned_leword(in_next) << (u8)bitsleft; \ - in_next += sizeof(bitbuf_t) - 1; \ - in_next -= (bitsleft >> 3) & 0x7; \ - bitsleft |= MAX_BITSLEFT & ~7; \ -} while (0) - -/* - * REFILL_BITS() loads bits from the input buffer until the bitbuffer variable - * contains at least CONSUMABLE_NBITS consumable bits. - * - * This checks for the end of input, and it doesn't guarantee - * FASTLOOP_PRELOADABLE_NBITS, so it can't be used in the fastloop. - * - * If we would overread the input buffer, we just don't read anything, leaving - * the bits zeroed but marking them filled. This simplifies the decompressor - * because it removes the need to always be able to distinguish between real - * overreads and overreads caused only by the decompressor's own lookahead. - * - * We do still keep track of the number of bytes that have been overread, for - * two reasons. First, it allows us to determine the exact number of bytes that - * were consumed once the stream ends or an uncompressed block is reached. - * Second, it allows us to stop early if the overread amount gets so large (more - * than sizeof bitbuf) that it can only be caused by a real overread. (The - * second part is arguably unneeded, since libdeflate is buffer-based; given - * infinite zeroes, it will eventually either completely fill the output buffer - * or return an error. However, we do it to be slightly more friendly to the - * not-recommended use case of decompressing with an unknown output size.) - */ -#define REFILL_BITS() \ -do { \ - if (UNALIGNED_ACCESS_IS_FAST && \ - likely(in_end - in_next >= sizeof(bitbuf_t))) { \ - REFILL_BITS_BRANCHLESS(); \ - } else { \ - while ((u8)bitsleft < CONSUMABLE_NBITS) { \ - if (likely(in_next != in_end)) { \ - bitbuf |= (bitbuf_t)*in_next++ << \ - (u8)bitsleft; \ - } else { \ - overread_count++; \ - SAFETY_CHECK(overread_count <= \ - sizeof(bitbuf_t)); \ - } \ - bitsleft += 8; \ - } \ - } \ -} while (0) - -/* - * REFILL_BITS_IN_FASTLOOP() is like REFILL_BITS(), but it doesn't check for the - * end of the input. It can only be used in the fastloop. - */ -#define REFILL_BITS_IN_FASTLOOP() \ -do { \ - STATIC_ASSERT(UNALIGNED_ACCESS_IS_FAST || \ - FASTLOOP_PRELOADABLE_NBITS == CONSUMABLE_NBITS); \ - if (UNALIGNED_ACCESS_IS_FAST) { \ - REFILL_BITS_BRANCHLESS(); \ - } else { \ - while ((u8)bitsleft < CONSUMABLE_NBITS) { \ - bitbuf |= (bitbuf_t)*in_next++ << (u8)bitsleft; \ - bitsleft += 8; \ - } \ - } \ -} while (0) - -/* - * This is the worst-case maximum number of output bytes that are written to - * during each iteration of the fastloop. The worst case is 2 literals, then a - * match of length DEFLATE_MAX_MATCH_LEN. Additionally, some slack space must - * be included for the intentional overrun in the match copy implementation. - */ -#define FASTLOOP_MAX_BYTES_WRITTEN \ - (2 + DEFLATE_MAX_MATCH_LEN + (5 * WORDBYTES) - 1) - -/* - * This is the worst-case maximum number of input bytes that are read during - * each iteration of the fastloop. To get this value, we first compute the - * greatest number of bits that can be refilled during a loop iteration. The - * refill at the beginning can add at most MAX_BITSLEFT, and the amount that can - * be refilled later is no more than the maximum amount that can be consumed by - * 2 literals that don't need a subtable, then a match. We convert this value - * to bytes, rounding up; this gives the maximum number of bytes that 'in_next' - * can be advanced. Finally, we add sizeof(bitbuf_t) to account for - * REFILL_BITS_BRANCHLESS() reading a word past 'in_next'. - */ -#define FASTLOOP_MAX_BYTES_READ \ - (DIV_ROUND_UP(MAX_BITSLEFT + (2 * LITLEN_TABLEBITS) + \ - LENGTH_MAXBITS + OFFSET_MAXBITS, 8) + \ - sizeof(bitbuf_t)) - -/***************************************************************************** - * Huffman decoding * - *****************************************************************************/ - -/* - * The fastest way to decode Huffman-encoded data is basically to use a decode - * table that maps the next TABLEBITS bits of data to their symbol. Each entry - * decode_table[i] maps to the symbol whose codeword is a prefix of 'i'. A - * symbol with codeword length 'n' has '2**(TABLEBITS-n)' entries in the table. - * - * Ideally, TABLEBITS and the maximum codeword length would be the same; some - * compression formats are designed with this goal in mind. Unfortunately, in - * DEFLATE, the maximum litlen and offset codeword lengths are 15 bits, which is - * too large for a practical TABLEBITS. It's not *that* much larger, though, so - * the workaround is to use a single level of subtables. In the main table, - * entries for prefixes of codewords longer than TABLEBITS contain a "pointer" - * to the appropriate subtable along with the number of bits it is indexed with. - * - * The most efficient way to allocate subtables is to allocate them dynamically - * after the main table. The worst-case number of table entries needed, - * including subtables, is precomputable; see the ENOUGH constants below. - * - * A useful optimization is to store the codeword lengths in the decode table so - * that they don't have to be looked up by indexing a separate table that maps - * symbols to their codeword lengths. We basically do this; however, for the - * litlen and offset codes we also implement some DEFLATE-specific optimizations - * that build in the consideration of the "extra bits" and the - * literal/length/end-of-block division. For the exact decode table entry - * format we use, see the definitions of the *_decode_results[] arrays below. - */ - - -/* - * These are the TABLEBITS values we use for each of the DEFLATE Huffman codes, - * along with their corresponding ENOUGH values. - * - * For the precode, we use PRECODE_TABLEBITS == 7 since this is the maximum - * precode codeword length. This avoids ever needing subtables. - * - * For the litlen and offset codes, we cannot realistically avoid ever needing - * subtables, since litlen and offset codewords can be up to 15 bits. A higher - * TABLEBITS reduces the number of lookups that need a subtable, which increases - * performance; however, it increases memory usage and makes building the table - * take longer, which decreases performance. We choose values that work well in - * practice, making subtables rarely needed without making the tables too large. - * - * Our choice of OFFSET_TABLEBITS == 8 is a bit low; without any special - * considerations, 9 would fit the trade-off curve better. However, there is a - * performance benefit to using exactly 8 bits when it is a compile-time - * constant, as many CPUs can take the low byte more easily than the low 9 bits. - * - * zlib treats its equivalents of TABLEBITS as maximum values; whenever it - * builds a table, it caps the actual table_bits to the longest codeword. This - * makes sense in theory, as there's no need for the table to be any larger than - * needed to support the longest codeword. However, having the table bits be a - * compile-time constant is beneficial to the performance of the decode loop, so - * there is a trade-off. libdeflate currently uses the dynamic table_bits - * strategy for the litlen table only, due to its larger maximum size. - * PRECODE_TABLEBITS and OFFSET_TABLEBITS are smaller, so going dynamic there - * isn't as useful, and OFFSET_TABLEBITS=8 is useful as mentioned above. - * - * Each TABLEBITS value has a corresponding ENOUGH value that gives the - * worst-case maximum number of decode table entries, including the main table - * and all subtables. The ENOUGH value depends on three parameters: - * - * (1) the maximum number of symbols in the code (DEFLATE_NUM_*_SYMS) - * (2) the maximum number of main table bits (*_TABLEBITS) - * (3) the maximum allowed codeword length (DEFLATE_MAX_*_CODEWORD_LEN) - * - * The ENOUGH values were computed using the utility program 'enough' from zlib. - */ -#define PRECODE_TABLEBITS 7 -#define PRECODE_ENOUGH 128 /* enough 19 7 7 */ -#define LITLEN_TABLEBITS 11 -#define LITLEN_ENOUGH 2342 /* enough 288 11 15 */ -#define OFFSET_TABLEBITS 8 -#define OFFSET_ENOUGH 402 /* enough 32 8 15 */ - -/* - * make_decode_table_entry() creates a decode table entry for the given symbol - * by combining the static part 'decode_results[sym]' with the dynamic part - * 'len', which is the remaining codeword length (the codeword length for main - * table entries, or the codeword length minus TABLEBITS for subtable entries). - * - * In all cases, we add 'len' to each of the two low-order bytes to create the - * appropriately-formatted decode table entry. See the definitions of the - * *_decode_results[] arrays below, where the entry format is described. - */ -static forceinline u32 -make_decode_table_entry(const u32 decode_results[], u32 sym, u32 len) -{ - return decode_results[sym] + (len << 8) + len; -} - -/* - * Here is the format of our precode decode table entries. Bits not explicitly - * described contain zeroes: - * - * Bit 20-16: presym - * Bit 10-8: codeword length [not used] - * Bit 2-0: codeword length - * - * The precode decode table never has subtables, since we use - * PRECODE_TABLEBITS == DEFLATE_MAX_PRE_CODEWORD_LEN. - * - * precode_decode_results[] contains the static part of the entry for each - * symbol. make_decode_table_entry() produces the final entries. - */ -static const u32 precode_decode_results[] = { -#define ENTRY(presym) ((u32)presym << 16) - ENTRY(0) , ENTRY(1) , ENTRY(2) , ENTRY(3) , - ENTRY(4) , ENTRY(5) , ENTRY(6) , ENTRY(7) , - ENTRY(8) , ENTRY(9) , ENTRY(10) , ENTRY(11) , - ENTRY(12) , ENTRY(13) , ENTRY(14) , ENTRY(15) , - ENTRY(16) , ENTRY(17) , ENTRY(18) , -#undef ENTRY -}; - -/* Litlen and offset decode table entry flags */ - -/* Indicates a literal entry in the litlen decode table */ -#define HUFFDEC_LITERAL 0x80000000 - -/* Indicates that HUFFDEC_SUBTABLE_POINTER or HUFFDEC_END_OF_BLOCK is set */ -#define HUFFDEC_EXCEPTIONAL 0x00008000 - -/* Indicates a subtable pointer entry in the litlen or offset decode table */ -#define HUFFDEC_SUBTABLE_POINTER 0x00004000 - -/* Indicates an end-of-block entry in the litlen decode table */ -#define HUFFDEC_END_OF_BLOCK 0x00002000 - -/* Maximum number of bits that can be consumed by decoding a match length */ -#define LENGTH_MAXBITS (DEFLATE_MAX_LITLEN_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_LENGTH_BITS) -#define LENGTH_MAXFASTBITS (LITLEN_TABLEBITS /* no subtable needed */ + \ - DEFLATE_MAX_EXTRA_LENGTH_BITS) - -/* - * Here is the format of our litlen decode table entries. Bits not explicitly - * described contain zeroes: - * - * Literals: - * Bit 31: 1 (HUFFDEC_LITERAL) - * Bit 23-16: literal value - * Bit 15: 0 (!HUFFDEC_EXCEPTIONAL) - * Bit 14: 0 (!HUFFDEC_SUBTABLE_POINTER) - * Bit 13: 0 (!HUFFDEC_END_OF_BLOCK) - * Bit 11-8: remaining codeword length [not used] - * Bit 3-0: remaining codeword length - * Lengths: - * Bit 31: 0 (!HUFFDEC_LITERAL) - * Bit 24-16: length base value - * Bit 15: 0 (!HUFFDEC_EXCEPTIONAL) - * Bit 14: 0 (!HUFFDEC_SUBTABLE_POINTER) - * Bit 13: 0 (!HUFFDEC_END_OF_BLOCK) - * Bit 11-8: remaining codeword length - * Bit 4-0: remaining codeword length + number of extra bits - * End of block: - * Bit 31: 0 (!HUFFDEC_LITERAL) - * Bit 15: 1 (HUFFDEC_EXCEPTIONAL) - * Bit 14: 0 (!HUFFDEC_SUBTABLE_POINTER) - * Bit 13: 1 (HUFFDEC_END_OF_BLOCK) - * Bit 11-8: remaining codeword length [not used] - * Bit 3-0: remaining codeword length - * Subtable pointer: - * Bit 31: 0 (!HUFFDEC_LITERAL) - * Bit 30-16: index of start of subtable - * Bit 15: 1 (HUFFDEC_EXCEPTIONAL) - * Bit 14: 1 (HUFFDEC_SUBTABLE_POINTER) - * Bit 13: 0 (!HUFFDEC_END_OF_BLOCK) - * Bit 11-8: number of subtable bits - * Bit 3-0: number of main table bits - * - * This format has several desirable properties: - * - * - The codeword length, length slot base, and number of extra length bits - * are all built in. This eliminates the need to separately look up this - * information by indexing separate arrays by symbol or length slot. - * - * - The HUFFDEC_* flags enable easily distinguishing between the different - * types of entries. The HUFFDEC_LITERAL flag enables a fast path for - * literals; the high bit is used for this, as some CPUs can test the - * high bit more easily than other bits. The HUFFDEC_EXCEPTIONAL flag - * makes it possible to detect the two unlikely cases (subtable pointer - * and end of block) in a single bit flag test. - * - * - The low byte is the number of bits that need to be removed from the - * bitstream; this makes this value easily accessible, and it enables the - * micro-optimization of doing 'bitsleft -= entry' instead of - * 'bitsleft -= (u8)entry'. It also includes the number of extra bits, - * so they don't need to be removed separately. - * - * - The flags in bits 15-13 are arranged to be 0 when the - * "remaining codeword length" in bits 11-8 is needed, making this value - * fairly easily accessible as well via a shift and downcast. - * - * - Similarly, bits 13-12 are 0 when the "subtable bits" in bits 11-8 are - * needed, making it possible to extract this value with '& 0x3F' rather - * than '& 0xF'. This value is only used as a shift amount, so this can - * save an 'and' instruction as the masking by 0x3F happens implicitly. - * - * litlen_decode_results[] contains the static part of the entry for each - * symbol. make_decode_table_entry() produces the final entries. - */ -static const u32 litlen_decode_results[] = { - - /* Literals */ -#define ENTRY(literal) (HUFFDEC_LITERAL | ((u32)literal << 16)) - ENTRY(0) , ENTRY(1) , ENTRY(2) , ENTRY(3) , - ENTRY(4) , ENTRY(5) , ENTRY(6) , ENTRY(7) , - ENTRY(8) , ENTRY(9) , ENTRY(10) , ENTRY(11) , - ENTRY(12) , ENTRY(13) , ENTRY(14) , ENTRY(15) , - ENTRY(16) , ENTRY(17) , ENTRY(18) , ENTRY(19) , - ENTRY(20) , ENTRY(21) , ENTRY(22) , ENTRY(23) , - ENTRY(24) , ENTRY(25) , ENTRY(26) , ENTRY(27) , - ENTRY(28) , ENTRY(29) , ENTRY(30) , ENTRY(31) , - ENTRY(32) , ENTRY(33) , ENTRY(34) , ENTRY(35) , - ENTRY(36) , ENTRY(37) , ENTRY(38) , ENTRY(39) , - ENTRY(40) , ENTRY(41) , ENTRY(42) , ENTRY(43) , - ENTRY(44) , ENTRY(45) , ENTRY(46) , ENTRY(47) , - ENTRY(48) , ENTRY(49) , ENTRY(50) , ENTRY(51) , - ENTRY(52) , ENTRY(53) , ENTRY(54) , ENTRY(55) , - ENTRY(56) , ENTRY(57) , ENTRY(58) , ENTRY(59) , - ENTRY(60) , ENTRY(61) , ENTRY(62) , ENTRY(63) , - ENTRY(64) , ENTRY(65) , ENTRY(66) , ENTRY(67) , - ENTRY(68) , ENTRY(69) , ENTRY(70) , ENTRY(71) , - ENTRY(72) , ENTRY(73) , ENTRY(74) , ENTRY(75) , - ENTRY(76) , ENTRY(77) , ENTRY(78) , ENTRY(79) , - ENTRY(80) , ENTRY(81) , ENTRY(82) , ENTRY(83) , - ENTRY(84) , ENTRY(85) , ENTRY(86) , ENTRY(87) , - ENTRY(88) , ENTRY(89) , ENTRY(90) , ENTRY(91) , - ENTRY(92) , ENTRY(93) , ENTRY(94) , ENTRY(95) , - ENTRY(96) , ENTRY(97) , ENTRY(98) , ENTRY(99) , - ENTRY(100) , ENTRY(101) , ENTRY(102) , ENTRY(103) , - ENTRY(104) , ENTRY(105) , ENTRY(106) , ENTRY(107) , - ENTRY(108) , ENTRY(109) , ENTRY(110) , ENTRY(111) , - ENTRY(112) , ENTRY(113) , ENTRY(114) , ENTRY(115) , - ENTRY(116) , ENTRY(117) , ENTRY(118) , ENTRY(119) , - ENTRY(120) , ENTRY(121) , ENTRY(122) , ENTRY(123) , - ENTRY(124) , ENTRY(125) , ENTRY(126) , ENTRY(127) , - ENTRY(128) , ENTRY(129) , ENTRY(130) , ENTRY(131) , - ENTRY(132) , ENTRY(133) , ENTRY(134) , ENTRY(135) , - ENTRY(136) , ENTRY(137) , ENTRY(138) , ENTRY(139) , - ENTRY(140) , ENTRY(141) , ENTRY(142) , ENTRY(143) , - ENTRY(144) , ENTRY(145) , ENTRY(146) , ENTRY(147) , - ENTRY(148) , ENTRY(149) , ENTRY(150) , ENTRY(151) , - ENTRY(152) , ENTRY(153) , ENTRY(154) , ENTRY(155) , - ENTRY(156) , ENTRY(157) , ENTRY(158) , ENTRY(159) , - ENTRY(160) , ENTRY(161) , ENTRY(162) , ENTRY(163) , - ENTRY(164) , ENTRY(165) , ENTRY(166) , ENTRY(167) , - ENTRY(168) , ENTRY(169) , ENTRY(170) , ENTRY(171) , - ENTRY(172) , ENTRY(173) , ENTRY(174) , ENTRY(175) , - ENTRY(176) , ENTRY(177) , ENTRY(178) , ENTRY(179) , - ENTRY(180) , ENTRY(181) , ENTRY(182) , ENTRY(183) , - ENTRY(184) , ENTRY(185) , ENTRY(186) , ENTRY(187) , - ENTRY(188) , ENTRY(189) , ENTRY(190) , ENTRY(191) , - ENTRY(192) , ENTRY(193) , ENTRY(194) , ENTRY(195) , - ENTRY(196) , ENTRY(197) , ENTRY(198) , ENTRY(199) , - ENTRY(200) , ENTRY(201) , ENTRY(202) , ENTRY(203) , - ENTRY(204) , ENTRY(205) , ENTRY(206) , ENTRY(207) , - ENTRY(208) , ENTRY(209) , ENTRY(210) , ENTRY(211) , - ENTRY(212) , ENTRY(213) , ENTRY(214) , ENTRY(215) , - ENTRY(216) , ENTRY(217) , ENTRY(218) , ENTRY(219) , - ENTRY(220) , ENTRY(221) , ENTRY(222) , ENTRY(223) , - ENTRY(224) , ENTRY(225) , ENTRY(226) , ENTRY(227) , - ENTRY(228) , ENTRY(229) , ENTRY(230) , ENTRY(231) , - ENTRY(232) , ENTRY(233) , ENTRY(234) , ENTRY(235) , - ENTRY(236) , ENTRY(237) , ENTRY(238) , ENTRY(239) , - ENTRY(240) , ENTRY(241) , ENTRY(242) , ENTRY(243) , - ENTRY(244) , ENTRY(245) , ENTRY(246) , ENTRY(247) , - ENTRY(248) , ENTRY(249) , ENTRY(250) , ENTRY(251) , - ENTRY(252) , ENTRY(253) , ENTRY(254) , ENTRY(255) , -#undef ENTRY - - /* End of block */ - HUFFDEC_EXCEPTIONAL | HUFFDEC_END_OF_BLOCK, - - /* Lengths */ -#define ENTRY(length_base, num_extra_bits) \ - (((u32)(length_base) << 16) | (num_extra_bits)) - ENTRY(3 , 0) , ENTRY(4 , 0) , ENTRY(5 , 0) , ENTRY(6 , 0), - ENTRY(7 , 0) , ENTRY(8 , 0) , ENTRY(9 , 0) , ENTRY(10 , 0), - ENTRY(11 , 1) , ENTRY(13 , 1) , ENTRY(15 , 1) , ENTRY(17 , 1), - ENTRY(19 , 2) , ENTRY(23 , 2) , ENTRY(27 , 2) , ENTRY(31 , 2), - ENTRY(35 , 3) , ENTRY(43 , 3) , ENTRY(51 , 3) , ENTRY(59 , 3), - ENTRY(67 , 4) , ENTRY(83 , 4) , ENTRY(99 , 4) , ENTRY(115, 4), - ENTRY(131, 5) , ENTRY(163, 5) , ENTRY(195, 5) , ENTRY(227, 5), - ENTRY(258, 0) , ENTRY(258, 0) , ENTRY(258, 0) , -#undef ENTRY -}; - -/* Maximum number of bits that can be consumed by decoding a match offset */ -#define OFFSET_MAXBITS (DEFLATE_MAX_OFFSET_CODEWORD_LEN + \ - DEFLATE_MAX_EXTRA_OFFSET_BITS) -#define OFFSET_MAXFASTBITS (OFFSET_TABLEBITS /* no subtable needed */ + \ - DEFLATE_MAX_EXTRA_OFFSET_BITS) - -/* - * Here is the format of our offset decode table entries. Bits not explicitly - * described contain zeroes: - * - * Offsets: - * Bit 31-16: offset base value - * Bit 15: 0 (!HUFFDEC_EXCEPTIONAL) - * Bit 14: 0 (!HUFFDEC_SUBTABLE_POINTER) - * Bit 11-8: remaining codeword length - * Bit 4-0: remaining codeword length + number of extra bits - * Subtable pointer: - * Bit 31-16: index of start of subtable - * Bit 15: 1 (HUFFDEC_EXCEPTIONAL) - * Bit 14: 1 (HUFFDEC_SUBTABLE_POINTER) - * Bit 11-8: number of subtable bits - * Bit 3-0: number of main table bits - * - * These work the same way as the length entries and subtable pointer entries in - * the litlen decode table; see litlen_decode_results[] above. - */ -static const u32 offset_decode_results[] = { -#define ENTRY(offset_base, num_extra_bits) \ - (((u32)(offset_base) << 16) | (num_extra_bits)) - ENTRY(1 , 0) , ENTRY(2 , 0) , ENTRY(3 , 0) , ENTRY(4 , 0) , - ENTRY(5 , 1) , ENTRY(7 , 1) , ENTRY(9 , 2) , ENTRY(13 , 2) , - ENTRY(17 , 3) , ENTRY(25 , 3) , ENTRY(33 , 4) , ENTRY(49 , 4) , - ENTRY(65 , 5) , ENTRY(97 , 5) , ENTRY(129 , 6) , ENTRY(193 , 6) , - ENTRY(257 , 7) , ENTRY(385 , 7) , ENTRY(513 , 8) , ENTRY(769 , 8) , - ENTRY(1025 , 9) , ENTRY(1537 , 9) , ENTRY(2049 , 10) , ENTRY(3073 , 10) , - ENTRY(4097 , 11) , ENTRY(6145 , 11) , ENTRY(8193 , 12) , ENTRY(12289 , 12) , - ENTRY(16385 , 13) , ENTRY(24577 , 13) , ENTRY(24577 , 13) , ENTRY(24577 , 13) , -#undef ENTRY -}; - -/* - * The main DEFLATE decompressor structure. Since libdeflate only supports - * full-buffer decompression, this structure doesn't store the entire - * decompression state, most of which is in stack variables. Instead, this - * struct just contains the decode tables and some temporary arrays used for - * building them, as these are too large to comfortably allocate on the stack. - * - * Storing the decode tables in the decompressor struct also allows the decode - * tables for the static codes to be reused whenever two static Huffman blocks - * are decoded without an intervening dynamic block, even across streams. - */ -struct libdeflate_decompressor { - - /* - * The arrays aren't all needed at the same time. 'precode_lens' and - * 'precode_decode_table' are unneeded after 'lens' has been filled. - * Furthermore, 'lens' need not be retained after building the litlen - * and offset decode tables. In fact, 'lens' can be in union with - * 'litlen_decode_table' provided that 'offset_decode_table' is separate - * and is built first. - */ - - union { - u8 precode_lens[DEFLATE_NUM_PRECODE_SYMS]; - - struct { - u8 lens[DEFLATE_NUM_LITLEN_SYMS + - DEFLATE_NUM_OFFSET_SYMS + - DEFLATE_MAX_LENS_OVERRUN]; - - u32 precode_decode_table[PRECODE_ENOUGH]; - } l; - - u32 litlen_decode_table[LITLEN_ENOUGH]; - } u; - - u32 offset_decode_table[OFFSET_ENOUGH]; - - /* used only during build_decode_table() */ - u16 sorted_syms[DEFLATE_MAX_NUM_SYMS]; - - bool static_codes_loaded; - unsigned litlen_tablebits; - - /* The free() function for this struct, chosen at allocation time */ - free_func_t free_func; -}; - -/* - * Build a table for fast decoding of symbols from a Huffman code. As input, - * this function takes the codeword length of each symbol which may be used in - * the code. As output, it produces a decode table for the canonical Huffman - * code described by the codeword lengths. The decode table is built with the - * assumption that it will be indexed with "bit-reversed" codewords, where the - * low-order bit is the first bit of the codeword. This format is used for all - * Huffman codes in DEFLATE. - * - * @decode_table - * The array in which the decode table will be generated. This array must - * have sufficient length; see the definition of the ENOUGH numbers. - * @lens - * An array which provides, for each symbol, the length of the - * corresponding codeword in bits, or 0 if the symbol is unused. This may - * alias @decode_table, since nothing is written to @decode_table until all - * @lens have been consumed. All codeword lengths are assumed to be <= - * @max_codeword_len but are otherwise considered untrusted. If they do - * not form a valid Huffman code, then the decode table is not built and - * %false is returned. - * @num_syms - * The number of symbols in the code, including all unused symbols. - * @decode_results - * An array which gives the incomplete decode result for each symbol. The - * needed values in this array will be combined with codeword lengths to - * make the final decode table entries using make_decode_table_entry(). - * @table_bits - * The log base-2 of the number of main table entries to use. - * If @table_bits_ret != NULL, then @table_bits is treated as a maximum - * value and it will be decreased if a smaller table would be sufficient. - * @max_codeword_len - * The maximum allowed codeword length for this Huffman code. - * Must be <= DEFLATE_MAX_CODEWORD_LEN. - * @sorted_syms - * A temporary array of length @num_syms. - * @table_bits_ret - * If non-NULL, then the dynamic table_bits is enabled, and the actual - * table_bits value will be returned here. - * - * Returns %true if successful; %false if the codeword lengths do not form a - * valid Huffman code. - */ -static bool -build_decode_table(u32 decode_table[], - const u8 lens[], - const unsigned num_syms, - const u32 decode_results[], - unsigned table_bits, - unsigned max_codeword_len, - u16 *sorted_syms, - unsigned *table_bits_ret) -{ - unsigned len_counts[DEFLATE_MAX_CODEWORD_LEN + 1]; - unsigned offsets[DEFLATE_MAX_CODEWORD_LEN + 1]; - unsigned sym; /* current symbol */ - unsigned codeword; /* current codeword, bit-reversed */ - unsigned len; /* current codeword length in bits */ - unsigned count; /* num codewords remaining with this length */ - u32 codespace_used; /* codespace used out of '2^max_codeword_len' */ - unsigned cur_table_end; /* end index of current table */ - unsigned subtable_prefix; /* codeword prefix of current subtable */ - unsigned subtable_start; /* start index of current subtable */ - unsigned subtable_bits; /* log2 of current subtable length */ - - /* Count how many codewords have each length, including 0. */ - for (len = 0; len <= max_codeword_len; len++) - len_counts[len] = 0; - for (sym = 0; sym < num_syms; sym++) - len_counts[lens[sym]]++; - - /* - * Determine the actual maximum codeword length that was used, and - * decrease table_bits to it if allowed. - */ - while (max_codeword_len > 1 && len_counts[max_codeword_len] == 0) - max_codeword_len--; - if (table_bits_ret != NULL) { - table_bits = MIN(table_bits, max_codeword_len); - *table_bits_ret = table_bits; - } - - /* - * Sort the symbols primarily by increasing codeword length and - * secondarily by increasing symbol value; or equivalently by their - * codewords in lexicographic order, since a canonical code is assumed. - * - * For efficiency, also compute 'codespace_used' in the same pass over - * 'len_counts[]' used to build 'offsets[]' for sorting. - */ - - /* Ensure that 'codespace_used' cannot overflow. */ - STATIC_ASSERT(sizeof(codespace_used) == 4); - STATIC_ASSERT(UINT32_MAX / (1U << (DEFLATE_MAX_CODEWORD_LEN - 1)) >= - DEFLATE_MAX_NUM_SYMS); - - offsets[0] = 0; - offsets[1] = len_counts[0]; - codespace_used = 0; - for (len = 1; len < max_codeword_len; len++) { - offsets[len + 1] = offsets[len] + len_counts[len]; - codespace_used = (codespace_used << 1) + len_counts[len]; - } - codespace_used = (codespace_used << 1) + len_counts[len]; - - for (sym = 0; sym < num_syms; sym++) - sorted_syms[offsets[lens[sym]]++] = sym; - - sorted_syms += offsets[0]; /* Skip unused symbols */ - - /* lens[] is done being used, so we can write to decode_table[] now. */ - - /* - * Check whether the lengths form a complete code (exactly fills the - * codespace), an incomplete code (doesn't fill the codespace), or an - * overfull code (overflows the codespace). A codeword of length 'n' - * uses proportion '1/(2^n)' of the codespace. An overfull code is - * nonsensical, so is considered invalid. An incomplete code is - * considered valid only in two specific cases; see below. - */ - - /* overfull code? */ - if (unlikely(codespace_used > (1U << max_codeword_len))) - return false; - - /* incomplete code? */ - if (unlikely(codespace_used < (1U << max_codeword_len))) { - u32 entry; - unsigned i; - - /* - * The DEFLATE RFC explicitly allows the offset code to be - * incomplete in two cases: a code containing just 1 codeword, - * if that codeword has length 1; and a code containing no - * codewords. Note: the list of offset codeword lengths is - * always nonempty, but lengths of 0 don't count as codewords. - * - * The RFC doesn't say whether the same cases are allowed for - * the litlen and pre codes. It's actually impossible for no - * symbols to be used from these codes; however, it's - * technically possible for only one symbol to be used. zlib - * allows 1 codeword for the litlen code, but not the precode. - * The RFC also doesn't say whether, when there is 1 codeword, - * that codeword is '0' or '1'. zlib uses '0'. - * - * We accept what zlib accepts, plus a bit more. First, we - * don't treat the precode more strictly than the litlen and - * offset codes. There's no convincing reason to add a special - * case for the precode here. - * - * Second, we just map each allowed incompete code to a complete - * code with only real symbols. To do this, we choose a symbol, - * either the used symbol (for codes with 1 codeword) or an - * arbitrary symbol (for empty codes), and give it both - * codewords '0' and '1'. zlib instead uses a special ERROR - * symbol in the part of the codespace the code doesn't use. - * However, having an ERROR symbol reduces the performance of - * the Huffman decoder, for no real benefit. Our approach also - * avoids having to decide whether '0' or '1' is correct. - * - * Like zlib, we still reject all incomplete codes that contain - * more than 1 codeword or a codeword length greater than 1. - */ - if (codespace_used == 0) { - sym = 0; /* arbitrary */ - } else { - if (codespace_used != (1U << (max_codeword_len - 1)) || - len_counts[1] != 1) - return false; - sym = sorted_syms[0]; - } - entry = make_decode_table_entry(decode_results, sym, 1); - for (i = 0; i < (1U << table_bits); i++) - decode_table[i] = entry; - return true; - } - - /* - * The lengths form a complete code. Now, enumerate the codewords in - * lexicographic order and fill the decode table entries for each one. - * - * First, process all codewords with len <= table_bits. Each one gets - * '2^(table_bits-len)' direct entries in the table. - * - * Since DEFLATE uses bit-reversed codewords, these entries aren't - * consecutive but rather are spaced '2^len' entries apart. This makes - * filling them naively somewhat awkward and inefficient, since strided - * stores are less cache-friendly and preclude the use of word or - * vector-at-a-time stores to fill multiple entries per instruction. - * - * To optimize this, we incrementally double the table size. When - * processing codewords with length 'len', the table is treated as - * having only '2^len' entries, so each codeword uses just one entry. - * Then, each time 'len' is incremented, the table size is doubled and - * the first half is copied to the second half. This significantly - * improves performance over naively doing strided stores. - * - * Note that some entries copied for each table doubling may not have - * been initialized yet, but it doesn't matter since they're guaranteed - * to be initialized later (because the Huffman code is complete). - */ - codeword = 0; - len = 1; - while ((count = len_counts[len]) == 0) - len++; - cur_table_end = 1U << len; - while (len <= table_bits) { - /* Process all 'count' codewords with length 'len' bits. */ - do { - unsigned bit; - - /* Fill the first entry for the current codeword. */ - decode_table[codeword] = - make_decode_table_entry(decode_results, - *sorted_syms++, len); - - if (codeword == cur_table_end - 1) { - /* Last codeword (all 1's) */ - for (; len < table_bits; len++) { - memcpy(&decode_table[cur_table_end], - decode_table, - cur_table_end * - sizeof(decode_table[0])); - cur_table_end <<= 1; - } - return true; - } - /* - * To advance to the lexicographically next codeword in - * the canonical code, the codeword must be incremented, - * then 0's must be appended to the codeword as needed - * to match the next codeword's length. - * - * Since the codeword is bit-reversed, appending 0's is - * a no-op. However, incrementing it is nontrivial. To - * do so efficiently, use the 'bsr' instruction to find - * the last (highest order) 0 bit in the codeword, set - * it, and clear any later (higher order) 1 bits. But - * 'bsr' actually finds the highest order 1 bit, so to - * use it first flip all bits in the codeword by XOR'ing - * it with (1U << len) - 1 == cur_table_end - 1. - */ - bit = 1U << bsr32(codeword ^ (cur_table_end - 1)); - codeword &= bit - 1; - codeword |= bit; - } while (--count); - - /* Advance to the next codeword length. */ - do { - if (++len <= table_bits) { - memcpy(&decode_table[cur_table_end], - decode_table, - cur_table_end * sizeof(decode_table[0])); - cur_table_end <<= 1; - } - } while ((count = len_counts[len]) == 0); - } - - /* Process codewords with len > table_bits. These require subtables. */ - cur_table_end = 1U << table_bits; - subtable_prefix = -1; - subtable_start = 0; - for (;;) { - u32 entry; - unsigned i; - unsigned stride; - unsigned bit; - - /* - * Start a new subtable if the first 'table_bits' bits of the - * codeword don't match the prefix of the current subtable. - */ - if ((codeword & ((1U << table_bits) - 1)) != subtable_prefix) { - subtable_prefix = (codeword & ((1U << table_bits) - 1)); - subtable_start = cur_table_end; - /* - * Calculate the subtable length. If the codeword has - * length 'table_bits + n', then the subtable needs - * '2^n' entries. But it may need more; if fewer than - * '2^n' codewords of length 'table_bits + n' remain, - * then the length will need to be incremented to bring - * in longer codewords until the subtable can be - * completely filled. Note that because the Huffman - * code is complete, it will always be possible to fill - * the subtable eventually. - */ - subtable_bits = len - table_bits; - codespace_used = count; - while (codespace_used < (1U << subtable_bits)) { - subtable_bits++; - codespace_used = (codespace_used << 1) + - len_counts[table_bits + subtable_bits]; - } - cur_table_end = subtable_start + (1U << subtable_bits); - - /* - * Create the entry that points from the main table to - * the subtable. - */ - decode_table[subtable_prefix] = - ((u32)subtable_start << 16) | - HUFFDEC_EXCEPTIONAL | - HUFFDEC_SUBTABLE_POINTER | - (subtable_bits << 8) | table_bits; - } - - /* Fill the subtable entries for the current codeword. */ - entry = make_decode_table_entry(decode_results, *sorted_syms++, - len - table_bits); - i = subtable_start + (codeword >> table_bits); - stride = 1U << (len - table_bits); - do { - decode_table[i] = entry; - i += stride; - } while (i < cur_table_end); - - /* Advance to the next codeword. */ - if (codeword == (1U << len) - 1) /* last codeword (all 1's)? */ - return true; - bit = 1U << bsr32(codeword ^ ((1U << len) - 1)); - codeword &= bit - 1; - codeword |= bit; - count--; - while (count == 0) - count = len_counts[++len]; - } -} - -/* Build the decode table for the precode. */ -static bool -build_precode_decode_table(struct libdeflate_decompressor *d) -{ - /* When you change TABLEBITS, you must change ENOUGH, and vice versa! */ - STATIC_ASSERT(PRECODE_TABLEBITS == 7 && PRECODE_ENOUGH == 128); - - STATIC_ASSERT(ARRAY_LEN(precode_decode_results) == - DEFLATE_NUM_PRECODE_SYMS); - - return build_decode_table(d->u.l.precode_decode_table, - d->u.precode_lens, - DEFLATE_NUM_PRECODE_SYMS, - precode_decode_results, - PRECODE_TABLEBITS, - DEFLATE_MAX_PRE_CODEWORD_LEN, - d->sorted_syms, - NULL); -} - -/* Build the decode table for the literal/length code. */ -static bool -build_litlen_decode_table(struct libdeflate_decompressor *d, - unsigned num_litlen_syms, unsigned num_offset_syms) -{ - /* When you change TABLEBITS, you must change ENOUGH, and vice versa! */ - STATIC_ASSERT(LITLEN_TABLEBITS == 11 && LITLEN_ENOUGH == 2342); - - STATIC_ASSERT(ARRAY_LEN(litlen_decode_results) == - DEFLATE_NUM_LITLEN_SYMS); - - return build_decode_table(d->u.litlen_decode_table, - d->u.l.lens, - num_litlen_syms, - litlen_decode_results, - LITLEN_TABLEBITS, - DEFLATE_MAX_LITLEN_CODEWORD_LEN, - d->sorted_syms, - &d->litlen_tablebits); -} - -/* Build the decode table for the offset code. */ -static bool -build_offset_decode_table(struct libdeflate_decompressor *d, - unsigned num_litlen_syms, unsigned num_offset_syms) -{ - /* When you change TABLEBITS, you must change ENOUGH, and vice versa! */ - STATIC_ASSERT(OFFSET_TABLEBITS == 8 && OFFSET_ENOUGH == 402); - - STATIC_ASSERT(ARRAY_LEN(offset_decode_results) == - DEFLATE_NUM_OFFSET_SYMS); - - return build_decode_table(d->offset_decode_table, - d->u.l.lens + num_litlen_syms, - num_offset_syms, - offset_decode_results, - OFFSET_TABLEBITS, - DEFLATE_MAX_OFFSET_CODEWORD_LEN, - d->sorted_syms, - NULL); -} - -/***************************************************************************** - * Main decompression routine - *****************************************************************************/ - -typedef enum libdeflate_result (*decompress_func_t) - (struct libdeflate_decompressor * restrict d, - const void * restrict in, size_t in_nbytes, - void * restrict out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, size_t *actual_out_nbytes_ret); - -#define FUNCNAME deflate_decompress_default -#undef ATTRIBUTES -#undef EXTRACT_VARBITS -#undef EXTRACT_VARBITS8 -#include "decompress_template.h" - -/* Include architecture-specific implementation(s) if available. */ -#undef DEFAULT_IMPL -#undef arch_select_decompress_func -#if defined(ARCH_X86_32) || defined(ARCH_X86_64) -# include "x86/decompress_impl.h" -#endif - -#ifndef DEFAULT_IMPL -# define DEFAULT_IMPL deflate_decompress_default -#endif - -#ifdef arch_select_decompress_func -static enum libdeflate_result -dispatch_decomp(struct libdeflate_decompressor * restrict d, - const void * restrict in, size_t in_nbytes, - void * restrict out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, size_t *actual_out_nbytes_ret); - -static volatile decompress_func_t decompress_impl = dispatch_decomp; - -/* Choose the best implementation at runtime. */ -static enum libdeflate_result -dispatch_decomp(struct libdeflate_decompressor * restrict d, - const void * restrict in, size_t in_nbytes, - void * restrict out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, size_t *actual_out_nbytes_ret) -{ - decompress_func_t f = arch_select_decompress_func(); - - if (f == NULL) - f = DEFAULT_IMPL; - - decompress_impl = f; - return f(d, in, in_nbytes, out, out_nbytes_avail, - actual_in_nbytes_ret, actual_out_nbytes_ret); -} -#else -/* The best implementation is statically known, so call it directly. */ -# define decompress_impl DEFAULT_IMPL -#endif - -/* - * This is the main DEFLATE decompression routine. See libdeflate.h for the - * documentation. - * - * Note that the real code is in decompress_template.h. The part here just - * handles calling the appropriate implementation depending on the CPU features - * at runtime. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_deflate_decompress_ex(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret) -{ - return decompress_impl(d, in, in_nbytes, out, out_nbytes_avail, - actual_in_nbytes_ret, actual_out_nbytes_ret); -} - -LIBDEFLATEAPI enum libdeflate_result -libdeflate_deflate_decompress(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret) -{ - return libdeflate_deflate_decompress_ex(d, in, in_nbytes, - out, out_nbytes_avail, - NULL, actual_out_nbytes_ret); -} - -LIBDEFLATEAPI struct libdeflate_decompressor * -libdeflate_alloc_decompressor_ex(const struct libdeflate_options *options) -{ - struct libdeflate_decompressor *d; - - /* - * Note: if more fields are added to libdeflate_options, this code will - * need to be updated to support both the old and new structs. - */ - if (options->sizeof_options != sizeof(*options)) - return NULL; - - d = (options->malloc_func ? options->malloc_func : - libdeflate_default_malloc_func)(sizeof(*d)); - if (d == NULL) - return NULL; - /* - * Note that only certain parts of the decompressor actually must be - * initialized here: - * - * - 'static_codes_loaded' must be initialized to false. - * - * - The first half of the main portion of each decode table must be - * initialized to any value, to avoid reading from uninitialized - * memory during table expansion in build_decode_table(). (Although, - * this is really just to avoid warnings with dynamic tools like - * valgrind, since build_decode_table() is guaranteed to initialize - * all entries eventually anyway.) - * - * - 'free_func' must be set. - * - * But for simplicity, we currently just zero the whole decompressor. - */ - memset(d, 0, sizeof(*d)); - d->free_func = options->free_func ? - options->free_func : libdeflate_default_free_func; - return d; -} - -LIBDEFLATEAPI struct libdeflate_decompressor * -libdeflate_alloc_decompressor(void) -{ - static const struct libdeflate_options defaults = { - .sizeof_options = sizeof(defaults), - }; - return libdeflate_alloc_decompressor_ex(&defaults); -} - -LIBDEFLATEAPI void -libdeflate_free_decompressor(struct libdeflate_decompressor *d) -{ - if (d) - d->free_func(d); -} diff --git a/dm/third_party/libdeflate/lib/gzip_compress.c b/dm/third_party/libdeflate/lib/gzip_compress.c deleted file mode 100644 index b7d5076..0000000 --- a/dm/third_party/libdeflate/lib/gzip_compress.c +++ /dev/null @@ -1,90 +0,0 @@ -/* - * gzip_compress.c - compress with a gzip wrapper - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "deflate_compress.h" -#include "gzip_constants.h" - -LIBDEFLATEAPI size_t -libdeflate_gzip_compress(struct libdeflate_compressor *c, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail) -{ - u8 *out_next = out; - unsigned compression_level; - u8 xfl; - size_t deflate_size; - - if (out_nbytes_avail <= GZIP_MIN_OVERHEAD) - return 0; - - /* ID1 */ - *out_next++ = GZIP_ID1; - /* ID2 */ - *out_next++ = GZIP_ID2; - /* CM */ - *out_next++ = GZIP_CM_DEFLATE; - /* FLG */ - *out_next++ = 0; - /* MTIME */ - put_unaligned_le32(GZIP_MTIME_UNAVAILABLE, out_next); - out_next += 4; - /* XFL */ - xfl = 0; - compression_level = libdeflate_get_compression_level(c); - if (compression_level < 2) - xfl |= GZIP_XFL_FASTEST_COMPRESSION; - else if (compression_level >= 8) - xfl |= GZIP_XFL_SLOWEST_COMPRESSION; - *out_next++ = xfl; - /* OS */ - *out_next++ = GZIP_OS_UNKNOWN; /* OS */ - - /* Compressed data */ - deflate_size = libdeflate_deflate_compress(c, in, in_nbytes, out_next, - out_nbytes_avail - GZIP_MIN_OVERHEAD); - if (deflate_size == 0) - return 0; - out_next += deflate_size; - - /* CRC32 */ - put_unaligned_le32(libdeflate_crc32(0, in, in_nbytes), out_next); - out_next += 4; - - /* ISIZE */ - put_unaligned_le32((u32)in_nbytes, out_next); - out_next += 4; - - return out_next - (u8 *)out; -} - -LIBDEFLATEAPI size_t -libdeflate_gzip_compress_bound(struct libdeflate_compressor *c, - size_t in_nbytes) -{ - return GZIP_MIN_OVERHEAD + - libdeflate_deflate_compress_bound(c, in_nbytes); -} diff --git a/dm/third_party/libdeflate/lib/gzip_constants.h b/dm/third_party/libdeflate/lib/gzip_constants.h deleted file mode 100644 index 35e4728..0000000 --- a/dm/third_party/libdeflate/lib/gzip_constants.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * gzip_constants.h - constants for the gzip wrapper format - */ - -#ifndef LIB_GZIP_CONSTANTS_H -#define LIB_GZIP_CONSTANTS_H - -#define GZIP_MIN_HEADER_SIZE 10 -#define GZIP_FOOTER_SIZE 8 -#define GZIP_MIN_OVERHEAD (GZIP_MIN_HEADER_SIZE + GZIP_FOOTER_SIZE) - -#define GZIP_ID1 0x1F -#define GZIP_ID2 0x8B - -#define GZIP_CM_DEFLATE 8 - -#define GZIP_FTEXT 0x01 -#define GZIP_FHCRC 0x02 -#define GZIP_FEXTRA 0x04 -#define GZIP_FNAME 0x08 -#define GZIP_FCOMMENT 0x10 -#define GZIP_FRESERVED 0xE0 - -#define GZIP_MTIME_UNAVAILABLE 0 - -#define GZIP_XFL_SLOWEST_COMPRESSION 0x02 -#define GZIP_XFL_FASTEST_COMPRESSION 0x04 - -#define GZIP_OS_FAT 0 -#define GZIP_OS_AMIGA 1 -#define GZIP_OS_VMS 2 -#define GZIP_OS_UNIX 3 -#define GZIP_OS_VM_CMS 4 -#define GZIP_OS_ATARI_TOS 5 -#define GZIP_OS_HPFS 6 -#define GZIP_OS_MACINTOSH 7 -#define GZIP_OS_Z_SYSTEM 8 -#define GZIP_OS_CP_M 9 -#define GZIP_OS_TOPS_20 10 -#define GZIP_OS_NTFS 11 -#define GZIP_OS_QDOS 12 -#define GZIP_OS_RISCOS 13 -#define GZIP_OS_UNKNOWN 255 - -#endif /* LIB_GZIP_CONSTANTS_H */ diff --git a/dm/third_party/libdeflate/lib/gzip_decompress.c b/dm/third_party/libdeflate/lib/gzip_decompress.c deleted file mode 100644 index 76b74f6..0000000 --- a/dm/third_party/libdeflate/lib/gzip_decompress.c +++ /dev/null @@ -1,144 +0,0 @@ -/* - * gzip_decompress.c - decompress with a gzip wrapper - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "lib_common.h" -#include "gzip_constants.h" - -LIBDEFLATEAPI enum libdeflate_result -libdeflate_gzip_decompress_ex(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret) -{ - const u8 *in_next = in; - const u8 * const in_end = in_next + in_nbytes; - u8 flg; - size_t actual_in_nbytes; - size_t actual_out_nbytes; - enum libdeflate_result result; - - if (in_nbytes < GZIP_MIN_OVERHEAD) - return LIBDEFLATE_BAD_DATA; - - /* ID1 */ - if (*in_next++ != GZIP_ID1) - return LIBDEFLATE_BAD_DATA; - /* ID2 */ - if (*in_next++ != GZIP_ID2) - return LIBDEFLATE_BAD_DATA; - /* CM */ - if (*in_next++ != GZIP_CM_DEFLATE) - return LIBDEFLATE_BAD_DATA; - flg = *in_next++; - /* MTIME */ - in_next += 4; - /* XFL */ - in_next += 1; - /* OS */ - in_next += 1; - - if (flg & GZIP_FRESERVED) - return LIBDEFLATE_BAD_DATA; - - /* Extra field */ - if (flg & GZIP_FEXTRA) { - u16 xlen = get_unaligned_le16(in_next); - in_next += 2; - - if (in_end - in_next < (u32)xlen + GZIP_FOOTER_SIZE) - return LIBDEFLATE_BAD_DATA; - - in_next += xlen; - } - - /* Original file name (zero terminated) */ - if (flg & GZIP_FNAME) { - while (*in_next++ != 0 && in_next != in_end) - ; - if (in_end - in_next < GZIP_FOOTER_SIZE) - return LIBDEFLATE_BAD_DATA; - } - - /* File comment (zero terminated) */ - if (flg & GZIP_FCOMMENT) { - while (*in_next++ != 0 && in_next != in_end) - ; - if (in_end - in_next < GZIP_FOOTER_SIZE) - return LIBDEFLATE_BAD_DATA; - } - - /* CRC16 for gzip header */ - if (flg & GZIP_FHCRC) { - in_next += 2; - if (in_end - in_next < GZIP_FOOTER_SIZE) - return LIBDEFLATE_BAD_DATA; - } - - /* Compressed data */ - result = libdeflate_deflate_decompress_ex(d, in_next, - in_end - GZIP_FOOTER_SIZE - in_next, - out, out_nbytes_avail, - &actual_in_nbytes, - actual_out_nbytes_ret); - if (result != LIBDEFLATE_SUCCESS) - return result; - - if (actual_out_nbytes_ret) - actual_out_nbytes = *actual_out_nbytes_ret; - else - actual_out_nbytes = out_nbytes_avail; - - in_next += actual_in_nbytes; - - /* CRC32 */ - if (libdeflate_crc32(0, out, actual_out_nbytes) != - get_unaligned_le32(in_next)) - return LIBDEFLATE_BAD_DATA; - in_next += 4; - - /* ISIZE */ - if ((u32)actual_out_nbytes != get_unaligned_le32(in_next)) - return LIBDEFLATE_BAD_DATA; - in_next += 4; - - if (actual_in_nbytes_ret) - *actual_in_nbytes_ret = in_next - (u8 *)in; - - return LIBDEFLATE_SUCCESS; -} - -LIBDEFLATEAPI enum libdeflate_result -libdeflate_gzip_decompress(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret) -{ - return libdeflate_gzip_decompress_ex(d, in, in_nbytes, - out, out_nbytes_avail, - NULL, actual_out_nbytes_ret); -} diff --git a/dm/third_party/libdeflate/lib/hc_matchfinder.h b/dm/third_party/libdeflate/lib/hc_matchfinder.h deleted file mode 100644 index a0cddfc..0000000 --- a/dm/third_party/libdeflate/lib/hc_matchfinder.h +++ /dev/null @@ -1,401 +0,0 @@ -/* - * hc_matchfinder.h - Lempel-Ziv matchfinding with a hash table of linked lists - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * --------------------------------------------------------------------------- - * - * Algorithm - * - * This is a Hash Chains (hc) based matchfinder. - * - * The main data structure is a hash table where each hash bucket contains a - * linked list (or "chain") of sequences whose first 4 bytes share the same hash - * code. Each sequence is identified by its starting position in the input - * buffer. - * - * The algorithm processes the input buffer sequentially. At each byte - * position, the hash code of the first 4 bytes of the sequence beginning at - * that position (the sequence being matched against) is computed. This - * identifies the hash bucket to use for that position. Then, this hash - * bucket's linked list is searched for matches. Then, a new linked list node - * is created to represent the current sequence and is prepended to the list. - * - * This algorithm has several useful properties: - * - * - It only finds true Lempel-Ziv matches; i.e., those where the matching - * sequence occurs prior to the sequence being matched against. - * - * - The sequences in each linked list are always sorted by decreasing starting - * position. Therefore, the closest (smallest offset) matches are found - * first, which in many compression formats tend to be the cheapest to encode. - * - * - Although fast running time is not guaranteed due to the possibility of the - * lists getting very long, the worst degenerate behavior can be easily - * prevented by capping the number of nodes searched at each position. - * - * - If the compressor decides not to search for matches at a certain position, - * then that position can be quickly inserted without searching the list. - * - * - The algorithm is adaptable to sliding windows: just store the positions - * relative to a "base" value that is updated from time to time, and stop - * searching each list when the sequences get too far away. - * - * ---------------------------------------------------------------------------- - * - * Optimizations - * - * The main hash table and chains handle length 4+ matches. Length 3 matches - * are handled by a separate hash table with no chains. This works well for - * typical "greedy" or "lazy"-style compressors, where length 3 matches are - * often only helpful if they have small offsets. Instead of searching a full - * chain for length 3+ matches, the algorithm just checks for one close length 3 - * match, then focuses on finding length 4+ matches. - * - * The longest_match() and skip_bytes() functions are inlined into the - * compressors that use them. This isn't just about saving the overhead of a - * function call. These functions are intended to be called from the inner - * loops of compressors, where giving the compiler more control over register - * allocation is very helpful. There is also significant benefit to be gained - * from allowing the CPU to predict branches independently at each call site. - * For example, "lazy"-style compressors can be written with two calls to - * longest_match(), each of which starts with a different 'best_len' and - * therefore has significantly different performance characteristics. - * - * Although any hash function can be used, a multiplicative hash is fast and - * works well. - * - * On some processors, it is significantly faster to extend matches by whole - * words (32 or 64 bits) instead of by individual bytes. For this to be the - * case, the processor must implement unaligned memory accesses efficiently and - * must have either a fast "find first set bit" instruction or a fast "find last - * set bit" instruction, depending on the processor's endianness. - * - * The code uses one loop for finding the first match and one loop for finding a - * longer match. Each of these loops is tuned for its respective task and in - * combination are faster than a single generalized loop that handles both - * tasks. - * - * The code also uses a tight inner loop that only compares the last and first - * bytes of a potential match. It is only when these bytes match that a full - * match extension is attempted. - * - * ---------------------------------------------------------------------------- - */ - -#ifndef LIB_HC_MATCHFINDER_H -#define LIB_HC_MATCHFINDER_H - -#include "matchfinder_common.h" - -#define HC_MATCHFINDER_HASH3_ORDER 15 -#define HC_MATCHFINDER_HASH4_ORDER 16 - -#define HC_MATCHFINDER_TOTAL_HASH_SIZE \ - (((1UL << HC_MATCHFINDER_HASH3_ORDER) + \ - (1UL << HC_MATCHFINDER_HASH4_ORDER)) * sizeof(mf_pos_t)) - -struct MATCHFINDER_ALIGNED hc_matchfinder { - - /* The hash table for finding length 3 matches */ - mf_pos_t hash3_tab[1UL << HC_MATCHFINDER_HASH3_ORDER]; - - /* The hash table which contains the first nodes of the linked lists for - * finding length 4+ matches */ - mf_pos_t hash4_tab[1UL << HC_MATCHFINDER_HASH4_ORDER]; - - /* The "next node" references for the linked lists. The "next node" of - * the node for the sequence with position 'pos' is 'next_tab[pos]'. */ - mf_pos_t next_tab[MATCHFINDER_WINDOW_SIZE]; -}; - -/* Prepare the matchfinder for a new input buffer. */ -static forceinline void -hc_matchfinder_init(struct hc_matchfinder *mf) -{ - STATIC_ASSERT(HC_MATCHFINDER_TOTAL_HASH_SIZE % - MATCHFINDER_SIZE_ALIGNMENT == 0); - - matchfinder_init((mf_pos_t *)mf, HC_MATCHFINDER_TOTAL_HASH_SIZE); -} - -static forceinline void -hc_matchfinder_slide_window(struct hc_matchfinder *mf) -{ - STATIC_ASSERT(sizeof(*mf) % MATCHFINDER_SIZE_ALIGNMENT == 0); - - matchfinder_rebase((mf_pos_t *)mf, sizeof(*mf)); -} - -/* - * Find the longest match longer than 'best_len' bytes. - * - * @mf - * The matchfinder structure. - * @in_base_p - * Location of a pointer which points to the place in the input data the - * matchfinder currently stores positions relative to. This may be updated - * by this function. - * @in_next - * Pointer to the next position in the input buffer, i.e. the sequence - * being matched against. - * @best_len - * Require a match longer than this length. - * @max_len - * The maximum permissible match length at this position. - * @nice_len - * Stop searching if a match of at least this length is found. - * Must be <= @max_len. - * @max_search_depth - * Limit on the number of potential matches to consider. Must be >= 1. - * @next_hashes - * The precomputed hash codes for the sequence beginning at @in_next. - * These will be used and then updated with the precomputed hashcodes for - * the sequence beginning at @in_next + 1. - * @offset_ret - * If a match is found, its offset is returned in this location. - * - * Return the length of the match found, or 'best_len' if no match longer than - * 'best_len' was found. - */ -static forceinline u32 -hc_matchfinder_longest_match(struct hc_matchfinder * const mf, - const u8 ** const in_base_p, - const u8 * const in_next, - u32 best_len, - const u32 max_len, - const u32 nice_len, - const u32 max_search_depth, - u32 * const next_hashes, - u32 * const offset_ret) -{ - u32 depth_remaining = max_search_depth; - const u8 *best_matchptr = in_next; - mf_pos_t cur_node3, cur_node4; - u32 hash3, hash4; - u32 next_hashseq; - u32 seq4; - const u8 *matchptr; - u32 len; - u32 cur_pos = in_next - *in_base_p; - const u8 *in_base; - mf_pos_t cutoff; - - if (cur_pos == MATCHFINDER_WINDOW_SIZE) { - hc_matchfinder_slide_window(mf); - *in_base_p += MATCHFINDER_WINDOW_SIZE; - cur_pos = 0; - } - - in_base = *in_base_p; - cutoff = cur_pos - MATCHFINDER_WINDOW_SIZE; - - if (unlikely(max_len < 5)) /* can we read 4 bytes from 'in_next + 1'? */ - goto out; - - /* Get the precomputed hash codes. */ - hash3 = next_hashes[0]; - hash4 = next_hashes[1]; - - /* From the hash buckets, get the first node of each linked list. */ - cur_node3 = mf->hash3_tab[hash3]; - cur_node4 = mf->hash4_tab[hash4]; - - /* Update for length 3 matches. This replaces the singleton node in the - * 'hash3' bucket with the node for the current sequence. */ - mf->hash3_tab[hash3] = cur_pos; - - /* Update for length 4 matches. This prepends the node for the current - * sequence to the linked list in the 'hash4' bucket. */ - mf->hash4_tab[hash4] = cur_pos; - mf->next_tab[cur_pos] = cur_node4; - - /* Compute the next hash codes. */ - next_hashseq = get_unaligned_le32(in_next + 1); - next_hashes[0] = lz_hash(next_hashseq & 0xFFFFFF, HC_MATCHFINDER_HASH3_ORDER); - next_hashes[1] = lz_hash(next_hashseq, HC_MATCHFINDER_HASH4_ORDER); - prefetchw(&mf->hash3_tab[next_hashes[0]]); - prefetchw(&mf->hash4_tab[next_hashes[1]]); - - if (best_len < 4) { /* No match of length >= 4 found yet? */ - - /* Check for a length 3 match if needed. */ - - if (cur_node3 <= cutoff) - goto out; - - seq4 = load_u32_unaligned(in_next); - - if (best_len < 3) { - matchptr = &in_base[cur_node3]; - if (load_u24_unaligned(matchptr) == loaded_u32_to_u24(seq4)) { - best_len = 3; - best_matchptr = matchptr; - } - } - - /* Check for a length 4 match. */ - - if (cur_node4 <= cutoff) - goto out; - - for (;;) { - /* No length 4 match found yet. Check the first 4 bytes. */ - matchptr = &in_base[cur_node4]; - - if (load_u32_unaligned(matchptr) == seq4) - break; - - /* The first 4 bytes did not match. Keep trying. */ - cur_node4 = mf->next_tab[cur_node4 & (MATCHFINDER_WINDOW_SIZE - 1)]; - if (cur_node4 <= cutoff || !--depth_remaining) - goto out; - } - - /* Found a match of length >= 4. Extend it to its full length. */ - best_matchptr = matchptr; - best_len = lz_extend(in_next, best_matchptr, 4, max_len); - if (best_len >= nice_len) - goto out; - cur_node4 = mf->next_tab[cur_node4 & (MATCHFINDER_WINDOW_SIZE - 1)]; - if (cur_node4 <= cutoff || !--depth_remaining) - goto out; - } else { - if (cur_node4 <= cutoff || best_len >= nice_len) - goto out; - } - - /* Check for matches of length >= 5. */ - - for (;;) { - for (;;) { - matchptr = &in_base[cur_node4]; - - /* Already found a length 4 match. Try for a longer - * match; start by checking either the last 4 bytes and - * the first 4 bytes, or the last byte. (The last byte, - * the one which would extend the match length by 1, is - * the most important.) */ - #if UNALIGNED_ACCESS_IS_FAST - if ((load_u32_unaligned(matchptr + best_len - 3) == - load_u32_unaligned(in_next + best_len - 3)) && - (load_u32_unaligned(matchptr) == - load_u32_unaligned(in_next))) - #else - if (matchptr[best_len] == in_next[best_len]) - #endif - break; - - /* Continue to the next node in the list. */ - cur_node4 = mf->next_tab[cur_node4 & (MATCHFINDER_WINDOW_SIZE - 1)]; - if (cur_node4 <= cutoff || !--depth_remaining) - goto out; - } - - #if UNALIGNED_ACCESS_IS_FAST - len = 4; - #else - len = 0; - #endif - len = lz_extend(in_next, matchptr, len, max_len); - if (len > best_len) { - /* This is the new longest match. */ - best_len = len; - best_matchptr = matchptr; - if (best_len >= nice_len) - goto out; - } - - /* Continue to the next node in the list. */ - cur_node4 = mf->next_tab[cur_node4 & (MATCHFINDER_WINDOW_SIZE - 1)]; - if (cur_node4 <= cutoff || !--depth_remaining) - goto out; - } -out: - *offset_ret = in_next - best_matchptr; - return best_len; -} - -/* - * Advance the matchfinder, but don't search for matches. - * - * @mf - * The matchfinder structure. - * @in_base_p - * Location of a pointer which points to the place in the input data the - * matchfinder currently stores positions relative to. This may be updated - * by this function. - * @in_next - * Pointer to the next position in the input buffer. - * @in_end - * Pointer to the end of the input buffer. - * @count - * The number of bytes to advance. Must be > 0. - * @next_hashes - * The precomputed hash codes for the sequence beginning at @in_next. - * These will be used and then updated with the precomputed hashcodes for - * the sequence beginning at @in_next + @count. - */ -static forceinline void -hc_matchfinder_skip_bytes(struct hc_matchfinder * const mf, - const u8 ** const in_base_p, - const u8 *in_next, - const u8 * const in_end, - const u32 count, - u32 * const next_hashes) -{ - u32 cur_pos; - u32 hash3, hash4; - u32 next_hashseq; - u32 remaining = count; - - if (unlikely(count + 5 > in_end - in_next)) - return; - - cur_pos = in_next - *in_base_p; - hash3 = next_hashes[0]; - hash4 = next_hashes[1]; - do { - if (cur_pos == MATCHFINDER_WINDOW_SIZE) { - hc_matchfinder_slide_window(mf); - *in_base_p += MATCHFINDER_WINDOW_SIZE; - cur_pos = 0; - } - mf->hash3_tab[hash3] = cur_pos; - mf->next_tab[cur_pos] = mf->hash4_tab[hash4]; - mf->hash4_tab[hash4] = cur_pos; - - next_hashseq = get_unaligned_le32(++in_next); - hash3 = lz_hash(next_hashseq & 0xFFFFFF, HC_MATCHFINDER_HASH3_ORDER); - hash4 = lz_hash(next_hashseq, HC_MATCHFINDER_HASH4_ORDER); - cur_pos++; - } while (--remaining); - - prefetchw(&mf->hash3_tab[hash3]); - prefetchw(&mf->hash4_tab[hash4]); - next_hashes[0] = hash3; - next_hashes[1] = hash4; -} - -#endif /* LIB_HC_MATCHFINDER_H */ diff --git a/dm/third_party/libdeflate/lib/ht_matchfinder.h b/dm/third_party/libdeflate/lib/ht_matchfinder.h deleted file mode 100644 index 6e5a187..0000000 --- a/dm/third_party/libdeflate/lib/ht_matchfinder.h +++ /dev/null @@ -1,234 +0,0 @@ -/* - * ht_matchfinder.h - Lempel-Ziv matchfinding with a hash table - * - * Copyright 2022 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * --------------------------------------------------------------------------- - * - * This is a Hash Table (ht) matchfinder. - * - * This is a variant of the Hash Chains (hc) matchfinder that is optimized for - * very fast compression. The ht_matchfinder stores the hash chains inline in - * the hash table, whereas the hc_matchfinder stores them in a separate array. - * Storing the hash chains inline is the faster method when max_search_depth - * (the maximum chain length) is very small. It is not appropriate when - * max_search_depth is larger, as then it uses too much memory. - * - * Due to its focus on speed, the ht_matchfinder doesn't support length 3 - * matches. It also doesn't allow max_search_depth to vary at runtime; it is - * fixed at build time as HT_MATCHFINDER_BUCKET_SIZE. - * - * See hc_matchfinder.h for more information. - */ - -#ifndef LIB_HT_MATCHFINDER_H -#define LIB_HT_MATCHFINDER_H - -#include "matchfinder_common.h" - -#define HT_MATCHFINDER_HASH_ORDER 15 -#define HT_MATCHFINDER_BUCKET_SIZE 2 - -#define HT_MATCHFINDER_MIN_MATCH_LEN 4 -/* Minimum value of max_len for ht_matchfinder_longest_match() */ -#define HT_MATCHFINDER_REQUIRED_NBYTES 5 - -struct MATCHFINDER_ALIGNED ht_matchfinder { - mf_pos_t hash_tab[1UL << HT_MATCHFINDER_HASH_ORDER] - [HT_MATCHFINDER_BUCKET_SIZE]; -}; - -static forceinline void -ht_matchfinder_init(struct ht_matchfinder *mf) -{ - STATIC_ASSERT(sizeof(*mf) % MATCHFINDER_SIZE_ALIGNMENT == 0); - - matchfinder_init((mf_pos_t *)mf, sizeof(*mf)); -} - -static forceinline void -ht_matchfinder_slide_window(struct ht_matchfinder *mf) -{ - matchfinder_rebase((mf_pos_t *)mf, sizeof(*mf)); -} - -/* Note: max_len must be >= HT_MATCHFINDER_REQUIRED_NBYTES */ -static forceinline u32 -ht_matchfinder_longest_match(struct ht_matchfinder * const mf, - const u8 ** const in_base_p, - const u8 * const in_next, - const u32 max_len, - const u32 nice_len, - u32 * const next_hash, - u32 * const offset_ret) -{ - u32 best_len = 0; - const u8 *best_matchptr = in_next; - u32 cur_pos = in_next - *in_base_p; - const u8 *in_base; - mf_pos_t cutoff; - u32 hash; - u32 seq; - mf_pos_t cur_node; - const u8 *matchptr; -#if HT_MATCHFINDER_BUCKET_SIZE > 1 - mf_pos_t to_insert; - u32 len; -#endif -#if HT_MATCHFINDER_BUCKET_SIZE > 2 - int i; -#endif - - /* This is assumed throughout this function. */ - STATIC_ASSERT(HT_MATCHFINDER_MIN_MATCH_LEN == 4); - - if (cur_pos == MATCHFINDER_WINDOW_SIZE) { - ht_matchfinder_slide_window(mf); - *in_base_p += MATCHFINDER_WINDOW_SIZE; - cur_pos = 0; - } - in_base = *in_base_p; - cutoff = cur_pos - MATCHFINDER_WINDOW_SIZE; - - hash = *next_hash; - STATIC_ASSERT(HT_MATCHFINDER_REQUIRED_NBYTES == 5); - *next_hash = lz_hash(get_unaligned_le32(in_next + 1), - HT_MATCHFINDER_HASH_ORDER); - seq = load_u32_unaligned(in_next); - prefetchw(&mf->hash_tab[*next_hash]); -#if HT_MATCHFINDER_BUCKET_SIZE == 1 - /* Hand-unrolled version for BUCKET_SIZE == 1 */ - cur_node = mf->hash_tab[hash][0]; - mf->hash_tab[hash][0] = cur_pos; - if (cur_node <= cutoff) - goto out; - matchptr = &in_base[cur_node]; - if (load_u32_unaligned(matchptr) == seq) { - best_len = lz_extend(in_next, matchptr, 4, max_len); - best_matchptr = matchptr; - } -#elif HT_MATCHFINDER_BUCKET_SIZE == 2 - /* - * Hand-unrolled version for BUCKET_SIZE == 2. The logic here also - * differs slightly in that it copies the first entry to the second even - * if nice_len is reached on the first, as this can be slightly faster. - */ - cur_node = mf->hash_tab[hash][0]; - mf->hash_tab[hash][0] = cur_pos; - if (cur_node <= cutoff) - goto out; - matchptr = &in_base[cur_node]; - - to_insert = cur_node; - cur_node = mf->hash_tab[hash][1]; - mf->hash_tab[hash][1] = to_insert; - - if (load_u32_unaligned(matchptr) == seq) { - best_len = lz_extend(in_next, matchptr, 4, max_len); - best_matchptr = matchptr; - if (cur_node <= cutoff || best_len >= nice_len) - goto out; - matchptr = &in_base[cur_node]; - if (load_u32_unaligned(matchptr) == seq && - load_u32_unaligned(matchptr + best_len - 3) == - load_u32_unaligned(in_next + best_len - 3)) { - len = lz_extend(in_next, matchptr, 4, max_len); - if (len > best_len) { - best_len = len; - best_matchptr = matchptr; - } - } - } else { - if (cur_node <= cutoff) - goto out; - matchptr = &in_base[cur_node]; - if (load_u32_unaligned(matchptr) == seq) { - best_len = lz_extend(in_next, matchptr, 4, max_len); - best_matchptr = matchptr; - } - } -#else - /* Generic version for HT_MATCHFINDER_BUCKET_SIZE > 2 */ - to_insert = cur_pos; - for (i = 0; i < HT_MATCHFINDER_BUCKET_SIZE; i++) { - cur_node = mf->hash_tab[hash][i]; - mf->hash_tab[hash][i] = to_insert; - if (cur_node <= cutoff) - goto out; - matchptr = &in_base[cur_node]; - if (load_u32_unaligned(matchptr) == seq) { - len = lz_extend(in_next, matchptr, 4, max_len); - if (len > best_len) { - best_len = len; - best_matchptr = matchptr; - if (best_len >= nice_len) - goto out; - } - } - to_insert = cur_node; - } -#endif -out: - *offset_ret = in_next - best_matchptr; - return best_len; -} - -static forceinline void -ht_matchfinder_skip_bytes(struct ht_matchfinder * const mf, - const u8 ** const in_base_p, - const u8 *in_next, - const u8 * const in_end, - const u32 count, - u32 * const next_hash) -{ - s32 cur_pos = in_next - *in_base_p; - u32 hash; - u32 remaining = count; - int i; - - if (unlikely(count + HT_MATCHFINDER_REQUIRED_NBYTES > in_end - in_next)) - return; - - if (cur_pos + count - 1 >= MATCHFINDER_WINDOW_SIZE) { - ht_matchfinder_slide_window(mf); - *in_base_p += MATCHFINDER_WINDOW_SIZE; - cur_pos -= MATCHFINDER_WINDOW_SIZE; - } - - hash = *next_hash; - do { - for (i = HT_MATCHFINDER_BUCKET_SIZE - 1; i > 0; i--) - mf->hash_tab[hash][i] = mf->hash_tab[hash][i - 1]; - mf->hash_tab[hash][0] = cur_pos; - - hash = lz_hash(get_unaligned_le32(++in_next), - HT_MATCHFINDER_HASH_ORDER); - cur_pos++; - } while (--remaining); - - prefetchw(&mf->hash_tab[hash]); - *next_hash = hash; -} - -#endif /* LIB_HT_MATCHFINDER_H */ diff --git a/dm/third_party/libdeflate/lib/lib_common.h b/dm/third_party/libdeflate/lib/lib_common.h deleted file mode 100644 index faedfcb..0000000 --- a/dm/third_party/libdeflate/lib/lib_common.h +++ /dev/null @@ -1,106 +0,0 @@ -/* - * lib_common.h - internal header included by all library code - */ - -#ifndef LIB_LIB_COMMON_H -#define LIB_LIB_COMMON_H - -#ifdef LIBDEFLATE_H - /* - * When building the library, LIBDEFLATEAPI needs to be defined properly before - * including libdeflate.h. - */ -# error "lib_common.h must always be included before libdeflate.h" -#endif - -#if defined(LIBDEFLATE_DLL) && (defined(_WIN32) || defined(__CYGWIN__)) -# define LIBDEFLATE_EXPORT_SYM __declspec(dllexport) -#elif defined(__GNUC__) -# define LIBDEFLATE_EXPORT_SYM __attribute__((visibility("default"))) -#else -# define LIBDEFLATE_EXPORT_SYM -#endif - -/* - * On i386, gcc assumes that the stack is 16-byte aligned at function entry. - * However, some compilers (e.g. MSVC) and programming languages (e.g. Delphi) - * only guarantee 4-byte alignment when calling functions. This is mainly an - * issue on Windows, but it has been seen on Linux too. Work around this ABI - * incompatibility by realigning the stack pointer when entering libdeflate. - * This prevents crashes in SSE/AVX code. - */ -#if defined(__GNUC__) && defined(__i386__) -# define LIBDEFLATE_ALIGN_STACK __attribute__((force_align_arg_pointer)) -#else -# define LIBDEFLATE_ALIGN_STACK -#endif - -#define LIBDEFLATEAPI LIBDEFLATE_EXPORT_SYM LIBDEFLATE_ALIGN_STACK - -#include "../common_defs.h" - -typedef void *(*malloc_func_t)(size_t); -typedef void (*free_func_t)(void *); - -extern malloc_func_t libdeflate_default_malloc_func; -extern free_func_t libdeflate_default_free_func; - -void *libdeflate_aligned_malloc(malloc_func_t malloc_func, - size_t alignment, size_t size); -void libdeflate_aligned_free(free_func_t free_func, void *ptr); - -#ifdef FREESTANDING -/* - * With -ffreestanding, <string.h> may be missing, and we must provide - * implementations of memset(), memcpy(), memmove(), and memcmp(). - * See https://gcc.gnu.org/onlinedocs/gcc/Standards.html - * - * Also, -ffreestanding disables interpreting calls to these functions as - * built-ins. E.g., calling memcpy(&v, p, WORDBYTES) will make a function call, - * not be optimized to a single load instruction. For performance reasons we - * don't want that. So, declare these functions as macros that expand to the - * corresponding built-ins. This approach is recommended in the gcc man page. - * We still need the actual function definitions in case gcc calls them. - */ -void *memset(void *s, int c, size_t n); -#define memset(s, c, n) __builtin_memset((s), (c), (n)) - -void *memcpy(void *dest, const void *src, size_t n); -#define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n)) - -void *memmove(void *dest, const void *src, size_t n); -#define memmove(dest, src, n) __builtin_memmove((dest), (src), (n)) - -int memcmp(const void *s1, const void *s2, size_t n); -#define memcmp(s1, s2, n) __builtin_memcmp((s1), (s2), (n)) - -#undef LIBDEFLATE_ENABLE_ASSERTIONS -#else -# include <string.h> - /* - * To prevent false positive static analyzer warnings, ensure that assertions - * are visible to the static analyzer. - */ -# ifdef __clang_analyzer__ -# define LIBDEFLATE_ENABLE_ASSERTIONS -# endif -#endif - -/* - * Runtime assertion support. Don't enable this in production builds; it may - * hurt performance significantly. - */ -#ifdef LIBDEFLATE_ENABLE_ASSERTIONS -NORETURN void -libdeflate_assertion_failed(const char *expr, const char *file, int line); -#define ASSERT(expr) { if (unlikely(!(expr))) \ - libdeflate_assertion_failed(#expr, __FILE__, __LINE__); } -#else -#define ASSERT(expr) (void)(expr) -#endif - -#define CONCAT_IMPL(a, b) a##b -#define CONCAT(a, b) CONCAT_IMPL(a, b) -#define ADD_SUFFIX(name) CONCAT(name, SUFFIX) - -#endif /* LIB_LIB_COMMON_H */ diff --git a/dm/third_party/libdeflate/lib/matchfinder_common.h b/dm/third_party/libdeflate/lib/matchfinder_common.h deleted file mode 100644 index 98e4bc7..0000000 --- a/dm/third_party/libdeflate/lib/matchfinder_common.h +++ /dev/null @@ -1,224 +0,0 @@ -/* - * matchfinder_common.h - common code for Lempel-Ziv matchfinding - */ - -#ifndef LIB_MATCHFINDER_COMMON_H -#define LIB_MATCHFINDER_COMMON_H - -#include "lib_common.h" - -#ifndef MATCHFINDER_WINDOW_ORDER -# error "MATCHFINDER_WINDOW_ORDER must be defined!" -#endif - -/* - * Given a 32-bit value that was loaded with the platform's native endianness, - * return a 32-bit value whose high-order 8 bits are 0 and whose low-order 24 - * bits contain the first 3 bytes, arranged in octets in a platform-dependent - * order, at the memory location from which the input 32-bit value was loaded. - */ -static forceinline u32 -loaded_u32_to_u24(u32 v) -{ - if (CPU_IS_LITTLE_ENDIAN()) - return v & 0xFFFFFF; - else - return v >> 8; -} - -/* - * Load the next 3 bytes from @p into the 24 low-order bits of a 32-bit value. - * The order in which the 3 bytes will be arranged as octets in the 24 bits is - * platform-dependent. At least 4 bytes (not 3) must be available at @p. - */ -static forceinline u32 -load_u24_unaligned(const u8 *p) -{ -#if UNALIGNED_ACCESS_IS_FAST - return loaded_u32_to_u24(load_u32_unaligned(p)); -#else - if (CPU_IS_LITTLE_ENDIAN()) - return ((u32)p[0] << 0) | ((u32)p[1] << 8) | ((u32)p[2] << 16); - else - return ((u32)p[2] << 0) | ((u32)p[1] << 8) | ((u32)p[0] << 16); -#endif -} - -#define MATCHFINDER_WINDOW_SIZE (1UL << MATCHFINDER_WINDOW_ORDER) - -typedef s16 mf_pos_t; - -#define MATCHFINDER_INITVAL ((mf_pos_t)-MATCHFINDER_WINDOW_SIZE) - -/* - * This is the memory address alignment, in bytes, required for the matchfinder - * buffers by the architecture-specific implementations of matchfinder_init() - * and matchfinder_rebase(). "Matchfinder buffer" means an entire struct - * hc_matchfinder, bt_matchfinder, or ht_matchfinder; the next_tab field of - * struct hc_matchfinder; or the child_tab field of struct bt_matchfinder. - * - * This affects how the entire 'struct deflate_compressor' is allocated, since - * the matchfinder structures are embedded inside it. - * - * Currently the maximum memory address alignment required is 32 bytes, needed - * by the AVX-2 matchfinder functions. - */ -#define MATCHFINDER_MEM_ALIGNMENT 32 - -/* - * This declares a size, in bytes, that is guaranteed to divide the sizes of the - * matchfinder buffers (where "matchfinder buffers" is as defined for - * MATCHFINDER_MEM_ALIGNMENT). The architecture-specific implementations of - * matchfinder_init() and matchfinder_rebase() take advantage of this value. - * - * Currently the maximum size alignment required is 128 bytes, needed by - * the AVX-2 matchfinder functions. However, the RISC-V Vector Extension - * matchfinder functions can, in principle, take advantage of a larger size - * alignment. Therefore, we set this to 1024, which still easily divides the - * actual sizes that result from the current matchfinder struct definitions. - * This value can safely be changed to any power of two that is >= 128. - */ -#define MATCHFINDER_SIZE_ALIGNMENT 1024 - -#undef matchfinder_init -#undef matchfinder_rebase -#ifdef _aligned_attribute -# define MATCHFINDER_ALIGNED _aligned_attribute(MATCHFINDER_MEM_ALIGNMENT) -# if defined(ARCH_ARM32) || defined(ARCH_ARM64) -# include "arm/matchfinder_impl.h" -# elif defined(ARCH_RISCV) -# include "riscv/matchfinder_impl.h" -# elif defined(ARCH_X86_32) || defined(ARCH_X86_64) -# include "x86/matchfinder_impl.h" -# endif -#else -# define MATCHFINDER_ALIGNED -#endif - -/* - * Initialize the hash table portion of the matchfinder. - * - * Essentially, this is an optimized memset(). - * - * 'data' must be aligned to a MATCHFINDER_MEM_ALIGNMENT boundary, and - * 'size' must be a multiple of MATCHFINDER_SIZE_ALIGNMENT. - */ -#ifndef matchfinder_init -static forceinline void -matchfinder_init(mf_pos_t *data, size_t size) -{ - size_t num_entries = size / sizeof(*data); - size_t i; - - for (i = 0; i < num_entries; i++) - data[i] = MATCHFINDER_INITVAL; -} -#endif - -/* - * Slide the matchfinder by MATCHFINDER_WINDOW_SIZE bytes. - * - * This must be called just after each MATCHFINDER_WINDOW_SIZE bytes have been - * run through the matchfinder. - * - * This subtracts MATCHFINDER_WINDOW_SIZE bytes from each entry in the given - * array, making the entries be relative to the current position rather than the - * position MATCHFINDER_WINDOW_SIZE bytes prior. To avoid integer underflows, - * entries that would become less than -MATCHFINDER_WINDOW_SIZE stay at - * -MATCHFINDER_WINDOW_SIZE, keeping them permanently out of bounds. - * - * The given array must contain all matchfinder data that is position-relative: - * the hash table(s) as well as any hash chain or binary tree links. Its - * address must be aligned to a MATCHFINDER_MEM_ALIGNMENT boundary, and its size - * must be a multiple of MATCHFINDER_SIZE_ALIGNMENT. - */ -#ifndef matchfinder_rebase -static forceinline void -matchfinder_rebase(mf_pos_t *data, size_t size) -{ - size_t num_entries = size / sizeof(*data); - size_t i; - - if (MATCHFINDER_WINDOW_SIZE == 32768) { - /* - * Branchless version for 32768-byte windows. Clear all bits if - * the value was already negative, then set the sign bit. This - * is equivalent to subtracting 32768 with signed saturation. - */ - for (i = 0; i < num_entries; i++) - data[i] = 0x8000 | (data[i] & ~(data[i] >> 15)); - } else { - for (i = 0; i < num_entries; i++) { - if (data[i] >= 0) - data[i] -= (mf_pos_t)-MATCHFINDER_WINDOW_SIZE; - else - data[i] = (mf_pos_t)-MATCHFINDER_WINDOW_SIZE; - } - } -} -#endif - -/* - * The hash function: given a sequence prefix held in the low-order bits of a - * 32-bit value, multiply by a carefully-chosen large constant. Discard any - * bits of the product that don't fit in a 32-bit value, but take the - * next-highest @num_bits bits of the product as the hash value, as those have - * the most randomness. - */ -static forceinline u32 -lz_hash(u32 seq, unsigned num_bits) -{ - return (u32)(seq * 0x1E35A7BD) >> (32 - num_bits); -} - -/* - * Return the number of bytes at @matchptr that match the bytes at @strptr, up - * to a maximum of @max_len. Initially, @start_len bytes are matched. - */ -static forceinline u32 -lz_extend(const u8 * const strptr, const u8 * const matchptr, - const u32 start_len, const u32 max_len) -{ - u32 len = start_len; - machine_word_t v_word; - - if (UNALIGNED_ACCESS_IS_FAST) { - - if (likely(max_len - len >= 4 * WORDBYTES)) { - - #define COMPARE_WORD_STEP \ - v_word = load_word_unaligned(&matchptr[len]) ^ \ - load_word_unaligned(&strptr[len]); \ - if (v_word != 0) \ - goto word_differs; \ - len += WORDBYTES; \ - - COMPARE_WORD_STEP - COMPARE_WORD_STEP - COMPARE_WORD_STEP - COMPARE_WORD_STEP - #undef COMPARE_WORD_STEP - } - - while (len + WORDBYTES <= max_len) { - v_word = load_word_unaligned(&matchptr[len]) ^ - load_word_unaligned(&strptr[len]); - if (v_word != 0) - goto word_differs; - len += WORDBYTES; - } - } - - while (len < max_len && matchptr[len] == strptr[len]) - len++; - return len; - -word_differs: - if (CPU_IS_LITTLE_ENDIAN()) - len += (bsfw(v_word) >> 3); - else - len += (WORDBITS - 1 - bsrw(v_word)) >> 3; - return len; -} - -#endif /* LIB_MATCHFINDER_COMMON_H */ diff --git a/dm/third_party/libdeflate/lib/riscv/matchfinder_impl.h b/dm/third_party/libdeflate/lib/riscv/matchfinder_impl.h deleted file mode 100644 index 6d8bf79..0000000 --- a/dm/third_party/libdeflate/lib/riscv/matchfinder_impl.h +++ /dev/null @@ -1,97 +0,0 @@ -/* - * riscv/matchfinder_impl.h - RISC-V implementations of matchfinder functions - * - * Copyright 2024 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_RISCV_MATCHFINDER_IMPL_H -#define LIB_RISCV_MATCHFINDER_IMPL_H - -#if defined(ARCH_RISCV) && defined(__riscv_vector) -#include <riscv_vector.h> - -/* - * Return the maximum number of 16-bit (mf_pos_t) elements that fit in 8 RISC-V - * vector registers and also evenly divide the sizes of the matchfinder buffers. - */ -static forceinline size_t -riscv_matchfinder_vl(void) -{ - const size_t vl = __riscv_vsetvlmax_e16m8(); - - STATIC_ASSERT(sizeof(mf_pos_t) == sizeof(s16)); - /* - * MATCHFINDER_SIZE_ALIGNMENT is a power of 2, as is 'vl' because the - * RISC-V Vector Extension requires that the vector register length - * (VLEN) be a power of 2. Thus, a simple MIN() gives the correct - * answer here; rounding to a power of 2 is not required. - */ - STATIC_ASSERT((MATCHFINDER_SIZE_ALIGNMENT & - (MATCHFINDER_SIZE_ALIGNMENT - 1)) == 0); - ASSERT((vl & (vl - 1)) == 0); - return MIN(vl, MATCHFINDER_SIZE_ALIGNMENT / sizeof(mf_pos_t)); -} - -/* matchfinder_init() optimized using the RISC-V Vector Extension */ -static forceinline void -matchfinder_init_rvv(mf_pos_t *p, size_t size) -{ - const size_t vl = riscv_matchfinder_vl(); - const vint16m8_t v = __riscv_vmv_v_x_i16m8(MATCHFINDER_INITVAL, vl); - - ASSERT(size > 0 && size % (vl * sizeof(p[0])) == 0); - do { - __riscv_vse16_v_i16m8(p, v, vl); - p += vl; - size -= vl * sizeof(p[0]); - } while (size != 0); -} -#define matchfinder_init matchfinder_init_rvv - -/* matchfinder_rebase() optimized using the RISC-V Vector Extension */ -static forceinline void -matchfinder_rebase_rvv(mf_pos_t *p, size_t size) -{ - const size_t vl = riscv_matchfinder_vl(); - - ASSERT(size > 0 && size % (vl * sizeof(p[0])) == 0); - do { - vint16m8_t v = __riscv_vle16_v_i16m8(p, vl); - - /* - * This should generate the vsadd.vx instruction - * (Vector Saturating Add, integer vector-scalar) - */ - v = __riscv_vsadd_vx_i16m8(v, (s16)-MATCHFINDER_WINDOW_SIZE, - vl); - __riscv_vse16_v_i16m8(p, v, vl); - p += vl; - size -= vl * sizeof(p[0]); - } while (size != 0); -} -#define matchfinder_rebase matchfinder_rebase_rvv - -#endif /* ARCH_RISCV && __riscv_vector */ - -#endif /* LIB_RISCV_MATCHFINDER_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/utils.c b/dm/third_party/libdeflate/lib/utils.c deleted file mode 100644 index 5bb34ed..0000000 --- a/dm/third_party/libdeflate/lib/utils.c +++ /dev/null @@ -1,141 +0,0 @@ -/* - * utils.c - utility functions for libdeflate - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "lib_common.h" - -#ifdef FREESTANDING -# define malloc NULL -# define free NULL -#else -# include <stdlib.h> -#endif - -malloc_func_t libdeflate_default_malloc_func = malloc; -free_func_t libdeflate_default_free_func = free; - -void * -libdeflate_aligned_malloc(malloc_func_t malloc_func, - size_t alignment, size_t size) -{ - void *ptr = (*malloc_func)(sizeof(void *) + alignment - 1 + size); - - if (ptr) { - void *orig_ptr = ptr; - - ptr = (void *)ALIGN((uintptr_t)ptr + sizeof(void *), alignment); - ((void **)ptr)[-1] = orig_ptr; - } - return ptr; -} - -void -libdeflate_aligned_free(free_func_t free_func, void *ptr) -{ - (*free_func)(((void **)ptr)[-1]); -} - -LIBDEFLATEAPI void -libdeflate_set_memory_allocator(malloc_func_t malloc_func, - free_func_t free_func) -{ - libdeflate_default_malloc_func = malloc_func; - libdeflate_default_free_func = free_func; -} - -/* - * Implementations of libc functions for freestanding library builds. - * Normal library builds don't use these. Not optimized yet; usually the - * compiler expands these functions and doesn't actually call them anyway. - */ -#ifdef FREESTANDING -#undef memset -void * __attribute__((weak)) -memset(void *s, int c, size_t n) -{ - u8 *p = s; - size_t i; - - for (i = 0; i < n; i++) - p[i] = c; - return s; -} - -#undef memcpy -void * __attribute__((weak)) -memcpy(void *dest, const void *src, size_t n) -{ - u8 *d = dest; - const u8 *s = src; - size_t i; - - for (i = 0; i < n; i++) - d[i] = s[i]; - return dest; -} - -#undef memmove -void * __attribute__((weak)) -memmove(void *dest, const void *src, size_t n) -{ - u8 *d = dest; - const u8 *s = src; - size_t i; - - if (d <= s) - return memcpy(d, s, n); - - for (i = n; i > 0; i--) - d[i - 1] = s[i - 1]; - return dest; -} - -#undef memcmp -int __attribute__((weak)) -memcmp(const void *s1, const void *s2, size_t n) -{ - const u8 *p1 = s1; - const u8 *p2 = s2; - size_t i; - - for (i = 0; i < n; i++) { - if (p1[i] != p2[i]) - return (int)p1[i] - (int)p2[i]; - } - return 0; -} -#endif /* FREESTANDING */ - -#ifdef LIBDEFLATE_ENABLE_ASSERTIONS -#include <stdio.h> -#include <stdlib.h> -NORETURN void -libdeflate_assertion_failed(const char *expr, const char *file, int line) -{ - fprintf(stderr, "Assertion failed: %s at %s:%d\n", expr, file, line); - abort(); -} -#endif /* LIBDEFLATE_ENABLE_ASSERTIONS */ diff --git a/dm/third_party/libdeflate/lib/x86/adler32_impl.h b/dm/third_party/libdeflate/lib/x86/adler32_impl.h deleted file mode 100644 index f1102b5..0000000 --- a/dm/third_party/libdeflate/lib/x86/adler32_impl.h +++ /dev/null @@ -1,136 +0,0 @@ -/* - * x86/adler32_impl.h - x86 implementations of Adler-32 checksum algorithm - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_X86_ADLER32_IMPL_H -#define LIB_X86_ADLER32_IMPL_H - -#include "cpu_features.h" - -/* SSE2 and AVX2 implementations. Used on older CPUs. */ -#if defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER) -# define adler32_x86_sse2 adler32_x86_sse2 -# define SUFFIX _sse2 -# define ATTRIBUTES _target_attribute("sse2") -# define VL 16 -# define USE_VNNI 0 -# define USE_AVX512 0 -# include "adler32_template.h" - -# define adler32_x86_avx2 adler32_x86_avx2 -# define SUFFIX _avx2 -# define ATTRIBUTES _target_attribute("avx2") -# define VL 32 -# define USE_VNNI 0 -# define USE_AVX512 0 -# include "adler32_template.h" -#endif - -/* - * AVX-VNNI implementation. This is used on CPUs that have AVX2 and AVX-VNNI - * but don't have AVX-512, for example Intel Alder Lake. - * - * Unusually for a new CPU feature, gcc added support for the AVX-VNNI - * intrinsics (in gcc 11.1) slightly before binutils added support for - * assembling AVX-VNNI instructions (in binutils 2.36). Distros can reasonably - * have gcc 11 with binutils 2.35. Because of this issue, we check for gcc 12 - * instead of gcc 11. (libdeflate supports direct compilation without a - * configure step, so checking the binutils version is not always an option.) - */ -#if (GCC_PREREQ(12, 1) || CLANG_PREREQ(12, 0, 13000000) || MSVC_PREREQ(1930)) && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX_VNNI) -# define adler32_x86_avx2_vnni adler32_x86_avx2_vnni -# define SUFFIX _avx2_vnni -# define ATTRIBUTES _target_attribute("avx2,avxvnni") -# define VL 32 -# define USE_VNNI 1 -# define USE_AVX512 0 -# include "adler32_template.h" -#endif - -#if (GCC_PREREQ(8, 1) || CLANG_PREREQ(6, 0, 10000000) || MSVC_PREREQ(1920)) && \ - !(CLANG_PREREQ(18, 0, 18000000) && !CLANG_PREREQ(19, 0, 19000000)) && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_AVX512VNNI) -/* - * AVX512VNNI implementation using 256-bit vectors. This is very similar to the - * AVX-VNNI implementation but takes advantage of masking and more registers. - * This is used on certain older Intel CPUs, specifically Ice Lake and Tiger - * Lake, which support AVX512VNNI but downclock a bit too eagerly when ZMM - * registers are used. - */ -# define adler32_x86_avx512_vl256_vnni adler32_x86_avx512_vl256_vnni -# define SUFFIX _avx512_vl256_vnni -# define ATTRIBUTES _target_attribute("avx512bw,avx512vl,avx512vnni") -# define VL 32 -# define USE_VNNI 1 -# define USE_AVX512 1 -# include "adler32_template.h" - -/* - * AVX512VNNI implementation using 512-bit vectors. This is used on CPUs that - * have a good AVX-512 implementation including AVX512VNNI. - */ -# define adler32_x86_avx512_vl512_vnni adler32_x86_avx512_vl512_vnni -# define SUFFIX _avx512_vl512_vnni -# define ATTRIBUTES _target_attribute("avx512bw,avx512vnni") -# define VL 64 -# define USE_VNNI 1 -# define USE_AVX512 1 -# include "adler32_template.h" -#endif - -static inline adler32_func_t -arch_select_adler32_func(void) -{ - const u32 features MAYBE_UNUSED = get_x86_cpu_features(); - -#ifdef adler32_x86_avx512_vl512_vnni - if ((features & X86_CPU_FEATURE_ZMM) && - HAVE_AVX512BW(features) && HAVE_AVX512VNNI(features)) - return adler32_x86_avx512_vl512_vnni; -#endif -#ifdef adler32_x86_avx512_vl256_vnni - if (HAVE_AVX512BW(features) && HAVE_AVX512VL(features) && - HAVE_AVX512VNNI(features)) - return adler32_x86_avx512_vl256_vnni; -#endif -#ifdef adler32_x86_avx2_vnni - if (HAVE_AVX2(features) && HAVE_AVXVNNI(features)) - return adler32_x86_avx2_vnni; -#endif -#ifdef adler32_x86_avx2 - if (HAVE_AVX2(features)) - return adler32_x86_avx2; -#endif -#ifdef adler32_x86_sse2 - if (HAVE_SSE2(features)) - return adler32_x86_sse2; -#endif - return NULL; -} -#define arch_select_adler32_func arch_select_adler32_func - -#endif /* LIB_X86_ADLER32_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/x86/adler32_template.h b/dm/third_party/libdeflate/lib/x86/adler32_template.h deleted file mode 100644 index 1593ee5..0000000 --- a/dm/third_party/libdeflate/lib/x86/adler32_template.h +++ /dev/null @@ -1,518 +0,0 @@ -/* - * x86/adler32_template.h - template for vectorized Adler-32 implementations - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * This file is a "template" for instantiating Adler-32 functions for x86. - * The "parameters" are: - * - * SUFFIX: - * Name suffix to append to all instantiated functions. - * ATTRIBUTES: - * Target function attributes to use. Must satisfy the dependencies of the - * other parameters as follows: - * VL=16 && USE_VNNI=0 && USE_AVX512=0: at least sse2 - * VL=32 && USE_VNNI=0 && USE_AVX512=0: at least avx2 - * VL=32 && USE_VNNI=1 && USE_AVX512=0: at least avx2,avxvnni - * VL=32 && USE_VNNI=1 && USE_AVX512=1: at least avx512bw,avx512vl,avx512vnni - * VL=64 && USE_VNNI=1 && USE_AVX512=1: at least avx512bw,avx512vnni - * (Other combinations are not useful and have not been tested.) - * VL: - * Vector length in bytes. Must be 16, 32, or 64. - * USE_VNNI: - * If 1, use the VNNI dot product based algorithm. - * If 0, use the legacy SSE2 and AVX2 compatible algorithm. - * USE_AVX512: - * If 1, take advantage of AVX-512 features such as masking. This doesn't - * enable the use of 512-bit vectors; the vector length is controlled by - * VL. If 0, assume that the CPU might not support AVX-512. - */ - -#if VL == 16 -# define vec_t __m128i -# define mask_t u16 -# define LOG2_VL 4 -# define VADD8(a, b) _mm_add_epi8((a), (b)) -# define VADD16(a, b) _mm_add_epi16((a), (b)) -# define VADD32(a, b) _mm_add_epi32((a), (b)) -# if USE_AVX512 -# define VDPBUSD(a, b, c) _mm_dpbusd_epi32((a), (b), (c)) -# else -# define VDPBUSD(a, b, c) _mm_dpbusd_avx_epi32((a), (b), (c)) -# endif -# define VLOAD(p) _mm_load_si128((const void *)(p)) -# define VLOADU(p) _mm_loadu_si128((const void *)(p)) -# define VMADD16(a, b) _mm_madd_epi16((a), (b)) -# define VMASKZ_LOADU(mask, p) _mm_maskz_loadu_epi8((mask), (p)) -# define VMULLO32(a, b) _mm_mullo_epi32((a), (b)) -# define VSAD8(a, b) _mm_sad_epu8((a), (b)) -# define VSET1_8(a) _mm_set1_epi8(a) -# define VSET1_32(a) _mm_set1_epi32(a) -# define VSETZERO() _mm_setzero_si128() -# define VSLL32(a, b) _mm_slli_epi32((a), (b)) -# define VUNPACKLO8(a, b) _mm_unpacklo_epi8((a), (b)) -# define VUNPACKHI8(a, b) _mm_unpackhi_epi8((a), (b)) -#elif VL == 32 -# define vec_t __m256i -# define mask_t u32 -# define LOG2_VL 5 -# define VADD8(a, b) _mm256_add_epi8((a), (b)) -# define VADD16(a, b) _mm256_add_epi16((a), (b)) -# define VADD32(a, b) _mm256_add_epi32((a), (b)) -# if USE_AVX512 -# define VDPBUSD(a, b, c) _mm256_dpbusd_epi32((a), (b), (c)) -# else -# define VDPBUSD(a, b, c) _mm256_dpbusd_avx_epi32((a), (b), (c)) -# endif -# define VLOAD(p) _mm256_load_si256((const void *)(p)) -# define VLOADU(p) _mm256_loadu_si256((const void *)(p)) -# define VMADD16(a, b) _mm256_madd_epi16((a), (b)) -# define VMASKZ_LOADU(mask, p) _mm256_maskz_loadu_epi8((mask), (p)) -# define VMULLO32(a, b) _mm256_mullo_epi32((a), (b)) -# define VSAD8(a, b) _mm256_sad_epu8((a), (b)) -# define VSET1_8(a) _mm256_set1_epi8(a) -# define VSET1_32(a) _mm256_set1_epi32(a) -# define VSETZERO() _mm256_setzero_si256() -# define VSLL32(a, b) _mm256_slli_epi32((a), (b)) -# define VUNPACKLO8(a, b) _mm256_unpacklo_epi8((a), (b)) -# define VUNPACKHI8(a, b) _mm256_unpackhi_epi8((a), (b)) -#elif VL == 64 -# define vec_t __m512i -# define mask_t u64 -# define LOG2_VL 6 -# define VADD8(a, b) _mm512_add_epi8((a), (b)) -# define VADD16(a, b) _mm512_add_epi16((a), (b)) -# define VADD32(a, b) _mm512_add_epi32((a), (b)) -# define VDPBUSD(a, b, c) _mm512_dpbusd_epi32((a), (b), (c)) -# define VLOAD(p) _mm512_load_si512((const void *)(p)) -# define VLOADU(p) _mm512_loadu_si512((const void *)(p)) -# define VMADD16(a, b) _mm512_madd_epi16((a), (b)) -# define VMASKZ_LOADU(mask, p) _mm512_maskz_loadu_epi8((mask), (p)) -# define VMULLO32(a, b) _mm512_mullo_epi32((a), (b)) -# define VSAD8(a, b) _mm512_sad_epu8((a), (b)) -# define VSET1_8(a) _mm512_set1_epi8(a) -# define VSET1_32(a) _mm512_set1_epi32(a) -# define VSETZERO() _mm512_setzero_si512() -# define VSLL32(a, b) _mm512_slli_epi32((a), (b)) -# define VUNPACKLO8(a, b) _mm512_unpacklo_epi8((a), (b)) -# define VUNPACKHI8(a, b) _mm512_unpackhi_epi8((a), (b)) -#else -# error "unsupported vector length" -#endif - -#define VADD32_3X(a, b, c) VADD32(VADD32((a), (b)), (c)) -#define VADD32_4X(a, b, c, d) VADD32(VADD32((a), (b)), VADD32((c), (d))) -#define VADD32_5X(a, b, c, d, e) VADD32((a), VADD32_4X((b), (c), (d), (e))) -#define VADD32_7X(a, b, c, d, e, f, g) \ - VADD32(VADD32_3X((a), (b), (c)), VADD32_4X((d), (e), (f), (g))) - -/* Sum the 32-bit elements of v_s1 and add them to s1, and likewise for s2. */ -#undef reduce_to_32bits -static forceinline ATTRIBUTES void -ADD_SUFFIX(reduce_to_32bits)(vec_t v_s1, vec_t v_s2, u32 *s1_p, u32 *s2_p) -{ - __m128i v_s1_128, v_s2_128; -#if VL == 16 - { - v_s1_128 = v_s1; - v_s2_128 = v_s2; - } -#else - { - __m256i v_s1_256, v_s2_256; - #if VL == 32 - v_s1_256 = v_s1; - v_s2_256 = v_s2; - #else - /* Reduce 512 bits to 256 bits. */ - v_s1_256 = _mm256_add_epi32(_mm512_extracti64x4_epi64(v_s1, 0), - _mm512_extracti64x4_epi64(v_s1, 1)); - v_s2_256 = _mm256_add_epi32(_mm512_extracti64x4_epi64(v_s2, 0), - _mm512_extracti64x4_epi64(v_s2, 1)); - #endif - /* Reduce 256 bits to 128 bits. */ - v_s1_128 = _mm_add_epi32(_mm256_extracti128_si256(v_s1_256, 0), - _mm256_extracti128_si256(v_s1_256, 1)); - v_s2_128 = _mm_add_epi32(_mm256_extracti128_si256(v_s2_256, 0), - _mm256_extracti128_si256(v_s2_256, 1)); - } -#endif - - /* - * Reduce 128 bits to 32 bits. - * - * If the bytes were summed into v_s1 using psadbw + paddd, then ignore - * the odd-indexed elements of v_s1_128 since they are zero. - */ -#if USE_VNNI - v_s1_128 = _mm_add_epi32(v_s1_128, _mm_shuffle_epi32(v_s1_128, 0x31)); -#endif - v_s2_128 = _mm_add_epi32(v_s2_128, _mm_shuffle_epi32(v_s2_128, 0x31)); - v_s1_128 = _mm_add_epi32(v_s1_128, _mm_shuffle_epi32(v_s1_128, 0x02)); - v_s2_128 = _mm_add_epi32(v_s2_128, _mm_shuffle_epi32(v_s2_128, 0x02)); - - *s1_p += (u32)_mm_cvtsi128_si32(v_s1_128); - *s2_p += (u32)_mm_cvtsi128_si32(v_s2_128); -} -#define reduce_to_32bits ADD_SUFFIX(reduce_to_32bits) - -static ATTRIBUTES u32 -ADD_SUFFIX(adler32_x86)(u32 adler, const u8 *p, size_t len) -{ -#if USE_VNNI - /* This contains the bytes [VL, VL-1, VL-2, ..., 1]. */ - static const u8 _aligned_attribute(VL) raw_mults[VL] = { - #if VL == 64 - 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, - 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, - #endif - #if VL >= 32 - 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, - #endif - 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, - }; - const vec_t ones = VSET1_8(1); -#else - /* - * This contains the 16-bit values [2*VL, 2*VL - 1, 2*VL - 2, ..., 1]. - * For VL==32 the ordering is weird because it has to match the way that - * vpunpcklbw and vpunpckhbw work on 128-bit lanes separately. - */ - static const u16 _aligned_attribute(VL) raw_mults[4][VL / 2] = { - #if VL == 16 - { 32, 31, 30, 29, 28, 27, 26, 25 }, - { 24, 23, 22, 21, 20, 19, 18, 17 }, - { 16, 15, 14, 13, 12, 11, 10, 9 }, - { 8, 7, 6, 5, 4, 3, 2, 1 }, - #elif VL == 32 - { 64, 63, 62, 61, 60, 59, 58, 57, 48, 47, 46, 45, 44, 43, 42, 41 }, - { 56, 55, 54, 53, 52, 51, 50, 49, 40, 39, 38, 37, 36, 35, 34, 33 }, - { 32, 31, 30, 29, 28, 27, 26, 25, 16, 15, 14, 13, 12, 11, 10, 9 }, - { 24, 23, 22, 21, 20, 19, 18, 17, 8, 7, 6, 5, 4, 3, 2, 1 }, - #else - # error "unsupported parameters" - #endif - }; - const vec_t mults_a = VLOAD(raw_mults[0]); - const vec_t mults_b = VLOAD(raw_mults[1]); - const vec_t mults_c = VLOAD(raw_mults[2]); - const vec_t mults_d = VLOAD(raw_mults[3]); -#endif - const vec_t zeroes = VSETZERO(); - u32 s1 = adler & 0xFFFF; - u32 s2 = adler >> 16; - - /* - * If the length is large and the pointer is misaligned, align it. - * For smaller lengths, just take the misaligned load penalty. - */ - if (unlikely(len > 65536 && ((uintptr_t)p & (VL-1)))) { - do { - s1 += *p++; - s2 += s1; - len--; - } while ((uintptr_t)p & (VL-1)); - s1 %= DIVISOR; - s2 %= DIVISOR; - } - -#if USE_VNNI - /* - * This is Adler-32 using the vpdpbusd instruction from AVX512VNNI or - * AVX-VNNI. vpdpbusd multiplies the unsigned bytes of one vector by - * the signed bytes of another vector and adds the sums in groups of 4 - * to the 32-bit elements of a third vector. We use it in two ways: - * multiplying the data bytes by a sequence like 64,63,62,...,1 for - * calculating part of s2, and multiplying the data bytes by an all-ones - * sequence 1,1,1,...,1 for calculating s1 and part of s2. The all-ones - * trick seems to be faster than the alternative of vpsadbw + vpaddd. - */ - while (len) { - /* - * Calculate the length of the next data chunk such that s1 and - * s2 are guaranteed to not exceed UINT32_MAX. - */ - size_t n = MIN(len, MAX_CHUNK_LEN & ~(4*VL - 1)); - vec_t mults = VLOAD(raw_mults); - vec_t v_s1 = zeroes; - vec_t v_s2 = zeroes; - - s2 += s1 * n; - len -= n; - - if (n >= 4*VL) { - vec_t v_s1_b = zeroes; - vec_t v_s1_c = zeroes; - vec_t v_s1_d = zeroes; - vec_t v_s2_b = zeroes; - vec_t v_s2_c = zeroes; - vec_t v_s2_d = zeroes; - vec_t v_s1_sums = zeroes; - vec_t v_s1_sums_b = zeroes; - vec_t v_s1_sums_c = zeroes; - vec_t v_s1_sums_d = zeroes; - vec_t tmp0, tmp1; - - do { - vec_t data_a = VLOADU(p + 0*VL); - vec_t data_b = VLOADU(p + 1*VL); - vec_t data_c = VLOADU(p + 2*VL); - vec_t data_d = VLOADU(p + 3*VL); - - /* - * Workaround for gcc bug where it generates - * unnecessary move instructions - * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107892) - */ - #if GCC_PREREQ(1, 0) - __asm__("" : "+v" (data_a), "+v" (data_b), - "+v" (data_c), "+v" (data_d)); - #endif - - v_s2 = VDPBUSD(v_s2, data_a, mults); - v_s2_b = VDPBUSD(v_s2_b, data_b, mults); - v_s2_c = VDPBUSD(v_s2_c, data_c, mults); - v_s2_d = VDPBUSD(v_s2_d, data_d, mults); - - v_s1_sums = VADD32(v_s1_sums, v_s1); - v_s1_sums_b = VADD32(v_s1_sums_b, v_s1_b); - v_s1_sums_c = VADD32(v_s1_sums_c, v_s1_c); - v_s1_sums_d = VADD32(v_s1_sums_d, v_s1_d); - - v_s1 = VDPBUSD(v_s1, data_a, ones); - v_s1_b = VDPBUSD(v_s1_b, data_b, ones); - v_s1_c = VDPBUSD(v_s1_c, data_c, ones); - v_s1_d = VDPBUSD(v_s1_d, data_d, ones); - - /* Same gcc bug workaround. See above */ - #if GCC_PREREQ(1, 0) && !defined(ARCH_X86_32) - __asm__("" : "+v" (v_s2), "+v" (v_s2_b), - "+v" (v_s2_c), "+v" (v_s2_d), - "+v" (v_s1_sums), - "+v" (v_s1_sums_b), - "+v" (v_s1_sums_c), - "+v" (v_s1_sums_d), - "+v" (v_s1), "+v" (v_s1_b), - "+v" (v_s1_c), "+v" (v_s1_d)); - #endif - p += 4*VL; - n -= 4*VL; - } while (n >= 4*VL); - - /* - * Reduce into v_s1 and v_s2 as follows: - * - * v_s2 = v_s2 + v_s2_b + v_s2_c + v_s2_d + - * (4*VL)*(v_s1_sums + v_s1_sums_b + - * v_s1_sums_c + v_s1_sums_d) + - * (3*VL)*v_s1 + (2*VL)*v_s1_b + VL*v_s1_c - * v_s1 = v_s1 + v_s1_b + v_s1_c + v_s1_d - */ - tmp0 = VADD32(v_s1, v_s1_b); - tmp1 = VADD32(v_s1, v_s1_c); - v_s1_sums = VADD32_4X(v_s1_sums, v_s1_sums_b, - v_s1_sums_c, v_s1_sums_d); - v_s1 = VADD32_3X(tmp0, v_s1_c, v_s1_d); - v_s2 = VADD32_7X(VSLL32(v_s1_sums, LOG2_VL + 2), - VSLL32(tmp0, LOG2_VL + 1), - VSLL32(tmp1, LOG2_VL), - v_s2, v_s2_b, v_s2_c, v_s2_d); - } - - /* Process the last 0 <= n < 4*VL bytes of the chunk. */ - if (n >= 2*VL) { - const vec_t data_a = VLOADU(p + 0*VL); - const vec_t data_b = VLOADU(p + 1*VL); - - v_s2 = VADD32(v_s2, VSLL32(v_s1, LOG2_VL + 1)); - v_s1 = VDPBUSD(v_s1, data_a, ones); - v_s1 = VDPBUSD(v_s1, data_b, ones); - v_s2 = VDPBUSD(v_s2, data_a, VSET1_8(VL)); - v_s2 = VDPBUSD(v_s2, data_a, mults); - v_s2 = VDPBUSD(v_s2, data_b, mults); - p += 2*VL; - n -= 2*VL; - } - if (n) { - /* Process the last 0 < n < 2*VL bytes of the chunk. */ - vec_t data; - - v_s2 = VADD32(v_s2, VMULLO32(v_s1, VSET1_32(n))); - - mults = VADD8(mults, VSET1_8((int)n - VL)); - if (n > VL) { - data = VLOADU(p); - v_s1 = VDPBUSD(v_s1, data, ones); - v_s2 = VDPBUSD(v_s2, data, mults); - p += VL; - n -= VL; - mults = VADD8(mults, VSET1_8(-VL)); - } - /* - * Process the last 0 < n <= VL bytes of the chunk. - * Utilize a masked load if it's available. - */ - #if USE_AVX512 - data = VMASKZ_LOADU((mask_t)-1 >> (VL - n), p); - #else - data = zeroes; - memcpy(&data, p, n); - #endif - v_s1 = VDPBUSD(v_s1, data, ones); - v_s2 = VDPBUSD(v_s2, data, mults); - p += n; - } - - reduce_to_32bits(v_s1, v_s2, &s1, &s2); - s1 %= DIVISOR; - s2 %= DIVISOR; - } -#else /* USE_VNNI */ - /* - * This is Adler-32 for SSE2 and AVX2. - * - * To horizontally sum bytes, use psadbw + paddd, where one of the - * arguments to psadbw is all-zeroes. - * - * For the s2 contribution from (2*VL - i)*data[i] for each of the 2*VL - * bytes of each iteration of the inner loop, use punpck{l,h}bw + paddw - * to sum, for each i across iterations, byte i into a corresponding - * 16-bit counter in v_byte_sums_*. After the inner loop, use pmaddwd - * to multiply each counter by (2*VL - i), then add the products to s2. - * - * An alternative implementation would use pmaddubsw and pmaddwd in the - * inner loop to do (2*VL - i)*data[i] directly and add the products in - * groups of 4 to 32-bit counters. However, on average that approach - * seems to be slower than the current approach which delays the - * multiplications. Also, pmaddubsw requires SSSE3; the current - * approach keeps the implementation aligned between SSE2 and AVX2. - * - * The inner loop processes 2*VL bytes per iteration. Increasing this - * to 4*VL doesn't seem to be helpful here. - */ - while (len) { - /* - * Calculate the length of the next data chunk such that s1 and - * s2 are guaranteed to not exceed UINT32_MAX, and every - * v_byte_sums_* counter is guaranteed to not exceed INT16_MAX. - * It's INT16_MAX, not UINT16_MAX, because v_byte_sums_* are - * used with pmaddwd which does signed multiplication. In the - * SSE2 case this limits chunks to 4096 bytes instead of 5536. - */ - size_t n = MIN(len, MIN(2 * VL * (INT16_MAX / UINT8_MAX), - MAX_CHUNK_LEN) & ~(2*VL - 1)); - len -= n; - - if (n >= 2*VL) { - vec_t v_s1 = zeroes; - vec_t v_s1_sums = zeroes; - vec_t v_byte_sums_a = zeroes; - vec_t v_byte_sums_b = zeroes; - vec_t v_byte_sums_c = zeroes; - vec_t v_byte_sums_d = zeroes; - vec_t v_s2; - - s2 += s1 * (n & ~(2*VL - 1)); - - do { - vec_t data_a = VLOADU(p + 0*VL); - vec_t data_b = VLOADU(p + 1*VL); - - v_s1_sums = VADD32(v_s1_sums, v_s1); - v_byte_sums_a = VADD16(v_byte_sums_a, - VUNPACKLO8(data_a, zeroes)); - v_byte_sums_b = VADD16(v_byte_sums_b, - VUNPACKHI8(data_a, zeroes)); - v_byte_sums_c = VADD16(v_byte_sums_c, - VUNPACKLO8(data_b, zeroes)); - v_byte_sums_d = VADD16(v_byte_sums_d, - VUNPACKHI8(data_b, zeroes)); - v_s1 = VADD32(v_s1, - VADD32(VSAD8(data_a, zeroes), - VSAD8(data_b, zeroes))); - /* - * Workaround for gcc bug where it generates - * unnecessary move instructions - * (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107892) - */ - #if GCC_PREREQ(1, 0) - __asm__("" : "+x" (v_s1), "+x" (v_s1_sums), - "+x" (v_byte_sums_a), - "+x" (v_byte_sums_b), - "+x" (v_byte_sums_c), - "+x" (v_byte_sums_d)); - #endif - p += 2*VL; - n -= 2*VL; - } while (n >= 2*VL); - - /* - * Calculate v_s2 as (2*VL)*v_s1_sums + - * [2*VL, 2*VL - 1, 2*VL - 2, ..., 1] * v_byte_sums. - * Then update s1 and s2 from v_s1 and v_s2. - */ - v_s2 = VADD32_5X(VSLL32(v_s1_sums, LOG2_VL + 1), - VMADD16(v_byte_sums_a, mults_a), - VMADD16(v_byte_sums_b, mults_b), - VMADD16(v_byte_sums_c, mults_c), - VMADD16(v_byte_sums_d, mults_d)); - reduce_to_32bits(v_s1, v_s2, &s1, &s2); - } - /* - * Process the last 0 <= n < 2*VL bytes of the chunk using - * scalar instructions and reduce s1 and s2 mod DIVISOR. - */ - ADLER32_CHUNK(s1, s2, p, n); - } -#endif /* !USE_VNNI */ - return (s2 << 16) | s1; -} - -#undef vec_t -#undef mask_t -#undef LOG2_VL -#undef VADD8 -#undef VADD16 -#undef VADD32 -#undef VDPBUSD -#undef VLOAD -#undef VLOADU -#undef VMADD16 -#undef VMASKZ_LOADU -#undef VMULLO32 -#undef VSAD8 -#undef VSET1_8 -#undef VSET1_32 -#undef VSETZERO -#undef VSLL32 -#undef VUNPACKLO8 -#undef VUNPACKHI8 - -#undef SUFFIX -#undef ATTRIBUTES -#undef VL -#undef USE_VNNI -#undef USE_AVX512 diff --git a/dm/third_party/libdeflate/lib/x86/cpu_features.c b/dm/third_party/libdeflate/lib/x86/cpu_features.c deleted file mode 100644 index a4e09ad..0000000 --- a/dm/third_party/libdeflate/lib/x86/cpu_features.c +++ /dev/null @@ -1,213 +0,0 @@ -/* - * x86/cpu_features.c - feature detection for x86 CPUs - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "../cpu_features_common.h" /* must be included first */ -#include "cpu_features.h" - -#ifdef X86_CPU_FEATURES_KNOWN -/* Runtime x86 CPU feature detection is supported. */ - -/* Execute the CPUID instruction. */ -static inline void -cpuid(u32 leaf, u32 subleaf, u32 *a, u32 *b, u32 *c, u32 *d) -{ -#ifdef _MSC_VER - int result[4]; - - __cpuidex(result, leaf, subleaf); - *a = result[0]; - *b = result[1]; - *c = result[2]; - *d = result[3]; -#else - __asm__ volatile("cpuid" : "=a" (*a), "=b" (*b), "=c" (*c), "=d" (*d) - : "a" (leaf), "c" (subleaf)); -#endif -} - -/* Read an extended control register. */ -static inline u64 -read_xcr(u32 index) -{ -#ifdef _MSC_VER - return _xgetbv(index); -#else - u32 d, a; - - /* - * Execute the "xgetbv" instruction. Old versions of binutils do not - * recognize this instruction, so list the raw bytes instead. - * - * This must be 'volatile' to prevent this code from being moved out - * from under the check for OSXSAVE. - */ - __asm__ volatile(".byte 0x0f, 0x01, 0xd0" : - "=d" (d), "=a" (a) : "c" (index)); - - return ((u64)d << 32) | a; -#endif -} - -static const struct cpu_feature x86_cpu_feature_table[] = { - {X86_CPU_FEATURE_SSE2, "sse2"}, - {X86_CPU_FEATURE_PCLMULQDQ, "pclmulqdq"}, - {X86_CPU_FEATURE_AVX, "avx"}, - {X86_CPU_FEATURE_AVX2, "avx2"}, - {X86_CPU_FEATURE_BMI2, "bmi2"}, - {X86_CPU_FEATURE_ZMM, "zmm"}, - {X86_CPU_FEATURE_AVX512BW, "avx512bw"}, - {X86_CPU_FEATURE_AVX512VL, "avx512vl"}, - {X86_CPU_FEATURE_VPCLMULQDQ, "vpclmulqdq"}, - {X86_CPU_FEATURE_AVX512VNNI, "avx512_vnni"}, - {X86_CPU_FEATURE_AVXVNNI, "avx_vnni"}, -}; - -volatile u32 libdeflate_x86_cpu_features = 0; - -static inline bool -os_supports_avx512(u64 xcr0) -{ -#ifdef __APPLE__ - /* - * The Darwin kernel had a bug where it could corrupt the opmask - * registers. See - * https://community.intel.com/t5/Software-Tuning-Performance/MacOS-Darwin-kernel-bug-clobbers-AVX-512-opmask-register-state/m-p/1327259 - * Darwin also does not initially set the XCR0 bits for AVX512, but they - * are set if the thread tries to use AVX512 anyway. Thus, to safely - * and consistently use AVX512 on macOS we'd need to check the kernel - * version as well as detect AVX512 support using a macOS-specific - * method. We don't bother with this, especially given Apple's - * transition to arm64. - */ - return false; -#else - return (xcr0 & 0xe6) == 0xe6; -#endif -} - -/* - * Don't use 512-bit vectors (ZMM registers) on Intel CPUs before Rocket Lake - * and Sapphire Rapids, due to the overly-eager downclocking which can reduce - * the performance of workloads that use ZMM registers only occasionally. - */ -static inline bool -allow_512bit_vectors(const u32 manufacturer[3], u32 family, u32 model) -{ -#ifdef TEST_SUPPORT__DO_NOT_USE - return true; -#endif - if (memcmp(manufacturer, "GenuineIntel", 12) != 0) - return true; - if (family != 6) - return true; - switch (model) { - case 85: /* Skylake (Server), Cascade Lake, Cooper Lake */ - case 106: /* Ice Lake (Server) */ - case 108: /* Ice Lake (Server) */ - case 126: /* Ice Lake (Client) */ - case 140: /* Tiger Lake */ - case 141: /* Tiger Lake */ - return false; - } - return true; -} - -/* Initialize libdeflate_x86_cpu_features. */ -void libdeflate_init_x86_cpu_features(void) -{ - u32 max_leaf; - u32 manufacturer[3]; - u32 family, model; - u32 a, b, c, d; - u64 xcr0 = 0; - u32 features = 0; - - /* EAX=0: Highest Function Parameter and Manufacturer ID */ - cpuid(0, 0, &max_leaf, &manufacturer[0], &manufacturer[2], - &manufacturer[1]); - if (max_leaf < 1) - goto out; - - /* EAX=1: Processor Info and Feature Bits */ - cpuid(1, 0, &a, &b, &c, &d); - family = (a >> 8) & 0xf; - model = (a >> 4) & 0xf; - if (family == 6 || family == 0xf) - model += (a >> 12) & 0xf0; - if (family == 0xf) - family += (a >> 20) & 0xff; - if (d & (1 << 26)) - features |= X86_CPU_FEATURE_SSE2; - /* - * No known CPUs have pclmulqdq without sse4.1, so in practice code - * targeting pclmulqdq can use sse4.1 instructions. But to be safe, - * explicitly check for both the pclmulqdq and sse4.1 bits. - */ - if ((c & (1 << 1)) && (c & (1 << 19))) - features |= X86_CPU_FEATURE_PCLMULQDQ; - if (c & (1 << 27)) - xcr0 = read_xcr(0); - if ((c & (1 << 28)) && ((xcr0 & 0x6) == 0x6)) - features |= X86_CPU_FEATURE_AVX; - - if (max_leaf < 7) - goto out; - - /* EAX=7, ECX=0: Extended Features */ - cpuid(7, 0, &a, &b, &c, &d); - if (b & (1 << 8)) - features |= X86_CPU_FEATURE_BMI2; - if ((xcr0 & 0x6) == 0x6) { - if (b & (1 << 5)) - features |= X86_CPU_FEATURE_AVX2; - if (c & (1 << 10)) - features |= X86_CPU_FEATURE_VPCLMULQDQ; - } - if (os_supports_avx512(xcr0)) { - if (allow_512bit_vectors(manufacturer, family, model)) - features |= X86_CPU_FEATURE_ZMM; - if (b & (1 << 30)) - features |= X86_CPU_FEATURE_AVX512BW; - if (b & (1U << 31)) - features |= X86_CPU_FEATURE_AVX512VL; - if (c & (1 << 11)) - features |= X86_CPU_FEATURE_AVX512VNNI; - } - - /* EAX=7, ECX=1: Extended Features */ - cpuid(7, 1, &a, &b, &c, &d); - if ((a & (1 << 4)) && ((xcr0 & 0x6) == 0x6)) - features |= X86_CPU_FEATURE_AVXVNNI; - -out: - disable_cpu_features_for_testing(&features, x86_cpu_feature_table, - ARRAY_LEN(x86_cpu_feature_table)); - - libdeflate_x86_cpu_features = features | X86_CPU_FEATURES_KNOWN; -} - -#endif /* X86_CPU_FEATURES_KNOWN */ diff --git a/dm/third_party/libdeflate/lib/x86/cpu_features.h b/dm/third_party/libdeflate/lib/x86/cpu_features.h deleted file mode 100644 index cb225b9..0000000 --- a/dm/third_party/libdeflate/lib/x86/cpu_features.h +++ /dev/null @@ -1,170 +0,0 @@ -/* - * x86/cpu_features.h - feature detection for x86 CPUs - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_X86_CPU_FEATURES_H -#define LIB_X86_CPU_FEATURES_H - -#include "../lib_common.h" - -#if defined(ARCH_X86_32) || defined(ARCH_X86_64) - -#define X86_CPU_FEATURE_SSE2 (1 << 0) -#define X86_CPU_FEATURE_PCLMULQDQ (1 << 1) -#define X86_CPU_FEATURE_AVX (1 << 2) -#define X86_CPU_FEATURE_AVX2 (1 << 3) -#define X86_CPU_FEATURE_BMI2 (1 << 4) -/* - * ZMM indicates whether 512-bit vectors (zmm registers) should be used. On - * some CPUs, to avoid downclocking issues we don't set ZMM even if the CPU and - * operating system support AVX-512. On these CPUs, we may still use AVX-512 - * instructions, but only with xmm and ymm registers. - */ -#define X86_CPU_FEATURE_ZMM (1 << 5) -#define X86_CPU_FEATURE_AVX512BW (1 << 6) -#define X86_CPU_FEATURE_AVX512VL (1 << 7) -#define X86_CPU_FEATURE_VPCLMULQDQ (1 << 8) -#define X86_CPU_FEATURE_AVX512VNNI (1 << 9) -#define X86_CPU_FEATURE_AVXVNNI (1 << 10) - -#if defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER) -/* Runtime x86 CPU feature detection is supported. */ -# define X86_CPU_FEATURES_KNOWN (1U << 31) -extern volatile u32 libdeflate_x86_cpu_features; - -void libdeflate_init_x86_cpu_features(void); - -static inline u32 get_x86_cpu_features(void) -{ - if (libdeflate_x86_cpu_features == 0) - libdeflate_init_x86_cpu_features(); - return libdeflate_x86_cpu_features; -} -/* - * x86 intrinsics are also supported. Include the headers needed to use them. - * Normally just immintrin.h suffices. With clang in MSVC compatibility mode, - * immintrin.h incorrectly skips including sub-headers, so include those too. - */ -# include <immintrin.h> -# if defined(_MSC_VER) && defined(__clang__) -# include <tmmintrin.h> -# include <smmintrin.h> -# include <wmmintrin.h> -# include <avxintrin.h> -# include <avx2intrin.h> -# include <avx512fintrin.h> -# include <avx512bwintrin.h> -# include <avx512vlintrin.h> -# if __has_include(<avx512vlbwintrin.h>) -# include <avx512vlbwintrin.h> -# endif -# if __has_include(<vpclmulqdqintrin.h>) -# include <vpclmulqdqintrin.h> -# endif -# if __has_include(<avx512vnniintrin.h>) -# include <avx512vnniintrin.h> -# endif -# if __has_include(<avx512vlvnniintrin.h>) -# include <avx512vlvnniintrin.h> -# endif -# if __has_include(<avxvnniintrin.h>) -# include <avxvnniintrin.h> -# endif -# endif -#else -static inline u32 get_x86_cpu_features(void) { return 0; } -#endif - -#if defined(__SSE2__) || \ - (defined(_MSC_VER) && \ - (defined(ARCH_X86_64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2))) -# define HAVE_SSE2(features) 1 -# define HAVE_SSE2_NATIVE 1 -#else -# define HAVE_SSE2(features) ((features) & X86_CPU_FEATURE_SSE2) -# define HAVE_SSE2_NATIVE 0 -#endif - -#if (defined(__PCLMUL__) && defined(__SSE4_1__)) || \ - (defined(_MSC_VER) && defined(__AVX2__)) -# define HAVE_PCLMULQDQ(features) 1 -#else -# define HAVE_PCLMULQDQ(features) ((features) & X86_CPU_FEATURE_PCLMULQDQ) -#endif - -#ifdef __AVX__ -# define HAVE_AVX(features) 1 -#else -# define HAVE_AVX(features) ((features) & X86_CPU_FEATURE_AVX) -#endif - -#ifdef __AVX2__ -# define HAVE_AVX2(features) 1 -#else -# define HAVE_AVX2(features) ((features) & X86_CPU_FEATURE_AVX2) -#endif - -#if defined(__BMI2__) || (defined(_MSC_VER) && defined(__AVX2__)) -# define HAVE_BMI2(features) 1 -# define HAVE_BMI2_NATIVE 1 -#else -# define HAVE_BMI2(features) ((features) & X86_CPU_FEATURE_BMI2) -# define HAVE_BMI2_NATIVE 0 -#endif - -#ifdef __AVX512BW__ -# define HAVE_AVX512BW(features) 1 -#else -# define HAVE_AVX512BW(features) ((features) & X86_CPU_FEATURE_AVX512BW) -#endif - -#ifdef __AVX512VL__ -# define HAVE_AVX512VL(features) 1 -#else -# define HAVE_AVX512VL(features) ((features) & X86_CPU_FEATURE_AVX512VL) -#endif - -#ifdef __VPCLMULQDQ__ -# define HAVE_VPCLMULQDQ(features) 1 -#else -# define HAVE_VPCLMULQDQ(features) ((features) & X86_CPU_FEATURE_VPCLMULQDQ) -#endif - -#ifdef __AVX512VNNI__ -# define HAVE_AVX512VNNI(features) 1 -#else -# define HAVE_AVX512VNNI(features) ((features) & X86_CPU_FEATURE_AVX512VNNI) -#endif - -#ifdef __AVXVNNI__ -# define HAVE_AVXVNNI(features) 1 -#else -# define HAVE_AVXVNNI(features) ((features) & X86_CPU_FEATURE_AVXVNNI) -#endif - -#endif /* ARCH_X86_32 || ARCH_X86_64 */ - -#endif /* LIB_X86_CPU_FEATURES_H */ diff --git a/dm/third_party/libdeflate/lib/x86/crc32_impl.h b/dm/third_party/libdeflate/lib/x86/crc32_impl.h deleted file mode 100644 index 4fbdef7..0000000 --- a/dm/third_party/libdeflate/lib/x86/crc32_impl.h +++ /dev/null @@ -1,160 +0,0 @@ -/* - * x86/crc32_impl.h - x86 implementations of the gzip CRC-32 algorithm - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_X86_CRC32_IMPL_H -#define LIB_X86_CRC32_IMPL_H - -#include "cpu_features.h" - -/* - * pshufb(x, shift_tab[len..len+15]) left shifts x by 16-len bytes. - * pshufb(x, shift_tab[len+16..len+31]) right shifts x by len bytes. - */ -static const u8 MAYBE_UNUSED shift_tab[48] = { - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, -}; - -#if defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER) -/* - * PCLMULQDQ implementation. This targets PCLMULQDQ+SSE4.1, since in practice - * all CPUs that support PCLMULQDQ also support SSE4.1. - */ -# define crc32_x86_pclmulqdq crc32_x86_pclmulqdq -# define SUFFIX _pclmulqdq -# define ATTRIBUTES _target_attribute("pclmul,sse4.1") -# define VL 16 -# define USE_AVX512 0 -# include "crc32_pclmul_template.h" - -/* - * PCLMULQDQ/AVX implementation. Same as above, but this is compiled with AVX - * enabled so that the compiler can generate VEX-coded instructions which can be - * slightly more efficient. It still uses 128-bit vectors. - */ -# define crc32_x86_pclmulqdq_avx crc32_x86_pclmulqdq_avx -# define SUFFIX _pclmulqdq_avx -# define ATTRIBUTES _target_attribute("pclmul,avx") -# define VL 16 -# define USE_AVX512 0 -# include "crc32_pclmul_template.h" -#endif - -/* - * VPCLMULQDQ/AVX2 implementation. This is used on CPUs that have AVX2 and - * VPCLMULQDQ but don't have AVX-512, for example Intel Alder Lake. - * - * Currently this can't be enabled with MSVC because MSVC has a bug where it - * incorrectly assumes that VPCLMULQDQ implies AVX-512: - * https://developercommunity.visualstudio.com/t/Compiler-incorrectly-assumes-VAES-and-VP/10578785 - * - * gcc 8.1 and 8.2 had a similar bug where they assumed that - * _mm256_clmulepi64_epi128() always needed AVX512. It's fixed in gcc 8.3. - * - * _mm256_zextsi128_si256() requires gcc 10. - */ -#if (GCC_PREREQ(10, 1) || CLANG_PREREQ(6, 0, 10000000)) && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_VPCLMULQDQ) -# define crc32_x86_vpclmulqdq_avx2 crc32_x86_vpclmulqdq_avx2 -# define SUFFIX _vpclmulqdq_avx2 -# define ATTRIBUTES _target_attribute("vpclmulqdq,pclmul,avx2") -# define VL 32 -# define USE_AVX512 0 -# include "crc32_pclmul_template.h" -#endif - -#if (GCC_PREREQ(10, 1) || CLANG_PREREQ(6, 0, 10000000) || MSVC_PREREQ(1920)) && \ - !(CLANG_PREREQ(18, 0, 18000000) && !CLANG_PREREQ(19, 0, 19000000)) && \ - !defined(LIBDEFLATE_ASSEMBLER_DOES_NOT_SUPPORT_VPCLMULQDQ) -/* - * VPCLMULQDQ/AVX512 implementation using 256-bit vectors. This is very similar - * to the VPCLMULQDQ/AVX2 implementation but takes advantage of the vpternlog - * instruction and more registers. This is used on certain older Intel CPUs, - * specifically Ice Lake and Tiger Lake, which support VPCLMULQDQ and AVX512 but - * downclock a bit too eagerly when ZMM registers are used. - * - * _mm256_zextsi128_si256() requires gcc 10. - */ -# define crc32_x86_vpclmulqdq_avx512_vl256 crc32_x86_vpclmulqdq_avx512_vl256 -# define SUFFIX _vpclmulqdq_avx512_vl256 -# define ATTRIBUTES _target_attribute("vpclmulqdq,pclmul,avx512bw,avx512vl") -# define VL 32 -# define USE_AVX512 1 -# include "crc32_pclmul_template.h" - -/* - * VPCLMULQDQ/AVX512 implementation using 512-bit vectors. This is used on CPUs - * that have a good AVX-512 implementation including VPCLMULQDQ. - * - * _mm512_zextsi128_si512() requires gcc 10. - */ -# define crc32_x86_vpclmulqdq_avx512_vl512 crc32_x86_vpclmulqdq_avx512_vl512 -# define SUFFIX _vpclmulqdq_avx512_vl512 -# define ATTRIBUTES _target_attribute("vpclmulqdq,pclmul,avx512bw,avx512vl") -# define VL 64 -# define USE_AVX512 1 -# include "crc32_pclmul_template.h" -#endif - -static inline crc32_func_t -arch_select_crc32_func(void) -{ - const u32 features MAYBE_UNUSED = get_x86_cpu_features(); - -#ifdef crc32_x86_vpclmulqdq_avx512_vl512 - if ((features & X86_CPU_FEATURE_ZMM) && - HAVE_VPCLMULQDQ(features) && HAVE_PCLMULQDQ(features) && - HAVE_AVX512BW(features) && HAVE_AVX512VL(features)) - return crc32_x86_vpclmulqdq_avx512_vl512; -#endif -#ifdef crc32_x86_vpclmulqdq_avx512_vl256 - if (HAVE_VPCLMULQDQ(features) && HAVE_PCLMULQDQ(features) && - HAVE_AVX512BW(features) && HAVE_AVX512VL(features)) - return crc32_x86_vpclmulqdq_avx512_vl256; -#endif -#ifdef crc32_x86_vpclmulqdq_avx2 - if (HAVE_VPCLMULQDQ(features) && HAVE_PCLMULQDQ(features) && - HAVE_AVX2(features)) - return crc32_x86_vpclmulqdq_avx2; -#endif -#ifdef crc32_x86_pclmulqdq_avx - if (HAVE_PCLMULQDQ(features) && HAVE_AVX(features)) - return crc32_x86_pclmulqdq_avx; -#endif -#ifdef crc32_x86_pclmulqdq - if (HAVE_PCLMULQDQ(features)) - return crc32_x86_pclmulqdq; -#endif - return NULL; -} -#define arch_select_crc32_func arch_select_crc32_func - -#endif /* LIB_X86_CRC32_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/x86/crc32_pclmul_template.h b/dm/third_party/libdeflate/lib/x86/crc32_pclmul_template.h deleted file mode 100644 index 8bb8bfd..0000000 --- a/dm/third_party/libdeflate/lib/x86/crc32_pclmul_template.h +++ /dev/null @@ -1,424 +0,0 @@ -/* - * x86/crc32_pclmul_template.h - gzip CRC-32 with PCLMULQDQ instructions - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * This file is a "template" for instantiating PCLMULQDQ-based crc32_x86 - * functions. The "parameters" are: - * - * SUFFIX: - * Name suffix to append to all instantiated functions. - * ATTRIBUTES: - * Target function attributes to use. Must satisfy the dependencies of the - * other parameters as follows: - * VL=16 && USE_AVX512=0: at least pclmul,sse4.1 - * VL=32 && USE_AVX512=0: at least vpclmulqdq,pclmul,avx2 - * VL=32 && USE_AVX512=1: at least vpclmulqdq,pclmul,avx512bw,avx512vl - * VL=64 && USE_AVX512=1: at least vpclmulqdq,pclmul,avx512bw,avx512vl - * (Other combinations are not useful and have not been tested.) - * VL: - * Vector length in bytes. Must be 16, 32, or 64. - * USE_AVX512: - * If 1, take advantage of AVX-512 features such as masking and the - * vpternlog instruction. This doesn't enable the use of 512-bit vectors; - * the vector length is controlled by VL. If 0, assume that the CPU might - * not support AVX-512. - * - * The overall algorithm used is CRC folding with carryless multiplication - * instructions. Note that the x86 crc32 instruction cannot be used, as it is - * for a different polynomial, not the gzip one. For an explanation of CRC - * folding with carryless multiplication instructions, see - * scripts/gen-crc32-consts.py and the following blog posts and papers: - * - * "An alternative exposition of crc32_4k_pclmulqdq" - * https://www.corsix.org/content/alternative-exposition-crc32_4k_pclmulqdq - * - * "Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction" - * https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf - * - * The original pclmulqdq instruction does one 64x64 to 128-bit carryless - * multiplication. The VPCLMULQDQ feature added instructions that do two - * parallel 64x64 to 128-bit carryless multiplications in combination with AVX - * or AVX512VL, or four in combination with AVX512F. - */ - -#if VL == 16 -# define vec_t __m128i -# define fold_vec fold_vec128 -# define VLOADU(p) _mm_loadu_si128((const void *)(p)) -# define VXOR(a, b) _mm_xor_si128((a), (b)) -# define M128I_TO_VEC(a) a -# define MULTS_8V _mm_set_epi64x(CRC32_X991_MODG, CRC32_X1055_MODG) -# define MULTS_4V _mm_set_epi64x(CRC32_X479_MODG, CRC32_X543_MODG) -# define MULTS_2V _mm_set_epi64x(CRC32_X223_MODG, CRC32_X287_MODG) -# define MULTS_1V _mm_set_epi64x(CRC32_X95_MODG, CRC32_X159_MODG) -#elif VL == 32 -# define vec_t __m256i -# define fold_vec fold_vec256 -# define VLOADU(p) _mm256_loadu_si256((const void *)(p)) -# define VXOR(a, b) _mm256_xor_si256((a), (b)) -# define M128I_TO_VEC(a) _mm256_zextsi128_si256(a) -# define MULTS(a, b) _mm256_set_epi64x(a, b, a, b) -# define MULTS_8V MULTS(CRC32_X2015_MODG, CRC32_X2079_MODG) -# define MULTS_4V MULTS(CRC32_X991_MODG, CRC32_X1055_MODG) -# define MULTS_2V MULTS(CRC32_X479_MODG, CRC32_X543_MODG) -# define MULTS_1V MULTS(CRC32_X223_MODG, CRC32_X287_MODG) -#elif VL == 64 -# define vec_t __m512i -# define fold_vec fold_vec512 -# define VLOADU(p) _mm512_loadu_si512((const void *)(p)) -# define VXOR(a, b) _mm512_xor_si512((a), (b)) -# define M128I_TO_VEC(a) _mm512_zextsi128_si512(a) -# define MULTS(a, b) _mm512_set_epi64(a, b, a, b, a, b, a, b) -# define MULTS_8V MULTS(CRC32_X4063_MODG, CRC32_X4127_MODG) -# define MULTS_4V MULTS(CRC32_X2015_MODG, CRC32_X2079_MODG) -# define MULTS_2V MULTS(CRC32_X991_MODG, CRC32_X1055_MODG) -# define MULTS_1V MULTS(CRC32_X479_MODG, CRC32_X543_MODG) -#else -# error "unsupported vector length" -#endif - -#undef fold_vec128 -static forceinline ATTRIBUTES __m128i -ADD_SUFFIX(fold_vec128)(__m128i src, __m128i dst, __m128i /* __v2du */ mults) -{ - dst = _mm_xor_si128(dst, _mm_clmulepi64_si128(src, mults, 0x00)); - dst = _mm_xor_si128(dst, _mm_clmulepi64_si128(src, mults, 0x11)); - return dst; -} -#define fold_vec128 ADD_SUFFIX(fold_vec128) - -#if VL >= 32 -#undef fold_vec256 -static forceinline ATTRIBUTES __m256i -ADD_SUFFIX(fold_vec256)(__m256i src, __m256i dst, __m256i /* __v4du */ mults) -{ -#if USE_AVX512 - /* vpternlog with immediate 0x96 is a three-argument XOR. */ - return _mm256_ternarylogic_epi32( - _mm256_clmulepi64_epi128(src, mults, 0x00), - _mm256_clmulepi64_epi128(src, mults, 0x11), - dst, - 0x96); -#else - return _mm256_xor_si256( - _mm256_xor_si256(dst, - _mm256_clmulepi64_epi128(src, mults, 0x00)), - _mm256_clmulepi64_epi128(src, mults, 0x11)); -#endif -} -#define fold_vec256 ADD_SUFFIX(fold_vec256) -#endif /* VL >= 32 */ - -#if VL >= 64 -#undef fold_vec512 -static forceinline ATTRIBUTES __m512i -ADD_SUFFIX(fold_vec512)(__m512i src, __m512i dst, __m512i /* __v8du */ mults) -{ - /* vpternlog with immediate 0x96 is a three-argument XOR. */ - return _mm512_ternarylogic_epi32( - _mm512_clmulepi64_epi128(src, mults, 0x00), - _mm512_clmulepi64_epi128(src, mults, 0x11), - dst, - 0x96); -} -#define fold_vec512 ADD_SUFFIX(fold_vec512) -#endif /* VL >= 64 */ - -/* - * Given 'x' containing a 16-byte polynomial, and a pointer 'p' that points to - * the next '1 <= len <= 15' data bytes, rearrange the concatenation of 'x' and - * the data into vectors x0 and x1 that contain 'len' bytes and 16 bytes, - * respectively. Then fold x0 into x1 and return the result. - * Assumes that 'p + len - 16' is in-bounds. - */ -#undef fold_lessthan16bytes -static forceinline ATTRIBUTES __m128i -ADD_SUFFIX(fold_lessthan16bytes)(__m128i x, const u8 *p, size_t len, - __m128i /* __v2du */ mults_128b) -{ - __m128i lshift = _mm_loadu_si128((const void *)&shift_tab[len]); - __m128i rshift = _mm_loadu_si128((const void *)&shift_tab[len + 16]); - __m128i x0, x1; - - /* x0 = x left-shifted by '16 - len' bytes */ - x0 = _mm_shuffle_epi8(x, lshift); - - /* - * x1 = the last '16 - len' bytes from x (i.e. x right-shifted by 'len' - * bytes) followed by the remaining data. - */ - x1 = _mm_blendv_epi8(_mm_shuffle_epi8(x, rshift), - _mm_loadu_si128((const void *)(p + len - 16)), - /* msb 0/1 of each byte selects byte from arg1/2 */ - rshift); - - return fold_vec128(x0, x1, mults_128b); -} -#define fold_lessthan16bytes ADD_SUFFIX(fold_lessthan16bytes) - -static ATTRIBUTES u32 -ADD_SUFFIX(crc32_x86)(u32 crc, const u8 *p, size_t len) -{ - /* - * mults_{N}v are the vectors of multipliers for folding across N vec_t - * vectors, i.e. N*VL*8 bits. mults_128b are the two multipliers for - * folding across 128 bits. mults_128b differs from mults_1v when - * VL != 16. All multipliers are 64-bit, to match what pclmulqdq needs, - * but since this is for CRC-32 only their low 32 bits are nonzero. - * For more details, see scripts/gen-crc32-consts.py. - */ - const vec_t mults_8v = MULTS_8V; - const vec_t mults_4v = MULTS_4V; - const vec_t mults_2v = MULTS_2V; - const vec_t mults_1v = MULTS_1V; - const __m128i mults_128b = _mm_set_epi64x(CRC32_X95_MODG, CRC32_X159_MODG); - const __m128i barrett_reduction_constants = - _mm_set_epi64x(CRC32_BARRETT_CONSTANT_2, CRC32_BARRETT_CONSTANT_1); - vec_t v0, v1, v2, v3, v4, v5, v6, v7; - __m128i x0 = _mm_cvtsi32_si128(crc); - __m128i x1; - - if (len < 8*VL) { - if (len < VL) { - STATIC_ASSERT(VL == 16 || VL == 32 || VL == 64); - if (len < 16) { - #if USE_AVX512 - if (len < 4) - return crc32_slice1(crc, p, len); - /* - * Handle 4 <= len <= 15 bytes by doing a masked - * load, XOR'ing the current CRC with the first - * 4 bytes, left-shifting by '16 - len' bytes to - * align the result to the end of x0 (so that it - * becomes the low-order coefficients of a - * 128-bit polynomial), and then doing the usual - * reduction from 128 bits to 32 bits. - */ - x0 = _mm_xor_si128( - x0, _mm_maskz_loadu_epi8((1 << len) - 1, p)); - x0 = _mm_shuffle_epi8( - x0, _mm_loadu_si128((const void *)&shift_tab[len])); - goto reduce_x0; - #else - return crc32_slice1(crc, p, len); - #endif - } - /* - * Handle 16 <= len < VL bytes where VL is 32 or 64. - * Use 128-bit instructions so that these lengths aren't - * slower with VL > 16 than with VL=16. - */ - x0 = _mm_xor_si128(_mm_loadu_si128((const void *)p), x0); - if (len >= 32) { - x0 = fold_vec128(x0, _mm_loadu_si128((const void *)(p + 16)), - mults_128b); - if (len >= 48) - x0 = fold_vec128(x0, _mm_loadu_si128((const void *)(p + 32)), - mults_128b); - } - p += len & ~15; - goto less_than_16_remaining; - } - v0 = VXOR(VLOADU(p), M128I_TO_VEC(x0)); - if (len < 2*VL) { - p += VL; - goto less_than_vl_remaining; - } - v1 = VLOADU(p + 1*VL); - if (len < 4*VL) { - p += 2*VL; - goto less_than_2vl_remaining; - } - v2 = VLOADU(p + 2*VL); - v3 = VLOADU(p + 3*VL); - p += 4*VL; - } else { - /* - * If the length is large and the pointer is misaligned, align - * it. For smaller lengths, just take the misaligned load - * penalty. Note that on recent x86 CPUs, vmovdqu with an - * aligned address is just as fast as vmovdqa, so there's no - * need to use vmovdqa in the main loop. - */ - if (len > 65536 && ((uintptr_t)p & (VL-1))) { - size_t align = -(uintptr_t)p & (VL-1); - - len -= align; - x0 = _mm_xor_si128(_mm_loadu_si128((const void *)p), x0); - p += 16; - if (align & 15) { - x0 = fold_lessthan16bytes(x0, p, align & 15, - mults_128b); - p += align & 15; - align &= ~15; - } - while (align) { - x0 = fold_vec128(x0, *(const __m128i *)p, - mults_128b); - p += 16; - align -= 16; - } - v0 = M128I_TO_VEC(x0); - # if VL == 32 - v0 = _mm256_inserti128_si256(v0, *(const __m128i *)p, 1); - # elif VL == 64 - v0 = _mm512_inserti32x4(v0, *(const __m128i *)p, 1); - v0 = _mm512_inserti64x4(v0, *(const __m256i *)(p + 16), 1); - # endif - p -= 16; - } else { - v0 = VXOR(VLOADU(p), M128I_TO_VEC(x0)); - } - v1 = VLOADU(p + 1*VL); - v2 = VLOADU(p + 2*VL); - v3 = VLOADU(p + 3*VL); - v4 = VLOADU(p + 4*VL); - v5 = VLOADU(p + 5*VL); - v6 = VLOADU(p + 6*VL); - v7 = VLOADU(p + 7*VL); - p += 8*VL; - - /* - * This is the main loop, processing 8*VL bytes per iteration. - * 4*VL is usually enough and would result in smaller code, but - * Skylake and Cascade Lake need 8*VL to get full performance. - */ - while (len >= 16*VL) { - v0 = fold_vec(v0, VLOADU(p + 0*VL), mults_8v); - v1 = fold_vec(v1, VLOADU(p + 1*VL), mults_8v); - v2 = fold_vec(v2, VLOADU(p + 2*VL), mults_8v); - v3 = fold_vec(v3, VLOADU(p + 3*VL), mults_8v); - v4 = fold_vec(v4, VLOADU(p + 4*VL), mults_8v); - v5 = fold_vec(v5, VLOADU(p + 5*VL), mults_8v); - v6 = fold_vec(v6, VLOADU(p + 6*VL), mults_8v); - v7 = fold_vec(v7, VLOADU(p + 7*VL), mults_8v); - p += 8*VL; - len -= 8*VL; - } - - /* Fewer than 8*VL bytes remain. */ - v0 = fold_vec(v0, v4, mults_4v); - v1 = fold_vec(v1, v5, mults_4v); - v2 = fold_vec(v2, v6, mults_4v); - v3 = fold_vec(v3, v7, mults_4v); - if (len & (4*VL)) { - v0 = fold_vec(v0, VLOADU(p + 0*VL), mults_4v); - v1 = fold_vec(v1, VLOADU(p + 1*VL), mults_4v); - v2 = fold_vec(v2, VLOADU(p + 2*VL), mults_4v); - v3 = fold_vec(v3, VLOADU(p + 3*VL), mults_4v); - p += 4*VL; - } - } - /* Fewer than 4*VL bytes remain. */ - v0 = fold_vec(v0, v2, mults_2v); - v1 = fold_vec(v1, v3, mults_2v); - if (len & (2*VL)) { - v0 = fold_vec(v0, VLOADU(p + 0*VL), mults_2v); - v1 = fold_vec(v1, VLOADU(p + 1*VL), mults_2v); - p += 2*VL; - } -less_than_2vl_remaining: - /* Fewer than 2*VL bytes remain. */ - v0 = fold_vec(v0, v1, mults_1v); - if (len & VL) { - v0 = fold_vec(v0, VLOADU(p), mults_1v); - p += VL; - } -less_than_vl_remaining: - /* - * Fewer than VL bytes remain. Reduce v0 (length VL bytes) to x0 - * (length 16 bytes) and fold in any 16-byte data segments that remain. - */ -#if VL == 16 - x0 = v0; -#else - { - #if VL == 32 - __m256i y0 = v0; - #else - const __m256i mults_256b = - _mm256_set_epi64x(CRC32_X223_MODG, CRC32_X287_MODG, - CRC32_X223_MODG, CRC32_X287_MODG); - __m256i y0 = fold_vec256(_mm512_extracti64x4_epi64(v0, 0), - _mm512_extracti64x4_epi64(v0, 1), - mults_256b); - if (len & 32) { - y0 = fold_vec256(y0, _mm256_loadu_si256((const void *)p), - mults_256b); - p += 32; - } - #endif - x0 = fold_vec128(_mm256_extracti128_si256(y0, 0), - _mm256_extracti128_si256(y0, 1), mults_128b); - } - if (len & 16) { - x0 = fold_vec128(x0, _mm_loadu_si128((const void *)p), - mults_128b); - p += 16; - } -#endif -less_than_16_remaining: - len &= 15; - - /* Handle any remainder of 1 to 15 bytes. */ - if (len) - x0 = fold_lessthan16bytes(x0, p, len, mults_128b); -#if USE_AVX512 -reduce_x0: -#endif - /* - * Multiply the remaining 128-bit message polynomial 'x0' by x^32, then - * reduce it modulo the generator polynomial G. This gives the CRC. - * - * This implementation matches that used in crc-pclmul-template.S from - * https://lore.kernel.org/r/20250210174540.161705-4-ebiggers@kernel.org/ - * with the parameters n=32 and LSB_CRC=1 (what the gzip CRC uses). See - * there for a detailed explanation of the math used here. - */ - x0 = _mm_xor_si128(_mm_clmulepi64_si128(x0, mults_128b, 0x10), - _mm_bsrli_si128(x0, 8)); - x1 = _mm_clmulepi64_si128(x0, barrett_reduction_constants, 0x00); - x1 = _mm_clmulepi64_si128(x1, barrett_reduction_constants, 0x10); - x0 = _mm_xor_si128(x0, x1); - return _mm_extract_epi32(x0, 2); -} - -#undef vec_t -#undef fold_vec -#undef VLOADU -#undef VXOR -#undef M128I_TO_VEC -#undef MULTS -#undef MULTS_8V -#undef MULTS_4V -#undef MULTS_2V -#undef MULTS_1V - -#undef SUFFIX -#undef ATTRIBUTES -#undef VL -#undef USE_AVX512 diff --git a/dm/third_party/libdeflate/lib/x86/decompress_impl.h b/dm/third_party/libdeflate/lib/x86/decompress_impl.h deleted file mode 100644 index daedcf2..0000000 --- a/dm/third_party/libdeflate/lib/x86/decompress_impl.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef LIB_X86_DECOMPRESS_IMPL_H -#define LIB_X86_DECOMPRESS_IMPL_H - -#include "cpu_features.h" - -/* - * BMI2 optimized decompression function. - * - * With gcc and clang we just compile the whole function with - * __attribute__((target("bmi2"))), and the compiler uses bmi2 automatically. - * - * With MSVC, there is no target function attribute, but it's still possible to - * use bmi2 intrinsics explicitly. Currently we mostly don't, but there's a - * case in which we do (see below), so we at least take advantage of that. - * However, MSVC from VS2017 (toolset v141) apparently miscompiles the _bzhi_*() - * intrinsics. It seems to be fixed in VS2022. Hence, use MSVC_PREREQ(1930). - */ -#if defined(__GNUC__) || defined(__clang__) || MSVC_PREREQ(1930) -# define deflate_decompress_bmi2 deflate_decompress_bmi2 -# define FUNCNAME deflate_decompress_bmi2 -# define ATTRIBUTES _target_attribute("bmi2") - /* - * Even with __attribute__((target("bmi2"))), gcc doesn't reliably use the - * bzhi instruction for 'word & BITMASK(count)'. So use the bzhi intrinsic - * explicitly. EXTRACT_VARBITS() is equivalent to 'word & BITMASK(count)'; - * EXTRACT_VARBITS8() is equivalent to 'word & BITMASK((u8)count)'. - * Nevertheless, their implementation using the bzhi intrinsic is identical, - * as the bzhi instruction truncates the count to 8 bits implicitly. - */ -# ifndef __clang__ -# ifdef ARCH_X86_64 -# define EXTRACT_VARBITS(word, count) _bzhi_u64((word), (count)) -# define EXTRACT_VARBITS8(word, count) _bzhi_u64((word), (count)) -# else -# define EXTRACT_VARBITS(word, count) _bzhi_u32((word), (count)) -# define EXTRACT_VARBITS8(word, count) _bzhi_u32((word), (count)) -# endif -# endif -# include "../decompress_template.h" -#endif - -#if defined(deflate_decompress_bmi2) && HAVE_BMI2_NATIVE -#define DEFAULT_IMPL deflate_decompress_bmi2 -#else -static inline decompress_func_t -arch_select_decompress_func(void) -{ -#ifdef deflate_decompress_bmi2 - if (HAVE_BMI2(get_x86_cpu_features())) - return deflate_decompress_bmi2; -#endif - return NULL; -} -#define arch_select_decompress_func arch_select_decompress_func -#endif - -#endif /* LIB_X86_DECOMPRESS_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/x86/matchfinder_impl.h b/dm/third_party/libdeflate/lib/x86/matchfinder_impl.h deleted file mode 100644 index 37a4960..0000000 --- a/dm/third_party/libdeflate/lib/x86/matchfinder_impl.h +++ /dev/null @@ -1,122 +0,0 @@ -/* - * x86/matchfinder_impl.h - x86 implementations of matchfinder functions - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#ifndef LIB_X86_MATCHFINDER_IMPL_H -#define LIB_X86_MATCHFINDER_IMPL_H - -#include "cpu_features.h" - -#ifdef __AVX2__ -static forceinline void -matchfinder_init_avx2(mf_pos_t *data, size_t size) -{ - __m256i *p = (__m256i *)data; - __m256i v = _mm256_set1_epi16(MATCHFINDER_INITVAL); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - p[0] = v; - p[1] = v; - p[2] = v; - p[3] = v; - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_init matchfinder_init_avx2 - -static forceinline void -matchfinder_rebase_avx2(mf_pos_t *data, size_t size) -{ - __m256i *p = (__m256i *)data; - __m256i v = _mm256_set1_epi16((u16)-MATCHFINDER_WINDOW_SIZE); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - /* PADDSW: Add Packed Signed Integers With Signed Saturation */ - p[0] = _mm256_adds_epi16(p[0], v); - p[1] = _mm256_adds_epi16(p[1], v); - p[2] = _mm256_adds_epi16(p[2], v); - p[3] = _mm256_adds_epi16(p[3], v); - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_rebase matchfinder_rebase_avx2 - -#elif HAVE_SSE2_NATIVE -static forceinline void -matchfinder_init_sse2(mf_pos_t *data, size_t size) -{ - __m128i *p = (__m128i *)data; - __m128i v = _mm_set1_epi16(MATCHFINDER_INITVAL); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - p[0] = v; - p[1] = v; - p[2] = v; - p[3] = v; - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_init matchfinder_init_sse2 - -static forceinline void -matchfinder_rebase_sse2(mf_pos_t *data, size_t size) -{ - __m128i *p = (__m128i *)data; - __m128i v = _mm_set1_epi16((u16)-MATCHFINDER_WINDOW_SIZE); - - STATIC_ASSERT(MATCHFINDER_MEM_ALIGNMENT % sizeof(*p) == 0); - STATIC_ASSERT(MATCHFINDER_SIZE_ALIGNMENT % (4 * sizeof(*p)) == 0); - STATIC_ASSERT(sizeof(mf_pos_t) == 2); - - do { - /* PADDSW: Add Packed Signed Integers With Signed Saturation */ - p[0] = _mm_adds_epi16(p[0], v); - p[1] = _mm_adds_epi16(p[1], v); - p[2] = _mm_adds_epi16(p[2], v); - p[3] = _mm_adds_epi16(p[3], v); - p += 4; - size -= 4 * sizeof(*p); - } while (size != 0); -} -#define matchfinder_rebase matchfinder_rebase_sse2 -#endif /* HAVE_SSE2_NATIVE */ - -#endif /* LIB_X86_MATCHFINDER_IMPL_H */ diff --git a/dm/third_party/libdeflate/lib/zlib_compress.c b/dm/third_party/libdeflate/lib/zlib_compress.c deleted file mode 100644 index 12d4360..0000000 --- a/dm/third_party/libdeflate/lib/zlib_compress.c +++ /dev/null @@ -1,82 +0,0 @@ -/* - * zlib_compress.c - compress with a zlib wrapper - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "deflate_compress.h" -#include "zlib_constants.h" - -LIBDEFLATEAPI size_t -libdeflate_zlib_compress(struct libdeflate_compressor *c, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail) -{ - u8 *out_next = out; - u16 hdr; - unsigned compression_level; - unsigned level_hint; - size_t deflate_size; - - if (out_nbytes_avail <= ZLIB_MIN_OVERHEAD) - return 0; - - /* 2 byte header: CMF and FLG */ - hdr = (ZLIB_CM_DEFLATE << 8) | (ZLIB_CINFO_32K_WINDOW << 12); - compression_level = libdeflate_get_compression_level(c); - if (compression_level < 2) - level_hint = ZLIB_FASTEST_COMPRESSION; - else if (compression_level < 6) - level_hint = ZLIB_FAST_COMPRESSION; - else if (compression_level < 8) - level_hint = ZLIB_DEFAULT_COMPRESSION; - else - level_hint = ZLIB_SLOWEST_COMPRESSION; - hdr |= level_hint << 6; - hdr |= 31 - (hdr % 31); - - put_unaligned_be16(hdr, out_next); - out_next += 2; - - /* Compressed data */ - deflate_size = libdeflate_deflate_compress(c, in, in_nbytes, out_next, - out_nbytes_avail - ZLIB_MIN_OVERHEAD); - if (deflate_size == 0) - return 0; - out_next += deflate_size; - - /* ADLER32 */ - put_unaligned_be32(libdeflate_adler32(1, in, in_nbytes), out_next); - out_next += 4; - - return out_next - (u8 *)out; -} - -LIBDEFLATEAPI size_t -libdeflate_zlib_compress_bound(struct libdeflate_compressor *c, - size_t in_nbytes) -{ - return ZLIB_MIN_OVERHEAD + - libdeflate_deflate_compress_bound(c, in_nbytes); -} diff --git a/dm/third_party/libdeflate/lib/zlib_constants.h b/dm/third_party/libdeflate/lib/zlib_constants.h deleted file mode 100644 index f304310..0000000 --- a/dm/third_party/libdeflate/lib/zlib_constants.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * zlib_constants.h - constants for the zlib wrapper format - */ - -#ifndef LIB_ZLIB_CONSTANTS_H -#define LIB_ZLIB_CONSTANTS_H - -#define ZLIB_MIN_HEADER_SIZE 2 -#define ZLIB_FOOTER_SIZE 4 -#define ZLIB_MIN_OVERHEAD (ZLIB_MIN_HEADER_SIZE + ZLIB_FOOTER_SIZE) - -#define ZLIB_CM_DEFLATE 8 - -#define ZLIB_CINFO_32K_WINDOW 7 - -#define ZLIB_FASTEST_COMPRESSION 0 -#define ZLIB_FAST_COMPRESSION 1 -#define ZLIB_DEFAULT_COMPRESSION 2 -#define ZLIB_SLOWEST_COMPRESSION 3 - -#endif /* LIB_ZLIB_CONSTANTS_H */ diff --git a/dm/third_party/libdeflate/lib/zlib_decompress.c b/dm/third_party/libdeflate/lib/zlib_decompress.c deleted file mode 100644 index f5e43ea..0000000 --- a/dm/third_party/libdeflate/lib/zlib_decompress.c +++ /dev/null @@ -1,104 +0,0 @@ -/* - * zlib_decompress.c - decompress with a zlib wrapper - * - * Copyright 2016 Eric Biggers - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "lib_common.h" -#include "zlib_constants.h" - -LIBDEFLATEAPI enum libdeflate_result -libdeflate_zlib_decompress_ex(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret) -{ - const u8 *in_next = in; - const u8 * const in_end = in_next + in_nbytes; - u16 hdr; - size_t actual_in_nbytes; - size_t actual_out_nbytes; - enum libdeflate_result result; - - if (in_nbytes < ZLIB_MIN_OVERHEAD) - return LIBDEFLATE_BAD_DATA; - - /* 2 byte header: CMF and FLG */ - hdr = get_unaligned_be16(in_next); - in_next += 2; - - /* FCHECK */ - if ((hdr % 31) != 0) - return LIBDEFLATE_BAD_DATA; - - /* CM */ - if (((hdr >> 8) & 0xF) != ZLIB_CM_DEFLATE) - return LIBDEFLATE_BAD_DATA; - - /* CINFO */ - if ((hdr >> 12) > ZLIB_CINFO_32K_WINDOW) - return LIBDEFLATE_BAD_DATA; - - /* FDICT */ - if ((hdr >> 5) & 1) - return LIBDEFLATE_BAD_DATA; - - /* Compressed data */ - result = libdeflate_deflate_decompress_ex(d, in_next, - in_end - ZLIB_FOOTER_SIZE - in_next, - out, out_nbytes_avail, - &actual_in_nbytes, actual_out_nbytes_ret); - if (result != LIBDEFLATE_SUCCESS) - return result; - - if (actual_out_nbytes_ret) - actual_out_nbytes = *actual_out_nbytes_ret; - else - actual_out_nbytes = out_nbytes_avail; - - in_next += actual_in_nbytes; - - /* ADLER32 */ - if (libdeflate_adler32(1, out, actual_out_nbytes) != - get_unaligned_be32(in_next)) - return LIBDEFLATE_BAD_DATA; - in_next += 4; - - if (actual_in_nbytes_ret) - *actual_in_nbytes_ret = in_next - (u8 *)in; - - return LIBDEFLATE_SUCCESS; -} - -LIBDEFLATEAPI enum libdeflate_result -libdeflate_zlib_decompress(struct libdeflate_decompressor *d, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret) -{ - return libdeflate_zlib_decompress_ex(d, in, in_nbytes, - out, out_nbytes_avail, - NULL, actual_out_nbytes_ret); -} diff --git a/dm/third_party/libdeflate/libdeflate.h b/dm/third_party/libdeflate/libdeflate.h deleted file mode 100644 index 1821acd..0000000 --- a/dm/third_party/libdeflate/libdeflate.h +++ /dev/null @@ -1,412 +0,0 @@ -/* - * libdeflate.h - public header for libdeflate - */ - -#ifndef LIBDEFLATE_H -#define LIBDEFLATE_H - -#include <stddef.h> -#include <stdint.h> - -#ifdef __cplusplus -extern "C" { -#endif - -#define LIBDEFLATE_VERSION_MAJOR 1 -#define LIBDEFLATE_VERSION_MINOR 26 -#define LIBDEFLATE_VERSION_STRING "1.26" - -/* - * Users of libdeflate.dll on Windows can define LIBDEFLATE_DLL to cause - * __declspec(dllimport) to be used. This should be done when it's easy to do. - * Otherwise it's fine to skip it, since it is a very minor performance - * optimization that is irrelevant for most use cases of libdeflate. - */ -#ifndef LIBDEFLATEAPI -# if defined(LIBDEFLATE_DLL) && (defined(_WIN32) || defined(__CYGWIN__)) -# define LIBDEFLATEAPI __declspec(dllimport) -# else -# define LIBDEFLATEAPI -# endif -#endif - -/* ========================================================================== */ -/* Compression */ -/* ========================================================================== */ - -struct libdeflate_compressor; -struct libdeflate_options; - -/* - * libdeflate_alloc_compressor() allocates a new compressor that supports - * DEFLATE, zlib, and gzip compression. 'compression_level' is the compression - * level on a zlib-like scale but with a higher maximum value (1 = fastest, 6 = - * medium/default, 9 = slow, 12 = slowest). Level 0 is also supported and means - * "no compression", specifically "create a valid stream, but only emit - * uncompressed blocks" (this will expand the data slightly). Level -1 is an - * alias indicating a default level of 6. - * - * The return value is a pointer to the new compressor, or NULL if out of memory - * or if the compression level is invalid (i.e. outside the range [-1, 12]). - * - * Note: for compression, the sliding window size is defined at compilation time - * to 32768, the largest size permissible in the DEFLATE format. It cannot be - * changed at runtime. - * - * A single compressor is not safe to use by multiple threads concurrently. - * However, different threads may use different compressors concurrently. - */ -LIBDEFLATEAPI struct libdeflate_compressor * -libdeflate_alloc_compressor(int compression_level); - -/* - * Like libdeflate_alloc_compressor(), but adds the 'options' argument. - */ -LIBDEFLATEAPI struct libdeflate_compressor * -libdeflate_alloc_compressor_ex(int compression_level, - const struct libdeflate_options *options); - -/* - * libdeflate_deflate_compress() performs raw DEFLATE compression on a buffer of - * data. It attempts to compress 'in_nbytes' bytes of data located at 'in' and - * write the result to 'out', which has space for 'out_nbytes_avail' bytes. The - * return value is the compressed size in bytes, or 0 if the data could not be - * compressed to 'out_nbytes_avail' bytes or fewer. - * - * If compression is successful, then the output data is guaranteed to be a - * valid DEFLATE stream that decompresses to the input data. No other - * guarantees are made about the output data. Notably, different versions of - * libdeflate can produce different compressed data for the same uncompressed - * data, even at the same compression level. Do ***NOT*** do things like - * writing tests that compare compressed data to a golden output, as this can - * break when libdeflate is updated. (This property isn't specific to - * libdeflate; the same is true for zlib and other compression libraries too.) - */ -LIBDEFLATEAPI size_t -libdeflate_deflate_compress(struct libdeflate_compressor *compressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail); - -/* - * libdeflate_deflate_compress_bound() returns a worst-case upper bound on the - * number of bytes of compressed data that may be produced by compressing any - * buffer of length less than or equal to 'in_nbytes' using - * libdeflate_deflate_compress() with the specified compressor. This bound will - * necessarily be a number greater than or equal to 'in_nbytes'. It may be an - * overestimate of the true upper bound. The return value is guaranteed to be - * the same for all invocations with the same compressor and same 'in_nbytes'. - * - * As a special case, 'compressor' may be NULL. This causes the bound to be - * taken across *any* libdeflate_compressor that could ever be allocated with - * this build of the library, with any options. - * - * Note that this function is not necessary in many applications. With - * block-based compression, it is usually preferable to separately store the - * uncompressed size of each block and to store any blocks that did not compress - * to less than their original size uncompressed. In that scenario, there is no - * need to know the worst-case compressed size, since the maximum number of - * bytes of compressed data that may be used would always be one less than the - * input length. You can just pass a buffer of that size to - * libdeflate_deflate_compress() and store the data uncompressed if - * libdeflate_deflate_compress() returns 0, indicating that the compressed data - * did not fit into the provided output buffer. - */ -LIBDEFLATEAPI size_t -libdeflate_deflate_compress_bound(struct libdeflate_compressor *compressor, - size_t in_nbytes); - -/* - * Like libdeflate_deflate_compress(), but uses the zlib wrapper format instead - * of raw DEFLATE. - */ -LIBDEFLATEAPI size_t -libdeflate_zlib_compress(struct libdeflate_compressor *compressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail); - -/* - * Like libdeflate_deflate_compress_bound(), but assumes the data will be - * compressed with libdeflate_zlib_compress() rather than with - * libdeflate_deflate_compress(). - */ -LIBDEFLATEAPI size_t -libdeflate_zlib_compress_bound(struct libdeflate_compressor *compressor, - size_t in_nbytes); - -/* - * Like libdeflate_deflate_compress(), but uses the gzip wrapper format instead - * of raw DEFLATE. - */ -LIBDEFLATEAPI size_t -libdeflate_gzip_compress(struct libdeflate_compressor *compressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail); - -/* - * Like libdeflate_deflate_compress_bound(), but assumes the data will be - * compressed with libdeflate_gzip_compress() rather than with - * libdeflate_deflate_compress(). - */ -LIBDEFLATEAPI size_t -libdeflate_gzip_compress_bound(struct libdeflate_compressor *compressor, - size_t in_nbytes); - -/* - * libdeflate_free_compressor() frees a compressor that was allocated with - * libdeflate_alloc_compressor(). If a NULL pointer is passed in, no action is - * taken. - */ -LIBDEFLATEAPI void -libdeflate_free_compressor(struct libdeflate_compressor *compressor); - -/* ========================================================================== */ -/* Decompression */ -/* ========================================================================== */ - -struct libdeflate_decompressor; -struct libdeflate_options; - -/* - * libdeflate_alloc_decompressor() allocates a new decompressor that can be used - * for DEFLATE, zlib, and gzip decompression. The return value is a pointer to - * the new decompressor, or NULL if out of memory. - * - * This function takes no parameters, and the returned decompressor is valid for - * decompressing data that was compressed at any compression level and with any - * sliding window size. - * - * A single decompressor is not safe to use by multiple threads concurrently. - * However, different threads may use different decompressors concurrently. - */ -LIBDEFLATEAPI struct libdeflate_decompressor * -libdeflate_alloc_decompressor(void); - -/* - * Like libdeflate_alloc_decompressor(), but adds the 'options' argument. - */ -LIBDEFLATEAPI struct libdeflate_decompressor * -libdeflate_alloc_decompressor_ex(const struct libdeflate_options *options); - -/* - * Result of a call to libdeflate_deflate_decompress(), - * libdeflate_zlib_decompress(), or libdeflate_gzip_decompress(). - */ -enum libdeflate_result { - /* Decompression was successful. */ - LIBDEFLATE_SUCCESS = 0, - - /* Decompression failed because the compressed data was invalid, - * corrupt, or otherwise unsupported. */ - LIBDEFLATE_BAD_DATA = 1, - - /* A NULL 'actual_out_nbytes_ret' was provided, but the data would have - * decompressed to fewer than 'out_nbytes_avail' bytes. */ - LIBDEFLATE_SHORT_OUTPUT = 2, - - /* The data would have decompressed to more than 'out_nbytes_avail' - * bytes. */ - LIBDEFLATE_INSUFFICIENT_SPACE = 3, -}; - -/* - * libdeflate_deflate_decompress() decompresses a DEFLATE stream from the buffer - * 'in' with compressed size up to 'in_nbytes' bytes. The uncompressed data is - * written to 'out', a buffer with size 'out_nbytes_avail' bytes. If - * decompression succeeds, then 0 (LIBDEFLATE_SUCCESS) is returned. Otherwise, - * a nonzero result code such as LIBDEFLATE_BAD_DATA is returned, and the - * contents of the output buffer are undefined. - * - * Decompression stops at the end of the DEFLATE stream (as indicated by the - * BFINAL flag), even if it is actually shorter than 'in_nbytes' bytes. - * - * libdeflate_deflate_decompress() can be used in cases where the actual - * uncompressed size is known (recommended) or unknown (not recommended): - * - * - If the actual uncompressed size is known, then pass the actual - * uncompressed size as 'out_nbytes_avail' and pass NULL for - * 'actual_out_nbytes_ret'. This makes libdeflate_deflate_decompress() fail - * with LIBDEFLATE_SHORT_OUTPUT if the data decompressed to fewer than the - * specified number of bytes. - * - * - If the actual uncompressed size is unknown, then provide a non-NULL - * 'actual_out_nbytes_ret' and provide a buffer with some size - * 'out_nbytes_avail' that you think is large enough to hold all the - * uncompressed data. In this case, if the data decompresses to less than - * or equal to 'out_nbytes_avail' bytes, then - * libdeflate_deflate_decompress() will write the actual uncompressed size - * to *actual_out_nbytes_ret and return 0 (LIBDEFLATE_SUCCESS). Otherwise, - * it will return LIBDEFLATE_INSUFFICIENT_SPACE if the provided buffer was - * not large enough but no other problems were encountered, or another - * nonzero result code if decompression failed for another reason. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_deflate_decompress(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret); - -/* - * Like libdeflate_deflate_decompress(), but adds the 'actual_in_nbytes_ret' - * argument. If decompression succeeds and 'actual_in_nbytes_ret' is not NULL, - * then the actual compressed size of the DEFLATE stream (aligned to the next - * byte boundary) is written to *actual_in_nbytes_ret. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_deflate_decompress_ex(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret); - -/* - * Like libdeflate_deflate_decompress(), but assumes the zlib wrapper format - * instead of raw DEFLATE. - * - * Decompression will stop at the end of the zlib stream, even if it is shorter - * than 'in_nbytes'. If you need to know exactly where the zlib stream ended, - * use libdeflate_zlib_decompress_ex(). - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_zlib_decompress(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret); - -/* - * Like libdeflate_zlib_decompress(), but adds the 'actual_in_nbytes_ret' - * argument. If 'actual_in_nbytes_ret' is not NULL and the decompression - * succeeds (indicating that the first zlib-compressed stream in the input - * buffer was decompressed), then the actual number of input bytes consumed is - * written to *actual_in_nbytes_ret. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_zlib_decompress_ex(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret); - -/* - * Like libdeflate_deflate_decompress(), but assumes the gzip wrapper format - * instead of raw DEFLATE. - * - * If multiple gzip-compressed members are concatenated, then only the first - * will be decompressed. Use libdeflate_gzip_decompress_ex() if you need - * multi-member support. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_gzip_decompress(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_out_nbytes_ret); - -/* - * Like libdeflate_gzip_decompress(), but adds the 'actual_in_nbytes_ret' - * argument. If 'actual_in_nbytes_ret' is not NULL and the decompression - * succeeds (indicating that the first gzip-compressed member in the input - * buffer was decompressed), then the actual number of input bytes consumed is - * written to *actual_in_nbytes_ret. - */ -LIBDEFLATEAPI enum libdeflate_result -libdeflate_gzip_decompress_ex(struct libdeflate_decompressor *decompressor, - const void *in, size_t in_nbytes, - void *out, size_t out_nbytes_avail, - size_t *actual_in_nbytes_ret, - size_t *actual_out_nbytes_ret); - -/* - * libdeflate_free_decompressor() frees a decompressor that was allocated with - * libdeflate_alloc_decompressor(). If a NULL pointer is passed in, no action - * is taken. - */ -LIBDEFLATEAPI void -libdeflate_free_decompressor(struct libdeflate_decompressor *decompressor); - -/* ========================================================================== */ -/* Checksums */ -/* ========================================================================== */ - -/* - * libdeflate_adler32() updates a running Adler-32 checksum with 'len' bytes of - * data and returns the updated checksum. When starting a new checksum, the - * required initial value for 'adler' is 1. This value is also returned when - * 'buffer' is specified as NULL. - */ -LIBDEFLATEAPI uint32_t -libdeflate_adler32(uint32_t adler, const void *buffer, size_t len); - - -/* - * libdeflate_crc32() updates a running CRC-32 checksum with 'len' bytes of data - * and returns the updated checksum. When starting a new checksum, the required - * initial value for 'crc' is 0. This value is also returned when 'buffer' is - * specified as NULL. - */ -LIBDEFLATEAPI uint32_t -libdeflate_crc32(uint32_t crc, const void *buffer, size_t len); - -/* ========================================================================== */ -/* Custom memory allocator */ -/* ========================================================================== */ - -/* - * Install a custom memory allocator which libdeflate will use for all memory - * allocations by default. 'malloc_func' is a function that must behave like - * malloc(), and 'free_func' is a function that must behave like free(). - * - * The per-(de)compressor custom memory allocator that can be specified in - * 'struct libdeflate_options' takes priority over this. - * - * This doesn't affect the free() function that will be used to free - * (de)compressors that were already in existence when this is called. - */ -LIBDEFLATEAPI void -libdeflate_set_memory_allocator(void *(*malloc_func)(size_t), - void (*free_func)(void *)); - -/* - * Advanced options. This is the options structure that - * libdeflate_alloc_compressor_ex() and libdeflate_alloc_decompressor_ex() - * require. Most users won't need this and should just use the non-"_ex" - * functions instead. If you do need this, it should be initialized like this: - * - * struct libdeflate_options options; - * - * memset(&options, 0, sizeof(options)); - * options.sizeof_options = sizeof(options); - * // Then set the fields that you need to override the defaults for. - */ -struct libdeflate_options { - - /* - * This field must be set to the struct size. This field exists for - * extensibility, so that fields can be appended to this struct in - * future versions of libdeflate while still supporting old binaries. - */ - size_t sizeof_options; - - /* - * An optional custom memory allocator to use for this (de)compressor. - * 'malloc_func' must be a function that behaves like malloc(), and - * 'free_func' must be a function that behaves like free(). - * - * This is useful in cases where a process might have multiple users of - * libdeflate who want to use different memory allocators. For example, - * a library might want to use libdeflate with a custom memory allocator - * without interfering with user code that might use libdeflate too. - * - * This takes priority over the "global" memory allocator (which by - * default is malloc() and free(), but can be changed by - * libdeflate_set_memory_allocator()). Moreover, libdeflate will never - * call the "global" memory allocator if a per-(de)compressor custom - * allocator is always given. - */ - void *(*malloc_func)(size_t); - void (*free_func)(void *); -}; - -#ifdef __cplusplus -} -#endif - -#endif /* LIBDEFLATE_H */ diff --git a/dm/tools/patch_nso.py b/dm/tools/patch_nso.py index eaa1b25..6dcff18 100644 --- a/dm/tools/patch_nso.py +++ b/dm/tools/patch_nso.py @@ -48,8 +48,6 @@ else: PATCH_OFF = (len(IMG) + 0xFFFF) & ~0xFFFF # ---- Registered cuts: {NSO build ID: [(offset, original bytes, new bytes, reason)]} ---- -# Networking cuts (excluded scope: Battle.net / online play): taken from a community offline fix (LFS Mod Offline Fix for v2228224, which differs from the official 94012 main in only these 7 words). -# Measured: the system service layer (nifm offline / DNS black hole / account service) cannot get around it: the game itself sends Battle.net requests directly and allows offline play according to the "last online" time. CUTS = { # Super Mario Bros. Wonder v0 main 'cd6e42aee7934f4d8393cd43893ae25e': [ @@ -63,78 +61,6 @@ CUTS = { 'ff773e90972d544eb79406eaa65396d5': [ (0x1cf9b0, 'e00317aa', 'e0124091', 'resource heap size: mov x0,x23 → add x0,x23,#0x4000 (ExpHeap::create size argument +16 KB)'), ], - 'db0507f36b84200ac05f75d76ed5026f': [ - # (Placed in the base rewrite shared by all build outputs): game video memory allocator budget 260 MB → 512 MB - (0x8524c4, '0808a2d2', '0800a4d2', 'video memory allocator init config: mov x8,#0x10400000 (260 MB) → mov x8,#0x20000000 (512 MB); the following movk #0xa0,lsl48 and str [sp,#0x238] are unchanged; ' - 'under the original budget an internal resolution above about 3.45 million pixels (2736x1260 passes, 2864x1240 crashes) leaves no room for the render targets: main+0x174b018 returns null and main+0x17417a4 crashes reading it; ' - 'only a video memory budget, it does not enter game logic: the cvd5 tick-by-tick comparison matches at both 1600x900 and 2868x1320'), - (0x1979bc, 'e8a79f1a', 'e8d79f1a', 'offline grace window: reads the last-online time of a save data settings key (the string at 0x1d53e80); ' - '"now − last online < 0x0009356907420000 ns (= 30 days)" sets [x20+0x2c]; lt→gt'), - (0x5b14dc, 'e8a79f1a', 'e8d79f1a', 'calls 0xddab50 (buffer [x10+0x138]+x25, remaining length, callback 0x527d50, table 0x209cdb8, 9 entries) and sets the flag when it returns < 1; ' - 'when set the 0x5b14f8 branch is taken; lt→gt (its meaning was not determined from strings)'), - (0x5f3184, 'e8a79f1a', 'e8d79f1a', 'data table [0x2d64a18+idx*16] → +0xf90 record (0x1fc bytes) +0x4c index → +0xfd0 record (0x128 bytes) +0xf1 signed byte < 0 sets the flag, ' - 'which is ANDed with (w26>0x24) and passed on as the 4th argument; lt→gt (its meaning was not determined from strings)'), - (0x1d53e80, '55736572204c617374204f6e6c696e6500', '566f6963654c616e677561676500000000', - 'read-only string "User Last Online" → "VoiceLanguage": the first site reads a settings key that always exists instead, "last online" gets a small value, the difference always exceeds 30 days, and with gt it always passes'), - # Product cut: the opening Logo and the "Press A to Start" page are cut, and startup goes straight to the main menu (the loading screen is kept) - (0x8019ec, '280f0034', 'e8feff34', 'front-end boot first-entry branch: cbz w8,#0x801bd0 (LogoMenuPanel → logo video → title page) becomes cbz w8,#0x8019c8' - ' (landing on the direct path 0x23e5e0(1) → 0xb156a0 into the main menu; it skips the two instructions from 0x8019bc that set online mode [0x2d7badc]=1, so online mode keeps its startup value 0 = offline, ' - 'and the network check at 0x80ec44, the disconnected popup and the return to the title page are not triggered); the r4 target 0x8019bc set online mode 1, and in testing it still returned to the title page with the disconnected popup'), - # Cuts for host-native implementations (host only: the ground-truth emulator does not apply them, Ryujinx does not know host call numbers from 0x80 up; equivalence is proven by per-block decoding matching 5511/5511) - (0x1b0ee60, 'fc6fbaa9fa6701a9', '011000d4c0035fd6', 'TACT BLTE block zlib decode entry (DecodeZlib) → svc #0x80 ; ret: the host implements it with libdeflate' - ' (hle_tact.c, call number registered in hle.h SVC_HOST_TACT_ZLIB); the rewriter applies cuts before scanning, so svc #0x80 becomes a host gate by itself, the gate block saves and restores x30, and ret returns to the original caller', 'host-only'), - # Hook points for host test switches (host only): by default the host forwards as is, behavior unchanged - (0x5d3a88, 'a27a0194', '211000d4', 'call site in the receive pump 0x5d3960 that delivers client command packets, bl 0x632510 → svc #0x81: by default the host calls 0x632510 with the original arguments (the caller ignores the return value); ' - 'with DM_C2S_REPLAY the commands the client issues itself are dropped and the tick entry injects them from the file instead (diag.c, call number registered in hle.h SVC_HOST_C2S_DELIVER)', 'host-only'), - (0x5d39c8, 'ba750194', '411000d4', 'call site in the receive pump 0x5d3960 that delivers system messages, bl 0x6310b0 → svc #0x82: by default the host calls 0x6310b0 with the original arguments (the caller ignores the return value); ' - 'with DM_C2S_REPLAY, from the first tick of the replay file on, the system messages the host client issues itself are dropped (join messages before that pass) and the tick entry injects them from the file instead (diag.c, call number registered in hle.h SVC_HOST_C2S_SYS)', 'host-only'), - # Screen aspect (host only, together with the resolution parameter slots in PARAMS below): output resolution cap {u32 width, u32 height}, read or written at only three places in resolution selection - # (0xe12e28 / 0xe10d70 read: if the selected value's width or height exceeds the cap, no switch is made; 0xe12f54 writes it to the selected value after initialization finishes selecting); initial value 1920x1080 → 4096x2160, so that a width widened to the screen aspect can pass - (0x211da30, '8007000038040000', '0010000070080000', 'output resolution cap initial value 1920x1080 → 4096x2160 (data segment): the width the host derives from the screen aspect ratio (e.g. 1956x900 on a 19.5:9 iPhone)' - ' is chosen only if it does not exceed the cap; the Mac front end still gives the original 1600x900 / 1280x720, behavior unchanged', 'host-only'), - # The width that the operation-mode notification handler (0xe10d30) gives to the "window size changed" event: original code mov w9,#0x740; and x9,x25,x9 -- the compiler knew the width could only be 1280 (0x500) / 1600 (0x640), - # and optimized "take the low 32 bits" into & (0x500|0x640). The KRES0 / KRES1 parameter slots replaced those two constants, but this derived mask was not changed along with them: 2868 & 0x740 = 768, 2296 & 0x740 = 64 - # (after switching docked / handheld while running, the output width was 64 / 768; the iOS overheating downgrade takes exactly this path). Changed to take the low 32 bits directly: same result for the original game's two widths, correct for any width - (0xe10e48, '09e880522903098a', 'e903192a1f2003d5', 'width in the window-size event sent by the operation-mode notification: mov w9,#0x740; and x9,x25,x9 → mov w9,w25; nop (takes the low 32 bits instead of the constant mask derived from the original two widths)', 'host-only'), - (0xe12e88, '09e880528902098a', 'e903142a1f2003d5', 'the same mask in the startup resolution choice (0xe12d70): mov w9,#0x740; and x9,x20,x9 → mov w9,w20; nop (found by the structck check of masks downstream of the parameter slots; at startup the event receiver usually does not exist yet, so it rarely shows)', 'host-only'), - # Product cut (reproducing the exclusion list item "classic picture toggle"): the two state writes of the classic picture toggle 0xb15d60 → nop. - # The picture transition state starts at 0x2dc0758 (+4 start, +8 target, +0xc progress; 1.0 = new picture); in the whole program the only writers are 0x852280 (startup init writes 1), 0x85cb20 (force back to the new picture, writes 1), - # 0x856be0 (writes only +0xc) and 0xb15d60 (when not in a run, or when the run version is 1 / 2, flips the target between 0 / 1 according to the current coefficient) -- the last is the only place that can push the target toward 0; - # the callers 0x36a5a0 / 0x4035e8 / 0xc25f48 / 0xc2725c are all toggle actions (the video option LegacyGraphicsMode, the character creation page's ToggleLegacy, the console version's ClassicToggle and the key LegacyToggle all converge here). - # What is cut is two plain stores, containing no calls: the list of synchronization primitives and external side effects of the cut part is empty (the cut rule). The hint branch for run version 3 in a run (from 0xb15e2c) is untouched - (0xb15e20, '0281002d', '1f2003d5', 'classic picture toggle writes start / target (stp s2, s0, [x8, #4]) → nop: the target stays 1.0 (new picture)'), - (0xb15e24, '1f0d00b9', '1f2003d5', 'classic picture toggle clears progress (str wzr, [x8, #0xc]) → nop'), - # Widescreen (host only, together with the KASPECT aspect ratio cap in PARAMS): the cap in UI canvas width = min(screen width, 4560 = 2160 × 19/9) is opened up to 0xffff. - # In the original game at 16:9 the screen width of 3840 units < 4560, so the result is unchanged; on iPhone 1952x900 the screen width is about 4685 units, which used to be clamped to 4560, leaving about 26 pixels on each side - (0xb1a660, '083a8252', 'e8ff9f52', 'UI canvas width cap mov w8, #4560 → mov w8, #0xffff (canvas width = screen width)', 'host-only'), - # (Reverted: the cuts 0x3172f8 / 0x3173e4 for "SetLanguage 0x317298 clear + reload string tables" in place since r13 / r15 (originally set up to exclude languages other than Simplified Chinese), - # reverted because of the English version -- switching language in the original game relies on this reload.) - # Product cut: the loading of string table A (legacy, 0x2d70ff0 / 0x2d71010) is skipped as a whole. The load function 0x7f64f0(w0 = whether to load legacy) - # branches at the tbz w23 at 0x7f669c: when not loading, the success flag is set to 1 (0x7f6800), the same result as all 17 strings-legacy/*.json loading successfully. - # No-reader proof: of the 128 functions in the whole program that reference table A, 124 also read the table-select flag [0x2d71098] and select the table by "flag = 0 → table B (0x2d71030 / 0x2d71050)" - # (e.g. the csel at 0x13b7d8); the other 4 are the table's own static constructor 0x7f8f80, load 0x7f64f0, table insert 0x7f72e0 and clear 0x7f5b60. The flag's only write site 0xb1bcf4 - # writes !obj[0xb9], and obj[0xb9] is obtained by comparing the picture transition state 0x2dc0758 with 1.0 -- r14 already cut the classic picture toggle, the only thing that can push the target away from 1.0; the flag is in .bss - # (initial value 0), and the probe at 0xb1bcfc over the whole r15 cvd5 run hit 0 times (the write site never executed) ⇒ only table B is read throughout. The cut tree = 17 single-file loads via 0x7f72e0; same as the supplementary proof of - # the language reload cut above (the same 0xdf67f0 read transaction, paired, no external waiters) - (0x7f669c, '370b0036', '59000014', 'string table load tbz w23, #0 (whether to load legacy table A) → b 0x7f6800: table A has no readers (the table-select flag is always 0 → table B), so none of the 17 legacy files is loaded'), - # The per-block decode call sites of the two streaming segment read paths of Granny segment decoding (BitKnit2): by default the host forwards as is to 0x11f4af0 (the replacement hook point of pre-decode plan A, hle_granny.c) - (0x11f02dc, '05120094', '611000d4', 'segment read 0x11f0110 per-block decode bl 0x11f4af0(ctx, block, length) → svc #0x83: by default the host calls it with the original arguments and writes back w0 (hle.h SVC_HOST_GRANNY_A)', 'host-only'), - # Drop light (host only): the item's appearance definition path lookup bl 0xa14dc4(resource manager, unit, x8 output string) → svc #0x87: the host forwards the call as is, - # and for items in mode 3 / 5 (on the ground / dropping) repoints the path by rule to the lit variant in the overlay (dm_dropfilter.c svc_ground_asset; hle.h SVC_HOST_GROUND_ASSET) - # Tier-one drop sound (host only): fetching the drop sound on landing bl 0x470c20(item, &trigger frame) → svc #0x88: the host forwards the call as is, and tier-one items get a notification sound instead (dm_dropfilter.c svc_drop_sound) - (0x468f2c, '3d1f0094', '011100d4', 'item landing drop-sound call site bl 0x470c20 → svc #0x88: the host makes the original call, then by rule replaces the drop sound of tier-one items with a notification sound (others unchanged)', 'host-only'), - (0x8fa590, '0d6a0494', 'e11000d4', 'item appearance path call site bl 0xa14dc4 → svc #0x87: the host makes the original call, then by rule points items on the ground at their lit variant (stashed / worn / belt / socketed untouched)', 'host-only'), - # Run-entry spawn point (entering a run lands next to the town waypoint, with the corpse in the same place as the player; host only): the player join function 0x5d7f70 fetches a spawn point once for the player and once for the corpse, both via bl 0x5596c0(data version, act, level, cell type 0, &x, &y, unit size) - # → svc #0x8a (player) / #0x8b (corpse): the host forwards the call as is; when the run is entered in town it instead picks the point by "waypoint cell 13 + a fixed safe distance", and the player, corpse and mercenary are placed in one go by the original game (the player is no longer moved by a level change on tick 2, and the corpse no longer stays at the original spawn point) (dm_townwp.c svc_join_spawn) - (0x5d8040, 'a005fe97', '411100d4', 'run-entry player spawn point call site bl 0x5596c0 → svc #0x8a: the host makes the original call, and when the run is entered in town the spawn point moves next to the waypoint (hle.h SVC_HOST_JOIN_SPAWN)', 'host-only'), - (0x5d8400, 'b004fe97', '611100d4', 'run-entry corpse placement call site bl 0x5596c0 → svc #0x8b: as above, the corpse gets the same point as the player (hle.h SVC_HOST_JOIN_CORPSE)', 'host-only'), - (0x11f0914, '77100094', '811000d4', 'segment read 0x11f07d0 per-block decode bl 0x11f4af0(ctx, block, length) → svc #0x84: as above (hle.h SVC_HOST_GRANNY_B)', 'host-only'), - ], - # Fire Emblem Engage 2.0.0 (v327680) sdk: AddressRegionManager node area 0x3fe00000 (about 1 GB) → 0x3e00000 (62 MB). - # The constructor (sdk+0x346420) carves the node area out of the start of the alias region (a unit heap of 0x30-byte nodes, mapped only as used) before AllocateAddressRegion hands out regions. - # The hardware alias region is 64 GB, so the 1 GB does not matter there; the host's alias region is only 5.375 GB (iOS caps a single reservation at 6 GB), and after StandardAllocator's 3.14 GB block (1 GB aligned) - # Unity's low-level virtual allocator (main+0x9aa0f4 → AllocateAddressRegion) had only about 1.2 GB left; about 357 s into the prologue battle it got no region → returned 0 → null write crash at main+0x9a4664. - # 62 MB holds about 1.35 million nodes (a few hundred regions are live in measurements). Both movs must change together: +0x346430 sets the node area limit, +0x3464cc registers it as an occupied region. # Fire Emblem Engage 2.0.0 (v327680) main: run the game's own 60 fps path (vertical sync count always 1). # The game's time class App.GameTime picks a vertical sync count per scene (Slow = 2, i.e. 30 fps: system, movies, demos, combat cutscenes, the hub; Normal is 1 only when GameConfig.IsPerformance) # and hands it to Unity through GameTime.SetVsyncCount(int) (QualitySettings.vSyncCount → NVN present interval). About 96 pieces of per-frame logic (cameras, map cursor, unit actors, menu scrolling and cursors, hub camera, minigames) @@ -143,7 +69,7 @@ CUTS = { # The cut is in Unity's managed wrapper QualitySettings.set_vSyncCount (main+0x32f92c0): both paths use w19 as the argument, mov w19,w0 → mov w19,#1, # so every caller (GameTime, QualitySettingsStack.PopImpl, XRSystem) can only set 1; the read side (get_vSyncCount) follows at 1 and the game stays consistent. No code calls SetQualityLevel. '8c08b9719e085f91847b5e0f935d9488': [ - (0x32f92d4, 'f303002a', '33008052', 'QualitySettings.set_vSyncCount wrapper: argument mov w19,w0 → mov w19,#1 (the 60 fps path of the game itself)', 'host-only'), + (0x32f92d4, 'f303002a', '33008052', 'QualitySettings.set_vSyncCount wrapper: argument mov w19,w0 → mov w19,#1 (the 60 fps path of the game itself)'), ], # The Legend of Zelda: Breath of the Wild 1.9.0 (v1114112) main: 60 fps. The main loop of sead::GameFrameworkNx (main+0x112660c) runs a dynamic interval (framework +0x267 = 1): # every frame it computes how many vsyncs the last frame needed (rounded up), clamped to [2, max 3] (the max comes from the interval controller +0xc, constructed at main+0x1126c04); it steps up after 3 overrunning frames in a row, @@ -151,29 +77,35 @@ CUTS = { # The game logic time step follows it: ksys::VFR is initialized with a base interval of 2 (constant 2 at main+0x127e740 → VFR::init main+0x151d2d0), and every frame VFR::updateInterval (main+0x151d3d0, # called at main+0xcf9ad4 / main+0x1281044) takes the framework's current interval (vtable +0x138); the frame ratio = current interval / 2 -- at interval 1 each frame advances half a 30 fps tick and the game speed stays the same; # when it struggles and steps back to 2 / 3 the ratio is 1 / 1.5, again at the same speed. So only the floor changes from 2 to 1: the lower bound of the candidate interval (two places), the step-down target recorded when stepping up, max(candidate, 3) - 1 → max(candidate, 2) - 1, - # and stepping down stops at < 3 → < 2. On hardware the same logic starting from 1 would also be 60 fps; it falls back to 30 by itself only when its input shows a slow frame (the last two cuts make that input include the GPU time) + # and stepping down stops at < 3 → < 2. On hardware the same logic starting from 1 would also be 60 fps; it falls back to 30 by itself only when its input shows a slow frame (the last two cuts make that input include the GPU time). + # With the frame step (FRAME_STEP below) the host's step replaces the controller's interval, except while the game's own VFR override is set (and at fps_target 0): the framework then keeps the controller's 'cd57b23fa4bbad65803d9788c01821ee': [ - (0x112670c, '6041201e', '4041201e', 'lower bound of the candidate interval: fmov s0,s11 (2.0) → fmov s0,s10 (1.0)', 'host-only'), - (0x1126710, '7f0b0071', '7f070071', 'lower bound of the candidate interval: cmp w27,#2 → cmp w27,#1', 'host-only'), - (0x112665c, '78008052', '58008052', 'step-down target recorded when stepping up, max(candidate, w24) − 1: mov w24,#3 → mov w24,#2', 'host-only'), - (0x1126748, '1f0d0071', '1f090071', 'step-down target recorded when stepping up: cmp w8,#3 → cmp w8,#2', 'host-only'), - (0x1126794, '3f0d0071', '3f090071', 'stepping down stops at < 3 → at < 2: cmp w9,#3 → cmp w9,#2', 'host-only'), + (0x112670c, '6041201e', '4041201e', 'lower bound of the candidate interval: fmov s0,s11 (2.0) → fmov s0,s10 (1.0)'), + (0x1126710, '7f0b0071', '7f070071', 'lower bound of the candidate interval: cmp w27,#2 → cmp w27,#1'), + (0x112665c, '78008052', '58008052', 'step-down target recorded when stepping up, max(candidate, w24) − 1: mov w24,#3 → mov w24,#2'), + (0x1126748, '1f0d0071', '1f090071', 'step-down target recorded when stepping up: cmp w8,#3 → cmp w8,#2'), + (0x1126794, '3f0d0071', '3f090071', 'stepping down stops at < 3 → at < 2: cmp w9,#3 → cmp w9,#2'), # The controller's input (framework +0x250, controller main+0x1126cec..) is the main thread's CPU span up to the draw step (about 2.7 ms in the first shrine), so GPU-bound frames stayed at interval 1 and the game ran slow # (iPhone 16 Pro Max: 25 ms frames, two thirds of the speed). The game has a GPU-time path that uses max(CPU span, GPU time between the frame's two timestamp reports, framework +0x220, read after the fence wait) # but takes it only while [main+0x1e3a638]+0x1d2b8 is set (64 of 6,046 shrine frames); both of its checks become branches into it. The GPU time needs the profile's gpu_timestamps segment - (0x112691c, 'c00000b4', '03000014', 'interval controller always takes the GPU time path (CPU span timing): cbz x0,#0x1126934 → b #0x1126928', 'host-only'), - (0x1126974, 'c00000b4', '03000014', 'interval controller always takes the GPU time path (takes max(CPU span, GPU time)): cbz x0,#0x112698c → b #0x1126980', 'host-only'), + (0x112691c, 'c00000b4', '03000014', 'interval controller always takes the GPU time path (CPU span timing): cbz x0,#0x1126934 → b #0x1126928'), + (0x1126974, 'c00000b4', '03000014', 'interval controller always takes the GPU time path (takes max(CPU span, GPU time)): cbz x0,#0x112698c → b #0x1126980'), ], + # Fire Emblem Engage 2.0.0 (v327680) sdk: AddressRegionManager node area 0x3fe00000 (about 1 GB) → 0x3e00000 (62 MB). + # The constructor (sdk+0x346420) carves the node area out of the start of the alias region (a unit heap of 0x30-byte nodes, mapped only as used) before AllocateAddressRegion hands out regions. + # The hardware alias region is 64 GB, so the 1 GB does not matter there; the host's alias region is only 5.375 GB (iOS caps a single reservation at 6 GB), and after StandardAllocator's 3.14 GB block (1 GB aligned) + # Unity's low-level virtual allocator (main+0x9aa0f4 → AllocateAddressRegion) had only about 1.2 GB left; about 357 s into the prologue battle it got no region → returned 0 → null write crash at main+0x9a4664. + # 62 MB holds about 1.35 million nodes (a few hundred regions are live in measurements). Both movs must change together: +0x346430 sets the node area limit, +0x3464cc registers it as an occupied region. '56ec24747ec9e7f19b3efb062483a19d': [ - (0x346430, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000', 'host-only'), - (0x3464cc, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000', 'host-only'), + (0x346430, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000'), + (0x3464cc, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000'), ], # Ace Attorney Investigations Collection 1.0.0.2 (v131072) sdk (SDK 17.5.3): the same node area cut as Engage's. Without it nninitStartup's StandardAllocator::Initialize(nullptr, pool − used) # (main+0x874b6c, virtual address memory) aborts at its first CentralHeap::Init: the 1 GB-aligned region of 3.14 GB did not fit in the 4.375 GB alias region after the node area. # The constructor is at sdk+0x139458 (limit = alias start + 0x3fe00000 at +0x28, a unit heap of 0x30-byte nodes, the area then registered as occupied) '311deab1f062b504f3d646a49ddcd4e8': [ - (0x139468, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000', 'host-only'), - (0x1394fc, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000', 'host-only'), + (0x139468, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000'), + (0x1394fc, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000'), ], # The Legend of Zelda: Link's Awakening 1.1.0 (v131072) main: the physics step (main+0x15730, main thread) rebuilds each world's PhysX completion task in place # (a PxLightCpuTask at world+0xc8) and passes it to simulate. A worker runs it (main+0x10ccf80: fetchResults, world+0x1d8 done = 1, notify, unlock) and then calls its @@ -184,7 +116,7 @@ CUTS = { # unlock within nanoseconds. The release becomes a plain ret: the destructor only resets the vtable and mTm (no resources), the step rebuilds every field, and this # release is used by this one vtable (main+0x1c51308 slot 7) only '909e904af78ac1b8deefe97ab2ccdb51': [ - (0x10cd070, '080040f9', 'c0035fd6', 'physics completion task release (calls its own destructor): ldr x8,[x0] → ret (no longer destroyed; the step function rebuilds it in place every step)', 'host-only'), + (0x10cd070, '080040f9', 'c0035fd6', 'physics completion task release (calls its own destructor): ldr x8,[x0] → ret (no longer destroyed; the step function rebuilds it in place every step)'), ], # Fire Emblem: Three Houses 1.2.0 (v327680) main: 60 fps. KTGL runs the game on the main thread at 60 updates a second whatever it shows: each update steps by # 1/fps with fps = 60.0 (main+0x1a38f08, set at main+0x50e114; dt at main+0x510010), and the frame end (main+0x50f680) waits out 60/fps ticks of 1/59.94 s in a yield loop @@ -194,28 +126,28 @@ CUTS = { # float at main+0xd13e40, the per-frame scroll step in main+0x4c2b30) becomes 1 for every mode, as in modes 1 and 4: every update is shown and the render thread's # own steps stay at the same speed. Resolutions stay per mode '89048449ba238c8cf565518b83bf02d3': [ - (0xd13de0, '020000000100000002000000020000000100000002000000', '010000000100000001000000010000000100000001000000', 'per-render-mode present interval table {2,1,2,2,1,2} → all 1 (60 fps; the game logic already runs at 60 updates a second)', 'host-only'), - (0xd13e40, '000000400000803f00000040000000400000803f00000040', '0000803f0000803f0000803f0000803f0000803f0000803f', 'per-render-mode interval as float {2.0,1.0,2.0,2.0,1.0,2.0} → all 1.0 (render-thread per-frame steps)', 'host-only'), + (0xd13de0, '020000000100000002000000020000000100000002000000', '010000000100000001000000010000000100000001000000', 'per-render-mode present interval table {2,1,2,2,1,2} → all 1 (60 fps; the game logic already runs at 60 updates a second)'), + (0xd13e40, '000000400000803f00000040000000400000803f00000040', '0000803f0000803f0000803f0000803f0000803f0000803f', 'per-render-mode interval as float {2.0,1.0,2.0,2.0,1.0,2.0} → all 1.0 (render-thread per-frame steps)'), # The render thread still presents with interval 2 while either of two flags is set: main+0x21c9a03, a full-screen image drawn before the frame (main+0x4b4908: the opening movie), # and frame +0xc55f / +0xc560 (from the object at app +0x40: the character creation screen after the movie, and so on). In the engine's own 60 fps modes this traded 60 fps for # resolution in those scenes (main+0x50f7d0 drops 720p mode 1 to 1080p mode 0); here it would show them at 30 with the render thread's own steps at half speed. Both are ignored - (0x50fb7c, '2801082a', 'e8031f2a', 'render thread forced interval 2: orr w8,w9,w8 (either flag) → mov w8,wzr (the mode\'s interval, 1)', 'host-only'), + (0x50fb7c, '2801082a', 'e8031f2a', 'render thread forced interval 2: orr w8,w9,w8 (either flag) → mov w8,wzr (the mode\'s interval, 1)'), # The frame limiter's wait (main+0x5be430: yield, read the tick, until 60/fps ticks of 1/59.94 s have passed since the last frame end; x8 = time passed and x23 = target, in 100 ns) # is a busy loop on the host: the yield back-off spins its first 2 ms on a performance core and then oversleeps by up to 250 µs, so frames took 16.8 ms (59.5 fps, a repeated # display frame every 2 s) and CPU energy went from 0.16 W at 30 fps to 1.5 W on the Mac. The yield becomes nn::os::SleepThread of 96% of the time left, and the loop goes back to # the limiter's own tick read and comparison at main+0x5be44c (the instructions after the b are no longer reached); the host's sleep never oversleeps, so the frames take 16.68 ms - (0x5be4c0, '20fa1494', 'e00208cb', 'frame limiter wait: bl YieldThread → sub x0,x23,x8 (time left, 100 ns units)', 'host-only'), - (0x5be4c4, 'bbf71494', '01e87bd3', 'frame limiter wait: bl GetSystemTick → lsl x1,x0,#5', 'host-only'), - (0x5be4c8, '680240f9', '2018008b', 'frame limiter wait: ldr x8,[x19] → add x0,x1,x0,lsl #6 (96 ns per 100 ns left)', 'host-only'), - (0x5be4cc, '080008cb', '8df71494', 'frame limiter wait: sub x8,x0,x8 → bl nn::os::SleepThread (PLT main+0xafc300)', 'host-only'), - (0x5be4d0, '087d149b', 'dfffff17', 'frame limiter wait: mul x8,x8,x20 → b main+0x5be44c (read the tick and compare again)', 'host-only'), + (0x5be4c0, '20fa1494', 'e00208cb', 'frame limiter wait: bl YieldThread → sub x0,x23,x8 (time left, 100 ns units)'), + (0x5be4c4, 'bbf71494', '01e87bd3', 'frame limiter wait: bl GetSystemTick → lsl x1,x0,#5'), + (0x5be4c8, '680240f9', '2018008b', 'frame limiter wait: ldr x8,[x19] → add x0,x1,x0,lsl #6 (96 ns per 100 ns left)'), + (0x5be4cc, '080008cb', '8df71494', 'frame limiter wait: sub x8,x0,x8 → bl nn::os::SleepThread (PLT main+0xafc300)'), + (0x5be4d0, '087d149b', 'dfffff17', 'frame limiter wait: mul x8,x8,x20 → b main+0x5be44c (read the tick and compare again)'), # The render thread waits for the next update with KTGL's event wait (main+0xaa90e0, timeout in µs: × 1000 → nn::os::TimedWaitEvent) and passes 1/fps × 1000 = 16, one frame in ms, # so it polls every 16 µs: at 60 fps it draws in about 3 ms and then polled for the rest of the frame (42,000–53,000 wakeups a second on the Mac, against 1,200 at 30 fps, where # it waited in the present instead). The other four callers wait without a timeout. The timeout becomes 1000/fps × 1000 = 16,666 µs (s8 = 1000.0, loaded first) - (0x50f8e8, '01102e1e', '483d0090', 'render thread wait timeout: fmov s1,#1.0 → adrp x8,#0xcb7000', 'host-only'), - (0x50f8ec, '2018201e', '08514bbd', 'render thread wait timeout: fdiv s0,s1,s0 → ldr s8,[x8,#0xb50] (1000.0)', 'host-only'), - (0x50f8f0, '483d0090', '0019201e', 'render thread wait timeout: adrp x8,#0xcb7000 → fdiv s0,s8,s0 (1000/fps; the fmul by s8 that follows makes it µs)', 'host-only'), - (0x50f8f4, '08514bbd', '1f2003d5', 'render thread wait timeout: ldr s8,[x8,#0xb50] → nop (moved up)', 'host-only'), + (0x50f8e8, '01102e1e', '483d0090', 'render thread wait timeout: fmov s1,#1.0 → adrp x8,#0xcb7000'), + (0x50f8ec, '2018201e', '08514bbd', 'render thread wait timeout: fdiv s0,s1,s0 → ldr s8,[x8,#0xb50] (1000.0)'), + (0x50f8f0, '483d0090', '0019201e', 'render thread wait timeout: adrp x8,#0xcb7000 → fdiv s0,s8,s0 (1000/fps; the fmul by s8 that follows makes it µs)'), + (0x50f8f4, '08514bbd', '1f2003d5', 'render thread wait timeout: ldr s8,[x8,#0xb50] → nop (moved up)'), ], # Hyrule Warriors: Age of Calamity 1.3.0 (v327680) main: the init (main+0x8d9610) hands the KTSL sound engine three heaps carved from one block: 15 MB engine memory, 2 MB, and # 142 MB sound data (sizes built by mov x8,#0xf00000; movk x8,#0x20,lsl #48 and mov w9,#0x8e00000; constructor main+0x12647a0, heaps created at main+0x1a2ba58). The audio renderer @@ -230,10 +162,10 @@ CUTS = { # keeps the interval (main+0x17c4ba8 → device +0x2aa4, passed to every present at main+0x17ced78, which sets it on the window when it changes) and its frame rate, # 59.94 / interval (main+0x17c37c8 → device +0x32d0), which the game steps its time by (the event scripts at main+0x7802c4: timescale / frame rate). Forcing interval 1 at the # present alone would keep the 1/29.97 step at 60 frames a second. With interval 1 in the config the device runs the engine's own 60 fps mode: frame rate 59.94, interval 1 - (0x8d94b0, '4900c0f2', '2900c0f2', 'KTGL device config present interval: movk x9,#0x2,lsl #32 → #0x1 (60 fps; the device frame rate becomes 59.94 / 1)', 'host-only'), + (0x8d94b0, '4900c0f2', '2900c0f2', 'KTGL device config present interval: movk x9,#0x2,lsl #32 → #0x1 (60 fps; the device frame rate becomes 59.94 / 1)'), # The frame itself is paced by a KIDS task (main+0x166d9c0, on a KIDSTaskScheduler thread): it waits until 1 / fps has passed since the last frame (tick frequency / fps, # SleepThread and a poll), then steps the game by the time that really passed (+0x24, seconds). fps (+0x30) is 30, from engine data (main+0x166daa4) - (0x166db80, '14159f1a', '94078052', 'KIDS frame task wait: csinc w20,w8,wzr,ne (fps, at least 1) → mov w20,#60 (wait 1/60 s; the step stays the measured time)', 'host-only'), + (0x166db80, '14159f1a', '94078052', 'KIDS frame task wait: csinc w20,w8,wzr,ne (fps, at least 1) → mov w20,#60 (wait 1/60 s; the step stays the measured time)'), ], # Octopath Traveler 0 1.0.5 (v327680) main: Unreal's nninitStartup (main+0x12d74) constructs the engine allocator, an FMallocBinned3, in place at main+0x68022c8 (main+0x12e40 → constructor # main+0x9f480). Its constructor reserves 37 GB of address space (main+0x9f748: 74 pools of 512 MB, pool index = offset >> 29), which only the Mac can give: an iPhone has about 5 GB without the @@ -241,14 +173,14 @@ CUTS = { # the host puts its own allocator's vtable in the object instead (dm/host/hle_uemalloc.c: Malloc, Realloc, Free and the size queries on a host malloc zone), and every allocation goes through it # (FMemory reaches the object through FPlatformMemory::BaseAllocator, main+0x132b4, and the thread-local storage allocator nninitStartup registers, main+0x12e80 / main+0x12ec0, through its vtable) '9fe60a212cff9b9a3656ab63d4f882e5': [ - (0x12e40, '90310294', '1f2003d5', 'nninitStartup: bl 0x9f480 (FMallocBinned3 constructor, 37 GB of address space) → nop; the host allocator is in the object already (hle_uemalloc.c)', 'host-only'), + (0x12e40, '90310294', '1f2003d5', 'nninitStartup: bl 0x9f480 (FMallocBinned3 constructor, 37 GB of address space) → nop; the host allocator is in the object already (hle_uemalloc.c)'), ], # Octopath Traveler 0 1.0.5 (v327680) sdk (SDK 19.3.13): the AddressRegionManager node area 0x3fe00000 → 0x3e00000 (62 MB), as Engage's SDK 12 (above). The same constructor (sdk+0x1423ec): +0x1423fc # sets the node area limit, +0x142468 registers it as an occupied region. Unreal's region (-SwitchVirtualAddressSpaceSize, the profile's game_args) then starts 62 MB into the alias region, and the guest # block stays small enough for an iPhone next to the Unreal heap (hle_uemalloc.c), which lives outside it '7bba185a35ee9b4d0af66a4df910a823': [ - (0x1423fc, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000', 'host-only'), - (0x142468, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000', 'host-only'), + (0x1423fc, '08fca752', '087ca052', 'AddressRegionManager constructor: node area limit mov w8,#0x3fe00000 → mov w8,#0x3e00000'), + (0x142468, '09fca752', '097ca052', 'AddressRegionManager constructor: length of the node area registered as an occupied region mov w9,#0x3fe00000 → mov w9,#0x3e00000'), ], } # ---- Registered observation gates: {NSO build ID: [(offset, number, original instruction word, reason)]}. Numbers match OBS_* in the host's hle.h; @@ -260,85 +192,16 @@ OBS = { (0x2247a4, 19, '910022d6', 'link request posted (loop at main+0x224728, x19 = request ring): wait for the dispatcher; orig add x22, x22, #8'), (0x2248e4, 20, '910022d6', 'unlink request posted (loop at main+0x224868, x19 = request ring): wait for the dispatcher before the object is freed; orig add x22, x22, #8'), ], - 'db0507f36b84200ac05f75d76ed5026f': [ - (0xb1dc60, 1, 'd10603ff', 'UI message bus virtual implementation entry (x1 = message {u64 type hash, u64 action hash, char* panel name, …}): ' - 'the only source of the DM_UIMSG message stream and the DM_BOOTPROF boot phases; orig sub sp, sp, #0x180'), - (0x5d3bc0, 2, 'd10503ff', 'server tick entry (x0 = game object G): DM_TICKS per-tick 6-field state hash (dm/verify/tick_state_format.md, the same capture point as the ground truth swtick.py); ' - 'orig sub sp, sp, #0x140'), - (0x6e5b40, 3, 'd10103ff', 'client command dispatch entry (G, unit, packet, length): DM_C2S_LOG command packet sequence (dm/verify/command_packet_format.md, the same capture point as the ground truth); ' - 'orig sub sp, sp, #0x40'), - (0x6310b0, 4, 'a9bb67fc', 'system client message handler entry (buf = u32 client number + packet, packet length): packet IDs ≥ 0x67 (join / heartbeat 0x6d / 0x6f etc.), the receive pump drains this queue first; ' - 'DM_C2S_LOG records it as "q":"sys"; orig stp x28, x25, [sp, #-0x50]!'), - (0x317298, 5, 'd10183ff', 'SetLanguage entry (x1 → u32 new language number, language name table main+0x20ae478): together with the language reload cut above, the host asserts that the new language is zhCN; ' - 'orig sub sp, sp, #0x60'), -]} +} BUILD_ID = d[0x40:0x50].hex() -# Build-time product rules: the old original-resource / comparison domain is unchanged by default; the touchscreen version is issued separately as a new version, and guest code is not written at runtime. -RULESET = os.environ.get('DM_RULESET', 'native') -assert RULESET in ('native', 'touch-run-v4'), f'unknown rule set {RULESET}' -RULE_BUILD = 'db0507f36b84200ac05f75d76ed5026f' -RULE_RUN = RULESET == 'touch-run-v4' and NAME == 'main' -if RULE_RUN: - import hashlib - assert BUILD_ID == RULE_BUILD, f'Run rule build ID mismatch {BUILD_ID}' - RULE_ORIG_SHA = hashlib.sha256(IMG).hexdigest() cuts = list(CUTS.get(BUILD_ID, [])) -# The touchscreen rule domain by default also carries the host-private gates; the rule gate check can turn them off explicitly, keeping the same packaging entry point and rule domain. -PRIVATE_GATES = os.environ.get("DM_PRIVATE_GATES", "1") != "0" -PRIVATE_AIM = RULE_RUN and PRIVATE_GATES -if PRIVATE_AIM: - cuts.append((0x2faf04, 'fde7fe97', 'c11000d4', - 'skill-aim-v1-private: skill ground direction call only; svc86 delegates original2b4ef8 then sampled-right/original rotation+grid; zero/nonplayer/non-skill retain original; busy/town/skill/server untouched', 'host-only')) -if RULE_RUN: - # Directional light shadow map edge length (quality level 1, 0x168a520 fills +0x48 → creates the 2-layer array 0x1697394): 2100 → 1024. Authorization: "visually indistinguishable is enough"; - # on the second Mac, 108 images + 16 consecutive frames: for camp / wilderness / combat, at every level down to 1024 the FLIP cannot be told apart from the noise of a rerun with the same settings, and the change between adjacent frames in shadow regions is 0.71 → 0.75; on a real device the two shadow maps total about 0.7–1.4 ms / frame. - # The shadow size constants and the viewport that the game passes to the shader change with the same config value (verified by dump). The native rule domain (the Mac gate check against the original game's ground truth) stays at 2100 - SHADOW_SIDE = 1024 - movz_w14 = lambda n: struct.pack('<I', 0x52800000 | (n << 5) | 14).hex() # movz w14,#n -- the bytes are computed by an expression, not written by hand (a hand calculation came out as #32768, and the shadow map allocation of 32768² failed, crashing at startup) - cuts.append((0x168a580, movz_w14(2100), movz_w14(SHADOW_SIDE), f'shadow map side mov w14,#2100 → mov w14,#{SHADOW_SIDE} (quality level 1)', 'host-only')) -if RULE_RUN: - # Purely informational system notifications do not swallow the function key: the controller version's notification panel handler 0x3d84a0, on "action 9 (PLUS) pressed", executes the confirm action carried by the notification and swallows the key (meant for "press PLUS on a level-up notification to go straight to the skill tree"). - # The confirm action carried by a system notification (event queue class 12, 0xab5c08; e.g. "game difficulty set to N..." after changing the difficulty) only closes itself, and the result is that during the 10 seconds the notification stays up the first press of the red cross does nothing (reproduced on a real device + Mac). - # Fix: system notifications carry no confirm action (the category hash is stored as 0); notifications without a confirm action do not handle PLUS (they take the original function's "forward to child widgets" exit 0x3d880c) and close by themselves at notificationSeconds. Notifications with a confirm action are unchanged - cbz_x8 = lambda at, to: struct.pack('<I', 0xB4000008 | (((to - at) >> 2) & 0x7ffff) << 5).hex() - str_x19_178 = lambda rt: struct.pack('<I', 0xF9000000 | (0x178 // 8) << 10 | 19 << 5 | rt).hex() - cuts.append((0xab5c28, str_x19_178(8), str_x19_178(31), 'system notification confirm-action category str x8,[x19,#0x178] → str xzr (no confirm action)', 'host-only')) - cuts.append((0x3d853c, cbz_x8(0x3d853c, 0x3d8560), cbz_x8(0x3d853c, 0x3d880c), 'notification panel: confirm-action category empty → PLUS not handled', 'host-only')) - cuts.append((0x3d8544, cbz_x8(0x3d8544, 0x3d8560), cbz_x8(0x3d8544, 0x3d880c), 'notification panel: confirm-action hash empty → PLUS not handled', 'host-only')) -if RULE_RUN: - # The skill tooltip appears beside the skill tree and does not cover the tree: the skill tree panel's per-frame update (0xc356b0) calls the tooltip entry 0xb16660 at 0xc35fd4, with position argument x1 = the icon's absolute coordinates (low 32 bits x, high 32 bits y + 3); - # the controller version's tooltip expands against the icon's lower right, which on a touchscreen covers exactly the few skills around the finger. The bfxil x1,x8,#0,#32 at 0xc35fb0 is a no-op that copies the low half of x1 back into x1 (x8 = x1), - # and is replaced with movk x1,#3000: the x coordinate is put off screen, and the tooltip entry's own clamp-back (0xb0d560, by TooltipsPanel's screenEdgePadding) snaps it to the right safe-area edge; the y coordinate is untouched - cuts.append((0xc35fb0, struct.pack('<I', 0xB3407D01).hex(), struct.pack('<I', 0xF2800000 | 3000 << 5 | 1).hex(), 'skill tooltip x coordinate put off screen, the original clamp-back snaps it to the right edge (does not cover the skill tree)', 'host-only')) -if RULE_RUN and PRIVATE_GATES: - # Only stamina eligibility / movement selection / walk-run preference are registered; direction, real magnitude, legal mode and the zero-axis branch are not changed. - # The type0 guard for the two generic cost / selector points is in the small native trampoline below; non-player keeps the original behavior. - run_words = [ - (0x2c620c, 0xb90e511f, 0x52800029, 'preference init: mov w9,1 (x8 still 2d20000)'), - (0x2c6210, 0xd00152c8, 0xb90e5109, 'preference init: str w9,[x8,e50]; replaces the duplicate adrp of the same page, keeps the original x8'), - (0x369a90, 0xd100c3ff, None, 'client RunOff entry tail-jumps to the original RunOn; keeps its player0 / legal mode / environment gates'), - (0x369980, 0x340006a0, 0xd503201f, 'client RunOn: only cancels the zero-stamina rejection after the existing player0 gate'), - (0x2b2c74, 0x340000a0, 0xd503201f, 'controller d29 eligibility: only cancels the stat10 zero limit; magnitude comparison and stick release unchanged'), - (0x2b2dc0, 0x3900031f, 0xd503201f, 'controller d29: only cancels the stat10 zero clear; magnitude normalization unchanged'), - (0x2b30fc, 0x0a090101, 0x52800021, 'movement encoding select bit fixed to Run once past the legal-action / zero-axis gates; magnitude unchanged'), - (0x2b3104, 0x2a1f03e1, 0x52800021, 'Run encoding bit fixed also without a user walk/run preference; direction / magnitude unchanged'), - (0x6dc2c4, 0x1a880502, 0x52800062, 'server42 legal move request fixed to Run3; the original type0 / death / lock / magnitude / direction gates stay'), - (0x6dce90, 0x340000e0, 0xd503201f, 'server49: only cancels the zero-stamina rejection after the existing player0 / mode2 gates'), - (0x6dcec0, 0xa9bf7bfd, None, 'server4A entry tail-jumps to the original 49; length / type0 / mode gates stay'), - ] - for off, old, new, why in run_words: - if new is None: - target = 0x369900 if off == 0x369a90 else 0x6dce50 - new = 0x14000000 | (((target - off) >> 2) & 0x3ffffff) - cuts.append((off, struct.pack('<I', old).hex(), struct.pack('<I', new).hex(), 'touch-run-v4: ' + why)) -cuts = [c if len(c) == 5 else (*c, 'all') for c in cuts] # 5th item is the scope: all | host-only (host-only = not applied by the ground-truth emulator) -for off, ob, nb, why, scope in cuts: +for off, ob, nb, why in cuts: ob, nb = bytes.fromhex(ob), bytes.fromhex(nb) assert len(ob) == len(nb) and IMG[off:off + len(ob)] == ob, f'cut original bytes mismatch {off:#x}: {bytes(IMG[off:off + len(ob)]).hex()}' IMG[off:off + len(nb)] = nb # Key page indices are fixed; the key page content is written by the loader as "actual key number × 8". -KEYS = {'K18': 508, 'KTLS': 509, 'KTLR': 510, 'KGATE': 511, 'KRES0': 506, 'KRES1': 507, 'KASPECT': 505, 'KASPECTUI': 504} # KRES*: resolution parameter slots; KASPECT: picture aspect ratio cap (see PARAMS) -KEY_INDEX = {k: i for i, k in enumerate(('K18', 'KTLS', 'KTLR', 'KGATE', 'KRES0', 'KRES1', 'KASPECT', 'KASPECTUI'))} +KEYS = {'K18': 508, 'KTLS': 509, 'KTLR': 510, 'KGATE': 511} +KEY_INDEX = {k: i for i, k in enumerate(('K18', 'KTLS', 'KTLR', 'KGATE'))} md = Cs(CS_ARCH_ARM64, CS_MODE_ARM); md.detail = True patch = bytearray(); fix = []; gates = []; stats = {} @@ -549,9 +412,8 @@ def site_tpidr_write(ins): def emit_gate_call(): """Common section of a gate block (right after stp x29,x30,[sp,#-16]! and before blr x30): x30 = the gate address in thread slot KGATE. - Fetching the slot through the key page needs a second register to be borrowed; x29 is borrowed and then immediately restored from the stack: the x29 saved by _dm_gate must be the guest's original value -- - the host identifies the caller by [x29+8] (skill binding observation gate 6, private aiming gate svc86), and the crash backtrace also walks the x29 chain - (since r24 plan C it was borrowed and not returned, the x29 the host got was the slot offset from the key page, and skill icons fell back to placeholder text).""" + Fetching the slot through the key page needs a second register to be borrowed; x29 is borrowed and then immediately restored from the stack: the x29 saved by _dm_gate must be + the guest's original value, since the crash backtrace walks the x29 chain.""" emit(MRS_TPIDRRO(30)); emit_slot_addr(30, 29, 'KGATE'); emit(LDRX(30, 30)); emit(LDRX(29, 31)) def site_gate(ins, kind, arg): @@ -930,182 +792,26 @@ for a in sorted(kinds): struct.pack_into('<I', IMG, a, 0xD2A00080 | arg); stat(k); continue put_branch(a, st) -# Private visual branch; not OBS, 0x85 is a read-only query. Only this experimental build explicitly opts in; the original Qty/children and Update are unchanged. -if RULE_RUN and PRIVATE_GATES: - off, word = 0x3e443c, 0x3969c268 - assert struct.unpack_from('<I', IMG, off)[0] == word - assert off < TEXT_END and off not in kinds and not any(seg[0].address <= off < seg[-1].address + 4 for seg in windows) - start = here() - emit(STP_PRE(8, 31, 31, -16)) # The w8 query result is used only for cbz; both paths restore the original x8. - emit(STP_PRE(29, 30, 31, -16)) - emit_gate_call(); emit(BLR(30)) - ret = here(); emit(LDP_POST(29, 30, 31, 16)) - # cbz w8, nonmatch; NZCV is not changed. On match, restore the registers and then preserve children. - emit(0x34000068) - emit(LDP_POST(8, 31, 31, 16)); emit(B(here(), 0x3e4460)) - emit(LDP_POST(8, 31, 31, 16)); emit(word); emit(B(here(), 0x3e4440)) - gates.append((ret, 'svc', 0x85, off)) - put_branch(off, start); stat('private_skill_render') - cuts.append((off, struct.pack('<I', word).hex(), struct.pack('<I', B(off,start)).hex(), - 'private confirmed six HUD only: own image/text skip ->4460; unknown original ->4440; svc85 full800', 'host-only')) -elif RULE_RUN: - patch.extend(bytes(0x80)) - stat('private_gates_reserved') - obs = list(OBS.get(BUILD_ID, [])) -if RULE_RUN and PRIVATE_GATES: - obs.append((0x43ecc8, 6, "b8686953", "private touch-run-v4 PRE-ldr; full800 host ABI")) - obs.append((0x1237c60, 8, "d10143ff", "ui world anchor: camera world->screen entry (camera, point, out); host swaps x0 to a copy whose projection is shifted by the UI rect y (iPad UI kept 16:9 inside a taller screen), game camera untouched")) - # Freeze the world while a panel is open (host diag.c freeze_tick, off by default, DM_PANEL_FREEZE=1 turns it on). Four entry gates, all of which only swap entry arguments. - obs.append((0x5eb2c0, 10, "a9bc5ff8", "server world-sim entry (game G): while frozen the host swaps x0 to a stand-in whose event queue is empty, so no unit / timer callback runs this tick; the real queue is left untouched")) - obs.append((0x4b7230, 11, "d10183ff", "client per-unit tick update entry (unit): while frozen the host swaps x0 to a stand-in unit carrying only the skip flag ([+0x124] bit 23), so the function takes its own early-out to the epilogue; all six call sites are inside the client tick simulation 0x461b80 / 0x461e60")) - obs.append((0x4b74d0, 12, "f81e0ff3", "client per-unit per-frame update entry (unit): drives model animation / interpolation from the tick fraction; while frozen the host swaps x0 to a stand-in unit (type 9, no render component) so both the callee 0x2a18d0 and this function take their own early-outs")) - obs.append((0x2c3c90, 13, "f81c0ff7", "render per-frame entry, called right after the main loop writes the world frame step [0x20cebb8] and world time [0x3114208]: while frozen the host rewrites the step to 0 and the world time to its value at freeze, the same thing the game's own pause path does at 0x2c1574")) - obs.append((0xadc434, 14, "d10143ff", "TextBoxWidget text-changed entry (widget): reads the raw text at +0x88 and rebuilds the display; host rewrites +0x88 for strings our item-notes generator annotated with colour codes (TextBoxWidget draws U+E07E as '*'), exact-match table only, everything else untouched")) - obs.append((0x442bc0, 15, "a9ba6ffc", "client event-message entry (x0 = packet; byte 1 = event subtype, jump table 0x1da6752): subtype 0x17 = offline difficulty scale changed -> 0x20f91c chat line + system notification; host rewrites byte 1 past the table (default branch, nothing shown) only for 0x17 and only in a rift game (dm_rift.c); otherwise untouched. orig stp x28, x27, [sp, #-0x60]!")) - obs.append((0x634000, 16, "d10143ff", "server level change by level number (game, player, level, kind), used by waypoints / portals / quest moves: in a rift game, when the player leaves a rift map for a level that is neither town nor a rift map, the host swaps w2 to the rift's next map so the destination is replaced inside the single native transition")) - obs.append((0x711040, 17, "d101c3ff", "server quest event 'monster killed' entry (game, monster, attacker), the only entry (callers 0x7d8550 kill, 0x7ab8dc); in a rift game, for a monster that died on a rift map, the host swaps x1 to a stand-in whose +0xc0 is null so the function takes its own early return (0x711064): bosses in the rift are combat only, no quest state / quest notifications")) - obs.append((0x3d84a0, 18, "d10143ff", "NotificationPanel message handler entry (panel, message): on InputMessage / ControllerActionBegin with action id 9 (the menu key) the native handler re-sends the notice callback and marks the message bus consumed, so the first menu press only dismisses the notice. In touch mode the host swaps x1 to a copy of the message whose action id is not 9: the notice stays until it times out and the menu key opens the menu; controller mode keeps the native behaviour")) - obs.append((0x2f7664, 7, "6dbb23e9", "touch target rank: candidate compare entry (closure, unit, kind, s0=score); host raises Boss/elite score, native eligibility unchanged")) for off, oid, ow, why in obs: w = struct.unpack_from('<I', IMG, off)[0] assert f'{w:08x}' == ow, f'observation gate original instruction mismatch {off:#x}: {w:08x}' # Function entries, or the instruction right after a bl, only: dm_gate writes back only x0–x8 / x30, and host C code may modify x9–x17; at entry these carry no values under AAPCS64 (arguments are only in x0–x7 / x8), # and right after a call returns the callee was free to clobber them anyway (x16 / x17 are the veneer registers) after_bl = off >= 4 and struct.unpack_from('<I', IMG, off - 4)[0] >> 26 == 0x25 - assert off in set(starts) or after_bl or (oid == 6 and off == 0x43ecc8), f'private obs point not approved {off:#x}' + assert off in set(starts) or after_bl, f'observation gate neither at a function entry nor after a bl {off:#x}' assert off < TEXT_END and off not in kinds and not any(lo <= off < hi for wseg in windows for lo, hi in [(wseg[0].address, wseg[-1].address + 4)]), f'observation gate conflicts with a rewrite site {off:#x}' put_branch(off, site_obs(off, oid, w)); stat('obs') -# Rift exit rerouting (since r36): branch gate 0x89, hooked at the entry of "change level by the exit tile's connection" main+0x7cc1e0(game, player, tile). Its four callers (clicking an exit tile 0x6dbea0, -# entrance objects 0x6aeaa0 / 0x6b0500, quest 0x7314c0) all call it only after their own checks have passed; the destination is not a level id but the tile's peer room (in the already generated map of the same act), so it cannot be switched to another act. -# In a rift run the host replaces w2 / w3 with (the rift's next map, 11) and makes x8≠0 → the gate block jumps straight to "change level by level id" 0x634000, same caller, exactly one level change; in all other cases the original instruction is executed and control returns to 0x7cc1e4. -# The gate block is word-for-word isomorphic with the 0x85 branch gate (structck ⑪ checks it against the hle.h registry); like observation gates 15 / 16 it exists only in build outputs with host-private gates (touch) -if RULE_RUN and PRIVATE_GATES: - off, word, tgt = 0x7cc1e0, 0xd10183ff, 0x634000 - assert struct.unpack_from('<I', IMG, off)[0] == word and off in set(starts) and tgt in set(starts) - assert off < TEXT_END and off not in kinds and not any(seg[0].address <= off < seg[-1].address + 4 for seg in windows) - start = here() - emit(STP_PRE(8, 31, 31, -16)) - emit(STP_PRE(29, 30, 31, -16)) - emit_gate_call(); emit(BLR(30)) - ret = here(); emit(LDP_POST(29, 30, 31, 16)) - emit(0x34000068) - emit(LDP_POST(8, 31, 31, 16)); emit(B(here(), tgt)) - emit(LDP_POST(8, 31, 31, 16)); emit(word); emit(B(here(), off + 4)) - gates.append((ret, 'svc', 0x89, off)) - put_branch(off, start); stat('rift_exit') - cuts.append((off, struct.pack('<I', word).hex(), struct.pack('<I', B(off, start)).hex(), - 'rift exit redirect: tile-link level change entry -> host swaps (level, kind) and x8!=0 takes the branch to level-number change 0x634000; otherwise original ->0x7cc1e4; svc89', 'host-only')) - -# Current act written to the save while in a Rift (since r38): branch gate 0x8c, hooked at the "read difficulty" instruction main+0x6fa9d0 (ldrb w10, -# [x21, #0x104]) inside the original save-header writer 0x6fa3e0. -# The preceding instruction 0x6fa9cc has just loaded "which act the player is currently in" into w9 (ldrb w9, [x24, #0x2e8]); below, 0x6fa9e4 / 0x6fa9e8 -# write it as the difficulty byte 0x80 | act number (.d2s 0x98–0x9a), -# which decides which town the next session starts in. While the player is in a Rift map the host replaces w9 with 4 (Act 5 → re-enter in Harrogath); -# only this one value written to the save is changed, the running act number is untouched. -# A branch gate is used rather than "replace one instruction": structck only recognizes three registered forms, "bl call site", "entry" and "branch -# gate". The host always sets x8 = 0 (the jump path is never taken); -# the gate block restores x8, executes the original instruction and returns to 0x6fa9d4; the jump target is set to the return point itself only to -# satisfy the template (never reached). The gate block is word-for-word identical in structure to 0x85 / 0x89 -if RULE_RUN and PRIVATE_GATES: - off, word, tgt = 0x6fa9d0, 0x394412aa, 0x6fa9d4 # An instruction in the middle of a function (same as gate 0x85); it need not be a function entry; the original instruction does not depend on pc - assert struct.unpack_from('<I', IMG, off)[0] == word - assert off < TEXT_END and off not in kinds and not any(seg[0].address <= off < seg[-1].address + 4 for seg in windows) - start = here() - emit(STP_PRE(8, 31, 31, -16)) - emit(STP_PRE(29, 30, 31, -16)) - emit_gate_call(); emit(BLR(30)) - ret = here(); emit(LDP_POST(29, 30, 31, 16)) - emit(0x34000068) - emit(LDP_POST(8, 31, 31, 16)); emit(B(here(), tgt)) - emit(LDP_POST(8, 31, 31, 16)); emit(word); emit(B(here(), off + 4)) - gates.append((ret, 'svc', 0x8c, off)) - put_branch(off, start); stat('rift_save_act') - cuts.append((off, struct.pack('<I', word).hex(), struct.pack('<I', B(off, start)).hex(), - 'rift save act: save-header writer, right after it loads the current act into w9 -> host swaps w9 to 4 (act V) while the player is in a rift map; x8 stays 0, original insn runs ->0x6fa9d4; svc8c', 'host-only')) -# Extra mercenary equipment slots (since r39): branch gate 0x8d, hooked in the monster-owner branch of the original game's generic placement check main+0x6493a8 at main+0x64a34c (mov w9, w25). -# This section (0x64a314–0x64a354) lets through only two kinds of items for monster-type owners: 0x574bc0(item, 3 armor) and 0x574bc0(item, 0x25 helm); if neither, it takes the weapon path (by the left / right hand types in monstats), and if that fails too it rejects. -# The gate is hooked after the "is it a helm" call returns and before cbnz w0: when "the owner is the player's mercenary + highest rift level cleared ≥ 10 + the item is a belt / boots / gloves / ring / amulet", the host changes w0 to 1, -# taking the same pass path as the helm (the generic checks that follow, such as the body location cell and whether the location is empty, run as usual). The host always sets x8 = 0; the gate block is word-for-word isomorphic with 0x85 / 0x89 / 0x8c -if RULE_RUN and PRIVATE_GATES: - off, word, tgt = 0x64a34c, 0x2a1903e9, 0x64a350 # The original instruction does not depend on pc - assert struct.unpack_from('<I', IMG, off)[0] == word - assert off < TEXT_END and off not in kinds and not any(seg[0].address <= off < seg[-1].address + 4 for seg in windows) - start = here() - emit(STP_PRE(8, 31, 31, -16)) - emit(STP_PRE(29, 30, 31, -16)) - emit_gate_call(); emit(BLR(30)) - ret = here(); emit(LDP_POST(29, 30, 31, 16)) - emit(0x34000068) - emit(LDP_POST(8, 31, 31, 16)); emit(B(here(), tgt)) - emit(LDP_POST(8, 31, 31, 16)); emit(word); emit(B(here(), off + 4)) - gates.append((ret, 'svc', 0x8d, off)) - put_branch(off, start); stat('merc_slot') - cuts.append((off, struct.pack('<I', word).hex(), struct.pack('<I', B(off, start)).hex(), - 'merc slots: placement check, monster-owner branch right after the helm type test -> host sets w0=1 for ring/amulet/boots/gloves/belt on a hireling (rift best >= 10 for new equips; always on load); x8 stays 0, original insn runs ->0x64a350; svc8d', 'host-only')) -# Client half of the extra mercenary equipment slots (same generation as r39): branch gate 0x8e, hooked inside the client's "can the mercenary wear this" function main+0x3bea44(mercenary, item) at main+0x3beb58 (mov x0, x19). -# This function follows the same rule as the server-side section: if either 0x574bc0(item, 3 armor) or 0x574bc0(item, 0x25 helm) matches it returns 1, and if neither does it takes the weapon path. The item cell widget uses its result to draw the "cannot place" red and -# to decide whether to place on release -- without changing it, an unlocked new cell still shows red when the right item is dragged over it, and the original game's drop fails (previously the touch layer's fallback called 0x3bed60 directly). -# The gate is hooked after "mov w1, #0x25" and before the bl of this type check: when "highest rift level cleared ≥ 10 + the item is a belt / boots / gloves / ring / amulet", the host replaces w1 with the item's own type number, -# so the type check passes naturally and takes the same return-1 path as the helm. The host always sets x8 = 0; the gate block is word-for-word isomorphic with 0x8d -if RULE_RUN and PRIVATE_GATES: - off, word, tgt = 0x3beb58, 0xaa1303e0, 0x3beb5c # The original instruction does not depend on pc - assert struct.unpack_from('<I', IMG, off)[0] == word - assert off < TEXT_END and off not in kinds and not any(seg[0].address <= off < seg[-1].address + 4 for seg in windows) - start = here() - emit(STP_PRE(8, 31, 31, -16)) - emit(STP_PRE(29, 30, 31, -16)) - emit_gate_call(); emit(BLR(30)) - ret = here(); emit(LDP_POST(29, 30, 31, 16)) - emit(0x34000068) - emit(LDP_POST(8, 31, 31, 16)); emit(B(here(), tgt)) - emit(LDP_POST(8, 31, 31, 16)); emit(word); emit(B(here(), off + 4)) - gates.append((ret, 'svc', 0x8e, off)) - put_branch(off, start); stat('merc_slot_client') - cuts.append((off, struct.pack('<I', word).hex(), struct.pack('<I', B(off, start)).hex(), - 'merc slots (client): hireling can-equip check, between "mov w1,#0x25" and the type test -> host swaps w1 to the item\'s own type for ring/amulet/boots/gloves/belt (rift best >= 10); x8 stays 0, original insn runs ->0x3beb5c; svc8e', 'host-only')) - -# ---- Parameter slots (host only): {NSO build ID: [(offset A, original word A, offset B, original word B, register, key, reason)]}. -# The two constant-building instructions at the original site are changed so that the first jumps to a parameter trampoline in the patch area and the second is a nop; the trampoline fetches the key page value indirectly and jumps back to just after the first (A+4): when A and B are not adjacent, the original instructions in between execute as usual -# (it used to jump back to B+4; in the operation-mode notification handler 0xe10d5c/0xe10d60 that skipped adrp x19.. and another trampoline, and switching to handheld crashed immediately; structck ⑩ checks the trampoline exit). -# xR must not be read or written between A and B (offline assertion: the instructions in between do not contain xR). The host supplies values according to the front end, Mac supplies the original constants → behavior unchanged ---- -PARAMS = {'db0507f36b84200ac05f75d76ed5026f': [ - (0xe12d8c, 'd280c813', 0xe12d90, 'f2c07093', 19, 'KRES1', 'startup init: docked-mode output resolution 1600x900 (packed as {u32 width, u32 height})'), - (0xe12e1c, 'd280a008', 0xe12e20, 'f2c05a08', 8, 'KRES0', 'startup init: handheld-mode output resolution 1280x720'), - (0xe10d60, 'd280c818', 0xe10d80, 'f2c07098', 24, 'KRES1', 'operation-mode change notification (0x1f) handler: docked mode 1600x900'), - (0xe10d5c, 'd280a016', 0xe10d74, 'f2c05a16', 22, 'KRES0', 'operation-mode change notification (0x1f) handler: handheld mode 1280x720'), - # Aspect ratio cap 19:9 (float 0x40071c72): screens wider than 19:9 get black bars on both sides. The host supplies the float bit pattern (low 32 bits) of max(19/9, output width / height); - # the original game outputs 16:9 (1600x900 / 1280x720) ⇒ still 19/9, behavior unchanged; iPhone 1952x900 (2.17:1) ⇒ 2.169, black bar width 0 - (0x1663ee8, '52838e49', 0x1663eec, '72a800e9', 9, 'KASPECTUI', 'UI constant buffer slot 0x15 [screen width, screen height, aspect ratio cap]: the UI sprite shader uses it to draw black bars on both sides beyond the cap (with a soft edge about 20–30 pixels inward; the host adds a margin that pushes the soft edge off screen)'), - (0xb152bc, '52838e48', 0xb152c0, '72a800e8', 8, 'KASPECT', 'new-picture mode viewport: margin = (screen width − screen height × cap) / 2, a centered viewport set through 0x85bc90'), - (0x23e888, '52838e48', 0x23e88c, '72a800e8', 8, 'KASPECT', 'new-picture mode coordinate clamp (0x23e760): x is clamped into [margin, margin + screen height × cap], so a pointer / touch at the sides of a wide screen is no longer clamped back to the middle'), -]} -params = PARAMS.get(BUILD_ID, []) -for oa, wa, ob_, wb, r, key, why in params: - for o, w0 in ((oa, wa), (ob_, wb)): - w = struct.unpack_from('<I', IMG, o)[0] - assert f'{w:08x}' == w0, f'parameter slot original instruction mismatch {o:#x}: {w:08x}' - assert o not in kinds and not any(lo <= o < hi for wseg in windows for lo, hi in [(wseg[0].address, wseg[-1].address + 4)]), f'parameter slot conflicts with a rewrite site {o:#x}' - for o in range(oa + 4, ob_, 4): - ins = dis(struct.unpack_from('<I', IMG, o)[0], o) - assert r not in regs_in(ins), f'x{r} is used between the parameter slot instructions: {o:#x} {ins.mnemonic} {ins.op_str}' - start = here() - tmp = 16 if r != 16 else 17 - emit(STUR(tmp, 31, -16)); emit(MRS_TPIDRRO(r)); emit_slot_addr(r, tmp, key); emit(LDRX(r, r)); emit(LDUR(tmp, 31, -16)); emit(B(here(), oa + 4)) - struct.pack_into('<I', IMG, oa, B(oa, start)); struct.pack_into('<I', IMG, ob_, NOP); stat('param') - # ---- Frame step in whole vsyncs (The Legend of Zelda: Tears of the Kingdom 1.4.3 and Breath of the Wild 1.9.0 main, host only) ---- # Both games step their game time by a whole number of vsyncs (1/60 s) per frame that their own interval logic picks. With the stubs the host picks it (hle_nvn.c fstep_tick, # nvn_fstep.h): the step is the number of vsyncs the host keeps the frame on screen (1 = 60 fps, 2 = 30 when 60 does not hold), plus one while the game is behind real time after # a late frame, so motion is even and game time = real time. The steps stay values the games' own logic uses (1 to 3). # State page = the page after the key page (zero in the file, read-write in both loaders): +0 float target fps written by the loader (fps_target, DM_FPS_TARGET; 0 = every stub -# runs the original instructions), +4 TOTK movie flag, +8 double lead (TOTK legacy), +0x10 float render scale (TOTK, below), +0x18 double game seconds (TOTK legacy), -# +0x20 u32 step in vsyncs (host; 0 = not decided yet), +0x24 u32 a step the game fixes itself (BotW's VFR override; 0 = none), +0x28 u32 mode (loader: 0 = whole vsyncs, 1 = legacy), -# +0x30 u64 vsyncs stepped (the stubs; logged by the host to check the game's speed). Legacy (DM_FRAME_STEP=legacy, profile frame_step) is each game's earlier method, kept for -# the iPhone A/B test: TOTK's elapsed-time step (below); for BotW the loader writes target 0 (its own interval controller with the 60 fps cuts above). The value per build is -# whether legacy keeps the state page on (fixups FPS_LEGACY_ELAPSED). Stub bodies assembled with clang -FRAME_STEP = {'277178b7dba1b6d4949a84778d4abc58': 1, 'cd57b23fa4bbad65803d9788c01821ee': 0} +# runs the original instructions), +4 TOTK movie flag, +8 double lead (TOTK, below), +0x10 float render scale (TOTK, below), +0x18 double game seconds (TOTK, below), +# +0x20 u32 step in vsyncs (host; 0 = not decided yet), +0x24 u32 a step the game fixes itself (BotW's VFR override; 0 = none), +0x30 u64 vsyncs stepped (the stubs; logged +# by the host to check the game's speed). Stub bodies assembled with clang +FRAME_STEP = {'277178b7dba1b6d4949a84778d4abc58', 'cd57b23fa4bbad65803d9788c01821ee'} FS_STATE_OFF = 0 if BUILD_ID in FRAME_STEP and NAME == 'main': FS_STATE_OFF = KEY_PAGE_OFF + 0x4000 @@ -1118,11 +824,11 @@ if BUILD_ID in FRAME_STEP and NAME == 'main': def fs_put(off, words, why): old = bytes(IMG[off:off + 4 * len(words)]) for i, w in enumerate(words): struct.pack_into('<I', IMG, off + 4 * i, w) - cuts.append((off, old.hex(), bytes(IMG[off:off + 4 * len(words)]).hex(), why, 'host-only')); stat('frame_step') + cuts.append((off, old.hex(), bytes(IMG[off:off + 4 * len(words)]).hex(), why)); stat('frame_step') # Tears of the Kingdom picks a present interval each frame (main+0x2db174: 2 = 30 fps, 3 under load) and steps its game time by 60 / fps (int fps at timing +0x50, read at main+0x2daa54 # and main+0x3e562c). The stub replaces that logic: int fps = 60 / step, and the float fps (timing +0x24, unused, per masagrator's FPSLocker config for this build, which found the -# sites) the same, which the consumers read. Legacy: the step is the measured frame time (timing frame +0xd8, 19.2 MHz ticks), clamped to [1 / cap, 1 / 15 s]; game time that ran -# ahead after a short frame (the "lead", at most 0.25 s; totk-mobile) is paid back by later frames. Prerendered movies set a 30 cap (flag set and cleared around main+0x2b36100 / +# sites) the same, which the consumers read. Until the host has decided a step (the first frames) the step is the measured frame time (timing frame +0xd8, 19.2 MHz +# ticks), clamped to [1 / cap, 1 / 15 s]; game time that ran ahead after a short frame (the "lead", at most 0.25 s; totk-mobile) is paid back by later frames. Prerendered movies set a 30 cap (flag set and cleared around main+0x2b36100 / # main+0x2b33918), the D-pad menu fade starts at full rate (FPSLocker's fix at main+0x1fbc80) and the swapchain argument at main+0xbe14a8 becomes 1 as in FPSLocker if FS_STATE_OFF and BUILD_ID == '277178b7dba1b6d4949a84778d4abc58': # frame step (replaces ldrb w9,[x0,#0x58], the first instruction after the prologue) @@ -1130,11 +836,9 @@ if FS_STATE_OFF and BUILD_ID == '277178b7dba1b6d4949a84778d4abc58': start = here(); emit(ADRP(16, here(), FS_STATE_OFF)) for w in (0xbd400200, # ldr s0,[x16] target fps 0x1e202008, # fcmp s0,#0.0 - 0x54000680, # b.eq off (+52) target 0: original path - 0xb9402a02, # ldr w2,[x16,#0x28] mode - 0x350001e2, # cbnz w2,elapsed (+15) legacy + 0x54000640, # b.eq off (+50) target 0: original path 0xb9402202, # ldr w2,[x16,#0x20] step in vsyncs - 0x340001a2, # cbz w2,elapsed (+13) not decided yet + 0x340001a2, # cbz w2,elapsed (+13) not decided yet: by the measured frame time 0x1e230041, # ucvtf s1,w2 0x1e27d003, # fmov s3,#30.0 0x1e211872, # fdiv s18,s3,s1 @@ -1185,7 +889,7 @@ if FS_STATE_OFF and BUILD_ID == '277178b7dba1b6d4949a84778d4abc58': emit(w) emit(B(here(), 0x2db284)) # the original epilogue emit(0x39416009); emit(B(here(), 0x2db190)) # off: ldrb w9,[x0,#0x58]; back - fs_put(0x2db18c, [B(0x2db18c, start)], 'frame step from the host in whole vsyncs, legacy by elapsed time (state page target fps; 0 = original interval logic)') + fs_put(0x2db18c, [B(0x2db18c, start)], 'frame step from the host in whole vsyncs, by elapsed time until it has decided one (state page target fps; 0 = original interval logic)') # the two readers of the integer fps: float fps when on; the scvtf a few instructions later moves into the stub (s0 is not touched in between) for site, cvt, r in ((0x2daa54, 0x2daa6c, 19), (0x3e562c, 0x3e5638, 22)): fs_site(site, struct.pack('<I', 0xbd425000 | r << 5).hex()); fs_site(cvt, '00d8215e') @@ -1268,9 +972,7 @@ if FS_STATE_OFF and BUILD_ID == 'cd57b23fa4bbad65803d9788c01821ee': fs_site(0x11269b8, 'fd7bbea9'); fs_site(0x151d3d0, 'fd7bbea9') start = here(); emit(ADRP(16, here(), FS_STATE_OFF)) for w in (0xb9400211, # ldr w17,[x16] target fps (0 = off) - 0x340001d1, # cbz w17,orig (+14) - 0xb9402a11, # ldr w17,[x16,#0x28] mode - 0x35000191): # cbnz w17,orig (+12) legacy + 0x34000191): # cbz w17,orig (+12) emit(w) emit(ADRP(17, here(), 0x1e93890)); emit(LDRX(17, 17, 0x890 // 8)) # the VFR for w in (0xb4000131, # cbz x17,orig (+9) @@ -1288,9 +990,7 @@ if FS_STATE_OFF and BUILD_ID == 'cd57b23fa4bbad65803d9788c01821ee': fs_put(0x11269b8, [B(0x11269b8, start)], 'setWaitVsyncInterval: the host\'s step unless the VFR override is set (state page target fps; 0 = the game\'s interval)') start = here(); emit(ADRP(16, here(), FS_STATE_OFF)) for w in (0xb9400211, # ldr w17,[x16] target fps (0 = off) - 0x340000f1, # cbz w17,orig (+7) - 0xb9402a11, # ldr w17,[x16,#0x28] mode - 0x350000b1, # cbnz w17,orig (+5) legacy + 0x340000b1, # cbz w17,orig (+5) 0x3940a411, # ldrb w17,[x0,#0x29] the VFR's override 0x34000051, # cbz w17,pub (+2) none: 0 0xb9402c11, # ldr w17,[x0,#0x2c] @@ -1328,43 +1028,6 @@ if FS_STATE_OFF and BUILD_ID == 'cd57b23fa4bbad65803d9788c01821ee': emit(B(here(), 0x1266880)); emit(B(here(), 0x1266950)) # apply the host's scale; skip: the original branch fs_put(0x126686c, [B(0x126686c, start)], 'scale apply block taken every frame when the host render scale is not 1.0') -# The three existing native rule trampolines all use only the dead temporary w8 that is overwritten afterwards, and do not change the w0/stat return, sp or NZCV. -# 6c6290 is after the original mode3 gate; the Blaze / reference count and data invalidation 6c62dc branches before it and the state update after it are untouched. -rule_blocks = [] -if RULE_RUN: - def run_branch(off, old, body, why): - assert struct.unpack_from('<I', IMG, off)[0] == old, f'Run trampoline original word mismatch {off:#x}' - start = here() - for word in body(start): emit(word) - new = B(off, start) - struct.pack_into('<I', IMG, off, new) - cuts.append((off, struct.pack('<I', old).hex(), struct.pack('<I', new).hex(), why, 'all')) - rule_blocks.append((start, bytes(patch[start - PATCH_OFF:]).hex(), why)) - def cbz(at, target, r, nonzero=False): - delta = target-at; assert delta%4==0 and -(1<<20)<=delta<(1<<20) - return (0x35000000 if nonzero else 0x34000000) | (((delta>>2)&0x7ffff)<<5) | r - run_branch(0x6c6648, 0x34000060, lambda a: [ - 0xb9400268, cbz(a+4,a+12,8,True), B(a+8,0x6c664c), - cbz(a+12,a+20,0), B(a+16,0x6c664c), B(a+20,0x6c6654)], - 'touch-run-v4: Run selection ignores the stamina gate for player0 only, non-player returns by the original stat; w8 is overwritten afterwards') - run_branch(0x6c6290, 0x710006df, lambda a: [ - 0xb9400268, cbz(a+4,a+12,8,True), B(a+8,0x6c62ec), 0x710006df, B(a+16,0x6c6294)], - 'touch-run-v4: in the original legal Run cost section only player0 skips the stat10 deduction / drop to walking when exhausted; non-player keeps the original cmp') - - - # After the original mode3 / currently controlled unit gate at client4abc60; the actual GetStat has already returned, W0 is preserved, and only for type0 is the STA auto Stop cancelled. - run_branch(0x4ac368, 0x35000460, lambda a: [ - 0xb9400268, cbz(a+4,a+12,8), B(a+8,a+16), B(a+12,0x4ac3f4), - cbz(a+16,a+24,0), B(a+20,0x4ac3f4), B(a+24,0x4ac36c)], - 'touch-run-v4: client currently controlled Run3: only player0 skips the STA-zero auto Stop; the original getter value and real stick release stay, non-player keeps the original gate') - - - # After the original unit null/type5 gate, with w8 still the original unitType; only player request 2 is normalized, and the other original action dispatch / eligibility checks run in full. - run_branch(0x6c6580, 0x51000849, lambda a: [ - cbz(a,a+16,8,True), 0x51000849, cbz(a+8,a+16,9,True), - 0x52800062, 0x51000849, B(a+20,0x6c6584)], - 'touch-run-v4: original generic legal move: only a player0 Walk2 request is normalized to Run3, non-player and other requests unchanged, w8 / original eligibility unchanged') - os.makedirs(outdir, exist_ok=True) P = lambda ext: os.path.join(outdir, f'{NAME}.{ext}') if FAR: # patch area before the base: file = text + ro + data (padded to 16K) + patch area @@ -1379,21 +1042,11 @@ else: import hashlib with open(os.path.join(outdir, 'build_src.sha256'), 'w') as f: f.write(hashlib.sha256(open(os.path.abspath(__file__), 'rb').read()).hexdigest() + ' patch_nso.py\n') -if PRIVATE_AIM: - with open(P('aim.tsv'), 'w') as f: - f.write('# RULESET\tskill-aim-v1-private\n# ORIGINAL_CALL\t0x2faf04\t97fee7fd\n# ORIGINAL_CALLEE\t0x2b4ef8\n# SKILL_CALLER\t0x2b5b80\n# SVC\t0x86\n# INPUT\tactual GetNpad sampled right; move axis unchanged\n') - f.write('# IMAGE_SHA256\t'+hashlib.sha256(open(P('patched.bin'),'rb').read()).hexdigest()+'\n') -if RULE_RUN: - with open(P('rules.tsv'), 'w') as f: - f.write(f'# RULESET\t{RULESET}\n# BUILD_ID\t{BUILD_ID}\n# ORIGINAL_SHA256\t{RULE_ORIG_SHA}\n# IMAGE_SHA256\t{hashlib.sha256(open(P("patched.bin"), "rb").read()).hexdigest()}\n') - for off, raw, why in rule_blocks: f.write(f'{off:#x}\t{raw}\t{hashlib.sha256(bytes.fromhex(raw)).hexdigest()}\t{why}\n') -elif os.path.exists(P('rules.tsv')): - os.unlink(P('rules.tsv')) # When rebuilding native, clear this module's old rule registration so the old manifest does not leak into the original-resource domain with open(P('fixups.tsv'), 'w') as f: f.write(f'# PATCH_OFF\t{PATCH_OFF:#x}\n# KEY_PAGE_OFF\t{KEY_PAGE_OFF:#x}\n# KEY_PAGE_SIZE\t0x4000\n# TEXT_END\t{TEXT_END:#x}\n# RO_OFF\t{RO_OFF:#x}\n# DATA_OFF\t{DATA_OFF:#x}\n') if FAR: f.write(f'# IMAGE_SIZE\t{IMAGE_SIZE:#x}\n# FILE_IMAGE_LEN\t{FILE_IMAGE_LEN:#x}\n') # patch area before the base (negative PATCH_OFF): whole image size, length of the image part of the file for k in KEY_INDEX: f.write(f'# KEY_{k}_IDX\t{KEY_INDEX[k]}\n') - if FS_STATE_OFF: f.write(f'# FPS_STATE_OFF\t{FS_STATE_OFF:#x}\n# FPS_LEGACY_ELAPSED\t{FRAME_STEP[BUILD_ID]:#x}\n') # frame step state page (FRAME_STEP): the loader writes the target fps and the mode there + if FS_STATE_OFF: f.write(f'# FPS_STATE_OFF\t{FS_STATE_OFF:#x}\n') # frame step state page (FRAME_STEP): the loader writes the target fps there f.write(f'# BUILD_ID\t{BUILD_ID}\n') # the host identifies the build by it: features with hard-coded addresses only run on the build they belong to with open(P('gates.tsv'), 'w') as f: for ret, k, arg, orig in gates: f.write(f'{ret:#x}\t{k}\t{arg}\t{orig:#x}\n') @@ -1405,25 +1058,8 @@ with open(P('sites.tsv'), 'w') as f: for a, tgt in tabjumps.items(): f.write(f'{a:#x}\tT\t' + ' '.join(f'{t:#x}' for t in tgt) + '\n') # offset-table jump and all its targets with open(P('cuts.tsv'), 'w') as f: f.write(f'# BUILD_ID\t{BUILD_ID}\n') - for off, ob, nb, why, scope in cuts: f.write(f'{off:#x}\t{ob}\t{nb}\t{scope}\t{why}\n') # Columns: offset, original bytes, new bytes, scope, reason -with open(P('params.tsv'), 'w') as f: - f.write(f'# BUILD_ID\t{BUILD_ID}\n') - for oa, wa, ob_, wb, r, key, why in params: f.write(f'{oa:#x}\t{ob_:#x}\tx{r}\t{key}\t{why}\n') + for off, ob, nb, why in cuts: f.write(f'{off:#x}\t{ob}\t{nb}\t{why}\n') # Columns: offset, original bytes, new bytes, reason with open(P('obs.tsv'), 'w') as f: f.write(f'# BUILD_ID\t{BUILD_ID}\n') for off, oid, ow, why in obs: f.write(f'{off:#x}\t{oid}\t{ow}\t{why}\n') print(NAME, stats, 'cuts', len(cuts), 'obs', len(obs), 'patch', len(patch), 'PATCH_OFF', hex(PATCH_OFF), 'slot_refs', len(fix), 'gates', len(gates)) -# The boot block list ships with main: take the repository's versioned data dm/data/bootlist_<main build ID>.tsv, copy it as is into the build output directory and write its sha256 (the list is checked in, not re-recorded for every version; -# the recording itself is racy -- three recordings of the same build output gave block sets of 5292 / 5114 / 5281, and the initiating threads also differed -- re-record only when the build ID changes or for a deliberate improvement, and attach an A/B from the second Mac to the commit) -if NAME == 'main': - import hashlib, shutil - BL = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', f'bootlist_{BUILD_ID}.tsv') - if not os.path.isfile(BL): # the public repository does not ship this list (each line carries the first bytes of an original resource block): without it there is no prefetch, so launch skips one ahead-of-time decode step and nothing else is affected - print('bootlist: no boot block list, skipped (no prefetch). To use one, record it yourself: dm/tools/tact_bootlist.py') - else: - hd = open(BL, encoding='utf-8').readline().split() - if hd[-1:] != [BUILD_ID]: sys.exit(f'[RED] {BL} header build ID {hd[-1:]} does not match main {BUILD_ID}') - shutil.copyfile(BL, os.path.join(outdir, 'bootlist.tsv')) - sha = hashlib.sha256(open(BL, 'rb').read()).hexdigest() - open(os.path.join(outdir, 'bootlist.sha256'), 'w').write(f'{sha} bootlist.tsv\n') - print('bootlist', os.path.relpath(BL), sha[:12]) diff --git a/dm/tools/product.sh b/dm/tools/product.sh deleted file mode 100755 index 6935e15..0000000 --- a/dm/tools/product.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Usage: product.sh name <kind> → 3.3.94012-r28-<kind> (kind: touch / rules / keypage) -# product.sh rev → r28 -# The codename of the current rewrite build output is written in exactly one place: dm/PRODUCT (one line, e.g. 3.3.94012-r28). A revision bump changes only that line. -# Usage: product.sh name <kind> → 3.3.94012-r28-<kind> (kind: touch / rules / keypage) -# product.sh path <kind> → local default build output directory $DM_ROOT/build/cache/dm/out/<name> -# product.sh rev → r28 -# product.sh build → rebuild the three kinds of build output (keypage / rules / touch) into the local default directory -set -u -P=$(head -1 "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/PRODUCT") || exit 2 -[ -n "$P" ] || { echo "dm/PRODUCT is empty" >&2; exit 2; } -case "${1:-}" in - name) echo "$P-${2:?kind}";; - path) echo "${DM_ROOT:-$HOME/dm-work}/build/cache/dm/out/$P-${2:?kind}";; - rev) echo "${P##*-}";; - build) # Rebuild the three kinds of build output with this worktree's rewriter into the local default directory (run after a revision bump / after changing patch_nso.py). Kind → rule set: keypage = native; rules = touchscreen rules, without host-private gates; touch = touchscreen rules + private gates - HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd); PY=${DM_PY:-${DM_ROOT:-$HOME/dm-work}/build/cache/dm/venv/bin/python3}; NSO=${DM_NSO_DIR:-${DM_ROOT:-$HOME/dm-work}/data/switch/3.3.94012/Update/ExeFS} - for k in keypage rules touch; do O=${DM_ROOT:-$HOME/dm-work}/build/cache/dm/out/$P-$k; mkdir -p "$O" - case $k in keypage) E=(DM_RULESET=native);; rules) E=(DM_RULESET=touch-run-v4 DM_PRIVATE_GATES=0);; touch) E=(DM_RULESET=touch-run-v4);; esac - for m in main sdk subsdk0; do env "${E[@]}" "$PY" "$HERE/patch_nso.py" "$NSO/$m" "$O" "$m" > "$O/$m.patch.log" 2>&1 || { echo "🔴 $k $m rewrite failed: $(tail -2 "$O/$m.patch.log")" >&2; exit 1; }; done - echo "$P-$k built: $(ls "$O" | wc -l | tr -d ' ') files"; done;; - *) echo "usage: product.sh name|path <kind> | rev | build" >&2; exit 2;; -esac diff --git a/docs/agents/02-survey.md b/docs/agents/02-survey.md index 96d55d9..47f6dc6 100644 --- a/docs/agents/02-survey.md +++ b/docs/agents/02-survey.md @@ -43,12 +43,12 @@ The tables below are what to check by hand afterwards. ## SVCs and instructions to rewrite - `dm/tools/insn_census.py <module.nso> [--sites tsv]` counts every site the rewriter must handle: `svc`, `mrs/msr tpidr*`, `x18` use, exclusive windows (`ldxr`/`stxr`), timers (`cntpct`, `cntfrq`), cache maintenance, and system registers. -- Run `DM_RULESET=native python3 dm/tools/patch_nso.py <nso> <out dir> <name>` and check that its counts match the census. New system registers appear with new SDKs: SDK 16 reads `CTR_EL0` (macOS traps it), now rewritten to a constant. +- Run `python3 dm/tools/patch_nso.py <nso> <out dir> <name>` and check that its counts match the census. New system registers appear with new SDKs: SDK 16 reads `CTR_EL0` (macOS traps it), now rewritten to a constant. - Mario's SVC set was a subset of D2R's. A new SVC needs a case in `hle_svc.c` / `hle_thread.c`; the default case logs `SVC %#x … (placeholder returns success)` once and returns success, which is wrong for anything that returns data. ## NVN surface -- Find how the game obtains NVN entry points. D2R wrote slots with `adrp`-to-slot code (`dm/gfx/tools/nvn_slots.py`); Mario's loader (`main+0x1070910`) calls `GetProcAddress` per name and stores through the GOT (`dm/tools/nvn_slots_got.py <main.bin> <loader> <text end> <out tsv>`). The output lists every fetched function and every call site. +- Find how the game obtains NVN entry points. Some games write the slots with `adrp`-to-slot code; Mario's loader (`main+0x1070910`) calls `GetProcAddress` per name and stores through the GOT (`dm/tools/nvn_slots_got.py <main.bin> <loader> <text end> <out tsv>`). The output lists every fetched function and every call site. - Mario fetched 534 functions and called 236 of them; 79 were new to the host (CommandBuffer 31, Texture 15, Buffer 10, Sampler 10). Names the host doesn't know at all go into `dm/gfx/nvn/names.txt` → `gen_protos.py`. ## Data containers that matter to the host diff --git a/docs/agents/03-bringup.md b/docs/agents/03-bringup.md index 987086f..3c471f6 100644 --- a/docs/agents/03-bringup.md +++ b/docs/agents/03-bringup.md @@ -3,7 +3,7 @@ ## The loop ```bash -DM_RULESET=native python3 dm/tools/patch_nso.py $W/data/v0/ExeFS/main $W/build/patched_v0 main # and sdk +python3 dm/tools/patch_nso.py $W/data/v0/ExeFS/main $W/build/patched_v0 main # and sdk make -C dm/host GAME=<title ID> dmhost $W/run/boot.sh <log name> [seconds] [VAR=value ...] ``` diff --git a/docs/agents/README.md b/docs/agents/README.md index b10242a..ef161d2 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -30,7 +30,7 @@ Everything here was learned on Diablo II: Resurrected (SDK ~12, NVN 53, the orig It extracts only the code modules plus a RomFS list for the survey. Unless given `--survey-only`, it then writes a provisional profile into `<NSP folder>/<title ID>_build/profile/<title ID>/`, extracts the game, boots it on the Mac until its heap request when it has virtual address memory (the heap check) and packs a best-effort IPA. That profile is the starting point for step 3: the static keys follow rules checked against every profile, the rest are guesses or defaults ([dm/games/README.md](../../dm/games/README.md)); the run-time keys (`render_cond`, `tex_flush_upload`, `core_preempt`, …) are the bring-up's to find. For Mac runs, export `DM_GAMES_DIR=<NSP folder>/<title ID>_build/profile` until the profile moves into `dm/games/`. 2. Extract base (and update, if in scope) — [01](01-extract.md). Finish the survey — [02](02-survey.md). Write down: SDK version, NVN API version, module list, imports the host lacks, SVCs, instruction census, NVN loader pattern, shader containers. 3. Add a game profile `dm/games/<title ID>/game.tsv`, named after the base game's title ID (start from `010015100B514000/game.tsv`: identity, `build` rows, `modules`; keep the memory / NVN values until they are measured) and build the host for it with `make -C dm/host GAME=<id>`. -4. Run the rewriter (`DM_RULESET=native`) and check its counts against `insn_census.py`. +4. Run the rewriter and check its counts against `insn_census.py`. 5. Boot on the Mac and iterate on the failure catalogue in [03](03-bringup.md) until the game reaches its frame loop. 6. Extract and translate the shaders, find the binding layout by census, and get the first frame — [04](04-graphics.md). 7. Check memory over a minute of running (`footprint`, the host's memory report line); fix growth before moving on. diff --git a/docs/development.md b/docs/development.md index 739a7c5..72618bc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,7 +6,7 @@ The README covers packing and playing. This page is for working on nxnative itse | Part | State | |---|---| -| Rewriter (`dm/tools/patch_nso.py`, `DM_RULESET=native`) | Runs unchanged on every module of every supported game. Per-build cuts are keyed by the module's build ID | +| Rewriter (`dm/tools/patch_nso.py`) | Runs unchanged on every module of every supported game. Per-build cuts are keyed by the module's build ID | | Host (`dm/host`) | Stands in for the Horizon kernel and the system services the games call: threads and synchronization, memory, file system with transactional saves, HID, audio, movies (VP9 through libvpx, Vorbis sound through stb_vorbis), software keyboard, focus and lifecycle | | Graphics (`dm/gfx`) | NVN on Metal. Shaders are extracted by `shader_split.py` from Nintendo's containers and from Unity's `Shader` objects, then translated offline. Mario: 16,266 of 16,268 shaders compile. Engage: 9,268 of 9,269 | | Audio | Games that carry the SDK's audio renderer (Mario) run it in-process; games that use AudioOut directly (Engage: Wwise) feed it straight. Output goes through Core Audio (default on iOS, `DM_AUDIO=1` on the Mac) | @@ -96,7 +96,7 @@ dotnet build dm/tools/nx_extract -c Release -o $W/build/nx_extract $W/build/nx_extract/nx_extract <prod.keys> <base NSP> - $W/data/v0 $W/build/nx_extract/nx_extract <prod.keys> <base NSP> <update NSP> $W/data/$VER $W/data/v0/RomFS # 2. rewrite the profile's code modules -for m in $(python3 dm/games/game.py $G modules); do DM_RULESET=native python3 dm/tools/patch_nso.py $W/data/$VER/ExeFS/$m $W/build/patched_$VER $m; done +for m in $(python3 dm/games/game.py $G modules); do python3 dm/tools/patch_nso.py $W/data/$VER/ExeFS/$m $W/build/patched_$VER $m; done # 3. shaders: extract from every version in the profile's shader_versions (comma-separated, base first), then translate into <cache>/b-<chain version>/ DM_GAME=$G python3 dm/gfx/tools/shader_split.py $W/data/v0/RomFS,$W/data/$VER/RomFS $W/data/v0/ExeFS/main,$W/data/$VER/ExeFS/main $W/shader/pkg dm/games/$G/shader_map.tsv dm/games/$G/shader_facts.tsv dm/games/$G/shader_pairs.tsv DM_GAME=$G python3 dm/gfx/tools/dmsh_chain.py --header @@ -167,9 +167,9 @@ The game's own work and its frame rate stay as they are; the host avoids work of - Real time lost to a late frame or a hitch is paid back one vsync per frame (at most 15; the rest of a long load is dropped), so game time = real time. The log line `frame step: game time 30.00 s in 30.00 s (1.000 of real time)` checks it every 1,800 presents, with the frames held 1 / 2 / 3 vsyncs, late frames, paid-back vsyncs and hold changes. - Scenes that fix the game's step themselves (the VFR's override: 2 at `main+0xc3b588` / `+0xc5ebf8`, 1 at `main+0xc609d8`) keep it, and the host holds frames that long (log line `the game fixes its own step`; not reached on the Mac yet). - Mac (M4 Pro), from the title into the save's scene: 1.000 of real time at 60; with 12 ms added to every frame (`DM_TEST_PRESENT_LAG=1,12`) it drops to 30 and stays at 1.000. `DM_FPS_TARGET=30` holds 30 (saves power). -- **Legacy method** (`DM_FRAME_STEP=legacy`, kept for the iPhone A/B test): patches to the game's frame-interval logic lift its floor from 2 to 1. The game's time step follows its frame interval, so the speed is right at a steady 60, 30 or 20. The game picks the interval from the frame's CPU time and its GPU time, which it reads from two GPU timestamps around the frame; the host gives each timestamp the GPU time of the work around it (profile key `gpu_timestamps`, which its dynamic resolution needs too). Before that it saw only the main thread's CPU time (about 3 ms a frame in the first shrine) and stayed at 60 in slow motion: at 40 fps, about two thirds of the normal speed (an iPhone 16 Pro Max in handheld mode, GitHub issue #9; [04](agents/04-graphics.md)). Under load it still ran at 0.93–0.98 of real time. +- **The game's own interval controller** (at `DM_FPS_TARGET=0`, and the interval the framework keeps while the VFR override holds): patches to it lift its floor from 2 to 1. The game's time step follows its frame interval, so the speed is right at a steady 60, 30 or 20. The game picks the interval from the frame's CPU time and its GPU time, which it reads from two GPU timestamps around the frame; the host gives each timestamp the GPU time of the work around it (profile key `gpu_timestamps`, which its dynamic resolution needs too). Before that it saw only the main thread's CPU time (about 3 ms a frame in the first shrine) and stayed at 60 in slow motion: at 40 fps, about two thirds of the normal speed (an iPhone 16 Pro Max in handheld mode, GitHub issue #9; [04](agents/04-graphics.md)). Under load it still ran at 0.93–0.98 of real time. - **Render scale:** `DM_RENDER_SCALE=0.5`–`1.0` (also from `Documents/env.txt`) fixes the 3D views at that fraction of 1600×900 docked (1280×720 handheld) and turns the game's own dynamic resolution off; 1.0, the default, keeps it. A stub sets the scale where the game applies its own (`main+0x1266880`), so every scene pass, the shader's resolution ratio and the LOD check follow it: at 0.5 the 1600×900 passes become 800×450 and the 800×450, 400×225, 200×112 and 100×56 chains halve too, while the shadow maps and the 1920×1080 output keep their size and the HUD stays sharp. It holds in handheld mode too (960×540 at 0.75). On the Mac (M4 Pro) the GPU time does not change (about 13 ms in the first shrine at 1.0 and at 0.5): a frame has about 650 passes, most of them tiny, whose fixed cost outweighs their pixels there. Whether it helps an iPhone, whose GPU loads and stores each pass's attachments, is what the setting is for. -- **Open: an intermittent crash at the world load on the Mac.** About 3 s after Start, a task worker's `operator new[]` (`main+0x155a79c`) gets null from a game heap and writes through it (`main+0x155a7a4`). It happens in legacy too (1 of 8 runs), so the frame step does not cause it, though it may make it likelier: 12 of 33 runs across the variants tried (each stub alone, no pay-back, the game's interval as a floor), solo or beside other runs. None of the about 95 earlier logs (up to 2026-10-19) has it. +- **Open: an intermittent crash at the world load on the Mac.** About 3 s after Start, a task worker's `operator new[]` (`main+0x155a79c`) gets null from a game heap and writes through it (`main+0x155a7a4`). It happened with the earlier, elapsed-time frame step too (1 of 8 runs), so the frame step does not cause it, though it may make it likelier: 12 of 33 runs across the variants tried (each stub alone, no pay-back, the game's interval as a floor), solo or beside other runs. None of the about 95 earlier logs (up to 2026-10-19) has it. - **Memory:** about 2.8 GB in the first shrine in handheld mode, idle or walking (Mac measurement), against the 3.4 GB an 8 GB iPhone gives the app. - The game waits for each frame's GPU work before submitting the next, so the host hands its translated commands to the GPU in segments ([04](agents/04-graphics.md)). - **HUD prompt shadows and glows, stamina wheel, System menu background:** the game rebuilds each layout capture (a blurred mask of a prompt bar, the menu's background) on the same storage with its own channel mapping and keeps the new texture only if `nvnTextureCompare` calls the two different; the host's comparison ignored the mapping, so captures read alpha 1 (hard-edged dark boxes, cream cloud rectangles, a black menu background). It compares the mapping now ([04](agents/04-graphics.md)). @@ -255,7 +255,7 @@ The game's own work and its frame rate stay as they are; the host avoids work of **The Legend of Zelda: Tears of the Kingdom (`0100F2C0115B6000`)** - **60 fps at normal speed:** the game picks a present interval each frame (2 = 30 fps, 3 under load) and steps its game time by 60 / fps. Stubs in `patch_nso.py` (`FRAME_STEP`; the sites come from masagrator's FPSLocker config for this build) replace that logic with the host's frame step in whole vsyncs, as in Breath of the Wild: int fps = 60 / step. Prerendered movies hold 30. On the Mac (M4 Pro) the opening cave holds 60 at 1.000 of real time; with 12 ms added to every frame it drops to 30 after three late frames and stays at 1.000. `DM_FPS_TARGET=30` saves power, `0` gives the game's own step back (every stub then runs the original instructions). -- **Legacy method** (`DM_FRAME_STEP=legacy`, iPhone-tested at 60, kept for the A/B test): the stub steps by the measured frame time, capped at the target, and presents follow the cap. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up. +- **Before the host has decided a step** (the first frames): the stub steps by the measured frame time, capped at the target. Game time that ran ahead after a short frame is paid back by the next frames (the idea from OJZen/totk-mobile); frames longer than 1/15 s are not caught up. - **Render scale:** `DM_RENDER_SCALE=0.5`–`1.0` multiplies the size of the 3D views (1600x896 docked) and fixes it there; the game's own dynamic resolution runs only at 1.0. The HUD keeps its resolution. **Monster Hunter Rise (`0100B04011742000`)** diff --git a/scripts/build_ipa_from_nsp.py b/scripts/build_ipa_from_nsp.py index dc8d700..1cef796 100755 --- a/scripts/build_ipa_from_nsp.py +++ b/scripts/build_ipa_from_nsp.py @@ -387,7 +387,7 @@ def survey(nsp_dir, app, fam, keys, repo, py, env): R += [f'| {n} | {m["kind"]} | `{m["bid"]}` | ' + ' / '.join(f'{z / 2**20:.1f}' for z in m['sizes']) + f' | {m["bss"] / 2**20:.1f} |' for n, m in mods.items()] if F['program'] is not None: R.append(f'\n- main.npdm program ID: `{F["program"]:016X}`') if (ex / 'ExeFS/main.npdm').exists(): R.append(f'- system resource: {F["sysres"] / 2**20:.0f} MB') - R.append(f'- SDK: {", ".join(sorted(F["sdkver"])) or "?"} (Mario: 16_2_0 / 16_2_12; D2R ≈ 12)') + R.append(f'- SDK: {", ".join(sorted(F["sdkver"])) or "?"} (Mario: 16_2_0 / 16_2_12)') R.append('- SDK components: ' + (', '.join(sorted(F['comps'])) or '—')) if F['unity']: R.append('- Unity: ' + ', '.join(sorted(F['unity']))) R.append('- engine and API markers: ' + ('; '.join(f'{k} ({", ".join(v)})' for k, v in F['markers'].items()) or '—')) @@ -619,14 +619,13 @@ def check_build(d, name, g): # every module's build ID must be the one registe def rewrite(py, repo, exe_dir, out, env, logs, g): - """patch_nso.py on every code module at once (each writes its own <module>.* files, main also the boot list, all the same build_src.sha256 line), each with its log. - Reused when the modules, patch_nso.py, the boot lists, its switches and capstone are as at the last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min)""" + """patch_nso.py on every code module at once (each writes its own <module>.* files, all the same build_src.sha256 line), each with its log. + Reused when the modules, patch_nso.py, its switch and capstone are as at the last successful rewrite (Monster Hunter Rise's main alone takes 5.5 min)""" import capstone mods = g.words('modules') PROG.stage('rewrite', step='checking the inputs') kf = out.with_name(out.name + '.key') - key = fingerprint(('files', [repo / 'dm/tools/patch_nso.py'] + [exe_dir / m for m in mods] + sorted((repo / 'dm/data').glob('bootlist_*.tsv'))), mods, capstone.__version__, - {k: env.get(k) for k in ('DM_PATCH_FRONT', 'DM_PRIVATE_GATES')}) + key = fingerprint(('files', [repo / 'dm/tools/patch_nso.py'] + [exe_dir / m for m in mods]), mods, capstone.__version__, {k: env.get(k) for k in ('DM_PATCH_FRONT',)}) if all((out / f'{m}.patched.bin').is_file() for m in mods) and reused(kf, key) is not None: PROG.end(f'{", ".join(mods)}, reused (inputs unchanged)'); return tmp = out.with_name(out.name + '.partial'); rmtree(tmp); tmp.mkdir(parents=True); logs.mkdir(parents=True, exist_ok=True) @@ -635,7 +634,7 @@ def rewrite(py, repo, exe_dir, out, env, logs, g): try: for m in mods: logf[m] = open(logs / f'rewrite_{m}.log', 'w') - procs[m] = subprocess.Popen([str(py), str(repo / 'dm/tools/patch_nso.py'), str(exe_dir / m), str(tmp), m], env=dict(env, DM_RULESET='native'), stdout=logf[m], stderr=subprocess.STDOUT) + procs[m] = subprocess.Popen([str(py), str(repo / 'dm/tools/patch_nso.py'), str(exe_dir / m), str(tmp), m], env=env, stdout=logf[m], stderr=subprocess.STDOUT) left = list(mods) while left: for m in [m for m in left if procs[m].poll() is not None]: