# HEAP is overridable so a smaller machine can still build:
#   make test HEAP=3072
HEAP ?= 4096
SBCL := sbcl --dynamic-space-size $(HEAP) --noinform --disable-debugger

# GPU work needs the driver from a guix shell.  If a render comes back
# black, verify with `nvidia-smi` *inside* this shell before suspecting
# the renderer — see src/render/preload.lisp and docs/concept.md §5.8.
GPU := guix shell nvda@580 --

# Software rendering, for machines with no GPU.  Mesa's llvmpipe gives a
# real 4.5 core context, so the render suite runs in full rather than
# skipping.  It is slow, which does not matter for a handful of small
# frames.
#
# The preload searches $GUIX_ENVIRONMENT/lib first, so entering a mesa
# profile is by itself enough to win over an NVIDIA system profile;
# LIBGL_ALWAYS_SOFTWARE then keeps Mesa off any hardware path.
MESA := guix shell mesa -- env LIBGL_ALWAYS_SOFTWARE=1

# The live window needs GLFW *and* the driver in one profile, and it needs
# LD_LIBRARY_PATH set INSIDE that profile — $GUIX_ENVIRONMENT does not
# exist until the shell has been entered, so setting it on the outside
# silently expands to nothing.  Hence `sh -c` rather than `env`.
WIN := guix shell glfw nvda@580 --

# The PNG writer emits *stored* deflate blocks — a valid zlib stream that
# needs no compressor, which is the right trade for a file a test writes
# and reads back, and the wrong one for a gallery: a 1600x1200 frame comes
# to 5.8 MB of essentially raw RGB.  So the gallery renders large and is
# then converted, and only the JPEGs are committed.
#
# Note the binary is `convert`, not `magick`: this is ImageMagick 6.
IM := guix shell imagemagick --
JPEG_QUALITY ?= 90

# Audio needs no wrapper on this box: libpulse-simple.so.0 is in the
# system profile.  If that ever stops being true, the recipe is the same
# trap as the GL one — LD_LIBRARY_PATH must be set *inside* the shell,
# because $GUIX_ENVIRONMENT does not exist until it has been entered:
#   guix shell pulseaudio -- sh -c 'LD_LIBRARY_PATH=$$GUIX_ENVIRONMENT/lib ...'

SMOKE_PNG ?= out/m2-frame.png
TONE_WAV  ?= out/m0-tone.wav

# Make the systems findable without symlinking into
# ~/quicklisp/local-projects: a checkout anywhere builds with no setup.
# The trailing ':' tells ASDF to append its default configuration, so
# Quicklisp's own dists still resolve.
export CL_SOURCE_REGISTRY := $(CURDIR):

.PHONY: all test test-audio test-render test-render-mesa test-render-ci \
        test-render-bare test-all test-app test-app-bare \
        smoke smoke-mesa listen sound-gallery \
        live gallery repl clean \
        binary binary-bare appimage appimage-bare icon dist-clean

all: test

## test — the core suite: util and the maths of §4.2.  No GPU, no sound.
test:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/test::cl-piston)) 0 1))'

## test-audio — WAV round trip and spectra (§7.7).  Renders sound to
## files and measures them; no sound card is opened, so this runs in CI.
test-audio:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/audio-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/audio-test::audio)) 0 1))'

## test-app — the shipped binary's command line: argv, usage, exit codes.
## Needs GLFW on the loader path, because cl-glfw3 opens it when it is
## *loaded* — but opens no window and needs no GPU.
test-app:
	$(WIN) sh -c 'LD_LIBRARY_PATH=$$GUIX_ENVIRONMENT/lib exec $(SBCL) \
	  --non-interactive \
	  --eval "(ql:quickload :cl-piston/app-test :silent t)" \
	  --eval "(uiop:quit (if (fiveam:run! (quote cl-piston/app-test::app)) 0 1))"'

## test-app-bare — the same, with no guix shell.  What CI runs, where
## libglfw3 is an ordinary system package.
test-app-bare:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/app-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/app-test::app)) 0 1))'

## test-render — renderer suite under the GPU shell.  This is the one
## that actually verifies rendering on the hardware.
test-render:
	$(GPU) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/render-test::render)) 0 1))'

## test-render-mesa — the same suite in software on llvmpipe.  Needs no
## GPU and skips nothing, so this is what a test environment should run.
test-render-mesa:
	$(MESA) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/render-test::render)) 0 1))'

## test-render-ci — the software path *inside a container*, where Mesa is
## installed by the image's package manager and there is no guix at all.
## Deliberately not an alias for test-render-mesa: that target wraps itself
## in `guix shell`, which does not exist on a CI runner, so the alias would
## fail before it ever reached a shader.
##
## CL_PISTON_REQUIRE_GL turns "no GL context" from a skip into a failure.
## Without it a container missing its Mesa EGL driver produces a green run
## that verified no rendering whatsoever — and a suite that silently skips
## its GL tests is worse than one that is slow.
##
## EGL_PLATFORM=surfaceless is what lets eglGetDisplay succeed with no
## display server of any kind; GALLIUM_DRIVER pins llvmpipe so the result
## does not depend on what else the image happens to ship.
test-render-ci:
	env LIBGL_ALWAYS_SOFTWARE=1 EGL_PLATFORM=surfaceless \
	    GALLIUM_DRIVER=llvmpipe CL_PISTON_REQUIRE_GL=1 \
	  $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/render-test::render)) 0 1))'

## test-render-bare — no wrapper at all: whatever GL the host happens to
## have.  GL tests SKIP if it has none, so a green run here proves only
## the PNG writer and the crank-slider.  The suite prints the backend it
## used; read that line before reading the result.
test-render-bare:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render-test :silent t)' \
	  --eval '(uiop:quit (if (fiveam:run! (quote cl-piston/render-test::render)) 0 1))'

## test-all — everything that does not need a human: core, audio, and
## the renderer in software.
test-all: test test-audio test-render-mesa

## smoke — one headless frame of the real engine, end to end.  M0's
## sketch is gone; this draws render/mesh.lisp's parts through the same
## path the window uses.
smoke:
	$(GPU) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render :silent t)' \
	  --eval '(pist:headless-frame :path #p"$(SMOKE_PNG)")'

## smoke-mesa — the same frame in software, for comparing the two stacks.
smoke-mesa:
	$(MESA) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render :silent t)' \
	  --eval '(pist:headless-frame :path #p"out/m2-smoke-mesa.png")'

## listen — run the engine and play it.  **This one makes a noise**, which
## is the point: it is the only target that needs a human, and everything
## else about the audio path is checked by `make test-audio` against files.
##   RPM=1800 THROTTLE=0.06 make listen   an idle, misfiring
##   RPM=7000 THROTTLE=0.95 make listen   wide open
RPM ?= 6000
THROTTLE ?= 0.9
SECONDS ?= 6.0
listen:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/audio :silent t)' \
	  --eval '(pist:listen-to-engine :rpm $(RPM) :throttle $(THROTTLE) :seconds $(SECONDS))'

## sound-gallery — record the engine at a range of conditions and write
## WAVs, opening no sound card at all (§7.7).  The audio equivalent of
## `make gallery`, and what the spectral tests measure.
sound-gallery:
	$(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/audio :silent t)' \
	  --eval '(pist:render-sound-gallery)'

## live — the interactive window (§8).  The window lists its keys on
## stdout when it opens; `h` toggles that.
##   left drag orbit · right drag pan · wheel zoom
##   up/down drive/brake · space pause · +/- time compression
##   s section on/off · , / . move the plane · r reset · q quit
##
## There is no combustion until M3, so the engine only turns while you
## drive it — and the coast-down afterwards is §3.9's friction model with
## the flywheel visibly doing its job.
## ENGINE picks one of EXAMPLE-ENGINES by displacement, so
##   make live ENGINE=125
## opens the window on the 125 instead of the 50.  A number rather than a
## name because it has to survive the shell without quoting.
ENGINE ?=
LIVE_ARGS = $(if $(ENGINE),:engine (pist:example-engine $(ENGINE)),)

live:
	$(WIN) sh -c 'LD_LIBRARY_PATH=$$GUIX_ENVIRONMENT/lib exec $(SBCL) \
	  --eval "(ql:quickload :cl-piston/live :silent t)" \
	  --eval "(pist:live-demo $(LIVE_ARGS))" \
	  --quit'

## profile — where a frame goes.
##
## Two measurements: wall clock around each stage of the live loop, which
## is the one that includes the GPU and therefore the one that decides
## what is worth optimising; and sb-sprof over the whole loop, which says
## which *Lisp* functions the CPU time went to and is blind to the GPU.
##
## Overridable, because the answer depends on all of them:
##   make profile FRAMES=600 RPM=7000 QUALITY=fine
##   make profile SOUND=0            without §7.4's 96 kHz step floor
##   make profile WIDTH=1920 HEIGHT=1080
profile:
	$(GPU) $(SBCL) --non-interactive --load scripts/profile.lisp

## gif — the README's teaser: one revolution of a running engine in slow
## motion, from the model like every other picture here.  Sampled at equal
## crank angles rather than equal times, so the power stroke does not
## linger; see RENDER-ANIMATION.
##
## OptimizeTransparency leaves unchanged pixels alone between frames, which
## on a static camera is most of them.  256 colours rather than fewer
## because the background is a dark gradient and it is the first thing to
## band.
gif:
	$(GPU) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render :silent t)' \
	  --eval '(pist:render-animation)'
	$(IM) sh -c 'convert -delay 6 -loop 0 out/animation/frame-*.png \
	  -layers OptimizeTransparency -colors 256 docs/images/00-running.gif'
	@rm -f out/animation/frame-*.png
	@ls -l docs/images/00-running.gif

## gallery — regenerate the documentation's images from the model.  Every
## picture in the docs comes from here rather than from a screenshot, so
## it cannot drift away from what the engine actually is.
##
## Rendered large and then converted: JPEG at 4:4:4, because these are
## synthetic frames with hard edges between flat areas and chroma
## subsampling puts coloured fringes on exactly the port outlines the
## pictures exist to show.
gallery:
	$(GPU) $(SBCL) --non-interactive \
	  --eval '(ql:quickload :cl-piston/render :silent t)' \
	  --eval '(pist:render-poster :width 1140 :height 1500)' \
	  --eval '(pist:render-gallery :width 1600 :height 1200)' \
	  --eval '(pist:render-engine-family)'
	$(IM) sh -c 'for f in docs/images/*.png; do \
	  convert "$$f" -quality $(JPEG_QUALITY) -sampling-factor 1x1 -strip \
	          "$${f%.png}.jpg" || exit 1; done'
	rm -f docs/images/*.png
	@ls -la docs/images/

## --------------------------------------------------------------------
## Shipping (docs/shipping.md)
## --------------------------------------------------------------------
##
## `binary` saves an executable; `appimage` wraps it for Linux.  Neither
## is part of `make all` — a release is a deliberate act and not something
## every build should be doing.

BINARY ?= out/cl-piston

## binary — save out/cl-piston (out/cl-piston.exe on Windows).
##
## Under the same guix shell the live window uses, so GLFW is findable
## while the image is being built.  The image is unhooked from this
## profile before it is saved, so the binary it produces is not tied to
## it — see the long comment in scripts/build-binary.lisp.
##
##   CL_PISTON_COMPRESS=1 make binary    smaller core, slower start
##   CL_PISTON_VERSION=1.0.0-rc1 make binary
binary:
	$(WIN) sh -c 'LD_LIBRARY_PATH=$$GUIX_ENVIRONMENT/lib exec \
	  sbcl --dynamic-space-size $(HEAP) \
	    --script scripts/build-binary.lisp $(BINARY)'

## binary-bare — the same save with no guix shell around it, for a
## distribution where GLFW is an ordinary system package.  This is what
## CI runs.
binary-bare:
	sbcl --dynamic-space-size $(HEAP) \
	  --script scripts/build-binary.lisp $(BINARY)

## appimage — dist/cl-piston-<version>-x86_64.AppImage.
##
## Builds the binary first.  Note that an AppImage built *here* is built
## against this machine's glibc and this machine's ELF interpreter, and
## the script warns when the latter is not a standard path.  Release
## builds come from CI, on the oldest Ubuntu supported: glibc is forward-
## compatible and not backward-compatible, so a binary runs on a newer one
## than it was built against and never on an older one.
appimage: binary
	packaging/build-appimage.sh

appimage-bare: binary-bare
	packaging/build-appimage.sh

## icon — packaging/cl-piston.png, the desktop icon, rendered from the
## model like every other picture here.  Committed, because packaging must
## not need a GPU; regenerate it when the engine's proportions change.
icon:
	$(GPU) sbcl --dynamic-space-size $(HEAP) \
	  --script scripts/build-icon.lisp

dist-clean:
	rm -rf dist

repl:
	sbcl --dynamic-space-size $(HEAP) \
	  --eval '(ql:quickload :cl-piston/render)' \
	  --eval '(ql:quickload :cl-piston/audio)' \
	  --eval '(in-package :cl-piston)'

clean:
	find . -name '*.fasl' -delete
	rm -rf out
