The native port builds as a libnx homebrew application with Mesa NVK Vulkan. Kazarov owns the shared Switch runtime, Mesa/SDL toolchain, dependency patches and development-unit test harness. Chronicle and ChronicleTwo use the same implementation.
Run ./build.sh switch with Docker available from Linux or WSL. The first build creates the pinned toolchain image; later builds reuse it. Game compilation defaults to four workers; JOBS overrides that limit, including the host bytecode compiler. KZ_SWITCH_IMAGE can select an existing compatible development image. The output is port/build/switch/darkchronicle.nro.
Extract current game data with the desktop dc2data tool. Copy these files to sdmc:/switch/chronicletwo/:
darkchronicle.nrofromport/build/switch/scripts/,glyphs/andfonts/from the same build directorydata.zipanddata.jsonfrom the current extraction
Keep disc-derived data outside Git. The runtime accepts the current ZIP64/Zstandard data archive directly; an extracted data/ tree also works. data.json must match the data version expected by the executable. The Switch does not extract disc images itself.
Launch with Homebrew Menu title override to give the game application memory. Put whitespace-separated game arguments in args.txt beside the NRO. Embedded romfs:/args.txt takes precedence during SDK testing. --env NAME=VALUE entries set diagnostics before startup. Runtime logs, saves/, settings and the Vulkan pipeline cache live beside the application. Existing --run-dir, --data and --save arguments can override those paths.
tools/kazarov/tools/switch/run.sh sends an existing NRO with nxlink; set SWITCH_NRO, SWITCH_INSTALL_PATH and, if needed, SWITCH_ADDRESS.
The Switch workflow (.github/workflows/switch.yml) runs this build on pushes to master, pull requests and manual dispatches, and uploads darkchronicle.nro and its ELF.
Mesa NVK is built from the same pinned Mesa 26.2.4 Switch backend used by Chronicle. SDL creates a Vulkan surface and the statically linked driver presents its scanout images without a CPU framebuffer copy. Shaders are compiled to SPIR-V on the build host; NVK creates device pipelines at startup and compatible later launches reuse saves/pipeline_cache.bin.
Handheld output targets 1280 × 720 and docked output targets 1920 × 1080. The main raster matches the output: handheld renders 1280 × 720 and docked renders 1920 × 1080 color and depth images, and the FPS counter is drawn on whole output pixels. Shared-depth outline targets follow that raster; independent auxiliary targets use a render scale of output height over 480. --env KZ_MAIN_HEIGHT_LIMIT=<lines> caps the main raster for comparisons (480 gives 848 × 477), with the presentation blit scaling the result to the output. Output fills the display and has a 60 FPS presentation cap. Game modes retain their authored simulation cadence: town frames span two 60 Hz fields, while title frames span one. Interpolation smooths the intervening display frames without speeding up gameplay. The settings page omits these fixed display controls. Actual presentation rates depend on the scene and GPU budget.
Modes already producing one game frame per field present their canonical image directly. Eligible two-field frames present at most one interpolated sample followed by the authored canonical endpoint. Renderer lifetime, allocation generation and render-commit tokens identify that endpoint; stale images are discarded before a newer canonical render overwrites them. Mouse-camera changes, pointer replacement, pause captures, cuts and incompatible transitions retain the replay fallback. --env DC2_SWITCH_CANONICAL_ENDPOINTS=0 disables this policy for matched measurements. Host overlays use output-pixel mapping so their physical size is independent of the internal raster. Game readbacks report the actual internal dimensions.
V-Sync is always on: the swapchain presents in FIFO order and the settings page has no V-Sync row. The 60 FPS cap keeps a display that starts within half a period of its slot on that slot, so wake-up lateness never builds up into a skipped tick. An authored frame's intermediate image interpolates exactly halfway between its endpoints, the field it is shown on, however long its render waited for the GPU. The canonical render of a tick is queued for the render thread after the tick's lease, and runs while the game thread starts the next tick; work that render thread queues opens no frame for the game thread, so the next tick always records. Mesh batches first drawn in a tick reach the GPU under that tick's lease, where the uploads run without waiting for the render thread.
Luau scripts are precompiled on the build host and interpreted on the console. Guarded game memory, cooperative fibers, process-core affinity, arguments, logging and missing-data instructions use Kazarov's shared libnx platform layer. Texture decoding retains the existing stamp-aware cache and prefetch behavior, with a 64 MiB decoded-pixel cache and at most two prefetch workers on Switch.
Asynchronous rendering gives the game and render threads a core each. With three application cores they both leave the main thread's starting core, which the audio, decoding and loader threads share; with two, the game thread keeps it. Use --env KZ_SWITCH_CORE_AFFINITY=auto in args.txt to compare libnx's normal scheduling. The log records the selected policy and verified core masks.
The comparison against Chronicle separates existing facilities from transferred changes:
| Area | ChronicleTwo action |
|---|---|
| Source-file lookup | Retain its existing folded-name hash index; Chronicle's catalog-scan replacement would duplicate it. |
| Parsed models and textures | Retain parsed glTF documents, texture-set/candidate caches and stamp-aware decoded textures; an additional raw-PNG cache would retain redundant data. |
| Archive prefixes and directories | Retain the persistent prefix stream and cached directory listings. |
| Pose and mesh submission | Retain existing camera/pose rebasing, separate bone palettes, storage-buffer records, strip/display-list rewrites, upload reuse and descriptor-binding reuse. |
| Display-list and mesh optimizations | Integrate shared commit 4db28dd: owned recording arenas, conservative opaque-alpha and near-fog specialization, combined depth planes, reciprocal near-fog calculation and inline dispatch. Enable accepted Switch paths; keep their fallback behavior. |
| Pipeline startup | Integrate shared commit e7ebfc0: reuse compatible untextured pipelines and avoid rewriting an unchanged warm pipeline cache. |
| Console diagnostics | Transfer the bounded 4 MiB asynchronous file writer and SDK system logging to Kazarov. |
| Native archive runtime | Add one bulk central-directory index with lazy local-header checks, ZIP64 counts, stored/deflate/Zstandard decoding and decompressed-size/CRC verification. |
| Arena lifetime lookup | Find exact monotonic lease/resource identities with binary search; validate only the requested metadata on acquisition. Reset/free/shrink still prune retired metadata, preserving allocation, generation and dependency checks. |
| Outline presentation | Omit only unobserved intermediate edge-alpha writes when the following full-area body restores them. Kazarov can then discard exact zero-alpha RGB-only source-over fragments; tiny positive alpha, depth/stencil effects and unsupported masks retain their original behavior. |
| Switch texture retention | Bound decoded pixels to 64 MiB and prefetch workers to two; desktop defaults remain unchanged. |
| Chronicle game-specific changes | Exclude its shadow clipping, MDT construction and haze adjustments from ChronicleTwo's different native game implementation. |
| Rejected experiments | Keep fused 2D and approximate shader variants disabled. |
Opaque-alpha and near-fog shortcuts retain their plain-mesh eligibility checks. ChronicleTwo's Prog0 lighting and alpha semantics use the general path; enabling these shared flags does not establish a performance gain for ordinary Prog0 visuals. Its blur strips also differ from the shared depth-plane API and retain their existing implementation.
Native host validation covers stored/deflate/Zstandard streams, empty compressed files, UTF-8 names, 65,536-entry ZIP64 fixtures and malformed/CRC failures. A current 89,750-entry, 3.78 GB data archive was indexed and 21 selected assets matched independent decoding; the largest 179 MB asset also passed native CRC validation. Through the Windows/WSL bind mount, bulk indexing took 66–141 ms, compared with 130,840 ms for the initial seek-per-entry implementation. These are host/storage measurements and do not establish console loading times or frame rates.
SDK development-unit checks on 2026-10-31 confirmed Mesa NVK on Tegra X1 at the stock handheld configuration, with 1280 × 720 output. Before enabling interpolated presentation, the title sustained approximately 59.9–60.3 presented frames per second; Palm Brinks presented approximately 30, matching its two-field cadence, while a dungeon with its tutorial overlay presented approximately 24–25. Enabling the existing interpolation path alone reduced Palm Brinks presentation to approximately 18 FPS: a 60 FPS cap does not establish sustained 60 FPS. The overlay's TICK value measures the base field clock rather than separate game updates. A 180-frame bounded run wrote its screenshot and completed normal process shutdown successfully. With matching FFmpeg 9 headers and libraries, the RUSH attract movie decoded and displayed correctly at approximately 54–59 presented frames per second.
Subsequent host checks passed 91 memory/native-record tests, including 768 retained metadata identities with an interior heap-block retirement. The outline optimization passed seven focused RTX checks, including full RGBA and later destination-alpha comparisons, sub-GS-unit bilinear alpha, depth/stencil side effects, pipeline caching and presentation replacements. The capped-raster/token batch passed 18 separate pacing, image-lifetime and native presentation checks. These are distinct checkpoints, not a full-suite result. Final shared archive and dynamic-exit regression runs also passed; the Chronicle consumer linked against the same shared runtime and renderer.
On the SDK device, samples grouped by reported 460 and 512 draw counts showed approximately 6–7% presentation improvement from the sparse outline shader. The 460-draw median changed from approximately 18.0 to 19.2 FPS; the 512-draw median changed from approximately 13.2 to 14.0. Matching draw count is not an exact scene replay, and this change alone does not reach 60 FPS. The main-raster cap and canonical-endpoint policy require their own device measurements.
The first pipeline initialization created 21,845 pipelines in 57.46 seconds; a compatible warm cache reduced initialization to 1.21 seconds. Creation remains eager across five vertex families, 52 blend equations, three alpha variants and the texture/fragment specializations. Untextured specialization reuse removes 6,240 redundant creations, but cold startup still needs substantial time. These are startup measurements rather than a comparison against an older renderer with fewer shader variants.
Run the shared archive regression fixtures with the instructions in tools/kazarov/tools/switch/tests/README.md. They cover empty files, UTF-8 paths, ZIP64, Zstandard, malformed data and CRC failures. A real-data comparison is optional and keeps the archive outside the test sources.
Use --env KZ_PERF_LOG=1 for once-per-second canonical/display CPU and GPU samples, acquire/present/fence waits and presentation-interval buckets. Means and maxima are reported separately, together with total GPU milliseconds over the measured interval. Display preparation is also reported separately and is included in the overall display CPU sample; it must not be added to that sample again. --env KZ_GPU_SCOPES=pass adds GPU pass timings; KZ_PERF_REPLAY_PHASES=1 and KZ_PERF_MESH_PHASES=1 provide separate CPU breakdowns. Run diagnostic modes individually when comparing their overhead.
tools/switch/devkit-perf.ps1 -Scene <scene> [-RunRoot <dir with data.zip>] measures one scene on the development unit: title, town, town-still, town-sky, town:<map>, georama:<n>, the dungeon variants, dungeon:<n>:<floor> and boss:<event id> jump straight to it with --fast-load (dungeons and bosses also with --skip-events --invincible --seed 1), and all but the still and sky scenes turn the camera for the whole run. --perf-seconds waits for control and one more second, logs the requested duration and exits; the summary adds the renderer's CPU and GPU samples and, for dungeons, the floor's live monsters and chests. -Seconds changes the length, -Extra adds arguments such as --env DC2_PERF_ZONES=1 (per-zone game-thread times) or --env KZ_GPU_SCOPES=draw (GPU time by target and pipeline), -Replay <input> drives the run from a replay instead of the turning stick, and -Screenshot captures the running scene. The FPS counter costs an extra full-frame copy per canonical endpoint; --env DC2_SWITCH_HIDE_FPS=1 measures without it.
New harness runs default to eight measured seconds, with a one-second readiness warmup and room for tick scheduling inside the ten-second scene budget. The measured deadline uses total elapsed time rather than accumulating rounded one-second windows. Avoid requesting an SDK screenshot during readiness or measurement; its asynchronous capture work can overlap the first measured windows. A screenshot during the optional frozen capture hold is outside the sample.
The harness maintains docs/SWITCH-PERF.json atomically. -Next selects the first unfinished supported destination; explicit scene aliases resolve to the same numeric ledger key. Passed entries cannot be measured again, and the next destination remains blocked until the current destination passes. Historical submit-pacing passes retain their original scope and are not retested. A completed timing run appends its executable hashes and summary evidence without accepting an FPS average as a pass.
-PreparedNso <path> reuses an independently converted NSO without launching WSL. Its <path>.json sidecar must record elf_sha256, nso_sha256 and the actual Docker image_id; the harness checks the ELF and NSO bytes, image identifier and NSO header before launching. The default path still caches conversions by exact ELF and toolchain-image identity. -ExternalCapture leaves screenshots and recording to a separate Target Manager client while preserving the same timing and ledger workflow.
-Scene town:10 -RecordCapture <proof.json> records inspected evidence without launching the device. For native timing, set pacing_source to native_frame_history and provide scene, run, elf_sha256, nso_sha256, capture_path, capture_sha256 and scene_evidence_path, with scene_verified, camera_sweep_verified and measurement_interval_verified explicitly true. The run must use KZ_PERF_FRAME_HISTORY=1 and contain one complete native history report. Acceptance requires a clean collection, resolved Presented timestamps, complete measurement coverage, consecutive frame identifiers and 60 Hz presentation intervals within 0.5 ms. Cadence checks include the nearest pre-start and first post-end anchors; a deliberately frozen end without a later anchor must remain less than one refresh after the last presentation. Target Manager must report TimeCompleted for the same capture, target and executable. The inspected video may be 30 fps: it verifies scene and camera coverage, while native BufferQueue timestamps establish presentation cadence. Accepted entries use native_presentation_pass, and their evidence files, log, manifest and summary are hashed into the ledger.
The alternative composited-video proof binds the exact run and executable to a hashed video and decoded marker report. It must contain scene, run, elf_sha256, nso_sha256, marker_report, capture_sha256, scene_evidence_path, and calibration_evidence_paths, with scene_verified, camera_sweep_verified, measurement_interval_verified and calibrated explicitly true. Calibration evidence must demonstrate that the detector distinguishes a known full-rate sequence from a repeated-frame control. The run's summary supplies scene_key and measurement_seconds; its log must contain native scene verification and no invalid-scene result. The marker report supplies the video path, selected start/end PTS and decoded frames with pts, sequence and confidence_margin. Acceptance requires the complete measured interval, readable markers, consecutive sequence values and even 60 Hz frame timestamps. Evidence files and reports are hashed into the ledger. This establishes SDK composited-video pacing; it does not independently measure physical LCD scanout.
The requested Water Channel floor-3 stress fixture uses a fixed view rather than a camera sweep. Its capture proof must declare coverage_kind=fixed_stress, fixed_view_verified=true and all20_distinguishable_verified=true, with inspected video showing ten enemy bodies and ten chests together. Native timing evidence remains mandatory. Unique perf stress setup: and perf stress summary: records must agree on schema, generation, dungeon 0, floor 3, seed 1 and the ten-per-class targets. Every measured live, submitted-body and projected/map-ray count must have minimum and maximum ten, with no failures and complete measured canonical-tick coverage. These runtime witnesses cover canonical 30 Hz body submissions; they do not certify final-pixel occlusion or every interpolated image. The real floor layout and camera are still undergoing preview validation before a development-unit sample.
For capture diagnostics, --env KZ_PERF_PRESENT_MARKER=1 writes a 144 × 16 output-pixel marker at the top left after each presentation blit. Its first two 8-pixel cells are black and white synchronization cells, followed by sixteen sequence bits, least significant bit first. The sequence increments for every presentation attempt and wraps at 65,536. The marker adds a small transfer to the measured workload, so record its use in comparisons. --env DC2_PERF_CAPTURE_HOLD_MS=5000 freezes further presentation after the performance total and holds the last image for the capture service; the hold is outside the measured interval and accepts at most 30,000 ms. Capture runners must wait for perf capture hold: rather than terminating immediately on the total. A decoded composited video can establish captured-frame cadence; physical scanout pacing requires its own evidence.
The current eight-second validation campaign has accepted native presentation evidence for town:10, town:11, town:12, town:13 and Rainbow Butterfly Wood floor 2 (dungeon:1:2); these destinations must not be retested. The Woods pass used feedback, deferred canonical commit and split replay submission, with the depth prepass disabled: 483 native intervals averaged 16.666690 ms, ranging from 16.351302 to 16.983021 ms. This is a validated candidate profile; production defaults and lifecycle checks remain pending. A completed sample whose native history fails is recorded as native_pacing_failed, with its area kept at needs_optimization. A native timing pass still requires inspected scene and camera evidence before acceptance.
Halloween (boss:21020) has accepted native presentation evidence with its bombs in frame: 488 native intervals averaged 16.667 ms, ranging from 16.39 to 16.95 ms. Without the additive layer it presents about 55.5 FPS, falling to 46–50 FPS in its blasts. Its blast meshes cover the screen several times over, so the renderer draws such additive runs into a target a third of the main target's size per axis and adds the result; the renderer notes give the conditions.
--env DC2_PERF_ROSTER_STRESS=1 with a dungeon:<n>:<floor> scene places one monster of every kind the dungeon's floors can place around the player and frames them all with a fixed camera. perf roster log lines record the kinds loaded and the fewest alive, drawn and in frame during the sample. Add --env KZ_PERF_PRESENTED_GATE=0 for Zelmite Mine's 95 kinds, whose frames outlast the presented gate's polling budget. With every kind present the game thread sets the rate: Underground Water Channel 56.75 FPS, Rainbow Butterfly Wood 27.15, Starlight Canyon 30.74, Ocean’s Roar Cave 24.93, Mount Gundor 23.43, Moon Flower Palace 21.63 and Zelmite Mine 4.27.
When a frame pair would miss a vblank, the presenter shows the tick's endpoint in place of its interpolated intermediate. It predicts the pair from the latest canonical render's GPU time and the last rendered replay's ratio to it, against a 33.5 ms budget, and also holds six pairs after three long pairs in a row whatever the GPU reported, since one missed vblank can stretch two. Held pairs keep the 30 Hz tick and an even 60 Hz present on time but show 30 Hz motion, so intervals on one vsync do not by themselves mean every frame was new: the held count in each perf window and perf total line says how many intermediates were replaced. Across the 25 measured boss fights an 8-second sample each held 8 intermediates in total, all in boss:24010 and the train fight boss:22010. DC2_PRESENT_HOLD set to 0 or 1 overrides the default, which is on for the Switch only.
Pacing experiments are opt-in. KZ_PERF_VSYNC_GATE=1 waits for a fresh VI event before queuing a present. KZ_PERF_PRESENTED_GATE=1 takes precedence and instead waits for the exact previous accepted frame identifier to have a positive Presented timestamp; acquired state alone is insufficient. It submits the current GPU work before polling, yields between queries, and reports perf invalid: on protocol or polling-budget failure. The polling budget is 100 ms and 128 queries; individual Binder queries remain synchronous. Its perf presented gate: summary reports identifiers and query costs. The feedback experiment removed paired native timestamps in the Woods but still missed refreshes, so it has not established stable 60 FPS.
With actual Presented feedback active, DC2_PERF_DEFERRED_CANONICAL_COMMIT=1 enables a bounded asynchronous experiment: render the next canonical image, retire the previous image through the existing token checks, then commit and publish the new image. It preserves presentation order and rejects unsupported aliases, overlays or source changes. Ordinary rendering remains the fallback. The perf deferred canonical: summary records selection, rendering, retirement and fallback counts. This improved the Woods sample from 41.67 to 53.64 FPS while preserving every fully observed midpoint and endpoint, but native timing still failed. Native cadence and midpoint admission must both be checked before promoting this experiment.
KZ_PERF_SPLIT_REPLAY_SUBMIT=1 separately enables an experimental replay submission path in Kazarov. Acquisition and target preparation stay early; two queue calls submit offscreen rendering first, then the swapchain copy with the existing acquire wait and final frame fence. The perf replay split submit: summary reports actual selection and both submissions. Split frame-role GPU timestamps describe an envelope that includes acquisition waits; optional render-end and copy-start timestamps distinguish its segments. Compare native cadence and the explicit segments rather than treating that envelope as GPU busy time.
Development-unit runs on 2026-11-01 at stock handheld clocks (1280 × 720, FPS counter shown, ten seconds of a turning camera after one second with control) presented:
| Scene | FPS | Repeated presents |
|---|---|---|
| Title; Palm Brinks standing; Underground Water Channel floor 2 | 60.0 | 0 |
| Palm Brinks turning | 59.3–59.5 | 0–5, all in the first two seconds |
Starlight Canyon (dungeon:2:3), Mount Gundor (dungeon:4:3), Moon Flower Palace (dungeon:5:3) |
59.9–60.1 | single dips |
Zelmite Mine (dungeon:6:3) |
57.5 | |
Rainbow Butterfly Wood (dungeon:1:2) |
52–55 | 56–101 |
| Georama 0, 2 and 4 | 60.0 | 0 |
| Georama 1 (Balance Valley) | 52–56 | 42–78 |
| Georama 3 (Heim Rada) | 58.6 | 14 |
| The 25 boss fights a jump reaches | 59.88–60.25 | 0–3, with 22 fights at 0 |
All 32 Palm Brinks debug destinations (including interiors), the five Georamas, 160 dungeon map rooms plus one orphan floor-table entry, 27 boss events and retained measurement evidence are recorded in SWITCH-PERF.json. Georama historical submit-pacing passes cover the fresh empty debug fixture; populated building and river fixtures remain unmeasured. The active campaign tests one representative floor per dungeon, as requested: Water Channel floor 3, Rainbow Butterfly Wood floor 2, and floor 3 of each remaining dungeon. The user ended further Palm Brinks testing; its rows and earlier evidence remain preserved with validation_required=false. The active Water Channel floor-3 request adds a default-off stress fixture with ten enemies and ten chests simultaneously in view; its setup, visibility and performance evidence remain pending. Other floor records and their evidence remain in excluded_dungeon_floor_inventory; they are source inventory, not required tests. Historical submit-pacing passes remain complete and must not be repeated during the optimization queue. The existing presentation counters measure submission return, so they do not establish physical scanout pacing. A nominal 60 FPS average without a repeated-present counter does not independently establish even pacing; the ledger retains those observations separately. The legacy harness aliases describe different dungeons than their names suggest: dungeon indices 0–6 are Underground Water Channel, Rainbow Butterfly Wood, Starlight Canyon, Ocean’s Roar Cave, Mount Gundor, Moon Flower Palace and Zelmite Mine. Use explicit numeric jumps for comparisons. Ocean’s Roar Cave (dungeon:3:3) has no completed measurement in the retained runs. The boss table also includes Flotsam’s subgame (boss:22010) and Boss Sfida (boss:23010), neither of which has a retained measurement.
King Mardan, Death Ark, Gaspard 2, Monster Gaspard and Inferno did not reach control under --skip-events. The specialized scripts for Death Ark, Gaspard 2, Monster Gaspard and Inferno directly place their opponents; their unsuccessful setup has not been established as an input wait. Performance readiness verifies the requested loop and map/floor, and combat bosses require live opponent actors before a sample begins. Scene and actor identities are logged at readiness and measurement start.
GPU work and presentation scheduling both affect cadence. Blended and alpha-tested meshes and outline composites cost heavily in some directions, and an interpolated intermediate costs nearly as much as the canonical render. Native frame-role traces also expose presentation waits and CPU gaps between GPU submissions. Loading on the development unit runs from host-streamed romfs, where each archive read costs a round trip, so its load times overstate a console's; model documents dominate the parsing that follows.
tools/kazarov/tools/switch/devkit-run.ps1 stages and tests a libnx ELF through the connected SDK development unit. It records hashes, logs and a screenshot, holds the hardware lock, and restores console policies. Such a run validates the SDK-converted ELF; direct Homebrew Menu NRO launch is a separate check. Establish cold/warm-cache, title, town, dungeon, movie, save/load, shutdown and dock/undock results before making gameplay or frame-rate claims.