1272 lines
45 KiB
C++
1272 lines
45 KiB
C++
#pragma once
|
||
|
||
#include <cstdint>
|
||
#include <vector>
|
||
#include <string>
|
||
#include <string_view>
|
||
#include <optional>
|
||
#include <functional>
|
||
#include <unordered_map>
|
||
#include <algorithm>
|
||
#include <cstring>
|
||
|
||
// ========================
|
||
// External texture view
|
||
// ========================
|
||
struct Texture {
|
||
uint32_t Width, Height;
|
||
const uint32_t* Pixels; // assumed 0xAARRGGBB
|
||
};
|
||
|
||
// ========================
|
||
// Bytecode words are uint16_t
|
||
// ========================
|
||
class TexturePipelineProgram {
|
||
public:
|
||
using Word = uint16_t;
|
||
|
||
struct OwnedTexture {
|
||
uint32_t Width = 0, Height = 0;
|
||
std::vector<uint32_t> Pixels;
|
||
Texture view() const { return Texture{Width, Height, Pixels.data()}; }
|
||
};
|
||
|
||
// name -> uint32 id
|
||
using IdResolver = std::function<std::optional<uint32_t>(std::string_view)>;
|
||
// id -> Texture view
|
||
using TextureProvider = std::function<std::optional<Texture>(uint32_t)>;
|
||
|
||
// Patch points to two consecutive u16 words where uint32 texId lives (lo, hi)
|
||
struct Patch {
|
||
size_t WordIndexLo = 0; // Code_[lo], Code_[lo+1] is hi
|
||
std::string Name;
|
||
};
|
||
|
||
// ---- compile / link / bake ----
|
||
bool compile(std::string src, std::string* err = nullptr) {
|
||
Source_ = std::move(src);
|
||
Code_.clear();
|
||
Patches_.clear();
|
||
return _parseProgram(err);
|
||
}
|
||
|
||
bool link(const IdResolver& resolver, std::string* err = nullptr) {
|
||
for(const auto& p : Patches_) {
|
||
auto id = resolver(p.Name);
|
||
if(!id) {
|
||
if(err) *err = "Unresolved texture name: " + p.Name;
|
||
return false;
|
||
}
|
||
if(p.WordIndexLo + 1 >= Code_.size()) {
|
||
if(err) *err = "Internal error: patch out of range";
|
||
return false;
|
||
}
|
||
Code_[p.WordIndexLo + 0] = _lo16(*id);
|
||
Code_[p.WordIndexLo + 1] = _hi16(*id);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool bake(const TextureProvider& provider, OwnedTexture& out, std::string* err = nullptr) const {
|
||
VM vm(provider);
|
||
return vm.run(Code_, out, err);
|
||
}
|
||
|
||
const std::vector<Word>& words() const { return Code_; }
|
||
const std::vector<Patch>& patches() const { return Patches_; }
|
||
|
||
// Serialize words to bytes (little-endian)
|
||
std::vector<uint8_t> toBytes() const {
|
||
std::vector<uint8_t> bytes(Code_.size() * sizeof(Word));
|
||
std::memcpy(bytes.data(), Code_.data(), bytes.size());
|
||
return bytes;
|
||
}
|
||
|
||
void fromWords(std::vector<Word> words) {
|
||
Code_ = std::move(words);
|
||
Patches_.clear();
|
||
Source_.clear();
|
||
}
|
||
|
||
private:
|
||
// ========================
|
||
// Word helpers
|
||
// ========================
|
||
static constexpr uint32_t _make_u32(uint16_t lo, uint16_t hi) {
|
||
return uint32_t(lo) | (uint32_t(hi) << 16);
|
||
}
|
||
static constexpr uint16_t _lo16(uint32_t v) { return uint16_t(v & 0xFFFFu); }
|
||
static constexpr uint16_t _hi16(uint32_t v) { return uint16_t((v >> 16) & 0xFFFFu); }
|
||
|
||
// ========================
|
||
// SrcRef encoding in u16 words
|
||
// kind + a + b (3 words)
|
||
// kind=0 TexId: a=_lo16(id), b=_hi16(id)
|
||
// kind=1 Sub : a=offsetWords, b=lenWords
|
||
// ========================
|
||
enum class SrcKind : Word { TexId = 0, Sub = 1 };
|
||
|
||
struct SrcRef {
|
||
SrcKind Kind;
|
||
Word A;
|
||
Word B;
|
||
};
|
||
|
||
// ========================
|
||
// Opcodes (fixed-length headers; some are variable like Combine)
|
||
// ========================
|
||
enum class Op : Word {
|
||
End = 0,
|
||
|
||
// Base producers (top-level expression must start with one of these)
|
||
Base_Tex = 1, // args: SrcRef(TexId) -> kind,lo,hi
|
||
Base_Fill = 2, // args: w, h, color_lo, color_hi
|
||
|
||
// Unary ops on current image
|
||
Resize = 10, // w, h
|
||
Transform = 11, // t(0..7)
|
||
Opacity = 12, // a(0..255)
|
||
NoAlpha = 13, // -
|
||
MakeAlpha = 14, // rgb_lo(0xRRGG), rgb_hi(0x00BB) packed as 24-bit in 2 words
|
||
Invert = 15, // mask bits (r=1 g=2 b=4 a=8)
|
||
Brighten = 16, // -
|
||
Contrast = 17, // contrast_bias(0..254), bright_bias(0..254) where v = bias-127
|
||
Multiply = 18, // color_lo, color_hi (0xAARRGGBB)
|
||
Screen = 19, // color_lo, color_hi
|
||
Colorize = 20, // color_lo, color_hi, ratio(0..255)
|
||
|
||
// Ops that consume a SrcRef (TexId or Sub)
|
||
Overlay = 30, // SrcRef (3 words)
|
||
Mask = 31, // SrcRef
|
||
LowPart = 32, // percent(0..100), SrcRef (1 + 3 words)
|
||
|
||
// Variable example (optional): Combine
|
||
// Combine: w,h, n, then n times: x,y, SrcRef (x,y,kind,a,b)
|
||
Combine = 40
|
||
};
|
||
|
||
// ========================
|
||
// 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 u16 words)
|
||
// ========================
|
||
struct Image {
|
||
uint32_t W=0,H=0;
|
||
std::vector<uint32_t> Px;
|
||
};
|
||
|
||
class VM {
|
||
public:
|
||
explicit VM(TextureProvider provider) : Provider_(std::move(provider)) {}
|
||
|
||
bool run(const std::vector<Word>& code, OwnedTexture& out, std::string* err) {
|
||
if(code.empty()) { if(err) *err="Empty bytecode"; return false; }
|
||
|
||
Image cur;
|
||
std::unordered_map<uint32_t, Image> texCache;
|
||
std::unordered_map<uint32_t, Image> subCache; // key = (offset<<16)|len (fits if <=65535)
|
||
|
||
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<Op>(code[ip++]);
|
||
if(op == Op::End) break;
|
||
|
||
switch (op) {
|
||
case Op::Base_Tex: {
|
||
if(!need(3)) return false;
|
||
SrcRef src = _readSrc(code, ip);
|
||
if(src.Kind != SrcKind::TexId) return _bad(err, "Base_Tex must be TexId");
|
||
cur = _loadTex(_make_u32(src.A, src.B), texCache, err);
|
||
if(cur.W == 0) return false;
|
||
} break;
|
||
|
||
case Op::Base_Fill: {
|
||
if(!need(4)) return false;
|
||
uint32_t w = code[ip++], h = code[ip++];
|
||
uint32_t colorLo = code[ip++];
|
||
uint32_t colorHi = code[ip++];
|
||
uint32_t color = _make_u32(colorLo, colorHi);
|
||
cur = _makeSolid(w, h, color);
|
||
} break;
|
||
|
||
case Op::Overlay: {
|
||
if(!need(3)) return false;
|
||
SrcRef src = _readSrc(code, ip);
|
||
Image over = _loadSrc(code, src, texCache, subCache, err);
|
||
if(over.W == 0) return false;
|
||
if(!cur.W) { cur = std::move(over); break; } // if no base, adopt
|
||
over = _resizeNN_ifNeeded(over, cur.W, cur.H);
|
||
_alphaOver(cur, over);
|
||
} break;
|
||
|
||
case Op::Mask: {
|
||
if(!need(3)) return false;
|
||
SrcRef src = _readSrc(code, ip);
|
||
Image m = _loadSrc(code, src, texCache, subCache, 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+3)) return false;
|
||
uint32_t pct = std::min<uint32_t>(100u, code[ip++]);
|
||
SrcRef src = _readSrc(code, ip);
|
||
Image over = _loadSrc(code, src, texCache, subCache, 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(!need(2)) return false;
|
||
uint32_t w = code[ip++], h = code[ip++];
|
||
if(!cur.W) return _bad(err, "Resize requires base image");
|
||
cur = _resizeNN(cur, w, h);
|
||
} break;
|
||
|
||
case Op::Transform: {
|
||
if(!need(1)) return false;
|
||
uint32_t t = code[ip++] & 7u;
|
||
if(!cur.W) return _bad(err, "Transform requires base image");
|
||
cur = _transform(cur, t);
|
||
} break;
|
||
|
||
case Op::Opacity: {
|
||
if(!need(1)) return false;
|
||
uint32_t a = code[ip++] & 0xFFu;
|
||
if(!cur.W) return _bad(err, "Opacity requires base image");
|
||
_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(!need(2)) return false;
|
||
uint32_t rgb24 = (uint32_t(code[ip+1]) << 16) | uint32_t(code[ip]); // lo has RR GG, hi has 00 BB
|
||
ip += 2;
|
||
if(!cur.W) return _bad(err, "MakeAlpha requires base image");
|
||
_makeAlpha(cur, rgb24 & 0x00FFFFFFu);
|
||
} break;
|
||
|
||
case Op::Invert: {
|
||
if(!need(1)) return false;
|
||
uint32_t mask = code[ip++] & 0xFu;
|
||
if(!cur.W) return _bad(err, "Invert requires base image");
|
||
_invert(cur, mask);
|
||
} break;
|
||
|
||
case Op::Brighten: {
|
||
if(!cur.W) return _bad(err, "Brighten requires base image");
|
||
_brighten(cur);
|
||
} break;
|
||
|
||
case Op::Contrast: {
|
||
if(!need(2)) return false;
|
||
int c = int(code[ip++]) - 127;
|
||
int b = int(code[ip++]) - 127;
|
||
if(!cur.W) return _bad(err, "Contrast requires base image");
|
||
_contrast(cur, c, b);
|
||
} break;
|
||
|
||
case Op::Multiply: {
|
||
if(!need(2)) return false;
|
||
uint32_t colorLo = code[ip++];
|
||
uint32_t colorHi = code[ip++];
|
||
uint32_t color = _make_u32(colorLo, colorHi);
|
||
if(!cur.W) return _bad(err, "Multiply requires base image");
|
||
_multiply(cur, color);
|
||
} break;
|
||
|
||
case Op::Screen: {
|
||
if(!need(2)) return false;
|
||
uint32_t colorLo = code[ip++];
|
||
uint32_t colorHi = code[ip++];
|
||
uint32_t color = _make_u32(colorLo, colorHi);
|
||
if(!cur.W) return _bad(err, "Screen requires base image");
|
||
_screen(cur, color);
|
||
} break;
|
||
|
||
case Op::Colorize: {
|
||
if(!need(3)) return false;
|
||
uint32_t colorLo = code[ip++];
|
||
uint32_t colorHi = code[ip++];
|
||
uint32_t color = _make_u32(colorLo, colorHi);
|
||
uint32_t ratio = code[ip++] & 0xFFu;
|
||
if(!cur.W) return _bad(err, "Colorize requires base image");
|
||
_colorize(cur, color, uint8_t(ratio));
|
||
} break;
|
||
|
||
case Op::Combine: {
|
||
// variable length:
|
||
// w,h,n then for each: x,y, SrcRef(3)
|
||
if(!need(3)) return false;
|
||
uint32_t w = code[ip++], h = code[ip++], n = code[ip++];
|
||
Image outImg; outImg.W=w; outImg.H=h; outImg.Px.assign(size_t(w)*size_t(h), 0u);
|
||
for(uint32_t i=0;i<n;i++){
|
||
if(!need(2+3)) return false;
|
||
int x = int(code[ip++]);
|
||
int y = int(code[ip++]);
|
||
SrcRef src = _readSrc(code, ip);
|
||
Image part = _loadSrc(code, src, texCache, subCache, err);
|
||
if(part.W == 0) return false;
|
||
_overlayAt(outImg, part, x, y);
|
||
}
|
||
cur = std::move(outImg);
|
||
} 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 SrcRef _readSrc(const std::vector<Word>& code, size_t& ip) {
|
||
SrcRef r;
|
||
r.Kind = static_cast<SrcKind>(code[ip++]);
|
||
r.A = code[ip++];
|
||
r.B = code[ip++];
|
||
return r;
|
||
}
|
||
|
||
Image _loadTex(uint32_t id, std::unordered_map<uint32_t, Image>& 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<Word>& code,
|
||
Word off, Word len,
|
||
std::unordered_map<uint32_t, Image>& texCache,
|
||
std::unordered_map<uint32_t, Image>& subCache,
|
||
std::string* err) {
|
||
uint32_t key = (uint32_t(off) << 16) | 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 {}; }
|
||
|
||
// Run subprogram slice by copying minimal (simple + safe).
|
||
std::vector<Word> slice(code.begin()+start, code.begin()+end);
|
||
OwnedTexture tmp;
|
||
VM nested(Provider_);
|
||
if(!nested.run(slice, tmp, 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<Word>& code,
|
||
const SrcRef& src,
|
||
std::unordered_map<uint32_t, Image>& texCache,
|
||
std::unordered_map<uint32_t, Image>& subCache,
|
||
std::string* err) {
|
||
if(src.Kind == SrcKind::TexId) {
|
||
return _loadTex(_make_u32(src.A, src.B), texCache, err);
|
||
}
|
||
if(src.Kind == SrcKind::Sub) {
|
||
return _loadSub(code, src.A, src.B, texCache, subCache, 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<nh;y++){
|
||
uint32_t sy = (uint64_t(y) * src.H) / nh;
|
||
for(uint32_t x=0;x<nw;x++){
|
||
uint32_t sx = (uint64_t(x) * src.W) / nw;
|
||
dst.Px[size_t(y)*nw + x] = src.Px[size_t(sy)*src.W + sx];
|
||
}
|
||
}
|
||
return dst;
|
||
}
|
||
|
||
static Image _resizeNN_ifNeeded(Image img, uint32_t w, uint32_t h) {
|
||
if(img.W == w && img.H == h) return img;
|
||
return _resizeNN(img, w, h);
|
||
}
|
||
|
||
static void _alphaOver(Image& base, const Image& over) {
|
||
const size_t n = base.Px.size();
|
||
for(size_t i=0;i<n;i++){
|
||
uint32_t b = base.Px[i], o = over.Px[i];
|
||
uint8_t ba=_a(b), br=_r(b), bg=_g(b), bb=_b(b);
|
||
uint8_t oa=_a(o), or_=_r(o), og=_g(o), ob=_b(o);
|
||
|
||
uint32_t brp = (uint32_t(br) * ba) / 255;
|
||
uint32_t bgp = (uint32_t(bg) * ba) / 255;
|
||
uint32_t bbp = (uint32_t(bb) * ba) / 255;
|
||
|
||
uint32_t orp = (uint32_t(or_) * oa) / 255;
|
||
uint32_t ogp = (uint32_t(og) * oa) / 255;
|
||
uint32_t obp = (uint32_t(ob) * oa) / 255;
|
||
|
||
uint32_t inv = 255 - oa;
|
||
uint32_t outA = oa + (uint32_t(ba) * inv) / 255;
|
||
uint32_t outRp = orp + (brp * inv) / 255;
|
||
uint32_t outGp = ogp + (bgp * inv) / 255;
|
||
uint32_t outBp = obp + (bbp * inv) / 255;
|
||
|
||
uint8_t outR=0,outG=0,outB=0;
|
||
if(outA) {
|
||
outR = uint8_t(std::min<uint32_t>(255, (outRp * 255) / outA));
|
||
outG = uint8_t(std::min<uint32_t>(255, (outGp * 255) / outA));
|
||
outB = uint8_t(std::min<uint32_t>(255, (outBp * 255) / outA));
|
||
}
|
||
base.Px[i] = _pack(uint8_t(outA), outR, outG, outB);
|
||
}
|
||
}
|
||
|
||
static void _overlayAt(Image& dst, const Image& src, int ox, int oy) {
|
||
for(uint32_t y=0;y<src.H;y++){
|
||
int dy = oy + int(y);
|
||
if(dy < 0 || dy >= int(dst.H)) continue;
|
||
for(uint32_t x=0;x<src.W;x++){
|
||
int dx = ox + int(x);
|
||
if(dx < 0 || dx >= int(dst.W)) continue;
|
||
size_t di = size_t(dy)*dst.W + uint32_t(dx);
|
||
uint32_t b = dst.Px[di], o = src.Px[size_t(y)*src.W + x];
|
||
|
||
uint8_t ba=_a(b), br=_r(b), bg=_g(b), bb=_b(b);
|
||
uint8_t oa=_a(o), or_=_r(o), og=_g(o), ob=_b(o);
|
||
|
||
uint32_t brp = (uint32_t(br) * ba) / 255;
|
||
uint32_t bgp = (uint32_t(bg) * ba) / 255;
|
||
uint32_t bbp = (uint32_t(bb) * ba) / 255;
|
||
|
||
uint32_t orp = (uint32_t(or_) * oa) / 255;
|
||
uint32_t ogp = (uint32_t(og) * oa) / 255;
|
||
uint32_t obp = (uint32_t(ob) * oa) / 255;
|
||
|
||
uint32_t inv = 255 - oa;
|
||
uint32_t outA = oa + (uint32_t(ba) * inv) / 255;
|
||
uint32_t outRp = orp + (brp * inv) / 255;
|
||
uint32_t outGp = ogp + (bgp * inv) / 255;
|
||
uint32_t outBp = obp + (bbp * inv) / 255;
|
||
|
||
uint8_t outR=0,outG=0,outB=0;
|
||
if(outA) {
|
||
outR = uint8_t(std::min<uint32_t>(255, (outRp * 255) / outA));
|
||
outG = uint8_t(std::min<uint32_t>(255, (outGp * 255) / outA));
|
||
outB = uint8_t(std::min<uint32_t>(255, (outBp * 255) / outA));
|
||
}
|
||
dst.Px[di] = _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<n;i++){
|
||
uint32_t b = base.Px[i], m = mask.Px[i];
|
||
uint8_t outA = uint8_t((uint32_t(_a(b)) * uint32_t(_a(m))) / 255);
|
||
base.Px[i] = _pack(outA, _r(b), _g(b), _b(b));
|
||
}
|
||
}
|
||
|
||
static void _opacity(Image& img, uint8_t mul) {
|
||
for(auto& p : img.Px) {
|
||
uint8_t na = uint8_t((uint32_t(_a(p)) * mul) / 255);
|
||
p = _pack(na, _r(p), _g(p), _b(p));
|
||
}
|
||
}
|
||
static void _noAlpha(Image& img) {
|
||
for(auto& p : img.Px) p = _pack(255, _r(p), _g(p), _b(p));
|
||
}
|
||
static void _makeAlpha(Image& img, uint32_t rgb24) {
|
||
uint8_t rr = uint8_t((rgb24 >> 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<base.H; y++){
|
||
for(uint32_t x=0; x<base.W; x++){
|
||
size_t i = size_t(y)*base.W + x;
|
||
// overlay one pixel
|
||
uint32_t b = base.Px[i], o = over.Px[i];
|
||
uint8_t ba=_a(b), br=_r(b), bg=_g(b), bb=_b(b);
|
||
uint8_t oa=_a(o), or_=_r(o), og=_g(o), ob=_b(o);
|
||
|
||
uint32_t brp = (uint32_t(br) * ba) / 255;
|
||
uint32_t bgp = (uint32_t(bg) * ba) / 255;
|
||
uint32_t bbp = (uint32_t(bb) * ba) / 255;
|
||
|
||
uint32_t orp = (uint32_t(or_) * oa) / 255;
|
||
uint32_t ogp = (uint32_t(og) * oa) / 255;
|
||
uint32_t obp = (uint32_t(ob) * oa) / 255;
|
||
|
||
uint32_t inv = 255 - oa;
|
||
uint32_t outA = oa + (uint32_t(ba) * inv) / 255;
|
||
uint32_t outRp = orp + (brp * inv) / 255;
|
||
uint32_t outGp = ogp + (bgp * inv) / 255;
|
||
uint32_t outBp = obp + (bbp * inv) / 255;
|
||
|
||
uint8_t outR=0,outG=0,outB=0;
|
||
if(outA) {
|
||
outR = uint8_t(std::min<uint32_t>(255, (outRp * 255) / outA));
|
||
outG = uint8_t(std::min<uint32_t>(255, (outGp * 255) / outA));
|
||
outB = uint8_t(std::min<uint32_t>(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<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(y, src.H-1-x));
|
||
} break;
|
||
case 2: { dst = make(src.W, src.H);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(src.W-1-x, src.H-1-y));
|
||
} break;
|
||
case 3: { dst = make(src.H, src.W);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(src.W-1-y, x));
|
||
} break;
|
||
case 4: { dst = make(src.W, src.H);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(src.W-1-x, y));
|
||
} break;
|
||
case 5: { dst = make(src.H, src.W);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(src.H-1-y, src.H-1-x));
|
||
} break;
|
||
case 6: { dst = make(src.W, src.H);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(x, src.H-1-y));
|
||
} break;
|
||
case 7: { dst = make(src.H, src.W);
|
||
for(uint32_t y=0;y<dst.H;y++) for(uint32_t x=0;x<dst.W;x++)
|
||
set(dst, x,y, at(y, x));
|
||
} break;
|
||
}
|
||
return dst;
|
||
}
|
||
};
|
||
|
||
// ========================
|
||
// Minimal DSL Lexer/Parser
|
||
// Supports:
|
||
// tex "name" |> op(args...)
|
||
// tex 32x32 "#RRGGBBAA" |> ...
|
||
// Grouping (subprogram) only where an op expects a texture arg:
|
||
// overlay( tex "b" |> ... )
|
||
// ========================
|
||
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;
|
||
|
||
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 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() && S[I]!='\n') I++; continue; } // line comment
|
||
if(c=='/' && I+1<S.size() && S[I+1]=='/'){ I+=2; while (I<S.size() && S[I]!='\n') I++; continue; }
|
||
break;
|
||
}
|
||
}
|
||
|
||
Tok next() {
|
||
skipWs();
|
||
if(I >= S.size()) return {TokKind::End, {}, 0};
|
||
|
||
if(S[I]=='|' && I+1<S.size() && S[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<S.size()) {
|
||
char esc = S[I++];
|
||
switch (esc) {
|
||
case 'n': out.push_back('\n'); break;
|
||
case 't': out.push_back('\t'); break;
|
||
case '"': out.push_back('"'); break;
|
||
case '\\':out.push_back('\\'); break;
|
||
default: out.push_back(esc); break;
|
||
}
|
||
} else out.push_back(ch);
|
||
}
|
||
return {TokKind::String, std::move(out), 0};
|
||
}
|
||
|
||
if(isNum(c)) {
|
||
uint64_t v=0;
|
||
size_t start=I;
|
||
while (I<S.size() && isNum(S[I])) { v = v*10 + uint64_t(S[I]-'0'); I++; }
|
||
return {TokKind::Number, std::string(S.substr(start, I-start)), uint32_t(v)};
|
||
}
|
||
|
||
if(isAlpha(c) || c=='#') {
|
||
size_t start=I;
|
||
I++;
|
||
while (I<S.size() && (isAlnum(S[I]) || S[I]=='.' || S[I]=='#')) I++;
|
||
return {TokKind::Ident, std::string(S.substr(start, I-start)), 0};
|
||
}
|
||
|
||
I = S.size();
|
||
return {TokKind::End, {}, 0};
|
||
}
|
||
};
|
||
|
||
struct ArgVal {
|
||
enum class ValueKind { U32, Str, Ident };
|
||
ValueKind Kind = ValueKind::U32;
|
||
uint32_t U32 = 0;
|
||
std::string S;
|
||
};
|
||
|
||
struct ParsedOp {
|
||
std::string Name;
|
||
std::vector<ArgVal> Pos;
|
||
std::unordered_map<std::string, ArgVal> Named;
|
||
// For ops that accept texture expression, we allow first positional arg to be "subexpr marker"
|
||
// but we handle that at compile-time by parsing texture expr inside parentheses.
|
||
};
|
||
|
||
// ========================
|
||
// Compiler state
|
||
// ========================
|
||
std::string Source_;
|
||
std::vector<Word> Code_;
|
||
std::vector<Patch> Patches_;
|
||
|
||
// ---- _emit helpers ----
|
||
void _emit(Op op) { Code_.push_back(Word(op)); }
|
||
void _emitW(uint32_t v) { Code_.push_back(Word(v & 0xFFFFu)); }
|
||
void _emitU32(uint32_t v) { Code_.push_back(_lo16(v)); Code_.push_back(_hi16(v)); }
|
||
|
||
void _emitTexRefName(const std::string& name) {
|
||
// reserve lo+hi for uint32 texId
|
||
size_t lo = Code_.size();
|
||
Code_.push_back(0);
|
||
Code_.push_back(0);
|
||
Patches_.push_back(Patch{lo, name});
|
||
}
|
||
|
||
void _emitSrcRef(const SrcRef& r) {
|
||
Code_.push_back(Word(r.Kind));
|
||
Code_.push_back(r.A);
|
||
Code_.push_back(r.B);
|
||
}
|
||
|
||
// ========================
|
||
// Color parsing: #RRGGBB or #RRGGBBAA
|
||
// Stored as 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<uint8_t>{
|
||
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;
|
||
}
|
||
|
||
// Parse base expression after tex:
|
||
// 1) "name"
|
||
// 2) Number X Number Ident(color)
|
||
// 3) (future) png("...")
|
||
Tok a = lx.next();
|
||
|
||
if(a.Kind == TokKind::String || a.Kind == TokKind::Ident) {
|
||
// tex "name.png"
|
||
_emit(Op::Base_Tex);
|
||
// SrcRef(TexId): kind + id(lo/hi)
|
||
Code_.push_back(Word(SrcKind::TexId));
|
||
_emitTexRefName(a.Text); // lo+hi patched later
|
||
} else 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 <w>x<h> <#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; }
|
||
_emit(Op::Base_Fill);
|
||
_emitW(w); _emitW(h);
|
||
_emitU32(color);
|
||
} else {
|
||
if(err) *err="Bad 'tex' base expression";
|
||
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 {
|
||
// no-arg op (like brighten) must be followed by next |> or end
|
||
nt = peek;
|
||
}
|
||
|
||
if(!_compileOp(lx, op, err)) return false;
|
||
}
|
||
|
||
_emit(Op::End);
|
||
return true;
|
||
}
|
||
|
||
// Parses either:
|
||
// - normal args list: (a,b,key=v)
|
||
// - OR for ops that take texture, allow: ( tex ... |> ... ) as the *first* positional "special"
|
||
bool _parseArgListOrTextureExpr(Lexer& lx, ParsedOp& op, std::string* err) {
|
||
// Lookahead: if next token is 'tex' => parse sub texture expression until ')'
|
||
Tok first = lx.next();
|
||
if(first.Kind==TokKind::Ident && first.Text=="tex") {
|
||
// We parse a full texture expression (starting after 'tex') into a subprogram bytecode vector.
|
||
// We'll store a marker in op.Named["_subtex"] with special string "<compiled later>"
|
||
// But easier: store the subprogram words immediately as a pseudo-arg in op.Pos[0].S = "<SUB>"
|
||
ArgVal av; av.Kind = ArgVal::ValueKind::Ident; av.S = "__SUBTEX__";
|
||
op.Pos.push_back(std::move(av));
|
||
|
||
// compile subprogram into vector<Word> sub
|
||
std::vector<Word> sub;
|
||
if(!_compileSubProgramFromAlreadySawTex(lx, sub, err)) return false;
|
||
|
||
// Expect ')'
|
||
Tok end = lx.next();
|
||
if(end.Kind != TokKind::RParen) { if(err) *err="Expected ')' after sub texture expr"; return false; }
|
||
|
||
// Stash the subprogram into an internal buffer attached to this op (hack: store in a map)
|
||
PendingSub_[&op] = std::move(sub);
|
||
return true;
|
||
}
|
||
|
||
// Otherwise parse 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 _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' token. Now we parse base + pipeline until we hit ')'
|
||
// Strategy:
|
||
// - compile into `sub` vector<Word>
|
||
// - stop when next token would be ')'
|
||
// - do NOT consume ')'
|
||
// ========================
|
||
bool _compileSubProgramFromAlreadySawTex(Lexer& lx, std::vector<Word>& sub, std::string* err) {
|
||
// We reuse a mini-compiler that writes into `sub` instead of Code_
|
||
auto emitS = [&](Op op){ sub.push_back(Word(op)); };
|
||
auto emitSW = [&](uint32_t v){ sub.push_back(Word(v & 0xFFFFu)); };
|
||
auto emitSU32 = [&](uint32_t v){ sub.push_back(_lo16(v)); sub.push_back(_hi16(v)); };
|
||
auto emitSTexName = [&](const std::string& name){
|
||
// IMPORTANT: patches must point into main Code_, not sub.
|
||
// Solution: subprogram words are appended into main Code_ later, so we can patch after append.
|
||
// Here we place placeholder lo/hi and store a *relative patch* into SubPatchesTemp_.
|
||
size_t lo = sub.size();
|
||
sub.push_back(0); sub.push_back(0);
|
||
SubPatchesTemp_.push_back({lo, name}); // relative to sub start
|
||
};
|
||
|
||
Tok a = lx.next();
|
||
if(a.Kind == TokKind::String || a.Kind == TokKind::Ident) {
|
||
emitS(Op::Base_Tex);
|
||
sub.push_back(Word(SrcKind::TexId));
|
||
emitSTexName(a.Text);
|
||
} else if(a.Kind == TokKind::Number) {
|
||
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="Sub tex: expected <w>x<h> <#color>";
|
||
return false;
|
||
}
|
||
uint32_t w = a.U32, h = b.U32;
|
||
uint32_t color=0;
|
||
if(!_parseHexColor(colTok.Text, color)) { if(err) *err="Sub tex: bad color"; return false; }
|
||
if(w>65535u || h>65535u) { if(err) *err="Sub tex: w/h must fit uint16"; return false; }
|
||
emitS(Op::Base_Fill);
|
||
emitSW(w); emitSW(h);
|
||
emitSU32(color);
|
||
} else {
|
||
if(err) *err="Sub tex: bad base";
|
||
return false;
|
||
}
|
||
|
||
// Pipeline until we see ')' lookahead (we can’t unread, so we detect by peeking in a copy)
|
||
while (true) {
|
||
// Peek next non-ws token without consuming by copying lexer
|
||
Lexer peek = lx;
|
||
Tok nt = peek.next();
|
||
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
|
||
}
|
||
|
||
// compile op into `sub` by temporarily swapping buffers
|
||
if(!_compileOpInto(lx, op, sub, emitS, emitSW, emitSU32, emitSTexName, err)) return false;
|
||
}
|
||
|
||
emitS(Op::End);
|
||
return true;
|
||
}
|
||
|
||
// Temporary relative patches inside subprogram being built
|
||
struct RelPatch { size_t RelLo; std::string Name; };
|
||
mutable std::vector<RelPatch> SubPatchesTemp_;
|
||
|
||
// Stash compiled subprogram per op pointer (simplifies this one-file example)
|
||
mutable std::unordered_map<const ParsedOp*, std::vector<Word>> PendingSub_;
|
||
|
||
// Append a subprogram to main Code_, returning SrcRef(Sub, offset,len) and migrating its patches
|
||
SrcRef _appendSubprogram(std::vector<Word>&& sub) {
|
||
// offset/len must fit u16
|
||
size_t offset = Code_.size();
|
||
size_t len = sub.size();
|
||
|
||
// migrate relative patches -> absolute patches into main Code_
|
||
// Each rel patch points to lo word within sub vector.
|
||
for(const auto& rp : SubPatchesTemp_) {
|
||
size_t absLo = offset + rp.RelLo;
|
||
Patches_.push_back(Patch{absLo, rp.Name});
|
||
}
|
||
SubPatchesTemp_.clear();
|
||
|
||
Code_.insert(Code_.end(), sub.begin(), sub.end());
|
||
|
||
SrcRef r;
|
||
r.Kind = SrcKind::Sub;
|
||
r.A = Word(offset & 0xFFFFu);
|
||
r.B = Word(len & 0xFFFFu);
|
||
return r;
|
||
}
|
||
|
||
// ========================
|
||
// compile operations
|
||
// ========================
|
||
bool _compileOp(Lexer& lx, const ParsedOp& op, std::string* err) {
|
||
// Normal compile into main Code_
|
||
auto it = PendingSub_.find(&op);
|
||
const bool hasSub = (it != PendingSub_.end());
|
||
return _compileOpInto(
|
||
lx, op, Code_,
|
||
[&](Op o){ _emit(o); },
|
||
[&](uint32_t v){ _emitW(v); },
|
||
[&](uint32_t v){ _emitU32(v); },
|
||
[&](const std::string& name){ _emitTexRefName(name); },
|
||
err,
|
||
hasSub ? &it->second : nullptr
|
||
);
|
||
}
|
||
|
||
// Core compiler that can target either main `Code_` or a `sub` vector.
|
||
template <class EmitOp, class EmitWFn, class EmitU32Fn, class EmitTexNameFn>
|
||
bool _compileOpInto(Lexer& /*lx*/,
|
||
const ParsedOp& op,
|
||
std::vector<Word>& out,
|
||
EmitOp emitOpFn,
|
||
EmitWFn emitWFn,
|
||
EmitU32Fn emitU32Fn,
|
||
EmitTexNameFn emitTexNameFn,
|
||
std::string* err,
|
||
std::vector<Word>* pendingSub = nullptr) {
|
||
auto posU = [&](size_t i)->std::optional<uint32_t>{
|
||
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<std::string>{
|
||
if(i >= op.Pos.size()) return std::nullopt;
|
||
return op.Pos[i].S;
|
||
};
|
||
auto namedU = [&](std::string_view k)->std::optional<uint32_t>{
|
||
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<std::string>{
|
||
auto it = op.Named.find(std::string(k));
|
||
if(it==op.Named.end()) return std::nullopt;
|
||
return it->second.S;
|
||
};
|
||
|
||
auto emitSrcTexName = [&](const std::string& texName){
|
||
// SrcRef(TexId): kind + id(lo/hi)
|
||
out.push_back(Word(SrcKind::TexId));
|
||
emitTexNameFn(texName);
|
||
};
|
||
|
||
auto emitSrcFromPendingSub = [&]()->bool{
|
||
if(!pendingSub) { if(err) *err="Internal: missing subprogram"; return false; }
|
||
// move pendingSub into main Code_ ONLY (grouping only makes sense there)
|
||
// If we're compiling inside a subprogram and we see another nested subprogram,
|
||
// this demo keeps it simple: it will still append into the *same* vector (out),
|
||
// so we can just inline by "append here". For production, you likely want a
|
||
// separate sub-table or a more structured approach.
|
||
//
|
||
// For simplicity: we append nested subprogram right into `out` and reference it by offset/len.
|
||
size_t offset = out.size();
|
||
size_t len = pendingSub->size();
|
||
out.insert(out.end(), pendingSub->begin(), pendingSub->end());
|
||
out.push_back(Word(SrcKind::Sub));
|
||
out.push_back(Word(offset & 0xFFFFu));
|
||
out.push_back(Word(len & 0xFFFFu));
|
||
return true;
|
||
};
|
||
|
||
// --- Ops that accept a "texture" argument: overlay/mask/lowpart/combine parts ---
|
||
if(op.Name == "overlay") {
|
||
emitOpFn(Op::Overlay);
|
||
if(!op.Pos.empty() && op.Pos[0].S == "__SUBTEX__") {
|
||
// Subprogram source
|
||
// In main compile path, we prefer storing subprograms at end and referencing by offset/len.
|
||
// Here we already have the compiled sub in pendingSub; we append + _emit SrcRef(Sub,...).
|
||
// For main program, we use _appendSubprogram() outside; in this generic function we inline.
|
||
return emitSrcFromPendingSub();
|
||
}
|
||
std::string tex = namedS("tex").value_or(posS(0).value_or(""));
|
||
if(tex.empty()) { if(err) *err="overlay requires texture arg"; return false; }
|
||
emitSrcTexName(tex);
|
||
return true;
|
||
}
|
||
|
||
if(op.Name == "mask") {
|
||
emitOpFn(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; }
|
||
emitSrcTexName(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; }
|
||
emitOpFn(Op::LowPart);
|
||
emitWFn(std::min<uint32_t>(100u, pct));
|
||
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 tex"; return false; }
|
||
emitSrcTexName(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; }
|
||
emitOpFn(Op::Resize); emitWFn(w); emitWFn(h);
|
||
return true;
|
||
}
|
||
|
||
if(op.Name == "transform") {
|
||
uint32_t t = namedU("t").value_or(posU(0).value_or(0));
|
||
emitOpFn(Op::Transform); emitWFn(t & 7u);
|
||
return true;
|
||
}
|
||
|
||
if(op.Name == "opacity") {
|
||
uint32_t a = namedU("a").value_or(posU(0).value_or(255));
|
||
emitOpFn(Op::Opacity); emitWFn(a & 0xFFu);
|
||
return true;
|
||
}
|
||
|
||
if(op.Name == "remove_alpha" || op.Name == "noalpha") {
|
||
emitOpFn(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;
|
||
// pack rgb24 into two u16: lo=0xRRGG, hi=0x00BB
|
||
emitOpFn(Op::MakeAlpha);
|
||
emitWFn((rgb24 >> 8) & 0xFFFFu); // RR GG
|
||
emitWFn(rgb24 & 0x00FFu); // BB
|
||
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;
|
||
}
|
||
emitOpFn(Op::Invert); emitWFn(mask);
|
||
return true;
|
||
}
|
||
|
||
if(op.Name == "brighten") {
|
||
emitOpFn(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));
|
||
emitOpFn(Op::Contrast);
|
||
emitWFn(uint32_t(c + 127));
|
||
emitWFn(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; }
|
||
emitOpFn(opcode);
|
||
emitU32Fn(argb);
|
||
if(needsRatio) {
|
||
uint32_t ratio = namedU("ratio").value_or(posU(1).value_or(255));
|
||
emitWFn(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(err) *err = "Unknown op: " + op.Name;
|
||
return false;
|
||
}
|
||
};
|