Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4ecc8d3
Fix build error by replacing `stbi_write_png_to_mem` with `stbi_write…
pedroac Mar 27, 2026
0151c4c
Fix build errors on macOS and improve portability
pedroac Mar 27, 2026
2ef4103
Fix build errors and bump version to v0.2.7
pedroac Mar 27, 2026
ce1f669
chore: enhance project organization and unit test coverage
claude May 4, 2026
e5f25c8
ci: implement pipeline improvements for testing and caching
claude May 4, 2026
4116e7f
feat: add duplicate detection, GPU compression, and artifact reduction
claude May 4, 2026
d5c54d2
chore: bump version to v0.3.0
claude May 8, 2026
84e8881
feat: Add deduplication mode support with exact and perceptual options
claude May 9, 2026
6ddb8ee
Update version
claude May 9, 2026
8c4e943
fix: Link spratcore against libarchive for Windows builds
claude May 11, 2026
3a8f84c
chore: Bump version to v0.3.2
claude May 11, 2026
2a9147e
Release v0.3.3 - libarchive linking fixes
claude May 11, 2026
82357b7
Bumb version
claude May 11, 2026
c5fa888
perf: Optimize layout generation hot paths
claude May 14, 2026
712d53d
Optimize memory reuse and reduce allocations across commands
claude May 14, 2026
b4d1441
Bump version
claude May 14, 2026
71e21ac
Release v0.4.0
claude May 16, 2026
bcd3076
Fix layout root path handling for list file inputs and add root line …
claude May 17, 2026
cfcb00d
Fix MSVC build: replace __builtin_popcountll with cross-platform popc…
claude May 17, 2026
ecd5c1b
Release v0.5.1: general [if] conditionals, JSON restructure, and new …
claude May 18, 2026
0101f0c
Fix transforms copy path for MSVC multi-config builds
claude May 18, 2026
f2ddf59
Fix config/transforms lookup order and platform paths
claude May 18, 2026
66364d4
Merge branch 'main' into release
pedroac May 18, 2026
3f1b238
Default sprite sort is now none; update man page
claude May 18, 2026
cb1bcb5
Enhance spratlayout performance and flexibility
claude May 20, 2026
022995b
Add stable sort, unify flip syntax, update default profiles
claude May 23, 2026
49e23b7
Here's a suggested commit description:
claude May 24, 2026
c928ffb
Add grid packing mode
claude May 25, 2026
421f232
feat(spratconvert): add Unity transforms and normalized pivot support
claude May 25, 2026
e6b8c4c
refactor: rename --output to --atlas in spratconvert and spratpack
claude May 27, 2026
3e03255
refactor(spratconvert): replace DSL transform engine with Jsonnet
claude May 31, 2026
13215af
fix(spratlayout): enforce uniform frame size in grid mode
claude May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 105 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,87 @@ if(NOT SQUISH_FOUND)
endif()
endif()

# Jsonnet dependency
include(FetchContent)
FetchContent_Declare(
jsonnet
GIT_REPOSITORY https://github.com/google/jsonnet.git
GIT_TAG f45e01d632b29e4c0757ec7ba188ce759298e6d3 # v0.20.0
)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
# Jsonnet's CMakeLists.txt uses cmake_minimum_required < 3.5; suppress the error
set(CMAKE_POLICY_VERSION_MINIMUM "3.5" CACHE INTERNAL "")
FetchContent_GetProperties(jsonnet)
if(NOT jsonnet_POPULATED)
# Silence FetchContent_Populate() deprecation warning on CMake >= 3.30.
if(POLICY CMP0169)
cmake_policy(SET CMP0169 OLD)
endif()
FetchContent_Populate(jsonnet)
if(MSVC)
# jsonnet v0.20.0 rejects non-GCC/Clang compilers with a FATAL_ERROR in
# the else() branch of its compiler-flags check (CMakeLists.txt:43).
# Downgrade it to STATUS so MSVC can configure and build with defaults.
file(READ "${jsonnet_SOURCE_DIR}/CMakeLists.txt" _jcml)
string(REGEX REPLACE
"message[(]FATAL_ERROR \"Compiler [^\"]+not supported\"[)]"
"message(STATUS \"MSVC detected; using default compiler flags\")"
_jcml "${_jcml}")
# jsonnet hardcodes CMAKE_CXX_STANDARD 11 unconditionally, which maps to
# no /std:c++ flag on MSVC (MSVC has no /std:c++11). Its headers use
# nested namespaces (C++17), so raise the standard to 17.
string(REPLACE
"set(CMAKE_CXX_STANDARD 11)"
"set(CMAKE_CXX_STANDARD 17)"
_jcml "${_jcml}")
file(WRITE "${jsonnet_SOURCE_DIR}/CMakeLists.txt" "${_jcml}")

# jsonnet's stdlib/CMakeLists.txt invokes to_c_array via
# ${GLOBAL_OUTPUT_PATH}/to_c_array, which is wrong for MSVC multi-config
# generators: those place the executable in a per-config subdirectory
# (e.g. Release/) that the plain path doesn't include.
# Replace it with $<TARGET_FILE:to_c_array>, a generator expression that
# resolves to the correct full path (config subdir + .exe) at build time.
file(READ "${jsonnet_SOURCE_DIR}/stdlib/CMakeLists.txt" _jstdlib)
string(REGEX REPLACE
"\\$\\{GLOBAL_OUTPUT_PATH\\}/to_c_array"
"$<TARGET_FILE:to_c_array>"
_jstdlib "${_jstdlib}")
file(WRITE "${jsonnet_SOURCE_DIR}/stdlib/CMakeLists.txt" "${_jstdlib}")
endif()
add_subdirectory("${jsonnet_SOURCE_DIR}" "${jsonnet_BINARY_DIR}")
endif()

# jsonnet v0.20.0 builds libjsonnet++ (shared) without declaring its dependency
# on libjsonnet, causing undefined-symbol errors on macOS where dylibs must have
# all symbols resolved at link time. Patch the target here when it is shared.
if(TARGET libjsonnet++ AND TARGET libjsonnet)
get_target_property(_ljnpp_type libjsonnet++ TYPE)
if(_ljnpp_type STREQUAL "SHARED_LIBRARY")
target_link_libraries(libjsonnet++ PRIVATE libjsonnet)
endif()
endif()

# Suppress common MSVC warnings in jsonnet targets (written for GCC/Clang).
if(MSVC)
foreach(_jt libjsonnet_static libjsonnet++_static)
if(TARGET ${_jt})
target_compile_options(${_jt} PRIVATE
/wd4100 # unreferenced formal parameter
/wd4127 # conditional expression is constant
/wd4244 # conversion, possible loss of data
/wd4267 # conversion from size_t
/wd4702 # unreachable code
/wd4706 # assignment within conditional expression
/wd4996 # deprecated CRT function
)
target_compile_definitions(${_jt} PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()
endforeach()
endif()

# Target definitions
add_library(spratcore STATIC
src/core/cli_parse.cpp
Expand All @@ -263,6 +344,7 @@ target_include_directories(spratcore PUBLIC src)
target_include_directories(spratcore SYSTEM PRIVATE ${STB_DIR})
target_include_directories(spratcore PRIVATE ${LIBARCHIVE_INCLUDE_DIRS})
target_link_libraries(spratcore PRIVATE ${LIBARCHIVE_LIBRARIES})
target_link_libraries(spratcore PRIVATE libjsonnet++_static libjsonnet_static)
if(SPRAT_GETTEXT_AVAILABLE)
target_compile_definitions(spratcore PUBLIC SPRAT_ENABLE_GETTEXT)
target_link_libraries(spratcore PUBLIC ${SPRAT_INTL_LIBRARIES})
Expand Down Expand Up @@ -311,19 +393,36 @@ target_compile_definitions(spratlayout PRIVATE

# Keep a default profile config beside built binaries (e.g. build-win/) so
# spratlayout can resolve it via exe-directory lookup without extra setup.
# Use a single custom target for the transforms copy to prevent parallel copy races
# when multiple executables complete simultaneously in a parallel build.
# NOTE: Use a static path instead of $<TARGET_FILE_DIR:spratlayout> to avoid a
# cyclic dependency (sprat_copy_transforms -> spratlayout -> sprat_copy_transforms).
# On multi-config generators (MSVC, Xcode) binaries land in a per-config subdir
# (e.g. build/Release/), so append /$<CONFIG> to match $<TARGET_FILE_DIR:...>.
if(CMAKE_RUNTIME_OUTPUT_DIRECTORY)
set(_sprat_bin_dir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
else()
set(_sprat_bin_dir ${CMAKE_CURRENT_BINARY_DIR})
endif()
get_property(_sprat_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(_sprat_multi_config)
string(APPEND _sprat_bin_dir "/$<CONFIG>")
endif()
add_custom_target(sprat_copy_transforms ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/transforms
${_sprat_bin_dir}/transforms
COMMENT "Copying transforms/ to ${_sprat_bin_dir}"
VERBATIM)

foreach(target spratlayout spratpack spratconvert spratframes spratunpack)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/spratprofiles.cfg
$<TARGET_FILE_DIR:${target}>/spratprofiles.cfg
COMMENT "Copying spratprofiles.cfg to $<TARGET_FILE_DIR:${target}>"
VERBATIM)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/transforms
$<TARGET_FILE_DIR:${target}>/transforms
COMMENT "Copying transforms/ to $<TARGET_FILE_DIR:${target}>"
VERBATIM)
add_dependencies(${target} sprat_copy_transforms)

if(WIN32)
if(SPRAT_STATIC)
Expand Down
53 changes: 53 additions & 0 deletions COMPACT_PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
● Based on the analysis, here's what the situation looks like and where there's room to improve:

Already parallelized:
The guided and shelf passes both fan out across threads, dividing width candidates into ranges. So the outer search loop
is already threaded.

The real bottlenecks:

1. Static range division → load imbalance
The current pattern divides [0, width_candidates.size()) evenly across threads. But different widths take different
amounts of time (a narrow atlas packs quickly; a near-square one generates many free-rect fragments and runs longer). A
slow thread stalls the join. Replacing the static split with an atomic work index (each thread atomically claims the
next unclaimed width) would eliminate this with minimal code change.

2. Two separate thread fan-out/join cycles
Guided and shelf are sequentially phased — spawn threads, join, spawn again, join. Merging them into one pass (each
worker does both MaxRects and shelf for its assigned widths) halves the thread lifecycle overhead and improves cache
locality since the same width's sprite list is touched once.

3. O(n²) free-rect pruning inside pack_compact_maxrects
Every time a sprite is placed, prune_free_rects does an O(n²) containment scan to discard dominated rectangles. For
large sprite sets the free-rect list can grow significantly. This is the core per-packing cost and runs entirely on a
single thread — no parallelism is possible inside it without a fundamentally different data structure (e.g. a spatial
index). This is the hardest win but potentially the biggest one.

4. No pruning across (sort × heuristic) combos within a width
For a given width, all 12 combinations (4 sorts × 3 heuristics) are always attempted. If the first combo already
produces a result that can't beat the global best, the remaining 11 could be skipped. The candidate-level pruning exists
before entering the width, but not within it.

5. Candidate count
~20–60 candidates × 16 packing attempts each. The offset step grid ({0, ±1, ±2, ±4, ±8, ±12, ±16, ±20} × 4 anchors) is
generous. With a warm seed cache the search could be narrowed to a smaller window around the known-good width rather
than the full grid.

Summary by effort vs. impact:

┌────────────────────────────────────────────────────┬────────┬───────────────────────────────────────┐
│ Change │ Effort │ Impact │
├────────────────────────────────────────────────────┼────────┼───────────────────────────────────────┤
│ Atomic work index (dynamic scheduling) │ Low │ Medium — eliminates load imbalance │
├────────────────────────────────────────────────────┼────────┼───────────────────────────────────────┤
│ Merge guided + shelf into one pass │ Low │ Small — saves two thread cycles │
├────────────────────────────────────────────────────┼────────┼───────────────────────────────────────┤
│ Per-width early exit across (sort × heuristic) │ Medium │ Small-medium │
├────────────────────────────────────────────────────┼────────┼───────────────────────────────────────┤
│ Narrower candidate window when seed is warm │ Medium │ Medium — fewer packing runs │
├────────────────────────────────────────────────────┼────────┼───────────────────────────────────────┤
│ Replace O(n²) free-rect pruning with spatial index │ High │ Potentially large for big sprite sets │
└────────────────────────────────────────────────────┴────────┴───────────────────────────────────────┘

The atomic work index is the most straightforward improvement given the existing code structure — want me to implement
it?
Loading
Loading