Repository navigation
Convert SCANsat to load / render textures from the GPU - #463
Merged
Azrail09Code merged 146 commits intoOct 11, 2026
Merged
Conversation
…ng loaded in when using Parallax
…in to reduce RAM usage. Something still goes wrong with altimetry on first load - TerrainConfig isn't correct until map switch.
…d the baked lighting improvements back in now that I know they rely on Y axis normal instead of Z.
SCANUtil palette generation now copies all parameters from existing definitions before overwriting mapColor, preserving display name. All legend entry calls now made with palette references to allow recolouring. Remove duplicate code from getLegend() generation - better optimizing of block splitting required.
…s yet though in stock - will work on this.
Removed all instances of data.TerrainConfig and replaced with SCANUtil getTerrainConfig methods for CelestialBody, SCANdata and string. Change legend numbering to round to the nearest 10m instead of 100m (pesky little asteroids). Want to add dynamic rounding here so we get x significant figures instead.
The readable ScaledSpace color+normal copies (SCANcontroller.readableTexture) are the only SCANsat allocation that scales with body texture size. Under RSS (4K-8K bodies) each is ~180 MB; color+normal ~358 MB/body, created on every map open regardless of mode and never freed on window close. - Load the copy only when the map is in Visual mode (its sole consumer); gate via new SCANmap.refreshVisualMapTexture() from setBody + resetMap. - Free it on SCAN_UI_BigMap/ZoomMap.Close() (previously leaked for the session). - Downsample the copy to VisibleMapMaxResolution (default 2048x1024) and drop mipmaps; ~180 MB -> ~8 MB each. New persisted setting. - CacheScaledSpaceTexture always stores an owned copy (fixes latent Destroy of a body's real ScaledSpace texture) and skips 1x1 on-demand placeholders. - Destroy the map texture before nulling on resize (2 sites). - Free resident copies + clear the static heightMaps cache in OnDestroy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…ild) Renders the Visual map mode on the GPU, sampling the body's ORIGINAL ScaledSpace color+normal textures in a fragment shader instead of a readable CPU copy - the copy is the only SCANsat allocation that scales with body texture size and the RSS RAM hog. When the GPU path handles a map, the readable copy is never created. - New shader source Unity/SCANsat/Assets/Shaders/SCANVisualComposite.shader (4 inverse projections, VisualHiRes/LoRes coverage stencil, base colour, normal soft-light, grayscale, terminator) + scan_shaders_bundle.xml entry. The asset bundle rebuild (Unity 2019.4.18f1, SCANsat -> Build All Bundles) is left to do; until then SCAN_UI_Loader.VisualCompositeShader is null and the CPU path runs. - SCANmap.tryRenderVisualGPU/willRenderVisualGPU: Blit into a RenderTexture, coverage-flags stencil upload, uniform setup; DisplayTexture exposes the RT. refreshVisualMapTexture now skips the readable copy when the GPU path will render. - SCANcontroller.getScaledSpaceSource returns the body's live GPU textures (no copy). - UpdateMapTexture widened Texture2D->Texture (SCAN_BigMap/SCAN_ZoomMap); big/zoom/RPM display via DisplayTexture; SCANmapExporter reads back the RenderTexture for PNG. Scope/fallbacks: resource-overlay Visual maps still use the CPU path. To validate after the bundle rebuild: projection alignment (all 4), Blit Y-orientation (_FlipY), gas-giant/Parallax bodies (colour sampled raw), and sRGB/gamma parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…n fixes Adds the red-scanline "sweep" reveal the CPU map modes have and the instant GPU composite lost, and commits the previously-uncommitted fixes that made the GPU Visual path work in-game (they share the shader + rebuilt bundle, so they land together). Validated in-game on ksp_ro_expanded (RSS, 3.8 GB textures): bundle loads clean ([SCANsat] All SCANsat asset bundles loaded), sweep matches CPU modes. Sweep (new): - SCANVisualComposite.shader: _SweepY / _MapBackgroundColor / _RedlineColor reveal block after the terminator - rows ahead of the scanline draw as background, the frontier as a ~2px redline; safe Property defaults (SweepY=1) so a fresh Material is fully revealed on bundle/DLL skew. - SCANmap.cs: gpuSweepDone/sweepStep; reset in resetMap; isMapComplete returns gpuSweepDone in the GPU branch so the update pump keeps re-compositing the moving scanline; tryRenderVisualGPU advances one row per call + sets the reveal uniforms. One row per call (not MapGenerationSpeed) matches the CPU one-line-per-call cadence, since the pump calls getPartialMap MapGenerationSpeed times/frame - so the reveal runs at CPU speed at every setting. Reveal follows the CPU's texture-row order so the on-screen direction matches by construction. Previously-uncommitted GPU Visual fixes folded in (share the shader/bundle): - SCANVisualComposite.shader: col.a = 1.0 for scanned pixels (raw ScaledSpace alpha ~0; CPU path is opaque via a material Blit) - fixes the faint/ghost map. - SCAN_UI_BigMap.cs: updateMap = true in the map-type/projection setters so leaving Visual re-points the RawImage off the GPU RenderTexture. - EdgeDetectColors.shader (+ .meta): recovered the real Hidden/EdgeDetectColors source so a bundle rebuild doesn't drop edge-detect. - Rebuilt scan_shaders.scan (Unity 2019.4.18f1) + bundle manifest / project state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…ide) Sets the fork up to publish as its own CKAN mod (identifier SCANsat-GPU, provides + conflicts SCANsat = drop-in substitute): - GameData/SCANsat/SCANsat.version: point the KSP-AVC NAME/URL/DOWNLOAD template at the fork (miskoltrans/SCANsat-GPU releases). KSPBuildTools still stamps VERSION + KSP_VERSION at build time. - SCANsat.version.props: 21.1 -> 21.1.1 to give the fork a distinct, bump-able version off the SCANsat 21.1 base. - SCANsat-GPU.netkan: CKAN metadata (BSD-3-clause, github $kref, ksp-avc $vref, depends ModuleManager, provides/conflicts SCANsat). Submit to KSP-CKAN/NetKAN. - build-release.ps1: build the DLL, assemble GameData/SCANsat from git-tracked content (excludes the gitignored DLL + user Settings.cfg) with the built DLLs + stamped .version overlaid, zip to dist/, and emit the AVC .version to attach to the release. - PUBLISHING.md: the per-release checklist + first-time NetKAN PR steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
… KSP bounds Shakedown of build-release.ps1 surfaced three issues, all fixed: - Version stamped as 1.0.0.0: SCANsat.csproj doesn't import SCANsat.version.props, so $(Version) never reached KSPBuildTools. The script now reads the version from SCANsat.version.props and passes it as -p:Version (-> 21.1.1.0). - Zip had backslash path separators (Windows PowerShell Compress-Archive), which violate the ZIP spec and can break CKAN's installer. Build the archive by hand via ZipArchive with forward-slash entry names instead. - SCANsat.version had a malformed "KSP_VERSION_MAX": "'1.12'" (KSPBuildTools derived it with embedded quotes). Set KSP_Version / KSP_Version_Max explicitly (1.12.5) in the csproj alongside the existing Min (1.12.3). - Output to Releases/ (already gitignored) instead of dist/. Verified: zip = 184 forward-slash entries incl. Plugins/SCANsat.dll (GPU build) + Resources/scan_shaders.scan (26371) + LICENSE.txt, no PluginData; AVC version clean (21.1.1.0, KSP 1.12.3-1.12.5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…own fixes
Continuation of the RSS RAM work (Stage 1 already fixed the readable ScaledSpace
copies). Fixes the remaining open leaks found in an audit; all C#, no shader change.
- 4a (top remaining RSS leak): SCANmap.setBody force-loads the body's Kopernicus
OnDemand ScaledSpace but nothing unloaded it on window Close or scene change (the
SCANmap is then orphaned, so setBody never runs again). Added unloadOnDemandScaledSpace
to SCAN_UI_BigMap/SCAN_UI_ZoomMap Close() and OnDestroy() (guards already avoid
evicting a body another window still shows).
- 1a: SCANmap (plain class, no finalizer) leaked its Texture2D/RenderTexture/Material
per scene. Added SCANmap.Destroy() (map/visualRenderTex/compositeMaterial/coverageFlags
+ exporter GO); called from both windows' OnDestroy and SCANsatRPM.
- 1b: SCANsatRPM (InternalModule) had no teardown -> leaked iconMaterial + SCANmap per
IVA load. Added OnDestroy.
- 4b: SCAN_UI_ZoomMap.OnDestroy unloaded PQS with mapSource.BigMap (wrong) -> could leave
zoomMapBodyPQS stale and skip a real unload. Fixed to ZoomMap.
- 4c: SCANcontroller.OnDestroy nulled _settings twice instead of _zoomMap.
- Tail: destroy-before-realloc in SCANmapLegend, SCANPaletteType, SCANEdgeDetect,
SCANuiUtil (ref-textures), SCAN_UI_Overlay (only leaked on runtime resolution change).
- Instrumentation: [SCANmem] lines via SCANdebugLog ([Conditional("DEBUG")], stripped
from Release) around load/unloadOnDemandScaledSpace + Texture.currentTextureMemory on
window teardown, to verify LOAD/UNLOAD balance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
Groundwork for rendering all map modes on the GPU (was Visual-only). Authored the whole shader in one pass so the bundle only needs ONE manual Unity 2019.4.18f1 rebuild; the C# per-mode wiring follows in later commits. - SCANVisualComposite.shader: branch frag on _MapMode (0 Altimetry, 1 Slope, 2 Biome, 3 Visual). Altimetry/Slope sample _ElevationTex + a baked _PaletteLUT / slope-colour uniforms; Biome samples _BiomeIndexTex + low/high gradient + border detection; Visual unchanged. Resource overlay (_ResourceTex + uniforms) applied over the base colour, then terminator, then the existing sweep. Data-texture geo-UV orientation is the main thing to verify in-game (flagged in comments). - Coverage stencil is now the raw 16-bit SCANdata.coverage packed R=low/G=high byte; shader decodes it and tests any SCANtype bit (covHas). This is COUPLED to the shader, so it ships together - the old 4-bit RGBA pack would misread under the new decode. Visual mode still works after this (default _MapMode=3, coverage now 16-bit); the other modes stay on the CPU until their C# is wired. Bundle rebuild required for any of this to take effect in-game. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
The Unity 2019.4.18f1 asset-bundle rebuild for the multi-mode shader (04106f8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
Generalizes the GPU path from Visual-only to all window map modes. UNVERIFIED in-game yet (compiles clean); the data-texture geographic orientations are the main thing to check (flagged in the shader). - willRenderVisualGPU -> willRenderGPU(mapType): Visual needs the scaled-space source; Altimetry/Slope need pqs; Biome needs biomeMap. Resource is a layer, not a gate. - tryRenderVisualGPU -> tryRenderGPU: sets _MapMode + setModeUniforms(), which uploads the mode's CPU cache as an R-float data texture (big_heightmap -> _ElevationTex, biome_indexmap -> _BiomeIndexTex, resourceCache -> _ResourceTex), bakes the altimetry _PaletteLUT from heightToColor (legend parity), and sets slope/biome/resource uniforms. - getPartialMap: Visual still short-circuits at the top; non-Visual modes run the prep loop (the unavoidable PQS/biome sampling that fills the caches), then a new branch skips the CPU colourize loop, uploads the data, Blits via tryRenderGPU, and advances mapstep/sweep. The whole CPU switch remains the fallback when the shader/pqs/biomeMap isn't available. - New per-map data textures + biome_indexmap cache, freed in Destroy(); reused upload buffer. Instant-recolor optimization (skip PQS re-sample on palette-only change) still deferred - this first cut re-samples on resetMap like the CPU path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
ZoomMap/RPM use setSize + cache=false, so big_heightmap/biome_indexmap are never allocated or filled; the non-Visual GPU branch dereferenced them and threw a per-frame NullReferenceException from SCAN_UI_ZoomMap.Update. Require cache in willRenderGPU for Altimetry/Slope/Biome so only BigMap (setWidth, cache=true) takes the GPU path; Zoom/RPM keep the CPU path for those modes. Visual GPU unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
- Non-Visual GPU modes rendered blank: tryRenderGPU runs in the getPartialMap branch (after the prep loop), which is after the BigMap pump consumes updateMap on the mode-switch frame, so gpuRendered was still false then and the RawImage stayed on the never-painted CPU map texture. Added primeGpuRenderTex() (create+clear the RT, set gpuRendered) called up-front for non-Visual GPU modes so DisplayTexture returns the RT immediately; the data render still happens in the branch below. - InCurrentBodyFamily NRE: the leak fix now calls unloadOnDemandScaledSpace from OnDestroy (scene teardown), where FlightGlobals.currentMainBody / PlanetariumCamera are null. Guarded both the FLIGHT and TRACKSTATION cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
- Incremental upload: stop rebuilding+uploading the whole elevation/biome texture every frame. ensureDataTex creates the R-float texture once; uploadDataRow uploads only the row the prep loop just filled per getPartialMap call. resourceCache uploads once (resourceTexReady flag). The old uploadFloatTexture is left as dead code. - Instant re-colour: track gpuDataComplete + a config hash (body/mode/projection/size/ offsets) when the sweep finishes. A colourisation-only change (palette/clamp/terminator) leaves the hash unchanged, so resetMap jumps to the last row and the next getPartialMap re-Blits once with the rebuilt LUT over the cached data textures - no PQS re-sample, no full re-sweep. Config change (mode/body/pan/projection) still does a full render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…rlay + LoRes altimetry grey - Stock biome colours: build a 1-D _BiomeLUT of the body's BiomeMap.Attributes[].mapColor, indexed by the biome fraction; _StockBiomes toggle picks stock vs the low/high gradient (matches CPU BigMapStockBiomes && colorMap). - Biome elevation underlay: biome now blends its colour with grey elevation by BiomeTransparency (was fading to clear); uploads _ElevationTex in biome mode too. - LoRes altimetry grey: bake a second grey LUT (heightToColor colour=false); the shader uses the colour ramp for HiRes-covered pixels and the grey ramp for LoRes-only (matches CPU nowColor). (Slope left as-is per request; -0.001 sentinel is negligible.) Shader change -> requires a Unity 2019.4.18f1 'Build All Bundles' rebuild. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
The instant-recolour path jumped straight to a complete re-Blit, dropping the sweep animation. Instead replay a cosmetic sweep: on a colour-only change, restart mapstep at 0 with gpuRecolorSweep set; getPartialMap then skips the whole prep/CPU loop and just re-Blits the already-uploaded data textures each frame with the rebuilt LUT while _SweepY advances. Keeps the sweep charm with no PQS re-sample and no CPU re-processing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…ome + grey LUTs) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…y consistency - Biome wrong in KavrayskiyVII/Polar (all non-Rectangular): biome_indexmap is filled per rendered pixel at the unprojected location = map-pixel space, unlike big_heightmap which is a geographic equirect grid. Sample _BiomeIndexTex (+ border neighbours) at the fragment uv, not geoUV. Elevation underlay stays geoUV (geographic). - Resource overlay vanished on refresh: resetMap -> resetResourceMap clears resourceCache, which only the prep loop rebuilds, but the fake-sweep fast path skips the prep. Skip the fast path when the resource overlay is active so a full render rebuilds the cache. Shader change -> needs a Unity 2019.4.18f1 bundle rebuild for the biome fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
…fix) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbeLovSqSgCMuQHa745Kjo
resetResourceMap zeroed resourceCache on every reset, so every refresh - and every cutoff nudge, colour change, window re-open and re-Blit after new scanning - re-sampled the whole grid. That is the most expensive thing left in a pass and the only part with no way to spend itself under the per-frame build budget: buildResourceCache runs to completion inside the first composite of the pass. The grid costs (2H/N)x(H/N) calls to stock GetAbundance, each of which computes a biome name first - CBAttributeMapSO.GetAtt plus getBiomeIndex's own linear scan over the attributes, twice O(biomes) per sample. At the shipped defaults (height 256, interpolation 8) that is 2,048 samples and nobody notices. At the ceiling the settings offer (height 1024, interpolation 2) it is 524,288 - a second of one frame, which is the hitch that prompted this. Nothing about that field depends on the pass. Stock planetary abundance is static: the shader masks the layer with the live coverage texture (covHas bits 7/8), so ground scanned since the last build reveals from the grid that is already there, and the cutoff, the range and the layer's colours are uniforms the composite sets every frame. So the grid is now kept and stamped with resourceConfigHash - body, resource, biome lock, projection, grid size, interpolation, window offsets, edge mode, source, resource seed - and re-sampled only when that key moves. buildResourceCache clears the array itself on the rebuild path, where resetResourceMap used to. The one part of the field that play does change is the biome unlock: with BiomeLock on (the default) stock returns the biome's average until a surface scan unlocks it, which used to refresh for free because every reset re-sampled. IsBiomeUnlocked is folded into the key, a few dozen lookups per reset, and only when the lock is on. With the grid persistent, the reason resetMap's instant-recolour shortcut refused resource maps is gone - it was there because the reset had just wiped the cache - so a resource map now takes it like any other and a refresh stops re-sampling every PQS height as well. Alongside it, the upload writes through GetRawTextureData<float> instead of SetPixels, the pattern ensureDataTex already uses: RFloat is one float per pixel in the same bottom-up row order, so the Color[] mirror was 16 bytes a pixel of garbage - 33 MB on the large object heap per upload at 2048x1024, thrown away again every reset. It is skipped entirely when the grid it already holds is the grid that was asked for. Not verified in game. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019X7B8em49g14VMtSYZbZHn
…ed of its own The map generation setting stopped being a speed when the GPU port replaced the row-per-frame cadence with a per-frame CPU budget: 1/2/3 on the slider already meant 2/4/8 ms, shared each frame by every map that is building plus the height map pump, with every builder guaranteed at least one step however long it takes. The name hid all of that. It is MapGenerationBudgetMs now, the stops are 2/4/8/16 ms and the label reads the value back. Settings files written before the rename carry over in carryOverLegacyValues, 1/2/3 to 2/4/8, so no install silently drops to the default on first load. The scanline was the other half of the confusion. It is a fixed one second wall-clock reveal, unrelated to the budget, and nothing could change it. ScanlineSpeed scales it - 0.5x, 1x, 2x, Instant - and Instant returns a fully revealed pass at once. The duration is taken once per pass, when the sweep clock starts, rather than read per frame: moving the slider mid-sweep would otherwise rescale the elapsed fraction and run the line back up over rows it had already revealed. Faster only goes as far as the build. The data modes clamp the reveal to mapstep / mapheight, so on a cold cache the line follows the real sampling pace whatever the scanline setting says, and the budget is the slider that moves it; Visual and the recolour re-sweep have their end state on the first Blit and take the setting literally. The help tips say this outright, because otherwise 2x on a first-time map reads as broken. Both sliders are four-stop indices into a table in SCAN_SettingsGeneral, while the settings store the value and not the index, so a hand-edited settings file keeps what it says and the slider shows the nearest stop. SCAN_General.prefab grew the second row through SCANsatSettingsRows, a new editor script, rather than by hand: the prefab is binary, so a scripted edit is the only reviewable record of what happened to it, and it fails safe when re-run. Re-saving also moved the asset from the 2017.1 serialisation it still carried to 2019.4, which is most of its size change. scan_prefabs.scan is rebuilt to match, and both rows were verified by loading that bundle back and dumping the page out of it. Nothing here is verified in game yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6aShLp1YJuZzcJ5xARScR
…aviour Everything a SCANmap did differently because of who owned it was decided in five places: the constructor (cache layout), usesHeightGrid, two mSource checks inside the composite (grid dots, unscanned colour), a switch on the source in setModeUniforms (biome toggles), and four properties the owning windows set after construction (sweep, biome underlay, planet UV, auto range), with the defaults they never set hidden in the field block. SCANmapProfile.For(mapSource) now decides all of it in one table, and SCANmap reads the profile wherever it used to ask which source it was. The owners keep only what really changes per pass: the overlay's per-selection base, alpha and grey blend, and the small map's sensor mask. No behaviour change: every value is the one the old code arrived at for that source. The height-grid rule is evaluated once per pass in resetMap (heightGridPass) rather than per pixel, so a pass cannot change elevation source part-way through. The unused parameterless constructor is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
The small map blends its biome colours toward the elevation underlay at zero transparency, so the underlay was invisible - but the underlay flag stayed on, and every Biome pass still sampled elevation for each covered pixel. With the body's height grid built that was an array read; opened straight into Biome mode, before the main map has pumped the grid, it was one PQS query per covered pixel, 64,800 of them per pass on a scanned body, repeated every second for a live display. Off in the profile. Pixels do not change: _HasElevation goes to 0 and the shader keeps the underlay grey, which a zero blend never shows anyway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
…at zero The row loop still carried the CPU renderer's shape: step -2 built the resource cache (a no-op since the composite builds it lazily), step -1 was a look-ahead that filled row 0, and step k filled row k+1 while staging row k, with row 0 double-staged at step 0. The look-ahead existed because the CPU slope compared a pixel with the row above it; the shader reads all four neighbouring texels itself and skips an unsampled one, and the two rows at the sweep frontier are under the redline anyway. prepRow(row) now samples exactly the row it is given, the build stages that same row, and mapstep counts rows built from 0. The auto-range pre-pass keys off its own cursor instead of "the counter is still negative". The RPM reserved-row check is one test on the row itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
getPartialMap ran four different shapes for what is one job: Visual composited straight away; a data mode first primed the render target, then built and composited inside the build; a recolour re-Blitted and closed itself; and a map nothing renders marked its counter complete. The target was created in two places, the composite reached from three, and "is the pass done" was read from two variables depending on whether anything had rendered. Now resetMap decides what there is to build (nothing for Visual, a recolour, a body with no data or the resource-only overlay; every row otherwise) and every pump does the same thing: ensureRenderTex, buildSlice while rows are left, composite with the reveal at the smaller of the timed sweep and the rows built. passComplete is the one completion flag, set by the fully revealed composite or by the no-renderer branch. The prime step is gone because the target is created before anything reads it; the recolour flag only chooses the log line. Two small behaviour differences, both toward the safer side: a reset no longer drops DisplayTexture to null for a frame (the target still holds the previous pass, which is what the discard-ahead-of-sweep shows anyway), and the end of a recolour no longer re-asserts gpuDataComplete, which was either redundant or wrong if a window cache had been cleared meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
…he profile Rebase fix-up: cd82957 (the abundance grid outliving the reset) landed in parallel with the per-source profile and added one more read of the autoRange field, which the profile commit had already replaced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
Whitespace only. Six lines carried one and two extra tabs from the old tryRenderGPU, where a brace had been flattened without reindenting. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
The class had grown four ways to answer "is this cached work still what I want": the data textures carried gpuDataComplete beside two hashes, the resource grid and its texture each carried a "ready" bool beside a hash, the palette LUT carried a hash built by XOR, and the biome LUT compared a count and a body reference. Twelve fields, five idioms, and nothing to copy when the next one arrives. Each is one CacheKey now - Valid(key), Stamp(key), Invalidate() - and every key is folded with the same mix()/seal() pair. Six fields in total. The doc comment says which caches are deliberately not CacheKey and why: the biome row flags and the elevation grid are filled a row at a time and are useful half built, so an all-or-nothing key would be the wrong shape for them. Two of these are not renames. The palette key was XOR-combined, which is order free and puts colorMap and useCustomRange in the same low bits as the palette's own hash, where they can cancel and hand back a LUT baked for the other colour mode; it is folded now. And gpuConfigHash could return 0, which was harmless while gpuDataComplete guarded the compare separately but is not once 0 means "nothing valid here", so it ends with seal() like the rest. resourceCacheReady and resourceTexReady are gone. Both builders self-guard by key, so what those bools actually bought was skipping resourceConfigHash on every frame of the sweep - not nothing, since under BiomeLock it walks the body's biomes - and that is a named per-pass memo now, resourceConfigKey, taken once and handed to both. One line went with them: resetMap's recolour branch cleared resourceTexReady "because resource colours may have changed too", but it had already been cleared thirty lines earlier, and the colours are shader uniforms that never reach the texture. uploadResourceTexture stamps the grid's key rather than the pass's, so a grid that threw mid-sample leaves the texture exactly as invalid as the grid behind it - what resourceTexHash = resourceCacheHash did before. The biome LUT is keyed on flightGlobalsIndex rather than the CelestialBody, so these static maps no longer hold the last browsed body's object alive for the session. Nulling it by hand is exactly what invalidateGpuDataCache was for. That method's comment claimed to drop every "the GPU has valid data" claim, which stopped being true when cd82957 gave the abundance grid a key that survives a reset on purpose. It says so now, and states the rule for the next cache: it belongs in there only if its key cannot tell the difference. Not verified in game. The failure mode for a wrong key is a map that does not refresh when it should, or refreshes when it needn't, and the compiler sees neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ccLXxnMAXH9Q25XSuZrjZ
willRenderGPU said it mirrored tryRenderVisualGPU's eligibility, a method that has never existed under that name since the composite was split out of it. composite() and clearWindowCaches still explained their addressing in terms of cache=true and cache=false, the constructor bool the per-source profile replaced. Comments only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ccLXxnMAXH9Q25XSuZrjZ
RSS_TerrainRanges.cfg holds the answer the survey was written to produce, so the tool itself is now dead weight in every install: 343 lines compiled into the plugin, a shipped example cfg, and a per-frame CheckForRequest in Update for a run nobody will ask for again. The scene-change sweep unloads every loaded body unconditionally again, now that nothing holds one across scenes, and the two comments that pointed at the tool describe the measurement instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019X7B8em49g14VMtSYZbZHn
…ger part of one setSize computed resourceMapScale as resourceMapWidth / 360 with both operands int, so a 512-wide RPM page got a scale of 1 instead of 1.42. The MapScale setter then recomputed it as (mapwidth / resourceMapWidth) * mapscale, another integer ratio, which happens to be 1 for every window map because the two widths are equal there, so the result came out right after all and nothing showed in game. Both sites now divide in floating point, so a window map whose widths ever differ, or that renders before MapScale is set, samples the resource grid at the map's own pixels per degree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
Opening the Unity project in an IDE writes SCANsat.sln and per-assembly .csproj files next to Assets; none of them is tracked and they kept showing up as untracked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
Elevation, biome index and resource each had their own rule for where a pixel reads them: elevation geographic on the big map and the overlay, else pixel space; biome always pixel space, filled per rendered pixel; resource pixel space on a window map and on the big map under Orthographic, because the cache generator unprojected there. Now there is one flag, _PixelSpace, set from the profile's GeographicCache: the big map and the planet overlay keep all three caches over the globe and read them at the pixel's unprojected lon/lat, so a projection change or a re-centring no longer re-samples the biomes or the resource grid; the zoom map, RPM and the small map keep their window in pixel space. The shader's dataUVAt is the rule, and a biome border is found by putting the neighbouring SCREEN pixel through it, so borders stay one screen pixel wide under every projection - the reason this was parked. The biome index texture is point-sampled, since a geographic cache is read between texel centres under a non-rectangular projection and a bilinear blend of two biome labels is a third biome. Under Rectangular a map texel is a pixel, and nothing changes on screen. Needs the rebuilt scan_shaders bundles: the two old flags are gone from the shader. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
The polar rows read as cliffs. The Slope block took the largest one-sided elevation difference to its four neighbouring texels and divided the east-west one by cos(lat), which is the right run for a meridian grid and the wrong thing to do with it: at 89 degrees on a 1440-wide map of Earth the neighbouring texels are 500 m apart, and any real relief over 500 m is steep at the map's scale, so the correction reported the truth about a distance the rest of the map never measures. The CPU renderer had no latitude term at all. Now the east and west samples are taken 1/cos(lat) texels out, so every pixel measures its gradient over one map texel of ground in every direction, and nothing is divided by cos(lat). Central differences and the gradient magnitude, so a slope reads the same whichever way it faces (the four-way max read a diagonal 29 percent low, and the CPU's three-below max hid a slope facing north-east altogether). Same unit and clamp as ever. An unsampled neighbour still drops out to a one-sided difference. The widened step wraps at the dateline on a geographic texture and is capped at half the texture, so the last texel beside a pole reads its north-south slope only rather than a number from a step that went round the world. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
…tch mode) For the one-rule data-texture addressing (_PixelSpace, dataUVAt) and the constant-ground-step Slope. Zero shader errors in the build log; the linux bundle is 51,922 bytes, so the Vulkan program is in it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
Remove unused coveredByAll function, draw dotted grid consistently over preview maps and remove visual map dimming. Null PQSController on perfectly flat bodies (gas giants in some packs) for consistent behaviour. Add SCANsat configs for Sol planet pack.
…tch mode) For 6f15035: the dotted 30-degree graticule on every small-map mode, static noise on a Visual map with no source texture, and the _SensorMask dimming plus coveredByAll removed. That commit shipped a 15 KB Windows bundle and left the Linux and macOS bundles at the previous shader, so all three are rebuilt from one source. Zero shader errors in the build log (only the pre-existing d3d11 unproject warnings); the linux bundle is 50,355 bytes, so the Vulkan program is in it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBwQchovNBCyuSB3YGUqP5
…pattern Jool's Visual map was pure red. KSP's Terrain/Gas Giant material has no colour map at all: the body's colour is a cloud PATTERN looked up in a 1-D gradient. SCANsat bound _DetailCloudPatternTexture as the colour map, and that texture (stock "DetailPattern", 8192x4096) is BC4 - one channel, sampled as (r, 0, 0, 1) - so the map drew the cloud mask in red. It is also the pattern the game TILES (_DetailTiling), so it was never a map of the globe in the first place. Bind the un-tiled _CloudPatternTexture plus the body's gradient (_CloudColorMap, Jool's 2x4096 ramp of greens) and let the composite do the lookup: gasColor() in SCANVisualComposite, behind _GasGiant, turns the pattern's red channel into the ramp colour before the normal map and the greyscale toggle see it. The base pattern alone would not have helped - it keeps one cloud layer in red and another in green and nothing in blue. Checked against the stock assets offline: decoding DetailPattern gives the reported red map exactly, ColorRamp[CloudPattern.r] gives Jool. Chosen by what the material has rather than by the shader's name, so a stripped or re-shadered gas giant is judged on its own textures, and KSP's low-quality gas giant material - which does have a real _MainTex - falls through to the ordinary checks instead of asking for pattern textures it does not own. The dead useMaterialForColor flag the GPU port had left for "a later gas-giant/Parallax pass" goes with it: this is that pass, and the answer turned out to be a ramp lookup, not a material blit. Needs the three scan_shaders bundles rebuilt (Unity 2019.4.18f1) before any of this shows in game; until then the old bundle ignores _GasGiant and draws the raw pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxchXjb45KoMfKz4Krn3yM
…tch mode) Carries gasColor() from a10914c, so a gas giant's Visual map stops drawing its cloud pattern raw. No shader errors in the build log; the linux bundle grew 50355 -> 50811 bytes, so the Vulkan program is still in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CxchXjb45KoMfKz4Krn3yM
…s UI scales. Used Qwen3.8 to work out why the buttons didn't scale in the first place, then manually worked out how to scale the anchor position to keep the buttons in a consistent position.
This was referenced Oct 11, 2026
Closed
Closed
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.
This moves visual data rendering from the CPU to the GPU through memory mapped textures. This allows for large scale textures and custom planets to not use additional RAM when running map mode.