diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c4be5f6..b5ce90a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -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] @@ -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" @@ -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:** diff --git a/README.md b/README.md index 4cb93ed..224a53c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/SPEC.md b/SPEC.md index e819105..848eed0 100644 --- a/SPEC.md +++ b/SPEC.md @@ -25,17 +25,96 @@ 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 # same fork, macOS universal binary (Intel + Apple Silicon) ├── 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`. +All three platforms ship compressonator-bc7e. `Extract()` still writes the +binary only when the embedded data is non-empty, and callers must check +`EmbeddedTools.CompressonatorPath == ""` to decide availability rather than +special-casing `runtime.GOOS` — that keeps a future platform without a build +from needing changes anywhere but `embed_.go`. + +### Building compressonator-bc7e for macOS + +The fork's README §8 lists macOS as out of scope, so the macOS binary is built +from source with `tools/macos/build_flavor.sh` in the fork +(`noisethanks/compressonator`, branch `bc7enc-rdo-integration`). One build per +architecture, joined with `lipo -create` and ad-hoc signed, matching how +texconv-macos and 7zz-macos ship. + +**The ISPC host architecture decides whether the encoder is correct.** ISPC +1.19 and later, when the *compiler itself* is an aarch64 build, silently +miscompiles `--` on a varying unsigned int into a no-op +([ispc#3882](https://github.com/ispc/ispc/issues/3882)), which corrupts bc7e's +block bit-packing +([bc7enc_rdo#23](https://github.com/richgel999/bc7enc_rdo/issues/23)). The +damage is not limited to arm64 output — an arm64 ISPC host emits a broken +encoder for the x86_64 target too. Compiled with assertions the failure is +loud (`bc7e.ispc:2890: Assertion failed: *pCur_ofs <= 128`); the release build +passes `--opt=disable-assertions`, so it would instead ship textures that are +quietly ~25 dB PSNR worse. + +Two independent routes avoid it, and `build_flavor.sh` accepts either: + +1. **Apply [bc7enc_rdo#29](https://github.com/richgel999/bc7enc_rdo/pull/29)**, + which rewrites the five affected `x--` sites as `x -= 1`. Any ISPC host + then compiles bc7e correctly. Verified byte-identical to an unpatched + build made with an x86_64 host, on both targets. +2. **Use the macOS x86_64 ISPC package**, which runs under Rosetta 2 on Apple + Silicon. + +The script refuses only the unsafe combination — an arm64 ISPC against a +bc7e.ispc that still carries the bare decrements. The shipped binary was built +both ways at once: PR #29 applied, x86_64 ISPC 1.31.0 host. + +Other fixes carried in the fork, all upstream defects rather than fork changes. +The first four are macOS-specific; the threading one is not: +- The C++ standard probe skipped Apple hosts and left them on C++11, which + disables the `std::filesystem` path in `cmp_fileio.cpp`. `CMP_GetJustFileExt` + then returns `dds` instead of `.dds`, `IsDestinationUnCompressed()` compares + against `".dds"` and answers true for every destination, and the CLI writes + a **decompressed** DDS while reporting success. Apple now takes the C++17 + branch. +- `CMP_Core_SSE` / `_AVX` / `_AVX512` are x86 intrinsic code compiled with + `-march=nehalem|haswell|skylake-avx512`; they are skipped on non-x86 targets, + with the declarations and the BC1 dispatch gated on `CMP_CORE_X86_SIMD`. +- `GetCPUID` was a no-op outside Windows but left its output buffer + uninitialized, so macOS chose BC1 SIMD kernels from stack garbage. It now + zero-fills, which also keeps macOS on the same scalar kernels the Linux + reference build uses. +- The CLI's Apple link list hardcoded `/usr/lib/libz.dylib` and five + `/usr/local/lib/libIlm*`-era OpenEXR paths. macOS has had no on-disk + `/usr/lib/libz.dylib` since Big Sur, so the link failed outright. +- **The BC7 worker pool handed slots between threads through a + `volatile CMP_BOOL run` flag.** `volatile` orders nothing between threads. + On arm64 the producer could see a slot go idle before the worker's writes + to the output buffer were visible, then reuse the slot and overwrite the + input the worker was still reading. Measured on Apple Silicon before the + fix: stock BC7 produced **10 different outputs from 10 identical runs**, + the bc7e batched path 3 to 6 distinct outputs from 10, one run lost 19 dB + of PSNR, and one run segfaulted. The flag is now `std::atomic` with + release stores and acquire loads on both sides of the handoff, after which + every configuration returns a single result across 12 runs and matches the + `-NumThreads 1` reference exactly. x86-64's store ordering hides this bug + entirely, which is why the Linux and Windows builds never showed it — it is + an upstream defect in stock Compressonator, not something the bc7e work + introduced, and it affects the stock BC7 codec on any weakly ordered CPU. + +**Cross-platform output is no longer bit-identical.** The arm64 slice encodes +through bc7e's NEON target and the scalar BC1/BC3/BC4/BC5 kernels compiled for +arm64; both differ in the low bits from the SSE/AVX build. Measured on a mixed +corpus, 18 of 38 format/mip configurations differ byte-wise between the two +macOS slices, while PSNR tracks to within ±0.1 dB. Output is reproducible +within a slice: the same input gives the same bytes on every run. + +Against the stock codec on the same machine, bc7e matches on quality and wins +decisively on time — within ±0.7 dB either way across the corpus, and 0.54 s +versus 24.70 s for the same three textures at `-Quality 1.0`. That ordering +(same quality tier, far faster) is what the fork's own README reports, and it +is the reason to choose this backend. 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. @@ -48,7 +127,7 @@ 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 every platform that embeds a build; empty string when none is embedded. On startup: 1. Extract every non-empty embedded binary to `os.MkdirTemp` @@ -56,10 +135,11 @@ On startup: 3. Store paths in an `EmbeddedTools` struct passed through the app 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. +**Binary size:** compressonator-bc7e adds ~9MB on Linux, ~6.4MB on macOS (two +slices in one universal binary) and ~3.5MB on Windows. Current stripped +(`-s -w`) sizes: macOS ~22MB, Linux ~20MB, Windows ~11MB — all under the +historical 25MB target, with macOS now the tightest. Watch this ceiling if +further binaries land. No other runtime dependencies. The binary must run on any supported platform without the user installing anything. @@ -361,7 +441,7 @@ atak/ │ ├── 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_darwin.go # //go:embed bin/texconv-macos, bin/7zz-macos, bin/compressonator-bc7e-macos │ ├── process_linux.go # setProcAttr / killProcess — Linux/macOS │ ├── process_windows.go # setProcAttr / killProcess — Windows Job Objects │ ├── lockfile.go # stale-process lockfile (Linux) @@ -728,22 +808,24 @@ assets are filtered to the chosen mod before passing to the worker pool. - `CompressonatorBackend` — invokes the embedded compressonator-bc7e CLI (`internal/compress/compressonator.go`). AMD Compressonator fork with the CPU-side BC7 codec replaced by `bc7e.ispc` from richgel999/bc7enc_rdo; - GPU codec paths compiled out of the fork. + GPU codec paths compiled out of the fork. Available on all three + platforms. `worker.RunPool` selects the primary backend once per run from `config.CompressionBackend` and passes it plus an optional fallback into each worker goroutine. No per-file backend switching except the explicit `maxTextureSize` fallback below. -- **compressonator-bc7e is always CPU, on both platforms.** The fork ships with +- **compressonator-bc7e is always CPU, on every platform.** The fork ships with its GPU codec paths compiled out — the GPU path isn't guaranteed to work and is never attempted. `compressonatorArgs` **hardcodes `-EncodeWith CPU` on every invocation** rather than relying on the binary's default, so a future upstream change to the default can't quietly re-enable a broken GPU path. This is not user-configurable. It also means Windows users choosing this backend give up texconv's DirectX BC7 acceleration on purpose — the tradeoff - buys cross-platform bit-identical output and the fork's fixed BC7 p-bit - correctness. The active backend name and its CPU/GPU character are surfaced + buys the fork's fixed BC7 p-bit correctness on every platform. Output is + bit-identical between the x86-64 builds; the macOS arm64 slice matches on + quality but not byte-for-byte (see Embedded Binaries). The active backend name and its CPU/GPU character are surfaced in the compress `OperationScreen` title (e.g. `Backend: compressonator-bc7e (CPU)` vs. `Backend: texconv (GPU for BC7)`) so mid-run timing expectations are legible. @@ -942,12 +1024,10 @@ 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 + `"texconv"` (default) and `"compressonator-bc7e"` (CPU-only). Both are + available on all three platforms. Unknown or empty values are coerced to + `"texconv"` on load, so a hand-edited or future-dated config can never name a + backend this build has no implementation for. Toggle in Settings with `space` / `←` / `→` — two-way selector, not free text. - Persist to `os.UserConfigDir()/atak/config.json` @@ -1003,7 +1083,7 @@ project URL, and a scrollable section with all third-party licenses: 1. texconv (Texconv-Custom-DLL) — MIT 2. 7-Zip — LGPL v2.1 3. Charmbracelet UI dependencies (bubbletea, bubbles, lipgloss) — MIT -4. **compressonator-bc7e** (optional Windows/Linux backend) — **dual-licensed: +4. **compressonator-bc7e** (optional backend, all platforms) — **dual-licensed: AMD Compressonator MIT + `bc7e.ispc` Apache License 2.0**. The Apache 2.0 grant requires the release to identify the incorporated Apache-2.0 component, and the About screen carries that attribution verbatim @@ -1241,17 +1321,26 @@ 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 +- `darwin/amd64` — macOS on Intel, community-tested +- `darwin/arm64` — macOS on Apple Silicon, native -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. +Note: macOS ships two archives, not one universal binary. The Go binary is +architecture-specific while the embedded tools are universal, so a universal +atak would carry two full copies of the tools — about 45MB against a 25MB +target. Two 22MB archives stay under it. + +The architecture of the atak process decides the architecture of every tool it +spawns: a universal child inherits the parent's slice, so an `atak-macos` built +for arm64 runs texconv, 7zz and compressonator-bc7e natively, and an x86-64 +build runs all three under Rosetta 2. That is why `darwin/arm64` is a release +target rather than an optional extra — before it existed, every release user on +Apple Silicon was translated end to end. 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. +all embedded tools. With compressonator-bc7e on all three platforms, stripped +release sizes are roughly macOS ~22MB, Linux ~20MB, Windows ~11MB — still under +the ceiling, with macOS the tightest since its universal tools carry two slices +each. Track this if further binaries land. ## Cross-Platform Rules @@ -1270,12 +1359,11 @@ 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 ships on all three platforms.** The macOS binary is + built from source rather than taken from the fork's releases; see "Building + compressonator-bc7e for macOS" under Embedded Binaries, and do not rebuild it + with an arm64 ISPC. Availability is still expressed as + `EmbeddedTools.CompressonatorPath == ""` rather than a `runtime.GOOS` test — + mirrors how the lockfile / Job-Object process split is keyed off feature + availability rather than raw OS checks, and it is what a future platform + without a build would rely on. diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt index b348acd..c33939a 100644 --- a/THIRD_PARTY_LICENSES.txt +++ b/THIRD_PARTY_LICENSES.txt @@ -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 diff --git a/internal/compress/backend.go b/internal/compress/backend.go index cc26a49..124deed 100644 --- a/internal/compress/backend.go +++ b/internal/compress/backend.go @@ -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 diff --git a/internal/compress/dispatch_srgb_test.go b/internal/compress/dispatch_srgb_test.go index 7b25f24..10cf868 100644 --- a/internal/compress/dispatch_srgb_test.go +++ b/internal/compress/dispatch_srgb_test.go @@ -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/ @@ -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() diff --git a/internal/compress/e2e_srgb_summary_test.go b/internal/compress/e2e_srgb_summary_test.go index 7b1dde0..54e1d21 100644 --- a/internal/compress/e2e_srgb_summary_test.go +++ b/internal/compress/e2e_srgb_summary_test.go @@ -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) diff --git a/internal/config/config.go b/internal/config/config.go index bf14f58..c2830b0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 @@ -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 diff --git a/internal/tools/bin/compressonator-bc7e-macos b/internal/tools/bin/compressonator-bc7e-macos new file mode 100755 index 0000000..50be40c Binary files /dev/null and b/internal/tools/bin/compressonator-bc7e-macos differ diff --git a/internal/tools/embed.go b/internal/tools/embed.go index 1ec0933..f2d4ce9 100644 --- a/internal/tools/embed.go +++ b/internal/tools/embed.go @@ -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 @@ -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 { diff --git a/internal/tools/embed_darwin.go b/internal/tools/embed_darwin.go index ae40fd8..330a35a 100644 --- a/internal/tools/embed_darwin.go +++ b/internal/tools/embed_darwin.go @@ -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" diff --git a/internal/tui/screens/about.go b/internal/tui/screens/about.go index efc3d8e..092835d 100644 --- a/internal/tui/screens/about.go +++ b/internal/tui/screens/about.go @@ -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. diff --git a/internal/tui/screens/compress.go b/internal/tui/screens/compress.go index 2669406..394adac 100644 --- a/internal/tui/screens/compress.go +++ b/internal/tui/screens/compress.go @@ -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 != "" { diff --git a/internal/tui/screens/settings.go b/internal/tui/screens/settings.go index 200ce1d..b892583 100644 --- a/internal/tui/screens/settings.go +++ b/internal/tui/screens/settings.go @@ -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 @@ -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 } @@ -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 } @@ -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} } @@ -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 {