Added bad shader warning instead of hard fail
This commit is contained in:
@@ -249,9 +249,9 @@ If neither variable is set, the workflow falls back to the repo-local defaults u
|
|||||||
- Audio.
|
- Audio.
|
||||||
- Improve text rendering.
|
- Improve text rendering.
|
||||||
- Genlock.
|
- Genlock.
|
||||||
|
- Don't hardfail on shader fail
|
||||||
- Find a better UI library.
|
- Find a better UI library.
|
||||||
- Logs.
|
- Logs.
|
||||||
- Continue source cleanup/refactoring. Pass 1 done
|
- Continue source cleanup/refactoring. Pass 1 done
|
||||||
- Display the control URL in the Windows app, ideally clickable, without rendering it on the video output.
|
- 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.
|
- Support a separate sound shader `.slang` file in shader packages.
|
||||||

|
|
||||||
|
|||||||
@@ -1602,12 +1602,14 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
{
|
{
|
||||||
std::map<std::string, ShaderPackage> packagesById;
|
std::map<std::string, ShaderPackage> packagesById;
|
||||||
std::vector<std::string> packageOrder;
|
std::vector<std::string> packageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> packageStatuses;
|
||||||
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
||||||
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
|
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mPackagesById.swap(packagesById);
|
mPackagesById.swap(packagesById);
|
||||||
mPackageOrder.swap(packageOrder);
|
mPackageOrder.swap(packageOrder);
|
||||||
|
mPackageStatuses.swap(packageStatuses);
|
||||||
|
|
||||||
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
||||||
{
|
{
|
||||||
@@ -1854,18 +1856,19 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
root.set("performance", performance);
|
root.set("performance", performance);
|
||||||
|
|
||||||
JsonValue shaderLibrary = JsonValue::MakeArray();
|
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();
|
JsonValue shader = JsonValue::MakeObject();
|
||||||
shader.set("id", JsonValue(shaderIt->second.id));
|
shader.set("id", JsonValue(status.id));
|
||||||
shader.set("name", JsonValue(shaderIt->second.displayName));
|
shader.set("name", JsonValue(status.displayName));
|
||||||
shader.set("description", JsonValue(shaderIt->second.description));
|
shader.set("description", JsonValue(status.description));
|
||||||
shader.set("category", JsonValue(shaderIt->second.category));
|
shader.set("category", JsonValue(status.category));
|
||||||
if (shaderIt->second.temporal.enabled)
|
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();
|
JsonValue temporal = JsonValue::MakeObject();
|
||||||
temporal.set("enabled", JsonValue(true));
|
temporal.set("enabled", JsonValue(true));
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ private:
|
|||||||
std::filesystem::path mPatchedGlslPath;
|
std::filesystem::path mPatchedGlslPath;
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
std::vector<std::string> mPackageOrder;
|
std::vector<std::string> mPackageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||||
bool mReloadRequested;
|
bool mReloadRequested;
|
||||||
bool mCompileSucceeded;
|
bool mCompileSucceeded;
|
||||||
std::string mCompileMessage;
|
std::string mCompileMessage;
|
||||||
|
|||||||
@@ -554,6 +554,36 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
|
|||||||
|
|
||||||
return true;
|
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)
|
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();
|
packagesById.clear();
|
||||||
packageOrder.clear();
|
packageOrder.clear();
|
||||||
|
packageStatuses.clear();
|
||||||
|
|
||||||
if (!std::filesystem::exists(shaderRoot))
|
if (!std::filesystem::exists(shaderRoot))
|
||||||
{
|
{
|
||||||
@@ -583,19 +619,27 @@ bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::m
|
|||||||
|
|
||||||
ShaderPackage shaderPackage;
|
ShaderPackage shaderPackage;
|
||||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
if (!ParseManifest(manifestPath, shaderPackage, error))
|
||||||
return false;
|
{
|
||||||
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
|
||||||
|
error.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
||||||
{
|
{
|
||||||
error = "Duplicate shader id found: " + shaderPackage.id;
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
packageOrder.push_back(shaderPackage.id);
|
packageOrder.push_back(shaderPackage.id);
|
||||||
|
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
|
||||||
packagesById[shaderPackage.id] = shaderPackage;
|
packagesById[shaderPackage.id] = shaderPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(packageOrder.begin(), packageOrder.end());
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ class ShaderPackageRegistry
|
|||||||
public:
|
public:
|
||||||
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
|
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;
|
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ struct ShaderPackage
|
|||||||
std::filesystem::file_time_type manifestWriteTime;
|
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
|
struct RuntimeRenderState
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
|
|||||||
15
shaders/broken-shader-example/shader.json
Normal file
15
shaders/broken-shader-example/shader.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
shaders/broken-shader-example/shader.slang
Normal file
4
shaders/broken-shader-example/shader.slang
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
return context.sourceColor;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "ShaderPackageRegistry.h"
|
#include "ShaderPackageRegistry.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -187,9 +188,39 @@ void TestDuplicateScan()
|
|||||||
ShaderPackageRegistry registry(4);
|
ShaderPackageRegistry registry(4);
|
||||||
std::map<std::string, ShaderPackage> packages;
|
std::map<std::string, ShaderPackage> packages;
|
||||||
std::vector<std::string> order;
|
std::vector<std::string> order;
|
||||||
|
std::vector<ShaderPackageStatus> statuses;
|
||||||
std::string error;
|
std::string error;
|
||||||
Expect(!registry.Scan(root, packages, order, error), "duplicate package ids are rejected");
|
Expect(registry.Scan(root, packages, order, statuses, error), "duplicate package ids do not fail the whole scan");
|
||||||
Expect(error.find("Duplicate shader id") != std::string::npos, "duplicate scan error is clear");
|
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);
|
std::filesystem::remove_all(root);
|
||||||
}
|
}
|
||||||
@@ -203,6 +234,7 @@ int main()
|
|||||||
TestInvalidTemporalSettings();
|
TestInvalidTemporalSettings();
|
||||||
TestDisabledTemporalSettingsAreIgnored();
|
TestDisabledTemporalSettingsAreIgnored();
|
||||||
TestDuplicateScan();
|
TestDuplicateScan();
|
||||||
|
TestInvalidPackageDoesNotFailScan();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function matchesShader(shader, query) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [shader.name, shader.id, shader.category, shader.description]
|
return [shader.name, shader.id, shader.category, shader.description, shader.error]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) => value.toLowerCase().includes(normalizedQuery));
|
.some((value) => value.toLowerCase().includes(normalizedQuery));
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,10 @@ function shaderSummary(shader) {
|
|||||||
return "Search available shaders";
|
return "Search available shaders";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shader.available === false) {
|
||||||
|
return shader.error || "Shader is unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
return shader.description || "No description";
|
return shader.description || "No description";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +29,8 @@ function ShaderOptionContent({ shader }) {
|
|||||||
<>
|
<>
|
||||||
<span className="shader-picker__option-head">
|
<span className="shader-picker__option-head">
|
||||||
<span className="shader-picker__name">{shader.name}</span>
|
<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>
|
||||||
<span className="shader-picker__meta">{shaderSummary(shader)}</span>
|
<span className="shader-picker__meta">{shaderSummary(shader)}</span>
|
||||||
</>
|
</>
|
||||||
@@ -60,7 +65,9 @@ export function ShaderPicker({ id, label = "Shader", shaders, value, onChange })
|
|||||||
<span>
|
<span>
|
||||||
<span className="shader-picker__option-head">
|
<span className="shader-picker__option-head">
|
||||||
<span className="shader-picker__name">{selectedShader?.name ?? "Choose shader"}</span>
|
<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>
|
<span className="shader-picker__category">{selectedShader.category}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
@@ -88,10 +95,14 @@ export function ShaderPicker({ id, label = "Shader", shaders, value, onChange })
|
|||||||
<button
|
<button
|
||||||
key={shader.id}
|
key={shader.id}
|
||||||
type="button"
|
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"
|
role="option"
|
||||||
aria-selected={shader.id === value}
|
aria-selected={shader.id === value}
|
||||||
|
disabled={shader.available === false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (shader.available === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onChange(shader.id);
|
onChange(shader.id);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setQuery("");
|
setQuery("");
|
||||||
|
|||||||
@@ -749,6 +749,22 @@ pre {
|
|||||||
box-shadow: inset 0 0 0 1px var(--app-primary-soft);
|
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__name,
|
||||||
.shader-picker__meta {
|
.shader-picker__meta {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -788,6 +804,12 @@ pre {
|
|||||||
white-space: nowrap;
|
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 {
|
.shader-picker__meta {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user