No description
  • C++ 57%
  • Rust 43%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-05 16:11:36 +03:00
.cargo * build: CRT-static Windows exe (bare machines have no vcruntime140.dll) + clang>=20 always_inline fix: plain CFLAGS/CXXFLAGS=-march=x86-64-v3 (cargo-xwin appends them to its CFLAGS_<target>); version untouched (build config) 2026-10-23 16:29:38 +03:00
.forgejo/workflows * robustness fixes + CI quality gate + libwebp-sys 0.14 2026-10-17 14:03:28 +03:00
.zed + terminal progress UX and module split 2026-09-07 17:24:04 +03:00
src * webp: exact=1 — libwebp default exact=0 lets the encoder repaint alpha=0 gap pixels with neighbour prediction (green button ghosting across empty zones of raider atlas, bleeding into the cell top rows); with filter0 already shipped the seam is now clean: mean tint 22->0.84, worst block blue-drop 36->2, ~+2% size; test now guards worst-block blue-drop 2026-11-05 16:11:36 +03:00
test_files * robustness fixes + CI quality gate + libwebp-sys 0.14 2026-10-17 14:03:28 +03:00
.gitattributes * ver. 0.1.0 2026-09-05 17:07:48 +03:00
.gitignore + multi-format pack (--format webp etc1 etc2) processed in parallel threads 2026-09-06 16:54:34 +03:00
AGENTS.md * AGENTS.md: require local clippy -D warnings + cargo test before every src/ commit 2026-11-04 08:31:46 +03:00
build.rs * fix playrix cores: dedupe same-name classes via namespaces + init node sentinels 2026-10-28 15:26:41 +03:00
Cargo.lock * webp: disable VP8 in-loop deblocking (cwebp -f 0) — it drags AA-edge chroma across 16px macroblock boundaries onto the opaque rows of the atlas sprite below (merge_raider: green button painted over the teal cell, visible in-game at 2x); tint 22->4.7 and +0.4 dB PSNR at same size; regression test on a real atlas band fixture; version -> 2026.11.5 2026-11-05 14:24:13 +03:00
Cargo.toml * webp: disable VP8 in-loop deblocking (cwebp -f 0) — it drags AA-edge chroma across 16px macroblock boundaries onto the opaque rows of the atlas sprite below (merge_raider: green button painted over the teal cell, visible in-game at 2x); tint 22->4.7 and +0.4 dB PSNR at same size; regression test on a real atlas band fixture; version -> 2026.11.5 2026-11-05 14:24:13 +03:00
README.md * webp: exact=1 — libwebp default exact=0 lets the encoder repaint alpha=0 gap pixels with neighbour prediction (green button ghosting across empty zones of raider atlas, bleeding into the cell top rows); with filter0 already shipped the seam is now clean: mean tint 22->0.84, worst block blue-drop 36->2, ~+2% size; test now guards worst-block blue-drop 2026-11-05 16:11:36 +03:00

packit — Sprite Atlas Packer

Упаковщик спрайтов в атласы с агрегацией в .images@<ppu>x_<fmt>.new.zip в формате референсного пайплайна ConvertAndRepack.

Формат атласов и atlas_data.jsonl совпадает с описанием референсных данных test_files/rf_common/AGENTS.md.

Возможности

  • Упаковка webp-изображений (по layout.jsonl) в атласы
  • Несколько форматов за один запуск (--format webp etc1 etc2), форматы пакуются параллельно на потоках
  • Per-format выбор набора: суффиксы webp@1x / etc1@2x; без суффикса — 1x и 2x
  • Баланс «площадь ↔ число атласов» через --area-tolerance (по умолчанию 5%): меньше файлов при почти той же площади
  • Шапка с входными данными (наборы, число спрайтов, форматы), простая ASCII-анимация (спиннер) во время упаковки и итоговый отчёт с общим временем выполнения; сводная таблица (атласы / площадь / fill / размер зипа / экономия сжатия / время)
  • Папка --out создаётся по полному пути и очищается перед упаковкой (с защитой от --out = входная папка)
  • Форматы вывода: PNG, WebP (webp), ETC1, ETC2 (атлас — png / webp / pvr; для ETC1 дополнительно atlas_N_alpha.pvr при наличии прозрачности); в режиме --restype sprite — те же форматы по одному файлу на спрайт (name.webp/name.pvr + name_alpha.pvr)
  • Рендер и кодирование изображений атласов: lossy webp (libwebp, VP8X+ALPH+VP8; настройки по умолчанию как у референсного cwebp -mt -m 6 -q 75 -pass 10, кроме двух отклонений, лечащих межспрайтовое протекание цвета на merge_raider (зелёная кнопка окрашивала teal-ячейку ниже, видно в игре при 2x): filter_strength = 0 (cwebp -f 0) — деблокинг VP8 перетаскивает хрому AA-краёв спрайта в непрозрачные строки соседа через 16px-границу макроблока; exact = 1 — с дефолтным exact=0 libwebp произвольно перекрашивает RGB прозрачных пикселей («призрак» кнопки расползается по пустым зонам, экономя биты). Вместе: тинт кромки 22.0 → 0.84 (blue-drop 36 → 2) ценой ~2% размера; filter 0 дополнительно даёт +0.4 dB PSNR на непрозрачных пикселях), ETC1/ETC2 в контейнер PVR v3 (заголовок и метадата побайтово как в референсе)
  • ETC-энкодер на выбор (--etc-encoder): playrix — брутфорс-ядро Playrix Etc*Compress (MIT) с перцептивной метрикой и SSIM-отбором; на rf_common@1x даёт ~+1.7 дБ (ETC1) и ~+1.9 дБ (ETC2) к wPSNR референс-пайплайна, ценой ~2× времени пайплайна; etcpak — быстрые эвристики etcpak (как в референсе по скорости). По умолчанию playrix на x86_64 unix (ядро SSE4.1), на прочих целях — etcpak
  • Детерминированный zip: фиксированные временные штампы (1980-08-16), без extra-полей — повторная упаковка тех же входов даёт бит-в-бит идентичный файл
  • Структура zip как в референсе: порядок записей (webp: .sprites → .webp; etc1: .pvr → .sprites → atlas_N_alpha.pvr; etc2: .pvr → .sprites), .webp/.png без сжатия (stored), .sprites/.pvr deflate уровня 12 (zip-крейт: уровни >9 — это zopfli, 12 = 3 итерации), CRLF в .sprites. Zopfli×3 экономнее референсного 7z -tzip -mx=9: на rf_common@1x etc1- и etc2-зипы на ~2% меньше референсных при том же контенте, ценой ~1.5 с/МБ payload (однопоточно, после параллельного кодирования)
  • Результат: .images@1x_<fmt>.new.zip + .images@2x_<fmt>.new.zip (при --input-2x), .layouts@1x|2x.new.list, маркеры restype_uisprite.txt / unity_import_flag — как в референсе
  • Дедупликация по hash из layout.jsonl (идентичность движка; фолбэк — хэш пикселей), tiled-спрайты → singleton wrap-атласы
  • Спрайты с # в имени: в атласных режимах пакуются как обычные (так делает референс — все атласы find_objects содержат #-слои); в режиме --restype sprite не конвертируются и не попадают в zip (референс удаляет loose #*.png)
  • Сравнение с референсными данными (test_files/atlas_data.jsonl)

Превью (ImageMagick)

Генерация превью-миниатюр сцен из PSD — как -previews референсного конвейера: слои сливаются, берётся кроп 1536x1536+256+0, результат сжимается до высоты 144px (ppu 1) или 288px (ppu 2), PNG q95 без метаданных, имя = имя PSD.

packit preview --input "res/locations/preview" --out previews --height 144
Флаг Описание По умолчанию
--input PSD-файлы или папки с PSD (без рекурсии) — (обязателен)
--out Общая папка превью previews
--height Высота превью в пикселях 144
--crop Кроп W:H:X:Y после слияния слоёв 1536:1536:256:0
--magick Бинарник ImageMagick magick

Требует ImageMagick в PATH (или путь через --magick); пиксельный результат зависит от версии ImageMagick.

Фоны (ImageMagick)

Масштабированные фоны сцен из PSD — порт референсной ветки -custom-scale:W:H:type: слои сливаются, масштабируются по режиму (fill / shrinklarger / shrinkcustom / none, с центральным кропом до целевого размера), результат — background.png + background {Rect=0 0 w h} мета + zip с webp, по подпапке на PSD.

packit background --input "res/backgrounds" --out out/backgrounds --width 1536 --height 1024 --scale-type fill
Флаг Описание По умолчанию
--input PSD-файлы или папки с PSD (без рекурсии) — (обязателен)
--out Корневая папка (подпапка на PSD) backgrounds
--width / --height Целевой логический размер 1536 / 1024
--scale-type Режим масштабирования: fill, shrinklarger, shrinkcustom, none fill
--ppu Множитель целевого размера (ppu ≠ 1) 1
--quality webp-качество фона 75
--magick Бинарник ImageMagick magick

Установка

cargo build --release

Использование

Упаковка папки с изображениями

# один формат
cargo run --release -- pack --input test_files/input/rf_common_1x --format webp --out out --padding 2

# несколько форматов за один запуск (параллельно, по потоку на формат)
cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp etc1 etc2 --out out

# per-format выбор набора: webp — 1x и 2x, etc1 — только 1x, etc2 — только 2x
cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp etc1@1x etc2@2x --out out

Входная папка должна содержать спрайты (.webp) и layout.jsonl ({"name","x","y","width","height","hash"[,"tiled","scale"]} — источник истины).

Флаг Описание По умолчанию
--input Папка со спрайтами 1x + layout.jsonl —
--input-2x Набор 2x, пакуется отдельно в .images@2x_… —
--format Форматы: png, webp, etc1, etc2; опциональный суффикс набора: webp@1x — только 1x, etc1@2x — только 2x, без суффикса — 1x и 2x. Несколько значений или повторов флага; повторы объединяются (webp webp@1x → оба набора) — (обязателен, ≥1)
--quality Качество lossy webp, cwebp -q (0–100) 75
--webp-method Усилие сжатия webp, cwebp -m (0–6) 6
--webp-pass Мультипроходный анализ webp, cwebp -pass (1–10) 10
--sharp-yuv Более резкое (но медленное) RGB→YUV преобразование для lossy webp, cwebp -sharp_yuv выкл. (как в референсе)
--etc-encoder Кодировщик ETC-блоков: playrix (качество, медленнее) или etcpak (быстро) playrix (x86_64 unix), иначе etcpak
--out Папка для вывода (создаётся по полному пути; перед упаковкой очищается) out
--append Не очищать --out (многопроходная сборка в одну папку, например locations/decors: webp-одиночки + etc-атласы) выкл.
--no-layouts Не писать .layouts@…new.list (для потребителей, берущих layout-данные из другого источника) выкл.
--padding Отступ между спрайтами в пикселях 2
--max-size Максимальный размер атласа (в режиме sprite не применяется) 4096
--restype Тип импорта: uisprite (атласы, по умолчанию), scenesprite (то же с другим маркером), sprite — без атласов: каждый спрайт = отдельный файл в том же zip + per-sprite .sprites; маркер restype_sprite.txt uisprite
--area-tolerance Допуск по площади атласов (%) ради меньшего числа файлов (0 — строгий минимум площади, даёт много мелких атласов) 5
--verbose Печатать список атласов (размеры и число спрайтов) выкл.

Перед упаковкой печатается шапка с входными данными: наборы (@1x/@2x) с числом спрайтов (быстрый pre-scan без декодирования), выбранные форматы и папка вывода:

packit 2026.10.16 - 2 set(s), 3 format(s)
  @1x  test_files/input/rf_common_1x  203 sprites
  @2x  test_files/input/rf_common_2x  203 sprites
  formats: webp, etc1 (@1x), etc2 (@2x)
  out: out/

Во время работы в терминале — простая ASCII-анимация-спиннер (| / - \ packing sprites...) без процентов, ETA и описания текущего действия; только ASCII-символы, поэтому одинаково работает в консолях Windows и Linux. Живой анимации нужен настоящий терминал на stderr; где его нет (панель запуска/отладки IDE, пайп, CI, TERM=dumb), вместо спиннера печатаются построчные события — старт загрузки набора, каждый закодированный атлас ([webp] atlas 2/5: ...), завершение формата.

После каждого набора — сводная таблица: число атласов, суммарная площадь, заполненность (fill), размер зипа против несжатого объёма (saved), время формата и wall time:

== test_files/input/rf_common_1x @1x : 203 sprites (201 normal, 2 tiled)
format  atlases       area    fill        zip    payload   saved   time
webp          5    2.12Mpx   91.3%  405.2 KiB  410.9 KiB    1.4%   4.1s
etc1          4    2.16Mpx   89.5%  473.9 KiB   2.07 MiB   77.7%   2.3s
set: 2 zips, 879.1 KiB, wall 4.1s

В самом конце — итог всего запуска: суммарные атласы/зипы/размер и общее время выполнения с разбивкой на загрузку и упаковку+кодирование:

done: 2 set(s), 406 sprites -> 26 atlases in 6 zip(s), 5.77 MiB (payload 22.48 MiB, saved 74.3%)
written to out in 8.5s total (load 0.2s, pack+encode 8.3s)

Метрики в отчётах

Колонки таблицы набора:

Колонка Значение
format формат атласов (png / webp / etc1 / etc2)
atlases число атласов в бандле, включая {Wrap}-синглтоны для tiled-спрайтов
area суммарная площадь атласов (Σ W×H); Mpx — мегапиксели
fill заполненность: площадь уникального контента спрайтов к area в % (100% = без пустот; реально чуть ниже из-за --padding и неплотной укладки)
zip итоговый размер .images@<ppu>_<fmt>.new.zip на диске
payload суммарный размер записей внутри зипа без сжатия (атласы + .sprites + atlas_N_alpha.pvr)
saved экономия сжатия zip-контейнера: 100% × (1 − zip / payload). У webp/png мала (атласы хранятся stored — уже сжаты кодеком, дефлейтится только текст .sprites); у etc1/etc2 велика (70–85%, pvr-блоки хорошо сжимаются)
time время потока формата: укладка + кодирование + запись. Форматы пакуются параллельно, поэтому сумма time по форматам больше wall

Строка set: — итог набора: число зипов, их суммарный размер и wall — реальное (по часам) время этапа упаковки набора, от старта потоков форматов до завершения последнего. Равна критическому пути (самый медленный формат + накладные расходы на потоки); загрузка/декодирование спрайтов в wall не входит.

Итог запуска (последние две строки):

Параметр Значение
done: … sprites -> … atlases in … zip(s) агрегаты по всем наборам: спрайты (normal + tiled), атласы, зипы
… MiB (payload …, saved …%) суммарный размер зипов против несжатого объёма и общая экономия сжатия
total общее время выполнения: от старта до записи всех артефактов
load суммарное время загрузки и декодирования входных наборов
pack+encode суммарное wall всех наборов (укладка + кодирование + запись)

С --verbose после таблицы печатается список артефактов: имя зипа и по каждому атласу строка atlas_N: WxH (M sprites).

Сравнение с референсом

cargo run --release -- compare-jsonl --limit 1954
Флаг Описание По умолчанию
--jsonl Путь к jsonl test_files/atlas_data.jsonl
--filter Подстрока для фильтрации ключа —
--limit Макс. строк для проверки 2000

Тест rf_common

Сравнение против референсных записей с "rf" и "common" в ключе:

cargo run --release -- test-rf-common --limit 20
Флаг Описание По умолчанию
--jsonl Путь к jsonl test_files/atlas_data.jsonl
--limit Макс. строк для проверки 2000

Обе команды сравнения печатают одинаковую сводку:

sets: 6 better=6 equal=0 worse=0
total area: ref=23897184 ours=14200314 ratio=0.5942
Параметр Значение
sets число проверенных наборов референса
better / equal / worse у скольких наборов наша суммарная площадь атласов меньше / равна / больше референсной
ref / ours суммарная площадь атласов по всем наборам: референс и наша
ratio ours / ref — отношение площадей; меньше 1 — в среднем компактнее референса

Формат вывода

out/
├── .images@1x_webp.new.zip      # atlas_N.webp + atlas_N.sprites
├── .images@1x_etc1.new.zip      # по одному зипу на каждый --format
├── .images@1x_etc2.new.zip
├── .images@2x_webp.new.zip      # при --input-2x (по каждому формату)
├── .layouts@1x.new.list         # name {Position=x y; Size=w h} <sha256>
├── .layouts@2x.new.list
├── restype_uisprite.txt
└── unity_import_flag

Состав повторяет референс; никаких дополнительных jsonl-файлов на выходе нет.

Для etc1/etc2 атласы — atlas_N.pvr (etc1: + atlas_N_alpha.pvr, только если в атласе есть прозрачность), для png — atlas_N.png.

PVR v3 (etc1/etc2)

52-байтовый little-endian заголовок (version 0x03525650, flags=0, глубина/поверхности/грани/mipmaps=1) + 15-байтовая метадата из референса, затем блоки ETC:

Формат pixelFormat Блок Размер данных
etc1 (цвет и альфа) 6 (ETC1_RGB8) 8 байт / 4×4 w·h/2
etc2 без прозрачности 22 (ETC2_RGB8) 8 байт / 4×4 w·h/2
etc2 с прозрачностью 23 (ETC2_RGBA8) 16 байт / 4×4 w·h

Альфа-текстура etc1 — альфа-канал, реплицированный в RGB и закодированный как ETC1. Размерности атласов дополняются до кратности 4 (размер ETC-блока).

Детерминизм

Повторная упаковка тех же входов даёт бит-в-бит идентичный zip: у всех записей фиксированное время 1980-08-16 00:00, extra-поля не пишутся, сжатие и порядок записей зависят только от содержимого.

test_files/atlas_data.jsonl

Референсный файл с метаданными упаковки (используется командами compare-jsonl / test-rf-common, на выходе pack не создаётся):

{"key":"/abs/path/rf_common/webp","atlases":[[1024,1024]],"sprites":[[768,513]],"positions":[[0,0,0,0]]}
Поле Тип Описание
key string Путь к папке (без .images_@2x.zip)
atlases [[w,h],...] Размеры атласов
sprites [[w,h],...] Размеры спрайтов
positions [[atlas_idx, sprite_idx, x, y],...] Позиции спрайтов в атласах

Алгоритм упаковки

  1. Дедупликация: спрайты с одинаковым hash из layout.jsonl (идентичность контента движка; у imaginum это blake3 пиксельных байт) делят одну область атласа; без хэша в layout — фолбэк на FNV-1a декодированных пикселей. Совпадение только размеров содержимое не объединяет
  2. MaxRects с портфолио из 5 правил размещения (Best Short/Long Side Fit, Best Area Fit, Bottom-Left, Contact Point)
  3. 13 порядков сортировки (по площади, сторонам, периметру, сторонам; в обе стороны)
  4. 3 стратегии score: минимальный абсолютный waste, минимальный относительный waste, максимум размещённых спрайтов
  5. Обрезка атласов по содержимому: точный bounding box (webp/etc2), округление до POT (etc1)
  6. Мульти-высотные кандидаты: для каждой ширины несколько высот (минимальная, POT, максимум) + «естественные» размеры спрайтов
  7. Принудительный первый атлас: перебор всех кандидатов в качестве первого (и двух первых для сложных наборов) с жадным хвостом
  8. Exact-поиск (backtracking) с бюджетом узлов: замыкает жадные тупики, которые MaxRects не видит (например, 8 decor-спрайтов в один 1024×4096)
  9. Subset DP для наборов ≤ 12 unique-спрайтов: оптимальное разбиение на атласы (3^n перебор разбиений поверх exact/greedy-упаковок подмножеств)
  10. Singleton-опция: по одному атласу на спрайт (как в референсе) — только если мульти-упаковка выходит за допуск по площади
  11. Count-aware выбор: среди всех результатов портфолио, лежащих в допуске --area-tolerance (по умолчанию 5%) от минимальной площади, выбирается вариант с наименьшим числом атласов; при --area-tolerance 0 — строгий минимум площади
  12. Merge-pass: жадное слияние мелких атласов (через точную упаковку объединения), пока суммарная площадь не выходит за допуск от исходной

Качество упаковки

Сравнение с test_files/atlas_data.jsonl (1954 наборов):

Формат Лучше Равно Хуже
etc1 226 257 0
etc2 474 9 0
webp 98 396 0
webp2x 98 396 0
  • 896 лучше / 1058 равно / 0 регрессий
  • Отношение площадей: 0.9385 (наши атласы = 93.9% от площади референса)
  • Все equal-случаи webp/webp2x достигают теоретического минимума (сумма площадей unique-спрайтов); etc1 equal — POT-минимумы (группировка не даёт выигрыша)
  • rf_common тест: 10 лучше / 6 равно / 0 хуже, ratio=0.6115

Итоговые зипы rf_common (рабочий режим, --area-tolerance 5) против референсных бандлов:

Бандл packit Референс Разница
webp 1x 415 КБ (5 атласов) 940 КБ (3) 2.3× меньше
webp 2x 1.08 МБ (4 атласа) 2.49 МБ (3) 2.3× меньше
etc1 1x 485 КБ (4 атласа) 907 КБ (6) 1.9× меньше
etc2 1x 586 КБ (5 атласов) 1.04 МБ (7) 1.8× меньше

(у референса бандлов 2x для etc1/etc2 нет; два «лишних» атласа в наших webp-бандлах — обязательные {Wrap}-синглтоны для tiled-спрайтов, как и у референса)

Производительность

Упаковка rf_common (203 спрайта 1x + 203 2x, форматы webp/etc1/etc2, release): ~4 с на набор (форматы кодируются параллельно, по потоку на формат), ~8 с на полный запуск 1x+2x. Сама укладка — десятки миллисекунд, практически всё время уходит на декодирование входных webp и кодирование атласов; входы декодируются один раз на набор и переиспользуются всеми форматами.

Осознанные отказы (функции референсного конвейера, не поддерживаемые)

  • --use-rectmask (бинпакер, 1 PSD в дереве) — не используется
  • PSD-оверрайды name@ppu.psd — пер-PPU подмена исходника
  • -skip-textures (только списки без текстур), форматы jng/dxt/pvrtc/rgba, -loc локализация (переговорные слои, .lang-зипы)
  • -remote, -generate-previews как флаги основного пака — вместо них отдельные подкоманды preview/background

Зависимости

  • image — загрузка webp, кодирование png
  • libwebp-sys (libwebp) — lossy-кодирование атласов webp с альфа-каналом (advanced API: -q/-m/-pass/-mt как в cwebp)
  • ctt-etcpak — компрессия ETC1/ETC2 (быстрый путь)
  • src/codec/playrix/ — вендоренные ядра Playrix EtcCompress/Etc2Compress (MIT, © Playrix LLC), собираются build.rs (cc, SSE4.1, только x86_64 unix); вызовы идут на выделенном потоке с 64 МБ стеком, воркеры ядер переведены на последовательное выполнение, Stride — thread_local (packit кодирует атласы параллельно)
  • clap — CLI
  • indicatif — спиннер-анимация в терминале
  • zip — создание zip-архивов
  • serde / serde_json — JSONL
  • thiserror / anyhow — обработка ошибок

Сборка webp и ctt-etcpak требует C/C++-компилятора.

Структура проекта

src/
├── main.rs          # Точка входа: Error/Result, диспетчер подкоманд
├── cli.rs           # Определения clap (Cli/Cmd/PackArgs) + разбор `--format` спеков
├── pack.rs          # Оркестрация `pack`: наборы → загрузка → потоки форматов → отчёты
├── packer.rs        # Алгоритм упаковки (MaxRects + exact + subset DP)
├── progress.rs      # Спиннер-анимация упаковки (+ построчный fallback без терминала)
├── report.rs        # Консольные отчёты: шапка входов, таблицы наборов, итог прогона
├── codec/mod.rs     # Форматы (Format, RgbaImage, load_dir)
├── codec/encode.rs  # Рендер атласов и кодирование (png/webp/etc1/etc2)
├── codec/pvr.rs     # Контейнер PVR v3 + метадата референса
├── layout.rs        # Парсинг входного layout.jsonl
├── compare.rs       # Сравнение с atlas_data.jsonl
└── bundle.rs        # Сборка .images@<ppu>x_<fmt>.new.zip + layouts-списков