Added bad shader warning instead of hard fail
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m32s
CI / Windows Release Package (push) Failing after 2m15s

This commit is contained in:
2026-05-06 12:44:22 +10:00
parent 414ef62479
commit ff1b7519a0
11 changed files with 170 additions and 23 deletions

View File

@@ -249,9 +249,9 @@ If neither variable is set, the workflow falls back to the repo-local defaults u
- Audio.
- Improve text rendering.
- Genlock.
- Don't hardfail on shader fail
- Find a better UI library.
- Logs.
- Continue source cleanup/refactoring. Pass 1 done
- Display the control URL in the Windows app, ideally clickable, without rendering it on the video output.
- Support a separate sound shader `.slang` file in shader packages.
![alt text](image.png)

View File

@@ -1602,12 +1602,14 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
{
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
std::vector<ShaderPackageStatus> packageStatuses;
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
return false;
mPackagesById.swap(packagesById);
mPackageOrder.swap(packageOrder);
mPackageStatuses.swap(packageStatuses);
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
{
@@ -1854,18 +1856,19 @@ JsonValue RuntimeHost::BuildStateValue() const
root.set("performance", performance);
JsonValue shaderLibrary = JsonValue::MakeArray();
for (const std::string& shaderId : mPackageOrder)
for (const ShaderPackageStatus& status : mPackageStatuses)
{
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
continue;
JsonValue shader = JsonValue::MakeObject();
shader.set("id", JsonValue(shaderIt->second.id));
shader.set("name", JsonValue(shaderIt->second.displayName));
shader.set("description", JsonValue(shaderIt->second.description));
shader.set("category", JsonValue(shaderIt->second.category));
if (shaderIt->second.temporal.enabled)
shader.set("id", JsonValue(status.id));
shader.set("name", JsonValue(status.displayName));
shader.set("description", JsonValue(status.description));
shader.set("category", JsonValue(status.category));
shader.set("available", JsonValue(status.available));
if (!status.available)
shader.set("error", JsonValue(status.error));
auto shaderIt = mPackagesById.find(status.id);
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.temporal.enabled)
{
JsonValue temporal = JsonValue::MakeObject();
temporal.set("enabled", JsonValue(true));

View File

@@ -151,6 +151,7 @@ private:
std::filesystem::path mPatchedGlslPath;
std::map<std::string, ShaderPackage> mPackagesById;
std::vector<std::string> mPackageOrder;
std::vector<ShaderPackageStatus> mPackageStatuses;
bool mReloadRequested;
bool mCompileSucceeded;
std::string mCompileMessage;

View File

@@ -554,6 +554,36 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
return true;
}
std::string UniqueUnavailableShaderId(const std::filesystem::path& manifestPath, const std::string& parsedId)
{
const std::string fallbackId = manifestPath.parent_path().filename().string();
const std::string baseId = parsedId.empty() ? fallbackId : parsedId;
return baseId + "@invalid:" + fallbackId;
}
ShaderPackageStatus BuildUnavailableStatus(const std::filesystem::path& manifestPath, const ShaderPackage& partialPackage, const std::string& packageError)
{
ShaderPackageStatus status;
status.id = UniqueUnavailableShaderId(manifestPath, partialPackage.id);
status.displayName = !partialPackage.displayName.empty() ? partialPackage.displayName : manifestPath.parent_path().filename().string();
status.description = partialPackage.description;
status.category = !partialPackage.category.empty() ? partialPackage.category : "Unavailable";
status.available = false;
status.error = packageError;
return status;
}
ShaderPackageStatus BuildAvailableStatus(const ShaderPackage& shaderPackage)
{
ShaderPackageStatus status;
status.id = shaderPackage.id;
status.displayName = shaderPackage.displayName;
status.description = shaderPackage.description;
status.category = shaderPackage.category;
status.available = true;
return status;
}
}
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
@@ -561,10 +591,16 @@ ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
{
}
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
bool ShaderPackageRegistry::Scan(
const std::filesystem::path& shaderRoot,
std::map<std::string, ShaderPackage>& packagesById,
std::vector<std::string>& packageOrder,
std::vector<ShaderPackageStatus>& packageStatuses,
std::string& error) const
{
packagesById.clear();
packageOrder.clear();
packageStatuses.clear();
if (!std::filesystem::exists(shaderRoot))
{
@@ -583,19 +619,27 @@ bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::m
ShaderPackage shaderPackage;
if (!ParseManifest(manifestPath, shaderPackage, error))
return false;
{
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
error.clear();
continue;
}
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
error = "Duplicate shader id found: " + shaderPackage.id;
return false;
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
continue;
}
packageOrder.push_back(shaderPackage.id);
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
return left.displayName < right.displayName;
});
return true;
}

View File

@@ -12,7 +12,12 @@ class ShaderPackageRegistry
public:
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
bool Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const;
bool Scan(
const std::filesystem::path& shaderRoot,
std::map<std::string, ShaderPackage>& packagesById,
std::vector<std::string>& packageOrder,
std::vector<ShaderPackageStatus>& packageStatuses,
std::string& error) const;
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
private:

View File

@@ -93,6 +93,16 @@ struct ShaderPackage
std::filesystem::file_time_type manifestWriteTime;
};
struct ShaderPackageStatus
{
std::string id;
std::string displayName;
std::string description;
std::string category;
bool available = false;
std::string error;
};
struct RuntimeRenderState
{
std::string layerId;

View File

@@ -0,0 +1,15 @@
{
"id": "broken-shader-example",
"name": "Broken Shader Example",
"description": "Intentionally invalid shader package used to verify that bad shaders appear as errors without blocking the app.",
"category": "Diagnostics",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "badToggle",
"label": "Bad Toggle",
"type": "boolean",
"default": true
}
]
}

View File

@@ -0,0 +1,4 @@
float4 shadeVideo(ShaderContext context)
{
return context.sourceColor;
}

View File

@@ -1,5 +1,6 @@
#include "ShaderPackageRegistry.h"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
@@ -187,9 +188,39 @@ void TestDuplicateScan()
ShaderPackageRegistry registry(4);
std::map<std::string, ShaderPackage> packages;
std::vector<std::string> order;
std::vector<ShaderPackageStatus> statuses;
std::string error;
Expect(!registry.Scan(root, packages, order, error), "duplicate package ids are rejected");
Expect(error.find("Duplicate shader id") != std::string::npos, "duplicate scan error is clear");
Expect(registry.Scan(root, packages, order, statuses, error), "duplicate package ids do not fail the whole scan");
Expect(packages.size() == 1, "first duplicate package remains available");
Expect(statuses.size() == 2, "duplicate package is surfaced in shader status list");
Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) {
return !status.available && status.error.find("Duplicate shader id") != std::string::npos;
}), "duplicate scan error is shown on unavailable package");
std::filesystem::remove_all(root);
}
void TestInvalidPackageDoesNotFailScan()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "good", R"({ "id": "good", "name": "Good", "parameters": [] })");
WriteShaderPackage(root, "bad", R"({
"id": "bad",
"name": "Bad",
"parameters": [{ "id": "enabled", "label": "Enabled", "type": "boolean" }]
})");
ShaderPackageRegistry registry(4);
std::map<std::string, ShaderPackage> packages;
std::vector<std::string> order;
std::vector<ShaderPackageStatus> statuses;
std::string error;
Expect(registry.Scan(root, packages, order, statuses, error), "invalid package does not fail the whole scan");
Expect(packages.find("good") != packages.end(), "valid package remains available");
Expect(packages.find("bad") == packages.end(), "invalid package is not available for rendering");
Expect(std::any_of(statuses.begin(), statuses.end(), [](const ShaderPackageStatus& status) {
return status.id.find("bad") != std::string::npos && !status.available && status.error.find("Unsupported parameter type") != std::string::npos;
}), "invalid package is surfaced with its parse error");
std::filesystem::remove_all(root);
}
@@ -203,6 +234,7 @@ int main()
TestInvalidTemporalSettings();
TestDisabledTemporalSettingsAreIgnored();
TestDuplicateScan();
TestInvalidPackageDoesNotFailScan();
if (gFailures != 0)
{

View File

@@ -7,7 +7,7 @@ function matchesShader(shader, query) {
return true;
}
return [shader.name, shader.id, shader.category, shader.description]
return [shader.name, shader.id, shader.category, shader.description, shader.error]
.filter(Boolean)
.some((value) => value.toLowerCase().includes(normalizedQuery));
}
@@ -17,6 +17,10 @@ function shaderSummary(shader) {
return "Search available shaders";
}
if (shader.available === false) {
return shader.error || "Shader is unavailable";
}
return shader.description || "No description";
}
@@ -25,7 +29,8 @@ function ShaderOptionContent({ shader }) {
<>
<span className="shader-picker__option-head">
<span className="shader-picker__name">{shader.name}</span>
{shader.category ? <span className="shader-picker__category">{shader.category}</span> : null}
{shader.available === false ? <span className="shader-picker__category shader-picker__category--error">Error</span> : null}
{shader.available !== false && shader.category ? <span className="shader-picker__category">{shader.category}</span> : null}
</span>
<span className="shader-picker__meta">{shaderSummary(shader)}</span>
</>
@@ -60,7 +65,9 @@ export function ShaderPicker({ id, label = "Shader", shaders, value, onChange })
<span>
<span className="shader-picker__option-head">
<span className="shader-picker__name">{selectedShader?.name ?? "Choose shader"}</span>
{selectedShader?.category ? (
{selectedShader?.available === false ? (
<span className="shader-picker__category shader-picker__category--error">Error</span>
) : selectedShader?.category ? (
<span className="shader-picker__category">{selectedShader.category}</span>
) : null}
</span>
@@ -88,10 +95,14 @@ export function ShaderPicker({ id, label = "Shader", shaders, value, onChange })
<button
key={shader.id}
type="button"
className={`shader-picker__option${shader.id === value ? " shader-picker__option--selected" : ""}`}
className={`shader-picker__option${shader.id === value ? " shader-picker__option--selected" : ""}${shader.available === false ? " shader-picker__option--unavailable" : ""}`}
role="option"
aria-selected={shader.id === value}
disabled={shader.available === false}
onClick={() => {
if (shader.available === false) {
return;
}
onChange(shader.id);
setOpen(false);
setQuery("");

View File

@@ -749,6 +749,22 @@ pre {
box-shadow: inset 0 0 0 1px var(--app-primary-soft);
}
.shader-picker__option--unavailable {
border-color: rgba(217, 83, 79, 0.45);
background: rgba(217, 83, 79, 0.08);
opacity: 1;
}
.shader-picker__option:disabled {
cursor: not-allowed;
opacity: 1;
}
.shader-picker__option--unavailable .shader-picker__name,
.shader-picker__option--unavailable .shader-picker__meta {
color: #ffd0cf;
}
.shader-picker__name,
.shader-picker__meta {
min-width: 0;
@@ -788,6 +804,12 @@ pre {
white-space: nowrap;
}
.shader-picker__category--error {
border-color: rgba(217, 83, 79, 0.45);
background: var(--app-error-soft);
color: #ffd0cf;
}
.shader-picker__meta {
display: -webkit-box;
overflow: hidden;