Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
838c6f4
cmake: only use -gcodeview on clang
jcelerier Aug 16, 2026
c976ba6
js: report --script failures instead of exiting 0
jcelerier Aug 16, 2026
9e3bebd
gfx: make headless frame capture available from master
jcelerier Aug 16, 2026
a16977f
gfx: render a fixed number of frames on demand
jcelerier Aug 16, 2026
0b43ceb
app: keep the platform the caller asked for when there is no display
jcelerier Aug 16, 2026
89243e1
gfx: do not return a Vulkan instance when creation failed
jcelerier Aug 16, 2026
d5e6cc9
app: treat offscreen as a rendering session, like eglfs and vkkhrdisplay
jcelerier Aug 16, 2026
dfd8f94
gfx: don't create a Vulkan QRhi without an instance
jcelerier Jul 20, 2026
f03613e
gfx: don't return a half-initialized render state when QRhi::create f…
jcelerier Jul 5, 2026
5588564
js: resolve the library prefixes in createProcess paths
jcelerier Aug 16, 2026
eb0c242
gfx: let a test select the offscreen window device by name
jcelerier Aug 16, 2026
e9ca948
gfx: step the process clock when rendering frames on demand
jcelerier Aug 16, 2026
4ef0e22
tests: make the determinism gate prove it rendered anything
jcelerier Aug 16, 2026
c0400df
js: let a script's exit code reach the caller
jcelerier Aug 16, 2026
0b6f3a3
gfx: let a script choose the frame-step rate
jcelerier Aug 16, 2026
4d9157a
tests: end the determinism runs instead of timing them out
jcelerier Aug 16, 2026
8fb210c
tests: sweep the whole tester corpus and say what came out
jcelerier Aug 16, 2026
3a91a00
gfx: emit #version 460 for generated shaders
jcelerier Aug 16, 2026
3a984d8
tests: report a crashed case as CRASH rather than a bare exit code
jcelerier Aug 16, 2026
a4b31c6
tests: report a skipped test as skipped
jcelerier Aug 17, 2026
be5f960
tests: let a test binary find its plug-ins from any directory
jcelerier Aug 17, 2026
b379c61
app: do not ask to save when there is no GUI to answer
jcelerier Aug 17, 2026
6288d39
score: add a settings lookup that reports absence instead of aborting
jcelerier Aug 17, 2026
1ccb27d
tests: give the shader sweep a real failure instead of an abort
jcelerier Aug 17, 2026
d2af484
tests: give each shader sweep the MODE it owns
jcelerier Aug 17, 2026
e846536
tests: drop the QtTest dependency, the suite is Catch2
jcelerier Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions cmake/ScoreCodeviewWindows.cmake
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
if(WIN32)
# Clang only. GCC's CodeView writer segfaults on any boost::container type,
# which this codebase uses in around thirty translation units, so -gcodeview
# makes a gcc Debug or RelWithDebInfo build impossible to complete:
#
# $ cat REPRO.cpp
# #include <boost/container/vector.hpp>
# void f() { boost::container::vector<int> v; (void)v; }
# $ g++ -gcodeview -c REPRO.cpp
# REPRO.cpp:2:54: internal compiler error: Segmentation fault
#
# Reproduced on gcc 16.2.0 (MSYS2 UCRT64) with boost 1.91. The flag alone is
# enough -- no optimisation level or standard setting is involved -- and DWARF
# is unaffected, as is clang. The crash is in the type-record emission reached
# from dwarf2out_finish, which is why every report points at the last line of
# the file.
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
if(NOT (CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"))
set(SCORE_COMPILER_NEEDS_GCODEVIEW 1)
endif()
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
set(SCORE_COMPILER_NEEDS_GCODEVIEW 1)
endif()

if (SCORE_COMPILER_NEEDS_GCODEVIEW)
Expand Down
12 changes: 12 additions & 0 deletions cmake/ScoreTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,23 @@ function(score_add_test NAME)
add_test(NAME ${NAME} COMMAND ${NAME})
endif()

# Catch2 exits with 4 when every test case in the binary was skipped
# (AllTestsSkippedExitCode, catch_session.cpp). A test that skips because its
# precondition is absent -- no display, no shader library, no capture device --
# is not a defect, and counting it as one silently inflates the failure count.
set_tests_properties(${NAME} PROPERTIES SKIP_RETURN_CODE 4)

# App/integration tests rely on runtime dynamic-plugin discovery from
# "<cwd>/plugins": run them from the build root where <build>/plugins lives.
if(ARG_APP OR ARG_GUI)
set_tests_properties(${NAME} PROPERTIES
WORKING_DIRECTORY "${SCORE_ROOT_BINARY_DIR}")

# ...and tell the fixture where that is, so running the executable by hand
# from some other directory boots the same application instead of one with
# no plug-ins at all. See prepare_test_environment().
target_compile_definitions(${NAME} PRIVATE
"SCORE_TEST_BINARY_DIR=\"${SCORE_ROOT_BINARY_DIR}\"")
endif()

if(ARG_APP)
Expand Down
3 changes: 2 additions & 1 deletion src/app/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ bool runningUnderAnUISession() noexcept
if(qgetenv("XDG_SESSION_TYPE") != "tty")
return true;
if(platform.contains("gl") || platform.contains("vkkhr")
|| platform.contains("linuxfb") || platform.contains("vnc"))
|| platform.contains("linuxfb") || platform.contains("vnc")
|| platform.contains("offscreen"))
return true;
return false;
#endif
Expand Down
8 changes: 6 additions & 2 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,12 @@ static void setup_x11(int argc, char** argv)

if(!x11 && !wayland)
{
// Try eglfs
qputenv("QT_QPA_PLATFORM", "eglfs");
// Try eglfs -- unless a platform was asked for. Overwriting it here sent
// QT_QPA_PLATFORM=offscreen to eglfs, which finds no display device, and
// Qt then qFatal()s on the first window for having no screens. A headless
// machine asking for offscreen has to get offscreen.
if(!has_platform)
qputenv("QT_QPA_PLATFORM", "eglfs");
return;
}
static constexpr auto setup_x11_error_handling = [] {
Expand Down
11 changes: 9 additions & 2 deletions src/lib/core/presenter/DocumentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,15 @@ void DocumentManager::setCurrentDocument(
bool DocumentManager::closeDocument(
const score::GUIApplicationContext& ctx, Document& doc)
{
// Warn the user if he might loose data
if(!doc.commandStack().isAtSavedIndex())
// Warn the user if he might loose data. Only when there is a user: with
// applicationSettings.gui false (headless, --script, offscreen QPA) nothing
// can answer a modal, and QMessageBox::exec() aborts instead of returning.
// Every score::MessageBox helper already guards on this same flag; this call
// site was the one raw QMessageBox left, which is why a scripted /exit on a
// modified document died in teardown with SIGABRT. No GUI means no one to
// save for, so proceed as Discard -- the same outcome forceExit() already
// produces, since it quits 500ms later whatever the answer would have been.
if(!doc.commandStack().isAtSavedIndex() && ctx.applicationSettings.gui)
{
QMessageBox msgBox;
msgBox.setText(tr("The document has been modified."));
Expand Down
25 changes: 21 additions & 4 deletions src/lib/score/application/ApplicationContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,34 @@ struct SCORE_LIB_BASE_EXPORT ApplicationContext
*/
template <typename T>
T& settings() const
{
if(auto c = findSettings<T>())
return *c;

SCORE_ABORT;
throw;
}

/**
* @brief Access a Settings model instance, or null when its plug-in did not
* register one.
*
* settings() aborts the process in that case, which is the right answer for
* application code (a missing settings model means the plug-in it belongs to
* is not loaded, and nothing downstream can work). Callers that can report
* the situation themselves want the null instead.
*/
template <typename T>
T* findSettings() const noexcept
{
for(auto& elt : this->m_settings)
{
if(auto c = dynamic_cast<T*>(elt.get()))
{
return *c;
return c;
}
}

SCORE_ABORT;
throw;
return nullptr;
}

const auto& allSettings() const noexcept { return m_settings; }
Expand Down
8 changes: 8 additions & 0 deletions src/lib/score/gfx/Vulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ QVulkanInstance* staticVulkanInstance(bool create)
if(!instance.create())
{
g_staticVulkanInstanceInvalid = true;
delete g_staticVulkanInstance;
g_staticVulkanInstance = nullptr;
}
});

// Re-check: on the very first call, create() may just have failed inside
// call_once — returning the half-initialized instance would send callers
// (Graph's API-fallback check, QRhi::create) straight into a crash.
if(g_staticVulkanInstanceInvalid)
return nullptr;

return g_staticVulkanInstance;
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/lib/score/tools/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ QString addUniqueSuffix(const QString& fileName)
}
}

QString locateFilePath(const QString& filename) noexcept
{
if(filename.startsWith("<LIBRARY>:"))
{
QSettings set;
QString path = filename;
path.replace("<LIBRARY>:", set.value("Library/RootPath").toString() + "/");
return QFileInfo{path}.absoluteFilePath();
}
return filename;
}

QString
locateFilePath(const QString& filename, const score::DocumentContext& ctx) noexcept
{
Expand Down
7 changes: 7 additions & 0 deletions src/lib/score/tools/FilePath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ SCORE_LIB_BASE_EXPORT
QString
locateFilePath(const QString& filename, const score::DocumentContext& ctx) noexcept;

//! Same, for callers that run before any document exists -- a --script file is
//! read while the application is still starting up. <LIBRARY>: is a settings
//! lookup and resolves fine; <PROJECT>: and relative paths have no document to
//! resolve against and are returned untouched.
SCORE_LIB_BASE_EXPORT
QString locateFilePath(const QString& filename) noexcept;

//! Will try to convert an absolute path
//! in a relative path from the document's point of view
SCORE_LIB_BASE_EXPORT
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/score-plugin-gfx/3rdparty/libisf/src/isf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace
{
static constexpr struct glsl45_t
{
static constexpr auto versionPrelude = R"_(#version 450
static constexpr auto versionPrelude = R"_(#version 460
)_";

static constexpr auto vertexPrelude = R"_(
Expand Down Expand Up @@ -3515,7 +3515,7 @@ void parser::parse_csf()
m_fragment.clear();

// Add version
m_fragment += "#version 450\n\n";
m_fragment += "#version 460\n\n";

// Add standard ProcessUBO uniforms (same as ISF/VSA)
m_fragment += GLSL45.defaultUniforms;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/score-plugin-gfx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ set(HDRS
Gfx/Window/CollapsibleSection.hpp
Gfx/Window/DesktopLayout.hpp
Gfx/Window/MultiWindowDevice.hpp
Gfx/Window/OffscreenDevice.hpp
Gfx/Window/OutputMapping.hpp
Gfx/Window/OutputPreview.hpp
Gfx/Window/TestCard.hpp
Expand Down
44 changes: 44 additions & 0 deletions src/plugins/score-plugin-gfx/Gfx/GfxContext.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <Gfx/GfxContext.hpp>
#include <Gfx/Graph/Graph.hpp>
#include <Gfx/Graph/Node.hpp>
#include <Gfx/Graph/OutputNode.hpp>
#include <Gfx/Settings/Model.hpp>

Expand All @@ -10,6 +11,8 @@
#include <score/tools/Timers.hpp>

#include <ossia/detail/flicks.hpp>

#include <cmath>
#include <ossia/detail/logger.hpp>
#include <ossia/detail/thread.hpp>

Expand Down Expand Up @@ -492,6 +495,47 @@ void GfxContext::on_watchdog_timer(score::HighResolutionTimer* self)
updateGraph();
}

void GfxContext::renderFrames(int frames)
{
if(frames <= 0 || !m_graph)
return;

const bool step = m_stepRate > 0.;
const int64_t frame_flicks
= step ? int64_t(std::llround(ossia::flicks_per_second<double> / m_stepRate)) : 0;
// Held for the whole call so PROGRESS sweeps 0..1 across it rather than
// restarting on every frame.
const ossia::time_value span{frame_flicks * (m_stepFrame + frames)};

for(int i = 0; i < frames; i++)
{
// Same order as the timer-driven path: parameters first, then draw, so a
// value written by the script is visible in the frame that follows it.
updateGraph();

// After updateGraph, which would otherwise overwrite the UBO with the date
// the transport last sent.
if(step)
{
const score::gfx::Timings tk{
.date = ossia::time_value{frame_flicks * m_stepFrame},
.parent_duration = span};
for(auto& [id, node] : nodes)
{
if(auto proc = dynamic_cast<score::gfx::ProcessNode*>(node.get()))
proc->process(tk);
}
m_stepFrame++;
}

for(auto output : m_graph->outputs())
{
if(output && output->canRender())
output->render();
}
}
}

void GfxContext::on_manual_timer(score::HighResolutionTimer* self)
{
if(auto ptr = m_manualTimers.find(self); ptr != m_manualTimers.end())
Expand Down
32 changes: 32 additions & 0 deletions src/plugins/score-plugin-gfx/Gfx/GfxContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,35 @@ class SCORE_PLUGIN_GFX_EXPORT GfxContext : public QObject
void update_inputs();
void updateGraph();

/**
* @brief Render exactly @p frames times, synchronously, and return.
*
* The normal path renders off wall-clock timers, so "how many frames have I
* drawn" depends on how long the caller happened to wait and how fast the
* machine is -- a harness that sleeps and then grabs gets a different frame
* on a Raspberry Pi than on a workstation, and an animated shader gives a
* different image every run.
*
* Stepping instead makes frame N mean the same thing everywhere, which is
* what lets a rendered frame be compared against a stored reference at all.
*
* Each step also hands every process node a synthetic token of
* `frame / stepRate()`, so TIME, TIMEDELTA, FRAMEINDEX and PROGRESS follow the
* counter rather than whatever the transport last delivered. Without it an
* animated shader keeps reading the execution clock and frame N is a
* different picture every run.
*
* Still on the execution clock: whatever a node takes from the transport
* itself rather than from its process UBO -- video decode position,
* automation -- so a graph built on those is only as reproducible as that
* clock is.
*/
void renderFrames(int frames);

//! Step used by renderFrames(), in frames per second.
double stepRate() const noexcept { return m_stepRate; }
void setStepRate(double fps) noexcept { m_stepRate = fps; }

void send_message(score::gfx::Message&& msg) noexcept
{
tick_messages.enqueue(std::move(msg));
Expand All @@ -102,6 +131,9 @@ class SCORE_PLUGIN_GFX_EXPORT GfxContext : public QObject
score::gfx::Graph* m_graph{};
QThread m_thread;

double m_stepRate{60.};
int64_t m_stepFrame{};

struct NodeCommand
{
enum
Expand Down
Loading
Loading