Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
90d07ef
threedim: add the PrimitiveCloud point-cloud / Gaussian-splat family
jcelerier Jul 15, 2026
1135fea
tests: add a render-regression mode + sweep driver to the object gallery
jcelerier Jul 15, 2026
5bd71d8
tests: render-regression for the CSF/ISF shader corpus
jcelerier Jul 16, 2026
7a43501
tests: glTF/FBX/OBJ + point-cloud loader unit tests + coverage flush
jcelerier Jul 17, 2026
59cbf92
gfx: fail the render target instead of aborting the process
jcelerier Aug 16, 2026
6d73619
tests: AssetLoader point-cloud dispatch and format override (f0a202a782)
jcelerier Aug 18, 2026
2a6ad5d
tests: register CaptureAdjustShaderTest and the ObjectGallery sweeps …
jcelerier Aug 18, 2026
e49202b
tests: hard-exit ObjectGallery's --shader non-success paths too
jcelerier Aug 18, 2026
8289a69
tests: threedim scene algebra, camera array, fisheye, cascades and tw…
jcelerier Aug 18, 2026
cdd2ef4
tests: multi-window output mapping — serialization and editing canvases
jcelerier Aug 19, 2026
03b0c90
tests: video colour-space and tone-mapping GLSL generation
jcelerier Aug 19, 2026
5e3c664
tests: GPU interop capability ladder predicates
jcelerier Aug 19, 2026
0266bb2
tests: test card rendering
jcelerier Aug 19, 2026
9dc75f3
tests: GPU video decoder selection matrix
jcelerier Aug 19, 2026
72c5c65
tests: presented-swapchain outputs — ScreenNode, Window and MultiWind…
jcelerier Aug 19, 2026
2175440
tests: do not register the shell harnesses on Windows
jcelerier Aug 19, 2026
030a1db
gfx/pipewire: do not dequeue from a stream that is not streaming
jcelerier Aug 19, 2026
5259b39
tests: the multi-window output preview windows
jcelerier Aug 19, 2026
134b83c
tests: the window output device settings widget
jcelerier Aug 19, 2026
6ab3501
tests: the FFmpeg device settings widget and its presets
jcelerier Aug 19, 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
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,9 @@
[submodule "3rdparty/OffsetAllocator"]
path = 3rdparty/OffsetAllocator
url = https://github.com/sebbbi/OffsetAllocator
[submodule "3rdparty/spz"]
path = 3rdparty/spz
url = https://github.com/nianticlabs/spz
[submodule "3rdparty/zstd"]
path = 3rdparty/zstd
url = https://github.com/facebook/zstd
1 change: 1 addition & 0 deletions 3rdparty/3rdparty.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ include(3rdparty/mimalloc.cmake)
include(3rdparty/sh4lt.cmake)
include(3rdparty/shmdata.cmake)
include(3rdparty/snappy.cmake)
include(3rdparty/zstd.cmake)
endif()
1 change: 1 addition & 0 deletions 3rdparty/spz
Submodule spz added at affd0e
1 change: 1 addition & 0 deletions 3rdparty/zstd
Submodule zstd added at 82d322
49 changes: 49 additions & 0 deletions 3rdparty/zstd.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
if(SCORE_USE_SYSTEM_LIBRARIES)
find_package(zstd GLOBAL CONFIG)
endif()

if(NOT TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared AND NOT TARGET zstd)
set(ZSTD_BUILD_PROGRAMS OFF CACHE INTERNAL "" FORCE)
set(ZSTD_BUILD_TESTS OFF CACHE INTERNAL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE INTERNAL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE INTERNAL "" FORCE)
set(ZSTD_BUILD_DICTBUILDER OFF CACHE INTERNAL "" FORCE)

set(old_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS OFF)

if(NOT MSVC AND NOT CMAKE_CROSSCOMPILING)
if(CMAKE_BUILD_TYPE MATCHES ".*Deb.*")
set(old_CFLAGS "${CMAKE_C_FLAGS}")
set(old_CXXFLAGS "${CMAKE_CXX_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -march=native")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native")
endif()
endif()

add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/zstd/build/cmake" EXCLUDE_FROM_ALL)

if(NOT MSVC AND NOT CMAKE_CROSSCOMPILING)
if(CMAKE_BUILD_TYPE MATCHES ".*Deb.*")
set(CMAKE_C_FLAGS "${old_CFLAGS}")
set(CMAKE_CXX_FLAGS "${old_CXXFLAGS}")
endif()
endif()

set(BUILD_SHARED_LIBS ${old_BUILD_SHARED_LIBS})
endif()

# Make later find_package(zstd) calls (e.g. 3rdparty/spz) resolve to the
# targets configured above: some prebuilt SDKs ship zstd configs pointing to
# files that do not exist, and a not-found result would trigger FetchContent
# fallbacks that clash with the vendored targets.
file(WRITE "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/zstd-config.cmake" [=[
if(TARGET libzstd_static AND NOT TARGET zstd::libzstd_static)
add_library(zstd::libzstd_static INTERFACE IMPORTED GLOBAL)
target_link_libraries(zstd::libzstd_static INTERFACE libzstd_static)
endif()
if(TARGET libzstd_shared AND NOT TARGET zstd::libzstd_shared)
add_library(zstd::libzstd_shared INTERFACE IMPORTED GLOBAL)
target_link_libraries(zstd::libzstd_shared INTERFACE libzstd_shared)
endif()
]=])
8 changes: 8 additions & 0 deletions src/plugins/score-plugin-gfx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,14 @@ if(SCORE_TESTING)
target_link_libraries(CaptureAdjustShaderTest PRIVATE
score_plugin_gfx ${QT_PREFIX}::Gui)

# Needs a GL-capable display; prints "no RHI available, skipping" and exits 0
# without one. Labelled "gui" like EncoderTester / ReadbackTester below.
add_test(NAME CaptureAdjustShaderTest COMMAND CaptureAdjustShaderTest)
set_tests_properties(CaptureAdjustShaderTest PROPERTIES
WORKING_DIRECTORY "${SCORE_ROOT_BINARY_DIR}"
ENVIRONMENT "SCORE_AUDIO_BACKEND=dummy;ASAN_OPTIONS=detect_leaks=0:halt_on_error=0"
LABELS "gui")

# The maths, the struct layout and the cross-thread slot: no GPU, no camera,
# no display.
score_add_test(CaptureAdjustTest
Expand Down
14 changes: 14 additions & 0 deletions src/plugins/score-plugin-gfx/Gfx/InvertYRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ void InvertYRenderer::init(
renderer.state, renderer.state.renderFormat, m_inputTarget.texture->pixelSize(),
renderer.samples(), renderer.requiresDepth(*this->node.input[0]));

// The backend can refuse the configuration -- createRenderTarget says so and
// hands back an empty target. There is nothing to render into, so stop here
// instead of building a pipeline around it.
if(!m_renderTarget)
{
qWarning() << "InvertYRenderer: no render target, this output will not render";
return;
}

const auto& mesh = renderer.defaultTriangle();
m_mesh = renderer.initMeshBuffer(mesh, res);

Expand Down Expand Up @@ -107,6 +116,11 @@ void InvertYRenderer::finishFrame(
score::gfx::RenderList& renderer, QRhiCommandBuffer& cb,
QRhiResourceUpdateBatch*& res)
{
// init() gives up without a render target when the backend refused to create
// one. beginPass would dereference it.
if(!m_renderTarget.renderTarget)
return;

cb.beginPass(m_renderTarget.renderTarget, Qt::black, {0.0f, 0}, res);
res = nullptr;
// m_p.pipeline is null when buildPipeline's QRhiGraphicsPipeline::create()
Expand Down
17 changes: 13 additions & 4 deletions src/plugins/score-plugin-gfx/Gfx/Pipewire/PipewireOutputDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
// caching. The copyTexture bridge is much simpler and still drops
// the CPU readback + memcpy cost.

#include <atomic>
#include "PipewireOutputDevice.hpp"

#include "PipewireFormats.hpp"
Expand Down Expand Up @@ -289,7 +290,7 @@ class PipewireProducer
if(!lp || !m_stream || m_dmabufBackend != DmaBufBackend::EglGbm)
return nullptr;
auto& pw = libremidi::pipewire::load();
if(!pw.stream_available)
if(!pw.stream_available || !m_streaming.load(std::memory_order_acquire))
return nullptr;
pw.thread_loop_lock(lp);

Expand Down Expand Up @@ -361,7 +362,7 @@ class PipewireProducer
if(!lp || !m_stream || m_dmabufBackend != DmaBufBackend::Vulkan)
return nullptr;
auto& pw = libremidi::pipewire::load();
if(!pw.stream_available)
if(!pw.stream_available || !m_streaming.load(std::memory_order_acquire))
return nullptr;
pw.thread_loop_lock(lp);
pw_buffer* b = pw.stream_dequeue_buffer(m_stream);
Expand Down Expand Up @@ -657,7 +658,7 @@ class PipewireProducer
if(!lp || !m_stream || !data || size == 0)
return false;
auto& pw = libremidi::pipewire::load();
if(!pw.stream_available)
if(!pw.stream_available || !m_streaming.load(std::memory_order_acquire))
return false;

pw.thread_loop_lock(lp);
Expand Down Expand Up @@ -693,9 +694,16 @@ class PipewireProducer

private:
static void on_state_changed(
void* /*self*/, pw_stream_state /*old*/, pw_stream_state state,
void* self_, pw_stream_state /*old*/, pw_stream_state state,
const char* error)
{
// pw_stream_dequeue_buffer dereferences stream internals that only exist
// once the stream is streaming; calling it on an UNCONNECTED or ERROR
// stream faults inside pipewire.
if(auto* self = static_cast<PipewireProducer*>(self_))
self->m_streaming.store(
state == PW_STREAM_STATE_STREAMING, std::memory_order_release);

auto& pw = libremidi::pipewire::load();
const char* s = pw.stream_state_as_string
? pw.stream_state_as_string(state)
Expand Down Expand Up @@ -894,6 +902,7 @@ class PipewireProducer
// start(). The thread_loop, pw_context and pw_core live here.
std::shared_ptr<libremidi::pipewire::context> m_shared;
pw_stream* m_stream{};
std::atomic_bool m_streaming{false};
spa_hook m_listener{};

#if defined(SCORE_PIPEWIRE_OUT_DMABUF)
Expand Down
32 changes: 31 additions & 1 deletion src/plugins/score-plugin-threedim/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ if(NOT TARGET fastgltf)
add_subdirectory("${3RDPARTY_FOLDER}/fastgltf" "${CMAKE_CURRENT_BINARY_DIR}/fastgltf" EXCLUDE_FROM_ALL)
endif()

# spz — Niantic / Adobe reference SPZ decoder for compressed 3DGS files.
# v1-3 only (v4/ZSTD stubbed; see 3rdparty/spz/CMakeLists.txt). Pulls
# in zlib via ZLIB::ZLIB.
if(NOT TARGET spz)
set(SPZ_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(SPZ_BUILD_PYTHON_BINDINGS OFF CACHE BOOL "" FORCE)
set(SPZ_BUILD_EXTENSIONS OFF CACHE BOOL "" FORCE)
add_subdirectory("${3RDPARTY_FOLDER}/spz" "${CMAKE_CURRENT_BINARY_DIR}/spz" EXCLUDE_FROM_ALL)

if(MSVC)
# The prebuilt sysroot's zconf.h includes <unistd.h> unconditionally;
# provide a stub so spz's zlib usage compiles with MSVC / clang-cl.
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/spz-msvc-compat/unistd.h"
"#include <io.h>\n#include <process.h>\n")
target_include_directories(spz PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/spz-msvc-compat")
endif()
endif()

# libssynth
add_library(
ssynth STATIC
Expand Down Expand Up @@ -158,6 +176,8 @@ add_library(
Threedim/InjectBuffer.cpp
Threedim/InjectTexture.hpp
Threedim/InjectTexture.cpp
Threedim/TagAs.hpp
Threedim/TagAs.cpp
Threedim/PBRMesh.hpp
Threedim/PBRMesh.cpp
Threedim/MaterialOverride.hpp
Expand Down Expand Up @@ -199,6 +219,16 @@ add_library(
Threedim/PCLToGeometry.cpp
Threedim/Ply.hpp
Threedim/Ply.cpp
Threedim/PrimitiveCloud/PlyParser.hpp
Threedim/PrimitiveCloud/PlyParser.cpp
Threedim/PrimitiveCloud/SplatBinary.hpp
Threedim/PrimitiveCloud/SplatBinary.cpp
Threedim/PrimitiveCloud/SpzCodec.hpp
Threedim/PrimitiveCloud/SpzCodec.cpp
Threedim/PrimitiveCloud/SceneFromCloud.hpp
Threedim/PrimitiveCloud/SceneFromCloud.cpp
Threedim/PrimitiveCloud/FormatOverride.hpp
Threedim/PrimitiveCloud/FormatOverride.cpp
Threedim/Primitive.hpp
Threedim/Primitive.cpp
Threedim/StructureSynth.hpp
Expand Down Expand Up @@ -255,4 +285,4 @@ target_include_directories(
target_link_libraries(
score_plugin_threedim PRIVATE score_plugin_engine score_plugin_avnd
score_plugin_gfx fmt::fmt ssynth Eigen3::Eigen
fastgltf::fastgltf)
fastgltf::fastgltf spz)
95 changes: 80 additions & 15 deletions src/plugins/score-plugin-threedim/Threedim/AssetLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
#include "FbxParser.hpp"
#include "GltfParser.hpp"
#include "Ply.hpp"
#include "PrimitiveCloud/FormatOverride.hpp"
#include "PrimitiveCloud/PlyParser.hpp"
#include "PrimitiveCloud/SceneFromCloud.hpp"
#include "PrimitiveCloud/SplatBinary.hpp"
#include "PrimitiveCloud/SpzCodec.hpp"
#include "SceneFromMeshes.hpp"
#include "VcgImporters.hpp"

Expand Down Expand Up @@ -155,12 +160,7 @@ AssetLoader::ins::asset_t::process(file_type tv)
}
else if(hasSuffixCI(fname, "gltf") || hasSuffixCI(fname, "glb"))
{
auto t0 = std::chrono::steady_clock::now();
loaded = runInnerParser<GltfParser>(tv, &GltfParser::ins::gltf_t::process);
auto t1 = std::chrono::steady_clock::now();
qDebug() << "LOADING TIME"
<< (std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0) / 1e6)
.count();
}
else if(hasSuffixCI(fname, "obj"))
{
Expand All @@ -176,14 +176,32 @@ AssetLoader::ins::asset_t::process(file_type tv)
}
else if(hasSuffixCI(fname, "ply"))
{
Threedim::float_vec buf;
auto meshes = Threedim::PlyFromFile(fname, buf);
if(!meshes.empty())
// Sniff the header first: a PLY whose vertex element carries
// splat-style columns (or no face element) goes through the
// primitive-cloud path; everything else stays on the existing
// mesh path. The sniff only reads the textual header, no row data.
if(Threedim::PrimitiveCloud::ply_is_splat_shaped(fname))
{
const QString label = QFileInfo(QString::fromStdString(std::string{fname}))
.fileName();
loaded = Threedim::sceneStateFromMeshes(
std::move(meshes), std::move(buf), label.toStdString());
auto cloud = Threedim::PrimitiveCloud::parse_ply(fname);
if(cloud)
{
const QString label
= QFileInfo(QString::fromStdString(std::string{fname})).fileName();
loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
std::move(cloud), label.toStdString());
}
}
else
{
Threedim::float_vec buf;
auto meshes = Threedim::PlyFromFile(fname, buf);
if(!meshes.empty())
{
const QString label
= QFileInfo(QString::fromStdString(std::string{fname})).fileName();
loaded = Threedim::sceneStateFromMeshes(
std::move(meshes), std::move(buf), label.toStdString());
}
}
}
else if(hasSuffixCI(fname, "stl"))
Expand All @@ -210,6 +228,34 @@ AssetLoader::ins::asset_t::process(file_type tv)
std::move(meshes), std::move(buf), label.toStdString());
}
}
else if(hasSuffixCI(fname, "splat"))
{
// Antimatter15 binary .splat: 32 bytes/primitive, fixed schema.
auto cloud = Threedim::PrimitiveCloud::parse_splat_binary(tv.bytes);
if(cloud)
{
const QString label
= QFileInfo(QString::fromStdString(std::string{fname})).fileName();
loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
std::move(cloud), label.toStdString());
}
}
else if(hasSuffixCI(fname, "spz"))
{
// Niantic .spz v1-3: gzip-compressed column-grouped 3DGS data.
// Decoded via the vendored Niantic library (3rdparty/spz),
// transposed into the canonical 62-float row layout that the
// 3dgs.classic preset reads. v4 (NGSP-magic + ZSTD) returns
// nullptr — see 3rdparty/spz/CMakeLists.txt for the rationale.
auto cloud = Threedim::PrimitiveCloud::parse_spz(tv.bytes);
if(cloud)
{
const QString label
= QFileInfo(QString::fromStdString(std::string{fname})).fileName();
loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
std::move(cloud), label.toStdString());
}
}
else
{
// Built-ins all missed — consult the addon-registered parsers.
Expand All @@ -223,21 +269,33 @@ AssetLoader::ins::asset_t::process(file_type tv)
return {};

return [state = std::move(loaded)](AssetLoader& self) mutable {
self.m_raw_state = std::move(state);
self.m_parsed_state = std::move(state);
self.rebuild_format_state(); // m_parsed → m_overridden
self.m_cached_xform.valid = false; // force wrap rebuild
self.rebuild_wrapped_state();
};
}

void AssetLoader::rebuild_format_state()
{
m_cached_format_override = inputs.format_override.value;
m_overridden_state = Threedim::PrimitiveCloud::applyFormatOverride(
m_parsed_state, m_cached_format_override);
// The wrapped state derives from m_overridden_state and must be
// rebuilt whenever the override changes.
m_cached_xform.valid = false;
rebuild_wrapped_state();
}

void AssetLoader::rebuild_wrapped_state()
{
m_wrapped_state = Threedim::wrapSceneWithTransform(
m_raw_state, inputs, m_cached_xform, m_version_counter, m_xform_ref);
m_overridden_state, inputs, m_cached_xform, m_version_counter, m_xform_ref);
}

void AssetLoader::operator()()
{
if(!m_raw_state)
if(!m_parsed_state)
{
outputs.scene_out.scene.state = nullptr;
outputs.scene_out.dirty = 0;
Expand Down Expand Up @@ -298,6 +356,13 @@ void AssetLoader::release(score::gfx::RenderList& r)
if(raw_transform_slot.valid())
r.registry().free(raw_transform_slot);
m_xform_ref = {};
// Clear cached scene_state so the next operator()() rebuilds against
// the post-release registry. Producer-state-drift Option A — see
// matching comment in Light::release. m_parsed_state stays valid
// (parser output, no slot refs); only m_overridden_state and
// m_wrapped_state embed registry refs and need clearing.
m_overridden_state.reset();
m_wrapped_state.reset();
}

} // namespace Threedim
Loading