Skip to content

WIP: Mario Kart World 1.8.0 on iOS - rendering, memory and presentation - #1383

Draft
sn0wst0rm wants to merge 1 commit into
seedds:mainfrom
sn0wst0rm:ios/mkw-current
Draft

sn0wst0rm wants to merge 1 commit into
seedds:mainfrom
sn0wst0rm:ios/mkw-current

Conversation

@sn0wst0rm

Copy link
Copy Markdown
Contributor

This draft collects the current iOS work for Mario Kart World 1.8.0.

Gameplay runs on a physical iPhone with HDR and Docked rendering. The previously observed waterfall jumps, transient water geometry and image-quality regression are resolved in the tested configuration.

The implementation includes:

  • Memory-footprint reductions through file-backed guest memory, sparse textures and bounded upload staging.
  • Texture-coherency and GPU queue-ordering fixes.
  • Reduced encoding-worker synchronization and indexed dirty-region lookup.
  • HDR presentation and correct synchronization between rendering and the classic presentation queue.

Sustained performance remains open. Gameplay can maintain 60 FPS, but some sessions enter a persistent slow state. Measurements show periods of increased CPU submission cost followed by increased GPU execution time. Background/foreground transitions sometimes recover performance, but not reliably. No definitive cause or stabilization fix is claimed.

Validation includes physical-device gameplay, iOS Release builds and targeted Metal tests covering resource lifetime, sparse mappings, upload ranges, presentation ordering and timestamp feedback.

This is a development snapshot, including opt-in diagnostics.

Capture the current development source, including memory and sparse texture fixes, HDR presentation, queue ordering, and opt-in diagnostics. Persistent iOS performance degradation remains under investigation.
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) textures Texture layouts and formats (dm/gfx/tex) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers ios The iPhone and iPad app (dm/ios) mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts) docs Documentation shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders game: 0400C3F00006E000 A game profile labels Nov 5, 2026
@sn0wst0rm sn0wst0rm changed the title WIP: Mario Kart World 1.8.0 on iOS — rendering, memory and presentation WIP: Mario Kart World 1.8.0 on iOS - rendering, memory and presentation Nov 5, 2026
@oliverames

Copy link
Copy Markdown
Contributor

Evidence comment for #1383 — post after run 4 confirms the menu visuals

Cross-referencing, from tonight's independent MKW 1.8.0 build on the Duo (iPhone19,4, iOS 27.3, the user's own phone; build e8ac738 + four upstream NUP-script cherry-picks on scripts/ only):

  1. The unbacked baseline reproduces the exact failure the note documents: launching without the tested Documents/env.txt settings, footprint filled to the wall at the world-map load — budget rows end at footprint 8189 MiB, available 2 MiB, and the JetsamEvent (2026-11-06-171000) names DmIOS, reason: highwater, rpages 524,332 × 16 KiB = 8,194 MiB, the same per-process-limit signature as the note's Nov 3 icon-launch kill. Our maps of the SDK-sizing crash on the same phone (oliverames lanes, Nov 3) are consistent with the shipped behavior the environment keys change.

  2. With the recipe from your own note (DM_GUEST_FILE_BACKED=1, DM_NVN_SPARSE_RT=1, DM_NVN_CPU_UPLOAD=1, DM_NVN_ENC_THREAD=1, DM_NVN_UPLOAD_RANGE=1, plus DM_LAUNCHER=0, DM_OPMODE=1, DM_HDR=1, DM_PRESENT_LINK=0 and DM_MEMSTAT=memstat.tsv for the trail):

  • the host takes the path: [mem] experimental shared-file guest backing: 11520 MiB reserved,
  • footprint by the minute: 4,572 (199 s) → 4,844-5,229 bands → 5,088 steady at 480 s, peak 5,262, available never below 3,103, graphics ledger ~3.4 GiB inside; no creep over two independent runs,
  • prewarm intact from the packed g19p archive: 3,009-row pso_user.tsv, 2,745 shaders + 185 render + 79 compute pipelines, zero "not in the shader map" keys,
  • sparse placement behaves per the PR: 17 placement heaps, ~21,000 tiles mapped, syncs uploading only a handful of guest tiles (0-1 MB) once streamed in,
  • the frames we see at menus load to present interval 2 with GPU 11-15 ms and only small "new pipelines 1-3" compile trickle; the only notable hitches are the occasional 130-250 ms frames where 128-135 MB of sparse re-uploads (paged-out guest tiles returning) coincide — the file-backed page-in shape,
  • and the visual claim backs up: a raw DM_NVN_SHOT from this Duo build (in an SDR pass — with DM_HDR=1 the screenshot writer answers [RED] screenshot does not support format 90, an open writer limitation, not a rendering one) shows the logo, the full distant towers/scenery and Mario in the kart with no missing textures, no magenta surfaces and no corruption; the black right/bottom area is the swap chain's unrendered region the present crops, as the note's raw-shot description says.
  1. Two other evidence points on the same wall (cheemu21's plumber-racer-2, MKW 1.0.0, whole cup at ~3.9 GB on an iPhone 18 Pro in LiveContainer; kartnative, origin of the sparse checks and mem_map_limit): three lines converged on file-backed guest memory, and two of them independently report the same unresolved post-60-second slow-state in races (your M5 Max clip comment on Add HDR presentation and restore macOS Game Mode #1249, quoted verbatim). We have not yet reproduced in-race 60 fps stability either; our session so far is menus + the load, with the memory ceiling gone.

Neither fork has upstream PRs open, so their evidence lives only in readmes; the README's community-credit section (PR #1581) now names them. No new upstream claims: everything above is from the note's own persisted configuration, checking that a from-scratch Duo installation reproduces the note's memory results and the pre-env failure.

@oliverames

Copy link
Copy Markdown
Contributor

The driven lap is in on the same Duo (SDR pass, free-roam, auto-accelerate, ~4.3 min driving after the memory run above). Two findings, the second is new upstream as far as we can tell:

  1. Frame profile in the drive: GPU p50 15.5 ms / p95 28.7 (not the constraint); whole-frame p50 61.3 ms / p95 245.7 with submit+present p50 33.5 / p95 225.3. Worst frames split between file-backed tile page-ins riding the submit thread (95 MB/163 ms) and upload-free 150-400 ms frames matching your submit-side stall description. Memory stayed 5.1-5.7 GiB peak with 2.5 GiB headroom — the recipe holds under load.

  2. The device's own restart record: the drive leaves 25 gpuEvent (bug_type 284) firmware restarts in two clusters (17:36 and 17:48-17:50), every one restart_reason 12: DSF Pause Timeout, guilty_dm: 0, firmware perf state pinned low (0-1) during them, fw_power_limit_controller: 2. The app's ledger matches: 121 MTL4CommandQueueErrorDomain errors, all kIOGPUCommandBufferCallbackErrorInnocentVictim — our buffers die as bystanders of a driver-side recovery that our own log cannot attribute. That is a device-level cause candidate for the sustained slow-state both our notes record as open, and it hints the characterization work belongs in the present/display path as much as in the render path. A cooled-pause control (your note asks for one) and a per-restart window capture are the obvious next probes on our side.

This is evidence, not a verdict: what pauses DSF is still unknown to us, and the clusters could be power-limiting rather than anything the host does. Fixed no code; offered for whoever is in this code (you, and the fork family).

@oliverames

Copy link
Copy Markdown
Contributor

Research index for reviewers (the projects this build lane and its credit PRs draw on), and where the Ocarina of Time build will pick this up:

Full link table + run numbers + the DSF-Pause-Timeout lead: kept in the session's research record (FOLLOWUP-MKW-PR1383-DEPLOY.md in oliverames/anygame, and a copy beside the OoT game files where the rebuild will live). Nothing above changes this PR's code; the port rides the same anchors this PR carries.

@oliverames

Copy link
Copy Markdown
Contributor

The driven session on the ported build (the last test of the night) closed the memory half of the iPhone question on this Duo:

  • footprint 3.7-3.9 GB through the whole free-roam drive (the recipe line above held the boot ramp at 3,737 and the session peaked at 3,868) — under the 4 GB bar the fork lines record for the whole cup;
  • whole-frame record p50 51.2 / p95 81.4 / max 372 ms as against 57.6/215.2/963.5 without the switches — the spike tail (the file-backed page-in bursts riding the submit thread, a shape the note's own memory section carries as an open hygiene item) disappears once those posts no longer wire whole pools;
  • the driven floors leave the same sustained-fps wall every line records: the 60 fps in-race state that both this PR and the forks list as open, with the submission-side victim errors and the first drive's gpuEvent clusters (DSF Pause Timeout in the device record) as the two leads nobody has named a mechanism for yet.

The soft path itself is filed upstream as PR #1599 (Soft textures...) on side-by-side anchors of #1252/#1383's family, Co-authored-by the authors who got it to run: kartnative (pahorema) and plumber-racer-2 (cheemu21), where their own lines carry the same numbers (3.9 GB / 851 MB wrapped pools on an iPhone 18 Pro). #1599's body carries this run's log excerpts and the comparison against the no-switch build. The port will also apply to this PR's anchors verbatim as soon as this line moves (my fork's branch pr-1383-deploy now carries both the NUP cherry-picks and the soft port at 19e98948 for whoever reviews this family).

oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 7, 2026
…its files is kept without its finest level, and its level-0 pages go back

A first upload of such a texture makes a host texture of its level 1 (Mario Kart World keeps large BC formats from its files with a packaged layout; the host texture that skips level 0 is sampled through the same normalized coordinates), and the guest pages only level 0 holds go back after 300 frames (nvn_dirty_release) unless the game copies from that texture (soft_harden keeps it whole, as Mario Kart World copies source textures into its terrain arrays; the kartnative line notes a copied-from texture that lost level 0 drawing purple). iOS wires the whole memory pool behind a zero-copy buffer, so these uploads run on the CPU while the pool has none (mtl_pool_wrapped) instead of handing the pool a buffer it never addresses (the kartnative/plumber-racer-2 measurements: wrapped pools 3.7 GB -> 851 MB there).

The mechanics: nvn_dirty_release gives back only still-protected whole pages whose bytes the host keeps (read-only again after the madvise dance, then given all-zero or left as-is); with the guest in files (DM_GUEST_FILE) there is nothing to give back (dm_guest_file_backed). The pipeline: tex_soft gates the descriptor (level shift sk) in tex_desc, and every upload reads the source one level down (pkg_upload_, CPU deswizzle included); the copy hooks (nvnCopyTexture t2t/t2b) harden the source first (soft_harden, then a pkg_upload of the whole levels); ReadTexels sees one lower level (tex_soft_level, an unmapped level-0 read reads 0). All default-off: the switches are DM_NVN_TEX_SOFT and DM_NVN_TEX_RELEASE, so no game changes until its profile names tex_soft; the sweep runs once a submission (mtl_sub_begin) and leaves a 300-frame grace (soft_sweep), as kartnative's does.

Where it came from: the kartnative (pahorema) and plumber-racer-2 (cheemu21) forks (MIT) had this first, for Mario Kart World on an iPhone 18 Pro in LiveContainer at about 3.9 GB. The port keeps their behaviour and takes this tree's own anchors (tex_desc, pkg_upload_, the copies, ReadTexels, the dirty layer). Measured on an iPhone Duo (iOS 27.3) through the seedds#1383 family's same anchors: a full free-roam session at 3.7-3.9 GB where the same content without the switches sat at 4.5-5.3, and the whole-frame record lost its p95 spikes (51/81 ms p50/p95 with the switches as against 57/215 without). The tests in this PR run the CPU-only layer here; the GPU checks and the phone runs are recorded on the source lines.
@oliverames

Copy link
Copy Markdown
Contributor

More evidence from an iPhone 19,4 (2 Performance + 4 Efficiency cores) running this PR's Mario Kart World 1.8.0 build, 2026-11-07, plus two follow-up PRs.

  • Crash after ~85 s: an uncaught CHHapticEngine checkEngineRunning exception, not memory (footprint flat at about 3.7 GiB). Fixed in A Core Haptics engine that stopped under a call raises an NSException: handle it as a failed engine instead of ending the app #1694, with a test that fails without it.
  • The first power line is preceded by the CPU's tiers (hw.perflevelN name, cores, L2) #1695 logs the CPU tiers before the first power line, so the performance-core share reads against the chip.
  • The slow state has two parts here. Around frame 2400 to 3600 the game flips its present interval 1 and 2 about 350 times while the mean CPU clock goes 3.5 to 1.6 GHz and the performance-core share 56% to 8% (thermal fair). From frame 3900 the clock is back at 3.5 GHz, but GPU time is 26 to 30 ms instead of 11 ms and the game sits at 30 fps. Memory stays flat throughout. The cause of the GPU part is still open.
  • HDR: telling the game the phone's own peak (1,600 nits by model, instead of the fixed 1,000) looks good on the phone. It needs the HDR code that is in this PR, so I'm noting it here instead of opening a separate PR.
  • cheemu21/plumber-racer-2 reports the same 60 fps then slow state on 1.0.0 and says the game sets interval 2 after a late frame, so the flips look like a reaction to late frames. Its README lists DM_NVN_BARRIER=game, DM_NVN_TBDR=1 and DM_RT=0 as ruled out.

Logs and the run record are kept in oliverames/anygame (FOLLOWUP-MKW-NOV7-FINDINGS.md).

@oliverames

Copy link
Copy Markdown
Contributor

Follow-up PRs from the same runs, all marked as not fully tested: #1699 (DM_PCORE), #1700 (DM_RT60), #1701 (DM_ITV_LATCH), each opt-in and off by default. The device HDR peak change is a stacked PR on this head, at oliverames#8, because main has no HDR code yet.

@oliverames

Copy link
Copy Markdown
Contributor

The slow state after about a minute (60 fps, then a plateau at about 3 times the GPU time) now has a measurement behind it. #1709 adds DM_GPU_CANARY=1, a fixed compute dispatch once a second whose GPU time is logged, so a GPU clock fall can be told from a heavier scene. On an iPhone 19,4 running this PR's Mario Kart World 1.8.0 build (handheld, HDR off), one run:

  • the canary took 4.3 to 4.8 ms from 11 to 28 s, ramped for about 4 s, then took 13.8 to 17.9 ms (3.1 to 3.3 times longer) until the end of the log at 62 s;
  • the game's own GPU p50 went from 7.5 to 8.9 ms to 29 to 31 ms (3.3 to 3.5 times) at the same moment;
  • the ProcessInfo thermal state stayed 1 and the CPU stayed at 3.5 to 3.8 GHz.

So in that run the GPU clock fell to about a third and the scene was not the cause. Earlier runs had already ruled out HDR (on or off), the interval flips, the display-interval mode and the scheduler declarations. One run on one device, so a second run is still needed; the next test is a 30 fps cap from the start, to see whether a lower sustained load avoids the drop.

oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 8, 2026
…its files is kept without its finest level, and its level-0 pages go back

A first upload of such a texture makes a host texture of its level 1 (Mario Kart World keeps large BC formats from its files with a packaged layout; the host texture that skips level 0 is sampled through the same normalized coordinates), and the guest pages only level 0 holds go back after 300 frames (nvn_dirty_release) unless the game copies from that texture (soft_harden keeps it whole, as Mario Kart World copies source textures into its terrain arrays; the kartnative line notes a copied-from texture that lost level 0 drawing purple). iOS wires the whole memory pool behind a zero-copy buffer, so these uploads run on the CPU while the pool has none (mtl_pool_wrapped) instead of handing the pool a buffer it never addresses (the kartnative/plumber-racer-2 measurements: wrapped pools 3.7 GB -> 851 MB there).

The mechanics: nvn_dirty_release gives back only still-protected whole pages whose bytes the host keeps (read-only again after the madvise dance, then given all-zero or left as-is); with the guest in files (DM_GUEST_FILE) there is nothing to give back (dm_guest_file_backed). The pipeline: tex_soft gates the descriptor (level shift sk) in tex_desc, and every upload reads the source one level down (pkg_upload_, CPU deswizzle included); the copy hooks (nvnCopyTexture t2t/t2b) harden the source first (soft_harden, then a pkg_upload of the whole levels); ReadTexels sees one lower level (tex_soft_level, an unmapped level-0 read reads 0). All default-off: the switches are DM_NVN_TEX_SOFT and DM_NVN_TEX_RELEASE, so no game changes until its profile names tex_soft; the sweep runs once a submission (mtl_sub_begin) and leaves a 300-frame grace (soft_sweep), as kartnative's does.

Where it came from: the kartnative (pahorema) and plumber-racer-2 (cheemu21) forks (MIT) had this first, for Mario Kart World on an iPhone 18 Pro in LiveContainer at about 3.9 GB. The port keeps their behaviour and takes this tree's own anchors (tex_desc, pkg_upload_, the copies, ReadTexels, the dirty layer). Measured on an iPhone Duo (iOS 27.3) through the seedds#1383 family's same anchors: a full free-roam session at 3.7-3.9 GB where the same content without the switches sat at 4.5-5.3, and the whole-frame record lost its p95 spikes (51/81 ms p50/p95 with the switches as against 57/215 without). The tests in this PR run the CPU-only layer here; the GPU checks and the phone runs are recorded on the source lines.
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
…ntry, so a project.yml that carries one prepares the app

A project source a front end or a fork copy keeps can carry DMCommit in its Info.plist values; xcodegen writes it into Gen/Info.plist at the generate step, and the prepare then stopped at the stamp: "Add: :DMCommit Entry Already Exists", "[RED] build_ipa.sh exit code 1" (the Mario Kart World seedds#1383 snapshot on the Duo, 2026-11-06, its prepare_xcode.log). The built commit belongs in the stamp, so it now falls back to Set: the same prepare succeeds and the app logs the real source and chain. Gen/Info.plist is regenerated at every prepare, so the replaced value does not outlive the build.

Checked: bash -n passes; the Mario Kart World prepare that failed before now stamps both keys and finishes with the same Xcode project output ('source: <built commit>' in its prepare_xcode.log).
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
…its files is kept without its finest level, and its level-0 pages go back

A first upload of such a texture makes a host texture of its level 1 (Mario Kart World keeps large BC formats from its files with a packaged layout; the host texture that skips level 0 is sampled through the same normalized coordinates), and the guest pages only level 0 holds go back after 300 frames (nvn_dirty_release) unless the game copies from that texture (soft_harden keeps it whole, as Mario Kart World copies source textures into its terrain arrays; the kartnative line notes a copied-from texture that lost level 0 drawing purple). iOS wires the whole memory pool behind a zero-copy buffer, so these uploads run on the CPU while the pool has none (mtl_pool_wrapped) instead of handing the pool a buffer it never addresses (the kartnative/plumber-racer-2 measurements: wrapped pools 3.7 GB -> 851 MB there).

The mechanics: nvn_dirty_release gives back only still-protected whole pages whose bytes the host keeps (read-only again after the madvise dance, then given all-zero or left as-is); with the guest in files (DM_GUEST_FILE) there is nothing to give back (dm_guest_file_backed). The pipeline: tex_soft gates the descriptor (level shift sk) in tex_desc, and every upload reads the source one level down (pkg_upload_, CPU deswizzle included); the copy hooks (nvnCopyTexture t2t/t2b) harden the source first (soft_harden, then a pkg_upload of the whole levels); ReadTexels sees one lower level (tex_soft_level, an unmapped level-0 read reads 0). All default-off: the switches are DM_NVN_TEX_SOFT and DM_NVN_TEX_RELEASE, so no game changes until its profile names tex_soft; the sweep runs once a submission (mtl_sub_begin) and leaves a 300-frame grace (soft_sweep), as kartnative's does.

Where it came from: the kartnative (pahorema) and plumber-racer-2 (cheemu21) forks (MIT) had this first, for Mario Kart World on an iPhone 18 Pro in LiveContainer at about 3.9 GB. The port keeps their behaviour and takes this tree's own anchors (tex_desc, pkg_upload_, the copies, ReadTexels, the dirty layer). Measured on an iPhone Duo (iOS 27.3) through the seedds#1383 family's same anchors: a full free-roam session at 3.7-3.9 GB where the same content without the switches sat at 4.5-5.3, and the whole-frame record lost its p95 spikes (51/81 ms p50/p95 with the switches as against 57/215 without). The tests in this PR run the CPU-only layer here; the GPU checks and the phone runs are recorded on the source lines.
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
…its files is kept without its finest level, and its level-0 pages go back

A first upload of such a texture makes a host texture of its level 1 (Mario Kart World keeps large BC formats from its files with a packaged layout; the host texture that skips level 0 is sampled through the same normalized coordinates), and the guest pages only level 0 holds go back after 300 frames (nvn_dirty_release) unless the game copies from that texture (soft_harden keeps it whole, as Mario Kart World copies source textures into its terrain arrays; the kartnative line notes a copied-from texture that lost level 0 drawing purple). iOS wires the whole memory pool behind a zero-copy buffer, so these uploads run on the CPU while the pool has none (mtl_pool_wrapped) instead of handing the pool a buffer it never addresses (the kartnative/plumber-racer-2 measurements: wrapped pools 3.7 GB -> 851 MB there).

The mechanics: nvn_dirty_release gives back only still-protected whole pages whose bytes the host keeps (read-only again after the madvise dance, then given all-zero or left as-is); with the guest in files (DM_GUEST_FILE) there is nothing to give back (dm_guest_file_backed). The pipeline: tex_soft gates the descriptor (level shift sk) in tex_desc, and every upload reads the source one level down (pkg_upload_, CPU deswizzle included); the copy hooks (nvnCopyTexture t2t/t2b) harden the source first (soft_harden, then a pkg_upload of the whole levels); ReadTexels sees one lower level (tex_soft_level, an unmapped level-0 read reads 0). All default-off: the switches are DM_NVN_TEX_SOFT and DM_NVN_TEX_RELEASE, so no game changes until its profile names tex_soft; the sweep runs once a submission (mtl_sub_begin) and leaves a 300-frame grace (soft_sweep), as kartnative's does.

Where it came from: the kartnative (pahorema) and plumber-racer-2 (cheemu21) forks (MIT) had this first, for Mario Kart World on an iPhone 18 Pro in LiveContainer at about 3.9 GB. The port keeps their behaviour and takes this tree's own anchors (tex_desc, pkg_upload_, the copies, ReadTexels, the dirty layer). Measured on an iPhone Duo (iOS 27.3) through the seedds#1383 family's same anchors: a full free-roam session at 3.7-3.9 GB where the same content without the switches sat at 4.5-5.3, and the whole-frame record lost its p95 spikes (51/81 ms p50/p95 with the switches as against 57/215 without). The tests in this PR run the CPU-only layer here; the GPU checks and the phone runs are recorded on the source lines.
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
…ntry, so a project.yml that carries one prepares the app

A project source a front end or a fork copy keeps can carry DMCommit in its Info.plist values; xcodegen writes it into Gen/Info.plist at the generate step, and the prepare then stopped at the stamp: "Add: :DMCommit Entry Already Exists", "[RED] build_ipa.sh exit code 1" (the Mario Kart World seedds#1383 snapshot on the Duo, 2026-11-06, its prepare_xcode.log). The built commit belongs in the stamp, so it now falls back to Set: the same prepare succeeds and the app logs the real source and chain. Gen/Info.plist is regenerated at every prepare, so the replaced value does not outlive the build.

Checked: bash -n passes; the Mario Kart World prepare that failed before now stamps both keys and finishes with the same Xcode project output ('source: <built commit>' in its prepare_xcode.log).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400C3F00006E000 A game profile host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) mac The Mac front end and app (dm/mac, front_mac.m) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders textures Texture layouts and formats (dm/gfx/tex) tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants