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

ossia / score / 30157002862

25 Jul 2026 11:53AM UTC coverage: 15.406% (+0.1%) from 15.311%
30157002862

Pull #2119

github

web-flow
Merge 51e0e1b89 into 13afd939a
Pull Request #2119: gfx: caching + offscreen infrastructure (pre-scene, from #2109)

247 of 597 new or added lines in 12 files covered. (41.37%)

9 existing lines in 4 files now uncovered.

30668 of 199059 relevant lines covered (15.41%)

981.72 hits per line

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

89.43
/src/plugins/score-plugin-gfx/Gfx/Graph/TextureLoader.cpp
1
#include <Gfx/Graph/TextureLoader.hpp>
2

3
#include <Gfx/Images/Process.hpp>
4

5
#include <QByteArray>
6
#include <QFile>
7
#include <QImage>
8
#include <QImageReader>
9

10
#include <private/qrhi_p.h>
11

12
#include <cstring>
13

14
namespace score::gfx
15
{
16
// Qt's tga handler only accepts TGA 2.0 files ending in the
17
// "TRUEVISION-XFILE." footer; original-spec TGAs (still produced by common
18
// tools) are rejected. When the header plausibly describes a TGA, retry the
19
// decode with the footer appended.
20
static QImage tryFooterlessTga(const QByteArray& bytes)
5✔
21
{
22
  if(bytes.size() < 18)
5✔
23
    return {};
3✔
24
  const auto* d = reinterpret_cast<const unsigned char*>(bytes.constData());
2✔
25
  const bool colormap_ok = d[1] <= 1;
2✔
26
  const bool type_ok
2✔
27
      = d[2] == 1 || d[2] == 2 || d[2] == 3 || d[2] == 9 || d[2] == 10 || d[2] == 11;
2✔
28
  const int bpp = d[16];
2✔
29
  const bool bpp_ok = bpp == 8 || bpp == 15 || bpp == 16 || bpp == 24 || bpp == 32;
2✔
30
  const int w = d[12] | (d[13] << 8);
2✔
31
  const int h = d[14] | (d[15] << 8);
2✔
32
  if(!colormap_ok || !type_ok || !bpp_ok || w <= 0 || h <= 0)
2✔
33
    return {};
2✔
34

NEW
35
  QByteArray footered = bytes;
×
NEW
36
  footered.append(26, '\0');
×
NEW
37
  std::memcpy(footered.data() + footered.size() - 18, "TRUEVISION-XFILE.", 18);
×
NEW
38
  QImage img;
×
NEW
39
  img.loadFromData(footered, "tga");
×
NEW
40
  return img;
×
41
}
5✔
42

43

44
// -----------------------------------------------------------------------------
45
// CPU decode
46
// -----------------------------------------------------------------------------
47

48
std::optional<DecodedImage> decodeImageFromPath(const QString& path)
9✔
49
{
50
  // Reuse the existing global CPU cache (Gfx/Images/Process.hpp). It's
51
  // refcounted; we deliberately never call release() — material textures
52
  // typically stay live for the program lifetime.
53
  auto cached = Gfx::ImageCache::instance().acquire(path);
9✔
54
  if(!cached || cached->frames.empty())
9✔
55
  {
56
    // The cache decodes via QImage, which rejects footer-less TGAs; retry
57
    // through the memory path and its TGA fallback.
58
    QFile f(path);
4✔
59
    if(f.open(QIODevice::ReadOnly))
4✔
NEW
60
      if(auto out = decodeImageFromMemory(f.readAll(), {}))
×
61
      {
NEW
62
        out->debug_name = path;
×
NEW
63
        return out;
×
64
      }
65
    return std::nullopt;
4✔
66
  }
4✔
67

68
  DecodedImage out;
5✔
69
  out.image = cached->frames.front();
5✔
70
  // Cache stores Format_ARGB32 (BGRA-swizzled by Qt). Convert to a
71
  // canonical RGBA8888 layout so QRhi's RGBA8 textures sample correctly.
72
  if(out.image.format() != QImage::Format_RGBA8888)
5✔
73
    out.image.convertTo(QImage::Format_RGBA8888);
5✔
74
  out.debug_name = path;
5✔
75
  return out;
5✔
76
}
9✔
77

78
std::optional<DecodedImage> decodeImageFromMemory(
12✔
79
    const QByteArray& bytes, const QString& mime_hint)
80
{
81
  QImage img;
12✔
82
  // QImage::loadFromData accepts a format hint as a const char* (e.g. "PNG").
83
  // Strip the "image/" prefix from the MIME type if present, then upper-case.
84
  QByteArray fmt;
12✔
85
  if(!mime_hint.isEmpty())
12✔
86
  {
87
    QString s = mime_hint;
9✔
88
    if(s.startsWith("image/"))
9✔
89
      s = s.mid(6);
8✔
90
    fmt = s.toUpper().toLatin1();
9✔
91
  }
9✔
92
  if(!img.loadFromData(bytes, fmt.isEmpty() ? nullptr : fmt.constData()))
12✔
93
  {
94
    img = tryFooterlessTga(bytes);
5✔
95
    if(img.isNull())
5✔
96
      return std::nullopt;
5✔
NEW
97
  }
×
98

99
  DecodedImage out;
7✔
100
  out.image = std::move(img);
7✔
101
  if(out.image.format() != QImage::Format_RGBA8888)
7✔
102
    out.image.convertTo(QImage::Format_RGBA8888);
7✔
103
  out.debug_name = QStringLiteral("blob:") + mime_hint;
7✔
104
  return out;
7✔
105
}
12✔
106

107
// -----------------------------------------------------------------------------
108
// GPU upload
109
// -----------------------------------------------------------------------------
110

111
QRhiTexture* uploadImageToTexture(
11✔
112
    QRhi& rhi, QRhiResourceUpdateBatch& batch, const QImage& img, bool srgb,
113
    const QString& debug_name)
114
{
115
  if(img.isNull())
11✔
116
    return nullptr;
1✔
117

118
  // sRGB is a Flag bit (not a separate format) — Qt RHI allocates an RGBA8
119
  // texture with sRGB sampling semantics when the flag is present.
120
  // MipMapped + UsedWithGenerateMips: required for the generateMips() call
121
  // below. Without a mip chain, sampling a high-resolution material texture
122
  // (Sponza floor at distance, etc.) point-samples the base level at sub-
123
  // pixel rate → uniform noise / TV-static aliasing.
124
  QRhiTexture::Flags flags
125
      = QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
10✔
126
  if(srgb)
10✔
127
    flags |= QRhiTexture::sRGB;
3✔
128
  // sampleCount=1 (no MSAA on a sampled material texture). The mip count
129
  // itself is implicit — set by the MipMapped flag and floor(log2(max(w,h)))+1.
130
  auto* tex = rhi.newTexture(QRhiTexture::RGBA8, img.size(), 1, flags);
10✔
131
  if(!tex)
10✔
NEW
132
    return nullptr;
×
133
  if(!debug_name.isEmpty())
10✔
134
    tex->setName(debug_name.toUtf8());
9✔
135
  if(!tex->create())
10✔
136
  {
NEW
137
    delete tex;
×
NEW
138
    return nullptr;
×
139
  }
140
  // QRhi accepts QImage directly; format conversion is handled internally.
141
  batch.uploadTexture(tex, img);
10✔
142
  // Filter the base level into the mip chain. Cheap (one-shot, on first
143
  // upload) and unblocks min-filter-linear-mipmap-linear sampling on the
144
  // material samplers — kills the floor-noise aliasing.
145
  batch.generateMips(tex);
10✔
146
  return tex;
10✔
147
}
11✔
148

149
// -----------------------------------------------------------------------------
150
// One-shot helpers
151
// -----------------------------------------------------------------------------
152

153
QRhiTexture* loadAndUploadTexture(
6✔
154
    QRhi& rhi, QRhiResourceUpdateBatch& batch, const QString& path, bool srgb)
155
{
156
  auto decoded = decodeImageFromPath(path);
6✔
157
  if(!decoded)
6✔
158
    return nullptr;
2✔
159
  return uploadImageToTexture(
4✔
160
      rhi, batch, decoded->image, srgb, decoded->debug_name);
4✔
161
}
6✔
162

163
QRhiTexture* loadAndUploadTexture(
6✔
164
    QRhi& rhi, QRhiResourceUpdateBatch& batch, const QByteArray& bytes,
165
    const QString& mime_hint, bool srgb)
166
{
167
  auto decoded = decodeImageFromMemory(bytes, mime_hint);
6✔
168
  if(!decoded)
6✔
169
    return nullptr;
2✔
170
  return uploadImageToTexture(
4✔
171
      rhi, batch, decoded->image, srgb, decoded->debug_name);
4✔
172
}
6✔
173

174
// -----------------------------------------------------------------------------
175
// TextureCache
176
// -----------------------------------------------------------------------------
177

178
std::size_t TextureCache::KeyHash::operator()(const Key& k) const noexcept
22✔
179
{
180
  std::size_t h = qHash(k.origin);
22✔
181
  // Mix the sRGB bit. Use a constant of decent dispersion.
182
  h ^= (k.srgb ? 0x9E3779B97F4A7C15ull : 0xBF58476D1CE4E5B9ull);
22✔
183
  return h;
22✔
184
}
185

186
TextureCache::~TextureCache()
3✔
187
{
188
  clear();
3✔
189
}
3✔
190

191
void TextureCache::clear()
4✔
192
{
193
  for(auto& [key, tex] : m_textures)
12✔
194
    if(tex)
8✔
195
      tex->deleteLater();
6✔
196
  m_textures.clear();
4✔
197
}
4✔
198

199
QRhiTexture* TextureCache::acquireFromPath(
7✔
200
    QRhi& rhi, QRhiResourceUpdateBatch& batch, const QString& path, bool srgb)
201
{
202
  if(path.isEmpty())
7✔
203
    return nullptr;
1✔
204
  Key k{path, srgb};
6✔
205
  if(auto it = m_textures.find(k); it != m_textures.end())
6✔
206
    return it->second;
2✔
207

208
  auto* tex = loadAndUploadTexture(rhi, batch, path, srgb);
4✔
209
  // Insert even if nullptr — avoids retrying decode every frame for a missing
210
  // file. Caller can detect failure via the nullptr return.
211
  m_textures.emplace(std::move(k), tex);
4✔
212
  return tex;
4✔
213
}
7✔
214

215
QRhiTexture* TextureCache::acquireFromMemory(
5✔
216
    QRhi& rhi, QRhiResourceUpdateBatch& batch, const QByteArray& bytes,
217
    const QString& mime_hint, uint64_t content_hash, bool srgb)
218
{
219
  Key k{
15✔
220
      QStringLiteral("blob:") + QString::number(content_hash, 16),
5✔
221
      srgb};
5✔
222
  if(auto it = m_textures.find(k); it != m_textures.end())
5✔
223
    return it->second;
1✔
224

225
  auto* tex = loadAndUploadTexture(rhi, batch, bytes, mime_hint, srgb);
4✔
226
  m_textures.emplace(std::move(k), tex);
4✔
227
  return tex;
4✔
228
}
5✔
229

230
}  // namespace score::gfx
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