Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
565 lines (483 loc) · 32.6 KB
/
Copy pathMakefile
File metadata and controls
565 lines (483 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# Savage Wind build
# macOS: Homebrew clang/SDL3/FFmpeg, Metal renderer.
# Windows: MSYS2 UCRT64 shell (see docs/BUILD-windows.md), clang + Vulkan renderer.
# Linux: clang + Vulkan renderer, x86-64 or aarch64 (see docs/linux.md).
UNAME := $(shell uname -s)
comma := ,
ifneq ($(filter MINGW% MSYS% CYGWIN% Windows_NT,$(UNAME) $(OS)),)
PLATFORM := windows
else ifeq ($(UNAME),Linux)
PLATFORM := linux
else
PLATFORM := macos
endif
# Linux: ARCH (x86_64 / aarch64, default this machine's) picks the target, separately from PLATFORM. The generated code
# differs per arch (AArch64 hosts run the guest's FP/SIMD as inline asm, others use the portable C of simd.h), so a
# cross build has its own generated-<arch>/ and build/<arch>/; SYSROOT is the target's root filesystem (headers, libs).
GEN ?= generated
ifeq ($(PLATFORM),linux)
HOSTARCH := $(patsubst arm64,aarch64,$(shell uname -m))
ARCH ?= $(HOSTARCH)
ifneq ($(ARCH),$(HOSTARCH))
CROSS := --target=$(ARCH)-linux-gnu $(if $(SYSROOT),--sysroot=$(SYSROOT))
GEN := generated-$(ARCH)
B ?= build/$(ARCH)
endif
GENSIMD := --simd $(if $(filter aarch64,$(ARCH)),asm,c)
endif
# B: build dir. MACOS_MIN: oldest macOS to run on (default: this one). DEPS: prefix with SDL3 / FFmpeg instead of
# Homebrew's (make deps). `make app` sets all three (build/mac15, 15.0, build/deps); plain `make` uses none.
B ?= build
MACOS_MIN ?=
DEPS ?=
MINFLAG := $(if $(MACOS_MIN),-mmacosx-version-min=$(MACOS_MIN))
CC := clang $(MINFLAG) $(CROSS)
CXX := clang++ $(MINFLAG) $(CROSS)
IMGUI := third_party/imgui
GENFLAGS := -O1 -fno-strict-aliasing -Wno-return-type -Wno-unused-label -fvisibility=hidden -Iruntime
# The start screen's zig cc reads -O1 as its own "fast" mode (clang -O2 with the loop and SLP vectorizers) and adds
# debug info and frame pointers: these give it what clang -O1 builds here, in a third less time (runtime/gamebuild.c)
GENFLAGS_ZIG := -Xclang $(filter -O%,$(GENFLAGS)) -fno-vectorize -fno-slp-vectorize -g0 -fomit-frame-pointer
# The game code (made by `make gen` from your own game files) is its own library next to the app (runtime/gamelib.h);
# the app itself holds none of it. $(GEN)/layout.c, then a folder per module (main, subsdk*, sdk)
GEN_SRC := $(wildcard $(GEN)/*.c) $(wildcard $(GEN)/*/*.c)
GEN_OBJ := $(patsubst %.c,$(B)/obj/%.o,$(patsubst $(GEN)/%,%,$(GEN_SRC))) $(B)/obj/tools/gamelib_entry.o
RT_SRC := $(filter-out runtime/control.c runtime/control_game.c,$(wildcard runtime/*.c)) \
runtime/control.c runtime/control_game.c $(wildcard runtime/hle/*.c) $(wildcard runtime/services/*.c) \
$(wildcard runtime/nvn/*.c) $(wildcard runtime/nvn/maxwell/*.c) runtime/platform/$(PLATFORM).c
RT_HDRS := $(wildcard runtime/*.h) $(wildcard runtime/platform/*.h) $(wildcard runtime/nvn/*.h) \
$(wildcard runtime/nvn/maxwell/*.h) $(wildcard runtime/nvn/vulkan/*.h) $(wildcard runtime/ui/*.h) \
$(wildcard runtime/services/*.h)
UI_SRC := $(wildcard runtime/ui/*.mm)
UI_OBJ := $(patsubst runtime/%.mm,$(B)/rt/%.o,$(UI_SRC))
IMGUI_OBJ := $(B)/imgui/imgui.o $(B)/imgui/imgui_draw.o $(B)/imgui/imgui_tables.o $(B)/imgui/imgui_widgets.o \
$(B)/imgui/imgui_impl_sdl3.o
ifeq ($(PLATFORM),macos)
SDL := $(or $(DEPS),/opt/homebrew/opt/sdl3)
FFMPEG := $(or $(DEPS),/opt/homebrew/opt/ffmpeg)
EXE := $(B)/savagewind
DEPINC := -I$(SDL)/include -I$(FFMPEG)/include
LDFLAGS := -L$(SDL)/lib -lSDL3 -Wl,-rpath,$(SDL)/lib -L$(FFMPEG)/lib -lavformat -lavcodec -lavutil -lswscale -Wl,-rpath,$(FFMPEG)/lib \
-framework CoreFoundation -framework Foundation -framework Metal -framework MetalFX -framework QuartzCore -framework AppKit -framework ImageIO -framework CoreGraphics -framework UniformTypeIdentifiers
BACKEND_SRC := $(wildcard runtime/nvn/*.m)
BACKEND_OBJ := $(patsubst runtime/%.m,$(B)/rt/%.o,$(BACKEND_SRC))
IMGUI_OBJ += $(B)/imgui/imgui_impl_metal.o
UIFLAGS := -fobjc-arc
GAMELIB := $(B)/swgame.dylib
GAMELIB_LDFLAGS := -dynamiclib -undefined dynamic_lookup # (the runtime functions it calls: the app's own)
GAMELIB_SHARED := -shared -Wl,-undefined,dynamic_lookup # (the same, for the start screen's build: zig cc)
else ifeq ($(PLATFORM),linux)
EXE := $(B)/savagewind
DEPINC := $(if $(DEPS),-I$(DEPS)/include,$(shell pkg-config --cflags libavformat 2>/dev/null)) # (openSUSE has them in /usr/include/ffmpeg)
ifeq ($(ARCH),x86_64)
GENARCH ?= -march=x86-64-v3 # as on Windows: AVX2 + FMA for the lifted FP/SIMD code (checked at startup)
GENARCH_COMPAT ?= -march=x86-64-v2 # CPUs without them (GAMELIB_ID_COMPAT below)
endif
GENFLAGS += $(GENARCH) -fPIC
OSFLAGS := -Wno-format # (%llx for uint64_t, which is unsigned long here: the same size)
# shaderc is libshaderc.so on Debian / Ubuntu and libshaderc_shared.so on Fedora, Arch and a shaderc built from source:
# tools/linux/build_deps.sh builds only the shaderc_shared target, so a DEPS prefix has no libshaderc.so at all - -lshaderc
# there would silently fall through to the system path, where Arch's shaderc package has a static libshaderc.a that is
# only the C wrapper (undefined shaderc_util symbols at the real link). A DEPS prefix therefore links -lshaderc_shared.
# Elsewhere pkg-config knows the name. Without a .pc, the probe links an executable that calls shaderc and tries
# -lshaderc_shared before -lshaderc, so an incomplete static libshaderc.a is rejected at probe time too. A shared-only
# probe cannot do that: a shared link keeps undefined symbols and only the executable link or run fails.
SHADERC_TEST := extern void *shaderc_compiler_initialize(void); int main(void){return shaderc_compiler_initialize()==0;}
SHADERC ?= $(if $(DEPS),-lshaderc_shared,$(or $(shell pkg-config --libs shaderc 2>/dev/null),\
$(shell echo '$(SHADERC_TEST)' | $(CC) -x c - -lshaderc_shared -o /dev/null 2>/dev/null && echo -lshaderc_shared),\
$(shell echo '$(SHADERC_TEST)' | $(CC) -x c - -lshaderc -o /dev/null 2>/dev/null && echo -lshaderc),-lshaderc))
# -rdynamic: crash backtraces name host functions (runtime/platform/linux.c)
LDFLAGS := -fuse-ld=lld -rdynamic $(if $(DEPS),-L$(DEPS)/lib) -lSDL3 -lavformat -lavcodec -lavutil -lswscale \
-lvulkan $(SHADERC) -lz -lpthread -ldl -lm
BACKEND_SRC := $(wildcard runtime/nvn/vulkan/*.c) $(wildcard runtime/nvn/vulkan/*.cpp)
BACKEND_OBJ := $(patsubst runtime/%.cpp,$(B)/rt/%.o,$(patsubst runtime/%.c,$(B)/rt/%.o,$(BACKEND_SRC)))
IMGUI_OBJ += $(B)/imgui/imgui_impl_vulkan.o
UIFLAGS := -x c++ # the .mm UI sources are plain C++ apart from __APPLE__ blocks
GAMELIB := $(B)/swgame.so
GAMELIB_LDFLAGS := -shared -fuse-ld=lld # (the runtime functions it calls: the app's own, -rdynamic)
GAMELIB_SHARED := -shared
else
EXE := $(B)/savagewind.exe
DEPINC := $(if $(DEPS),-I$(DEPS)/include)
OSFLAGS := -D_FILE_OFFSET_BITS=64 -D_POSIX_THREAD_SAFE_FUNCTIONS -D_CRT_RAND_S -DWIN32_LEAN_AND_MEAN -DNOMINMAX
GENARCH ?= -march=x86-64-v3 # AVX2 + FMA for the lifted FP/SIMD code (simd.h)
GENARCH_COMPAT ?= -march=x86-64-v2 # CPUs without them (GAMELIB_ID_COMPAT below)
GENFLAGS += -D_FILE_OFFSET_BITS=64 $(GENARCH)
# --stack: every thread's default stack, reserved (committed as it grows): 256 MB, the guest threads' recursion runs on
# it (runtime/kernel.c thread_start)
LDFLAGS := -fuse-ld=lld -Wl,--stack,268435456 $(if $(DEPS),-L$(DEPS)/lib) -lSDL3 -lavformat -lavcodec -lavutil -lswscale \
-lvulkan-1 -lwinmm -lole32 -lshell32 -lcomdlg32 -luuid -lshaderc_shared -lpdh -lws2_32 -l:libz.a \
$(if $(RELEASE),-Wl$(comma)--subsystem$(comma)windows) # release: no console window (log -> savagewind.log)
BACKEND_SRC := $(wildcard runtime/nvn/vulkan/*.c) $(wildcard runtime/nvn/vulkan/*.cpp)
BACKEND_OBJ := $(patsubst runtime/%.cpp,$(B)/rt/%.o,$(patsubst runtime/%.c,$(B)/rt/%.o,$(BACKEND_SRC)))
BACKEND_OBJ += $(B)/rt/platform/windows_rc.o # the .exe's icon (Explorer, taskbar)
WINDRES ?= windres
IMGUI_OBJ += $(B)/imgui/imgui_impl_vulkan.o
UIFLAGS := -x c++ $(OSFLAGS) # the .mm UI sources are plain C++ apart from __APPLE__ blocks
GAMELIB := $(B)/swgame.dll
# the runtime functions it calls: exported by the .exe, linked through its import library
LDFLAGS += -Wl,--export-all-symbols -Wl,--out-implib,$(B)/savagewind.dll.a
GAMELIB_LDFLAGS := -shared -fuse-ld=lld $(B)/savagewind.dll.a
GAMELIB_SHARED := -shared # (+ the import library: runtime/gamebuild.c)
# "Savage Wind Configuration.exe" in the release: starts savagewind.exe --config (runtime/platform/windows_config.c)
CONFIG_EXE := $(B)/savagewind-config.exe
endif
# GAMELIB_ID: what the game code library is built from (the generator, the headers it is compiled against, the
# flags). A library with another id is rebuilt by the start screen, never loaded.
GAMELIB_SRCS := tools/gen.py tools/lifter.py tools/analyze.py tools/nso.py tools/passes.py tools/patches.py \
tools/simd_c.py tools/gamelib_entry.c runtime/hle/hle_symbols.txt \
runtime/cpu.h runtime/simd.h runtime/simd_fp.h runtime/simd_softfma.h runtime/gamelib.h
GAMELIB_SRCS += tools/layout.py tools/game_sites.py
GAMELIB_ID := $(shell (cat $(GAMELIB_SRCS); echo '$(strip $(GENFLAGS))'; echo '$(GENFLAGS_ZIG)') | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-16)
# The compatible target (#9): x86-64 CPUs without AVX2 / FMA / BMI2 get the game code built with GENARCH_COMPAT in
# place of GENARCH (no -mfma: simd.h's fma is runtime/simd_softfma.h, slower). The start screen builds the one the CPU
# runs (runtime/gamelib.h gamelib_compat); its id differs, so neither library is ever loaded in place of the other.
# `make` builds the GENARCH one.
ifneq ($(strip $(GENARCH)),)
ifneq ($(strip $(GENARCH_COMPAT)),)
GENFLAGS_COMPAT := $(subst $(strip $(GENARCH)),$(strip $(GENARCH_COMPAT)),$(GENFLAGS))
GAMELIB_ID_COMPAT := $(shell (cat $(GAMELIB_SRCS); echo '$(strip $(GENFLAGS_COMPAT))'; echo '$(GENFLAGS_ZIG)') | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-16)
GAMELIB_TARGETS := -DGAMELIB_ARCH='"$(strip $(GENARCH))"' -DGAMELIB_ARCH_COMPAT='"$(strip $(GENARCH_COMPAT))"' \
-DGAMELIB_ID_COMPAT='"$(GAMELIB_ID_COMPAT)"'
endif
endif
# TRANSLATOR_ID / SPIRV_ID: what translated shaders and their SPIR-V are made from (the Maxwell translator, the
# GLSL -> SPIR-V step). The shader caches (cache/xlate, cache/spirv, the pipeline cache and list) are kept while these
# stay the same, so builds that do not touch those sources keep them.
TRANSLATOR_SRCS := runtime/nvn/maxwell/translate.c runtime/nvn/maxwell/shader.h
TRANSLATOR_ID := $(shell cat $(TRANSLATOR_SRCS) | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-16)
SPIRV_ID := $(shell cat runtime/nvn/vulkan/spirv.c | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-16)
# SW_SOURCE: the source this build is made from, in the log's first line (runtime/main.c): its commit and tree, and
# "modified" when the runtime, the tools or this Makefile differ from them ("unknown" outside a git checkout). A
# commit published to test has the tree of the local commit it was made from. $(B)/source_id follows it, so main.o is
# rebuilt when it changes.
comma := ,
SW_SOURCE := $(or $(shell git log -1 --format='commit %h, tree %t' 2>/dev/null),unknown)$(if $(shell git status --porcelain --untracked-files=no -- runtime tools Makefile 2>/dev/null),$(comma) modified)
$(shell mkdir -p $(B) && [ "$$(cat $(B)/source_id 2>/dev/null)" = '$(SW_SOURCE)' ] || echo '$(SW_SOURCE)' > $(B)/source_id)
RT_OBJ := $(patsubst runtime/%.c,$(B)/rt/%.o,$(RT_SRC))
RTFLAGS := -O2 -g -fno-strict-aliasing -Wall -Wno-unused-function -Iruntime -Iruntime/nvn -isystem third_party -isystem third_party/stb $(DEPINC) $(OSFLAGS)
CXXFLAGS := -O2 -g -std=c++17 -I$(IMGUI) -I$(IMGUI)/backends -Iruntime -Iruntime/nvn -isystem third_party -Ithird_party/stb $(DEPINC)
.DEFAULT_GOAL := all
# AMD FidelityFX SDK 1.1.4 (third_party/FidelityFX, MIT): FSR 3 frame interpolation and optical flow
# (runtime/nvn/vulkan/framegen.cpp) and the FSR 3 upscaler (runtime/nvn/vulkan/fsr3.cpp) on its Vulkan backend. Its
# sources unchanged, with ffx_compat.h for what MSVC's headers bring; the shaders are tools/ffx_shaders.py's
# (third_party/FidelityFX/spirv).
FFX := third_party/FidelityFX
FFX_SRC := $(FFX)/sdk/src/components/frameinterpolation/ffx_frameinterpolation.cpp \
$(FFX)/sdk/src/components/opticalflow/ffx_opticalflow.cpp \
$(FFX)/sdk/src/components/fsr3upscaler/ffx_fsr3upscaler.cpp $(FFX)/sdk/src/backends/vk/ffx_vk.cpp \
$(wildcard $(FFX)/sdk/src/shared/*.cpp)
FFX_OBJ := $(patsubst $(FFX)/sdk/src/%.cpp,$(B)/ffx/%.o,$(FFX_SRC))
FFX_INC := -I$(FFX)/sdk/include -I$(FFX)/sdk/src/components -I$(FFX)/sdk/src/shared -I$(FFX)/sdk/src/backends/shared
ifneq ($(PLATFORM),macos)
BACKEND_OBJ += $(FFX_OBJ)
$(B)/rt/nvn/vulkan/fsr3.o: CXXFLAGS := -DSW_FSR3=1 -include $(FFX)/ffx_compat.h $(FFX_INC) $(CXXFLAGS)
endif
$(B)/ffx/%.o: $(FFX)/sdk/src/%.cpp $(FFX)/ffx_compat.h
@mkdir -p $(dir $@)
$(CXX) -O2 -g -std=c++17 -w -include $(FFX)/ffx_compat.h $(FFX_INC) -I$(FFX)/sdk/include/FidelityFX/gpu $(DEPINC) -c $< -o $@
# (the SDK's headers ahead of $(DEPINC)'s: another copy of them there, from a build of the SDK, has other context
# sizes, and a context smaller than the SDK's own code writes overran the next one in memory)
$(B)/rt/nvn/vulkan/framegen.o: CXXFLAGS := -include $(FFX)/ffx_compat.h $(FFX_INC) -I$(FFX)/spirv $(CXXFLAGS)
$(B)/rt/nvn/vulkan/framegen.o: $(FFX)/spirv/ffx_shaders_vk.h $(FFX)/spirv/ffx_fsr3upscaler_vk.h $(FFX)/ffx_compat.h
all: $(EXE) $(CONFIG_EXE) $(if $(strip $(GEN_SRC)),$(GAMELIB)) # (GEN_SRC is a space when there is no generated/)
$(B)/obj/%.o: $(GEN)/%.c runtime/cpu.h runtime/simd.h runtime/simd_fp.h runtime/simd_softfma.h # (cpu.h includes them)
@mkdir -p $(dir $@)
@$(CC) $(GENFLAGS) -c $< -o $@
$(B)/obj/layout.o: runtime/gamelib.h
$(B)/obj/tools/gamelib_entry.o: tools/gamelib_entry.c $(GAMELIB_SRCS) Makefile # (the id: the flags are in here too)
@mkdir -p $(dir $@)
$(CC) $(GENFLAGS) -DGAMELIB_ID='"$(GAMELIB_ID)"' -c $< -o $@
$(B)/rt/nvn/maxwell/translate.o: RTFLAGS += -DTRANSLATOR_ID='"$(TRANSLATOR_ID)"'
$(B)/rt/nvn/vulkan/spirv.o: RTFLAGS += -DSPIRV_ID='"$(SPIRV_ID)"'
$(B)/rt/main.o: $(B)/source_id
$(B)/rt/main.o: RTFLAGS += -DSW_SOURCE='"$(SW_SOURCE)"'
$(B)/rt/gamelib.o: $(GAMELIB_SRCS) Makefile
$(B)/rt/gamelib.o: RTFLAGS += -DGAMELIB_ID='"$(GAMELIB_ID)"' $(GAMELIB_TARGETS)
# the start screen's build of it (runtime/gamebuild.c) uses the same flags
$(B)/rt/gamebuild.o: $(GAMELIB_SRCS) Makefile
$(B)/rt/gamebuild.o: RTFLAGS += -DGAMELIB_CFLAGS='"$(strip $(filter-out -Iruntime,$(GENFLAGS)))"' \
$(if $(GENFLAGS_COMPAT),-DGAMELIB_CFLAGS_COMPAT='"$(strip $(filter-out -Iruntime,$(GENFLAGS_COMPAT)))"') \
-DGAMELIB_ZIGFLAGS='"$(GENFLAGS_ZIG)"' -DGAMELIB_SHARED='"$(GAMELIB_SHARED)"'
# Windows guarded memory scans use SEH.
$(B)/rt/memscan_os.o: RTFLAGS += -fms-extensions
$(B)/rt/%.o: runtime/%.c $(RT_HDRS)
@mkdir -p $(dir $@)
$(CC) $(RTFLAGS) -c $< -o $@
# SMAA (third_party/smaa, MIT): the reference shader as a C string for runtime/nvn/smaa_data.c (both backends build
# their passes from it); the non-ASCII bytes in its comments become '?', and its CRLF line ends plain LF (a CR left in
# the string literals ends them early on macOS clang)
$(B)/inc/smaa_hlsl.inc: third_party/smaa/SMAA.hlsl
@mkdir -p $(dir $@)
LC_ALL=C tr -d '\r' < $< | LC_ALL=C tr '\200-\377' '?' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/"/' -e 's/$$/\\n"/' > $@
$(B)/rt/nvn/smaa_data.o: $(B)/inc/smaa_hlsl.inc
$(B)/rt/nvn/smaa_data.o: RTFLAGS += -I$(B)/inc -Ithird_party/smaa/Textures
$(B)/rt/%.o: runtime/%.cpp $(RT_HDRS)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(OSFLAGS) -c $< -o $@
$(B)/rt/%.o: runtime/%.m $(RT_HDRS)
@mkdir -p $(dir $@)
$(CC) $(RTFLAGS) -fobjc-arc -c $< -o $@
# linked to a temp file, then renamed over: a copy of the game still running keeps its (old) file - relinking in
# place gets running copies killed by macOS (code signature of the changed pages). The temp file keeps the real name
# (in a folder of its own): on Windows the import library records the name the game code library loads it by.
$(EXE): $(RT_OBJ) $(BACKEND_OBJ) $(UI_OBJ) $(IMGUI_OBJ)
@mkdir -p $(dir $@)link
$(CXX) -o $(dir $@)link/$(notdir $@) $^ $(LDFLAGS)
mv -f $(dir $@)link/$(notdir $@) $@
# its id file (<library>.id) says what it was built from without loading it (runtime/gamelib.c)
$(GAMELIB): $(GEN_OBJ) | $(EXE)
$(CC) $(GAMELIB_LDFLAGS) -o $@.tmp $^
mv -f $@.tmp $@
echo $(GAMELIB_ID) > $@.id
$(B)/rt/ui/i18n.o: $(wildcard runtime/ui/i18n/*.inc) runtime/ui/i18n.h
$(B)/rt/ui/%.o: runtime/ui/%.mm $(RT_HDRS)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(UIFLAGS) -c $< -o $@
$(B)/imgui/%.o: $(IMGUI)/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(B)/imgui/imgui_impl_%.o: $(IMGUI)/backends/imgui_impl_%.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(B)/imgui/imgui_impl_metal.o: $(IMGUI)/backends/imgui_impl_metal.mm
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -fobjc-arc -c $< -o $@
# Python: tools/venv (macOS, Linux: bin/python, Windows: Scripts/python.exe)
PY := $(firstword $(wildcard tools/venv/bin/python) $(wildcard tools/venv/Scripts/python.exe))
# GENARGS=--any-version: a game version not supported yet (tools/game_sites.py; docs/SAVAGE-WIND.md)
gen: # every module of the exefs, at the addresses tools/layout.py gives them
cd tools && ../$(PY) gen.py all ../extracted/exefs ../$(GEN) --hle ../runtime/hle/hle_symbols.txt $(GENSIMD) $(GENARGS)
gen-objs: $(GEN_OBJ)
# Runtime objects only (no generated code, no link): quick compile check
runtime-objs: $(RT_OBJ) $(BACKEND_OBJ) $(UI_OBJ)
$(B)/rt/platform/windows_rc.o: runtime/platform/windows.rc runtime/platform/windows.manifest docs/icon/icon.ico
@mkdir -p $(dir $@)
$(WINDRES) -I docs/icon -I runtime/platform -O coff $< -o $@
$(B)/rt/platform/windows_config.o: runtime/platform/windows_config.c
@mkdir -p $(dir $@)
$(CC) -O2 -Wall $(OSFLAGS) -c $< -o $@
$(B)/savagewind-config.exe: $(B)/rt/platform/windows_config.o $(B)/rt/platform/windows_rc.o
$(CC) -o $@ $^ -fuse-ld=lld -Wl,--subsystem,windows
VERSION := $(shell sed -n 's/^\#define SAVAGEWIND_VERSION "\(.*\)"/\1/p' runtime/ui/launcher.mm)
# macOS: build/Savage Wind.app, runs on macOS $(APP_MIN)+: the game rebuilt for that in build/mac15 against build/deps
# (`make deps`: SDL3, LGPL FFmpeg, Python, from pinned sources), the generator helper when built
# (`make gen-helper` after `make deps`), packaged by tools/bundle_macos.sh. No game files or fonts.
APP_MIN := 15.0
APP_B := build/mac15
deps:
MACOS_MIN=$(APP_MIN) tools/build_deps_macos.sh
app:
@[ -f build/deps/lib/libSDL3.0.dylib ] || { echo "make deps first"; exit 1; }
$(MAKE) B=$(APP_B) MACOS_MIN=$(APP_MIN) DEPS=$(CURDIR)/build/deps $(APP_B)/savagewind
MACOS_MIN=$(APP_MIN) tools/bundle_macos.sh $(APP_B)/savagewind
# macOS release disk image (the .app + an Applications shortcut to drag it onto): build/dist/Savage Wind-<v>-macOS.dmg
dmg: app
tools/make_dmg.sh
# Windows: build/dist/SavageWind-<version>-windows.zip, top folder "Savage Wind/": the exe, what the start screen
# builds the game code with on the player's machine (make gen-helper, tools/fetch_zig.sh slimmed by
# tools/zig_slim.sh: its runtime built ahead of time, gamelib-src/, the exe's import library)
# and the DLLs they load (runs without MSYS2). No game code. The exe is a release build in $(WIN_B):
# linked against the LGPL FFmpeg from `make deps-windows` (MSYS2's is a GPLv3 build) and without a console window
# (RELEASE=1: the log goes to savagewind.log next to the exe, fatal errors show a message box). Its DLLs are looked up
# with build/deps-win/bin first (the FFmpeg DLL names are the same as MSYS2's GPL ones). vulkan-1.dll is left
# out: every Vulkan driver installs the loader in System32. Saves and game files go next to the exe at run time.
# Needs: Windows 10+ x64, a Vulkan 1.3 driver, an SSE4.2 CPU (GENARCH_COMPAT x86-64-v2; checked at startup).
DIST := build/dist/Savage Wind
WIN_B := build/win-release
deps-windows:
tools/build_deps_windows.sh
dist:
@[ -f build/deps-win/lib/libavcodec.dll.a ] || { echo "make deps-windows first (LGPL FFmpeg)"; exit 1; }
@[ -d build/helper/swgen ] || { echo "make gen-helper first (the game code generator)"; exit 1; }
@! find tools -maxdepth 1 -name '*.py' -newer build/helper/swgen/swgen.exe | grep -q . || \
{ echo "make gen-helper first: tools/*.py changed since build/helper/swgen was frozen"; exit 1; }
@[ -x build/zig/zig.exe ] || { echo "tools/fetch_zig.sh first (the compiler)"; exit 1; }
$(MAKE) B=$(WIN_B) DEPS=$(CURDIR)/build/deps-win RELEASE=1 $(WIN_B)/savagewind.exe $(WIN_B)/savagewind-config.exe
! nm $(WIN_B)/savagewind.exe | grep ' f_70' >/dev/null # (no game code inside)
rm -rf build/dist && mkdir -p "$(DIST)/gamelib-src"
cp $(WIN_B)/savagewind.exe $(WIN_B)/savagewind.dll.a "$(DIST)/"
cp $(WIN_B)/savagewind-config.exe "$(DIST)/Savage Wind Configuration.exe"
tools/zig_slim.sh $(strip $(filter-out -Iruntime,$(GENFLAGS))) \
$(if $(GENFLAGS_COMPAT),-- $(strip $(filter-out -Iruntime,$(GENFLAGS_COMPAT)))) # (zig, its runtime built ahead of time)
cp -R build/helper/swgen "$(DIST)/"
cp -R build/zig-slim "$(DIST)/zig"
cp runtime/cpu.h runtime/simd.h runtime/simd_fp.h runtime/simd_softfma.h runtime/gamelib.h tools/gamelib_entry.c \
runtime/hle/hle_symbols.txt "$(DIST)/gamelib-src/"
cp docs/icon/icon_1024.png "$(DIST)/icon.png"
cp third_party/fonts/FiraSans/FiraSans-SemiBold.ttf third_party/fonts/FiraSans/FiraSans-Medium.ttf "$(DIST)/" # (menus, OFL)
cp third_party/fonts/FiraSans/OFL.txt "$(DIST)/FiraSans-OFL.txt"
cp third_party/fonts/NotoSansThai/NotoSansThai-Medium.ttf "$(DIST)/" # (Thai menus, OFL)
cp third_party/fonts/NotoSansThai/OFL.txt "$(DIST)/NotoSansThai-OFL.txt"
cp third_party/FidelityFX/LICENSE.txt "$(DIST)/FidelityFX-LICENSE.txt" # (AMD FidelityFX SDK, MIT: README "License")
mkdir "$(DIST)/colour-luts" && cp third_party/luts/*.cube third_party/luts/README.md "$(DIST)/colour-luts/" # (CC0: Video > Colour)
cp README.md LICENSE "$(DIST)/" # (licenses and source links of the bundled components: README "License")
PATH="$(CURDIR)/build/deps-win/bin:$$PATH" ldd $(WIN_B)/savagewind.exe | awk '(/ucrt64/ || /deps-win/) && !/vulkan-1/ {print $$3}' | sort -u | \
xargs -I{} cp -u {} "$(DIST)/"
! ls "$(DIST)" | grep -iE 'x264|x265|xvid|postproc' # (no GPL codec libraries in the release)
! ls "$(DIST)" | grep -i splash # (no start screen picture: the player's own, never shipped)
cd build/dist && if command -v zip >/dev/null; then zip -qr "SavageWind-$(VERSION)-windows.zip" "Savage Wind"; \
else /c/Windows/System32/tar.exe -a -cf "SavageWind-$(VERSION)-windows.zip" "Savage Wind"; fi
@echo "build/dist/SavageWind-$(VERSION)-windows.zip"
# The game code generator as a standalone helper, so players need no Python: build/helper/swgen/ (swgen[.exe] +
# _internal/), frozen by PyInstaller from tools/swgen.spec in its own venv (build/gen-venv). Shipped as swgen/ next to
# the exe (Windows, Linux) or in "Savage Wind.app"/Contents/Resources. The start screen's setup runs it on the player's own
# game files (runtime/gamebuild.c). PyInstaller only builds for the OS it runs on.
# macOS arm64: the relocatable Python 3.12 from `make deps` (macOS 11+ / 15+: the helper runs where the app does);
# without it Homebrew's python@3.12 (then only on the macOS it was built for).
# Windows x86-64: from the MSYS2 shell with a python.org CPython 3.12 (prebuilt unicorn / capstone wheels), e.g.
# make gen-helper GEN_PY="/c/Users/<you>/AppData/Local/Programs/Python/Python312/python.exe"
ifeq ($(PLATFORM),macos)
GEN_PY ?= $(firstword $(wildcard build/deps/python/bin/python3.12) python3.12)
GEN_VPY := build/gen-venv/bin/python
else
GEN_PY ?= py -3.12
GEN_VPY := build/gen-venv/Scripts/python.exe
endif
gen-helper: build/gen-venv/.ok
$(if $(filter windows,$(PLATFORM)),USERPROFILE="$$(cygpath -w ~ 2>/dev/null || echo $$HOME)") \
$(GEN_VPY) -m PyInstaller --noconfirm --clean --log-level WARN --distpath build/helper --workpath build/pyinstaller \
tools/swgen.spec
$(if $(filter windows,$(PLATFORM)),$(GEN_VPY) tools/pe_no_cfg.py build/helper/swgen/swgen.exe)
@du -sh build/helper/swgen
build/gen-venv/.ok:
$(GEN_PY) -m venv build/gen-venv
$(GEN_VPY) -m pip install -q capstone==5.0.9 unicorn==2.1.4 lz4==4.4.5 pyinstaller==6.22.3
touch $@
# Linux: build/dist/Savage_Wind-<version>-<arch>.AppImage, built in Ubuntu 22.04 containers (podman) with SDL3, LGPL
# FFmpeg, shaderc and the game code generator helper inside; the Vulkan loader and GPU drivers are the system's.
# `make appimage` (ARCH: this machine's, or aarch64 cross-compiled), `make appimages` both. See docs/linux.md.
appimage:
tools/linux/appimage.sh $(ARCH)
appimages:
tools/linux/appimage.sh x86_64
tools/linux/appimage.sh aarch64
.PHONY: dmg all gen gen-helper gen-objs runtime-objs deps deps-windows app dist appimage appimages
build/test_shaders: tools/test_shaders.m runtime/nvn/maxwell/translate.c runtime/nvn/maxwell/shader.h
clang -O1 -fobjc-arc -framework Metal -framework Foundation -o $@ tools/test_shaders.m runtime/nvn/maxwell/translate.c
build/test_romfs: tools/test_romfs.c runtime/romfs_build.c runtime/romfs_image.c runtime/romfs_build.h runtime/romfs_image.h
clang -O1 -Iruntime -o $@ tools/test_romfs.c runtime/romfs_build.c runtime/romfs_image.c -lpthread
$(B)/test_bind_capture: tools/test_bind_capture.c runtime/hle/bindings.c runtime/input_bind.h
@mkdir -p $(B)
$(CC) -O1 -DSDL_STATIC_LIB $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_bind_capture.c runtime/hle/bindings.c -lSDL3
test-bind-capture: $(B)/test_bind_capture
$(B)/test_bind_capture
.PHONY: test-bind-capture
$(B)/test_pc_controls: tests/test_pc_controls.c runtime/hle/input.c runtime/hle/bindings.c runtime/settings.h
@mkdir -p $(dir $@)
$(CC) -O2 -ffunction-sections -fdata-sections -DSDL_STATIC_LIB $(OSFLAGS) $(DEPINC) -Iruntime -isystem third_party -o $@ $< -fuse-ld=lld -Wl,--gc-sections $(if $(filter windows,$(PLATFORM)),-Wl$(comma)--exclude-all-symbols) -lSDL3 -lm
$(B)/test_pc_settings: tests/test_pc_settings.c runtime/settings.c runtime/settings.h runtime/hle/bindings.c
@mkdir -p $(dir $@)
$(CC) -O1 -DSDL_STATIC_LIB $(OSFLAGS) $(DEPINC) -Iruntime -o $@ $< -lSDL3 -lm
$(B)/test_options_pointer: tests/test_options_pointer.c runtime/pc_cursor.c runtime/pc_cursor.h runtime/pc_cursor_math.h
@mkdir -p $(dir $@)
$(CC) -O2 -ffunction-sections -fdata-sections -DSDL_STATIC_LIB $(OSFLAGS) $(DEPINC) -Iruntime -isystem third_party -o $@ $< -fuse-ld=lld -Wl,--gc-sections $(if $(filter windows,$(PLATFORM)),-Wl$(comma)--exclude-all-symbols) -lSDL3 -lm
test-pc-controls: $(B)/test_pc_controls $(B)/test_pc_settings $(B)/test_options_pointer
$(B)/test_pc_controls
$(B)/test_options_pointer
@set -e; tmp=$$(mktemp); trap 'rm -f "$$tmp"' EXIT; \
printf 'settings_version = 3\npc_cursor = 1\n' > "$$tmp"; \
$(B)/test_pc_settings "$$tmp" 1; \
$(B)/test_pc_settings "$$tmp" 0; \
SAVAGEWIND_PC_CURSOR=0 $(B)/test_pc_settings "$$tmp" 0
.PHONY: test-pc-controls
$(B)/test_vibration: tools/test_vibration.c runtime/hle/vibration.c runtime/vibration.h runtime/services/hid_set.c
@mkdir -p $(B)
$(CC) -O1 -DSDL_STATIC_LIB $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_vibration.c -lpthread -lm
test-vibration: $(B)/test_vibration
$(B)/test_vibration
.PHONY: test-vibration
# The input test includes input.c; discard its unused HLE functions and their runtime dependencies.
$(B)/test_input_focus: tools/test_input_focus.c tools/test_pointer_disabled.h runtime/hle/input.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -ffunction-sections -fdata-sections $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_input_focus.c \
$(if $(filter macos,$(PLATFORM)),-Wl$(comma)-dead_strip,-Wl$(comma)--gc-sections) \
$(if $(filter macos,$(PLATFORM)),-L$(SDL)/lib,$(if $(DEPS),-L$(DEPS)/lib)) -lSDL3 -lm
test-input-focus: $(B)/test_input_focus
$(B)/test_input_focus
.PHONY: test-input-focus
# The virtual gyro at a shrine apparatus (docs/VIRTUAL_GYRO.md): the gyro keys through input.c and the bindings, the
# two Controls switches saved and loaded, the turns (motion.c), the apparatus heartbeat, the drag (no game files)
TEST_LD := $(if $(filter macos,$(PLATFORM)),-Wl$(comma)-dead_strip,-Wl$(comma)--gc-sections) \
$(if $(filter macos,$(PLATFORM)),-L$(SDL)/lib,$(if $(DEPS),-L$(DEPS)/lib))
$(B)/test_gyro_bindings: tools/test_gyro_bindings.c tools/test_pointer_disabled.h runtime/hle/input.c runtime/hle/bindings.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -ffunction-sections -fdata-sections $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_gyro_bindings.c \
runtime/hle/bindings.c $(TEST_LD) -lSDL3 -lm
$(B)/test_gyro_settings: tools/test_gyro_settings.c runtime/settings.c runtime/hle/bindings.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -ffunction-sections -fdata-sections $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_gyro_settings.c \
$(TEST_LD) -lSDL3 -lm
$(B)/test_motion_keyboard: tools/test_motion_keyboard.c runtime/hle/motion.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -ffunction-sections -fdata-sections $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_motion_keyboard.c \
$(TEST_LD) -lSDL3 -lm
$(B)/test_gyro_activation: tools/test_gyro_activation.c runtime/pc_gyro_scene.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -ffunction-sections -fdata-sections $(OSFLAGS) $(DEPINC) -Iruntime -o $@ tools/test_gyro_activation.c \
$(TEST_LD) -lm
$(B)/test_gyro_drag: tools/test_gyro_drag.cpp runtime/ui/gyro_drag.h $(B)/imgui/imgui.o $(B)/imgui/imgui_draw.o \
$(B)/imgui/imgui_tables.o $(B)/imgui/imgui_widgets.o
@mkdir -p $(B)
$(CXX) -O1 -std=c++17 $(OSFLAGS) -I$(IMGUI) -Iruntime -o $@ tools/test_gyro_drag.cpp $(filter %.o,$^)
test-gyro: $(B)/test_gyro_bindings $(B)/test_gyro_settings $(B)/test_motion_keyboard $(B)/test_gyro_activation \
$(B)/test_gyro_drag test-bind-capture
$(B)/test_gyro_bindings
@set -e; tmp=$$(mktemp); trap 'rm -f "$$tmp"' EXIT; \
printf 'settings_version = 3\npc_gyro = 0\ngyro_scene = 1\n' > "$$tmp"; \
$(B)/test_gyro_settings "$$tmp" 0 1; \
$(B)/test_gyro_settings "$$tmp" 1 0 saved; \
SAVAGEWIND_PC_GYRO=1 SAVAGEWIND_GYRO_SCENE=0 $(B)/test_gyro_settings "$$tmp" 1 0
$(B)/test_motion_keyboard
$(B)/test_gyro_activation
$(B)/test_gyro_drag
python3 tools/test_gyro_site.py
.PHONY: test-gyro
$(B)/test_swkbd: tools/test_swkbd.c runtime/hle/swkbd.c $(RT_HDRS)
@mkdir -p $(B)
$(CC) -O1 -std=gnu11 -Wall -Wextra -Werror $(OSFLAGS) -Iruntime -o $@ tools/test_swkbd.c
test-swkbd: $(B)/test_swkbd
$(B)/test_swkbd
.PHONY: test-swkbd
# The Wii U / Cemu save importer against tools/botw_save_convert.py: both convert synthetic saves and every output file
# is compared (no game files needed). With your own: python3 tools/test_save_convert.py --src <profile> --bootup <pack>
$(B)/test_save_convert: tools/test_save_convert.c runtime/services/save_convert.c runtime/services/save_convert.h runtime/services/archive.c runtime/services/archive.h
@mkdir -p $(B)
$(CC) -O1 -std=gnu11 -D_FILE_OFFSET_BITS=64 -Iruntime -o $@ tools/test_save_convert.c runtime/services/archive.c
test-save-convert: $(B)/test_save_convert
python3 tools/test_save_convert.py --exe $(B)/test_save_convert
.PHONY: test-save-convert
$(B)/test_game_file_size: tools/test_game_file_size.c runtime/game_file_size.c runtime/game_file_size.h runtime/game_files.h
@mkdir -p $(B)
$(CC) -O1 -Wall $(OSFLAGS) -Iruntime -o $@ tools/test_game_file_size.c runtime/game_file_size.c
test-game-file-size: $(B)/test_game_file_size
$(B)/test_game_file_size
$(B)/test_file_size_cache: tools/test_file_size_cache.cpp runtime/ui/file_size_cache.mm runtime/ui/file_size_cache.h runtime/game_file_size.h
@mkdir -p $(B)
$(CXX) -O1 -Wall -std=c++17 $(OSFLAGS) -Iruntime -o $@ tools/test_file_size_cache.cpp -lpthread
test-file-size-cache: $(B)/test_file_size_cache
$(B)/test_file_size_cache
.PHONY: test-game-file-size test-file-size-cache
# the command layer of the control endpoint (who may run what, the queue, the text)
$(B)/test_control: tools/test_control.c runtime/control.c runtime/control.h
@mkdir -p $(B)
$(CC) -O1 -g -Wall $(OSFLAGS) -Iruntime -o $@ tools/test_control.c runtime/control.c -lpthread -lm \
$(if $(filter windows,$(PLATFORM)),-lws2_32)
test-control: $(B)/test_control
env -u SAVAGEWIND_CONTROL -u SAVAGEWIND_DEV $(B)/test_control
env -u SAVAGEWIND_CONTROL SAVAGEWIND_DEV=1 $(B)/test_control dev
.PHONY: test-control
# the in-game console's lines, history and completion
$(B)/test_console_core: tools/test_console_core.c runtime/console_core.c runtime/console_core.h
@mkdir -p $(B)
$(CC) -O1 -g -Wall $(OSFLAGS) -Iruntime -o $@ tools/test_console_core.c runtime/console_core.c
test-console: $(B)/test_console_core
$(B)/test_console_core
.PHONY: test-console
$(B)/test_hour: tools/test_hour.c runtime/services/test_warp.c runtime/game_sites.h
@mkdir -p $(B)
$(CC) -O1 -g -Wall -Wno-unused-function $(OSFLAGS) -Iruntime -o $@ tools/test_hour.c -lm
test-hour: $(B)/test_hour
env -u SAVAGEWIND_TEST_TIME $(B)/test_hour
env SAVAGEWIND_TEST_TIME=18 $(B)/test_hour env
.PHONY: test-hour