diff --git a/src/app/RuntimeLayerController.h b/src/app/RuntimeLayerController.h index 069af90..cf0ff30 100644 --- a/src/app/RuntimeLayerController.h +++ b/src/app/RuntimeLayerController.h @@ -47,7 +47,7 @@ private: void RequestRuntimeStatePersistence(); void RequestRuntimeStatePersistenceLocked(); std::filesystem::path ResolveRuntimeStatePath() const; - void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId); + void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId, bool preserveExistingRenderArtifact = false); void RetireLayerShaderBuild(const std::string& layerId); void CleanupRetiredShaderBuilds(); void StopAllRuntimeShaderBuilds(); diff --git a/src/app/RuntimeLayerControllerBuild.cpp b/src/app/RuntimeLayerControllerBuild.cpp index 8826f0a..ddd3602 100644 --- a/src/app/RuntimeLayerControllerBuild.cpp +++ b/src/app/RuntimeLayerControllerBuild.cpp @@ -107,7 +107,7 @@ void RuntimeLayerController::RequestRuntimeStatePersistenceLocked() mPersistenceWriter.RequestSave(mRuntimeLayerModel.Snapshot()); } -void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId) +void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId, bool preserveExistingRenderArtifact) { CleanupRetiredShaderBuilds(); RetireLayerShaderBuild(layerId); @@ -115,7 +115,7 @@ void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, c { std::lock_guard lock(mRuntimeLayerMutex); std::string error; - mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error); + mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error, preserveExistingRenderArtifact); } auto bridge = std::make_unique(); diff --git a/src/app/RuntimeLayerControllerControls.cpp b/src/app/RuntimeLayerControllerControls.cpp index 4c93c9f..adaeac0 100644 --- a/src/app/RuntimeLayerControllerControls.cpp +++ b/src/app/RuntimeLayerControllerControls.cpp @@ -154,9 +154,8 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo for (const auto& build : buildsToStart) { Log("runtime-shader", "Reload queued shader rebuild: " + build.first + " shader=" + build.second); - StartLayerShaderBuild(build.first, build.second); + StartLayerShaderBuild(build.first, build.second, true); } - PublishRuntimeRenderLayers(); return { true, std::string() }; } case RuntimeControlCommandType::Unsupported: diff --git a/src/runtime/RuntimeLayerModel.cpp b/src/runtime/RuntimeLayerModel.cpp index 96c44ea..fb58e06 100644 --- a/src/runtime/RuntimeLayerModel.cpp +++ b/src/runtime/RuntimeLayerModel.cpp @@ -402,12 +402,6 @@ bool RuntimeLayerModel::ReloadFromCatalog(const SupportedShaderCatalog& shaderCa layer.packageFingerprint = nextFingerprint; layer.parameterDefinitions = shaderPackage->parameters; layer.parameterValues = std::move(nextValues); - if (layer.renderReady) - { - layer.artifact.parameterValues = layer.parameterValues; - std::string prepareError; - PrepareRuntimeTextTextures(layer.artifact, prepareError); - } buildsToStart.push_back({ layer.id, layer.shaderId }); } @@ -420,7 +414,7 @@ void RuntimeLayerModel::Clear() mLayers.clear(); } -bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error) +bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error, bool preserveExistingRenderArtifact) { Layer* layer = FindLayer(layerId); if (!layer) @@ -431,8 +425,12 @@ bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std:: layer->buildState = RuntimeLayerBuildState::Pending; layer->message = message; - layer->renderReady = false; - layer->artifact = RuntimeShaderArtifact(); + layer->preserveRenderDuringBuild = preserveExistingRenderArtifact && layer->renderReady; + if (!layer->preserveRenderDuringBuild) + { + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + } error.clear(); return true; } @@ -448,21 +446,28 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st return false; } + RuntimeShaderArtifact nextArtifact = artifact; + nextArtifact.parameterValues = layer->parameterValues; + if (!PrepareRuntimeTextTextures(nextArtifact, error)) + { + layer->buildState = RuntimeLayerBuildState::Failed; + layer->message = error; + if (!layer->preserveRenderDuringBuild) + { + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + } + layer->preserveRenderDuringBuild = false; + return false; + } + layer->shaderName = artifact.displayName.empty() ? artifact.shaderId : artifact.displayName; layer->packageFingerprint = artifact.packageFingerprint; layer->buildState = RuntimeLayerBuildState::Ready; layer->message = artifact.message; layer->renderReady = true; - layer->artifact = artifact; - layer->artifact.parameterValues = layer->parameterValues; - if (!PrepareRuntimeTextTextures(layer->artifact, error)) - { - layer->buildState = RuntimeLayerBuildState::Failed; - layer->message = error; - layer->renderReady = false; - layer->artifact = RuntimeShaderArtifact(); - return false; - } + layer->preserveRenderDuringBuild = false; + layer->artifact = std::move(nextArtifact); error.clear(); return true; } @@ -488,8 +493,12 @@ bool RuntimeLayerModel::MarkBuildFailed(const std::string& layerId, const std::s layer->buildState = RuntimeLayerBuildState::Failed; layer->message = message; - layer->renderReady = false; - layer->artifact = RuntimeShaderArtifact(); + if (!layer->preserveRenderDuringBuild) + { + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + } + layer->preserveRenderDuringBuild = false; error.clear(); return true; } @@ -515,10 +524,11 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const { RuntimeRenderLayerModel renderLayer; renderLayer.id = layer.id; - renderLayer.shaderId = layer.shaderId; renderLayer.bypass = layer.bypass; renderLayer.artifact = layer.artifact; - renderLayer.artifact.parameterValues = layer.parameterValues; + renderLayer.shaderId = renderLayer.artifact.shaderId.empty() ? layer.shaderId : renderLayer.artifact.shaderId; + if (layer.buildState == RuntimeLayerBuildState::Ready) + renderLayer.artifact.parameterValues = layer.parameterValues; renderLayer.artifact.fontAtlases.clear(); snapshot.renderLayers.push_back(std::move(renderLayer)); } diff --git a/src/runtime/RuntimeLayerModel.h b/src/runtime/RuntimeLayerModel.h index 22027d7..080d903 100644 --- a/src/runtime/RuntimeLayerModel.h +++ b/src/runtime/RuntimeLayerModel.h @@ -64,7 +64,7 @@ public: bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error); bool ResetParameters(const std::string& layerId, std::string& error); bool ReloadFromCatalog(const SupportedShaderCatalog& shaderCatalog, std::vector>& buildsToStart, std::string& error); - bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error); + bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error, bool preserveExistingRenderArtifact = false); bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error); bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message); bool MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error); @@ -85,6 +85,7 @@ private: RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending; std::string message; bool renderReady = false; + bool preserveRenderDuringBuild = false; std::vector parameterDefinitions; std::map parameterValues; RuntimeShaderArtifact artifact; diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index b9c0ed3..1c67c7c 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -405,6 +405,84 @@ void TestReloadRefreshesChangedShaderMetadataAndPreservesValues() std::filesystem::remove_all(root); } +RuntimeShaderArtifact MakeReadyArtifact( + const RenderCadenceCompositor::SupportedShaderCatalog& catalog, + const std::string& layerId, + const std::string& shaderId, + const std::string& sourceToken) +{ + const ShaderPackage* shaderPackage = catalog.FindPackage(shaderId); + RuntimeShaderArtifact artifact; + artifact.layerId = layerId; + artifact.shaderId = shaderId; + artifact.displayName = shaderPackage ? shaderPackage->displayName : shaderId; + artifact.packageFingerprint = shaderPackage ? RenderCadenceCompositor::ShaderPackageFingerprint(*shaderPackage) : sourceToken; + artifact.fragmentShaderSource = "void main(){/*" + sourceToken + "*/}"; + if (shaderPackage) + artifact.parameterDefinitions = shaderPackage->parameters; + artifact.message = "build ready"; + return artifact; +} + +void TestReloadRebuildKeepsLastGoodRenderArtifacts() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + WriteFile(root / "passthrough" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 1.0, 1.0); }\n"); + WriteFile(root / "passthrough" / "shader.json", R"({ + "id": "passthrough", + "name": "Passthrough", + "description": "Passthrough test shader", + "category": "Tests", + "entryPoint": "shadeVideo", + "parameters": [ + { "id": "opacity", "label": "Opacity", "type": "float", "default": 1.0 } + ] + })"); + catalog = LoadCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + std::string solidLayerId; + std::string passthroughLayerId; + Expect(model.AddLayer(catalog, "solid", solidLayerId, error), "reload preserve solid layer can be added"); + Expect(model.AddLayer(catalog, "passthrough", passthroughLayerId, error), "reload preserve passthrough layer can be added"); + Expect(model.MarkBuildReady(MakeReadyArtifact(catalog, solidLayerId, "solid", "solid-old"), error), "solid layer starts render-ready"); + Expect(model.MarkBuildReady(MakeReadyArtifact(catalog, passthroughLayerId, "passthrough", "passthrough-old"), error), "passthrough layer starts render-ready"); + + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.renderLayers.size() == 2, "ready stack exposes both render layers before reload"); + const std::string oldSolidSource = snapshot.renderLayers[0].artifact.fragmentShaderSource; + const std::string oldPassthroughSource = snapshot.renderLayers[1].artifact.fragmentShaderSource; + + WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.25, true)); + RenderCadenceCompositor::SupportedShaderCatalog reloadedCatalog = LoadCatalog(root); + std::vector> buildsToStart; + Expect(model.ReloadFromCatalog(reloadedCatalog, buildsToStart, error), "reload preserve refreshes catalog"); + for (const auto& build : buildsToStart) + Expect(model.MarkBuildStarted(build.first, "reload build started", error, true), "reload build preserves last good artifact"); + + snapshot = model.Snapshot(); + Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "reload marks first layer pending"); + Expect(snapshot.renderLayers.size() == 2, "pending reload keeps full render stack"); + Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "pending reload keeps old first layer artifact"); + Expect(snapshot.renderLayers[1].artifact.fragmentShaderSource == oldPassthroughSource, "pending reload keeps old second layer artifact"); + + Expect(model.MarkBuildFailed(solidLayerId, "reload compile failed", error), "failed reload marks layer failed"); + snapshot = model.Snapshot(); + Expect(!snapshot.compileSucceeded, "failed reload reports compile failure"); + Expect(snapshot.renderLayers.size() == 2, "failed reload keeps last good render stack"); + Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "failed reload keeps old failed layer artifact"); + + Expect(model.MarkBuildReady(MakeReadyArtifact(reloadedCatalog, passthroughLayerId, "passthrough", "passthrough-new"), error), "other reload layer can commit"); + snapshot = model.Snapshot(); + Expect(snapshot.renderLayers.size() == 2, "partial reload commit still keeps complete render stack"); + Expect(snapshot.renderLayers[0].artifact.fragmentShaderSource == oldSolidSource, "partial reload commit keeps old failed layer artifact"); + Expect(snapshot.renderLayers[1].artifact.fragmentShaderSource.find("passthrough-new") != std::string::npos, "partial reload commit updates ready layer"); + + std::filesystem::remove_all(root); +} + void TestTextTexturesArePreparedInRuntimeModel() { std::filesystem::path root = MakeTestRoot(); @@ -456,6 +534,7 @@ int main() TestInvalidRuntimeStateCanFallBackToConfiguredShader(); TestLayerControlsUpdateDisplayAndRenderModels(); TestReloadRefreshesChangedShaderMetadataAndPreservesValues(); + TestReloadRebuildKeepsLastGoodRenderArtifacts(); TestTextTexturesArePreparedInRuntimeModel(); if (gFailures != 0)