Skip to content

Commit 2521664

Browse files
jcelerierclaude
andcommitted
gfx: detect dropped shaders properly, and pair their two stages
Three things were wrong at once. The drop handlers stored score::FilePath::relative, so a shader under the document folder or the library arrived as "<PROJECT>:Shaders/x.fs" and every model opened it with a bare QFile. It only ever worked in an unsaved document, where relativize hands back an absolute path. The models resolve through locateShaderPath now, as Gfx::Video::Model already did. One handler could claim an extension: m_perFileExtension assigned rather than appended, so of the four families sharing .fs exactly one survived, chosen by hash order. The list asks every handler registered for the extension, and each sniffs its own MODE marker and stays silent otherwise -- which also lets raw raster have a drop handler at all, from the other plugin, without either plugin having to name the other's process. Vertex pairing used QString::replace, rewriting every occurrence in the path (a directory named x.fs broke it), and RenderPipeline used baseName(), which truncates at the first dot, so my.shader.fs looked for my.vs. Both go through one sibling helper built on completeBaseName. Dropping a .fs and its .vs now yields one process: the .vs half is refused by VSA when a fragment sibling exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4N6KsxzvT1o9qoPvBThyz
1 parent 3fe4857 commit 2521664

16 files changed

Lines changed: 426 additions & 80 deletions

File tree

‎src/plugins/score-lib-process/Process/Drop/ProcessDropHandler.cpp‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,26 @@ std::vector<ProcessDropHandler::ProcessDrop> ProcessDropHandlerList::getDrop(
199199
if(auto it = m_perFileExtension.find(ext.toStdString());
200200
it != m_perFileExtension.end())
201201
{
202-
auto& handler = *it->second;
203-
204-
// First check if a custom drop is in order, which handles everything.
205-
if(handleCustomDrop(handler))
206-
{
207-
// qDebug() << "handled through getCustomDrops";
208-
return res;
209-
}
210-
211-
// Then fall back to the normal mime data drop
212-
score::FilePath p{
202+
const score::FilePath p{
213203
.absolute = path,
214204
.relative = score::relativizeFilePath(path, ctx),
215205
.filename = f.fileName(),
216206
.basename = f.baseName()};
217-
handler.getFileDrops(res, mime, p, ctx);
207+
208+
for(auto* h : it->second)
209+
{
210+
auto& handler = *h;
211+
212+
// First check if a custom drop is in order, which handles everything.
213+
if(handleCustomDrop(handler))
214+
{
215+
// qDebug() << "handled through getCustomDrops";
216+
return res;
217+
}
218+
219+
// Then fall back to the normal mime data drop
220+
handler.getFileDrops(res, mime, p, ctx);
221+
}
218222
}
219223
}
220224
}
@@ -262,7 +266,7 @@ void ProcessDropHandlerList::initCaches() const
262266
{
263267
for(const auto& ext : handler.fileExtensions())
264268
{
265-
m_perFileExtension[ext.toLower().toStdString()] = &handler;
269+
m_perFileExtension[ext.toLower().toStdString()].push_back(&handler);
266270
}
267271

268272
for(const auto& ext : handler.mimeTypes())

‎src/plugins/score-lib-process/Process/Drop/ProcessDropHandler.hpp‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <score_lib_process_export.h>
1717

1818
#include <string>
19+
#include <vector>
1920

2021
namespace Process
2122
{
@@ -85,7 +86,11 @@ class SCORE_LIB_PROCESS_EXPORT ProcessDropHandlerList final
8586
private:
8687
void initCaches() const;
8788
mutable ossia::hash_map<std::string, ProcessDropHandler*> m_perMimeTypes{};
88-
mutable ossia::hash_map<std::string, ProcessDropHandler*> m_perFileExtension{};
89+
// Several handlers legitimately claim one extension -- four shader families
90+
// share ".fs" and tell each other apart by reading the file header -- so each
91+
// of them gets its turn and the ones the file does not belong to say nothing.
92+
mutable ossia::hash_map<std::string, std::vector<ProcessDropHandler*>>
93+
m_perFileExtension{};
8994
mutable std::size_t m_lastCacheSize{};
9095
};
9196
}

‎src/plugins/score-plugin-gfx/Gfx/CSF/Library.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <Gfx/CSF/Library.hpp>
22
#include <Gfx/CSF/Process.hpp>
33
#include <Gfx/Filter/PreviewWidget.hpp>
4+
#include <Gfx/ShaderProgram.hpp>
45
#include <Library/LibrarySettings.hpp>
56
#include <Library/ProcessesItemModel.hpp>
67

@@ -60,6 +61,18 @@ void DropHandler::dropPath(
6061
std::vector<ProcessDrop>& vec, const score::FilePath& filename,
6162
const score::DocumentContext& ctx) const noexcept
6263
{
64+
// A compute extension is not shared with any other family, so a file that
65+
// declares no MODE at all is still taken -- but one that claims to be
66+
// something else is left to its own handler.
67+
switch(Gfx::shaderFileFamily(filename.absolute))
68+
{
69+
case Gfx::ShaderFamily::Compute:
70+
case Gfx::ShaderFamily::Unknown:
71+
break;
72+
default:
73+
return;
74+
}
75+
6376
Process::ProcessDropHandler::ProcessDrop p;
6477
p.creation.key = Metadata<ConcreteKey_k, Gfx::CSF::Model>::get();
6578
p.creation.prettyName = filename.basename;

‎src/plugins/score-plugin-gfx/Gfx/CSF/Process.cpp‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ Model::Model(
7979
{
8080
metadata().setInstanceName(*this);
8181

82-
QFile f{init};
83-
if(f.open(QIODevice::ReadOnly))
82+
const QString path = locateShaderPath(init, *this);
83+
if(QFile f{path}; f.open(QIODevice::ReadOnly))
8484
{
85-
m_scriptPath = init;
85+
m_scriptPath = path;
8686
(void)setCompute(f.readAll());
8787
}
8888
}

‎src/plugins/score-plugin-gfx/Gfx/Filter/Library.cpp‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#include <Gfx/Filter/Library.hpp>
33
#include <Gfx/Filter/PreviewWidget.hpp>
44
#include <Gfx/Filter/Process.hpp>
5-
#include <Gfx/GeometryFilter/Process.hpp>
5+
#include <Gfx/ShaderProgram.hpp>
66
#include <Gfx/GfxDevice.hpp>
77
#include <Library/LibrarySettings.hpp>
88
#include <Library/ProcessesItemModel.hpp>
@@ -154,13 +154,13 @@ void DropHandler::dropPath(
154154
std::vector<ProcessDrop>& vec, const score::FilePath& filename,
155155
const score::DocumentContext& ctx) const noexcept
156156
{
157-
// See Gfx::GeometryFilter::DropHandler::dropPath: "glsl" resolves to one of
158-
// the two handlers and either has to produce the right process.
159-
QFile f{filename.absolute};
157+
// ISF is the family without a MODE of its own: anything that declares one
158+
// belongs to the handler of that family, which gets its own turn at the file.
159+
if(Gfx::shaderFileFamily(filename.absolute) != Gfx::ShaderFamily::Unknown)
160+
return;
161+
160162
Process::ProcessDropHandler::ProcessDrop p;
161-
p.creation.key = score::fileContains(f, "\"GEOMETRY_FILTER\"")
162-
? Metadata<ConcreteKey_k, Gfx::GeometryFilter::Model>::get()
163-
: Metadata<ConcreteKey_k, Gfx::Filter::Model>::get();
163+
p.creation.key = Metadata<ConcreteKey_k, Gfx::Filter::Model>::get();
164164
p.creation.prettyName = filename.basename;
165165
p.creation.customData = filename.relative;
166166

‎src/plugins/score-plugin-gfx/Gfx/Filter/Process.cpp‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,16 @@ Model::Model(
7171
metadata().setInstanceName(*this);
7272
m_outlets.push_back(new TextureOutlet{"Texture Out", Id<Process::Port>(1), this});
7373

74-
if(init.endsWith("fs") || init.endsWith("frag"))
74+
const QString path = locateShaderPath(init, *this);
75+
if(path.endsWith("fs") || path.endsWith("frag"))
7576
{
76-
m_scriptPath = init;
77-
(void)setProgram(programFromISFFragmentShaderPath(init, {}));
77+
m_scriptPath = path;
78+
(void)setProgram(programFromISFFragmentShaderPath(path, {}));
7879
}
79-
else if(init.endsWith("vs") || init.endsWith("vert"))
80+
else if(path.endsWith("vs") || path.endsWith("vert"))
8081
{
81-
m_scriptPath = init;
82-
(void)setProgram(programFromVSAVertexShaderPath(init, {}));
82+
m_scriptPath = path;
83+
(void)setProgram(programFromVSAVertexShaderPath(path, {}));
8384
}
8485
}
8586

‎src/plugins/score-plugin-gfx/Gfx/GeometryFilter/Library.cpp‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#include <Gfx/Filter/Process.hpp>
21
#include <Gfx/GeometryFilter/Library.hpp>
32
#include <Gfx/GeometryFilter/PreviewWidget.hpp>
43
#include <Gfx/GeometryFilter/Process.hpp>
4+
#include <Gfx/ShaderProgram.hpp>
55
#include <Library/LibrarySettings.hpp>
66
#include <Library/ProcessesItemModel.hpp>
77

@@ -53,14 +53,11 @@ void DropHandler::dropPath(
5353
std::vector<ProcessDrop>& vec, const score::FilePath& filename,
5454
const score::DocumentContext& ctx) const noexcept
5555
{
56-
// Both this handler and Gfx::Filter's claim "glsl", and only one of them is
57-
// in ProcessDropHandlerList's per-extension map, so both have to sort the two
58-
// shader kinds out the way the library handlers do.
59-
QFile f{filename.absolute};
56+
if(Gfx::shaderFileFamily(filename.absolute) != Gfx::ShaderFamily::GeometryFilter)
57+
return;
58+
6059
Process::ProcessDropHandler::ProcessDrop p;
61-
p.creation.key = score::fileContains(f, "\"GEOMETRY_FILTER\"")
62-
? Metadata<ConcreteKey_k, Gfx::GeometryFilter::Model>::get()
63-
: Metadata<ConcreteKey_k, Gfx::Filter::Model>::get();
60+
p.creation.key = Metadata<ConcreteKey_k, Gfx::GeometryFilter::Model>::get();
6461
p.creation.prettyName = filename.basename;
6562
p.creation.customData = filename.relative;
6663

‎src/plugins/score-plugin-gfx/Gfx/GeometryFilter/Process.cpp‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ Model::Model(
3838
m_inlets.push_back(new GeometryInlet{"Geometry In", Id<Process::Port>(0), this});
3939
m_outlets.push_back(new GeometryOutlet{"Geometry Out", Id<Process::Port>(1), this});
4040

41-
QFile f{init};
42-
if(f.open(QIODevice::ReadOnly))
41+
if(QFile f{locateShaderPath(init, *this)}; f.open(QIODevice::ReadOnly))
4342
(void)setScript(f.readAll());
4443
}
4544

‎src/plugins/score-plugin-gfx/Gfx/ShaderProgram.cpp‎

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
#include <Library/LibrarySettings.hpp>
66

77
#include <score/application/ApplicationContext.hpp>
8+
#include <score/tools/File.hpp>
9+
#include <score/tools/FilePath.hpp>
10+
11+
#include <core/document/Document.hpp>
812

913
#include <ossia/detail/flat_map.hpp>
1014
#include <ossia/detail/hash_map.hpp>
@@ -597,36 +601,82 @@ ProgramCache::get(const ShaderSource& program, const QString& originPath) noexce
597601
return {std::nullopt, "Unknown error"};
598602
}
599603

604+
static QString shaderSibling(const QString& path, std::initializer_list<QLatin1String> exts)
605+
{
606+
const QFileInfo fi{path};
607+
// completeBaseName, not baseName: `my.shader.fs` pairs with `my.shader.vs`,
608+
// and path surgery on the string as a whole would also rewrite a folder
609+
// called `fs-shaders`.
610+
const QString base = fi.path() + '/' + fi.completeBaseName() + '.';
611+
for(const QLatin1String& ext : exts)
612+
{
613+
if(fi.suffix() == ext)
614+
continue;
615+
if(const QString candidate = base + ext; QFileInfo::exists(candidate))
616+
return candidate;
617+
}
618+
return {};
619+
}
620+
621+
QString vertexShaderSibling(const QString& fsPath) noexcept
622+
{
623+
return shaderSibling(fsPath, {QLatin1String{"vert"}, QLatin1String{"vs"}});
624+
}
625+
626+
QString fragmentShaderSibling(const QString& vsPath) noexcept
627+
{
628+
return shaderSibling(vsPath, {QLatin1String{"frag"}, QLatin1String{"fs"}});
629+
}
630+
631+
ShaderFamily shaderFileFamily(const QString& path) noexcept
632+
{
633+
struct
634+
{
635+
std::string_view marker;
636+
ShaderFamily family;
637+
} static constexpr markers[]{
638+
{"\"RAW_RASTER_PIPELINE\"", ShaderFamily::RawRaster},
639+
{"\"GEOMETRY_FILTER\"", ShaderFamily::GeometryFilter},
640+
{"\"COMPUTE_SHADER\"", ShaderFamily::Compute},
641+
{"\"VERTEX_SHADER_ART\"", ShaderFamily::VertexShaderArt},
642+
};
643+
644+
for(const auto& [marker, family] : markers)
645+
{
646+
// A fresh QFile per marker: fileContains reads from wherever the device
647+
// currently is.
648+
QFile f{path};
649+
if(score::fileContains(f, marker))
650+
return family;
651+
}
652+
return ShaderFamily::Unknown;
653+
}
654+
655+
QString locateShaderPath(const QString& path, const QObject& process) noexcept
656+
{
657+
if(path.isEmpty())
658+
return path;
659+
660+
// Walked here rather than through score::IDocument::documentFromObject,
661+
// which THROWS for an object that is not in a document -- a process being
662+
// built for a preview or a preset is not, and this is noexcept.
663+
for(const QObject* obj = &process; obj; obj = obj->parent())
664+
if(auto* doc = qobject_cast<const score::Document*>(obj))
665+
return score::locateFilePath(path, doc->context());
666+
667+
return path;
668+
}
669+
600670
ShaderSource
601671
programFromISFFragmentShaderPath(
602672
const QString& fsFilename, QByteArray fsData, ShaderSource::ProgramType type)
603673
{
604-
// ISF works by storing a vertex shader next to the fragment shader.
605-
// Score recognises both the long (.frag/.vert) and short (.fs/.vs)
606-
// extension conventions; pairings are tried independently of the FS
607-
// file's own naming so a `foo.frag` next to `foo.vs` (or `foo.fs` next
608-
// to `foo.vert`) also resolves. Without this, the .vs sibling is
609-
// silently ignored and the descriptor falls back to the ISF default
610-
// vertex shader — which doesn't know about user-declared
611-
// VERTEX_INPUTS, so the consumer renders nothing.
612-
const QString candidates[] = {
613-
QString(fsFilename).replace(".frag", ".vert").replace(".fs", ".vs"),
614-
QString(fsFilename).replace(".frag", ".vs"),
615-
QString(fsFilename).replace(".fs", ".vert"),
616-
};
617-
618674
// If empty: will be using the ISF's default
619675
QByteArray vertexData;
620-
for(const QString& vertexName : candidates)
676+
if(const QString vertexName = vertexShaderSibling(fsFilename); !vertexName.isEmpty())
621677
{
622-
if(vertexName == fsFilename)
623-
continue;
624-
if(QFile vertexFile{vertexName};
625-
vertexFile.exists() && vertexFile.open(QIODevice::ReadOnly))
626-
{
678+
if(QFile vertexFile{vertexName}; vertexFile.open(QIODevice::ReadOnly))
627679
vertexData = vertexFile.readAll();
628-
break;
629-
}
630680
}
631681

632682
if(fsData.isEmpty())

‎src/plugins/score-plugin-gfx/Gfx/ShaderProgram.hpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,36 @@ struct SCORE_PLUGIN_GFX_EXPORT ShaderSource
121121
}
122122
};
123123

124+
//! Which of the shader families a file on disk belongs to, read from the
125+
//! "MODE" key of its ISF header. `Unknown` is a shader with no such key: an
126+
//! ISF without MODE, a bare GLSL snippet, a hand-written companion vertex
127+
//! shader. Deciding this is what lets several drop handlers share an
128+
//! extension: each one claims its own family and leaves the rest alone.
129+
enum class ShaderFamily
130+
{
131+
Unknown,
132+
RawRaster,
133+
GeometryFilter,
134+
Compute,
135+
VertexShaderArt
136+
};
137+
138+
SCORE_PLUGIN_GFX_EXPORT ShaderFamily shaderFileFamily(const QString& path) noexcept;
139+
140+
//! Path of the vertex shader that goes with @p fsPath -- `foo.vert` or
141+
//! `foo.vs` next to `foo.fs` / `foo.frag` -- or an empty string.
142+
SCORE_PLUGIN_GFX_EXPORT QString vertexShaderSibling(const QString& fsPath) noexcept;
143+
144+
//! Path of the fragment shader @p vsPath is the companion of, or an empty
145+
//! string. A vertex shader that has one is not a standalone process.
146+
SCORE_PLUGIN_GFX_EXPORT QString fragmentShaderSibling(const QString& vsPath) noexcept;
147+
148+
//! Absolute path of the shader a process was created with. Construction data
149+
//! travels as a <PROJECT>: / <LIBRARY>: path so that it survives a project
150+
//! being moved; the model needs the resolved one, like the load path does.
151+
SCORE_PLUGIN_GFX_EXPORT
152+
QString locateShaderPath(const QString& path, const QObject& process) noexcept;
153+
124154
SCORE_PLUGIN_GFX_EXPORT ShaderSource
125155
programFromISFFragmentShaderPath(
126156
const QString& fsFilename, QByteArray fsData,

0 commit comments

Comments
 (0)