From 2e12a37bceebef3787abddc1ca10d82fdc427e9d Mon Sep 17 00:00:00 2001 From: Michel Pollet Date: Sun, 18 Feb 2024 10:32:33 +0000 Subject: [PATCH] Floppy bitstream view, also major refactor of UI code Split the UI into separate bits. + Bits that are X11/GLX only + Bits that are 'pure' opengl + Bits that are UI related but not renderer related Hopefully with help porting to other platformsI Signed-off-by: Michel Pollet --- Makefile | 7 +- README.md | 10 + docs/heat_map.png | Bin 0 -> 90130 bytes libmui/mui/mui.h | 12 +- libmui/mui/mui_controls.c | 2 +- libmui/mui/mui_drawable.c | 3 + src/drivers/mii_disk2.c | 99 +++++-- src/drivers/mii_disk2.h | 9 - src/drivers/mii_epromcard.c | 10 +- src/drivers/mii_smartport.c | 10 +- src/drivers/mii_ssc.c | 6 +- src/format/mii_floppy.c | 97 ++++--- src/format/mii_floppy.h | 45 +++- src/mii_mish.c | 3 +- src/mii_slot.h | 4 +- ui_gl/mii_emu_gl.c | 506 ++++++------------------------------ ui_gl/mii_loadbin.c | 3 +- ui_gl/mii_mui.c | 193 ++++++++++++++ ui_gl/mii_mui.h | 56 +++- ui_gl/mii_mui_2dsk.c | 25 +- ui_gl/mii_mui_gl.c | 446 +++++++++++++++++++++++++++++++ ui_gl/mii_mui_gl.h | 23 ++ ui_gl/mii_mui_menus.c | 39 +-- 23 files changed, 1038 insertions(+), 570 deletions(-) create mode 100644 docs/heat_map.png delete mode 100644 src/drivers/mii_disk2.h create mode 100644 ui_gl/mii_mui.c create mode 100644 ui_gl/mii_mui_gl.c create mode 100644 ui_gl/mii_mui_gl.h diff --git a/Makefile b/Makefile index 1d21c6f..52c7148 100644 --- a/Makefile +++ b/Makefile @@ -129,8 +129,13 @@ lsp: -include $(O)/*.d -include $(O)/obj/*.d +DESTDIR := /usr/local install: mkdir -p $(DESTDIR)/bin - mkdir -p $(DESTDIR)/share/games/mii/ cp $(BIN)/mii_emu_gl $(DESTDIR)/bin/ + +avail: + mkdir -p $(DESTDIR)/bin + rm -f $(DESTDIR)/bin/mii_emu_gl && \ + ln -sf $(realpath $(BIN)/mii_emu_gl) $(DESTDIR)/bin/mii_emu_gl diff --git a/README.md b/README.md index 724369c..1ae5be1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,16 @@

# MII Version Changelog +## 1.7 + * New animated about box, because, everyone loves a good about box. + * Added support for Write Protect of floppy disks; disk can be write protected manually, or if the file lacks write permissions, OR if the file format (NIB, DSK) doesn't support writes. + * New fancypants 'bit view' of the floppy are they are read/written, with a + heat map to show where the head was last. Drive 1 appears left of the screen, + drive 2 on the right. It fades out after the drive motor stops. + +![Heat map disk viewq](docs/heat_map.png) +*DOS 3.3 Disk 'bitstream view' on the left, the green trace shows what's just be read.* + ## 1.6 * Another big update, yanked the old DiskII driver, and replaced it with a homebrew one, low level, with native support for WOZ (1 & 2) files (*read AND write!*) as well as NIB and DSK (read only). diff --git a/docs/heat_map.png b/docs/heat_map.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e8c57d0ee3a4bd355fac2b836a664feb587d41 GIT binary patch literal 90130 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ÙY-^ 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 literal 0 HcmV?d00001 diff --git a/libmui/mui/mui.h b/libmui/mui/mui.h index cfbb27b..d452c27 100644 --- a/libmui/mui/mui.h +++ b/libmui/mui/mui.h @@ -64,6 +64,7 @@ enum mui_key_e { MUI_KEY_RALT, MUI_KEY_RSUPER, MUI_KEY_LSUPER, + MUI_KEY_CAPSLOCK, MUI_KEY_MODIFIERS_LAST, MUI_KEY_F1 = 0x100, MUI_KEY_F2, @@ -330,7 +331,11 @@ typedef struct mui_drawable_t { dispose_pixels : 1, dispose_drawable : 1; // not used internally, but useful for the application - unsigned int texture_id; + struct { + float opacity; + c2_pt_t size; + unsigned int id; + } texture; // (default) position in destination when drawing c2_pt_t origin; mui_clip_stack_t clip; @@ -364,6 +369,11 @@ mui_drawable_init( void mui_drawable_dispose( mui_drawable_t * dr); +// Clear, but do not dispose of the drawable +void +mui_drawable_clear( + mui_drawable_t * dr); + // get/allocate a pixman structure for this drawable union pixman_image * mui_drawable_get_pixman( diff --git a/libmui/mui/mui_controls.c b/libmui/mui/mui_controls.c index bd57f2f..cd5e334 100644 --- a/libmui/mui/mui_controls.c +++ b/libmui/mui/mui_controls.c @@ -148,7 +148,7 @@ _mui_control_highlight_timer_cb( { mui_control_t * c = param; - printf("%s: %s\n", __func__, c->title); +// printf("%s: %s\n", __func__, c->title); mui_control_set_state(c, MUI_CONTROL_STATE_NORMAL); if (c->cdef) c->cdef(c, MUI_CDEF_SELECT, NULL); diff --git a/libmui/mui/mui_drawable.c b/libmui/mui/mui_drawable.c index 17d3689..045457b 100644 --- a/libmui/mui/mui_drawable.c +++ b/libmui/mui/mui_drawable.c @@ -73,6 +73,9 @@ mui_drawable_clear( mui_clip_stack_clear(&dr->clip); if (dr->pix.pixels && dr->dispose_pixels) free(dr->pix.pixels); + static const mui_pixmap_t zero = {}; + dr->pix = zero; + dr->dispose_pixels = 0; dr->_pix_hash = NULL; } diff --git a/src/drivers/mii_disk2.c b/src/drivers/mii_disk2.c index ffc9940..32b241f 100644 --- a/src/drivers/mii_disk2.c +++ b/src/drivers/mii_disk2.c @@ -13,7 +13,7 @@ #include #include "mii.h" #include "mii_bank.h" -#include "mii_disk2.h" + #include "mii_rom_disk2.h" #include "mii_woz.h" #include "mii_floppy.h" @@ -62,8 +62,8 @@ _mii_floppy_motor_off_cb( { mii_card_disk2_t *c = param; mii_floppy_t *f = &c->floppy[c->selected]; - printf("%s drive %d off\n", __func__, c->selected); - if (c->drive[c->selected].file && f->tracks_dirty) +// printf("%s drive %d off\n", __func__, c->selected); + if (c->drive[c->selected].file && f->seed_dirty != f->seed_saved) mii_floppy_update_tracks(f, c->drive[c->selected].file); f->motor = 0; return 0; @@ -103,21 +103,25 @@ _mii_disk2_switch_track( mii_floppy_t *f = &c->floppy[c->selected]; int qtrack = f->qtrack + delta; if (qtrack < 0) qtrack = 0; - if (qtrack >= 35 * 4) qtrack = (35 * 4) -1; + if (qtrack >= MII_FLOPPY_TRACK_COUNT * 4) + qtrack = (MII_FLOPPY_TRACK_COUNT * 4) -1; if (qtrack == f->qtrack) return f->qtrack; uint8_t track_id = f->track_id[f->qtrack]; - if (track_id != MII_FLOPPY_RANDOM_TRACK_ID) - printf("NEW TRACK D%d: %d\n", c->selected, track_id); +// if (track_id != 0xff) +// printf("NEW TRACK D%d: %d\n", c->selected, track_id); uint8_t track_id_new = f->track_id[qtrack]; - + if (track_id_new >= MII_FLOPPY_TRACK_COUNT) + track_id_new = MII_FLOPPY_NOISE_TRACK; /* adapt the bit position from one track to the others, from WOZ specs */ - uint32_t track_size = f->tracks[track_id].bit_count; - uint32_t new_size = f->tracks[track_id_new].bit_count; - uint32_t new_pos = f->bit_position * new_size / track_size; - f->bit_position = new_pos; + if (track_id_new != MII_FLOPPY_NOISE_TRACK) { + uint32_t track_size = f->tracks[track_id].bit_count; + uint32_t new_size = f->tracks[track_id_new].bit_count; + uint32_t new_pos = f->bit_position * new_size / track_size; + f->bit_position = new_pos; + } f->qtrack = qtrack; return f->qtrack; } @@ -208,6 +212,13 @@ _mii_disk2_access( case 0x0C: case 0x0D: c->lss_mode = (c->lss_mode & ~(1 << LOAD_BIT)) | (!!on << LOAD_BIT); + if (!(c->lss_mode & (1 << WRITE_BIT)) && f->heat) { + uint8_t track_id = f->track_id[f->qtrack]; + uint32_t byte_index = f->bit_position >> 3; + unsigned int dstb = byte_index / MII_FLOPPY_HM_HIT_SIZE; + f->heat->read.map[track_id][dstb] = 255; + f->heat->read.seed++; + } break; case 0x0E: case 0x0F: @@ -230,19 +241,23 @@ static int _mii_disk2_command( mii_t * mii, struct mii_slot_t *slot, - uint8_t cmd, + uint32_t cmd, void * param) { mii_card_disk2_t *c = slot->drv_priv; + int res = -1; switch (cmd) { case MII_SLOT_DRIVE_COUNT: - if (param) + if (param) { *(int *)param = 2; + res = 0; + } break; case MII_SLOT_DRIVE_WP ... MII_SLOT_DRIVE_WP + 2 - 1: { int drive = cmd - MII_SLOT_DRIVE_WP; int *wp = param; if (wp) { + res = 0; printf("Drive %d WP: 0x%x set %s\n", drive, c->floppy[drive].write_protected, *wp ? "ON" : "OFF"); @@ -271,9 +286,18 @@ _mii_disk2_command( mii_floppy_init(&c->floppy[drive]); mii_dd_drive_load(&c->drive[drive], file); mii_floppy_load(&c->floppy[drive], file); + res = 0; + } break; + case MII_SLOT_D2_GET_FLOPPY: { + if (param) { + mii_floppy_t ** fp = param; + fp[0] = &c->floppy[0]; + fp[1] = &c->floppy[1]; + res = 0; + } } break; } - return 0; + return res; } static mii_slot_drv_t _driver = { @@ -372,16 +396,16 @@ _mii_disk2_lss_tick( if (c->clock >= f->bit_timing) { c->clock -= f->bit_timing; uint8_t track_id = f->track_id[f->qtrack]; - + uint8_t * track = f->track_data[track_id]; uint32_t byte_index = f->bit_position >> 3; uint8_t bit_index = 7 - (f->bit_position & 7); if (!(c->lss_mode & (1 << WRITE_BIT))) { - uint8_t bit = f->tracks[track_id].data[byte_index]; + uint8_t bit = track[byte_index]; bit = (bit >> bit_index) & 1; c->head = (c->head << 1) | bit; // see WOZ spec for how we do this here if ((c->head & 0xf) == 0) { - bit = f->tracks[MII_FLOPPY_RANDOM_TRACK_ID].data[byte_index]; + bit = f->track_data[MII_FLOPPY_NOISE_TRACK][byte_index]; bit = (bit >> bit_index) & 1; // printf("RANDOM TRACK %2d %2d %2d : %d\n", // track_id, byte_index, bit_index, bit); @@ -390,16 +414,20 @@ _mii_disk2_lss_tick( } c->lss_mode = (c->lss_mode & ~(1 << RP_BIT)) | (bit << RP_BIT); } - if ((c->lss_mode & (1 << WRITE_BIT))) { + if ((c->lss_mode & (1 << WRITE_BIT)) && track_id != 0xff) { uint8_t msb = c->data_register >> 7; - +#if 0 + printf("WRITE %2d %4d %d : %d LSS State %x mode %x\n", + track_id, byte_index, bit_index, msb, + c->lss_state, c->lss_mode); +#endif if (!f->tracks[track_id].dirty) { - printf("DIRTY TRACK %2d \n", track_id); + // printf("DIRTY TRACK %2d \n", track_id); f->tracks[track_id].dirty = 1; - f->tracks_dirty = 1; + f->seed_dirty++; } - f->tracks[track_id].data[byte_index] &= ~(1 << bit_index); - f->tracks[track_id].data[byte_index] |= (msb << bit_index); + f->track_data[track_id][byte_index] &= ~(1 << bit_index); + f->track_data[track_id][byte_index] |= (msb << bit_index); } f->bit_position = (f->bit_position + 1) % f->tracks[track_id].bit_count; } @@ -418,9 +446,17 @@ _mii_disk2_lss_tick( c->data_register = (c->data_register >> 1) | (!!f->write_protected << 7); break; - case 3: // LD + case 3: {// LD + uint8_t track_id = f->track_id[f->qtrack]; c->data_register = c->write_register; - break; + f->seed_dirty++; + if (f->heat) { + uint32_t byte_index = f->bit_position >> 3; + unsigned int dstb = byte_index/MII_FLOPPY_HM_HIT_SIZE; + f->heat->write.map[track_id][dstb] = 255; + f->heat->write.seed++; + } + } break; } } else { // CLR c->data_register = 0; @@ -448,8 +484,9 @@ _mii_mish_d2( for (int i = 0; i < 2; i++) { mii_floppy_t *f = &c->floppy[i]; printf("Drive %d %s\n", f->id, f->write_protected ? "WP" : "RW"); - printf("\tMotor: %3s qtrack:%d Bit %6d\n", - f->motor ? "ON" : "OFF", f->qtrack, f->bit_position); + printf("\tMotor: %3s qtrack:%3d Bit %6d/%6d\n", + f->motor ? "ON" : "OFF", f->qtrack, + f->bit_position, f->tracks[0].bit_count); } return; } @@ -480,7 +517,7 @@ _mii_mish_d2( count = atoi(argv[3]); mii_card_disk2_t *c = _mish_d2; mii_floppy_t *f = &c->floppy[sel]; - uint8_t *data = f->tracks[track].data; + uint8_t *data = f->track_data[track]; for (int i = 0; i < count; i += 8) { uint8_t *line = data + i; @@ -503,6 +540,12 @@ _mii_mish_d2( } return; } + if (!strcmp(argv[1], "dirty")) { + mii_card_disk2_t *c = _mish_d2; + mii_floppy_t *f = &c->floppy[sel]; + f->seed_dirty = f->seed_saved = rand(); + return; + } } #include "mish.h" diff --git a/src/drivers/mii_disk2.h b/src/drivers/mii_disk2.h deleted file mode 100644 index a90d993..0000000 --- a/src/drivers/mii_disk2.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * mii_disk2.h - * - * Copyright (C) 2023 Michel Pollet - * - * SPDX-License-Identifier: MIT - */ - -#pragma once diff --git a/src/drivers/mii_epromcard.c b/src/drivers/mii_epromcard.c index 8bf663b..3c54499 100644 --- a/src/drivers/mii_epromcard.c +++ b/src/drivers/mii_epromcard.c @@ -103,14 +103,17 @@ static int _mii_ee_command( mii_t * mii, struct mii_slot_t *slot, - uint8_t cmd, + uint32_t cmd, void * param) { mii_card_ee_t *c = slot->drv_priv; + int res = -1; switch (cmd) { case MII_SLOT_DRIVE_COUNT: - if (param) + if (param) { *(int *)param = 1; + res = 0; + } break; case MII_SLOT_DRIVE_LOAD: const char *filename = param; @@ -122,9 +125,10 @@ _mii_ee_command( } mii_dd_drive_load(&c->drive[0], file); c->file = file ? file->map : (uint8_t*)mii_1mb_rom_data; + res = 0; break; } - return 0; + return res; } static mii_slot_drv_t _driver = { diff --git a/src/drivers/mii_smartport.c b/src/drivers/mii_smartport.c index e280c57..01b36fd 100644 --- a/src/drivers/mii_smartport.c +++ b/src/drivers/mii_smartport.c @@ -292,14 +292,17 @@ static int _mii_sm_command( mii_t * mii, struct mii_slot_t *slot, - uint8_t cmd, + uint32_t cmd, void * param) { mii_card_sm_t *c = slot->drv_priv; + int res = -1; switch (cmd) { case MII_SLOT_DRIVE_COUNT: - if (param) + if (param) { *(int *)param = MII_SM_DRIVE_COUNT; + res = 0; + } break; case MII_SLOT_DRIVE_LOAD ... MII_SLOT_DRIVE_LOAD + MII_SM_DRIVE_COUNT - 1: int drive = cmd - MII_SLOT_DRIVE_LOAD; @@ -311,9 +314,10 @@ _mii_sm_command( return -1; } mii_dd_drive_load(&c->drive[drive], file); + res = 0; break; } - return 0; + return res; } static uint8_t diff --git a/src/drivers/mii_ssc.c b/src/drivers/mii_ssc.c index c3e5bee..2fe5eb7 100644 --- a/src/drivers/mii_ssc.c +++ b/src/drivers/mii_ssc.c @@ -119,17 +119,19 @@ static int _mii_ssc_command( mii_t * mii, struct mii_slot_t *slot, - uint8_t cmd, + uint32_t cmd, void * param) { // mii_card_ssc_t *c = slot->drv_priv; + int res = -1; switch (cmd) { case MII_SLOT_SSC_SET_TTY: { const char * tty = param; printf("%s: set tty %s\n", __func__, tty); + res = 0; } break; } - return -1; + return res; } static mii_slot_drv_t _driver = { diff --git a/src/format/mii_floppy.c b/src/format/mii_floppy.c index 6fdae72..55afd24 100644 --- a/src/format/mii_floppy.c +++ b/src/format/mii_floppy.c @@ -25,7 +25,7 @@ mii_floppy_init( f->bit_timing = 32; f->qtrack = 15; // just to see something at seek time f->bit_position = 0; - f->tracks_dirty = 0; + f->seed_dirty = f->seed_saved = 0; f->write_protected &= ~MII_FLOPPY_WP_MANUAL;// keep the manual WP bit /* this will look like this; ie half tracks are 'random' 0: 0 1: 0 2:35 3: 1 @@ -34,9 +34,9 @@ mii_floppy_init( */ for (int i = 0; i < (int)sizeof(f->track_id); i++) f->track_id[i] = ((i + 1) % 4) == 3 ? - MII_FLOPPY_RANDOM_TRACK_ID : ((i + 2) / 4); + MII_FLOPPY_NOISE_TRACK : ((i + 2) / 4); /* generate a buffer with about 30% one bits */ - uint8_t *random = f->tracks[MII_FLOPPY_RANDOM_TRACK_ID].data; + uint8_t *random = f->track_data[MII_FLOPPY_NOISE_TRACK]; memset(random, 0, 256); uint32_t bits = 256 * 8; uint32_t ones = bits * 0.3; // 30% ones @@ -48,29 +48,39 @@ mii_floppy_init( random[bit >> 3] |= (1 << (bit & 7)); ones--; } - // we also fill up the last (ramdom) track with copies of the first - // 256 uint8_ts of itself, to make it look like a real track + // copy all that random stuff across the rest of the 'track' int rbi = 0; - for (int i = 0; i < 36; i++) { + for (int bi = 256; bi < MII_FLOPPY_DEFAULT_TRACK_SIZE; bi++) + random[bi] = random[rbi++ % 256]; + // important, the +1 means we initialize the random track too + for (int i = 0; i < MII_FLOPPY_TRACK_COUNT + 1; i++) { f->tracks[i].dirty = 0; - f->tracks[i].bit_count = MII_FLOPPY_DEFAULT_TRACK_SIZE * 8; + f->tracks[i].bit_count = 6500 * 8; // fill the whole array up to the end.. - for (int bi = 0; bi < (int)sizeof(f->tracks[i].data); bi++) - f->tracks[i].data[bi] = random[rbi++ % 256]; + uint8_t *track = f->track_data[i]; + if (i != MII_FLOPPY_NOISE_TRACK) { +#if 1 + memset(track, 0, MII_FLOPPY_DEFAULT_TRACK_SIZE); +#else + for (int bi = 0; bi < MII_FLOPPY_DEFAULT_TRACK_SIZE; bi++) + track[bi] = random[rbi++ % 256]; +#endif + } } } static void mii_track_write_bits( mii_floppy_track_t * dst, + uint8_t * track_data, uint8_t bits, uint8_t count ) { while (count--) { - uint32_t uint8_t_index = dst->bit_count >> 3; + uint32_t byte_index = dst->bit_count >> 3; uint8_t bit_index = 7 - (dst->bit_count & 7); - dst->data[uint8_t_index] &= ~(1 << bit_index); - dst->data[uint8_t_index] |= (!!(bits >> 7) << bit_index); + track_data[byte_index] &= ~(1 << bit_index); + track_data[byte_index] |= (!!(bits >> 7) << bit_index); dst->bit_count++; bits <<= 1; } @@ -91,8 +101,9 @@ static uint8_t _de44(uint8_t a, uint8_t b) { static void mii_nib_rebit_track( - uint8_t *track, - mii_floppy_track_t * dst) + uint8_t *src_track, + mii_floppy_track_t * dst, + uint8_t * dst_track) { dst->bit_count = 0; uint32_t window = 0; @@ -100,18 +111,18 @@ mii_nib_rebit_track( int seccount = 0; int state = 0; // look for address field do { - window = (window << 8) | track[srci++]; + window = (window << 8) | src_track[srci++]; switch (state) { case 0: { if (window != 0xffd5aa96) break; for (int i = 0; i < (seccount == 0 ? 40 : 20); i++) - mii_track_write_bits(dst, 0xff, 10); - uint8_t * h = track + srci - 4; // incs first 0xff - int tid = _de44(h[6], h[7]); - int sid = _de44(h[8], h[9]); - printf("Track %2d sector %2d\n", tid, sid); - memcpy(dst->data + (dst->bit_count >> 3), track + srci - 4, 15); + mii_track_write_bits(dst, dst_track, 0xff, 10); + uint8_t * h = src_track + srci - 4; // incs first 0xff + // int tid = _de44(h[6], h[7]); + // int sid = _de44(h[8], h[9]); + // printf("Track %2d sector %2d\n", tid, sid); + memcpy(dst_track + (dst->bit_count >> 3), h, 15); dst->bit_count += 15 * 8; srci += 11; state = 1; @@ -120,9 +131,9 @@ mii_nib_rebit_track( if (window != 0xffd5aaad) break; for (int i = 0; i < 4; i++) - mii_track_write_bits(dst, 0xff, 10); - uint8_t *h = track + srci - 4; - memcpy(dst->data + (dst->bit_count >> 3), h, 4 + 342 + 4); + mii_track_write_bits(dst, dst_track, 0xff, 10); + uint8_t *h = src_track + srci - 4; + memcpy(dst_track + (dst->bit_count >> 3), h, 4 + 342 + 4); dst->bit_count += (4 + 342 + 4) * 8; srci += 4 + 342; seccount++; @@ -141,7 +152,7 @@ mii_floppy_load_nib( printf("%s: loading NIB %s\n", __func__, filename); for (int i = 0; i < 35; i++) { uint8_t *track = file->map + (i * 6656); - mii_nib_rebit_track(track, &f->tracks[i]); + mii_nib_rebit_track(track, &f->tracks[i], f->track_data[i]); if (f->tracks[i].bit_count < 100) { printf("%s: %s: Invalid track %d has zero bits!\n", __func__, filename, i); @@ -185,7 +196,8 @@ mii_floppy_write_track_woz( trks->track[track_id].bit_count_le = htole32(f->tracks[track_id].bit_count); uint32_t byte_count = (le32toh(trks->track[track_id].bit_count_le) + 7) >> 3; - memcpy(trks->track[track_id].bits, f->tracks[track_id].data, byte_count); + memcpy(trks->track[track_id].bits, + f->track_data[track_id], byte_count); trks->track[track_id].byte_count_le = htole16(byte_count); } else { mii_woz2_info_t *info = (mii_woz2_info_t *)(header + 1); @@ -199,7 +211,7 @@ mii_floppy_write_track_woz( trks->track[track_id].bit_count_le = htole32(f->tracks[track_id].bit_count); uint32_t byte_count = (le32toh(trks->track[track_id].bit_count_le) + 7) >> 3; - memcpy(track, f->tracks[track_id].data, byte_count); + memcpy(track, f->track_data[track_id], byte_count); } f->tracks[track_id].dirty = 0; return 0; @@ -213,12 +225,10 @@ mii_floppy_woz_load_tmap( uint64_t used_tracks = 0; int tmap_size = le32toh(tmap->chunk.size_le); for (int ti = 0; ti < (int)sizeof(f->track_id) && ti < tmap_size; ti++) { - if (tmap->track_id[ti] == 0xff) { - f->track_id[ti] = MII_FLOPPY_RANDOM_TRACK_ID; - continue; - } - f->track_id[ti] = tmap->track_id[ti]; - used_tracks |= 1L << f->track_id[ti]; + f->track_id[ti] = tmap->track_id[ti] == 0xff ? + MII_FLOPPY_NOISE_TRACK : tmap->track_id[ti]; + if (tmap->track_id[ti] != 0xff) + used_tracks |= 1L << f->track_id[ti]; } return used_tracks; } @@ -259,13 +269,13 @@ mii_floppy_load_woz( (char*)&trks->chunk.id_le, le32toh(trks->chunk.size_le)); #endif int max_track = le32toh(trks->chunk.size_le) / sizeof(trks->track[0]); - for (int i = 0; i < 35 && i < max_track; i++) { + for (int i = 0; i < MII_FLOPPY_TRACK_COUNT && i < max_track; i++) { uint8_t *track = trks->track[i].bits; if (!(used_tracks & (1L << i))) { // printf("WOZ: Track %d not used\n", i); continue; } - memcpy(f->tracks[i].data, track, le16toh(trks->track[i].byte_count_le)); + memcpy(f->track_data[i], track, le16toh(trks->track[i].byte_count_le)); f->tracks[i].bit_count = le32toh(trks->track[i].bit_count_le); } } else { @@ -289,7 +299,7 @@ mii_floppy_load_woz( #endif /* TODO: this doesn't work yet... */ // f->bit_timing = info->optimal_bit_timing; - for (int i = 0; i < 35; i++) { + for (int i = 0; i < MII_FLOPPY_TRACK_COUNT; i++) { if (!(used_tracks & (1L << i))) { // printf("WOZ: Track %d not used\n", i); continue; @@ -297,7 +307,7 @@ mii_floppy_load_woz( uint8_t *track = file->map + (le16toh(trks->track[i].start_block_le) << 9); uint32_t byte_count = (le32toh(trks->track[i].bit_count_le) + 7) >> 3; - memcpy(f->tracks[i].data, track, byte_count); + memcpy(f->track_data[i], track, byte_count); f->tracks[i].bit_count = le32toh(trks->track[i].bit_count_le); } } @@ -434,7 +444,7 @@ mii_floppy_load_dsk( mii_floppy_nibblize_sector(VOLUME_NUMBER, i, phys_sector, &writePtr, track); } - mii_nib_rebit_track(nibbleBuf, &f->tracks[i]); + mii_nib_rebit_track(nibbleBuf, &f->tracks[i], f->track_data[i]); } free(nibbleBuf); // DSK is read only @@ -452,23 +462,23 @@ mii_floppy_update_tracks( return -1; if (f->write_protected & MII_FLOPPY_WP_RO_FILE) return -1; - if (!f->tracks_dirty) + if (f->seed_dirty == f->seed_saved) return 0; - for (int i = 0; i < 35; i++) { + for (int i = 0; i < MII_FLOPPY_TRACK_COUNT; i++) { if (!f->tracks[i].dirty) continue; - printf("%s: track %d is dirty, saving\n", __func__, i); +// printf("%s: track %d is dirty, saving\n", __func__, i); switch (file->format) { case MII_DD_FILE_NIB: break; case MII_DD_FILE_WOZ: mii_floppy_write_track_woz(f, file, i); - printf("%s: WOZ track %d updated\n", __func__, i); +// printf("%s: WOZ track %d updated\n", __func__, i); break; } f->tracks[i].dirty = 0; } - f->tracks_dirty = 0; + f->seed_saved = f->seed_dirty; return 0; } @@ -498,5 +508,6 @@ mii_floppy_load( f->write_protected |= MII_FLOPPY_WP_RO_FILE; else f->write_protected &= ~MII_FLOPPY_WP_RO_FILE; + f->seed_dirty = f->seed_saved = rand(); return res; } diff --git a/src/format/mii_floppy.h b/src/format/mii_floppy.h index 1220248..7e35f6f 100644 --- a/src/format/mii_floppy.h +++ b/src/format/mii_floppy.h @@ -13,9 +13,7 @@ // for NIB and others. can be bigger on .WOZ #define MII_FLOPPY_DEFAULT_TRACK_SIZE 6656 -// track containing random bits -#define MII_FLOPPY_RANDOM_TRACK_ID 35 - +#define MII_FLOPPY_TRACK_COUNT 35 /* * Reasons for write protect. Ie checkbox in the UI, or file format * doesn't support writes, or the file has no write permissions. @@ -29,9 +27,31 @@ enum { typedef struct mii_floppy_track_t { uint8_t dirty : 1; // track has been written to uint32_t bit_count; - uint8_t data[6680]; // max suggested by WOZ spec } mii_floppy_track_t; + +// 32 bytes of track data corresponds to one byte of heatmap +#define MII_FLOPPY_HM_HIT_SIZE 32 +// thats 208 bytes per track or about 7KB*2 for the whole disk for read+write +// we align it on 16 bytes to make it easier to use in a shader +#define MII_FLOPPY_HM_TRACK_SIZE \ + (((MII_FLOPPY_DEFAULT_TRACK_SIZE / MII_FLOPPY_HM_HIT_SIZE) + 15) & ~15) + +typedef struct mii_track_heatmap_t { + // 32 bytes of track data corresponds to one byte of heatmap + uint32_t seed, tex, cleared; + // this needs to be aligned, otherwise SSE code will die horribly + uint8_t map[MII_FLOPPY_TRACK_COUNT][MII_FLOPPY_HM_TRACK_SIZE] + __attribute__((aligned(16))); +} mii_track_heatmap_t; + +typedef struct mii_floppy_heatmap_t { + mii_track_heatmap_t read, write; +} mii_floppy_heatmap_t; + +// +#define MII_FLOPPY_NOISE_TRACK MII_FLOPPY_TRACK_COUNT + typedef struct mii_floppy_t { uint8_t write_protected : 3, id : 2; uint8_t bit_timing; // 0=32 (default) @@ -39,9 +59,16 @@ typedef struct mii_floppy_t { uint8_t stepper; // last step we did... uint8_t qtrack; // quarter track we are on uint32_t bit_position; - uint8_t tracks_dirty; // needs saving - uint8_t track_id[35 * 4]; - mii_floppy_track_t tracks[36]; + // this is incremented each time a track is marked dirty + uint32_t seed_dirty; + uint32_t seed_saved; // last seed we saved at + uint8_t track_id[MII_FLOPPY_TRACK_COUNT * 4]; + mii_floppy_track_t tracks[MII_FLOPPY_TRACK_COUNT + 1]; + // keep all the data together, we'll use it to make a texture + // the last track is used for noise + uint8_t track_data[MII_FLOPPY_TRACK_COUNT + 1][MII_FLOPPY_DEFAULT_TRACK_SIZE]; + /* This is set by the UI to trakc the head movements, no functional use */ + mii_floppy_heatmap_t * heat; // optional heatmap } mii_floppy_t; /* @@ -54,8 +81,8 @@ mii_floppy_init( int mii_floppy_load( - mii_floppy_t *f, - mii_dd_file_t *file ); + mii_floppy_t *f, + mii_dd_file_t *file ); int mii_floppy_update_tracks( diff --git a/src/mii_mish.c b/src/mii_mish.c index 9909a5e..8b0053d 100644 --- a/src/mii_mish.c +++ b/src/mii_mish.c @@ -51,7 +51,8 @@ _mii_mish_cmd( int argc, const char * argv[]) { - const char * state[] = { "RUNNING", "STOPPED", "STEP" }; + const char * state[] = { + "INIT", "RUNNING", "STOPPED", "STEP", "TERMINATE"}; mii_t * mii = param; if (!argv[1]) { show_state: diff --git a/src/mii_slot.h b/src/mii_slot.h index 58732b1..9afc798 100644 --- a/src/mii_slot.h +++ b/src/mii_slot.h @@ -53,7 +53,7 @@ typedef struct mii_slot_drv_t { int (*command)( mii_t * mii, struct mii_slot_t *slot, - uint8_t cmd, + uint32_t cmd, void * param); } mii_slot_drv_t; @@ -80,6 +80,8 @@ enum { MII_SLOT_DRIVE_WP = 0x30, // + drive index 0...n MII_SLOT_SSC_SET_TTY = 0x10, // param is a pathname, or NULL for a pty + // + drive index 0..1. Param is a mii_floppy_t ** + MII_SLOT_D2_GET_FLOPPY = 0x40, }; // send a command to a slot/driver. Return >=0 if ok, -1 if error diff --git a/ui_gl/mii_emu_gl.c b/ui_gl/mii_emu_gl.c index be426af..9e438a5 100644 --- a/ui_gl/mii_emu_gl.c +++ b/ui_gl/mii_emu_gl.c @@ -5,6 +5,9 @@ * * SPDX-License-Identifier: MIT */ +/* + * This is the main file for the X11/GLX version of the MII emulator + */ #define _GNU_SOURCE // for asprintf #include #include @@ -12,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -24,69 +26,38 @@ #include "mish.h" #include "mii_thread.h" -#include "mii_mui.h" -#include "minipt.h" +#include "mii_mui_gl.h" #include "miigl_counter.h" #define MII_ICON64_DEFINE #include "mii-icon-64.h" /* - * Note: This *assumes* that the GL implementation has support for non-power-of-2 - * textures, which is not a given for older implementations. However, I think - * (by 2024) that's a safe assumption. + * Note: This *assumes* that the GL implementation has support for + * non-power-of-2 * textures, which is not a given for older + * implementations. However, I think (by 2024) that's a safe assumption. */ #define WINDOW_WIDTH 1280 #define WINDOW_HEIGHT 720 -#define POWER_OF_TWO 0 - -typedef struct mii_gl_tex_t { -// c2_rect_t frame; - GLuint id; -} mii_gl_tex_t; +#define MII_MUI_GL_POW2 0 typedef struct mii_x11_t { mii_mui_t video; pthread_t cpu_thread; - mui_drawable_t dr; // drawable - uint32_t dr_padded_y; - - union { - struct { - mii_gl_tex_t mii_tex, mui_tex; - }; - mii_gl_tex_t tex[2]; - }; - - c2_rect_t video_frame; // current video frame - float mui_alpha; - void * transision_state; - struct { - mui_time_t start, end; - c2_rect_t from, to; - } transition; - Cursor cursor; Display * dpy; Window win; - long last_button_click; - struct { - int ungrab, grab, grabbed, down; - c2_pt_t pos; - } mouse; - mui_event_t key; XVisualInfo * vis; Colormap cmap; XSetWindowAttributes swa; - XWindowAttributes attr; GLXFBConfig fbc; Atom wm_delete_window; int width, height; GLXContext glContext; - miigl_counter_t videoc, redrawc, sleepc; +// miigl_counter_t videoc, redrawc, sleepc; } mii_x11_t; @@ -120,97 +91,6 @@ has_gl_extension( return false; } -c2_rect_t -c2_rect_interpolate( - c2_rect_t *a, - c2_rect_t *b, - float t) -{ - c2_rect_t r = {}; - r.l = 0.5 + a->l + (b->l - a->l) * t; - r.r = 0.5 + a->r + (b->r - a->r) * t; - r.t = 0.5 + a->t + (b->t - a->t) * t; - r.b = 0.5 + a->b + (b->b - a->b) * t; - return r; -} - -static c2_rect_t -_mii_get_video_position( - mii_x11_t * ui, - bool ui_visible ) -{ - c2_rect_t r = C2_RECT(0, 0, MII_VIDEO_WIDTH, MII_VIDEO_HEIGHT); - if (ui_visible) { - float fac = (ui->attr.height - 38) / (float)MII_VIDEO_HEIGHT; - c2_rect_scale(&r, fac); - c2_rect_offset(&r, - (ui->attr.width / 2) - (c2_rect_width(&r) / 2), 36); - } else { - float fac = (ui->attr.height) / (float)MII_VIDEO_HEIGHT; - c2_rect_scale(&r, fac); - c2_rect_offset(&r, - (ui->attr.width / 2) - (c2_rect_width(&r) / 2), - (ui->attr.height / 2) - (c2_rect_height(&r) / 2)); - c2_rect_inset(&r, 10, 10); - } - return r; -} - -static void -_mii_transition( - mii_x11_t * ui ) -{ - pt_start(ui->transision_state); - - while (ui->video.transition == MII_MUI_TRANSITION_NONE) - pt_yield(ui->transision_state); - - ui->transition.start = mui_get_time(); - ui->transition.end = ui->transition.start + (MUI_TIME_SECOND / 2); - ui->transition.from = ui->video_frame; - - switch (ui->video.transition) { - case MII_MUI_TRANSITION_HIDE_UI: - ui->transition.to = _mii_get_video_position(ui, false); - ui->video.mui_visible = true; - break; - case MII_MUI_TRANSITION_SHOW_UI: - ui->transition.to = _mii_get_video_position(ui, true); - ui->video.mui_visible = true; - break; - } - while (1) { - mui_time_t now = mui_get_time(); - float t = (now - ui->transition.start) / - (float)(ui->transition.end - ui->transition.start); - if (t >= 1.0f) - break; - switch (ui->video.transition) { - case MII_MUI_TRANSITION_HIDE_UI: - ui->mui_alpha = 1.0f - t; - break; - case MII_MUI_TRANSITION_SHOW_UI: - ui->mui_alpha = t; - break; - } - ui->video_frame = c2_rect_interpolate( - &ui->transition.from, &ui->transition.to, t); - pt_yield(ui->transision_state); - } - switch (ui->video.transition) { - case MII_MUI_TRANSITION_HIDE_UI: - ui->video.mui_visible = false; - ui->mui_alpha = 0.0f; - break; - case MII_MUI_TRANSITION_SHOW_UI: - ui->mui_alpha = 1.0f; - break; - } - ui->video.transition = MII_MUI_TRANSITION_NONE; - - pt_end(ui->transision_state); -} - /* * xmodmap -pke or -pk will print the list of keycodes */ @@ -244,6 +124,7 @@ _mii_x11_convert_keycode( case XK_Alt_R: out->key.key = MUI_KEY_RALT; break; case XK_Super_L: out->key.key = MUI_KEY_LSUPER; break; case XK_Super_R: out->key.key = MUI_KEY_RSUPER; break; + case XK_Caps_Lock: out->key.key = MUI_KEY_CAPSLOCK; break; default: out->key.key = sym & 0xff; break; @@ -256,7 +137,7 @@ static int mii_x11_init( struct mii_x11_t *ui ) { - mui_t * mui = &ui->video.mui; +// mui_t * mui = &ui->video.mui; if (!setlocale(LC_ALL,"") || !XSupportsLocale() || @@ -355,6 +236,18 @@ mii_x11_init( sizeof(mii_icon64) / sizeof(mii_icon64[0])); XFlush(ui->dpy); } + { + XSizeHints *hints = XAllocSizeHints(); + hints->flags = PMinSize | PAspect;// | PMaxSize; + hints->min_width = WINDOW_WIDTH / 2; + hints->min_height = WINDOW_HEIGHT / 2; + hints->max_aspect.x = WINDOW_WIDTH; + hints->max_aspect.y = WINDOW_HEIGHT; + hints->min_aspect.x = WINDOW_WIDTH; + hints->min_aspect.y = WINDOW_HEIGHT; + XSetWMNormalHints(ui->dpy, ui->win, hints); + XFree(hints); + } XMapWindow(ui->dpy, ui->win); ui->wm_delete_window = XInternAtom(ui->dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(ui->dpy, ui->win, &ui->wm_delete_window, 1); @@ -401,92 +294,15 @@ mii_x11_init( ui->glContext = create_context(ui->dpy, ui->fbc, 0, True, attr); } } - XSync(ui->dpy, False); XSetErrorHandler(old_handler); if (gl_err || !ui->glContext) die("[X11]: Failed to create an OpenGL context\n"); glXMakeCurrent(ui->dpy, ui->win, ui->glContext); } - { // create the MUI 'screen' at the window size - mui_pixmap_t* pix = &ui->dr.pix; - pix->size.y = WINDOW_HEIGHT; - pix->size.x = WINDOW_WIDTH; - // annoyingly I have to make it a LOT bigger to handle that the - // non-power-of-2 texture extension is not avialable everywhere - // textures, which is a bit of a waste of memory, but oh well. - -#if POWER_OF_TWO - int padded_x = 1; - int padded_y = 1; - while (padded_x < pix->size.x) - padded_x <<= 1; - while (padded_y < pix->size.y) - padded_y <<= 1; -#else - int padded_x = pix->size.x; - int padded_y = pix->size.y; -#endif - pix->row_bytes = padded_x * 4; - pix->bpp = 32; - - ui->dr_padded_y = padded_y; - printf("MUI Padded UI size is %dx%d\n", padded_x, padded_y); - - pix->pixels = malloc(pix->row_bytes * ui->dr_padded_y); - mui->screen_size = pix->size; - } - { - XGetWindowAttributes(ui->dpy, ui->win, &ui->attr); - ui->mui_alpha = 1.0f; - ui->video.mui_visible = true; - ui->video_frame = _mii_get_video_position(ui, ui->video.mui_visible); - } return 0; } -static void -mii_x11_update_mouse_card( - mii_x11_t * ui) -{ - mii_t * mii = &ui->video.mii; - mui_t * mui = &ui->video.mui; - /* - * We can grab the mouse if it is enabled by the driver, it is in the - * video frame, and there is no active MUI windows (or menus). - */ - if (mii->mouse.enabled && - c2_rect_contains_pt(&ui->video_frame, &ui->mouse.pos) && - !(ui->video.mui_visible && mui_has_active_windows(mui))) { - if (!ui->mouse.grabbed) { - ui->mouse.grab = 1; - ui->mouse.grabbed = 1; - // printf("Grab mouse\n"); - } - } else { - if (ui->mouse.grabbed) { - ui->mouse.ungrab = 1; - ui->mouse.grabbed = 0; - // printf("Ungrab mouse\n"); - } - } - if (!ui->mouse.grabbed) - return; - double x = ui->mouse.pos.x - ui->video_frame.l; - double y = ui->mouse.pos.y - ui->video_frame.t; - // get mouse button state - int button = ui->mouse.down; - // clamp coordinates inside bounds - double vw = c2_rect_width(&ui->video_frame); - double vh = c2_rect_height(&ui->video_frame); - double mw = mii->mouse.max_x - mii->mouse.min_x; - double mh = mii->mouse.max_y - mii->mouse.min_y; - // normalize mouse coordinates - mii->mouse.x = mii->mouse.min_x + (x * mw / vw) + 0.5; - mii->mouse.y = mii->mouse.min_y + (y * mh / vh) + 0.5; - mii->mouse.button = button; -} - static int mii_x11_handle_event( mii_x11_t * ui, @@ -496,15 +312,16 @@ mii_x11_handle_event( /* We don't actually 'grab' as in warp the pointer, we just show/hide it * dynamically when we enter/leave the video rectangle */ - if (ui->mouse.grab) { + if (ui->video.mouse.grab) { XDefineCursor(ui->dpy, ui->win, ui->cursor); - ui->mouse.grab = 0; - } else if (ui->mouse.ungrab) { + ui->video.mouse.grab = 0; + } else if (ui->video.mouse.ungrab) { XUndefineCursor(ui->dpy, ui->win); - ui->mouse.ungrab = 0; + ui->video.mouse.ungrab = 0; } mui_t * mui = &ui->video.mui; mii_t * mii = &ui->video.mii; + switch (evt->type) { case FocusIn: case FocusOut: @@ -517,28 +334,31 @@ mii_x11_handle_event( case KeyRelease: case KeyPress: { int ret, down = (evt->type == KeyPress); - KeySym *code = XGetKeyboardMapping(ui->dpy, ( - KeyCode)evt->xkey.keycode, 1, &ret); - ui->key.type = down ? MUI_EVENT_KEYDOWN : MUI_EVENT_KEYUP; - ui->key.key.up = 0; + KeySym *code = XGetKeyboardMapping(ui->dpy, + (KeyCode)evt->xkey.keycode, 1, &ret); + ui->video.key.type = down ? MUI_EVENT_KEYDOWN : MUI_EVENT_KEYUP; + ui->video.key.key.up = 0; bool handled = false; - bool converted = _mii_x11_convert_keycode(ui, *code, &ui->key); - bool is_modifier = ui->key.key.key >= MUI_KEY_MODIFIERS && - ui->key.key.key <= MUI_KEY_MODIFIERS_LAST; + bool converted = _mii_x11_convert_keycode(ui, *code, + &ui->video.key); + bool is_modifier = ui->video.key.key.key >= MUI_KEY_MODIFIERS && + ui->video.key.key.key <= MUI_KEY_MODIFIERS_LAST; if (converted) { // convert keycodes into a bitfields of current modifiers - if (ui->key.key.key >= MUI_KEY_MODIFIERS && - ui->key.key.key <= MUI_KEY_MODIFIERS_LAST) { + if (ui->video.key.key.key >= MUI_KEY_MODIFIERS && + ui->video.key.key.key <= MUI_KEY_MODIFIERS_LAST) { if (down) - mui->modifier_keys |= (1 << (ui->key.key.key - MUI_KEY_MODIFIERS)); + mui->modifier_keys |= (1 << + (ui->video.key.key.key - MUI_KEY_MODIFIERS)); else - mui->modifier_keys &= ~(1 << (ui->key.key.key - MUI_KEY_MODIFIERS)); + mui->modifier_keys &= ~(1 << + (ui->video.key.key.key - MUI_KEY_MODIFIERS)); } - ui->key.modifiers = mui->modifier_keys; - switch (ui->key.key.key) { + ui->video.key.modifiers = mui->modifier_keys; + switch (ui->video.key.key.key) { case MUI_KEY_RSUPER: case MUI_KEY_LSUPER: { - int apple = ui->key.key.key - MUI_KEY_RSUPER; + int apple = ui->video.key.key.key - MUI_KEY_RSUPER; mii_bank_t *bank = &mii->bank[MII_BANK_MAIN]; uint8_t old = mii_bank_peek(bank, 0xc061 + apple); mii_bank_poke(bank, 0xc061 + apple, down ? 0x80 : 0); @@ -548,7 +368,7 @@ mii_x11_handle_event( } } break; } - handled = mui_handle_event(mui, &ui->key); + handled = mui_handle_event(mui, &ui->video.key); // if not handled and theres a window visible, assume // it's a dialog and it OUGHT to eat the key if (!handled) @@ -556,7 +376,7 @@ mii_x11_handle_event( // printf("%s key handled %d\n", __func__, handled); } if (!handled && down && !is_modifier) { - uint16_t mii_key = ui->key.key.key; + uint16_t mii_key = ui->video.key.key.key; char buf[32] = ""; KeySym keysym = 0; if (XLookupString((XKeyEvent*)evt, buf, 32, &keysym, NULL) != NoSymbol) { @@ -583,23 +403,23 @@ mii_x11_handle_event( case ButtonRelease: { // printf("%s %s button %d grabbed:%d\n", __func__, // evt->type == ButtonPress ? "Down":"Up ", - // evt->xbutton.button, ui->mouse.grabbed); + // evt->xbutton.button, ui->video.mouse.grabbed); switch (evt->xbutton.button) { case Button1: { - ui->mouse.down = evt->type == ButtonPress; - ui->mouse.pos.x = evt->xbutton.x; - ui->mouse.pos.y = evt->xbutton.y; + ui->video.mouse.down = evt->type == ButtonPress; + ui->video.mouse.pos.x = evt->xbutton.x; + ui->video.mouse.pos.y = evt->xbutton.y; if (ui->video.mui_visible) { mui_event_t ev = { - .type = ui->mouse.down ? + .type = ui->video.mouse.down ? MUI_EVENT_BUTTONDOWN : MUI_EVENT_BUTTONUP, - .mouse.where = ui->mouse.pos, + .mouse.where = ui->video.mouse.pos, .modifiers = mui->modifier_keys, // | MUI_MODIFIER_EVENT_TRACE, }; mui_handle_event(mui, &ev); } - mii_x11_update_mouse_card(ui); + mii_mui_update_mouse_card(&ui->video); } break; case Button4: case Button5: { @@ -609,7 +429,7 @@ mii_x11_handle_event( mui_event_t ev = { .type = MUI_EVENT_WHEEL, .modifiers = mui->modifier_keys,// | MUI_MODIFIER_EVENT_TRACE, - .wheel.where = ui->mouse.pos, + .wheel.where = ui->video.mouse.pos, .wheel.delta = evt->xbutton.button == Button4 ? -1 : 1, }; mui_handle_event(mui, &ev); @@ -617,16 +437,27 @@ mii_x11_handle_event( } break; } } break; + case ConfigureNotify: + if (evt->xconfigure.width != ui->video.window_size.x || + evt->xconfigure.height != ui->video.window_size.y) { + // Window is being resized + // Handle the resize event here + ui->video.window_size.x = evt->xconfigure.width; + ui->video.window_size.y = evt->xconfigure.height; + ui->video.video_frame = mii_mui_get_video_position(&ui->video); + mii_mui_update_mouse_card(&ui->video); + } + break; case MotionNotify: { - ui->mouse.pos.x = evt->xmotion.x; - ui->mouse.pos.y = evt->xmotion.y; - mii_x11_update_mouse_card(ui); - if (ui->mouse.grabbed) + ui->video.mouse.pos.x = evt->xmotion.x; + ui->video.mouse.pos.y = evt->xmotion.y; + mii_mui_update_mouse_card(&ui->video); + if (ui->video.mouse.grabbed) break; if (ui->video.mui_visible) { mui_event_t ev = { .type = MUI_EVENT_DRAG, - .mouse.where = ui->mouse.pos, + .mouse.where = ui->video.mouse.pos, .modifiers = mui->modifier_keys, }; mui_handle_event(mui, &ev); @@ -655,133 +486,6 @@ mii_x11_terminate( XCloseDisplay(ui->dpy); } -void -mii_x11_prepare_textures( - mii_x11_t *ui) -{ - mii_t * mii = &ui->video.mii; -// mui_t * mui = &ui->video.mui; - GLuint tex[2]; - glGenTextures(2, tex); - for (int i = 0; i < 2; i++) { - mii_gl_tex_t * t = &ui->tex[i]; - memset(t, 0, sizeof(*t)); - t->id = tex[i]; - } - glEnable(GL_TEXTURE_2D); - // bind the mii texture using the GL_ARB_texture_rectangle extension - glBindTexture(GL_TEXTURE_2D, ui->mii_tex.id); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // disable the repeat of textures - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glTexImage2D(GL_TEXTURE_2D, 0, 4, - MII_VRAM_WIDTH, - MII_VRAM_HEIGHT, 0, GL_BGRA, // note BGRA here, not RGBA - GL_UNSIGNED_BYTE, - mii->video.pixels); - // bind the mui texture using the GL_ARB_texture_rectangle as well - glEnable(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, ui->mui_tex.id); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - glTexImage2D(GL_TEXTURE_2D, 0, 4, - ui->dr.pix.row_bytes / 4, // already power of two. - ui->dr_padded_y, 0, GL_RGBA, - GL_UNSIGNED_BYTE, - ui->dr.pix.pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - -// printf("%s texture created %d\n", __func__, mii_apple_screen_tex); -// display opengl error - GLenum err = glGetError(); - if (err != GL_NO_ERROR) { - printf("Error creating texture: %d\n", err); - } -} - -void -mii_x11_render( - mii_x11_t *ui) -{ - glClearColor( - .6f * ui->mui_alpha, - .6f * ui->mui_alpha, - .6f * ui->mui_alpha, - 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glPushAttrib(GL_ENABLE_BIT|GL_COLOR_BUFFER_BIT|GL_TRANSFORM_BIT); - glDisable(GL_CULL_FACE); - glDisable(GL_DEPTH_TEST); - glDisable(GL_SCISSOR_TEST); - glEnable(GL_BLEND); - glEnable(GL_TEXTURE_2D); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - /* setup viewport/project */ - glViewport(0, 0, (GLsizei)ui->attr.width, (GLsizei)ui->attr.height); - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - glLoadIdentity(); - glOrtho(0.0f, ui->attr.width, ui->attr.height, 0.0f, -1.0f, 1.0f); - glMatrixMode(GL_MODELVIEW); - glPushMatrix(); - glLoadIdentity(); - // This (was) the recommended way to handle pixel alignment in glOrtho - // mode, but this seems to have changed -- now it looks like Linear filtering -// glTranslatef(0.375f, 0.375f, 0.0f); - { - /* draw mii texture */ - glColor3f(1.0f, 1.0f, 1.0f); - glBindTexture(GL_TEXTURE_2D, ui->mii_tex.id); - glBegin(GL_QUADS); - c2_rect_t r = ui->video_frame; - glTexCoord2f(0, 0); - glVertex2f(r.l, r.t); - glTexCoord2f(MII_VIDEO_WIDTH / (double)MII_VRAM_WIDTH, 0); - glVertex2f(r.r, r.t); - glTexCoord2f(MII_VIDEO_WIDTH / (double)MII_VRAM_WIDTH, - MII_VIDEO_HEIGHT / (double)MII_VRAM_HEIGHT); - glVertex2f(r.r, r.b); - glTexCoord2f(0, - MII_VIDEO_HEIGHT / (double)MII_VRAM_HEIGHT); - glVertex2f(r.l, r.b); - glEnd(); - /* draw mui texture */ - if (ui->mui_alpha > 0.0f) { - glColor4f(1.0f, 1.0f, 1.0f, ui->mui_alpha); - glBindTexture(GL_TEXTURE_2D, ui->mui_tex.id); - glBegin(GL_QUADS); - glTexCoord2f(0, 0); glVertex2f(0, 0); - glTexCoord2f(ui->attr.width / (double)(ui->dr.pix.row_bytes / 4), 0); - glVertex2f(ui->attr.width, 0); - glTexCoord2f(ui->attr.width / (double)(ui->dr.pix.row_bytes / 4), - ui->attr.height / (double)(ui->dr_padded_y)); - glVertex2f(ui->attr.width, ui->attr.height); - glTexCoord2f(0, - ui->attr.height / (double)(ui->dr_padded_y)); - glVertex2f(0, ui->attr.height); - glEnd(); - } - } - glDisable(GL_CULL_FACE); - glDisable(GL_DEPTH_TEST); - glDisable(GL_SCISSOR_TEST); - glDisable(GL_BLEND); - glDisable(GL_TEXTURE_2D); - - glBindTexture(GL_TEXTURE_2D, 0); - glMatrixMode(GL_MODELVIEW); - glPopMatrix(); - glMatrixMode(GL_PROJECTION); - glPopMatrix(); - glPopAttrib(); -} - // TODO factor this into a single table, this is dupped from mii_mui_settings.c! static const struct { const char * name; @@ -885,6 +589,7 @@ mii_x11_reload_config( _mii_ui_load_config(mii, config, &flags); mii_prepare(mii, flags); mii_reset(mii, true); + mii_mui_gl_prepare_textures(&ui->video); /* start the CPU/emulator thread */ ui->cpu_thread = mii_threads_start(mii); @@ -902,7 +607,8 @@ main( mkdir(conf_path, 0755); mii_x11_t * ui = &g_mii; - mii_t * mii = &g_mii.video.mii; + mii_t * mii = &g_mii.video.mii; + mui_t * mui = &g_mii.video.mui; bool no_config_found = false; if (mii_settings_load( @@ -940,17 +646,11 @@ main( printf("MISH_TELNET_PORT = %s\n", getenv("MISH_TELNET_PORT")); } mii_x11_init(ui); - mui_t * mui = &ui->video.mui; // to move to a function later - mui_init(mui); - mui->color.clear.value = 0; + mii_mui_init(&ui->video, C2_PT(WINDOW_WIDTH, WINDOW_HEIGHT)); + mii_mui_gl_init(&ui->video); + asprintf(&mui->pref_directory, "%s/.local/share/mii", getenv("HOME")); - mii_mui_menus_init((mii_mui_t*)ui); - ui->video.mui_visible = 1; - mii_mui_menu_slot_menu_update(&ui->video); - - mii_x11_prepare_textures(ui); - // use a 60fps timerfd here as well int timerfd = timerfd_create(CLOCK_MONOTONIC, 0); if (timerfd < 0) { @@ -980,53 +680,13 @@ main( continue; mii_x11_handle_event(ui, &evt); } - mui_run(mui); - bool draw = false; - if (pixman_region32_not_empty(&mui->inval)) { - draw = true; - mui_draw(mui, &ui->dr, 0); - glBindTexture(GL_TEXTURE_2D, ui->mui_tex.id); - - pixman_region32_intersect_rect(&mui->redraw, &mui->redraw, - 0, 0, ui->dr.pix.size.x, ui->dr.pix.size.y); - int rc = 0; - c2_rect_t *ra = (c2_rect_t*)pixman_region32_rectangles(&mui->redraw, &rc); - // rc = 1; ra = &C2_RECT(0, 0, mui->screen_size.x, mui->screen_size.y); - if (rc) { - // printf("GL: %d rects to redraw\n", rc); - for (int i = 0; i < rc; i++) { - c2_rect_t r = ra[i]; - // printf("GL: %d,%d %dx%d\n", r.l, r.t, c2_rect_width(&r), c2_rect_height(&r)); - glPixelStorei(GL_UNPACK_ROW_LENGTH, ui->dr.pix.row_bytes / 4); - glTexSubImage2D(GL_TEXTURE_2D, 0, r.l, r.t, - c2_rect_width(&r), c2_rect_height(&r), - GL_BGRA, GL_UNSIGNED_BYTE, - ui->dr.pix.pixels + (r.t * ui->dr.pix.row_bytes) + (r.l * 4)); - } - } - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - pixman_region32_clear(&mui->redraw); - } - uint32_t current_frame = mii->video.frame_count; - if (current_frame != mii->video.frame_drawn) { - miigl_counter_tick(&ui->videoc, miigl_get_time()); - draw = true; - mii->video.frame_drawn = current_frame; - // update the whole texture - glBindTexture(GL_TEXTURE_2D, ui->mii_tex.id); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, - MII_VRAM_WIDTH, - MII_VIDEO_HEIGHT, GL_RGBA, - GL_UNSIGNED_BYTE, - mii->video.pixels); - } - /* Draw */ + bool draw = mii_mui_gl_run(&ui->video); if (draw) { - miigl_counter_tick(&ui->redrawc, miigl_get_time()); - XGetWindowAttributes(ui->dpy, ui->win, &ui->attr); - glViewport(0, 0, ui->width, ui->height); - _mii_transition(ui); - mii_x11_render(ui); + // miigl_counter_tick(&ui->redrawc, miigl_get_time()); + // XGetWindowAttributes(ui->dpy, ui->win, &ui->attr); + glViewport(0, 0, ui->video.window_size.x, ui->video.window_size.y); + mii_mui_showhide_ui_machine(&ui->video); + mii_mui_gl_render(&ui->video); glFlush(); glXSwapBuffers(ui->dpy, ui->win); } diff --git a/ui_gl/mii_loadbin.c b/ui_gl/mii_loadbin.c index a4ead0f..a013433 100644 --- a/ui_gl/mii_loadbin.c +++ b/ui_gl/mii_loadbin.c @@ -6,7 +6,6 @@ * SPDX-License-Identifier: MIT */ - #include #include #include @@ -33,7 +32,7 @@ static void * mii_thread_loadbin( void *arg) { - + return NULL; } diff --git a/ui_gl/mii_mui.c b/ui_gl/mii_mui.c new file mode 100644 index 0000000..6e7cf1b --- /dev/null +++ b/ui_gl/mii_mui.c @@ -0,0 +1,193 @@ +/* + * mii_mui.c + * + * Copyright (C) 2024 Michel Pollet + * + * SPDX-License-Identifier: MIT + */ + +/* + * This contains the integration between the MII video and the MUI interface + * without any specific windowing system, it should be possible to use this + * with a native windowing system, or a portable one like SDL2 or GLFW + * This doesn't do anything to draw on screen, it just moves the video + * rectangle around, and handles the mouse mapping to the video frame. + */ +#include +#include + +#include "mii_mui.h" +#include "minipt.h" + +#define MII_MUI_GL_POW2 0 + +c2_rect_t +c2_rect_interpolate( + c2_rect_t *a, + c2_rect_t *b, + float t) +{ + c2_rect_t r = {}; + r.l = 0.5 + a->l + (b->l - a->l) * t; + r.r = 0.5 + a->r + (b->r - a->r) * t; + r.t = 0.5 + a->t + (b->t - a->t) * t; + r.b = 0.5 + a->b + (b->b - a->b) * t; + return r; +} + +c2_rect_t +mii_mui_get_video_position( + mii_mui_t * ui) +{ + c2_rect_t r = C2_RECT(0, 0, MII_VIDEO_WIDTH, MII_VIDEO_HEIGHT); + if (ui->mui_visible) { + float fac = (ui->window_size.y - 38) / (float)MII_VIDEO_HEIGHT; + c2_rect_scale(&r, fac); + c2_rect_offset(&r, + (ui->window_size.x / 2) - (c2_rect_width(&r) / 2), 36); + } else { + float fac = (ui->window_size.y) / (float)MII_VIDEO_HEIGHT; + c2_rect_scale(&r, fac); + c2_rect_offset(&r, + (ui->window_size.x / 2) - (c2_rect_width(&r) / 2), + (ui->window_size.y / 2) - (c2_rect_height(&r) / 2)); + c2_rect_inset(&r, 10, 10); + } + return r; +} + +void +mii_mui_showhide_ui_machine( + mii_mui_t * ui ) +{ + pt_start(ui->transision_state); + + while (ui->transition.state == MII_MUI_TRANSITION_NONE) + pt_yield(ui->transision_state); + + ui->transition.start = mui_get_time(); + ui->transition.end = ui->transition.start + (MUI_TIME_SECOND / 2); + ui->transition.from = ui->video_frame; + + switch (ui->transition.state) { + case MII_MUI_TRANSITION_HIDE_UI: + ui->mui_visible = false; + ui->transition.to = mii_mui_get_video_position(ui); + ui->mui_visible = true; + break; + case MII_MUI_TRANSITION_SHOW_UI: + ui->mui_visible = true; + ui->transition.to = mii_mui_get_video_position(ui); + break; + } + while (1) { + mui_time_t now = mui_get_time(); + float t = (now - ui->transition.start) / + (float)(ui->transition.end - ui->transition.start); + if (t >= 1.0f) + break; + switch (ui->transition.state) { + case MII_MUI_TRANSITION_HIDE_UI: + ui->mui_alpha = 1.0f - t; + break; + case MII_MUI_TRANSITION_SHOW_UI: + ui->mui_alpha = t; + break; + } + ui->video_frame = c2_rect_interpolate( + &ui->transition.from, &ui->transition.to, t); + pt_yield(ui->transision_state); + } + switch (ui->transition.state) { + case MII_MUI_TRANSITION_HIDE_UI: + ui->mui_visible = false; + ui->mui_alpha = 0.0f; + break; + case MII_MUI_TRANSITION_SHOW_UI: + ui->mui_alpha = 1.0f; + break; + } + ui->transition.state = MII_MUI_TRANSITION_NONE; + + pt_end(ui->transision_state); +} + +void +mii_mui_update_mouse_card( + mii_mui_t * ui) +{ + mii_t * mii = &ui->mii; + mui_t * mui = &ui->mui; + /* + * We can grab the mouse if it is enabled by the driver, it is in the + * video frame, and there is no active MUI windows (or menus). + */ + if (mii->mouse.enabled && + c2_rect_contains_pt(&ui->video_frame, &ui->mouse.pos) && + !(ui->mui_visible && mui_has_active_windows(mui))) { + if (!ui->mouse.grabbed) { + ui->mouse.grab = 1; + ui->mouse.grabbed = 1; + // printf("Grab mouse\n"); + } + } else { + if (ui->mouse.grabbed) { + ui->mouse.ungrab = 1; + ui->mouse.grabbed = 0; + // printf("Ungrab mouse\n"); + } + } + if (!ui->mouse.grabbed) + return; + double x = ui->mouse.pos.x - ui->video_frame.l; + double y = ui->mouse.pos.y - ui->video_frame.t; + // get mouse button state + int button = ui->mouse.down; + // clamp coordinates inside bounds + double vw = c2_rect_width(&ui->video_frame); + double vh = c2_rect_height(&ui->video_frame); + double mw = mii->mouse.max_x - mii->mouse.min_x; + double mh = mii->mouse.max_y - mii->mouse.min_y; + // normalize mouse coordinates + mii->mouse.x = mii->mouse.min_x + (x * mw / vw) + 0.5; + mii->mouse.y = mii->mouse.min_y + (y * mh / vh) + 0.5; + mii->mouse.button = button; +} + +void +mii_mui_init( + mii_mui_t * ui, + c2_pt_t window_size) +{ + mui_drawable_t * dr = &ui->pixels.mui; + // annoyingly I have to make it a LOT bigger to handle that the + // non-power-of-2 texture extension is not avialable everywhere + // textures, which is a bit of a waste of memory, but oh well. +#if MII_MUI_GL_POW2 + int padded_x = 1; + int padded_y = 1; + while (padded_x < window_size.x) + padded_x <<= 1; + while (padded_y < window_size.y) + padded_y <<= 1; +#else + int padded_x = window_size.x; + int padded_y = window_size.y; +#endif + mui_drawable_init(dr, C2_PT(padded_x, padded_y), 32, NULL, 0); + dr->texture.size = C2_PT(padded_x, padded_y); + printf("MUI Padded UI size is %dx%d\n", padded_x, padded_y); + ui->mui.screen_size = dr->pix.size; + + ui->window_size = window_size; + ui->mui_alpha = 1.0f; + ui->mui_visible = true; + ui->video_frame = mii_mui_get_video_position(ui); + + mui_t * mui = &ui->mui; + mui_init(mui); + mii_mui_menus_init(ui); + mii_mui_menu_slot_menu_update(ui); + // Tell libmui to clear the background with transparency. + mui->color.clear.value = 0; +} diff --git a/ui_gl/mii_mui.h b/ui_gl/mii_mui.h index 6be2a47..edbbedd 100644 --- a/ui_gl/mii_mui.h +++ b/ui_gl/mii_mui.h @@ -9,8 +9,8 @@ /* This tries to contains a structure that is the MUI interface over the MII video, but without any attachment to x11 or opengl. Basically hopefully - segregating the relevant logic without tying it to a specific windowing system. - + segregating the relevant logic without tying it to a specific windowing + system. Hopefully with a bit more work this OUGHT to allow Windows/macOS port with a native frontend. */ @@ -20,6 +20,7 @@ #include "mii.h" #include "mui.h" #include "mii_mui_settings.h" +#include "mii_floppy.h" enum mii_mui_transition_e { MII_MUI_TRANSITION_NONE, @@ -27,12 +28,46 @@ enum mii_mui_transition_e { MII_MUI_TRANSITION_SHOW_UI, }; +#define MII_PIXEL_LAYERS 8 + typedef struct mii_mui_t { mui_t mui; // mui interface mii_t mii; // apple II emulator + c2_pt_t window_size; + long last_button_click; + struct { + int ungrab, grab, grabbed, down; + c2_pt_t pos; + } mouse; + mui_event_t key; + c2_rect_t video_frame; // current video frame + float mui_alpha; bool mui_visible; - uint8_t transition; + void * transision_state; + struct { + uint8_t state; + mui_time_t start, end; + c2_rect_t from, to; + } transition; + unsigned int tex_id[MII_PIXEL_LAYERS]; + union { + struct { + mui_drawable_t mii; + mui_drawable_t mui; + struct { + mui_drawable_t bits; + mui_drawable_t hm_read; + mui_drawable_t hm_write; + } floppy[2]; + }; + mui_drawable_t v[MII_PIXEL_LAYERS]; + } pixels; + struct { + mii_floppy_t * floppy; + uint32_t seed_load; + float max_width; + } floppy[2]; mii_machine_config_t config; mii_loadbin_conf_t loadbin_conf; @@ -40,6 +75,10 @@ typedef struct mii_mui_t { mii_config_file_t cf; } mii_mui_t; +void +mii_mui_init( + mii_mui_t * ui, + c2_pt_t window_size); void mii_mui_menus_init( @@ -47,6 +86,17 @@ mii_mui_menus_init( void mii_mui_menu_slot_menu_update( mii_mui_t * ui); + +void +mii_mui_update_mouse_card( + mii_mui_t * ui); +void +mii_mui_showhide_ui_machine( + mii_mui_t * ui ); +c2_rect_t +mii_mui_get_video_position( + mii_mui_t * ui); + // slot can be <= 0 to open the machine dialog instead void mii_config_open_slots_dialog( diff --git a/ui_gl/mii_mui_2dsk.c b/ui_gl/mii_mui_2dsk.c index 20d8cca..12016f5 100644 --- a/ui_gl/mii_mui_2dsk.c +++ b/ui_gl/mii_mui_2dsk.c @@ -43,12 +43,12 @@ typedef struct mii_mui_2dsk_t { #include #include -typedef struct mii_floppy_check_t { +typedef struct mii_imagefile_check_t { char * error; char * warning; int file_ro; int file_ro_format; -} mii_floppy_check_t; +} mii_imagefile_check_t; #define NIB_SIZE 232960; #define DSK_SIZE 143360; @@ -64,7 +64,7 @@ _size_string( static int _mii_floppy_check_file( const char * path, - mii_floppy_check_t * out) + mii_imagefile_check_t * out) { char *filename = basename((char*)path); @@ -162,13 +162,15 @@ mii_mui_2dsk_load_conf( for (int i = 0; i < 2; i++) { if (config->drive[i].disk[0]) { ok = 1; - mii_floppy_check_t check = {}; - if (_mii_floppy_check_file(config->drive[i].disk, &check) < 0) { - mui_alert(m->win.ui, C2_PT(0,0), - "Invalid Disk Image", - check.error, MUI_ALERT_FLAG_OK); - free(check.error); - ok = 0; + mii_imagefile_check_t check = {}; + if (m->drive_kind == MII_2DSK_DISKII) { + if (_mii_floppy_check_file(config->drive[i].disk, &check) < 0) { + mui_alert(m->win.ui, C2_PT(0,0), + "Invalid Disk Image", + check.error, MUI_ALERT_FLAG_OK); + free(check.error); + ok = 0; + } } config->drive[i].ro_file = check.file_ro; config->drive[i].ro_format = check.file_ro_format; @@ -389,6 +391,9 @@ mii_mui_load_2dsk( cf, MUI_BUTTON_STYLE_CHECKBOX, "Write Protect", i == 0 ? MII_2DSK_WP1 : MII_2DSK_WP2); + // Smartport don't support write protect right now + if (drive_kind == MII_2DSK_SMARTPORT) + c->state = MUI_CONTROL_STATE_DISABLED; c2_rect_right_of(&cf, cf.r, margin * 0.5); cf.r = c2_rect_width(&w->frame) - margin * 1.2; m->drive[i].warning = c = mui_textbox_new(w, cf, diff --git a/ui_gl/mii_mui_gl.c b/ui_gl/mii_mui_gl.c new file mode 100644 index 0000000..3bade28 --- /dev/null +++ b/ui_gl/mii_mui_gl.c @@ -0,0 +1,446 @@ +/* + * mii_mui_gl.c + * + * Copyright (C) 2024 Michel Pollet + * + * SPDX-License-Identifier: MIT + */ + +/* + * This contains OpenGL code, no x11 or GLx allowed in here, this is to be + * used by a native windowing system, or a portable one like SDL2 or GLFW + */ + +#include +#include +#include + +#ifdef __SSE2__ +#include // SSE2 intrinsics +#endif + +#include "mii_mui_gl.h" +#include "mii_floppy.h" + + +typedef struct c2_rect_f { + float l,t,r,b; +} c2_rect_f; + +void +mii_mui_gl_init( + mii_mui_t *ui) +{ + GLuint tex[MII_PIXEL_LAYERS]; + glGenTextures(MII_PIXEL_LAYERS, tex); + for (int i = 0; i < MII_PIXEL_LAYERS; i++) { + printf("Texture %d created %d\n", i, tex[i]); + ui->pixels.v[i].texture.id = tex[i]; + ui->tex_id[i] = tex[i]; + } + + mii_mui_gl_prepare_textures(ui); +} + +static void +_prep_grayscale_texture( + mui_drawable_t * dr) +{ + dr->texture.size = dr->pix.size; + printf("Creating texture %4d %4dx%3d row_byte %4d\n", + dr->texture.id, + dr->pix.size.x, dr->pix.size.y, + dr->pix.row_bytes); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, 1, + dr->pix.row_bytes, dr->texture.size.y, 0, GL_LUMINANCE, + GL_UNSIGNED_BYTE, + dr->pix.pixels); +} + +void +mii_mui_gl_prepare_textures( + mii_mui_t *ui) +{ + mii_t * mii = &ui->mii; + + glEnable(GL_TEXTURE_2D); + mui_drawable_t * dr = &ui->pixels.mii; + // bind the mii texture using the GL_ARB_texture_rectangle extension + printf("Creating texture %4d %4dx%3d row_byte %4d (MII)\n", + dr->texture.id, + dr->pix.size.x, dr->pix.size.y, + dr->pix.row_bytes); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // disable the repeat of textures + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, 4, + MII_VRAM_WIDTH, + MII_VRAM_HEIGHT, 0, GL_BGRA, // note BGRA here, not RGBA + GL_UNSIGNED_BYTE, + mii->video.pixels); + + // bind the mui texture using the GL_ARB_texture_rectangle as well + dr = &ui->pixels.mui; + printf("Creating texture %4d %4dx%3d row_byte %4d (MUI)\n", + dr->texture.id, + dr->pix.size.x, dr->pix.size.y, + dr->pix.row_bytes); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, 4, + dr->pix.row_bytes / 4, // already power of two. + dr->texture.size.y, 0, GL_BGRA, + GL_UNSIGNED_BYTE, + dr->pix.pixels); + + mii_floppy_t * floppy[2] = {}; + for (int i = 0; i < 7; i++) { + if (mii_slot_command(mii, i, MII_SLOT_D2_GET_FLOPPY, floppy) == 0) + break; + } + if (floppy[0]) { + for (int fi = 0; fi < 2; fi++) { + mii_floppy_t * f = floppy[fi]; + ui->floppy[fi].floppy = f; + + dr = &ui->pixels.floppy[fi].bits; + // the init() call clears the structure, keep our id around + unsigned int tex = dr->texture.id; + mui_drawable_init(dr, + C2_PT(MII_FLOPPY_DEFAULT_TRACK_SIZE, MII_FLOPPY_TRACK_COUNT), + 8, floppy[fi]->track_data, MII_FLOPPY_DEFAULT_TRACK_SIZE); + dr->texture.id = tex; + _prep_grayscale_texture(dr); + if (!f->heat) + f->heat = calloc(1, sizeof(*f->heat)); + dr = &ui->pixels.floppy[fi].hm_read; + tex = dr->texture.id; + mui_drawable_init(dr, + C2_PT(MII_FLOPPY_HM_TRACK_SIZE, MII_FLOPPY_TRACK_COUNT), + 8, f->heat->read.map, MII_FLOPPY_HM_TRACK_SIZE); + dr->texture.id = tex; + _prep_grayscale_texture(dr); + dr = &ui->pixels.floppy[fi].hm_write; + tex = dr->texture.id; + mui_drawable_init(dr, + C2_PT(MII_FLOPPY_HM_TRACK_SIZE, MII_FLOPPY_TRACK_COUNT), + 8, f->heat->write.map, MII_FLOPPY_HM_TRACK_SIZE); + dr->texture.id = tex; + _prep_grayscale_texture(dr); + } + } else { + printf("No floppy found\n"); + for (int fi = 0; fi < 2; fi++) { + ui->floppy[fi].floppy = NULL; + mui_drawable_clear(&ui->pixels.floppy[fi].bits); + mui_drawable_clear(&ui->pixels.floppy[fi].hm_read); + mui_drawable_clear(&ui->pixels.floppy[fi].hm_write); + } + } +// printf("%s texture created %d\n", __func__, mii_apple_screen_tex); +// display opengl error + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + printf("Error creating texture: %d\n", err); + } +} + + +static void +_mii_decay_heatmap_one( + mii_track_heatmap_t *hm) +{ + uint32_t count = 0; +#ifdef __SSE2__ + const int size = (MII_FLOPPY_TRACK_COUNT * MII_FLOPPY_HM_TRACK_SIZE) / 16; + __m128i * hmw = (__m128i*)&hm->map[0]; + const __m128i s = _mm_set1_epi8(2); + for (int i = 0; i < size; i++) { + __m128i b = _mm_load_si128(hmw + i); + __m128i c = _mm_subs_epu8(b, s); + hmw[i] = c; + count += _mm_movemask_epi8(_mm_cmpgt_epi8(c, _mm_setzero_si128())); + } +#else + const int size = MII_FLOPPY_TRACK_COUNT * MII_FLOPPY_HM_TRACK_SIZE; + uint8_t * hmb = (uint8_t*)&hm->map[0]; + for (int i = 0; i < size; i++) { + uint8_t b = hmb[i]; + b = b > 2 ? b - 2 : 0; + hmb[i] = b; + count += !!b; + } +#endif + hm->cleared = count == 0; +} + +static void +_mii_decay_heatmap( + mii_floppy_heatmap_t *h) +{ + if (h->read.seed != h->read.tex || !h->read.cleared) { + h->read.tex = h->read.tex; + _mii_decay_heatmap_one(&h->read); + } + if (h->write.seed != h->write.tex || !h->write.cleared) { + h->write.tex = h->write.tex; + _mii_decay_heatmap_one(&h->write); + } +} + +bool +mii_mui_gl_run( + mii_mui_t *ui) +{ + mii_t * mii = &ui->mii; + mui_t * mui = &ui->mui; + + mui_run(mui); + bool draw = false; + if (pixman_region32_not_empty(&mui->inval)) { + draw = true; + mui_drawable_t * dr = &ui->pixels.mui; + mui_draw(mui, dr, 0); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + + pixman_region32_intersect_rect(&mui->redraw, &mui->redraw, + 0, 0, dr->pix.size.x, dr->pix.size.y); + int rc = 0; + c2_rect_t *ra = (c2_rect_t*)pixman_region32_rectangles(&mui->redraw, &rc); + // rc = 1; ra = &C2_RECT(0, 0, mui->screen_size.x, mui->screen_size.y); + if (rc) { + // printf("GL: %d rects to redraw\n", rc); + for (int i = 0; i < rc; i++) { + c2_rect_t r = ra[i]; + // printf("GL: %d,%d %dx%d\n", r.l, r.t, c2_rect_width(&r), c2_rect_height(&r)); + glPixelStorei(GL_UNPACK_ROW_LENGTH, dr->pix.row_bytes / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, r.l, r.t, + c2_rect_width(&r), c2_rect_height(&r), + GL_BGRA, GL_UNSIGNED_BYTE, + dr->pix.pixels + (r.t * dr->pix.row_bytes) + (r.l * 4)); + } + } + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + pixman_region32_clear(&mui->redraw); + } + uint32_t current_frame = mii->video.frame_count; + if (current_frame != mii->video.frame_drawn) { + // miigl_counter_tick(&ui->videoc, miigl_get_time()); + draw = true; + mii->video.frame_drawn = current_frame; + // update the whole texture + mui_drawable_t * dr = &ui->pixels.mii; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + MII_VRAM_WIDTH, + MII_VIDEO_HEIGHT, GL_BGRA, + GL_UNSIGNED_BYTE, + mii->video.pixels); + } + for (int fi = 0; fi < 2; fi++) { + if (!ui->floppy[fi].floppy) + continue; + mui_drawable_t * dr = NULL; + mii_floppy_t * f = ui->floppy[fi].floppy; + if (ui->floppy[fi].seed_load != f->seed_dirty) { + draw = true; + ui->floppy[fi].seed_load = f->seed_dirty; + // printf("Floppy %d: Reloading texture\n", fi); + dr = &ui->pixels.floppy[fi].bits; + int bc = (f->tracks[0].bit_count + 7) / 8; + int max = MII_FLOPPY_DEFAULT_TRACK_SIZE; + ui->floppy[fi].max_width = (double)bc / (double)max; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + dr->pix.row_bytes, dr->pix.size.y, + GL_LUMINANCE, GL_UNSIGNED_BYTE, + f->track_data); + } +// int rm = f->heat->read.tex != f->heat->read.seed; +// int wm = f->heat->write.tex != f->heat->write.seed; + _mii_decay_heatmap(f->heat); + glPixelStorei(GL_UNPACK_ROW_LENGTH, MII_FLOPPY_HM_TRACK_SIZE); +// if (rm) { + dr = &ui->pixels.floppy[fi].hm_read; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + dr->pix.row_bytes, dr->pix.size.y, + GL_LUMINANCE, GL_UNSIGNED_BYTE, + f->heat->read.map); +// } +// if (wm) { + dr = &ui->pixels.floppy[fi].hm_write; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, + dr->pix.row_bytes, dr->pix.size.y, + GL_LUMINANCE, GL_UNSIGNED_BYTE, + f->heat->write.map); +// } + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } + return draw; +} + +void +mii_mui_gl_render( + mii_mui_t *ui) +{ + glClearColor( + .6f * ui->mui_alpha, + .6f * ui->mui_alpha, + .6f * ui->mui_alpha, + 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glPushAttrib(GL_ENABLE_BIT|GL_COLOR_BUFFER_BIT|GL_TRANSFORM_BIT); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glEnable(GL_BLEND); + glEnable(GL_TEXTURE_2D); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + /* setup viewport/project */ + glViewport(0, 0, + (GLsizei)ui->window_size.x, + (GLsizei)ui->window_size.y); + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(0.0f, ui->window_size.x, ui->window_size.y, + 0.0f, -1.0f, 1.0f); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + // This (was) the recommended way to handle pixel alignment in glOrtho + // mode, but this seems to have changed -- now it looks like Linear filtering +// glTranslatef(0.375f, 0.375f, 0.0f); + { + /* draw mii texture */ + glColor3f(1.0f, 1.0f, 1.0f); + mui_drawable_t * dr = &ui->pixels.mii; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glBegin(GL_QUADS); + c2_rect_t r = ui->video_frame; + glTexCoord2f(0, 0); + glVertex2f(r.l, r.t); + glTexCoord2f(MII_VIDEO_WIDTH / (double)MII_VRAM_WIDTH, 0); + glVertex2f(r.r, r.t); + glTexCoord2f(MII_VIDEO_WIDTH / (double)MII_VRAM_WIDTH, + MII_VIDEO_HEIGHT / (double)MII_VRAM_HEIGHT); + glVertex2f(r.r, r.b); + glTexCoord2f(0, + MII_VIDEO_HEIGHT / (double)MII_VRAM_HEIGHT); + glVertex2f(r.l, r.b); + glEnd(); + + /* draw floppy textures, floppy 0 is left of the screen, + floppy 1 is right */ + for (int i = 0; i < 2; i++) { + dr = &ui->pixels.floppy[i].bits; + mii_floppy_t *f = ui->floppy[i].floppy; + if (!f || !dr->pix.pixels) + continue; + if (f->motor) { + dr->texture.opacity = 1.0f; + } else { + if (dr->texture.opacity > 0.0f) + dr->texture.opacity -= 0.01f; + if (dr->texture.opacity < 0.0f) + dr->texture.opacity = 0.0f; + } + if (dr->texture.opacity <= 0.0f) + continue; + c2_rect_t r = C2_RECT_WH( 0, 0, + ui->video_frame.l - 20, + c2_rect_height(&ui->video_frame) - 22); + c2_rect_f tr = { 0, 0, ui->floppy[i].max_width, 1 }; + if (i == 0) + c2_rect_offset(&r, + ui->video_frame.l - c2_rect_width(&r) - 10, + ui->video_frame.t + 10); + else + c2_rect_offset(&r, ui->video_frame.r + 10, + ui->video_frame.t + 10); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(1.0f, 1.0f, 1.0f, dr->texture.opacity); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glBegin(GL_QUADS); + // rotate texture 90 clockwise, and mirror left-right + glTexCoord2f(tr.l, tr.t); glVertex2f(r.l, r.t); + glTexCoord2f(tr.l, tr.b); glVertex2f(r.r, r.t); + glTexCoord2f(tr.r, tr.b); glVertex2f(r.r, r.b); + glTexCoord2f(tr.r, tr.t); glVertex2f(r.l, r.b); + glEnd(); + + if (f->heat) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR); + dr = &ui->pixels.floppy[i].hm_read; + glColor4f(0.0f, 1.0f, 0.0f, 1.0); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glBegin(GL_QUADS); + // rotate texture 90 clockwise, and mirror left-right + glTexCoord2f(tr.l, tr.t); glVertex2f(r.l, r.t); + glTexCoord2f(tr.l, tr.b); glVertex2f(r.r, r.t); + glTexCoord2f(tr.r, tr.b); glVertex2f(r.r, r.b); + glTexCoord2f(tr.r, tr.t); glVertex2f(r.l, r.b); + glEnd(); + dr = &ui->pixels.floppy[i].hm_write; + glColor4f(1.0f, 0.0f, 0.0f, 1.0); + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glBegin(GL_QUADS); + // rotate texture 90 clockwise, and mirror left-right + glTexCoord2f(tr.l, tr.t); glVertex2f(r.l, r.t); + glTexCoord2f(tr.l, tr.b); glVertex2f(r.r, r.t); + glTexCoord2f(tr.r, tr.b); glVertex2f(r.r, r.b); + glTexCoord2f(tr.r, tr.t); glVertex2f(r.l, r.b); + glEnd(); + } + } + /* draw mui texture */ + if (ui->mui_alpha > 0.0f) { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(1.0f, 1.0f, 1.0f, ui->mui_alpha); + dr = &ui->pixels.mui; + glBindTexture(GL_TEXTURE_2D, dr->texture.id); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(0, 0); + glTexCoord2f( + ui->window_size.x / (double)(dr->pix.row_bytes / 4), 0); + glVertex2f(ui->window_size.x, 0); + glTexCoord2f(ui->window_size.x / (double)(dr->pix.row_bytes / 4), + ui->window_size.y / (double)(dr->texture.size.y)); + glVertex2f(ui->window_size.x, ui->window_size.y); + glTexCoord2f(0, + ui->window_size.y / (double)(dr->texture.size.y)); + glVertex2f(0, ui->window_size.y); + glEnd(); + } + } + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + glDisable(GL_TEXTURE_2D); + + glBindTexture(GL_TEXTURE_2D, 0); + glMatrixMode(GL_MODELVIEW); + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glPopAttrib(); +} diff --git a/ui_gl/mii_mui_gl.h b/ui_gl/mii_mui_gl.h new file mode 100644 index 0000000..086caa8 --- /dev/null +++ b/ui_gl/mii_mui_gl.h @@ -0,0 +1,23 @@ +/* + * mii_mui_gl.h + * + * Copyright (C) 2023 Michel Pollet + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +#include "mii_mui.h" + +void +mii_mui_gl_init( + mii_mui_t *ui); +void +mii_mui_gl_prepare_textures( + mii_mui_t *ui); +void +mii_mui_gl_render( + mii_mui_t *ui); +bool +mii_mui_gl_run( + mii_mui_t *ui); diff --git a/ui_gl/mii_mui_menus.c b/ui_gl/mii_mui_menus.c index c9075da..b50cb0c 100644 --- a/ui_gl/mii_mui_menus.c +++ b/ui_gl/mii_mui_menus.c @@ -39,12 +39,12 @@ mii_quit_confirm_cb( void * param) { mii_mui_t * ui = cb_param; - printf("%s %4.4s\n", __func__, (char*)&what); +// printf("%s %4.4s\n", __func__, (char*)&what); if (what == MUI_CONTROL_ACTION_SELECT) { mui_control_t * c = param; - printf("%s %4.4s\n", __func__, (char*)&c->uid); +// printf("%s %4.4s\n", __func__, (char*)&c->uid); if (c->uid == MUI_ALERT_BUTTON_OK) { - printf("%s Quit\n", __func__); +// printf("%s Quit\n", __func__); mii_t * mii = &ui->mii; mii->state = MII_TERMINATE; } @@ -92,25 +92,6 @@ mii_config_open_slots_dialog( mii_config_save_cb, ui); } - -static void -_mii_show_about( - mii_mui_t * ui) -{ - mui_t * mui = &ui->mui; - mui_window_t *w = mui_window_get_by_id(mui, FCC('a','b','o','t')); - if (w) { - mui_window_select(w); - return; - } - w = mui_alert(mui, C2_PT(0,0), - "About MII", - "Version " MII_VERSION "\n" - "Build " __DATE__ " " __TIME__, - MUI_ALERT_INFO); - mui_window_set_id(w, FCC('a','b','o','t')); -} - static int mii_menubar_action( mui_window_t *win, // window (menubar) @@ -198,7 +179,7 @@ mii_menubar_action( case FCC('s','h','m','b'): { items[i].disabled = (mui_window_front(mui) != NULL) || - (ui->transition != MII_MUI_TRANSITION_NONE); + (ui->transition.state != MII_MUI_TRANSITION_NONE); } break; } } @@ -209,14 +190,12 @@ mii_menubar_action( // (char*)&item->uid, item->title); switch (item->uid) { case FCC('a','b','o','t'): { -// _mii_show_about(ui); mii_mui_about(&ui->mui); } break; case FCC('q','u','i','t'): { -// printf("%s Quit?\n", __func__); if (!ui->mui_visible && - ui->transition == MII_MUI_TRANSITION_NONE) - ui->transition = MII_MUI_TRANSITION_SHOW_UI; + ui->transition.state == MII_MUI_TRANSITION_NONE) + ui->transition.state = MII_MUI_TRANSITION_SHOW_UI; mui_window_t * really = mui_window_get_by_id( &ui->mui, FCC('q','u','i','t')); if (really) @@ -231,12 +210,12 @@ mii_menubar_action( } } break; case FCC('s','h','m','b'): { - if (ui->transition != MII_MUI_TRANSITION_NONE) + if (ui->transition.state != MII_MUI_TRANSITION_NONE) break; if (ui->mui_visible) { - ui->transition = MII_MUI_TRANSITION_HIDE_UI; + ui->transition.state = MII_MUI_TRANSITION_HIDE_UI; } else { - ui->transition = MII_MUI_TRANSITION_SHOW_UI; + ui->transition.state = MII_MUI_TRANSITION_SHOW_UI; } } break; case FCC('d','s','k','0'):