Доработка пайплайн машины (требуется пересмотр технологии)

This commit is contained in:
2026-01-03 00:41:09 +06:00
parent f56b46f669
commit 776e9bfaca
31 changed files with 2684 additions and 601 deletions

View File

@@ -14,6 +14,34 @@ PipelinedTextureAtlas::AtlasTextureId PipelinedTextureAtlas::getByPipeline(const
_AddictedTextures[texId].push_back(pipeline);
}
{
std::vector<TexturePipelineProgram::AnimSpec> animMeta =
TexturePipelineProgram::extractAnimationSpecs(pipeline._Pipeline.data(), pipeline._Pipeline.size());
if (!animMeta.empty()) {
AnimatedPipelineState entry;
entry.Specs.reserve(animMeta.size());
for (const auto& spec : animMeta) {
detail::AnimSpec16 outSpec{};
outSpec.TexId = spec.HasTexId ? spec.TexId : TextureAtlas::kOverflowId;
outSpec.FrameW = spec.FrameW;
outSpec.FrameH = spec.FrameH;
outSpec.FrameCount = spec.FrameCount;
outSpec.FpsQ = spec.FpsQ;
outSpec.Flags = spec.Flags;
entry.Specs.push_back(outSpec);
}
entry.LastFrames.resize(entry.Specs.size(), std::numeric_limits<uint32_t>::max());
entry.Smooth = false;
for (const auto& spec : entry.Specs) {
if (spec.Flags & detail::AnimSmooth) {
entry.Smooth = true;
break;
}
}
_AnimatedPipelines.emplace(pipeline, std::move(entry));
}
}
return atlasTexId;
}
@@ -37,6 +65,7 @@ void PipelinedTextureAtlas::freeByPipeline(const HashedPipeline& pipeline) {
Super.removeTexture(iter->second);
_AtlasCpuTextures.erase(iter->second);
_PipeToTexId.erase(iter);
_AnimatedPipelines.erase(pipeline);
}
void PipelinedTextureAtlas::updateTexture(uint32_t texId, const StoredTexture& texture) {
@@ -90,7 +119,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
}
TexturePipelineProgram program;
program.fromWords(std::move(words));
program.fromBytes(std::move(words));
TexturePipelineProgram::OwnedTexture baked;
auto provider = [this](uint32_t texId) -> std::optional<Texture> {
@@ -109,7 +138,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
return tex;
};
if (!program.bake(provider, baked, nullptr)) {
if (!program.bake(provider, baked, _AnimTimeSeconds, nullptr)) {
if (auto tex = tryCopyFirstDependencyTexture(pipeline)) {
return *tex;
}
@@ -135,6 +164,7 @@ StoredTexture PipelinedTextureAtlas::_generatePipelineTexture(const HashedPipeli
void PipelinedTextureAtlas::flushNewPipelines() {
std::vector<uint32_t> changedTextures = std::move(_ChangedTextures);
_ChangedTextures.clear();
std::sort(changedTextures.begin(), changedTextures.end());
changedTextures.erase(std::unique(changedTextures.begin(), changedTextures.end()), changedTextures.end());
@@ -150,6 +180,7 @@ void PipelinedTextureAtlas::flushNewPipelines() {
}
changedPipelineTextures.append_range(std::move(_ChangedPipelines));
_ChangedPipelines.clear();
changedTextures.clear();
std::sort(changedPipelineTextures.begin(), changedPipelineTextures.end());
@@ -165,6 +196,18 @@ void PipelinedTextureAtlas::flushNewPipelines() {
auto& stored = _AtlasCpuTextures[atlasTexId];
stored = std::move(texture);
if (!stored._Pixels.empty()) {
// Смена порядка пикселей
for (uint32_t& pixel : stored._Pixels) {
union {
struct { uint8_t r, g, b, a; } color;
uint32_t data;
};
data = pixel;
std::swap(color.r, color.b);
pixel = data;
}
Super.setTextureData(atlasTexId,
stored._Widht,
stored._Height,
@@ -182,6 +225,72 @@ void PipelinedTextureAtlas::notifyGpuFinished() {
Super.notifyGpuFinished();
}
bool PipelinedTextureAtlas::updateAnimatedPipelines(double timeSeconds) {
_AnimTimeSeconds = timeSeconds;
if (_AnimatedPipelines.empty()) {
return false;
}
bool changed = false;
for (auto& [pipeline, entry] : _AnimatedPipelines) {
if (entry.Specs.empty()) {
continue;
}
if (entry.Smooth) {
_ChangedPipelines.push_back(pipeline);
changed = true;
continue;
}
if (entry.LastFrames.size() != entry.Specs.size())
entry.LastFrames.assign(entry.Specs.size(), std::numeric_limits<uint32_t>::max());
bool pipelineChanged = false;
for (size_t i = 0; i < entry.Specs.size(); ++i) {
const auto& spec = entry.Specs[i];
uint32_t fpsQ = spec.FpsQ ? spec.FpsQ : TexturePipelineProgram::DefaultAnimFpsQ;
double fps = double(fpsQ) / 256.0;
double frameTime = timeSeconds * fps;
if (frameTime < 0.0)
frameTime = 0.0;
uint32_t frameCount = spec.FrameCount;
// Авторасчёт количества кадров
if (frameCount == 0) {
auto iterTex = _ResToTexture.find(spec.TexId);
if (iterTex != _ResToTexture.end()) {
uint32_t fw = spec.FrameW ? spec.FrameW : iterTex->second._Widht;
uint32_t fh = spec.FrameH ? spec.FrameH : iterTex->second._Widht;
if (fw > 0 && fh > 0) {
if (spec.Flags & detail::AnimHorizontal)
frameCount = iterTex->second._Widht / fw;
else
frameCount = iterTex->second._Height / fh;
}
}
}
if (frameCount == 0)
frameCount = 1;
uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u;
if (entry.LastFrames[i] != frameIndex) {
entry.LastFrames[i] = frameIndex;
pipelineChanged = true;
}
}
if (pipelineChanged) {
_ChangedPipelines.push_back(pipeline);
changed = true;
}
}
return changed;
}
std::optional<StoredTexture> PipelinedTextureAtlas::tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const {
auto deps = pipeline.getDependencedTextures();
if (!deps.empty()) {

View File

@@ -6,6 +6,7 @@
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <optional>
#include <unordered_map>
#include <utility>
@@ -22,6 +23,7 @@ enum class Op16 : Word {
End = 0,
Base_Tex = 1,
Base_Fill = 2,
Base_Anim = 3,
Resize = 10,
Transform = 11,
Opacity = 12,
@@ -33,6 +35,7 @@ enum class Op16 : Word {
Multiply = 18,
Screen = 19,
Colorize = 20,
Anim = 21,
Overlay = 30,
Mask = 31,
LowPart = 32,
@@ -43,13 +46,24 @@ enum class SrcKind16 : Word { TexId = 0, Sub = 1 };
struct SrcRef16 {
SrcKind16 kind{SrcKind16::TexId};
Word a = 0;
Word b = 0;
uint32_t TexId = 0;
uint32_t Off = 0;
uint32_t Len = 0;
};
inline uint32_t makeU32(Word lo, Word hi) {
return uint32_t(lo) | (uint32_t(hi) << 16);
}
enum AnimFlags16 : Word {
AnimSmooth = 1 << 0,
AnimHorizontal = 1 << 1
};
struct AnimSpec16 {
uint32_t TexId = 0;
uint16_t FrameW = 0;
uint16_t FrameH = 0;
uint16_t FrameCount = 0;
uint16_t FpsQ = 0;
uint16_t Flags = 0;
};
inline void addUniqueDep(boost::container::small_vector<uint32_t, 8>& deps, uint32_t id) {
if (id == TextureAtlas::kOverflowId) {
@@ -60,16 +74,52 @@ inline void addUniqueDep(boost::container::small_vector<uint32_t, 8>& deps, uint
}
}
inline bool readSrc(const std::vector<Word>& words, size_t end, size_t& ip, SrcRef16& out) {
inline bool read16(const std::vector<Word>& words, size_t end, size_t& ip, uint16_t& out) {
if (ip + 1 >= end) {
return false;
}
out = uint16_t(words[ip]) | (uint16_t(words[ip + 1]) << 8);
ip += 2;
return true;
}
inline bool read24(const std::vector<Word>& words, size_t end, size_t& ip, uint32_t& out) {
if (ip + 2 >= end) {
return false;
}
out.kind = static_cast<SrcKind16>(words[ip++]);
out.a = words[ip++];
out.b = words[ip++];
out = uint32_t(words[ip]) |
(uint32_t(words[ip + 1]) << 8) |
(uint32_t(words[ip + 2]) << 16);
ip += 3;
return true;
}
inline bool read32(const std::vector<Word>& words, size_t end, size_t& ip, uint32_t& out) {
if (ip + 3 >= end) {
return false;
}
out = uint32_t(words[ip]) |
(uint32_t(words[ip + 1]) << 8) |
(uint32_t(words[ip + 2]) << 16) |
(uint32_t(words[ip + 3]) << 24);
ip += 4;
return true;
}
inline bool readSrc(const std::vector<Word>& words, size_t end, size_t& ip, SrcRef16& out) {
if (ip >= end) {
return false;
}
out.kind = static_cast<SrcKind16>(words[ip++]);
if (out.kind == SrcKind16::TexId) {
return read24(words, end, ip, out.TexId);
}
if (out.kind == SrcKind16::Sub) {
return read24(words, end, ip, out.Off) && read24(words, end, ip, out.Len);
}
return false;
}
inline void extractPipelineDependencies(const std::vector<Word>& words,
size_t start,
size_t end,
@@ -88,12 +138,12 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
auto need = [&](size_t n) { return ip + n <= end; };
auto handleSrc = [&](const SrcRef16& src) {
if (src.kind == SrcKind16::TexId) {
addUniqueDep(deps, makeU32(src.a, src.b));
addUniqueDep(deps, src.TexId);
return;
}
if (src.kind == SrcKind16::Sub) {
size_t subStart = static_cast<size_t>(src.a);
size_t subEnd = subStart + static_cast<size_t>(src.b);
size_t subStart = static_cast<size_t>(src.Off);
size_t subEnd = subStart + static_cast<size_t>(src.Len);
if (subStart < subEnd && subEnd <= words.size()) {
extractPipelineDependencies(words, subStart, subEnd, deps, visited);
}
@@ -108,37 +158,54 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
return;
case Op16::Base_Tex: {
if (!need(3)) return;
SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return;
handleSrc(src);
} break;
case Op16::Base_Fill:
if (!need(4)) return;
ip += 4;
break;
case Op16::Base_Anim: {
SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return;
handleSrc(src);
uint16_t tmp16 = 0;
uint8_t tmp8 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!need(1)) return;
tmp8 = words[ip++];
(void)tmp8;
} break;
case Op16::Base_Fill: {
uint16_t tmp16 = 0;
uint32_t tmp32 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read32(words, end, ip, tmp32)) return;
} break;
case Op16::Overlay:
case Op16::Mask: {
if (!need(3)) return;
SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return;
handleSrc(src);
} break;
case Op16::LowPart: {
if (!need(1 + 3)) return;
if (!need(1)) return;
ip += 1; // percent
SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return;
handleSrc(src);
} break;
case Op16::Resize:
if (!need(2)) return;
ip += 2;
break;
case Op16::Resize: {
uint16_t tmp16 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
} break;
case Op16::Transform:
case Op16::Opacity:
@@ -151,8 +218,8 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
break;
case Op16::MakeAlpha:
if (!need(2)) return;
ip += 2;
if (!need(3)) return;
ip += 3;
break;
case Op16::Invert:
@@ -166,27 +233,42 @@ inline void extractPipelineDependencies(const std::vector<Word>& words,
break;
case Op16::Multiply:
case Op16::Screen:
if (!need(2)) return;
ip += 2;
break;
case Op16::Screen: {
uint32_t tmp32 = 0;
if (!read32(words, end, ip, tmp32)) return;
} break;
case Op16::Colorize:
if (!need(3)) return;
ip += 3;
break;
case Op16::Colorize: {
uint32_t tmp32 = 0;
if (!read32(words, end, ip, tmp32)) return;
if (!need(1)) return;
ip += 1;
} break;
case Op16::Anim: {
uint16_t tmp16 = 0;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!read16(words, end, ip, tmp16)) return;
if (!need(1)) return;
ip += 1;
} break;
case Op16::Combine: {
if (!need(3)) return;
ip += 2; // skip w,h
uint32_t n = words[ip++];
uint16_t w = 0, h = 0, n = 0;
if (!read16(words, end, ip, w)) return;
if (!read16(words, end, ip, h)) return;
if (!read16(words, end, ip, n)) return;
for (uint32_t i = 0; i < n; ++i) {
if (!need(2 + 3)) return;
ip += 2; // x, y
uint16_t tmp16 = 0;
if (!read16(words, end, ip, tmp16)) return; // x
if (!read16(words, end, ip, tmp16)) return; // y
SrcRef16 src{};
if (!readSrc(words, end, ip, src)) return;
handleSrc(src);
}
(void)w; (void)h;
} break;
default:
@@ -227,8 +309,9 @@ struct Pipeline {
_Pipeline = {
static_cast<detail::Word>(detail::Op16::Base_Tex),
static_cast<detail::Word>(detail::SrcKind16::TexId),
static_cast<detail::Word>(texId & 0xFFFFu),
static_cast<detail::Word>((texId >> 16) & 0xFFFFu),
static_cast<detail::Word>(texId & 0xFFu),
static_cast<detail::Word>((texId >> 8) & 0xFFu),
static_cast<detail::Word>((texId >> 16) & 0xFFu),
static_cast<detail::Word>(detail::Op16::End)
};
}
@@ -253,9 +336,7 @@ struct HashedPipeline {
constexpr std::size_t prime = 1099511628211ull;
for(detail::Word w : _Pipeline) {
hash ^= static_cast<uint8_t>(w & 0xFF);
hash *= prime;
hash ^= static_cast<uint8_t>((w >> 8) & 0xFF);
hash ^= static_cast<uint8_t>(w);
hash *= prime;
}
@@ -328,6 +409,13 @@ private:
std::vector<uint32_t> _ChangedTextures;
// Необходимые к созданию/обновлению пайплайны
std::vector<HashedPipeline> _ChangedPipelines;
struct AnimatedPipelineState {
std::vector<detail::AnimSpec16> Specs;
std::vector<uint32_t> LastFrames;
bool Smooth = false;
};
std::unordered_map<HashedPipeline, AnimatedPipelineState, HashedPipelineKeyHash, HashedPipelineKeyEqual> _AnimatedPipelines;
double _AnimTimeSeconds = 0.0;
public:
PipelinedTextureAtlas(TextureAtlas&& tk);
@@ -348,6 +436,26 @@ public:
return atlasLayers();
}
uint32_t maxLayers() const {
return Super.maxLayers();
}
uint32_t maxTextureId() const {
return Super.maxTextureId();
}
TextureAtlas::TextureId reservedOverflowId() const {
return Super.reservedOverflowId();
}
TextureAtlas::TextureId reservedLayerId(uint32_t layer) const {
return Super.reservedLayerId(layer);
}
void requestLayerCount(uint32_t layers) {
Super.requestLayerCount(layers);
}
// Должны всегда бронировать идентификатор, либо отдавать kOverflowId. При этом запись tex+pipeline остаётся
// Выдаёт стабильный идентификатор, привязанный к пайплайну
AtlasTextureId getByPipeline(const HashedPipeline& pipeline);
@@ -373,6 +481,8 @@ public:
void notifyGpuFinished();
bool updateAnimatedPipelines(double timeSeconds);
private:
std::optional<StoredTexture> tryCopyFirstDependencyTexture(const HashedPipeline& pipeline) const;

View File

@@ -29,11 +29,13 @@ TextureAtlas::TextureAtlas(VkDevice device,
EntriesCpu_.resize(Cfg_.MaxTextureId);
std::memset(EntriesCpu_.data(), 0, EntriesCpu_.size() * sizeof(Entry));
_initReservedEntries();
EntriesDirty_ = true;
Slots_.resize(Cfg_.MaxTextureId);
FreeIds_.reserve(Cfg_.MaxTextureId);
PendingInQueue_.assign(Cfg_.MaxTextureId, false);
NextId_ = _allocatableStart();
if(Cfg_.ExternalSampler != VK_NULL_HANDLE) {
Sampler_ = Cfg_.ExternalSampler;
@@ -70,13 +72,19 @@ TextureAtlas::TextureId TextureAtlas::registerTexture() {
_ensureAliveOrThrow();
TextureId id = kOverflowId;
if(NextId_ < _allocatableStart()) {
NextId_ = _allocatableStart();
}
while(!FreeIds_.empty() && isReservedId(FreeIds_.back())) {
FreeIds_.pop_back();
}
if(!FreeIds_.empty()) {
id = FreeIds_.back();
FreeIds_.pop_back();
} else if(NextId_ < Cfg_.MaxTextureId) {
} else if(NextId_ < _allocatableLimit()) {
id = NextId_++;
} else {
return kOverflowId;
return reservedOverflowId();
}
Slot& s = Slots_[id];
@@ -96,7 +104,7 @@ void TextureAtlas::setTextureData(TextureId id,
const void* pixelsRGBA8,
uint32_t rowPitchBytes) {
_ensureAliveOrThrow();
if(id == kOverflowId) return;
if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id);
if(w == 0 || h == 0) {
@@ -151,7 +159,7 @@ void TextureAtlas::setTextureData(TextureId id,
void TextureAtlas::clearTextureData(TextureId id) {
_ensureAliveOrThrow();
if(id == kOverflowId) return;
if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id);
Slot& s = Slots_[id];
@@ -172,7 +180,7 @@ void TextureAtlas::clearTextureData(TextureId id) {
void TextureAtlas::removeTexture(TextureId id) {
_ensureAliveOrThrow();
if(id == kOverflowId) return;
if(isInvalidId(id)) return;
_ensureRegisteredIdOrThrow(id);
Slot& s = Slots_[id];
@@ -217,7 +225,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
while (!queue.empty()) {
TextureId id = queue.front();
queue.pop_front();
if(id == kOverflowId || id >= inQueue.size()) {
if(isInvalidId(id) || id >= inQueue.size()) {
continue;
}
if(!inQueue[id]) {
@@ -253,7 +261,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
bool outOfSpace = false;
for(TextureId id : pendingNow) {
if(id == kOverflowId) continue;
if(isInvalidId(id)) continue;
if(id >= Slots_.size()) continue;
Slot& s = Slots_[id];
if(!s.InUse || !s.HasCpuData) continue;
@@ -310,7 +318,7 @@ TextureAtlas::DescriptorOut TextureAtlas::flushUploadsAndBarriers(VkCommandBuffe
};
for(TextureId id : pendingNow) {
if(id == kOverflowId) continue;
if(isInvalidId(id)) continue;
Slot& s = Slots_[id];
if(!s.InUse || !s.HasCpuData || !s.HasPlacement) continue;
if(!uploadTextureIntoAtlas(s, s.Place, Atlas_, false)) {

View File

@@ -10,7 +10,7 @@ TextureAtlas — как пользоваться (кратко)
2) Зарегистрируйте текстуру и получите стабильный ID:
TextureId id = atlas.registerTexture();
if(id == TextureAtlas::kOverflowId) { ... } // нет свободных ID
if(id == TextureAtlas::kOverflowId || id == atlas.reservedOverflowId()) { ... } // нет свободных ID
3) Задайте данные (RGBA8), можно много раз — ID не меняется:
atlas.setTextureData(id, w, h, pixels, rowPitchBytes);
@@ -32,7 +32,11 @@ TextureAtlas — как пользоваться (кратко)
atlas.removeTexture(id); // освободить ID (после этого использовать нельзя)
Примечания:
- Вызовы API с kOverflowId игнорируются (no-op).
- Вызовы API с kOverflowId и зарезервированными ID игнорируются (no-op).
- ID из начала диапазона зарезервированы под служебные нужды:
reservedOverflowId() == 0,
reservedLayerId(0) == 1 (первый слой),
reservedLayerId(1) == 2 (второй слой) и т.д.
- Ошибки ресурсов (нет места/стейджинга/oom) НЕ бросают исключения — дают события.
- Исключения только за неверный ввод/неверное использование (см. ТЗ).
- Класс не thread-safe: синхронизацию обеспечивает пользователь.
@@ -192,11 +196,60 @@ public:
/// Текущее число слоёв атласа.
uint32_t atlasLayers() const { return Atlas_.Layers; }
uint32_t maxLayers() const { return Cfg_.MaxLayers; }
uint32_t maxTextureId() const { return Cfg_.MaxTextureId; }
TextureId reservedOverflowId() const { return 0; }
TextureId reservedLayerId(uint32_t layer) const { return 1u + layer; }
bool isReservedId(TextureId id) const {
if(id >= Cfg_.MaxTextureId) return false;
return id < _reservedCount();
}
bool isReservedLayerId(TextureId id) const {
if(id >= Cfg_.MaxTextureId) return false;
return id != reservedOverflowId() && id < _reservedCount();
}
bool isInvalidId(TextureId id) const {
return id == kOverflowId || isReservedId(id);
}
void requestLayerCount(uint32_t layers) {
_ensureAliveOrThrow();
_scheduleLayerGrow(layers);
}
/// Общий staging-буфер (может быть задан извне).
std::shared_ptr<SharedStagingBuffer> getStagingBuffer() const { return Staging_; }
private:
void _moveFrom(TextureAtlas&& other) noexcept;
uint32_t _reservedCount() const { return Cfg_.MaxLayers + 1; }
uint32_t _reservedStart() const { return 0; }
uint32_t _allocatableStart() const { return _reservedCount(); }
uint32_t _allocatableLimit() const { return Cfg_.MaxTextureId; }
void _initReservedEntries() {
if(Cfg_.MaxTextureId <= _reservedCount()) {
return;
}
_setEntryInvalid(reservedOverflowId(), /*diagPending*/false, /*diagTooLarge*/false);
for(uint32_t layer = 0; layer < Cfg_.MaxLayers; ++layer) {
TextureId id = reservedLayerId(layer);
Entry& e = EntriesCpu_[id];
e.UVMinMax[0] = 0.0f;
e.UVMinMax[1] = 0.0f;
e.UVMinMax[2] = 1.0f;
e.UVMinMax[3] = 1.0f;
e.Layer = layer;
e.Flags = ENTRY_VALID;
}
EntriesDirty_ = true;
}
// ============================= Ошибки/валидация =============================
struct InputError : std::runtime_error {
@@ -225,6 +278,9 @@ private:
if(Cfg_.MaxTextureId == 0) {
throw _inputError("Config.MaxTextureId must be > 0");
}
if(Cfg_.MaxTextureId <= (Cfg_.MaxLayers + 1)) {
throw _inputError("Config.MaxTextureId must be > MaxLayers + 1 (reserved ids)");
}
if(Cfg_.MaxTextureSize != 2048) {
/// TODO:
@@ -246,7 +302,7 @@ private:
}
void _ensureRegisteredIdOrThrow(TextureId id) const {
if(id >= Cfg_.MaxTextureId) {
if(id >= Cfg_.MaxTextureId || isReservedId(id)) {
throw _inputError("TextureId out of range");
}
if(!Slots_[id].InUse || Slots_[id].StateValue == State::REMOVED) {

File diff suppressed because it is too large Load Diff