Kaynağa Gözat

TEXnFORMAT combos: implemented, major cleanup

UwU 4 gün önce
ebeveyn
işleme
8e9bbd042f

+ 0 - 29
docs/OS-Waves-rendering-issues.md

@@ -1,29 +0,0 @@
-# OS Waves (3014738359) - Open Rendering Issue
-
-The earlier investigation of this wallpaper listed six differences from the native
-Windows Wallpaper Engine. Script-driven user settings, text objects, the orthographic
-"auto" camera and `autosize` are implemented now, and skipping passthrough images
-without effects matches the real engine (`CImage.cpp`, `sub_140175830`). One item is
-left to confirm on real hardware.
-
-## Audio spectrum response may differ
-
-The pulse effect on the three OS logos (objects 173, 57 and 61) uses:
-- `AUDIOPROCESSING=3`
-- `audioamount=1.0`, `audiobounds=0.5-1.0`, `audioexponent=0.35`
-
-The spectrum these read now follows the real engine's pipeline, taken from `wallpaper64.exe`
-(`Audio/SpectrumAnalyzer` and `Audio/SpectrumProcessor`): same FFT size, band layout, per-group level
-tracking, smoothing and separate left/right channels. Still unverified: a side-by-side comparison with
-Windows playing the same audio, and whether WE's loopback capture and the Linux monitor source both see
-the signal before or after the system volume.
-
-## Reproduction
-
-```bash
-linux-wallpaperengine \
-  --assets-dir ~/.local/share/Steam/steamapps/common/wallpaper_engine/assets \
-  --screen-root DP-2 --screen-root HDMI-A-1 \
-  --fps 60 \
-  ~/.local/share/Steam/steamapps/workshop/content/431960/3014738359
-```

+ 15 - 2
src/WallpaperEngine/Data/Model/Object.h

@@ -239,11 +239,19 @@ public:
     TurbulentVelocityRandomInitializer (
 	UserSettingUniquePtr speedMin, UserSettingUniquePtr speedMax, UserSettingUniquePtr scale,
 	UserSettingUniquePtr offset, UserSettingUniquePtr forward, UserSettingUniquePtr timeScale,
-	UserSettingUniquePtr phaseMin, UserSettingUniquePtr phaseMax, UserSettingUniquePtr right
+	UserSettingUniquePtr phaseMin, UserSettingUniquePtr phaseMax, UserSettingUniquePtr right,
+	UserSettingUniquePtr audioProcessingMode, UserSettingUniquePtr audioProcessingBounds,
+	UserSettingUniquePtr audioProcessingExponent, UserSettingUniquePtr audioProcessingFrequencyStart,
+	UserSettingUniquePtr audioProcessingFrequencyEnd
     ) :
 	speedMin (std::move (speedMin)), speedMax (std::move (speedMax)), scale (std::move (scale)),
 	offset (std::move (offset)), forward (std::move (forward)), timeScale (std::move (timeScale)),
-	phaseMin (std::move (phaseMin)), phaseMax (std::move (phaseMax)), right (std::move (right)) { }
+	phaseMin (std::move (phaseMin)), phaseMax (std::move (phaseMax)), right (std::move (right)),
+	audioProcessingMode (std::move (audioProcessingMode)),
+	audioProcessingBounds (std::move (audioProcessingBounds)),
+	audioProcessingExponent (std::move (audioProcessingExponent)),
+	audioProcessingFrequencyStart (std::move (audioProcessingFrequencyStart)),
+	audioProcessingFrequencyEnd (std::move (audioProcessingFrequencyEnd)) { }
     UserSettingUniquePtr speedMin;
     UserSettingUniquePtr speedMax;
     UserSettingUniquePtr scale;
@@ -253,6 +261,11 @@ public:
     UserSettingUniquePtr phaseMin;
     UserSettingUniquePtr phaseMax;
     UserSettingUniquePtr right;
+    UserSettingUniquePtr audioProcessingMode;
+    UserSettingUniquePtr audioProcessingBounds;
+    UserSettingUniquePtr audioProcessingExponent;
+    UserSettingUniquePtr audioProcessingFrequencyStart;
+    UserSettingUniquePtr audioProcessingFrequencyEnd;
 };
 
 class MapSequenceAroundControlPointInitializer : public ParticleInitializerBase {

+ 5 - 2
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -280,7 +280,6 @@ ImageEffectPassOverrideUniquePtr ObjectParser::parseEffectPass (const JSON& it,
     const auto& constants = it.optional ("constantshadervalues");
     const auto& usertextures = it.optional ("usertextures");
 
-    // TODO: parse constant shader values and find refs?
     return std::make_unique<ImageEffectPassOverride> (ImageEffectPassOverride {
 	.id = it.optional<int> ("id", -1),
 	.combos = combos.has_value () ? parseComboMap (combos.value ()) : ComboMap {},
@@ -701,7 +700,11 @@ ParticleInitializerUniquePtr ObjectParser::parseParticleInitializer (const JSON&
 	    it.user ("scale", properties, 1.0f), it.user ("offset", properties, 0.0f),
 	    it.user ("forward", properties, glm::vec3 (0.0f, 1.0f, 0.0f)), it.user ("timescale", properties, 1.0f),
 	    it.user ("phasemin", properties, 0.0f), it.user ("phasemax", properties, 0.1f),
-	    it.user ("right", properties, glm::vec3 (0.0f, 0.0f, 1.0f))
+	    it.user ("right", properties, glm::vec3 (0.0f, 0.0f, 1.0f)), it.user ("audioprocessingmode", properties, 0),
+	    it.user ("audioprocessingbounds", properties, glm::vec2 (0.8f, 1.0f)),
+	    it.user ("audioprocessingexponent", properties, 2.0f),
+	    it.user ("audioprocessingfrequencystart", properties, 0),
+	    it.user ("audioprocessingfrequencyend", properties, 1)
 	);
     } else if (name == "mapsequencearoundcontrolpoint") {
 	return std::make_unique<MapSequenceAroundControlPointInitializer> (

+ 1 - 1
src/WallpaperEngine/Render/Camera.cpp

@@ -8,7 +8,7 @@ using namespace WallpaperEngine::Render;
 
 Camera::Camera (Wallpapers::CScene& scene, const SceneData::Camera& camera) :
     m_width (0), m_height (0), m_camera (camera), m_scene (scene) {
-    // TODO: ensure this is only used for non-orthographic cameras, it throws off points otherwise
+    // WE builds this view matrix for orthographic cameras too (sub_140159080)
     this->m_lookat = glm::lookAt (this->getEye (), this->getCenter (), this->getUp ());
 }
 

+ 4 - 150
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -557,8 +557,7 @@ std::optional<size_t> findNextPuppetClipHeader (
 
 // Parses every baked animation clip out of the MDLA section (see docs/rendering/MDL_FILES.md).
 std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
-    const std::vector<char>& data, const BinaryReader& reader, size_t mdlaOffset, uint32_t expectedBoneCount,
-    bool dumpBone29
+    const std::vector<char>& data, const BinaryReader& reader, size_t mdlaOffset, uint32_t expectedBoneCount
 ) {
     reader.base ().seekg (static_cast<std::streamoff> (mdlaOffset), std::ios::beg);
 
@@ -631,25 +630,6 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 		keyframe.scale = { reader.nextFloat (), reader.nextFloat (), reader.nextFloat () };
 		track.push_back (keyframe);
 	    }
-
-	    // TEMP-DIAG: raw keyframe dump for bone 29
-	    if (dumpBone29 && boneIndex == 29 && expectedBoneCount > 29) {
-		float minRotZ = std::numeric_limits<float>::max (), maxRotZ = std::numeric_limits<float>::lowest ();
-		float minPosX = std::numeric_limits<float>::max (), maxPosX = std::numeric_limits<float>::lowest ();
-		for (const auto& kf : track) {
-		    minRotZ = std::min (minRotZ, kf.rotation.z);
-		    maxRotZ = std::max (maxRotZ, kf.rotation.z);
-		    minPosX = std::min (minPosX, kf.position.x);
-		    maxPosX = std::max (maxPosX, kf.position.x);
-		}
-		sLog.out (
-		    "TEMP-DIAG bone29 raw track for clip '", clip.name, "': samples=", track.size (), " rotZ=[",
-		    minRotZ, ",", maxRotZ, "] posX=[", minPosX, ",", maxPosX, "] first3=(",
-		    track.size () > 0 ? track[0].rotation.z : 0.0f, ",", track.size () > 1 ? track[1].rotation.z : 0.0f,
-		    ",", track.size () > 2 ? track[2].rotation.z : 0.0f, ") mid=(",
-		    track.size () > 90 ? track[90].rotation.z : 0.0f, ")"
-		);
-	    }
 	}
 
 	clips.push_back (std::move (clip));
@@ -817,15 +797,6 @@ CImage::ResolvedTransform CImage::resolveTransform (const Object& object) const
 	local.scale.x *= anchorScale.x;
 	local.scale.y *= anchorScale.y;
 	resolved = { local.origin, local.scale * resolved.scale, local.angle + anchorAngle, meshPivotAngle };
-
-	if (chain[i]->id == 134 && !this->m_finalOriginLogged.contains (chain[i]->id)) {
-	    this->m_finalOriginLogged.insert (chain[i]->id);
-	    sLog.out (
-		"TEMP-DIAG final resolved origin for ", chain[i]->name, " (", chain[i]->id, "): anchorOrigin=(",
-		anchorOrigin.x, ",", anchorOrigin.y, ") offset=(", offset.x, ",", offset.y, ") finalOrigin=(",
-		resolved.origin.x, ",", resolved.origin.y, ")"
-	    );
-	}
     }
 
     return resolved;
@@ -917,7 +888,8 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     // register both FBOs into the scene
     std::ostringstream nameA, nameB;
 
-    // TODO: determine when _rt_imageLayerComposite and _rt_imageLayerAlbedo is used
+    // WE also renders layers with LIGHTING or REFLECTION unlit into _rt_imageLayerAlbedo_<id> when they have
+    // offscreen passes and applies the lighting last (sub_1401914B0), not needed while lighting is a stub
     nameA << "_rt_imageLayerComposite_" << this->getImage ().id << "_a";
     nameB << "_rt_imageLayerComposite_" << this->getImage ().id << "_b";
 
@@ -1139,8 +1111,6 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 	std::copy (data.begin (), data.end (), meshBuffer.get ());
 	const BinaryReader reader (std::make_shared<MemoryStream> (std::move (meshBuffer), data.size ()));
 
-	const bool isDiagTarget
-	    = this->getImage ().name == "bodyhairkochuru" || this->getImage ().name == "spiritblossomahribase";
 	const auto layout = resolvePuppetVertexLayout (reader, markerSize, mdlsOffset, meshHeaderSize);
 	if (!layout.has_value ()) {
 	    sLog.error ("Could not find a usable MDLV mesh block in ", *this->getImage ().model->puppet);
@@ -1168,54 +1138,6 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 
 	this->m_puppetIndexCount = static_cast<GLsizei> (mesh->indices.size ());
 
-	if (isDiagTarget) {
-	    this->m_puppetTexCoordData = mesh->texcoords;
-	    this->m_puppetIndicesData = mesh->indices;
-	}
-
-	// TEMP-DIAG: per-triangle UV-vs-position area ratio
-	if (isDiagTarget) {
-	    std::vector<double> ratios;
-	    ratios.reserve (mesh->indices.size () / 3);
-	    for (size_t t = 0; t + 2 < mesh->indices.size (); t += 3) {
-		const auto i0 = mesh->indices[t], i1 = mesh->indices[t + 1], i2 = mesh->indices[t + 2];
-		const glm::vec2 p0 (mesh->positions[i0 * 3], mesh->positions[i0 * 3 + 1]);
-		const glm::vec2 p1 (mesh->positions[i1 * 3], mesh->positions[i1 * 3 + 1]);
-		const glm::vec2 p2 (mesh->positions[i2 * 3], mesh->positions[i2 * 3 + 1]);
-		const glm::vec2 u0 (mesh->texcoords[i0 * 2], mesh->texcoords[i0 * 2 + 1]);
-		const glm::vec2 u1 (mesh->texcoords[i1 * 2], mesh->texcoords[i1 * 2 + 1]);
-		const glm::vec2 u2 (mesh->texcoords[i2 * 2], mesh->texcoords[i2 * 2 + 1]);
-		const double posArea = std::abs ((p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y));
-		const double uvArea = std::abs ((u1.x - u0.x) * (u2.y - u0.y) - (u2.x - u0.x) * (u1.y - u0.y));
-		if (posArea <= 1e-6) {
-		    continue;
-		}
-		ratios.push_back (uvArea / posArea);
-	    }
-	    std::vector<double> sorted = ratios;
-	    std::sort (sorted.begin (), sorted.end ());
-	    const double median = sorted.empty () ? 0.0 : sorted[sorted.size () / 2];
-	    sLog.out (
-		"TEMP-DIAG uv/pos area ratio for ", this->getImage ().name, ": triCount=", ratios.size (), " median=", median
-	    );
-	    for (size_t t = 0; t + 2 < mesh->indices.size (); t += 3) {
-		const size_t triIndex = t / 3;
-		if (triIndex >= ratios.size ()) {
-		    break;
-		}
-		const double ratio = ratios[triIndex];
-		if (median > 0.0 && (ratio > median * 20.0 || ratio < median / 20.0)) {
-		    const auto i0 = mesh->indices[t], i1 = mesh->indices[t + 1], i2 = mesh->indices[t + 2];
-		    sLog.out (
-			"TEMP-DIAG   outlier tri=", triIndex, " ratio=", ratio, " verts=(", i0, ",", i1, ",", i2,
-			") uv0=(", mesh->texcoords[i0 * 2], ",", mesh->texcoords[i0 * 2 + 1], ") uv1=(",
-			mesh->texcoords[i1 * 2], ",", mesh->texcoords[i1 * 2 + 1], ") uv2=(", mesh->texcoords[i2 * 2],
-			",", mesh->texcoords[i2 * 2 + 1], ")"
-		    );
-		}
-	    }
-	}
-
 	sLog.out (
 	    "Loaded puppet mesh ", *this->getImage ().model->puppet, " version=", puppetVersion, " stride=",
 	    layout->vertexStride, " vertices=", this->m_puppetRawPositions.size () / 3, " indices=", this->m_puppetIndexCount
@@ -1279,7 +1201,7 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		std::vector<PuppetAnimationClip> clips;
 		if (mdlaOffsetLooksValid) {
 		    clips = parsePuppetAnimationClips (
-			data, reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ()), isDiagTarget
+			data, reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ())
 		    );
 		} else {
 		    sLog.error (
@@ -1496,7 +1418,6 @@ void CImage::updatePuppetSkinning () {
 	bool positionBased = false;
 	glm::vec3 rotation (0.0f);
 	glm::vec3 scale (1.0f);
-	bool anyTrack = false;
 
 	// each layer contributes a blend-weighted delta from the shared baseline (bind position, zero
 	// rotation, unit scale) rather than replacing it outright
@@ -1505,7 +1426,6 @@ void CImage::updatePuppetSkinning () {
 		continue;
 	    }
 
-	    anyTrack = true;
 	    const auto& track = sample.clip->boneTracks[i];
 	    const glm::vec3 trackPosition = lerp (track[sample.frame0].position, track[sample.frame1].position, sample.alpha);
 	    const glm::vec3 trackRotation = lerp (track[sample.frame0].rotation, track[sample.frame1].rotation, sample.alpha);
@@ -1529,17 +1449,6 @@ void CImage::updatePuppetSkinning () {
 	local = glm::scale (local, scale);
 
 	animatedLocals[i] = local;
-
-	// TEMP-DIAG: bones 29/30 logged every frame
-	if ((i == 29 || i == 30) && this->getImage ().name == "bodyhairkochuru") {
-	    sLog.out (
-		"TEMP-DIAG bone anim for ", this->getImage ().name, " (", this->getId (), ") i=", i, " parent=",
-		bone.parent, " bindLocalPos=(", bone.bindLocal[3].x, ",", bone.bindLocal[3].y, ") animatedPos=(",
-		position.x, ",", position.y, ",", position.z, ") rotationDeg=(", glm::degrees (rotation.x), ",",
-		glm::degrees (rotation.y), ",", glm::degrees (rotation.z), ") scale=(", scale.x, ",", scale.y, ",",
-		scale.z, ") hasTrack=", anyTrack, " activeLayers=", samples.size (), " time=", g_Time
-	    );
-	}
     }
 
     const std::vector<glm::mat4> worldAnimated = composeBoneWorldTransforms (animatedParents, animatedLocals);
@@ -1585,61 +1494,6 @@ void CImage::updatePuppetSkinning () {
 	this->m_puppetSkinnedPositions[v * 3 + 2] = skinned.z;
     }
 
-    // TEMP-DIAG: triangles that overlap on screen post-skinning despite sampling distant UV regions
-    if (!this->m_puppetOverlapDiagLogged && !this->m_puppetIndicesData.empty ()
-        && (this->getImage ().name == "bodyhairkochuru" || this->getImage ().name == "spiritblossomahribase")) {
-	this->m_puppetOverlapDiagLogged = true;
-
-	struct TriBounds {
-	    glm::vec2 min, max, uvCentroid;
-	};
-	std::vector<TriBounds> tris;
-	const size_t triCount = this->m_puppetIndicesData.size () / 3;
-	tris.reserve (triCount);
-
-	for (size_t t = 0; t < triCount; t++) {
-	    const auto i0 = this->m_puppetIndicesData[t * 3];
-	    const auto i1 = this->m_puppetIndicesData[t * 3 + 1];
-	    const auto i2 = this->m_puppetIndicesData[t * 3 + 2];
-	    const glm::vec2 p0 (this->m_puppetSkinnedPositions[i0 * 3], this->m_puppetSkinnedPositions[i0 * 3 + 1]);
-	    const glm::vec2 p1 (this->m_puppetSkinnedPositions[i1 * 3], this->m_puppetSkinnedPositions[i1 * 3 + 1]);
-	    const glm::vec2 p2 (this->m_puppetSkinnedPositions[i2 * 3], this->m_puppetSkinnedPositions[i2 * 3 + 1]);
-	    const glm::vec2 uv0 (this->m_puppetTexCoordData[i0 * 2], this->m_puppetTexCoordData[i0 * 2 + 1]);
-	    const glm::vec2 uv1 (this->m_puppetTexCoordData[i1 * 2], this->m_puppetTexCoordData[i1 * 2 + 1]);
-	    const glm::vec2 uv2 (this->m_puppetTexCoordData[i2 * 2], this->m_puppetTexCoordData[i2 * 2 + 1]);
-	    tris.push_back (TriBounds {
-		.min = glm::min (p0, glm::min (p1, p2)), .max = glm::max (p0, glm::max (p1, p2)),
-		.uvCentroid = (uv0 + uv1 + uv2) / 3.0f });
-	}
-
-	size_t overlapCount = 0;
-	for (size_t a = 0; a < triCount && overlapCount < 15; a++) {
-	    for (size_t b = a + 1; b < triCount && overlapCount < 15; b++) {
-		const auto& ta = tris[a];
-		const auto& tb = tris[b];
-		const bool boxesOverlap
-		    = ta.min.x <= tb.max.x && ta.max.x >= tb.min.x && ta.min.y <= tb.max.y && ta.max.y >= tb.min.y;
-		if (!boxesOverlap) {
-		    continue;
-		}
-		if (glm::distance (ta.uvCentroid, tb.uvCentroid) < 0.15f) {
-		    continue;
-		}
-		overlapCount++;
-		sLog.out (
-		    "TEMP-DIAG overlap for ", this->getImage ().name, " tri", a, " box=(", ta.min.x, ",", ta.min.y, ")-(",
-		    ta.max.x, ",", ta.max.y, ") uv=(", ta.uvCentroid.x, ",", ta.uvCentroid.y, ")  vs  tri", b, " box=(",
-		    tb.min.x, ",", tb.min.y, ")-(", tb.max.x, ",", tb.max.y, ") uv=(", tb.uvCentroid.x, ",", tb.uvCentroid.y,
-		    ")"
-		);
-	    }
-	}
-	sLog.out (
-	    "TEMP-DIAG overlap scan for ", this->getImage ().name, " done: triCount=", triCount, " overlapsLogged=",
-	    overlapCount
-	);
-    }
-
     this->updatePuppetPositionBuffer (this->m_size);
 }
 

+ 0 - 6
src/WallpaperEngine/Render/Objects/CImage.h

@@ -183,7 +183,6 @@ private:
     mutable bool m_puppetDrawErrorChecked = false;
     bool m_puppetPositionDiagnosticLogged = false;
     mutable std::set<int> m_attachmentDiagnosticLogged = {};
-    mutable std::set<int> m_finalOriginLogged = {};
     std::vector<GLfloat> m_puppetRawPositions = {};
     /** This object's current resolved scale, mirrored here so updatePuppetSkinning() (called after
      *  updateGeometryBuffers() each frame, see render()) can fold it into puppet vertex positions
@@ -202,11 +201,6 @@ private:
     std::vector<PuppetActiveAnimation> m_puppetActiveAnimations = {};
     std::vector<GLfloat> m_puppetSkinnedPositions = {};
 
-    // TEMP-DIAG: CPU-side puppet texcoords/indices, only used by the overlap check
-    std::vector<GLfloat> m_puppetTexCoordData = {};
-    std::vector<GLushort> m_puppetIndicesData = {};
-    bool m_puppetOverlapDiagLogged = false;
-
     std::vector<PuppetAttachmentPoint> m_puppetAttachmentPoints = {};
     /** Per-bone current animated world transform, in the puppet's own local mesh space; starts out equal
      *  to the bind pose and is refreshed every frame by updatePuppetSkinning while animation is active */

+ 13 - 3
src/WallpaperEngine/Render/Objects/CParticle.cpp

@@ -886,9 +886,15 @@ InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const Turbu
     DynamicValue* phaseMaxVal = init.phaseMax->value.get ();
     DynamicValue* rightVal = init.right->value.get ();
     DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
+    DynamicValue* audioModeValue = init.audioProcessingMode->value.get ();
+    DynamicValue* audioBoundsValue = init.audioProcessingBounds->value.get ();
+    DynamicValue* audioExponentValue = init.audioProcessingExponent->value.get ();
+    DynamicValue* audioStartValue = init.audioProcessingFrequencyStart->value.get ();
+    DynamicValue* audioEndValue = init.audioProcessingFrequencyEnd->value.get ();
 
     return [this, speedMin, speedMax, offsetVal, scaleVal, forwardVal, timeScaleVal, phaseMinVal, phaseMaxVal, rightVal,
-	    speedOverride] (ParticleInstance& p) {
+	    speedOverride, audioModeValue, audioBoundsValue, audioExponentValue, audioStartValue,
+	    audioEndValue] (ParticleInstance& p) {
 	glm::vec3 forward = forwardVal->getVec3 ();
 	glm::vec3 right = rightVal->getVec3 ();
 	// Y-flip for coordinate system conversion
@@ -920,8 +926,12 @@ InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const Turbu
 	glm::vec3 noisePos = p.position * 0.1f;
 	noisePos += glm::vec3 (static_cast<float> (m_time) * timeScale);
 
-	// Phase adds per-particle randomization to noise position
-	float phase = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax);
+	// Phase adds per-particle randomization to noise position, WE scales its random range by the audio level
+	const float audio = sampleAudio (
+	    audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
+	    audioStartValue->getInt (), audioEndValue->getInt ()
+	);
+	float phase = phaseMin + WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * (phaseMax - phaseMin) * audio;
 	glm::vec3 samplePos = noisePos + glm::vec3 (phase, phase * 0.7f, phase * 1.3f);
 
 	glm::vec3 result = curlNoise (samplePos);

+ 57 - 1
src/WallpaperEngine/Render/Objects/Effects/CPass.cpp

@@ -781,7 +781,7 @@ void CPass::setupShaders () {
 
     this->m_combos.insert (this->m_pass.combos.begin (), this->m_pass.combos.end ());
 
-    // TODO: the values match the enum - maybe this needs to apply to texture0 for all elements?
+    // particle shaders read TEX0FORMAT without a formatcombo sampler, the other slots are handled below
     if (texture0 != nullptr) {
 	if (texture0->getFormat () == TextureFormat_RG88) {
 	    this->m_combos.insert_or_assign ("TEX0FORMAT", 8);
@@ -818,6 +818,16 @@ void CPass::setupShaders () {
 	overrideTextures, this->m_override.constants
     );
 
+    // samplers marked "formatcombo" get TEX<slot>FORMAT set to their texture's format, like wallpaper64.exe
+    // (sub_14015EC30). Which slots those are is only known once the shader is parsed, so rebuild it when one changed
+    if (this->applyFormatCombos (passTextures, overrideTextures)) {
+	delete this->m_shader;
+	this->m_shader = new Render::Shaders::Shader (
+	    this->m_renderable.getAssetLocator (), shaderName, this->m_combos, this->m_override.combos, passTextures,
+	    overrideTextures, this->m_override.constants
+	);
+    }
+
     auto [vertex, fragment] = Shaders::GLSLContext::get ().toGlsl (this->m_shader->vertex (), this->m_shader->fragment ());
 
     if (shaderName == XRAY_EFFECT_SHADER) {
@@ -887,6 +897,52 @@ void CPass::setupShaders () {
     this->g_Texture0Translation = glGetUniformLocation (this->m_programID, "g_Texture0Translation");
 }
 
+bool CPass::applyFormatCombos (const TextureMap& passTextures, const TextureMap& overrideTextures) {
+    const auto& fragment = this->m_shader->getFragment ();
+    bool changed = false;
+
+    for (const int slot : fragment.getFormatComboSlots ()) {
+	std::shared_ptr<const TextureProvider> texture;
+
+	if (slot == 0) {
+	    texture = this->m_renderable.getTexture ();
+	} else {
+	    std::optional<std::string> name;
+
+	    for (const TextureMap* map : { &overrideTextures, &passTextures, &fragment.getTextures () }) {
+		if (const auto it = map->find (slot); it != map->end () && !it->second.empty ()) {
+		    name = it->second;
+		    break;
+		}
+	    }
+
+	    if (!name.has_value () || name->starts_with ("_rt_") || name->starts_with ("_alias_")) {
+		continue;
+	    }
+
+	    try {
+		texture = this->getContext ().resolveTexture (*name, this->m_renderable.getScene ().getScene ().project);
+	    } catch (const std::exception&) {
+		continue;
+	    }
+	}
+
+	if (texture == nullptr || texture->getFormat () == TextureFormat_UNKNOWN) {
+	    continue;
+	}
+
+	const std::string combo = "TEX" + std::to_string (slot) + "FORMAT";
+	const int format = static_cast<int> (texture->getFormat ());
+
+	if (const auto it = this->m_combos.find (combo); it == this->m_combos.end () || it->second != format) {
+	    this->m_combos.insert_or_assign (combo, format);
+	    changed = true;
+	}
+    }
+
+    return changed;
+}
+
 void CPass::setupAttributes () {
     this->addAttribute ("a_TexCoord", GL_FLOAT, 2, &this->a_TexCoord);
     this->addAttribute ("a_Position", GL_FLOAT, 3, &this->a_Position);

+ 2 - 0
src/WallpaperEngine/Render/Objects/Effects/CPass.h

@@ -133,6 +133,8 @@ private:
 
     static GLuint compileShader (const char* shader, GLuint type);
     void setupShaders ();
+    /** Sets TEX<slot>FORMAT for the shader's formatcombo samplers, true if a combo changed */
+    bool applyFormatCombos (const TextureMap& passTextures, const TextureMap& overrideTextures);
     void setupShaderVariables ();
     /** GL type the linked program declares for a uniform (GL_NONE if it has no such active uniform) */
     [[nodiscard]] GLenum getDeclaredUniformType (const std::string& name) const;

+ 7 - 0
src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp

@@ -930,6 +930,11 @@ void ShaderUnit::parseParameterConfiguration (
 	    this->m_defaultTextures.emplace (index, *textureName);
 	}
 
+	if (const auto formatCombo = data.find ("formatcombo");
+	    formatCombo != data.end () && formatCombo->is_boolean () && formatCombo->get<bool> ()) {
+	    this->m_formatComboSlots.insert (static_cast<int> (index));
+	}
+
 	return;
     } else {
 	sLog.error ("Unknown parameter type: ", type, " for ", name, " in shader ", this->m_file);
@@ -1093,3 +1098,5 @@ const std::string& ShaderUnit::compile () {
 
 const std::vector<Variables::ShaderVariable*>& ShaderUnit::getParameters () const { return this->m_parameters; }
 const TextureMap& ShaderUnit::getTextures () const { return this->m_defaultTextures; }
+
+const std::set<int>& ShaderUnit::getFormatComboSlots () const { return this->m_formatComboSlots; }

+ 4 - 0
src/WallpaperEngine/Render/Shaders/ShaderUnit.h

@@ -2,6 +2,7 @@
 
 #include <map>
 #include <memory>
+#include <set>
 #include <string>
 
 #include "GLSLContext.h"
@@ -34,6 +35,8 @@ public:
 
     [[nodiscard]] const std::vector<Variables::ShaderVariable*>& getParameters () const;
     [[nodiscard]] const TextureMap& getTextures () const;
+    /** Texture slots whose sampler asks for a TEX<slot>FORMAT combo ("formatcombo") */
+    [[nodiscard]] const std::set<int>& getFormatComboSlots () const;
     [[nodiscard]] const ComboMap& getCombos () const;
     /** Combos discovered during preprocessing that weren't in the configured combo list */
     [[nodiscard]] const ComboMap& getDiscoveredCombos () const;
@@ -97,6 +100,7 @@ private:
     const TextureMap& m_overrideTextures;
     /** The default textures to use when a texture is not applied in a given slot */
     TextureMap m_defaultTextures = {};
+    std::set<int> m_formatComboSlots = {};
     const ShaderUnit* m_link;
     const AssetLocator& m_assetLocator;
 };