Skip to content

Commit

Permalink
perf(TS): Reduce TS memory by slimming down internal LevelInfo size (A…
Browse files Browse the repository at this point in the history
…cademySoftwareFoundation#4337)

Reduces the memory footprint of the `LevelInfo` struct. Three changes
are here, in order of most to least impactful:

- Makes one `ImageSpec` optional, most of the time `spec` and
`nativespec` are the same, it appears they are only different when
autotile is enabled, this saves about 160 bytes.
- Uses `std::unique_ptr<float[]>` (8 bytes) instead of
`std::vector<float>` (24 bytes).
- Reorders members to remove excess padding.

We noticed this memory usage on scenes with per-face textures with
multiple mips per face. For a scene with 12.5 million mips, this saves
about 2GB.

Note, it would probably also be possible to remove the
`polecolorcomputed` member, instead relying on the `polecolor` pointer.
But since this bool occupies existing padding in the struct it wouldn't
save any memory currently.

---------

Signed-off-by: Curtis Black <curtis.w.black@gmail.com>
  • Loading branch information
curtisblack authored Jul 16, 2024
1 parent 736d253 commit 5cc8860
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 37 deletions.
57 changes: 36 additions & 21 deletions src/libtexture/imagecache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,12 @@ ImageCacheStatistics::merge(const ImageCacheStatistics& s)



ImageCacheFile::LevelInfo::LevelInfo(const ImageSpec& spec_,
ImageCacheFile::LevelInfo::LevelInfo(std::unique_ptr<ImageSpec> spec_,
const ImageSpec& nativespec_)
: spec(spec_)
: m_spec(std::move(spec_))
, nativespec(nativespec_)
{
const ImageSpec& spec = m_spec ? *m_spec : nativespec;
full_pixel_range
= (spec.x == spec.full_x && spec.y == spec.full_y
&& spec.z == spec.full_z && spec.width == spec.full_width
Expand Down Expand Up @@ -250,16 +251,21 @@ ImageCacheFile::LevelInfo::LevelInfo(const ImageSpec& spec_,


ImageCacheFile::LevelInfo::LevelInfo(const LevelInfo& src)
: spec(src.spec)
, nativespec(src.nativespec)
, full_pixel_range(src.full_pixel_range)
, onetile(src.onetile)
, polecolorcomputed(src.polecolorcomputed)
, polecolor(src.polecolor)
: nativespec(src.nativespec)
, nxtiles(src.nxtiles)
, nytiles(src.nytiles)
, nztiles(src.nztiles)
, full_pixel_range(src.full_pixel_range)
, onetile(src.onetile)
, polecolorcomputed(src.polecolorcomputed)
{
if (src.m_spec)
m_spec = std::make_unique<ImageSpec>(*src.m_spec);
const ImageSpec& spec = m_spec ? *m_spec : nativespec;
if (src.polecolor) {
polecolor.reset(new float[2 * spec.nchannels]);
std::copy_n(src.polecolor.get(), 2 * spec.nchannels, polecolor.get());
}
int nwords = round_to_multiple(nxtiles * nytiles * nztiles, 64) / 64;
tiles_read = new atomic_ll[nwords];
for (int i = 0; i < nwords; ++i)
Expand Down Expand Up @@ -563,8 +569,9 @@ ImageCacheFile::open(ImageCachePerThreadInfo* thread_info)
int max_mip_res = imagecache().max_mip_res();
int nmip = 0;
do {
nativespec = inp->spec(nsubimages, nmip);
tempspec = nativespec;
nativespec = inp->spec(nsubimages, nmip);
tempspec = nativespec;
bool tmpspecmodified = false;
if (nmip == 0) {
// Things to do on MIP level 0, i.e. once per subimage
si.init(*this, tempspec, imagecache().forcefloat());
Expand Down Expand Up @@ -598,6 +605,7 @@ ImageCacheFile::open(ImageCachePerThreadInfo* thread_info)
tempspec.tile_height = tempspec.height;
tempspec.tile_depth = tempspec.depth;
}
tmpspecmodified = true;
}
// If a request was made for a maximum MIP resolution to use for
// texture lookups and this level is too big, bump up this
Expand All @@ -620,8 +628,14 @@ ImageCacheFile::open(ImageCachePerThreadInfo* thread_info)
}
// ImageCache can't store differing formats per channel
tempspec.channelformats.clear();
LevelInfo levelinfo(tempspec, nativespec);
si.levels.push_back(levelinfo);
if (tmpspecmodified) {
LevelInfo levelinfo(std::make_unique<ImageSpec>(tempspec),
nativespec);
si.levels.push_back(levelinfo);
} else {
LevelInfo levelinfo(nativespec);
si.levels.push_back(levelinfo);
}
++nmip;
} while (inp->seek_subimage(nsubimages, nmip));

Expand Down Expand Up @@ -663,7 +677,7 @@ ImageCacheFile::open(ImageCachePerThreadInfo* thread_info)
s.tile_depth = d;
}
++nmip;
LevelInfo levelinfo(s, s);
LevelInfo levelinfo(s);
si.levels.push_back(levelinfo);
}
}
Expand Down Expand Up @@ -1583,18 +1597,18 @@ ImageCacheTile::read(ImageCachePerThreadInfo* thread_info)
if (m_valid) {
ImageCacheFile::LevelInfo& lev(
file.levelinfo(m_id.subimage(), m_id.miplevel()));
m_tile_width = lev.spec.tile_width;
const ImageSpec& spec(lev.spec());
m_tile_width = spec.tile_width;
OIIO_DASSERT(m_tile_width > 0);
int whichtile = ((m_id.x() - lev.spec.x) / lev.spec.tile_width)
+ ((m_id.y() - lev.spec.y) / lev.spec.tile_height)
* lev.nxtiles
+ ((m_id.z() - lev.spec.z) / lev.spec.tile_depth)
int whichtile = ((m_id.x() - spec.x) / spec.tile_width)
+ ((m_id.y() - spec.y) / spec.tile_height) * lev.nxtiles
+ ((m_id.z() - spec.z) / spec.tile_depth)
* (lev.nxtiles * lev.nytiles);
int index = whichtile / 64;
int64_t bitmask = int64_t(1ULL << (whichtile & 63));
int64_t oldval = lev.tiles_read[index].fetch_or(bitmask);
if (oldval & bitmask) // Was it previously read?
file.register_redundant_tile(lev.spec.tile_bytes());
file.register_redundant_tile(spec.tile_bytes());
} else {
// (! m_valid)
m_used = false; // Don't let it hold mem if invalid
Expand Down Expand Up @@ -3586,8 +3600,9 @@ ImageCacheImpl::invalidate_all(bool force)
if (sub.untiled) {
for (int m = 0, mend = f->miplevels(s); m < mend; ++m) {
const ImageCacheFile::LevelInfo& level(f->levelinfo(s, m));
if (level.spec.tile_width != m_autotile
|| level.spec.tile_height != m_autotile) {
const ImageSpec& spec(level.spec());
if (spec.tile_width != m_autotile
|| spec.tile_height != m_autotile) {
all_files.push_back(name);
break;
}
Expand Down
35 changes: 22 additions & 13 deletions src/libtexture/imagecache_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ class OIIO_API ImageCacheFile final : public RefCnt {
}
const ImageSpec& spec(int subimage, int miplevel) const
{
return levelinfo(subimage, miplevel).spec;
return levelinfo(subimage, miplevel).spec();
}
ImageSpec& spec(int subimage, int miplevel)
{
return levelinfo(subimage, miplevel).spec;
return levelinfo(subimage, miplevel).spec();
}
const ImageSpec& nativespec(int subimage, int miplevel) const
{
Expand Down Expand Up @@ -250,18 +250,27 @@ class OIIO_API ImageCacheFile final : public RefCnt {
/// Info for each MIP level that isn't in the ImageSpec, or that we
/// precompute.
struct LevelInfo {
ImageSpec spec; ///< ImageSpec for the mip level
ImageSpec nativespec; ///< Native ImageSpec for the mip level
std::unique_ptr<ImageSpec> m_spec; ///< ImageSpec for the mip level,
// only specified if different from nativespec
ImageSpec nativespec; ///< Native ImageSpec for the mip level
mutable std::unique_ptr<float[]> polecolor; ///< Pole colors
atomic_ll* tiles_read; ///< Bitfield for tiles read at least once
int nxtiles, nytiles, nztiles; ///< Number of tiles in each dimension
bool full_pixel_range; ///< pixel data window matches image window
bool onetile; ///< Whole level fits on one tile
mutable bool polecolorcomputed; ///< Pole color was computed
mutable std::vector<float> polecolor; ///< Pole colors
int nxtiles, nytiles, nztiles; ///< Number of tiles in each dimension
atomic_ll* tiles_read; ///< Bitfield for tiles read at least once
LevelInfo(const ImageSpec& spec,
mutable bool polecolorcomputed; ///< Pole color was computed

LevelInfo(std::unique_ptr<ImageSpec> spec,
const ImageSpec& nativespec); ///< Initialize based on spec
LevelInfo(const LevelInfo& src); // needed for vector<LevelInfo>
LevelInfo(const ImageSpec& nativespec)
: LevelInfo(nullptr, nativespec)
{
}
LevelInfo(const LevelInfo& src); // needed for vector<LevelInfo>
~LevelInfo() { delete[] tiles_read; }

ImageSpec& spec() { return m_spec ? *m_spec : nativespec; }
const ImageSpec& spec() const { return m_spec ? *m_spec : nativespec; }
};

/// Info for each subimage
Expand All @@ -278,8 +287,8 @@ class OIIO_API ImageCacheFile final : public RefCnt {
bool full_pixel_range = false; ///< data window matches image window
bool is_constant_image = false; ///< Is the image a constant color?
bool has_average_color = false; ///< We have an average color
std::vector<float> average_color; ///< Average color
spin_mutex average_color_mutex; ///< protect average_color
std::vector<float> average_color; ///< Average color
std::unique_ptr<Imath::M44f> Mlocal; ///< shadows/volumes: world-to-local
// The scale/offset accounts for crops or overscans, converting
// 0-1 texture space relative to the "display/full window" into
Expand All @@ -294,8 +303,8 @@ class OIIO_API ImageCacheFile final : public RefCnt {
SubimageInfo() {}
void init(ImageCacheFile& icfile, const ImageSpec& spec,
bool forcefloat);
ImageSpec& spec(int m) { return levels[m].spec; }
const ImageSpec& spec(int m) const { return levels[m].spec; }
ImageSpec& spec(int m) { return levels[m].spec(); }
const ImageSpec& spec(int m) const { return levels[m].spec(); }
const ImageSpec& nativespec(int m) const
{
return levels[m].nativespec;
Expand Down
6 changes: 3 additions & 3 deletions src/libtexture/texturesys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1981,13 +1981,13 @@ TextureSystemImpl::pole_color(TextureFile& texturefile,
{
if (!levelinfo.onetile)
return NULL; // Only compute color for one-tile MIP levels
const ImageSpec& spec(levelinfo.spec);
const ImageSpec& spec(levelinfo.spec());
if (!levelinfo.polecolorcomputed) {
static spin_mutex mutex; // Protect everybody's polecolor
spin_lock lock(mutex);
if (!levelinfo.polecolorcomputed) {
OIIO_DASSERT(levelinfo.polecolor.size() == 0);
levelinfo.polecolor.resize(2 * spec.nchannels);
OIIO_DASSERT(!levelinfo.polecolor);
levelinfo.polecolor.reset(new float[2 * spec.nchannels]);
OIIO_DASSERT(tile->id().nchannels() == spec.nchannels
&& "pole_color doesn't work for channel subsets");
int pixelsize = tile->pixelsize();
Expand Down

0 comments on commit 5cc8860

Please sign in to comment.