Repository navigation
Conversation
…orm/game_icon to this layout)
Interpolated() re-based every prog0 draw under a moving camera with two 4x4 inversions and copied the 2 KB block three times, even for unskinned draws. Consecutive draws under the same transforms now share the cached eye transforms, bone blending runs only for skinned blocks, overrides are built in place, and the world-to-clip matrix is computed once per camera. Display frames stay bit-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Horizon opens no directory as a file, so SyncDirectory failed every save; it has no directory sync, and commits a name when the call that made it returns. Its rename never replaces: that is the exclusive rename, and a replacing commit moves the old file aside first, removing it once the new one has its name, so an interruption leaves one of the two under a name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Switch every path starts with its device (sdmc:/switch/...), which std::filesystem over newlib reads as a relative first element, so fs::absolute put the working directory in front of the save directory a second time and the game stopped at startup: cannot create the save directory sdmc:/switch/chronicle/sdmc:/switch/chronicle/save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std::filesystem::rename never replaces a file on Horizon, so from the second start on the Switch the cache was never saved (cannot write the pipeline cache: File exists). FilesWrite moves it into place on every system. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skinned prog0 draws carry their 32-bone Prog0Palette as a separate record in the display list, bound from storage-buffer arrays with indices in push constants, so plain and prog0 draws share one pipeline layout. Transient chunks prefer device-local host-visible memory, display frames copy a grab twin only when the re-grab does not cover it, and the renderer can be instrumented with Tracy zones through the KZ_PORT_TRACY option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Device work can now run on a worker thread. RenderThreadMode::Off keeps the single-threaded path and stays the default. Lockstep runs the same work on the worker while the caller waits, and is byte-identical to Off. Async lets the worker interpolate, replay and present display frames while the game records the next tick; the game takes a render lease once per tick around the canonical render and other per-tick device work. Headless and screenshot runs always use Off. Tracy zones cover both threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tick submits twice, so two slots kept one tick in flight and the CPU waited for the GPU's last tick before recording the next. On the Switch's GPU that serialised the two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NAK turns a uniform branch into ssy/bra/sync and nops, and on the Switch's GPU a fragment costs about its instruction count. The push constants now carry the fog colour, blend factor, transform and target choices as values to select between, and the alpha test as a bit per less, equal and greater. Untextured draws go from 95 instructions to 43, alpha-tested ones from 132 to 71, palette ones from 302 to about 180. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An index texture drawn with a palette is expanded on the CPU into an RGBA texture the sampler filters, and expanded again when either is uploaded to, instead of the shader looking the palette up four times a pixel. An image written other than by an upload, a pair that changes every few frames and a pair already drawn this frame keep the lookup; expansions past a megatexel in a frame wait for the next. Off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cs + Cd * (1 - As), which the GS's (A - B) * C + D cannot express: a premultiplied image drawn over the target in one draw where the GS needs two (scale the target by the coverage, then add the image). One more blend state, after the GS's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WindowConfig::opengl makes an SDL_WINDOW_OPENGL window with a 4.3 core context's attributes and no depth of its own, for a renderer that draws into its own targets (Chronicle's GL renderer for the Switch's Mesa). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EndFrame waited for its frame to finish on the GPU whenever a depth query was made, so a game that reads depth every tick (Chronicle's pick-Z, the sun's lens flare) never recorded a tick while the GPU drew the last. On the Switch, whose GPU is the slower side, that held Norune at 30 frames a second near the sun. There each frame keeps the queries it read, and they are answered once its fence has passed (kAsyncDepthQueries): DepthResult holds the answer a tick or two later. Elsewhere nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An immediate mesh with the same vertices as the last one recorded (a shadow volume's adding and subtracting passes draw one corner array) keeps an index to that entry instead of its own copy, unless it blends its vertices between ticks; the Vulkan replay uploads such an array once a frame. In a dungeon about half of the immediate meshes share, which spares their copy into the list and their upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game can record a tick while the tick before renders and presents on a thread of its own (README, "Threads"). Recording and frames belong to the thread that opened them: RecordingCalls, Recording and InFrame answer for the calling thread (atomic thread markers), so a render on another thread executes its entries instead of recording them. One recursive lock guards the renderer: RenderList and PresentCanonical hold it throughout, BeginFrame until EndFrame, and every call that changes the renderer outside a recording takes it (creation and destruction, immediate uploads, readback, scale and layout, a list's release of what it doomed). Calls that only record take none. They still look handles up, so the texture and mesh tables are reserved whole and never move, and NamedRenderTarget finds an existing target without the lock. The main targets' size and the depth query answers are published atomically for the recording thread. PrepareRecording takes the lock only when the main targets must follow the window. ThreadSanitizer finds no race between the two threads over a save's title, loading and town, and a dungeon floor. Single-threaded, nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gfx: give Tracy's calibration buffer a pool that allows a reset TracyVkContext without calibrated-timestamp functions measures the GPU clock in the device time domain: it begins, submits and waits on the buffer three times. The second and third begins reset it implicitly, which a buffer from g_oneshot_pool (TRANSIENT only) may not do, so every renderer start logs two VUID-vkBeginCommandBuffer-commandBuffer-00050 errors. Under the validation layer every GPU test that counts validation messages fails. The calibration buffer now comes from its own pool with VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, destroyed once Tracy has calibrated. * Tests: give the unnamed arena overflow case a fixed address OverflowOfAnUnnamedArenaGivesItsAddress expected the address of a static in the parent's message. That holds only while the death test's child shares the parent's layout: a fork, or a fresh process on Windows, where an image keeps its base within a boot. With gtest's threadsafe style the child re-runs the binary, and on Linux and macOS a position-independent image gets a new base, so the case failed although the message was right. The case now passes a fixed address that ArenaOverflow only prints, so parent and child expect the same exact text, still spelled by the platform's %p. * Build: link Rust's macOS system libraries from the link's own SDK Corrosion 0.6.1 gives every macOS Rust target INTERFACE_LINK_DIRECTORIES /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib, so each executable linking kazarov_core searched the Command Line Tools SDK for -lSystem, -lc and -lm, whatever SDK the build uses. On the macos-26 runner that SDK is MacOSX27.0, whose .tbd files list arm64e.x1-macos, which the selected Xcode 26.6 linker cannot read: "tapi error: malformed file ... unknown architecture" for darkchronicle, dc2data and dc2data_bridge_test. The directory is dropped; the linker finds the libraries in the sysroot it is given. --------- Co-authored-by: carrdylan52 <17089587+carrdylan52@users.noreply.github.com>
Add a walk action (Left Shift) that halves keyboard left-stick deflection outside menus, and HostSettings::keyboard_walk to swap walking and running. Bind debug_menu to F1 as well as paddle 1. InputSetTextEntry/InputTakeText let a game's text field take typed characters while those keys stop pressing their pad bindings.
A host action for a game's in-mode debug menu, bound to F2 by default.
…ost action The controller gets a binding set of its own (HostSettings::pad_bindings, InputBindPadButtons): each pad button and host action takes SDL gamepad button names or lefttrigger/righttrigger. An action without one keeps its default, the pad's own layout (GamepadButtonPad) for the pad's buttons, so nothing changes until a binding is set. The host actions' paddle defaults move from the keyboard set to the controller's; Gamepad: names in the keyboard set still work. Adds the quick_loadout host action (Down, dpdown), a capture of any gamepad button or trigger (InputTakeBindPadButton), the bound names for a bindings menu (InputPadBindingNames) and the button a remapped action's prompt should show (InputPadGlyphAction). The on-screen gamepad presses host actions bound to the buttons it shows.
NVK's compiler (NAK) gives every uniform branch a sync and padding, and on the Switch's Maxwell GPU a fragment costs about its instruction count. Fog, TEXA, the alpha test's function, the blend factor, the transform and the opaque target are now values the shader selects between; only the palette paths and the filtered AEM gathers keep a branch (19 conditional branches to 7). Pictures are unchanged: Dark Chronicle's town, opening, dungeon map and a seeded dungeon floor render bit-identical on the PC build. On the Switch a fogged prog0 draw costs 11 to 25 percent less GPU time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An index texture drawn with a palette is expanded on the CPU into an RGBA texture the sampler filters, and expanded again when either is uploaded to, instead of the shader looking the palette up four times a pixel. An image written other than by an upload, a pair that changes every few frames and a pair already drawn this frame keep the lookup; expansions past a megatexel in a frame wait for the next. Off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cs + Cd * (1 - As), which the GS's (A - B) * C + D cannot express: a premultiplied image drawn over the target in one draw where the GS needs two (scale the target by the coverage, then add the image). One more blend state, after the GS's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tint's selects in draw.frag run for every pixel of every draw, tint or not. On the Switch's GPU they made Norune's frames about 3 ms slower (7.5 to 10.6 ms per submission, from 60 fps to 40-50). KZ_PORT_INTERPOLATION_TINT builds them in; it is on except for NINTENDO_SWITCH. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDL's Horizon joysticks report no gamepad type, so the handheld Joy-Con took the Xbox fallback. On the console every pad is a Nintendo one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 2D draw with depth_coverage planes reads the main depth buffer instead of testing it: its colour and alpha are scaled by the fraction of coverage_depths at or nearer than the depth at the pixel. That is the image eight blended full-screen draws at those depths make, each adding its share, which is how Chronicle's heat haze accumulates the frame beyond its planes; on the Switch those took 13 ms of GPU a tick in Muska Lacka, and one draw per image takes about 3. The draw renders without a depth attachment, the main or display depth buffer moved to DEPTH_STENCIL_READ_ONLY_OPTIMAL and sampled through a depth-only view at two new reserved slots (kFirstUserSlot is 5), and put back to rest when that rendering ends. draw.frag takes it as the kDepthCoverage specialisation constant, with the depth slot in palette_slot and the plane count in fog_color, which such a draw has no use for, and the depths in eight floats at the end of the push block (104 bytes), which only draw.frag declares. The pipeline is one more after all the others, only where the device samples its depth format (RendererFeatures::depth_coverage). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libnx starts every thread on the process's core, where the game thread runs, so the worker only interleaved with the game instead of running beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h platform support) Upstream's own Switch platform commit (726b1ee) has the file commits on Horizon, libnx's device paths and the pipeline cache through FilesWrite, and its master since has a render thread, answers depth queries without waiting for the GPU and keeps four submissions in flight. This branch now takes all of that from upstream and keeps only what it adds on top, replayed onto 726b1ee (the commit Chronicle pins): - draw.frag without per-draw branches (the shader-only version, for upstream's push constants) - RendererConfig::expand_palettes, on the render thread - DrawState::premultiplied - the interpolation tint as a build option, off on the Switch - Switch glyphs for every pad on the Switch - DrawState::depth_coverage, its depths in eight floats at the end of the push block - the render worker on core 1 on the Switch Left behind: the Android fork's commits (upstream has its own Android support), the game-side render thread's locking (upstream's render thread replaces it), RendererConfig::target_scale (render_scale means the render targets' scale again), and the OpenGL window and GetTextureInfo binding check that only Chronicle's GL renderer used, which is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict: the Switch's glyph family by default sits beside master's first gamepad type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one branch On the Switch's GPU (Maxwell, through NVK) draw.frag was bound by its arithmetic in busy scenes, and two things in it were new since Chronicle's Switch build ran at 60: - It decoded the draw's fog colour, FIX, the transform, TA0 and the alpha test's function from packed bits for every pixel. Integer conversions and variable shifts are slow there; the CPU now works them out once a draw (PushConstants::per_draw, and the alpha test's verdicts in alpha bits 11 to 13). - The fog that stays continuous across clipped edges (da0e9cf) passed eight values from every vertex and worked out every pixel's fog whether it was fogged or not. Its per-draw terms (limits, mode, constant F) now come in push constants, and the unlimited F, affine in 1/w, from a + b / w with gl_FragCoord.w, which is exact; the vertices pass three values. The block is behind one uniform branch, which costs less than the work for Dark Cloud's mostly unfogged pixels. The push block is 128 bytes, Vulkan's guaranteed size; a depth_coverage draw keeps its planes in per_draw. In Norune on the Switch a render submission takes about 11.8 ms of GPU, where it had taken 17 to 18.5 and Chronicle's earlier Switch build 11.3; every fog test draws the same pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adubbz
pushed a commit
that referenced
this pull request
Oct 24, 2026
SDL's Horizon joysticks report no gamepad type, so the handheld Joy-Con took the Xbox fallback. On the console every pad is a Nintendo one. Partially integrates #25. The rest of that pull request is already on master in its own form (Horizon file commits, device paths, the pipeline cache through FilesWrite, four frames in flight, the render thread, the TextureDesc logical size) or adds renderer interfaces that nothing here uses yet.
Member
|
Partially integrated into master in 25e8594: the Switch button glyphs for every pad on the Switch. The rest is already on master in its own form (Horizon file commits, sdmc: paths, pipeline cache through FilesWrite, four frames in flight, the render thread, TextureDesc logical size), and the other renderer interfaces need consumer code no project has yet. Thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I spent some time vibe coding a nintendo-switch port. This is a huge PR and may be a burden to maintain so feel free to let this PR rot but I figured I'd PR in case anyone is interested.
This is the engine half of Chronicle's Nintendo Switch port (homebrew, NVK on the Tegra X1). It adds a Horizon platform layer, renderer changes that the Switch's GPU and driver need, recording on one thread while another renders, and a few small interfaces so that a renderer other than the Vulkan one (Chronicle's
port/gl) can sit behind thegfxAPI.Without
__SWITCH__/NINTENDO_SWITCH, behaviour is unchanged apart from the shared changes listed below.Chronicle side: columna1/Chronicle-nintendo-switch
switch-port(separate PR).Platform (Horizon)
renamenever replaces a file. A replacing commit therefore moves the old file aside first, removing it once the new one is in place.sdmc:/…) accepted as absolute.FilesWrite, so it is committed like any other save file.Renderer
draw.fragwithout per-draw branches. NVK's compiler (NAK) puts a sync at each uniform branch, and on the Tegra X1 a fragment costs about its instruction count. Per-draw choices are now values the shader selects between.RendererConfig::expand_palettes. Palette textures are expanded to RGBA on the CPU and filtered by the sampler, instead of being looked up four times a pixel.DrawState::premultiplied, so an effect can composite in one draw where the GS needed two.kAsyncDepthQueries, on for the Switch). The renderer no longer waits for the whole frame on the GPU; the answer arrives a tick or two later.RendererConfig::target_scale. This keeps the old meaning ofrender_scale: render targets' pixels per logical texel. The Android merge turnedrender_scaleinto a multiplier on the main targets' size, so a caller that set it to 1.5 got 1920x1080 main targets.target_scaledefaults to 0, the old rounding of window height over logical height.render_scalestill multiplies it.KZ_PORT_INTERPOLATION_TINT(CMake option). It builds the smooth-motion debugging tint intodraw.frag. It is on by default except forNINTENDO_SWITCH, where the tint's selects, which run for every pixel of every draw, made a busy town's frames about 3 ms slower.DrawState::depth_coverage. A 2D draw that reads the main depth buffer instead of testing it: its colour and alpha are scaled by the fraction of up to 8 given depths at or nearer than the depth at each pixel. It draws in one pass what a stack of depth-tested additive draws would. Chronicle's heat haze uses it for the 8 blended full-screen passes it drew per image; in Muska Lacka on the Switch that took the haze from 13 ms of GPU a frame to about 3, and the town from about 40 fps to 60. The pass leaves the depth buffer unattached, samples it in a read-only layout, and puts it back afterwards. It's one extra pipeline, built only where the device can sample its depth format (RendererFeatures::depth_coverage). Two texture slots are now reserved for the depth buffers, sokFirstUserSlotis 5.Threads
One thread records while another renders (
db656fe; gfx README, "Threads"):LockUnlessRecording()for calls that are recorded rather than run;RecordingList()) and to replays on their own thread (ReplayingHere()).ThreadSanitizer is clean over the game's test scenes, and pictures match single-threaded rendering exactly.
For other renderers
WindowConfig::opengl: an OpenGL window, for a GL renderer.CanRecordBindingchecks throughGetTextureInfo, which every renderer has, instead ofLookupTexture, which returns the Vulkan renderer's own records.Also on this branch
611182dfrom the Android fork, which brings thesrc/cpplayout and the Android platform.dee2dbc, the TextureDesc logical size change (gfx: TextureDesc logical size, for a texture with more pixels than th… #7). Chronicle's texture overrides need it, and611182ddoes not contain it.efbd047(a shadow volume's two passes share their vertices) is reverted by6c7281e. The sharing clashed with the new interpolation code, which reads every immediate mesh's own vertices. They can be squashed away.The branch merges rather than rebases. I can squash or rebase it if you prefer.
Testing
masterfails on its own.switch-port. Norune holds 60 fps handheld at 1280x720.