#pragma once #include #include #include #include #include #include #include #include #include #include // ======================== // External texture view // ======================== struct Texture { uint32_t Width, Height; const uint32_t* Pixels; // assumed 0xAARRGGBB }; // ======================== // Bytecode words are uint8_t (1 byte machine word) // TexId is u24 (3 bytes, little-endian) // Subprogram refs use off24/len24 in BYTES (<=65535) // ======================== class TexturePipelineProgram { public: using Word = uint8_t; enum AnimFlags : Word { AnimSmooth = 1u << 0, AnimHorizontal = 1u << 1 }; static constexpr uint16_t DefaultAnimFpsQ = uint16_t(8u * 256u); static constexpr size_t MaxCodeBytes = (1u << 16) + 1u; // 65537 struct OwnedTexture { uint32_t Width = 0, Height = 0; std::vector Pixels; Texture view() const { return Texture{Width, Height, Pixels.data()}; } }; using IdResolverFunc = std::function(std::string_view)>; using TextureProviderFunc = std::function(uint32_t)>; // Patch point to 3 consecutive bytes where u24 texId lives (b0,b1,b2) struct Patch { size_t ByteIndex0 = 0; // Code_[i], Code_[i+1], Code_[i+2] std::string Name; }; bool compile(std::string src, std::string* err = nullptr) { Source_ = std::move(src); Code_.clear(); Patches_.clear(); PendingSub_.clear(); return _parseProgram(err); } bool link(const IdResolverFunc& resolver, std::string* err = nullptr) { for (const auto& p : Patches_) { auto idOpt = resolver(p.Name); if(!idOpt) { if(err) *err = "Не удалось разрешить имя текстуры: " + p.Name; return false; } uint32_t id = *idOpt; if(id >= (1u << 24)) { if(err) *err = "TexId выходит за 24 бита (u24): " + p.Name + " => " + std::to_string(id); return false; } if(p.ByteIndex0 + 2 >= Code_.size()) { if(err) *err = "Внутренняя ошибка: применение идентификатора выходит за рамки кода"; return false; } Code_[p.ByteIndex0 + 0] = uint8_t(id & 0xFFu); Code_[p.ByteIndex0 + 1] = uint8_t((id >> 8) & 0xFFu); Code_[p.ByteIndex0 + 2] = uint8_t((id >> 16) & 0xFFu); } return true; } bool bake(const TextureProviderFunc& provider, OwnedTexture& out, std::string* err = nullptr) const { return bake(provider, out, 0.0, err); } bool bake(const TextureProviderFunc& provider, OwnedTexture& out, double timeSeconds, std::string* err = nullptr) const { VM vm(provider); return vm.run(Code_, out, timeSeconds, err); } const std::vector& words() const { return Code_; } const std::vector& patches() const { return Patches_; } std::vector toBytes() const { return Code_; } struct AnimSpec { uint32_t TexId = 0; bool HasTexId = false; uint16_t FrameW = 0; uint16_t FrameH = 0; uint16_t FrameCount = 0; uint16_t FpsQ = 0; uint16_t Flags = 0; }; static std::vector extractAnimationSpecs(const Word* code, size_t size) { std::vector specs; if(!code || size == 0) { return specs; } struct Range { size_t Start = 0; size_t End = 0; }; std::vector visited; auto read8 = [&](size_t& ip, uint8_t& out)->bool{ if(ip >= size) return false; out = code[ip++]; return true; }; auto read16 = [&](size_t& ip, uint16_t& out)->bool{ if(ip + 1 >= size) return false; out = uint16_t(code[ip]) | (uint16_t(code[ip + 1]) << 8); ip += 2; return true; }; auto read24 = [&](size_t& ip, uint32_t& out)->bool{ if(ip + 2 >= size) return false; out = uint32_t(code[ip]) | (uint32_t(code[ip + 1]) << 8) | (uint32_t(code[ip + 2]) << 16); ip += 3; return true; }; auto read32 = [&](size_t& ip, uint32_t& out)->bool{ if(ip + 3 >= size) return false; out = uint32_t(code[ip]) | (uint32_t(code[ip + 1]) << 8) | (uint32_t(code[ip + 2]) << 16) | (uint32_t(code[ip + 3]) << 24); ip += 4; return true; }; struct SrcMeta { SrcKind Kind = SrcKind::TexId; uint32_t TexId = 0; uint32_t Off = 0; uint32_t Len = 0; }; auto readSrc = [&](size_t& ip, SrcMeta& out)->bool{ uint8_t kind = 0; if(!read8(ip, kind)) return false; out.Kind = static_cast(kind); if(out.Kind == SrcKind::TexId) { return read24(ip, out.TexId); } if(out.Kind == SrcKind::Sub) { return read24(ip, out.Off) && read24(ip, out.Len); } return false; }; auto scan = [&](auto&& self, size_t start, size_t end) -> void { if(start >= end || end > size) { return; } for(const auto& r : visited) { if(r.Start == start && r.End == end) { return; } } visited.push_back(Range{start, end}); size_t ip = start; while(ip < end) { uint8_t opByte = 0; if(!read8(ip, opByte)) return; Op op = static_cast(opByte); switch(op) { case Op::End: return; case Op::Base_Tex: { SrcMeta src{}; if(!readSrc(ip, src)) return; if(src.Kind == SrcKind::Sub) { size_t subStart = src.Off; size_t subEnd = subStart + src.Len; if(subStart < subEnd && subEnd <= size) { self(self, subStart, subEnd); } } } break; case Op::Base_Fill: { uint16_t tmp16 = 0; uint32_t tmp32 = 0; if(!read16(ip, tmp16)) return; if(!read16(ip, tmp16)) return; if(!read32(ip, tmp32)) return; } break; case Op::Base_Anim: { SrcMeta src{}; if(!readSrc(ip, src)) return; uint16_t frameW = 0; uint16_t frameH = 0; uint16_t frameCount = 0; uint16_t fpsQ = 0; uint8_t flags = 0; if(!read16(ip, frameW)) return; if(!read16(ip, frameH)) return; if(!read16(ip, frameCount)) return; if(!read16(ip, fpsQ)) return; if(!read8(ip, flags)) return; if(src.Kind == SrcKind::TexId) { AnimSpec spec{}; spec.TexId = src.TexId; spec.HasTexId = true; spec.FrameW = frameW; spec.FrameH = frameH; spec.FrameCount = frameCount; spec.FpsQ = fpsQ; spec.Flags = flags; specs.push_back(spec); } else if(src.Kind == SrcKind::Sub) { size_t subStart = src.Off; size_t subEnd = subStart + src.Len; if(subStart < subEnd && subEnd <= size) { self(self, subStart, subEnd); } } } break; case Op::Resize: { uint16_t tmp16 = 0; if(!read16(ip, tmp16)) return; if(!read16(ip, tmp16)) return; } break; case Op::Transform: case Op::Opacity: case Op::Invert: if(!read8(ip, opByte)) return; break; case Op::NoAlpha: case Op::Brighten: break; case Op::MakeAlpha: if(ip + 2 >= size) return; ip += 3; break; case Op::Contrast: if(ip + 1 >= size) return; ip += 2; break; case Op::Multiply: case Op::Screen: { uint32_t tmp32 = 0; if(!read32(ip, tmp32)) return; } break; case Op::Colorize: { uint32_t tmp32 = 0; if(!read32(ip, tmp32)) return; if(!read8(ip, opByte)) return; } break; case Op::Anim: { uint16_t frameW = 0; uint16_t frameH = 0; uint16_t frameCount = 0; uint16_t fpsQ = 0; uint8_t flags = 0; if(!read16(ip, frameW)) return; if(!read16(ip, frameH)) return; if(!read16(ip, frameCount)) return; if(!read16(ip, fpsQ)) return; if(!read8(ip, flags)) return; AnimSpec spec{}; spec.HasTexId = false; spec.FrameW = frameW; spec.FrameH = frameH; spec.FrameCount = frameCount; spec.FpsQ = fpsQ; spec.Flags = flags; specs.push_back(spec); } break; case Op::Overlay: case Op::Mask: { SrcMeta src{}; if(!readSrc(ip, src)) return; if(src.Kind == SrcKind::Sub) { size_t subStart = src.Off; size_t subEnd = subStart + src.Len; if(subStart < subEnd && subEnd <= size) { self(self, subStart, subEnd); } } } break; case Op::LowPart: { if(!read8(ip, opByte)) return; SrcMeta src{}; if(!readSrc(ip, src)) return; if(src.Kind == SrcKind::Sub) { size_t subStart = src.Off; size_t subEnd = subStart + src.Len; if(subStart < subEnd && subEnd <= size) { self(self, subStart, subEnd); } } } break; case Op::Combine: { uint16_t w = 0, h = 0, n = 0; if(!read16(ip, w)) return; if(!read16(ip, h)) return; if(!read16(ip, n)) return; for(uint16_t i = 0; i < n; ++i) { uint16_t tmp16 = 0; if(!read16(ip, tmp16)) return; if(!read16(ip, tmp16)) return; SrcMeta src{}; if(!readSrc(ip, src)) return; if(src.Kind == SrcKind::Sub) { size_t subStart = src.Off; size_t subEnd = subStart + src.Len; if(subStart < subEnd && subEnd <= size) { self(self, subStart, subEnd); } } } (void)w; (void)h; } break; default: return; } } }; scan(scan, 0, size); return specs; } static std::vector extractAnimationSpecs(const std::vector& code) { return extractAnimationSpecs(code.data(), code.size()); } void fromBytes(std::vector bytes) { Code_ = std::move(bytes); Patches_.clear(); Source_.clear(); PendingSub_.clear(); } private: // ======================== // Byte helpers (little-endian) // ======================== static inline uint16_t _rd16(const std::vector& c, size_t& ip) { uint16_t v = uint16_t(c[ip]) | (uint16_t(c[ip+1]) << 8); ip += 2; return v; } static inline uint32_t _rd24(const std::vector& c, size_t& ip) { uint32_t v = uint32_t(c[ip]) | (uint32_t(c[ip+1]) << 8) | (uint32_t(c[ip+2]) << 16); ip += 3; return v; } static inline uint32_t _rd32(const std::vector& c, size_t& ip) { uint32_t v = uint32_t(c[ip]) | (uint32_t(c[ip+1]) << 8) | (uint32_t(c[ip+2]) << 16) | (uint32_t(c[ip+3]) << 24); ip += 4; return v; } static inline void _wr8 (std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); } static inline void _wr16(std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); o.push_back(uint8_t((v >> 8) & 0xFFu)); } static inline void _wr24(std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); o.push_back(uint8_t((v >> 8) & 0xFFu)); o.push_back(uint8_t((v >> 16) & 0xFFu)); } static inline void _wr32(std::vector& o, uint32_t v){ o.push_back(uint8_t(v & 0xFFu)); o.push_back(uint8_t((v >> 8) & 0xFFu)); o.push_back(uint8_t((v >> 16) & 0xFFu)); o.push_back(uint8_t((v >> 24) & 0xFFu)); } // ======================== // SrcRef encoding in bytes (variable length) // kind(1) + payload // TexId: id24(3) => total 4 // Sub : off16(3) + len16(3) => total 7 // ======================== enum class SrcKind : uint8_t { TexId = 0, Sub = 1 }; struct SrcRef { SrcKind Kind{}; uint32_t TexId24 = 0; // for TexId uint16_t Off24 = 0; // for Sub uint16_t Len24 = 0; // for Sub }; // ======================== // Opcodes (1 byte) // ======================== enum class Op : uint8_t { End = 0, Base_Tex = 1, // SrcRef(TexId) Base_Fill = 2, // w16, h16, color32 Base_Anim = 3, // SrcRef(TexId), frameW16, frameH16, frames16, fpsQ16, flags8 Resize = 10, // w16, h16 Transform = 11, // t8 Opacity = 12, // a8 NoAlpha = 13, // - MakeAlpha = 14, // rgb24 (3 bytes) RR,GG,BB Invert = 15, // mask8 Brighten = 16, // - Contrast = 17, // cBias8, bBias8 (bias-127) Multiply = 18, // color32 Screen = 19, // color32 Colorize = 20, // color32, ratio8 Anim = 21, // frameW16, frameH16, frames16, fpsQ16, flags8 Overlay = 30, // SrcRef (var) Mask = 31, // SrcRef (var) LowPart = 32, // percent8, SrcRef (var) Combine = 40 // w16,h16,n16 then n*(x16,y16,SrcRef) (если понадобится — допишем DSL) }; // ======================== // Pixel helpers (assume 0xAARRGGBB) // ======================== static inline uint8_t _a(uint32_t c){ return uint8_t((c >> 24) & 0xFF); } static inline uint8_t _r(uint32_t c){ return uint8_t((c >> 16) & 0xFF); } static inline uint8_t _g(uint32_t c){ return uint8_t((c >> 8) & 0xFF); } static inline uint8_t _b(uint32_t c){ return uint8_t((c >> 0) & 0xFF); } static inline uint32_t _pack(uint8_t a,uint8_t r,uint8_t g,uint8_t b){ return (uint32_t(a)<<24)|(uint32_t(r)<<16)|(uint32_t(g)<<8)|(uint32_t(b)); } static inline uint8_t _clampu8(int v){ return uint8_t(std::min(255, std::max(0, v))); } // ======================== // VM (executes bytes) // ======================== struct Image { uint32_t W=0,H=0; std::vector Px; }; class VM { public: using TextureProvider = TexturePipelineProgram::TextureProviderFunc; explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {} bool run(const std::vector& code, OwnedTexture& out, double timeSeconds, std::string* err) { if(code.empty()) { if(err) *err="Empty bytecode"; return false; } Image cur; std::unordered_map texCache; std::unordered_map subCache; // key = (off<<24) | len size_t ip = 0; auto need = [&](size_t n)->bool{ if(ip + n > code.size()) { if(err) *err="Bytecode truncated"; return false; } return true; }; while(true) { if(!need(1)) return false; Op op = static_cast(code[ip++]); if(op == Op::End) break; switch(op) { case Op::Base_Tex: { SrcRef src; if(!_readSrc(code, ip, src, err)) return false; if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Tex must be TexId"); cur = _loadTex(src.TexId24, texCache, err); if(cur.W == 0) return false; } break; case Op::Base_Fill: { if(!need(2+2+4)) return false; uint32_t w = _rd16(code, ip); uint32_t h = _rd16(code, ip); uint32_t color = _rd32(code, ip); cur = _makeSolid(w, h, color); } break; case Op::Base_Anim: { SrcRef src; if(!_readSrc(code, ip, src, err)) return false; if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Anim must be TexId"); if(!need(2+2+2+2+1)) return false; uint32_t frameW = _rd16(code, ip); uint32_t frameH = _rd16(code, ip); uint32_t frameCount = _rd16(code, ip); uint32_t fpsQ = _rd16(code, ip); uint32_t flags = code[ip++]; Image sheet = _loadTex(src.TexId24, texCache, err); if(sheet.W == 0) return false; uint32_t fw = frameW ? frameW : sheet.W; uint32_t fh = frameH ? frameH : sheet.H; if(fw == 0 || fh == 0) return _bad(err, "Base_Anim invalid frame size"); bool horizontal = (flags & AnimHorizontal) != 0; if(frameCount == 0) { uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); frameCount = std::max(1u, avail); } uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; double fps = double(fpsQv) / 256.0; double frameTime = timeSeconds * fps; if(frameTime < 0.0) frameTime = 0.0; uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; double frac = frameTime - std::floor(frameTime); cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); if(flags & AnimSmooth) { uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); _lerp(cur, next, frac); } } break; case Op::Anim: { if(!cur.W || !cur.H) return _bad(err, "Anim requires base image"); if(!need(2+2+2+2+1)) return false; uint32_t frameW = _rd16(code, ip); uint32_t frameH = _rd16(code, ip); uint32_t frameCount = _rd16(code, ip); uint32_t fpsQ = _rd16(code, ip); uint32_t flags = code[ip++]; const Image& sheet = cur; uint32_t fw = frameW ? frameW : sheet.W; uint32_t fh = frameH ? frameH : sheet.H; if(fw == 0 || fh == 0) return _bad(err, "Anim invalid frame size"); bool horizontal = (flags & AnimHorizontal) != 0; if(frameCount == 0) { uint32_t avail = horizontal ? (sheet.W / fw) : (sheet.H / fh); frameCount = std::max(1u, avail); } uint32_t fpsQv = fpsQ ? fpsQ : DefaultAnimFpsQ; double fps = double(fpsQv) / 256.0; double frameTime = timeSeconds * fps; if(frameTime < 0.0) frameTime = 0.0; uint32_t frameIndex = frameCount ? (uint32_t(frameTime) % frameCount) : 0u; double frac = frameTime - std::floor(frameTime); cur = _cropFrame(sheet, frameIndex, fw, fh, horizontal); if(flags & AnimSmooth) { uint32_t nextIndex = frameCount ? ((frameIndex + 1u) % frameCount) : 0u; Image next = _cropFrame(sheet, nextIndex, fw, fh, horizontal); _lerp(cur, next, frac); } } break; case Op::Overlay: { SrcRef src; if(!_readSrc(code, ip, src, err)) return false; Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); if(over.W == 0) return false; if(!cur.W) { cur = std::move(over); break; } over = _resizeNN_ifNeeded(over, cur.W, cur.H); _alphaOver(cur, over); } break; case Op::Mask: { SrcRef src; if(!_readSrc(code, ip, src, err)) return false; Image m = _loadSrc(code, src, texCache, subCache, timeSeconds, err); if(m.W == 0) return false; if(!cur.W) return _bad(err, "Mask requires base image"); m = _resizeNN_ifNeeded(m, cur.W, cur.H); _applyMask(cur, m); } break; case Op::LowPart: { if(!need(1)) return false; uint32_t pct = std::min(100u, uint32_t(code[ip++])); SrcRef src; if(!_readSrc(code, ip, src, err)) return false; Image over = _loadSrc(code, src, texCache, subCache, timeSeconds, err); if(over.W == 0) return false; if(!cur.W) return _bad(err, "LowPart requires base image"); over = _resizeNN_ifNeeded(over, cur.W, cur.H); _lowpart(cur, over, pct); } break; case Op::Resize: { if(!cur.W) return _bad(err, "Resize requires base image"); if(!need(2+2)) return false; uint32_t w = _rd16(code, ip); uint32_t h = _rd16(code, ip); cur = _resizeNN(cur, w, h); } break; case Op::Transform: { if(!cur.W) return _bad(err, "Transform requires base image"); if(!need(1)) return false; uint32_t t = code[ip++] & 7u; cur = _transform(cur, t); } break; case Op::Opacity: { if(!cur.W) return _bad(err, "Opacity requires base image"); if(!need(1)) return false; uint32_t a = code[ip++] & 0xFFu; _opacity(cur, uint8_t(a)); } break; case Op::NoAlpha: { if(!cur.W) return _bad(err, "NoAlpha requires base image"); _noAlpha(cur); } break; case Op::MakeAlpha: { if(!cur.W) return _bad(err, "MakeAlpha requires base image"); if(!need(3)) return false; uint32_t rr = code[ip++], gg = code[ip++], bb = code[ip++]; uint32_t rgb24 = (rr << 16) | (gg << 8) | bb; _makeAlpha(cur, rgb24); } break; case Op::Invert: { if(!cur.W) return _bad(err, "Invert requires base image"); if(!need(1)) return false; uint32_t mask = code[ip++] & 0xFu; _invert(cur, mask); } break; case Op::Brighten: { if(!cur.W) return _bad(err, "Brighten requires base image"); _brighten(cur); } break; case Op::Contrast: { if(!cur.W) return _bad(err, "Contrast requires base image"); if(!need(2)) return false; int c = int(code[ip++]) - 127; int b = int(code[ip++]) - 127; _contrast(cur, c, b); } break; case Op::Multiply: { if(!cur.W) return _bad(err, "Multiply requires base image"); if(!need(4)) return false; uint32_t color = _rd32(code, ip); _multiply(cur, color); } break; case Op::Screen: { if(!cur.W) return _bad(err, "Screen requires base image"); if(!need(4)) return false; uint32_t color = _rd32(code, ip); _screen(cur, color); } break; case Op::Colorize: { if(!cur.W) return _bad(err, "Colorize requires base image"); if(!need(4+1)) return false; uint32_t color = _rd32(code, ip); uint32_t ratio = code[ip++] & 0xFFu; _colorize(cur, color, uint8_t(ratio)); } break; default: return _bad(err, "Unknown opcode (no skip table in this minimal VM)"); } } out.Width = cur.W; out.Height = cur.H; out.Pixels = std::move(cur.Px); return true; } private: TextureProvider Provider_; static bool _bad(std::string* err, const char* msg){ if(err) *err = msg; return false; } static bool _readSrc(const std::vector& code, size_t& ip, SrcRef& out, std::string* err) { if(ip >= code.size()) return _bad(err, "Bytecode truncated (SrcRef.kind)"); out.Kind = static_cast(code[ip++]); if(out.Kind == SrcKind::TexId) { if(ip + 3 > code.size()) return _bad(err, "Bytecode truncated (TexId24)"); out.TexId24 = _rd24(code, ip); out.Off24 = 0; out.Len24 = 0; return true; } if(out.Kind == SrcKind::Sub) { if(ip + 6 > code.size()) return _bad(err, "Bytecode truncated (Sub off/len)"); out.Off24 = _rd24(code, ip); out.Len24 = _rd24(code, ip); out.TexId24 = 0; return true; } return _bad(err, "Unknown SrcKind"); } Image _loadTex(uint32_t id, std::unordered_map& cache, std::string* err) { auto it = cache.find(id); if(it != cache.end()) return it->second; auto t = Provider_(id); if(!t || !t->Pixels || !t->Width || !t->Height) { if(err) *err = "Texture id not found: " + std::to_string(id); return {}; } Image img; img.W = t->Width; img.H = t->Height; img.Px.assign(t->Pixels, t->Pixels + size_t(img.W)*size_t(img.H)); cache.emplace(id, img); return img; } Image _loadSub(const std::vector& code, uint32_t off, uint32_t len, std::unordered_map& /*texCache*/, std::unordered_map& subCache, double timeSeconds, std::string* err) { uint64_t key = (uint32_t(off) << 24) | uint32_t(len); auto it = subCache.find(key); if(it != subCache.end()) return it->second; size_t start = size_t(off); size_t end = start + size_t(len); if(end > code.size()) { if(err) *err="Subprogram out of range"; return {}; } std::vector slice(code.begin()+start, code.begin()+end); OwnedTexture tmp; VM nested(Provider_); if(!nested.run(slice, tmp, timeSeconds, err)) return {}; Image img; img.W = tmp.Width; img.H = tmp.Height; img.Px = std::move(tmp.Pixels); subCache.emplace(key, img); return img; } Image _loadSrc(const std::vector& code, const SrcRef& src, std::unordered_map& texCache, std::unordered_map& subCache, double timeSeconds, std::string* err) { if(src.Kind == SrcKind::TexId) return _loadTex(src.TexId24, texCache, err); if(src.Kind == SrcKind::Sub) return _loadSub(code, src.Off24, src.Len24, texCache, subCache, timeSeconds, err); if(err) *err = "Unknown SrcKind"; return {}; } // ---- image ops (как в исходнике) ---- static Image _makeSolid(uint32_t w, uint32_t h, uint32_t color) { Image img; img.W=w; img.H=h; img.Px.assign(size_t(w)*size_t(h), color); return img; } static Image _resizeNN(const Image& src, uint32_t nw, uint32_t nh) { Image dst; dst.W=nw; dst.H=nh; dst.Px.resize(size_t(nw)*size_t(nh)); for(uint32_t y=0;y= sheet.H) continue; for(uint32_t x = 0; x < fw; ++x) { uint32_t sx = baseX + x; if(sx >= sheet.W) continue; out.Px[size_t(y) * fw + x] = sheet.Px[size_t(sy) * sheet.W + sx]; } } return out; } static void _lerp(Image& base, const Image& over, double t) { if(t <= 0.0) return; if(t >= 1.0) { base = over; return; } if(base.W != over.W || base.H != over.H) return; const size_t n = base.Px.size(); for(size_t i = 0; i < n; ++i) { uint32_t a = base.Px[i]; uint32_t b = over.Px[i]; int ar = _r(a), ag = _g(a), ab = _b(a), aa = _a(a); int br = _r(b), bg = _g(b), bb = _b(b), ba = _a(b); uint8_t rr = _clampu8(int(ar + (br - ar) * t)); uint8_t rg = _clampu8(int(ag + (bg - ag) * t)); uint8_t rb = _clampu8(int(ab + (bb - ab) * t)); uint8_t ra = _clampu8(int(aa + (ba - aa) * t)); base.Px[i] = _pack(ra, rr, rg, rb); } } static void _alphaOver(Image& base, const Image& over) { const size_t n = base.Px.size(); for(size_t i=0;i(255, (outRp * 255) / outA)); outG = uint8_t(std::min(255, (outGp * 255) / outA)); outB = uint8_t(std::min(255, (outBp * 255) / outA)); } base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); } } static void _applyMask(Image& base, const Image& mask) { const size_t n = base.Px.size(); for(size_t i=0;i> 16) & 0xFF); uint8_t gg = uint8_t((rgb24 >> 8) & 0xFF); uint8_t bb = uint8_t((rgb24 >> 0) & 0xFF); for(auto& p : img.Px) { if(_r(p)==rr && _g(p)==gg && _b(p)==bb) p = _pack(0, _r(p), _g(p), _b(p)); } } static void _invert(Image& img, uint32_t maskBits) { for(auto& p : img.Px) { uint8_t a=_a(p), r=_r(p), g=_g(p), b=_b(p); if(maskBits & 1u) r = 255 - r; if(maskBits & 2u) g = 255 - g; if(maskBits & 4u) b = 255 - b; if(maskBits & 8u) a = 255 - a; p = _pack(a,r,g,b); } } static void _brighten(Image& img) { for(auto& p : img.Px) { int r = _r(p), g = _g(p), b = _b(p); r = r + (255 - r) / 3; g = g + (255 - g) / 3; b = b + (255 - b) / 3; p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); } } static void _contrast(Image& img, int c, int br) { double C = double(std::max(-127, std::min(127, c))); double factor = (259.0 * (C + 255.0)) / (255.0 * (259.0 - C)); for(auto& p : img.Px) { int r = int(factor * (int(_r(p)) - 128) + 128) + br; int g = int(factor * (int(_g(p)) - 128) + 128) + br; int b = int(factor * (int(_b(p)) - 128) + 128) + br; p = _pack(_a(p), _clampu8(r), _clampu8(g), _clampu8(b)); } } static void _multiply(Image& img, uint32_t color) { uint8_t cr=_r(color), cg=_g(color), cb=_b(color); for(auto& p : img.Px) { uint8_t r = uint8_t((uint32_t(_r(p)) * cr) / 255); uint8_t g = uint8_t((uint32_t(_g(p)) * cg) / 255); uint8_t b = uint8_t((uint32_t(_b(p)) * cb) / 255); p = _pack(_a(p), r,g,b); } } static void _screen(Image& img, uint32_t color) { uint8_t cr=_r(color), cg=_g(color), cb=_b(color); for(auto& p : img.Px) { uint8_t r = uint8_t(255 - ((255 - _r(p)) * (255 - cr)) / 255); uint8_t g = uint8_t(255 - ((255 - _g(p)) * (255 - cg)) / 255); uint8_t b = uint8_t(255 - ((255 - _b(p)) * (255 - cb)) / 255); p = _pack(_a(p), r,g,b); } } static void _colorize(Image& img, uint32_t color, uint8_t ratio) { uint8_t cr=_r(color), cg=_g(color), cb=_b(color); for(auto& p : img.Px) { int r = (int(_r(p)) * (255 - ratio) + int(cr) * ratio) / 255; int g = (int(_g(p)) * (255 - ratio) + int(cg) * ratio) / 255; int b = (int(_b(p)) * (255 - ratio) + int(cb) * ratio) / 255; p = _pack(_a(p), uint8_t(r), uint8_t(g), uint8_t(b)); } } static void _lowpart(Image& base, const Image& over, uint32_t percent) { uint32_t startY = base.H - (base.H * percent) / 100; for(uint32_t y=startY; y(255, (outRp * 255) / outA)); outG = uint8_t(std::min(255, (outGp * 255) / outA)); outB = uint8_t(std::min(255, (outBp * 255) / outA)); } base.Px[i] = _pack(uint8_t(outA), outR, outG, outB); } } } static Image _transform(const Image& src, uint32_t t) { Image dst; auto at = [&](uint32_t x, uint32_t y)->uint32_t { return src.Px[size_t(y)*src.W + x]; }; auto make = [&](uint32_t w, uint32_t h){ Image d; d.W=w; d.H=h; d.Px.resize(size_t(w)*size_t(h)); return d; }; auto set = [&](Image& im, uint32_t x, uint32_t y, uint32_t v){ im.Px[size_t(y)*im.W + x] = v; }; switch (t & 7u) { case 0: return src; case 1: { dst = make(src.H, src.W); for(uint32_t y=0;y op(...) // tex 32x32 "#RRGGBBAA" // nested only where op expects a texture arg: // overlay( tex other |> ... ) // Also supports overlay(other) / mask(other) / lowpart(50, other) // ======================== enum class TokKind { End, Ident, Number, String, Pipe, Comma, LParen, RParen, Eq, X }; struct Tok { TokKind Kind = TokKind::End; std::string Text; uint32_t U32 = 0; }; struct Lexer { std::string_view S; size_t I=0; bool HasBuf = false; Tok Buf; static bool isAlpha(char c){ return (c>='a'&&c<='z')||(c>='A'&&c<='Z')||c=='_'; } static bool isNum(char c){ return (c>='0'&&c<='9'); } static bool isAlnum(char c){ return isAlpha(c)||isNum(c); } void unread(const Tok& t) { // allow only 1-level unread HasBuf = true; Buf = t; } Tok peek() { Tok t = next(); unread(t); return t; } void skipWs() { while (I < S.size()) { char c = S[I]; if(c==' '||c=='\t'||c=='\r'||c=='\n'){ I++; continue; } if(c=='#'){ while (I= S.size()) return {TokKind::End, {}, 0}; if(S[I]=='|' && I+1') { I+=2; return {TokKind::Pipe, "|>",0}; } char c = S[I]; if(c==','){ I++; return {TokKind::Comma,",",0}; } if(c=='('){ I++; return {TokKind::LParen,"(",0}; } if(c==')'){ I++; return {TokKind::RParen,")",0}; } if(c=='='){ I++; return {TokKind::Eq,"=",0}; } if(c=='x' || c=='X'){ I++; return {TokKind::X,"x",0}; } if(c=='"') { I++; std::string out; while (I < S.size()) { char ch = S[I++]; if(ch=='"') break; if(ch=='\\' && I Pos; std::unordered_map Named; }; // ======================== // Compiler state // ======================== std::string Source_; std::vector Code_; std::vector Patches_; // ---- emit helpers (target = arbitrary out vector) ---- static inline void _emitOp(std::vector& out, Op op) { _wr8(out, uint8_t(op)); } static inline void _emitU8(std::vector& out, uint32_t v){ _wr8(out, v); } static inline void _emitU16(std::vector& out, uint32_t v){ _wr16(out, v); } static inline void _emitU24(std::vector& out, uint32_t v){ _wr24(out, v); } static inline void _emitU32(std::vector& out, uint32_t v){ _wr32(out, v); } // reserve 3 bytes for u24 texId and register patch (absolute or relative) struct RelPatch { size_t Rel0; std::string Name; }; static void _emitTexPatchU24(std::vector& out, std::vector* absPatches, std::vector* relPatches, const std::string& name) { const size_t idx = out.size(); out.push_back(0); out.push_back(0); out.push_back(0); if(absPatches) absPatches->push_back(Patch{idx, name}); if(relPatches) relPatches->push_back(RelPatch{idx, name}); } static void _emitSrcTexName(std::vector& out, std::vector* absPatches, std::vector* relPatches, const std::string& name) { _emitU8(out, uint8_t(SrcKind::TexId)); _emitTexPatchU24(out, absPatches, relPatches, name); } static void _emitSrcSub(std::vector& out, uint32_t off24, uint32_t len24) { _emitU8(out, uint8_t(SrcKind::Sub)); _emitU24(out, off24); _emitU24(out, len24); } // ======================== // Color parsing: #RRGGBB or #RRGGBBAA -> 0xAARRGGBB // ======================== static bool _parseHexColor(std::string_view s, uint32_t& outARGB) { if(s.size()!=7 && s.size()!=9) return false; if(s[0] != '#') return false; auto hex = [](char c)->int{ if(c>='0'&&c<='9') return c-'0'; if(c>='a'&&c<='f') return 10+(c-'a'); if(c>='A'&&c<='F') return 10+(c-'A'); return -1; }; auto byteAt = [&](size_t idx)->std::optional{ int hi=hex(s[idx]), lo=hex(s[idx+1]); if(hi<0||lo<0) return std::nullopt; return uint8_t((hi<<4)|lo); }; auto r = byteAt(1), g = byteAt(3), b = byteAt(5); if(!r||!g||!b) return false; uint8_t a = 255; if(s.size()==9) { auto aa = byteAt(7); if(!aa) return false; a = *aa; } outARGB = (uint32_t(a)<<24) | (uint32_t(*r)<<16) | (uint32_t(*g)<<8) | (uint32_t(*b)); return true; } // ======================== // Parsing entry: full program // ======================== bool _parseProgram(std::string* err) { Lexer lx{Source_}; Tok t = lx.next(); if(!(t.Kind==TokKind::Ident && t.Text=="tex")) { if(err) *err="Expected 'tex' at start"; return false; } // compile base into main Code_ if(!_compileBaseAfterTex(lx, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; // pipeline: |> op ... Tok nt = lx.next(); while (nt.Kind == TokKind::Pipe) { Tok opName = lx.next(); if(opName.Kind != TokKind::Ident) { if(err) *err="Expected op name after |>"; return false; } ParsedOp op; op.Name = opName.Text; Tok peek = lx.next(); if(peek.Kind == TokKind::LParen) { if(!_parseArgListOrTextureExpr(lx, op, err)) return false; nt = lx.next(); } else { nt = peek; // no-arg op } if(!_compileOpInto(lx, op, Code_, /*abs*/&Patches_, /*rel*/nullptr, err)) return false; } _emitOp(Code_, Op::End); if (Code_.size() > MaxCodeBytes) { if (err) *err = "Pipeline bytecode too large: " + std::to_string(Code_.size()) + " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; return false; } return true; } // ======================== // Base compilation after 'tex' // supports: // 1) tex name // 2) tex "name(.png/.jpg/.jpeg)" (allowed but normalized) // 3) tex anim(...) // 4) tex 32x32 "#RRGGBBAA" // ======================== bool _compileBaseAfterTex(Lexer& lx, std::vector& out, std::vector* absPatches, std::vector* relPatches, std::string* err) { Tok a = lx.next(); if(a.Kind == TokKind::Ident && a.Text == "anim") { Tok lp = lx.next(); if(lp.Kind != TokKind::LParen) { if(err) *err="Expected '(' after anim"; return false; } ParsedOp op; op.Name="anim"; if(!_parseArgList(lx, op, err)) return false; auto posU = [&](size_t i)->std::optional{ if(i >= op.Pos.size()) return std::nullopt; if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; return op.Pos[i].U32; }; auto posS = [&](size_t i)->std::optional{ if(i >= op.Pos.size()) return std::nullopt; return op.Pos[i].S; }; auto namedU = [&](std::string_view k)->std::optional{ auto it = op.Named.find(std::string(k)); if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; return it->second.U32; }; auto namedS = [&](std::string_view k)->std::optional{ auto it = op.Named.find(std::string(k)); if(it==op.Named.end()) return std::nullopt; return it->second.S; }; std::string tex = namedS("tex").value_or(posS(0).value_or("")); if(tex.empty()) { if(err) *err="anim requires texture name"; return false; } uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(1).value_or(0))); uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(2).value_or(0))); uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(3).value_or(0))); uint32_t fps = namedU("fps").value_or(posU(4).value_or(0)); uint32_t smooth = namedU("smooth").value_or(posU(5).value_or(0)); std::string axis = namedS("axis").value_or(""); bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); if(frameW > 65535u || frameH > 65535u || frames > 65535u) { if(err) *err="anim params must fit uint16"; return false; } uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); _emitOp(out, Op::Base_Anim); _emitSrcTexName(out, absPatches, relPatches, tex); _emitU16(out, frameW); _emitU16(out, frameH); _emitU16(out, frames); _emitU16(out, fpsQ); _emitU8(out, flags); return true; } if(a.Kind == TokKind::Ident || a.Kind == TokKind::String) { // tex name (or tex "name.png" => normalized) _emitOp(out, Op::Base_Tex); _emitSrcTexName(out, absPatches, relPatches, a.Text); return true; } if(a.Kind == TokKind::Number) { // tex 32x32 "#RRGGBBAA" Tok xTok = lx.next(); Tok b = lx.next(); Tok colTok = lx.next(); if(xTok.Kind != TokKind::X || b.Kind != TokKind::Number || (colTok.Kind!=TokKind::Ident && colTok.Kind!=TokKind::String)) { if(err) *err="Expected: tex x <#color>"; return false; } uint32_t w = a.U32, h = b.U32; uint32_t color = 0; if(!_parseHexColor(colTok.Text, color)) { if(err) *err="Bad color literal (use #RRGGBB or #RRGGBBAA)"; return false; } if(w>65535u || h>65535u) { if(err) *err="w/h must fit in uint16"; return false; } _emitOp(out, Op::Base_Fill); _emitU16(out, w); _emitU16(out, h); _emitU32(out, color); return true; } if(err) *err="Bad 'tex' base expression"; return false; } // ======================== // Args parsing: // - normal args: (a,b,key=v) // - OR if first token inside '(' is 'tex' => parse nested program until ')' // ======================== bool _parseArgListOrTextureExpr(Lexer& lx, ParsedOp& op, std::string* err) { Tok first = lx.next(); if(first.Kind==TokKind::Ident && first.Text=="tex") { // marker ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = "__SUBTEX__"; op.Pos.push_back(av); PendingSubData sub; if(!_compileSubProgramFromAlreadySawTex(lx, sub, err)) return false; Tok end = lx.next(); if(end.Kind != TokKind::RParen) { if(err) *err="Expected ')' after sub texture expr"; return false; } PendingSub_[&op] = std::move(sub); return true; } // otherwise parse as normal arg list, where `first` is first token inside '(' Tok t = first; if(t.Kind == TokKind::RParen) return true; while (true) { if(t.Kind == TokKind::Ident) { Tok maybeEq = lx.next(); if(maybeEq.Kind == TokKind::Eq) { Tok v = lx.next(); ArgVal av; if(!_tokToVal(v, av, err)) return false; op.Named[t.Text] = std::move(av); t = lx.next(); } else { ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; op.Pos.push_back(std::move(av)); t = maybeEq; } } else { ArgVal av; if(!_tokToVal(t, av, err)) return false; op.Pos.push_back(std::move(av)); t = lx.next(); } if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } if(t.Kind == TokKind::RParen) return true; if(err) *err = "Expected ',' or ')' in argument list"; return false; } } bool _parseArgList(Lexer& lx, ParsedOp& op, std::string* err) { Tok t = lx.next(); if(t.Kind == TokKind::RParen) return true; while (true) { if(t.Kind == TokKind::Ident) { Tok maybeEq = lx.next(); if(maybeEq.Kind == TokKind::Eq) { Tok v = lx.next(); ArgVal av; if(!_tokToVal(v, av, err)) return false; op.Named[t.Text] = std::move(av); t = lx.next(); } else { ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = t.Text; op.Pos.push_back(std::move(av)); t = maybeEq; } } else { ArgVal av; if(!_tokToVal(t, av, err)) return false; op.Pos.push_back(std::move(av)); t = lx.next(); } if(t.Kind == TokKind::Comma) { t = lx.next(); continue; } if(t.Kind == TokKind::RParen) return true; if(err) *err = "Expected ',' or ')' in argument list"; return false; } } bool _tokToVal(const Tok& t, ArgVal& out, std::string* err) { if(t.Kind == TokKind::Number) { out.Kind=ArgVal::ValueKind::U32; out.U32=t.U32; return true; } if(t.Kind == TokKind::String) { out.Kind=ArgVal::ValueKind::Str; out.S=t.Text; return true; } if(t.Kind == TokKind::Ident) { out.Kind=ArgVal::ValueKind::Ident; out.S=t.Text; return true; } if(err) *err = "Expected value token"; return false; } // ======================== // Subprogram compilation: // we already consumed 'tex'. Parse base + pipeline until next token is ')' // DO NOT consume ')' // ======================== struct PendingSubData { std::vector Bytes; std::vector RelPatches; }; bool _compileSubProgramFromAlreadySawTex(Lexer& lx, PendingSubData& outSub, std::string* err) { outSub.Bytes.clear(); outSub.RelPatches.clear(); // base if(!_compileBaseAfterTex(lx, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) return false; // pipeline until ')' while(true) { // peek Tok nt = lx.peek(); if(nt.Kind == TokKind::RParen) break; if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } // consume pipe lx.next(); Tok opName = lx.next(); if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } ParsedOp op; op.Name = opName.Text; Tok lp = lx.next(); if(lp.Kind == TokKind::LParen) { if(!_parseArgListOrTextureExpr(lx, op, err)) return false; } else { // no-arg op, lp already is next token (pipe or ')'), so we need to "unread" — can't. // simplest: treat it as next token for outer loop by rewinding lexer state. // We'll do it by storing the token back via a small hack: rebuild peek? Too heavy. // Instead: enforce parentheses for ops in subprogram except no-arg ops (brighten/noalpha) which can be without. // To keep behavior identical to main, we handle no-arg by rewinding I one token is not possible, // so we accept that in subprogram, no-arg ops must be written as brighten() etc. if(err) *err="Sub tex: no-arg ops must use parentheses, e.g. brighten()"; return false; } if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) return false; } // Pipeline until we see ')' while (true) { Tok nt = lx.peek(); if(nt.Kind == TokKind::RParen) break; if(nt.Kind != TokKind::Pipe) { if(err) *err="Sub tex: expected '|>' or ')'"; return false; } // consume pipe lx.next(); Tok opName = lx.next(); if(opName.Kind != TokKind::Ident) { if(err) *err="Sub tex: expected op name"; return false; } ParsedOp op; op.Name = opName.Text; // allow both op and op(...) Tok maybe = lx.peek(); if(maybe.Kind == TokKind::LParen) { lx.next(); // consume '(' if(!_parseArgListOrTextureExpr(lx, op, err)) return false; } else { // no-arg op; nothing to parse } if(!_compileOpInto(lx, op, outSub.Bytes, /*abs*/nullptr, /*rel*/&outSub.RelPatches, err)) return false; } _emitOp(outSub.Bytes, Op::End); return true; } // pending subprogram associated with ParsedOp pointer (created during parsing) mutable std::unordered_map PendingSub_; // Append subprogram to `out` and emit SrcRef(Sub, off16, len16), migrating patches properly. static bool _appendSubprogram(std::vector& out, PendingSubData&& sub, std::vector* absPatches, std::vector* relPatches, uint32_t& outOff, uint32_t& outLen, std::string* err) { const size_t offset = out.size(); const size_t len = sub.Bytes.size(); if(offset > 0xFFFFFFu || len > 0xFFFFFFu || (offset + len) > 0xFFFFFFu) { if(err) *err = "Subprogram слишком большой (off/len должны влезать в u24 байт)"; return false; } if(offset + len > MaxCodeBytes) { if(err) *err = "Pipeline bytecode too large after sub append: " + std::to_string(offset + len) + " > MaxCodeBytes(" + std::to_string(MaxCodeBytes) + ")"; return false; } // migrate patches if(absPatches) { for(const auto& rp : sub.RelPatches) { absPatches->push_back(Patch{offset + rp.Rel0, rp.Name}); } } if(relPatches) { for(const auto& rp : sub.RelPatches) { relPatches->push_back(RelPatch{offset + rp.Rel0, rp.Name}); } } out.insert(out.end(), sub.Bytes.begin(), sub.Bytes.end()); outOff = uint32_t(offset); outLen = uint32_t(len); return true; } // ======================== // Compile operations into arbitrary `out` // absPatches != nullptr => patches recorded as absolute for this buffer // relPatches != nullptr => patches recorded as relative for this buffer // ======================== bool _compileOpInto(Lexer& /*lx*/, const ParsedOp& op, std::vector& out, std::vector* absPatches, std::vector* relPatches, std::string* err) { auto posU = [&](size_t i)->std::optional{ if(i >= op.Pos.size()) return std::nullopt; if(op.Pos[i].Kind != ArgVal::ValueKind::U32) return std::nullopt; return op.Pos[i].U32; }; auto posS = [&](size_t i)->std::optional{ if(i >= op.Pos.size()) return std::nullopt; return op.Pos[i].S; }; auto namedU = [&](std::string_view k)->std::optional{ auto it = op.Named.find(std::string(k)); if(it==op.Named.end() || it->second.Kind!=ArgVal::ValueKind::U32) return std::nullopt; return it->second.U32; }; auto namedS = [&](std::string_view k)->std::optional{ auto it = op.Named.find(std::string(k)); if(it==op.Named.end()) return std::nullopt; return it->second.S; }; auto emitSrcFromName = [&](const std::string& n){ _emitSrcTexName(out, absPatches, relPatches, n); }; auto emitSrcFromPendingSub = [&]()->bool{ auto it = PendingSub_.find(&op); if(it == PendingSub_.end()) { if(err) *err="Internal: missing subprogram"; return false; } uint32_t off=0, len=0; if(!_appendSubprogram(out, std::move(it->second), absPatches, relPatches, off, len, err)) return false; PendingSub_.erase(it); _emitSrcSub(out, off, len); return true; }; // --- Ops that accept a "texture" argument: overlay/mask/lowpart --- if(op.Name == "overlay") { _emitOp(out, Op::Overlay); if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); // allow overlay(name) or overlay(tex=name) std::string tex = namedS("tex").value_or(posS(0).value_or("")); if(tex.empty()) { if(err) *err="overlay requires texture arg"; return false; } emitSrcFromName(tex); return true; } if(op.Name == "mask") { _emitOp(out, Op::Mask); if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") return emitSrcFromPendingSub(); std::string tex = namedS("tex").value_or(posS(0).value_or("")); if(tex.empty()) { if(err) *err="mask requires texture arg"; return false; } emitSrcFromName(tex); return true; } if(op.Name == "lowpart") { uint32_t pct = namedU("percent").value_or(posU(0).value_or(0)); if(!pct) { if(err) *err="lowpart requires percent"; return false; } _emitOp(out, Op::LowPart); _emitU8(out, std::min(100u, pct)); // 2nd arg can be nested subtex or name if(op.Pos.size() >= 2 && op.Pos[1].S == "__SUBTEX__") return emitSrcFromPendingSub(); std::string tex = namedS("tex").value_or(posS(1).value_or("")); if(tex.empty()) { if(err) *err="lowpart requires texture arg"; return false; } emitSrcFromName(tex); return true; } // --- Unary ops --- if(op.Name == "resize") { uint32_t w = namedU("w").value_or(posU(0).value_or(0)); uint32_t h = namedU("h").value_or(posU(1).value_or(0)); if(!w || !h || w>65535u || h>65535u) { if(err) *err="resize(w,h) must fit uint16"; return false; } _emitOp(out, Op::Resize); _emitU16(out, w); _emitU16(out, h); return true; } if(op.Name == "transform") { uint32_t t = namedU("t").value_or(posU(0).value_or(0)); _emitOp(out, Op::Transform); _emitU8(out, t & 7u); return true; } if(op.Name == "opacity") { uint32_t a = namedU("a").value_or(posU(0).value_or(255)); _emitOp(out, Op::Opacity); _emitU8(out, a & 0xFFu); return true; } if(op.Name == "remove_alpha" || op.Name == "noalpha") { _emitOp(out, Op::NoAlpha); return true; } if(op.Name == "make_alpha") { std::string col = namedS("color").value_or(posS(0).value_or("")); uint32_t argb=0; if(!_parseHexColor(col, argb)) { if(err) *err="make_alpha requires color #RRGGBB"; return false; } uint32_t rgb24 = argb & 0x00FFFFFFu; _emitOp(out, Op::MakeAlpha); _emitU8(out, (rgb24 >> 16) & 0xFFu); _emitU8(out, (rgb24 >> 8) & 0xFFu); _emitU8(out, (rgb24 >> 0) & 0xFFu); return true; } if(op.Name == "invert") { std::string ch = namedS("channels").value_or(posS(0).value_or("rgb")); uint32_t mask=0; for(char c : ch) { if(c=='r') mask |= 1; if(c=='g') mask |= 2; if(c=='b') mask |= 4; if(c=='a') mask |= 8; } _emitOp(out, Op::Invert); _emitU8(out, mask & 0xFu); return true; } if(op.Name == "brighten") { _emitOp(out, Op::Brighten); return true; } if(op.Name == "contrast") { int c = int(namedU("value").value_or(posU(0).value_or(0))); int b = int(namedU("brightness").value_or(posU(1).value_or(0))); c = std::max(-127, std::min(127, c)); b = std::max(-127, std::min(127, b)); _emitOp(out, Op::Contrast); _emitU8(out, uint32_t(c + 127)); _emitU8(out, uint32_t(b + 127)); return true; } auto compileColorOp = [&](Op opcode, bool needsRatio)->bool{ std::string col = namedS("color").value_or(posS(0).value_or("")); uint32_t argb=0; if(!_parseHexColor(col, argb)) { if(err) *err="Bad color literal"; return false; } _emitOp(out, opcode); _emitU32(out, argb); if(needsRatio) { uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255)); _emitU8(out, ratio & 0xFFu); } return true; }; if(op.Name == "multiply") return compileColorOp(Op::Multiply, false); if(op.Name == "screen") return compileColorOp(Op::Screen, false); if(op.Name == "colorize") return compileColorOp(Op::Colorize, true); if(op.Name == "anim") { uint32_t frameW = namedU("frame_w").value_or(namedU("w").value_or(posU(0).value_or(0))); uint32_t frameH = namedU("frame_h").value_or(namedU("h").value_or(posU(1).value_or(0))); uint32_t frames = namedU("frames").value_or(namedU("count").value_or(posU(2).value_or(0))); uint32_t fps = namedU("fps").value_or(posU(3).value_or(0)); uint32_t smooth = namedU("smooth").value_or(posU(4).value_or(0)); std::string axis = namedS("axis").value_or(""); bool horizontal = (!axis.empty() && (axis[0] == 'x' || axis[0] == 'h')); if(frameW > 65535u || frameH > 65535u || frames > 65535u) { if(err) *err="anim params must fit uint16"; return false; } uint32_t fpsQ = fps ? std::min(0xFFFFu, fps * 256u) : DefaultAnimFpsQ; uint32_t flags = (smooth ? AnimSmooth : 0) | (horizontal ? AnimHorizontal : 0); _emitOp(out, Op::Anim); _emitU16(out, frameW); _emitU16(out, frameH); _emitU16(out, frames); _emitU16(out, fpsQ); _emitU8(out, flags); return true; } if(err) *err = "Unknown op: " + op.Name; return false; } };