Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
345 lines (280 loc) · 16.2 KB
/
Copy pathMakefile
File metadata and controls
345 lines (280 loc) · 16.2 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
# ClickDOOM task runner. `make help` lists every target.
#
# Editing rules:
#
# No `@` prefix on a target that checks something, so the log shows the real
# invocation. `help` is the exception.
#
# No pipes. A pipeline reports only the last command's exit status, so a
# failure upstream of the last stage passes silently. `.SHELLFLAGS` sets
# pipefail for the pipes inside the scripts these targets call.
#
# One `.PHONY` list, below. Nothing here builds a file of its own name.
#
# Not parallel-safe. Most targets share one ClickHouse container, and the
# compiled-expression cache is server-global, so two timing runs at once
# measure each other. Do not pass -j.
.DEFAULT_GOAL := help
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
# Connection. Environment values win, which is how CI passes its own.
CLICKHOUSE_PASSWORD ?= clickdoom
CH_HOST ?= localhost
CH_PORT ?= 9000
CH_HTTP_PORT ?= 8123
conn = --host $(CH_HOST) --port $(CH_PORT) --password "$(CLICKHOUSE_PASSWORD)"
# The clickdoom binary speaks ClickHouse's HTTP interface, not the native
# protocol scripts/clickhouse-client use, so it takes its own port.
CLICKDOOM ?= ./target/release/clickdoom
clickdoom_conn = --host $(CH_HOST) --port $(CH_HTTP_PORT) --password "$(CLICKHOUSE_PASSWORD)" --database "$(CLICKDOOM_DATABASE)"
# Native mode has a database of its own. A native load empties the tables its
# schema declares, and those names are not the emulation ones, but keeping the
# two apart means a native run cannot slow an emulation run's merges either.
CLICKDOOM_NATIVE_DATABASE ?= clickdoom_native
native_conn = --host $(CH_HOST) --port $(CH_HTTP_PORT) --password "$(CLICKHOUSE_PASSWORD)" --database "$(CLICKDOOM_NATIVE_DATABASE)"
# clickhouse-client blocks forever on an INSERT when stdin is an open pipe
# rather than at EOF. Every target here is non-interactive, so stdin is closed
# for all of them. Without it `make diff` inside a pipeline, an editor task
# runner or a CI step hangs with no output and no query running server-side.
no_stdin = < /dev/null
# Where the reference trace lands. Named after the ROM it was generated from,
# the same way the generator names its own output, so a re-pinned ROM does not
# silently reuse the previous one's trace.
ROM_BIN ?= rom/build/doom-rv32im.bin
ROM_ELF ?= rom/build/doom-rv32im.elf
ROM_MANIFEST ?= rom/build/manifest.json
CLICKDOOM_DATABASE ?= clickdoom
CLICKDOOM_RUN_K ?= 60000
CLICKDOOM_RUN_HWM ?= 20000
CLICKDOOM_TARGET_ICOUNT ?= 15393136
# The reference emulator, and the numbers a regenerated trace is checked
# against. These move when rom/PINNED_HASH does, and they live here so the
# ROM's hash and the milestones it implies sit together.
REFEMU ?= ./target/release/refemu
REFERENCE_TRACE_MAX ?= 15728640
EXPECT_INIT_GRAPHICS ?= 11014966
EXPECT_FIRST_FRAME_FBHASH ?= fe5d82c0f42d45f1
DEMO3_MAX ?= 4000000000
reference_trace = refemu/reference_traces/demo-boot-to-first-frame.$$(cut -c1-12 rom/PINNED_HASH).tsv
.PHONY: help up down build-rom \
test test-group smoke diff \
bench-canonical-throughput machine-lock \
preflight-milestone run-milestone \
build-refemu build-clickdoom build-riscv-tests-fixtures gen-reference-trace gen-demo3-trace \
gen-layout gen-probe-trace gen-probe-fixture \
native-smoke native-load native-demo native-demo-sim native-play native-parity require-probe-trace \
fuzz \
lint check-purity shellcheck format clippy typos actionlint zizmor \
adr-new check-adr require-rom \
gates check-rom-hash
help: ## List every target
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage: make <target>\n"} \
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-28s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
@echo
##@ Environment
up: ## Start the pinned ClickHouse container
# A server already answering is left alone, so `make test` behaves the
# same against a local compose container and against CI's service
# container, which binds these ports before make ever runs.
curl -fsS "http://$(CH_HOST):$(CH_HTTP_PORT)/ping" >/dev/null 2>&1 \
|| docker compose up -d --wait
down: ## Stop it
docker compose down
##@ Build
build-rom: ## Build the DOOM ROM reproducibly, in the pinned toolchain image
make -C rom
build-refemu: ## Build the reference emulator
cargo build --locked --release -p refemu
build-clickdoom: ## Build the driver binary
cargo build --locked --release -p clickdoom-driver
require-rom:
test -f $(ROM_BIN) || { echo "$(ROM_BIN) missing. Run: make build-rom" >&2; exit 1; }
test -f $(ROM_ELF) || { echo "$(ROM_ELF) missing. Run: make build-rom" >&2; exit 1; }
##@ Test
# Two invocations, not one: the reference-trace and demo3 comparisons need
# --release to finish in reasonable time, and everything else runs in debug.
# The live suites need the container `up` starts; the rest do not.
test: up require-rom build-refemu ## Every suite, live ones included
CLICKHOUSE_HOST=$(CH_HOST) CLICKHOUSE_HTTP_PORT=$(CH_HTTP_PORT) CLICKHOUSE_PASSWORD="$(CLICKHOUSE_PASSWORD)" \
cargo test --locked --workspace --features clickhouse-tests -- --nocapture
cargo test --locked --release -p refemu --features rom-tests \
--test reference_trace --test demo3_parity --test rom_symbols \
--test probe_fixture -- --nocapture
# The suites in the groups ci.yml runs side by side, through cargo-nextest
# (`cargo install cargo-nextest --locked`). scripts/test-group.sh says what
# each group holds.
test-group: up require-rom build-refemu ## One CI group of suites: make test-group GROUP=native-sim-a
CLICKHOUSE_HOST=$(CH_HOST) CLICKHOUSE_HTTP_PORT=$(CH_HTTP_PORT) CLICKHOUSE_PASSWORD="$(CLICKHOUSE_PASSWORD)" \
scripts/test-group.sh "$(GROUP)"
N ?= 100000
diff: up require-rom build-refemu build-clickdoom ## Differential run of N instructions, reporting the first divergence
$(CLICKDOOM) emulation diff $(N) --bin $(ROM_BIN) --manifest $(ROM_MANIFEST) \
--hwm "$(CLICKDOOM_RUN_HWM)" --refemu-bin $(REFEMU) $(clickdoom_conn)
smoke: ## The differential run CI uses, at 100,000 instructions
$(MAKE) diff N=100000
##@ Native
#
# The committed probe fixture and the metadata beside it.
# refemu/tests/probe_fixture.rs holds the directory to one file per ROM, so a
# wildcard names it without repeating the ROM hash here.
probe_fixture = $(wildcard refemu/probe/fixtures/*.tsv)
probe_fixture_meta = $(probe_fixture:.tsv=.json)
# The first frame after the screen melt, which the fixture's own metadata
# names. Rendering it needs every frame before it, and the fixture carries the
# melt's last frame, so the run is three frames long.
native_smoke_frame = $(shell sed -n 's/.*"first_gameplay_frame": *\([0-9]*\).*/\1/p' $(probe_fixture_meta))
# The reference emulator's state at every demo3 frame, which gen-probe-trace
# writes and which is not committed. Named the way reference_trace is.
probe_trace = refemu/reference_traces/demo3/probe.$$(cut -c1-12 rom/PINNED_HASH).tsv
require-probe-trace:
test -f $(probe_trace) || { echo "$(probe_trace) missing. Run: make gen-probe-trace" >&2; exit 1; }
native-load: up build-clickdoom ## Decode the WAD into the native database, and load the probe trace when it exists
$(CLICKDOOM) native load --fresh $(native_conn) $(no_stdin)
if test -f $(probe_trace); then $(CLICKDOOM) native load --probe $(probe_trace) $(native_conn) $(no_stdin); fi
native-demo: up build-clickdoom require-probe-trace ## Play demo3 at 35 Hz in a window from the probed states
$(CLICKDOOM) native load --probe $(probe_trace) $(native_conn) $(no_stdin)
$(CLICKDOOM) native demo demo3 $(native_conn) $(no_stdin)
native-demo-sim: up build-clickdoom ## Play demo3 at 35 Hz in a window, from the simulation's own commands
$(CLICKDOOM) native demo demo3 --from sim $(native_conn) $(no_stdin)
native-play: up build-clickdoom ## Play the loaded level from the keyboard and mouse
$(CLICKDOOM) native play $(native_conn) $(no_stdin)
# Both halves of the differential over the whole of demo3: every frame the
# renderer draws against the engine's own hash, and the simulation tic by tic
# against the engine's own state. Exit 3 on the first frame or tic that differs.
DEMO3_TICS ?= 2134
native-parity: up build-clickdoom require-probe-trace ## Every demo3 frame and tic against the reference emulator
$(CLICKDOOM) native load --fresh $(native_conn) $(no_stdin)
$(CLICKDOOM) native load --probe $(probe_trace) $(native_conn) $(no_stdin)
$(CLICKDOOM) native demo demo3 --no-window --expect-probe-fbhash $(native_conn) $(no_stdin)
$(CLICKDOOM) native diff $(DEMO3_TICS) --probe $(probe_trace) $(native_conn) $(no_stdin)
native-smoke: up build-clickdoom ## Render the first gameplay frame from the committed probe fixture and check its hash
test -n "$(probe_fixture)" || { echo "no probe fixture in refemu/probe/fixtures/. Run: make gen-probe-fixture" >&2; exit 1; }
test -n "$(native_smoke_frame)" || { echo "$(probe_fixture_meta) names no first_gameplay_frame" >&2; exit 1; }
$(CLICKDOOM) native load --fresh $(native_conn) $(no_stdin)
$(CLICKDOOM) native load --probe $(probe_fixture) $(native_conn) $(no_stdin)
$(CLICKDOOM) native render --frame $(native_smoke_frame) --expect-probe-fbhash $(native_conn) $(no_stdin)
##@ Bench
#
# Timings need a quiet machine. docs/benchmarks.md indexes what has already
# been measured, and DEVELOPING.md says what to record alongside a number.
# The image each bench arm starts its own container from, read out of
# docker-compose.yml so the pin is stated in one place.
clickhouse_image = $(shell sed -n 's|^ *image: \(clickhouse/clickhouse-server.*\)$$|\1|p' docker-compose.yml)
# Who the bench target takes the machine lock as. Override it with a name a
# reader of the lock can reach.
MACHINE_LOCK_HOLDER ?= $(USER)
machine-lock: ## Who holds the machine lock
./scripts/machine-lock.sh status
# No `up`: each arm starts and removes a container of its own, so this target
# does not touch the shared one. It still holds the machine lock, because a
# timing measures whatever else is running on the box.
bench-canonical-throughput: require-rom build-refemu build-clickdoom ## Real-ROM throughput: boot and gameplay windows, fold-alone and end to end
./scripts/machine-lock.sh run "$(MACHINE_LOCK_HOLDER)" bench-canonical-throughput -- \
$(CLICKDOOM) emulation bench canonical --bin $(ROM_BIN) --manifest $(ROM_MANIFEST) \
--image "$(clickhouse_image)" \
--k "$(CLICKDOOM_RUN_K)" --hwm "$(CLICKDOOM_RUN_HWM)" \
--refemu-bin $(REFEMU)
##@ Milestone
preflight-milestone: up require-rom build-clickdoom ## Fail-closed gates before a multi-hour run. Refuses to start rather than advising
$(CLICKDOOM) emulation preflight --bin "$(ROM_BIN)" --manifest "$(ROM_MANIFEST)" \
--k "$(CLICKDOOM_RUN_K)" --hwm "$(CLICKDOOM_RUN_HWM)" \
$(clickdoom_conn)
run-milestone: up require-rom build-clickdoom ## The resumable batch loop. Runs its own preflight and refuses to start if it fails
$(CLICKDOOM) emulation run --bin "$(ROM_BIN)" --manifest "$(ROM_MANIFEST)" \
--k "$(CLICKDOOM_RUN_K)" --hwm "$(CLICKDOOM_RUN_HWM)" \
--trace "$(reference_trace)" \
--target-icount "$(CLICKDOOM_TARGET_ICOUNT)" \
--stop-at-frame 0 \
$(clickdoom_conn)
##@ Maintenance
FUZZ_SECONDS ?= 60
FUZZ_TARGETS ?= predecode_equivalence step_invariants elf_loader snapshot_reader
fuzz: ## Coverage-guided fuzzing. Needs the nightly fuzz/ pins and cargo-fuzz
for target in $(FUZZ_TARGETS); do \
echo "== $$target =="; \
(cd fuzz && cargo +nightly fuzz run "$$target" -- \
-max_total_time=$(FUZZ_SECONDS) -print_final_stats=1); \
done
build-riscv-tests-fixtures: ## Regenerate refemu's committed riscv-tests fixtures
./refemu/scripts/build_riscv_tests.sh
gen-reference-trace: require-rom build-refemu ## Regenerate the committed reference trace. Refuses to run against an unpinned ROM
$(REFEMU) run $(ROM_BIN) --manifest $(ROM_MANIFEST) \
--pinned-hash rom/PINNED_HASH \
--stop-at frame:0 -n $(REFERENCE_TRACE_MAX) \
--expect-icount $(CLICKDOOM_TARGET_ICOUNT) \
--expect-fbhash $(EXPECT_FIRST_FRAME_FBHASH)
$(REFEMU) trace $(ROM_BIN) --manifest $(ROM_MANIFEST) \
--pinned-hash rom/PINNED_HASH -n $(REFERENCE_TRACE_MAX) \
--console-milestone 'I_InitGraphics: framebuffer=init_graphics' \
--expect-milestone init_graphics=$(EXPECT_INIT_GRAPHICS) \
--out-dir refemu/reference_traces --name demo-boot-to-first-frame
LAYOUT ?= refemu/probe/layout.tsv
# The frames the committed fixture holds: the first, the last three of the
# screen melt with the first gameplay frame among them, and one from the middle
# of the demo. A row carries every mobj and every sector, so a handful is all
# that fits in a file worth committing.
PROBE_FIXTURE_FRAMES ?= 0,39..41,1000
gen-probe-trace: require-rom build-refemu ## Dump the engine's state at every demo3 frame. The .tsv is not committed
$(REFEMU) probe $(ROM_ELF) --manifest $(ROM_MANIFEST) \
--pinned-hash rom/PINNED_HASH --layout $(LAYOUT) \
--stop-at halt -n $(DEMO3_MAX) \
--out-dir refemu/reference_traces/demo3 --name probe --rng-name probe-rng
gen-probe-fixture: require-rom build-refemu ## Regenerate the committed probe fixture
$(REFEMU) probe $(ROM_ELF) --manifest $(ROM_MANIFEST) \
--pinned-hash rom/PINNED_HASH --layout $(LAYOUT) \
--frames $(PROBE_FIXTURE_FRAMES) -n $(DEMO3_MAX) \
--out-dir refemu/probe/fixtures --name demo3-frames
gen-layout: ## Regenerate the committed struct layout from the pinned toolchain
make -C rom layout
cp rom/build/layout.tsv $(LAYOUT)
gen-demo3-trace: require-rom build-refemu ## Run demo3 to completion and write its manifest. The .tsv is not committed
$(REFEMU) trace $(ROM_BIN) --manifest $(ROM_MANIFEST) \
--pinned-hash rom/PINNED_HASH --stop-at halt -n $(DEMO3_MAX) \
--out-dir refemu/reference_traces/demo3 --name demo3
##@ Docs
SLUG ?=
adr-new: ## Scaffold an ADR: make adr-new SLUG=some-decision
./scripts/adr.sh --new "$(SLUG)"
check-adr: ## The ADR set is numbered contiguously and fully indexed
./scripts/adr.sh --check
##@ Lint
lint: check-purity shellcheck format clippy typos check-adr actionlint zizmor check-bare ## Every check that needs no container and no ROM
check-purity: ## Mechanical enforcement of PURITY.md
./scripts/check_purity.sh
shellcheck: ## Every shell script in the tree
git ls-files '*.sh' | xargs shellcheck
format: ## Formatting, every language. Rust at rust-toolchain.toml's version
cargo fmt --all --check
find rom \( -name '*.c' -o -name '*.h' \) -exec clang-format --dry-run --Werror {} +
clippy: ## Rust lints. --all-targets so the test files are covered too
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
# The driver's `window` feature pulls in a window system. Both shapes are
# built, because a host with no window system builds the other one.
cargo clippy --locked -p clickdoom-driver --no-default-features --all-targets \
--features clickhouse-tests -- -D warnings
check-bare: ## The workspace with no features, which DEVELOPING.md says works without a container
# clippy runs --all-features, so nothing else builds this shape. The live
# suites compile to nothing here and report zero tests rather than being
# skipped, which is the part the sentence promises.
cargo test --locked --workspace
typos: ## Spelling, over prose and identifiers. _typos.toml holds the exceptions
cargo install --locked --quiet typos-cli@1.50.0
typos --config _typos.toml
actionlint: ## Workflow syntax. Has no CI job, so run it before pushing a workflow change
actionlint .github/workflows/*.yml
zizmor: ## Workflow security posture
cargo install --locked --quiet zizmor@1.28.0
zizmor --persona=regular .github/
##@ Gates
#
# Prerequisites in cost order. `lint` needs no container and no ROM, and
# `check-rom-hash` builds the ROM that `test` and `smoke` both require. Make stops at the first one that fails.
#
# `check-rom-hash` names every goal in one `make -C rom` rather than depending
# on `build-rom` and recursing twice. rom/Makefile's binary depends on the
# phony `toolchain-image`, so it is rebuilt on every entry, and two entries
# compile the ROM twice.
gates: lint check-rom-hash test smoke native-smoke ## Every check ci.yml runs on a pull request
check-rom-hash: ## The built ROM matches rom/PINNED_HASH, the ELF rebuilds byte-identically, and the committed layout matches the headers
make -C rom all check-pinned-hash check-elf-reproducible check-layout