Skip to content

Convert SCANsat to load / render textures from the GPU - #463

Merged
Azrail09Code merged 146 commits into
KSPModStewards:devfrom
miskoltrans:rss-ram-gpu-visual
Oct 11, 2026
Merged

Azrail09Code merged 146 commits into
KSPModStewards:devfrom
miskoltrans:rss-ram-gpu-visual

Conversation

@Azrail09Code

Copy link
Copy Markdown
Collaborator

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.

Azrail09Code and others added 30 commits May 27, 2026 18:42
…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.
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
miskoltrans and others added 21 commits September 16, 2026 14:09
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants