SNES first test
authorJohn Tsiombikas <nuclear@member.fsf.org>
Fri, 3 Jul 2020 16:03:17 +0000 (19:03 +0300)
committerJohn Tsiombikas <nuclear@member.fsf.org>
Fri, 3 Jul 2020 16:03:17 +0000 (19:03 +0300)
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
hwregs.inc [new file with mode: 0644]
img2snes/Makefile [new file with mode: 0644]
img2snes/src/dynarr.c [new file with mode: 0644]
img2snes/src/dynarr.h [new file with mode: 0644]
img2snes/src/image.c [new file with mode: 0644]
img2snes/src/image.h [new file with mode: 0644]
img2snes/src/main.c [new file with mode: 0644]
snes.ld [new file with mode: 0644]
test.asm [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..c8f7c7a
--- /dev/null
@@ -0,0 +1,8 @@
+*.o
+*.d
+*.swp
+*.png
+data.inc
+*.sfc
+img2snes/img2snes
+link.map
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..ebe9b8a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+test.sfc: test.o
+       ld65 -o $@ -m link.map -C snes.ld $<
+
+test.o: test.asm hwregs.inc data.inc
+
+%.o: %.asm
+       ca65 -o $@ --cpu 65816 $<
+
+data.inc: logo4bpp.png img2snes/img2snes
+       img2snes/img2snes -o data.inc logo4bpp.png
+
+img2snes/img2snes:
+       $(MAKE) -C img2snes
+
+.PHONY: clean
+clean:
+       rm -f test.sfc test.o
diff --git a/hwregs.inc b/hwregs.inc
new file mode 100644 (file)
index 0000000..672aedf
--- /dev/null
@@ -0,0 +1,125 @@
+       ; PPU write-only
+       REG_INIDISP     = $2100
+       REG_OBSEL       = $2101
+       REG_OAMADDL     = $2102
+       REG_OAMADDH     = $2103
+       REG_OAMDATA     = $2104
+       REG_BGMODE      = $2105
+       REG_MOSAIC      = $2106
+       REG_BG1SC       = $2107
+       REG_BG2SC       = $2108
+       REG_BG3SC       = $2109
+       REG_BG4SC       = $210a
+       REG_BG12NBA     = $210b
+       REG_BG34NBA     = $210c
+       REG_BG1HOFFS    = $210d
+       REG_BG1VOFFS    = $210e
+       REG_BG2HOFFS    = $210f
+       REG_BG2VOFFS    = $2110
+       REG_BG3HOFFS    = $2111
+       REG_BG3VOFFS    = $2112
+       REG_BG4HOFFS    = $2113
+       REG_BG4VOFFS    = $2114
+       REG_VMAINC      = $2115
+       REG_VMADDL      = $2116
+       REG_VMADDH      = $2117
+       REG_VMDATAL     = $2118
+       REG_VMDATAH     = $2119
+       REG_M7SEL       = $211a
+       REG_M7A         = $211b
+       REG_M7B         = $211c
+       REG_M7C         = $211d
+       REG_M7D         = $211e
+       REG_M7X         = $211f
+       REG_M7Y         = $2120
+       REG_CGADD       = $2121
+       REG_CGDATA      = $2122
+       REG_W12SEL      = $2123
+       REG_W34SEL      = $2124
+       REG_WOBJSEL     = $2125
+       REG_WH0         = $2126
+       REG_WH1         = $2127
+       REG_WH2         = $2128
+       REG_WH3         = $2129
+       REG_WBGLOG      = $212a
+       REG_WOBJLOG     = $212b
+       REG_TM          = $212c
+       REG_TS          = $212d
+       REG_TMW         = $212e
+       REG_TSW         = $212f
+       REG_CGWSEL      = $2130
+       REG_CGADSUB     = $2131
+       REG_COLDATA     = $2132
+       REG_SETINI      = $2133
+       ; PPU read-only
+       REG_MPYL        = $2134
+       REG_MPYM        = $2135
+       REG_MPYH        = $2136
+       REG_SLHV        = $2137
+       REG_RDOAM       = $2138
+       REG_RDVRAML     = $2139
+       REG_RDVRAMH     = $213a
+       REG_RDCGRAM     = $213b
+       REG_OPHCT       = $213c
+       REG_OPVCT       = $213d
+       REG_STAT77      = $213e
+       REG_STAT78      = $213f
+
+       REG_NMITIMEN    = $4200
+       REG_WRIO        = $4201
+       REG_WRMPYA      = $4202
+       REG_WRMPYB      = $4203
+       REG_WRDIVL      = $4204
+       REG_WRDIVH      = $4205
+       REG_WRDIVB      = $4206
+       REG_HTIMEL      = $4207
+       REG_HTIMEH      = $4208
+       REG_VTIMEL      = $4209
+       REG_VTIMEH      = $420a
+       REG_MEMSEL      = $420d
+
+       ; DMA
+       REG_MDMAEN      = $420b
+       REG_HDMAEN      = $420c
+
+       REG_DMAP_BASE   = $4300
+       REG_BBAD_BASE   = $4301
+       REG_A1TL_BASE   = $4302
+       REG_A1TH_BASE   = $4303
+       REG_A1B_BASE    = $4304
+       REG_DASL_BASE   = $4305
+       REG_DASH_BASE   = $4306
+       REG_DASB_BASE   = $4307
+       REG_A2AL_BASE   = $4308
+       REG_A2AH_BASE   = $4309
+       REG_NTRL_BASE   = $430a
+
+       .define REG_DMAP(n)     (REG_DMAP_BASE | (n << 4))
+       .define REG_BBAD(n)     (REG_BBAD_BASE | (n << 4))
+       .define REG_A1TL(n)     (REG_A1TL_BASE | (n << 4))
+       .define REG_A1TH(n)     (REG_A1TH_BASE | (n << 4))
+       .define REG_A1B(n)      (REG_A1B_BASE | (n << 4))
+       .define REG_DASL(n)     (REG_DASL_BASE | (n << 4))
+       .define REG_DASH(n)     (REG_DASH_BASE | (n << 4))
+       .define REG_DASB(n)     (REG_DASB_BASE | (n << 4))
+       .define REG_A2AL(n)     (REG_A2AL_BASE | (n << 4))
+       .define REG_A2AH(n)     (REG_A2AH_BASE | (n << 4))
+       .define REG_NTRL(n)     (REG_NTRL_BASE | (n << 4))
+
+
+       .macro setreg reg, val
+       lda #val
+       sta reg
+       .endmacro
+
+       .macro fblank onoff
+       lda #($0f | (onoff << 7))
+       sta REG_INIDISP
+       .endmacro
+
+       .macro setpal idx, r, g, b
+       setreg REG_CGADD, idx
+       setreg REG_CGDATA, (r | g << 5)
+       setreg REG_CGDATA, (g >> 3 | b << 2)
+       .endmacro
+
diff --git a/img2snes/Makefile b/img2snes/Makefile
new file mode 100644 (file)
index 0000000..b578678
--- /dev/null
@@ -0,0 +1,24 @@
+PREFIX = /usr/local
+
+src = $(wildcard src/*.c)
+obj = $(src:.c=.o)
+bin = img2snes
+
+CFLAGS = -pedantic -Wall -g
+LDFLAGS = -lpng -lz
+
+$(bin): $(obj)
+       $(CC) -o $@ $(obj) $(LDFLAGS)
+
+.PHONY: clean
+clean:
+       rm -f $(obj) $(bin)
+
+.PHONY: install
+install: $(bin)
+       mkdir -p $(DESTDIR)$(PREFIX)/bin
+       cp $(bin) $(DESTDIR)$(PREFIX)/bin/$(bin)
+
+.PHONY: uninstall
+uninstall:
+       rm -f $(DESTDIR)$(PREFIX)/bin/$(bin)
diff --git a/img2snes/src/dynarr.c b/img2snes/src/dynarr.c
new file mode 100644 (file)
index 0000000..59bbf8c
--- /dev/null
@@ -0,0 +1,141 @@
+/* dynarr - dynamic resizable C array data structure
+ * author: John Tsiombikas <nuclear@member.fsf.org>
+ * license: public domain
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "dynarr.h"
+
+/* The array descriptor keeps auxilliary information needed to manipulate
+ * the dynamic array. It's allocated adjacent to the array buffer.
+ */
+struct arrdesc {
+       int nelem, szelem;
+       int max_elem;
+       int bufsz;      /* not including the descriptor */
+};
+
+#define DESC(x)                ((struct arrdesc*)((char*)(x) - sizeof(struct arrdesc)))
+
+void *dynarr_alloc(int elem, int szelem)
+{
+       struct arrdesc *desc;
+
+       if(!(desc = malloc(elem * szelem + sizeof *desc))) {
+               return 0;
+       }
+       desc->nelem = desc->max_elem = elem;
+       desc->szelem = szelem;
+       desc->bufsz = elem * szelem;
+       return (char*)desc + sizeof *desc;
+}
+
+void dynarr_free(void *da)
+{
+       if(da) {
+               free(DESC(da));
+       }
+}
+
+void *dynarr_resize(void *da, int elem)
+{
+       int newsz;
+       void *tmp;
+       struct arrdesc *desc;
+
+       if(!da) return 0;
+       desc = DESC(da);
+
+       newsz = desc->szelem * elem;
+
+       if(!(tmp = realloc(desc, newsz + sizeof *desc))) {
+               return 0;
+       }
+       desc = tmp;
+
+       desc->nelem = desc->max_elem = elem;
+       desc->bufsz = newsz;
+       return (char*)desc + sizeof *desc;
+}
+
+int dynarr_empty(void *da)
+{
+       return DESC(da)->nelem ? 0 : 1;
+}
+
+int dynarr_size(void *da)
+{
+       return DESC(da)->nelem;
+}
+
+
+void *dynarr_clear(void *da)
+{
+       return dynarr_resize(da, 0);
+}
+
+/* stack semantics */
+void *dynarr_push(void *da, void *item)
+{
+       struct arrdesc *desc;
+       int nelem;
+
+       desc = DESC(da);
+       nelem = desc->nelem;
+
+       if(nelem >= desc->max_elem) {
+               /* need to resize */
+               struct arrdesc *tmp;
+               int newsz = desc->max_elem ? desc->max_elem * 2 : 1;
+
+               if(!(tmp = dynarr_resize(da, newsz))) {
+                       fprintf(stderr, "failed to resize\n");
+                       return da;
+               }
+               da = tmp;
+               desc = DESC(da);
+               desc->nelem = nelem;
+       }
+
+       if(item) {
+               memcpy((char*)da + desc->nelem * desc->szelem, item, desc->szelem);
+       }
+       desc->nelem++;
+       return da;
+}
+
+void *dynarr_pop(void *da)
+{
+       struct arrdesc *desc;
+       int nelem;
+
+       desc = DESC(da);
+       nelem = desc->nelem;
+
+       if(!nelem) return da;
+
+       if(nelem <= desc->max_elem / 3) {
+               /* reclaim space */
+               struct arrdesc *tmp;
+               int newsz = desc->max_elem / 2;
+
+               if(!(tmp = dynarr_resize(da, newsz))) {
+                       fprintf(stderr, "failed to resize\n");
+                       return da;
+               }
+               da = tmp;
+               desc = DESC(da);
+               desc->nelem = nelem;
+       }
+       desc->nelem--;
+
+       return da;
+}
+
+void *dynarr_finalize(void *da)
+{
+       struct arrdesc *desc = DESC(da);
+       memmove(desc, da, desc->bufsz);
+       return desc;
+}
diff --git a/img2snes/src/dynarr.h b/img2snes/src/dynarr.h
new file mode 100644 (file)
index 0000000..8690b5a
--- /dev/null
@@ -0,0 +1,80 @@
+/* dynarr - dynamic resizable C array data structure
+ * author: John Tsiombikas <nuclear@member.fsf.org>
+ * license: public domain
+ */
+#ifndef DYNARR_H_
+#define DYNARR_H_
+
+/* usage example:
+ * -------------
+ * int *arr = dynarr_alloc(0, sizeof *arr);
+ *
+ * int x = 10;
+ * arr = dynarr_push(arr, &x);
+ * x = 5;
+ * arr = dynarr_push(arr, &x);
+ * x = 42;
+ * arr = dynarr_push(arr, &x);
+ *
+ * for(i=0; i<dynarr_size(arr); i++) {
+ *     printf("%d\n", arr[i]);
+ *  }
+ *  dynarr_free(arr);
+ */
+
+void *dynarr_alloc(int elem, int szelem);
+void dynarr_free(void *da);
+void *dynarr_resize(void *da, int elem);
+
+/* dynarr_empty returns non-zero if the array is empty
+ * Complexity: O(1) */
+int dynarr_empty(void *da);
+/* dynarr_size returns the number of elements in the array
+ * Complexity: O(1) */
+int dynarr_size(void *da);
+
+void *dynarr_clear(void *da);
+
+/* stack semantics */
+void *dynarr_push(void *da, void *item);
+void *dynarr_pop(void *da);
+
+/* Finalize the array. No more resizing is possible after this call.
+ * Use free() instead of dynarr_free() to deallocate a finalized array.
+ * Returns pointer to the finalized array.
+ * dynarr_finalize can't fail.
+ * Complexity: O(n)
+ */
+void *dynarr_finalize(void *da);
+
+/* helper macros */
+#define DYNARR_RESIZE(da, n) \
+       do { (da) = dynarr_resize((da), (n)); } while(0)
+#define DYNARR_CLEAR(da) \
+       do { (da) = dynarr_clear(da); } while(0)
+#define DYNARR_PUSH(da, item) \
+       do { (da) = dynarr_push((da), (item)); } while(0)
+#define DYNARR_POP(da) \
+       do { (da) = dynarr_pop(da); } while(0)
+
+/* utility macros to push characters to a string. assumes and maintains
+ * the invariant that the last element is always a zero
+ */
+#define DYNARR_STRPUSH(da, c) \
+       do { \
+               char cnull = 0, ch = (char)(c); \
+               (da) = dynarr_pop(da); \
+               (da) = dynarr_push((da), &ch); \
+               (da) = dynarr_push((da), &cnull); \
+       } while(0)
+
+#define DYNARR_STRPOP(da) \
+       do { \
+               char cnull = 0; \
+               (da) = dynarr_pop(da); \
+               (da) = dynarr_pop(da); \
+               (da) = dynarr_push((da), &cnull); \
+       } while(0)
+
+
+#endif /* DYNARR_H_ */
diff --git a/img2snes/src/image.c b/img2snes/src/image.c
new file mode 100644 (file)
index 0000000..0ae86f9
--- /dev/null
@@ -0,0 +1,237 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+#include <png.h>
+#include "image.h"
+
+int alloc_image(struct image *img, int x, int y, int bpp)
+{
+       memset(img, 0, sizeof *img);
+       img->width = x;
+       img->height = y;
+       img->bpp = bpp;
+       img->scansz = img->pitch = x * bpp / 8;
+
+       if(!(img->pixels = malloc(y * img->scansz))) {
+               fprintf(stderr, "failed to allocate %dx%d (%dbpp) pixel buffer\n", x, y, bpp);
+               return -1;
+       }
+
+       /* just a guess, assume the user will fill the details, but set up reasonable
+        * defaults just in case...
+        */
+       if(bpp <= 8) {
+               img->nchan = 1;
+               img->cmap_ncolors = 1 << bpp;
+       } else if(bpp <= 24) {
+               img->nchan = 3;
+       } else {
+               img->nchan = 4;
+       }
+       return 0;
+}
+
+int load_image(struct image *img, const char *fname)
+{
+       int i;
+       FILE *fp;
+       png_struct *png;
+       png_info *info;
+       int chan_bits, color_type;
+       png_uint_32 xsz, ysz;
+       png_color *palette;
+       unsigned char **scanline;
+       unsigned char *dptr;
+
+       if(!(fp = fopen(fname, "rb"))) {
+               fprintf(stderr, "failed to open: %s: %s\n", fname, strerror(errno));
+               return -1;
+       }
+
+       if(!(png = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0))) {
+               fclose(fp);
+               return -1;
+       }
+       if(!(info = png_create_info_struct(png))) {
+               fclose(fp);
+               png_destroy_read_struct(&png, 0, 0);
+               return -1;
+       }
+       if(setjmp(png_jmpbuf(png))) {
+               fclose(fp);
+               png_destroy_read_struct(&png, &info, 0);
+               return -1;
+       }
+
+       png_init_io(png, fp);
+       png_read_png(png, info, 0, 0);
+
+       png_get_IHDR(png, info, &xsz, &ysz, &chan_bits, &color_type, 0, 0, 0);
+       img->width = xsz;
+       img->height = ysz;
+       img->nchan = png_get_channels(png, info);
+       img->bpp = img->nchan * chan_bits;
+       img->scansz = img->pitch = xsz * img->bpp / 8;
+       img->cmap_ncolors = 0;
+
+       if(color_type == PNG_COLOR_TYPE_PALETTE) {
+               png_get_PLTE(png, info, &palette, &img->cmap_ncolors);
+               memcpy(img->cmap, palette, img->cmap_ncolors * sizeof *img->cmap);
+       }
+
+       if(!(img->pixels = malloc(ysz * img->scansz))) {
+               perror("failed to allocate pixel buffer");
+               fclose(fp);
+               png_destroy_read_struct(&png, &info, 0);
+               return -1;
+       }
+       dptr = img->pixels;
+
+       scanline = (unsigned char**)png_get_rows(png, info);
+       for(i=0; i<ysz; i++) {
+               memcpy(dptr, scanline[i], img->scansz);
+               dptr += img->pitch;
+       }
+
+       fclose(fp);
+       png_destroy_read_struct(&png, &info, 0);
+       return 0;
+}
+
+int save_image(struct image *img, const char *fname)
+{
+       int i, chan_bits, coltype;
+       FILE *fp;
+       png_struct *png;
+       png_info *info;
+       png_text txt;
+       unsigned char **scanline = 0;
+       unsigned char *pptr;
+
+       if(!(fp = fopen(fname, "wb"))) {
+               fprintf(stderr, "save_image: failed to open: %s: %s\n", fname, strerror(errno));
+               return -1;
+       }
+
+       if(!(png = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0))) {
+               fclose(fp);
+               return -1;
+       }
+       if(!(info = png_create_info_struct(png))) {
+               png_destroy_write_struct(&png, 0);
+               fclose(fp);
+               return -1;
+       }
+
+       txt.compression = PNG_TEXT_COMPRESSION_NONE;
+       txt.key = "Software";
+       txt.text = "img2tiles";
+       txt.text_length = 0;
+
+       if(setjmp(png_jmpbuf(png))) {
+               png_destroy_write_struct(&png, &info);
+               free(scanline);
+               fclose(fp);
+               return -1;
+       }
+
+       switch(img->nchan) {
+       case 1:
+               if(img->cmap_ncolors > 0) {
+                       coltype = PNG_COLOR_TYPE_PALETTE;
+               } else {
+                       coltype = PNG_COLOR_TYPE_GRAY;
+               }
+               break;
+       case 2:
+               coltype = PNG_COLOR_TYPE_GRAY_ALPHA;
+               break;
+       case 3:
+               coltype = PNG_COLOR_TYPE_RGB;
+               break;
+       case 4:
+               coltype = PNG_COLOR_TYPE_RGB_ALPHA;
+               break;
+       }
+
+       chan_bits = img->bpp / img->nchan;
+       png_set_IHDR(png, info, img->width, img->height, chan_bits, coltype, PNG_INTERLACE_NONE,
+                       PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+       png_set_text(png, info, &txt, 1);
+
+       if(img->cmap_ncolors > 0) {
+               png_set_PLTE(png, info, (png_color*)img->cmap, img->cmap_ncolors);
+       }
+
+       if(!(scanline = malloc(img->height * sizeof *scanline))) {
+               png_destroy_write_struct(&png, &info);
+               fclose(fp);
+               return -1;
+       }
+
+       pptr = img->pixels;
+       for(i=0; i<img->height; i++) {
+               scanline[i] = pptr;
+               pptr += img->pitch;
+       }
+       png_set_rows(png, info, scanline);
+
+       png_init_io(png, fp);
+       png_write_png(png, info, 0, 0);
+       png_destroy_write_struct(&png, &info);
+       free(scanline);
+       fclose(fp);
+       return 0;
+}
+
+
+int cmp_image(struct image *a, struct image *b)
+{
+       int i;
+       unsigned char *aptr = a->pixels;
+       unsigned char *bptr = b->pixels;
+
+       if(a->width != b->width || a->height != b->height || a->bpp != b->bpp || a->nchan != b->nchan) {
+               return -1;
+       }
+
+       for(i=0; i<a->height; i++) {
+               if(memcmp(aptr, bptr, a->scansz) != 0) {
+                       return -1;
+               }
+               aptr += a->pitch;
+               bptr += b->pitch;
+       }
+       return 0;
+}
+
+void blit(struct image *src, int sx, int sy, int w, int h, struct image *dst, int dx, int dy)
+{
+       int i;
+       unsigned char *sptr, *dptr;
+
+       assert(src->bpp == dst->bpp);
+       assert(src->nchan == dst->nchan);
+
+       if(sx < 0) { w += sx; sx = 0; }
+       if(sy < 0) { h += sy; sy = 0; }
+       if(dx < 0) { w += dx; sx -= dx; dx = 0; }
+       if(dy < 0) { h += dy; sy -= dy; dy = 0; }
+       if(sx + w >= src->width) w = src->width - sx;
+       if(sy + h >= src->height) h = src->height - sy;
+       if(dx + w >= dst->width) w = dst->width - dx;
+       if(dy + h >= dst->height) h = dst->height - dy;
+
+       if(w <= 0 || h <= 0) return;
+
+       sptr = src->pixels + sy * src->pitch + sx * src->bpp / 8;
+       dptr = dst->pixels + dy * dst->pitch + dx * dst->bpp / 8;
+
+       for(i=0; i<h; i++) {
+               memcpy(dptr, sptr, w * dst->bpp / 8);
+               dptr += dst->pitch;
+               sptr += src->pitch;
+       }
+}
diff --git a/img2snes/src/image.h b/img2snes/src/image.h
new file mode 100644 (file)
index 0000000..b9f24b4
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef IMAGE_H_
+#define IMAGE_H_
+
+struct cmapent {
+       unsigned char r, g, b;
+};
+
+struct image {
+       int width, height;
+       int bpp;
+       int nchan;
+       int scansz, pitch;
+       int cmap_ncolors;
+       struct cmapent cmap[256];
+       unsigned char *pixels;
+};
+
+int alloc_image(struct image *img, int x, int y, int bpp);
+int load_image(struct image *img, const char *fname);
+int save_image(struct image *img, const char *fname);
+
+int cmp_image(struct image *a, struct image *b);
+
+void blit(struct image *src, int sx, int sy, int w, int h, struct image *dst, int dx, int dy);
+
+#endif /* IMAGE_H_ */
diff --git a/img2snes/src/main.c b/img2snes/src/main.c
new file mode 100644 (file)
index 0000000..4b7f4bc
--- /dev/null
@@ -0,0 +1,334 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include "image.h"
+#include "dynarr.h"
+
+void print_usage(const char *argv0);
+int proc_image(const char *fname);
+void wrtiles(FILE *fp, const char *name, struct image *timg, int ntiles);
+void wrtilemap(FILE *fp, const char *name, int *tmap, int xtiles, int ytiles);
+void wrpalette(FILE *fp, const char *name, struct cmapent *cmap, int ncol);
+
+int tile_xsz = 8, tile_ysz = 8;
+const char *output_filename;
+
+int main(int argc, char **argv)
+{
+       int i;
+
+       for(i=1; i<argc; i++) {
+               if(argv[i][0] == '-') {
+                       if(argv[i][2] == 0) {
+                               switch(argv[i][1]) {
+                               case 'o':
+                                       output_filename = argv[++i];
+                                       break;
+
+                               case 't':
+                                       if(sscanf(argv[++i], "%dx%d", &tile_xsz, &tile_ysz) != 2) {
+                                               fprintf(stderr, "-t must be followed by the tile size (WxH)\n");
+                                               return 1;
+                                       }
+                                       break;
+
+                               case 'h':
+                                       print_usage(argv[0]);
+                                       return 0;
+
+                               default:
+                                       fprintf(stderr, "invalid option: %s\n", argv[i]);
+                                       print_usage(argv[0]);
+                                       return 1;
+                               }
+                       } else {
+                               fprintf(stderr, "invalid option: %s\n", argv[i]);
+                               print_usage(argv[0]);
+                               return 1;
+                       }
+
+               } else {
+                       if(proc_image(argv[i]) == -1) {
+                               return 1;
+                       }
+               }
+       }
+
+       return 0;
+}
+
+void print_usage(const char *argv0)
+{
+       printf("Usage: %s [options] <img1> [<img2> ... <imgN>]\n", argv0);
+       printf("Options:\n");
+       printf(" -o <file>: output file\n");
+       printf(" -t WxH: tile size (default 8x8)\n");
+       printf(" -h: print usage and exit\n\n");
+}
+
+int find_tile(struct image *tile, struct image *tiles)
+{
+       int i, count;
+       count = dynarr_size(tiles);
+       for(i=0; i<count; i++) {
+               if(cmp_image(tile, tiles + i) == 0) {
+                       return i;
+               }
+       }
+       return -1;
+}
+
+int proc_image(const char *fname)
+{
+       int i, j, k, idx, xtiles, ytiles, ntiles, result = -1;
+       FILE *fp;
+       struct image img, tile;
+       struct image *tiles = 0;
+       unsigned char *tiles_pixels, *tptr;
+       unsigned char *sptr;
+       int *tilemap = 0, *mapptr;
+       char *basename, *suffix, *outfile;
+
+       if(load_image(&img, fname) == -1) {
+               fprintf(stderr, "failed to load image: %s\n", fname);
+               return -1;
+       }
+       basename = alloca(strlen(fname) + 1);
+       strcpy(basename, fname);
+       if((suffix = strrchr(basename, '/'))) {
+               basename = suffix + 1;
+       }
+       if((suffix = strchr(basename, '.'))) {
+               *suffix = 0;
+       }
+
+       xtiles = img.width / tile_xsz;
+       ytiles = img.height / tile_ysz;
+       if(img.width % tile_xsz != 0 || img.height % tile_ysz != 0) {
+               fprintf(stderr, "image size (%dx%d) not evenly divisible into %dx%d tiles\n",
+                               img.width, img.height, tile_xsz, tile_ysz);
+               goto err;
+       }
+
+       if(!(tilemap = malloc(xtiles * ytiles * sizeof *tilemap))) {
+               fprintf(stderr, "failed to allocate %dx%d tilemap\n", xtiles, ytiles);
+               goto err;
+       }
+       mapptr = tilemap;
+
+       if(!(tiles = dynarr_alloc(0, sizeof *tiles))) {
+               fprintf(stderr, "failed to allocate tile array\n");
+               goto err;
+       }
+       /* alloc a contiguous buffer for the full tileset pixels, to make it easier to write it
+        * out as a single image in the end
+        */
+       if(!(tiles_pixels = dynarr_alloc(0, tile_xsz * tile_ysz * img.bpp / 8))) {
+               fprintf(stderr, "failed to allocate tile pixel buffer\n");
+               goto err;
+       }
+
+       tile = img;
+       tile.width = tile_xsz;
+       tile.height = tile_ysz;
+       tile.scansz = tile_xsz * tile.bpp / 8;
+
+       sptr = img.pixels;
+       for(i=0; i<ytiles; i++) {
+               for(j=0; j<xtiles; j++) {
+                       tile.pixels = sptr;
+
+                       if((idx = find_tile(&tile, tiles)) == -1) {
+                               /* we don't have a duplicate of this tile */
+                               idx = dynarr_size(tiles);
+
+                               if(!(tiles = dynarr_push(tiles, 0))) {
+                                       goto err;
+                               }
+                               if(!(tptr = dynarr_push(tiles_pixels, 0))) {
+                                       goto err;
+                               }
+
+                               tiles[idx] = tile;
+                               tiles[idx].scansz = tiles[idx].pitch = tile.scansz;
+
+                               /* did the array get relocated? */
+                               if(tptr != tiles_pixels) {
+                                       tiles_pixels = tptr;
+                                       /* make each tile's pixels pointer point to the right place in the large pixelbuffer */
+                                       for(k=0; k<idx+1; k++) {
+                                               tiles[k].pixels = tptr;
+                                               tptr += tile_ysz * tiles[idx].pitch;
+                                       }
+                               } else {
+                                       /* otherwise just set the new one */
+                                       tiles[idx].pixels = tiles_pixels + idx * tile_ysz * tiles[idx].pitch;
+                               }
+
+                               blit(&tile, 0, 0, tile_xsz, tile_ysz, tiles + idx, 0, 0);
+                       }
+
+                       *mapptr++ = idx;
+
+                       sptr += tile.scansz;
+               }
+               sptr += (tile_ysz - 1) * tile.pitch;
+       }
+
+       ntiles = dynarr_size(tiles);
+       fprintf(stderr, "%s (%dx%d) -> %d tiles, %d unique\n", fname, img.width, img.height,
+                       xtiles * ytiles, ntiles);
+
+       assert(ntiles > 0);
+
+       /* make a big image out of the tiles and write it out */
+       tile = tiles[0];
+       tile.height = ntiles * tile_ysz;
+
+       if(output_filename) {
+               outfile = (char*)output_filename;
+       } else {
+               outfile = alloca(strlen(basename) + 5);
+               sprintf(outfile, "%s.asm", basename);
+       }
+
+       if(!(fp = fopen(outfile, "w"))) {
+               fprintf(stderr, "failed to open output file: %s: %s\n", outfile, strerror(errno));
+               goto err;
+       }
+       wrtiles(fp, basename, &tile, ntiles);
+       wrtilemap(fp, basename, tilemap, xtiles, ytiles);
+       wrpalette(fp, basename, tile.cmap, tile.cmap_ncolors);
+       fclose(fp);
+
+       result = 0;
+
+err:
+       dynarr_free(tiles_pixels);
+       dynarr_free(tiles);
+       free(tilemap);
+       free(img.pixels);
+       return result;
+}
+
+
+unsigned int packbitplane4bpp(unsigned char *pix, int bit)
+{
+       int i;
+       unsigned int val = 0;
+
+       for(i=0; i<4; i++) {
+               val = (val << 1) | ((*pix >> (bit + 4)) & 1);
+               val = (val << 1) | ((*pix >> bit) & 1);
+               pix++;
+       }
+
+       return val;
+}
+
+void wrtile4bpp(FILE *fp, unsigned char *tpix, struct image *timg)
+{
+       int i, x, y, curx = 0;
+       unsigned char *ptr;
+       unsigned int val;
+
+       /* for each pair of bitplanes */
+       for(i=0; i<timg->bpp/2; i++) {
+               ptr = tpix;
+               for(y=0; y<tile_ysz; y++) {
+                       for(x=0; x<tile_xsz/8; x++) {
+                               val = packbitplane4bpp(ptr, i * 2);
+                               val |= packbitplane4bpp(ptr, i * 2 + 1) << 8;
+
+                               if(curx == 0) {
+                                       curx = 7 + fprintf(fp, "\t.word $%x", (unsigned int)val);
+                               } else {
+                                       curx += fprintf(fp, ", $%x", (unsigned int)val);
+                               }
+                               if(curx >= 75) {
+                                       fputc('\n', fp);
+                                       curx = 0;
+                               }
+                       }
+                       ptr += tile_xsz * timg->bpp / 8;
+               }
+       }
+
+       if(curx != 0) {
+               fputc('\n', fp);
+       }
+}
+
+void wrtiles(FILE *fp, const char *name, struct image *timg, int ntiles)
+{
+       int i, j;
+       unsigned char *ptr = timg->pixels;
+
+       fprintf(fp, "\n\t%s_num_tiles = %d\n", name, ntiles);
+       fprintf(fp, "\t%s_tiles_width = %d\n", name, timg->width);
+       fprintf(fp, "\t%s_tiles_height = %d\n", name, timg->height);
+       fprintf(fp, "\t%s_tiles_bpp = %d\n", name, timg->bpp);
+       fprintf(fp, "%s_tiles:\n", name);
+
+       for(i=0; i<ntiles; i++) {
+               fprintf(fp, "\t; tile %d:", i);
+               for(j=0; j<timg->scansz * tile_ysz; j++) {
+                       fprintf(fp, " %x", ptr[j]);
+               }
+               fputc('\n', fp);
+
+               switch(timg->bpp) {
+               case 4:
+                       wrtile4bpp(fp, ptr, timg);
+                       break;
+
+               default:
+                       fprintf(stderr, "unsupported bpp: %d\n", timg->bpp);
+                       return;
+               }
+               ptr += timg->scansz * tile_ysz;
+               fputc('\n', fp);
+       }
+
+       fputc('\n', fp);
+}
+
+void wrtilemap(FILE *fp, const char *name, int *tmap, int xtiles, int ytiles)
+{
+       int i, sz, curx = 0;
+
+       fprintf(fp, "\n\t%s_tilemap_cols = %d\n", name, xtiles);
+       fprintf(fp, "\t%s_tilemap_rows = %d\n", name, ytiles);
+       fprintf(fp, "%s_tilemap:\n", name);
+
+       sz = xtiles * ytiles;
+       for(i=0; i<sz; i++) {
+               if(curx == 0) {
+                       curx = 7 + fprintf(fp, "\t.word %u", (unsigned int)tmap[i]);
+               } else {
+                       curx += fprintf(fp, ", %u", (unsigned int)tmap[i]);
+               }
+               if(curx >= 75) {
+                       fprintf(fp, "\n");
+                       curx = 0;
+               }
+       }
+
+       fprintf(fp, "\n\n");
+}
+
+void wrpalette(FILE *fp, const char *name, struct cmapent *cmap, int ncol)
+{
+       int i;
+
+       fprintf(fp, "\n\t%s_cmap_colors = %d\n", name, ncol);
+       fprintf(fp, "%s_cmap:\n", name);
+       for(i=0; i<ncol; i++) {
+               unsigned int col = (cmap[i].r * 31 / 255) | ((cmap[i].g * 31 / 255) << 5) |
+                       ((cmap[i].b * 31 / 255) << 10);
+               fprintf(fp, "\t.word $%04x\n", col);
+       }
+       fprintf(fp, "\n\n");
+}
diff --git a/snes.ld b/snes.ld
new file mode 100644 (file)
index 0000000..4cf5207
--- /dev/null
+++ b/snes.ld
@@ -0,0 +1,12 @@
+MEMORY {
+       WRAM: start = 0, size = $2000;
+       ROM: start = $8000, size = $8000;
+}
+
+SEGMENTS {
+       code: load = ROM, type = ro;
+       rodata: load = ROM, type = ro;
+       data: load = ROM, run = WRAM, type = rw, define = yes;
+       bss: load = WRAM, type = bss, define = yes;
+       carthdr: load = ROM, start = $ffc0, type = ro;
+}
diff --git a/test.asm b/test.asm
new file mode 100644 (file)
index 0000000..9919308
--- /dev/null
+++ b/test.asm
@@ -0,0 +1,224 @@
+       .p816
+       .include "hwregs.inc"
+
+       .segment "rodata"
+       .include "data.inc"
+
+       .segment "code"
+
+       vmem_tiles_offs = 4096
+
+       sei
+       clc
+       xce
+
+       rep #$10
+       .i16
+       ldx #$1fff
+       txs
+       sep #$10
+       .i8
+
+       jsr snes_init
+       setreg REG_BGMODE, $02  ; mode 2, 8x8 tiles
+       setreg REG_BG12NBA, $1  ; tiles at offs 4kb
+       setreg REG_TM, $1       ; main screen: BG1
+
+       ldx #0
+cmap_loop:
+       txa
+       lsr
+       sta REG_CGADD
+       lda logo4bpp_cmap,x
+       inx
+       sta REG_CGDATA
+       lda logo4bpp_cmap,x
+       inx
+       sta REG_CGDATA
+       cpx #32         ; 16 entries 2 bytes each
+       bne cmap_loop
+
+       pea logo4bpp_tiles_width * logo4bpp_tiles_height / 2
+       pea logo4bpp_tiles
+       pea vmem_tiles_offs
+       jsr copy_vmem
+       rep #$20
+       .a16
+       pla
+       pla
+       pla
+       sep #$20
+       .a8
+
+       pea logo4bpp_tilemap_rows * logo4bpp_tilemap_cols * 2
+       pea logo4bpp_tilemap
+       pea 0
+       jsr copy_vmem
+       rep #$20
+       .a16
+       pla
+       pla
+       pla
+       sep #$20
+       .a8
+
+       fblank 0
+
+halt:
+       jmp halt
+
+       ; copy_vmem(vmem_offset, src, num_words)
+copy_vmem:
+       rep #$30        ; 16bit accumulator and index registers
+       .a16
+       .i16
+       phd             ; save d
+       tsc             ; and make it point to the stack
+       tcd
+       sep #$20        ; restore 8bit accum
+       .a8
+       ; stack frame
+       ; $1 saved D
+       ; $3 return address
+       ; $5 vmem_offs
+       ; $7 src
+       ; $9 num_words
+
+       lda #$80        ; auto incerment after wiriting high byte
+       sta REG_VMAINC
+       lda $5
+       sta REG_VMADDL
+       lda $6
+       sta REG_VMADDH
+       ldy #0
+@loop:
+       lda ($7),y
+       sta REG_VMDATAL
+       iny
+       lda ($7),y
+       sta REG_VMDATAH
+       iny
+       cpy $9
+       bne @loop
+       
+       pld
+       sep #$10        ; back to 8bit index registers
+       .i8
+       rts
+
+
+snes_init:
+       fblank 1
+       stz REG_OBSEL
+       stz REG_OAMADDL
+       stz REG_OAMADDH
+       stz REG_OAMDATA
+       stz REG_OAMDATA
+       stz REG_BGMODE
+       stz REG_MOSAIC
+       stz REG_BG1SC
+       stz REG_BG2SC
+       stz REG_BG3SC
+       stz REG_BG4SC
+       stz REG_BG12NBA
+       stz REG_BG34NBA
+       stz REG_BG1HOFFS
+       stz REG_BG1HOFFS
+       stz REG_BG1VOFFS
+       stz REG_BG1VOFFS
+       stz REG_BG2HOFFS
+       stz REG_BG2HOFFS
+       stz REG_BG2VOFFS
+       stz REG_BG2VOFFS
+       stz REG_BG3HOFFS
+       stz REG_BG3HOFFS
+       stz REG_BG3VOFFS
+       stz REG_BG3VOFFS
+       stz REG_BG4HOFFS
+       stz REG_BG4HOFFS
+       stz REG_BG4VOFFS
+       stz REG_BG4VOFFS
+       setreg REG_VMAINC, $80
+       stz REG_VMADDL
+       stz REG_VMADDH
+       stz REG_VMDATAL
+       stz REG_VMDATAH
+       stz REG_M7SEL
+       stz REG_M7A
+       stz REG_M7A
+       stz REG_M7B
+       stz REG_M7B
+       stz REG_M7C
+       stz REG_M7C
+       stz REG_M7D
+       stz REG_M7D
+       stz REG_M7X
+       stz REG_M7X
+       stz REG_M7Y
+       stz REG_M7Y
+       stz REG_CGADD
+       stz REG_CGDATA
+       stz REG_CGDATA
+       stz REG_W12SEL
+       stz REG_W34SEL
+       stz REG_WH0
+       stz REG_WH1
+       stz REG_WH2
+       stz REG_WH3
+       stz REG_WBGLOG
+       stz REG_WOBJLOG
+       stz REG_TM
+       stz REG_TS
+       stz REG_TMW
+       stz REG_TSW
+       setreg REG_CGWSEL, $30
+       stz REG_CGADSUB
+       setreg REG_COLDATA, $e0
+       stz REG_SETINI
+       stz REG_NMITIMEN
+       setreg REG_WRIO, $ff
+       stz REG_WRMPYA
+       stz REG_WRMPYB
+       stz REG_WRDIVL
+       stz REG_WRDIVH
+       stz REG_WRDIVB
+       stz REG_HTIMEL
+       stz REG_HTIMEH
+       stz REG_VTIMEL
+       stz REG_VTIMEH
+       stz REG_MDMAEN
+       stz REG_HDMAEN
+       stz REG_MEMSEL
+       rts
+
+       ; cartridge header
+       .segment "carthdr"
+       .byte "TEST                 "
+       .byte $20       ; fast ROM, LoROM mapping
+       .byte 0         ; ROM only
+       .byte 1         ; ROM banks 1 = 32k
+       .byte 0         ; RAM size
+       .byte 2         ; country europe/oceania/asia
+       .byte 0         ; developer id: none
+       .byte 0         ; ROM version
+       .word $ffff     ; checksum complement
+       .word 0         ; checksum
+       ; 65816 vectors
+       .word 0, 0
+       .word 0         ; cop
+       .word 0         ; brk
+       .word 0         ; abort
+       .word 0         ; NMI (vblank)
+       .word 0
+       .word 0         ; IRQ
+       ; 6502 vectors
+       .word 0, 0
+       .word 0         ; cop
+       .word 0
+       .word 0         ; abort
+       .word 0         ; NMI (vblank)
+       .word $8000     ; reset
+       .word 0         ; IRQ/BRK
+
+
+       ; vi:ft=asm_ca65: