Skip to content
Closed
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
29 changes: 24 additions & 5 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ builds:
- -s -w -X main.version={{.Version}}
binary: atak-windows

- id: macos
- id: macos-amd64
goos: [darwin]
goarch: [amd64]
env:
Expand All @@ -34,6 +34,15 @@ builds:
- -s -w -X main.version={{.Version}}
binary: atak-macos

- 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 All @@ -53,15 +62,24 @@ archives:
- SPEC.md
- THIRD_PARTY_LICENSES.txt

- id: macos
builds: [macos]
- id: macos-amd64
builds: [macos-amd64]
format: tar.gz
name_template: "atak-{{.Version}}-macos-x64"
files:
- README.md
- 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 @@ -80,12 +98,13 @@ release:
### Download
- **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` — native, uses the CPU-accelerated compressonator-bc7e backend by default
- **macOS (Intel):** `atak-{{.Version}}-macos-x64.tar.gz` — uses texconv by default (compressonator-bc7e-macos-arm64 is Apple Silicon only)

### Installation
**Linux / macOS:**
```bash
tar -xzf atak-{{.Version}}-linux-x64.tar.gz # or macos-x64
tar -xzf atak-{{.Version}}-linux-x64.tar.gz # or macos-x64 / macos-arm64
chmod +x atak-linux # or atak-macos
./atak-linux # or ./atak-macos
```
Expand Down
100 changes: 60 additions & 40 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,21 @@ internal/tools/bin/
├── texconv-macos # matyalatte macOS universal binary (Intel + Apple Silicon)
├── compressonator-bc7e-linux # AMD Compressonator fork with bc7e.ispc BC7 encoder (Linux)
├── compressonator-bc7e-windows.exe # same fork, Windows build
├── compressonator-bc7e-macos-arm64 # same fork, natively compiled Apple Silicon (ARM64, neon-i32x4)
├── 7zz # 7-Zip standalone Linux binary
├── 7za.exe # 7-Zip standalone Windows binary
└── 7zz-macos # 7-Zip standalone macOS universal binary (Intel + Apple Silicon)
```

**macOS excludes compressonator-bc7e** — the upstream fork is Linux/Windows only
(GPU codec paths removed, tested on GCC and MSVC; darwin is out of scope per the
fork's own README §8). `embed_darwin.go` declares `compressonatorBin` as an
empty byte slice and `compressonatorName` as `""`, and `Extract()` skips the
write when the data is empty. Callers must check `EmbeddedTools.CompressonatorPath == ""`
to know the backend is unavailable rather than special-casing `runtime.GOOS`.
**macOS ships compressonator-bc7e as the default backend.** `embed_darwin.go`
embeds the ARM64 `compressonator-bc7e-macos-arm64` binary; it is the default
`CompressionBackend` on darwin (`config.defaultConfig` /
`normalizeBackend`), with `texconv` available as a CPU fallback via the
Settings toggle — same toggle as Linux/Windows.

macOS universal binaries contain both x86-64 and ARM64 slices — one binary covers
all Mac hardware. No need to split darwin/amd64 and darwin/arm64 build tags.
macOS `texconv`/`7zz` binaries are universal (Intel + Apple Silicon) — one
binary covers all Mac hardware. `compressonator-bc7e-macos-arm64` is ARM64-only
(compiled via ISPC with `neon-i32x4`); no Intel Mac build exists yet.

Each platform has its own `embed_<platform>.go` with `//go:build` tag and
`//go:embed` directives. All three use the same variable names (`texconvBin`,
Expand All @@ -48,7 +49,9 @@ platform-agnostic. See `internal/tools/embed_linux.go` for the canonical pattern
`EmbeddedTools` fields:
- `TexconvPath` — always populated.
- `SevenZipPath` — always populated.
- `CompressonatorPath` — populated on Linux/Windows; empty string on darwin.
- `CompressonatorPath` — populated on Linux/Windows/macOS. Empty string only
if the embedded data for the current platform is somehow missing; callers
check `== ""` rather than special-casing `runtime.GOOS`.

On startup:
1. Extract every non-empty embedded binary to `os.MkdirTemp`
Expand All @@ -57,9 +60,9 @@ On startup:
4. `defer tools.Cleanup()` in main

**Binary size:** adding compressonator-bc7e grows the Linux release binary the
most (~9MB extra); Windows adds ~3.5MB. Current stripped (`-s -w`) sizes:
Linux ~21MB, Windows ~12MB, macOS ~17MB — all still under the historical 25MB
target. Watch this ceiling if further binaries land.
most (~9MB extra); Windows adds ~3.5MB. macOS now embeds compressonator-bc7e-macos-arm64
too (~3MB raw) — re-measure the stripped (`-s -w`) macOS release size before
relying on the historical 25MB target; last recorded pre-bc7e figure was ~17MB.

No other runtime dependencies. The binary must run on any supported platform
without the user installing anything.
Expand Down Expand Up @@ -355,13 +358,14 @@ atak/
├── profiles.json # repo-root copy of current default profile
├── bin/
│ ├── texconv-linux / texconv-windows.exe / texconv-macos
│ ├── compressonator-bc7e-linux / compressonator-bc7e-windows.exe / compressonator-bc7e-macos-arm64
│ └── 7zz / 7zz.exe / 7zz-macos
└── internal/
├── tools/
│ ├── embed.go # EmbeddedTools struct, extraction, cleanup
│ ├── embed_linux.go # //go:embed bin/texconv-linux, bin/7zz
│ ├── embed_windows.go # //go:embed bin/texconv-windows.exe, bin/7zz.exe
│ ├── embed_darwin.go # //go:embed bin/texconv-macos, bin/7zz-macos
│ ├── embed_linux.go # //go:embed bin/texconv-linux, bin/7zz, bin/compressonator-bc7e-linux
│ ├── embed_windows.go # //go:embed bin/texconv-windows.exe, bin/7zz.exe, bin/compressonator-bc7e-windows.exe
│ ├── embed_darwin.go # //go:embed bin/texconv-macos, bin/7zz-macos, bin/compressonator-bc7e-macos-arm64
│ ├── process_linux.go # setProcAttr / killProcess — Linux/macOS
│ ├── process_windows.go # setProcAttr / killProcess — Windows Job Objects
│ ├── lockfile.go # stale-process lockfile (Linux)
Expand Down Expand Up @@ -942,13 +946,13 @@ All platforms expose the same interface: `SetProcAttr(cmd)`, `KillProcess(cmd)`,
for flares/reticles. Never affects `generateMips:true`. Settings screen label:
"Strip Mips When Disabled". Resolved in `compress.ShouldGenerateMips`.
- **Compression backend** (`compressionBackend`) — string enum, valid values
`"texconv"` (default, all platforms) and `"compressonator-bc7e"` (Linux/Windows
only, CPU-only, deterministic across platforms). Unknown or empty values are
coerced to `"texconv"` on load. **On darwin, always coerced to `"texconv"` on
load** regardless of what's stored, so a config synced over from another OS
can't select a backend that isn't built for this platform. The Settings row
is hidden entirely on darwin rather than shown disabled. Toggle in Settings
with `space` / `←` / `→` — two-way selector, not free text.
`"texconv"` and `"compressonator-bc7e"` (CPU-only, deterministic across
platforms) on all three platforms. Default is `"texconv"` on Linux/Windows,
`"compressonator-bc7e"` on darwin. Unknown or empty values are coerced to
the platform default on load (`config.normalizeBackend`); a valid value —
including one synced over from another OS — is left as-is. The Settings
row is shown on every platform. Toggle in Settings with `space` / `←` /
`→` — two-way selector, not free text.
- Persist to `os.UserConfigDir()/atak/config.json`

Full config.json schema (see `internal/config/config.go` for canonical struct):
Expand Down Expand Up @@ -1226,11 +1230,17 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=v0.1.0" \
-o atak-windows.exe ./main.go

# Release — macOS (universal embedded tools, Go binary is amd64)
# Release — macOS Intel (see Cross-Platform Rules re: compressonator-bc7e-macos-arm64
# being ARM64-only — this target defaults to texconv)
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=v0.1.0" \
-o atak-macos ./main.go

# Release — macOS Apple Silicon (native; defaults to compressonator-bc7e)
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build \
-ldflags="-s -w -X main.version=v0.1.0" \
-o atak-macos ./main.go

# goreleaser handles all targets in CI — version injected from git tag
```

Expand All @@ -1241,17 +1251,20 @@ builds without the flag, version displays as `dev`.

- `linux/amd64` — primary, tested by maintainer
- `windows/amd64` — supported, community-tested
- `darwin/amd64` — macOS universal binary (Intel + Apple Silicon), community-tested

Note: goreleaser only needs one darwin target since the embedded binaries are
universal. The Go binary itself is architecture-specific but the embedded tools
work on both Intel and Apple Silicon.
- `darwin/amd64` — "macOS (Intel)" in release notes. texconv/7zz are
universal (Intel + Apple Silicon) so they work fine here;
compressonator-bc7e-macos-arm64 is ARM64-only and is embedded but never
selected by default on this target (see Cross-Platform Rules). Defaults
to `"texconv"`.
- `darwin/arm64` — "macOS (Apple Silicon)" in release notes. Native build,
uses the CPU-accelerated compressonator-bc7e-macos-arm64 backend by default.

The `-s -w` flags strip debug info. Final binaries should be under 25MB including
all embedded tools. As of the compressonator-bc7e addition, stripped release
sizes are roughly Linux ~21MB, Windows ~12MB, macOS ~17MB — still comfortably
under the ceiling, with Linux the tightest since it embeds compressonator-bc7e
(~9MB) alongside texconv + 7zz. Track this if further binaries land.
sizes are roughly Linux ~21MB, Windows ~12MB; re-measure macOS after this
change (last recorded pre-bc7e figure was ~17MB) — Linux is the tightest
today since it embeds compressonator-bc7e (~9MB) alongside texconv + 7zz.
Track this if further binaries land.

## Cross-Platform Rules

Expand All @@ -1270,12 +1283,19 @@ These must be followed in every file or platform support silently breaks:
- **macOS process management:** Same as Linux — `syscall.SysProcAttr{Setpgid: true}`
and `syscall.Kill(-pid, syscall.SIGKILL)` work on Darwin. `process_linux.go`
build tag should be `//go:build linux || darwin`.
- **compressonator-bc7e is Windows/Linux only.** The upstream fork is not built
for darwin (§8 of the fork's README: "macOS: out of scope"). `embed_darwin.go`
declares `compressonatorBin` as an empty byte slice and `compressonatorName`
as `""`, `Extract()` skips writing it, and `config.normalizeBackend()` coerces
`compressionBackend` to `"texconv"` on load whenever `runtime.GOOS == "darwin"`.
The Settings row is hidden on darwin rather than shown disabled. Callers
must check `EmbeddedTools.CompressonatorPath == ""` for availability rather
than `runtime.GOOS` — mirrors how the lockfile / Job-Object process split is
keyed off feature availability rather than raw OS checks.
- **compressonator-bc7e-macos-arm64 is ARM64-only.** It's compiled natively via ISPC
with `--target=neon-i32x4`; there is no Intel/amd64 build of the fork for
darwin. `config.defaultConfig()` and `normalizeBackend()` therefore only
default to `"compressonator-bc7e"` when `runtime.GOOS == "darwin" &&
runtime.GOARCH == "arm64"` — on `darwin/amd64` the default stays
`"texconv"`, same as Linux/Windows. **The goreleaser macOS build
(`darwin/amd64`, labeled "macOS (Intel)") ships a Go binary that embeds
the ARM64 `compressonator-bc7e-macos-arm64` binary but never runs it by
default** — it's still baked in (harmless, unexecuted) since `embed_darwin.go`
isn't GOARCH-gated. If goreleaser ever adds a `darwin/arm64` target, that
build gets the working default; `darwin/amd64` users can still pick
compressonator-bc7e manually in Settings, but it will fail to exec on real
Intel hardware. Callers must check `EmbeddedTools.CompressonatorPath == ""`
for availability rather than `runtime.GOOS` — mirrors how the lockfile /
Job-Object process split is keyed off feature availability rather than raw
OS checks.
13 changes: 9 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ func Load() (*Config, error) {
// a config.json copied from another OS (e.g. Windows → macOS) never selects a
// backend that isn't built for the current platform.
func normalizeBackend(v string) string {
if runtime.GOOS == "darwin" {
return BackendTexconv
}
switch v {
case BackendTexconv, BackendCompressonatorBc7e:
return v
}
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
return BackendCompressonatorBc7e
}
return BackendTexconv
}

Expand Down Expand Up @@ -180,7 +180,7 @@ func LoadProfiles() ([]Profile, []string, int, bool, error) {
}

func defaultConfig() *Config {
return &Config{
cfg := &Config{
ModsDir: detectModsDir(),
WorkerCount: 1,
BackupLevel: 6,
Expand All @@ -190,6 +190,11 @@ func defaultConfig() *Config {
ModlistPath: "",
CompressionBackend: BackendTexconv,
}

if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
cfg.CompressionBackend = BackendCompressonatorBc7e
}
return cfg
}

func detectModsDir() string {
Expand Down
Binary file not shown.
9 changes: 4 additions & 5 deletions internal/tools/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
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.
// CompressonatorPath is empty if the embedded binary is missing for the
// current platform — callers must treat "" as "unavailable" rather than
// special-casing runtime.GOOS.
type EmbeddedTools struct {
TexconvPath string
SevenZipPath string
Expand Down Expand Up @@ -43,8 +43,7 @@ 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.
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-arm64
var compressonatorBin []byte
var _ = compressonatorBin

const texconvName = "texconv"
const sevenZipName = "7zz"
const compressonatorName = ""
const compressonatorName = "compressonator-bc7e"
16 changes: 4 additions & 12 deletions internal/tui/screens/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,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 +213,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 +266,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 All @@ -289,7 +281,7 @@ func (m SettingsModel) View() string {
b.WriteString(style.StyleBody.Render(label) + "\n")
}
b.WriteString(value + "\n")
b.WriteString(style.StyleMuted.Render("compressonator-bc7e: CPU only, all 5 BC formats, deterministic across platforms.\n texconv: GPU-accelerated on Windows for BC7 (much faster there); CPU on Linux.\n space/←/→ to switch.") + "\n\n")
b.WriteString(style.StyleMuted.Render("compressonator-bc7e: CPU only, all 5 BC formats, deterministic across platforms.\n texconv: GPU-accelerated on Windows for BC7; CPU on Linux/macOS.\n space/←/→ to switch.") + "\n\n")
}

// Mod Output Mode toggle.
Expand Down