From f83a676b3c4b305604ccb5d192ef70463893e790 Mon Sep 17 00:00:00 2001
From: gotmachine <24925209+gotmachine@users.noreply.github.com>
Date: Fri, 2 Jun 2023 15:01:16 +0200
Subject: [PATCH 1/4] Experimental stuff
---
KSPCommunityFixes/KSPCommunityFixes.csproj | 1 +
.../Performance/FastBiomeQuery.cs | 523 ++++++++++++++++++
2 files changed, 524 insertions(+)
create mode 100644 KSPCommunityFixes/Performance/FastBiomeQuery.cs
diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj
index 960b8fa4..ef1360c5 100644
--- a/KSPCommunityFixes/KSPCommunityFixes.csproj
+++ b/KSPCommunityFixes/KSPCommunityFixes.csproj
@@ -125,6 +125,7 @@
+
diff --git a/KSPCommunityFixes/Performance/FastBiomeQuery.cs b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
new file mode 100644
index 00000000..f3d497d2
--- /dev/null
+++ b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
@@ -0,0 +1,523 @@
+using Smooth.Algebraics;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.WindowsRuntime;
+using System.Text;
+using System.Threading.Tasks;
+using Steamworks;
+using UnityEngine;
+using VehiclePhysics;
+using Debug = UnityEngine.Debug;
+using Random = UnityEngine.Random;
+
+namespace KSPCommunityFixes.Performance
+{
+ [KSPAddon(KSPAddon.Startup.MainMenu, false)]
+ internal class BiomeMapOptimizer : MonoBehaviour
+ {
+ const float Byte2Float = 0.003921569f;
+
+ public static byte[][] biomeMaps;
+
+ void Start()
+ {
+ biomeMaps = new byte[FlightGlobals.Bodies.Count][];
+
+ foreach (CelestialBody body in FlightGlobals.Bodies)
+ {
+ if (body.BiomeMap != null)
+ {
+ byte[] newMap = Optimize(body.BiomeMap);
+ biomeMaps[body.flightGlobalsIndex] = newMap;
+ }
+ }
+ }
+
+ void Update()
+ {
+
+ }
+
+ private byte[] Optimize(CBAttributeMapSO biomeMap)
+ {
+ int biomeCount = biomeMap.Attributes.Length;
+ Color[] biomeColors = new Color[biomeCount];
+ for (int i = 0; i < biomeCount; i++)
+ {
+ biomeColors[i] = biomeMap.Attributes[i].mapColor;
+ }
+
+ byte[] oldMap = biomeMap._data;
+ int bpp = biomeMap._bpp;
+
+ byte[] newMap = new byte[biomeMap._height * biomeMap._width];
+
+ for (int i = 0; i < newMap.Length; i++)
+ {
+ int colorIndex = i * bpp;
+ float r = oldMap[colorIndex] * Byte2Float;
+ float g = oldMap[colorIndex + 1] * Byte2Float;
+ float b = oldMap[colorIndex + 2] * Byte2Float;
+
+ int biomeIndex = TryGetExactBiomeColorIndex(r, g, b, biomeColors);
+ if (biomeIndex > -1)
+ {
+ newMap[i] = (byte)biomeIndex;
+ continue;
+ }
+
+ biomeIndex = TryGetNearBiomeColorIndex(r, g, b, biomeMap.neighborColorThresh, biomeColors);
+ if (biomeIndex > -1)
+ {
+ newMap[i] = (byte)biomeIndex;
+ continue;
+ }
+
+ biomeIndex = GetBestNeighborBiomeColorIndex(r, g, b, i, biomeMap, biomeColors);
+ newMap[i] = (byte)biomeIndex;
+ }
+
+ return newMap;
+ }
+
+ private int TryGetExactBiomeColorIndex(float r, float g, float b, Color[] biomeColors)
+ {
+ for (int i = 0; i < biomeColors.Length; i++)
+ {
+ Color biomeColor = biomeColors[i];
+ if (biomeColor.r == r && biomeColor.g == g && biomeColor.b == b)
+ return i;
+ }
+
+ return -1;
+ }
+
+ private static int TryGetNearBiomeColorIndex(float r, float g, float b, float threshold, Color[] biomeColors)
+ {
+ int bestIndex = -1;
+ float bestColorDiff = float.MaxValue;
+
+ for (int i = 0; i < biomeColors.Length; i++)
+ {
+ Color biomeColor = biomeColors[i];
+ float colorDiff = EuclideanColorDiff(r, g, b, biomeColor.r, biomeColor.g, biomeColor.b);
+ if (colorDiff < threshold && colorDiff < bestColorDiff)
+ {
+ bestColorDiff = colorDiff;
+ bestIndex = i;
+ }
+ }
+
+ return bestIndex;
+ }
+
+ private static int[] neighborIndices = new int[9];
+ private static List colorBuffer = new List(9);
+
+ private static int GetBestNeighborBiomeColorIndex(float r, float g, float b, int pixelIndex, CBAttributeMapSO biomeMap, Color[] biomeColors)
+ {
+ int width = biomeMap._width;
+ neighborIndices[0] = pixelIndex - 1 - width; // top-left
+ neighborIndices[1] = pixelIndex - width; // top
+ neighborIndices[2] = pixelIndex + 1 - width; // top-right
+ neighborIndices[3] = pixelIndex + 1; // right
+ neighborIndices[4] = pixelIndex + 1 + width; // bottom-right
+ neighborIndices[5] = pixelIndex + width; // bottom
+ neighborIndices[6] = pixelIndex - 1 + width; // bottom-left
+ neighborIndices[7] = pixelIndex - 1; // left
+ neighborIndices[8] = pixelIndex; // center
+
+ colorBuffer.Clear();
+ int textureSize = biomeMap._width * biomeMap._height;
+ int bpp = biomeMap._bpp;
+ byte[] data = biomeMap._data;
+ for (int i = 0; i < 8; i++)
+ {
+ int neighborIndex = neighborIndices[i];
+ if (neighborIndex < 0)
+ neighborIndex += textureSize;
+ else if (neighborIndex >= textureSize)
+ neighborIndex -= textureSize;
+
+ int colorIndex = neighborIndex * bpp;
+
+ Color neighborColor = new Color(data[colorIndex] * Byte2Float, data[colorIndex + 1] * Byte2Float, data[colorIndex + 2] * Byte2Float, 1f);
+ if (!colorBuffer.Contains(neighborColor))
+ colorBuffer.Add(neighborColor);
+ }
+
+ int bestIndex = 0;
+ float bestColorWeight = float.MaxValue;
+
+ for (int i = 0; i < biomeColors.Length; i++)
+ {
+ float colorWeight = 0f;
+ Color biomeColor = biomeColors[i];
+ for (int j = 0; j < colorBuffer.Count; j++)
+ {
+ colorWeight += EuclideanColorDiff(colorBuffer[j], biomeColor);
+ }
+
+ if (colorWeight < bestColorWeight)
+ {
+ bestIndex = i;
+ bestColorWeight = colorWeight;
+ }
+ }
+
+ return bestIndex;
+ }
+
+ private static float EuclideanColorDiff(float r1, float g1, float b1, float r2, float g2, float b2)
+ {
+ float r = r1 - r2;
+ float g = g1 - g2;
+ float b = b1 - b2;
+ return r * r + g * g + b * b;
+ }
+
+ private static float EuclideanColorDiff(Color colA, Color colB)
+ {
+ float r = colA.r - colB.r;
+ float g = colA.g - colB.g;
+ float b = colA.b - colB.b;
+ return r * r + g * g + b * b;
+ }
+ }
+
+
+ //[KSPAddon(KSPAddon.Startup.MainMenu, false)]
+ internal class FastBiomeQuery : MonoBehaviour
+ {
+ private static RGBA32[][] biomeColorsByBody;
+
+ IEnumerator Start()
+ {
+ for (int i = 0; i < 60; i++)
+ {
+ yield return null;
+ }
+
+ biomeColorsByBody = new RGBA32[FlightGlobals.Bodies.Count][];
+
+ foreach (CelestialBody body in FlightGlobals.Bodies)
+ {
+ CBAttributeMapSO biomeMap = body.BiomeMap;
+ if (biomeMap == null)
+ continue;
+
+ RGBA32[] biomeColors = new RGBA32[biomeMap.Attributes.Length];
+ biomeColorsByBody[body.flightGlobalsIndex] = biomeColors;
+
+ for (int i = 0; i < biomeMap.Attributes.Length; i++)
+ {
+ biomeColors[i] = biomeMap.Attributes[i].mapColor;
+ }
+ }
+
+ List ratios = new List();
+
+ foreach (CelestialBody body in FlightGlobals.Bodies)
+ {
+ if (body.BiomeMap == null)
+ continue;
+
+ CBAttributeMapSO biomeMap = body.BiomeMap;
+ Debug.Log($"[{body.name}] exactSearch={biomeMap.exactSearch}, neighborColorThresh={biomeMap.neighborColorThresh}, nonExactThreshold={biomeMap.nonExactThreshold}, bpp={biomeMap._bpp}");
+
+ int sampleCount = 1000000;
+ Vector2d[] coords = new Vector2d[sampleCount];
+ for (int i = 0; i < sampleCount; i++)
+ {
+ double lat = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLat(Random.Range(-90f, 90f)));
+ double lon = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLon(Random.Range(-180f, 180f)));
+ coords[i] = new Vector2d(lat, lon);
+ }
+
+ CBAttributeMapSO.MapAttribute[] stockBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
+ Stopwatch stockWatch = new Stopwatch();
+
+ stockWatch.Start();
+ for (int i = 0; i < sampleCount; i++)
+ {
+ Vector2d latLon = coords[i];
+ stockBiomes[i] = biomeMap.GetAtt(latLon.x, latLon.y);
+ }
+ stockWatch.Stop();
+ //Debug.Log($"[FastBiomeQuery] Stock sampling : {stockWatch.Elapsed.TotalMilliseconds:F3}ms");
+
+ CBAttributeMapSO.MapAttribute[] cfBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
+ Stopwatch cfWatch = new Stopwatch();
+
+ cfWatch.Start();
+ for (int i = 0; i < sampleCount; i++)
+ {
+ Vector2d latLon = coords[i];
+ cfBiomes[i] = GetAttInt(biomeMap, body.flightGlobalsIndex, latLon.x, latLon.y);
+ //cfBiomes[i] = GetAtt(biomeMap, latLon.x, latLon.y);
+ }
+ cfWatch.Stop();
+
+ double ratio = stockWatch.Elapsed.TotalMilliseconds / cfWatch.Elapsed.TotalMilliseconds;
+ ratios.Add(ratio);
+ Debug.Log($"[FastBiomeQuery] Sampling {sampleCount} biomes on {body.name,10} : {cfWatch.Elapsed.TotalMilliseconds,8:F2}ms vs {stockWatch.Elapsed.TotalMilliseconds,8:F2}ms, ratio:{ratio:F1}");
+
+ //for (int i = 0; i < sampleCount; i++)
+ //{
+ // if (stockBiomes[i] != cfBiomes[i])
+ // {
+ // Debug.LogWarning($"[FastBiomeQuery] Result mismatch at coords {coords[i]}, stock={stockBiomes[i].name}, kspcf={cfBiomes[i].name}");
+ // }
+ //}
+
+ int mismatchCount = 0;
+ for (int i = 0; i < sampleCount; i++)
+ {
+ if (stockBiomes[i] != cfBiomes[i])
+ {
+ mismatchCount++;
+ }
+ }
+
+ Debug.LogWarning($"[FastBiomeQuery] {mismatchCount} mismatchs ({mismatchCount/sampleCount:P3})");
+ }
+
+ Debug.Log($"[FastBiomeQuery] Average ratio : {ratios.Average():F2}");
+ }
+
+
+
+
+
+ private const double HalfPI = Math.PI / 2.0;
+ private const double DoublePI = Math.PI * 2.0;
+ private const double InversePI = 1.0 / Math.PI;
+ private const double InverseDoublePI = 1.0 / (2.0 * Math.PI);
+
+ public static CBAttributeMapSO.MapAttribute GetAttInt(CBAttributeMapSO biomeMap, int bodyIndex, double lat, double lon)
+ {
+ //if (biomeMap.exactSearch || biomeMap.nonExactThreshold != -1f)
+ //{
+ // return biomeMap.GetAtt(lat, lon);
+ //}
+
+ lon -= HalfPI;
+ if (lon < 0.0)
+ lon += DoublePI;
+ lon %= DoublePI;
+ double x = 1.0 - lon * InverseDoublePI;
+ double y = lat * InversePI + 0.5;
+
+ biomeMap.ConstructBilinearCoords(x, y);
+ RGBA32 c0 = GetColorRGBA32(biomeMap, biomeMap.minX, biomeMap.minY);
+ RGBA32 c1 = GetColorRGBA32(biomeMap, biomeMap.maxX, biomeMap.minY);
+ RGBA32 c2 = GetColorRGBA32(biomeMap, biomeMap.minX, biomeMap.maxY);
+ RGBA32 c3 = GetColorRGBA32(biomeMap, biomeMap.maxX, biomeMap.maxY);
+
+ RGBA32 pixelColor;
+
+ if (c0 == c1 && c0 == c2 && c0 == c3)
+ {
+ pixelColor = c0;
+ }
+ else
+ {
+ Color cf0 = c0;
+ Color cf1 = c1;
+ Color cf2 = c2;
+ Color cf3 = c3;
+
+ Color lerpColor = BilinearColor(cf0, cf1, cf2, cf3, biomeMap.midX, biomeMap.midY);
+ pixelColor = c0;
+ float maxMag = RGBADiffSqrMag(cf0, lerpColor);
+ float mag = RGBADiffSqrMag(cf1, lerpColor);
+ if (mag < maxMag)
+ {
+ maxMag = mag;
+ pixelColor = c1;
+ }
+
+ mag = RGBADiffSqrMag(cf2, lerpColor);
+ if (mag < maxMag)
+ {
+ maxMag = mag;
+ pixelColor = c2;
+ }
+
+ // There is a bug in the stock method, where it doesn't check the fourth color...
+ //mag = RGBADiffSqrMag(cf3, lerpColor);
+ //if (mag < maxMag)
+ //{
+ // pixelColor = c3;
+ //}
+ }
+
+ RGBA32[] biomeColors = biomeColorsByBody[bodyIndex];
+ int length = biomeColors.Length;
+ for (int i = 0; i < length; i++)
+ {
+ if (biomeColors[i] == pixelColor)
+ {
+ return biomeMap.Attributes[i];
+ }
+ }
+
+ // fallback path if the color doesn't match exactly for some reason, this is the default stock code
+ CBAttributeMapSO.MapAttribute result = biomeMap.Attributes[0];
+ float maxColorMag = float.MaxValue;
+ for (int i = 0; i < length; i++)
+ {
+ float colorMag = EuclideanColorDiff(pixelColor, biomeColors[i]);
+ if (colorMag < maxColorMag)
+ {
+ result = biomeMap.Attributes[i];
+ maxColorMag = colorMag;
+ }
+ }
+
+ return result;
+ }
+
+ private static Color BilinearColor(Color col0, Color col1, Color col2, Color col3, double midX, double midY)
+ {
+ double r0 = col0.r + (col1.r - col0.r) * midX;
+ double r1 = col2.r + (col3.r - col2.r) * midX;
+ double r = r0 + (r1 - r0) * midY;
+
+ double g0 = col0.g + (col1.g - col0.g) * midX;
+ double g1 = col2.g + (col3.g - col2.g) * midX;
+ double g = g0 + (g1 - g0) * midY;
+
+ double b0 = col0.b + (col1.b - col0.b) * midX;
+ double b1 = col2.b + (col3.b - col2.b) * midX;
+ double b = b0 + (b1 - b0) * midY;
+
+ //double a0 = col0.a + (col1.a - col0.a) * midX;
+ //double a1 = col2.a + (col3.a - col2.a) * midX;
+ //double a = a0 + (a1 - a0) * midY;
+
+ return new Color((float)r, (float)g, (float)b, 1f);
+ }
+
+ private static float EuclideanColorDiff(Color colA, Color colB)
+ {
+ float r = colA.r - colB.r;
+ float g = colA.g - colB.g;
+ float b = colA.b - colB.b;
+ return r * r + g * g + b * b;
+ }
+
+ private static float RGBADiffSqrMag(Color colA, Color colB)
+ {
+ float r = colA.r - colB.r;
+ float g = colA.g - colB.g;
+ float b = colA.b - colB.b;
+ float a = colA.a - colB.a;
+ return r * r + g * g + b * b + a * a;
+ }
+
+ private static RGBA32 BilinearRGBA32(RGBA32 col0, RGBA32 col1, RGBA32 col2, RGBA32 col3, double midX, double midY)
+ {
+ double r0 = col0.r + (col1.r - col0.r) * midX;
+ double r1 = col2.r + (col3.r - col2.r) * midX;
+ double r = r0 + (r1 - r0) * midY;
+
+ double g0 = col0.g + (col1.g - col0.g) * midX;
+ double g1 = col2.g + (col3.g - col2.g) * midX;
+ double g = g0 + (g1 - g0) * midY;
+
+ double b0 = col0.b + (col1.b - col0.b) * midX;
+ double b1 = col2.b + (col3.b - col2.b) * midX;
+ double b = b0 + (b1 - b0) * midY;
+
+ //double a0 = col0.a + (col1.a - col0.a) * midX;
+ //double a1 = col2.a + (col3.a - col2.a) * midX;
+ //double a = a0 + (a1 - a0) * midY;
+
+ return new RGBA32((byte)r, (byte)g, (byte)b, 255);
+ }
+
+ private static int RGBA32DiffSqrMag(RGBA32 colA, RGBA32 colB)
+ {
+ int r = colA.r - colB.r;
+ int g = colA.g - colB.g;
+ int b = colA.b - colB.b;
+ return r * r + g * g + b * b;
+ }
+
+ private static RGBA32 GetColorRGBA32(CBAttributeMapSO biomeMap, int x, int y)
+ {
+ int index = biomeMap.PixelIndex(x, y);
+ byte[] data = biomeMap._data;
+
+ switch (biomeMap._bpp)
+ {
+ case 3:
+ return new RGBA32(data[index], data[index + 1], data[index + 2], 255);
+ case 4:
+ return new RGBA32(data[index], data[index + 1], data[index + 2], data[index + 3]);
+ case 2:
+ {
+ byte rgb = data[index];
+ return new RGBA32(rgb, rgb, rgb, data[index + 1]);
+ }
+ default:
+ {
+ byte rgb = data[index];
+ return new RGBA32(rgb, rgb, rgb, 255);
+ }
+ }
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct RGBA32
+ {
+ private const float Byte2Float = 0.003921569f;
+
+ [FieldOffset(0)]
+ public int rgba;
+
+ [FieldOffset(0)]
+ public byte r;
+
+ [FieldOffset(1)]
+ public byte g;
+
+ [FieldOffset(2)]
+ public byte b;
+
+ [FieldOffset(3)]
+ public byte a;
+
+ public RGBA32(byte r, byte g, byte b, byte a)
+ {
+ rgba = 0;
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ this.a = a;
+ }
+
+ public static implicit operator RGBA32(Color c)
+ {
+ return new RGBA32((byte)(c.r * 255f), (byte)(c.g * 255f), (byte)(c.b * 255f), (byte)(c.a * 255f));
+ }
+
+ public static implicit operator Color(RGBA32 c)
+ {
+ return new Color(c.r * Byte2Float, c.g * Byte2Float, c.b * Byte2Float, c.a * Byte2Float);
+ }
+
+ public bool Equals(RGBA32 other) => rgba == other.rgba;
+ public override bool Equals(object obj) => obj is RGBA32 other && Equals(other);
+ public static bool operator ==(RGBA32 lhs, RGBA32 rhs) => lhs.rgba == rhs.rgba;
+ public static bool operator !=(RGBA32 lhs, RGBA32 rhs) => lhs.rgba != rhs.rgba;
+ public override int GetHashCode() => rgba;
+ }
+ }
+}
From 2db56e2b7917de61dbe2d666b8763b6e66a25439 Mon Sep 17 00:00:00 2001
From: gotmachine <24925209+gotmachine@users.noreply.github.com>
Date: Mon, 5 Jun 2023 11:02:41 +0200
Subject: [PATCH 2/4] - FastBiomeQuery : finalized implementation, currently
always applied, need to condition BiomeMapOptimizer from a KSPCF patch -
MapSOCorrectWrapping : fixed some issues and improved performance a bit
---
GameData/KSPCommunityFixes/Settings.cfg | 2 +-
.../BugFixes/MapSOCorrectWrapping.cs | 76 +-
KSPCommunityFixes/Library/StaticHelpers.cs | 79 +-
.../Performance/FastBiomeQuery.cs | 985 +++++++++++-------
4 files changed, 740 insertions(+), 402 deletions(-)
diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg
index 0d526eee..924f99d5 100644
--- a/GameData/KSPCommunityFixes/Settings.cfg
+++ b/GameData/KSPCommunityFixes/Settings.cfg
@@ -154,7 +154,7 @@ KSP_COMMUNITY_FIXES
// Avoids big spikes on the poles and incorrect biomes where the oles have disparate biomes.
MapSOCorrectWrapping = true
// The Mohole is a result of the bug the MapSOCorrectWrapping patch is fixing. If set to
- // true, the patch won't apply to the stock Moho height/biome maps
+ // true, the patch won't apply to the stock Moho height map
MapSOCorrectWrappingIgnoreMoho = true
// Fix spread angle still being applied after decoupling symmetry-placed parachutes.
diff --git a/KSPCommunityFixes/BugFixes/MapSOCorrectWrapping.cs b/KSPCommunityFixes/BugFixes/MapSOCorrectWrapping.cs
index 4721e757..8d7227d4 100644
--- a/KSPCommunityFixes/BugFixes/MapSOCorrectWrapping.cs
+++ b/KSPCommunityFixes/BugFixes/MapSOCorrectWrapping.cs
@@ -1,6 +1,7 @@
using HarmonyLib;
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using UnityEngine;
namespace KSPCommunityFixes
@@ -26,8 +27,8 @@ protected override void ApplyPatches(List patches)
// see https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/121
// The patched ConstructBilinearCoords() methods will have the side effect of removing the Mohole because Squad
// choose that "this is not a bug, it's a feature".
- // To prevent it, we acquire references to the Moho MapSO instances (biome map and height map) and fall back
- // to the original stock implementation if the method is called on those instances.
+ // To prevent it, we acquire references to the Moho MapSO heightmap instance and fall back to the original stock
+ // implementation if the method is called on those instances.
// Note that all stock bodies will actually get a significantly different terrain at the poles due to this patch,
// but due to how performance sensitive those method are, checking all stock MapSO instances (there are 40 of them)
// isn't really an option.
@@ -46,8 +47,9 @@ protected override void ApplyPatches(List patches)
}
}
- private static MapSO moho_biomes;
private static MapSO moho_height;
+ private static readonly double lessThanOneDouble = StaticHelpers.BitDecrement(1.0);
+ private static readonly float lessThanOneFloat = StaticHelpers.BitDecrement(1f);
static void PSystemSetup_Awake_Postfix(PSystemSetup __instance)
{
@@ -58,15 +60,7 @@ static void PSystemSetup_Awake_Postfix(PSystemSetup __instance)
foreach (MapSO mapSo in so)
{
- if (mapSo.MapName == "moho_biomes"
- && mapSo.Size == 6291456
- && mapSo._data[0] == 216
- && mapSo._data[1] == 178
- && mapSo._data[2] == 144)
- {
- moho_biomes = mapSo;
- }
- else if (mapSo.MapName == "moho_height"
+ if (mapSo.MapName == "moho_height"
&& mapSo.Size == 2097152
&& mapSo._data[1509101] == 146
&& mapSo._data[1709108] == 162
@@ -79,54 +73,70 @@ static void PSystemSetup_Awake_Postfix(PSystemSetup __instance)
static bool MapSO_ConstructBilinearCoords_Float(MapSO __instance, float x, float y)
{
- if (ReferenceEquals(__instance, moho_biomes) || ReferenceEquals(__instance, moho_height))
- return true;
-
// X wraps around as it is longitude.
x = Mathf.Abs(x - Mathf.Floor(x));
+
__instance.centerX = x * __instance._width;
- __instance.minX = Mathf.FloorToInt(__instance.centerX);
- __instance.maxX = Mathf.CeilToInt(__instance.centerX);
+ __instance.minX = (int)__instance.centerX;
+ __instance.maxX = __instance.minX + 1;
__instance.midX = __instance.centerX - __instance.minX;
if (__instance.maxX == __instance._width)
__instance.maxX = 0;
// Y clamps as it is latitude and the poles don't wrap to each other.
- y = Mathf.Clamp(y, 0, 0.99999f);
+ if (y >= 1f)
+ y = lessThanOneFloat;
+ else if (y < 0f)
+ y = 0f;
+
__instance.centerY = y * __instance._height;
- __instance.minY = Mathf.FloorToInt(__instance.centerY);
- __instance.maxY = Mathf.CeilToInt(__instance.centerY);
+ __instance.minY = (int)__instance.centerY;
+ __instance.maxY = __instance.minY + 1;
__instance.midY = __instance.centerY - __instance.minY;
- if (__instance.maxY >= __instance._height)
- __instance.maxY = __instance._height - 1;
-
+ if (__instance.maxY == __instance._height)
+ {
+ if (ReferenceEquals(__instance, moho_height))
+ __instance.maxY = 0; // use incorrect wrapping for moho
+ else
+ __instance.maxY = __instance._height - 1;
+ }
+
return false;
}
static bool MapSO_ConstructBilinearCoords_Double(MapSO __instance, double x, double y)
{
- if (ReferenceEquals(__instance, moho_biomes) || ReferenceEquals(__instance, moho_height))
- return true;
-
// X wraps around as it is longitude.
x = Math.Abs(x - Math.Floor(x));
+
__instance.centerXD = x * __instance._width;
- __instance.minX = (int)Math.Floor(__instance.centerXD);
- __instance.maxX = (int)Math.Ceiling(__instance.centerXD);
+ __instance.minX = (int)__instance.centerXD;
+ __instance.maxX = __instance.minX + 1;
__instance.midX = (float)__instance.centerXD - __instance.minX;
if (__instance.maxX == __instance._width)
__instance.maxX = 0;
// Y clamps as it is latitude and the poles don't wrap to each other.
- y = Math.Min(Math.Max(y, 0), 0.99999);
+ if (y >= 1.0)
+ y = lessThanOneDouble;
+ else if (y < 0.0)
+ y = 0.0;
+
__instance.centerYD = y * __instance._height;
- __instance.minY = (int)Math.Floor(__instance.centerYD);
- __instance.maxY = (int)Math.Ceiling(__instance.centerYD);
+ __instance.minY = (int)__instance.centerYD;
+ __instance.maxY = __instance.minY + 1;
__instance.midY = (float)__instance.centerYD - __instance.minY;
- if (__instance.maxY >= __instance._height)
- __instance.maxY = __instance._height - 1;
+ if (__instance.maxY == __instance._height)
+ {
+ if (ReferenceEquals(__instance, moho_height))
+ __instance.maxY = 0; // use incorrect wrapping for moho
+ else
+ __instance.maxY = __instance._height - 1;
+ }
return false;
}
+
+
}
}
diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs
index d9689802..55dc346e 100644
--- a/KSPCommunityFixes/Library/StaticHelpers.cs
+++ b/KSPCommunityFixes/Library/StaticHelpers.cs
@@ -1,4 +1,7 @@
-namespace KSPCommunityFixes
+using System.Runtime.CompilerServices;
+using System;
+
+namespace KSPCommunityFixes
{
static class StaticHelpers
{
@@ -38,5 +41,79 @@ public static string HumanReadableBytes(long bytes)
// Return formatted number with suffix
return readable.ToString("0.### ") + suffix;
}
+
+ // https://github.com/dotnet/runtime/blob/af4efb1936b407ca5f4576e81484cf5687b79a26/src/libraries/System.Private.CoreLib/src/System/Math.cs#L210
+ public static double BitDecrement(double x)
+ {
+ long bits = BitConverter.DoubleToInt64Bits(x);
+
+ if (((bits >> 32) & 0x7FF00000) >= 0x7FF00000)
+ {
+ // NaN returns NaN
+ // -Infinity returns -Infinity
+ // +Infinity returns double.MaxValue
+ return (bits == 0x7FF00000_00000000) ? double.MaxValue : x;
+ }
+
+ if (bits == 0x00000000_00000000)
+ {
+ // +0.0 returns -double.Epsilon
+ return -double.Epsilon;
+ }
+
+ // Negative values need to be incremented
+ // Positive values need to be decremented
+
+ bits += ((bits < 0) ? +1 : -1);
+ return BitConverter.Int64BitsToDouble(bits);
+ }
+
+ // https://github.com/dotnet/runtime/blob/af4efb1936b407ca5f4576e81484cf5687b79a26/src/libraries/System.Private.CoreLib/src/System/MathF.cs#L52
+ public static float BitDecrement(float x)
+ {
+ int bits = SingleToInt32Bits(x);
+
+ if ((bits & 0x7F800000) >= 0x7F800000)
+ {
+ // NaN returns NaN
+ // -Infinity returns -Infinity
+ // +Infinity returns float.MaxValue
+ return (bits == 0x7F800000) ? float.MaxValue : x;
+ }
+
+ if (bits == 0x00000000)
+ {
+ // +0.0 returns -float.Epsilon
+ return -float.Epsilon;
+ }
+
+ // Negative values need to be incremented
+ // Positive values need to be decremented
+
+ bits += ((bits < 0) ? +1 : -1);
+ return Int32BitsToSingle(bits);
+ }
+
+ ///
+ /// Converts the specified single-precision floating point number to a 32-bit signed integer.
+ ///
+ /// The number to convert.
+ /// A 32-bit signed integer whose bits are identical to .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe int SingleToInt32Bits(float value)
+ {
+ return *((int*)&value);
+ }
+
+ ///
+ /// Converts the specified 32-bit signed integer to a single-precision floating point number.
+ ///
+ /// The number to convert.
+ /// A single-precision floating point number whose bits are identical to .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe float Int32BitsToSingle(int value)
+ {
+ return *((float*)&value);
+ }
}
}
diff --git a/KSPCommunityFixes/Performance/FastBiomeQuery.cs b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
index f3d497d2..7274db51 100644
--- a/KSPCommunityFixes/Performance/FastBiomeQuery.cs
+++ b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
@@ -1,523 +1,774 @@
-using Smooth.Algebraics;
+//#define BIOME_CONVERSION_VALIDATE
+//#define DEBUG_BIOME_MISMATCH
+//#define WITH_STOCK_BILINEAR_BUG
+//#define BIOME_FLIGHTDEBUG
+
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.WindowsRuntime;
-using System.Text;
+using System.Threading;
using System.Threading.Tasks;
-using Steamworks;
+using Unity.Collections;
using UnityEngine;
-using VehiclePhysics;
using Debug = UnityEngine.Debug;
using Random = UnityEngine.Random;
namespace KSPCommunityFixes.Performance
{
- [KSPAddon(KSPAddon.Startup.MainMenu, false)]
- internal class BiomeMapOptimizer : MonoBehaviour
+ ///
+ /// Reimplemementation of the stock biome map class (CBAttributeMapSO), fixing a bug and massively
+ /// improving performance (around x17 on average, up to x30 on bodies with a large amount of biomes) when
+ /// calling the GetAtt() method.
+ /// Notable changes are :
+ /// - Storing directly the Attribute index in the byte array (instead of a color), reducing memory usage
+ /// to 8bpp instead of 24bpp, and making the array a lookup table instead of having to compare colors.
+ /// - Fixed a bug in the stock bilinear interpolation, causing incorrect results in a specific direction
+ /// on biome transitions.
+ ///
+ public class KSPCFFastBiomeMap : CBAttributeMapSO
{
- const float Byte2Float = 0.003921569f;
+ private static readonly double lessThanOneDouble = StaticHelpers.BitDecrement(1.0);
- public static byte[][] biomeMaps;
-
- void Start()
+ ///
+ /// Return the biome definition at the given position defined in normalized [0, 1] texture coordinates,
+ /// performing bilinear sampling at biomes intersections.
+ ///
+ private MapAttribute GetPixelBiome(double x, double y)
{
- biomeMaps = new byte[FlightGlobals.Bodies.Count][];
-
- foreach (CelestialBody body in FlightGlobals.Bodies)
- {
- if (body.BiomeMap != null)
- {
- byte[] newMap = Optimize(body.BiomeMap);
- biomeMaps[body.flightGlobalsIndex] = newMap;
- }
- }
- }
+ GetBilinearCoordinates(x, y, _width, _height, out int minX, out int maxX, out int minY, out int maxY, out double midX, out double midY);
- void Update()
- {
+ // Get the 4 closest pixels for bilinear interpolation
+ byte b00 = _data[minX + minY * _rowWidth];
+ byte b10 = _data[maxX + minY * _rowWidth];
+ byte b01 = _data[minX + maxY * _rowWidth];
+ byte b11 = _data[maxX + maxY * _rowWidth];
- }
+ byte biomeIndex;
- private byte[] Optimize(CBAttributeMapSO biomeMap)
- {
- int biomeCount = biomeMap.Attributes.Length;
- Color[] biomeColors = new Color[biomeCount];
- for (int i = 0; i < biomeCount; i++)
+ // if all 4 pixels are the same, we don't need to interpolate
+ if (b00 == b10 && b00 == b01 && b00 == b11)
{
- biomeColors[i] = biomeMap.Attributes[i].mapColor;
+ biomeIndex = b00;
}
-
- byte[] oldMap = biomeMap._data;
- int bpp = biomeMap._bpp;
-
- byte[] newMap = new byte[biomeMap._height * biomeMap._width];
-
- for (int i = 0; i < newMap.Length; i++)
+ else
{
- int colorIndex = i * bpp;
- float r = oldMap[colorIndex] * Byte2Float;
- float g = oldMap[colorIndex + 1] * Byte2Float;
- float b = oldMap[colorIndex + 2] * Byte2Float;
-
- int biomeIndex = TryGetExactBiomeColorIndex(r, g, b, biomeColors);
- if (biomeIndex > -1)
+ // Cast to double once
+ double d00 = b00;
+ double d10 = b10;
+ double d01 = b01;
+ double d11 = b11;
+
+ // Get bilinear value
+ double d20 = d00 + (d10 - d00) * midX;
+ double d21 = d01 + (d11 - d01) * midX;
+ double bilinear = d20 + (d21 - d20) * midY;
+
+ // Amongst the 4 candidates, find the closest one to the bilinear value
+ biomeIndex = b00;
+ double bestMagnitude = DiffMagnitude(d00, bilinear);
+ double candidateMagnitude = DiffMagnitude(d10, bilinear);
+ if (candidateMagnitude < bestMagnitude)
{
- newMap[i] = (byte)biomeIndex;
- continue;
+ bestMagnitude = candidateMagnitude;
+ biomeIndex = b10;
}
- biomeIndex = TryGetNearBiomeColorIndex(r, g, b, biomeMap.neighborColorThresh, biomeColors);
- if (biomeIndex > -1)
+ candidateMagnitude = DiffMagnitude(d01, bilinear);
+ if (candidateMagnitude < bestMagnitude)
{
- newMap[i] = (byte)biomeIndex;
- continue;
+ bestMagnitude = candidateMagnitude;
+ biomeIndex = b01;
}
- biomeIndex = GetBestNeighborBiomeColorIndex(r, g, b, i, biomeMap, biomeColors);
- newMap[i] = (byte)biomeIndex;
+ // Note : the equivalent code in the stock implementation has a bug causing
+ // the last sampled pixel to be ignored, resulting in non-interpolated results
+ // in a specific direction. Getting the same results as the stock implementation
+ // can be achieved by commenting this last check
+#if !WITH_STOCK_BILINEAR_BUG
+ candidateMagnitude = DiffMagnitude(d11, bilinear);
+ if (candidateMagnitude < bestMagnitude)
+ {
+ biomeIndex = b11;
+ }
+#endif
}
- return newMap;
+ return Attributes[biomeIndex];
}
- private int TryGetExactBiomeColorIndex(float r, float g, float b, Color[] biomeColors)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double DiffMagnitude(double valA, double valB)
{
- for (int i = 0; i < biomeColors.Length; i++)
- {
- Color biomeColor = biomeColors[i];
- if (biomeColor.r == r && biomeColor.g == g && biomeColor.b == b)
- return i;
- }
-
- return -1;
+ double diff = valA - valB;
+ return diff * diff;
}
- private static int TryGetNearBiomeColorIndex(float r, float g, float b, float threshold, Color[] biomeColors)
+ private const double HalfPI = Math.PI / 2.0;
+ private const double DoublePI = Math.PI * 2.0;
+ private const double InversePI = 1.0 / Math.PI;
+ private const double InverseDoublePI = 1.0 / (2.0 * Math.PI);
+
+ public override MapAttribute GetAtt(double lat, double lon)
{
- int bestIndex = -1;
- float bestColorDiff = float.MaxValue;
+ // Transform lat/lon into normalized texture coordinates
+ lon -= HalfPI;
+ if (lon < 0.0)
+ lon += DoublePI;
+ lon %= DoublePI;
+ double x = 1.0 - lon * InverseDoublePI;
- for (int i = 0; i < biomeColors.Length; i++)
- {
- Color biomeColor = biomeColors[i];
- float colorDiff = EuclideanColorDiff(r, g, b, biomeColor.r, biomeColor.g, biomeColor.b);
- if (colorDiff < threshold && colorDiff < bestColorDiff)
- {
- bestColorDiff = colorDiff;
- bestIndex = i;
- }
- }
+ double y = lat * InversePI + 0.5;
- return bestIndex;
+ return GetPixelBiome(x, y);
}
- private static int[] neighborIndices = new int[9];
- private static List colorBuffer = new List(9);
-
- private static int GetBestNeighborBiomeColorIndex(float r, float g, float b, int pixelIndex, CBAttributeMapSO biomeMap, Color[] biomeColors)
+ public override Color GetPixelColor(double x, double y)
{
- int width = biomeMap._width;
- neighborIndices[0] = pixelIndex - 1 - width; // top-left
- neighborIndices[1] = pixelIndex - width; // top
- neighborIndices[2] = pixelIndex + 1 - width; // top-right
- neighborIndices[3] = pixelIndex + 1; // right
- neighborIndices[4] = pixelIndex + 1 + width; // bottom-right
- neighborIndices[5] = pixelIndex + width; // bottom
- neighborIndices[6] = pixelIndex - 1 + width; // bottom-left
- neighborIndices[7] = pixelIndex - 1; // left
- neighborIndices[8] = pixelIndex; // center
-
- colorBuffer.Clear();
- int textureSize = biomeMap._width * biomeMap._height;
- int bpp = biomeMap._bpp;
- byte[] data = biomeMap._data;
- for (int i = 0; i < 8; i++)
- {
- int neighborIndex = neighborIndices[i];
- if (neighborIndex < 0)
- neighborIndex += textureSize;
- else if (neighborIndex >= textureSize)
- neighborIndex -= textureSize;
-
- int colorIndex = neighborIndex * bpp;
-
- Color neighborColor = new Color(data[colorIndex] * Byte2Float, data[colorIndex + 1] * Byte2Float, data[colorIndex + 2] * Byte2Float, 1f);
- if (!colorBuffer.Contains(neighborColor))
- colorBuffer.Add(neighborColor);
- }
+ return GetPixelBiome(x, y).mapColor;
+ }
- int bestIndex = 0;
- float bestColorWeight = float.MaxValue;
+ public override Color GetPixelColor(float x, float y)
+ {
+ return GetPixelBiome(x, y).mapColor;
+ }
- for (int i = 0; i < biomeColors.Length; i++)
- {
- float colorWeight = 0f;
- Color biomeColor = biomeColors[i];
- for (int j = 0; j < colorBuffer.Count; j++)
- {
- colorWeight += EuclideanColorDiff(colorBuffer[j], biomeColor);
- }
+ public override Color GetPixelColor(int x, int y)
+ {
+ return Attributes[_data[x + y * _rowWidth]].mapColor;
+ }
- if (colorWeight < bestColorWeight)
- {
- bestIndex = i;
- bestColorWeight = colorWeight;
- }
- }
+ public override Color GetPixelColor32(double x, double y)
+ {
+ return GetPixelBiome(x, y).mapColor;
+ }
- return bestIndex;
+ public override Color GetPixelColor32(float x, float y)
+ {
+ return GetPixelBiome(x, y).mapColor;
}
- private static float EuclideanColorDiff(float r1, float g1, float b1, float r2, float g2, float b2)
+ public override Color32 GetPixelColor32(int x, int y)
{
- float r = r1 - r2;
- float g = g1 - g2;
- float b = b1 - b2;
- return r * r + g * g + b * b;
+ return Attributes[_data[x + y * _rowWidth]].mapColor;
}
- private static float EuclideanColorDiff(Color colA, Color colB)
+ public override void CreateMap(MapDepth depth, string name, int width, int height)
{
- float r = colA.r - colB.r;
- float g = colA.g - colB.g;
- float b = colA.b - colB.b;
- return r * r + g * g + b * b;
+ _name = name;
+ _width = width;
+ _height = height;
+ _bpp = 1;
+ _rowWidth = _width;
+ _data = new byte[_width * _height];
+ _isCompiled = true;
}
- }
+ ///
+ /// Create a "compiled" biome map from a texture. Attributes **must** be populated prior to calling this.
+ /// Note that the depth param is ignored, as we always encode biomes in a 1 Bpp array.
+ ///
+ public override void CreateMap(MapDepth depth, Texture2D tex)
+ {
+ _name = tex.name;
+ _width = tex.width;
+ _height = tex.height;
+ _bpp = 1;
+ _rowWidth = _width;
+ _isCompiled = true;
- //[KSPAddon(KSPAddon.Startup.MainMenu, false)]
- internal class FastBiomeQuery : MonoBehaviour
- {
- private static RGBA32[][] biomeColorsByBody;
+ if (Attributes == null || Attributes.Length == 0)
+ throw new Exception("Attributes must be populated before creating the map from a texture !");
- IEnumerator Start()
- {
- for (int i = 0; i < 60; i++)
- {
- yield return null;
- }
+ int biomeCount = Attributes.Length;
+ RGBA32[] biomeColors = new RGBA32[biomeCount];
- biomeColorsByBody = new RGBA32[FlightGlobals.Bodies.Count][];
+ for (int i = biomeCount; i-- > 0;)
+ biomeColors[i] = Attributes[i].mapColor;
- foreach (CelestialBody body in FlightGlobals.Bodies)
- {
- CBAttributeMapSO biomeMap = body.BiomeMap;
- if (biomeMap == null)
- continue;
+ int badPixelsCount = 0;
+ int size = _height * _width;
- RGBA32[] biomeColors = new RGBA32[biomeMap.Attributes.Length];
- biomeColorsByBody[body.flightGlobalsIndex] = biomeColors;
+ Color32[] colorData = tex.GetPixels32();
+ _data = new byte[size];
- for (int i = 0; i < biomeMap.Attributes.Length; i++)
+ Parallel.For(0, _width, x =>
+ {
+ for (int y = _height; y-- > 0;)
{
- biomeColors[i] = biomeMap.Attributes[i].mapColor;
- }
- }
+ int biomeIndex = -1;
+ RGBA32 pixelColor = colorData[x + y * _width];
+ pixelColor.ClearAlpha();
- List ratios = new List();
+ for (int i = biomeCount; i-- > 0;)
+ {
+ if (biomeColors[i] == pixelColor)
+ {
+ biomeIndex = i;
+ break;
+ }
+ }
- foreach (CelestialBody body in FlightGlobals.Bodies)
+ if (biomeIndex == -1)
+ {
+ Interlocked.Increment(ref badPixelsCount);
+ biomeIndex = GetBiomeIndexFromTexture((double)x / _width, (double)y / _height, biomeColors, colorData, _width, _height, nonExactThreshold);
+ }
+
+ _data[x + y * _width] = (byte)biomeIndex;
+ }
+ });
+
+ if (badPixelsCount > 0)
{
- if (body.BiomeMap == null)
- continue;
+ Debug.LogWarning($"[KSPCF/FastBiomeMap] Loading {_name} : {badPixelsCount} ({(badPixelsCount / (float)size):P3}) pixels not matching a biome color, check the biome texture and biome definitions.");
+ }
+ }
- CBAttributeMapSO biomeMap = body.BiomeMap;
- Debug.Log($"[{body.name}] exactSearch={biomeMap.exactSearch}, neighborColorThresh={biomeMap.neighborColorThresh}, nonExactThreshold={biomeMap.nonExactThreshold}, bpp={biomeMap._bpp}");
+ private static int GetBiomeIndexFromTexture(double x, double y, RGBA32[] biomeColors, Color32[] colorData, int width, int height, float nonExactThreshold)
+ {
+ GetBilinearCoordinates(x, y, width, height, out int minX, out int maxX, out int minY, out int maxY, out double midX, out double midY);
- int sampleCount = 1000000;
- Vector2d[] coords = new Vector2d[sampleCount];
- for (int i = 0; i < sampleCount; i++)
- {
- double lat = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLat(Random.Range(-90f, 90f)));
- double lon = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLon(Random.Range(-180f, 180f)));
- coords[i] = new Vector2d(lat, lon);
- }
+ // Get 4 samples pixels for bilinear interpolation
+ RGBA32 c00 = GetRGBA32AtTextureCoords(minX, minY, colorData, width);
+ RGBA32 c10 = GetRGBA32AtTextureCoords(maxX, minY, colorData, width);
+ RGBA32 c01 = GetRGBA32AtTextureCoords(minX, maxY, colorData, width);
+ RGBA32 c11 = GetRGBA32AtTextureCoords(maxX, maxY, colorData, width);
- CBAttributeMapSO.MapAttribute[] stockBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
- Stopwatch stockWatch = new Stopwatch();
+ return GetBiomeIndexStockBilinearSampling(biomeColors, c00, c10, c01, c11, midX, midY, GetIntNonExactThreshold(nonExactThreshold));
+ }
- stockWatch.Start();
- for (int i = 0; i < sampleCount; i++)
+ ///
+ /// Convert a 3 Bpp stock biome map encoding rgb colors into a 1 Bpp biome map encoding biome indices.
+ ///
+ public bool CopyFromMap(CBAttributeMapSO fromMap)
+ {
+ if (fromMap._bpp != 3)
+ return false;
+
+ Attributes = fromMap.Attributes;
+ _name = fromMap._name;
+ _width = fromMap._width;
+ _height = fromMap._height;
+ _bpp = 1;
+ _rowWidth = _width;
+ _isCompiled = true;
+
+ int biomeCount = Attributes.Length;
+ RGBA32[] biomeColors = new RGBA32[biomeCount];
+
+ for (int i = biomeCount; i-- > 0;)
+ biomeColors[i] = Attributes[i].mapColor;
+
+ int badPixelsCount = 0;
+ int size = _height * _width;
+ int fromDataRowWidth = fromMap._rowWidth;
+ byte[] fromData = fromMap._data;
+ _data = new byte[size];
+
+ Parallel.For(0, _width, x =>
+ {
+ for (int y = _height; y-- > 0;)
{
- Vector2d latLon = coords[i];
- stockBiomes[i] = biomeMap.GetAtt(latLon.x, latLon.y);
- }
- stockWatch.Stop();
- //Debug.Log($"[FastBiomeQuery] Stock sampling : {stockWatch.Elapsed.TotalMilliseconds:F3}ms");
+ int fromDataIndex = x * 3 + y * fromDataRowWidth;
+ RGBA32 pixelColor = new RGBA32(fromData[fromDataIndex], fromData[fromDataIndex + 1], fromData[fromDataIndex + 2]);
- CBAttributeMapSO.MapAttribute[] cfBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
- Stopwatch cfWatch = new Stopwatch();
+ int biomeIndex = -1;
+ for (int i = biomeCount; i-- > 0;)
+ {
+ if (biomeColors[i] == pixelColor)
+ {
+ biomeIndex = i;
+ break;
+ }
+ }
- cfWatch.Start();
- for (int i = 0; i < sampleCount; i++)
- {
- Vector2d latLon = coords[i];
- cfBiomes[i] = GetAttInt(biomeMap, body.flightGlobalsIndex, latLon.x, latLon.y);
- //cfBiomes[i] = GetAtt(biomeMap, latLon.x, latLon.y);
- }
- cfWatch.Stop();
-
- double ratio = stockWatch.Elapsed.TotalMilliseconds / cfWatch.Elapsed.TotalMilliseconds;
- ratios.Add(ratio);
- Debug.Log($"[FastBiomeQuery] Sampling {sampleCount} biomes on {body.name,10} : {cfWatch.Elapsed.TotalMilliseconds,8:F2}ms vs {stockWatch.Elapsed.TotalMilliseconds,8:F2}ms, ratio:{ratio:F1}");
-
- //for (int i = 0; i < sampleCount; i++)
- //{
- // if (stockBiomes[i] != cfBiomes[i])
- // {
- // Debug.LogWarning($"[FastBiomeQuery] Result mismatch at coords {coords[i]}, stock={stockBiomes[i].name}, kspcf={cfBiomes[i].name}");
- // }
- //}
-
- int mismatchCount = 0;
- for (int i = 0; i < sampleCount; i++)
- {
- if (stockBiomes[i] != cfBiomes[i])
+ if (biomeIndex == -1)
{
- mismatchCount++;
+ Interlocked.Increment(ref badPixelsCount);
+ biomeIndex = GetBiomeIndexFromStockBiomeMap((double)x / _width, (double)y / _height, biomeColors, fromData, _width, _height, fromMap._rowWidth, fromMap.nonExactThreshold);
}
+
+ _data[x + y * _width] = (byte)biomeIndex;
}
+ });
- Debug.LogWarning($"[FastBiomeQuery] {mismatchCount} mismatchs ({mismatchCount/sampleCount:P3})");
+ if (badPixelsCount > 0)
+ {
+ Debug.LogWarning($"[KSPCF/FastBiomeMap] Converting {fromMap._name} : {badPixelsCount} ({(badPixelsCount / (float)size):P3}) pixels not matching a biome color, check the biome texture and biome definitions.");
}
- Debug.Log($"[FastBiomeQuery] Average ratio : {ratios.Average():F2}");
+ return true;
}
+ private static int GetBiomeIndexFromStockBiomeMap(double x, double y, RGBA32[] biomeColors, byte[] rgbData, int width, int height, int rowWidth, float nonExactThreshold)
+ {
+ GetBilinearCoordinates(x, y, width, height, out int minX, out int maxX, out int minY, out int maxY, out double midX, out double midY);
+ // Get 4 samples pixels for bilinear interpolation
+ RGBA32 c00 = GetRGB3BppAtTextureCoords(minX, minY, rgbData, rowWidth);
+ RGBA32 c10 = GetRGB3BppAtTextureCoords(maxX, minY, rgbData, rowWidth);
+ RGBA32 c01 = GetRGB3BppAtTextureCoords(minX, maxY, rgbData, rowWidth);
+ RGBA32 c11 = GetRGB3BppAtTextureCoords(maxX, maxY, rgbData, rowWidth);
+ return GetBiomeIndexStockBilinearSampling(biomeColors, c00, c10, c01, c11, midX, midY, GetIntNonExactThreshold(nonExactThreshold));
+ }
+ public override void ConstructBilinearCoords(double x, double y)
+ {
+ // X wraps around [0, 1[ as it is longitude.
+ x = Math.Abs(x - Math.Floor(x));
+ centerXD = x * _width;
+ minX = (int)centerXD;
+ maxX = minX + 1;
+ midX = (float)(centerXD - minX);
+ if (maxX == _width)
+ maxX = 0;
+
+ // Y is clamped to [0, 1[ as it latitude and the poles don't wrap to each other.
+ if (y >= 1.0)
+ y = lessThanOneDouble;
+ else if (y < 0.0)
+ y = 0.0;
+ centerYD = y * _height;
+ minY = (int)centerYD;
+ maxY = minY + 1;
+ midY = (float)(centerYD - minY);
+ if (maxY == _height)
+ maxY = _height - 1;
+ }
- private const double HalfPI = Math.PI / 2.0;
- private const double DoublePI = Math.PI * 2.0;
- private const double InversePI = 1.0 / Math.PI;
- private const double InverseDoublePI = 1.0 / (2.0 * Math.PI);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void GetBilinearCoordinates(double x, double y, int width, int height, out int minX, out int maxX, out int minY, out int maxY, out double midX, out double midY)
+ {
+ // X wraps around [0, 1[ as it is longitude.
+ x = Math.Abs(x - Math.Floor(x));
+ double centerXD = x * width;
+ minX = (int)centerXD;
+ maxX = minX + 1;
+ midX = centerXD - minX;
+ if (maxX == width)
+ maxX = 0;
+
+ // Y is clamped to [0, 1[ as it latitude and the poles don't wrap to each other.
+ if (y >= 1.0)
+ y = lessThanOneDouble;
+ else if (y < 0.0)
+ y = 0.0;
+ double centerYD = y * height;
+ minY = (int)centerYD;
+ maxY = minY + 1;
+ midY = centerYD - minY;
+ if (maxY == height)
+ maxY = height - 1;
+ }
- public static CBAttributeMapSO.MapAttribute GetAttInt(CBAttributeMapSO biomeMap, int bodyIndex, double lat, double lon)
+ ///
+ /// Reimplementation of the stock biome sampler (CBAttributeMapSO.GetAtt()), including all the stock sampling stuff handling incorrect pixel colors in the base texture.
+ ///
+ private static unsafe int GetBiomeIndexStockBilinearSampling(RGBA32[] biomeColors, RGBA32 c00, RGBA32 c10, RGBA32 c01, RGBA32 c11, double midX, double midY, int nonExactThreshold)
{
- //if (biomeMap.exactSearch || biomeMap.nonExactThreshold != -1f)
- //{
- // return biomeMap.GetAtt(lat, lon);
- //}
+ int biomeCount = biomeColors.Length;
+ bool flag = true; // I still don't understand the logic behind this...
+ bool* notNear = stackalloc bool[biomeCount];
- lon -= HalfPI;
- if (lon < 0.0)
- lon += DoublePI;
- lon %= DoublePI;
- double x = 1.0 - lon * InverseDoublePI;
- double y = lat * InversePI + 0.5;
+ // note : iterating in reverse order from stock, but shouldn't matter here
+ for (int i = biomeCount; i-- > 0;)
+ {
+ RGBA32 biomeColor = biomeColors[i];
+ if (biomeColor != c00 && biomeColor != c10 && biomeColor != c01 && biomeColor != c11)
+ {
+ notNear[i] = true;
+ }
+ else
+ {
+ notNear[i] = false;
+ flag = false;
+ }
+ }
- biomeMap.ConstructBilinearCoords(x, y);
- RGBA32 c0 = GetColorRGBA32(biomeMap, biomeMap.minX, biomeMap.minY);
- RGBA32 c1 = GetColorRGBA32(biomeMap, biomeMap.maxX, biomeMap.minY);
- RGBA32 c2 = GetColorRGBA32(biomeMap, biomeMap.minX, biomeMap.maxY);
- RGBA32 c3 = GetColorRGBA32(biomeMap, biomeMap.maxX, biomeMap.maxY);
+ RGBA32 bilinearSample = BilinearSample(c00, c10, c01, c11, midX, midY);
- RGBA32 pixelColor;
+ int biomeIndex = 0;
+ int bestMag = int.MaxValue;
- if (c0 == c1 && c0 == c2 && c0 == c3)
+ if (flag)
{
- pixelColor = c0;
- }
- else
- {
- Color cf0 = c0;
- Color cf1 = c1;
- Color cf2 = c2;
- Color cf3 = c3;
-
- Color lerpColor = BilinearColor(cf0, cf1, cf2, cf3, biomeMap.midX, biomeMap.midY);
- pixelColor = c0;
- float maxMag = RGBADiffSqrMag(cf0, lerpColor);
- float mag = RGBADiffSqrMag(cf1, lerpColor);
- if (mag < maxMag)
+ for (int i = biomeCount; i-- > 0;)
{
- maxMag = mag;
- pixelColor = c1;
+ int mag = RGBEuclideanDiff(bilinearSample, biomeColors[i]);
+ if (mag < bestMag && mag < nonExactThreshold)
+ {
+ biomeIndex = i;
+ bestMag = mag;
+ }
}
- mag = RGBADiffSqrMag(cf2, lerpColor);
- if (mag < maxMag)
- {
- maxMag = mag;
- pixelColor = c2;
- }
+ return biomeIndex;
+ }
- // There is a bug in the stock method, where it doesn't check the fourth color...
- //mag = RGBADiffSqrMag(cf3, lerpColor);
- //if (mag < maxMag)
- //{
- // pixelColor = c3;
- //}
+ RGBA32 bestSample = c00;
+ bestMag = RGBEuclideanDiff(c00, bilinearSample);
+ int sampleMag = RGBEuclideanDiff(c10, bilinearSample);
+ if (sampleMag < bestMag)
+ {
+ bestMag = sampleMag;
+ bestSample = c10;
}
- RGBA32[] biomeColors = biomeColorsByBody[bodyIndex];
- int length = biomeColors.Length;
- for (int i = 0; i < length; i++)
+ sampleMag = RGBEuclideanDiff(c01, bilinearSample);
+ if (sampleMag < bestMag)
{
- if (biomeColors[i] == pixelColor)
- {
- return biomeMap.Attributes[i];
- }
+ bestMag = sampleMag;
+ bestSample = c01;
+ }
+
+ sampleMag = RGBEuclideanDiff(c11, bilinearSample);
+ if (sampleMag < bestMag)
+ {
+ bestSample = c11;
}
- // fallback path if the color doesn't match exactly for some reason, this is the default stock code
- CBAttributeMapSO.MapAttribute result = biomeMap.Attributes[0];
- float maxColorMag = float.MaxValue;
- for (int i = 0; i < length; i++)
+ bestMag = int.MaxValue;
+ for (int i = biomeCount; i-- > 0;)
{
- float colorMag = EuclideanColorDiff(pixelColor, biomeColors[i]);
- if (colorMag < maxColorMag)
+ if (notNear[i])
+ continue;
+
+ int mag = RGBEuclideanDiff(bestSample, biomeColors[i]);
+ if (mag < bestMag && mag < nonExactThreshold)
{
- result = biomeMap.Attributes[i];
- maxColorMag = colorMag;
+ biomeIndex = i;
+ bestMag = mag;
}
}
- return result;
+ return biomeIndex;
}
- private static Color BilinearColor(Color col0, Color col1, Color col2, Color col3, double midX, double midY)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetIntNonExactThreshold(float nonExactThreshold)
{
- double r0 = col0.r + (col1.r - col0.r) * midX;
- double r1 = col2.r + (col3.r - col2.r) * midX;
- double r = r0 + (r1 - r0) * midY;
-
- double g0 = col0.g + (col1.g - col0.g) * midX;
- double g1 = col2.g + (col3.g - col2.g) * midX;
- double g = g0 + (g1 - g0) * midY;
-
- double b0 = col0.b + (col1.b - col0.b) * midX;
- double b1 = col2.b + (col3.b - col2.b) * midX;
- double b = b0 + (b1 - b0) * midY;
-
- //double a0 = col0.a + (col1.a - col0.a) * midX;
- //double a1 = col2.a + (col3.a - col2.a) * midX;
- //double a = a0 + (a1 - a0) * midY;
+ return nonExactThreshold < 0f ? int.MaxValue : (int)(nonExactThreshold * (255 * 255));
+ }
- return new Color((float)r, (float)g, (float)b, 1f);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static RGBA32 GetRGB3BppAtTextureCoords(int x, int y, byte[] rgbData, int rowWidth)
+ {
+ int index = x * 3 + y * rowWidth;
+ return new RGBA32(rgbData[index], rgbData[index + 1], rgbData[index + 2]);
}
- private static float EuclideanColorDiff(Color colA, Color colB)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static RGBA32 GetRGBA32AtTextureCoords(int x, int y, Color32[] colorData, int width)
{
- float r = colA.r - colB.r;
- float g = colA.g - colB.g;
- float b = colA.b - colB.b;
- return r * r + g * g + b * b;
+ Color32 color32 = colorData[x + y * width];
+ color32.a = 255;
+ return color32;
}
- private static float RGBADiffSqrMag(Color colA, Color colB)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int RGBEuclideanDiff(RGBA32 c1, RGBA32 c2)
{
- float r = colA.r - colB.r;
- float g = colA.g - colB.g;
- float b = colA.b - colB.b;
- float a = colA.a - colB.a;
- return r * r + g * g + b * b + a * a;
+ int r = c1.r - c2.r;
+ int g = c1.g - c2.g;
+ int b = c1.b - c2.b;
+ return r * r + g * g + b * b;
}
- private static RGBA32 BilinearRGBA32(RGBA32 col0, RGBA32 col1, RGBA32 col2, RGBA32 col3, double midX, double midY)
+ private static RGBA32 BilinearSample(RGBA32 c00, RGBA32 c10, RGBA32 c01, RGBA32 c11, double midX, double midY)
{
- double r0 = col0.r + (col1.r - col0.r) * midX;
- double r1 = col2.r + (col3.r - col2.r) * midX;
+ double r0 = c00.r + (c10.r - c00.r) * midX;
+ double r1 = c01.r + (c11.r - c01.r) * midX;
double r = r0 + (r1 - r0) * midY;
- double g0 = col0.g + (col1.g - col0.g) * midX;
- double g1 = col2.g + (col3.g - col2.g) * midX;
+ double g0 = c00.g + (c10.g - c00.g) * midX;
+ double g1 = c01.g + (c11.g - c01.g) * midX;
double g = g0 + (g1 - g0) * midY;
- double b0 = col0.b + (col1.b - col0.b) * midX;
- double b1 = col2.b + (col3.b - col2.b) * midX;
+ double b0 = c00.b + (c10.b - c00.b) * midX;
+ double b1 = c01.b + (c11.b - c01.b) * midX;
double b = b0 + (b1 - b0) * midY;
- //double a0 = col0.a + (col1.a - col0.a) * midX;
- //double a1 = col2.a + (col3.a - col2.a) * midX;
- //double a = a0 + (a1 - a0) * midY;
-
- return new RGBA32((byte)r, (byte)g, (byte)b, 255);
+ return new RGBA32((byte)Math.Round(r), (byte)Math.Round(g), (byte)Math.Round(b));
}
- private static int RGBA32DiffSqrMag(RGBA32 colA, RGBA32 colB)
- {
- int r = colA.r - colB.r;
- int g = colA.g - colB.g;
- int b = colA.b - colB.b;
- return r * r + g * g + b * b;
- }
+ public override Texture2D CompileToTexture() => CompileRGB();
- private static RGBA32 GetColorRGBA32(CBAttributeMapSO biomeMap, int x, int y)
+ public override Texture2D CompileRGB()
{
- int index = biomeMap.PixelIndex(x, y);
- byte[] data = biomeMap._data;
-
- switch (biomeMap._bpp)
+ Texture2D texture2D = new Texture2D(_width, _height, TextureFormat.RGB24, mipChain: false);
+ NativeArray textureData = texture2D.GetRawTextureData();
+ for (int i = _data.Length; i-- > 0;)
{
- case 3:
- return new RGBA32(data[index], data[index + 1], data[index + 2], 255);
- case 4:
- return new RGBA32(data[index], data[index + 1], data[index + 2], data[index + 3]);
- case 2:
- {
- byte rgb = data[index];
- return new RGBA32(rgb, rgb, rgb, data[index + 1]);
- }
- default:
- {
- byte rgb = data[index];
- return new RGBA32(rgb, rgb, rgb, 255);
- }
+ Color pixelColor = Attributes[_data[i]].mapColor;
+ int texIndex = i * 3;
+ textureData[texIndex] = (byte)Math.Round(pixelColor.r * 255f);
+ textureData[texIndex + 1] = (byte)Math.Round(pixelColor.g * 255f);
+ textureData[texIndex + 2] = (byte)Math.Round(pixelColor.b * 255f);
}
+
+ texture2D.Apply(updateMipmaps: false, makeNoLongerReadable: true);
+ return texture2D;
}
- [StructLayout(LayoutKind.Explicit)]
- private struct RGBA32
+ public override Texture2D CompileRGBA()
{
- private const float Byte2Float = 0.003921569f;
+ Texture2D texture2D = new Texture2D(_width, _height, TextureFormat.RGBA32, mipChain: false);
+ NativeArray textureData = texture2D.GetRawTextureData();
+ for (int i = _data.Length; i-- > 0;)
+ {
+ Color pixelColor = Attributes[_data[i]].mapColor;
+ textureData[i] = new Color32(
+ (byte)Math.Round(pixelColor.r * 255f),
+ (byte)Math.Round(pixelColor.g * 255f),
+ (byte)Math.Round(pixelColor.b * 255f),
+ 255);
+ }
- [FieldOffset(0)]
- public int rgba;
+ texture2D.Apply(updateMipmaps: false, makeNoLongerReadable: true);
+ return texture2D;
+ }
- [FieldOffset(0)]
- public byte r;
+ public override Texture2D CompileGreyscale() => throw new NotImplementedException("Can't create Greyscale texture from a biome map");
- [FieldOffset(1)]
- public byte g;
+ public override Texture2D CompileHeightAlpha() => throw new NotImplementedException("Can't create HeightAlpha texture from a biome map");
- [FieldOffset(2)]
- public byte b;
+ ///
+ /// RGB24 color with identical layout to Color32 with FieldOffset tricks to provide fast equality comparison.
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ private struct RGBA32
+ {
+ [FieldOffset(0)] public byte r;
+ [FieldOffset(1)] public byte g;
+ [FieldOffset(2)] public byte b;
+ [FieldOffset(3)] private byte a;
- [FieldOffset(3)]
- public byte a;
+ [FieldOffset(0)] private int rgba;
- public RGBA32(byte r, byte g, byte b, byte a)
+ public RGBA32(byte r, byte g, byte b)
{
rgba = 0;
this.r = r;
this.g = g;
this.b = b;
- this.a = a;
+ a = 255;
}
+ public void ClearAlpha() => a = 255;
+
public static implicit operator RGBA32(Color c)
{
- return new RGBA32((byte)(c.r * 255f), (byte)(c.g * 255f), (byte)(c.b * 255f), (byte)(c.a * 255f));
+ return new RGBA32((byte)Math.Round(c.r * 255f), (byte)Math.Round(c.g * 255f), (byte)Math.Round(c.b * 255f));
}
- public static implicit operator Color(RGBA32 c)
+ public static unsafe implicit operator RGBA32(Color32 c)
{
- return new Color(c.r * Byte2Float, c.g * Byte2Float, c.b * Byte2Float, c.a * Byte2Float);
+ return *(RGBA32*)&c;
}
- public bool Equals(RGBA32 other) => rgba == other.rgba;
- public override bool Equals(object obj) => obj is RGBA32 other && Equals(other);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(RGBA32 lhs, RGBA32 rhs) => lhs.rgba == rhs.rgba;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(RGBA32 lhs, RGBA32 rhs) => lhs.rgba != rhs.rgba;
+
+ public bool Equals(RGBA32 other) => rgba == other.rgba;
+ public override bool Equals(object obj) => obj is RGBA32 other && rgba == other.rgba;
public override int GetHashCode() => rgba;
}
}
+
+ ///
+ /// Convert all biome maps on the fly after system spawn, before main menu is loaded.
+ ///
+ [KSPAddon(KSPAddon.Startup.PSystemSpawn, false)]
+ internal class BiomeMapOptimizer : MonoBehaviour
+ {
+#if BIOME_FLIGHTDEBUG
+ public static CBAttributeMapSO[] stockBiomeMaps;
+#endif
+
+ void OnDestroy()
+ {
+ Stopwatch watch = new Stopwatch();
+ List optimizedBodies = new List(FlightGlobals.Bodies.Count);
+ long oldMapsBytes = 0;
+ long newMapsBytes = 0;
+
+#if BIOME_FLIGHTDEBUG
+ stockBiomeMaps = new CBAttributeMapSO[FlightGlobals.Bodies.Count];
+#endif
+ foreach (CelestialBody body in FlightGlobals.Bodies)
+ {
+ if (body.BiomeMap == null)
+ continue;
+
+ CBAttributeMapSO stockMap = body.BiomeMap;
+ if (stockMap.GetType() != typeof(CBAttributeMapSO))
+ continue;
+
+ watch.Start();
+ KSPCFFastBiomeMap fastBiomeMap = ScriptableObject.CreateInstance();
+ if (!fastBiomeMap.CopyFromMap(stockMap))
+ {
+ Debug.LogWarning($"[KSPCF/FastBiomeMap] Unable to optimize biome map for {body.name}, {stockMap._bpp} Bpp maps aren't supported");
+ Destroy(fastBiomeMap);
+ continue;
+ }
+
+ body.BiomeMap = fastBiomeMap;
+
+ optimizedBodies.Add(body.name);
+ oldMapsBytes += stockMap.Size;
+ newMapsBytes += fastBiomeMap.Size;
+ watch.Stop();
+
+#if BIOME_CONVERSION_VALIDATE
+ Validate(body, stockMap, fastBiomeMap);
+#endif
+#if BIOME_FLIGHTDEBUG
+ stockBiomeMaps[body.flightGlobalsIndex] = stockMap;
+#endif
+ Destroy(stockMap);
+ }
+
+ if (optimizedBodies.Count > 0)
+ {
+ Debug.Log($"[KSPCF/FastBiomeMap] Optimized {optimizedBodies.Count} biome maps in {watch.Elapsed.TotalSeconds:F3}s, old size: {StaticHelpers.HumanReadableBytes(oldMapsBytes)}, new size: {StaticHelpers.HumanReadableBytes(newMapsBytes)}\nOptimized bodies : {string.Join(", ", optimizedBodies)}");
+ }
+ }
+
+#if BIOME_CONVERSION_VALIDATE
+ void Validate(CelestialBody body, CBAttributeMapSO stockMap, KSPCFFastBiomeMap fastBiomeMap)
+ {
+ Random.InitState(0);
+ int sampleCount = 1000000;
+ Vector2d[] coords = new Vector2d[sampleCount];
+ for (int i = 0; i < sampleCount; i++)
+ {
+ double lat = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLat(Random.Range(-90f, 90f)));
+ double lon = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLon(Random.Range(-180f, 180f)));
+ coords[i] = new Vector2d(lat, lon);
+ }
+
+ CBAttributeMapSO.MapAttribute[] stockBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
+ Stopwatch stockWatch = new Stopwatch();
+
+ stockWatch.Start();
+ for (int i = 0; i < sampleCount; i++)
+ {
+ Vector2d latLon = coords[i];
+ stockBiomes[i] = stockMap.GetAtt(latLon.x, latLon.y);
+ }
+ stockWatch.Stop();
+
+ CBAttributeMapSO.MapAttribute[] cfBiomes = new CBAttributeMapSO.MapAttribute[sampleCount];
+
+ Stopwatch cfWatch = new Stopwatch();
+ cfWatch.Start();
+ for (int i = 0; i < sampleCount; i++)
+ {
+ Vector2d latLon = coords[i];
+ cfBiomes[i] = fastBiomeMap.GetAtt(latLon.x, latLon.y);
+ }
+ cfWatch.Stop();
+
+ double ratio = stockWatch.Elapsed.TotalMilliseconds / cfWatch.Elapsed.TotalMilliseconds;
+ int mismatchCount = 0;
+ for (int i = 0; i < sampleCount; i++)
+ {
+ if (stockBiomes[i] != cfBiomes[i])
+ {
+ mismatchCount++;
+#if DEBUG_BIOME_MISMATCH
+ Vector2d latLon = coords[i];
+ Debug.Log($"[KSPCF/FastBiomeMap] Biome mismatch on {body.name,10}, lat/lon: {latLon.x * Mathf.Rad2Deg,7:F2} / {latLon.y * Mathf.Rad2Deg,7:F2}, {stockBiomes[i].name,15} / {cfBiomes[i].name,15} (stock/fast)");
+#endif
+ }
+ }
+ Debug.Log($"[KSPCF/FastBiomeMap] Sampling {sampleCount} biomes on {body.name,10} : {cfWatch.Elapsed.TotalMilliseconds,8:F2}ms vs {stockWatch.Elapsed.TotalMilliseconds,8:F2}ms, ratio: {ratio,4:F1}, samples mismatchs :{mismatchCount,6} ({(mismatchCount / (float)sampleCount),6:P3})");
+ }
+#endif
+
+ }
+
+#if BIOME_FLIGHTDEBUG
+
+ [KSPAddon(KSPAddon.Startup.Flight, false)]
+ public class CheckBiome : MonoBehaviour
+ {
+ private static Vector3 defaultScale = new Vector3(75f, 75f, 75f);
+ const int count = 50;
+ GameObject[] spheres = new GameObject[count * count];
+ Material[] sphereMaterials = new Material[count * count];
+
+ private void Start()
+ {
+ for (int i = 0; i < spheres.Length; i++)
+ {
+ GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
+ sphere.transform.localScale = defaultScale;
+ Destroy(sphere.GetComponent());
+ spheres[i] = sphere;
+ sphereMaterials[i] = sphere.GetComponent().material;
+ }
+ }
+
+ private void Update()
+ {
+ if (FlightGlobals.ActiveVessel == null || FlightGlobals.currentMainBody == null)
+ return;
+
+ Vector3 vesselPos = FlightGlobals.ActiveVessel.transform.position;
+ Vector3 down = (FlightGlobals.currentMainBody.position - vesselPos).normalized;
+ Vector3 up = -down;
+
+ float du = Vector3.Dot(up, Vector3.up);
+ float df = Vector3.Dot(up, Vector3.forward);
+ Vector3 v1 = Mathf.Abs(du) < Mathf.Abs(df) ? Vector3.up : Vector3.forward;
+ Vector3 forward = Vector3.Cross(v1, up).normalized;
+ Vector3 right = Vector3.Cross(up, forward);
+
+ Vector3 upOrigin = up * 500f;
+
+ LayerMask mask = LayerMask.GetMask("Local Scenery");
+
+ CelestialBody body = FlightGlobals.currentMainBody;
+ CBAttributeMapSO stockMap = BiomeMapOptimizer.stockBiomeMaps[body.flightGlobalsIndex];
+ CBAttributeMapSO fastMap = FlightGlobals.currentMainBody.BiomeMap;
+
+ for (int i = 0; i < count; i++)
+ {
+ for (int j = 0; j < count; j++)
+ {
+ Vector3 origin = vesselPos + upOrigin + (forward * (i - count / 2) * 100f) + (right * (j - count / 2) * 100f);
+ if (Physics.Raycast(origin, down, out RaycastHit hitInfo, 5000f, mask))
+ {
+ body.GetLatLonAlt(origin, out double lat, out double lon, out _);
+ lat = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLat(lat));
+ lon = ResourceUtilities.Deg2Rad(ResourceUtilities.clampLon(lon));
+
+ CBAttributeMapSO.MapAttribute stockBiome = stockMap.GetAtt(lat, lon);
+ CBAttributeMapSO.MapAttribute fastBiome = fastMap.GetAtt(lat, lon);
+
+ int index = i * count + j;
+
+ sphereMaterials[index].color = fastBiome.mapColor;
+
+ Transform transform = spheres[index].transform;
+ transform.position = hitInfo.point;
+ if (stockBiome == fastBiome)
+ transform.localScale = defaultScale;
+ else
+ transform.localScale = defaultScale * 2f;
+ }
+ }
+ }
+ }
+ }
+
+#endif
}
From 97466f5a7acae2322e1b72545e2270db70e42465 Mon Sep 17 00:00:00 2001
From: gotmachine <24925209+gotmachine@users.noreply.github.com>
Date: Mon, 5 Jun 2023 12:07:41 +0200
Subject: [PATCH 3/4] Comment typo
---
KSPCommunityFixes/Performance/FastBiomeQuery.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/KSPCommunityFixes/Performance/FastBiomeQuery.cs b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
index 7274db51..5b89d596 100644
--- a/KSPCommunityFixes/Performance/FastBiomeQuery.cs
+++ b/KSPCommunityFixes/Performance/FastBiomeQuery.cs
@@ -23,7 +23,7 @@ namespace KSPCommunityFixes.Performance
/// calling the GetAtt() method.
/// Notable changes are :
/// - Storing directly the Attribute index in the byte array (instead of a color), reducing memory usage
- /// to 8bpp instead of 24bpp, and making the array a lookup table instead of having to compare colors.
+ /// to 8Bpp instead of 24Bpp, and making the array a lookup table instead of having to compare colors.
/// - Fixed a bug in the stock bilinear interpolation, causing incorrect results in a specific direction
/// on biome transitions.
///
From ffa2b9ce782a5dd6e2f40811f08271fb5fa88779 Mon Sep 17 00:00:00 2001
From: gotmachine <24925209+gotmachine@users.noreply.github.com>
Date: Mon, 16 Oct 2023 08:35:11 +0200
Subject: [PATCH 4/4] Added missing comment
---
KSPCommunityFixes/Library/StaticHelpers.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs
index 55dc346e..74e983b8 100644
--- a/KSPCommunityFixes/Library/StaticHelpers.cs
+++ b/KSPCommunityFixes/Library/StaticHelpers.cs
@@ -42,7 +42,12 @@ public static string HumanReadableBytes(long bytes)
return readable.ToString("0.### ") + suffix;
}
- // https://github.com/dotnet/runtime/blob/af4efb1936b407ca5f4576e81484cf5687b79a26/src/libraries/System.Private.CoreLib/src/System/Math.cs#L210
+ ///
+ /// Returns the largest value that compares less than a specified value.
+ ///
+ /// The value to decrement.
+ /// The largest value that compares less than x, or NegativeInfinity if x equals NegativeInfinity, or NaN if x equals NaN.
+ /// // https://github.com/dotnet/runtime/blob/af4efb1936b407ca5f4576e81484cf5687b79a26/src/libraries/System.Private.CoreLib/src/System/Math.cs#L210
public static double BitDecrement(double x)
{
long bits = BitConverter.DoubleToInt64Bits(x);