Loading...
Searching...
No Matches
ISFProcess.hpp
1#pragma once
2
3#include <Process/Dataflow/WidgetInlets.hpp>
4#include <Process/ProcessFactory.hpp>
5
6#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
7
8#include <Gfx/Graph/Utils.hpp>
9#include <Gfx/LongInputControl.hpp>
10#include <Gfx/TexturePort.hpp>
11#include <Gfx/WindowDevice.hpp>
12
13#include <score/tools/File.hpp>
14
15#include <ossia/detail/flat_map.hpp>
16
17#include <QFile>
18
19#include <isf.hpp>
20
21#include <algorithm>
22
23namespace Gfx
24{
26{
28 template <typename T>
29 static QString firstWindowDeviceName(T& self)
30 {
31 auto& ctx = score::IDocument::documentContext(self);
32 auto& device_plug = ctx.template plugin<Explorer::DeviceDocumentPlugin>();
33 for(auto dev : device_plug.list().devices())
34 if(auto win = qobject_cast<WindowDevice*>(dev))
35 return win->name();
36 return {};
37 }
38
39 template <typename T>
40 static Process::Descriptor descriptorFromISFFile(QString path)
41 {
43 if(path.isEmpty())
44 return base;
45
46 // The static author is the one of the process, not of the shader file:
47 // a file that credits nobody has no known author.
48 base.author.clear();
49
50 QFile f{path};
51 if(!f.open(QIODevice::ReadOnly))
52 return base;
53
54 try
55 {
56 auto [_, desc] = isf::parser::parse_isf_header(score::readFileAsString(f));
57 if(desc.credits.starts_with("Automatically converted from "))
58 desc.credits = desc.credits.substr(strlen("Automatically converted from "));
59 else if(desc.credits.starts_with("by "))
60 desc.credits = desc.credits.substr(strlen("by "));
61 if(!desc.credits.empty())
62 base.author = QString::fromStdString(desc.credits);
63 if(!desc.description.empty())
64 base.description = QString::fromStdString(desc.description);
65 for(auto& cat : desc.categories)
66 base.tags.push_back(QString::fromStdString(cat));
67 }
68 catch(...)
69 {
70 }
71
72 return base;
73 }
74
75 template <typename T>
76 static void setupISFModelPorts(
77 T& self, const isf::descriptor& desc,
78 const ossia::flat_map<QString, ossia::value>& previous_values)
79 {
80 /*
81 {
82 auto& [shader, error] = score::gfx::ShaderCache::get(
83 m_processedProgram.vertex.toLatin1(), QShader::Stage::VertexStage);
84 SCORE_ASSERT(error.isEmpty());
85 }
86 {
87 auto& [shader, error] = score::gfx::ShaderCache::get(
88 m_processedProgram.fragment.toLatin1(), QShader::Stage::FragmentStage);
89 SCORE_ASSERT(error.isEmpty());
90 }
91 */
92
93 int i = 0;
94 using namespace isf;
95
96 struct input_vis
97 {
98 const ossia::flat_map<QString, ossia::value>& previous_values;
99 const isf::input& input;
100 const int i;
101 T& self;
102 // Outlet id allocator for write-access storage / image inputs. Starts at
103 // a high base so it never collides with inlet ids (input index `i`), the
104 // default "Texture Out" outlet (id 1), or the MRT outlet base (10000).
105 int& outlet_id;
107 bool vsa;
108
109 Process::Inlet* operator()(const float_input& v)
110 {
111 auto nm = QString::fromStdString(input.name);
112 auto port = new Process::FloatSlider(
113 v.min, v.max, v.def, nm, Id<Process::Port>(i), &self);
114
115 self.m_inlets.push_back(port);
116 if(auto it = previous_values.find(nm);
117 it != previous_values.end()
118 && it->second.get_type() == ossia::val_type::FLOAT)
119 port->setValue(it->second);
120
121 self.controlAdded(port->id());
122 return port;
123 }
124
125 Process::Inlet* operator()(const long_input& v)
126 {
127 auto nm = QString::fromStdString(input.name);
128
129 auto port = makeLongInputControl(v, nm, Id<Process::Port>(i), &self);
130
131 if(auto it = previous_values.find(nm);
132 it != previous_values.end()
133 && it->second.get_type() == port->value().get_type())
134 port->setValue(it->second);
135
136 self.m_inlets.push_back(port);
137 self.controlAdded(port->id());
138 return port;
139 }
140
141 Process::Inlet* operator()(const event_input& v)
142 {
143 auto nm = QString::fromStdString(input.name);
144 auto port = new Process::Button(nm, Id<Process::Port>(i), &self);
145
146 self.m_inlets.push_back(port);
147 self.controlAdded(port->id());
148 return port;
149 }
150
151 Process::Inlet* operator()(const bool_input& v)
152 {
153 auto nm = QString::fromStdString(input.name);
154 auto port = new Process::Toggle(v.def, nm, Id<Process::Port>(i), &self);
155
156 if(auto it = previous_values.find(nm);
157 it != previous_values.end()
158 && it->second.get_type() == port->value().get_type())
159 port->setValue(it->second);
160
161 self.m_inlets.push_back(port);
162 self.controlAdded(port->id());
163 return port;
164 }
165
166 Process::Inlet* operator()(const point2d_input& v)
167 {
168 auto nm = QString::fromStdString(input.name);
169 ossia::vec2f min{-100., -100.};
170 ossia::vec2f max{100., 100.};
171 ossia::vec2f init{0.0, 0.0};
172 if(v.def)
173 std::copy_n(v.def->begin(), 2, init.begin());
174 if(v.min)
175 std::copy_n(v.min->begin(), 2, min.begin());
176 if(v.max)
177 std::copy_n(v.max->begin(), 2, max.begin());
178 auto port = new Process::XYSpinboxes{
179 min, max, init, false, nm, Id<Process::Port>(i), &self};
180
181 // The pointer addresses of the first window device (PointerAddresses.hpp),
182 // set as any other address: shown on the inlet, changeable there.
183 // vertexshaderart's `mouse` is clip space, -1..1, y up (src/js/main.js
184 // recordMouseMove; src/index.html "mouse position normalized (-1 to
185 // 1)"); in an ISF a `mouse` is 0..1, y up, an `iMouse` (Shadertoy) in
186 // pixels.
187 if(const QString win = ISFHelpers::firstWindowDeviceName(self); !win.isEmpty())
188 {
189 if(nm.contains("iMouse"))
190 port->setAddress(
191 State::AddressAccessor{State::Address{win, {"cursor", "absolute"}}});
192 else if(vsa && nm == QStringLiteral("mouse"))
193 port->setAddress(
194 State::AddressAccessor{State::Address{win, {"cursor", "clip"}}});
195 else if(nm.contains("mouse", Qt::CaseInsensitive))
196 port->setAddress(
197 State::AddressAccessor{State::Address{win, {"cursor", "gl"}}});
198 }
199
200 if(auto it = previous_values.find(nm);
201 it != previous_values.end()
202 && it->second.get_type() == port->value().get_type())
203 port->setValue(it->second);
204
205 self.m_inlets.push_back(port);
206 self.controlAdded(port->id());
207 return port;
208 }
209
210 Process::Inlet* operator()(const point3d_input& v)
211 {
212 auto nm = QString::fromStdString(input.name);
213 ossia::vec3f min{-100., -100., -100.};
214 ossia::vec3f max{100., 100., 100.};
215 ossia::vec3f init{0., 0., 0.};
216 if(v.def)
217 std::copy_n(v.def->begin(), 3, init.begin());
218 if(v.min)
219 std::copy_n(v.min->begin(), 3, min.begin());
220 if(v.max)
221 std::copy_n(v.max->begin(), 3, max.begin());
222 auto port = new Process::XYZSpinboxes{
223 min, max, init, false, nm, Id<Process::Port>(i), &self};
224
225 if(auto it = previous_values.find(nm);
226 it != previous_values.end()
227 && it->second.get_type() == port->value().get_type())
228 port->setValue(it->second);
229
230 self.m_inlets.push_back(port);
231 self.controlAdded(port->id());
232 return port;
233 }
234
235 Process::Inlet* operator()(const color_input& v)
236 {
237 auto nm = QString::fromStdString(input.name);
238 ossia::vec4f init{0.5, 0.5, 0.5, 1.};
239 if(v.def)
240 {
241 std::copy_n(v.def->begin(), 4, init.begin());
242 }
243 auto port = new Process::HSVSlider(
244 init, QString::fromStdString(input.name), Id<Process::Port>(i), &self);
245
246 if(auto it = previous_values.find(nm);
247 it != previous_values.end()
248 && it->second.get_type() == port->value().get_type())
249 port->setValue(it->second);
250
251 self.m_inlets.push_back(port);
252 self.controlAdded(port->id());
253 return port;
254 }
255 Process::Inlet* operator()(const image_input& v)
256 {
257 auto port = new Gfx::TextureInlet(
258 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
259
260 // vertexshaderart's `touch`, in any shader format: the window's touch
261 // history (PointerAddresses.hpp, TouchHistoryNode), read through a
262 // target of its own size and format, NEAREST, clamped
263 // (src/js/main.js: 32 x 240, FLOAT, NEAREST, CLAMP_TO_EDGE). All of it
264 // is set on the inlet, where it shows and can be changed.
265 if(input.name == "touch" && v.dimensions == 2 && !v.is_array && !v.is_static
266 && !v.depth)
267 {
268 if(const QString win = ISFHelpers::firstWindowDeviceName(self); !win.isEmpty())
269 {
270 port->setAddress(
271 State::AddressAccessor{State::Address{win, {"touch", "history"}}});
272 port->setRenderSize(QSize{32, 240});
273 port->setTextureFormat(ossia::texture_format::RGBA32F);
274 port->setTextureFilter(ossia::texture_filter::NEAREST);
275 port->setTextureAddressMode(ossia::texture_address_mode::CLAMP_TO_EDGE);
276 }
277 }
278
279 self.m_inlets.push_back(port);
280 return port;
281 }
282 Process::Inlet* operator()(const cubemap_input& v)
283 {
284 auto port = new Gfx::TextureInlet(
285 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
286
287 self.m_inlets.push_back(port);
288 return port;
289 }
290 Process::Inlet* operator()(const audio_input& v)
291 {
292 auto port = new Process::AudioInlet(
293 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
294 self.m_inlets.push_back(port);
295 return port;
296 }
297 Process::Inlet* operator()(const audioFFT_input& v)
298 {
299 auto port = new Process::AudioInlet(
300 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
301 self.m_inlets.push_back(port);
302 return port;
303 }
304 Process::Inlet* operator()(const audioHist_input& v)
305 {
306 auto port = new Process::AudioInlet(
307 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
308 self.m_inlets.push_back(port);
309 return port;
310 }
311
312 // CSF-specific input handlers
313 Process::Inlet* operator()(const storage_input& v)
314 {
315 // Mirror the renderer (isf_input_port_vis in ISFNode.cpp): the access
316 // qualifier decides inlet vs outlet. Treating every storage_input as a
317 // read inlet would give write buffers a phantom TextureInlet, shifting
318 // every later port by one (positional routing) and never exposing the
319 // TextureOutlet the renderer produces.
320 if(v.access == "read_only")
321 {
322 // read inlet: an upstream Buffer-producing node (ScenePreprocessor's
323 // scene_* auxes, ExtractBuffer2 outputs, ...) has a target to land on.
324 // For aux-named storage_inputs the RawRaster renderer also auto-binds
325 // by name, so this inlet is optional but allows explicit wiring.
326 auto port = new Gfx::TextureInlet(
327 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
328 self.m_inlets.push_back(port);
329 return port;
330 }
331
332 // write_only / read_write: the renderer pushes a Buffer output port for
333 // the produced SSBO so downstream nodes can connect to it.
334 auto outport = new Gfx::TextureOutlet(
335 QString::fromStdString(input.name), Id<Process::Port>(outlet_id++),
336 &self);
337 self.m_outlets.push_back(outport);
338
339 // Conditional sizing inlet: only buffers whose layout ends in a
340 // flexible-array member synthesize a "size" control -- the same
341 // condition as CSF/Process.cpp setupCSF, the renderer and the
342 // generated GLSL.
343 if(!v.layout.empty()
344 && v.layout.back().type.find("[]") != std::string::npos)
345 {
346 auto size_inl = new Process::IntSpinBox{
347 1, 536870911, 1024,
348 QString::fromStdString(input.name) + " size",
349 Id<Process::Port>(i), &self};
350 self.m_inlets.push_back(size_inl);
351 self.controlAdded(size_inl->id());
352 return size_inl;
353 }
354 return nullptr;
355 }
356 Process::Inlet* operator()(const uniform_input& v)
357 {
358 // uniform_input expects an upstream Buffer port (ScenePreprocessor's
359 // camera/env aux buffers, ExtractBuffer2 outputs, etc.). TextureInlet
360 // is score's Process-layer inlet for SSBO / texture / UBO data flow.
361 // Without this, the Process model has no inlet for the cable to land
362 // on and Score.inlet(proc, i) returns null.
363 auto port = new Gfx::TextureInlet(
364 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
365 self.m_inlets.push_back(port);
366 return port;
367 }
368 Process::Inlet* operator()(const texture_input& v)
369 {
370 // The renderer (isf_input_port_vis) creates an Image input port for
371 // every texture_input, so the model must create the matching inlet:
372 // returning nullptr here would shift every subsequent port.
373 auto port = new Gfx::TextureInlet(
374 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
375 self.m_inlets.push_back(port);
376 return port;
377 }
378 Process::Inlet* operator()(const csf_image_input& v)
379 {
380 // Mirror the renderer: read_only → input port (an upstream texture
381 // cable lands on it); write_only / read_write → output port for the
382 // produced storage image. Always creating an inlet would give write
383 // images a phantom inlet (port shift) and no outlet for downstream
384 // connection.
385 if(v.access == "read_only")
386 {
387 auto port = new Gfx::TextureInlet(
388 QString::fromStdString(input.name), Id<Process::Port>(i), &self);
389 self.m_inlets.push_back(port);
390 return port;
391 }
392 auto outport = new Gfx::TextureOutlet(
393 QString::fromStdString(input.name), Id<Process::Port>(outlet_id++),
394 &self);
395 self.m_outlets.push_back(outport);
396 return nullptr;
397 }
398 Process::Inlet* operator()(const geometry_input& v) { return nullptr; }
399 };
400
401 // Outlet ids for write-access storage / image inputs. Base 20000 keeps
402 // them clear of inlet ids (input index), the default outlet (id 1) and the
403 // MRT base (10000), and lets the MRT block below tell them apart.
404 static constexpr int storage_outlet_base = 20000;
405 int outlet_id = storage_outlet_base;
406
407 for(const isf::input& input : desc.inputs)
408 {
409 ossia::visit(
410 input_vis{
411 previous_values, input, i, self, outlet_id,
412 desc.mode == isf::descriptor::VSA},
413 input.data);
414 i++;
415 }
416
417 // The renderer (isf_input_port_vis) pushes write-storage / write-image
418 // output ports first (in input order), then the color / MRT outputs. The
419 // model's outlets must follow the same order for positional routing. The
420 // default "Texture Out" outlet was created by the constructor *before* this
421 // loop, so it currently sits ahead of any storage outlets — pull the
422 // storage outlets (ids >= storage_outlet_base) to the front to match.
423 {
424 std::stable_partition(
425 self.m_outlets.begin(), self.m_outlets.end(),
426 [](Process::Outlet* o) { return o->id().val() >= storage_outlet_base; });
427 }
428
429 // MRT: recreate the color outlets from OUTPUTS declarations. Preserve the
430 // storage / image write outlets (ids >= storage_outlet_base); only the
431 // color / default outlets are replaced.
432 if(!desc.outputs.empty())
433 {
434 for(auto it = self.m_outlets.begin(); it != self.m_outlets.end();)
435 {
436 if((*it)->id().val() < storage_outlet_base)
437 {
438 delete *it;
439 it = self.m_outlets.erase(it);
440 }
441 else
442 {
443 ++it;
444 }
445 }
446
447 int outId = 10000; // High base to avoid ID collisions with inlets
448 for(const auto& out : desc.outputs)
449 {
450 self.m_outlets.push_back(new Gfx::TextureOutlet{
451 QString::fromStdString(out.name),
452 Id<Process::Port>(outId++), &self});
453 }
454 }
455 }
456};
457}
Definition Port.hpp:356
Definition Port.hpp:221
Definition Port.hpp:329
The id_base_t class.
Definition Identifier.hpp:59
Binds the rendering pipeline to ossia processes.
Definition AssetTable.cpp:7
Process::ControlInlet * makeLongInputControl(const isf::long_input &v, const QString &name, Id< Process::Port > id, QObject *parent)
The control of an ISF / CSF / geometry-filter long input.
Definition LongInputControl.hpp:23
Definition ISFProcess.hpp:26
static QString firstWindowDeviceName(T &self)
The first window device of the document holding self, or empty.
Definition ISFProcess.hpp:29
static void setupISFModelPorts(T &self, const isf::descriptor &desc, const ossia::flat_map< QString, ossia::value > &previous_values)
Definition ISFProcess.hpp:76
Static metadata implementation.
Definition lib/score/tools/Metadata.hpp:36
Definition score-lib-process/Process/ProcessMetadata.hpp:36
Definition Address.hpp:136
The Address struct.
Definition Address.hpp:81