Level visibility
What decides which track faces, sky and kart wheels are drawn far away, and what the native version changes. Read this before touching level culling in game/MAIN/MainFrame.c, game/MAIN/MainFrame_RenderFrame.c, the level renderers (game/226 to game/229), game/DrawSky.c or game/DrawTires.c.
The stages
A level face is drawn only if it passes every stage, in this order:
- Visibility lists (PVS). Each quadblock can carry precomputed lists: the BSP leaves and quadblocks visible from it, nearby level instances, and the water and vertex-colour vertices to animate. The camera picks the lists of the quadblock under it (
CAM.c, afterCAM_FindClosestQuadblock), andMainFrame_VisMemFullFramecopies them intogGT->visMem1. Native ignores them for level geometry:MainFrame_VisMemFullFramesets every leaf, quadblock, water vertex and vertex-colour vertex visible each frame (why). Instances keep their lists, because warp pads (AH_WarpPad.c) and cutscenes (CS_Instance.c) read them as gameplay state. - Frustum. The BSP walk drops leaves outside the view. Widescreen widens it (
Widescreen_ScaleFrustumX). - Distance. Far leaves and quadblocks drop out, and textures and models change LOD by distance.
increase_draw_distancemoves all these limits out by the same factor, 4 (include/platform/native_draw_distance.h): the frustum's far clip (PushBuffer.c), the depth where a BSP leaf switches to the cheaper full-dynamic emitter (1P and split screen), a model's LOD distance, and the depths that pick a level texture's LOD and a quadblock's subdivision. The last two go together: a quadblock's full-resolution texture only loads in the deepest subdivision. One factor for all, because the nearest limit decides what the player sees disappear. - Near clip. Faces crossing the near plane are clipped through clip records, but only when a corner is inside a box in front of the camera (
DrawLevelOvr1P_SourceInsideClipRecordWindow); otherwise the face is dropped whole. The box widens with the aspect ratio.
Why the lists are off
The lists were baked for the retail view: 4:3, the retail draw distance and the camera positions the developers sampled. With widescreen, the far camera (L2) or increase_draw_distance, the camera sees past them, so stretches of far road and scenery vanish until the camera reaches a quadblock with other lists. A split-screen camera can even lose the floor under its kart.
They only saved PS1 time. Faces hidden behind nearer ones are still drawn, but the ordering table puts them first, so nearer faces paint over them. The cost is primitive-arena space and frame time:
- Arena peak per frame at 16:9 with the far camera: 120 to 292 KB per track without the lists (79 to 130 KB with). With
increase_draw_distancetoo: up to 586 KB in 1P, 820 KB in two-player split screen (N. Gin Labs) and 365 KB in four-player. The arena is 2 MB (CTR_NATIVE_PRIM_ARENA_BYTES,platform/native_memory.c). A dev run logs[CTR Dev] primitive arena peak. - With
increase_draw_distanceon N. Gin Labs, the heaviest track: about 1.5 ms a frame more than the retail limits in 1P and 2.7 ms in 2P. The option is off by default, Steam Deck included.
The sky
The sky (game/DrawSky.c) is a dome of large gouraud triangles around the camera, in eight 45-degree slices. Retail draws the four slices facing the camera and drops a face whole when the GTE flags a corner too close to the camera plane, and the GPU drops any triangle 512 or more pixels tall (NativeGpu_HardwareSkipsTriangle). From the far camera and in widescreen, a face still covering a screen corner hits one of those, and the clear colour shows through.
Native draws all eight slices and sends every face through DrawSky_ClipFace: clipped in view space against the near plane and the screen edges (8 pixels outside), projected through the same view as the level (the precise one under PGXP), and drawn as a fan with colours interpolated at the cuts, so pieces stay within the GPU's limits. Every face goes this way, not only those retail would drop, so neighbours share bit-identical corners (mixing GTE-projected and clipped faces leaves hairline seams).
Distant wheels
A kart's tires are four camera-facing sprites (game/DrawTires.c), drawn only at the kart's top model LOD. Their centres are taken relative to the camera at 4 times world scale and go through the GTE's 16-bit vectors, which hold about 8,000 world units; with increase_draw_distance a top-LOD kart can be farther. Such a wheel keeps its full-width centre (the GTE's 32-bit MAC1 to MAC3, not the saturated IR1 to IR3), takes its facing from it, and has its corners projected by NativeGte_ProjectWide (platform/native_gte_core.c), an RTPS with a 32-bit input that advances the same registers and PGXP state. Wheels that fit keep the retail GTE path bit for bit. Packing an out-of-range corner into 16 bits would wrap it behind the camera and draw a long dark sliver across the screen.
Checking it
CTR_DEV_RETAIL_CULL=1keeps the retail lists and sky, for A/B captures (DEV-TOOLS.md). Every differing pixel should be far geometry the lists dropped or sky that was missing, never a near face overdrawn. The clear colour must not show in any frame with the sky; Hot Air Skyway atCTR_DEV_WARP=10799,325,-2381,2271with the far camera shows it with the retail sky and not without.- Distant wheels: where wheels fit, no pixel changes. On Dingo Canyon (frame 3390 of a
CTR_DEV_RACE=0autopilot run) the far karts have wheels;CTR_DEV_DUMP_FRAMElists them as@tiresquads next to the karts' polygons. - A spot a player reports: they press
F7there, andwhere.txtin the capture gives theCTR_DEV_WARPline. Warp there withCTR_DEV_PAD=2400:2for the far camera and capture both ways.