using UnityEngine; public partial class MeshPainter { // --- Backend selection / switching --- private void SetupBackend(bool gpu, Color32[][] canonical) { // SetupGpu returns false if the shaders are missing and it fell back to CPU; honor // the real outcome so _gpuActive never claims GPU while _gpu is null (would NRE on // the next SwitchBackend / paint). if (gpu) { _gpuActive = SetupGpu(canonical); } else { SetupCpu(canonical); _gpuActive = false; } } /// Switches the backend at runtime, carrying over all painted maps. private void SwitchBackend(bool toGpu) { var canonical = new Color32[MAP_COUNT][]; for (int mi = 0; mi < MAP_COUNT; mi++) canonical[mi] = ReadCanonical(mi); RebuildBackend(toGpu, canonical); Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this); } /// /// Re-seeds the backend from externally supplied canonical maps (load / late-join snapshot / undo), /// rebuilding the island/dilation edge padding for the new content. The input arrays are never mutated /// (both seed paths clone before touching a map). /// /// Fast path: when the backend is already built on this same type + size - the common runtime reseed /// (remote snapshot apply, carry/library load, undo/redo) - the existing RTs/textures are re-filled in /// place instead of a full teardown+rebuild. At 1024² a rebuild frees and re-creates nine ARGBHalf RTs /// (~70 MB of GPU allocation) every call, which stalls the frame and shows up as a hitch on every /// observer when a (freshly painted) avatar spawns. Re-seeding reuses the RTs (identical size/format) /// for a bit-identical result. A pending GPU<->CPU switch or an un-built backend falls through to the /// full rebuild. /// private void ReseedCanonical(Color32[][] canonical) { _backendSeeded = true; // the backend is about to hold real content (fast-path reseed or full rebuild) if (_setupValid) { if (useGpu && _gpuActive && _gpu != null) { ReseedGpuInPlace(canonical); _hasLastScreen = false; return; } if (!useGpu && !_gpuActive && _cpu != null) { ReseedCpuInPlace(canonical); _hasLastScreen = false; return; } } RebuildBackend(useGpu, canonical); } /// /// Materializes a DEFERRED backend's base seed on demand: a remote replica that skipped its Start seed /// (see ) must fill the base BEFORE a replicated stamp lands on it or it is /// captured, so nothing paints over / serves uninitialized pixels. No-op once seeded (the normal case: the /// snapshot apply already re-seeded it). A null canonical resolves each map to the material seed. /// private void EnsureBackendSeeded() { if (!_backendSeeded) ReseedCanonical(null); } /// Tears down the active backend and sets up the requested one on the given maps. private void RebuildBackend(bool gpu, Color32[][] canonical) { if (_gpuActive) TeardownGpu(); else TeardownCpu(); SetupBackend(gpu, canonical); _hasLastScreen = false; } /// Seed source for a map: the supplied canonical data when valid, else the material seed. private Color32[] ResolveSeed(int mi, Color32[][] canonical) { return (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize) ? canonical[mi] : _seedCanonical[mi]; } // Backend-agnostic stroke dispatch (GPU vs CPU), shared by local painting and replayed net stamps. private void BeginBackendStroke() { if (_gpuActive) BeginGpuStroke(); else ResetCpuDirty(); } private void DrawBackendStamp(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n) { if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, n); else PaintStamp3D(pLocal, radiusLocal, u, v, n); } private void EndBackendStroke() { if (_gpuActive) EndGpuStroke(); else EndCpuStroke(); } /// Reads a map from the active backend into canonical (material) layout. private Color32[] ReadCanonical(int mi) { if (_gpuActive) { var gm = _gpu[mi]; // MSO: read the packed final mask RT (R=metallic, G=occlusion, A=smoothness). // Others: the display RT. The mask map is linear data; color maps read sRGB. if (mi == MAP_MSO) return ReadRtPixels(gm.final, linear: true); return ReadRtPixels(gm.display, linear: MapIsLinear(mi)); } return (Color32[])_cpu[mi].pixels.Clone(); } // MSO and fur are DATA maps (raw values, no sRGB): MSO packs metallic/occlusion/smoothness, fur's R is // painted strand length. Storing them linearly keeps the values undistorted for the shaders that read them. private static bool MapIsLinear(int mi) => mi == MAP_MSO || mi == MAP_FUR; // Material slots per map, as pre-hashed IDs (this runs per stroke end - no per-call allocations). // MSO: one mask map drives three URP slots: _MetallicGlossMap (R/A) + _OcclusionMap (G) + // _ParallaxMap (G = occlusion reused as a 0.5-centred height for parallax/relief). // Built lazily on first use (runtime): the *Id fields live in the MeshPainter.cs partial and the // static field-initializer order ACROSS partial files is unspecified - a plain field initializer // here can run first and capture 0 for every id, so HasProperty(0) fails and no map ever binds // (white defaults -> full-glossy + emissive). Deferring to first access sidesteps that ordering. private static int[][] _mapPropIds; private static int[][] MapPropIds => _mapPropIds ??= new[] { new[] { BaseMapId, MainTexId }, // MAP_ALBEDO new[] { MetallicGlossMapId, OcclusionMapId, ParallaxMapId }, // MAP_MSO new[] { EmissionMapId }, // MAP_EM System.Array.Empty(), // MAP_FUR: read by FurShellRenderer, not the base material }; private void AssignMapTexture(int mi, Texture tex) { if (!_materialInstance) return; foreach (int id in MapPropIds[mi]) if (_materialInstance.HasProperty(id)) _materialInstance.SetTexture(id, tex); } /// /// The live fur map (R = painted strand length, linear data), for a sibling FurShellRenderer to sample /// as _FurMap. GPU backend: the display RT (stable - fur skips the dilation ping-pong swap); CPU backend: /// the Texture2D mirror. Null until the backend is set up. /// public Texture FurMapTexture { get { if (!_setupValid) return null; if (_gpuActive) return _gpu != null ? _gpu[MAP_FUR]?.display : null; return _cpu != null ? _cpu[MAP_FUR]?.tex : null; } } /// /// The live albedo map (the painted camo), for a sibling FurShellRenderer to sample as the fur strand /// colour so the fur wears the camo per texel. GPU backend: the current display RT (ping-pongs on the /// dilation swap, so callers must re-fetch each frame); CPU backend: the Texture2D mirror. Null until the /// backend is set up. /// public Texture AlbedoMapTexture { get { if (!_setupValid) return null; if (_gpuActive) return _gpu != null ? _gpu[MAP_ALBEDO]?.display : null; return _cpu != null ? _cpu[MAP_ALBEDO]?.tex : null; } } }