diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2d0ac19 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,116 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + # A commit to main gets a group of its own. Sharing one group would let a + # third push cancel the second while it waits, and that commit would ship + # no release without anything failing. Pull requests still collapse. + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + examples: + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + # From source, the way the SDK tells everyone to install it. bin/cl65 + # finds its own ../target, so there is nothing to install afterwards. + - name: Build cc65 + run: | + set -euo pipefail + git clone --depth 1 https://github.com/cc65/cc65 "$RUNNER_TEMP/cc65" + make -C "$RUNNER_TEMP/cc65" -j"$(nproc)" + echo "$RUNNER_TEMP/cc65/bin" >> "$GITHUB_PATH" + + # rp6502_xram() runs a program on the emulator while CMake configures. + # These two variables skip the tool download, so the examples build + # against the tools this repository holds rather than the newest ones. + - name: Fetch the emulator + run: | + set -euo pipefail + cmake -DRP6502_TOOLS_RELOADED=TRUE -DRP6502_TOOLS_FETCHED=TRUE \ + -P tools/rp6502.cmake + # A download that fails is a notice rather than an error, and the + # build would go on to report a header that is not at fault. + test -x tools/rp6502-emu + + - name: Build every example + run: | + set -euo pipefail + cmake --preset cc65/Release + cmake --build --preset cc65/Release + + - name: Collect the ROMs + id: pack + run: | + set -euo pipefail + sha=$(git rev-parse --short HEAD) + # A release is named for the commit it ships. A pull request ships + # nothing, and its HEAD is a merge commit nobody can look up, so it + # is named for the run instead. + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + name="rp6502-${sha}-examples" + else + name="rp6502-${GITHUB_RUN_ID}-examples" + fi + mkdir "$name" + cp src/README.txt "$name/" + # Only the target ROMs. CMakeFiles holds directories with the same + # suffix, and the program rp6502_xram() runs while CMake configures + # is a .rp6502 of its own. + find build/cc65/release/src -mindepth 2 -maxdepth 2 -type f \ + -name '*.rp6502' -exec cp -t "$name" {} + + # One ROM per example, so a target that quietly stopped producing + # one fails the release rather than leaving a gap in it. + want=$(grep -c '^add_subdirectory(src/' CMakeLists.txt) + got=$(find "$name" -name '*.rp6502' | wc -l) + if [ "$got" -ne "$want" ]; then + echo "packed ${got} ROMs, expected ${want}" >&2 + exit 1 + fi + ls -l "$name" + # Zipped from inside, so the ROMs are at the root of the archive + # rather than one folder down. + ( cd "$name" && cmake -E tar cf "../${name}.zip" --format=zip . ) + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + echo "name=${name}" >> "$GITHUB_OUTPUT" + + # Only where nothing is released. The name carries .zip because that is + # the filename the download is served with, and the directory is what + # goes in it, so the one archive holds the ROMs and nothing else. + - uses: actions/upload-artifact@v7 + if: github.ref != 'refs/heads/main' + with: + name: ${{ steps.pack.outputs.name }}.zip + path: ${{ steps.pack.outputs.name }}/ + + - name: Publish the release + if: github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v3 + with: + tag_name: build-${{ steps.pack.outputs.sha }} + # Without this the tag is created on whatever the default branch + # points at when the release is published, not on what was built. + target_commitish: ${{ github.sha }} + name: Examples ${{ steps.pack.outputs.sha }} + files: ${{ steps.pack.outputs.name }}.zip + fail_on_unmatched_files: true + body: | + Example ROMs for the Picocomputer 6502, built with cc65. + + Unzip and run a `.rp6502` in the emulator, or load one on a + Picocomputer. + + https://picocomputer.github.io diff --git a/src/README.txt b/src/README.txt new file mode 100644 index 0000000..4440bef --- /dev/null +++ b/src/README.txt @@ -0,0 +1,7 @@ +RP6502 example ROMs + +Each .rp6502 file is one program for the Picocomputer 6502. Run it in the +emulator, or load it on a Picocomputer from the monitor. + +Documentation https://picocomputer.github.io +Source https://github.com/picocomputer/examples diff --git a/src/paint/paint.c b/src/paint/paint.c index a5eaf0d..0f2ee04 100644 --- a/src/paint/paint.c +++ b/src/paint/paint.c @@ -70,8 +70,8 @@ static void setup_bitmap(unsigned config, int width, int height, unsigned data) // The point of the arrow is one pixel in from the corner of its image. static void move_pointer(int x, int y) { - xram0_struct_set(XRAM_POINTER_CONFIG, mode3_config_t, x_pos_px, x - 1); - xram0_struct_set(XRAM_POINTER_CONFIG, mode3_config_t, y_pos_px, y - 1); + xram0_struct_set(XRAM_CONFIG_POINTER, mode3_config_t, x_pos_px, x - 1); + xram0_struct_set(XRAM_CONFIG_POINTER, mode3_config_t, y_pos_px, y - 1); } static void draw_pointer(void) @@ -86,7 +86,7 @@ static void draw_pointer(void) }; // clang-format on unsigned i; - RIA.addr0 = XRAM_POINTER_DATA; + RIA.addr0 = XRAM_DATA_POINTER; RIA.step0 = 1; for (i = 0; i < sizeof(image); i++) RIA.rw0 = image[i]; @@ -113,7 +113,7 @@ static void mouse_sample(void) VIA.ifr = 0x40; // acknowledge timer 1 - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, x); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, x); RIA.step0 = 1; count = RIA.rw0; raw_x += (int8_t)(count - mouse_last_x); @@ -155,8 +155,8 @@ static void mouse_init(void) // in 8 ms. unsigned period = ria_attr_get(RIA_ATTR_PHI2_KHZ) * 8 - 2; - xreg_ria_mouse(XRAM_MOU_DATA); - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, x); + xreg_ria_mouse(XRAM_DATA_MOU); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, x); RIA.step0 = 1; mouse_last_x = RIA.rw0; mouse_last_y = RIA.rw0; @@ -183,7 +183,7 @@ static uint8_t mouse_read(int *x, int *y) *x = mouse_x; *y = mouse_y; CLI(); - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, buttons); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, buttons); return RIA.rw0 & (MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT); } @@ -195,16 +195,12 @@ static uint8_t mouse_read(int *x, int *y) // the host can draw a cursor, as the emulator can for a mouse, the program // hides its own pointer and asks for a crosshair. -#define TABLET_CONTROL (XRAM_TAB_DATA + offsetof(tablet_t, control)) -#define TABLET_STATUS (XRAM_TAB_DATA + offsetof(tablet_t, status)) -#define TABLET_CONTACT (XRAM_TAB_DATA + offsetof(tablet_t, contact)) - static int tablet_x, tablet_y; static bool host_cursor; static void tablet_init(void) { - xreg_ria_tablet(XRAM_TAB_DATA); + xreg_ria_tablet(XRAM_DATA_TAB); } static uint8_t tablet_read(int *x, int *y) @@ -213,12 +209,12 @@ static uint8_t tablet_read(int *x, int *y) bool offered; int tries; - RIA.addr0 = TABLET_STATUS; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, status); offered = RIA.rw0 & TABLET_STATUS_HOST_CURSOR; if (offered != host_cursor) { host_cursor = offered; - RIA.addr0 = TABLET_CONTROL; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, control); RIA.rw0 = host_cursor ? TABLET_CURSOR_CROSSHAIR : TABLET_CURSOR_OFF; } @@ -226,7 +222,7 @@ static uint8_t tablet_read(int *x, int *y) // every window zero, so the contact is read a second time. for (tries = 0; tries < 2; tries++) { - RIA.addr0 = TABLET_CONTACT; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, contact); RIA.step0 = 1; flags = RIA.rw0; x0 = RIA.rw0; @@ -266,7 +262,7 @@ static uint8_t tablet_read(int *x, int *y) static void erase_picture(void) { unsigned i; - RIA.addr0 = XRAM_PICTURE_DATA; + RIA.addr0 = XRAM_DATA_PICTURE; RIA.step0 = 1; for (i = 0; i < CANVAS_WIDTH / 2 * (unsigned)CANVAS_HEIGHT; i++) RIA.rw0 = 0; @@ -276,7 +272,7 @@ static void erase_picture(void) // program starts, and this one, a file to put it back. static void load_logo(void) { - unsigned addr = XRAM_PICTURE_DATA; + unsigned addr = XRAM_DATA_PICTURE; int fd = open("ROM:logo", O_RDONLY); int count; while ((count = read_xram(addr, 0x7FFF, fd)) > 0) @@ -289,7 +285,7 @@ static void draw_pixel(int x, int y) { uint8_t pair; RIA.step0 = 0; - RIA.addr0 = XRAM_PICTURE_DATA + (unsigned)y * (CANVAS_WIDTH / 2) + x / 2; + RIA.addr0 = XRAM_DATA_PICTURE + (unsigned)y * (CANVAS_WIDTH / 2) + x / 2; pair = RIA.rw0; if (x & 1) RIA.rw0 = (pair & 0xF0) | draw_color; @@ -337,7 +333,7 @@ static void draw_picker_box(uint8_t shade, int x1, int y1, int x2, int y2) RIA.step0 = 1; for (y = y1; y <= y2; y++) { - RIA.addr0 = XRAM_PICKER_DATA + PICKER_WIDTH * y + x1; + RIA.addr0 = XRAM_DATA_PICKER + PICKER_WIDTH * y + x1; for (x = x1; x <= x2; x++) RIA.rw0 = shade; } @@ -382,8 +378,8 @@ static void move_picker(int x, int y) { picker_x = clamp(x, 0, CANVAS_WIDTH - PICKER_WIDTH); picker_y = clamp(y, 0, CANVAS_HEIGHT - PICKER_HEIGHT); - xram0_struct_set(XRAM_PICKER_CONFIG, mode3_config_t, x_pos_px, picker_x); - xram0_struct_set(XRAM_PICKER_CONFIG, mode3_config_t, y_pos_px, picker_y); + xram0_struct_set(XRAM_CONFIG_PICKER, mode3_config_t, x_pos_px, picker_x); + xram0_struct_set(XRAM_CONFIG_PICKER, mode3_config_t, y_pos_px, picker_y); } static int picker_pick(int x, int y) @@ -470,10 +466,10 @@ int main(int argc, char *argv[]) load_logo(); - xreg_vga_canvas(CANVAS_320X240); - setup_bitmap(XRAM_PICTURE_CONFIG, CANVAS_WIDTH, CANVAS_HEIGHT, XRAM_PICTURE_DATA); - setup_bitmap(XRAM_PICKER_CONFIG, PICKER_WIDTH, PICKER_HEIGHT, XRAM_PICKER_DATA); - setup_bitmap(XRAM_POINTER_CONFIG, POINTER_SIZE, POINTER_SIZE, XRAM_POINTER_DATA); + xreg_vga_canvas(1); // 320x240 + setup_bitmap(XRAM_CONFIG_PICTURE, CANVAS_WIDTH, CANVAS_HEIGHT, XRAM_DATA_PICTURE); + setup_bitmap(XRAM_CONFIG_PICKER, PICKER_WIDTH, PICKER_HEIGHT, XRAM_DATA_PICKER); + setup_bitmap(XRAM_CONFIG_POINTER, POINTER_SIZE, POINTER_SIZE, XRAM_DATA_POINTER); draw_picker(); move_picker((CANVAS_WIDTH - PICKER_WIDTH) / 2, 0); @@ -481,9 +477,9 @@ int main(int argc, char *argv[]) set_color(RIGHT, 0); draw_pointer(); - xreg_vga_mode3(MODE3_4BPP, XRAM_PICTURE_CONFIG, 0); // plane 0 - xreg_vga_mode3(MODE3_8BPP, XRAM_PICKER_CONFIG, 1); // plane 1 - xreg_vga_mode3(MODE3_8BPP, XRAM_POINTER_CONFIG, 2); // plane 2 + xreg_vga_mode3(2, XRAM_CONFIG_PICTURE, 0); // 4 bits per pixel, plane 0 + xreg_vga_mode3(3, XRAM_CONFIG_PICKER, 1); // 8 bits per pixel, plane 1 + xreg_vga_mode3(3, XRAM_CONFIG_POINTER, 2); // 8 bits per pixel, plane 2 if (use_mouse) mouse_init(); diff --git a/src/paint/xram.h b/src/paint/xram.h index 5395edd..eb81934 100644 --- a/src/paint/xram.h +++ b/src/paint/xram.h @@ -115,13 +115,14 @@ typedef struct uint8_t pointer[POINTER_SIZE * POINTER_SIZE]; } xram_layout_t; -#define XRAM_PICTURE_CONFIG offsetof(xram_layout_t, picture_config) -#define XRAM_PICKER_CONFIG offsetof(xram_layout_t, picker_config) -#define XRAM_POINTER_CONFIG offsetof(xram_layout_t, pointer_config) -#define XRAM_TAB_DATA offsetof(xram_layout_t, tab) -#define XRAM_MOU_DATA offsetof(xram_layout_t, mou) -#define XRAM_PICTURE_DATA offsetof(xram_layout_t, picture) -#define XRAM_PICKER_DATA offsetof(xram_layout_t, picker) -#define XRAM_POINTER_DATA offsetof(xram_layout_t, pointer) +#define XRAM_CONFIG_PICTURE offsetof(xram_layout_t, picture_config) +#define XRAM_CONFIG_PICKER offsetof(xram_layout_t, picker_config) +#define XRAM_CONFIG_POINTER offsetof(xram_layout_t, pointer_config) + +#define XRAM_DATA_TAB offsetof(xram_layout_t, tab) +#define XRAM_DATA_MOU offsetof(xram_layout_t, mou) +#define XRAM_DATA_PICTURE offsetof(xram_layout_t, picture) +#define XRAM_DATA_PICKER offsetof(xram_layout_t, picker) +#define XRAM_DATA_POINTER offsetof(xram_layout_t, pointer) #endif diff --git a/tools/rp6502.cmake b/tools/rp6502.cmake index d8d2c3e..2c040ee 100644 --- a/tools/rp6502.cmake +++ b/tools/rp6502.cmake @@ -382,6 +382,12 @@ function(rp6502_executable name) VERBATIM ) add_custom_target(${name}_rp6502 ALL DEPENDS "${rom_file}") + # A layout that fails its checks must not produce a ROM. + get_directory_property(xram_checks RP6502_XRAM_CHECKS) + if (xram_checks) + add_dependencies(${name}_rp6502 ${xram_checks}) + endif() + set_property(DIRECTORY APPEND PROPERTY RP6502_ROM_TARGETS "${name}_rp6502") # Mark that rp6502_executable has been called for this target set_property(TARGET ${name} PROPERTY RP6502_EXECUTABLE_CALLED TRUE) endfunction() @@ -391,23 +397,74 @@ endfunction() # RP6502 Asset ROM # ^^^^^^^^^^^^^^^^ # -# rp6502_asset( address in_file) +# rp6502_asset(
) # # If the address is numeric, the in_file will be loaded into # RAM ($0-FFFF) or XRAM ($10000-1FFFF) when the ROM is loaded. # Non-numeric addresses become filenames that can be opened # with "ROM:filename" from a micro filesystem in the ROM. -# A name defined by rp6502_xram() is the XRAM address it stands for. +# Writing the address as RAM() or XRAM() checks that it is in range, +# and XRAM() sets the bit that tells XRAM from RAM, so an offset from +# rp6502_xram() loads into XRAM. Inside the parentheses, is a number +# or the name of a CMake variable. # -function(rp6502_asset name addr in_file) +function(rp6502_asset name) get_target_property(executable_called ${name} RP6502_EXECUTABLE_CALLED) if (executable_called) message(FATAL_ERROR "rp6502_asset(${name} ...) must be registered BEFORE calling rp6502_executable()." ) endif() - if (DEFINED ${addr}) - set(addr "${${addr}}") + # CMake gives every parenthesis to a command as an argument of its own, + # so RAM() arrives here as four arguments and nothing named RAM or + # XRAM is ever defined. + set(args ${ARGN}) + list(LENGTH args argc) + list(GET args 0 addr) + if (addr STREQUAL "RAM" OR addr STREQUAL "XRAM") + set(form "${addr}") + if (NOT argc EQUAL 5) + message(FATAL_ERROR "rp6502_asset(${name} ${form}(
) )") + endif() + list(GET args 1 opened) + list(GET args 3 closed) + if (NOT opened STREQUAL "(" OR NOT closed STREQUAL ")") + message(FATAL_ERROR "rp6502_asset(${name} ${form}(
) )") + endif() + list(GET args 2 value) + if (DEFINED ${value}) + set(value "${${value}}") + endif() + set(written "${value}") + # A leading $ is how a 6502 program writes hex, which rp6502.py + # takes as well. + string(REGEX REPLACE "^\\$" "0x" value "${value}") + if (NOT value MATCHES "^[-+]?(0[xX][0-9a-fA-F]+|[0-9]+)$") + message(FATAL_ERROR + "rp6502_asset(${name} ${form}(...)): ${written} is not a number.") + endif() + if (form STREQUAL "RAM") + set(limit 65535) + set(ends "0xFFFF") + else() + set(limit 131071) + set(ends "0x1FFFF") + endif() + math(EXPR value "(${value})") + if (value LESS 0 OR value GREATER ${limit}) + message(FATAL_ERROR + "rp6502_asset(${name} ${form}(...)): ${written} is outside ${form}, which ends at ${ends}.") + endif() + if (form STREQUAL "XRAM") + math(EXPR value "${value} | 0x10000") + endif() + math(EXPR addr "${value}" OUTPUT_FORMAT HEXADECIMAL) + list(GET args 4 in_file) + else() + if (NOT argc EQUAL 2) + message(FATAL_ERROR "rp6502_asset(
)") + endif() + list(GET args 1 in_file) endif() get_filename_component(src_file "${in_file}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") file(RELATIVE_PATH rel_path "${CMAKE_SOURCE_DIR}" "${src_file}") @@ -434,148 +491,284 @@ function(rp6502_asset name addr in_file) ) endfunction() -# Give CMake the XRAM addresses a header defines. +# Give CMake the addresses a header defines. # # RP6502 XRAM Layout # ^^^^^^^^^^^^^^^^^^ # # rp6502_xram(
[]) # -# Reads ``#define NAME offsetof(...)`` lines from ``
`` whose NAME -# matches ```` and sets each NAME as a variable holding the XRAM -# address it stands for. The names then work as rp6502_asset() addresses. +# Reads the ``#define`` lines of ``
`` whose name matches +# ```` and sets each name as a variable holding the value the +# header computes. The names then work as rp6502_asset() addresses. # Names matching ```` are exempt from 16-bit alignment. +# A commented out define, a define with no value, and a function-like +# macro are all skipped. +# +# The layout is checked while the project builds rather than while it +# configures, so a header that will not compile still leaves a configured +# project behind, and every problem is reported by the compiler against +# the line in the header. # function(rp6502_xram header regex) set(unaligned "${ARGV2}") get_filename_component(header_file "${header}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") get_filename_component(header_name "${header_file}" NAME) + get_filename_component(header_dir "${header_file}" DIRECTORY) get_filename_component(stem "${header_file}" NAME_WE) + get_directory_property(rom_targets RP6502_ROM_TARGETS) + if (rom_targets) + message(FATAL_ERROR + "rp6502_xram(${header}) must be registered BEFORE calling rp6502_executable()." + ) + endif() # Editing the layout has to configure again, since these values are read # at configure time. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${header_file}") - file(READ "${header_file}" text) - # A definition may continue onto the next line. - string(REGEX REPLACE "\\\\[ \t]*\r?\n" " " text "${text}") - # CMake's ^ is the start of the input, not of a line, so each definition is - # found with the newline ahead of it. - string(REGEX MATCHALL - "\n[ \t]*#[ \t]*define[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]+offsetof[ \t]*\\([^,]+," - defines "\n${text}") + # Read the header a line at a time. file(STRINGS), and every idiom that + # escapes the text and splits it, join a line ending in a backslash to + # the line after it, which moves every line number that follows. + # Nothing here reads comments or conditionals, because each name is + # written out under an #ifdef and the preprocessor settles what exists. + file(READ "${header_file}" rest) + string(REPLACE "\r\n" "\n" rest "${rest}") set(names) - set(types) - foreach(define IN LISTS defines) - string(REGEX MATCH - "define[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+offsetof[ \t]*\\(([^,]+)," - ignored "${define}") - set(name "${CMAKE_MATCH_1}") - string(STRIP "${CMAKE_MATCH_2}" type) - if (name MATCHES "^(${regex})$") - list(APPEND names "${name}") - list(APPEND types "${type}") + set(lines) + set(values) + set(lineno 0) + set(pending "") + set(pending_line 0) + while(TRUE) + string(FIND "${rest}" "\n" pos) + if (pos LESS 0) + set(line "${rest}") + set(rest "") + set(last TRUE) + else() + string(SUBSTRING "${rest}" 0 ${pos} line) + math(EXPR pos "${pos}+1") + string(SUBSTRING "${rest}" ${pos} -1 rest) + set(last FALSE) + endif() + math(EXPR lineno "${lineno}+1") + + # A continued definition is read under the number of its first line. + if (pending STREQUAL "") + set(cur "${line}") + set(cur_line ${lineno}) + else() + set(cur "${pending}${line}") + set(cur_line ${pending_line}) + endif() + if (cur MATCHES "\\\\$") + string(REGEX REPLACE "\\\\$" " " pending "${cur}") + set(pending_line ${cur_line}) + else() + set(pending "") + if (cur MATCHES "^[ \t]*#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)([^A-Za-z0-9_(].*)$") + set(name "${CMAKE_MATCH_1}") + string(STRIP "${CMAKE_MATCH_2}" value) + # An include guard has no value and only integers are carried. + if (NOT value STREQUAL "" AND NOT value MATCHES "^[\"']" + AND name MATCHES "^(${regex})$") + list(APPEND names "${name}") + list(APPEND lines "${cur_line}") + list(APPEND values "${value}") + endif() + endif() + endif() + if (last) + break() + endif() + endwhile() + + # Every structure an offsetof names is probed below, whatever else is + # written around it, and the first name that uses one carries its line. + set(probes) + set(probe_lines) + set(probe_guards) + foreach(name value line IN ZIP_LISTS names values lines) + if (value MATCHES "offsetof[ \t]*\\(([^,]+),") + string(STRIP "${CMAKE_MATCH_1}" type) + list(FIND probes "${type}" index) + if (index LESS 0) + list(APPEND probes "${type}") + list(APPEND probe_lines "${line}") + list(APPEND probe_guards "defined(${name})") + else() + # Any one of them being defined is enough to probe with. + list(GET probe_guards ${index} guard) + list(REMOVE_AT probe_guards ${index}) + list(INSERT probe_guards ${index} "${guard} || defined(${name})") + endif() endif() endforeach() - set(distinct_types ${types}) - if (distinct_types) - list(REMOVE_DUPLICATES distinct_types) - endif() - # A program that prints the addresses. Everything is unsigned long, so - # neither compiler's 16 bit size_t truncates what it prints or compares. - # Included by name with -I below, because cc65 cannot find a quoted - # include given as an absolute path. + set(dir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${stem}.xram") + string(REPLACE "\\" "/" header_c "${header_file}") + + # A program that prints the values. Everything is unsigned long, so + # neither compiler's 16 bit size_t truncates what it prints. Included by + # name with -I below, because cc65 cannot find a quoted include given as + # an absolute path. set(stub "#include \n#include \"${header_name}\"\n\nint main(void)\n{\n") - list(LENGTH distinct_types type_count) - if (type_count) - string(APPEND stub " int too_large[${type_count}] = {0};\n") - endif() - string(APPEND stub " int bad = 0;\n\n") - foreach(name type IN ZIP_LISTS names types) - list(FIND distinct_types "${type}" type_index) + foreach(name line IN ZIP_LISTS names lines) string(APPEND stub - " printf(\"${name} 0x%lX\\n\", 0x10000UL + (unsigned long)${name});\n" - " if ((unsigned long)${name} >= (unsigned long)sizeof(${type}))\n" - " too_large[${type_index}] = 1;\n") + "#ifdef ${name}\n" + "#line ${line} \"${header_c}\"\n" + " printf(\"${name} 0x%lX\\n\", (unsigned long)${name});\n" + "#endif\n") + endforeach() + string(APPEND stub " return 0;\n}\n") + file(WRITE "${dir}/xram_stub.c" "${stub}") + + # A program of assertions, compiled but never run, so the compiler + # reports a bad layout against the line in the header. Every name gets a + # line of its own, since a name with no assertion would let a define + # that will not compile through. Each declaration is one line, because + # cc65 reports the line it finished reading rather than the one it + # started on. + set(check "#include \n#include \"${header_name}\"\n\n") + string(APPEND check + "/* size_t is 16 bits, so a structure larger than 64K wraps instead of\n" + " being refused. An array of one is refused outright, because the\n" + " size of an array is checked against the largest object the target\n" + " allows and not against the truncated sizeof. */\n") + # The leading underscore is the namespace C keeps for file scope, which + # is the one place a name here cannot collide with the header's own. + foreach(type line guard IN ZIP_LISTS probes probe_lines probe_guards) + string(MAKE_C_IDENTIFIER "${type}" probe) + string(APPEND check + "\n#if ${guard}\n#line ${line} \"${header_c}\"\n" + "extern ${type} _xram_fits_${probe}[1];\n#endif\n") + endforeach() + # An offset and arithmetic between offsets are size_t, so 16 bits, and + # can never trip this. A number that does not fit is a long and does. + foreach(name line IN ZIP_LISTS names lines) + string(APPEND check + "\n#ifdef ${name}\n#line ${line} \"${header_c}\"\n" + "_Static_assert((${name}) < 0x10000L," + " \"${name} overflows 16 bits.\");\n") if (NOT unaligned OR NOT name MATCHES "^(${unaligned})$") - string(APPEND stub - " if ((unsigned long)${name} & 1UL)\n" - " {\n" - " printf(\"${header_name}: ${name} is unaligned at $%lX.\"\n" - " \" To allow, use the []\"\n" - " \" in rp6502_xram.\\n\", (unsigned long)${name});\n" - " bad = 1;\n" - " }\n") + string(APPEND check + "#line ${line} \"${header_c}\"\n" + "_Static_assert(!((${name}) & 1), \"${name} is unaligned." + " To allow, use the [] in rp6502_xram.\");\n") endif() + string(APPEND check "#endif\n") endforeach() - foreach(type IN LISTS distinct_types) - list(FIND distinct_types "${type}" type_index) - # The words cc65 uses when it rejects the same layout itself. - string(APPEND stub - " if (too_large[${type_index}])\n" - " {\n" - " printf(\"${header_name}: Error: Size of '${type}'\"\n" - " \" is too large\\n\");\n" - " bad = 1;\n" - " }\n") - endforeach() - string(APPEND stub "\n return bad;\n}\n") - set(dir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${stem}.xram") - file(WRITE "${dir}/stub.c" "${stub}") + file(WRITE "${dir}/xram_check.c" "${check}") - # cc65's CMAKE_C_COMPILER is a wrapper around cl65 for the IDE. - set(compiler "${CMAKE_C_COMPILER}") - if (CC65_C_COMPILER) - set(compiler "${CC65_C_COMPILER}") + # cc65's CMAKE_C_COMPILER is a wrapper around cl65 that puts diagnostics + # in the form an IDE matches, so both programs are built through it. + set(compiler_args) + if (CMAKE_C_COMPILER_ARG1) + separate_arguments(compiler_args NATIVE_COMMAND "${CMAKE_C_COMPILER_ARG1}") endif() - get_filename_component(header_dir "${header_file}" DIRECTORY) separate_arguments(flags NATIVE_COMMAND "${CMAKE_C_FLAGS}") + + set(failed FALSE) execute_process( - COMMAND "${compiler}" ${flags} -I "${header_dir}" - -o "${dir}/stub" "${dir}/stub.c" + COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" + -o "${dir}/xram_stub" "${dir}/xram_stub.c" WORKING_DIRECTORY "${dir}" RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output ) if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + set(failed TRUE) endif() - rp6502_default_address(load_addr) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process( - COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" - -a "${load_addr}" -r "${load_addr}" - -o "${dir}/stub.rp6502" create "${dir}/stub" - RESULT_VARIABLE result - OUTPUT_VARIABLE output - ERROR_VARIABLE output - ) - if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + if (NOT failed) + rp6502_default_address(load_addr) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" + -a "${load_addr}" -r "${load_addr}" + -o "${dir}/xram_stub.rp6502" create "${dir}/xram_stub" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + if (NOT result EQUAL 0) + set(failed TRUE) + endif() endif() - execute_process( - COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" - -c "${RP6502_PROJECT_DIR}/.rp6502" - execute "${dir}/stub.rp6502" - TIMEOUT 60 - RESULT_VARIABLE result - OUTPUT_VARIABLE output - ERROR_VARIABLE output - ) - if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + if (NOT failed) + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" + -c "${RP6502_PROJECT_DIR}/.rp6502" + execute "${dir}/xram_stub.rp6502" + TIMEOUT 60 + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + if (NOT result EQUAL 0) + set(failed TRUE) + endif() endif() + # A failure here is the header's, and the build reports it in full, so + # the configure finishes with every address at zero rather than leaving + # the project unconfigured. string(REPLACE "\r" "" output "${output}") foreach(name IN LISTS names) - if (NOT output MATCHES "(^|\n)${name} (0x[0-9A-Fa-f]+)") - message(FATAL_ERROR "rp6502_xram(${header}) found no address for ${name}\n${output}") + if (failed) + set(${name} 0 PARENT_SCOPE) + elseif (output MATCHES "(^|\n)${name} (0x[0-9A-Fa-f]+)") + # Out of range is zeroed, so it reaches rp6502_asset() as nothing + # while the build reports it against the header. + set(found "${CMAKE_MATCH_2}") + math(EXPR numeric "${found}") + if (numeric GREATER 65535) + set(found 0) + endif() + set(${name} "${found}" PARENT_SCOPE) + else() + # The preprocessor skipped it, so it is not a name at all. + unset(${name} PARENT_SCOPE) endif() - set(${name} "${CMAKE_MATCH_2}" PARENT_SCOPE) endforeach() + # A header that will not compile is reported by the compile below, but a + # tool that did not run leaves a header that compiles and every address + # at zero, which would otherwise build a ROM that loads everything over + # the start of XRAM. So the reason is carried to the build and fails it. + set(unread) + if (failed) + message(STATUS "rp6502_xram(${header}) read no addresses; the build reports why.") + file(WRITE "${dir}/xram_unread.txt" + "rp6502_xram(${header}) read no addresses, so every name is zero.\n${output}\n") + set(unread + COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/xram_unread.txt" + COMMAND "${CMAKE_COMMAND}" -E false) + endif() + + # Target names are global, and one header can be shared by two + # directories, so the name carries where it was called from. + file(RELATIVE_PATH id "${CMAKE_SOURCE_DIR}" "${header_file}") + if (id MATCHES "^\\.\\.") + set(id "${header_file}") + endif() + string(MAKE_C_IDENTIFIER "${id}" id) + string(MD5 hash "${CMAKE_CURRENT_BINARY_DIR}|${header_file}") + string(SUBSTRING "${hash}" 0 8 hash) + set(target "rp6502_xram_${id}_${hash}") + add_custom_command( + OUTPUT "${dir}/xram_check.stamp" + DEPENDS "${header_file}" "${dir}/xram_check.c" + COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" + -c -o "${dir}/xram_check.o" "${dir}/xram_check.c" + ${unread} + COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/xram_check.stamp" + COMMENT "Checking ${header_name}" + VERBATIM + ) + add_custom_target(${target} ALL DEPENDS "${dir}/xram_check.stamp") + set_property(DIRECTORY APPEND PROPERTY RP6502_XRAM_CHECKS "${target}") endfunction() # Declare files as byproducts of building .