Skip to content

Multiple bugs with PNG cartridges #2042

@bztsrc

Description

@bztsrc

Hi,

Very cool console you have! I like it very much! I only have issues with the PNG files. The way how TIC-80 currently handles them is unacceptable for several reasons:

  • First, it's not supported everywhere, only in studio and in surf (but not from command line for example).
  • Second, it does not work on systems with case-sensitive file systems (due to the LoadPngCart call depends on file extension!)
  • Third, and this is the biggest problem, a real no-go for me, is the way how TIC-80 embeds data generates CORRUPT png files! Tools are refusing to resize the image because of that, or worse, they might corrupt the cartridge data silently.

Here's a proposed solution that fixes all of these problems at once and as a side effect improves performance a lot. This renders the use of LoadPngCart completely unnecessary, it Just Works(tm) and gets the cartridge from a png file without actually decoding the data:

diff --git a/src/cart.c b/src/cart.c
index 9be32c7..1c6079d 100644
--- a/src/cart.c
+++ b/src/cart.c
@@ -87,6 +87,28 @@ void tic_cart_load(tic_cartridge* cart, const u8* buffer, s32 size)
 
 #define LOAD_CHUNK(to) memcpy(&to, ptr, MIN(sizeof(to), chunk->size ? retro_le_to_cpu16(chunk->size) : TIC_BANK_SIZE))
 
+    // check if the cartridge buffer is a PNG file
+    if (!memcmp(buffer, "\x89PNG", 4))
+    {
+        s32 siz;
+        const u8* ptr = buffer + 8;
+        while (ptr < end)
+        {
+            siz = ((ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3]) + 12;
+            if (!memcmp(ptr + 4, "caRt", 4))
+            {
+                buffer = ptr + 8;
+                size = siz;
+                end = buffer + size;
+                break;
+            }
+            ptr += siz;
+        }
+        // error, no TIC-80 cartridge chunk in PNG???
+        if (ptr >= end)
+            return;
+    }
+
     // load palette chunk first
     {
         const u8* ptr = buffer;

The advantage is, this patch is extremely simple, dependency-free and it does not require to decode the png at all! It just iterates through png chunks without interpreting them.

I've chosen the chunk magic to be caRt. This isn't some foolishness, rather selected in accordance to the PNG Specification section 9.8. "Use of private chunks", and means ancillary, private, safe-to-copy chunk.

As for creating such VALID png files, here's an extremely simple command line tool to do that (I've written it deliberately in a way so that you can reuse the tic2png function in TIC-80). Again, this is totally dependency-free (note no png libraries used!), and does not require to decode nor to encode png data, all it does is just manipulating png chunks. It simply inserts a caRt chunk with the cartridge data as the last chunk (well, the one before the IEND chunk). It also takes care of the case if the png already had a caRt chunk.

/*
 * tic2png.c
 *
 * Copyright (C) 2022 bzt (bztsrc@gitlab) MIT license
 *
 * @brief Small dependency-free tool to embed a .tic cartridge into a .png file
 * Compilation: gcc tic2png.c -o tic2png
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* the png specification is very strict, *do not* change uppercase/lowercase */
#define TICMAGIC "caRt"

/**
 * Add a tic file into a png chunk
 */
int tic2png(uint8_t *tic, size_t ticsize, uint8_t **png, size_t *pngsize)
{
    int i, j;
    uint32_t crc, crc_table[256], size;
    uint8_t *buf, *in, *out, *end, *ptr = tic;
    size_t s;

    if(!tic || !png || !*png || memcmp(*png, "\x89PNG", 4) || !pngsize || *pngsize < 8+12+12) return 0;
    end = *png + *pngsize;
    buf = malloc(*pngsize + ticsize + 12);
    if(!buf) return 0;
    s = 8; in = *png; out = buf; memcpy(out, in, 8); in += 8; out += 8;
    /* copy all chunks, except tic chunk */
    while(in < end - 12 && memcmp(in + 4, "IEND", 4)) {
        size = ((in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3]) + 12;
        if(memcmp(in + 4, TICMAGIC, 4)) { memcpy(out, in, size); out += size; s += size; }
        in += size;
    }
    /* add new tic chunk */
    s += 4;
    *out++ = (ticsize >> 24) & 0xff; *out++ = (ticsize >> 16) & 0xff; *out++ = (ticsize >>  8) & 0xff; *out++ = (ticsize >>  0) & 0xff;
    for(i = 0; i < 256; i++) {
        crc = (uint32_t)i; for(j = 0; j < 8; j++) { crc = crc & 1 ? 0xedb88320L ^ (crc >> 1) : (crc >> 1); crc_table[i] = crc;
    }
    crc = 0xffffffff;
    for(i = 0; i < 4; i++, s++, out++) { *out = TICMAGIC[i]; crc = crc_table[(crc ^ *out) & 0xff] ^ (crc >> 8); }
    for(; ticsize; ticsize--, s++, ptr++, out++) { *out = *ptr; crc = crc_table[(crc ^ *out) & 0xff] ^ (crc >> 8); }
    crc ^= 0xffffffff; s += 4;
    *out++ = (crc >> 24) & 0xff; *out++ = (crc >> 16) & 0xff; *out++ = (crc >>  8) & 0xff; *out++ = (crc >>  0) & 0xff;
    /* add end chunk */
    memcpy(out, in, 12);
    s += 12;
    /* replace original png buffer with the new one */
    free(*png);
    *png = buf;
    *pngsize = s;
    return 1;
}

/**
 * Read in one file into memory
 */
uint8_t *readfile(char *fn, size_t *size)
{
    FILE *f;
    uint8_t *buf = NULL;

    f = fopen(fn, "rb");
    if(f) {
        fseek(f, 0, SEEK_END);
        *size = ftell(f);
        fseek(f, 0, SEEK_SET);
        buf = malloc(*size);
        if(!buf) { fclose(f); fprintf(stderr, "tic2png: unable to allocate memory\r\n"); exit(1); }
        fread(buf, 1, *size, f);
        fclose(f);
    } else {
        fprintf(stderr, "tic2png: unable to read %s\r\n", fn);
        exit(1);
    }
    return buf;
}

/**
 * Main procedure
 */
int main(int argc, char **argv)
{
    FILE *f;
    int ret;
    size_t ticsize, pngsize, outsize;
    uint8_t *tic, *png, *out;

    /* check arguments */
    if(argc < 3) {
        printf("tic2png - by bzt MIT\r\n\r\n  %s <.tic file> <.png file>\r\n", argv[0]);
        return 1;
    }

    /* read in files */
    tic = readfile(argv[1], &ticsize);
    png = readfile(argv[2], &pngsize);

    /* add tic to png */
    if(!tic2png(tic, ticsize, &png, &pngsize)) {
        fprintf(stderr, "tic2png: unable to add png chunk\r\n");
        return 1;
    }

    /* save new png file */
    f = fopen(argv[2], "wb");
    if(f) {
        fwrite(png, 1, pngsize, f);
        fclose(f);
    } else {
        fprintf(stderr, "tic2png: unable to write %s\r\n", argv[2]);
        return 1;
    }
    return 0;
}

All these are licensed under MIT, feel free to use them in TIC-80.

Cheers,
bzt

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions