• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30593138169

31 Jul 2026 12:17AM UTC coverage: 18.537%. First build
30593138169

Pull #2123

github

web-flow
Merge 7f26e630a into 30d7f5983
Pull Request #2123: threedim: PrimitiveCloud point-cloud / Gaussian-splat family (from #2109 stack)

311 of 375 new or added lines in 10 files covered. (82.93%)

42189 of 227589 relevant lines covered (18.54%)

5328.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.73
/src/plugins/score-plugin-threedim/Threedim/AssetLoader.cpp
1
#include "AssetLoader.hpp"
2

3
#include "FbxParser.hpp"
4
#include "GltfParser.hpp"
5
#include "Ply.hpp"
6
#include "PrimitiveCloud/FormatOverride.hpp"
7
#include "PrimitiveCloud/PlyParser.hpp"
8
#include "PrimitiveCloud/SceneFromCloud.hpp"
9
#include "PrimitiveCloud/SplatBinary.hpp"
10
#include "PrimitiveCloud/SpzCodec.hpp"
11
#include "SceneFromMeshes.hpp"
12
#include "VcgImporters.hpp"
13

14
#include <Gfx/Graph/RenderList.hpp>
15
#include <Gfx/Graph/SceneGPUState.hpp>
16

17
#include <QFileInfo>
18
#include <QQuaternion>
19
#include <QString>
20

21
#include <cstring>
22
#include <mutex>
23
#include <string>
24
#include <vector>
25

26
namespace Threedim
27
{
28

29
// =============================================================================
30
// AssetLoaderRegistry — process-wide parser dispatch table.
31
//
32
// Storage is a function-local Meyers singleton so registrations at
33
// static-init time work without worrying about dynamic-init order across
34
// translation units. The small-vector-ish layout (O(N) lookup over a
35
// ~4-entry list) is fine: registrations are one-shot per addon.
36
// =============================================================================
37
namespace
38
{
39
struct RegistryState
40
{
41
  std::mutex mutex;
42
  std::vector<std::pair<std::string, AssetLoaderRegistry::ParseFn>> entries;
43
};
44
RegistryState& registryInstance()
11✔
45
{
46
  static RegistryState s;
11✔
47
  return s;
11✔
48
}
49

50
std::string toLower(std::string_view s)
7✔
51
{
52
  std::string out;
7✔
53
  out.reserve(s.size());
7✔
54
  for(char c : s)
41✔
55
    out.push_back(char(std::tolower((unsigned char)c)));
34✔
56
  return out;
7✔
57
}
7✔
58
} // namespace
59

60
void AssetLoaderRegistry::register_parser(
6✔
61
    std::string_view extension, ParseFn fn)
62
{
63
  if(!fn || extension.empty())
6✔
64
    return;
2✔
65
  auto key = toLower(extension);
4✔
66
  auto& r = registryInstance();
4✔
67
  std::lock_guard lock{r.mutex};
4✔
68
  for(auto& e : r.entries)
7✔
69
  {
70
    if(e.first == key)
4✔
71
    {
72
      e.second = fn;  // Last writer wins.
1✔
73
      return;
1✔
74
    }
75
  }
76
  r.entries.emplace_back(std::move(key), fn);
3✔
77
}
6✔
78

79
AssetLoaderRegistry::ParseFn
80
AssetLoaderRegistry::lookup(std::string_view extension_lower) noexcept
10✔
81
{
82
  if(extension_lower.empty())
10✔
83
    return nullptr;
3✔
84
  auto& r = registryInstance();
7✔
85
  std::lock_guard lock{r.mutex};
7✔
86
  for(auto const& e : r.entries)
10✔
87
    if(e.first == extension_lower)
7✔
88
      return e.second;
4✔
89
  return nullptr;
3✔
90
}
10✔
91

92
namespace
93
{
94

95
static bool hasSuffixCI(std::string_view path, std::string_view ext) noexcept
83✔
96
{
97
  if(path.size() < ext.size() + 1)
83✔
98
    return false;
×
99
  if(path[path.size() - ext.size() - 1] != '.')
83✔
100
    return false;
44✔
101
  auto a = path.rbegin();
39✔
102
  auto b = ext.rbegin();
39✔
103
  for(; b != ext.rend(); ++a, ++b)
71✔
104
  {
105
    char x = (char)std::tolower((unsigned char)*a);
61✔
106
    char y = (char)std::tolower((unsigned char)*b);
61✔
107
    if(x != y) return false;
61✔
108
  }
32✔
109
  return true;
10✔
110
}
83✔
111

112
// Extract the lowercased suffix after the final '.' (no dot). Empty
113
// on a dotless path. Used to consult AssetLoaderRegistry after the
114
// built-in dispatch misses.
115
static std::string extensionLowerCI(std::string_view path)
4✔
116
{
117
  auto pos = path.find_last_of('.');
4✔
118
  if(pos == std::string_view::npos || pos + 1 >= path.size())
4✔
119
    return {};
1✔
120
  return toLower(path.substr(pos + 1));
3✔
121
}
4✔
122

123
// Reuse FbxParser / GltfParser's static parsers by constructing a throwaway
124
// inner instance, invoking the apply-lambda they return, and lifting the
125
// parsed raw scene_state out. No cross-frame state from the inner loader
126
// leaks into AssetLoader; its m_raw_state shared_ptr is copied into ours.
127
//
128
// Pin the file_type explicitly (halp::text_file_view — the default for
129
// every loader's halp::file_port<"..."> here). A forwarding-reference
130
// template parameter deduced from both the data arg and the function
131
// pointer's by-value parameter produces a deduction conflict
132
// (FileT& vs FileT), so we skip deduction.
133
template <typename Loader>
134
static std::shared_ptr<const ossia::scene_state>
135
runInnerParser(const halp::text_file_view& data,
3✔
136
               std::function<void(Loader&)> (*parse)(halp::text_file_view))
137
{
138
  auto apply = parse(data);
3✔
139
  if(!apply)
3✔
140
    return nullptr;
×
141
  Loader inner;
3✔
142
  apply(inner);
3✔
143
  return inner.m_raw_state;
3✔
144
}
3✔
145

146
} // namespace
147

148
std::function<void(AssetLoader&)>
149
AssetLoader::ins::asset_t::process(file_type tv)
15✔
150
{
151
  if(tv.filename.empty())
15✔
152
    return {};
1✔
153

154
  const std::string_view fname{tv.filename};
14✔
155
  std::shared_ptr<const ossia::scene_state> loaded;
14✔
156

157
  if(hasSuffixCI(fname, "fbx"))
14✔
158
  {
159
    loaded = runInnerParser<FbxParser>(tv, &FbxParser::ins::fbx_t::process);
×
160
  }
×
161
  else if(hasSuffixCI(fname, "gltf") || hasSuffixCI(fname, "glb"))
14✔
162
  {
163
    loaded = runInnerParser<GltfParser>(tv, &GltfParser::ins::gltf_t::process);
3✔
164
  }
3✔
165
  else if(hasSuffixCI(fname, "obj"))
11✔
166
  {
167
    Threedim::float_vec buf;
1✔
168
    auto meshes = Threedim::ObjFromString(tv.bytes, buf);
1✔
169
    if(!meshes.empty())
1✔
170
    {
171
      const QString label = QFileInfo(QString::fromStdString(std::string{fname}))
×
172
                                .fileName();
×
173
      loaded = Threedim::sceneStateFromMeshes(
×
174
          std::move(meshes), std::move(buf), label.toStdString());
×
175
    }
×
176
  }
1✔
177
  else if(hasSuffixCI(fname, "ply"))
10✔
178
  {
179
    // Sniff the header first: a PLY whose vertex element carries
180
    // splat-style columns (or no face element) goes through the
181
    // primitive-cloud path; everything else stays on the existing
182
    // mesh path. The sniff only reads the textual header, no row data.
183
    if(Threedim::PrimitiveCloud::ply_is_splat_shaped(fname))
2✔
184
    {
NEW
185
      auto cloud = Threedim::PrimitiveCloud::parse_ply(fname);
×
NEW
186
      if(cloud)
×
187
      {
188
        const QString label
NEW
189
            = QFileInfo(QString::fromStdString(std::string{fname})).fileName();
×
NEW
190
        loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
×
NEW
191
            std::move(cloud), label.toStdString());
×
NEW
192
      }
×
NEW
193
    }
×
194
    else
195
    {
196
      Threedim::float_vec buf;
2✔
197
      auto meshes = Threedim::PlyFromFile(fname, buf);
2✔
198
      if(!meshes.empty())
2✔
199
      {
200
        const QString label
201
            = QFileInfo(QString::fromStdString(std::string{fname})).fileName();
1✔
202
        loaded = Threedim::sceneStateFromMeshes(
1✔
203
            std::move(meshes), std::move(buf), label.toStdString());
1✔
204
      }
1✔
205
    }
2✔
206
  }
2✔
207
  else if(hasSuffixCI(fname, "stl"))
8✔
208
  {
209
    Threedim::float_vec buf;
2✔
210
    auto meshes = Threedim::StlFromFile(fname, buf);
2✔
211
    if(!meshes.empty())
2✔
212
    {
213
      const QString label = QFileInfo(QString::fromStdString(std::string{fname}))
2✔
214
                                .fileName();
1✔
215
      loaded = Threedim::sceneStateFromMeshes(
1✔
216
          std::move(meshes), std::move(buf), label.toStdString());
1✔
217
    }
1✔
218
  }
2✔
219
  else if(hasSuffixCI(fname, "off"))
6✔
220
  {
221
    Threedim::float_vec buf;
2✔
222
    auto meshes = Threedim::OffFromFile(fname, buf);
2✔
223
    if(!meshes.empty())
2✔
224
    {
225
      const QString label = QFileInfo(QString::fromStdString(std::string{fname}))
2✔
226
                                .fileName();
1✔
227
      loaded = Threedim::sceneStateFromMeshes(
1✔
228
          std::move(meshes), std::move(buf), label.toStdString());
1✔
229
    }
1✔
230
  }
2✔
231
  else if(hasSuffixCI(fname, "splat"))
4✔
232
  {
233
    // Antimatter15 binary .splat: 32 bytes/primitive, fixed schema.
NEW
234
    auto cloud = Threedim::PrimitiveCloud::parse_splat_binary(tv.bytes);
×
NEW
235
    if(cloud)
×
236
    {
237
      const QString label
NEW
238
          = QFileInfo(QString::fromStdString(std::string{fname})).fileName();
×
NEW
239
      loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
×
NEW
240
          std::move(cloud), label.toStdString());
×
NEW
241
    }
×
NEW
242
  }
×
243
  else if(hasSuffixCI(fname, "spz"))
4✔
244
  {
245
    // Niantic .spz v1-3: gzip-compressed column-grouped 3DGS data.
246
    // Decoded via the vendored Niantic library (3rdparty/spz),
247
    // transposed into the canonical 62-float row layout that the
248
    // 3dgs.classic preset reads. v4 (NGSP-magic + ZSTD) returns
249
    // nullptr — see 3rdparty/spz/CMakeLists.txt for the rationale.
NEW
250
    auto cloud = Threedim::PrimitiveCloud::parse_spz(tv.bytes);
×
NEW
251
    if(cloud)
×
252
    {
253
      const QString label
NEW
254
          = QFileInfo(QString::fromStdString(std::string{fname})).fileName();
×
NEW
255
      loaded = Threedim::PrimitiveCloud::sceneStateFromCloud(
×
NEW
256
          std::move(cloud), label.toStdString());
×
NEW
257
    }
×
NEW
258
  }
×
259
  else
260
  {
261
    // Built-ins all missed — consult the addon-registered parsers.
262
    // score-addon-academy registers its USD loader here at module load.
263
    const std::string ext = extensionLowerCI(fname);
4✔
264
    if(auto fn = AssetLoaderRegistry::lookup(ext))
4✔
265
      loaded = fn(tv);
2✔
266
  }
4✔
267

268
  if(!loaded)
14✔
269
    return {};
7✔
270

271
  return [state = std::move(loaded)](AssetLoader& self) mutable {
13✔
272
    self.m_parsed_state = std::move(state);
6✔
273
    self.rebuild_format_state();        // m_parsed → m_overridden
6✔
274
    self.m_cached_xform.valid = false;  // force wrap rebuild
6✔
275
    self.rebuild_wrapped_state();
6✔
276
  };
6✔
277
}
15✔
278

279
void AssetLoader::rebuild_format_state()
6✔
280
{
281
  m_cached_format_override = inputs.format_override.value;
6✔
282
  m_overridden_state = Threedim::PrimitiveCloud::applyFormatOverride(
6✔
283
      m_parsed_state, m_cached_format_override);
6✔
284
  // The wrapped state derives from m_overridden_state and must be
285
  // rebuilt whenever the override changes.
286
  m_cached_xform.valid = false;
6✔
287
  rebuild_wrapped_state();
6✔
288
}
6✔
289

290
void AssetLoader::rebuild_wrapped_state()
12✔
291
{
292
  m_wrapped_state = Threedim::wrapSceneWithTransform(
12✔
293
      m_overridden_state, inputs, m_cached_xform, m_version_counter, m_xform_ref);
12✔
294
}
12✔
295

296
void AssetLoader::operator()()
×
297
{
NEW
298
  if(!m_parsed_state)
×
299
  {
300
    outputs.scene_out.scene.state = nullptr;
×
301
    outputs.scene_out.dirty = 0;
×
302
    return;
×
303
  }
304

305
  if(Threedim::transformChanged(inputs, m_cached_xform))
×
306
    rebuild_wrapped_state();
×
307

308
  outputs.scene_out.scene.state = m_wrapped_state;
×
309
  outputs.scene_out.dirty = ossia::scene_port::dirty_transform;
×
310
}
×
311

312
void AssetLoader::init(score::gfx::RenderList& r, QRhiResourceUpdateBatch& res)
×
313
{
314
  if(!raw_transform_slot.valid())
×
315
  {
316
    raw_transform_slot = r.registry().allocate(
×
317
        score::gfx::GpuResourceRegistry::Arena::RawTransform,
318
        sizeof(score::gfx::RawLocalTransform));
319
    m_xform_ref = r.registry().toOssiaRef(raw_transform_slot);
×
320
    // Force the wrapped state to be rebuilt so the emitted
321
    // scene_transform carries the fresh ref.
322
    m_cached_xform.valid = false;
×
323
  }
×
324
  if(raw_transform_slot.valid())
×
325
  {
326
    score::gfx::RawLocalTransform seed{};
×
327
    r.registry().updateSlot(res, raw_transform_slot, &seed, sizeof(seed));
328
  }
329
}
330

331
void AssetLoader::update(
×
332
    score::gfx::RenderList& r, QRhiResourceUpdateBatch& res, score::gfx::Edge*)
333
{
334
  if(!raw_transform_slot.valid())
×
335
    return;
×
336

337
  score::gfx::RawLocalTransform xform{};
×
338
  xform.translation[0] = inputs.position.value.x;
339
  xform.translation[1] = inputs.position.value.y;
340
  xform.translation[2] = inputs.position.value.z;
341
  QQuaternion q = QQuaternion::fromEulerAngles(
342
      inputs.rotation.value.x, inputs.rotation.value.y,
343
      inputs.rotation.value.z);
344
  xform.rotation[0] = q.x();
345
  xform.rotation[1] = q.y();
346
  xform.rotation[2] = q.z();
347
  xform.rotation[3] = q.scalar();
348
  xform.scale[0] = inputs.scale.value.x;
349
  xform.scale[1] = inputs.scale.value.y;
350
  xform.scale[2] = inputs.scale.value.z;
351
  r.registry().updateSlot(res, raw_transform_slot, &xform, sizeof(xform));
352
}
353

354
void AssetLoader::release(score::gfx::RenderList& r)
×
355
{
356
  if(raw_transform_slot.valid())
×
357
    r.registry().free(raw_transform_slot);
×
358
  m_xform_ref = {};
×
359
  // Clear cached scene_state so the next operator()() rebuilds against
360
  // the post-release registry. Producer-state-drift Option A — see
361
  // matching comment in Light::release.  m_parsed_state stays valid
362
  // (parser output, no slot refs); only m_overridden_state and
363
  // m_wrapped_state embed registry refs and need clearing.
364
  m_overridden_state.reset();
365
  m_wrapped_state.reset();
366
}
367

368
} // namespace Threedim
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc