// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // amigaCheck.c - the Amiga gates' checks of a screenshot and the probes' // logs (scripts/verify-amiga-icon.sh and verify-amiga-palette.sh). // // amigaCheck icon WORK STACK WBTOOL LABEL // amigaCheck palette PNG LOG MINRATE LABEL // // icon: WORK holds WBLAUNCH's log (wblaunch.txt), Prog/Palprobe.info, and // the screenshot (screen.png). Every line of the log with FAIL in it fails; // the log must say icon.library read the icon as a tool's (type WBTOOL) of // STACK bytes of stack and the file's image size and depth; and the // screenshot must show the icon. The screen is Workbench's gray and the // icon's colored pixels lie inside it: the file's colored pixels (index // not 0) and the screenshot's (not gray) must have the same bounds, scaled, // and each of the file's pixels is looked at in the middle of its cell. // Only the gray's upper half is searched: FS-UAE prints its notices over // the bottom. Then Palprobe's log must be in Prog/ with its last line, and // no log in WORK: it ran in its own folder. // // palette: LOG must have PALPROBE's two rates (palette and SCB changes // presented a second; the last of each the log has), each at least // MINRATE, and the color bands down the screenshot's middle column, top to // bottom, black (the border) dropped, must be red, green, yellow, and red. // // Prints LABEL: PASS or LABEL: FAIL - the reasons, as the gate's verdict; // exits 0 or 1, or 2 on a bad command line. #include #include #include #include #include "gateArg.h" #include "gateImage.h" #include "gateText.h" #include "toolBuf.h" #include "toolFile.h" #define TOOL_NAME "amigaCheck" #define FAIL_EXIT 1 #define PATH_BYTES 4096u #define NAME_BYTES 32u #define SEPARATOR "; " #define LIST_SEPARATOR ", " // A pattern: its text, '#' standing for a run of digits (a number that // fits a long, so of NUMBER_DIGITS_MAX digits at most). #define PATTERN_NUMBER '#' #define PATTERN_VALUES_MAX 8u #define NUMBER_DIGITS_MAX 19u // The icon file: a 78-byte DiskObject and the struct Image's first 4 // bytes, then its width, height, and depth (big-endian words); its planes // follow the 20-byte struct Image, rows of whole 16-bit words. #define INFO_SIZE_AT 82u #define INFO_PLANES_AT 98u #define INFO_DEPTH_MAX 8 #define WORD_PIXELS 16 #define WORD_BYTES 2 #define BYTE_BITS 8 #define TOP_BIT 0x80u #define AMIGA_COLORS 4u #define GRAY_STEP 2 #define HALF 0.5 #define ICON_PATTERN "icon type=# stack=# gadget=#x# image=#x#x#" #define ICON_TYPE 0u #define ICON_STACK 1u #define ICON_IMAGE_W 4u #define ICON_IMAGE_H 5u #define ICON_IMAGE_DEPTH 6u #define PALPROBE_DONE "PALPROBE: done" #define FAIL_TEXT "FAIL" // The palette probe: its two rates (the third number of its line), and // the bands it draws. #define RATE_KINDS 2u #define RATE_VALUE 2u #define BAND_COUNT 4u #define BAND_NAMES 3u #define BAND_OTHER BAND_NAMES // The reasons a gate fails, "; " between them. typedef struct FailsT { ToolBufT text; uint32_t count; } FailsT; // A box round some pixels, and whether there are any. typedef struct BoundsT { long x0; long x1; long y0; long y1; bool any; } BoundsT; // A Workbench icon's image: its size and depth as the file gives them, and // each pixel's color index (no pixels when the size is not positive). typedef struct IconT { long width; long height; long depth; long cols; long rows; uint8_t *indices; } IconT; static long be16(const uint8_t *p); static void boundsAdd(BoundsT *bounds, long x, long y); static int checkIcon(char **args); static int checkPalette(char **args); static void failAdd(FailsT *fails, const char *fmt, ...) __attribute__((format(printf, 2, 3))); static void failLine(FailsT *fails, const uint8_t *line, size_t len); static void failSeparate(FailsT *fails); static bool fileExists(const char *path); static bool findPattern(const uint8_t *text, size_t len, size_t *at, const char *pattern, long *values); static void iconRead(const char *path, IconT *icon); static void iconShown(const char *path, const IconT *icon, const char *label, FailsT *fails); static bool matchAt(const uint8_t *text, size_t len, size_t at, const char *pattern, long *values); static int printVerdict(const FailsT *fails, const char *label); static uint8_t *readText(const char *path, size_t *len); static const GateModeT kModes[] = { { "icon", 4, checkIcon }, { "palette", 4, checkPalette } }; // Workbench's gray, black, white, and blue. static const uint8_t kWorkbench[AMIGA_COLORS][GATE_RGB_BYTES] = { { 170, 170, 170 }, { 0, 0, 0 }, { 255, 255, 255 }, { 102, 136, 187 } }; static const char *const kRateKinds[RATE_KINDS] = { "palette", "scb" }; static const char *const kRatePatterns[RATE_KINDS] = { "PALPROBE: palette+present: # in # ms = #/s", "PALPROBE: scb+present: # in # ms = #/s" }; static const uint8_t kBandColors[BAND_NAMES][GATE_RGB_BYTES] = { { 255, 0, 0 }, { 0, 255, 0 }, { 255, 255, 0 } }; static const char *const kBandNames[BAND_NAMES] = { "red", "green", "yellow" }; static const uint32_t kWantBands[BAND_COUNT] = { 0u, 1u, 2u, 0u }; static const uint8_t kBlack[GATE_RGB_BYTES] = { 0, 0, 0 }; static const char *const kUsage[] = { "usage:", " amigaCheck icon WORK STACK WBTOOL LABEL WBLAUNCH read and drew the", " icon; Palprobe ran in Prog/", " amigaCheck palette PNG LOG MINRATE LABEL PALPROBE's rates and bands" }; // A signed big-endian 16-bit number. static long be16(const uint8_t *p) { return (int16_t)(((uint32_t)p[0] << BYTE_BITS) | p[1]); } static void boundsAdd(BoundsT *bounds, long x, long y) { bounds->x0 = (!bounds->any || x < bounds->x0) ? x : bounds->x0; bounds->x1 = (!bounds->any || x > bounds->x1) ? x : bounds->x1; bounds->y0 = (!bounds->any || y < bounds->y0) ? y : bounds->y0; bounds->y1 = (!bounds->any || y > bounds->y1) ? y : bounds->y1; bounds->any = true; } static int checkIcon(char **args) { FailsT fails; IconT icon; char path[PATH_BYTES]; uint8_t *log; uint8_t *palLog; long got[PATTERN_VALUES_MAX]; size_t logLen; size_t palLogLen; size_t at; size_t end; size_t lineLen; long wantStack; long wbTool; memset(&fails, 0, sizeof(fails)); wantStack = gateArgLong("stack", args[1]); wbTool = gateArgLong("tool type", args[2]); snprintf(path, sizeof(path), "%s/wblaunch.txt", args[0]); log = readText(path, &logLen); // Each line (a \r before its \n dropped) with FAIL in it. for (at = 0; at < logLen; at = end + 1u) { end = gateTextFind(log, logLen, at, "\n", 1u); end = (end == GATE_TEXT_NOT_FOUND) ? logLen : end; lineLen = (end > at && log[end - 1u] == '\r') ? end - at - 1u : end - at; if (gateTextFind(log + at, lineLen, 0u, FAIL_TEXT, strlen(FAIL_TEXT)) != GATE_TEXT_NOT_FOUND) { failLine(&fails, log + at, lineLen); } } snprintf(path, sizeof(path), "%s/Prog/Palprobe.info", args[0]); iconRead(path, &icon); at = 0u; if (!findPattern(log, logLen, &at, ICON_PATTERN, got)) { failAdd(&fails, "icon.library never read the icon"); } else { if (got[ICON_TYPE] != wbTool) { failAdd(&fails, "icon type %ld, want WBTOOL (%ld)", got[ICON_TYPE], wbTool); } if (got[ICON_STACK] != wantStack) { failAdd(&fails, "icon stack %ld, want %ld", got[ICON_STACK], wantStack); } if (got[ICON_IMAGE_W] != icon.width || got[ICON_IMAGE_H] != icon.height || got[ICON_IMAGE_DEPTH] != icon.depth) { failAdd(&fails, "icon.library's image %ldx%ldx%ld, the file's %ldx%ldx%ld", got[ICON_IMAGE_W], got[ICON_IMAGE_H], got[ICON_IMAGE_DEPTH], icon.width, icon.height, icon.depth); } } snprintf(path, sizeof(path), "%s/screen.png", args[0]); iconShown(path, &icon, args[3], &fails); snprintf(path, sizeof(path), "%s/Prog/joeylog.txt", args[0]); palLog = fileExists(path) ? readText(path, &palLogLen) : NULL; if (palLog == NULL || gateTextFind(palLog, palLogLen, 0u, PALPROBE_DONE, strlen(PALPROBE_DONE)) == GATE_TEXT_NOT_FOUND) { failAdd(&fails, "Palprobe's log is not in Prog/ with its last line: it did not run in its own folder"); } snprintf(path, sizeof(path), "%s/joeylog.txt", args[0]); if (fileExists(path)) { failAdd(&fails, "a joeylog.txt is in the drive's root: Palprobe ran outside its folder"); } if (fails.count != 0u) { return printVerdict(&fails, args[3]); } printf("%s: PASS (icon.library reads a tool's icon, %ld bytes of stack, %ldx%ldx%ld, and draws every pixel of it; started from it as Workbench does, Palprobe ran in its own folder and its message came back)\n", args[3], wantStack, icon.width, icon.height, icon.depth); return 0; } static int checkPalette(char **args) { GateImageT img; FailsT fails; ToolBufT bands; uint8_t *log; const uint8_t *rgb; long values[PATTERN_VALUES_MAX]; long rates[RATE_KINDS]; bool seen[RATE_KINDS]; uint32_t bandIds[BAND_COUNT]; uint32_t bandCount; uint32_t band; uint32_t k; uint32_t y; size_t logLen; size_t at; long minRate; bool matched; char name[NAME_BYTES]; char last[NAME_BYTES]; memset(&fails, 0, sizeof(fails)); memset(&bands, 0, sizeof(bands)); minRate = gateArgLong("minimum rate", args[2]); log = readText(args[1], &logLen); // Each kind's rate: the last the log has. for (k = 0; k < RATE_KINDS; k++) { seen[k] = false; rates[k] = 0; for (at = 0u; findPattern(log, logLen, &at, kRatePatterns[k], values); at++) { seen[k] = true; rates[k] = values[RATE_VALUE]; } } if (!seen[0] || !seen[1]) { printf("%s: FAIL - the log does not have both rates (", args[3]); for (k = 0; k < RATE_KINDS; k++) { printf("%s%s ", (k == 0u) ? "" : LIST_SEPARATOR, kRateKinds[k]); if (seen[k]) { printf("%ld/s", rates[k]); } else { printf("missing"); } } printf(")\n"); return FAIL_EXIT; } // The bands down the middle column, top to bottom, black (the border) // dropped: each run of one color is a band. gateImageLoad(args[0], &img); bandCount = 0u; for (y = 0; y < img.height; y++) { rgb = gateImagePixel(&img, img.width / 2u, y); if (memcmp(rgb, kBlack, GATE_RGB_BYTES) == 0) { continue; } band = BAND_OTHER; for (k = 0; k < BAND_NAMES && band == BAND_OTHER; k++) { band = (memcmp(rgb, kBandColors[k], GATE_RGB_BYTES) == 0) ? k : BAND_OTHER; } if (band != BAND_OTHER) { snprintf(name, sizeof(name), "%s", kBandNames[band]); } else { snprintf(name, sizeof(name), "other (%u, %u, %u)", (unsigned)rgb[0], (unsigned)rgb[1], (unsigned)rgb[2]); } if (bandCount > 0u && strcmp(name, last) == 0) { continue; } gateTextAdd(&bands, "%s%s", (bandCount == 0u) ? "" : LIST_SEPARATOR, name); if (bandCount < BAND_COUNT) { bandIds[bandCount] = band; } bandCount++; memcpy(last, name, sizeof(last)); } for (k = 0; k < RATE_KINDS; k++) { if (rates[k] < minRate) { failAdd(&fails, "%s changes presented %ld/s, want at least %ld", kRateKinds[k], rates[k], minRate); } } matched = bandCount == BAND_COUNT; for (k = 0; k < BAND_COUNT && matched; k++) { matched = bandIds[k] == kWantBands[k]; } if (!matched) { failAdd(&fails, "bands %s, want red, green, yellow, red", (bandCount == 0u) ? "none" : gateTextString(&bands)); } if (fails.count != 0u) { return printVerdict(&fails, args[3]); } printf("%s: PASS (palette changes %ld/s, SCB changes %ld/s; bands red, green, yellow, red after a palette change and a same-layout SCB change)\n", args[3], rates[0], rates[1]); return 0; } static void failAdd(FailsT *fails, const char *fmt, ...) { va_list args; failSeparate(fails); va_start(args, fmt); gateTextAddV(&fails->text, fmt, args); va_end(args); } // A line of a log as a reason, its bytes as they are. static void failLine(FailsT *fails, const uint8_t *line, size_t len) { failSeparate(fails); toolBufAdd(&fails->text, line, len); } // "; " before every reason but the first. static void failSeparate(FailsT *fails) { if (fails->count > 0u) { toolBufAdd(&fails->text, SEPARATOR, strlen(SEPARATOR)); } fails->count++; } static bool fileExists(const char *path) { FILE *f; f = fopen(path, "rb"); if (f == NULL) { return false; } fclose(f); return true; } // The first place at or after *at that the pattern matches: there, in *at, // and its numbers in values. static bool findPattern(const uint8_t *text, size_t len, size_t *at, const char *pattern, long *values) { for (; *at < len; (*at)++) { if (matchAt(text, len, *at, pattern, values)) { return true; } } return false; } // The image of the icon file at path: its size and depth, and its pixels' // color indices, a bit of each from each plane. static void iconRead(const char *path, IconT *icon) { uint8_t *info; size_t infoLen; size_t byteAt; long rowBytes; long plane; long x; long y; info = readText(path, &infoLen); if (infoLen < INFO_PLANES_AT) { toolFail("%s is too short for an icon", path); } icon->width = be16(info + INFO_SIZE_AT); icon->height = be16(info + INFO_SIZE_AT + WORD_BYTES); icon->depth = be16(info + INFO_SIZE_AT + 2u * WORD_BYTES); if (icon->depth > INFO_DEPTH_MAX) { toolFail("%s has %ld planes", path, icon->depth); } icon->cols = (icon->width > 0 && icon->height > 0) ? icon->width : 0; icon->rows = (icon->cols > 0) ? icon->height : 0; icon->indices = (uint8_t *)calloc((size_t)(icon->cols * icon->rows) + 1u, 1u); if (icon->indices == NULL) { toolFail("out of memory"); } rowBytes = (icon->width + WORD_PIXELS - 1) / WORD_PIXELS * WORD_BYTES; for (plane = 0; plane < icon->depth; plane++) { for (y = 0; y < icon->rows; y++) { for (x = 0; x < icon->cols; x++) { byteAt = INFO_PLANES_AT + (size_t)(plane * rowBytes * icon->rows + y * rowBytes + x / BYTE_BITS); if (byteAt >= infoLen) { toolFail("%s's planes are truncated", path); } if (info[byteAt] & (TOP_BIT >> (x % BYTE_BITS))) { icon->indices[y * icon->cols + x] |= (uint8_t)(1u << plane); } } } } free(info); } // Whether the screenshot at path shows the icon: the bounds of the gray // (looked for at every other pixel), the non-gray inside its upper half, // and each of the icon's pixels in the middle of its cell there. Prints // where the icon is and how many of its pixels are right; adds what is // wrong to fails. static void iconShown(const char *path, const IconT *icon, const char *label, FailsT *fails) { GateImageT shot; BoundsT gray; BoundsT shown; BoundsT held; const uint8_t *pixel; long x; long y; long cells; long wrong; uint8_t index; double scaleX; double scaleY; gateImageLoad(path, &shot); memset(&gray, 0, sizeof(gray)); for (y = 0; y < (long)shot.height; y += GRAY_STEP) { for (x = 0; x < (long)shot.width; x += GRAY_STEP) { if (memcmp(gateImagePixel(&shot, (uint32_t)x, (uint32_t)y), kWorkbench[0], GATE_RGB_BYTES) == 0) { boundsAdd(&gray, x, y); } } } if (!gray.any) { failAdd(fails, "no Workbench gray on the screenshot"); return; } memset(&shown, 0, sizeof(shown)); for (y = gray.y0; y <= (gray.y0 + gray.y1) / 2; y++) { for (x = gray.x0; x <= gray.x1; x++) { if (memcmp(gateImagePixel(&shot, (uint32_t)x, (uint32_t)y), kWorkbench[0], GATE_RGB_BYTES) != 0) { boundsAdd(&shown, x, y); } } } memset(&held, 0, sizeof(held)); for (y = 0; y < icon->rows; y++) { for (x = 0; x < icon->cols; x++) { if (icon->indices[y * icon->cols + x] != 0u) { boundsAdd(&held, x, y); } } } if (!shown.any || !held.any) { failAdd(fails, held.any ? "no icon on the screenshot" : "the icon file is empty"); return; } scaleX = (double)(shown.x1 + 1 - shown.x0) / (double)(held.x1 + 1 - held.x0); scaleY = (double)(shown.y1 + 1 - shown.y0) / (double)(held.y1 + 1 - held.y0); wrong = 0; for (y = held.y0; y <= held.y1; y++) { for (x = held.x0; x <= held.x1; x++) { index = icon->indices[y * icon->cols + x]; if (index >= AMIGA_COLORS) { toolFail("the icon's pixel %ld,%ld is color %u, past Workbench's four", x, y, (unsigned)index); } pixel = gateImagePixel(&shot, (uint32_t)(shown.x0 + (x - held.x0 + HALF) * scaleX), (uint32_t)(shown.y0 + (y - held.y0 + HALF) * scaleY)); if (memcmp(pixel, kWorkbench[index], GATE_RGB_BYTES) != 0) { wrong++; } } } cells = (held.x1 + 1 - held.x0) * (held.y1 + 1 - held.y0); printf("%s: icon shown at %ld,%ld scaled %.2fx%.2f: %ld/%ld pixels right\n", label, shown.x0, shown.y0, scaleX, scaleY, cells - wrong, cells); if (wrong != 0) { failAdd(fails, "%ld of the icon's %ld pixels wrong on the screen", wrong, cells); } gateImageFree(&shot); } // Whether the pattern matches text at at: its characters as they are, a // PATTERN_NUMBER a run of digits that fits a long, each number in values. static bool matchAt(const uint8_t *text, size_t len, size_t at, const char *pattern, long *values) { char digits[NUMBER_DIGITS_MAX + 1u]; uint32_t count; size_t n; count = 0u; for (; *pattern != '\0'; pattern++) { if (*pattern != PATTERN_NUMBER) { if (at >= len || text[at] != (uint8_t)*pattern) { return false; } at++; continue; } n = 0u; while (at + n < len && text[at + n] >= '0' && text[at + n] <= '9') { n++; } if (n == 0u || n > NUMBER_DIGITS_MAX) { return false; } memcpy(digits, text + at, n); digits[n] = '\0'; if (!gateArgParseLong(digits, &values[count])) { return false; } count++; at += n; } return true; } static int printVerdict(const FailsT *fails, const char *label) { printf("%s: FAIL - ", label); fwrite(fails->text.data, 1u, fails->text.len, stdout); printf("\n"); return FAIL_EXIT; } // A whole file; one that cannot be read is fatal. static uint8_t *readText(const char *path, size_t *len) { uint8_t *data; if (!toolReadFile(path, &data, len)) { toolFail("cannot read %s", path); } return data; } int main(int argc, char **argv) { int exitCode; toolFileName(TOOL_NAME); if (!gateModeRun(kModes, GATE_COUNT_OF(kModes), argc, argv, &exitCode)) { return gateUsage(kUsage, GATE_COUNT_OF(kUsage)); } return exitCode; }