|
9 | 9 | env: |
10 | 10 | # The mcpp release the consumers build with. Raising it is what admits a |
11 | 11 | # member that relies on a newer engine; the README states each member's floor. |
12 | | - MCPP_VERSION: 2026.9.7.1 |
| 12 | + MCPP_VERSION: 2026.9.8.1 |
13 | 13 | # PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING. |
14 | 14 | # |
15 | 15 | # A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env` |
@@ -472,6 +472,157 @@ jobs: |
472 | 472 | || { echo "FAIL: it failed for some other reason"; cat /tmp/away.log; exit 1; } |
473 | 473 | echo "ok: found from the package root, reported missing from elsewhere" |
474 | 474 |
|
| 475 | + # AN EDITED SHADER REACHES THE ARTIFACT, UNDER EVERY STORAGE. |
| 476 | + # |
| 477 | + # The three storage steps above assert STRUCTURE: the `.S` names the |
| 478 | + # payload, the section is aligned, no C initialiser was written. All three |
| 479 | + # passed against 0.3.0, in which object storage served a STALE artifact -- |
| 480 | + # the shader recompiled, the object did not, and the program reported the |
| 481 | + # previous payload's byte count with a green build. Structure cannot see |
| 482 | + # that. Only editing a shader and reading what comes out can. |
| 483 | + # |
| 484 | + # THE DENOMINATOR IS THE POINT. Header storage was already correct, so a |
| 485 | + # run where only `object` fails names the mechanism, and a run where all |
| 486 | + # three fail names the harness -- a distinction one fixture cannot make. |
| 487 | + # |
| 488 | + # The edit adds a statement guarded by a value the host never passes, so |
| 489 | + # the compiled module must grow while the program's output stays |
| 490 | + # comparable. |
| 491 | + - name: an edited shader reaches the artifact under every storage |
| 492 | + run: | |
| 493 | + set -u |
| 494 | + fail=0 |
| 495 | + for fx in spirv-module-consumer:header spirv-object-storage:object spirv-sidecar:sidecar; do |
| 496 | + dir=${fx%%:*}; store=${fx##*:} |
| 497 | + cd "$GITHUB_WORKSPACE/tests/$dir" |
| 498 | + shader=$(find shaders -name '*.comp' | sort | head -1) |
| 499 | + cp "$shader" /tmp/shader.bak |
| 500 | + # `|| { ...; continue; }` and not a bare command: a `run:` block |
| 501 | + # runs under `bash -e`, so a bare failing build would end the STEP |
| 502 | + # here and the other two storages would never run -- losing the |
| 503 | + # denominator this check is built around. |
| 504 | + "$MCPP" build > /dev/null \ |
| 505 | + || { echo "FAIL[$store]: build"; fail=1; cp /tmp/shader.bak "$shader"; continue; } |
| 506 | + before=$("$MCPP" run 2>&1 | sed -n 's/.*bytes=\([0-9]*\).*/\1/p' | awk '{s+=$1} END {print s+0}') |
| 507 | + sed -i 's|void main() {|void main() {\n if (push.n == 0xdeadu) { v[0] = 1.0; v[1] = 2.0; v[2] = 3.0; }|' "$shader" |
| 508 | + "$MCPP" build > /dev/null \ |
| 509 | + || { echo "FAIL[$store]: rebuild"; fail=1; cp /tmp/shader.bak "$shader"; continue; } |
| 510 | + after=$("$MCPP" run 2>&1 | sed -n 's/.*bytes=\([0-9]*\).*/\1/p' | awk '{s+=$1} END {print s+0}') |
| 511 | + cp /tmp/shader.bak "$shader" |
| 512 | + if [ -z "$before" ] || [ -z "$after" ]; then |
| 513 | + echo "FAIL[$store]: no byte count printed (before='$before' after='$after')" |
| 514 | + fail=1 |
| 515 | + elif [ "$before" = "$after" ]; then |
| 516 | + echo "FAIL[$store]: the shader changed and the artifact did not (bytes=$before both times)" |
| 517 | + fail=1 |
| 518 | + else |
| 519 | + echo "ok[$store]: $before -> $after" |
| 520 | + fi |
| 521 | + done |
| 522 | + [ "$fail" -eq 0 ] || exit 1 |
| 523 | +
|
| 524 | + # AN EDITED `#include` REACHES THE ARTIFACT. |
| 525 | + # |
| 526 | + # The step above edits the shader itself, which every rule declares as an |
| 527 | + # action input. This edits a file the shader INCLUDES, which no rule |
| 528 | + # declares: `a.input()` is fixed when build.mcpp runs, before the compiler |
| 529 | + # has read a line, so the only channel is the depfile the compiler writes |
| 530 | + # afterwards. `grep depfile rules/` returned nothing across all six rules |
| 531 | + # until 0.3.1, and editing an included `.glsl` rebuilt nothing. |
| 532 | + # |
| 533 | + # ON THE HEADER-STORAGE FIXTURE, AND THAT IS NOT ARBITRARY. Run against |
| 534 | + # `spirv-object-storage` this step tests two mechanisms at once: measured, |
| 535 | + # it fails when EITHER the depfile or the `.incbin` declaration is absent, |
| 536 | + # so a red run would not say which. Header storage compiles the payload in |
| 537 | + # as generated source, where the compile edge is dirty for the ordinary |
| 538 | + # reason, and what remains under test is the depfile alone. |
| 539 | + # |
| 540 | + # Both shader compilers were measured writing one that names the include: |
| 541 | + # glslangValidator --depfile -> out.spv: scale.comp ./common.glsl |
| 542 | + # glslc -MD -MF -> out2.spv: scale.comp common.glsl |
| 543 | + - name: an edited include reaches the artifact |
| 544 | + working-directory: tests/spirv-module-consumer |
| 545 | + run: | |
| 546 | + set -u |
| 547 | + shader=shaders/default/scale.comp |
| 548 | + inc=shaders/default/common.glsl |
| 549 | + cp "$shader" /tmp/scale.bak |
| 550 | + printf 'const float kBias = 0.0;\n' > "$inc" |
| 551 | + sed -i 's|#version 450|#version 450\n#extension GL_GOOGLE_include_directive : require\n#include "common.glsl"|' "$shader" |
| 552 | + sed -i 's|+ v\[push.n + i\];|+ v[push.n + i] + kBias;|' "$shader" |
| 553 | + "$MCPP" build > /dev/null |
| 554 | + before=$("$MCPP" run 2>&1 | sed -n 's/.*bytes=\([0-9]*\).*/\1/p' | awk '{s+=$1} END {print s+0}') |
| 555 | + # ONLY THE INCLUDED FILE CHANGES NOW. Nothing any rule declared as an |
| 556 | + # input is touched, so a rebuild happens only if the depfile said so. |
| 557 | + printf 'const float kBias = 1.0;\nconst float kPad0 = 2.0;\nconst float kPad1 = 3.0;\n' > "$inc" |
| 558 | + "$MCPP" build > /dev/null |
| 559 | + after=$("$MCPP" run 2>&1 | sed -n 's/.*bytes=\([0-9]*\).*/\1/p' | awk '{s+=$1} END {print s+0}') |
| 560 | + cp /tmp/scale.bak "$shader" |
| 561 | + rm -f "$inc" |
| 562 | + if [ -z "$before" ] || [ -z "$after" ]; then |
| 563 | + echo "FAIL: no byte count printed (before='$before' after='$after')" |
| 564 | + exit 1 |
| 565 | + fi |
| 566 | + if [ "$before" = "$after" ]; then |
| 567 | + echo "FAIL: the included file changed and the artifact did not (bytes=$before both times)" |
| 568 | + exit 1 |
| 569 | + fi |
| 570 | + echo "ok: an included file reaches the artifact ($before -> $after)" |
| 571 | +
|
| 572 | + # OBJECT STORAGE WITHOUT THE TOOL IS REFUSED, AND THE REFUSAL SAYS WHAT |
| 573 | + # TO ADD. |
| 574 | + # |
| 575 | + # `tools = [...]` is default-off: nothing is built unless a consumer asks, |
| 576 | + # which is what keeps the DEFAULT storage free of a tool build. The cost |
| 577 | + # is that a project can ask for object storage and not for the tool, and |
| 578 | + # what it gets then is a message -- so the message is a contract. Asserted |
| 579 | + # on the three strings a reader needs: that the tool is what is missing, |
| 580 | + # the key that supplies it, and the storage that needs none. |
| 581 | + - name: object storage without the tool is refused, naming the fix |
| 582 | + run: | |
| 583 | + set -u |
| 584 | + W="$GITHUB_WORKSPACE/tests/spirv-object-storage" |
| 585 | + cp "$W/mcpp.toml" /tmp/objstore.toml |
| 586 | + # Remove only the tools request, leaving everything else in place. |
| 587 | + sed -i 's/, tools = \["mcpp-embed"\]//' "$W/mcpp.toml" |
| 588 | + cd "$W" |
| 589 | + rm -rf target |
| 590 | + if "$MCPP" build > refusal.log 2>&1; then |
| 591 | + cp /tmp/objstore.toml mcpp.toml |
| 592 | + echo "FAIL: object storage built with no way to generate its assembly" |
| 593 | + exit 1 |
| 594 | + fi |
| 595 | + cp /tmp/objstore.toml mcpp.toml |
| 596 | + fail=0 |
| 597 | + grep -q 'mcpp-embed' refusal.log || { echo "FAIL: the refusal does not name the tool"; fail=1; } |
| 598 | + grep -q 'tools = ' refusal.log || { echo "FAIL: the refusal does not name the key that supplies it"; fail=1; } |
| 599 | + grep -q 'storage::header' refusal.log || { echo "FAIL: the refusal does not name the way out"; fail=1; } |
| 600 | + if [ "$fail" -ne 0 ]; then cat refusal.log; exit 1; fi |
| 601 | + rm -f refusal.log |
| 602 | + echo "ok: refused, naming the tool, the key and the alternative" |
| 603 | +
|
| 604 | + # EVERY RULE PASSES A DEPFILE, AND THE DENOMINATOR IS THE RULE COUNT. |
| 605 | + # |
| 606 | + # The step above proves the channel works for the one lane CI can run. |
| 607 | + # The other four compile for accelerators no runner has, so what can be |
| 608 | + # asserted about them is that each one declares the field -- read from the |
| 609 | + # code rather than from a list written here, so a seventh rule is counted |
| 610 | + # the day it is added and not the day someone remembers this step. |
| 611 | + - name: every rule declares a depfile |
| 612 | + run: | |
| 613 | + set -u |
| 614 | + cd "$GITHUB_WORKSPACE" |
| 615 | + rules=$(ls rules/*.cppm | wc -l) |
| 616 | + withdep=$(grep -l 'depfile' rules/*.cppm | wc -l) |
| 617 | + echo "rules with a depfile: $withdep of $rules" |
| 618 | + if [ "$withdep" -ne "$rules" ]; then |
| 619 | + echo "FAIL: these rules pass no depfile:" |
| 620 | + for f in rules/*.cppm; do |
| 621 | + grep -q 'depfile' "$f" || echo " $f" |
| 622 | + done |
| 623 | + exit 1 |
| 624 | + fi |
| 625 | +
|
475 | 626 | # SLANG: A DIFFERENT LANGUAGE, THE SAME SURFACE. |
476 | 627 | # |
477 | 628 | # The point of this step is not that Slang compiles -- it is that a |
@@ -601,8 +752,25 @@ jobs: |
601 | 752 | # `<name> = { ... }`. Reading keys out of the table BODY matched |
602 | 753 | # `sources` once per feature and reported the fixture as |
603 | 754 | # incomplete while the extractor was what broke. |
604 | | - feats=$(grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \ |
605 | | - | sed 's/^\[features\.//; s/\]$//' | sort) |
| 755 | + # MEMBERS, NOT EVERY FEATURE. A feature that another feature |
| 756 | + # IMPLIES is internal -- `surface` carries the build-program half of |
| 757 | + # the generated surface and every member implies it, and a consumer |
| 758 | + # never writes it. Subtracting the implied ones keeps this check |
| 759 | + # meaning "the fixture names every member a consumer can activate"; |
| 760 | + # padding the fixture instead would make it mean less. |
| 761 | + # |
| 762 | + # The rule is read out of the manifest, not listed here, so a seventh |
| 763 | + # MEMBER is still caught: nothing implies it. |
| 764 | + # A TEMP FILE, NOT `<(...)`. This step also runs on windows-2022 |
| 765 | + # through Git Bash, where process substitution is emulated and not |
| 766 | + # dependable -- and a check that behaves differently on one of the |
| 767 | + # three hosts is the exact class of difference this job exists to |
| 768 | + # catch, so it must not introduce one. |
| 769 | + grep -oE '^implies[[:space:]]*=.*' mcpp.toml \ |
| 770 | + | grep -oE '"[a-z0-9-]+"' | tr -d '"' | sort -u > /tmp/implied.txt |
| 771 | + grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \ |
| 772 | + | sed 's/^\[features\.//; s/\]$//' | sort > /tmp/allfeats.txt |
| 773 | + feats=$(comm -23 /tmp/allfeats.txt /tmp/implied.txt) |
606 | 774 | used=$(sed -n '/features = \[/,/\], host-module/p' tests/all-rules-compile/mcpp.toml \ |
607 | 775 | | grep -oE '"[a-z-]+"' | tr -d '"' | sort) |
608 | 776 | [ -n "$feats" ] || { |
@@ -898,8 +1066,23 @@ jobs: |
898 | 1066 | # `<name> = { ... }`. Reading keys out of the table BODY matched |
899 | 1067 | # `sources` once per feature and reported the fixture as |
900 | 1068 | # incomplete while the extractor was what broke. |
901 | | - feats=$(grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \ |
902 | | - | sed 's/^\[features\.//; s/\]$//' | sort) |
| 1069 | + # MEMBERS, NOT EVERY FEATURE. A feature that another feature |
| 1070 | + # IMPLIES is internal -- `surface` carries the build-program half of |
| 1071 | + # the generated surface and every member implies it, and a consumer |
| 1072 | + # never writes it. Subtracting the implied ones keeps this check |
| 1073 | + # meaning "the fixture names every member a consumer can activate"; |
| 1074 | + # padding the fixture instead would make it mean less. The rule is |
| 1075 | + # read out of the manifest, so a seventh MEMBER is still caught. |
| 1076 | + # |
| 1077 | + # A temp file, not `<(...)`: this step also runs on windows-2022 |
| 1078 | + # through Git Bash, where process substitution is emulated and not |
| 1079 | + # dependable -- and a check that behaves differently on one of the |
| 1080 | + # three hosts is the class of difference this job exists to catch. |
| 1081 | + grep -oE '^implies[[:space:]]*=.*' mcpp.toml \ |
| 1082 | + | grep -oE '"[a-z0-9-]+"' | tr -d '"' | sort -u > /tmp/implied.txt |
| 1083 | + grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \ |
| 1084 | + | sed 's/^\[features\.//; s/\]$//' | sort > /tmp/allfeats.txt |
| 1085 | + feats=$(comm -23 /tmp/allfeats.txt /tmp/implied.txt) |
903 | 1086 | used=$(sed -n '/features = \[/,/\], host-module/p' tests/all-rules-compile/mcpp.toml \ |
904 | 1087 | | grep -oE '"[a-z-]+"' | tr -d '"' | sort) |
905 | 1088 | [ -n "$feats" ] || { |
|