Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ builds:
- -s -w -X main.version={{.Version}}
binary: atak-macos

# Apple Silicon gets its own artifact rather than a universal binary.
# A universal atak would carry two full copies of the embedded tools,
# ~45MB against the 25MB target, because the tools are embedded per GOOS
# and each already holds both slices.
- id: macos-arm64
goos: [darwin]
goarch: [arm64]
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X main.version={{.Version}}
binary: atak-macos

archives:
- id: linux
builds: [linux]
Expand Down Expand Up @@ -62,6 +75,15 @@ archives:
- SPEC.md
- THIRD_PARTY_LICENSES.txt

- id: macos-arm64
builds: [macos-arm64]
format: tar.gz
name_template: "atak-{{.Version}}-macos-arm64"
files:
- README.md
- SPEC.md
- THIRD_PARTY_LICENSES.txt

checksum:
name_template: "checksums.txt"

Expand All @@ -81,6 +103,7 @@ release:
- **Linux:** `atak-{{.Version}}-linux-x64.tar.gz`
- **Windows:** `atak-{{.Version}}-windows-x64.zip`
- **macOS (Intel):** `atak-{{.Version}}-macos-x64.tar.gz`
- **macOS (Apple Silicon):** `atak-{{.Version}}-macos-arm64.tar.gz`

### Installation
**Linux / macOS:**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ for S.T.A.L.K.E.R. Anomaly maintained by Grok. Join the community on
- [Bubbles](https://github.com/charmbracelet/bubbles) — MIT — Charmbracelet
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) — MIT — Charmbracelet

The optional compressonator-bc7e compression backend (Windows/Linux) additionally
The optional compressonator-bc7e compression backend additionally
incorporates:

- [AMD Compressonator](https://github.com/GPUOpen-Tools/compressonator) — MIT — © 2024 Advanced Micro Devices, Inc.; © 2004-2006 ATI Technologies Inc.
Expand Down
168 changes: 128 additions & 40 deletions SPEC.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion THIRD_PARTY_LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


==============================================================
Compression backend compressonator-bc7e (optional, Windows/Linux)
Compression backend compressonator-bc7e (optional)
==============================================================

See licenses/compressonator-bc7e/ for full text of the two licenses
Expand Down
2 changes: 1 addition & 1 deletion internal/compress/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// Backend abstracts a single-file texture compressor. Two implementations exist
// today — texconv (all platforms, GPU-accelerated on Windows for BC7) and
// compressonator-bc7e (Linux/Windows, CPU-only, all five BC formats). The
// compressonator-bc7e (all platforms, CPU-only, all five BC formats). The
// interface exists so worker.go can select once per run and route every job
// through the same code path, and so the compressonator→texconv fallbacks in
// dispatch() can swap backends per-file without leaking backend specifics into
Expand Down
39 changes: 32 additions & 7 deletions internal/compress/dispatch_srgb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,42 @@ import (
"encoding/binary"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/noisethanks/atak/internal/scan"
)

// repoToolPath resolves one of the embedded tool binaries straight from
// internal/tools/bin for the platform running the test. These tests shell out
// to a real binary, and the embedded copy only becomes a file at runtime via
// tools.Extract, so the source tree is the one location that always has it —
// and it keeps the test running on every platform that ships the tool instead
// of only on the machine that wrote the path.
func repoToolPath(t *testing.T, stem string) string {
t.Helper()
var suffix string
switch runtime.GOOS {
case "darwin":
suffix = "-macos"
case "linux":
suffix = "-linux"
case "windows":
suffix = "-windows.exe"
default:
t.Skipf("no %s build for %s", stem, runtime.GOOS)
}
path, err := filepath.Abs(filepath.Join("..", "tools", "bin", stem+suffix))
if err != nil {
t.Skipf("resolve %s: %v", stem, err)
}
if _, err := os.Stat(path); err != nil {
t.Skipf("binary missing: %v", err)
}
return path
}

// synthSrgbDX10DDS builds a minimal, self-contained DDS with a DX10 extended
// header advertising dxgiFormat = 91 (DXGI_FORMAT_B8G8R8A8_UNORM_SRGB) — the
// exact subvariant compressonator-bc7e's DDS reader rejects but DirectXTex/
Expand Down Expand Up @@ -98,13 +128,8 @@ func writeFixture(t *testing.T, name string, data []byte) string {
// source file"; dispatch retries via texconv, which accepts it. The
// synthesized fixture stays in-tree so this regression can't recur silently.
func TestDispatchCompressonatorSrgbFallback(t *testing.T) {
compressBin := "/home/abhi/stalker-tex/internal/tools/bin/compressonator-bc7e-linux"
texconvBin := "/home/abhi/stalker-tex/internal/tools/bin/texconv-linux"
for _, p := range []string{compressBin, texconvBin} {
if _, err := os.Stat(p); err != nil {
t.Skipf("binary missing: %v", err)
}
}
compressBin := repoToolPath(t, "compressonator-bc7e")
texconvBin := repoToolPath(t, "texconv")

src := writeFixture(t, "srgb_dx10.dds", synthSrgbDX10DDS(true))
outDir := t.TempDir()
Expand Down
9 changes: 2 additions & 7 deletions internal/compress/e2e_srgb_summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,8 @@ func TestE2ERealFileFallbackAttribution(t *testing.T) {
if _, err := os.Stat(src); err != nil {
t.Skipf("real modfile absent: %v", err)
}
compressBin := "/home/abhi/stalker-tex/internal/tools/bin/compressonator-bc7e-linux"
texconvBin := "/home/abhi/stalker-tex/internal/tools/bin/texconv-linux"
for _, p := range []string{compressBin, texconvBin} {
if _, err := os.Stat(p); err != nil {
t.Skipf("binary missing: %v", err)
}
}
compressBin := repoToolPath(t, "compressonator-bc7e")
texconvBin := repoToolPath(t, "texconv")

primary := NewCompressonatorBackend(compressBin)
fallback := NewTexconvBackend(texconvBin)
Expand Down
15 changes: 5 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ type Config struct {
WorkerCount int `json:"workerCount"`
BackupLevel int `json:"backupLevel,omitempty"`
// CompressionBackend selects which embedded compressor runs. Valid values:
// "texconv" (default, all platforms) and "compressonator-bc7e"
// (Linux/Windows only). On darwin the field is coerced back to "texconv"
// on load, so a config synced over from another OS can't select an
// unavailable backend.
// "texconv" and "compressonator-bc7e", both available on every supported
// platform. An unrecognized value is coerced back to "texconv" on load.
CompressionBackend string `json:"compressionBackend,omitempty"`
// ScanExclusions are directory globs pruned during the scan. A plain name matches a
// directory (or mod) anywhere; a path pattern like */textures/ui/SquareDOV matches a
Expand Down Expand Up @@ -107,13 +105,10 @@ func Load() (*Config, error) {
}

// normalizeBackend validates a persisted backend selection and coerces unknown
// or platform-unavailable values back to the default. Called on every Load so
// a config.json copied from another OS (e.g. Windows → macOS) never selects a
// backend that isn't built for the current platform.
// values back to the default. Called on every Load so a hand-edited or
// future-dated config.json can never name a backend this build has no
// implementation for.
func normalizeBackend(v string) string {
if runtime.GOOS == "darwin" {
return BackendTexconv
}
switch v {
case BackendTexconv, BackendCompressonatorBc7e:
return v
Expand Down
Binary file added internal/tools/bin/compressonator-bc7e-macos
Binary file not shown.
9 changes: 5 additions & 4 deletions internal/tools/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ var LicenseText []byte

// EmbeddedTools holds paths to extracted binaries for the current session.
// CompressonatorPath is empty on platforms where the compressonator-bc7e backend
// is unavailable (currently macOS) — callers must treat "" as "unavailable"
// rather than special-casing runtime.GOOS.
// is unavailable — callers must treat "" as "unavailable" rather than
// special-casing runtime.GOOS. Every supported platform ships a build today,
// so the empty case is a guard, not a routine state.
type EmbeddedTools struct {
TexconvPath string
SevenZipPath string
Expand Down Expand Up @@ -43,8 +44,8 @@ func writeBin(dir, name string, data []byte) (string, error) {
}

// Extract writes embedded binaries to a temp dir and returns the tool paths.
// The compressonator binary is only written when the embedded data is non-empty
// (i.e. skipped on darwin where the fork isn't built).
// The compressonator binary is only written when the embedded data is non-empty,
// which keeps a platform without a build from writing a zero-byte executable.
func Extract() (*EmbeddedTools, error) {
dir, err := os.MkdirTemp("", "atak-*")
if err != nil {
Expand Down
6 changes: 2 additions & 4 deletions internal/tools/embed_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ var texconvBin []byte
//go:embed bin/7zz-macos
var sevenZipBin []byte

// compressonator-bc7e is not built for macOS (upstream fork is Linux/Windows only).
// Stub the vars so callers can check len(compressonatorBin) == 0 to detect absence.
//go:embed bin/compressonator-bc7e-macos
var compressonatorBin []byte
var _ = compressonatorBin

const texconvName = "texconv"
const sevenZipName = "7zz"
const compressonatorName = ""
const compressonatorName = "compressonatorcli"
2 changes: 1 addition & 1 deletion internal/tui/screens/about.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


compressonator-bc7e (optional compression backend, Windows/Linux only)
compressonator-bc7e (optional compression backend)
------------------------------------------------------------------------
This backend is a fork of AMD Compressonator with the CPU-side BC7 codec
replaced by bc7e.ispc from richgel999/bc7enc_rdo. Two licenses apply.
Expand Down
6 changes: 3 additions & 3 deletions internal/tui/screens/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ func (m CompressModel) startCompression() tea.Cmd {
// selectBackends picks the primary backend from cfg and, when primary is
// compressonator, also supplies a texconv fallback for the maxTextureSize
// resize case (compressonator has no exact-size resize flag). When
// compressonator was selected but its binary isn't extracted (e.g. the config
// was hand-edited on darwin), fall back cleanly to texconv rather than
// erroring — the fallback path is already the safe choice.
// compressonator was selected but its binary isn't extracted, fall back
// cleanly to texconv rather than erroring — the fallback path is already the
// safe choice.
func selectBackends(cfg *config.Config, t *tools.EmbeddedTools) (primary, fallback compress.Backend) {
tex := compress.NewTexconvBackend(t.TexconvPath)
if cfg.CompressionBackend == config.BackendCompressonatorBc7e && t.CompressonatorPath != "" {
Expand Down
19 changes: 5 additions & 14 deletions internal/tui/screens/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (
fieldWorkers
fieldBackupLevel
fieldStripMips // bool toggle — no text input
fieldCompressionBackend // two-way selector, hidden on darwin (no compressonator build)
fieldCompressionBackend // two-way selector
fieldModOutputMode // bool toggle — no text input
fieldModOutputName // text input, shown only when ModOutputMode is on
fieldModlistPath // text input, shown only when ModOutputMode is on
Expand Down Expand Up @@ -59,8 +59,7 @@ type SettingsModel struct {
// stripMips mirrors cfg.StripMipsWhenDisabled while the toggle is being edited.
stripMips bool
// compressionBackend mirrors cfg.CompressionBackend while the selector is
// being edited. On darwin this stays "texconv" — the row is hidden and
// there's no way to change it.
// being edited.
compressionBackend string
}

Expand Down Expand Up @@ -188,9 +187,6 @@ func (m SettingsModel) isVisible(f settingsField) bool {
if f == fieldModOutputName || f == fieldModlistPath {
return m.modOutputMode
}
if f == fieldCompressionBackend {
return runtime.GOOS != "darwin"
}
return true
}

Expand All @@ -216,11 +212,7 @@ func (m SettingsModel) save() (SettingsModel, tea.Cmd) {
updated.ModOutputName = modOutputName
updated.ModlistPath = strings.TrimSpace(m.inputs[5].Value())
updated.StripMipsWhenDisabled = m.stripMips
if runtime.GOOS == "darwin" {
updated.CompressionBackend = config.BackendTexconv
} else {
updated.CompressionBackend = m.compressionBackend
}
updated.CompressionBackend = m.compressionBackend
return m, func() tea.Msg {
return NavigateMsg{To: NavSaveConfig, Data: &updated}
}
Expand Down Expand Up @@ -273,9 +265,8 @@ func (m SettingsModel) View() string {
b.WriteString(style.StyleMuted.Render("When on, profiles with generateMips=false skip mips entirely, dropping any chain the\n source shipped. When off (default), a mipped source keeps its chain (flares, reticles).") + "\n\n")
}

// Compression backend selector — hidden on macOS since compressonator-bc7e
// isn't built for darwin.
if runtime.GOOS != "darwin" {
// Compression backend selector.
{
label := "Compression Backend"
var value string
if m.compressionBackend == config.BackendCompressonatorBc7e {
Expand Down